jatin gyass
update for the new web data source
4db2d34
Raw
History Blame Contribute Delete
12.4 kB
"""
backend/tools/registry.py
All tools available to the Executor agent.
Each tool:
- Has a JSON schema (for OpenAI function calling)
- Is async
- Returns structured output with metadata
- Handles errors gracefully
"""
from __future__ import annotations
import asyncio
import json
import math
import re
import time
from datetime import datetime
from typing import Any
import httpx
from langchain_core.tools import tool
# ── Tool result wrapper ───────────────────────────────────────────────────────
def ok(data: Any, metadata: dict | None = None) -> dict:
return {"status": "ok", "data": data, "metadata": metadata or {}, "timestamp": time.time()}
def err(message: str, details: str = "") -> dict:
return {"status": "error", "error": message, "details": details, "timestamp": time.time()}
# ── Web search ────────────────────────────────────────────────────────────────
async def web_search(query: str, max_results: int = 5) -> dict:
"""Search via Serper (Google), falling back to DuckDuckGo if unavailable."""
from ..core.config import get_settings
settings = get_settings()
# ── Primary: Serper (Google Search API) ──────────────────────────────────
if settings.serper_api_key:
try:
async with httpx.AsyncClient(timeout=10) as c:
r = await c.post(
"https://google.serper.dev/search",
headers={"X-API-KEY": settings.serper_api_key, "Content-Type": "application/json"},
json={"q": query, "num": int(max_results)},
)
if r.status_code == 200:
data = r.json()
items = data.get("organic", [])
formatted = "\n\n".join(
f"**{item.get('title', '')}**\n{item.get('link', '')}\n{item.get('snippet', '')}"
for item in items
)
return ok({"query": query, "results": formatted[:3000], "count": len(items), "source": "serper"})
except Exception:
pass # fall through to DuckDuckGo
# ── Fallback: DuckDuckGo ──────────────────────────────────────────────────
try:
from duckduckgo_search import DDGS
loop = asyncio.get_event_loop()
def _search():
with DDGS() as ddgs:
return list(ddgs.text(query, max_results=int(max_results)))
results = await loop.run_in_executor(None, _search)
formatted = "\n\n".join(
f"**{r.get('title', '')}**\n{r.get('href', '')}\n{r.get('body', '')}"
for r in results
)
return ok({"query": query, "results": formatted[:3000], "count": len(results), "source": "duckduckgo"})
except Exception as e:
return err(f"Search failed: {e}", str(e))
# ── URL fetch ─────────────────────────────────────────────────────────────────
async def fetch_url(url: str, max_chars: int = 4000) -> dict:
"""Fetch content from a URL and extract clean text."""
try:
from bs4 import BeautifulSoup
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
resp = await client.get(url, headers={"User-Agent": "Mozilla/5.0"})
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
for tag in soup(["script", "style", "nav", "footer"]):
tag.decompose()
text = " ".join(soup.get_text(separator=" ").split())
return ok({"url": url, "content": text[:max_chars], "status_code": resp.status_code})
except Exception as e:
return err(f"Fetch failed: {e}", str(e))
# ── Calculator ────────────────────────────────────────────────────────────────
def calculate(expression: str) -> dict:
"""
Safely evaluate a mathematical expression.
Supports: +, -, *, /, **, sqrt, log, sin, cos, etc.
"""
# Whitelist safe operations only
safe_names = {
"abs": abs, "round": round, "min": min, "max": max,
"sum": sum, "sqrt": math.sqrt, "log": math.log,
"log10": math.log10, "exp": math.exp,
"sin": math.sin, "cos": math.cos, "tan": math.tan,
"pi": math.pi, "e": math.e, "pow": math.pow, "floor": math.floor,
"ceil": math.ceil,
}
# Remove any potentially dangerous tokens
cleaned = re.sub(r'[^0-9+\-*/().,\s\w]', '', expression)
try:
result = eval(cleaned, {"__builtins__": {}}, safe_names)
return ok({"expression": expression, "result": result})
except Exception as e:
return err(f"Calculation failed: {e}", f"Expression: {expression}")
# ── In-memory file system ─────────────────────────────────────────────────────
_file_store: dict[str, str] = {}
def write_file(filename: str, content: str) -> dict:
"""Write content to an in-memory file store."""
_file_store[filename] = content
return ok({"filename": filename, "bytes": len(content), "files": list(_file_store.keys())})
def read_file(filename: str) -> dict:
"""Read content from in-memory file store."""
if filename not in _file_store:
return err(f"File not found: {filename}", f"Available: {list(_file_store.keys())}")
return ok({"filename": filename, "content": _file_store[filename]})
def list_files() -> dict:
"""List all files in the in-memory store."""
files = [{"name": k, "size": len(v)} for k, v in _file_store.items()]
return ok({"files": files, "count": len(files)})
# ── Code runner ───────────────────────────────────────────────────────────────
def run_python(code: str) -> dict:
"""
Execute Python code in a sandbox with common data-science libs available.
Blocks truly dangerous ops (os, subprocess, socket, open).
"""
import io, contextlib, builtins
# Block dangerous modules only
blocked = ["subprocess", "socket", "requests"]
for b in blocked:
if f"import {b}" in code:
return err(f"Blocked: '{b}' not allowed in sandbox")
if "open(" in code:
return err("Blocked: file 'open()' not allowed in sandbox")
# Allow standard builtins + safe data libs
import math, json
from datetime import datetime as _dt
safe_globals = {
"__builtins__": builtins, # full builtins so import works
"math": math,
"json": json,
"datetime": _dt,
}
# Try to inject optional libs if installed
for lib in ["pandas", "numpy", "matplotlib"]:
try:
import importlib
safe_globals[lib.split(".")[0]] = importlib.import_module(lib)
except ImportError:
pass
stdout_capture = io.StringIO()
try:
with contextlib.redirect_stdout(stdout_capture):
exec(code, safe_globals)
output = stdout_capture.getvalue()
return ok({"output": output[:2000] or "(no output)", "code": code[:500]})
except Exception as e:
return err(f"Execution error: {type(e).__name__}: {e}", code[:200])
# ── Current date/time ─────────────────────────────────────────────────────────
def get_datetime() -> dict:
"""Get current date, time, and timezone info."""
now = datetime.utcnow()
return ok({
"utc": now.isoformat(),
"date": now.strftime("%Y-%m-%d"),
"time": now.strftime("%H:%M:%S"),
"day_of_week": now.strftime("%A"),
})
# ── Tool registry ─────────────────────────────────────────────────────────────
TOOL_SCHEMAS = [
{
"type": "function",
"function": {
"name": "web_search",
"description": "Search the internet for current information. Use for facts, news, research.",
"parameters": {
"type": "object",
"required": ["query"],
"properties": {
"query": {"type": "string", "description": "Search query string"},
"max_results": {"type": "integer", "default": 5},
},
},
},
},
{
"type": "function",
"function": {
"name": "fetch_url",
"description": "Fetch and read content from a specific URL.",
"parameters": {
"type": "object",
"required": ["url"],
"properties": {
"url": {"type": "string"},
"max_chars": {"type": "integer", "default": 4000},
},
},
},
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a mathematical expression. Supports arithmetic, sqrt, log, trig.",
"parameters": {
"type": "object",
"required": ["expression"],
"properties": {
"expression": {"type": "string", "description": "Math expression, e.g. 'sqrt(144) + 2**8'"},
},
},
},
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "Save content to a named file for later use.",
"parameters": {
"type": "object",
"required": ["filename", "content"],
"properties": {
"filename": {"type": "string"},
"content": {"type": "string"},
},
},
},
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read previously saved file content.",
"parameters": {
"type": "object",
"required": ["filename"],
"properties": {
"filename": {"type": "string"},
},
},
},
},
{
"type": "function",
"function": {
"name": "run_python",
"description": "Execute Python code and capture output. Use for data processing, calculations.",
"parameters": {
"type": "object",
"required": ["code"],
"properties": {
"code": {"type": "string", "description": "Python code to execute"},
},
},
},
},
{
"type": "function",
"function": {
"name": "get_datetime",
"description": "Get current UTC date and time.",
"parameters": {"type": "object", "properties": {}},
},
},
]
async def execute_tool(name: str, args: dict) -> dict:
"""Dispatch a tool call by name."""
dispatch = {
"web_search": lambda: web_search(**args),
"fetch_url": lambda: fetch_url(**args),
"calculate": lambda: calculate(**args),
"write_file": lambda: write_file(**args),
"read_file": lambda: read_file(**args),
"run_python": lambda: run_python(**args),
"get_datetime": lambda: get_datetime(),
}
fn = dispatch.get(name)
if fn is None:
return err(f"Unknown tool: {name}")
try:
result = fn()
if asyncio.iscoroutine(result):
return await result
return result
except Exception as e:
return err(f"Tool '{name}' crashed: {e}", str(e))