vortex-omega-final / core /continuous_benchmark.py
RDS777's picture
Upload 17 files
52b0da0 verified
Raw
History Blame Contribute Delete
11.4 kB
"""
benchmark/continuous_benchmark.py — VORTEX GOD v1.2
Benchmark continu avec détection de régression.
Tourne en thread daemon et :
- Évalue le système toutes les N heures sur un suite de tâches fixes
- Détecte les régressions vs le meilleur score historique
- Enregistre les résultats dans benchmark_history.jsonl
- Expose une API simple pour l'onglet Gradio
"""
from __future__ import annotations
import asyncio
import json
import logging
import os
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
log = logging.getLogger("vortex.benchmark")
DATA_DIR = Path(os.environ.get("DATA_DIR", "/app/data"))
BENCH_LOG = DATA_DIR / "benchmark_history.jsonl"
BENCH_INTERVAL = float(os.environ.get("BENCH_INTERVAL_HOURS", "6.0"))
REGRESSION_THR = float(os.environ.get("REGRESSION_THRESHOLD", "0.05")) # -5% = régression
@dataclass
class BenchmarkRun:
run_id: str
ts: float = field(default_factory=time.time)
scores: Dict[str, float] = field(default_factory=dict)
global_score: float = 0.0
duration_s: float = 0.0
regression: bool = False
regression_details: List[str] = field(default_factory=list)
def to_dict(self) -> Dict:
return {
"run_id": self.run_id,
"ts": self.ts,
"scores": self.scores,
"global_score": round(self.global_score, 4),
"duration_s": round(self.duration_s, 1),
"regression": self.regression,
"regression_details": self.regression_details,
}
# ─────────────────────────────────────────────
# Suite de tâches benchmark (déterministes)
# ─────────────────────────────────────────────
BENCHMARK_SUITE = {
"engineer": [
{
"user": "Écris une fonction Python `fibonacci(n)` qui retourne le n-ième nombre de Fibonacci.",
"checks": [
lambda r: "def fibonacci" in r,
lambda r: "return" in r,
lambda r: any(w in r for w in ["n-1", "n - 1", "n-2", "n - 2", "memo", "cache"]),
],
},
{
"user": "Écris une fonction `is_palindrome(s)` qui vérifie si une chaîne est un palindrome.",
"checks": [
lambda r: "def is_palindrome" in r,
lambda r: "return" in r,
lambda r: any(w in r for w in ["reverse", "[::-1]", "lower", "==", "!="]),
],
},
{
"user": "Écris un générateur Python qui yield les nombres premiers jusqu'à N.",
"checks": [
lambda r: "def " in r,
lambda r: "yield" in r,
lambda r: any(w in r for w in ["prime", "premier", "divisible", "sqrt", "%"]),
],
},
],
"planner": [
{
"user": "Planifie le déploiement d'une application Flask sur un serveur Ubuntu.",
"checks": [
lambda r: any(w in r.lower() for w in ["étape", "step", "install", "nginx", "gunicorn", "systemd"]),
lambda r: len(r) > 100,
],
},
{
"user": "Planifie la mise en place d'un pipeline de données ETL.",
"checks": [
lambda r: any(w in r.lower() for w in ["extract", "transform", "load", "source", "étape"]),
lambda r: len(r) > 80,
],
},
],
"critic": [
{
"user": "Audite : import pickle; data = pickle.loads(user_input)",
"checks": [
lambda r: any(w in r.lower() for w in ["pickle", "dangereux", "dangerous", "injection", "unsafe"]),
],
},
{
"user": "Audite : cursor.execute('DELETE FROM users WHERE id=' + user_id)",
"checks": [
lambda r: any(w in r.lower() for w in ["injection", "sql", "paramètre", "parameter", "dangereux"]),
],
},
],
"researcher": [
{
"user": "Qu'est-ce que le RAG (Retrieval-Augmented Generation) ?",
"checks": [
lambda r: any(w in r.lower() for w in ["retrieval", "récupération", "vectoriel", "contexte", "llm"]),
lambda r: len(r) > 100,
],
},
],
"optimizer": [
{
"user": "Optimise : for i in range(len(lst)): if lst[i] in target: result.append(lst[i])",
"checks": [
lambda r: any(w in r.lower() for w in ["set", "intersection", "comprehension", "filter", "o(1)"]),
],
},
],
}
AGENT_SYSTEMS = {
"engineer": "Tu es un ingénieur Python. Génère uniquement du code Python valide.",
"planner": "Tu es un planificateur. Décris les étapes principales en 5 lignes max.",
"critic": "Tu es un expert sécurité. Identifie les problèmes critiques.",
"researcher": "Tu es un chercheur. Explique en 3-5 phrases.",
"optimizer": "Tu es un optimiseur Python. Propose une version améliorée.",
}
class ContinuousBenchmark:
"""
Benchmark continu qui évalue le système en appelant directement le LLM engine.
Ne dépend pas des agents spécialisés (pour isoler les régressions du LLM seul).
"""
def __init__(self, llm_engine):
self._engine = llm_engine
self.history: List[BenchmarkRun] = self._load_history()
self._best: float = max((r.global_score for r in self.history), default=0.0)
self._running: bool = False
def _load_history(self) -> List[BenchmarkRun]:
runs = []
if BENCH_LOG.exists():
with open(BENCH_LOG) as f:
for line in f:
try:
d = json.loads(line)
runs.append(BenchmarkRun(
run_id = d["run_id"],
ts = d["ts"],
scores = d["scores"],
global_score = d["global_score"],
duration_s = d["duration_s"],
regression = d.get("regression", False),
))
except Exception:
pass
return runs
# ── Évaluation ───────────────────────────────────────────────────────────
async def _eval_task(self, task_type: str, task: Dict) -> float:
"""Évalue une tâche individuelle. Retourne 0.0–1.0."""
system = AGENT_SYSTEMS.get(task_type, "Tu es un assistant.")
try:
resp = await self._engine.call(
agent = None,
system = system,
user = task["user"],
max_tokens = 400,
temperature = 0.1,
use_cache = False,
)
content = resp.content
checks = task.get("checks", [])
if not checks:
return 0.5
passed = sum(1 for c in checks if c(content))
return round(passed / len(checks), 3)
except Exception as exc:
log.debug(f"[Bench] Tâche {task_type} échouée : {exc}")
return 0.0
async def run_once(self) -> BenchmarkRun:
"""Lance un passage complet du benchmark. Retourne le BenchmarkRun."""
import uuid
run = BenchmarkRun(run_id=str(uuid.uuid4())[:8])
t0 = time.time()
scores = {}
regs = []
for task_type, tasks in BENCHMARK_SUITE.items():
task_scores = await asyncio.gather(
*[self._eval_task(task_type, t) for t in tasks],
return_exceptions=True,
)
valid = [s for s in task_scores if isinstance(s, float)]
avg = round(sum(valid) / len(valid), 4) if valid else 0.0
scores[task_type] = avg
# Détecter régression par rapport au meilleur historique
best_for_type = max(
(r.scores.get(task_type, 0) for r in self.history),
default=0.0,
)
if best_for_type > 0.1 and avg < best_for_type - REGRESSION_THR:
regs.append(f"{task_type}: {avg:.3f} vs best {best_for_type:.3f} (Δ={avg-best_for_type:+.3f})")
run.scores = scores
run.global_score = round(sum(scores.values()) / len(scores), 4) if scores else 0.0
run.duration_s = time.time() - t0
run.regression = bool(regs)
run.regression_details = regs
if run.regression:
log.warning(f"[Bench] ⚠️ RÉGRESSION détectée : {regs}")
else:
log.info(f"[Bench] Score global : {run.global_score:.4f} (best={self._best:.4f})")
if run.global_score > self._best:
self._best = run.global_score
self.history.append(run)
self._save_run(run)
return run
def _save_run(self, run: BenchmarkRun):
DATA_DIR.mkdir(parents=True, exist_ok=True)
with open(BENCH_LOG, "a") as f:
f.write(json.dumps(run.to_dict(), ensure_ascii=False) + "\n")
# ── Boucle daemon ────────────────────────────────────────────────────────
async def run_forever(self, interval_hours: float = BENCH_INTERVAL):
self._running = True
log.info(f"[Bench] Benchmark continu démarré (intervalle={interval_hours}h)")
# Premier run immédiat au démarrage
await asyncio.sleep(60)
while self._running:
try:
await self.run_once()
except Exception as exc:
log.error(f"[Bench] Erreur : {exc}")
await asyncio.sleep(interval_hours * 3600)
def stop(self):
self._running = False
# ── API pour Gradio ───────────────────────────────────────────────────────
def get_dashboard(self) -> Dict[str, Any]:
"""Données pour l'onglet Gradio."""
if not self.history:
return {"status": "Aucun benchmark exécuté", "history": []}
last = self.history[-1]
trend = []
for r in self.history[-20:]:
trend.append({"ts": r.ts, "score": r.global_score, "regression": r.regression})
return {
"last_run": {
"run_id": last.run_id,
"global_score": last.global_score,
"scores": last.scores,
"duration_s": last.duration_s,
"regression": last.regression,
"regression_details": last.regression_details,
},
"best_score": self._best,
"total_runs": len(self.history),
"regressions": sum(1 for r in self.history if r.regression),
"trend": trend,
}