"""Startpage search: the second independent engine behind DuckDuckGo. DuckDuckGo is one operator, so its bot challenge is one point of failure for every keyless user. Startpage serves Google's index and answered 8/8 on the same machine and rounds where DuckDuckGo's shipped client shape answered 4/8, so it is a genuine second opinion rather than a retry. It must be a POST: a GET to /sp/search is answered with an Anubis proof-of-work interstitial (measured, ~10KB and zero results), while the POST 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. 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 from pydantic import BaseModel, ConfigDict from typeguard import typechecked from backend.apps.agents.tools.browser_http import browser_request P_SEARCH_URL = "https://www.startpage.com/sp/search" P_TIMEOUT = 12.0 P_ANCHOR_RE = re.compile( r"]*(?:result-link|gl-title-link)[^>]*)>(.*?)", flags=re.DOTALL, ) P_HREF_RE = re.compile(r'href="([^"]+)"') P_TITLE_RE = re.compile(r"
]*class="[^"]*\bdescription\b[^"]*"[^>]*>(.*?)
', flags=re.DOTALL, ) # Startpage inlines a