Spaces:
Running
Running
File size: 19,132 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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 | """
planner.py β Speculative Hybrid Planner (Gap-5: eliminazione bottleneck sequenziale)
Architettura:
FASE 1 Quick-Start Draft < 500ms Cerebras gpt-oss-120b (o Groq 8B fallback)
Genera 2-3 subtask immediati β agente parte istantaneamente.
FASE 2 Master Plan background DeepSeek-R1 via OpenRouter
Piano architetturale completo; raffina i passi successivi.
FASE 3 Parallel Sub-Graphs β Campo `parallel_groups` nel JSON
Rami indipendenti (es. Backend vs Frontend) identificati esplicitamente.
CompatibilitΓ backward: `create_plan()` restituisce sempre dict con "subtasks".
Flag `_speculative: True` β piano quick-start; `_speculative: False` β master plan.
"""
from __future__ import annotations
import asyncio
import json
import logging
import re
from models.ai_client import AIClient
_logger = logging.getLogger("agente_ai")
# ββ Prompt master (DeepSeek-R1): piano architetturale completo βββββββββββββββ
PLANNER_SYSTEM = """Sei un planner AI avanzato. Dato un obiettivo, decomponilo in subtask concreti.
Rispondi SOLO con JSON valido nel formato:
{
"goal": "obiettivo originale",
"complexity": "low|medium|high",
"data_model": [
{"entity": "NomeEntitΓ ", "fields": ["id: str", "campo1: tipo", "campo2: tipo"]}
],
"api_contract": [
{"method": "GET|POST|PUT|DELETE", "path": "/api/risorsa", "body": {}, "response": {"campo": "tipo"}}
],
"subtasks": [
{
"id": 1,
"description": "cosa fare",
"tool": "<tool>",
"requires": [],
"risk": "low|medium|high",
"priority": "low|medium|high"
}
],
"parallel_groups": [[1,2],[3,4,5]],
"estimated_steps": 3,
"impacted_files": []
}
data_model: lista di entitΓ dati con i loro campi tipizzati (SOLO per goal con entitΓ persistenti).
- Ogni entitΓ : {"entity": "Nome", "fields": ["campo: tipo", ...]}
- Esempi di tipi: str, int, float, bool, datetime, list[str], dict.
- Lascia [] per task senza entitΓ dati (web search, domande, spiegazioni, singole funzioni).
- P25-B5: definisci data_model PRIMA dei subtask di codice β Γ¨ il contratto condiviso tra tutti i subtask.
- I subtask di codice DEVONO referenziare le entitΓ definite qui, MAI inventare nomi diversi on-the-fly.
api_contract: endpoint REST/GraphQL con shape request+response (SOLO per goal con interfaccia API).
- Ogni endpoint: {"method": "GET", "path": "/api/path", "body": {"campo": "tipo"}, "response": {"campo": "tipo"}}
- Lascia [] per task senza endpoint API (script standalone, funzioni pure, task di analisi).
- P25-B5: il primo subtask di codice backend DEVE implementare esattamente questo contratto.
- MAI aggiungere endpoint non dichiarati qui senza aggiornare api_contract nel piano.
parallel_groups: lista di liste di id subtask che possono girare in PARALLELO tra loro.
- Ogni lista interna = un gruppo di subtask eseguibili contemporaneamente (nessuna dipendenza reciproca).
- Subtask con requires:[] vanno sempre in un gruppo parallelo.
- Esempio: backend (id:1,2) e frontend (id:3,4) senza dipendenze reciproche β [[1,2],[3,4]].
- Se tutto Γ¨ sequenziale: [[1],[2],[3]].
impacted_files: lista di path file VFS che potrebbero essere impattati (vuota se non applicabile).
Tool disponibili:
web_search β cerca informazioni online in tempo reale
code β genera/modifica codice (Python, TS, JS, etc.)
read_page β legge una pagina web per URL
memory β accede a dati precedentemente memorizzati
direct_response β risposta diretta senza tool esterni
send_email β invia email via Resend
database_query β esegue query su database PostgreSQL/SQLite
web_research β ricerca approfondita multi-fonte con sintesi AI
execute_sql β esegue SQL su dati in-memory
create_pdf β genera un documento PDF
call_api β chiama un REST API esterno
generate_image β genera un'immagine AI
run_python β esegue codice Python in sandbox sicura
write_file β scrive un file nel filesystem virtuale (risk: medium)
read_file β legge un file dal filesystem virtuale (risk: low)
apply_patch β applica una patch unificata a un file (risk: medium)
execute_shell β esegue un comando shell in sandbox (risk: high)
directory_tree β elenca struttura ad albero di una directory (risk: low)
file_search β cerca pattern nei file con grep (risk: low)
git_status β mostra branch corrente e file modificati (risk: low)
git_clone β clona una repo remota (risk: medium)
git_diff β mostra modifiche in sospeso (risk: low)
git_commit β esegue add -A + commit (risk: medium)
npm_install β installa dipendenze node (risk: medium)
npm_run β esegue script node (risk: medium)
pip_install β installa pacchetti Python (risk: medium)
type_check β type check tsc o mypy (risk: low)
scaffold_project β crea struttura progetto da template (risk: medium)
delegate_task β delega un sotto-obiettivo a un micro-agente indipendente (risk: low)
REGOLA TASK SEMPLICI (S-ROBUSTNESS): Se il task chiede una singola funzione TypeScript pura
(sum, add, calculate, map, filter) anche se il prompt ha rumore/distrazioni/noise:
β piano con 1 SOLO subtask: tool=direct_response
β MAI run_python, type_check o npm_run per funzioni TypeScript di 1-3 righe
REGOLA DATA INTEGRITY (S-RECOVERY): Prima di pianificare analisi su dati numerici:
- Controlla: conversion rate > 100%? conversioni > utenti? β IMPOSSIBILE
- Se dati impossibili β piano con SOLO 1 subtask: tool=direct_response
description: "segnala anomalia nei dati: incoerente/impossibile, non calcolare"
- MAI pianificare run_python/execute_sql su dati statisticamente impossibili
REGOLA ASSOLUTA (S-GAP2): Per qualsiasi richiesta di creazione app/progetto/boilerplate,
DEVI verificare se esiste scaffold_project corrispondente. Se esiste β PRIMO subtask.
REGOLA ORCHESTRATION (S-GAP9): Per task complessi (>5 passi), includi SEMPRE un subtask finale di "Verifica Integrazione e Test End-to-End".
Scomponi i rami Backend e Frontend in parallel_groups separati per massimizzare l'efficienza.
REGOLA RECOVERY & ROBUSTNESS (S-GAP12, S-GAP7):
- Se l'obiettivo Γ¨ ambiguo o i dati sembrano incoerenti, il primo subtask DEVE essere "Analisi Critica e Validazione Requisiti" (tool: direct_response).
- Per ogni integrazione API, aggiungi un subtask di "Health Check / Verifica ConnettivitΓ " prima delle operazioni core.
- Se il task fallisce 2 volte, il piano deve includere un passo di "Debug e Analisi Log" (tool: read_file/execute_shell).
REGOLE GRAFO DI DIPENDENZE:
- requires:[] β subtask eseguibile immediatamente in parallelo con altri requires:[]
- requires:[N] β subtask che dipende dall'output di subtask id N
- priority:high: subtask bloccante; i dipendenti mettono il suo id in requires
- priority:low: subtask indipendente; eseguibile in parallelo
- Identifica SEMPRE rami indipendenti (es. Backend vs Frontend, Read vs Write diversi file)
- Aggiungi entrambi i rami in parallel_groups per massimizzare il parallelismo"""
# ββ Prompt quick-start (Cerebras/Groq): 2-3 passi immediati βββββββββββββββββ
PLANNER_QUICK_SYSTEM = """Sei un planner rapido. Dato un obiettivo, genera SOLO i primi 2-3 passi immediati e concreti.
Rispondi SOLO con JSON valido:
{
"goal": "obiettivo",
"complexity": "low|medium|high",
"subtasks": [
{"id": 1, "description": "primo passo", "tool": "<tool>", "requires": [], "risk": "low", "priority": "high"},
{"id": 2, "description": "secondo passo", "tool": "<tool>", "requires": [1], "risk": "low", "priority": "medium"}
],
"parallel_groups": [[1],[2]],
"estimated_steps": 2,
"impacted_files": []
}
Regole:
- MAX 3 subtask β solo le azioni piΓΉ immediate e ovvie
- Scegli tool giusto: web_search/read_page per info, run_python per codice, write_file per file
- Non pianificare l'intero progetto β solo il "prossimo passo" logico
- requires:[] per passi indipendenti (possono partire subito in parallelo)"""
def _extract_json_balanced(raw: str) -> str | None:
"""P16-B3: depth-counting bilanciato β sostituisce regex greedy r'{[\s\S]+}'.
Gestisce JSON annidati correttamente (piani con subtask oggetti complessi).
"""
depth = 0
start = -1
for i, ch in enumerate(raw):
if ch == '{':
if depth == 0:
start = i
depth += 1
elif ch == '}':
depth -= 1
if depth == 0 and start != -1:
return raw[start:i + 1]
return None
def _parse_plan(raw: str) -> dict | None:
"""Estrae e valida il JSON del piano dalla risposta LLM."""
if not raw:
return None
json_match = _extract_json_balanced(raw)
if not json_match:
return None
try:
plan = json.loads(json_match)
if not plan.get("subtasks"):
return None
return plan
except (json.JSONDecodeError, ValueError):
return None
class Planner:
def __init__(self, llm_client: AIClient | None = None):
if llm_client is not None:
self.llm = llm_client
else:
# Master planner: DeepSeek-R1 per deep reasoning
try:
from models.role_router import RoleRouter, Role
self.llm = RoleRouter.get_client(Role.ARCHITECT)
except Exception:
self.llm = AIClient()
@classmethod
def from_ollama(cls, ollama=None) -> "Planner":
return cls()
def _get_fast_llm(self) -> AIClient:
"""Gap-5: Cerebras gpt-oss-120b (2000+ tok/s) per quick-start draft.
Fallback: Groq llama-3.1-8b-instant se CEREBRAS_API_KEY assente."""
try:
from models.role_router import RoleRouter, Role
return RoleRouter.get_client(Role.REASONER) # Cerebras 120B
except Exception as _exc:
_logger.debug("[planner] silenced %s", type(_exc).__name__) # noqa: BLE001
try:
from models.role_router import RoleRouter, Role
return RoleRouter.get_client(Role.FAST) # Groq 8B fallback
except Exception:
return AIClient()
def _build_messages(self, system: str, goal: str,
context: list | None = None) -> list[dict]:
msgs = [
{"role": "system", "content": system},
{"role": "user", "content": f"Obiettivo: {goal}"},
]
if context:
ctx_str = "\n".join(m.get("content", "")[:500] for m in context[-5:])
msgs[1]["content"] += f"\n\nContesto recente:\n{ctx_str}"
return msgs
async def create_plan(self, goal: str,
context: list | None = None,
model: str | None = None) -> dict:
"""
Gap-5: Speculative Hybrid Planning.
Lancia in parallelo:
1. Quick-Start (Cerebras/Groq) β risponde in < 500ms con 2-3 subtask immediati
2. Master Plan (DeepSeek-R1) β risponde in 8-15s con piano architetturale completo
Logica:
- attende max QUICK_TIMEOUT per il quick-start
- se arriva β restituisce subito (flag _speculative=True) cosΓ¬ l'agente parte
- se DeepSeek-R1 arriva prima β piano completo (flag _speculative=False)
- se entrambi timeout β fallback euristico
"""
QUICK_TIMEOUT = 1.2 # secondi: soglia "fast-first" win
MASTER_TIMEOUT = 30.0 # secondi: timeout totale DeepSeek-R1
msgs_quick = self._build_messages(PLANNER_QUICK_SYSTEM, goal, context)
msgs_master = self._build_messages(PLANNER_SYSTEM, goal, context)
fast_llm = self._get_fast_llm()
async def _call_quick() -> dict | None:
try:
raw = await asyncio.wait_for(
fast_llm.chat(msgs_quick, temperature=0.2, max_tokens=512),
timeout=QUICK_TIMEOUT,
)
plan = _parse_plan(raw)
if plan:
plan["_speculative"] = True
plan["_raw"] = raw[:400]
return plan
except Exception:
return None
async def _call_master() -> dict | None:
for _attempt in range(3):
try:
raw = await asyncio.wait_for(
self.llm.chat(msgs_master, temperature=0.3, max_tokens=2048),
timeout=MASTER_TIMEOUT,
)
plan = _parse_plan(raw)
if plan:
plan["_speculative"] = False
plan["_raw"] = raw[:400]
return plan
except (asyncio.TimeoutError, TimeoutError):
if _attempt < 2:
await asyncio.sleep(1.0 * (2 ** _attempt))
continue
break
except Exception:
break
return None
# ββ Speculative dual-fire ββββββββββββββββββββββββββββββββββββββββββββ
# Entrambi i modelli partono simultaneamente.
# asyncio.wait(FIRST_COMPLETED) con soglia QUICK_TIMEOUT:
# - Se quick-start risponde prima β agente parte subito (< 1s)
# - Master plan continua in background; il loop lo ignora (non ha callback)
# - Se master arriva per primo (es. R1 cold-start veloce) β piano completo
quick_task = asyncio.create_task(_call_quick())
master_task = asyncio.create_task(_call_master())
done, pending = await asyncio.wait(
{quick_task, master_task},
timeout=QUICK_TIMEOUT,
return_when=asyncio.FIRST_COMPLETED,
)
# Caso 1: quick-start ha risposto entro QUICK_TIMEOUT
if quick_task in done:
quick_plan = quick_task.result()
if quick_plan:
# P25-B3: per goal complessi (app/progetto/sistema), ignora quick-plan con β€3 subtask
# e attendi il master plan architetturale. Il quick-plan 2-3 step causa rework
# sistematico su task multi-file che richiedono schema + contratto API.
_COMPLEX_APP_RE = re.compile(
r'\b(crea\s+(?:una\s+)?(?:app|applicazione|sistema|sito|progetto|piattaforma|servizio)|'
r'build\s+(?:a\s+)?(?:app|system|platform|service|website)|'
r'sviluppa|realizza|implementa\s+(?:un[ao]\s+)?(?:sistema|app|servizio)|'
r'full.?stack|backend\s+e\s+frontend|frontend\s+e\s+backend)\b',
re.IGNORECASE,
)
_is_complex_app = bool(_COMPLEX_APP_RE.search(goal)) and len(goal) > 60
_n_quick_subtasks = len(quick_plan.get("subtasks", []))
if _is_complex_app and _n_quick_subtasks <= 3:
_logger.info(
"P25-B3: goal complesso rilevato (%d subtask quick) β attendo master plan",
_n_quick_subtasks,
)
# Non restituire il quick-plan; lascia cadere al Caso 2 (wait master)
else:
_logger.info(
"Gap-5 speculative: quick-start plan (%d subtask), master in background",
_n_quick_subtasks,
)
# Master task continua in background; risultato non bloccante
master_task.add_done_callback(
lambda t: (
_logger.info(
"Gap-5 master plan ready (%d subtask) β successiva chiamata beneficerΓ ",
len((t.result() or {}).get("subtasks", [])) if not t.cancelled() and t.exception() is None else 0,
)
if not t.cancelled() and t.exception() is None
else None
)
)
return quick_plan
# Caso 2: nessuno ha risposto in QUICK_TIMEOUT β aspetta il master plan
if master_task in done:
master_plan = master_task.result()
if master_plan:
quick_task.cancel()
return master_plan
# Caso 3: nessuno ancora pronto β aspetta fino a MASTER_TIMEOUT
remaining = {t for t in {quick_task, master_task} if not t.done()}
if remaining:
done2, _ = await asyncio.wait(remaining, timeout=MASTER_TIMEOUT - QUICK_TIMEOUT)
for t in done2:
if t.exception() is None and not t.cancelled():
result = t.result()
if result:
for other in remaining - {t}:
other.cancel()
return result
# Cancella task pendenti
for t in {quick_task, master_task}:
if not t.done():
t.cancel()
# ββ Fallback euristico (piano semplice) ββββββββββββββββββββββββββββββ
_logger.warning("Gap-5 planner: tutti i modelli in timeout per goal: %s", goal[:80])
_g = goal.lower()
_ft, _fr = "direct_response", "low"
if re.search(r"https?://", _g): _ft = "read_page"
elif re.search(r"\b(cerca|search|notizie|news)\b", _g): _ft = "web_search"
elif re.search(r"\b(git|branch|commit|diff)\b", _g): _ft = "git_status"
elif re.search(r"\b(struttura|directory|tree|elenca)\b", _g): _ft = "directory_tree"
elif re.search(r"\b(grep|occorrenze|cerca.*codice)\b", _g): _ft = "file_search"
elif re.search(r"\b(npm|pnpm|yarn)\b", _g): _ft, _fr = "npm_run", "medium"
elif re.search(r"\b(pip |pip3 |installa pacchett)\b", _g): _ft, _fr = "pip_install", "medium"
elif re.search(r"\b(codice|python|script)\b", _g): _ft, _fr = "run_python", "medium"
elif re.search(r"\b(scaffold|bootstrap)\b|crea.*app|crea.*progetto|nuovo.*progetto", _g): _ft, _fr = "scaffold_project", "medium"
elif re.search(r"\b(genera|crea).*immagine\b", _g): _ft, _fr = "generate_image", "medium"
return {
"goal": goal, "complexity": "medium",
"subtasks": [{"id": 1, "description": goal, "tool": _ft,
"requires": [], "risk": _fr, "priority": "high"}],
"parallel_groups": [[1]],
"estimated_steps": 1, "impacted_files": [], "_fallback": True,
}
|