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.
Files changed (149) hide show
  1. benchkit/__init__.py +1 -0
  2. benchkit/_pi_proxy.py +219 -0
  3. benchkit/answer_key_guard.ts +49 -0
  4. benchkit/artifacts.py +87 -0
  5. benchkit/benchmarks/__init__.py +105 -0
  6. benchkit/benchmarks/aider_polyglot.py +405 -0
  7. benchkit/benchmarks/arc.py +52 -0
  8. benchkit/benchmarks/banking77.py +149 -0
  9. benchkit/benchmarks/base.py +19 -0
  10. benchkit/benchmarks/boolq.py +49 -0
  11. benchkit/benchmarks/chartqa.py +133 -0
  12. benchkit/benchmarks/code_verifier.py +77 -0
  13. benchkit/benchmarks/downloads.py +166 -0
  14. benchkit/benchmarks/evalplus.py +288 -0
  15. benchkit/benchmarks/git_surgery.py +485 -0
  16. benchkit/benchmarks/gpqa.py +56 -0
  17. benchkit/benchmarks/gsm8k.py +97 -0
  18. benchkit/benchmarks/hellaswag.py +49 -0
  19. benchkit/benchmarks/humaneval.py +103 -0
  20. benchkit/benchmarks/ifeval.py +56 -0
  21. benchkit/benchmarks/ifeval_instructions.py +477 -0
  22. benchkit/benchmarks/mbpp.py +60 -0
  23. benchkit/benchmarks/mc_arena.py +479 -0
  24. benchkit/benchmarks/mcq.py +94 -0
  25. benchkit/benchmarks/medfailbench.py +537 -0
  26. benchkit/benchmarks/mmlu.py +54 -0
  27. benchkit/benchmarks/mmlu_pro.py +62 -0
  28. benchkit/benchmarks/mmvp.py +115 -0
  29. benchkit/benchmarks/ocrbench.py +96 -0
  30. benchkit/benchmarks/openbookqa.py +52 -0
  31. benchkit/benchmarks/patcheval.py +929 -0
  32. benchkit/benchmarks/piqa.py +52 -0
  33. benchkit/benchmarks/ruler.py +172 -0
  34. benchkit/benchmarks/ruler_generator.py +535 -0
  35. benchkit/benchmarks/sanity.py +174 -0
  36. benchkit/benchmarks/treejs_arena.py +268 -0
  37. benchkit/benchmarks/truthfulqa.py +52 -0
  38. benchkit/benchmarks/utils.py +25 -0
  39. benchkit/benchmarks/vision.py +230 -0
  40. benchkit/benchmarks/vstar.py +108 -0
  41. benchkit/benchmarks/winogrande.py +52 -0
  42. benchkit/benchmarks/xstest.py +116 -0
  43. benchkit/browser.py +429 -0
  44. benchkit/catalog.py +121 -0
  45. benchkit/cli.py +1000 -0
  46. benchkit/client.py +1413 -0
  47. benchkit/controls.py +87 -0
  48. benchkit/datasets/MMLU_LICENSE.txt +21 -0
  49. benchkit/datasets/README.md +142 -0
  50. benchkit/datasets/arc_challenge.jsonl +1172 -0
  51. benchkit/datasets/banking77.jsonl +3080 -0
  52. benchkit/datasets/banking77_intents.json +79 -0
  53. benchkit/datasets/boolq.jsonl +3270 -0
  54. benchkit/datasets/code_verifier.jsonl +500 -0
  55. benchkit/datasets/gpqa_diamond.jsonl +198 -0
  56. benchkit/datasets/gsm8k.jsonl +1319 -0
  57. benchkit/datasets/hellaswag.jsonl +1000 -0
  58. benchkit/datasets/humaneval.jsonl +164 -0
  59. benchkit/datasets/ifeval.jsonl +541 -0
  60. benchkit/datasets/mbpp.jsonl +500 -0
  61. benchkit/datasets/mc_blocks_1_20_1.jsonl +1003 -0
  62. benchkit/datasets/medfailbench_hard30.jsonl +30 -0
  63. benchkit/datasets/medfailbench_hard30.md +13 -0
  64. benchkit/datasets/openbookqa.jsonl +500 -0
  65. benchkit/datasets/piqa.jsonl +1838 -0
  66. benchkit/datasets/sanity_code.jsonl +6 -0
  67. benchkit/datasets/truthfulqa.jsonl +817 -0
  68. benchkit/datasets/winogrande.jsonl +1267 -0
  69. benchkit/datasets/xstest.csv +451 -0
  70. benchkit/decision.py +194 -0
  71. benchkit/demo.py +451 -0
  72. benchkit/dockerfiles.py +247 -0
  73. benchkit/engine.py +1270 -0
  74. benchkit/evaluation.py +195 -0
  75. benchkit/events.py +141 -0
  76. benchkit/executor.py +96 -0
  77. benchkit/git_surgery/backport-release-stack/setup.sh +328 -0
  78. benchkit/git_surgery/backport-release-stack/verify.sh +119 -0
  79. benchkit/git_surgery/bisect-the-regression/setup.sh +49 -0
  80. benchkit/git_surgery/bisect-the-regression/verify.sh +34 -0
  81. benchkit/git_surgery/rebase-conflict-chain/setup.sh +65 -0
  82. benchkit/git_surgery/rebase-conflict-chain/verify.sh +43 -0
  83. benchkit/git_surgery/recover-complex-stash/setup.sh +152 -0
  84. benchkit/git_surgery/recover-complex-stash/verify.sh +95 -0
  85. benchkit/git_surgery/recover-lost-work/setup.sh +40 -0
  86. benchkit/git_surgery/recover-lost-work/verify.sh +36 -0
  87. benchkit/git_surgery/repair-force-pushed-remote/setup.sh +171 -0
  88. benchkit/git_surgery/repair-force-pushed-remote/verify.sh +83 -0
  89. benchkit/git_surgery/revert-merge-with-followups/setup.sh +198 -0
  90. benchkit/git_surgery/revert-merge-with-followups/verify.sh +98 -0
  91. benchkit/git_surgery/secret-in-history/README.md +27 -0
  92. benchkit/git_surgery/secret-in-history/setup.sh +107 -0
  93. benchkit/git_surgery/secret-in-history/verify.sh +106 -0
  94. benchkit/git_surgery/split-the-mega-commit/setup.sh +50 -0
  95. benchkit/git_surgery/split-the-mega-commit/verify.sh +48 -0
  96. benchkit/git_surgery/untangle-nested-submodules/setup.sh +193 -0
  97. benchkit/git_surgery/untangle-nested-submodules/verify.sh +90 -0
  98. benchkit/history.py +474 -0
  99. benchkit/jobs.py +332 -0
  100. benchkit/leakage.py +51 -0
  101. benchkit/looping.py +873 -0
  102. benchkit/mc_render.py +274 -0
  103. benchkit/mc_viewer/dist/atlas.png +0 -0
  104. benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
  105. benchkit/mc_viewer/dist/version.json +4 -0
  106. benchkit/mc_viewer/dist/viewer.html +23 -0
  107. benchkit/mc_viewer/dist/viewer.js +2 -0
  108. benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +25 -0
  109. benchkit/mc_viewer/dist/worker.js +2 -0
  110. benchkit/mc_viewer/dist/worker.js.LICENSE.txt +19 -0
  111. benchkit/metrics.py +100 -0
  112. benchkit/ollama.py +60 -0
  113. benchkit/perf.py +353 -0
  114. benchkit/perf_report.py +207 -0
  115. benchkit/perturbations.py +208 -0
  116. benchkit/pi_agent.py +476 -0
  117. benchkit/pi_package/package-lock.json +1827 -0
  118. benchkit/pi_package/package.json +8 -0
  119. benchkit/report.py +142 -0
  120. benchkit/report_markdown.py +645 -0
  121. benchkit/results.py +688 -0
  122. benchkit/retry.py +163 -0
  123. benchkit/runner.py +769 -0
  124. benchkit/sandbox.py +1167 -0
  125. benchkit/template_check.py +291 -0
  126. benchkit/templates/arena.html +504 -0
  127. benchkit/templates/history.html +58 -0
  128. benchkit/templates/perf.html +509 -0
  129. benchkit/templates/report.html +769 -0
  130. benchkit/tui/__init__.py +5 -0
  131. benchkit/tui/app.py +87 -0
  132. benchkit/tui/app.tcss +512 -0
  133. benchkit/tui/formatting.py +84 -0
  134. benchkit/tui/screens/__init__.py +1 -0
  135. benchkit/tui/screens/connect.py +191 -0
  136. benchkit/tui/screens/detail.py +288 -0
  137. benchkit/tui/screens/help.py +84 -0
  138. benchkit/tui/screens/modals.py +544 -0
  139. benchkit/tui/screens/results.py +361 -0
  140. benchkit/tui/screens/run.py +988 -0
  141. benchkit/tui/screens/setup.py +733 -0
  142. benchkit/tui/theme.py +135 -0
  143. benchkit/tui/widgets.py +120 -0
  144. benchkit_cli-0.1.0.dist-info/METADATA +294 -0
  145. benchkit_cli-0.1.0.dist-info/RECORD +149 -0
  146. benchkit_cli-0.1.0.dist-info/WHEEL +5 -0
  147. benchkit_cli-0.1.0.dist-info/entry_points.txt +3 -0
  148. benchkit_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  149. 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()