* llama-eval : add sandboxed execution for code suites Adds a lightweight replacement for the Docker containers usually used to run generated code, so a benchmark can be executed without a daemon, an image or root. A cached venv is provisioned per suite and reused; candidate programs run in a throwaway working directory as their own process group, under the strongest isolation the host offers: bwrap read-only root, tmpfs /tmp and $HOME, no network unshare no network, via --map-current-user rlimit cpu/memory/file-size/process caps only Candidate code must not run as uid 0, which is why "unshare -r" is a last-resort tier that warns: root bypasses file permission bits, so a test asserting that a write to a chmod 0444 file fails would instead see it succeed. Two details worth noting for the bwrap tier. Only paths hidden by the $HOME tmpfs are bound back, because bwrap cannot mount onto a symlink and version-manager alias directories usually are one. And the venv's base interpreter tree is resolved by walking the symlink chain, since a version-managed interpreter is commonly reached through an unversioned alias that startup resolves through. Suites can declare provisioning steps, which run once at build time while the network is still up, so test time never needs it. Assisted-by: Claude Opus 4.8 <noreply@anthropic.com> * llama-eval : add HumanEval and ClassEval code suites Adds two execution-graded suites alongside the existing text-graded ones. The model's reply is parsed for a python code block, combined with the task's own unit tests, and run in the sandbox. --dataset humaneval 164 function-level problems, scored pass@k --dataset classeval 100 class-level tasks, scored at class level (feeding pass@k) and method level, matching upstream pass@k reuses the existing chunk mechanism: --n_cases at a multiple of the dataset size gives that many samples per problem, aggregated with the unbiased estimator from the Codex paper. Program construction and result interpretation are dataset-owned hooks, because the two suites differ: one HumanEval problem is a function judged by a single assert, whereas one ClassEval task is a class judged by many independent test methods, and reducing that to pass/fail would throw away partial credit. Suites also declare their own package set, provisioning steps, interpreter and timeout. The package set is derived from the dataset's own imports rather than a requirements file. HumanEval turns out to need no third-party packages at all; ClassEval needs thirteen, two of which must be declared because no import mentions them -- lxml is selected by string in ClassEval_44, and scipy is a gensim requirement. Notes on ClassEval specifically: * gensim publishes no wheel past cp313, so 3.13 is preferred; on 3.14+ the two pure-Python APIs it uses are provided by a small shim instead. * The three nltk corpora it needs are pre-seeded at venv build time. These are the modern names -- the dataset asks for the since-renamed punkt and averaged_perceptron_tagger. * It defaults to a patched fork, ilintar/ClassEval, because three upstream tests cannot be passed by anything: a hard-coded 2024 date checked against datetime.now(), an assertion on the dataset author's own hostname, and an exact float comparison. Reference solutions go from 96/100 to 100/100 classes and 2182/2196 to 2196/2196 test methods. No skeleton is touched, so what the model is asked to write is unchanged; --dataset-source selects the original. --exec-seed (default 0) seeds the RNG before each test, so suites whose tests draw random inputs score reproducibly. ClassEval_58 fails its own reference solution about 30% of the time without it. The simulator gains both suites, so the harness can be exercised without a model. Assisted-by: Claude Opus 4.8 <noreply@anthropic.com> * llama-eval : fix silent isolation downgrade, generalise the sandbox runner The isolation probe ran the interpreter without binding it back, so whenever this tool is itself started from a venv under $HOME -- the usual case, since it needs `datasets` -- bwrap's tmpfs hid the interpreter, the probe failed, and the sandbox quietly fell back to a tier that does not hide $HOME at all. Nothing in the output looked wrong when that happened, and the existing suites have been running that way. The probe now uses the same interpreter and the same binds the suite will actually get. Two additions the agentic suite needs, neither of which changes the existing suites: * run_argv() runs an arbitrary command in a caller-owned directory that survives the call. run() is now a thin wrapper over it, so HumanEval and ClassEval take exactly the same path as before. A multi-step agent needs its edits to persist across many invocations, and a suite for a language other than Python needs to drive a linter and a test runner rather than a single source string. * RLIMIT_AS is now per-sandbox, defaulting to what it was. It caps reserved address space rather than resident memory, which is the wrong instrument for a runtime that reserves a large region and commits little of it: node's TypeScript type-stripping goes through WebAssembly, reserves gigabytes per instance, and aborts at 4 GiB with an out-of-memory error that reads like a fault in the code under test. HumanEval stays at 164/164 and ClassEval at 100/100. Assisted-by: Claude Opus 5 <noreply@anthropic.com> * llama-eval : add SACB, a multi-turn agentic coding suite `--dataset agentic` is a different shape from the other code suites. Rather than answering a single prompt, the model is given a small but real repository, a narrow tool set and a bug report, drives its own multi-turn conversation, and is graded by running a test suite it was never allowed to see. The corpus is SACB (Simple Agent Coding Benchmark), 60 tasks over three hand-written repositories in Python and TypeScript, hosted at ilintar/SACB. Grading needs no container: a cached venv gives ruff and mypy for Python, and node's own test runner plus tsc for TypeScript, so the whole dependency set is four packages. Three constraints are deliberate: * No shell, and no way to run the tests. The only feedback channel is `lint`. A syntax or type error can be driven out mechanically; a logic error has to be reasoned about. A harness that lets an agent iterate against the test suite measures something closer to search than to understanding. * Two edit tools, one line-addressed and one content-addressed. Which one a model reaches for, and whether it keeps line numbers straight after its own earlier edit, is itself a signal. * The tests never touch the working tree. They are held outside it and copied into a throwaway copy only once the agent has stopped, so they can be neither read nor edited. Every tool path is resolved and confinement-checked against the repository root, so a symlink planted inside the tree cannot reach out of it. A task counts as resolved when every test that was failing passes and nothing that already passed broke. Both sets are derived by running the tests against the defective tree and against the reference fix, never written by hand, so a task whose defect no test exercises cannot enter the corpus. Many tasks carry several independent defects, so the fraction of target tests passed is reported alongside, separating "fixed two of the three faults" from "changed nothing". An episode that arrives with no tool call and no `finish` is nudged rather than ended. Models routinely narrate their analysis mid-task, and treating that as completion ends the episode with no edits and scores a reasoning failure that never happened; about one episode in four exits that way. Nothing here is installed unless this suite is selected, and then only for the language being run: agentic_eval is imported lazily and each toolchain is keyed per language. Assisted-by: Claude Opus 5 <noreply@anthropic.com> * llama-eval : make the agentic hooks typed rather than discovered `ty` flagged nine unresolved attributes, of which the seven in main() predate the agentic suite: `EvalState.dataset` is Optional and was dereferenced without narrowing. * `loaded_dataset` turns the Optional into a value in one place, raising rather than handing back None, so callers do not each have to narrow it. * `is_agentic`, `prepare()` and `run()` are declared on BaseDataset. They were reached through getattr with a default at three call sites, which no checker can see through; `run()` now raises a clear error naming the suite instead of failing as a missing attribute. * The sandbox setup is one branch on `is_agentic` rather than two tests of `grader_type`, which also drops a duplicated condition. No behaviour change: HumanEval stays at 164/164, and `ty check` on examples/llama-eval is clean. Assisted-by: Claude Opus 5 <noreply@anthropic.com> * feat: API key * fix reasoning and response length for agentic runs, add option to save entire transcripts * llama-eval : fix ty unresolved-attribute on loaded_dataset.ae Assisted-by: AI coding agent * llama-eval : trim comments and remove non-ASCII from README Assisted-by: AI coding agent
2799 lines
105 KiB
Python
Executable File
2799 lines
105 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
|
||
import argparse
|
||
import ast
|
||
import json
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
import threading
|
||
import time
|
||
import uuid
|
||
from abc import ABC, abstractmethod
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
from dataclasses import dataclass, asdict, field
|
||
from pathlib import Path
|
||
from queue import Queue
|
||
from typing import Dict, List, Optional, Any, Tuple
|
||
from urllib.parse import urlparse
|
||
import requests
|
||
from tqdm import tqdm
|
||
import random
|
||
from math import sqrt
|
||
|
||
from eval_sandbox import Sandbox, resolve_python
|
||
|
||
|
||
@dataclass
|
||
class ServerConfig:
|
||
url: str
|
||
threads: int
|
||
name: str = ""
|
||
api_key: Optional[str] = None
|
||
|
||
def is_opencode(self) -> bool:
|
||
"""Whether this endpoint is an opencode gateway rather than a llama-server."""
|
||
return "opencode" in (urlparse(self.url).hostname or "")
|
||
|
||
def headers(self, session_id: Optional[str] = None) -> Dict[str, str]:
|
||
headers = auth_headers(self.api_key)
|
||
if session_id and self.is_opencode():
|
||
headers["x-opencode-session"] = session_id
|
||
return headers
|
||
|
||
|
||
def auth_headers(api_key: Optional[str]) -> Dict[str, str]:
|
||
"""Standard request headers, carrying a bearer token when one is configured."""
|
||
headers = {"Content-Type": "application/json"}
|
||
if api_key:
|
||
headers["Authorization"] = f"Bearer {api_key}"
|
||
return headers
|
||
|
||
|
||
SERVER_ERROR = "server_error"
|
||
|
||
RETRYABLE_HTTP = {408, 409, 425, 429}
|
||
|
||
|
||
class ServerError(Exception):
|
||
"""A request that still failed, or came back empty, after every retry."""
|
||
|
||
|
||
def null_reply_reason(result: Any) -> Optional[str]:
|
||
"""Why a chat completion carries no usable reply, or None when it does."""
|
||
if not isinstance(result, dict):
|
||
return "reply is not a JSON object"
|
||
choices = result.get("choices")
|
||
if not choices:
|
||
return "reply has no choices"
|
||
msg = choices[0].get("message")
|
||
if not msg:
|
||
return "reply has no message"
|
||
if not (msg.get("content") or msg.get("tool_calls") or msg.get("reasoning_content")):
|
||
return "reply is empty"
|
||
return None
|
||
|
||
|
||
def post_with_retry(url: str, headers: Dict[str, str], data: Dict[str, Any],
|
||
retries: int, retry_delay: float,
|
||
timeout: Optional[float] = None) -> Dict[str, Any]:
|
||
"""POST a chat completion, resending on errors and null replies.
|
||
|
||
The wait before each resend doubles, starting at `retry_delay` seconds.
|
||
Raises ServerError once `retries` resends have all failed.
|
||
"""
|
||
reason = ""
|
||
attempts = 0
|
||
for attempt in range(retries + 1):
|
||
if attempt:
|
||
wait = retry_delay * 2 ** (attempt - 1)
|
||
print(f" [{url}] {reason}; retry {attempt}/{retries} in {wait:.0f}s",
|
||
file=sys.stderr)
|
||
time.sleep(wait)
|
||
attempts += 1
|
||
try:
|
||
response = requests.post(url, headers=headers, json=data, timeout=timeout)
|
||
except requests.RequestException as e:
|
||
reason = f"{type(e).__name__}: {e}"
|
||
continue
|
||
if response.status_code >= 400:
|
||
reason = f"HTTP {response.status_code}: {response.text[:300]}"
|
||
if response.status_code < 500 and response.status_code not in RETRYABLE_HTTP:
|
||
break
|
||
continue
|
||
try:
|
||
result = response.json()
|
||
except ValueError:
|
||
reason = f"invalid JSON: {response.text[:300]}"
|
||
continue
|
||
reason = null_reply_reason(result) or ""
|
||
if not reason:
|
||
return result
|
||
raise ServerError(f"{reason} (after {attempts} attempt{'s' if attempts > 1 else ''})")
|
||
|
||
|
||
def new_session_id(task_id: str) -> str:
|
||
"""A fresh session id for one task, unique across tasks, runs and resumes."""
|
||
return f"{task_id}-{uuid.uuid4().hex}"
|
||
|
||
def wilson_interval(correct: int, total: int, z: float = 1.96) -> Tuple[float, float]:
|
||
"""Wilson score confidence interval for a proportion."""
|
||
if total == 0:
|
||
return (0.0, 1.0)
|
||
p = correct / total
|
||
z2 = z * z / total
|
||
center = (p + z2 / 2) / (1 + z2)
|
||
margin = z * sqrt((p * (1 - p) + z2 / 4) / total) / (1 + z2)
|
||
return (center - margin, center + margin)
|
||
|
||
cache_dir = Path.home() / ".cache" / "huggingface" / "datasets"
|
||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||
os.environ["HF_DATASETS_CACHE"] = str(cache_dir)
|
||
os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
|
||
|
||
GRADER_PATTERNS = {
|
||
"aime": r'\boxed{(\d+)}|\b(\d+)\b',
|
||
"aime2025": r'\boxed{(\d+)}|\b(\d+)\b',
|
||
"aime2026": r'\boxed{(\d+)}|\b(\d+)\b',
|
||
"gsm8k": r'\b(\d+)\b',
|
||
}
|
||
|
||
SAMPLE_ANSWERS = {
|
||
"aime": [
|
||
"42",
|
||
"-123",
|
||
"999"
|
||
],
|
||
"aime2025": [
|
||
"42",
|
||
"-123",
|
||
"999"
|
||
],
|
||
"aime2026": [
|
||
"42",
|
||
"-123",
|
||
"999"
|
||
],
|
||
"gsm8k": [
|
||
"42",
|
||
"-123",
|
||
"999"
|
||
],
|
||
"gpqa": [
|
||
"A",
|
||
"D",
|
||
"C"
|
||
],
|
||
}
|
||
|
||
TEMPLATE_REGISTRY = {
|
||
"aime": """Solve the following math problem step by step. Put your answer inside \\boxed{{}}.
|
||
|
||
{question}
|
||
|
||
Remember to put your answer inside \\boxed{{}}.
|
||
""",
|
||
"aime2025": """Solve the following math problem step by step. Put your answer inside \\boxed{{}}.
|
||
|
||
{question}
|
||
|
||
Remember to put your answer inside \\boxed{{}}.
|
||
""",
|
||
"aime2026": """Solve the following math problem step by step. Put your answer inside \\boxed{{}}.
|
||
|
||
{question}
|
||
|
||
Remember to put your answer inside \\boxed{{}}.
|
||
""",
|
||
"gsm8k": """{question}
|
||
Please reason step by step, and put your final numeric answer within \\boxed{{}} without any extra characters.
|
||
""",
|
||
"gpqa": """Answer the following multiple choice question. The last line of your response should be in the following format: 'Answer: A/B/C/D' (e.g. 'Answer: A').
|
||
|
||
{Question}
|
||
|
||
A) {A}
|
||
B) {B}
|
||
C) {C}
|
||
D) {D}
|
||
""",
|
||
"humaneval": """Complete the following Python function.
|
||
|
||
```python
|
||
{question}
|
||
```
|
||
|
||
Reply with the complete function in a single ```python code block, including the \
|
||
signature and any imports it needs. Do not include tests, example usage, or explanation.
|
||
""",
|
||
"classeval": """Complete the following Python class.
|
||
|
||
```python
|
||
{question}
|
||
```
|
||
|
||
Reply with the complete class in a single ```python code block: keep the class name \
|
||
and every method signature exactly as given, implement every method body, and include \
|
||
any imports the class needs. Do not include tests, example usage, or explanation.
|
||
""",
|
||
}
|
||
|
||
# Suites graded by running the generated code instead of matching its text.
|
||
CODE_SUITES = {"humaneval", "classeval", "agentic"}
|
||
|
||
AGENTIC_SUITES = {"agentic"}
|
||
|
||
# Import name -> pip name, for the cases where they differ. Anything not listed is assumed to install under the name it imports as.
|
||
PIP_NAMES = {
|
||
"bs4": "beautifulsoup4",
|
||
"PIL": "pillow",
|
||
"docx": "python-docx",
|
||
"fitz": "pymupdf",
|
||
"sklearn": "scikit-learn",
|
||
"yaml": "PyYAML",
|
||
"cv2": "opencv-python-headless",
|
||
"PyPDF2": "pypdf2",
|
||
"Levenshtein": "levenshtein",
|
||
"dateutil": "python-dateutil",
|
||
"attr": "attrs",
|
||
"OpenSSL": "pyOpenSSL",
|
||
"serial": "pyserial",
|
||
"Crypto": "pycryptodome",
|
||
}
|
||
|
||
DATASET_SOURCE: Optional[str] = None
|
||
|
||
# Extra construction options for the agentic suite, set from the CLI.
|
||
AGENTIC_OPTS: Dict[str, Any] = {}
|
||
|
||
|
||
class BaseDataset(ABC):
|
||
questions: List[Dict]
|
||
|
||
is_agentic: bool = False
|
||
|
||
def prepare(self, base_python: Optional[str] = None, force: bool = False) -> None:
|
||
"""Provision whatever the suite needs before any task runs.
|
||
|
||
Single-shot suites are provisioned from required_packages() instead, so
|
||
this is a no-op for them.
|
||
"""
|
||
|
||
def run(self, index: int, chat) -> Dict[str, Any]:
|
||
"""Drive one episode to completion and return its scored result."""
|
||
raise NotImplementedError(
|
||
f"{type(self).__name__} does not drive its own conversation")
|
||
|
||
def required_packages(self) -> List[str]:
|
||
"""Third-party pip packages the suite's tests need."""
|
||
return []
|
||
|
||
def provision_steps(self) -> List[str]:
|
||
"""Python snippets run once at venv build time, with network access."""
|
||
return []
|
||
|
||
def preferred_python(self) -> Optional[str]:
|
||
"""Interpreter the suite's dependencies need, if not the current one."""
|
||
return None
|
||
|
||
def configure_python(self, python: str):
|
||
"""Told which interpreter the sandbox will use, before it is built.
|
||
|
||
Lets a suite adapt its package set to what that version can actually
|
||
install.
|
||
"""
|
||
|
||
def default_exec_timeout(self) -> float:
|
||
"""Per-task wall-clock limit that suits this suite's workload."""
|
||
return 15.0
|
||
|
||
def sandbox_env(self) -> Dict[str, str]:
|
||
"""Extra env for every run; '{venv}' expands to the venv path."""
|
||
return {}
|
||
|
||
def extract(self, response: str, question: Dict) -> str:
|
||
"""Pull the candidate code out of a chat reply."""
|
||
return extract_code(response)
|
||
|
||
def build_program(self, question: Dict, completion: str,
|
||
seed: Optional[int] = None) -> str:
|
||
"""Assemble the program whose exit status decides pass/fail."""
|
||
raise NotImplementedError
|
||
|
||
def interpret(self, result: Any, question: Dict) -> Tuple[bool, Dict[str, Any]]:
|
||
"""Turn an ExecResult into (passed, detail-for-the-log)."""
|
||
return result.passed, {
|
||
"status": result.status,
|
||
"reason": result.summary(),
|
||
"duration": round(result.duration, 3),
|
||
}
|
||
|
||
@abstractmethod
|
||
def get_question(self, index: int) -> Dict:
|
||
pass
|
||
|
||
@abstractmethod
|
||
def get_question_text(self, question: Dict) -> str:
|
||
pass
|
||
|
||
@abstractmethod
|
||
def get_answer(self, question: Dict) -> str:
|
||
pass
|
||
|
||
@abstractmethod
|
||
def get_prompt(self, question: Dict) -> str:
|
||
pass
|
||
|
||
def __len__(self) -> int:
|
||
return len(self.questions)
|
||
|
||
|
||
@dataclass
|
||
class TaskState:
|
||
task_id: str
|
||
prompt: str
|
||
expected: str
|
||
question_text: str = ""
|
||
response: Optional[str] = None
|
||
answer: Optional[str] = None
|
||
grader_log: Dict[str, Any] = field(default_factory=dict)
|
||
correct: bool = False
|
||
status: str = "pending"
|
||
tokens: Optional[int] = None
|
||
tps_gen: Optional[float] = None
|
||
t_gen_ms: Optional[float] = None
|
||
reasoning_content: Optional[str] = None
|
||
server_name: Optional[str] = None
|
||
chunk_idx: int = 0
|
||
problem_idx: int = 0
|
||
|
||
|
||
class EvalState:
|
||
def __init__(
|
||
self,
|
||
dataset_type: str,
|
||
sampling_config: Dict[str, Any],
|
||
output_file: Path = Path("llama-eval-state.json"),
|
||
model_name: Optional[str] = None
|
||
):
|
||
self.dataset_type = dataset_type
|
||
self.sampling_config = sampling_config
|
||
self.output_file = output_file
|
||
self.model_name = model_name
|
||
self.dataset: Optional[BaseDataset] = None
|
||
self.tasks: List[Tuple[int, str]] = []
|
||
self.all_tasks: List[Tuple[int, str]] = []
|
||
self.task_states: Dict[str, Any] = {}
|
||
self.total = 0
|
||
self.correct = 0
|
||
self.processed = 0
|
||
self.total_time: float = 0.0
|
||
self._lock = threading.Lock()
|
||
|
||
@property
|
||
def loaded_dataset(self) -> BaseDataset:
|
||
"""The dataset, once loaded. Raises rather than handing back None."""
|
||
if self.dataset is None:
|
||
raise RuntimeError("dataset not loaded; call load_dataset() first")
|
||
return self.dataset
|
||
|
||
def load_dataset(self, seed: int = 1234):
|
||
if self.dataset_type == "aime":
|
||
self.dataset = AimeDataset()
|
||
elif self.dataset_type == "aime2025":
|
||
self.dataset = Aime2025Dataset()
|
||
elif self.dataset_type == "aime2026":
|
||
self.dataset = Aime2026Dataset()
|
||
elif self.dataset_type == "gsm8k":
|
||
self.dataset = Gsm8kDataset()
|
||
elif self.dataset_type == "gpqa":
|
||
self.dataset = GpqaDataset(variant="diamond", seed=seed)
|
||
elif self.dataset_type == "humaneval":
|
||
self.dataset = HumanEvalDataset(source=DATASET_SOURCE)
|
||
elif self.dataset_type == "classeval":
|
||
self.dataset = ClassEvalDataset(source=DATASET_SOURCE)
|
||
elif self.dataset_type == "agentic":
|
||
self.dataset = AgenticDataset(source=DATASET_SOURCE, **AGENTIC_OPTS)
|
||
else:
|
||
raise ValueError(f"Unknown dataset type: {self.dataset_type}")
|
||
|
||
def setup_tasks(self, n_cases: Optional[int] = None, seed: int = 1234):
|
||
if self.dataset is None:
|
||
raise ValueError("Dataset not loaded. Call load_dataset() first.")
|
||
|
||
if n_cases is None:
|
||
n_cases = len(self.dataset)
|
||
|
||
dataset_size = len(self.dataset)
|
||
rng = random.Random(seed)
|
||
|
||
self.tasks = []
|
||
for chunk_idx in range((n_cases + dataset_size - 1) // dataset_size):
|
||
chunk_size = min(dataset_size, n_cases - chunk_idx * dataset_size)
|
||
indices = list(range(dataset_size))
|
||
rng.shuffle(indices)
|
||
chunk_indices = indices[:chunk_size]
|
||
|
||
for i in chunk_indices:
|
||
task_id = f"{self.dataset_type}_{chunk_idx:03d}_{i:03d}"
|
||
self.tasks.append((i, task_id))
|
||
|
||
self.all_tasks = list(self.tasks)
|
||
|
||
def get_case(self, index: int) -> Tuple[str, str, str]:
|
||
if self.dataset is None:
|
||
raise ValueError("Dataset not loaded.")
|
||
question = self.dataset.get_question(index)
|
||
question_text = self.dataset.get_question_text(question)
|
||
prompt = self.dataset.get_prompt(question)
|
||
expected = self.dataset.get_answer(question)
|
||
return question_text, prompt, expected
|
||
|
||
def add_result(
|
||
self,
|
||
task_id: str,
|
||
prompt: str,
|
||
expected: str,
|
||
response: Optional[str],
|
||
answer: Optional[str],
|
||
grader_log: Dict[str, Any],
|
||
correct: bool,
|
||
status: str,
|
||
tokens: Optional[int] = None,
|
||
tps_gen: Optional[float] = None,
|
||
t_gen_ms: Optional[float] = None,
|
||
reasoning_content: Optional[str] = None,
|
||
server_name: Optional[str] = None,
|
||
chunk_idx: int = 0,
|
||
problem_idx: int = 0,
|
||
):
|
||
with self._lock:
|
||
if "cases" not in self.task_states:
|
||
self.task_states["cases"] = {}
|
||
|
||
self.task_states["cases"][task_id] = {
|
||
"task_id": task_id,
|
||
"prompt": prompt,
|
||
"expected": expected,
|
||
"response": response,
|
||
"answer": answer,
|
||
"grader_log": grader_log,
|
||
"correct": correct,
|
||
"status": status,
|
||
"tokens": tokens,
|
||
"tps_gen": tps_gen,
|
||
"t_gen_ms": t_gen_ms,
|
||
"reasoning_content": reasoning_content,
|
||
"server_name": server_name,
|
||
"chunk_idx": chunk_idx,
|
||
"problem_idx": problem_idx,
|
||
}
|
||
|
||
self.correct = sum(1 for c in self.task_states.get("cases", {}).values() if c.get("correct", False))
|
||
|
||
def _display_pair(self, task_state: TaskState) -> Tuple[str, str]:
|
||
"""The (expected, answer) columns, kept narrow for code suites.
|
||
|
||
There the gold value is a whole JSON test harness and the answer is a
|
||
whole function, so show the task id and the failure cause instead.
|
||
"""
|
||
if self.dataset_type not in CODE_SUITES:
|
||
return (
|
||
task_state.expected,
|
||
task_state.answer if task_state.answer else "N/A",
|
||
)
|
||
try:
|
||
expected = json.loads(task_state.expected)["task_id"]
|
||
except (ValueError, KeyError, TypeError):
|
||
expected = self.dataset_type
|
||
reason = (task_state.grader_log or {}).get("reason", "")
|
||
if task_state.correct:
|
||
reason = "ok"
|
||
return expected, (reason[:28] or "N/A")
|
||
|
||
def print_progress(self, task_state: TaskState, total_tasks: int, n_correct: int = 0):
|
||
display_expected, display_answer = self._display_pair(task_state)
|
||
display_tokens = str(task_state.tokens) if task_state.tokens is not None else "N/A"
|
||
display_tps = f"{task_state.tps_gen:.1f}" if task_state.tps_gen is not None else "N/A"
|
||
display_t_gen = f"{task_state.t_gen_ms/1000:.1f}" if task_state.t_gen_ms is not None else "N/A"
|
||
display_server = task_state.server_name if task_state.server_name else "N/A"
|
||
success_ratio = n_correct / self.processed if self.processed > 0 else 0.0
|
||
first_line = next(
|
||
(ln for ln in task_state.question_text.split('\n') if ln.strip()), ""
|
||
)
|
||
truncated_question = first_line[:43]
|
||
if len(first_line) > 43:
|
||
truncated_question += "..."
|
||
else:
|
||
truncated_question = truncated_question.ljust(43) + "..."
|
||
print(f"{self.processed:3}/{total_tasks:3} {task_state.task_id:<20} {self.dataset_type.upper()} {truncated_question:<40} {display_expected:<16} {display_answer:<28} {display_tokens:<6} {display_tps:<6} {display_t_gen:<8} {'✓' if task_state.correct else ('⚠' if task_state.status == SERVER_ERROR else '✗')} [{n_correct:3}/{self.processed:3}, {success_ratio:.3f}] {display_server}")
|
||
|
||
def n_server_errors(self) -> int:
|
||
cases = self.task_states.get("cases", {})
|
||
return sum(1 for c in cases.values() if c.get("status") == SERVER_ERROR)
|
||
|
||
def print_server_errors(self):
|
||
n = self.n_server_errors()
|
||
if n:
|
||
print(f"Server errors: {n} (not counted in accuracy; run with --resume to retry them)")
|
||
|
||
def print_summary(self):
|
||
print()
|
||
self.print_existing_summary()
|
||
|
||
def dump(self):
|
||
with self._lock:
|
||
tasks_to_save = self.all_tasks if self.all_tasks else self.tasks
|
||
all_cases = {}
|
||
for i, task_id in tasks_to_save:
|
||
question_text, prompt, expected = self.get_case(i)
|
||
# Extract chunk_idx from task_id for pending cases
|
||
_parts = task_id.rsplit("_", 2)
|
||
_chunk_idx = int(_parts[-2]) if len(_parts) >= 3 else 0
|
||
if task_id in self.task_states.get("cases", {}):
|
||
all_cases[task_id] = self.task_states["cases"][task_id]
|
||
else:
|
||
all_cases[task_id] = {
|
||
"task_id": task_id,
|
||
"prompt": prompt,
|
||
"expected": expected,
|
||
"question_text": question_text,
|
||
"response": None,
|
||
"answer": None,
|
||
"grader_log": {},
|
||
"correct": False,
|
||
"status": "pending",
|
||
"tokens": None,
|
||
"tps_gen": None,
|
||
"t_gen_ms": None,
|
||
"reasoning_content": None,
|
||
"server_name": None,
|
||
"chunk_idx": _chunk_idx,
|
||
"problem_idx": i,
|
||
}
|
||
|
||
ci_lower, ci_upper = self.accuracy_ci()
|
||
data = {
|
||
"id": self.dataset_type,
|
||
"model_name": self.model_name,
|
||
"tasks": [tid for _, tid in tasks_to_save],
|
||
"task_states": {
|
||
"total": self.total,
|
||
"correct": self.correct,
|
||
"total_time": self.total_time,
|
||
"ci_lower": ci_lower,
|
||
"ci_upper": ci_upper,
|
||
"cases": all_cases,
|
||
},
|
||
"sampling_config": self.sampling_config
|
||
}
|
||
with open(self.output_file, "w") as f:
|
||
json.dump(data, f, indent=2)
|
||
|
||
self.dump_html(tasks_to_save, all_cases)
|
||
|
||
def dump_html(self, tasks_to_save: List[Tuple[int, str]], all_cases: Dict[str, Any]):
|
||
html_file = Path(str(self.output_file) + ".html")
|
||
|
||
cases = all_cases
|
||
completed = {tid: c for tid, c in cases.items() if c.get("status") == "ok"}
|
||
n_correct = sum(1 for c in completed.values() if c.get("correct", False))
|
||
n_incorrect = len(completed) - n_correct
|
||
n_server_errors = sum(1 for c in cases.values() if c.get("status") == SERVER_ERROR)
|
||
n_pending = len(tasks_to_save) - len(completed) - n_server_errors
|
||
accuracy = n_correct / len(completed) * 100 if completed else 0.0
|
||
ci_lower, ci_upper = wilson_interval(n_correct, len(completed)) if completed else (0.0, 1.0)
|
||
|
||
sampling_parts = []
|
||
for k, v in self.sampling_config.items():
|
||
if v is not None:
|
||
sampling_parts.append(f"{k}={v}")
|
||
sampling_str = ", ".join(sampling_parts) if sampling_parts else "default"
|
||
|
||
rows = []
|
||
for i, task_id in tasks_to_save:
|
||
case = cases.get(task_id, {})
|
||
status = case.get("status", "pending")
|
||
expected = case.get("expected", "")
|
||
answer = case.get("answer") or "" if status == "ok" else ""
|
||
is_correct = case.get("correct", False) if status == "ok" else False
|
||
response = case.get("response", "") or ""
|
||
prompt = case.get("prompt", "") or ""
|
||
grader_log = case.get("grader_log", {})
|
||
|
||
if status == "ok":
|
||
status_class = "correct" if is_correct else "incorrect"
|
||
status_text = "✓" if is_correct else "✗"
|
||
elif status == "pending":
|
||
status_class = "pending"
|
||
status_text = "–"
|
||
elif status == SERVER_ERROR:
|
||
status_class = "server-error"
|
||
status_text = "⚠"
|
||
else:
|
||
status_class = "error"
|
||
status_text = "!"
|
||
|
||
tokens = case.get("tokens")
|
||
tokens_str = str(tokens) if tokens is not None else ""
|
||
tps_gen = case.get("tps_gen")
|
||
tps_str = f"{tps_gen:.1f}" if tps_gen is not None else ""
|
||
t_gen_ms = case.get("t_gen_ms")
|
||
t_gen_str = f"{t_gen_ms/1000:.1f}" if t_gen_ms is not None else ""
|
||
reasoning_content = case.get("reasoning_content", "") or ""
|
||
server_name = case.get("server_name", "") or ""
|
||
|
||
escaped_response = self._escape_html(response)
|
||
escaped_prompt = self._escape_html(prompt)
|
||
escaped_reasoning = self._escape_html(reasoning_content)
|
||
grader_log_str = self._escape_html(json.dumps(grader_log, indent=2))
|
||
escaped_server = self._escape_html(server_name)
|
||
transcript = grader_log.get("transcript") if isinstance(grader_log, dict) else None
|
||
transcript_link = (
|
||
f'<b>Transcript</b> <a href="{self._escape_html(transcript[:-5])}.html">'
|
||
f'{self._escape_html(transcript[:-5])}.html</a><br>'
|
||
if transcript and transcript.endswith(".json") else "")
|
||
|
||
answer_class = status_class if status == "ok" else ""
|
||
rows.append(f"""<tr class="task-row" onclick="toggleDetails('{task_id}')">
|
||
<td>{task_id}</td>
|
||
<td class="{status_class}">{status_text}</td>
|
||
<td>{self._escape_html(expected)}</td>
|
||
<td class="{answer_class}">{self._escape_html(answer)}</td>
|
||
<td>{tokens_str}</td>
|
||
<td>{tps_str}</td>
|
||
<td>{t_gen_str}</td>
|
||
<td>{escaped_server}</td>
|
||
</tr>
|
||
<tr id="details-{task_id}" class="details-row">
|
||
<td colspan="8">
|
||
<div class="details-content">
|
||
{transcript_link}
|
||
<b>Prompt</b><pre>{escaped_prompt}</pre>
|
||
<b>Response</b><pre>{escaped_response}</pre>
|
||
{f'<b>Reasoning</b><pre>{escaped_reasoning}</pre>' if escaped_reasoning else ''}
|
||
<b>Grader</b><pre>{grader_log_str}</pre>
|
||
</div>
|
||
</td>
|
||
</tr>""")
|
||
|
||
rows_html = "\n".join(rows)
|
||
|
||
# ---- per-problem summary table ----
|
||
problem_groups: Dict[int, List[Dict[str, Any]]] = {}
|
||
for _tid, _case in cases.items():
|
||
if _case.get("status") != "ok":
|
||
continue
|
||
_pidx = _case.get("problem_idx")
|
||
if _pidx is None:
|
||
_p_parts = _tid.rsplit("_", 2)
|
||
_pidx = int(_p_parts[-1]) if len(_p_parts) >= 3 else 0
|
||
problem_groups.setdefault(_pidx, []).append(_case)
|
||
|
||
summary_rows_html = ""
|
||
if problem_groups:
|
||
def _stat(v, fmt=".1f", avg_fmt=None):
|
||
if not v:
|
||
return ("–", "–", "–")
|
||
af = fmt if avg_fmt is None else avg_fmt
|
||
return (f"{min(v):{fmt}}", f"{sum(v)/len(v):{af}}", f"{max(v):{fmt}}")
|
||
|
||
summary_data = []
|
||
for pidx, g in problem_groups.items():
|
||
runs = len(g)
|
||
n_ok = sum(1 for c in g if c.get("correct", False))
|
||
toks = [c["tokens"] for c in g if c.get("tokens") is not None]
|
||
tps = [c["tps_gen"] for c in g if c.get("tps_gen") is not None]
|
||
tg = [c["t_gen_ms"] / 1000 for c in g if c.get("t_gen_ms") is not None]
|
||
summary_data.append((
|
||
pidx, runs, n_ok,
|
||
_stat(toks, "d", ".0f"),
|
||
_stat(tps),
|
||
_stat(tg),
|
||
))
|
||
|
||
summary_data.sort(key=lambda r: r[0]) # sort by problem index ascending
|
||
|
||
summary_rows_html = "\n".join(
|
||
f"""<tr class="summary-row">
|
||
<td>{p:03d}</td>
|
||
<td>{r}</td>
|
||
<td>{n}/{r}</td>
|
||
<td>{tk[0]}</td><td>{tk[1]}</td><td>{tk[2]}</td>
|
||
<td>{tp[0]}</td><td>{tp[1]}</td><td>{tp[2]}</td>
|
||
<td>{tg[0]}</td><td>{tg[1]}</td><td>{tg[2]}</td>
|
||
</tr>"""
|
||
for p, r, n, tk, tp, tg in summary_data
|
||
)
|
||
|
||
html_content = f"""<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>{self.dataset_type.upper()} Eval</title>
|
||
<style>
|
||
body {{ font-family: system-ui, sans-serif; margin: 0; padding: 16px; background: #fff; color: #222; }}
|
||
.bar {{ padding: 8px 0; font-size: 13px; color: #555; font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; display: grid; grid-template-columns: auto 1fr auto 1fr; gap: 2px 12px; align-items: baseline; }}
|
||
.bar .label {{ color: #888; }}
|
||
.bar .value {{ color: #222; }}
|
||
table {{ width: 100%; border-collapse: collapse; font-size: 13px; font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; }}
|
||
th {{ text-align: left; padding: 6px 8px; border-bottom: 2px solid #ccc; font-weight: 600; }}
|
||
td {{ padding: 4px 8px; border-bottom: 1px solid #eee; vertical-align: top; }}
|
||
.task-row {{ cursor: pointer; }}
|
||
.task-row:hover {{ background: #f5f5f5; }}
|
||
.correct {{ color: #1a7f37; }}
|
||
.incorrect {{ color: #cf222e; }}
|
||
.pending {{ color: #888; }}
|
||
.error {{ color: #9a6700; }}
|
||
.server-error {{ color: #8250df; }}
|
||
.details-row {{ display: none; }}
|
||
.details-row.open {{ display: table-row; }}
|
||
.details-content {{ padding: 8px 16px; background: #f6f8fa; font-size: 12px; }}
|
||
.details-content b {{ color: #555; }}
|
||
.details-content pre {{ background: #fff; border: 1px solid #e1e4e8; padding: 8px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; margin: 4px 0 8px; }}
|
||
.summary-table {{ margin-bottom: 16px; font-size: 13px; width: 100%; }}
|
||
.summary-row {{ background: #fafbfc; }}
|
||
.summary-row:hover {{ background: #f5f5f5; }}
|
||
.summary-table th {{ text-align: right; font-weight: 600; }}
|
||
.summary-table th:first-child {{ text-align: left; }}
|
||
.summary-table th[colspan] {{ text-align: center; }}
|
||
.summary-table td {{ text-align: right; }}
|
||
.summary-table td:first-child {{ text-align: left; }}
|
||
.tabs {{ display: flex; border-bottom: 2px solid #ddd; margin: 12px 0 0; }}
|
||
.tab-btn {{ padding: 6px 16px; border: none; background: none; font-size: 13px; cursor: pointer; color: #555; border-bottom: 2px solid transparent; margin-bottom: -2px; font-weight: 500; }}
|
||
.tab-btn:hover {{ color: #222; }}
|
||
.tab-btn.active {{ color: #222; border-bottom-color: #222; font-weight: 600; }}
|
||
.tab-content {{ display: none; }}
|
||
.tab-content.active {{ display: block; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="bar">
|
||
<div class="label">Dataset</div><div class="value"><b>{self.dataset_type.upper()}</b></div>
|
||
<div class="label">Model</div><div class="value"><b>{self.model_name or 'N/A'}</b></div>
|
||
<div class="label">Accuracy</div><div class="value"><b>{accuracy:.1f}%</b> [{ci_lower*100:.1f}%, {ci_upper*100:.1f}%]</div>
|
||
<div class="label">Correct</div><div class="value"><span class="correct">{n_correct}</span> / {len(completed)}</div>
|
||
<div class="label">Pending</div><div class="value">{n_pending}</div>
|
||
<div class="label">Server errors</div><div class="value"><span class="server-error">{n_server_errors}</span></div>
|
||
<div class="label">Time</div><div class="value">{self.total_time:.1f}s</div>
|
||
<div class="label">Sampling</div><div class="value">{sampling_str}</div>
|
||
</div>
|
||
<div class="tabs">
|
||
<button class="tab-btn active" data-tab="detailed" onclick="switchTab(this)">Detailed</button>
|
||
<button class="tab-btn" data-tab="summary" onclick="switchTab(this)">Summary</button>
|
||
</div>
|
||
<div id="tab-detailed" class="tab-content active">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th></th>
|
||
<th>Gold</th>
|
||
<th>Answer</th>
|
||
<th>Tokens</th>
|
||
<th>T/s</th>
|
||
<th>Gen s</th>
|
||
<th>Server</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{rows_html}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div id="tab-summary" class="tab-content">
|
||
<table class="summary-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Problem</th>
|
||
<th>Runs</th>
|
||
<th>Correct</th>
|
||
<th colspan="3">Tokens</th>
|
||
<th colspan="3">T/s</th>
|
||
<th colspan="3">Gen s</th>
|
||
</tr>
|
||
<tr>
|
||
<th></th>
|
||
<th></th>
|
||
<th></th>
|
||
<th>min</th><th>avg</th><th>max</th>
|
||
<th>min</th><th>avg</th><th>max</th>
|
||
<th>min</th><th>avg</th><th>max</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{summary_rows_html}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<script>
|
||
function toggleDetails(id) {{ document.getElementById('details-'+id).classList.toggle('open'); }}
|
||
function switchTab(btn) {{
|
||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
document.getElementById('tab-'+btn.dataset.tab).classList.add('active');
|
||
}}
|
||
</script>
|
||
</body>
|
||
</html>"""
|
||
|
||
with open(html_file, "w") as f:
|
||
f.write(html_content)
|
||
|
||
def _escape_html(self, s: str) -> str:
|
||
return (s.replace("&", "&")
|
||
.replace("<", "<")
|
||
.replace(">", ">")
|
||
.replace('"', """)
|
||
.replace("'", "'"))
|
||
|
||
@classmethod
|
||
def load(cls, path: Path) -> "EvalState":
|
||
with open(path, "r") as f:
|
||
data = json.load(f)
|
||
|
||
eval_state = cls(
|
||
dataset_type=data["id"],
|
||
sampling_config=data["sampling_config"],
|
||
output_file=path,
|
||
model_name=data.get("model_name")
|
||
)
|
||
eval_state.load_dataset()
|
||
|
||
eval_state.tasks = []
|
||
eval_state.all_tasks = []
|
||
for task_id in data.get("tasks", []):
|
||
parts = task_id.rsplit("_", 2)
|
||
if len(parts) >= 3:
|
||
idx = int(parts[-1])
|
||
else:
|
||
idx = 0
|
||
eval_state.tasks.append((idx, task_id))
|
||
eval_state.all_tasks.append((idx, task_id))
|
||
|
||
eval_state.task_states = data.get("task_states", {})
|
||
|
||
cases = eval_state.task_states.get("cases", {})
|
||
eval_state.total = eval_state.task_states.get("total", 0)
|
||
eval_state.correct = eval_state.task_states.get("correct", 0)
|
||
eval_state.total_time = eval_state.task_states.get("total_time", 0.0)
|
||
|
||
if eval_state.total == 0:
|
||
eval_state.total = len(cases)
|
||
eval_state.correct = sum(1 for c in cases.values() if c.get("correct", False))
|
||
|
||
return eval_state
|
||
|
||
def is_complete(self) -> bool:
|
||
if not self.all_tasks:
|
||
return False
|
||
cases = self.task_states.get("cases", {})
|
||
completed = {tid for tid in self.task_states.get("cases", {}).keys() if cases.get(tid, {}).get("status") == "ok"}
|
||
return len(completed) == len(self.all_tasks)
|
||
|
||
def get_pending_tasks(self) -> List[Tuple[int, str]]:
|
||
cases = self.task_states.get("cases", {})
|
||
pending = []
|
||
for i, task_id in self.all_tasks:
|
||
status = cases.get(task_id, {}).get("status", "pending")
|
||
if status != "ok":
|
||
pending.append((i, task_id))
|
||
return pending
|
||
|
||
def print_all_tasks(self):
|
||
cases = self.task_states.get("cases", {})
|
||
tasks_to_show = self.all_tasks if self.all_tasks else self.tasks
|
||
print()
|
||
print("Tasks:")
|
||
print(" Task ID Dataset Prompt (first 40 chars) Expected Answer Tokens T/s Gen s Status")
|
||
for i, task_id in tasks_to_show:
|
||
question, prompt, expected = self.get_case(i)
|
||
case = cases.get(task_id, {})
|
||
status = case.get("status", "pending")
|
||
answer = case.get("answer") or "N/A" if status == "ok" else "N/A"
|
||
tokens = case.get("tokens")
|
||
tokens_str = str(tokens) if tokens is not None else "N/A"
|
||
tps_gen = case.get("tps_gen")
|
||
tps_str = f"{tps_gen:.1f}" if tps_gen is not None else "N/A"
|
||
t_gen_ms = case.get("t_gen_ms")
|
||
t_gen_str = f"{t_gen_ms/1000:.1f}" if t_gen_ms is not None else "N/A"
|
||
server_name = case.get("server_name", "") or ""
|
||
is_correct = case.get("correct", False) if status == "ok" else False
|
||
symbol = "✓ " if is_correct else ("✗ " if status == "ok" else "")
|
||
first_line = question.split('\n')[0]
|
||
question_trunc = first_line[:43]
|
||
if len(first_line) > 43:
|
||
question_trunc += "..."
|
||
else:
|
||
question_trunc = question_trunc.ljust(43) + "..."
|
||
print(f" {task_id:<20} {self.dataset_type.upper()} {question_trunc:<40} {expected:<10} {answer:<10} {tokens_str:<6} {tps_str:<6} {t_gen_str:<8} {symbol}{status} {server_name}")
|
||
print()
|
||
|
||
def print_existing_summary(self):
|
||
cases = self.task_states.get("cases", {})
|
||
completed_cases = {tid: c for tid, c in cases.items() if c.get("status") == "ok"}
|
||
correct = sum(1 for c in completed_cases.values() if c.get("correct", False))
|
||
total = len(completed_cases)
|
||
if total == 0:
|
||
print(f"{'='*60}")
|
||
print(f"Results: 0/0 correct (0.0%)")
|
||
print(f"{'='*60}")
|
||
else:
|
||
ci_lower, ci_upper = self.accuracy_ci()
|
||
print(f"{'='*60}")
|
||
print(f"Results: {correct}/{total} correct ({correct/total*100:.1f}%) [{ci_lower*100:.1f}%, {ci_upper*100:.1f}%]")
|
||
print(f"{'='*60}")
|
||
self.print_server_errors()
|
||
|
||
def accuracy_ci(self) -> Tuple[float, float]:
|
||
"""Compute Wilson score confidence interval from completed cases."""
|
||
cases = self.task_states.get("cases", {})
|
||
completed = {tid: c for tid, c in cases.items() if c.get("status") == "ok"}
|
||
correct = sum(1 for c in completed.values() if c.get("correct", False))
|
||
total = len(completed)
|
||
return wilson_interval(correct, total)
|
||
|
||
def normalize_number(s: str) -> Optional[int]:
|
||
match = re.match(r"\d+", s) # match digits from the start
|
||
if not match:
|
||
return None
|
||
return int(match.group(0))
|
||
|
||
class AimeDataset(BaseDataset):
|
||
def __init__(self, split: str = "train"):
|
||
self.split = split
|
||
self.questions = []
|
||
self._load_dataset()
|
||
|
||
def _load_dataset(self):
|
||
print(f"Loading AIME dataset (split: {self.split})...")
|
||
from datasets import load_dataset
|
||
|
||
cache_path = cache_dir / "AI-MO___aimo-validation-aime" / "default" / "0.0.0"
|
||
if cache_path.exists():
|
||
print(f"Using cached dataset from {cache_path}")
|
||
ds = load_dataset("AI-MO/aimo-validation-aime", split=self.split, cache_dir=str(cache_path))
|
||
else:
|
||
ds = load_dataset("AI-MO/aimo-validation-aime", split=self.split)
|
||
|
||
self.questions = []
|
||
for row in ds:
|
||
question = dict(row)
|
||
question["dataset_type"] = "aime"
|
||
self.questions.append(question)
|
||
|
||
print(f"AIME dataset loaded: {len(self.questions)} questions")
|
||
|
||
def get_question(self, index: int) -> Dict:
|
||
"""Get question by index"""
|
||
return self.questions[index]
|
||
|
||
def get_question_text(self, question: Dict) -> str:
|
||
"""Get question string"""
|
||
return question["problem"] if "problem" in question else question["question"]
|
||
|
||
def get_answer(self, question: Dict) -> str:
|
||
answer = question["answer"]
|
||
if isinstance(answer, str):
|
||
normalized = normalize_number(answer)
|
||
return str(normalized) if normalized is not None else answer
|
||
return str(answer)
|
||
|
||
def get_prompt(self, question: Dict) -> str:
|
||
"""Get formatted prompt for the question"""
|
||
return TEMPLATE_REGISTRY[question["dataset_type"]].format(
|
||
question=self.get_question_text(question),
|
||
)
|
||
|
||
class Aime2025Dataset(BaseDataset):
|
||
def __init__(self):
|
||
self.questions = []
|
||
self._load_dataset()
|
||
|
||
def _load_dataset(self):
|
||
print(f"Loading AIME2025 dataset...")
|
||
from datasets import load_dataset
|
||
|
||
config_name = "AIME2025-I"
|
||
cache_path = cache_dir / "opencompass___AIME2025" / "default" / "0.0.0"
|
||
if cache_path.exists():
|
||
print(f"Using cached dataset from {cache_path}")
|
||
ds = load_dataset("opencompass/AIME2025", config_name, split="test", cache_dir=str(cache_path))
|
||
else:
|
||
ds = load_dataset("opencompass/AIME2025", config_name, split="test")
|
||
|
||
self.questions = []
|
||
for row in ds:
|
||
question = dict(row)
|
||
question["dataset_type"] = "aime2025"
|
||
self.questions.append(question)
|
||
|
||
print(f"AIME2025 dataset loaded: {len(self.questions)} questions")
|
||
|
||
print(f"Loading AIME2025 dataset (part 2)...")
|
||
config_name_2 = "AIME2025-II"
|
||
cache_path_2 = cache_dir / "opencompass___AIME2025" / "default" / "0.0.0"
|
||
if cache_path_2.exists():
|
||
print(f"Using cached dataset from {cache_path_2}")
|
||
ds_2 = load_dataset("opencompass/AIME2025", config_name_2, split="test", cache_dir=str(cache_path_2))
|
||
else:
|
||
ds_2 = load_dataset("opencompass/AIME2025", config_name_2, split="test")
|
||
|
||
for row in ds_2:
|
||
question = dict(row)
|
||
question["dataset_type"] = "aime2025"
|
||
self.questions.append(question)
|
||
|
||
print(f"AIME2025 dataset loaded: {len(self.questions)} questions (total)")
|
||
|
||
def get_question(self, index: int) -> Dict:
|
||
"""Get question by index"""
|
||
return self.questions[index]
|
||
|
||
def get_question_text(self, question: Dict) -> str:
|
||
"""Get question string"""
|
||
return question["question"]
|
||
|
||
def get_answer(self, question: Dict) -> str:
|
||
answer = question["answer"]
|
||
if isinstance(answer, str):
|
||
normalized = normalize_number(answer)
|
||
return str(normalized) if normalized is not None else answer
|
||
return str(answer)
|
||
|
||
def get_prompt(self, question: Dict) -> str:
|
||
"""Get formatted prompt for the question"""
|
||
return TEMPLATE_REGISTRY["aime2025"].format(
|
||
question=self.get_question_text(question),
|
||
)
|
||
|
||
class Aime2026Dataset(BaseDataset):
|
||
def __init__(self):
|
||
self.questions = []
|
||
self._load_dataset()
|
||
|
||
def _load_dataset(self):
|
||
print(f"Loading AIME2026 dataset...")
|
||
from datasets import load_dataset
|
||
|
||
cache_path = cache_dir / "MathArena___aime_2026" / "default" / "0.0.0"
|
||
if cache_path.exists():
|
||
print(f"Using cached dataset from {cache_path}")
|
||
ds = load_dataset("MathArena/aime_2026", "default", split="train", cache_dir=str(cache_path))
|
||
else:
|
||
ds = load_dataset("MathArena/aime_2026", "default", split="train")
|
||
|
||
self.questions = []
|
||
for row in ds:
|
||
question = dict(row)
|
||
question["dataset_type"] = "aime2026"
|
||
self.questions.append(question)
|
||
|
||
print(f"AIME2026 dataset loaded: {len(self.questions)} questions")
|
||
|
||
def get_question(self, index: int) -> Dict:
|
||
"""Get question by index"""
|
||
return self.questions[index]
|
||
|
||
def get_question_text(self, question: Dict) -> str:
|
||
"""Get question string"""
|
||
return question["problem"]
|
||
|
||
def get_answer(self, question: Dict) -> str:
|
||
return str(question["answer"])
|
||
|
||
def get_prompt(self, question: Dict) -> str:
|
||
"""Get formatted prompt for the question"""
|
||
return TEMPLATE_REGISTRY["aime2026"].format(
|
||
question=self.get_question_text(question),
|
||
)
|
||
|
||
class Gsm8kDataset(BaseDataset):
|
||
def __init__(self, split: str = "test"):
|
||
self.split = split
|
||
self.questions = []
|
||
self._load_dataset()
|
||
|
||
def _load_dataset(self):
|
||
print(f"Loading GSM8K dataset (split: {self.split})...")
|
||
from datasets import load_dataset
|
||
|
||
cache_path = cache_dir / "openai___gsm8k" / "default" / "0.0.0"
|
||
if cache_path.exists():
|
||
print(f"Using cached dataset from {cache_path}")
|
||
ds = load_dataset("openai/gsm8k", "main", split=self.split, cache_dir=str(cache_path))
|
||
else:
|
||
ds = load_dataset("openai/gsm8k", "main", split=self.split)
|
||
|
||
self.questions = []
|
||
for row in ds:
|
||
question = dict(row)
|
||
question["dataset_type"] = "gsm8k"
|
||
|
||
# Extract numeric answer from the answer field (already has #### prefix)
|
||
gold = question["answer"]
|
||
# Split by #### and take the last part
|
||
parts = gold.split("####")
|
||
if len(parts) > 1:
|
||
gold = parts[-1].strip()
|
||
# Extract the first number from the remaining text
|
||
normalized = normalize_number(gold)
|
||
question["gold"] = str(normalized) if normalized is not None else gold
|
||
|
||
self.questions.append(question)
|
||
|
||
print(f"GSM8K dataset loaded: {len(self.questions)} questions")
|
||
|
||
def get_question(self, index: int) -> Dict:
|
||
"""Get question by index"""
|
||
return self.questions[index]
|
||
|
||
def get_question_text(self, question: Dict) -> str:
|
||
"""Get question string"""
|
||
return question["problem"] if "problem" in question else question["question"]
|
||
|
||
def get_answer(self, question: Dict) -> str:
|
||
# GSM8K has pre-extracted gold field, AIME uses answer field
|
||
if "gold" in question:
|
||
return question["gold"]
|
||
answer = question["answer"]
|
||
if isinstance(answer, str):
|
||
normalized = normalize_number(answer)
|
||
return str(normalized) if normalized is not None else answer
|
||
return str(answer)
|
||
|
||
def get_prompt(self, question: Dict) -> str:
|
||
"""Get formatted prompt for the question"""
|
||
return TEMPLATE_REGISTRY[question["dataset_type"]].format(
|
||
question=self.get_question_text(question),
|
||
)
|
||
|
||
class GpqaDataset(BaseDataset):
|
||
def __init__(self, variant: str = "diamond", seed: int = 1234):
|
||
self.variant = variant
|
||
self.seed = seed
|
||
self.questions = []
|
||
self._load_dataset()
|
||
|
||
def _load_dataset(self):
|
||
print(f"Loading GPQA dataset (variant: {self.variant})...")
|
||
import pandas as pd
|
||
|
||
url = f"https://openaipublic.blob.core.windows.net/simple-evals/gpqa_{self.variant}.csv"
|
||
df = pd.read_csv(url)
|
||
|
||
rng = random.Random(self.seed)
|
||
|
||
self.questions = []
|
||
for _, row in df.iterrows():
|
||
question = row.to_dict()
|
||
question["dataset_type"] = "gpqa"
|
||
|
||
# Shuffle the answer options
|
||
correct_answer = question["Correct Answer"]
|
||
incorrect_answers = [
|
||
question["Incorrect Answer 1"],
|
||
question["Incorrect Answer 2"],
|
||
question["Incorrect Answer 3"]
|
||
]
|
||
|
||
# Create list of (answer, is_correct) tuples
|
||
options = [(ans, ans == correct_answer) for ans in incorrect_answers]
|
||
options.append((correct_answer, True))
|
||
|
||
# Shuffle the options
|
||
rng.shuffle(options)
|
||
|
||
# Extract shuffled answers and determine correct letter
|
||
shuffled_answers = [ans for ans, _ in options]
|
||
correct_letter = chr(ord('A') + options.index((correct_answer, True)))
|
||
|
||
# Store shuffled answers and correct letter
|
||
question["shuffled_answers"] = shuffled_answers
|
||
question["correct_letter"] = correct_letter
|
||
|
||
self.questions.append(question)
|
||
|
||
print(f"GPQA dataset loaded: {len(self.questions)} questions")
|
||
|
||
def get_question(self, index: int) -> Dict:
|
||
"""Get question by index"""
|
||
return self.questions[index]
|
||
|
||
def get_question_text(self, question: Dict) -> str:
|
||
"""Get question string"""
|
||
return question["Question"]
|
||
|
||
def get_answer(self, question: Dict) -> str:
|
||
# GPQA returns the correct letter (A, B, C, or D)
|
||
return question["correct_letter"]
|
||
|
||
def get_prompt(self, question: Dict) -> str:
|
||
"""Get formatted prompt for the question"""
|
||
return TEMPLATE_REGISTRY["gpqa"].format(
|
||
Question=self.get_question_text(question),
|
||
A=question["shuffled_answers"][0],
|
||
B=question["shuffled_answers"][1],
|
||
C=question["shuffled_answers"][2],
|
||
D=question["shuffled_answers"][3]
|
||
)
|
||
|
||
FENCE_RE = re.compile(r"```[ \t]*([A-Za-z0-9_+-]*)[ \t]*\n(.*?)(?:```|\Z)", re.DOTALL)
|
||
|
||
RESULT_MARKER = "__LLAMA_EVAL__"
|
||
|
||
GENSIM_SHIM = {
|
||
"__init__.py": (
|
||
'"""Minimal stand-in for the two gensim entry points ClassEval uses."""\n'
|
||
"from . import utils, matutils # noqa: F401\n"
|
||
),
|
||
"utils.py": (
|
||
"import re, html\n"
|
||
"RE_ENTITY = re.compile(r'&(#?)([xX]?)(\\w{1,8});')\n"
|
||
"def decode_htmlentities(text):\n"
|
||
" return RE_ENTITY.sub(lambda m: html.unescape(m.group(0)), text)\n"
|
||
),
|
||
"matutils.py": (
|
||
"import numpy as np\n"
|
||
"def unitvec(vec, norm='l2', return_norm=False):\n"
|
||
" v = np.asarray(vec, dtype=float)\n"
|
||
" n = float(np.sum(np.abs(v))) if norm == 'l1' "
|
||
"else float(np.sqrt(np.sum(v ** 2)))\n"
|
||
" if n == 0.0:\n"
|
||
" return (v, 1.0) if return_norm else v\n"
|
||
" out = v / n\n"
|
||
" return (out, n) if return_norm else out\n"
|
||
),
|
||
}
|
||
|
||
UNITTEST_PROLOGUE = '''
|
||
import unittest as _ut_pre
|
||
_ut_pre.main = lambda *a, **k: None
|
||
'''
|
||
|
||
UNITTEST_RUNNER = f'''
|
||
|
||
if True:
|
||
import json as _json, os as _os, unittest as _ut
|
||
_loader = _ut.TestLoader()
|
||
_suite = _ut.TestSuite()
|
||
for _c in [v for v in list(globals().values())
|
||
if isinstance(v, type) and issubclass(v, _ut.TestCase)
|
||
and v is not _ut.TestCase]:
|
||
_suite.addTests(_loader.loadTestsFromTestCase(_c))
|
||
with open(_os.devnull, "w") as _null:
|
||
_res = _ut.TextTestRunner(stream=_null, verbosity=0).run(_suite)
|
||
_bad = {{t.id() for t, _ in _res.failures}} | {{t.id() for t, _ in _res.errors}}
|
||
_total = _res.testsRun
|
||
print("{RESULT_MARKER}" + _json.dumps({{
|
||
"total": _total,
|
||
"passed": _total - len(_bad),
|
||
"failed": sorted(_bad),
|
||
"first_error": (_res.failures + _res.errors)[0][1].strip().splitlines()[-1]
|
||
if (_res.failures or _res.errors) else None,
|
||
}}))
|
||
'''
|
||
|
||
|
||
def parse_test_report(stdout: str) -> Optional[Dict[str, Any]]:
|
||
"""Read the JSON line emitted by UNITTEST_RUNNER, if the program got that far."""
|
||
for line in reversed(stdout.splitlines()):
|
||
if line.startswith(RESULT_MARKER):
|
||
try:
|
||
return json.loads(line[len(RESULT_MARKER):])
|
||
except ValueError:
|
||
return None
|
||
return None
|
||
|
||
|
||
def extract_code(response: str, entry_point: Optional[str] = None,
|
||
class_name: Optional[str] = None) -> str:
|
||
"""Pull the candidate program out of a chat reply.
|
||
|
||
Prefers a fenced block that actually defines the thing being asked for,
|
||
since models like to emit a usage example or a test in a second block.
|
||
"""
|
||
if not response:
|
||
return ""
|
||
|
||
if class_name:
|
||
wanted = rf"^\s*class\s+{re.escape(class_name)}\s*[:\(]"
|
||
elif entry_point:
|
||
wanted = rf"^\s*(?:async\s+)?def\s+{re.escape(entry_point)}\s*\("
|
||
else:
|
||
wanted = None
|
||
|
||
blocks = [(lang.lower(), body) for lang, body in FENCE_RE.findall(response)]
|
||
if blocks:
|
||
py = [b for lang, b in blocks if lang in ("python", "py", "python3", "")]
|
||
candidates = py or [b for _, b in blocks]
|
||
if wanted:
|
||
defining = [b for b in candidates if re.search(wanted, b, re.M)]
|
||
if defining:
|
||
return defining[-1].strip("\n")
|
||
return candidates[0].strip("\n")
|
||
|
||
return response.strip("\n")
|
||
|
||
|
||
def _import_preamble(prompt: str) -> str:
|
||
"""The import lines from the original stub.
|
||
|
||
A chat model often returns just the function and drops the imports that the
|
||
stub had above it; replaying them keeps that from being scored as a failure.
|
||
"""
|
||
lines = []
|
||
for line in prompt.splitlines():
|
||
stripped = line.strip()
|
||
if stripped.startswith(("import ", "from ")) and not line.startswith((" ", "\t")):
|
||
lines.append(stripped)
|
||
return "\n".join(lines)
|
||
|
||
|
||
def build_program(question: Dict, completion: str, seed: Optional[int] = 0) -> str:
|
||
"""Assemble the full program that decides pass/fail for one problem."""
|
||
entry_point = question["entry_point"]
|
||
code = completion
|
||
|
||
if not re.search(rf"^\s*(?:async\s+)?def\s+{re.escape(entry_point)}\s*\(", code, re.M):
|
||
body = completion if completion.startswith((" ", "\t")) else \
|
||
"\n".join(" " + ln for ln in completion.splitlines())
|
||
code = question["prompt"] + "\n" + body
|
||
|
||
prologue = f"import random; random.seed({seed})" if seed is not None else ""
|
||
|
||
return "\n\n".join(part for part in [
|
||
_import_preamble(question["prompt"]),
|
||
code,
|
||
question["test"],
|
||
prologue,
|
||
f"check({entry_point})",
|
||
] if part)
|
||
|
||
|
||
def pass_at_k(n: int, c: int, k: int) -> float:
|
||
"""Unbiased pass@k estimator from the Codex paper (arXiv:2107.03374).
|
||
|
||
n samples drawn, c of them correct. Computed as 1 - C(n-c,k)/C(n,k) via a
|
||
product so it stays stable for large n.
|
||
"""
|
||
if n - c < k:
|
||
return 1.0
|
||
prod = 1.0
|
||
for i in range(n - c + 1, n + 1):
|
||
prod *= 1.0 - k / i
|
||
return 1.0 - prod
|
||
|
||
|
||
class HumanEvalDataset(BaseDataset):
|
||
"""OpenAI HumanEval, 164 hand-written Python problems.
|
||
|
||
Every problem in this suite imports only from the standard library, so the
|
||
execution venv needs no pip installs at all -- see `required_packages`.
|
||
"""
|
||
|
||
HF_REPO = "openai/openai_humaneval"
|
||
|
||
def __init__(self, source: Optional[str] = None, split: str = "test"):
|
||
self.source = source or self.HF_REPO
|
||
self.split = split
|
||
self.questions: List[Dict] = []
|
||
self._load_dataset()
|
||
|
||
def _load_dataset(self):
|
||
print(f"Loading HumanEval dataset (source: {self.source})...")
|
||
|
||
local = Path(self.source)
|
||
if local.exists():
|
||
rows = [json.loads(ln) for ln in local.read_text().splitlines() if ln.strip()]
|
||
else:
|
||
from datasets import load_dataset
|
||
rows = [dict(r) for r in load_dataset(self.source, split=self.split)]
|
||
|
||
required = {"task_id", "prompt", "entry_point", "test"}
|
||
for row in rows:
|
||
missing = required - row.keys()
|
||
if missing:
|
||
raise ValueError(
|
||
f"{self.source}: record {row.get('task_id', '?')} is missing {sorted(missing)}"
|
||
)
|
||
row["dataset_type"] = "humaneval"
|
||
self.questions.append(row)
|
||
|
||
print(f"HumanEval dataset loaded: {len(self.questions)} problems")
|
||
|
||
def required_packages(self) -> List[str]:
|
||
"""Third-party pip packages this suite needs. Derived, not hardcoded."""
|
||
return sorted(third_party_imports(
|
||
"\n".join(q["prompt"] + "\n" + q["test"] for q in self.questions)
|
||
))
|
||
|
||
def extract(self, response: str, question: Dict) -> str:
|
||
return extract_code(response, question.get("entry_point"))
|
||
|
||
def build_program(self, question: Dict, completion: str,
|
||
seed: Optional[int] = None) -> str:
|
||
return build_program(question, completion, seed=seed)
|
||
|
||
def get_question(self, index: int) -> Dict:
|
||
return self.questions[index]
|
||
|
||
def get_question_text(self, question: Dict) -> str:
|
||
return question["prompt"]
|
||
|
||
def get_answer(self, question: Dict) -> str:
|
||
return json.dumps({
|
||
"task_id": question["task_id"],
|
||
"entry_point": question["entry_point"],
|
||
"prompt": question["prompt"],
|
||
"test": question["test"],
|
||
})
|
||
|
||
def get_prompt(self, question: Dict) -> str:
|
||
return TEMPLATE_REGISTRY["humaneval"].format(
|
||
question=self.get_question_text(question).rstrip(),
|
||
)
|
||
|
||
|
||
class ClassEvalDataset(BaseDataset):
|
||
"""ClassEval: 100 class-level Python generation tasks.
|
||
|
||
A task is a whole class judged by several independent unittest classes, so
|
||
it is scored twice -- class level (every test passes) and method level
|
||
(what fraction of tests passed), matching the upstream protocol.
|
||
"""
|
||
|
||
HF_REPO = "ilintar/ClassEval"
|
||
|
||
def __init__(self, source: Optional[str] = None):
|
||
self.source = source or self.HF_REPO
|
||
self.questions: List[Dict] = []
|
||
self._gensim_shim = False
|
||
self._load_dataset()
|
||
|
||
def _load_dataset(self):
|
||
print(f"Loading ClassEval dataset (source: {self.source})...")
|
||
|
||
local = Path(self.source)
|
||
if local.exists():
|
||
text = local.read_text()
|
||
rows = (json.loads(text) if local.suffix == ".json"
|
||
else [json.loads(ln) for ln in text.splitlines() if ln.strip()])
|
||
else:
|
||
from datasets import load_dataset
|
||
rows = [dict(r) for r in load_dataset(self.source, split="test")]
|
||
|
||
required = {"task_id", "skeleton", "test", "class_name"}
|
||
for row in rows:
|
||
missing = required - row.keys()
|
||
if missing:
|
||
raise ValueError(
|
||
f"{self.source}: record {row.get('task_id', '?')} is missing {sorted(missing)}"
|
||
)
|
||
row["dataset_type"] = "classeval"
|
||
row["n_tests"] = _count_test_methods(row["test"])
|
||
self.questions.append(row)
|
||
|
||
print(f"ClassEval dataset loaded: {len(self.questions)} classes, "
|
||
f"{sum(q['n_tests'] for q in self.questions)} test methods")
|
||
|
||
EXTRA_PACKAGES = [
|
||
"lxml",
|
||
"scipy",
|
||
]
|
||
|
||
def configure_python(self, python: str):
|
||
"""gensim stops at cp313, so on 3.14+ swap it for a small local shim."""
|
||
try:
|
||
r = subprocess.run(
|
||
[python, "-c", "import sys; print('%d.%d' % sys.version_info[:2])"],
|
||
capture_output=True, text=True, timeout=60,
|
||
)
|
||
major, minor = (int(x) for x in r.stdout.strip().split("."))
|
||
except Exception:
|
||
return
|
||
self._gensim_shim = (major, minor) >= (3, 14)
|
||
|
||
def required_packages(self) -> List[str]:
|
||
blob = "\n\n".join(
|
||
"\n\n".join(_as_text(q.get(f)) for f in
|
||
("skeleton", "test", "solution_code", "import_statement"))
|
||
for q in self.questions
|
||
)
|
||
found = {PIP_NAMES.get(m, m) for m in third_party_imports(blob)}
|
||
found |= set(self.EXTRA_PACKAGES)
|
||
if self._gensim_shim:
|
||
found.discard("gensim")
|
||
return sorted(found)
|
||
|
||
NLTK_CORPORA = ("punkt_tab", "averaged_perceptron_tagger_eng", "wordnet")
|
||
|
||
def provision_steps(self) -> List[str]:
|
||
steps = []
|
||
|
||
if any("nltk" in _as_text(q.get("skeleton")) + _as_text(q.get("test"))
|
||
for q in self.questions):
|
||
steps.append(
|
||
"import os, nltk\n"
|
||
"target = os.path.join(os.environ['SANDBOX_VENV'], 'nltk_data')\n"
|
||
"os.makedirs(target, exist_ok=True)\n"
|
||
f"for corpus in {self.NLTK_CORPORA!r}:\n"
|
||
" nltk.download(corpus, download_dir=target, quiet=True)\n"
|
||
)
|
||
|
||
if self._gensim_shim:
|
||
steps.append(
|
||
"import os, sysconfig\n"
|
||
"pkg = os.path.join(sysconfig.get_paths()['purelib'], 'gensim')\n"
|
||
"os.makedirs(pkg, exist_ok=True)\n"
|
||
f"files = {GENSIM_SHIM!r}\n"
|
||
"for name, body in files.items():\n"
|
||
" open(os.path.join(pkg, name), 'w').write(body)\n"
|
||
"import gensim.utils, gensim.matutils\n"
|
||
)
|
||
|
||
return steps
|
||
|
||
def sandbox_env(self) -> Dict[str, str]:
|
||
return {"NLTK_DATA": "{venv}/nltk_data"}
|
||
|
||
def preferred_python(self) -> Optional[str]:
|
||
return "3.13"
|
||
|
||
def default_exec_timeout(self) -> float:
|
||
return 30.0
|
||
|
||
def extract(self, response: str, question: Dict) -> str:
|
||
return extract_code(response, class_name=question.get("class_name"))
|
||
|
||
def build_program(self, question: Dict, completion: str,
|
||
seed: Optional[int] = None) -> str:
|
||
prologue = f"import random; random.seed({seed})" if seed is not None else ""
|
||
return "\n\n".join(part for part in [
|
||
_as_text(question.get("import_statement")),
|
||
UNITTEST_PROLOGUE,
|
||
completion,
|
||
question["test"],
|
||
prologue,
|
||
UNITTEST_RUNNER,
|
||
] if part)
|
||
|
||
def interpret(self, result: Any, question: Dict) -> Tuple[bool, Dict[str, Any]]:
|
||
expected = question.get("n_tests", 0)
|
||
|
||
report = parse_test_report(result.stdout)
|
||
if report is None:
|
||
return False, {
|
||
"status": result.status if result.status != "ok" else "no-report",
|
||
"reason": result.summary(),
|
||
"tests_total": expected,
|
||
"tests_passed": 0,
|
||
"duration": round(result.duration, 3),
|
||
}
|
||
|
||
total, passed = report.get("total", 0) or expected, report.get("passed", 0)
|
||
ok = total > 0 and passed == total
|
||
return ok, {
|
||
"status": "ok" if ok else "failed",
|
||
"reason": "ok" if ok else (report.get("first_error")
|
||
or f"{passed}/{total} tests passed")[:200],
|
||
"tests_total": total,
|
||
"tests_passed": passed,
|
||
"duration": round(result.duration, 3),
|
||
}
|
||
|
||
def get_question(self, index: int) -> Dict:
|
||
return self.questions[index]
|
||
|
||
def get_question_text(self, question: Dict) -> str:
|
||
return question["skeleton"]
|
||
|
||
def get_answer(self, question: Dict) -> str:
|
||
return json.dumps({
|
||
"task_id": question["task_id"],
|
||
"class_name": question["class_name"],
|
||
"import_statement": _as_text(question.get("import_statement")),
|
||
"test": question["test"],
|
||
"n_tests": question["n_tests"],
|
||
})
|
||
|
||
def get_prompt(self, question: Dict) -> str:
|
||
return TEMPLATE_REGISTRY["classeval"].format(
|
||
question=self.get_question_text(question).rstrip(),
|
||
)
|
||
|
||
|
||
class AgenticDataset(BaseDataset):
|
||
"""Multi-turn tool-driven repair tasks over a small real repository.
|
||
|
||
The other code suites answer one prompt. Here the model is handed a tool
|
||
set and drives its own conversation, so the Processor delegates the whole
|
||
episode rather than making a single request.
|
||
|
||
Everything language-specific is imported lazily and provisioned per
|
||
language, so selecting the Python tasks never installs a node toolchain --
|
||
and running any other suite installs none of it.
|
||
"""
|
||
|
||
HF_REPO = "ilintar/SACB"
|
||
is_agentic = True
|
||
ae: Any
|
||
|
||
def __init__(self, source: Optional[str] = None, lang: Optional[str] = None,
|
||
max_turns: int = 50, context_limit: Optional[int] = None,
|
||
max_tokens: Optional[int] = None):
|
||
self.source = source or self.HF_REPO
|
||
self.lang_filter = lang
|
||
self.max_turns = max_turns
|
||
self.context_limit = context_limit
|
||
self.max_tokens = max_tokens
|
||
self._sandboxes: Dict[str, Any] = {}
|
||
self._load_dataset()
|
||
|
||
def _load_dataset(self):
|
||
import agentic_eval as ae
|
||
self.ae = ae
|
||
|
||
print(f"Loading agentic dataset (source: {self.source})...")
|
||
path = Path(self.source)
|
||
if path.exists():
|
||
rows = [json.loads(line) for line in path.read_text().splitlines()
|
||
if line.strip()]
|
||
else:
|
||
from datasets import load_dataset
|
||
rows = [dict(r) for r in load_dataset(self.source, split="test")]
|
||
|
||
if self.lang_filter:
|
||
rows = [r for r in rows if r.get("lang") == self.lang_filter]
|
||
if not rows:
|
||
raise ValueError(
|
||
f"no agentic tasks in {self.source}"
|
||
+ (f" for language {self.lang_filter!r}" if self.lang_filter else ""))
|
||
|
||
self.tasks = [ae.AgenticTask.from_record(r) for r in rows]
|
||
self.languages = sorted({t.lang for t in self.tasks})
|
||
print(f"Agentic dataset loaded: {len(self.tasks)} tasks "
|
||
f"({', '.join(self.languages)})")
|
||
|
||
def __len__(self) -> int:
|
||
return len(self.tasks)
|
||
|
||
def get_question(self, index: int) -> Dict:
|
||
return {"index": index}
|
||
|
||
def get_question_text(self, question: Dict) -> str:
|
||
task = self.tasks[question["index"]]
|
||
first = task.instruction.strip().splitlines()[0]
|
||
return f"[{task.task_id}] {first}"
|
||
|
||
def get_answer(self, question: Dict) -> str:
|
||
task = self.tasks[question["index"]]
|
||
return json.dumps({"task_id": task.task_id, "index": question["index"]})
|
||
|
||
def get_prompt(self, question: Dict) -> str:
|
||
return self.ae.user_prompt(self.tasks[question["index"]])
|
||
|
||
def default_exec_timeout(self) -> float:
|
||
return 90.0
|
||
|
||
def required_packages(self) -> List[str]:
|
||
return []
|
||
|
||
def prepare(self, base_python: Optional[str] = None, force: bool = False):
|
||
"""Provision one sandbox per language present in the selected tasks."""
|
||
for lang in self.languages:
|
||
sandbox = self.ae.make_sandbox(lang, Sandbox, base_python=base_python)
|
||
sandbox.ensure(force=force)
|
||
self._sandboxes[lang] = sandbox
|
||
return self._sandboxes
|
||
|
||
def run(self, index: int, chat) -> Dict[str, Any]:
|
||
task = self.tasks[index]
|
||
return self.ae.run_task(
|
||
task, chat, self._sandboxes[task.lang],
|
||
max_turns=self.max_turns, context_limit=self.context_limit,
|
||
test_timeout=self.default_exec_timeout())
|
||
|
||
|
||
def _as_text(value: Any) -> str:
|
||
"""Some dataset fields arrive as a list of lines rather than a blob."""
|
||
if not value:
|
||
return ""
|
||
if isinstance(value, (list, tuple)):
|
||
return "\n".join(str(v) for v in value)
|
||
return str(value)
|
||
|
||
|
||
def _count_test_methods(test_code: str) -> int:
|
||
"""How many `def test_*` a task's test suite defines."""
|
||
try:
|
||
tree = ast.parse(test_code)
|
||
except SyntaxError:
|
||
return len(re.findall(r"^\s*def\s+test\w*\s*\(", test_code, re.M))
|
||
return sum(
|
||
1
|
||
for node in ast.walk(tree)
|
||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||
and node.name.startswith("test")
|
||
)
|
||
|
||
|
||
def third_party_imports(source: str) -> set:
|
||
"""Top-level modules imported by `source` that are not in the stdlib."""
|
||
import ast
|
||
|
||
mods = set()
|
||
for chunk in source.split("\n\n"):
|
||
try:
|
||
tree = ast.parse(chunk)
|
||
except SyntaxError:
|
||
continue
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.Import):
|
||
mods.update(a.name.split(".")[0] for a in node.names)
|
||
elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
|
||
mods.add(node.module.split(".")[0])
|
||
|
||
stdlib = set(sys.stdlib_module_names)
|
||
return {m for m in mods if m and m not in stdlib}
|
||
|
||
|
||
class Grader:
|
||
def __init__(
|
||
self,
|
||
grader_type: str = "llm",
|
||
grader_script: Optional[str] = None,
|
||
grader_model_name: Optional[str] = None,
|
||
grader_server_url: str = "",
|
||
grader_api_key: Optional[str] = None,
|
||
dataset_type: str = "aime",
|
||
sandbox: Optional[Sandbox] = None,
|
||
exec_timeout: float = 15.0,
|
||
exec_seed: Optional[int] = 0,
|
||
dataset: Optional[BaseDataset] = None,
|
||
):
|
||
self.grader_type = grader_type
|
||
self.grader_script = grader_script
|
||
self.grader_model_name = grader_model_name
|
||
self.grader_server_url = grader_server_url
|
||
self.grader_api_key = grader_api_key
|
||
self.dataset_type = dataset_type
|
||
self.sandbox = sandbox
|
||
self.exec_timeout = exec_timeout
|
||
self.exec_seed = exec_seed
|
||
self.dataset = dataset
|
||
self.pattern = self._get_pattern()
|
||
self._local = threading.local()
|
||
|
||
@property
|
||
def last_exec(self) -> Dict[str, Any]:
|
||
return getattr(self._local, "exec_info", {})
|
||
|
||
def _get_pattern(self) -> Optional[str]:
|
||
if self.grader_type == "regex":
|
||
return GRADER_PATTERNS.get(self.dataset_type) # Use dataset_type as key
|
||
return None
|
||
|
||
def _extract_answer_regex(self, pred: str) -> Optional[str]:
|
||
"""Extract answer using regex pattern"""
|
||
if not self.pattern:
|
||
return None
|
||
|
||
# For AIME datasets, prioritize boxed answers
|
||
if self.dataset_type in ["aime", "aime2025"]:
|
||
boxed_pattern = r'\\boxed{([^}]+)}'
|
||
boxed_matches = re.findall(boxed_pattern, pred, re.IGNORECASE)
|
||
if boxed_matches:
|
||
# Return the last boxed answer found (most likely the final answer)
|
||
return boxed_matches[-1].strip()
|
||
|
||
# For other datasets, search for numbers from the end of the text
|
||
# This prioritizes numbers that appear later in the response
|
||
matches = re.findall(self.pattern, pred, re.IGNORECASE)
|
||
if not matches:
|
||
return None
|
||
|
||
# Process matches from end to start
|
||
for match in reversed(matches):
|
||
if isinstance(match, tuple):
|
||
match = match[0] if match[0] else match[1]
|
||
answer = match.strip()
|
||
if answer:
|
||
return answer
|
||
return None
|
||
|
||
def _grade_regex(self, gold: str, pred: str) -> Tuple[bool, Optional[str]]:
|
||
"""Grade using regex pattern matching"""
|
||
answer = self._extract_answer_regex(pred)
|
||
if answer is None:
|
||
return False, None
|
||
is_correct = answer.strip() == gold.strip()
|
||
return is_correct, answer
|
||
|
||
def _grade_cli(self, gold: str, pred: str) -> Tuple[bool, Optional[str]]:
|
||
"""Grade using external CLI script"""
|
||
if not self.grader_script:
|
||
raise ValueError("CLI grader requires --grader-script")
|
||
|
||
script_path = Path(self.grader_script)
|
||
if not script_path.exists():
|
||
raise FileNotFoundError(f"Grader script not found: {self.grader_script}")
|
||
|
||
try:
|
||
result = subprocess.run(
|
||
[str(script_path), "--answer", pred, "--expected", gold],
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=30
|
||
)
|
||
is_correct = result.returncode == 0
|
||
answer = pred if is_correct else None
|
||
return is_correct, answer
|
||
except subprocess.TimeoutExpired:
|
||
return False, None
|
||
except Exception as e:
|
||
return False, None
|
||
|
||
def _grade_llm(self, gold: str, pred: str, problem: str) -> Tuple[bool, Optional[str]]:
|
||
"""Grade using LLM-based extraction with few-shot examples"""
|
||
sample_answers = SAMPLE_ANSWERS.get(self.dataset_type, [])
|
||
sample_examples = "\n".join([
|
||
f"Example {i+1}: {ans}" for i, ans in enumerate(sample_answers)
|
||
])
|
||
|
||
system_prompt = f"""You are an answer extraction system. Your task is to extract the answer from the model's response.
|
||
|
||
Here are some examples of extracted answers to demonstrate what you are supposed to output:
|
||
|
||
{sample_examples}
|
||
|
||
When extracting the answer, provide only the extracted answer itself, nothing else. If there is no clear answer that can be extracted from the response, reply with 'no answer'."""
|
||
|
||
user_prompt = f"""Extract the answer from the following response:
|
||
|
||
"{pred}"
|
||
|
||
Please provide only the extracted answer, nothing else. If there is no clear answer that can be extracted from the response, reply with 'no answer'."""
|
||
|
||
url = f"{self.grader_server_url}/v1/chat/completions"
|
||
headers = auth_headers(self.grader_api_key)
|
||
data = {
|
||
"model": self.grader_model_name,
|
||
"messages": [
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": user_prompt}
|
||
],
|
||
"temperature": 0,
|
||
}
|
||
#print(json.dumps(data, indent=2))
|
||
|
||
try:
|
||
response = requests.post(url, headers=headers, json=data)
|
||
response.raise_for_status()
|
||
answer = response.json()["choices"][0]["message"]["content"].strip()
|
||
is_correct = answer.strip().lower() == gold.strip().lower()
|
||
return is_correct, answer
|
||
except Exception as e:
|
||
return False, None
|
||
|
||
def _truncate_response(self, response: str, max_lines: int = 6) -> str:
|
||
"""Keep only last N lines of response"""
|
||
lines = response.split('\n')
|
||
return '\n'.join(lines[-max_lines:]) if len(lines) > max_lines else response
|
||
|
||
def _grade_exec(self, gold: str, pred: str) -> Tuple[bool, Optional[str]]:
|
||
"""Grade by running the generated code against the suite's own tests.
|
||
|
||
`gold` is the JSON harness produced by the dataset's get_answer(); the
|
||
dataset owns how the program is assembled and how its result is read.
|
||
"""
|
||
if self.sandbox is None or self.dataset is None:
|
||
raise ValueError("exec grader requires a sandbox and a dataset")
|
||
|
||
question = json.loads(gold)
|
||
completion = self.dataset.extract(pred, question)
|
||
if not completion.strip():
|
||
self._local.exec_info = {"status": "empty", "reason": "no code in reply"}
|
||
return False, None
|
||
|
||
program = self.dataset.build_program(question, completion, seed=self.exec_seed)
|
||
result = self.sandbox.run(program, timeout=self.exec_timeout)
|
||
passed, detail = self.dataset.interpret(result, question)
|
||
|
||
self._local.exec_info = detail
|
||
return passed, completion
|
||
|
||
def grade(self, gold: str, pred: str, problem: str = "") -> Tuple[bool, Optional[str]]:
|
||
"""Grade the response"""
|
||
if self.grader_type == "regex":
|
||
return self._grade_regex(gold, pred)
|
||
elif self.grader_type == "cli":
|
||
return self._grade_cli(gold, pred)
|
||
elif self.grader_type == "llm":
|
||
return self._grade_llm(gold, pred, problem)
|
||
elif self.grader_type == "exec":
|
||
return self._grade_exec(gold, pred)
|
||
else:
|
||
raise ValueError(f"Unknown grader type: {self.grader_type}")
|
||
|
||
class Processor:
|
||
def __init__(
|
||
self,
|
||
server_configs: List[ServerConfig],
|
||
grader: Grader,
|
||
model_name: Optional[str] = None,
|
||
n_predict: int = -1,
|
||
retries: int = 5,
|
||
retry_delay: float = 10.0,
|
||
transcript_dir: Optional[Path] = None,
|
||
):
|
||
self.server_configs = server_configs
|
||
self.grader = grader
|
||
self.model_name = model_name
|
||
self.n_predict = n_predict
|
||
self.retries = retries
|
||
self.retry_delay = retry_delay
|
||
self.transcript_dir = transcript_dir
|
||
|
||
@staticmethod
|
||
def _check_server(server_config: ServerConfig) -> List[str]:
|
||
url = f"{server_config.url}/v1/models"
|
||
try:
|
||
response = requests.get(url, headers=server_config.headers())
|
||
response.raise_for_status()
|
||
models = [m["id"] for m in response.json().get("data", [])]
|
||
return models
|
||
except Exception as e:
|
||
print(f"Error: Cannot reach server {server_config.name} ({server_config.url}): {e}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
def _make_request(
|
||
self, server_config: ServerConfig, eval_state: EvalState, prompt: str,
|
||
session_id: Optional[str] = None
|
||
) -> Tuple[Dict[str, Any], int, Optional[float], Optional[float], str]:
|
||
url = f"{server_config.url}/v1/chat/completions"
|
||
headers = server_config.headers(session_id)
|
||
data = {
|
||
"model": self.model_name if self.model_name else "llama",
|
||
"messages": [{"role": "user", "content": prompt}],
|
||
"n_predict": self.n_predict
|
||
}
|
||
if eval_state.sampling_config.get("temperature") is not None:
|
||
data["temperature"] = eval_state.sampling_config["temperature"]
|
||
if eval_state.sampling_config.get("top_k") is not None:
|
||
data["top_k"] = eval_state.sampling_config["top_k"]
|
||
if eval_state.sampling_config.get("top_p") is not None:
|
||
data["top_p"] = eval_state.sampling_config["top_p"]
|
||
if eval_state.sampling_config.get("min_p") is not None:
|
||
data["min_p"] = eval_state.sampling_config["min_p"]
|
||
if eval_state.sampling_config.get("reasoning_effort") is not None:
|
||
data["reasoning_effort"] = eval_state.sampling_config["reasoning_effort"]
|
||
|
||
result = post_with_retry(url, headers, data, self.retries, self.retry_delay)
|
||
tokens = result.get("usage", {}).get("completion_tokens", 0)
|
||
timings = result.get("timings", {})
|
||
tps_gen = timings.get("predicted_per_second") if timings else None
|
||
t_gen_ms = timings.get("predicted_ms") if timings else None
|
||
finish_reason = result.get("choices", [{}])[0].get("finish_reason", "stop")
|
||
return result, tokens, tps_gen, t_gen_ms, finish_reason
|
||
|
||
def _process_single_case(
|
||
self, server_config: ServerConfig, eval_state: EvalState, i: int, task_id: str
|
||
) -> TaskState:
|
||
question_text, prompt, expected = eval_state.get_case(i)
|
||
|
||
# Extract chunk_idx from task_id: "{dataset_type}_{chunk_idx:03d}_{index:03d}"
|
||
_parts = task_id.rsplit("_", 2)
|
||
chunk_idx = int(_parts[-2]) if len(_parts) >= 3 else 0
|
||
problem_idx = i
|
||
|
||
task_state = TaskState(
|
||
task_id=task_id,
|
||
prompt=prompt,
|
||
expected=expected,
|
||
question_text=question_text,
|
||
server_name=server_config.name,
|
||
chunk_idx=chunk_idx,
|
||
problem_idx=problem_idx,
|
||
)
|
||
|
||
session_id = new_session_id(task_id)
|
||
|
||
if eval_state.loaded_dataset.is_agentic:
|
||
return self._process_agentic_case(
|
||
server_config, eval_state, i, task_id, task_state, session_id)
|
||
|
||
try:
|
||
response, tokens, tps_gen, t_gen_ms, finish_reason = self._make_request(server_config, eval_state, prompt, session_id)
|
||
result = response["choices"][0]["message"]["content"]
|
||
reasoning_content = response["choices"][0].get("message", {}).get("reasoning_content")
|
||
task_state.response = result
|
||
task_state.tokens = tokens
|
||
task_state.tps_gen = tps_gen
|
||
task_state.t_gen_ms = t_gen_ms
|
||
task_state.reasoning_content = reasoning_content
|
||
|
||
if finish_reason != "stop":
|
||
task_state.status = f"error: finish_reason={finish_reason}"
|
||
eval_state.add_result(
|
||
task_id, prompt, expected, result, None,
|
||
{"finish_reason": finish_reason}, False, task_state.status,
|
||
tokens, tps_gen, t_gen_ms, reasoning_content, server_config.name,
|
||
chunk_idx, problem_idx,
|
||
)
|
||
eval_state.dump()
|
||
return task_state
|
||
|
||
if self.grader.grader_type == "exec":
|
||
graded_text = result
|
||
else:
|
||
graded_text = self.grader._truncate_response(result, max_lines=10)
|
||
is_correct, answer = self.grader.grade(expected, graded_text, prompt)
|
||
|
||
grader_log = {
|
||
"pred": graded_text,
|
||
"grader_type": self.grader.grader_type
|
||
}
|
||
if self.grader.grader_type == "regex" and self.grader.pattern:
|
||
grader_log["pattern"] = self.grader.pattern
|
||
if self.grader.grader_type == "exec":
|
||
grader_log.update(self.grader.last_exec)
|
||
|
||
task_state.correct = is_correct
|
||
task_state.answer = answer
|
||
task_state.grader_log = grader_log
|
||
task_state.status = "ok"
|
||
|
||
eval_state.add_result(
|
||
task_id, prompt, expected, result, answer,
|
||
grader_log, is_correct, "ok",
|
||
tokens, tps_gen, t_gen_ms, reasoning_content, server_config.name,
|
||
chunk_idx, problem_idx,
|
||
)
|
||
|
||
eval_state.dump()
|
||
|
||
except ServerError as e:
|
||
task_state.status = SERVER_ERROR
|
||
task_state.grader_log = {"error": str(e)}
|
||
eval_state.add_result(
|
||
task_id, prompt, expected, None, None, task_state.grader_log,
|
||
False, SERVER_ERROR, None, None, None, None, server_config.name,
|
||
chunk_idx, problem_idx,
|
||
)
|
||
eval_state.dump()
|
||
except Exception as e:
|
||
task_state.status = f"error: {str(e)}"
|
||
|
||
return task_state
|
||
|
||
def _agentic_chat(self, server_config: ServerConfig, eval_state: EvalState,
|
||
session_id: Optional[str] = None):
|
||
"""A chat callable for agentic_eval: messages + tools in, response out."""
|
||
url = f"{server_config.url}/v1/chat/completions"
|
||
|
||
def chat(messages, tools):
|
||
data = {
|
||
"model": self.model_name if self.model_name else "llama",
|
||
"messages": messages,
|
||
"tools": tools,
|
||
"tool_choice": "auto",
|
||
}
|
||
per_turn = getattr(eval_state.dataset, "max_tokens", None)
|
||
if self.n_predict and self.n_predict > 0:
|
||
data["max_tokens"] = self.n_predict
|
||
elif per_turn:
|
||
data["max_tokens"] = per_turn
|
||
for key in ("temperature", "top_k", "top_p", "min_p", "reasoning_effort"):
|
||
value = eval_state.sampling_config.get(key)
|
||
if value is not None:
|
||
data[key] = value
|
||
return post_with_retry(url, server_config.headers(session_id), data,
|
||
self.retries, self.retry_delay, timeout=1800)
|
||
|
||
return chat
|
||
|
||
def _process_agentic_case(
|
||
self, server_config: ServerConfig, eval_state: EvalState, i: int,
|
||
task_id: str, task_state: TaskState, session_id: Optional[str] = None
|
||
) -> TaskState:
|
||
"""Run one whole tool-driven episode and grade the tree it leaves behind."""
|
||
try:
|
||
result = eval_state.loaded_dataset.run(
|
||
i, self._agentic_chat(server_config, eval_state, session_id))
|
||
except Exception as e:
|
||
task_state.status = f"error: {type(e).__name__}: {e}"
|
||
eval_state.add_result(
|
||
task_id, task_state.prompt, task_state.expected,
|
||
task_state.status, None,
|
||
{"error": f"{type(e).__name__}: {e}", "resolved": False},
|
||
False, task_state.status, 0, None, None, None,
|
||
server_config.name, task_state.chunk_idx, i,
|
||
)
|
||
eval_state.dump()
|
||
return task_state
|
||
|
||
episode = result.get("episode", {})
|
||
messages = result.pop("messages", None)
|
||
if messages is not None and self.transcript_dir is not None:
|
||
self._save_transcript(eval_state, task_id, messages, result)
|
||
|
||
if episode.get("stop_reason") in ("request_failed", "no_choices"):
|
||
task_state.status = SERVER_ERROR
|
||
task_state.grader_log = result
|
||
task_state.response = f"server error on turn {episode.get('turns')}: {episode.get('error')}"
|
||
eval_state.add_result(
|
||
task_id, task_state.prompt, task_state.expected, task_state.response,
|
||
None, result, False, SERVER_ERROR,
|
||
episode.get("completion_tokens", 0), None, None, None,
|
||
server_config.name, task_state.chunk_idx, i,
|
||
)
|
||
eval_state.dump()
|
||
return task_state
|
||
|
||
task_state.tokens = episode.get("completion_tokens", 0)
|
||
task_state.correct = bool(result["resolved"])
|
||
task_state.answer = ", ".join(result.get("changed_files", [])) or None
|
||
task_state.grader_log = result
|
||
task_state.status = "ok"
|
||
task_state.response = (
|
||
f"stop={episode.get('stop_reason')} turns={episode.get('turns')} "
|
||
f"calls={episode.get('tool_calls')} edits={episode.get('edits')} "
|
||
f"errors={episode.get('tool_errors')} nudges={episode.get('nudges')} truncated={episode.get('truncated_turns')} "
|
||
f"peak_ctx={episode.get('peak_context')}\n"
|
||
f"changed: {task_state.answer or '(nothing)'}"
|
||
)
|
||
|
||
eval_state.add_result(
|
||
task_id, task_state.prompt, task_state.expected, task_state.response,
|
||
task_state.answer, result, task_state.correct, "ok",
|
||
task_state.tokens, None, None, None, server_config.name,
|
||
task_state.chunk_idx, i,
|
||
)
|
||
eval_state.dump()
|
||
return task_state
|
||
|
||
def _save_transcript(self, eval_state: EvalState, task_id: str,
|
||
messages: List[Dict[str, Any]], result: Dict[str, Any]):
|
||
"""Write the episode's messages as <task_id>.json plus a readable .html."""
|
||
assert self.transcript_dir is not None
|
||
ep = result.get("episode", {})
|
||
verdict = "RESOLVED" if result.get("resolved") else "not resolved"
|
||
meta = (
|
||
f"{verdict} f2p {result.get('f2p_passed')}/{result.get('f2p_total')} "
|
||
f"p2p {result.get('p2p_passed')}/{result.get('p2p_total')}\n"
|
||
f"stop={ep.get('stop_reason')} turns={ep.get('turns')} "
|
||
f"calls={ep.get('tool_calls')} edits={ep.get('edits')} "
|
||
f"errors={ep.get('tool_errors')} nudges={ep.get('nudges')} truncated={ep.get('truncated_turns')} "
|
||
f"prompt_tokens={ep.get('prompt_tokens')} "
|
||
f"completion_tokens={ep.get('completion_tokens')} "
|
||
f"peak_ctx={ep.get('peak_context')}\n"
|
||
f"changed: {', '.join(result.get('changed_files') or []) or '(nothing)'}"
|
||
+ (f"\nerror: {ep.get('error')}" if ep.get("error") else "")
|
||
)
|
||
title = f"{task_id} ({result.get('task_id')})"
|
||
dataset = eval_state.loaded_dataset
|
||
assert isinstance(dataset, AgenticDataset)
|
||
try:
|
||
path = dataset.ae.write_transcript(
|
||
messages, self.transcript_dir / task_id, title, meta)
|
||
except OSError as e:
|
||
print(f"Warning: could not write transcript for {task_id}: {e}")
|
||
return
|
||
base = Path(eval_state.output_file).resolve().parent
|
||
result["transcript"] = os.path.relpath(path.resolve(), base)
|
||
|
||
@staticmethod
|
||
def _worker(
|
||
server_config: ServerConfig,
|
||
processor: "Processor",
|
||
eval_state: EvalState,
|
||
task_queue: Queue,
|
||
results_queue: Queue,
|
||
):
|
||
"""Worker that pulls tasks from a shared queue and sends them to its server."""
|
||
while True:
|
||
task = task_queue.get()
|
||
if task is None: # sentinel
|
||
task_queue.task_done()
|
||
break
|
||
try:
|
||
i, task_id = task
|
||
result = processor._process_single_case(server_config, eval_state, i, task_id)
|
||
results_queue.put(result)
|
||
finally:
|
||
task_queue.task_done()
|
||
|
||
def evaluate(self, eval_state: EvalState, verbose: bool = False, resume: bool = False):
|
||
total_tasks = len(eval_state.tasks)
|
||
eval_state.total = len(eval_state.all_tasks) if eval_state.all_tasks else total_tasks
|
||
eval_state.processed = 0
|
||
start_time = time.time()
|
||
|
||
# Check servers and list models
|
||
server_models = [self._check_server(sc) for sc in self.server_configs]
|
||
|
||
# Print server info
|
||
print(f"\nProcessing {len(eval_state.tasks)} {eval_state.dataset_type.upper()} tasks ...")
|
||
print(f"Servers ({len(self.server_configs)}):")
|
||
for i, sc in enumerate(self.server_configs):
|
||
models_str = ", ".join(server_models[i]) if server_models[i] else "(none)"
|
||
print(f" {i+1}. {sc.name} — {sc.url} ({sc.threads} threads) [{models_str}]")
|
||
print(f"Model: {self.model_name}")
|
||
print(f"Grader: {self.grader.grader_type}")
|
||
print(f"Sampling: temp={eval_state.sampling_config.get('temperature', 'skip')}, top-k={eval_state.sampling_config.get('top_k', 'skip')}, top-p={eval_state.sampling_config.get('top_p', 'skip')}, min-p={eval_state.sampling_config.get('min_p', 'skip')}, reasoning-effort={eval_state.sampling_config.get('reasoning_effort', 'skip')}")
|
||
print()
|
||
|
||
# Shared task queue: all workers compete for tasks
|
||
task_queue: Queue = Queue()
|
||
for i, task_id in eval_state.tasks:
|
||
task_queue.put((i, task_id))
|
||
|
||
# Results queue: workers push completed TaskStates here
|
||
results_queue: Queue = Queue()
|
||
|
||
# Total worker threads across all servers
|
||
total_threads = sum(sc.threads for sc in self.server_configs)
|
||
|
||
# Add one sentinel per worker so every worker exits cleanly
|
||
for _ in range(total_threads):
|
||
task_queue.put(None)
|
||
|
||
# Launch workers: one ThreadPoolExecutor per server
|
||
executors: List[ThreadPoolExecutor] = []
|
||
worker_futures: List[Any] = []
|
||
for server_config in self.server_configs:
|
||
executor = ThreadPoolExecutor(max_workers=server_config.threads)
|
||
executors.append(executor)
|
||
for _ in range(server_config.threads):
|
||
future = executor.submit(
|
||
self._worker, server_config, self, eval_state,
|
||
task_queue, results_queue
|
||
)
|
||
worker_futures.append(future)
|
||
|
||
# Drain results as they complete
|
||
n_correct = 0
|
||
session_time = 0.0
|
||
completed_count = 0
|
||
|
||
while completed_count < total_tasks:
|
||
task_state = results_queue.get()
|
||
eval_state.processed += 1
|
||
completed_count += 1
|
||
if task_state.correct:
|
||
n_correct += 1
|
||
elapsed = time.time() - start_time
|
||
eval_state.total_time += elapsed
|
||
session_time += elapsed
|
||
start_time = time.time()
|
||
eval_state.print_progress(task_state, total_tasks, n_correct)
|
||
|
||
if verbose:
|
||
print(f"\nCase {eval_state.processed}: {task_state.correct}")
|
||
print(f" Expected: {task_state.expected}")
|
||
if task_state.response:
|
||
print(f" Response: {task_state.response}")
|
||
if task_state.answer:
|
||
print(f" Answer: {task_state.answer}")
|
||
print(f" Status: {task_state.status}")
|
||
|
||
# Wait for all workers to finish and shut down executors
|
||
for future in worker_futures:
|
||
future.result()
|
||
for executor in executors:
|
||
executor.shutdown(wait=True)
|
||
|
||
print(f"\nSession time: {session_time:.1f}s | Total accumulated time: {eval_state.total_time:.1f}s")
|
||
eval_state.print_summary()
|
||
eval_state.dump()
|
||
|
||
def preflight_sandbox(sandbox: Sandbox):
|
||
"""Catch host policies that would fail problems for non-model reasons.
|
||
|
||
A FIPS-enforcing OpenSSL makes hashlib.md5() raise, which would sink
|
||
HumanEval/162 no matter what the model generated.
|
||
"""
|
||
probe = sandbox.run("import hashlib; hashlib.md5(b'x').hexdigest()", timeout=15)
|
||
if not probe.passed:
|
||
print(f"Warning: md5 is unavailable in the execution venv "
|
||
f"({probe.summary(120)}). Problems that hash with md5 will fail "
|
||
f"regardless of the model -- this is a host policy, not a model result.")
|
||
|
||
|
||
def print_pass_at_k(eval_state: "EvalState"):
|
||
"""Report pass@k plus a breakdown of how the failures failed.
|
||
|
||
Each chunk is one full pass over the suite, so running --n_cases at a
|
||
multiple of the dataset size gives n samples per problem for free.
|
||
"""
|
||
cases = eval_state.task_states.get("cases", {})
|
||
if not cases:
|
||
return
|
||
|
||
by_problem: Dict[int, List[bool]] = {}
|
||
reasons: Dict[str, int] = {}
|
||
for case in cases.values():
|
||
by_problem.setdefault(case.get("problem_idx", 0), []).append(
|
||
bool(case.get("correct", False))
|
||
)
|
||
if not case.get("correct", False):
|
||
status = (case.get("grader_log") or {}).get("status", case.get("status", "?"))
|
||
reasons[status] = reasons.get(status, 0) + 1
|
||
|
||
n = min(len(v) for v in by_problem.values())
|
||
n_problems = len(by_problem)
|
||
|
||
print(f"\n{'=' * 60}")
|
||
print(f" {eval_state.dataset_type}: {n_problems} problems, {n} sample(s) each")
|
||
print(f"{'=' * 60}")
|
||
|
||
for k in sorted({1, 5, 10, 100, n}):
|
||
if k > n:
|
||
continue
|
||
score = sum(
|
||
pass_at_k(len(v), sum(v), k) for v in by_problem.values()
|
||
) / n_problems
|
||
print(f" pass@{k:<4d} {score:.4f}")
|
||
|
||
if n == 1:
|
||
solved = sum(1 for v in by_problem.values() if v[0])
|
||
lo, hi = wilson_interval(solved, n_problems)
|
||
print(f" {solved}/{n_problems} solved 95% CI [{lo:.4f}, {hi:.4f}]")
|
||
|
||
t_total = t_passed = 0
|
||
for case in cases.values():
|
||
log = case.get("grader_log") or {}
|
||
if "tests_total" in log:
|
||
t_total += log["tests_total"]
|
||
else:
|
||
try:
|
||
t_total += json.loads(case.get("expected") or "{}").get("n_tests", 0)
|
||
except ValueError:
|
||
pass
|
||
t_passed += log.get("tests_passed", 0)
|
||
if t_total:
|
||
print(f"\n test methods passed {t_passed}/{t_total} ({t_passed / t_total:.4f})")
|
||
|
||
if reasons:
|
||
print("\n failures by cause:")
|
||
for status, count in sorted(reasons.items(), key=lambda kv: -kv[1]):
|
||
print(f" {status:<12s} {count}")
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(
|
||
description="Simplified evaluation tool for llama.cpp"
|
||
)
|
||
parser.add_argument(
|
||
"--server",
|
||
type=str,
|
||
default="http://localhost:8033",
|
||
help="Comma-separated llama-server URLs (default: http://localhost:8033)"
|
||
)
|
||
parser.add_argument(
|
||
"--server-name",
|
||
type=str,
|
||
default="",
|
||
help="Comma-separated display names for servers (default: use URLs)"
|
||
)
|
||
parser.add_argument(
|
||
"--api-key",
|
||
type=str,
|
||
default=None,
|
||
help="Bearer token for the eval servers: one key for all of them, or a "
|
||
"comma-separated key per --server (default: $LLAMA_API_KEY, else none)"
|
||
)
|
||
parser.add_argument(
|
||
"--dataset",
|
||
type=str,
|
||
default="aime",
|
||
choices=["aime", "aime2025", "aime2026", "gsm8k", "gpqa", "humaneval",
|
||
"classeval", "agentic"],
|
||
help="Dataset type (default: aime)"
|
||
)
|
||
parser.add_argument(
|
||
"--dataset-source",
|
||
type=str,
|
||
default=None,
|
||
help="Override the dataset location: a local .jsonl path or a HuggingFace "
|
||
"repo id (e.g. a patched HumanEval). Default: the suite's canonical repo"
|
||
)
|
||
parser.add_argument(
|
||
"--n_cases",
|
||
type=int,
|
||
default=None,
|
||
help="Number of cases to evaluate (default: all)"
|
||
)
|
||
parser.add_argument(
|
||
"--seed",
|
||
type=int,
|
||
default=1234,
|
||
help="Random seed for shuffling (default: 1234)"
|
||
)
|
||
parser.add_argument(
|
||
"--n_predict",
|
||
type=int,
|
||
default=-1,
|
||
help="Max tokens to predict per prompt (default: -1, infinite)"
|
||
)
|
||
parser.add_argument(
|
||
"--temperature",
|
||
type=float,
|
||
default=None,
|
||
help="Sampling temperature (default: not passed)"
|
||
)
|
||
parser.add_argument(
|
||
"--top-k",
|
||
type=int,
|
||
default=None,
|
||
help="Top K sampling (default: not passed)"
|
||
)
|
||
parser.add_argument(
|
||
"--top-p",
|
||
type=float,
|
||
default=None,
|
||
help="Top P sampling (default: not passed)"
|
||
)
|
||
parser.add_argument(
|
||
"--min-p",
|
||
type=float,
|
||
default=None,
|
||
help="Min P sampling (default: not passed)"
|
||
)
|
||
parser.add_argument(
|
||
"--retries",
|
||
type=int,
|
||
default=5,
|
||
help="Times to resend a request that errors or comes back empty before "
|
||
"recording the case as a server error (default: 5)"
|
||
)
|
||
parser.add_argument(
|
||
"--retry-delay",
|
||
type=float,
|
||
default=10.0,
|
||
help="Seconds to wait before the first resend; doubles on each further one (default: 10)"
|
||
)
|
||
parser.add_argument(
|
||
"--reasoning-effort",
|
||
type=str,
|
||
default=None,
|
||
help="Reasoning effort sent as reasoning_effort with every request, e.g. low/medium/high (default: not passed)"
|
||
)
|
||
parser.add_argument(
|
||
"--threads",
|
||
type=str,
|
||
default="32",
|
||
help="Comma-separated thread counts per server (default: 32)"
|
||
)
|
||
parser.add_argument(
|
||
"--model",
|
||
type=str,
|
||
default=None,
|
||
help="Model name to append as query parameter (e.g., gpt-oss-20b-hf)"
|
||
)
|
||
parser.add_argument(
|
||
"--verbose",
|
||
action="store_true",
|
||
help="Show detailed output for each case"
|
||
)
|
||
parser.add_argument(
|
||
"--output",
|
||
type=Path,
|
||
default=Path("llama-eval-state.json"),
|
||
help="Output file for eval state (default: llama-eval-state.json)"
|
||
)
|
||
parser.add_argument(
|
||
"--grader-type",
|
||
type=str,
|
||
default=None,
|
||
choices=["regex", "cli", "llm", "exec"],
|
||
help="Grader type: regex, cli, llm, or exec (default: exec for code suites, "
|
||
"llm otherwise). 'exec' runs the generated code against the suite tests "
|
||
"in a sandboxed venv"
|
||
)
|
||
parser.add_argument(
|
||
"--exec-timeout",
|
||
type=float,
|
||
default=None,
|
||
help="Per-problem wall-clock limit for the exec grader, in seconds "
|
||
"(default: per-suite, 15s for humaneval, 30s for classeval)"
|
||
)
|
||
parser.add_argument(
|
||
"--exec-seed",
|
||
type=int,
|
||
default=0,
|
||
help="Seed the global RNG before running each test, so suites whose tests "
|
||
"draw random inputs score reproducibly (default: 0). Use -1 to leave "
|
||
"the RNG unseeded, matching the stock upstream harness"
|
||
)
|
||
parser.add_argument(
|
||
"--sandbox-python",
|
||
type=str,
|
||
default=None,
|
||
help="Base interpreter used to build the execution venv (default: this one)"
|
||
)
|
||
parser.add_argument(
|
||
"--rebuild-sandbox",
|
||
action="store_true",
|
||
help="Discard and re-provision the cached execution venv"
|
||
)
|
||
parser.add_argument(
|
||
"--grader-script",
|
||
type=str,
|
||
default=None,
|
||
help="CLI grader script path (required for --grader-type cli)"
|
||
)
|
||
parser.add_argument(
|
||
"--grader-server",
|
||
type=str,
|
||
default="",
|
||
help="Server URL for LLM grader (default: same as main server)"
|
||
)
|
||
parser.add_argument(
|
||
"--grader-model",
|
||
type=str,
|
||
default="",
|
||
help="Model name for LLM grader (default: same as main model)"
|
||
)
|
||
parser.add_argument(
|
||
"--grader-api-key",
|
||
type=str,
|
||
default=None,
|
||
help="Bearer token for the LLM grader server (default: the first "
|
||
"--api-key when the grader runs on the main server, else none)"
|
||
)
|
||
parser.add_argument(
|
||
"--agentic-lang",
|
||
type=str,
|
||
default=None,
|
||
choices=["python", "typescript"],
|
||
help="Restrict the agentic suite to one language. Only that language's "
|
||
"toolchain is then provisioned."
|
||
)
|
||
parser.add_argument(
|
||
"--agentic-max-turns",
|
||
type=int,
|
||
default=50,
|
||
help="Maximum assistant turns per agentic episode (default: 50)"
|
||
)
|
||
parser.add_argument(
|
||
"--agentic-max-tokens",
|
||
type=int,
|
||
default=None,
|
||
help="Cap on generated tokens per agentic turn (default: no cap). "
|
||
"Bounds a model that falls into a repetition loop, but also cuts "
|
||
"off long reasoning; truncated turns are counted in the results."
|
||
)
|
||
parser.add_argument(
|
||
"--agentic-context-limit",
|
||
type=int,
|
||
default=None,
|
||
help="Abandon an agentic episode once its context exceeds this many "
|
||
"tokens (default: no limit)"
|
||
)
|
||
parser.add_argument(
|
||
"--agentic-transcripts",
|
||
type=Path,
|
||
default=None,
|
||
metavar="DIR",
|
||
help="Save each agentic episode's full message history to "
|
||
"DIR/<task_id>.json, with a readable DIR/<task_id>.html showing "
|
||
"every reasoning block, reply, tool call and tool result"
|
||
)
|
||
parser.add_argument(
|
||
"--resume",
|
||
action="store_true",
|
||
help="Resume from existing eval state"
|
||
)
|
||
|
||
args = parser.parse_args()
|
||
|
||
global DATASET_SOURCE, AGENTIC_OPTS
|
||
DATASET_SOURCE = args.dataset_source
|
||
AGENTIC_OPTS = {
|
||
"lang": args.agentic_lang,
|
||
"max_turns": args.agentic_max_turns,
|
||
"context_limit": args.agentic_context_limit,
|
||
"max_tokens": args.agentic_max_tokens,
|
||
}
|
||
|
||
if args.grader_type is None:
|
||
args.grader_type = "exec" if args.dataset in CODE_SUITES else "llm"
|
||
|
||
if args.grader_type == "exec" and args.dataset not in CODE_SUITES:
|
||
print(f"Error: --grader-type exec is only meaningful for code suites "
|
||
f"({', '.join(sorted(CODE_SUITES))}), not '{args.dataset}'")
|
||
sys.exit(1)
|
||
|
||
# Parse server URLs and thread counts
|
||
server_urls = [u.strip() for u in args.server.split(",") if u.strip()]
|
||
thread_counts = [int(t.strip()) for t in args.threads.split(",") if t.strip()]
|
||
|
||
if len(server_urls) != len(thread_counts):
|
||
print(f"Error: --server ({len(server_urls)} URLs) and --threads ({len(thread_counts)} values) must have the same count")
|
||
sys.exit(1)
|
||
|
||
# Parse server names (optional, defaults to URLs)
|
||
if args.server_name:
|
||
server_names = [n.strip() for n in args.server_name.split(",") if n.strip()]
|
||
if len(server_names) != len(server_urls):
|
||
print(f"Error: --server-name ({len(server_names)} names) and --server ({len(server_urls)} URLs) must have the same count")
|
||
sys.exit(1)
|
||
else:
|
||
server_names = server_urls # fallback to URLs
|
||
|
||
api_key_arg = args.api_key if args.api_key is not None else os.environ.get("LLAMA_API_KEY")
|
||
if api_key_arg:
|
||
api_keys = [k.strip() for k in api_key_arg.split(",")]
|
||
if len(api_keys) == 1:
|
||
api_keys = api_keys * len(server_urls)
|
||
elif len(api_keys) != len(server_urls):
|
||
print(f"Error: --api-key ({len(api_keys)} keys) must be a single key or "
|
||
f"match --server ({len(server_urls)} URLs)")
|
||
sys.exit(1)
|
||
else:
|
||
api_keys = [None] * len(server_urls)
|
||
|
||
server_configs = [
|
||
ServerConfig(url=url, threads=threads, name=name, api_key=key)
|
||
for url, threads, name, key in zip(server_urls, thread_counts, server_names, api_keys)
|
||
]
|
||
|
||
if args.grader_api_key is not None:
|
||
grader_api_key = args.grader_api_key
|
||
else:
|
||
grader_url = args.grader_server or server_configs[0].url
|
||
grader_api_key = next(
|
||
(s.api_key for s in server_configs if s.url == grader_url), None)
|
||
|
||
if args.dataset == "gpqa" and args.grader_type != "llm":
|
||
print("Error: GPQA dataset requires --grader-type llm")
|
||
parser.print_help()
|
||
sys.exit(1)
|
||
|
||
if args.output.exists():
|
||
print(f"Loading existing eval state from {args.output}")
|
||
eval_state = EvalState.load(args.output)
|
||
|
||
# Verify model matches
|
||
if eval_state.model_name is not None and args.model != eval_state.model_name:
|
||
print(f"Error: Model mismatch. State has '{eval_state.model_name}', but --model is '{args.model}'")
|
||
sys.exit(1)
|
||
|
||
eval_state.print_all_tasks()
|
||
eval_state.print_existing_summary()
|
||
|
||
if eval_state.is_complete():
|
||
return
|
||
|
||
print()
|
||
|
||
if not args.resume:
|
||
print(f"Evaluation incomplete. Run with --resume to continue.")
|
||
return
|
||
|
||
pending_tasks = eval_state.get_pending_tasks()
|
||
print(f"Resuming from {len(pending_tasks)} pending tasks")
|
||
|
||
existing_cases = eval_state.task_states.get("cases", {})
|
||
|
||
eval_state.tasks = pending_tasks
|
||
eval_state.task_states["cases"] = existing_cases
|
||
|
||
grader_server_url = args.grader_server if args.grader_server else server_configs[0].url
|
||
grader_model_name = args.grader_model if args.grader_model else args.model
|
||
if args.grader_type == "llm" and not grader_model_name:
|
||
print("Error: --grader-type llm requires --grader-model or --model")
|
||
sys.exit(1)
|
||
grader = Grader(
|
||
grader_type=args.grader_type,
|
||
grader_script=args.grader_script,
|
||
grader_model_name=grader_model_name,
|
||
grader_server_url=grader_server_url,
|
||
grader_api_key=grader_api_key,
|
||
dataset_type=eval_state.dataset_type
|
||
)
|
||
resume = True
|
||
else:
|
||
if args.resume:
|
||
print("Error: No existing eval state found to resume")
|
||
sys.exit(1)
|
||
|
||
grader_server_url = args.grader_server if args.grader_server else server_configs[0].url
|
||
grader_model_name = args.grader_model if args.grader_model else args.model
|
||
if args.grader_type == "llm" and not grader_model_name:
|
||
print("Error: --grader-type llm requires --grader-model or --model")
|
||
sys.exit(1)
|
||
|
||
grader = Grader(
|
||
grader_type=args.grader_type,
|
||
grader_script=args.grader_script,
|
||
grader_model_name=grader_model_name,
|
||
grader_server_url=grader_server_url,
|
||
grader_api_key=grader_api_key,
|
||
dataset_type=args.dataset
|
||
)
|
||
|
||
if args.grader_type == "llm" and not args.grader_server:
|
||
print("Warning: Using same server for LLM grader (no --grader-server specified)")
|
||
|
||
sampling_config = {}
|
||
if args.temperature is not None:
|
||
sampling_config["temperature"] = args.temperature
|
||
if args.top_k is not None:
|
||
sampling_config["top_k"] = args.top_k
|
||
if args.top_p is not None:
|
||
sampling_config["top_p"] = args.top_p
|
||
if args.min_p is not None:
|
||
sampling_config["min_p"] = args.min_p
|
||
if args.reasoning_effort is not None:
|
||
sampling_config["reasoning_effort"] = args.reasoning_effort
|
||
|
||
eval_state = EvalState(
|
||
dataset_type=args.dataset,
|
||
sampling_config=sampling_config,
|
||
output_file=args.output,
|
||
model_name=args.model
|
||
)
|
||
eval_state.load_dataset(seed=args.seed)
|
||
eval_state.setup_tasks(n_cases=args.n_cases, seed=args.seed)
|
||
eval_state.dump()
|
||
resume = False
|
||
|
||
eval_state.print_all_tasks()
|
||
|
||
if args.grader_type == "exec":
|
||
dataset = eval_state.loaded_dataset
|
||
|
||
if dataset.is_agentic:
|
||
dataset.prepare(base_python=args.sandbox_python,
|
||
force=args.rebuild_sandbox)
|
||
else:
|
||
base_python = args.sandbox_python
|
||
want = dataset.preferred_python()
|
||
if not base_python and want:
|
||
base_python = resolve_python(want)
|
||
if base_python:
|
||
print(f"[sandbox] using Python {want} for "
|
||
f"{eval_state.dataset_type} ({base_python})")
|
||
|
||
dataset.configure_python(base_python or sys.executable)
|
||
|
||
sandbox = Sandbox(
|
||
name=eval_state.dataset_type,
|
||
packages=dataset.required_packages(),
|
||
provision=dataset.provision_steps(),
|
||
env=dataset.sandbox_env(),
|
||
base_python=base_python,
|
||
)
|
||
sandbox.ensure(force=args.rebuild_sandbox)
|
||
preflight_sandbox(sandbox)
|
||
grader.sandbox = sandbox
|
||
grader.dataset = dataset
|
||
grader.exec_timeout = (args.exec_timeout if args.exec_timeout is not None
|
||
else dataset.default_exec_timeout())
|
||
grader.exec_seed = None if args.exec_seed < 0 else args.exec_seed
|
||
|
||
processor = Processor(
|
||
server_configs=server_configs,
|
||
grader=grader,
|
||
model_name=args.model,
|
||
n_predict=args.n_predict,
|
||
retries=args.retries,
|
||
retry_delay=args.retry_delay,
|
||
transcript_dir=args.agentic_transcripts,
|
||
)
|
||
|
||
processor.evaluate(eval_state, verbose=args.verbose, resume=resume)
|
||
|
||
if args.grader_type == "exec":
|
||
print_pass_at_k(eval_state)
|
||
|
||
print(f"\nEval state dumped to {args.output}")
|
||
|
||
if __name__ == "__main__":
|
||
main()
|