yc1838
feat: implement web search and file processing tools with robust retrieval fallbacks
44ad484
Raw
History Blame Contribute Delete
2.34 kB
"""DuckDuckGo web search – zero API key required.
Uses the ddgs library (pip install ddgs).
Falls back gracefully if the library is not installed.
"""
from __future__ import annotations
import logging
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
log = logging.getLogger(__name__)
_HARD_TIMEOUT_S = 15
def _run_ddgs(query: str, max_results: int) -> list[dict]:
from ddgs import DDGS
with DDGS(timeout=10) as ddgs:
return list(ddgs.text(query, max_results=max_results))
def web_search(query: str, max_results: int = 5) -> str:
"""Search the web via DuckDuckGo. Returns title/url/snippet results."""
try:
import ddgs # noqa: F401
except ImportError:
return "ERROR: ddgs not installed. Run: pip install ddgs"
# Run in a worker thread with a hard wall-clock timeout: DDGS' pagination
# loop can stall beyond its per-request timeout and block the agent.
pool = ThreadPoolExecutor(max_workers=1, thread_name_prefix="web_search")
try:
future = pool.submit(_run_ddgs, query, max_results)
try:
results = future.result(timeout=_HARD_TIMEOUT_S)
except FuturesTimeout:
log.warning("[web_search] hard timeout after %ss, query=%r", _HARD_TIMEOUT_S, query)
return f"ERROR: web_search timed out after {_HARD_TIMEOUT_S}s"
finally:
# Don't block on the possibly-stuck worker thread on shutdown.
pool.shutdown(wait=False, cancel_futures=True)
except Exception as e:
log.warning("[web_search] DuckDuckGo error: %s", e)
return f"ERROR: DuckDuckGo search failed: {e}"
if not results:
return "No results."
lines: list[str] = []
for r in results:
title = r.get("title", "No Title")
url = r.get("href", "No URL")
body = r.get("body", "No snippet available")
lines.append(f"- {title} ({url})\n {body}")
import json
metadata = {
"value": len(results),
"data_source": "duckduckgo",
"record_type": "web-snippet",
"type_strictness": "broad",
"note": "Web snippets are unstructured and may contain irrelevant or duplicate information."
}
res_text = "\n".join(lines)
return f"{res_text}\n\nMETADATA:\n{json.dumps(metadata, indent=2)}"