mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
perf(brave): lazy-load web search runtime
This commit is contained in:
367
extensions/brave/src/brave-web-search-provider.runtime.ts
Normal file
367
extensions/brave/src/brave-web-search-provider.runtime.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
import type { SearchConfigRecord } from "openclaw/plugin-sdk/provider-web-search";
|
||||
import {
|
||||
buildSearchCacheKey,
|
||||
DEFAULT_SEARCH_COUNT,
|
||||
formatCliCommand,
|
||||
normalizeFreshness,
|
||||
parseIsoDateRange,
|
||||
readCachedSearchPayload,
|
||||
readConfiguredSecretString,
|
||||
readNumberParam,
|
||||
readProviderEnvValue,
|
||||
readStringParam,
|
||||
resolveSearchCacheTtlMs,
|
||||
resolveSearchCount,
|
||||
resolveSearchTimeoutSeconds,
|
||||
resolveSiteName,
|
||||
withTrustedWebSearchEndpoint,
|
||||
wrapWebContent,
|
||||
writeCachedSearchPayload,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import {
|
||||
type BraveLlmContextResponse,
|
||||
mapBraveLlmContextResults,
|
||||
normalizeBraveCountry,
|
||||
normalizeBraveLanguageParams,
|
||||
resolveBraveConfig,
|
||||
resolveBraveMode,
|
||||
} from "./brave-web-search-provider.shared.js";
|
||||
|
||||
const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
|
||||
const BRAVE_LLM_CONTEXT_ENDPOINT = "https://api.search.brave.com/res/v1/llm/context";
|
||||
|
||||
type BraveSearchResult = {
|
||||
title?: string;
|
||||
url?: string;
|
||||
description?: string;
|
||||
age?: string;
|
||||
};
|
||||
|
||||
type BraveSearchResponse = {
|
||||
web?: {
|
||||
results?: BraveSearchResult[];
|
||||
};
|
||||
};
|
||||
|
||||
function resolveBraveApiKey(searchConfig?: SearchConfigRecord): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(searchConfig?.apiKey, "tools.web.search.apiKey") ??
|
||||
readProviderEnvValue(["BRAVE_API_KEY"])
|
||||
);
|
||||
}
|
||||
|
||||
function missingBraveKeyPayload() {
|
||||
return {
|
||||
error: "missing_brave_api_key",
|
||||
message: `web_search (brave) needs a Brave Search API key. Run \`${formatCliCommand("openclaw configure --section web")}\` to store it, or set BRAVE_API_KEY in the Gateway environment.`,
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
|
||||
async function runBraveLlmContextSearch(params: {
|
||||
query: string;
|
||||
apiKey: string;
|
||||
timeoutSeconds: number;
|
||||
country?: string;
|
||||
search_lang?: string;
|
||||
freshness?: string;
|
||||
}): Promise<{
|
||||
results: Array<{
|
||||
url: string;
|
||||
title: string;
|
||||
snippets: string[];
|
||||
siteName?: string;
|
||||
}>;
|
||||
sources?: BraveLlmContextResponse["sources"];
|
||||
}> {
|
||||
const url = new URL(BRAVE_LLM_CONTEXT_ENDPOINT);
|
||||
url.searchParams.set("q", params.query);
|
||||
if (params.country) {
|
||||
url.searchParams.set("country", params.country);
|
||||
}
|
||||
if (params.search_lang) {
|
||||
url.searchParams.set("search_lang", params.search_lang);
|
||||
}
|
||||
if (params.freshness) {
|
||||
url.searchParams.set("freshness", params.freshness);
|
||||
}
|
||||
|
||||
return withTrustedWebSearchEndpoint(
|
||||
{
|
||||
url: url.toString(),
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
init: {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"X-Subscription-Token": params.apiKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (response) => {
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new Error(
|
||||
`Brave LLM Context API error (${response.status}): ${detail || response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as BraveLlmContextResponse;
|
||||
return { results: mapBraveLlmContextResults(data), sources: data.sources };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function runBraveWebSearch(params: {
|
||||
query: string;
|
||||
count: number;
|
||||
apiKey: string;
|
||||
timeoutSeconds: number;
|
||||
country?: string;
|
||||
search_lang?: string;
|
||||
ui_lang?: string;
|
||||
freshness?: string;
|
||||
dateAfter?: string;
|
||||
dateBefore?: string;
|
||||
}): Promise<Array<Record<string, unknown>>> {
|
||||
const url = new URL(BRAVE_SEARCH_ENDPOINT);
|
||||
url.searchParams.set("q", params.query);
|
||||
url.searchParams.set("count", String(params.count));
|
||||
if (params.country) {
|
||||
url.searchParams.set("country", params.country);
|
||||
}
|
||||
if (params.search_lang) {
|
||||
url.searchParams.set("search_lang", params.search_lang);
|
||||
}
|
||||
if (params.ui_lang) {
|
||||
url.searchParams.set("ui_lang", params.ui_lang);
|
||||
}
|
||||
if (params.freshness) {
|
||||
url.searchParams.set("freshness", params.freshness);
|
||||
} else if (params.dateAfter && params.dateBefore) {
|
||||
url.searchParams.set("freshness", `${params.dateAfter}to${params.dateBefore}`);
|
||||
} else if (params.dateAfter) {
|
||||
url.searchParams.set(
|
||||
"freshness",
|
||||
`${params.dateAfter}to${new Date().toISOString().slice(0, 10)}`,
|
||||
);
|
||||
} else if (params.dateBefore) {
|
||||
url.searchParams.set("freshness", `1970-01-01to${params.dateBefore}`);
|
||||
}
|
||||
|
||||
return withTrustedWebSearchEndpoint(
|
||||
{
|
||||
url: url.toString(),
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
init: {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"X-Subscription-Token": params.apiKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (response) => {
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new Error(
|
||||
`Brave Search API error (${response.status}): ${detail || response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as BraveSearchResponse;
|
||||
const results = Array.isArray(data.web?.results) ? (data.web?.results ?? []) : [];
|
||||
return results.map((entry) => {
|
||||
const description = entry.description ?? "";
|
||||
const title = entry.title ?? "";
|
||||
const url = entry.url ?? "";
|
||||
return {
|
||||
title: title ? wrapWebContent(title, "web_search") : "",
|
||||
url,
|
||||
description: description ? wrapWebContent(description, "web_search") : "",
|
||||
published: entry.age || undefined,
|
||||
siteName: resolveSiteName(url) || undefined,
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function executeBraveSearch(
|
||||
args: Record<string, unknown>,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const apiKey = resolveBraveApiKey(searchConfig);
|
||||
if (!apiKey) {
|
||||
return missingBraveKeyPayload();
|
||||
}
|
||||
|
||||
const braveConfig = resolveBraveConfig(searchConfig);
|
||||
const braveMode = resolveBraveMode(braveConfig);
|
||||
const query = readStringParam(args, "query", { required: true });
|
||||
const count =
|
||||
readNumberParam(args, "count", { integer: true }) ?? searchConfig?.maxResults ?? undefined;
|
||||
const country = normalizeBraveCountry(readStringParam(args, "country"));
|
||||
const language = readStringParam(args, "language");
|
||||
const search_lang = readStringParam(args, "search_lang");
|
||||
const ui_lang = readStringParam(args, "ui_lang");
|
||||
const normalizedLanguage = normalizeBraveLanguageParams({
|
||||
search_lang: search_lang || language,
|
||||
ui_lang,
|
||||
});
|
||||
|
||||
if (normalizedLanguage.invalidField === "search_lang") {
|
||||
return {
|
||||
error: "invalid_search_lang",
|
||||
message:
|
||||
"search_lang must be a Brave-supported language code like 'en', 'en-gb', 'zh-hans', or 'zh-hant'.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
if (normalizedLanguage.invalidField === "ui_lang") {
|
||||
return {
|
||||
error: "invalid_ui_lang",
|
||||
message: "ui_lang must be a language-region locale like 'en-US'.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
if (normalizedLanguage.ui_lang && braveMode === "llm-context") {
|
||||
return {
|
||||
error: "unsupported_ui_lang",
|
||||
message:
|
||||
"ui_lang is not supported by Brave llm-context mode. Remove ui_lang or use Brave web mode for locale-based UI hints.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
|
||||
const rawFreshness = readStringParam(args, "freshness");
|
||||
if (rawFreshness && braveMode === "llm-context") {
|
||||
return {
|
||||
error: "unsupported_freshness",
|
||||
message:
|
||||
"freshness filtering is not supported by Brave llm-context mode. Remove freshness or use Brave web mode.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
const freshness = rawFreshness ? normalizeFreshness(rawFreshness, "brave") : undefined;
|
||||
if (rawFreshness && !freshness) {
|
||||
return {
|
||||
error: "invalid_freshness",
|
||||
message: "freshness must be day, week, month, or year.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
|
||||
const rawDateAfter = readStringParam(args, "date_after");
|
||||
const rawDateBefore = readStringParam(args, "date_before");
|
||||
if (rawFreshness && (rawDateAfter || rawDateBefore)) {
|
||||
return {
|
||||
error: "conflicting_time_filters",
|
||||
message:
|
||||
"freshness and date_after/date_before cannot be used together. Use either freshness (day/week/month/year) or a date range (date_after/date_before), not both.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
if ((rawDateAfter || rawDateBefore) && braveMode === "llm-context") {
|
||||
return {
|
||||
error: "unsupported_date_filter",
|
||||
message:
|
||||
"date_after/date_before filtering is not supported by Brave llm-context mode. Use Brave web mode for date filters.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
|
||||
const parsedDateRange = parseIsoDateRange({
|
||||
rawDateAfter,
|
||||
rawDateBefore,
|
||||
invalidDateAfterMessage: "date_after must be YYYY-MM-DD format.",
|
||||
invalidDateBeforeMessage: "date_before must be YYYY-MM-DD format.",
|
||||
invalidDateRangeMessage: "date_after must be before date_before.",
|
||||
});
|
||||
if ("error" in parsedDateRange) {
|
||||
return parsedDateRange;
|
||||
}
|
||||
|
||||
const { dateAfter, dateBefore } = parsedDateRange;
|
||||
const cacheKey = buildSearchCacheKey([
|
||||
"brave",
|
||||
braveMode,
|
||||
query,
|
||||
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
country,
|
||||
normalizedLanguage.search_lang,
|
||||
normalizedLanguage.ui_lang,
|
||||
freshness,
|
||||
dateAfter,
|
||||
dateBefore,
|
||||
]);
|
||||
const cached = readCachedSearchPayload(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig);
|
||||
const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig);
|
||||
|
||||
if (braveMode === "llm-context") {
|
||||
const { results, sources } = await runBraveLlmContextSearch({
|
||||
query,
|
||||
apiKey,
|
||||
timeoutSeconds,
|
||||
country: country ?? undefined,
|
||||
search_lang: normalizedLanguage.search_lang,
|
||||
freshness,
|
||||
});
|
||||
const payload = {
|
||||
query,
|
||||
provider: "brave",
|
||||
mode: "llm-context" as const,
|
||||
count: results.length,
|
||||
tookMs: Date.now() - start,
|
||||
externalContent: {
|
||||
untrusted: true,
|
||||
source: "web_search",
|
||||
provider: "brave",
|
||||
wrapped: true,
|
||||
},
|
||||
results: results.map((entry) => ({
|
||||
title: entry.title ? wrapWebContent(entry.title, "web_search") : "",
|
||||
url: entry.url,
|
||||
snippets: entry.snippets.map((snippet) => wrapWebContent(snippet, "web_search")),
|
||||
siteName: entry.siteName,
|
||||
})),
|
||||
sources,
|
||||
};
|
||||
writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
|
||||
return payload;
|
||||
}
|
||||
|
||||
const results = await runBraveWebSearch({
|
||||
query,
|
||||
count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
apiKey,
|
||||
timeoutSeconds,
|
||||
country: country ?? undefined,
|
||||
search_lang: normalizedLanguage.search_lang,
|
||||
ui_lang: normalizedLanguage.ui_lang,
|
||||
freshness,
|
||||
dateAfter,
|
||||
dateBefore,
|
||||
});
|
||||
const payload = {
|
||||
query,
|
||||
provider: "brave",
|
||||
count: results.length,
|
||||
tookMs: Date.now() - start,
|
||||
externalContent: {
|
||||
untrusted: true,
|
||||
source: "web_search",
|
||||
provider: "brave",
|
||||
wrapped: true,
|
||||
},
|
||||
results,
|
||||
};
|
||||
writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
|
||||
return payload;
|
||||
}
|
||||
274
extensions/brave/src/brave-web-search-provider.shared.ts
Normal file
274
extensions/brave/src/brave-web-search-provider.shared.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
export type BraveConfig = {
|
||||
mode?: string;
|
||||
};
|
||||
|
||||
export type BraveLlmContextResult = { url: string; title: string; snippets: string[] };
|
||||
export type BraveLlmContextResponse = {
|
||||
grounding: { generic?: BraveLlmContextResult[] };
|
||||
sources?: { url?: string; hostname?: string; date?: string }[];
|
||||
};
|
||||
|
||||
const BRAVE_COUNTRY_CODES = new Set([
|
||||
"AR",
|
||||
"AU",
|
||||
"AT",
|
||||
"BE",
|
||||
"BR",
|
||||
"CA",
|
||||
"CL",
|
||||
"DK",
|
||||
"FI",
|
||||
"FR",
|
||||
"DE",
|
||||
"GR",
|
||||
"HK",
|
||||
"IN",
|
||||
"ID",
|
||||
"IT",
|
||||
"JP",
|
||||
"KR",
|
||||
"MY",
|
||||
"MX",
|
||||
"NL",
|
||||
"NZ",
|
||||
"NO",
|
||||
"CN",
|
||||
"PL",
|
||||
"PT",
|
||||
"PH",
|
||||
"RU",
|
||||
"SA",
|
||||
"ZA",
|
||||
"ES",
|
||||
"SE",
|
||||
"CH",
|
||||
"TW",
|
||||
"TR",
|
||||
"GB",
|
||||
"US",
|
||||
"ALL",
|
||||
]);
|
||||
|
||||
const BRAVE_SEARCH_LANG_CODES = new Set([
|
||||
"ar",
|
||||
"eu",
|
||||
"bn",
|
||||
"bg",
|
||||
"ca",
|
||||
"zh-hans",
|
||||
"zh-hant",
|
||||
"hr",
|
||||
"cs",
|
||||
"da",
|
||||
"nl",
|
||||
"en",
|
||||
"en-gb",
|
||||
"et",
|
||||
"fi",
|
||||
"fr",
|
||||
"gl",
|
||||
"de",
|
||||
"el",
|
||||
"gu",
|
||||
"he",
|
||||
"hi",
|
||||
"hu",
|
||||
"is",
|
||||
"it",
|
||||
"jp",
|
||||
"kn",
|
||||
"ko",
|
||||
"lv",
|
||||
"lt",
|
||||
"ms",
|
||||
"ml",
|
||||
"mr",
|
||||
"nb",
|
||||
"pl",
|
||||
"pt-br",
|
||||
"pt-pt",
|
||||
"pa",
|
||||
"ro",
|
||||
"ru",
|
||||
"sr",
|
||||
"sk",
|
||||
"sl",
|
||||
"es",
|
||||
"sv",
|
||||
"ta",
|
||||
"te",
|
||||
"th",
|
||||
"tr",
|
||||
"uk",
|
||||
"vi",
|
||||
]);
|
||||
|
||||
const BRAVE_SEARCH_LANG_ALIASES: Record<string, string> = {
|
||||
ja: "jp",
|
||||
zh: "zh-hans",
|
||||
"zh-cn": "zh-hans",
|
||||
"zh-hk": "zh-hant",
|
||||
"zh-sg": "zh-hans",
|
||||
"zh-tw": "zh-hant",
|
||||
};
|
||||
|
||||
const BRAVE_UI_LANG_LOCALE = /^([a-z]{2})-([a-z]{2})$/i;
|
||||
const MAX_BRAVE_SEARCH_COUNT = 10;
|
||||
|
||||
function normalizeBraveSearchLang(value: string | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const canonical = BRAVE_SEARCH_LANG_ALIASES[trimmed.toLowerCase()] ?? trimmed.toLowerCase();
|
||||
if (!BRAVE_SEARCH_LANG_CODES.has(canonical)) {
|
||||
return undefined;
|
||||
}
|
||||
return canonical;
|
||||
}
|
||||
|
||||
export function normalizeBraveCountry(value: string | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const canonical = trimmed.toUpperCase();
|
||||
return BRAVE_COUNTRY_CODES.has(canonical) ? canonical : "ALL";
|
||||
}
|
||||
|
||||
function normalizeBraveUiLang(value: string | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const match = trimmed.match(BRAVE_UI_LANG_LOCALE);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const [, language, region] = match;
|
||||
return `${language.toLowerCase()}-${region.toUpperCase()}`;
|
||||
}
|
||||
|
||||
export function resolveBraveConfig(searchConfig?: Record<string, unknown>): BraveConfig {
|
||||
const brave = searchConfig?.brave;
|
||||
return brave && typeof brave === "object" && !Array.isArray(brave) ? (brave as BraveConfig) : {};
|
||||
}
|
||||
|
||||
export function resolveBraveMode(brave?: BraveConfig): "web" | "llm-context" {
|
||||
return brave?.mode === "llm-context" ? "llm-context" : "web";
|
||||
}
|
||||
|
||||
export function normalizeBraveLanguageParams(params: { search_lang?: string; ui_lang?: string }): {
|
||||
search_lang?: string;
|
||||
ui_lang?: string;
|
||||
invalidField?: "search_lang" | "ui_lang";
|
||||
} {
|
||||
const rawSearchLang = params.search_lang?.trim() || undefined;
|
||||
const rawUiLang = params.ui_lang?.trim() || undefined;
|
||||
let searchLangCandidate = rawSearchLang;
|
||||
let uiLangCandidate = rawUiLang;
|
||||
|
||||
if (normalizeBraveUiLang(rawSearchLang) && normalizeBraveSearchLang(rawUiLang)) {
|
||||
searchLangCandidate = rawUiLang;
|
||||
uiLangCandidate = rawSearchLang;
|
||||
}
|
||||
|
||||
const search_lang = normalizeBraveSearchLang(searchLangCandidate);
|
||||
if (searchLangCandidate && !search_lang) {
|
||||
return { invalidField: "search_lang" };
|
||||
}
|
||||
|
||||
const ui_lang = normalizeBraveUiLang(uiLangCandidate);
|
||||
if (uiLangCandidate && !ui_lang) {
|
||||
return { invalidField: "ui_lang" };
|
||||
}
|
||||
|
||||
return { search_lang, ui_lang };
|
||||
}
|
||||
|
||||
function resolveSiteName(url: string | undefined): string | undefined {
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function mapBraveLlmContextResults(
|
||||
data: BraveLlmContextResponse,
|
||||
): { url: string; title: string; snippets: string[]; siteName?: string }[] {
|
||||
const genericResults = Array.isArray(data.grounding?.generic) ? data.grounding.generic : [];
|
||||
return genericResults.map((entry) => ({
|
||||
url: entry.url ?? "",
|
||||
title: entry.title ?? "",
|
||||
snippets: (entry.snippets ?? []).filter(
|
||||
(snippet) => typeof snippet === "string" && snippet.length > 0,
|
||||
),
|
||||
siteName: resolveSiteName(entry.url) || undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
export function createBraveSchema() {
|
||||
return Type.Object({
|
||||
query: Type.String({ description: "Search query string." }),
|
||||
count: Type.Optional(
|
||||
Type.Number({
|
||||
description: "Number of results to return (1-10).",
|
||||
minimum: 1,
|
||||
maximum: MAX_BRAVE_SEARCH_COUNT,
|
||||
}),
|
||||
),
|
||||
country: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"2-letter country code for region-specific results (e.g., 'DE', 'US', 'ALL'). Default: 'US'.",
|
||||
}),
|
||||
),
|
||||
language: Type.Optional(
|
||||
Type.String({
|
||||
description: "ISO 639-1 language code for results (e.g., 'en', 'de', 'fr').",
|
||||
}),
|
||||
),
|
||||
freshness: Type.Optional(
|
||||
Type.String({
|
||||
description: "Filter by time: 'day' (24h), 'week', 'month', or 'year'.",
|
||||
}),
|
||||
),
|
||||
date_after: Type.Optional(
|
||||
Type.String({
|
||||
description: "Only results published after this date (YYYY-MM-DD).",
|
||||
}),
|
||||
),
|
||||
date_before: Type.Optional(
|
||||
Type.String({
|
||||
description: "Only results published before this date (YYYY-MM-DD).",
|
||||
}),
|
||||
),
|
||||
search_lang: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Brave language code for search results (e.g., 'en', 'de', 'en-gb', 'zh-hans', 'zh-hant', 'pt-br').",
|
||||
}),
|
||||
),
|
||||
ui_lang: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Locale code for UI elements in language-region format (e.g., 'en-US', 'de-DE', 'fr-FR', 'tr-TR'). Must include region subtag.",
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -1,449 +1,106 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import {
|
||||
buildSearchCacheKey,
|
||||
DEFAULT_SEARCH_COUNT,
|
||||
MAX_SEARCH_COUNT,
|
||||
formatCliCommand,
|
||||
mergeScopedSearchConfig,
|
||||
normalizeFreshness,
|
||||
parseIsoDateRange,
|
||||
readCachedSearchPayload,
|
||||
readConfiguredSecretString,
|
||||
readNumberParam,
|
||||
readProviderEnvValue,
|
||||
readStringParam,
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
resolveSearchCacheTtlMs,
|
||||
resolveSearchCount,
|
||||
resolveSearchTimeoutSeconds,
|
||||
resolveSiteName,
|
||||
setTopLevelCredentialValue,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
type SearchConfigRecord,
|
||||
type WebSearchProviderPlugin,
|
||||
type WebSearchProviderToolDefinition,
|
||||
withTrustedWebSearchEndpoint,
|
||||
wrapWebContent,
|
||||
writeCachedSearchPayload,
|
||||
import type {
|
||||
SearchConfigRecord,
|
||||
WebSearchProviderPlugin,
|
||||
WebSearchProviderToolDefinition,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import {
|
||||
createBraveSchema,
|
||||
mapBraveLlmContextResults,
|
||||
normalizeBraveCountry,
|
||||
normalizeBraveLanguageParams,
|
||||
resolveBraveConfig,
|
||||
resolveBraveMode,
|
||||
} from "./brave-web-search-provider.shared.js";
|
||||
|
||||
const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
|
||||
const BRAVE_LLM_CONTEXT_ENDPOINT = "https://api.search.brave.com/res/v1/llm/context";
|
||||
// Mirror Brave's documented country enum so unsupported locale guesses can collapse to ALL.
|
||||
const BRAVE_COUNTRY_CODES = new Set([
|
||||
"AR",
|
||||
"AU",
|
||||
"AT",
|
||||
"BE",
|
||||
"BR",
|
||||
"CA",
|
||||
"CL",
|
||||
"DK",
|
||||
"FI",
|
||||
"FR",
|
||||
"DE",
|
||||
"GR",
|
||||
"HK",
|
||||
"IN",
|
||||
"ID",
|
||||
"IT",
|
||||
"JP",
|
||||
"KR",
|
||||
"MY",
|
||||
"MX",
|
||||
"NL",
|
||||
"NZ",
|
||||
"NO",
|
||||
"CN",
|
||||
"PL",
|
||||
"PT",
|
||||
"PH",
|
||||
"RU",
|
||||
"SA",
|
||||
"ZA",
|
||||
"ES",
|
||||
"SE",
|
||||
"CH",
|
||||
"TW",
|
||||
"TR",
|
||||
"GB",
|
||||
"US",
|
||||
"ALL",
|
||||
]);
|
||||
const BRAVE_SEARCH_LANG_CODES = new Set([
|
||||
"ar",
|
||||
"eu",
|
||||
"bn",
|
||||
"bg",
|
||||
"ca",
|
||||
"zh-hans",
|
||||
"zh-hant",
|
||||
"hr",
|
||||
"cs",
|
||||
"da",
|
||||
"nl",
|
||||
"en",
|
||||
"en-gb",
|
||||
"et",
|
||||
"fi",
|
||||
"fr",
|
||||
"gl",
|
||||
"de",
|
||||
"el",
|
||||
"gu",
|
||||
"he",
|
||||
"hi",
|
||||
"hu",
|
||||
"is",
|
||||
"it",
|
||||
"jp",
|
||||
"kn",
|
||||
"ko",
|
||||
"lv",
|
||||
"lt",
|
||||
"ms",
|
||||
"ml",
|
||||
"mr",
|
||||
"nb",
|
||||
"pl",
|
||||
"pt-br",
|
||||
"pt-pt",
|
||||
"pa",
|
||||
"ro",
|
||||
"ru",
|
||||
"sr",
|
||||
"sk",
|
||||
"sl",
|
||||
"es",
|
||||
"sv",
|
||||
"ta",
|
||||
"te",
|
||||
"th",
|
||||
"tr",
|
||||
"uk",
|
||||
"vi",
|
||||
]);
|
||||
const BRAVE_SEARCH_LANG_ALIASES: Record<string, string> = {
|
||||
ja: "jp",
|
||||
zh: "zh-hans",
|
||||
"zh-cn": "zh-hans",
|
||||
"zh-hk": "zh-hant",
|
||||
"zh-sg": "zh-hans",
|
||||
"zh-tw": "zh-hant",
|
||||
};
|
||||
const BRAVE_UI_LANG_LOCALE = /^([a-z]{2})-([a-z]{2})$/i;
|
||||
type ConfigInput = Parameters<
|
||||
NonNullable<WebSearchProviderPlugin["getConfiguredCredentialValue"]>
|
||||
>[0];
|
||||
type ConfigTarget = Parameters<
|
||||
NonNullable<WebSearchProviderPlugin["setConfiguredCredentialValue"]>
|
||||
>[0];
|
||||
|
||||
type BraveConfig = {
|
||||
mode?: string;
|
||||
};
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
type BraveSearchResult = {
|
||||
title?: string;
|
||||
url?: string;
|
||||
description?: string;
|
||||
age?: string;
|
||||
};
|
||||
function resolveProviderWebSearchPluginConfig(
|
||||
config: ConfigInput,
|
||||
pluginId: string,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!isRecord(config)) {
|
||||
return undefined;
|
||||
}
|
||||
const plugins = isRecord(config.plugins) ? config.plugins : undefined;
|
||||
const entries = isRecord(plugins?.entries) ? plugins.entries : undefined;
|
||||
const entry = isRecord(entries?.[pluginId]) ? entries[pluginId] : undefined;
|
||||
const pluginConfig = isRecord(entry?.config) ? entry.config : undefined;
|
||||
return isRecord(pluginConfig?.webSearch) ? pluginConfig.webSearch : undefined;
|
||||
}
|
||||
|
||||
type BraveSearchResponse = {
|
||||
web?: {
|
||||
results?: BraveSearchResult[];
|
||||
function ensureObject(target: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
const current = target[key];
|
||||
if (isRecord(current)) {
|
||||
return current;
|
||||
}
|
||||
const next: Record<string, unknown> = {};
|
||||
target[key] = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
function setProviderWebSearchPluginConfigValue(
|
||||
configTarget: ConfigTarget,
|
||||
pluginId: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void {
|
||||
const plugins = ensureObject(configTarget as Record<string, unknown>, "plugins");
|
||||
const entries = ensureObject(plugins, "entries");
|
||||
const entry = ensureObject(entries, pluginId);
|
||||
if (entry.enabled === undefined) {
|
||||
entry.enabled = true;
|
||||
}
|
||||
const config = ensureObject(entry, "config");
|
||||
const webSearch = ensureObject(config, "webSearch");
|
||||
webSearch[key] = value;
|
||||
}
|
||||
|
||||
function setTopLevelCredentialValue(
|
||||
searchConfigTarget: Record<string, unknown>,
|
||||
value: unknown,
|
||||
): void {
|
||||
searchConfigTarget.apiKey = value;
|
||||
}
|
||||
|
||||
function mergeScopedSearchConfig(
|
||||
searchConfig: Record<string, unknown> | undefined,
|
||||
key: string,
|
||||
pluginConfig: Record<string, unknown> | undefined,
|
||||
options?: { mirrorApiKeyToTopLevel?: boolean },
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!pluginConfig) {
|
||||
return searchConfig;
|
||||
}
|
||||
|
||||
const currentScoped = isRecord(searchConfig?.[key]) ? searchConfig?.[key] : {};
|
||||
const next: Record<string, unknown> = {
|
||||
...searchConfig,
|
||||
[key]: {
|
||||
...currentScoped,
|
||||
...pluginConfig,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type BraveLlmContextResult = { url: string; title: string; snippets: string[] };
|
||||
type BraveLlmContextResponse = {
|
||||
grounding: { generic?: BraveLlmContextResult[] };
|
||||
sources?: { url?: string; hostname?: string; date?: string }[];
|
||||
};
|
||||
|
||||
function resolveBraveConfig(searchConfig?: SearchConfigRecord): BraveConfig {
|
||||
const brave = searchConfig?.brave;
|
||||
return brave && typeof brave === "object" && !Array.isArray(brave) ? (brave as BraveConfig) : {};
|
||||
}
|
||||
|
||||
function resolveBraveMode(brave?: BraveConfig): "web" | "llm-context" {
|
||||
return brave?.mode === "llm-context" ? "llm-context" : "web";
|
||||
}
|
||||
|
||||
function resolveBraveApiKey(searchConfig?: SearchConfigRecord): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(searchConfig?.apiKey, "tools.web.search.apiKey") ??
|
||||
readProviderEnvValue(["BRAVE_API_KEY"])
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeBraveSearchLang(value: string | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const canonical = BRAVE_SEARCH_LANG_ALIASES[trimmed.toLowerCase()] ?? trimmed.toLowerCase();
|
||||
if (!BRAVE_SEARCH_LANG_CODES.has(canonical)) {
|
||||
return undefined;
|
||||
}
|
||||
return canonical;
|
||||
}
|
||||
|
||||
function normalizeBraveCountry(value: string | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const canonical = trimmed.toUpperCase();
|
||||
return BRAVE_COUNTRY_CODES.has(canonical) ? canonical : "ALL";
|
||||
}
|
||||
|
||||
function normalizeBraveUiLang(value: string | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const match = trimmed.match(BRAVE_UI_LANG_LOCALE);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const [, language, region] = match;
|
||||
return `${language.toLowerCase()}-${region.toUpperCase()}`;
|
||||
}
|
||||
|
||||
function normalizeBraveLanguageParams(params: { search_lang?: string; ui_lang?: string }): {
|
||||
search_lang?: string;
|
||||
ui_lang?: string;
|
||||
invalidField?: "search_lang" | "ui_lang";
|
||||
} {
|
||||
const rawSearchLang = params.search_lang?.trim() || undefined;
|
||||
const rawUiLang = params.ui_lang?.trim() || undefined;
|
||||
let searchLangCandidate = rawSearchLang;
|
||||
let uiLangCandidate = rawUiLang;
|
||||
|
||||
if (normalizeBraveUiLang(rawSearchLang) && normalizeBraveSearchLang(rawUiLang)) {
|
||||
searchLangCandidate = rawUiLang;
|
||||
uiLangCandidate = rawSearchLang;
|
||||
if (options?.mirrorApiKeyToTopLevel && pluginConfig.apiKey !== undefined) {
|
||||
next.apiKey = pluginConfig.apiKey;
|
||||
}
|
||||
|
||||
const search_lang = normalizeBraveSearchLang(searchLangCandidate);
|
||||
if (searchLangCandidate && !search_lang) {
|
||||
return { invalidField: "search_lang" };
|
||||
}
|
||||
|
||||
const ui_lang = normalizeBraveUiLang(uiLangCandidate);
|
||||
if (uiLangCandidate && !ui_lang) {
|
||||
return { invalidField: "ui_lang" };
|
||||
}
|
||||
|
||||
return { search_lang, ui_lang };
|
||||
}
|
||||
|
||||
function mapBraveLlmContextResults(
|
||||
data: BraveLlmContextResponse,
|
||||
): { url: string; title: string; snippets: string[]; siteName?: string }[] {
|
||||
const genericResults = Array.isArray(data.grounding?.generic) ? data.grounding.generic : [];
|
||||
return genericResults.map((entry) => ({
|
||||
url: entry.url ?? "",
|
||||
title: entry.title ?? "",
|
||||
snippets: (entry.snippets ?? []).filter((s) => typeof s === "string" && s.length > 0),
|
||||
siteName: resolveSiteName(entry.url) || undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
async function runBraveLlmContextSearch(params: {
|
||||
query: string;
|
||||
apiKey: string;
|
||||
timeoutSeconds: number;
|
||||
country?: string;
|
||||
search_lang?: string;
|
||||
freshness?: string;
|
||||
}): Promise<{
|
||||
results: Array<{
|
||||
url: string;
|
||||
title: string;
|
||||
snippets: string[];
|
||||
siteName?: string;
|
||||
}>;
|
||||
sources?: BraveLlmContextResponse["sources"];
|
||||
}> {
|
||||
const url = new URL(BRAVE_LLM_CONTEXT_ENDPOINT);
|
||||
url.searchParams.set("q", params.query);
|
||||
if (params.country) {
|
||||
url.searchParams.set("country", params.country);
|
||||
}
|
||||
if (params.search_lang) {
|
||||
url.searchParams.set("search_lang", params.search_lang);
|
||||
}
|
||||
if (params.freshness) {
|
||||
url.searchParams.set("freshness", params.freshness);
|
||||
}
|
||||
|
||||
return withTrustedWebSearchEndpoint(
|
||||
{
|
||||
url: url.toString(),
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
init: {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"X-Subscription-Token": params.apiKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (res) => {
|
||||
if (!res.ok) {
|
||||
const detail = await res.text();
|
||||
throw new Error(`Brave LLM Context API error (${res.status}): ${detail || res.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as BraveLlmContextResponse;
|
||||
return { results: mapBraveLlmContextResults(data), sources: data.sources };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function runBraveWebSearch(params: {
|
||||
query: string;
|
||||
count: number;
|
||||
apiKey: string;
|
||||
timeoutSeconds: number;
|
||||
country?: string;
|
||||
search_lang?: string;
|
||||
ui_lang?: string;
|
||||
freshness?: string;
|
||||
dateAfter?: string;
|
||||
dateBefore?: string;
|
||||
}): Promise<Array<Record<string, unknown>>> {
|
||||
const url = new URL(BRAVE_SEARCH_ENDPOINT);
|
||||
url.searchParams.set("q", params.query);
|
||||
url.searchParams.set("count", String(params.count));
|
||||
if (params.country) {
|
||||
url.searchParams.set("country", params.country);
|
||||
}
|
||||
if (params.search_lang) {
|
||||
url.searchParams.set("search_lang", params.search_lang);
|
||||
}
|
||||
if (params.ui_lang) {
|
||||
url.searchParams.set("ui_lang", params.ui_lang);
|
||||
}
|
||||
if (params.freshness) {
|
||||
url.searchParams.set("freshness", params.freshness);
|
||||
} else if (params.dateAfter && params.dateBefore) {
|
||||
url.searchParams.set("freshness", `${params.dateAfter}to${params.dateBefore}`);
|
||||
} else if (params.dateAfter) {
|
||||
url.searchParams.set(
|
||||
"freshness",
|
||||
`${params.dateAfter}to${new Date().toISOString().slice(0, 10)}`,
|
||||
);
|
||||
} else if (params.dateBefore) {
|
||||
url.searchParams.set("freshness", `1970-01-01to${params.dateBefore}`);
|
||||
}
|
||||
|
||||
return withTrustedWebSearchEndpoint(
|
||||
{
|
||||
url: url.toString(),
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
init: {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"X-Subscription-Token": params.apiKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (res) => {
|
||||
if (!res.ok) {
|
||||
const detail = await res.text();
|
||||
throw new Error(`Brave Search API error (${res.status}): ${detail || res.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as BraveSearchResponse;
|
||||
const results = Array.isArray(data.web?.results) ? (data.web?.results ?? []) : [];
|
||||
return results.map((entry) => {
|
||||
const description = entry.description ?? "";
|
||||
const title = entry.title ?? "";
|
||||
const url = entry.url ?? "";
|
||||
return {
|
||||
title: title ? wrapWebContent(title, "web_search") : "",
|
||||
url,
|
||||
description: description ? wrapWebContent(description, "web_search") : "",
|
||||
published: entry.age || undefined,
|
||||
siteName: resolveSiteName(url) || undefined,
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createBraveSchema() {
|
||||
return Type.Object({
|
||||
query: Type.String({ description: "Search query string." }),
|
||||
count: Type.Optional(
|
||||
Type.Number({
|
||||
description: "Number of results to return (1-10).",
|
||||
minimum: 1,
|
||||
maximum: MAX_SEARCH_COUNT,
|
||||
}),
|
||||
),
|
||||
country: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"2-letter country code for region-specific results (e.g., 'DE', 'US', 'ALL'). Default: 'US'.",
|
||||
}),
|
||||
),
|
||||
language: Type.Optional(
|
||||
Type.String({
|
||||
description: "ISO 639-1 language code for results (e.g., 'en', 'de', 'fr').",
|
||||
}),
|
||||
),
|
||||
freshness: Type.Optional(
|
||||
Type.String({
|
||||
description: "Filter by time: 'day' (24h), 'week', 'month', or 'year'.",
|
||||
}),
|
||||
),
|
||||
date_after: Type.Optional(
|
||||
Type.String({
|
||||
description: "Only results published after this date (YYYY-MM-DD).",
|
||||
}),
|
||||
),
|
||||
date_before: Type.Optional(
|
||||
Type.String({
|
||||
description: "Only results published before this date (YYYY-MM-DD).",
|
||||
}),
|
||||
),
|
||||
search_lang: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Brave language code for search results (e.g., 'en', 'de', 'en-gb', 'zh-hans', 'zh-hant', 'pt-br').",
|
||||
}),
|
||||
),
|
||||
ui_lang: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Locale code for UI elements in language-region format (e.g., 'en-US', 'de-DE', 'fr-FR', 'tr-TR'). Must include region subtag.",
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function missingBraveKeyPayload() {
|
||||
return {
|
||||
error: "missing_brave_api_key",
|
||||
message: `web_search (brave) needs a Brave Search API key. Run \`${formatCliCommand("openclaw configure --section web")}\` to store it, or set BRAVE_API_KEY in the Gateway environment.`,
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
return next;
|
||||
}
|
||||
|
||||
function createBraveToolDefinition(
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): WebSearchProviderToolDefinition {
|
||||
const braveConfig = resolveBraveConfig(searchConfig);
|
||||
const braveMode = resolveBraveMode(braveConfig);
|
||||
const braveMode = resolveBraveMode(resolveBraveConfig(searchConfig));
|
||||
|
||||
return {
|
||||
description:
|
||||
@@ -452,178 +109,8 @@ function createBraveToolDefinition(
|
||||
: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.",
|
||||
parameters: createBraveSchema(),
|
||||
execute: async (args) => {
|
||||
const apiKey = resolveBraveApiKey(searchConfig);
|
||||
if (!apiKey) {
|
||||
return missingBraveKeyPayload();
|
||||
}
|
||||
|
||||
const params = args;
|
||||
const query = readStringParam(params, "query", { required: true });
|
||||
const count =
|
||||
readNumberParam(params, "count", { integer: true }) ??
|
||||
searchConfig?.maxResults ??
|
||||
undefined;
|
||||
const country = normalizeBraveCountry(readStringParam(params, "country"));
|
||||
const language = readStringParam(params, "language");
|
||||
const search_lang = readStringParam(params, "search_lang");
|
||||
const ui_lang = readStringParam(params, "ui_lang");
|
||||
const normalizedLanguage = normalizeBraveLanguageParams({
|
||||
search_lang: search_lang || language,
|
||||
ui_lang,
|
||||
});
|
||||
if (normalizedLanguage.invalidField === "search_lang") {
|
||||
return {
|
||||
error: "invalid_search_lang",
|
||||
message:
|
||||
"search_lang must be a Brave-supported language code like 'en', 'en-gb', 'zh-hans', or 'zh-hant'.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
if (normalizedLanguage.invalidField === "ui_lang") {
|
||||
return {
|
||||
error: "invalid_ui_lang",
|
||||
message: "ui_lang must be a language-region locale like 'en-US'.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
if (normalizedLanguage.ui_lang && braveMode === "llm-context") {
|
||||
return {
|
||||
error: "unsupported_ui_lang",
|
||||
message:
|
||||
"ui_lang is not supported by Brave llm-context mode. Remove ui_lang or use Brave web mode for locale-based UI hints.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
|
||||
const rawFreshness = readStringParam(params, "freshness");
|
||||
if (rawFreshness && braveMode === "llm-context") {
|
||||
return {
|
||||
error: "unsupported_freshness",
|
||||
message:
|
||||
"freshness filtering is not supported by Brave llm-context mode. Remove freshness or use Brave web mode.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
const freshness = rawFreshness ? normalizeFreshness(rawFreshness, "brave") : undefined;
|
||||
if (rawFreshness && !freshness) {
|
||||
return {
|
||||
error: "invalid_freshness",
|
||||
message: "freshness must be day, week, month, or year.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
|
||||
const rawDateAfter = readStringParam(params, "date_after");
|
||||
const rawDateBefore = readStringParam(params, "date_before");
|
||||
if (rawFreshness && (rawDateAfter || rawDateBefore)) {
|
||||
return {
|
||||
error: "conflicting_time_filters",
|
||||
message:
|
||||
"freshness and date_after/date_before cannot be used together. Use either freshness (day/week/month/year) or a date range (date_after/date_before), not both.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
if ((rawDateAfter || rawDateBefore) && braveMode === "llm-context") {
|
||||
return {
|
||||
error: "unsupported_date_filter",
|
||||
message:
|
||||
"date_after/date_before filtering is not supported by Brave llm-context mode. Use Brave web mode for date filters.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
const parsedDateRange = parseIsoDateRange({
|
||||
rawDateAfter,
|
||||
rawDateBefore,
|
||||
invalidDateAfterMessage: "date_after must be YYYY-MM-DD format.",
|
||||
invalidDateBeforeMessage: "date_before must be YYYY-MM-DD format.",
|
||||
invalidDateRangeMessage: "date_after must be before date_before.",
|
||||
});
|
||||
if ("error" in parsedDateRange) {
|
||||
return parsedDateRange;
|
||||
}
|
||||
const { dateAfter, dateBefore } = parsedDateRange;
|
||||
|
||||
const cacheKey = buildSearchCacheKey([
|
||||
"brave",
|
||||
braveMode,
|
||||
query,
|
||||
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
country,
|
||||
normalizedLanguage.search_lang,
|
||||
normalizedLanguage.ui_lang,
|
||||
freshness,
|
||||
dateAfter,
|
||||
dateBefore,
|
||||
]);
|
||||
const cached = readCachedSearchPayload(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig);
|
||||
const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig);
|
||||
|
||||
if (braveMode === "llm-context") {
|
||||
const { results, sources } = await runBraveLlmContextSearch({
|
||||
query,
|
||||
apiKey,
|
||||
timeoutSeconds,
|
||||
country: country ?? undefined,
|
||||
search_lang: normalizedLanguage.search_lang,
|
||||
freshness,
|
||||
});
|
||||
const payload = {
|
||||
query,
|
||||
provider: "brave",
|
||||
mode: "llm-context" as const,
|
||||
count: results.length,
|
||||
tookMs: Date.now() - start,
|
||||
externalContent: {
|
||||
untrusted: true,
|
||||
source: "web_search",
|
||||
provider: "brave",
|
||||
wrapped: true,
|
||||
},
|
||||
results: results.map((entry) => ({
|
||||
title: entry.title ? wrapWebContent(entry.title, "web_search") : "",
|
||||
url: entry.url,
|
||||
snippets: entry.snippets.map((snippet) => wrapWebContent(snippet, "web_search")),
|
||||
siteName: entry.siteName,
|
||||
})),
|
||||
sources,
|
||||
};
|
||||
writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
|
||||
return payload;
|
||||
}
|
||||
|
||||
const results = await runBraveWebSearch({
|
||||
query,
|
||||
count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
apiKey,
|
||||
timeoutSeconds,
|
||||
country: country ?? undefined,
|
||||
search_lang: normalizedLanguage.search_lang,
|
||||
ui_lang: normalizedLanguage.ui_lang,
|
||||
freshness,
|
||||
dateAfter,
|
||||
dateBefore,
|
||||
});
|
||||
const payload = {
|
||||
query,
|
||||
provider: "brave",
|
||||
count: results.length,
|
||||
tookMs: Date.now() - start,
|
||||
externalContent: {
|
||||
untrusted: true,
|
||||
source: "web_search",
|
||||
provider: "brave",
|
||||
wrapped: true,
|
||||
},
|
||||
results,
|
||||
};
|
||||
writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
|
||||
return payload;
|
||||
const { executeBraveSearch } = await import("./brave-web-search-provider.runtime.js");
|
||||
return await executeBraveSearch(args, searchConfig);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -662,7 +149,6 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
normalizeFreshness,
|
||||
normalizeBraveCountry,
|
||||
normalizeBraveLanguageParams,
|
||||
resolveBraveMode,
|
||||
|
||||
Reference in New Issue
Block a user