Coverage for src/local_deep_research/web_search_engines/engines/search_engine_brave.py: 98%

51 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-22 21:20 +0000

1from typing import Any, Dict, List, Optional 

2 

3from langchain_community.tools import BraveSearch 

4from langchain_core.language_models import BaseLLM 

5 

6from ...security.secure_logging import logger 

7 

8from ..rate_limiting import RateLimitError 

9from ..search_engine_base import ( 

10 VALID_TIME_PERIODS, 

11 BaseSearchEngine, 

12 Exposure, 

13 Sensitivity, 

14) 

15 

16 

17class BraveSearchEngine(BaseSearchEngine): 

18 """Brave search engine implementation with two-phase approach""" 

19 

20 # Mark as public search engine 

21 is_public = True 

22 egress_sensitivity = Sensitivity.NON_SENSITIVE 

23 egress_exposure = Exposure.EXPOSING 

24 # Mark as generic search engine (general web search) 

25 is_generic = True 

26 # secrets to redact from error messages (see BaseSearchEngine._scrub_error) 

27 _secret_attrs = ("_brave_api_key",) 

28 

29 def __init__( 

30 self, 

31 max_results: int = 10, 

32 region: str = "US", 

33 time_period: str = "y", 

34 safe_search: bool = True, 

35 search_language: str = "English", 

36 api_key: Optional[str] = None, 

37 language_code_mapping: Optional[Dict[str, str]] = None, 

38 llm: Optional[BaseLLM] = None, 

39 include_full_content: bool = True, 

40 max_filtered_results: Optional[int] = None, 

41 settings_snapshot: Optional[Dict[str, Any]] = None, 

42 **kwargs, 

43 ): 

44 """ 

45 Initialize the Brave search engine. 

46 

47 Args: 

48 max_results: Maximum number of search results 

49 region: Region code for search results 

50 time_period: Time period filter (d/w/m/y); forwarded to Brave's 

51 ``freshness`` param as p{d,w,m,y}. ``all`` (or any 

52 unrecognized value) omits the filter. 

53 safe_search: Whether to enable safe search 

54 search_language: Language for search results 

55 api_key: Brave Search API key (can also be set via LDR_SEARCH_ENGINE_WEB_BRAVE_API_KEY env var or in UI settings) 

56 language_code_mapping: Mapping from language names to codes 

57 llm: Language model for relevance filtering 

58 include_full_content: Retained for backward compatibility; full-content 

59 retrieval is handled via the factory wrapper. 

60 max_filtered_results: Maximum number of results to keep after filtering 

61 settings_snapshot: Settings snapshot for thread context 

62 **kwargs: Additional parameters (ignored but accepted for compatibility) 

63 """ 

64 # Initialize the BaseSearchEngine with LLM, max_filtered_results, and max_results 

65 super().__init__( 

66 llm=llm, 

67 max_filtered_results=max_filtered_results, 

68 max_results=max_results, 

69 include_full_content=include_full_content, 

70 settings_snapshot=settings_snapshot, 

71 ) 

72 

73 # Set up language code mapping 

74 if language_code_mapping is None: 

75 from ...utilities.search_utilities import LANGUAGE_CODE_MAP 

76 

77 language_code_mapping = LANGUAGE_CODE_MAP 

78 

79 # Get API key - check params, settings, or env vars 

80 brave_api_key = self._resolve_api_key( 

81 api_key, 

82 "search.engine.web.brave.api_key", 

83 engine_name="Brave Search", 

84 settings_snapshot=settings_snapshot, 

85 ) 

86 self._brave_api_key = brave_api_key 

87 

88 # Get language code 

89 language_code = language_code_mapping.get(search_language.lower(), "en") 

90 

91 # Convert safe search to Brave's format 

92 brave_safe_search = "moderate" if safe_search else "off" 

93 

94 # Initialize Brave Search 

95 search_kwargs = { 

96 "count": min(20, max_results), 

97 "country": region.upper(), 

98 "search_lang": language_code, 

99 "safesearch": brave_safe_search, 

100 } 

101 # Brave's freshness param prefixes time_period with "p" (pd/pw/pm/py). 

102 # Only forward recognized codes; "all", any unrecognized value 

103 # (typo, None, "") and non-string types omit freshness entirely so 

104 # the search is unfiltered rather than sending an invalid value 

105 # like "pall" (the isinstance guard also keeps unhashable values 

106 # from raising on the frozenset lookup). 

107 if isinstance(time_period, str) and time_period in VALID_TIME_PERIODS: 

108 search_kwargs["freshness"] = f"p{time_period}" 

109 self.engine = BraveSearch.from_api_key( 

110 api_key=brave_api_key, 

111 search_kwargs=search_kwargs, 

112 ) 

113 

114 # User agent is not needed for Brave Search API 

115 

116 # Full-content retrieval is the factory wrapper's job — the 

117 # wrapper fetches pages via ``batch_fetch_and_extract`` and 

118 # populates ``full_content`` on each result; the inner engine 

119 # only emits snippets from ``_get_previews``. 

120 

121 def _get_previews(self, query: str) -> List[Dict[str, Any]]: 

122 """ 

123 Get preview information from Brave Search. 

124 

125 Args: 

126 query: The search query 

127 

128 Returns: 

129 List of preview dictionaries 

130 """ 

131 logger.info("Getting search results from Brave Search") 

132 

133 try: 

134 # Get search results from Brave Search 

135 raw_results = self.engine.run(query[:400]) 

136 

137 # Parse results if they're in string format 

138 if isinstance(raw_results, str): 

139 try: 

140 import json 

141 

142 raw_results = json.loads(raw_results) 

143 except json.JSONDecodeError as e: 

144 safe_msg = self._scrub_error(e) 

145 logger.warning( 

146 f"Unable to parse BraveSearch response as JSON: {safe_msg}" 

147 ) 

148 return [] 

149 

150 # Format results as previews 

151 previews = [] 

152 for i, result in enumerate(raw_results): 

153 preview = { 

154 "id": i, # Use index as ID 

155 "title": result.get("title", ""), 

156 "link": result.get("link", ""), 

157 "snippet": result.get("snippet", ""), 

158 "displayed_link": result.get("link", ""), 

159 "position": i, 

160 } 

161 

162 # Store full Brave result for later 

163 preview["_full_result"] = result 

164 

165 previews.append(preview) 

166 

167 # Store the previews for potential full content retrieval 

168 self._search_results = previews 

169 

170 return previews 

171 

172 except RateLimitError: 

173 raise 

174 except Exception as e: 

175 safe_msg = self._scrub_error(e) 

176 logger.warning(f"Error getting Brave Search results: {safe_msg}") 

177 self._raise_if_rate_limit(e) 

178 return []