""" tool_generator.py — COG-4: Dynamic Tool Generation at runtime. Quando l'executor non trova un tool adatto per un subtask, chiede all'LLM di scrivere una piccola funzione Python async, la valida in sandbox (run_python), e la registra nel TOOL_REGISTRY per la durata della sessione corrente. Security constraints: - Blocca import di rete (requests, httpx, socket, urllib, subprocess) - Blocca accesso filesystem in write (open() write mode) - Timeout 12s generazione + 10s validazione - Max 5 tool generati per sessione (evita context explosion) - Tool generati hanno prefisso "_dyn_" per identificazione e pulizia Integration: chiamato da unified_loop.py quando _TOOL_MAP.get(tool) == (None, None) e needs_dynamic_tool() restituisce True. """ from __future__ import annotations import asyncio import logging import re from typing import Any _logger = logging.getLogger("agente_ai.tool_gen") _GENERATED_COUNT = 0 _MAX_GENERATED = 5 _BLOCKED_RE = re.compile( r"\b(import\s+(requests|httpx|socket|urllib|subprocess|paramiko|ftplib)" r"|os\.system\s*\(|os\.popen\s*\(|eval\s*\(|exec\s*\(" r"|__import__\s*\(|open\s*\([^)]*['\"][wa][+bt]*['\"])", re.IGNORECASE | re.MULTILINE, ) _TOOL_NAME_RE = re.compile(r"^[a-z][a-z0-9_]{2,39}$") _GEN_SYSTEM = """\ Sei un tool engineer Python. Scrivi UNA funzione Python asincrona che risolve il task specificato. REGOLE ASSOLUTE: 1. Firma obbligatoria: `async def tool_fn(**kwargs) -> dict` 2. Ritorna sempre: `{"success": True/False, "output": , "error": ""}` 3. ZERO import di rete (no requests, httpx, socket, urllib, subprocess) 4. ZERO filesystem esterno (no open() in write mode) 5. Solo stdlib: json, re, math, datetime, collections, itertools, hashlib, base64, urllib.parse 6. Max 30 righe 7. Rispondi SOLO con il blocco ```python ... ``` — zero testo extra Esempio: ```python import re async def tool_fn(**kwargs) -> dict: text = kwargs.get("text", "") words = re.findall(r"\\b\\w+\\b", text.lower()) freq = {} for w in words: freq[w] = freq.get(w, 0) + 1 return {"success": True, "output": sorted(freq.items(), key=lambda x: -x[1])[:10], "error": ""} ```\ """ def needs_dynamic_tool(tool_name: str, description: str) -> bool: """ Decide se generare un tool dinamico per questo subtask. Condizioni positive: - La descrizione suggerisce un'operazione computazionale locale - Non siamo al limite di tool generati per sessione Non genera tool per: operazioni di rete, filesystem esterno, browser. """ global _GENERATED_COUNT if _GENERATED_COUNT >= _MAX_GENERATED: return False _NETWORK_RE = re.compile( r"\b(http|url|fetch|scrape|download|upload|api|request|socket)\b", re.IGNORECASE, ) if _NETWORK_RE.search(description): return False # operazioni di rete → non generare tool locale _COMPUTE_RE = re.compile( r"\b(calcola|converti|trasforma|analizza|estrai|filtra|ordina|conta" r"|parse|format|encode|decode|compress|hash|valida|validate" r"|split|merge|aggregate|summarize|riassumi|statistiche)\b", re.IGNORECASE, ) return bool(_COMPUTE_RE.search(description)) async def generate_and_register( description: str, tool_name: str, llm: Any, executor: Any, ) -> "tuple[bool, str]": """ Genera un tool Python via LLM, lo valida in sandbox, lo registra. Returns: (success: bool, dyn_name: str) """ global _GENERATED_COUNT if _GENERATED_COUNT >= _MAX_GENERATED: return False, "" safe_name = re.sub(r"[^a-z0-9_]", "_", tool_name.lower())[:36] if not _TOOL_NAME_RE.match(safe_name): safe_name = f"gen{_GENERATED_COUNT + 1}" dyn_name = f"_dyn_{safe_name}" # Step 1: genera il codice via LLM try: raw = await asyncio.wait_for( llm.chat( [ {"role": "system", "content": _GEN_SYSTEM}, {"role": "user", "content": f"Scrivi un tool per: {description[:400]}"}, ], temperature=0.1, max_tokens=600, ), timeout=12.0, ) except Exception as exc: _logger.warning("COG-4 gen LLM failed: %s", exc) return False, "" # Step 2: estrai blocco codice m = re.search(r"```(?:python)?\n?([\s\S]+?)```", raw) code = m.group(1).strip() if m else raw.strip() # Step 3: security gate if _BLOCKED_RE.search(code): _logger.warning("COG-4 blocked unsafe import in generated tool: %s", dyn_name) return False, "" # Step 4: valida in sandbox validation = ( f"{code}\n\n" "import asyncio as _asyncio\n" "_r = _asyncio.run(tool_fn())\n" "assert isinstance(_r, dict), 'must return dict'\n" "assert 'output' in _r, 'must have output key'\n" "print('COG4_OK')\n" ) try: val = await asyncio.wait_for( executor.run_tool("run_python", {"code": validation}), timeout=10.0, ) out = val.get("output", {}) stdout = out.get("stdout", "") if isinstance(out, dict) else str(out) if "COG4_OK" not in stdout: _logger.warning("COG-4 validation failed: %s", stdout[:200]) return False, "" except Exception as exc: _logger.warning("COG-4 sandbox error: %s", exc) return False, "" # Step 5: registra nel TOOL_REGISTRY (runtime only, non persiste) try: from tools.registry import TOOL_REGISTRY ns: dict = {} exec(compile(code, f"", "exec"), ns) # noqa: S102 fn = ns.get("tool_fn") if not fn or not asyncio.iscoroutinefunction(fn): _logger.warning("COG-4 tool_fn missing or not async") return False, "" TOOL_REGISTRY[dyn_name] = { "description": f"[DYN] {description[:200]}", "required_inputs": [], "_fn": fn, "_generated": True, } _GENERATED_COUNT += 1 _logger.info( "COG-4 registered '%s' (%d/%d total dynamic tools)", dyn_name, _GENERATED_COUNT, _MAX_GENERATED, ) return True, dyn_name except Exception as exc: _logger.warning("COG-4 registration failed: %s", exc) return False, ""