Spaces:
Build error
Build error
| """ | |
| 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 | |
| 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, | |
| } | |