File size: 12,434 Bytes
2eef9ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4db2d34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2eef9ea
 
 
 
 
 
4db2d34
2eef9ea
 
 
 
 
 
4db2d34
2eef9ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4db2d34
 
2eef9ea
4db2d34
 
 
2eef9ea
4db2d34
2eef9ea
4db2d34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2eef9ea
 
 
 
4db2d34
2eef9ea
4db2d34
2eef9ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
"""
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))