diff --git a/.env.example b/.env.example index 170dd4f..6a0f1e6 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,10 @@ SERPER_KEY="your_serper_key" # https://serper.dev/ JINA_KEY="your_jina_key" # https://jina.ai/ MINERU_TOKEN="your_mineru_token" # https://mineru.net/ +# Optional – search provider selection +SEARCH_PROVIDER="serper" # Search backend: "serper" (default) or "tavily". +TAVILY_API_KEY="your_tavily_api_key" # https://app.tavily.com/ – required when SEARCH_PROVIDER=tavily. + # Optional WORKSPACE_ROOT="./workspace" # Default local workspace root when --workspace-root is not provided. MAX_ROUNDS=500 # Maximum ReAct loop rounds before forced termination. diff --git a/agent_base/tools/tool_web.py b/agent_base/tools/tool_web.py index 8496e7d..59a359d 100644 --- a/agent_base/tools/tool_web.py +++ b/agent_base/tools/tool_web.py @@ -150,6 +150,42 @@ def contains_chinese_basic(text: str) -> bool: content = f"A Google search for '{query}' found {len(web_snippets)} results:\n\n## Web Results\n" + "\n\n".join(web_snippets) return content + def google_search_with_tavily(self, query: str): + try: + from tavily import TavilyClient + except ImportError: + return "[WebSearch] tavily-python is not installed." + + api_key = os.getenv("TAVILY_API_KEY", "").strip() + if not api_key: + return "[WebSearch] TAVILY_API_KEY is not set." + + try: + client = TavilyClient(api_key=api_key) + response = client.search(query=query, max_results=10, search_depth="basic") + except Exception as exc: + if search_debug_enabled(): + print(exc) + return f"[WebSearch] Tavily request failed for '{query}': {exc}" + + results = response.get("results", []) + if not results: + return f"No results found for '{query}'. Try with a more general query." + + web_snippets = [] + for idx, page in enumerate(results, start=1): + title = str(page.get("title", "Untitled result")) + link = str(page.get("url", "")) + snippet = f"\n{page['content']}" if page.get("content") else "" + redacted_version = f"{idx}. [{title}]({link}){snippet}" + web_snippets.append(redacted_version) + + if not web_snippets: + return f"No results found for '{query}'. Try with a more general query." + + content = f"A Google search for '{query}' found {len(web_snippets)} results:\n\n## Web Results\n" + "\n\n".join(web_snippets) + return content + def call(self, params: Union[str, dict], **kwargs) -> str: try: params = self.parse_json_args(params) @@ -160,6 +196,9 @@ def call(self, params: Union[str, dict], **kwargs) -> str: if not isinstance(query, str) or not query.strip(): return "[WebSearch] 'query' must be a non-empty string." + provider = os.getenv("SEARCH_PROVIDER", "serper").strip().lower() + if provider == "tavily": + return self.google_search_with_tavily(query.strip()) return self.google_search_with_serp(query.strip()) diff --git a/agent_base/tools/tooling.py b/agent_base/tools/tooling.py index 8d93d17..08c07aa 100644 --- a/agent_base/tools/tooling.py +++ b/agent_base/tools/tooling.py @@ -66,6 +66,7 @@ SENSITIVE_ENV_EXACT = { "API_KEY", "SERPER_KEY", + "TAVILY_API_KEY", "JINA_KEY", "MINERU_TOKEN", "OPENAI_API_KEY", diff --git a/requirements.txt b/requirements.txt index 214730c..1b3c15f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,6 @@ openai>=2.3.0 Pillow>=11.3.0 requests>=2.32.5 structai>=0.1.23 +tavily-python>=0.5.0 tiktoken>=0.12.0 uvicorn>=0.34.0