Add Brave LLM Context endpoint as opt-in web search mode

Brave's /res/v1/llm/context returns pre-extracted text chunks
optimized for LLM consumption instead of URLs + short snippets.
Enable with BRAVE_LLM_CONTEXT=1 env var; same API key and pricing.

- Add _search_llm_context() and _normalize_llm_context() to brave_search.py
- Wire opt-in flag through _search_web() in last30days.py
- Update module docstring (free tier eliminated Feb 2026)
- Add 23 tests covering normalization, filtering, date parsing
This commit is contained in:
Jeffrey Sperling
2026-03-11 17:46:34 -07:00
parent 4fde52459d
commit 859f6c5829
3 changed files with 318 additions and 3 deletions
+118 -2
View File
@@ -1,7 +1,12 @@
"""Brave Search web search for last30days skill.
Uses the Brave Search API as a fallback web search backend.
Simple, cheap (free tier: 2,000 queries/month), widely available.
Uses the Brave Search API as a web search backend.
Requires a paid Brave Search subscription (free tier eliminated Feb 2026).
Two modes:
- Standard: /res/v1/web/search — returns URLs + snippets (default)
- LLM Context: /res/v1/llm/context — returns pre-extracted text chunks
optimized for LLM consumption. Enable with BRAVE_LLM_CONTEXT=1 env var.
API docs: https://api-dashboard.search.brave.com/app/documentation/web-search/get-started
"""
@@ -16,6 +21,7 @@ from urllib.parse import urlencode, urlparse
from . import http
ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
LLM_CONTEXT_ENDPOINT = "https://api.search.brave.com/res/v1/llm/context"
# Freshness codes: pd=24h, pw=7d, pm=31d
FRESHNESS_MAP = {1: "pd", 7: "pw", 31: "pm"}
@@ -33,6 +39,7 @@ def search_web(
to_date: str,
api_key: str,
depth: str = "default",
use_llm_context: bool = False,
) -> List[Dict[str, Any]]:
"""Search the web via Brave Search API.
@@ -42,6 +49,7 @@ def search_web(
to_date: End date (YYYY-MM-DD)
api_key: Brave Search API key
depth: 'quick', 'default', or 'deep'
use_llm_context: Use LLM Context endpoint for pre-extracted content
Returns:
List of result dicts with keys: url, title, snippet, source_domain, date, relevance
@@ -49,6 +57,9 @@ def search_web(
Raises:
http.HTTPError: On API errors
"""
if use_llm_context:
return _search_llm_context(topic, from_date, to_date, api_key, depth)
count = {"quick": 8, "default": 15, "deep": 25}.get(depth, 15)
# Calculate days for freshness filter
@@ -81,6 +92,48 @@ def search_web(
return _normalize_results(response, from_date, to_date)
def _search_llm_context(
topic: str,
from_date: str,
to_date: str,
api_key: str,
depth: str = "default",
) -> List[Dict[str, Any]]:
"""Search via Brave LLM Context endpoint for pre-extracted web content.
Returns results in the same schema as search_web() for downstream compatibility.
Snippets contain actual page content instead of short descriptions.
"""
count = {"quick": 5, "default": 20, "deep": 50}.get(depth, 20)
max_tokens = {"quick": 2048, "default": 8192, "deep": 16384}.get(depth, 8192)
days = _days_between(from_date, to_date)
freshness = _brave_freshness(days)
params = {
"q": topic,
"count": count,
"maximum_number_of_tokens": max_tokens,
"context_threshold_mode": "balanced",
}
if freshness:
params["freshness"] = freshness
url = f"{LLM_CONTEXT_ENDPOINT}?{urlencode(params)}"
sys.stderr.write(f"[Web] Searching Brave LLM Context for: {topic}\n")
sys.stderr.flush()
response = http.request(
"GET",
url,
headers={"X-Subscription-Token": api_key},
timeout=30,
)
return _normalize_llm_context(response)
def _days_between(from_date: str, to_date: str) -> int:
"""Calculate days between two YYYY-MM-DD dates."""
try:
@@ -169,6 +222,69 @@ def _normalize_results(
return items
def _normalize_llm_context(response: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Convert Brave LLM Context response to websearch item schema.
LLM Context returns grounding.generic[] with url, title, snippets[].
Sources metadata provides hostname and age for each URL.
"""
items = []
grounding = response.get("grounding", {})
sources = response.get("sources", {})
for i, result in enumerate(grounding.get("generic", [])):
if not isinstance(result, dict):
continue
url = result.get("url", "")
if not url:
continue
# Skip excluded domains
try:
domain = urlparse(url).netloc.lower()
if domain in EXCLUDED_DOMAINS:
continue
if domain.startswith("www."):
domain = domain[4:]
except Exception:
domain = ""
title = str(result.get("title", "")).strip()
snippets = result.get("snippets", [])
snippet = "\n".join(str(s).strip() for s in snippets if s)
if not title and not snippet:
continue
# Parse date from sources metadata
source_meta = sources.get(url, {})
age_list = source_meta.get("age") or []
date = None
for age_str in age_list:
date = _parse_brave_date(age_str, None)
if date:
break
date_confidence = "med" if date else "low"
items.append({
"id": f"W{i+1}",
"title": title[:200],
"url": url,
"source_domain": source_meta.get("hostname", domain),
"snippet": snippet[:1500], # LLM Context returns richer content
"date": date,
"date_confidence": date_confidence,
"relevance": 0.7, # LLM Context pre-filters for relevance
"why_relevant": "",
})
sys.stderr.write(f"[Web] Brave LLM Context: {len(items)} results\n")
sys.stderr.flush()
return items
def _clean_html(text: str) -> str:
"""Remove HTML tags and decode entities."""
text = re.sub(r"<[^>]*>", "", text)