Spaces:
Running
Running
File size: 6,382 Bytes
28a08e7 | 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 | """
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, ""
|