#!/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'Transcript ' f'{self._escape_html(transcript[:-5])}.html
' if transcript and transcript.endswith(".json") else "") answer_class = status_class if status == "ok" else "" rows.append(f""" {task_id} {status_text} {self._escape_html(expected)} {self._escape_html(answer)} {tokens_str} {tps_str} {t_gen_str} {escaped_server}
{transcript_link} Prompt
{escaped_prompt}
Response
{escaped_response}
{f'Reasoning
{escaped_reasoning}
' if escaped_reasoning else ''} Grader
{grader_log_str}
""") 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""" {p:03d} {r} {n}/{r} {tk[0]}{tk[1]}{tk[2]} {tp[0]}{tp[1]}{tp[2]} {tg[0]}{tg[1]}{tg[2]} """ for p, r, n, tk, tp, tg in summary_data ) html_content = f""" {self.dataset_type.upper()} Eval
Dataset
{self.dataset_type.upper()}
Model
{self.model_name or 'N/A'}
Accuracy
{accuracy:.1f}% [{ci_lower*100:.1f}%, {ci_upper*100:.1f}%]
Correct
{n_correct} / {len(completed)}
Pending
{n_pending}
Server errors
{n_server_errors}
Time
{self.total_time:.1f}s
Sampling
{sampling_str}
{rows_html}
ID Gold Answer Tokens T/s Gen s Server
{summary_rows_html}
Problem Runs Correct Tokens T/s Gen s
minavgmax minavgmax minavgmax
""" 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 .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/.json, with a readable DIR/.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()