Coverage for src/local_deep_research/web_search_engines/engines/search_engine_brave.py: 98%
50 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-20 01:24 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-20 01:24 +0000
1from typing import Any, Dict, List, Optional
3from langchain_community.tools import BraveSearch
4from langchain_core.language_models import BaseLLM
6from ...security.secure_logging import logger
8from ..rate_limiting import RateLimitError
9from ..search_engine_base import BaseSearchEngine, Exposure, Sensitivity
12class BraveSearchEngine(BaseSearchEngine):
13 """Brave search engine implementation with two-phase approach"""
15 # Mark as public search engine
16 is_public = True
17 egress_sensitivity = Sensitivity.NON_SENSITIVE
18 egress_exposure = Exposure.EXPOSING
19 # Mark as generic search engine (general web search)
20 is_generic = True
21 # secrets to redact from error messages (see BaseSearchEngine._scrub_error)
22 _secret_attrs = ("_brave_api_key",)
24 def __init__(
25 self,
26 max_results: int = 10,
27 region: str = "US",
28 time_period: str = "y",
29 safe_search: bool = True,
30 search_language: str = "English",
31 api_key: Optional[str] = None,
32 language_code_mapping: Optional[Dict[str, str]] = None,
33 llm: Optional[BaseLLM] = None,
34 include_full_content: bool = True,
35 max_filtered_results: Optional[int] = None,
36 settings_snapshot: Optional[Dict[str, Any]] = None,
37 **kwargs,
38 ):
39 """
40 Initialize the Brave search engine.
42 Args:
43 max_results: Maximum number of search results
44 region: Region code for search results
45 time_period: Time period for search results
46 safe_search: Whether to enable safe search
47 search_language: Language for search results
48 api_key: Brave Search API key (can also be set via LDR_SEARCH_ENGINE_WEB_BRAVE_API_KEY env var or in UI settings)
49 language_code_mapping: Mapping from language names to codes
50 llm: Language model for relevance filtering
51 include_full_content: Whether to include full webpage content in results
52 max_filtered_results: Maximum number of results to keep after filtering
53 settings_snapshot: Settings snapshot for thread context
54 **kwargs: Additional parameters (ignored but accepted for compatibility)
55 """
56 # Initialize the BaseSearchEngine with LLM, max_filtered_results, and max_results
57 super().__init__(
58 llm=llm,
59 max_filtered_results=max_filtered_results,
60 max_results=max_results,
61 include_full_content=include_full_content,
62 settings_snapshot=settings_snapshot,
63 )
65 # Set up language code mapping
66 if language_code_mapping is None:
67 from ...utilities.search_utilities import LANGUAGE_CODE_MAP
69 language_code_mapping = LANGUAGE_CODE_MAP
71 # Get API key - check params, settings, or env vars
72 brave_api_key = self._resolve_api_key(
73 api_key,
74 "search.engine.web.brave.api_key",
75 engine_name="Brave Search",
76 settings_snapshot=settings_snapshot,
77 )
78 self._brave_api_key = brave_api_key
80 # Get language code
81 language_code = language_code_mapping.get(search_language.lower(), "en")
83 # Convert time period format to Brave's format
84 brave_time_period = f"p{time_period}"
86 # Convert safe search to Brave's format
87 brave_safe_search = "moderate" if safe_search else "off"
89 # Initialize Brave Search
90 self.engine = BraveSearch.from_api_key(
91 api_key=brave_api_key,
92 search_kwargs={
93 "count": min(20, max_results),
94 "country": region.upper(),
95 "search_lang": language_code,
96 "safesearch": brave_safe_search,
97 "freshness": brave_time_period,
98 },
99 )
101 # User agent is not needed for Brave Search API
103 # If full content is requested, initialize FullSearchResults
104 self._init_full_search(
105 web_search=self.engine,
106 language=search_language,
107 max_results=max_results,
108 region=region,
109 time_period=time_period,
110 safe_search=brave_safe_search,
111 )
113 def _get_previews(self, query: str) -> List[Dict[str, Any]]:
114 """
115 Get preview information from Brave Search.
117 Args:
118 query: The search query
120 Returns:
121 List of preview dictionaries
122 """
123 logger.info("Getting search results from Brave Search")
125 try:
126 # Get search results from Brave Search
127 raw_results = self.engine.run(query[:400])
129 # Parse results if they're in string format
130 if isinstance(raw_results, str):
131 try:
132 import json
134 raw_results = json.loads(raw_results)
135 except json.JSONDecodeError as e:
136 safe_msg = self._scrub_error(e)
137 logger.warning(
138 f"Unable to parse BraveSearch response as JSON: {safe_msg}"
139 )
140 return []
142 # Format results as previews
143 previews = []
144 for i, result in enumerate(raw_results):
145 preview = {
146 "id": i, # Use index as ID
147 "title": result.get("title", ""),
148 "link": result.get("link", ""),
149 "snippet": result.get("snippet", ""),
150 "displayed_link": result.get("link", ""),
151 "position": i,
152 }
154 # Store full Brave result for later
155 preview["_full_result"] = result
157 previews.append(preview)
159 # Store the previews for potential full content retrieval
160 self._search_results = previews
162 return previews
164 except RateLimitError:
165 raise
166 except Exception as e:
167 safe_msg = self._scrub_error(e)
168 logger.warning(f"Error getting Brave Search results: {safe_msg}")
169 self._raise_if_rate_limit(e)
170 return []