From 7805c10a8a2e2059388d69c5f54124cc2f6dda89 Mon Sep 17 00:00:00 2001 From: Tavily PR Agent Date: Mon, 11 May 2026 13:07:51 +0000 Subject: [PATCH] feat: add Tavily as configurable web search provider --- forgegod/config.py | 3 ++- forgegod/researcher.py | 2 ++ forgegod/tools/web.py | 31 +++++++++++++++++++++++++++++-- pyproject.toml | 3 ++- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/forgegod/config.py b/forgegod/config.py index a230a1c..ebdfeb6 100644 --- a/forgegod/config.py +++ b/forgegod/config.py @@ -262,10 +262,11 @@ class ReconConfig(BaseModel): enabled: bool = False max_searches: int = 15 max_fetch_chars: int = 3000 # per-page content limit - search_provider: str = "searxng" # searxng, brave, exa + search_provider: str = "searxng" # searxng, duckduckgo, brave, exa, tavily searxng_url: str = "http://localhost:8888" brave_api_key: str = "" exa_api_key: str = "" + tavily_api_key: str = "" debate_rounds: int = 3 min_approval_score: float = 7.0 # 0-10, plan must score above this cache_results: bool = True diff --git a/forgegod/researcher.py b/forgegod/researcher.py index d29ca3f..eb3ea1e 100644 --- a/forgegod/researcher.py +++ b/forgegod/researcher.py @@ -200,6 +200,7 @@ async def _execute_searches( searxng_url = self.recon.searxng_url brave_key = self.recon.brave_api_key exa_key = self.recon.exa_api_key + tavily_key = self.recon.tavily_api_key async def _search_one(q: SearchQuery) -> list[SearchResult]: raw = await web_search( @@ -209,6 +210,7 @@ async def _search_one(q: SearchQuery) -> list[SearchResult]: searxng_url=searxng_url, brave_api_key=brave_key, exa_api_key=exa_key, + tavily_api_key=tavily_key, ) try: items = json.loads(raw) diff --git a/forgegod/tools/web.py b/forgegod/tools/web.py index 600f47b..ce9bfae 100644 --- a/forgegod/tools/web.py +++ b/forgegod/tools/web.py @@ -144,6 +144,30 @@ async def _search_exa( return [] +async def _search_tavily( + query: str, api_key: str, max_results: int = 5 +) -> list[dict]: + """Search via Tavily API (optimised for LLM pipelines).""" + if not api_key: + return [] + try: + from tavily import AsyncTavilyClient + + client = AsyncTavilyClient(api_key=api_key) + data = await client.search(query=query, max_results=max_results) + results = [] + for r in data.get("results", [])[:max_results]: + results.append({ + "url": r.get("url", ""), + "title": r.get("title", ""), + "snippet": (r.get("content") or "")[:500], + }) + return results + except Exception as e: + logger.warning("Tavily search failed: %s", e) + return [] + + async def _search_duckduckgo( query: str, max_results: int = 5 ) -> list[dict]: @@ -174,15 +198,16 @@ async def web_search( query: str, provider: str = "searxng", max_results: int = 5, searxng_url: str = "http://localhost:8888", brave_api_key: str = "", exa_api_key: str = "", + tavily_api_key: str = "", ) -> str: """Search the web. Returns JSON array of {url, title, snippet}. - Tries providers in order: requested → SearXNG → Brave → Exa. + Tries providers in order: requested → SearXNG → DuckDuckGo → Brave → Exa → Tavily. """ results: list[dict] = [] # Try requested provider first, then fallback chain - providers = [provider, "searxng", "duckduckgo", "brave", "exa"] + providers = [provider, "searxng", "duckduckgo", "brave", "exa", "tavily"] seen = set() for p in providers: @@ -198,6 +223,8 @@ async def web_search( results = await _search_brave(query, brave_api_key, max_results) elif p == "exa": results = await _search_exa(query, exa_api_key, max_results) + elif p == "tavily": + results = await _search_tavily(query, tavily_api_key, max_results) if not results: return json.dumps({"error": "All search providers failed", "query": query}) diff --git a/pyproject.toml b/pyproject.toml index ef59436..476e0d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,8 @@ anthropic = ["anthropic>=0.52"] dotenv = ["python-dotenv>=1.0"] taste = ["taste-agent>=0.1.0"] effort = ["effort-agent>=0.1.0"] -all = ["anthropic>=0.52", "python-dotenv>=1.0", "taste-agent>=0.1.0", "effort-agent>=0.1.0"] +tavily = ["tavily-python>=0.5"] +all = ["anthropic>=0.52", "python-dotenv>=1.0", "taste-agent>=0.1.0", "effort-agent>=0.1.0", "tavily-python>=0.5"] dev = ["pytest>=8", "pytest-asyncio>=0.25", "ruff>=0.11"] [project.scripts]