Spaces:
Running
Running
File size: 37,382 Bytes
28a08e7 201bed4 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 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 | """backend/api/benchmark.py β Self-test endpoint server-side (S-BENCH).
Endpoint: GET /api/debug/benchmark?token=<daily_hmac>
Auth: HMAC-SHA256(seed=_BENCH_SEED, msg=YYYY-MM-DD) β calcolabile da Replit
senza dover conoscere INTERNAL_TOKEN. Token cambia ogni giorno (replay protection).
Zero config aggiuntivo su HF Spaces.
Il benchmark esegue ~20 test interni e restituisce JSON con:
{ ok, score, pass, fail, warn, duration_ms, tests: [...], gaps: [...] }
Uso da Replit:
python3 -c "
import hmac, hashlib, datetime
seed = 'agente-ai-bench-2026'
day = datetime.date.today().isoformat()
tok = hmac.new(seed.encode(), day.encode(), hashlib.sha256).hexdigest()
print(tok)
"
curl '<hf-space-a-url>/api/debug/benchmark?token=<tok>'
"""
import os, sys, asyncio, time, hmac, hashlib, datetime, importlib, tempfile, subprocess, re, uuid
from fastapi import APIRouter, Depends, BackgroundTasks, HTTPException, Query, Request
from .auth_guard import require_role, AuthRole
from fastapi.responses import JSONResponse
router = APIRouter( dependencies=[Depends(require_role(AuthRole.MACHINE))]) # GAP-1-fix: router-level auth
# ββ Seed per HMAC daily token β NON Γ¨ un secret, Γ¨ solo anti-scraping βββββββββ
_BENCH_SEED = "agente-ai-bench-2026"
def _daily_token() -> str:
day = datetime.date.today().isoformat()
return hmac.new(_BENCH_SEED.encode(), day.encode(), hashlib.sha256).hexdigest()
# ββ Result helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _ok(id: str, desc: str, note: str = "") -> dict:
return {"id": id, "desc": desc, "ok": True, "warn": False, "note": note}
def _ko(id: str, desc: str, note: str = "") -> dict:
return {"id": id, "desc": desc, "ok": False, "warn": False, "note": note}
def _wn(id: str, desc: str, note: str = "") -> dict:
return {"id": id, "desc": desc, "ok": False, "warn": True, "note": note}
async def _run_tests() -> list[dict]:
results: list[dict] = []
t_global = time.monotonic()
# ββ T01: Sprint / version βββββββββββββββββββββββββββββββββββββββββββββββββ
try:
from api.providers import api_version
ver = await api_version() if asyncio.iscoroutinefunction(api_version) else api_version()
sprint = ver.get("sprint", "?") if isinstance(ver, dict) else getattr(ver, "sprint", "?")
body = ver if isinstance(ver, dict) else ver.__dict__
results.append(_ok("T01", f"Sprint: {sprint} β {body.get('version','?')}", f"build={body.get('build_date','?')}"))
except Exception as e:
results.append(_ko("T01", "Sprint/version import fallito", str(e)))
# ββ T02: INTERNAL_TOKEN configurato ββββββββββββββββββββββββββββββββββββββ
itok = os.getenv("INTERNAL_TOKEN", "")
if itok and len(itok) >= 16:
results.append(_ok("T02", "INTERNAL_TOKEN configurato in env", f"len={len(itok)}"))
elif itok:
results.append(_wn("T02", "INTERNAL_TOKEN troppo corto (< 16 chars)", f"len={len(itok)}"))
else:
results.append(_wn("T02", "INTERNAL_TOKEN non configurato β token effimero generato a ogni boot"))
# ββ T03: UnifiedAgentLoop import βββββββββββββββββββββββββββββββββββββββββ
t = time.monotonic()
try:
from agents.unified_loop import UnifiedAgentLoop
ms = int((time.monotonic() - t) * 1000)
results.append(_ok("T03", f"UnifiedAgentLoop import OK ({ms}ms)"))
except Exception as e:
ms = int((time.monotonic() - t) * 1000)
results.append(_ko("T03", "UnifiedAgentLoop import FALLITO", str(e)[:120]))
# ββ T04: RoleRouter + Role.FAST ββββββββββββββββββββββββββββββββββββββββββ
t = time.monotonic()
try:
from models.role_router import RoleRouter, Role
fast_role = Role.FAST
client = RoleRouter.get_client(Role.FAST)
ms = int((time.monotonic() - t) * 1000)
provider = getattr(client, "provider_name", type(client).__name__)
results.append(_ok("T04", f"Role.FAST client OK ({ms}ms) β provider={provider}"))
except Exception as e:
ms = int((time.monotonic() - t) * 1000)
results.append(_ko("T04", f"Role.FAST client FALLITO ({ms}ms)", str(e)[:120]))
# ββ T05: Python exec via asyncio subprocess βββββββββββββββββββββββββββββββ
t = time.monotonic()
try:
proc = await asyncio.wait_for(
asyncio.create_subprocess_exec(
sys.executable, "-c",
"import sys; print(f'py{sys.version_info.major}.{sys.version_info.minor} ok')",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
),
timeout=10.0,
)
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=10.0)
ms = int((time.monotonic() - t) * 1000)
out = stdout.decode().strip()
if "ok" in out:
results.append(_ok("T05", f"Python exec subprocess OK ({ms}ms)", out))
else:
results.append(_ko("T05", f"Python exec output inatteso ({ms}ms)", out[:80]))
except Exception as e:
ms = int((time.monotonic() - t) * 1000)
results.append(_ko("T05", f"Python exec FALLITO ({ms}ms)", str(e)[:120]))
# ββ T06: Shell echo + date ββββββββββββββββββββββββββββββββββββββββββββββββ
t = time.monotonic()
try:
with tempfile.TemporaryDirectory() as tmpdir:
proc = await asyncio.wait_for(
asyncio.create_subprocess_shell(
"echo bench_ok && date +%s",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
cwd=tmpdir,
),
timeout=8.0,
)
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=8.0)
ms = int((time.monotonic() - t) * 1000)
out = stdout.decode().strip()
if "bench_ok" in out:
results.append(_ok("T06", f"Shell execute OK ({ms}ms)", out[:60]))
else:
results.append(_ko("T06", f"Shell output inatteso ({ms}ms)", out[:60]))
except Exception as e:
ms = int((time.monotonic() - t) * 1000)
results.append(_ko("T06", f"Shell execute FALLITO ({ms}ms)", str(e)[:120]))
# ββ T07: Shell denylist β BLOCKED_CMDS + _EXEC_BLOCKED_RE ββββββββββββββββββ
# exec.py ha due meccanismi: BLOCKED_CMDS (pattern esatti per /api/execute-shell)
# e _EXEC_BLOCKED_RE (regex per /api/exec sandbox Python).
# Test: verifica che almeno uno dei due meccanismi esista e funzioni.
try:
from api.exec import BLOCKED_CMDS
# BLOCKED_CMDS deve contenere pattern per i comandi piΓΉ pericolosi
# RealtΓ : {'rm -rf /', 'mkfs', ':(){:|:&};:', 'dd if=/dev/zero'}
must_have = ["rm -rf /", "mkfs", "dd if=/dev/zero"]
present = [p for p in must_have if any(p in b or b in p for b in BLOCKED_CMDS)]
if len(present) >= 2:
results.append(_ok("T07", f"Shell denylist OK β BLOCKED_CMDS: {len(BLOCKED_CMDS)} pattern",
f"include: {list(BLOCKED_CMDS)[:3]}"))
else:
results.append(_wn("T07", f"Shell denylist parziale β {len(present)}/{len(must_have)} pattern critici",
f"BLOCKED_CMDS={list(BLOCKED_CMDS)}"))
except ImportError:
try:
from api.exec import _EXEC_BLOCKED_RE
results.append(_ok("T07", "Shell denylist OK β _EXEC_BLOCKED_RE presente"))
except ImportError:
results.append(_wn("T07", "denylist non importabile da api.exec (modulo assente)"))
# ββ T08: asyncio.Semaphore S734 ββββββββββββββββββββββββββββββββββββββββββ
try:
from agents.unified_loop_tools import DirectToolsMixin
src_path = importlib.util.find_spec("agents.unified_loop_tools")
if src_path:
import inspect
src = inspect.getsource(DirectToolsMixin)
if "asyncio.Semaphore(4)" in src or "Semaphore" in src:
results.append(_ok("T08", "asyncio.Semaphore S734 presente in DirectToolsMixin"))
else:
results.append(_wn("T08", "asyncio.Semaphore non trovato in DirectToolsMixin"))
else:
results.append(_wn("T08", "unified_loop_tools non trovato"))
except Exception as e:
results.append(_wn("T08", "S734 check non eseguibile", str(e)[:80]))
# ββ T09: Provider env keys β tutti i provider supportati βββββββββββββββββ
# Aggiornato 2026-06-14: aggiunto SAMBANOVA_API_KEY (DeepSeek-V3.1 100% bench)
providers_conf = {
"GROQ_API_KEY": "Groq",
"OPENROUTER_API_KEY": "OpenRouter",
"GEMINI_API_KEY": "Gemini",
"HF_TOKEN": "HuggingFace",
"CEREBRAS_API_KEY": "Cerebras",
"SAMBANOVA_API_KEY": "SambaNova",
}
configured = [name for key, name in providers_conf.items() if os.getenv(key)]
missing = [name for key, name in providers_conf.items() if not os.getenv(key)]
if len(configured) >= 2:
results.append(_ok("T09", f"Provider keys: {len(configured)}/{len(providers_conf)} configurati", ", ".join(configured)))
elif len(configured) == 1:
results.append(_wn("T09", f"Provider keys: solo 1/{len(providers_conf)} ({configured[0]}) β fallback chain ridotta", f"mancanti: {', '.join(missing)}"))
else:
results.append(_ko("T09", "Nessuna provider API key configurata!", f"mancanti: {', '.join(missing)}"))
# ββ T10: SQLite DB write/read βββββββββββββββββββββββββββββββββββββββββββββ
t = time.monotonic()
try:
import sqlite3
with tempfile.NamedTemporaryFile(suffix=".db", delete=True) as f:
db_path = f.name
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE bench (k TEXT, v TEXT)")
conn.execute("INSERT INTO bench VALUES ('test', 'bench_ok')")
conn.commit()
row = conn.execute("SELECT v FROM bench WHERE k='test'").fetchone()
conn.close()
os.unlink(db_path)
ms = int((time.monotonic() - t) * 1000)
if row and row[0] == "bench_ok":
results.append(_ok("T10", f"SQLite write/read OK ({ms}ms)"))
else:
results.append(_ko("T10", f"SQLite round-trip fallito ({ms}ms)", str(row)))
except Exception as e:
ms = int((time.monotonic() - t) * 1000)
results.append(_ko("T10", f"SQLite FALLITO ({ms}ms)", str(e)[:120]))
# ββ T11: /tmp write + read ββββββββββββββββββββββββββββββββββββββββββββββββ
t = time.monotonic()
try:
with tempfile.NamedTemporaryFile(mode="w", suffix=".bench", delete=False) as f:
f.write("bench_filesystem_ok")
fname = f.name
with open(fname) as _bf: content = _bf.read()
os.unlink(fname)
ms = int((time.monotonic() - t) * 1000)
if content == "bench_filesystem_ok":
results.append(_ok("T11", f"/tmp filesystem write/read OK ({ms}ms)"))
else:
results.append(_ko("T11", f"/tmp read mismatch ({ms}ms)", content[:40]))
except Exception as e:
ms = int((time.monotonic() - t) * 1000)
results.append(_ko("T11", f"/tmp filesystem FALLITO ({ms}ms)", str(e)[:120]))
# ββ T12: asyncio concurrency β 5 task paralleli βββββββββββββββββββββββββββ
t = time.monotonic()
try:
async def _noop(i: int) -> int:
await asyncio.sleep(0.01)
return i * 2
outcomes = await asyncio.gather(*[_noop(i) for i in range(5)])
ms = int((time.monotonic() - t) * 1000)
if outcomes == [0, 2, 4, 6, 8]:
results.append(_ok("T12", f"asyncio concurrency 5x tasks OK ({ms}ms)"))
else:
results.append(_ko("T12", f"asyncio concurrency output inatteso ({ms}ms)", str(outcomes)))
except Exception as e:
ms = int((time.monotonic() - t) * 1000)
results.append(_ko("T12", f"asyncio concurrency FALLITA ({ms}ms)", str(e)[:120]))
# ββ T13: Memory manager import ββββββββββββββββββββββββββββββββββββββββββββ
t = time.monotonic()
try:
from memory.manager import MemoryManager
ms = int((time.monotonic() - t) * 1000)
results.append(_ok("T13", f"MemoryManager import OK ({ms}ms)"))
except Exception as e:
ms = int((time.monotonic() - t) * 1000)
results.append(_wn("T13", f"MemoryManager import WARN ({ms}ms)", str(e)[:80]))
# ββ T14: Role.FAST _run_fast_path β verifica wiring nel codice sorgente ββ
try:
import inspect
from agents.unified_loop import UnifiedAgentLoop
src = inspect.getsource(UnifiedAgentLoop)
has_fast_llm = "_fast_llm" in src
has_get_fast = "_get_fast_llm" in src
has_fast_client = "_fast_client" in src
if has_fast_llm and has_get_fast and has_fast_client:
results.append(_ok("T14", "Role.FAST wiring completo in UnifiedAgentLoop",
"_fast_llm + _get_fast_llm() + _fast_client.chat()"))
else:
missing = [k for k, v in [("_fast_llm", has_fast_llm), ("_get_fast_llm", has_get_fast), ("_fast_client", has_fast_client)] if not v]
results.append(_ko("T14", "Role.FAST wiring incompleto", f"mancanti: {missing}"))
except Exception as e:
results.append(_ko("T14", "Role.FAST wiring check FALLITO", str(e)[:120]))
# ββ T15: Python deps critici importabili ββββββββββββββββββββββββββββββββββ
critical_deps = ["fastapi", "pydantic", "httpx", "aiohttp", "groq", "google.generativeai"]
dep_ok, dep_ko = [], []
for dep in critical_deps:
try:
importlib.import_module(dep)
dep_ok.append(dep)
except ImportError:
dep_ko.append(dep)
if not dep_ko:
results.append(_ok("T15", f"Deps critici: tutti {len(dep_ok)} importabili", ", ".join(dep_ok)))
elif len(dep_ko) <= 2:
results.append(_wn("T15", f"Deps critici: {len(dep_ko)} mancanti", f"ko={dep_ko}"))
else:
results.append(_ko("T15", f"Deps critici: {len(dep_ko)}/{len(critical_deps)} mancanti", f"ko={dep_ko}"))
# ββ T16: Groq FAST path β latenza client init ββββββββββββββββββββββββββββ
t = time.monotonic()
groq_key = os.getenv("GROQ_API_KEY", "")
if groq_key:
try:
from models.role_router import RoleRouter, Role
client = RoleRouter.get_client(Role.FAST)
ms = int((time.monotonic() - t) * 1000)
results.append(_ok("T16", f"Groq FAST client init ({ms}ms)",
getattr(client, "provider_name", type(client).__name__)))
except Exception as e:
ms = int((time.monotonic() - t) * 1000)
results.append(_ko("T16", f"Groq FAST client FALLITO ({ms}ms)", str(e)[:120]))
else:
results.append(_wn("T16", "GROQ_API_KEY non configurata β Role.FAST userΓ self.llm come fallback"))
# ββ T17: Latenza totale benchmark βββββββββββββββββββββββββββββββββββββββββ
total_ms = int((time.monotonic() - t_global) * 1000)
results.append(_ok("T17", f"Benchmark completato in {total_ms}ms",
f"{'OK' if total_ms < 5000 else 'SLOW'} (target <5s)"))
return results
@router.get("/api/debug/benchmark")
async def run_benchmark(
token: str = Query(..., description="Daily HMAC token β vedi docstring modulo"),
pretty: bool = Query(False, description="Output human-readable invece di JSON compatto"),
):
"""S-BENCH: Self-test server-side completo β zero dipendenza da Replit.
Auth: HMAC-SHA256 daily token (seed fisso in codice, non Γ¨ un secret).
Calcola il token del giorno con:
python3 -c "import hmac,hashlib,datetime; print(hmac.new(b'agente-ai-bench-2026', datetime.date.today().isoformat().encode(), hashlib.sha256).hexdigest())"
"""
expected = _daily_token()
if not hmac.compare_digest(token, expected):
raise HTTPException(
status_code=401,
detail={
"error": "Token non valido o scaduto (cambia ogni giorno).",
"hint": "python3 -c \"import hmac,hashlib,datetime; "
"print(hmac.new(b'agente-ai-bench-2026', datetime.date.today().isoformat().encode(), hashlib.sha256).hexdigest())\"",
},
)
t0 = time.monotonic()
results = await _run_tests()
elapsed_ms = int((time.monotonic() - t0) * 1000)
pass_n = sum(1 for r in results if r["ok"])
fail_n = sum(1 for r in results if not r["ok"] and not r["warn"])
warn_n = sum(1 for r in results if r["warn"])
total_n = len(results)
score = round(pass_n / max(1, pass_n + fail_n) * 100)
gaps = [r for r in results if not r["ok"] and not r["warn"]]
payload = {
"ok": fail_n == 0,
"score": score,
"pass": pass_n,
"fail": fail_n,
"warn": warn_n,
"total": total_n,
"duration_ms": elapsed_ms,
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
"tests": results,
"gaps": [{"id": r["id"], "desc": r["desc"], "note": r.get("note", "")} for r in gaps],
}
return JSONResponse(content=payload, status_code=200 if fail_n == 0 else 207)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# QUALITY BENCHMARK β misura miglioramenti LLM per sprint (S-BENCH-Q)
# POST /api/benchmark/quality/run β avvia background task, ritorna task_id
# GET /api/benchmark/quality/status/{id} β polling risultati
#
# Per ogni categoria agente (DA / ORCH / MC / REC):
# 1. Inietta la context rule via UnifiedLoopPrompts._pick_context_rules()
# 2. Chiama il LLM (ARCHITECT = openai/gpt-oss-120b) a temperatura 0.3
# 3. Valuta la risposta con checker regex (stessa logica di benchmark-extended.mjs)
# 4. Produce score 0-100 per categoria + media totale
#
# Auth: stesso token HMAC daily del /api/debug/benchmark.
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_QUALITY_RUNS: dict[str, dict] = {} # task_id β {status, results, β¦}
# ββ Definizione task benchmark qualitΓ βββββββββββββββββββββββββββββββββββββββββ
_QUALITY_TASKS = [
{
"id": "DA",
"category": "data_analysis",
"label": "Analisi Dati",
"goal_for_rules": "analisi dati vendite statistiche media picco anomalia trend",
"user_prompt": (
"Analizza questi dati di vendite mensili:\n"
"Gen=100, Feb=120, Mar=80, Apr=150, Mag=90, Giu=200.\n\n"
"Fornisci un'analisi strutturata con Media, Picco, Anomalia e Trend."
),
"criteria": [
{"id": "media", "label": "**Media: N**", "weight": 25},
{"id": "picco", "label": "**Picco: MESE**", "weight": 25},
{"id": "anomalia", "label": "**Anomalia: MESE**", "weight": 25},
{"id": "trend", "label": "**Trend: ...**", "weight": 25},
],
},
{
"id": "ORCH",
"category": "orchestration",
"label": "Orchestrazione",
"goal_for_rules": "implementa sistema backend typescript asincrono dipendenze sql async",
"user_prompt": (
"Implementa un sistema di notifiche email per e-commerce con:\n"
"- Invio email alla conferma ordine\n"
"- Retry automatico su failure (3 tentativi)\n"
"- Tracking stato consegna\n\n"
"TypeScript + Node.js. Mostra: piano β implementazione β dipendenze."
),
"criteria": [
{"id": "hasPlan", "label": "Piano / sezione strutturata", "weight": 25},
{"id": "hasCode", "label": "Codice TypeScript", "weight": 25},
{"id": "hasAsync", "label": "async/await o Promise", "weight": 25},
{"id": "hasDeps", "label": "Dipendenze elencate", "weight": 25},
],
},
{
"id": "MC",
"category": "memory_context",
"label": "Memory Context",
"goal_for_rules": "interface typescript endpoint apiresponse stack architettura libreria",
"user_prompt": (
"Implementa la route Express per GET /api/users/:id.\n"
"Usa il pattern ApiResponse<T> con i campi: success, data, error, requestId.\n"
"Rispetta le convenzioni dello stack del progetto (Drizzle, Zod, Express)."
),
"criteria": [
{"id": "hasApiResponse", "label": "ApiResponse<T> usato", "weight": 35},
{"id": "hasRequestId", "label": "requestId nel response", "weight": 35},
{"id": "hasStack", "label": "Stack reale (Drizzle/Zod/..)", "weight": 30},
],
},
{
"id": "REC",
"category": "recovery",
"label": "Recovery",
"goal_for_rules": "task ambiguo input mancante cosa fare rollback vincoli",
"user_prompt": (
"L'utente scrive solo: 'Fammi un'analisi'.\n"
"Non specifica cosa analizzare, non ha fornito dati.\n\n"
"Cosa fai? (non inventare dati, non procedere silenziosamente)"
),
"criteria": [
{"id": "asksDetails", "label": "Chiede chiarimenti", "weight": 40},
{"id": "listAssumptions", "label": "Lista assunzioni / ipotesi", "weight": 30},
{"id": "noHallucinate", "label": "Non inventa dati (anti-hallucination)", "weight": 30},
],
},
{
"id": "ROB",
"category": "robustness",
"label": "Robustness",
"goal_for_rules": "istruzioni diventano progressivamente meno specifiche gestisci l ambiguitΓ rendila ancora piΓΉ efficiente ordinamento typescript",
"user_prompt": (
"Implementa una funzione di ordinamento TypeScript efficiente.\n"
"Poi rendila ancora piΓΉ efficiente.\n"
"Ottimizzala per il caso d'uso tipico.\n"
"Assicurati che funzioni.\n\n"
"Nota: le istruzioni diventano progressivamente meno specifiche. "
"Gestisci l'ambiguitΓ in modo esplicito."
),
"criteria": [
{"id": "hasCode", "label": "Codice TypeScript (sort)", "weight": 25},
{"id": "handlesAmbiguity", "label": "Dichiara assunzioni esplicite", "weight": 25},
{"id": "hasSort", "label": "Usa sort/algorithm", "weight": 25},
{"id": "hasRationale", "label": "Motivazione / perchΓ©", "weight": 25},
],
},
]
def _score_quality_task(task: dict, response: str) -> dict:
"""Valuta risposta LLM su ogni criterio β ritorna score 0-100."""
r = response
tid = task["id"]
if tid == "DA":
checks = {
"media": bool(re.search(r'\*\*Media', r, re.IGNORECASE)),
"picco": bool(re.search(r'\*\*Picco', r, re.IGNORECASE)),
"anomalia": bool(re.search(r'\*\*Anomali', r, re.IGNORECASE)),
"trend": bool(re.search(r'\*\*(Trend|Andamento|Tendenz)', r, re.IGNORECASE)),
}
elif tid == "ORCH":
checks = {
"hasPlan": bool(re.search(r'(piano|step\s*\d|fase\s*\d|\d+\.\s+[A-Z])', r, re.IGNORECASE)),
"hasCode": bool(re.search(r'```(ts|typescript|javascript|js)', r, re.IGNORECASE)),
"hasAsync": bool(re.search(r'(async|await|Promise\.all)', r)),
"hasDeps": bool(re.search(r'(npm|pnpm|yarn|install|dependen|dipendenz|package\.json)', r, re.IGNORECASE)),
}
elif tid == "MC":
checks = {
"hasApiResponse": bool(re.search(r'ApiResponse', r)),
"hasRequestId": bool(re.search(r'requestId', r)),
"hasStack": bool(re.search(r'(Drizzle|Zod|Express|Prisma|knex)', r, re.IGNORECASE)),
}
elif tid == "REC":
checks = {
"asksDetails": bool(re.search(
r'(\?|qual[ei]|cosa intendi|chiar|specificar|dettagl|di\s+pi)', r, re.IGNORECASE)),
"listAssumptions": bool(re.search(
r'(assumo|ipotesi|assunzion|potrebbe essere|se intendi|per esempio|ad esempio'
r'|se si tratta|che tipo|quale tipo|se vuole|potrei fare|opzione [ab]'
r'|se intende|potrebbe trattarsi|quale delle|in base a cosa)', r, re.IGNORECASE)),
# pass se NON inventa dati concreti senza chiedere
"noHallucinate": not bool(re.search(
r'(ecco l.analisi|ecco i dati|i dati mostrano|risultati:|media:\s*\d)', r, re.IGNORECASE)),
}
elif tid == "ROB":
import re as _re
code_blocks = _re.findall(r"```(?:typescript|ts)[\s\S]*?```", r, _re.IGNORECASE)
code_txt = " ".join(code_blocks)
checks = {
"hasCode": bool(code_blocks) and len(code_txt) > 50,
"handlesAmbiguity": bool(_re.search(r"assumo|ipotizzo|ambiguo|caso tipico|interpretto", r, _re.IGNORECASE)),
"hasSort": bool(_re.search(r"sort|quicksort|mergesort|compareFn|algorithm", r, _re.IGNORECASE)),
"hasRationale": bool(_re.search(r"perchΓ©|motivazione|scelta|rationale|perche", r, _re.IGNORECASE)),
}
else:
checks = {}
scored = [{**c, "pass": checks.get(c["id"], False)} for c in task["criteria"]]
score = sum(c["weight"] for c in scored if c["pass"])
return {
"id": task["id"],
"category": task["category"],
"label": task["label"],
"score": score,
"criteria": scored,
"response_preview": r[:400] + ("\u2026" if len(r) > 400 else ""),
}
async def _run_quality_benchmark(task_id: str) -> None:
"""Background task: 4 chiamate LLM in parallelo β scorecard qualitΓ ."""
_QUALITY_RUNS[task_id]["status"] = "running"
results: list[dict] = []
errors: list[dict] = []
try:
from models.role_router import RoleRouter, Role # noqa: PLC0415
from agents.unified_loop_prompts import PromptBuilderMixin # noqa: PLC0415
# System prompt leggero per benchmark: NO tool definitions (evita tool-call da FAST)
# Le context rules iniettano le istruzioni specifiche per categoria
prompts_obj = PromptBuilderMixin()
_BENCH_SYS = (
"Sei un assistente AI specializzato in sviluppo software e analisi dati. "
"Rispondi in italiano usando markdown. "
"Usa blocchi di codice ```typescript``` / ```javascript``` per il codice. "
"NON usare tool calls o function calls β rispondi sempre con testo e codice."
)
# Provider chain: ARCHITECT prima (qualitΓ ), poi fallback per 402/depleted
_PROVIDER_CHAIN = ["ARCHITECT", "REASONER", "CODER", "FAST"]
async def _run_one_task(msgs: list[dict]) -> str:
"""Retry indipendente per task β ARCHITECT first, FAST come ultimo resort."""
last_exc: Exception | None = None
for _rname in _PROVIDER_CHAIN:
_r = getattr(Role, _rname, None)
if _r is None:
continue
try:
_c = RoleRouter.get_client(_r)
result = await asyncio.wait_for(
_c.chat(msgs, temperature=0.3, max_tokens=1200),
timeout=35.0,
)
return str(result)
except Exception as _exc:
last_exc = _exc
_exc_s = str(_exc).lower()
# 402 / depleted / rate-limit β prova il prossimo provider
if any(k in _exc_s for k in ["402", "depleted", "rate limit", "too many", "429"]):
continue
raise # errore non-recuperabile β propaga subito
raise last_exc or Exception("Nessun provider disponibile")
# Esecuzione sequenziale β evita rate-limit da 5 richieste simultanee allo stesso provider
# Latenza: ~30-50s (5 Γ ~7-10s) vs 402 su tutto con parallelo
results_map: list[str | Exception] = []
for task in _QUALITY_TASKS:
ctx_rules = prompts_obj._pick_context_rules(task["goal_for_rules"])
system = _BENCH_SYS + (("\n\n" + ctx_rules) if ctx_rules else "")
msgs = [
{"role": "system", "content": system},
{"role": "user", "content": task["user_prompt"]},
]
try:
resp = await _run_one_task(msgs)
results_map.append(resp)
except Exception as _exc:
results_map.append(_exc)
responses = results_map
for task, response in zip(_QUALITY_TASKS, responses):
if isinstance(response, Exception):
err_msg = str(response)[:150]
errors.append({"id": task["id"], "error": err_msg})
results.append({
"id": task["id"], "category": task["category"],
"label": task["label"], "score": 0, "criteria": [], "error": err_msg,
})
else:
results.append(_score_quality_task(task, str(response)))
except Exception as exc:
errors.append({"global": str(exc)[:250]})
passed = [r for r in results if not r.get("error")]
avg_score = round(sum(r["score"] for r in passed) / max(1, len(passed)))
_QUALITY_RUNS[task_id].update({
"status": "done",
"results": results,
"errors": errors,
"total_score": avg_score,
"categories_run": len(results),
"finished_at": datetime.datetime.utcnow().isoformat() + "Z",
})
@router.post("/api/benchmark/quality/run")
async def start_quality_benchmark(
background_tasks: BackgroundTasks,
token: str = Query(..., description="Daily HMAC token β stesso del /api/debug/benchmark"),
):
"""Avvia il benchmark qualitΓ LLM in background (S-BENCH-Q).
Testa 4 categorie agentiche iniettando le context rules del sprint corrente,
chiama il LLM e valuta la risposta con checker regex.
Ritorna task_id per polling: GET /api/benchmark/quality/status/{task_id}
Calcola il token del giorno con:
python3 -c "import hmac,hashlib,datetime; \\
print(hmac.new(b'agente-ai-bench-2026', \\
datetime.date.today().isoformat().encode(), \\
hashlib.sha256).hexdigest())"
"""
expected = _daily_token()
if not hmac.compare_digest(token, expected):
raise HTTPException(
status_code=401,
detail={
"error": "Token non valido o scaduto (cambia ogni giorno).",
"hint": ("python3 -c \"import hmac,hashlib,datetime; "
"print(hmac.new(b'agente-ai-bench-2026',"
"datetime.date.today().isoformat().encode(),"
"hashlib.sha256).hexdigest())\""),
},
)
task_id = uuid.uuid4().hex[:12]
now_iso = datetime.datetime.utcnow().isoformat() + "Z"
_QUALITY_RUNS[task_id] = {
"status": "queued",
"started_at": now_iso,
"results": [],
"errors": [],
"total_score": None,
"categories_run": 0,
}
background_tasks.add_task(_run_quality_benchmark, task_id)
return JSONResponse({
"task_id": task_id,
"status": "queued",
"poll_url": f"/api/benchmark/quality/status/{task_id}",
"categories": [f"{t['id']} ({t['label']})" for t in _QUALITY_TASKS],
"started_at": now_iso,
}, status_code=202)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# RUN-SELF β il bot esegue il proprio benchmark in autonomia (S-BENCH-SELF)
# POST /api/benchmark/run-self β auth: X-Internal-Token header
#
# Esegue il quality benchmark su tutte le categorie + ROB e ritorna i risultati
# direttamente (sincrono, ~20-30s). Il bot puΓ² chiamare questo endpoint
# autonomamente senza Replit, senza token HMAC, senza configurazione esterna.
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@router.post("/api/benchmark/run-self")
async def run_self_benchmark(request: "Request"):
"""Self-benchmark: il bot misura le proprie performance in autonomia.
Auth: X-Internal-Token header (stesso usato per /api/agent/run-stream).
Nessun token HMAC, nessuna dipendenza da Replit.
Esegue il quality benchmark su tutte le categorie (DA, ORCH, MC, REC, ROB)
e ritorna lo scorecard completo.
Esempio:
curl -X POST <hf-space-a-url>/api/benchmark/run-self \
-H "X-Internal-Token: <token>"
"""
import os as _os
from fastapi import Request as _Req
itok_conf = _os.getenv("INTERNAL_TOKEN", "")
itok_recv = request.headers.get("X-Internal-Token", "")
if not itok_conf or not itok_recv or itok_recv != itok_conf:
from fastapi import HTTPException as _HTTP
raise _HTTP(status_code=401, detail="X-Internal-Token non valido o mancante.")
t0 = __import__("time").monotonic()
task_id = __import__("uuid").uuid4().hex[:12]
_QUALITY_RUNS[task_id] = {
"status": "running", "started_at": datetime.datetime.utcnow().isoformat() + "Z",
"results": [], "errors": [], "total_score": None, "categories_run": 0,
}
await _run_quality_benchmark(task_id)
elapsed_ms = int((__import__("time").monotonic() - t0) * 1000)
run = _QUALITY_RUNS[task_id]
results = run.get("results", [])
passed = [r for r in results if not r.get("error")]
avg = round(sum(r["score"] for r in passed) / max(1, len(passed)))
return JSONResponse({
"ok": len(run.get("errors", [])) == 0,
"total_score": avg,
"categories_run": len(results),
"duration_ms": elapsed_ms,
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
"results": results,
"errors": run.get("errors", []),
"gaps": [
{"id": r["id"], "label": r["label"], "score": r["score"],
"failed_criteria": [c["label"] for c in r.get("criteria", []) if not c.get("pass")]}
for r in results if r["score"] < 75
],
})
@router.get("/api/benchmark/quality/status/{task_id}")
async def quality_benchmark_status(task_id: str):
"""Polling sullo stato del benchmark qualitΓ .
Lifecycle: queued β running β done
Response fields:
status: queued | running | done
total_score: 0-100 (media delle categorie senza errori)
results: per-task score + criteri + preview risposta
errors: errori LLM per task (score=0 se presente)
categories_run: quante categorie hanno prodotto un risultato
"""
run = _QUALITY_RUNS.get(task_id)
if run is None:
raise HTTPException(
status_code=404,
detail=f"Task '{task_id}' non trovato. Avvia prima POST /api/benchmark/quality/run",
)
return JSONResponse(run)
|