Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions backend/apps/web/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
}
Expand Down
16 changes: 16 additions & 0 deletions backend/tests/test_web_search_cascade.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading