Spaces:
Sleeping
Sleeping
| """ | |
| 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)) | |