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,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"]
|