import re from typing import Any, Optional from smolagents.tools import Tool try: from duckduckgo_search import DDGS except ImportError: try: from ddgs import DDGS except ImportError as e: raise ImportError( "You must install package `duckduckgo_search` or `ddgs` to run this tool." ) from e class DuckDuckGoSearchTool(Tool): name = "web_search" description = ( "Performs a DuckDuckGo web search. Returns titles, links, and text snippets." ) inputs = { "query": { "type": "string", "description": "The search query to perform.", } } output_type = "string" def __init__(self, max_results: int = 5, **kwargs: Any) -> None: super().__init__(**kwargs) self.max_results = max_results def _sanitize_query(self, query: str) -> str: """Fixes common LLM mistakes like putting URL paths inside site: operators.""" # Find site:domain.com/path/to/page and trim it to site:domain.com def clean_site(match: re.Match) -> str: domain_part = match.group(1).split("/")[0].strip() return f"site:{domain_part}" cleaned = re.sub(r"site:https?://", "site:", query) cleaned = re.sub(r"site:([^\s]+)", clean_site, cleaned) return cleaned.strip() def forward(self, query: str) -> str: safe_query = self._sanitize_query(query) headers = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" ) } results = [] # Try different backends (html, api, lite) in case one is rate-limited for backend in ["api", "html", "lite"]: try: with DDGS(headers=headers, timeout=10) as ddgs: res = list( ddgs.text( safe_query, region="wt-wt", safesearch="moderate", backend=backend, max_results=self.max_results, ) ) if res: results = res break except Exception: continue if not results: return ( f"No results found for query: '{safe_query}'. " "Instead of retrying generic searches, visit a calendar page directly with `visit_webpage`." ) postprocessed = [ f"[{r.get('title', 'No Title')}]({r.get('href', '')})\n{r.get('body', '')}" for r in results ] return "## Search Results\n\n" + "\n\n".join(postprocessed)