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

"""Tavily web-search backend.

Implements the ``TavilyBackend`` dataclass that wraps the ``tavily-python`` SDK
to deliver web-search results via the Tavily API. Requires ``pip install
ralph-workflow[web-search]`` (or ``pip install tavily-python``) at runtime;
importing without the SDK is safe, but calling ``search`` raises
``WebSearchError``.

API key resolution:

- Pass ``api_key`` directly, or
- set ``api_key_env`` to an environment variable name that holds the key
  (resolved via ``ralph.mcp.websearch.secrets.resolve_secret``).

Typical usage (from ``ralph.config.mcp_models`` backend selection)::

    backend = TavilyBackend(api_key_env="TAVILY_API_KEY")
    results = backend.search("FastAPI dependency injection", limit=5)
"""

from __future__ import annotations

from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from importlib import import_module
from typing import TYPE_CHECKING, cast

from ..secrets import resolve_secret
from .base import SearchResult, WebSearchError

if TYPE_CHECKING:
    from typing import Protocol

    class _TavilyClient(Protocol):
        def search(self, query: str, *, max_results: int) -> Mapping[str, object]: ...

    class _TavilyClientType(Protocol):
        def __call__(self, *, api_key: str) -> _TavilyClient: ...


[docs] @dataclass(frozen=True) class TavilyBackend: """Backend powered by the Tavily Python SDK.""" api_key: str | None = None api_key_env: str | None = None def search(self, query: str, *, limit: int = 10) -> list[SearchResult]: client_type = _load_tavily_client_type() resolved_key = resolve_secret(self.api_key, self.api_key_env) try: payload = client_type(api_key=resolved_key).search(query, max_results=limit) except Exception as exc: raise WebSearchError("tavily search failed") from exc return _normalize_results(payload)
def _load_tavily_client_type() -> _TavilyClientType: try: module = import_module("tavily") except ImportError as exc: raise WebSearchError( "backend 'tavily' requires 'pip install ralph-workflow[web-search]'" ) from exc client_type = cast("object | None", getattr(module, "TavilyClient", None)) if client_type is None: raise WebSearchError("tavily backend is unavailable") return cast("_TavilyClientType", client_type) def _normalize_results(payload: Mapping[str, object]) -> list[SearchResult]: raw_results = payload.get("results") if not isinstance(raw_results, Iterable): return [] results: list[SearchResult] = [] for item in raw_results: if not isinstance(item, Mapping): continue title = _string_value(item, "title") url = _string_value(item, "url") snippet = _string_value(item, "content") 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__ = ["TavilyBackend"]