Fix Brave search auth and prompt variables

This commit is contained in:
2026-05-19 21:33:46 +02:00
parent 68934b23a0
commit 5ed1104c12
8 changed files with 187 additions and 19 deletions

View File

@@ -1,10 +1,13 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
import re
DEFAULT_SYSTEM_PROMPT = "You are a concise, accurate Telegram assistant."
TEMPLATE_RE = re.compile(r"{{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*}}")
@dataclass(slots=True)
@@ -16,6 +19,30 @@ class PromptBook:
def _read_file(self, path: Path) -> str:
return path.read_text(encoding="utf-8").strip()
def _template_context(self, now: datetime | None = None) -> dict[str, str]:
current = now.astimezone() if now else datetime.now().astimezone()
utc = current.astimezone(UTC)
return {
"current_date": current.date().isoformat(),
"current_time": current.strftime("%H:%M:%S"),
"current_datetime": current.isoformat(timespec="seconds"),
"current_weekday": current.strftime("%A"),
"timezone": current.tzname() or "",
"utc_date": utc.date().isoformat(),
"utc_time": utc.strftime("%H:%M:%S"),
"utc_datetime": utc.isoformat(timespec="seconds"),
"unix_timestamp": str(int(current.timestamp())),
}
def _render_template(self, text: str, now: datetime | None = None) -> str:
context = self._template_context(now)
def replace(match: re.Match[str]) -> str:
name = match.group(1)
return context.get(name, match.group(0))
return TEMPLATE_RE.sub(replace, text)
def load_system_prompt(self) -> str:
sections: list[str] = []
system_path = self.root / self.system_file
@@ -34,4 +61,4 @@ class PromptBook:
if not sections:
return DEFAULT_SYSTEM_PROMPT
return "\n\n".join(sections).strip()
return self._render_template("\n\n".join(sections)).strip()

View File

@@ -67,9 +67,12 @@ class HistoryMessage:
class AIService:
def __init__(self, config: AIConfig) -> None:
self.config = config
self.prompt_book = PromptBook(config.prompt.root)
self.prompt_book = PromptBook(
config.prompt.root,
system_file=config.prompt.system_file,
parts_dir=config.prompt.parts_dir,
)
self._clients: dict[str, AsyncOpenAI] = {}
self._system_prompt: str | None = None
self.web_search = BraveWebSearchClient(config.web_search)
self.page_extractor = PageExtractor(config.page_extract)
@@ -128,9 +131,7 @@ class AIService:
return client
def load_system_prompt(self) -> str:
if self._system_prompt is None:
self._system_prompt = self.prompt_book.load_system_prompt()
return self._system_prompt
return self.prompt_book.load_system_prompt()
async def _maybe_call_progress(self, progress: ProgressCallback | None, text: str) -> None:
if progress is None:

View File

@@ -172,18 +172,37 @@ def format_results(response: WebSearchResponse, *, include_heading: bool = True)
class BraveWebSearchClient:
def __init__(self, config: AIWebSearchConfig) -> None:
def __init__(
self,
config: AIWebSearchConfig,
*,
transport: httpx.AsyncBaseTransport | None = None,
) -> None:
self.config = config
self.transport = transport
@property
def available(self) -> bool:
return self.config.available
def _build_headers(self) -> dict[str, str]:
api_key = str(self.config.api_key or "").strip()
if api_key.lower().startswith("bearer "):
api_key = api_key[7:].strip()
if not api_key:
raise ValueError(
"Brave web search is not configured. Set BRAVE_API_KEY or BRAVE_SEARCH_API_KEY."
)
return {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": api_key,
}
async def search(self, arguments: dict[str, Any]) -> WebSearchResponse:
if not self.config.enabled:
raise ValueError("Brave web search is disabled")
if not self.config.api_key:
raise ValueError("Brave web search is not configured. Set BRAVE_API_KEY.")
headers = self._build_headers()
query = build_query(self.config, arguments)
params: dict[str, Any] = {
@@ -202,14 +221,11 @@ class BraveWebSearchClient:
if query.extra_snippets:
params["extra_snippets"] = True
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": self.config.api_key,
}
try:
async with httpx.AsyncClient(timeout=self.config.timeout) as client:
async with httpx.AsyncClient(
timeout=self.config.timeout,
transport=self.transport,
) as client:
response = await client.get(self.config.endpoint, params=params, headers=headers)
except httpx.HTTPError as exc:
raise ValueError(f"Brave web search request failed: {exc}") from exc