Add Brave web search support

This commit is contained in:
2026-05-18 22:38:54 +02:00
parent 1ef2893345
commit 7147c657ab
10 changed files with 640 additions and 2 deletions

View File

@@ -60,6 +60,24 @@ class AIPromptConfig:
user_template: str | None = None
@dataclass(slots=True)
class AIWebSearchConfig:
enabled: bool = True
api_key: str | None = None
endpoint: str = "https://api.search.brave.com/res/v1/web/search"
count: int = 5
country: str = "US"
search_lang: str = "en"
ui_lang: str = "en-US"
safesearch: str = "moderate"
timeout: float = 10.0
extra_snippets: bool = False
@property
def available(self) -> bool:
return self.enabled and bool(self.api_key)
@dataclass(slots=True)
class AIConfig:
default_model: str
@@ -69,6 +87,7 @@ class AIConfig:
models: dict[str, AIModelConfig] = field(default_factory=dict)
workspace_dir: Path = field(default_factory=lambda: Path("workspace"))
tool_max_rounds: int = 10
web_search: AIWebSearchConfig = field(default_factory=AIWebSearchConfig)
@dataclass(slots=True)
@@ -127,6 +146,20 @@ def _parse_float(value: Any, *, field_name: str) -> float:
raise ValueError(f"{field_name} must be a number") from exc
def _parse_bool(value: Any, *, field_name: str) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in {"1", "true", "yes", "on"}:
return True
if normalized in {"0", "false", "no", "off"}:
return False
if isinstance(value, int):
return bool(value)
raise ValueError(f"{field_name} must be a boolean")
def _parse_mapping(value: Any, *, field_name: str) -> dict[str, Any]:
if value is None:
return {}
@@ -135,6 +168,69 @@ def _parse_mapping(value: Any, *, field_name: str) -> dict[str, Any]:
return value
def _parse_web_search_config(raw_ai: dict[str, Any]) -> AIWebSearchConfig:
raw = _parse_mapping(raw_ai.get("web_search"), field_name="ai.web_search")
enabled = _parse_bool(raw.get("enabled", True), field_name="ai.web_search.enabled")
api_key = str(raw.get("api_key") or "").strip() or None
endpoint = str(
raw.get("endpoint") or "https://api.search.brave.com/res/v1/web/search"
).strip()
if not endpoint:
raise ValueError("ai.web_search.endpoint is required")
count = _parse_int(raw.get("count", 5), field_name="ai.web_search.count")
count = min(max(count, 1), 20)
country = str(raw.get("country", "US") or "US").strip().upper()
search_lang = str(raw.get("search_lang", "en") or "en").strip().lower()
ui_lang = str(raw.get("ui_lang", "en-US") or "en-US").strip()
safesearch = str(raw.get("safesearch", "moderate") or "moderate").strip().lower()
if safesearch not in {"off", "moderate", "strict"}:
raise ValueError("ai.web_search.safesearch must be one of: off, moderate, strict")
timeout = _parse_float(raw.get("timeout", 10.0), field_name="ai.web_search.timeout")
if timeout <= 0:
raise ValueError("ai.web_search.timeout must be > 0")
extra_snippets = _parse_bool(
raw.get("extra_snippets", False),
field_name="ai.web_search.extra_snippets",
)
env_api_key = os.environ.get("BRAVE_API_KEY")
if env_api_key is not None:
api_key = env_api_key.strip() or None
if os.environ.get("BRAVE_SEARCH_COUNT") is not None:
count = min(
max(_parse_int(os.environ["BRAVE_SEARCH_COUNT"], field_name="BRAVE_SEARCH_COUNT"), 1),
20,
)
if os.environ.get("BRAVE_SEARCH_COUNTRY") is not None:
country = os.environ["BRAVE_SEARCH_COUNTRY"].strip().upper() or country
if os.environ.get("BRAVE_SEARCH_LANG") is not None:
search_lang = os.environ["BRAVE_SEARCH_LANG"].strip().lower() or search_lang
if os.environ.get("BRAVE_SEARCH_UI_LANG") is not None:
ui_lang = os.environ["BRAVE_SEARCH_UI_LANG"].strip() or ui_lang
if os.environ.get("BRAVE_SEARCH_SAFESEARCH") is not None:
safesearch = os.environ["BRAVE_SEARCH_SAFESEARCH"].strip().lower()
if safesearch not in {"off", "moderate", "strict"}:
raise ValueError("BRAVE_SEARCH_SAFESEARCH must be one of: off, moderate, strict")
return AIWebSearchConfig(
enabled=enabled,
api_key=api_key,
endpoint=endpoint,
count=count,
country=country,
search_lang=search_lang,
ui_lang=ui_lang,
safesearch=safesearch,
timeout=timeout,
extra_snippets=extra_snippets,
)
def _parse_accounts(raw_accounts: Any, *, default_api_id: int | None, default_api_hash: str | None, default_session_dir: Path) -> list[AccountConfig]:
if raw_accounts is None:
return []
@@ -287,6 +383,8 @@ def _parse_ai_config(raw_ai: Any, *, root: Path) -> AIConfig | None:
if tool_max_rounds < 1:
raise ValueError("ai.tool_max_rounds must be >= 1")
web_search = _parse_web_search_config(raw_ai)
return AIConfig(
default_model=default_model,
context_messages=context_messages,
@@ -295,6 +393,7 @@ def _parse_ai_config(raw_ai: Any, *, root: Path) -> AIConfig | None:
models=models,
workspace_dir=workspace_dir,
tool_max_rounds=tool_max_rounds,
web_search=web_search,
)