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,405 @@
1
+ """Aider Polyglot exercises executed by the stock Pi coding agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ import threading
11
+ from pathlib import Path
12
+
13
+ from benchkit.benchmarks.base import Task
14
+ from benchkit.benchmarks.downloads import Dataset
15
+ from benchkit.evaluation import EvaluationResult
16
+ from benchkit.sandbox import (
17
+ AIDER_POLYGLOT_COMMIT,
18
+ DockerTaskEnvironment,
19
+ aider_pi_image,
20
+ )
21
+
22
+ _REPOSITORY = "https://github.com/Aider-AI/polyglot-benchmark.git"
23
+ _LANGUAGES = ("cpp", "go", "java", "javascript", "python", "rust")
24
+ _LANGUAGE_ALIASES = {
25
+ "cpp": "cpp",
26
+ "c++": "cpp",
27
+ "go": "go",
28
+ "java": "java",
29
+ "js": "javascript",
30
+ "javascript": "javascript",
31
+ "py": "python",
32
+ "python": "python",
33
+ "rs": "rust",
34
+ "rust": "rust",
35
+ }
36
+ _EXPECTED_COUNTS = {
37
+ "cpp": 26,
38
+ "go": 39,
39
+ "java": 47,
40
+ "javascript": 49,
41
+ "python": 34,
42
+ "rust": 30,
43
+ }
44
+ _TEST_COMMANDS = {
45
+ "cpp": [
46
+ "bash",
47
+ "-lc",
48
+ "timeout --signal=KILL 120s bash -lc "
49
+ "'cmake -S . -B build -DEXERCISM_RUN_ALL_TESTS=ON "
50
+ "&& cmake --build build --parallel 2'",
51
+ ],
52
+ "go": ["go", "test", "./..."],
53
+ "java": [
54
+ "gradle",
55
+ "-Dfile.encoding=UTF-8",
56
+ "test",
57
+ "--offline",
58
+ "--no-daemon",
59
+ ],
60
+ "javascript": ["npm", "test", "--", "--runInBand"],
61
+ "python": ["python3", "-m", "unittest", "discover", "-v", "-p", "*_test.py"],
62
+ "rust": ["cargo", "test", "--offline"],
63
+ }
64
+ _ENABLE_TEST_COMMANDS = {
65
+ "java": [
66
+ "bash",
67
+ "-lc",
68
+ "find src/test -name '*.java' -exec "
69
+ "sed -i '/^[[:space:]]*@Disabled/d;"
70
+ "/import org.junit.jupiter.api.Disabled;/d' {} +",
71
+ ],
72
+ "javascript": [
73
+ "bash",
74
+ "-lc",
75
+ "find . -maxdepth 1 -name '*.spec.js' -exec "
76
+ "sed -i -E 's/\\b(xit|xtest)\\s*\\(/test(/g; "
77
+ "s/\\b(xdescribe)\\s*\\(/describe(/g; "
78
+ "s/\\b(test|it|describe)\\.skip\\s*\\(/\\1(/g' {} +",
79
+ ],
80
+ "rust": [
81
+ "bash",
82
+ "-lc",
83
+ "find tests -name '*.rs' -exec "
84
+ "sed -i '/^[[:space:]]*#\\[ignore[^]]*\\][[:space:]]*$/d' {} +",
85
+ ],
86
+ }
87
+ _SAFE_NAME = re.compile(r"^[a-z0-9][a-z0-9-]*$")
88
+ _CACHE_LOCK = threading.Lock()
89
+ _ANSWER_KEY_GUARD_LOG = "/tmp/benchkit-answer-key-guard.jsonl"
90
+
91
+
92
+ def _cache_root() -> Path:
93
+ configured = os.environ.get("BENCHKIT_AIDER_DATASET")
94
+ if configured:
95
+ return Path(configured).expanduser()
96
+ return Path.home() / ".cache" / "benchkit" / "aider-polyglot"
97
+
98
+
99
+ def _run_git(args: list[str], *, timeout: float = 300) -> None:
100
+ git = shutil.which("git")
101
+ if git is None:
102
+ raise RuntimeError("Aider Polyglot requires git to download its task corpus")
103
+ completed = subprocess.run(
104
+ [git, *args],
105
+ text=True,
106
+ capture_output=True,
107
+ timeout=timeout,
108
+ )
109
+ if completed.returncode:
110
+ detail = (completed.stderr or completed.stdout).strip()[-2000:]
111
+ raise RuntimeError(f"could not prepare Aider Polyglot dataset: {detail}")
112
+
113
+
114
+ def _dataset_root() -> Path:
115
+ """Return a pinned upstream checkout, downloading it once when necessary."""
116
+ root = _cache_root()
117
+ marker = root / ".git"
118
+ with _CACHE_LOCK:
119
+ if not marker.exists():
120
+ root.parent.mkdir(parents=True, exist_ok=True)
121
+ _run_git(["clone", _REPOSITORY, str(root)])
122
+ completed = subprocess.run(
123
+ [shutil.which("git") or "git", "-C", str(root), "rev-parse", "HEAD"],
124
+ text=True,
125
+ capture_output=True,
126
+ timeout=30,
127
+ )
128
+ if completed.returncode or completed.stdout.strip() != AIDER_POLYGLOT_COMMIT:
129
+ _run_git(["-C", str(root), "fetch", "origin", AIDER_POLYGLOT_COMMIT])
130
+ _run_git(["-C", str(root), "checkout", "--detach", AIDER_POLYGLOT_COMMIT])
131
+ return root
132
+
133
+
134
+ class AiderPolyglot:
135
+ """Run the complete Aider exercise corpus through Pi's native tools."""
136
+
137
+ name = "aider-polyglot"
138
+ task_count = sum(_EXPECTED_COUNTS.values())
139
+ data_source = Dataset(
140
+ summary="pinned Aider polyglot-benchmark checkout (needs git)",
141
+ # A full clone; git's packing makes the exact figure drift.
142
+ size=25_000_000,
143
+ approximate=True,
144
+ location=_cache_root,
145
+ is_installed=lambda: (_cache_root() / ".git").exists(),
146
+ install=lambda _progress: _dataset_root(),
147
+ )
148
+ variant_count = len(_LANGUAGES)
149
+ workspace_task = True
150
+ evaluation_activity = "running exercise tests in the Pi sandbox"
151
+
152
+ def variants(self, _client: object, _model: str) -> tuple[str, ...]:
153
+ return _LANGUAGES
154
+
155
+ def resolve_variant(self, selector: str) -> str | None:
156
+ """Resolve a CLI language selector to its canonical variant name."""
157
+ return _LANGUAGE_ALIASES.get(selector.strip().lower())
158
+
159
+ def variant_task_count(self, variant: str) -> int:
160
+ return _EXPECTED_COUNTS[variant]
161
+
162
+ def load_tasks(self) -> list[Task]:
163
+ root = _dataset_root()
164
+ tasks: list[Task] = []
165
+ for language in _LANGUAGES:
166
+ exercises = root / language / "exercises" / "practice"
167
+ names = sorted(path.name for path in exercises.iterdir() if path.is_dir())
168
+ expected = _EXPECTED_COUNTS[language]
169
+ if len(names) != expected:
170
+ raise RuntimeError(
171
+ f"Aider Polyglot {language} checkout has {len(names)} tasks; "
172
+ f"expected {expected} at {AIDER_POLYGLOT_COMMIT[:12]}"
173
+ )
174
+ tasks.extend(
175
+ Task(
176
+ id=f"{language}/{name}",
177
+ prompt="",
178
+ metadata={"language": language, "exercise": name},
179
+ )
180
+ for name in names
181
+ )
182
+ return tasks
183
+
184
+ def tasks_for_variant(self, tasks: list[Task], variant: str) -> list[Task]:
185
+ return [task for task in tasks if task.metadata["language"] == variant]
186
+
187
+ def build_prompt(self, task: Task) -> str:
188
+ language = task.metadata["language"]
189
+ exercise = task.metadata["exercise"]
190
+ if language == "go" and exercise == "counter":
191
+ return (
192
+ "Solve the Aider Polyglot go exercise `counter` in the current "
193
+ "workspace. This is a test-authoring exercise: read "
194
+ "`.docs/instructions.md`, inspect the supplied implementations, "
195
+ "and write a comprehensive `counter_test.go` that accepts the "
196
+ "correct implementation and rejects every incorrect one. Use the "
197
+ "terminal to run checks yourself, bounding every command with "
198
+ "`timeout --signal=KILL 120s`. Do not modify the supplied "
199
+ "implementations or exercise instructions."
200
+ )
201
+ return (
202
+ f"Solve the Aider Polyglot {language} exercise `{exercise}` in the "
203
+ "current workspace. Read `.docs/instructions.md` and any appendices, "
204
+ "inspect the starter code and tests, then edit the implementation so "
205
+ "all tests pass. Use the terminal to run the tests yourself, but bound "
206
+ "every test command with `timeout --signal=KILL 120s` so a broken "
207
+ "implementation cannot deadlock the agent. Do not change the tests or "
208
+ "exercise instructions."
209
+ )
210
+
211
+ def evaluate(self, _task: Task, _response: str) -> bool:
212
+ raise RuntimeError("Aider Polyglot must be evaluated inside its workspace")
213
+
214
+ def pi_image(self):
215
+ return aider_pi_image()
216
+
217
+ def prepare_workspace(
218
+ self,
219
+ task: Task,
220
+ environment: DockerTaskEnvironment,
221
+ ) -> None:
222
+ language, exercise = self._coordinates(task)
223
+ workspace = f"/workspace/{exercise}"
224
+ environment.workdir = workspace
225
+ source = f"/opt/aider-polyglot/{language}/exercises/practice/{exercise}/."
226
+ environment.exec(["mkdir", "-p", workspace])
227
+ environment.exec(["cp", "-a", source, f"{workspace}/"])
228
+ environment.exec(
229
+ [
230
+ "find",
231
+ workspace,
232
+ "-depth",
233
+ "(",
234
+ "-name",
235
+ ".meta",
236
+ "-o",
237
+ "-name",
238
+ ".approaches",
239
+ "-o",
240
+ "-iname",
241
+ "*example*",
242
+ "-o",
243
+ "-iname",
244
+ "*reference*",
245
+ "-o",
246
+ "-iname",
247
+ "*proof*",
248
+ ")",
249
+ "-exec",
250
+ "rm",
251
+ "-rf",
252
+ "{}",
253
+ "+",
254
+ ]
255
+ )
256
+ environment.exec(["rm", "-rf", f"{workspace}/.git"])
257
+ environment.exec(["rm", "-f", _ANSWER_KEY_GUARD_LOG])
258
+ environment.exec(["git", "init", "-q", workspace])
259
+ environment.exec(["git", "-C", workspace, "config", "user.name", "BenchKit"])
260
+ environment.exec(
261
+ [
262
+ "git",
263
+ "-C",
264
+ workspace,
265
+ "config",
266
+ "user.email",
267
+ "benchkit@localhost",
268
+ ]
269
+ )
270
+ environment.exec(["git", "-C", workspace, "add", "."])
271
+ environment.exec(["git", "-C", workspace, "commit", "-q", "-m", "baseline"])
272
+
273
+ def verify_workspace(
274
+ self,
275
+ task: Task,
276
+ environment: DockerTaskEnvironment,
277
+ _tool_trace: list[dict] | None = None,
278
+ ) -> EvaluationResult:
279
+ language, exercise = self._coordinates(task)
280
+ workspace = f"/workspace/{exercise}"
281
+ command = _TEST_COMMANDS[language]
282
+ try:
283
+ patch = environment.exec(
284
+ ["git", "-C", workspace, "diff", "--no-ext-diff"],
285
+ timeout=30,
286
+ check=False,
287
+ ).stdout
288
+ changed = environment.exec(
289
+ ["git", "-C", workspace, "diff", "--name-only"],
290
+ timeout=30,
291
+ check=False,
292
+ ).stdout.splitlines()
293
+ changed_tests = [
294
+ path for path in changed if self._is_test_file(language, path)
295
+ ]
296
+ if language == "go" and exercise == "counter":
297
+ changed_tests = [
298
+ path for path in changed_tests if path != "counter_test.go"
299
+ ]
300
+ if changed_tests:
301
+ environment.exec(
302
+ [
303
+ "git",
304
+ "-C",
305
+ workspace,
306
+ "checkout",
307
+ "HEAD",
308
+ "--",
309
+ *changed_tests,
310
+ ],
311
+ timeout=30,
312
+ )
313
+ enable_command = _ENABLE_TEST_COMMANDS.get(language)
314
+ if enable_command is not None:
315
+ environment.exec(
316
+ enable_command,
317
+ workdir=workspace,
318
+ timeout=30,
319
+ )
320
+ if language == "go" and exercise == "counter":
321
+ command = [
322
+ "bash",
323
+ "-lc",
324
+ "for implementation in 1 2 3; do "
325
+ "if COUNTER_IMPL=$implementation go test ./...; then exit 1; fi; "
326
+ "done; COUNTER_IMPL=4 go test ./...",
327
+ ]
328
+ completed = environment.exec(
329
+ command,
330
+ workdir=workspace,
331
+ timeout=180,
332
+ check=False,
333
+ )
334
+ guard_result = environment.exec(
335
+ ["cat", _ANSWER_KEY_GUARD_LOG], timeout=10, check=False
336
+ )
337
+ guard_hits = []
338
+ for line in guard_result.stdout.splitlines():
339
+ try:
340
+ hit = json.loads(line)
341
+ except (ValueError, TypeError):
342
+ continue
343
+ if isinstance(hit, dict):
344
+ guard_hits.append(hit)
345
+ except Exception as exc:
346
+ return EvaluationResult(
347
+ 0.0,
348
+ error=f"workspace verification failed: {type(exc).__name__}: {exc}",
349
+ )
350
+ output = (completed.stdout + completed.stderr).strip()
351
+ passed = completed.returncode == 0
352
+ contaminated = bool(guard_hits)
353
+ feedback = ""
354
+ if not passed:
355
+ feedback = (
356
+ "The exercise tests still fail. Review this test output, fix the "
357
+ f"workspace, and run the tests again:\n\n{output[-6000:]}"
358
+ )
359
+ return EvaluationResult(
360
+ score=1.0 if passed and not contaminated else 0.0,
361
+ feedback=feedback,
362
+ details={
363
+ "language": language,
364
+ "contamination_verdict": ("CONTAMINATED" if contaminated else "CLEAR"),
365
+ "answer_key_guard_hits": guard_hits,
366
+ "test_command": command,
367
+ "test_exit_code": completed.returncode,
368
+ "test_output": output[-8000:],
369
+ "test_output_truncated": len(output) > 8000,
370
+ "test_files_restored": changed_tests,
371
+ "patch": patch[-16000:],
372
+ "patch_truncated": len(patch) > 16000,
373
+ },
374
+ )
375
+
376
+ @staticmethod
377
+ def _coordinates(task: Task) -> tuple[str, str]:
378
+ language = str(task.metadata.get("language") or "")
379
+ exercise = str(task.metadata.get("exercise") or "")
380
+ if language not in _LANGUAGES or not _SAFE_NAME.fullmatch(exercise):
381
+ raise ValueError(f"invalid Aider Polyglot task coordinates: {task.id}")
382
+ return language, exercise
383
+
384
+ @staticmethod
385
+ def _is_test_file(language: str, path: str) -> bool:
386
+ name = Path(path).name
387
+ if language == "cpp":
388
+ return name.endswith("_test.cpp") or path.startswith("test/")
389
+ if language == "go":
390
+ return name.endswith("_test.go")
391
+ if language == "java":
392
+ return path.startswith("src/test/")
393
+ if language == "javascript":
394
+ return name.endswith(".spec.js")
395
+ if language == "python":
396
+ return name.endswith("_test.py")
397
+ return language == "rust" and path.startswith("tests/")
398
+
399
+ def result_metadata(self, variant: str | None) -> dict:
400
+ return {
401
+ "dataset_commit": AIDER_POLYGLOT_COMMIT,
402
+ "language": variant,
403
+ "protocol": "pi-agent",
404
+ "official_aider_comparable": False,
405
+ }
@@ -0,0 +1,52 @@
1
+ """ARC-Challenge benchmark - science multiple choice questions."""
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" / "arc_challenge.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 ARC:
25
+ name = "arc"
26
+ decision_instructions = "Which option correctly answers the question?"
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,149 @@
1
+ """BANKING77 intent routing - classify a customer message into one of 77 intents.
2
+
3
+ Routing is what decision models are deployed for, so the suite is shaped like
4
+ a router: every task offers all 77 intents (no option filtering) and the answer
5
+ is the intent label itself rather than an option letter. Chat models get the
6
+ same list and are scored on an exact match of the label they reply with.
7
+ """
8
+
9
+ import json
10
+ from collections import Counter
11
+ from functools import cache
12
+ from pathlib import Path
13
+
14
+ from benchkit.benchmarks.base import Task
15
+ from benchkit.benchmarks.utils import strip_think_tags
16
+ from benchkit.evaluation import EvaluationResult
17
+
18
+ DATASETS = Path(__file__).parent.parent / "datasets"
19
+ DATASET = DATASETS / "banking77.jsonl"
20
+ INTENTS = DATASETS / "banking77_intents.json"
21
+
22
+ SYSTEM = (
23
+ "Route the customer's message to exactly one of the 77 banking intents "
24
+ "below. Reply with ONLY the intent name, exactly as written."
25
+ )
26
+
27
+ # Quotes, emphasis and heading marks a model may put around an exact label.
28
+ _WRAPPERS = " \t`'\"*#.,;:"
29
+
30
+
31
+ @cache
32
+ def intents() -> tuple[str, ...]:
33
+ """The 77 intent labels in upstream order."""
34
+ return tuple(json.loads(INTENTS.read_text(encoding="utf-8")))
35
+
36
+
37
+ @cache
38
+ def _gold() -> dict[str, str]:
39
+ """Expected intent by task id."""
40
+ with open(DATASET, encoding="utf-8") as f:
41
+ return {row["task_id"]: row["answer"] for row in map(json.loads, f)}
42
+
43
+
44
+ def extract_intent(response: str) -> str | None:
45
+ """Return the intent a response names, or None.
46
+
47
+ The label must be exact up to case and surrounding quotes, backticks,
48
+ emphasis or heading marks, optionally after an ``intent:`` prefix. The whole
49
+ answer is tried first, then its last line, since a model may explain before
50
+ committing.
51
+ """
52
+ labels = {label.casefold(): label for label in intents()}
53
+ text = strip_think_tags(response).strip()
54
+ lines = [line for line in text.splitlines() if line.strip()]
55
+ for candidate in (text, lines[-1] if lines else ""):
56
+ cleaned = candidate.strip(_WRAPPERS)
57
+ if cleaned.casefold().startswith("intent:"):
58
+ cleaned = cleaned[len("intent:") :].strip(_WRAPPERS)
59
+ label = labels.get(cleaned.casefold())
60
+ if label:
61
+ return label
62
+ return None
63
+
64
+
65
+ def macro_f1(pairs: list[tuple[str, str | None]]) -> float:
66
+ """Unweighted mean F1 over every intent that is gold or predicted."""
67
+ true_pos: Counter[str] = Counter()
68
+ predicted: Counter[str] = Counter()
69
+ gold: Counter[str] = Counter()
70
+ for expected, guess in pairs:
71
+ gold[expected] += 1
72
+ if guess is not None:
73
+ predicted[guess] += 1
74
+ true_pos[guess] += guess == expected
75
+ labels = set(gold) | set(predicted)
76
+ if not labels:
77
+ return 0.0
78
+ scores = [
79
+ 2 * true_pos[label] / (gold[label] + predicted[label]) for label in labels
80
+ ]
81
+ return sum(scores) / len(scores)
82
+
83
+
84
+ class Banking77:
85
+ name = "banking77"
86
+ decision_instructions = "Which intent does the customer's message express?"
87
+
88
+ def load_tasks(self) -> list[Task]:
89
+ tasks = []
90
+ with open(DATASET, encoding="utf-8") as f:
91
+ for line in f:
92
+ d = json.loads(line)
93
+ tasks.append(
94
+ Task(
95
+ id=d["task_id"],
96
+ prompt=d["question"],
97
+ metadata={"answer": d["answer"]},
98
+ )
99
+ )
100
+ return tasks
101
+
102
+ def build_prompt(self, task: Task) -> str:
103
+ listing = "\n".join(f"- {label}" for label in intents())
104
+ return f"{SYSTEM}\n\nIntents:\n{listing}\n\nMessage: {task.prompt}"
105
+
106
+ def decision_criteria(self, task: Task) -> dict[str, str]:
107
+ """All 77 intents, keyed by label so the chosen key is the answer."""
108
+ return {label: label.replace("_", " ").lower() for label in intents()}
109
+
110
+ def evaluate_with_feedback(self, task: Task, response: str) -> EvaluationResult:
111
+ predicted = extract_intent(response)
112
+ expected = task.metadata["answer"]
113
+ if predicted is None:
114
+ feedback = (
115
+ "The answer did not name one of the listed intents. Reply with "
116
+ "exactly one intent name from the list and nothing else."
117
+ )
118
+ elif predicted != expected:
119
+ feedback = (
120
+ f"{predicted} is not the intent of this message. Re-read it and "
121
+ "reply with exactly one intent name from the list."
122
+ )
123
+ else:
124
+ feedback = ""
125
+ return EvaluationResult(
126
+ score=float(predicted == expected),
127
+ feedback=feedback,
128
+ details={"predicted_intent": predicted, "expected_intent": expected},
129
+ )
130
+
131
+ def evaluate(self, task: Task, response: str) -> bool:
132
+ return extract_intent(response) == task.metadata["answer"]
133
+
134
+ def summary_fields(self, records: list[object]) -> dict:
135
+ """Macro-F1 over the scored tasks, for a router's per-intent quality.
136
+
137
+ Tasks that never reached the verifier (timeouts, loop kills) count as
138
+ no prediction, exactly as they count against accuracy.
139
+ """
140
+ gold = _gold()
141
+ pairs = [
142
+ (
143
+ gold[record.task_id],
144
+ (getattr(record, "workspace", None) or {}).get("predicted_intent"),
145
+ )
146
+ for record in records
147
+ if record.task_id in gold
148
+ ]
149
+ return {"macro_f1": round(macro_f1(pairs) * 100, 1)} if pairs else {}
@@ -0,0 +1,19 @@
1
+ """Benchmark base protocol."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Protocol
5
+
6
+
7
+ @dataclass
8
+ class Task:
9
+ id: str
10
+ prompt: str
11
+ metadata: dict = field(default_factory=dict)
12
+
13
+
14
+ class Benchmark(Protocol):
15
+ name: str
16
+
17
+ def load_tasks(self) -> list[Task]: ...
18
+ def build_prompt(self, task: Task) -> str: ...
19
+ def evaluate(self, task: Task, response: str) -> bool | float: ...
@@ -0,0 +1,49 @@
1
+ """BoolQ benchmark - yes/no reading comprehension."""
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+
7
+ from benchkit.benchmarks.base import Task
8
+ from benchkit.benchmarks.utils import strip_think_tags
9
+
10
+ DATASET = Path(__file__).parent.parent / "datasets" / "boolq.jsonl"
11
+
12
+ SYSTEM = "Read the passage and answer the question. Reply with ONLY YES or NO."
13
+
14
+
15
+ def _extract_answer(response: str) -> str | None:
16
+ text = strip_think_tags(response)
17
+ answers = re.findall(r"\b(?:yes|no)\b", text.lower())
18
+ return answers[-1] if answers else None
19
+
20
+
21
+ class BoolQ:
22
+ name = "boolq"
23
+ # Yes/no without a choices list; two options means no repair.
24
+ answer_options = 2
25
+ decision_instructions = "Based on the passage, is the answer to the question yes?"
26
+
27
+ def load_tasks(self) -> list[Task]:
28
+ tasks = []
29
+ with open(DATASET) as f:
30
+ for line in f:
31
+ d = json.loads(line)
32
+ tasks.append(
33
+ Task(
34
+ id=d["task_id"],
35
+ prompt=d["question"],
36
+ metadata={
37
+ "passage": d["passage"],
38
+ "answer": d["answer"],
39
+ },
40
+ )
41
+ )
42
+ return tasks
43
+
44
+ def build_prompt(self, task: Task) -> str:
45
+ passage = task.metadata["passage"]
46
+ return f"{SYSTEM}\n\nPassage:\n{passage}\n\nQuestion: {task.prompt}"
47
+
48
+ def evaluate(self, task: Task, response: str) -> bool:
49
+ return _extract_answer(response) == task.metadata["answer"]