From a8de20482f0d4e22713fdd5c9d0ab48116564762 Mon Sep 17 00:00:00 2001 From: abccodes Date: Tue, 30 Jun 2026 14:14:33 -0700 Subject: [PATCH] [aidan] fix/web-search: nudge model to browser fallback when search backends fail --- backend/apps/web/web.py | 14 ++++++++++++-- backend/tests/test_web_search_cascade.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/backend/apps/web/web.py b/backend/apps/web/web.py index bf632be58..9cf7a5f99 100644 --- a/backend/apps/web/web.py +++ b/backend/apps/web/web.py @@ -74,6 +74,15 @@ def p_join_text(parts: list[dict[str, Any]]) -> str: # Local httpx + trafilatura fetch of a real page; the fast path for /fetch (normal pages return in <2s). Set just above WebFetchTool's own 30s httpx ceiling so a valid-but-slow page still completes locally instead of being clipped down to a grounded summary; only a truly hung server gets cut. P_LOCAL_FETCH_TIMEOUT = 32.0 +# When every search backend fails, point the model at the in-product browser (always-on CreateBrowserAgent tool) instead of telling it to "wait and retry", which it can't do and just relays as a dead end. The real Chromium renders pages and isn't subject to the DDG scrape throttle. +def p_browser_fallback_nudge(query: str) -> str: + return ( + "Don't stop here: fall back to the in-product browser, which renders real pages and " + "isn't subject to this rate limit. Call CreateBrowserAgent with a task like: " + f'"Search the web for: {query}. Report the top results with their titles and URLs, ' + 'plus a direct answer if you find one."' + ) + async def p_gemini_grounded_call(api_key: str, prompt: str, *, use_url_context: bool) -> dict: """Call Gemini with googleSearch (+ optionally urlContext) grounding. @@ -470,11 +479,12 @@ async def try_ddg(): else: tail = ( "DuckDuckGo is rate-limiting this network and every configured provider " - "errored (see details below). Wait a moment and retry." + "errored (see details below)." ) + nudge = p_browser_fallback_nudge(body.query) return { "query": body.query, - "results": f"No results for: {body.query}\n\n{tail}", + "results": f"No results for: {body.query}\n\n{tail}\n\n{nudge}", "backend": "none", "cascade_errors": errors, } diff --git a/backend/tests/test_web_search_cascade.py b/backend/tests/test_web_search_cascade.py index c50994711..23f543069 100644 --- a/backend/tests/test_web_search_cascade.py +++ b/backend/tests/test_web_search_cascade.py @@ -128,6 +128,22 @@ async def test_everything_fails_is_honest_not_empty(monkeypatch): assert "Settings" in res["results"] or "API key" in res["results"] +@pytest.mark.asyncio +async def test_everything_fails_nudges_browser_not_retry(monkeypatch): + # All-fail must hand the model the browser as an escape hatch, not a dead-end "wait and retry". + p_ddg_throttled(monkeypatch) + monkeypatch.setattr(W, "p_resolve_openai_api_key", lambda: "okey") # configured but errors + + async def p_openai_boom(*a, **k): + raise RuntimeError("openai down") + monkeypatch.setattr(W, "p_openai_websearch", p_openai_boom) + + res = await search(SearchBody(query="sony zv-e10 price")) + assert res["backend"] == "none" + assert "CreateBrowserAgent" in res["results"] + assert "retry" not in res["results"].lower() + + # -------------------------------------------------------------------------- /fetch mirrors /search: local httpx + trafilatura is the fast path, grounded fetchers are the fallback for JS/paywalled pages, every attempt is bounded. -------------------------------------------------------------------------- from backend.apps.web.web import fetch, FetchBody