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

52 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-06 15:42 +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: Whether to include full webpage content in results 

59 max_filtered_results: Maximum number of results to keep after filtering 

60 settings_snapshot: Settings snapshot for thread context 

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

62 """ 

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

64 super().__init__( 

65 llm=llm, 

66 max_filtered_results=max_filtered_results, 

67 max_results=max_results, 

68 include_full_content=include_full_content, 

69 settings_snapshot=settings_snapshot, 

70 ) 

71 

72 # Set up language code mapping 

73 if language_code_mapping is None: 

74 from ...utilities.search_utilities import LANGUAGE_CODE_MAP 

75 

76 language_code_mapping = LANGUAGE_CODE_MAP 

77 

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

79 brave_api_key = self._resolve_api_key( 

80 api_key, 

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

82 engine_name="Brave Search", 

83 settings_snapshot=settings_snapshot, 

84 ) 

85 self._brave_api_key = brave_api_key 

86 

87 # Get language code 

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

89 

90 # Convert safe search to Brave's format 

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

92 

93 # Initialize Brave Search 

94 search_kwargs = { 

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

96 "country": region.upper(), 

97 "search_lang": language_code, 

98 "safesearch": brave_safe_search, 

99 } 

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

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

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

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

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

105 # from raising on the frozenset lookup). 

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

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

108 self.engine = BraveSearch.from_api_key( 

109 api_key=brave_api_key, 

110 search_kwargs=search_kwargs, 

111 ) 

112 

113 # User agent is not needed for Brave Search API 

114 

115 # If full content is requested, initialize FullSearchResults 

116 self._init_full_search( 

117 web_search=self.engine, 

118 language=search_language, 

119 max_results=max_results, 

120 region=region, 

121 time_period=time_period, 

122 safe_search=brave_safe_search, 

123 ) 

124 

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

126 """ 

127 Get preview information from Brave Search. 

128 

129 Args: 

130 query: The search query 

131 

132 Returns: 

133 List of preview dictionaries 

134 """ 

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

136 

137 try: 

138 # Get search results from Brave Search 

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

140 

141 # Parse results if they're in string format 

142 if isinstance(raw_results, str): 

143 try: 

144 import json 

145 

146 raw_results = json.loads(raw_results) 

147 except json.JSONDecodeError as e: 

148 safe_msg = self._scrub_error(e) 

149 logger.warning( 

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

151 ) 

152 return [] 

153 

154 # Format results as previews 

155 previews = [] 

156 for i, result in enumerate(raw_results): 

157 preview = { 

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

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

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

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

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

163 "position": i, 

164 } 

165 

166 # Store full Brave result for later 

167 preview["_full_result"] = result 

168 

169 previews.append(preview) 

170 

171 # Store the previews for potential full content retrieval 

172 self._search_results = previews 

173 

174 return previews 

175 

176 except RateLimitError: 

177 raise 

178 except Exception as e: 

179 safe_msg = self._scrub_error(e) 

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

181 self._raise_if_rate_limit(e) 

182 return []