From 108ef03451b014ebc95158b16d8bbca193bc235e Mon Sep 17 00:00:00 2001 From: ciregenz Date: Thu, 30 Jul 2026 16:35:51 -0700 Subject: [PATCH] [eric] web: let Startpage say 'nothing matched' and 'I refuse' differently so a closed engine stops costing its budget --- .../agents/tools/search/search_startpage.py | 34 ++++++++++++++----- backend/apps/web/web.py | 9 +++-- backend/tests/test_wayback_snapshot.py | 14 ++++++-- backend/tests/test_web_search_cascade.py | 27 +++++++++++++++ backend/tests/test_web_search_startpage.py | 32 ++++++++++++++--- backend/tests/web_cascade_fixtures.py | 11 ++++-- 6 files changed, 107 insertions(+), 20 deletions(-) diff --git a/backend/apps/agents/tools/search/search_startpage.py b/backend/apps/agents/tools/search/search_startpage.py index 692cb578..30dd0465 100644 --- a/backend/apps/agents/tools/search/search_startpage.py +++ b/backend/apps/agents/tools/search/search_startpage.py @@ -11,13 +11,18 @@ returns the real result page. Parsing is anchored on `result-link` / `gl-title-link` and the `description` paragraph, never on the emotion CSS hashes in the same class attributes, which change build to build. -Returns None when Startpage served a challenge instead of results, so the -caller can tell "closed" apart from "genuinely no hits".""" +Answers a `StartpageAnswer` rather than a string because "closed" and +"genuinely no hits" look identical on the wire (both are a 200 with no result +anchors) and the caller has to act on them differently: a refusal is a failed +tier that should count against the engine, an empty result set is the honest +answer to a nonsense query. Startpage names the second case itself, in an +"Uh-oh, there are no results for this search" page.""" import html import re -from typing import List, Optional +from typing import List +from pydantic import BaseModel, ConfigDict from typeguard import typechecked from backend.apps.agents.tools.browser_http import browser_request @@ -36,6 +41,15 @@ P_DESC_RE = re.compile( # Startpage inlines a