5.6 KiB
Design: Brave Search API Integration for Phase 2
Date: 2026-03-25 Scope: Add Brave Search as an alternative to LLM web search in Phase 2
Context
Phase 2 currently uses LLM web grounding (Gemini/OpenAI) to find articles for unfilled categories. The results are often imprecise. Brave Search API provides a high-quality web index that can produce more relevant results.
When enabled, Brave Search replaces the LLM web search. The returned URLs are then scraped and classified/summarized by the LLM — same as Phase 1's per-article flow.
Note: The app is French-only (i18n-ready but French for now), so search queries and language are hardcoded to French.
1. Settings & API Key
New setting
use_brave_search: bool (default false) in the settings table. When enabled, Phase 2 uses Brave Search API instead of LLM web search.
API key storage
Stored in user_api_keys with provider_name = "brave_search". Reuses existing encrypted storage and CRUD endpoints.
Frontend: Brave Search section in Settings
A dedicated section in Settings.tsx (separate from the LLM ApiKeyManager component) with:
- A Brave API key input (using the existing
user_api_keysCRUD API withprovider_name = "brave_search") - The
use_brave_searchtoggle, disabled (grayed out) until a Brave API key is configured - Key status display (configured/not configured, prefix)
The Brave key is NOT rendered via ApiKeyManager (which only renders admin-configured LLM providers). It gets its own standalone section.
Auto-disable on key deletion
Frontend-side: after a successful DELETE /api/v1/user/api-keys/brave_search, the frontend also sends a PUT /api/v1/settings with use_brave_search: false if it was previously on.
Backend validation
At generation time, if use_brave_search is true but no Brave key is found, return an error (same behavior as a missing LLM provider key).
Test endpoint
The existing POST /api/v1/user/api-keys/:provider/test handler must handle brave_search gracefully. Instead of creating an LLM provider (which would fail), it should call the Brave Search API with a simple test query and verify a 200 response.
2. Brave Search Service
New file: backend/src/services/brave_search.rs
A standalone module that calls the Brave Search API.
Function: search(http_client, api_key, query, count, freshness) -> Result<Vec<BraveResult>>
BraveResult struct: { title: String, url: String, description: String }
API call:
GET https://api.search.brave.com/res/v1/web/search- Header:
X-Subscription-Token: {api_key} - Query params:
q(theme + "actualites"),count(20),freshness(mapped frommax_age_days),search_lang("fr") - Parses
web.resultsarray from JSON response - Returns up to 20 results
Freshness mapping from max_age_days:
<= 1→"pd"(past day)<= 7→"pw"(past week)<= 30→"pm"(past month)> 30→"py"(past year)
Error handling: On Brave API failure (network error, non-200 status, malformed response), return an AppError::Internal with a descriptive message. The generation fails with a clear error — no silent fallback to LLM search.
3. Phase 2 Pipeline Change
When use_brave_search is true:
- Decrypt Brave API key from
user_api_keyswhereprovider_name = "brave_search" - Call Brave Search — query:
"{theme} actualites", count: 20, freshness based onmax_age_days - Filter — same as current Phase 2: homepage URL filter, cross-phase dedup (
seen_urls), article history dedup, source diversity limit. Source type for tracing:"brave_search". - Scrape + LLM classify/summarize — reuse the same batched loop as Phase 1 (respects
batch_sizesetting, parallel scrape viaJoinSet, parallel LLM classify,filled_countstracking, category overflow to "Autre",max_totalcap). The LLM rate limiter applies to classify calls (not to the Brave API call itself). - Merge results into
article_scrapedand updatefilled_counts
When use_brave_search is false: the existing LLM web search flow is unchanged.
Code reuse strategy: The Phase 1 batched scrape+classify loop (lines ~400-550 of synthesis.rs) should be extracted into a shared helper function that both Phase 1 and the Brave Phase 2 path can call, rather than duplicating the logic.
4. Files to Modify
- Create:
backend/migrations/20260325000022_add_brave_search_setting.sql - Create:
backend/src/services/brave_search.rs— Brave Search API client +BraveResultstruct + test function - Modify:
backend/src/services/mod.rs— addpub mod brave_search - Modify:
backend/src/models/settings.rs— adduse_brave_searchtoUserSettings,SettingsResponse,UpdateSettingsRequest,Default, validation - Modify:
backend/src/db/settings.rs— adduse_brave_searchtoSettingsRow, queries, binds - Modify:
backend/src/services/synthesis.rs— extract shared scrape+classify helper; Phase 2 branch: ifuse_brave_search, decrypt Brave key, call Brave, filter, run shared helper; else existing LLM search - Modify:
backend/src/handlers/api_keys.rs— handlebrave_searchin the test endpoint (call Brave API instead of LLM provider) - Modify:
frontend/src/types.ts— adduse_brave_search: booleantoUserSettingsandDEFAULT_SETTINGS - Modify:
frontend/src/pages/Settings.tsx— add standalone Brave Search section with key input + toggle - Modify:
frontend/src/i18n/fr.ts— labels for toggle, key section, and Brave-specific strings - Modify:
CLAUDE.md— migration count