From b4dcc93c74b94c6b5f27cbd28217dd26b627c805 Mon Sep 17 00:00:00 2001 From: Tavily PR Agent Date: Mon, 30 Mar 2026 16:02:08 +0000 Subject: [PATCH] feat: add Tavily as configurable search provider alongside Google Search --- backend/.env.example | 3 +- backend/pyproject.toml | 1 + backend/src/agent/configuration.py | 7 ++++ backend/src/agent/graph.py | 52 ++++++++++++++++++++++++++++-- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index fde5f6baf..f9466b05e 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1 +1,2 @@ -# GEMINI_API_KEY= \ No newline at end of file +# GEMINI_API_KEY= +# TAVILY_API_KEY= # Required when search_provider=tavily \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 09eb59885..a44a33a48 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "langgraph-api", "fastapi", "google-genai", + "tavily-python", ] diff --git a/backend/src/agent/configuration.py b/backend/src/agent/configuration.py index e57122d23..eb4bd3ac8 100644 --- a/backend/src/agent/configuration.py +++ b/backend/src/agent/configuration.py @@ -29,6 +29,13 @@ class Configuration(BaseModel): }, ) + search_provider: str = Field( + default="google", + metadata={ + "description": "The search provider to use for web research. Options: 'google' (default) or 'tavily'." + }, + ) + number_of_initial_queries: int = Field( default=3, metadata={"description": "The number of initial search queries to generate."}, diff --git a/backend/src/agent/graph.py b/backend/src/agent/graph.py index 0f19c3f2f..8ea5dbdaf 100644 --- a/backend/src/agent/graph.py +++ b/backend/src/agent/graph.py @@ -8,6 +8,7 @@ from langgraph.graph import START, END from langchain_core.runnables import RunnableConfig from google.genai import Client +from tavily import TavilyClient from agent.state import ( OverallState, @@ -93,9 +94,10 @@ def continue_to_web_research(state: QueryGenerationState): def web_research(state: WebSearchState, config: RunnableConfig) -> OverallState: - """LangGraph node that performs web research using the native Google Search API tool. + """LangGraph node that performs web research using Google Search or Tavily. - Executes a web search using the native Google Search API tool in combination with Gemini 2.0 Flash. + Executes a web search using the configured search provider. When search_provider + is 'tavily', uses the Tavily API; otherwise defaults to Google Search grounding. Args: state: Current graph state containing the search query and research loop count @@ -104,8 +106,16 @@ def web_research(state: WebSearchState, config: RunnableConfig) -> OverallState: Returns: Dictionary with state update, including sources_gathered, research_loop_count, and web_research_results """ - # Configure configurable = Configuration.from_runnable_config(config) + + if configurable.search_provider == "tavily": + return _web_research_tavily(state, configurable) + else: + return _web_research_google(state, configurable) + + +def _web_research_google(state: WebSearchState, configurable: Configuration) -> OverallState: + """Perform web research using Google Search grounding via Gemini.""" formatted_prompt = web_searcher_instructions.format( current_date=get_current_date(), research_topic=state["search_query"], @@ -136,6 +146,42 @@ def web_research(state: WebSearchState, config: RunnableConfig) -> OverallState: } +def _web_research_tavily(state: WebSearchState, configurable: Configuration) -> OverallState: + """Perform web research using the Tavily search API.""" + tavily_client = TavilyClient() + response = tavily_client.search( + query=state["search_query"], + max_results=5, + search_depth="advanced", + ) + + # Build sources and research text matching the existing schema + sources_gathered = [] + research_parts = [] + for idx, result in enumerate(response.get("results", [])): + url = result.get("url", "") + title = result.get("title", "") + content = result.get("content", "") + short_url = f"https://tavily.com/id/{state['id']}-{idx}" + + # label: use the domain or title + label = title.split(" - ")[0] if title else url + sources_gathered.append({ + "label": label, + "short_url": short_url, + "value": url, + }) + research_parts.append(f"{content} [{label}]({short_url})") + + modified_text = "\n\n".join(research_parts) if research_parts else "" + + return { + "sources_gathered": sources_gathered, + "search_query": [state["search_query"]], + "web_research_result": [modified_text], + } + + def reflection(state: OverallState, config: RunnableConfig) -> ReflectionState: """LangGraph node that identifies knowledge gaps and generates potential follow-up queries.