Source code for ralph.mcp.websearch.backends.ddgs

"""DuckDuckGo Search backend implementation."""

from __future__ import annotations

from typing import TYPE_CHECKING, cast

from .base import SearchResult, WebSearchError

if TYPE_CHECKING:
    from collections.abc import Callable, Iterable, Mapping
    from typing import Protocol

    class _DdgsTextClient(Protocol):
        """Minimal structural protocol matching the DDGS text-search API."""

        def text(self, query: str, max_results: int) -> Iterable[Mapping[str, object]] | None: ...


_ddgs_module: object | None
try:  # pragma: no cover - exercised via monkeypatch in tests
    import ddgs as _loaded_ddgs_module
except ImportError:  # pragma: no cover - depends on optional runtime dependency
    _ddgs_module = None
else:
    _ddgs_module = _loaded_ddgs_module

DDGS = cast("Callable[[], _DdgsTextClient] | None", getattr(_ddgs_module, "DDGS", None))


[docs] class DdgsBackend: """In-process default backend backed by the ``ddgs`` package.""" def search(self, query: str, *, limit: int = 10) -> list[SearchResult]: client = self._create_client() try: raw_results = client.text(query, max_results=limit) except Exception as exc: raise WebSearchError("ddgs search failed") from exc return self._normalize_results(raw_results) def _create_client(self) -> _DdgsTextClient: if DDGS is None: raise WebSearchError("ddgs backend is unavailable") return DDGS() @staticmethod def _normalize_results( raw_results: Iterable[Mapping[str, object]] | None, ) -> list[SearchResult]: if raw_results is None: return [] results: list[SearchResult] = [] for item in raw_results: title = _string_value(item, "title") url = _string_value(item, "url", "href") snippet = _string_value(item, "snippet", "body", "description") if not title or not url: continue results.append(SearchResult(title=title, url=url, snippet=snippet)) return results
def _string_value(payload: Mapping[str, object], *keys: str) -> str: for key in keys: value = payload.get(key) if isinstance(value, str) and value: return value return "" __all__ = ["DdgsBackend"]