mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-02 09:25:01 -04:00
search: make SearchEngine declaration order the .auto preference order
Mirror the zenai provider pattern: the enum's declaration order is the single source of truth for the .auto cascade, asserted at comptime against api_engines (auto first, one table entry per API engine in tag order, duckduckgo last with no entry). The enum reorders brave before tavily to match the table's actual preference — persisted settings are unaffected (ZON stores tag names). Each table entry now carries a .keyless field; the hardcoded keenable keyless rung, searchEngineKeyless, and searchExplicit's comptime gate all derive from it, engineIndex collapses to arithmetic, and apiSearch takes a typed ?[]const u8 key instead of anytype.
This commit is contained in:
1 parent
f1742ad1af
commit
12ecf27fb1
1 file changed
+56
-25
+56
-25
@@ -979,19 +979,23 @@ pub const SearchParams = struct {
|
||||
timeout: ?u32 = null,
|
||||
};
|
||||
|
||||
/// Search backend for `execSearch`. `.auto` tries the `api_engines` in
|
||||
/// order (each only when its API key is set), then Keenable's keyless
|
||||
/// public endpoint, then the DuckDuckGo scrape; an explicit engine is
|
||||
/// used alone. Only the agent REPL's `/searchEngine` mutates this.
|
||||
pub const SearchEngine = enum { auto, tavily, brave, exa, keenable, duckduckgo };
|
||||
/// Search backend for `execSearch`. `.auto` tries the API engines in
|
||||
/// declaration order (each only when its API key is set), then the keyless
|
||||
/// public endpoint of `.keyless` engines (keenable), then the DuckDuckGo
|
||||
/// scrape; an explicit engine is used alone. Declaration order after `auto`
|
||||
/// is the `.auto` preference order (asserted against `api_engines` below);
|
||||
/// `duckduckgo` (the scrape fallback, no table entry) is last. Only the
|
||||
/// agent REPL's `/searchEngine` mutates this.
|
||||
pub const SearchEngine = enum { auto, brave, tavily, exa, keenable, duckduckgo };
|
||||
pub var search_engine: SearchEngine = .auto;
|
||||
|
||||
// Ordered by `.auto` preference; the `search` tool description prose must
|
||||
// agree with this table.
|
||||
// One entry per API engine, in `SearchEngine` declaration order (asserted
|
||||
// below); the `search` tool description prose must agree with this table.
|
||||
const api_engines = .{
|
||||
.{
|
||||
.tag = SearchEngine.brave,
|
||||
.env_var = "BRAVE_API_KEY",
|
||||
.keyless = false,
|
||||
.Client = brave.Client,
|
||||
.init_options = brave.Client.InitOptions{},
|
||||
// text_decorations=false: no <strong> markup in model-read snippets.
|
||||
@@ -1001,6 +1005,7 @@ const api_engines = .{
|
||||
.{
|
||||
.tag = SearchEngine.tavily,
|
||||
.env_var = "TAVILY_API_KEY",
|
||||
.keyless = false,
|
||||
.Client = tavily.Client,
|
||||
.init_options = tavily.Client.InitOptions{},
|
||||
.options = tavily.types.SearchOptions{ .max_results = 10 },
|
||||
@@ -1009,6 +1014,7 @@ const api_engines = .{
|
||||
.{
|
||||
.tag = SearchEngine.exa,
|
||||
.env_var = "EXA_API_KEY",
|
||||
.keyless = false,
|
||||
.Client = exa.Client,
|
||||
.init_options = exa.Client.InitOptions{},
|
||||
// highlights: Exa returns no snippet text unless contents is requested;
|
||||
@@ -1019,6 +1025,9 @@ const api_engines = .{
|
||||
.{
|
||||
.tag = SearchEngine.keenable,
|
||||
.env_var = "KEENABLE_API_KEY",
|
||||
// Also works with no API key: a `null` key routes the client to the
|
||||
// keyless public endpoint (rate-limited per client IP).
|
||||
.keyless = true,
|
||||
.Client = keenable.Client,
|
||||
// Retries off: the fallback cascade is the retry mechanism, and
|
||||
// honoring the public endpoint's Retry-After (60s per sleep) would
|
||||
@@ -1031,11 +1040,27 @@ const api_engines = .{
|
||||
},
|
||||
};
|
||||
|
||||
// `SearchEngine` declaration order is the single source of truth: tags must
|
||||
// appear in `api_engines` in declaration order, with `auto` first and
|
||||
// `duckduckgo` (scrape fallback, no table entry) last.
|
||||
comptime {
|
||||
std.debug.assert(@intFromEnum(SearchEngine.auto) == 0);
|
||||
std.debug.assert(api_engines.len == std.enums.values(SearchEngine).len - 2);
|
||||
std.debug.assert(@intFromEnum(SearchEngine.duckduckgo) == api_engines.len + 1);
|
||||
for (api_engines, 0..) |e, i| {
|
||||
if (@intFromEnum(e.tag) != i + 1)
|
||||
@compileError("api_engines order must match SearchEngine declaration order: " ++ @tagName(e.tag));
|
||||
}
|
||||
}
|
||||
|
||||
/// Engines that also work with no API key (a `null` key routes the client
|
||||
/// to the provider's public endpoint). `searchExplicit` and the REPL's
|
||||
/// `/searchEngine` warning consult this.
|
||||
pub fn searchEngineKeyless(engine: SearchEngine) bool {
|
||||
return engine == .keenable;
|
||||
inline for (api_engines) |e| {
|
||||
if (engine == e.tag) return e.keyless;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn searchEnvVar(engine: SearchEngine) ?[:0]const u8 {
|
||||
@@ -1045,11 +1070,9 @@ pub fn searchEnvVar(engine: SearchEngine) ?[:0]const u8 {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Valid for any API engine tag; the comptime block above pins tag order.
|
||||
fn engineIndex(comptime tag: SearchEngine) usize {
|
||||
inline for (api_engines, 0..) |e, i| {
|
||||
if (e.tag == tag) return i;
|
||||
}
|
||||
@compileError("engine missing from api_engines: " ++ @tagName(tag));
|
||||
return @intFromEnum(tag) - 1;
|
||||
}
|
||||
|
||||
fn execSearch(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.Registry, arguments: ?std.json.Value) ToolError!ToolResult {
|
||||
@@ -1069,14 +1092,17 @@ fn execSearch(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode
|
||||
}
|
||||
}
|
||||
}
|
||||
// One rung above the DDG scrape: the keyless public endpoint
|
||||
// returns the same structured results under its own rate-limit
|
||||
// regime — worth trying even after a failed keyed attempt.
|
||||
const kn = api_engines[comptime engineIndex(.keenable)];
|
||||
if (apiSearch(kn, arena, null, args.query)) |markdown_| {
|
||||
return .{ .text = markdown_ };
|
||||
} else |err| {
|
||||
log.warn(.browser, "keenable keyless fallback", .{ .err = err });
|
||||
// One rung above the DDG scrape: keyless public endpoints return
|
||||
// the same structured results under their own rate-limit regime
|
||||
// — worth trying even after a failed keyed attempt.
|
||||
inline for (api_engines) |engine| {
|
||||
if (comptime engine.keyless) {
|
||||
if (apiSearch(engine, arena, null, args.query)) |markdown_| {
|
||||
return .{ .text = markdown_ };
|
||||
} else |err| {
|
||||
log.warn(.browser, @tagName(engine.tag) ++ " keyless fallback", .{ .err = err });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
.duckduckgo => {},
|
||||
@@ -1105,7 +1131,7 @@ fn searchExplicit(arena: std.mem.Allocator, comptime engine: anytype, query: []c
|
||||
break :blk apiSearch(engine, arena, std.mem.span(z), query);
|
||||
// Comptime-gated so the null-key call is only instantiated for
|
||||
// clients whose `init` takes an optional key.
|
||||
if (comptime searchEngineKeyless(engine.tag))
|
||||
if (comptime engine.keyless)
|
||||
break :blk apiSearch(engine, arena, null, query);
|
||||
return .{
|
||||
.text = "web search engine is set to " ++ label ++ " but " ++ engine.env_var ++ " is not set in the environment",
|
||||
@@ -1120,15 +1146,20 @@ fn searchExplicit(arena: std.mem.Allocator, comptime engine: anytype, query: []c
|
||||
}
|
||||
|
||||
/// `arena` owns the returned slice. `api_key` is the environment value, or
|
||||
/// `null` for an engine that supports keyless calls (its client's `init`
|
||||
/// takes a `?[]const u8` key — see `searchEngineKeyless`).
|
||||
/// `null` for a `.keyless` engine's public endpoint (its client's `init`
|
||||
/// takes a `?[]const u8` key; keyed clients get the unwrapped slice).
|
||||
fn apiSearch(
|
||||
comptime engine: anytype,
|
||||
arena: std.mem.Allocator,
|
||||
api_key: anytype,
|
||||
api_key: ?[]const u8,
|
||||
query: []const u8,
|
||||
) ![]const u8 {
|
||||
var client: engine.Client = .init(lp.io, arena, api_key, engine.init_options);
|
||||
var client: engine.Client = .init(
|
||||
lp.io,
|
||||
arena,
|
||||
if (comptime engine.keyless) api_key else api_key.?,
|
||||
engine.init_options,
|
||||
);
|
||||
defer client.deinit();
|
||||
|
||||
var response = client.search(query, engine.options) catch |err| {
|
||||
|
||||
Reference in new issue
Block a user