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, ""