@@ -101,6 +101,18 @@ async def _fetch_and_extract_async(url: str) -> str:
101101 raise DocumentFetchError (f"Failed to extract content from { url } : { e } " ) from e
102102
103103
104+ def _get_snippets_from_result (result : dict ) -> List [str ]:
105+ """Return merged snippets/extra_snippets as a list, guarding against None."""
106+ snippets = result .get ("snippets" ) or []
107+ extra_snippets = result .get ("extra_snippets" ) or []
108+ # Both are expected to be lists of strings; fall back to one or the other if needed.
109+ if snippets and not extra_snippets :
110+ return snippets
111+ if extra_snippets and not snippets :
112+ return extra_snippets
113+ return snippets or extra_snippets
114+
115+
104116async def _extract_and_summarize_snippets_async (query : str , url : str ) -> List [str ]:
105117 """Fetch, extract and summarize text content from the URL.
106118
@@ -143,22 +155,45 @@ async def _fetch_and_store_async(url: str, document_store, **kwargs) -> None:
143155
144156
145157async def _query_brave_api_async (query : str ) -> List [dict ]:
146- """Query the Brave Search API and return the raw results."""
147- url = "https://api.search.brave.com/res/v1/web/search"
158+ """Query the Brave Search API and return the raw results.
159+
160+ Uses either the LLM context endpoint (res/v1/llm/context) or the classic web search
161+ endpoint (res/v1/web/search) depending on the BRAVE_USE_LLM_CONTEXT setting.
162+ """
163+ if settings .BRAVE_USE_LLM_CONTEXT :
164+ logger .debug ("Using LLM context endpoint" )
165+ url = "https://api.search.brave.com/res/v1/llm/context"
166+ data = {
167+ "q" : query ,
168+ "country" : settings .BRAVE_SEARCH_COUNTRY ,
169+ "search_lang" : settings .BRAVE_SEARCH_LANG ,
170+ "count" : settings .BRAVE_MAX_RESULTS ,
171+ "safesearch" : settings .BRAVE_SEARCH_SAFE_SEARCH ,
172+ "spellcheck" : settings .BRAVE_SEARCH_SPELLCHECK ,
173+ "result_filter" : "web,faq,query" ,
174+ "extra_snippets" : settings .BRAVE_SEARCH_EXTRA_SNIPPETS ,
175+ "maximum_number_of_urls" : settings .BRAVE_MAX_RESULTS ,
176+ "maximum_number_of_tokens" : settings .BRAVE_MAX_TOKENS ,
177+ "maximum_number_of_snippets" : settings .BRAVE_MAX_SNIPPETS ,
178+ "maximum_number_of_snippets_per_url" : settings .BRAVE_MAX_SNIPPETS_PER_URL ,
179+ }
180+ else :
181+ logger .debug ("Using classic web search endpoint" )
182+ url = "https://api.search.brave.com/res/v1/web/search"
183+ data = {
184+ "q" : query ,
185+ "country" : settings .BRAVE_SEARCH_COUNTRY ,
186+ "search_lang" : settings .BRAVE_SEARCH_LANG ,
187+ "count" : settings .BRAVE_MAX_RESULTS ,
188+ "safesearch" : settings .BRAVE_SEARCH_SAFE_SEARCH ,
189+ "spellcheck" : settings .BRAVE_SEARCH_SPELLCHECK ,
190+ "result_filter" : "web,faq,query" ,
191+ "extra_snippets" : settings .BRAVE_SEARCH_EXTRA_SNIPPETS ,
192+ }
148193 headers = {
149194 "Accept" : "application/json" ,
150195 "X-Subscription-Token" : settings .BRAVE_API_KEY ,
151196 }
152- data = {
153- "q" : query ,
154- "country" : settings .BRAVE_SEARCH_COUNTRY ,
155- "search_lang" : settings .BRAVE_SEARCH_LANG ,
156- "count" : settings .BRAVE_MAX_RESULTS ,
157- "safesearch" : settings .BRAVE_SEARCH_SAFE_SEARCH ,
158- "spellcheck" : settings .BRAVE_SEARCH_SPELLCHECK ,
159- "result_filter" : "web,faq,query" ,
160- "extra_snippets" : settings .BRAVE_SEARCH_EXTRA_SNIPPETS ,
161- }
162197 params = {k : v for k , v in data .items () if v is not None }
163198
164199 try :
@@ -167,6 +202,29 @@ async def _query_brave_api_async(query: str) -> List[dict]:
167202 response .raise_for_status ()
168203 json_response = response .json ()
169204
205+ # LLM context API: results are under `grounding.generic`
206+ # See: https://api-dashboard.search.brave.com/documentation/services/llm-context
207+ if "grounding" in json_response :
208+ generic_results = json_response .get ("grounding" , {}).get ("generic" , []) or []
209+ normalized_results : List [dict ] = []
210+ for item in generic_results :
211+ item_url = item .get ("url" )
212+ if not item_url :
213+ continue
214+
215+ normalized_results .append (
216+ {
217+ "url" : item_url ,
218+ # Fallback to URL if no title is provided
219+ "title" : item .get ("title" ) or item_url ,
220+ # `snippets` is already a list
221+ "snippets" : item .get ("snippets" ) or [],
222+ }
223+ )
224+
225+ return normalized_results
226+
227+ # Fallback for classic web search JSON shape, if we ever switch back
170228 # https://api-dashboard.search.brave.com/app/documentation/web-search/responses#Result
171229 return json_response .get ("web" , {}).get ("results" , [])
172230
@@ -217,14 +275,14 @@ def format_tool_return(raw_search_results: List[dict]) -> ToolReturn:
217275 str (idx ): {
218276 "url" : result ["url" ],
219277 "title" : result ["title" ],
220- "snippets" : result . get ( "extra_snippets" , [] ),
278+ "snippets" : _get_snippets_from_result ( result ),
221279 }
222280 for idx , result in enumerate (raw_search_results )
223- if result . get ( "extra_snippets" , [] )
281+ if _get_snippets_from_result ( result )
224282 },
225283 metadata = {
226284 "sources" : {
227- result ["url" ] for result in raw_search_results if result . get ( "extra_snippets" , [] )
285+ result ["url" ] for result in raw_search_results if _get_snippets_from_result ( result )
228286 }
229287 },
230288 )
@@ -239,14 +297,18 @@ async def web_search_brave(_ctx: RunContext, query: str) -> ToolReturn:
239297 _ctx (RunContext): The run context, used by the wrapper.
240298 query (str): The query to search for.
241299 """
300+ logger .debug ("Starting web search without RAG backend for query: %s" , query )
242301 try :
243302 raw_search_results = await _query_brave_api_async (query )
244303
245304 await sync_to_async (reset_caches )() # Clear trafilatura caches to avoid memory bloat/leaks
246305
247- # Parallelize fetch/extract for results that don't include extra_snippets
306+ # Parallelize fetch/extract only for results that don't already include any snippets
307+ # (neither Brave `snippets` nor `extra_snippets`).
248308 to_process = [
249- (idx , r ) for idx , r in enumerate (raw_search_results ) if not r .get ("extra_snippets" )
309+ (idx , r )
310+ for idx , r in enumerate (raw_search_results )
311+ if not r .get ("extra_snippets" ) and not r .get ("snippets" )
250312 ]
251313
252314 if to_process :
@@ -292,7 +354,7 @@ async def web_search_brave_with_document_backend(ctx: RunContext, query: str) ->
292354 ctx (RunContext): The run context containing the conversation.
293355 query (str): The query to search for.
294356 """
295- logger .info ("Starting web search with RAG backend for query: %s" , query )
357+ logger .debug ("Starting web search with RAG backend for query: %s" , query )
296358 try :
297359 raw_search_results = await _query_brave_api_async (query )
298360
@@ -328,7 +390,7 @@ async def web_search_brave_with_document_backend(ctx: RunContext, query: str) ->
328390 session = ctx .deps .session ,
329391 user_sub = ctx .deps .user .sub ,
330392 )
331- logger .info ("RAG search returned: %s" , rag_results )
393+ logger .debug ("RAG search returned: %s" , rag_results )
332394
333395 ctx .usage += RunUsage (
334396 input_tokens = rag_results .usage .prompt_tokens ,
@@ -366,3 +428,21 @@ async def web_search_brave_with_document_backend(ctx: RunContext, query: str) ->
366428 f"An unexpected error occurred during web search with RAG: { type (e ).__name__ } . "
367429 "You must explain this to the user and not try to answer based on your knowledge."
368430 ) from e
431+
432+
433+ @last_model_retry_soft_fail
434+ async def web_search_brave_configurable (ctx : RunContext , query : str ) -> ToolReturn :
435+ """Route web search implementation based on Brave settings.
436+
437+ Priority:
438+ 1) BRAVE_USE_LLM_CONTEXT=True => always use non-RAG flow.
439+ 2) BRAVE_USE_RAG=True => use document-backend (RAG) flow.
440+ 3) Otherwise => use non-RAG flow.
441+ """
442+ if settings .BRAVE_USE_LLM_CONTEXT :
443+ return await web_search_brave (ctx , query )
444+
445+ if settings .BRAVE_USE_RAG :
446+ return await web_search_brave_with_document_backend (ctx , query )
447+
448+ return await web_search_brave (ctx , query )
0 commit comments