diff --git a/backend/apps/agents/tools/search/engine_answer.py b/backend/apps/agents/tools/search/engine_answer.py new file mode 100644 index 00000000..54afb766 --- /dev/null +++ b/backend/apps/agents/tools/search/engine_answer.py @@ -0,0 +1,16 @@ +"""What a keyless search engine actually said: results, an honest nothing, or a refusal. + +"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's breaker, an +empty result set is the honest answer to a nonsense query.""" + +from pydantic import BaseModel, ConfigDict + + +class EngineAnswer(BaseModel): + model_config = ConfigDict(validate_assignment=True) + + results: str = "" + # Challenge, HTTP error, or markup we no longer recognise: the engine did not answer the question. + refused: bool = False diff --git a/backend/apps/agents/tools/search/search_bing.py b/backend/apps/agents/tools/search/search_bing.py new file mode 100644 index 00000000..c0999060 --- /dev/null +++ b/backend/apps/agents/tools/search/search_bing.py @@ -0,0 +1,88 @@ +"""Bing search: the third keyless engine, straight from Microsoft's own index. + +DuckDuckGo largely serves Bing's index through DuckDuckGo's frontend, so when +DDG's anti-bot wall is up the index itself is usually still reachable here. +Probed live 2026-08-10 through the Chrome-impersonating client: 200 with +server-rendered results on 3/3 queries at 0.2-0.3s, the fastest of every +engine probed. + +Organic results are `
]*>(.*?)
', flags=re.DOTALL) +P_REDIRECT_RE = re.compile(r"[?&]u=a1([A-Za-z0-9_\-]+)") +P_NO_RESULTS_MARKER = "There are no results for" + + +@typechecked +def p_real_url(raw: str) -> str: + raw = html.unescape(raw) + m = P_REDIRECT_RE.search(raw) + if not m: + return raw + token = m.group(1) + try: + return base64.urlsafe_b64decode(token + "=" * (-len(token) % 4)).decode("utf-8", "replace") + except Exception: + return raw + + +@typechecked +def parse_bing_results(body: str, num_results: int) -> str: + """Format Bing's b_algo rows with their redirect hrefs decoded to real URLs.""" + entries: List[str] = [] + for block in P_BLOCK_RE.findall(body): + if len(entries) >= num_results: + break + h2 = P_H2_RE.search(block) + if not h2: + continue + href = P_HREF_RE.search(h2.group(1)) + title = strip_tags(h2.group(1)) + if not href or not title: + continue + entry = f"[{len(entries) + 1}] {title}\n {p_real_url(href.group(1))}" + snippet_match = P_SNIPPET_RE.search(block) + if snippet_match: + snippet = strip_tags(snippet_match.group(1)) + if snippet: + entry += f"\n {snippet}" + entries.append(entry) + return "\n\n".join(entries) + + +@typechecked +async def search_bing(query: str, num_results: int) -> EngineAnswer: + """Bing's answer: results, an honest nothing, or a refusal.""" + reply = await browser_request(P_SEARCH_URL, params={"q": query}, timeout=P_TIMEOUT) + if reply.status != 200: + return EngineAnswer(refused=True) + results = parse_bing_results(reply.text, num_results) + if results: + return EngineAnswer(results=results) + if P_NO_RESULTS_MARKER in reply.text: + return EngineAnswer() + return EngineAnswer(refused=True) diff --git a/backend/apps/agents/tools/search/search_brave.py b/backend/apps/agents/tools/search/search_brave.py new file mode 100644 index 00000000..1b4a47be --- /dev/null +++ b/backend/apps/agents/tools/search/search_brave.py @@ -0,0 +1,78 @@ +"""Brave search: a keyless engine with its OWN index behind DuckDuckGo. + +Brave runs its own crawler, so it fails independently of the Bing-fed engines +(DuckDuckGo, Bing itself) and of Google (Startpage). Probed live 2026-08-10 +through the Chrome-impersonating client: 200 with server-rendered results on +3/3 queries at 0.7-0.9s, while Mojeek, Ecosia and Yep 403'd the same client +on the same machine and Qwant served a JS shell. + +The SERP is server-rendered Svelte: each organic result is a +`]*class="[^"]*\bdescription\b[^"]*"[^>]*>(.*?)
', flags=re.DOTALL, ) -# Startpage inlines a