diff --git a/.env.example b/.env.example index cd6c51f..67cb62c 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,10 @@ ANTHROPIC_MODEL_CHAT=claude-3-5-sonnet-latest # --- Google Gemini (optional, used by MCP server) --- GOOGLE_API_KEY= +# --- Search Provider --- +SEARCH_PROVIDER=duckduckgo # duckduckgo | tavily +TAVILY_API_KEY= # Required when SEARCH_PROVIDER=tavily + # --- Storage --- CHROMA_DIR=.chroma SQLITE_PATH=.sqlite/agent.db diff --git a/requirements.txt b/requirements.txt index d10d119..2c0f0c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ typing-extensions>=4.12.2 # Discovery duckduckgo-search>=6.2.10 +tavily-python>=0.3.0 httpx>=0.27.0 trafilatura>=1.10.0 beautifulsoup4>=4.12.3 diff --git a/src/agentic_ai/layers/tools.py b/src/agentic_ai/layers/tools.py index 4818ed8..53d3d2a 100644 --- a/src/agentic_ai/layers/tools.py +++ b/src/agentic_ai/layers/tools.py @@ -6,8 +6,8 @@ from ..tools.knowledge import KbAdd, KbSearch from ..tools.ops import Calculator, Emailer, FileWrite -from ..tools.webtools import WebFetch, WebSearch +from ..tools.webtools import WebFetch, get_web_search_tool def registry() -> List[BaseTool]: - return [WebSearch(), WebFetch(), KbSearch(), KbAdd(), Calculator(), FileWrite(), Emailer()] + return [get_web_search_tool(), WebFetch(), KbSearch(), KbAdd(), Calculator(), FileWrite(), Emailer()] diff --git a/src/agentic_ai/tools/webtools.py b/src/agentic_ai/tools/webtools.py index 5561259..ca847e0 100644 --- a/src/agentic_ai/tools/webtools.py +++ b/src/agentic_ai/tools/webtools.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import os import httpx import trafilatura @@ -23,6 +24,38 @@ def _run(self, query: str) -> str: return json.dumps(list(results), ensure_ascii=False) +class TavilyWebSearch(BaseTool): + name: str = "web_search" + description: str = ( + "Search the web. Input: a natural language query. " + "Output: JSON list of {title, url, snippet}." + ) + + @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=4)) + def _run(self, query: str) -> str: + from tavily import TavilyClient + + client = TavilyClient() + response = client.search(query=query, max_results=8) + results = [ + { + "title": r.get("title", ""), + "url": r.get("url", ""), + "snippet": r.get("content", ""), + } + for r in response.get("results", []) + ] + return json.dumps(results, ensure_ascii=False) + + +def get_web_search_tool() -> BaseTool: + """Return the active web search tool based on SEARCH_PROVIDER env var.""" + provider = os.environ.get("SEARCH_PROVIDER", "duckduckgo").lower() + if provider == "tavily": + return TavilyWebSearch() + return WebSearch() + + class WebFetch(BaseTool): name: str = "web_fetch" description: str = "Fetch a URL and extract main readable text. Input: URL. Output: text."