From 7c2dd13b3ed606a53e020b2c6b8a1693506cafe7 Mon Sep 17 00:00:00 2001 From: Tavily PR Agent Date: Fri, 10 Apr 2026 14:47:26 +0000 Subject: [PATCH] feat: add Tavily as configurable search provider option --- deepworm/__main__.py | 2 +- deepworm/config.py | 4 ++-- deepworm/search.py | 38 ++++++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/deepworm/__main__.py b/deepworm/__main__.py index 19ca5d1..f937239 100644 --- a/deepworm/__main__.py +++ b/deepworm/__main__.py @@ -137,7 +137,7 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument( "--search-provider", type=str, - choices=["duckduckgo", "brave", "searxng"], + choices=["duckduckgo", "brave", "searxng", "tavily"], default=None, help="Search engine provider (default: duckduckgo)", ) diff --git a/deepworm/config.py b/deepworm/config.py index f0ccb90..62dd627 100644 --- a/deepworm/config.py +++ b/deepworm/config.py @@ -44,7 +44,7 @@ class Config: # Search settings search_region: str = "wt-wt" search_max_results: int = 8 - search_provider: str = "duckduckgo" # duckduckgo, brave, searxng + search_provider: str = "duckduckgo" # duckduckgo, brave, searxng, tavily # Rate limiting max_requests_per_minute: int = 60 # LLM API rate limit @@ -124,7 +124,7 @@ def validate(self) -> None: """ VALID_PROVIDERS = {"openai", "anthropic", "google", "openrouter", "ollama"} VALID_FORMATS = {"markdown", "html", "text", "json", "pdf"} - VALID_SEARCH_PROVIDERS = {"duckduckgo", "brave", "searxng"} + VALID_SEARCH_PROVIDERS = {"duckduckgo", "brave", "searxng", "tavily"} if self.provider not in VALID_PROVIDERS: raise ValueError( diff --git a/deepworm/search.py b/deepworm/search.py index a1ccbe5..12eaf7c 100644 --- a/deepworm/search.py +++ b/deepworm/search.py @@ -1,6 +1,6 @@ """Web search with multiple providers. -Supports DuckDuckGo (default), Brave Search API, and SearXNG. +Supports DuckDuckGo (default), Brave Search API, SearXNG, and Tavily. """ from __future__ import annotations @@ -41,7 +41,7 @@ def search_web( ) -> list[SearchResult]: """Search the web using the specified provider. - Providers: duckduckgo (default), brave, searxng + Providers: duckduckgo (default), brave, searxng, tavily """ # Check cache first if cache is not None: @@ -62,6 +62,11 @@ def search_web( results = _search_searxng(query, max_results) except Exception as e: logger.debug("SearXNG search failed: %s", e) + elif provider == "tavily": + try: + results = _search_tavily(query, max_results) + except Exception as e: + logger.debug("Tavily search failed: %s", e) # Default/fallback: DuckDuckGo if not results: @@ -287,3 +292,32 @@ def _search_searxng(query: str, max_results: int) -> list[SearchResult]: snippet=item.get("content", ""), )) return results + + +def _search_tavily(query: str, max_results: int) -> list[SearchResult]: + """Search using Tavily API. Requires TAVILY_API_KEY env var.""" + import os + + api_key = os.getenv("TAVILY_API_KEY", "") + if not api_key: + raise ValueError("TAVILY_API_KEY not set") + + from tavily import TavilyClient + + client = TavilyClient(api_key=api_key) + response = client.search( + query=query, + max_results=max_results, + search_depth="advanced", + include_raw_content=True, + ) + + results = [] + for item in response.get("results", [])[:max_results]: + results.append(SearchResult( + title=item.get("title", ""), + url=item.get("url", ""), + snippet=item.get("content", ""), + body=item.get("raw_content", None), + )) + return results diff --git a/pyproject.toml b/pyproject.toml index fe89667..762595e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "rich>=13.0.0", "openai>=1.0.0", "ddgs>=6.0.0", + "tavily-python>=0.3.0", ] [project.optional-dependencies]