/** * Brave Search request normalization and result mapping. It validates Brave * country/language params and converts LLM-context responses into web results. */ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, } from "openclaw/plugin-sdk/string-coerce-runtime"; type BraveConfig = { baseUrl?: unknown; mode?: string; }; type BraveLlmContextResult = { url: string; title: string; snippets: string[] }; /** Brave LLM Context API response subset used by OpenClaw. */ 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 = { 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; function normalizeBraveSearchLang(value: string | undefined): string | undefined { if (!value) { return undefined; } const trimmed = value.trim(); if (!trimmed) { return undefined; } const lower = normalizeLowercaseStringOrEmpty(trimmed); const canonical = BRAVE_SEARCH_LANG_ALIASES[lower] ?? lower; if (!BRAVE_SEARCH_LANG_CODES.has(canonical)) { return undefined; } return canonical; } /** Normalize Brave country filter values. */ 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 `${normalizeLowercaseStringOrEmpty(language)}-${region.toUpperCase()}`; } /** Resolve Brave-specific web-search config from scoped search config. */ export function resolveBraveConfig(searchConfig?: Record): BraveConfig { const brave = searchConfig?.brave; return brave && typeof brave === "object" && !Array.isArray(brave) ? (brave as BraveConfig) : {}; } /** Resolve whether Brave should use web search or LLM Context API mode. */ export function resolveBraveMode(brave?: BraveConfig): "web" | "llm-context" { return brave?.mode === "llm-context" ? "llm-context" : "web"; } /** Normalize Brave search and UI language params, detecting swapped fields. */ export function normalizeBraveLanguageParams(params: { search_lang?: string; ui_lang?: string }): { search_lang?: string; ui_lang?: string; invalidField?: "search_lang" | "ui_lang"; } { const rawSearchLang = normalizeOptionalString(params.search_lang); const rawUiLang = normalizeOptionalString(params.ui_lang); 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; } } /** Map Brave LLM Context API grounding results into web-search result rows. */ 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, })); }