diff --git a/extensions/brave/src/brave-web-search-provider.test.ts b/extensions/brave/src/brave-web-search-provider.test.ts index 996c4599249..245feb07ab8 100644 --- a/extensions/brave/src/brave-web-search-provider.test.ts +++ b/extensions/brave/src/brave-web-search-provider.test.ts @@ -1,7 +1,8 @@ import fs from "node:fs"; import { afterEach, describe, expect, it, vi } from "vitest"; import { validateJsonSchemaValue } from "../../../src/plugins/schema-validator.js"; -import { __testing, createBraveWebSearchProvider } from "./brave-web-search-provider.js"; +import { __testing } from "../test-api.js"; +import { createBraveWebSearchProvider } from "./brave-web-search-provider.js"; const braveManifest = JSON.parse( fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"), diff --git a/extensions/brave/src/brave-web-search-provider.ts b/extensions/brave/src/brave-web-search-provider.ts index fba71e8b282..9fd8140a868 100644 --- a/extensions/brave/src/brave-web-search-provider.ts +++ b/extensions/brave/src/brave-web-search-provider.ts @@ -3,25 +3,59 @@ import type { WebSearchProviderPlugin, WebSearchProviderToolDefinition, } from "openclaw/plugin-sdk/provider-web-search"; -import { isRecord } from "openclaw/plugin-sdk/text-runtime"; -import { - createBraveSchema, - mapBraveLlmContextResults, - normalizeBraveCountry, - normalizeBraveLanguageParams, - resolveBraveConfig, - resolveBraveMode, -} from "./brave-web-search-provider.shared.js"; +import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-config-contract"; -type ConfigInput = Parameters< - NonNullable ->[0]; -type ConfigTarget = Parameters< - NonNullable ->[0]; +const BRAVE_CREDENTIAL_PATH = "plugins.entries.brave.config.webSearch.apiKey"; +const BraveSearchSchema = { + type: "object", + properties: { + query: { type: "string", description: "Search query string." }, + count: { + type: "number", + description: "Number of results to return (1-10).", + minimum: 1, + maximum: 10, + }, + country: { + type: "string", + description: + "2-letter country code for region-specific results (e.g., 'DE', 'US', 'ALL'). Default: 'US'.", + }, + language: { + type: "string", + description: "ISO 639-1 language code for results (e.g., 'en', 'de', 'fr').", + }, + freshness: { + type: "string", + description: "Filter by time: 'day' (24h), 'week', 'month', or 'year'.", + }, + date_after: { + type: "string", + description: "Only results published after this date (YYYY-MM-DD).", + }, + date_before: { + type: "string", + description: "Only results published before this date (YYYY-MM-DD).", + }, + search_lang: { + type: "string", + description: + "Brave language code for search results (e.g., 'en', 'de', 'en-gb', 'zh-hans', 'zh-hant', 'pt-br').", + }, + ui_lang: { + 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.", + }, + }, +} satisfies Record; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} function resolveProviderWebSearchPluginConfig( - config: ConfigInput, + config: unknown, pluginId: string, ): Record | undefined { if (!isRecord(config)) { @@ -34,40 +68,6 @@ function resolveProviderWebSearchPluginConfig( return isRecord(pluginConfig?.webSearch) ? pluginConfig.webSearch : undefined; } -function ensureObject(target: Record, key: string): Record { - const current = target[key]; - if (isRecord(current)) { - return current; - } - const next: Record = {}; - target[key] = next; - return next; -} - -function setProviderWebSearchPluginConfigValue( - configTarget: ConfigTarget, - pluginId: string, - key: string, - value: unknown, -): void { - const plugins = ensureObject(configTarget as Record, "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, - value: unknown, -): void { - searchConfigTarget.apiKey = value; -} - function mergeScopedSearchConfig( searchConfig: Record | undefined, key: string, @@ -94,17 +94,22 @@ function mergeScopedSearchConfig( return next; } +function resolveBraveMode(searchConfig?: Record): "web" | "llm-context" { + const brave = isRecord(searchConfig?.brave) ? searchConfig.brave : undefined; + return brave?.mode === "llm-context" ? "llm-context" : "web"; +} + function createBraveToolDefinition( searchConfig?: SearchConfigRecord, ): WebSearchProviderToolDefinition { - const braveMode = resolveBraveMode(resolveBraveConfig(searchConfig)); + const braveMode = resolveBraveMode(searchConfig); return { description: braveMode === "llm-context" ? "Search the web using Brave Search LLM Context API. Returns pre-extracted page content (text chunks, tables, code blocks) optimized for LLM grounding." : "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(), + parameters: BraveSearchSchema, execute: async (args) => { const { executeBraveSearch } = await import("./brave-web-search-provider.runtime.js"); return await executeBraveSearch(args, searchConfig); @@ -124,15 +129,12 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin { signupUrl: "https://brave.com/search/api/", docsUrl: "https://docs.openclaw.ai/brave-search", autoDetectOrder: 10, - credentialPath: "plugins.entries.brave.config.webSearch.apiKey", - inactiveSecretPaths: ["plugins.entries.brave.config.webSearch.apiKey"], - getCredentialValue: (searchConfig) => searchConfig?.apiKey, - setCredentialValue: setTopLevelCredentialValue, - getConfiguredCredentialValue: (config) => - resolveProviderWebSearchPluginConfig(config, "brave")?.apiKey, - setConfiguredCredentialValue: (configTarget, value) => { - setProviderWebSearchPluginConfigValue(configTarget, "brave", "apiKey", value); - }, + credentialPath: BRAVE_CREDENTIAL_PATH, + ...createWebSearchProviderContractFields({ + credentialPath: BRAVE_CREDENTIAL_PATH, + searchCredential: { type: "top-level" }, + configuredCredential: { pluginId: "brave" }, + }), createTool: (ctx) => createBraveToolDefinition( mergeScopedSearchConfig( @@ -144,10 +146,3 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin { ), }; } - -export const __testing = { - normalizeBraveCountry, - normalizeBraveLanguageParams, - resolveBraveMode, - mapBraveLlmContextResults, -} as const; diff --git a/extensions/brave/test-api.ts b/extensions/brave/test-api.ts index b523a2c51b1..c1c12b7dc13 100644 --- a/extensions/brave/test-api.ts +++ b/extensions/brave/test-api.ts @@ -1 +1,13 @@ -export { __testing } from "./src/brave-web-search-provider.js"; +import { + mapBraveLlmContextResults, + normalizeBraveCountry, + normalizeBraveLanguageParams, + resolveBraveMode, +} from "./src/brave-web-search-provider.shared.js"; + +export const __testing = { + normalizeBraveCountry, + normalizeBraveLanguageParams, + resolveBraveMode, + mapBraveLlmContextResults, +} as const; diff --git a/extensions/brave/web-search-provider.ts b/extensions/brave/web-search-provider.ts index 634c7931c97..01041edf46b 100644 --- a/extensions/brave/web-search-provider.ts +++ b/extensions/brave/web-search-provider.ts @@ -1 +1 @@ -export { __testing, createBraveWebSearchProvider } from "./src/brave-web-search-provider.js"; +export { createBraveWebSearchProvider } from "./src/brave-web-search-provider.js";