Terminal / api /benchmark.py
Pulka
sync: 2 changed, 0 deleted — eacb561c (2026-06-22 14:19)
3c42bb7 verified
Raw
History Blame
37.3 kB
"""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 'https://arjanit98-terminal.hf.space/api/debug/benchmark?token=<tok>'
"""
import os, sys, asyncio, time, hmac, hashlib, datetime, importlib, tempfile, subprocess, re, uuid
from fastapi import APIRouter, BackgroundTasks, HTTPException, Query, Request
from fastapi.responses import JSONResponse
router = APIRouter()
# ── 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 = llama-3.3-70b-versatile) 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 https://arjanit98-terminal.hf.space/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)