Terminal / agents /tool_generator.py
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
Raw
History Blame Contribute Delete
6.38 kB
"""
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, ""