RDS777's picture
Upload 48 files
380a7fc verified
Raw History Blame Contribute Delete
7.45 kB
"""Test-Time Compute pour du code : best-of-N, arbre de réparations (beam) et MCTS, avec pytest comme juge exact.
Dans un MCTS classique, la valeur d'une feuille s'estime par des simulations. Ici, chaque nœud est un programme
complet que pytest note exactement (part des tests réussis) : pas de simulation, l'évaluation EST le jugement.
"""
from __future__ import annotations
import hashlib
import json
import math
from dataclasses import dataclass, field
from typing import Optional
from mouh.verification.generators import parse_files
from ..contracts import Candidate
def signature(files: dict) -> str:
return hashlib.sha256(json.dumps(files, sort_keys=True).encode()).hexdigest()
def _files(text: str) -> dict:
return {k: v for k, v in parse_files(text).items() if not k.startswith("test_")}
class _Search:
"""Comptabilité commune : budget de générations, déduplication, notation."""
def __init__(self, backend, verifier, tests: dict, budget: int) -> None:
self.backend, self.verifier, self.tests, self.budget = backend, verifier, tests, budget
self.spent = 0
self.seen: dict[str, Candidate] = {}
@property
def done(self) -> bool:
return self.spent >= self.budget or any(c.ok for c in self.seen.values())
def expand(self, messages, n: int, depth: int, origin: str, greedy_first: bool = True) -> list[Candidate]:
n = max(0, min(n, self.budget - self.spent))
if n == 0:
return []
out = []
for text in self.backend.complete(messages, n, greedy_first=greedy_first):
self.spent += 1
files = _files(text)
sig = signature(files) if files else ""
if files and sig not in self.seen:
c = self.verifier.score(files, self.tests)
c.origin, c.depth = origin, depth
self.seen[sig] = c
out.append(c)
return out
def ranked(self) -> list[Candidate]:
return sorted(self.seen.values(), key=Candidate.key, reverse=True)
class BestOfN:
"""Une génération gloutonne + des échantillons ; la référence à battre, à budget égal."""
name = "best_of_n"
def solve(self, request, plan, backend, verifier, build) -> list[Candidate]:
tests = dict(request.task.test_files)
if not tests: # rien pour juger : une seule proposition, comme la v3.8
files = _files(backend.complete(build(), 1)[0])
return [Candidate(files, "racine")] if files else []
s = _Search(backend, verifier, tests, plan.budget)
s.expand(build(), plan.budget, 0, "racine")
return s.ranked()
class RepairBeam:
"""Arbre de réparations en faisceau : à chaque niveau, les `width` meilleurs sont réparés avec leur trace."""
name = "beam"
def __init__(self, width: int = 2, depth: int = 3) -> None:
self.width, self.depth = width, depth
def solve(self, request, plan, backend, verifier, build) -> list[Candidate]:
tests = dict(request.task.test_files)
if not tests:
return BestOfN().solve(request, plan, backend, verifier, build)
s = _Search(backend, verifier, tests, plan.budget)
frontier = s.expand(build(), max(1, plan.budget // 2), 0, "racine")
for d in range(1, self.depth + 1):
if s.done or not frontier:
break
parents = sorted(frontier, key=Candidate.key, reverse=True)[: self.width]
frontier = []
for p in parents:
frontier += s.expand(build(previous=p.files, diagnostics=[p.output]), 1, d, "reparation")
return s.ranked()
@dataclass
class _Node:
cand: Optional[Candidate] # None pour la racine (l'énoncé seul)
parent: Optional["_Node"] = None
children: list = field(default_factory=list)
visits: int = 0
total: float = 0.0
expansions: int = 0 # générations déjà tirées depuis ce nœud
@property
def q(self) -> float:
return self.total / self.visits if self.visits else 0.0
class RepairMCTS:
"""MCTS (sélection UCT) sur l'arbre des réparations.
- Sélection : parmi la racine (nouvelle proposition depuis l'énoncé) et tous les candidats déjà notés,
on choisit celui qui maximise Q + c * sqrt(ln N / (1 + n)).
- Expansion : la racine produit un nouvel échantillon ; un candidat est réparé avec sa propre sortie pytest
(1re réparation gloutonne, les suivantes échantillonnées).
- Évaluation : pytest, exact (part des tests réussis).
- Rétropropagation : la valeur remonte jusqu'à la racine.
Arrêt au premier candidat qui passe tous les tests ou quand le budget est dépensé.
"""
name = "mcts"
def __init__(self, c: float = 1.0, root_samples: int = 2, max_children: int = 3) -> None:
self.c, self.root_samples, self.max_children = c, root_samples, max_children
def solve(self, request, plan, backend, verifier, build) -> list[Candidate]:
tests = dict(request.task.test_files)
if not tests:
return BestOfN().solve(request, plan, backend, verifier, build)
s = _Search(backend, verifier, tests, plan.budget)
root = _Node(None)
nodes: list[_Node] = []
def attach(parent: _Node, cands: list[Candidate]) -> None:
for c in cands:
node = _Node(c, parent)
parent.children.append(node)
nodes.append(node)
v, cur = c.value, node
while cur is not None:
cur.visits += 1
cur.total += v
cur = cur.parent
first = s.expand(build(), min(self.root_samples, plan.budget), 0, "racine")
root.expansions += min(self.root_samples, plan.budget)
attach(root, first)
while not s.done:
pool = [root] + [n for n in nodes if n.expansions < self.max_children and not n.cand.ok]
total = max(1, root.visits)
def uct(n: _Node) -> float: # Q = moyenne du sous-arbre (le nœud compris)
return n.q + self.c * math.sqrt(math.log(total + 1) / (1 + n.expansions))
target = max(pool, key=uct)
if target is root:
found = s.expand(build(), 1, 0, "racine", greedy_first=False)
else:
found = s.expand(build(previous=target.cand.files, diagnostics=[target.cand.output]), 1,
target.cand.depth + 1, "reparation", greedy_first=target.expansions == 0)
target.expansions += 1
if found:
attach(target, found)
else: # doublon ou sortie illisible : la visite compte quand même, pour ne pas reboucler
cur = target
while cur is not None:
cur.visits += 1
cur = cur.parent
return s.ranked()
class AdaptiveStrategy:
"""Choisit la stratégie selon le plan du routeur : le raisonnement hybride."""
name = "adaptatif"
def __init__(self) -> None:
self.by_mode = {"direct": BestOfN(), "arbre": RepairBeam(), "mcts": RepairMCTS()}
def solve(self, request, plan, backend, verifier, build) -> list[Candidate]:
return self.by_mode.get(plan.mode, self.by_mode["direct"]).solve(request, plan, backend, verifier, build)