Spaces:
Configuration error
Configuration error
Pulka
fix(backend): sync missing/outdated modules from GitHub — escalation_ladder, planner v2, goal_verifier v2, strategic_healer, dynamic_replanner, backend_antiregress, tdd_runner v2, unified_loop v5, unified_loop_tools, auth_guard, decision_memory, incident_registry, notify_bot
35b603c verified | """ | |
| 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": <risultato>, "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"<dyn:{dyn_name}>", "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, "" | |