benchkit-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchkit/__init__.py +1 -0
- benchkit/_pi_proxy.py +219 -0
- benchkit/answer_key_guard.ts +49 -0
- benchkit/artifacts.py +87 -0
- benchkit/benchmarks/__init__.py +105 -0
- benchkit/benchmarks/aider_polyglot.py +405 -0
- benchkit/benchmarks/arc.py +52 -0
- benchkit/benchmarks/banking77.py +149 -0
- benchkit/benchmarks/base.py +19 -0
- benchkit/benchmarks/boolq.py +49 -0
- benchkit/benchmarks/chartqa.py +133 -0
- benchkit/benchmarks/code_verifier.py +77 -0
- benchkit/benchmarks/downloads.py +166 -0
- benchkit/benchmarks/evalplus.py +288 -0
- benchkit/benchmarks/git_surgery.py +485 -0
- benchkit/benchmarks/gpqa.py +56 -0
- benchkit/benchmarks/gsm8k.py +97 -0
- benchkit/benchmarks/hellaswag.py +49 -0
- benchkit/benchmarks/humaneval.py +103 -0
- benchkit/benchmarks/ifeval.py +56 -0
- benchkit/benchmarks/ifeval_instructions.py +477 -0
- benchkit/benchmarks/mbpp.py +60 -0
- benchkit/benchmarks/mc_arena.py +479 -0
- benchkit/benchmarks/mcq.py +94 -0
- benchkit/benchmarks/medfailbench.py +537 -0
- benchkit/benchmarks/mmlu.py +54 -0
- benchkit/benchmarks/mmlu_pro.py +62 -0
- benchkit/benchmarks/mmvp.py +115 -0
- benchkit/benchmarks/ocrbench.py +96 -0
- benchkit/benchmarks/openbookqa.py +52 -0
- benchkit/benchmarks/patcheval.py +929 -0
- benchkit/benchmarks/piqa.py +52 -0
- benchkit/benchmarks/ruler.py +172 -0
- benchkit/benchmarks/ruler_generator.py +535 -0
- benchkit/benchmarks/sanity.py +174 -0
- benchkit/benchmarks/treejs_arena.py +268 -0
- benchkit/benchmarks/truthfulqa.py +52 -0
- benchkit/benchmarks/utils.py +25 -0
- benchkit/benchmarks/vision.py +230 -0
- benchkit/benchmarks/vstar.py +108 -0
- benchkit/benchmarks/winogrande.py +52 -0
- benchkit/benchmarks/xstest.py +116 -0
- benchkit/browser.py +429 -0
- benchkit/catalog.py +121 -0
- benchkit/cli.py +1000 -0
- benchkit/client.py +1413 -0
- benchkit/controls.py +87 -0
- benchkit/datasets/MMLU_LICENSE.txt +21 -0
- benchkit/datasets/README.md +142 -0
- benchkit/datasets/arc_challenge.jsonl +1172 -0
- benchkit/datasets/banking77.jsonl +3080 -0
- benchkit/datasets/banking77_intents.json +79 -0
- benchkit/datasets/boolq.jsonl +3270 -0
- benchkit/datasets/code_verifier.jsonl +500 -0
- benchkit/datasets/gpqa_diamond.jsonl +198 -0
- benchkit/datasets/gsm8k.jsonl +1319 -0
- benchkit/datasets/hellaswag.jsonl +1000 -0
- benchkit/datasets/humaneval.jsonl +164 -0
- benchkit/datasets/ifeval.jsonl +541 -0
- benchkit/datasets/mbpp.jsonl +500 -0
- benchkit/datasets/mc_blocks_1_20_1.jsonl +1003 -0
- benchkit/datasets/medfailbench_hard30.jsonl +30 -0
- benchkit/datasets/medfailbench_hard30.md +13 -0
- benchkit/datasets/openbookqa.jsonl +500 -0
- benchkit/datasets/piqa.jsonl +1838 -0
- benchkit/datasets/sanity_code.jsonl +6 -0
- benchkit/datasets/truthfulqa.jsonl +817 -0
- benchkit/datasets/winogrande.jsonl +1267 -0
- benchkit/datasets/xstest.csv +451 -0
- benchkit/decision.py +194 -0
- benchkit/demo.py +451 -0
- benchkit/dockerfiles.py +247 -0
- benchkit/engine.py +1270 -0
- benchkit/evaluation.py +195 -0
- benchkit/events.py +141 -0
- benchkit/executor.py +96 -0
- benchkit/git_surgery/backport-release-stack/setup.sh +328 -0
- benchkit/git_surgery/backport-release-stack/verify.sh +119 -0
- benchkit/git_surgery/bisect-the-regression/setup.sh +49 -0
- benchkit/git_surgery/bisect-the-regression/verify.sh +34 -0
- benchkit/git_surgery/rebase-conflict-chain/setup.sh +65 -0
- benchkit/git_surgery/rebase-conflict-chain/verify.sh +43 -0
- benchkit/git_surgery/recover-complex-stash/setup.sh +152 -0
- benchkit/git_surgery/recover-complex-stash/verify.sh +95 -0
- benchkit/git_surgery/recover-lost-work/setup.sh +40 -0
- benchkit/git_surgery/recover-lost-work/verify.sh +36 -0
- benchkit/git_surgery/repair-force-pushed-remote/setup.sh +171 -0
- benchkit/git_surgery/repair-force-pushed-remote/verify.sh +83 -0
- benchkit/git_surgery/revert-merge-with-followups/setup.sh +198 -0
- benchkit/git_surgery/revert-merge-with-followups/verify.sh +98 -0
- benchkit/git_surgery/secret-in-history/README.md +27 -0
- benchkit/git_surgery/secret-in-history/setup.sh +107 -0
- benchkit/git_surgery/secret-in-history/verify.sh +106 -0
- benchkit/git_surgery/split-the-mega-commit/setup.sh +50 -0
- benchkit/git_surgery/split-the-mega-commit/verify.sh +48 -0
- benchkit/git_surgery/untangle-nested-submodules/setup.sh +193 -0
- benchkit/git_surgery/untangle-nested-submodules/verify.sh +90 -0
- benchkit/history.py +474 -0
- benchkit/jobs.py +332 -0
- benchkit/leakage.py +51 -0
- benchkit/looping.py +873 -0
- benchkit/mc_render.py +274 -0
- benchkit/mc_viewer/dist/atlas.png +0 -0
- benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
- benchkit/mc_viewer/dist/version.json +4 -0
- benchkit/mc_viewer/dist/viewer.html +23 -0
- benchkit/mc_viewer/dist/viewer.js +2 -0
- benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +25 -0
- benchkit/mc_viewer/dist/worker.js +2 -0
- benchkit/mc_viewer/dist/worker.js.LICENSE.txt +19 -0
- benchkit/metrics.py +100 -0
- benchkit/ollama.py +60 -0
- benchkit/perf.py +353 -0
- benchkit/perf_report.py +207 -0
- benchkit/perturbations.py +208 -0
- benchkit/pi_agent.py +476 -0
- benchkit/pi_package/package-lock.json +1827 -0
- benchkit/pi_package/package.json +8 -0
- benchkit/report.py +142 -0
- benchkit/report_markdown.py +645 -0
- benchkit/results.py +688 -0
- benchkit/retry.py +163 -0
- benchkit/runner.py +769 -0
- benchkit/sandbox.py +1167 -0
- benchkit/template_check.py +291 -0
- benchkit/templates/arena.html +504 -0
- benchkit/templates/history.html +58 -0
- benchkit/templates/perf.html +509 -0
- benchkit/templates/report.html +769 -0
- benchkit/tui/__init__.py +5 -0
- benchkit/tui/app.py +87 -0
- benchkit/tui/app.tcss +512 -0
- benchkit/tui/formatting.py +84 -0
- benchkit/tui/screens/__init__.py +1 -0
- benchkit/tui/screens/connect.py +191 -0
- benchkit/tui/screens/detail.py +288 -0
- benchkit/tui/screens/help.py +84 -0
- benchkit/tui/screens/modals.py +544 -0
- benchkit/tui/screens/results.py +361 -0
- benchkit/tui/screens/run.py +988 -0
- benchkit/tui/screens/setup.py +733 -0
- benchkit/tui/theme.py +135 -0
- benchkit/tui/widgets.py +120 -0
- benchkit_cli-0.1.0.dist-info/METADATA +294 -0
- benchkit_cli-0.1.0.dist-info/RECORD +149 -0
- benchkit_cli-0.1.0.dist-info/WHEEL +5 -0
- benchkit_cli-0.1.0.dist-info/entry_points.txt +3 -0
- benchkit_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- benchkit_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Fast 25-task sanity check across core model and harness behaviors."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from benchkit.benchmarks.arc import ARC
|
|
7
|
+
from benchkit.benchmarks.base import Task
|
|
8
|
+
from benchkit.benchmarks.gsm8k import GSM8K
|
|
9
|
+
from benchkit.benchmarks.ifeval import IFEval
|
|
10
|
+
from benchkit.benchmarks.piqa import PIQA
|
|
11
|
+
from benchkit.benchmarks.utils import strip_think_tags
|
|
12
|
+
from benchkit.evaluation import EvaluationResult
|
|
13
|
+
from benchkit.executor import execute_with_feedback
|
|
14
|
+
|
|
15
|
+
CODE_DATASET = Path(__file__).parent.parent / "datasets" / "sanity_code.jsonl"
|
|
16
|
+
CODE_SYSTEM = (
|
|
17
|
+
"Complete the following Python function. "
|
|
18
|
+
"Output ONLY the function body. "
|
|
19
|
+
"No markdown fences, no explanation, no examples."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Hand-reviewed for clarity and complementary coverage. These are intentionally
|
|
23
|
+
# easy-to-moderate diagnostics: a strong model failing several usually indicates
|
|
24
|
+
# a prompt template, output parsing, quantization, or generation problem.
|
|
25
|
+
SELECTION = (
|
|
26
|
+
("code", "sanity-code", "SanityCode/0"), # case-insensitive sets
|
|
27
|
+
("code", "sanity-code", "SanityCode/2"), # overlapping substrings
|
|
28
|
+
("code", "sanity-code", "SanityCode/4"), # numeric edge cases
|
|
29
|
+
("code", "sanity-code", "SanityCode/7"), # parsing and validation
|
|
30
|
+
("code", "sanity-code", "SanityCode/8"), # branching string logic
|
|
31
|
+
("math", "gsm8k", "GSM8K/0"), # chained arithmetic
|
|
32
|
+
("math", "gsm8k", "GSM8K/5"), # discount pattern
|
|
33
|
+
("math", "gsm8k", "GSM8K/7"), # percentage and rates
|
|
34
|
+
("math", "gsm8k", "GSM8K/13"), # reverse word problem
|
|
35
|
+
("math", "gsm8k", "GSM8K/19"), # average-speed constraint
|
|
36
|
+
("instruction", "ifeval", "IFEval/1001"), # forbidden punctuation
|
|
37
|
+
("instruction", "ifeval", "IFEval/1005"), # required placeholders
|
|
38
|
+
("instruction", "ifeval", "IFEval/1019"), # lowercase output
|
|
39
|
+
("instruction", "ifeval", "IFEval/1075"), # valid JSON only
|
|
40
|
+
("instruction", "ifeval", "IFEval/1107"), # two simultaneous constraints
|
|
41
|
+
("science", "arc", "ARC/4"), # gravity and mass
|
|
42
|
+
("science", "arc", "ARC/9"), # atomic units
|
|
43
|
+
("science", "arc", "ARC/13"), # experimental method
|
|
44
|
+
("science", "arc", "ARC/17"), # heat transfer
|
|
45
|
+
("science", "arc", "ARC/19"), # evidence and hypothesis
|
|
46
|
+
("commonsense", "piqa", "PIQA/6"), # safe container reuse
|
|
47
|
+
("commonsense", "piqa", "PIQA/14"), # tool construction
|
|
48
|
+
("commonsense", "piqa", "PIQA/18"), # kitchen tool choice
|
|
49
|
+
("commonsense", "piqa", "PIQA/23"), # appliance operation
|
|
50
|
+
("commonsense", "piqa", "PIQA/24"), # material suitability
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _extract_code(response: str) -> str:
|
|
55
|
+
text = strip_think_tags(response).rstrip()
|
|
56
|
+
if "```python" in text:
|
|
57
|
+
text = text.split("```python", 1)[1].split("```", 1)[0]
|
|
58
|
+
elif "```" in text:
|
|
59
|
+
text = text.split("```", 1)[1].split("```", 1)[0]
|
|
60
|
+
text = text.strip("\n")
|
|
61
|
+
lines = text.split("\n")
|
|
62
|
+
if any(
|
|
63
|
+
line.startswith(("def ", "class ", "import ", "from ", "@")) for line in lines
|
|
64
|
+
):
|
|
65
|
+
return text
|
|
66
|
+
first = next((line for line in lines if line.strip()), "")
|
|
67
|
+
if first and not first.startswith((" ", "\t")):
|
|
68
|
+
return "\n".join((" " + line) if line.strip() else line for line in lines)
|
|
69
|
+
return text
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _SanityCode:
|
|
73
|
+
name = "sanity-code"
|
|
74
|
+
|
|
75
|
+
def load_tasks(self) -> list[Task]:
|
|
76
|
+
tasks = []
|
|
77
|
+
with open(CODE_DATASET, encoding="utf-8") as handle:
|
|
78
|
+
for line in handle:
|
|
79
|
+
if not line.strip():
|
|
80
|
+
continue
|
|
81
|
+
row = json.loads(line)
|
|
82
|
+
tasks.append(
|
|
83
|
+
Task(
|
|
84
|
+
id=row["task_id"],
|
|
85
|
+
prompt=row["prompt"],
|
|
86
|
+
metadata={
|
|
87
|
+
"test": row["test"],
|
|
88
|
+
"entry_point": row["entry_point"],
|
|
89
|
+
"canonical_solution": row["canonical_solution"],
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
return tasks
|
|
94
|
+
|
|
95
|
+
def build_prompt(self, task: Task) -> str:
|
|
96
|
+
return f"{CODE_SYSTEM}\n\n{task.prompt}"
|
|
97
|
+
|
|
98
|
+
def evaluate(self, task: Task, response: str) -> bool:
|
|
99
|
+
return self.evaluate_with_feedback(task, response).passed
|
|
100
|
+
|
|
101
|
+
def evaluate_with_feedback(self, task: Task, response: str) -> EvaluationResult:
|
|
102
|
+
code = _extract_code(response)
|
|
103
|
+
entry = task.metadata["entry_point"]
|
|
104
|
+
if f"def {entry}" in code:
|
|
105
|
+
imports = [
|
|
106
|
+
line
|
|
107
|
+
for line in task.prompt.split("\n")
|
|
108
|
+
if line.startswith(("import ", "from "))
|
|
109
|
+
]
|
|
110
|
+
function_code = "\n".join(imports) + "\n\n" + code if imports else code
|
|
111
|
+
else:
|
|
112
|
+
function_code = task.prompt + code
|
|
113
|
+
full = function_code + "\n\n" + task.metadata["test"] + f"\ncheck({entry})\n"
|
|
114
|
+
result = execute_with_feedback(full)
|
|
115
|
+
return EvaluationResult(float(result.passed), result.feedback)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Sanity:
|
|
119
|
+
"""Dispatch the curated checks to their native deterministic evaluators."""
|
|
120
|
+
|
|
121
|
+
name = "sanity"
|
|
122
|
+
task_count = len(SELECTION)
|
|
123
|
+
|
|
124
|
+
def __init__(self) -> None:
|
|
125
|
+
self._benchmarks = {
|
|
126
|
+
"sanity-code": _SanityCode(),
|
|
127
|
+
"gsm8k": GSM8K(),
|
|
128
|
+
"ifeval": IFEval(),
|
|
129
|
+
"arc": ARC(),
|
|
130
|
+
"piqa": PIQA(),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
def load_tasks(self) -> list[Task]:
|
|
134
|
+
tasks_by_source = {
|
|
135
|
+
name: {task.id: task for task in benchmark.load_tasks()}
|
|
136
|
+
for name, benchmark in self._benchmarks.items()
|
|
137
|
+
}
|
|
138
|
+
tasks = []
|
|
139
|
+
for index, (category, source, source_id) in enumerate(SELECTION):
|
|
140
|
+
source_task = tasks_by_source[source][source_id]
|
|
141
|
+
metadata = dict(source_task.metadata)
|
|
142
|
+
metadata.update(
|
|
143
|
+
sanity_category=category,
|
|
144
|
+
source_benchmark=source,
|
|
145
|
+
source_task_id=source_id,
|
|
146
|
+
)
|
|
147
|
+
tasks.append(
|
|
148
|
+
Task(id=f"Sanity/{index}", prompt=source_task.prompt, metadata=metadata)
|
|
149
|
+
)
|
|
150
|
+
return tasks
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _source_task(task: Task) -> Task:
|
|
154
|
+
metadata = dict(task.metadata)
|
|
155
|
+
source_id = metadata.pop("source_task_id")
|
|
156
|
+
metadata.pop("source_benchmark")
|
|
157
|
+
metadata.pop("sanity_category")
|
|
158
|
+
return Task(id=source_id, prompt=task.prompt, metadata=metadata)
|
|
159
|
+
|
|
160
|
+
def build_prompt(self, task: Task) -> str:
|
|
161
|
+
benchmark = self._benchmarks[task.metadata["source_benchmark"]]
|
|
162
|
+
return benchmark.build_prompt(self._source_task(task))
|
|
163
|
+
|
|
164
|
+
def evaluate(self, task: Task, response: str) -> bool | float:
|
|
165
|
+
benchmark = self._benchmarks[task.metadata["source_benchmark"]]
|
|
166
|
+
return benchmark.evaluate(self._source_task(task), response)
|
|
167
|
+
|
|
168
|
+
def evaluate_with_feedback(self, task: Task, response: str) -> EvaluationResult:
|
|
169
|
+
benchmark = self._benchmarks[task.metadata["source_benchmark"]]
|
|
170
|
+
source_task = self._source_task(task)
|
|
171
|
+
evaluator = getattr(benchmark, "evaluate_with_feedback", None)
|
|
172
|
+
if evaluator is not None:
|
|
173
|
+
return evaluator(source_task, response)
|
|
174
|
+
return EvaluationResult(float(benchmark.evaluate(source_task, response)))
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Creative three.js arena: one-shot scenes judged by whether they render.
|
|
2
|
+
|
|
3
|
+
Unlike every other suite in BenchKit there is no ground truth here. Each task
|
|
4
|
+
asks for one self-contained HTML file, the file is opened in headless Chromium,
|
|
5
|
+
and the only automatic score is binary: did the page come up without errors and
|
|
6
|
+
put a live canvas on screen. The screenshots are the interesting output and are
|
|
7
|
+
carried into the HTML report so runs can be compared side by side.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from urllib.parse import urlsplit
|
|
13
|
+
|
|
14
|
+
from benchkit.artifacts import task_dir
|
|
15
|
+
from benchkit.benchmarks.base import Task
|
|
16
|
+
from benchkit.benchmarks.utils import strip_think_tags
|
|
17
|
+
from benchkit.browser import (
|
|
18
|
+
probe_environment,
|
|
19
|
+
render_page,
|
|
20
|
+
render_settle_s,
|
|
21
|
+
render_timeout_s,
|
|
22
|
+
)
|
|
23
|
+
from benchkit.evaluation import EvaluationResult
|
|
24
|
+
|
|
25
|
+
# Frozen prompt set. Bump the version (and never edit a shipped prompt in
|
|
26
|
+
# place) so screenshots from different runs stay comparable.
|
|
27
|
+
PROMPT_SET_VERSION = "v2"
|
|
28
|
+
|
|
29
|
+
PROMPT_PREFIX = (
|
|
30
|
+
"Write a single self-contained HTML file that runs by opening it in a "
|
|
31
|
+
"browser. No build step, no local asset files. The canvas must fill the "
|
|
32
|
+
"window. Output only the HTML file."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# fmt: off
|
|
36
|
+
PROMPTS: tuple[tuple[str, str], ...] = (
|
|
37
|
+
("black-hole", "A black hole with a gravitationally lensed accretion disk, slowly orbiting camera."),
|
|
38
|
+
("japanese-garden", "A voxel Japanese garden at dusk with a pagoda, a bridge over a pond, and glowing stone lanterns."),
|
|
39
|
+
("alien-planet", "A procedurally generated alien planet surface with drifting fog."),
|
|
40
|
+
("spur-gears", "Three meshing spur gears with 12, 24 and 36 teeth, rotating at correct relative speeds and directions."),
|
|
41
|
+
("inner-solar-system", "The inner solar system with orbital periods in correct relative proportion, Earth completing one orbit every 10 seconds."),
|
|
42
|
+
("newtons-cradle", "A Newton's cradle with five balls where momentum transfers correctly."),
|
|
43
|
+
("walkable-maze", "A first-person walkable maze on a 10x10 grid with WASD and mouse look, with wall collision."),
|
|
44
|
+
("bar-chart", 'A 3D bar chart of {"A":42,"B":17,"C":88,"D":63,"E":5}, bars labeled and proportional.'),
|
|
45
|
+
("particle-field", "50,000 particles orbiting a central attractor with a live FPS counter."),
|
|
46
|
+
("mobius-strip", "A parametric Mobius strip with a sphere traveling along the surface."),
|
|
47
|
+
("rubiks-cube", "A Rubik's cube that repeatedly performs the sequence R U R' U' with correctly pivoted face turns."),
|
|
48
|
+
("raymarched-pillars", "A raymarched scene drawn entirely in one fragment shader: an infinite field of rounded pillars with soft shadows and distance fog, using no mesh geometry."),
|
|
49
|
+
("cloth-flag", "A cloth flag pinned at two corners, simulated with verlet integration, rippling in a steady wind."),
|
|
50
|
+
("double-pendulum", "A double pendulum released from horizontal, with a fading trail following the tip."),
|
|
51
|
+
("boids", "300 boids flocking with separation, alignment and cohesion, steering around a sphere that drifts through them."),
|
|
52
|
+
("glass-sphere", "A glass sphere with refraction and chromatic dispersion above a checkerboard floor, lit by a moving light."),
|
|
53
|
+
("neon-tunnel", "A neon wireframe tunnel flown through at speed, with bloom post-processing."),
|
|
54
|
+
("picking-grid", "Twelve labeled cubes in a grid where hovering highlights a cube and clicking locks a readout of its name."),
|
|
55
|
+
("analog-clock", "An analog clock showing the viewer's real local time, with correctly proportioned hour, minute and second hands."),
|
|
56
|
+
("instanced-city", "A city of 5,000 instanced buildings under a day-night cycle with a moving sun and cast shadows."),
|
|
57
|
+
("ps5-gamepad", "A PlayStation 5 DualSense controller, modeled in detail and slowly rotating under studio lighting."),
|
|
58
|
+
("hexapod-walk", "A six-legged robot walking forward across a plane with a tripod gait, its feet staying planted while they carry the body."),
|
|
59
|
+
("domino-chain", "A chain of dominoes falling along an S-curve, toppling one after another."),
|
|
60
|
+
("l-system-tree", "A tree grown from an L-system, branching recursively and swaying in the wind."),
|
|
61
|
+
("reflective-pool", "A rippling pool reflecting the sky and a few floating objects, seen from a low angle."),
|
|
62
|
+
)
|
|
63
|
+
# fmt: on
|
|
64
|
+
|
|
65
|
+
# Diagnostics are pasted verbatim into the repair turn, so cap them well below
|
|
66
|
+
# anything that would crowd out the model's own previous answer.
|
|
67
|
+
MAX_FEEDBACK_CHARS = 4000
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def extract_html(response: str) -> str:
|
|
71
|
+
"""Pull the HTML document out of a model response, fences and all."""
|
|
72
|
+
text = strip_think_tags(response).strip()
|
|
73
|
+
if "```" in text:
|
|
74
|
+
blocks = []
|
|
75
|
+
parts = text.split("```")
|
|
76
|
+
for index in range(1, len(parts), 2):
|
|
77
|
+
block = parts[index]
|
|
78
|
+
first, _, rest = block.partition("\n")
|
|
79
|
+
if first.strip().lower() in {"html", "xml", ""}:
|
|
80
|
+
blocks.append(rest if "\n" in block else block)
|
|
81
|
+
else:
|
|
82
|
+
blocks.append(block)
|
|
83
|
+
candidates = [block for block in blocks if "<" in block]
|
|
84
|
+
if candidates:
|
|
85
|
+
text = max(candidates, key=len).strip()
|
|
86
|
+
|
|
87
|
+
lowered = text.lower()
|
|
88
|
+
for opener in ("<!doctype html", "<html"):
|
|
89
|
+
start = lowered.find(opener)
|
|
90
|
+
if start != -1:
|
|
91
|
+
end = lowered.rfind("</html>")
|
|
92
|
+
return (
|
|
93
|
+
text[start : end + len("</html>")] if end != -1 else text[start:]
|
|
94
|
+
).strip()
|
|
95
|
+
# A bare fragment is still worth rendering, as long as it is markup.
|
|
96
|
+
if "<canvas" in lowered or "<script" in lowered or "<body" in lowered:
|
|
97
|
+
return text
|
|
98
|
+
return ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _environment_fault(outcome) -> str:
|
|
102
|
+
"""Name the machine-level reason a page could not render, if there is one.
|
|
103
|
+
|
|
104
|
+
A model is only responsible for what it wrote. Headless boxes without a
|
|
105
|
+
browser, without a working WebGL stack, or without a route to the module
|
|
106
|
+
CDN are ordinary places to run BenchKit, so these are not the model's
|
|
107
|
+
failures and are not scored as such: the render check is skipped and the
|
|
108
|
+
task keeps its credit, with the reason recorded on the task.
|
|
109
|
+
"""
|
|
110
|
+
if outcome.unreachable:
|
|
111
|
+
hosts = sorted({urlsplit(url).hostname or url for url in outcome.unreachable})
|
|
112
|
+
return (
|
|
113
|
+
"the page could not reach "
|
|
114
|
+
+ ", ".join(hosts)
|
|
115
|
+
+ " — BenchKit allows these hosts, so this machine has no route to "
|
|
116
|
+
"the module CDN (proxy, firewall, or DNS). Fix outbound HTTPS, or "
|
|
117
|
+
"set BENCHKIT_RENDER_ALLOWED_HOSTS to a mirror you can reach."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if outcome.contexts:
|
|
121
|
+
return ""
|
|
122
|
+
ok, detail = probe_environment()
|
|
123
|
+
if not ok:
|
|
124
|
+
return (
|
|
125
|
+
f"headless rendering is broken on this machine: {detail}. "
|
|
126
|
+
"Install the browser's system dependencies "
|
|
127
|
+
"('playwright install --with-deps chromium' on Debian/Ubuntu); "
|
|
128
|
+
"BenchKit already asks Chromium for the SwiftShader software "
|
|
129
|
+
"renderer, so no GPU is required."
|
|
130
|
+
)
|
|
131
|
+
return ""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _skipped(details: dict, reason: str) -> EvaluationResult:
|
|
135
|
+
"""Pass the task without a verdict: this machine cannot render it."""
|
|
136
|
+
return EvaluationResult(
|
|
137
|
+
score=1.0,
|
|
138
|
+
details={**details, "render_status": "skipped", "skip_reason": reason},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _clip(text: str) -> str:
|
|
143
|
+
if len(text) <= MAX_FEEDBACK_CHARS:
|
|
144
|
+
return text
|
|
145
|
+
return text[:MAX_FEEDBACK_CHARS] + "\n… diagnostics truncated"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TreeJSArena:
|
|
149
|
+
"""Frozen creative prompts scored only on whether the page renders."""
|
|
150
|
+
|
|
151
|
+
name = "treejs-arena"
|
|
152
|
+
task_count = len(PROMPTS)
|
|
153
|
+
prompt_set_version = PROMPT_SET_VERSION
|
|
154
|
+
# A render rate is not a capability score, so it stays out of the headline
|
|
155
|
+
# average the way RULER's context curve does.
|
|
156
|
+
include_in_overall = False
|
|
157
|
+
list_note = f"creative · render-only scoring · prompt set {PROMPT_SET_VERSION}"
|
|
158
|
+
evaluation_activity = "rendering the page in headless Chromium"
|
|
159
|
+
|
|
160
|
+
def load_tasks(self) -> list[Task]:
|
|
161
|
+
return [
|
|
162
|
+
Task(
|
|
163
|
+
id=f"TreeJSArena/{index}",
|
|
164
|
+
prompt=f"{PROMPT_PREFIX}\n\n{prompt}",
|
|
165
|
+
metadata={
|
|
166
|
+
"slug": slug,
|
|
167
|
+
"scene": prompt,
|
|
168
|
+
"prompt_set_version": PROMPT_SET_VERSION,
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
for index, (slug, prompt) in enumerate(PROMPTS)
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
def build_prompt(self, task: Task) -> str:
|
|
175
|
+
return task.prompt
|
|
176
|
+
|
|
177
|
+
def result_metadata(self, variant: str | None = None) -> dict:
|
|
178
|
+
return {
|
|
179
|
+
"include_in_overall": self.include_in_overall,
|
|
180
|
+
"prompt_set_version": PROMPT_SET_VERSION,
|
|
181
|
+
"scoring": "render-only",
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def evaluate_with_feedback(self, task: Task, response: str) -> EvaluationResult:
|
|
185
|
+
slug = str(task.metadata.get("slug") or task.id)
|
|
186
|
+
base = {
|
|
187
|
+
"scoring": "render-only",
|
|
188
|
+
"prompt_set_version": PROMPT_SET_VERSION,
|
|
189
|
+
"prompt_slug": slug,
|
|
190
|
+
"scene": str(task.metadata.get("scene") or ""),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
directory = task_dir(f"treejs-{slug}")
|
|
194
|
+
html = extract_html(response)
|
|
195
|
+
if not html:
|
|
196
|
+
# Nothing to render, but the answer is still the artifact of record:
|
|
197
|
+
# keep it on disk so the gallery has something to open.
|
|
198
|
+
answer = directory / "response.txt"
|
|
199
|
+
answer.write_text(response, encoding="utf-8")
|
|
200
|
+
return EvaluationResult(
|
|
201
|
+
score=0.0,
|
|
202
|
+
feedback=(
|
|
203
|
+
"No HTML document was found in the answer. Reply with the "
|
|
204
|
+
"complete single-file HTML document and nothing else."
|
|
205
|
+
),
|
|
206
|
+
details={
|
|
207
|
+
**base,
|
|
208
|
+
"render_status": "no_html",
|
|
209
|
+
"response_text": str(answer),
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
page = directory / "page.html"
|
|
214
|
+
page.write_text(html, encoding="utf-8")
|
|
215
|
+
|
|
216
|
+
outcome = render_page(
|
|
217
|
+
page,
|
|
218
|
+
directory / "screenshot.png",
|
|
219
|
+
directory / "screenshot.jpg",
|
|
220
|
+
settle_s=render_settle_s(),
|
|
221
|
+
timeout_s=render_timeout_s(),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
details = {
|
|
225
|
+
**base,
|
|
226
|
+
"page_html": str(page),
|
|
227
|
+
"html_bytes": len(html.encode("utf-8")),
|
|
228
|
+
"console_errors": outcome.console_errors,
|
|
229
|
+
"page_errors": outcome.page_errors,
|
|
230
|
+
"blocked_requests": outcome.blocked_requests,
|
|
231
|
+
"failed_requests": outcome.failed_requests,
|
|
232
|
+
"canvases": outcome.canvases,
|
|
233
|
+
"contexts": outcome.contexts,
|
|
234
|
+
"animation_frames": outcome.frames,
|
|
235
|
+
}
|
|
236
|
+
if outcome.screenshot:
|
|
237
|
+
details["screenshot"] = outcome.screenshot
|
|
238
|
+
if outcome.thumbnail:
|
|
239
|
+
details["screenshot_thumbnail"] = outcome.thumbnail
|
|
240
|
+
|
|
241
|
+
if outcome.error:
|
|
242
|
+
return _skipped(details, outcome.error)
|
|
243
|
+
|
|
244
|
+
if outcome.rendered:
|
|
245
|
+
return EvaluationResult(
|
|
246
|
+
score=1.0,
|
|
247
|
+
details={**details, "render_status": "rendered"},
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if fault := _environment_fault(outcome):
|
|
251
|
+
return _skipped(details, fault)
|
|
252
|
+
|
|
253
|
+
return EvaluationResult(
|
|
254
|
+
score=0.0,
|
|
255
|
+
feedback=(
|
|
256
|
+
"The page was opened in headless Chromium and did not render "
|
|
257
|
+
"cleanly. Browser diagnostics:\n\n"
|
|
258
|
+
f"{_clip(outcome.diagnostics)}\n\n"
|
|
259
|
+
"Fix the cause and reply with the complete corrected "
|
|
260
|
+
"single-file HTML document and nothing else. Network access is "
|
|
261
|
+
"restricted to public module CDNs, so anything a blocked "
|
|
262
|
+
"request refers to must be replaced or inlined."
|
|
263
|
+
),
|
|
264
|
+
details={**details, "render_status": "failed"},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def evaluate(self, task: Task, response: str) -> bool:
|
|
268
|
+
return self.evaluate_with_feedback(task, response).passed
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""TruthfulQA MC1 benchmark - truthful multiple choice QA."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from benchkit.benchmarks.base import Task
|
|
7
|
+
from benchkit.benchmarks.mcq import extract_choice
|
|
8
|
+
from benchkit.benchmarks.utils import strip_think_tags
|
|
9
|
+
|
|
10
|
+
DATASET = Path(__file__).parent.parent / "datasets" / "truthfulqa.jsonl"
|
|
11
|
+
|
|
12
|
+
SYSTEM = (
|
|
13
|
+
"Answer the following multiple choice question. "
|
|
14
|
+
"Reply with ONLY the letter (A, B, C, or D)."
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _format_choices(choices: list[str]) -> str:
|
|
19
|
+
return "\n".join(
|
|
20
|
+
f"{letter}) {choice}" for letter, choice in zip("ABCD", choices, strict=False)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TruthfulQA:
|
|
25
|
+
name = "truthfulqa"
|
|
26
|
+
decision_instructions = "Which answer is true?"
|
|
27
|
+
|
|
28
|
+
def load_tasks(self) -> list[Task]:
|
|
29
|
+
tasks = []
|
|
30
|
+
with open(DATASET) as f:
|
|
31
|
+
for line in f:
|
|
32
|
+
d = json.loads(line)
|
|
33
|
+
tasks.append(
|
|
34
|
+
Task(
|
|
35
|
+
id=d["task_id"],
|
|
36
|
+
prompt=d["question"],
|
|
37
|
+
metadata={
|
|
38
|
+
"choices": d["choices"],
|
|
39
|
+
"answer": d["answer"],
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
return tasks
|
|
44
|
+
|
|
45
|
+
def build_prompt(self, task: Task) -> str:
|
|
46
|
+
choices = _format_choices(task.metadata["choices"])
|
|
47
|
+
return f"{SYSTEM}\n\n{task.prompt}\n\n{choices}"
|
|
48
|
+
|
|
49
|
+
def evaluate(self, task: Task, response: str) -> bool:
|
|
50
|
+
text = strip_think_tags(response)
|
|
51
|
+
choice = extract_choice(text, task.metadata["choices"])
|
|
52
|
+
return choice == task.metadata["answer"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Small shared helpers for benchmark evaluation."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def strip_think_tags(text: str) -> str:
|
|
5
|
+
"""Remove model reasoning blocks wrapped in <think> tags.
|
|
6
|
+
|
|
7
|
+
Some chat templates open ``<think>`` in the prompt itself, so the response
|
|
8
|
+
carries only the closing tag. A ``</think>`` with no ``<think>`` before it
|
|
9
|
+
marks everything up to and including it as reasoning.
|
|
10
|
+
"""
|
|
11
|
+
cleaned = text
|
|
12
|
+
|
|
13
|
+
close = cleaned.find("</think>")
|
|
14
|
+
if close != -1 and "<think>" not in cleaned[:close]:
|
|
15
|
+
cleaned = cleaned[close + len("</think>") :]
|
|
16
|
+
|
|
17
|
+
while "<think>" in cleaned:
|
|
18
|
+
start = cleaned.index("<think>")
|
|
19
|
+
end = cleaned.find("</think>", start)
|
|
20
|
+
if end == -1:
|
|
21
|
+
cleaned = cleaned[:start]
|
|
22
|
+
break
|
|
23
|
+
cleaned = cleaned[:start] + cleaned[end + len("</think>") :]
|
|
24
|
+
|
|
25
|
+
return cleaned.lstrip("\n").rstrip()
|