Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/agentic_ai/layers/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()]
33 changes: 33 additions & 0 deletions src/agentic_ai/tools/webtools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import os

import httpx
import trafilatura
Expand All @@ -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()
Comment on lines +51 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of this factory function has two robustness issues:

  1. It doesn't validate that TAVILY_API_KEY is set when SEARCH_PROVIDER is 'tavily', leading to a runtime error instead of a configuration error.
  2. It silently falls back to DuckDuckGo for any unsupported SEARCH_PROVIDER value, which can hide misconfigurations.

It would be better to fail fast with clear error messages for invalid configurations.

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":
        if not os.environ.get("TAVILY_API_KEY"):
            raise ValueError(
                "SEARCH_PROVIDER is 'tavily', but TAVILY_API_KEY environment variable is not set."
            )
        return TavilyWebSearch()
    elif provider == "duckduckgo":
        return WebSearch()
    else:
        raise ValueError(
            f"Unsupported SEARCH_PROVIDER: '{provider}'. Supported values are 'duckduckgo' and 'tavily'."
        )



class WebFetch(BaseTool):
name: str = "web_fetch"
description: str = "Fetch a URL and extract main readable text. Input: URL. Output: text."
Expand Down