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,485 @@
|
|
|
1
|
+
"""Deterministic Git repository repair tasks executed by stock Pi."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from benchkit.benchmarks.base import Task
|
|
10
|
+
from benchkit.evaluation import EvaluationResult
|
|
11
|
+
from benchkit.sandbox import DockerTaskEnvironment, git_surgery_pi_image
|
|
12
|
+
|
|
13
|
+
_TASK_ROOT = Path(__file__).parents[1] / "git_surgery"
|
|
14
|
+
_SEED = 424242
|
|
15
|
+
_SEARCH_RE = re.compile(r"\bgit\s+.*(?:log|show|rev-list|grep|bisect)\b")
|
|
16
|
+
_REWRITE_RE = re.compile(
|
|
17
|
+
r"\bgit\s+(?:rebase|filter-branch|filter-repo|replace|commit-tree)\b"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_TASKS = [
|
|
21
|
+
(
|
|
22
|
+
"secret-in-history",
|
|
23
|
+
"Repair the Git repository in the current workspace. An AWS-style "
|
|
24
|
+
"credential was committed earlier in the reachable history. Remove "
|
|
25
|
+
"that credential from the entire reachable history while preserving "
|
|
26
|
+
"all legitimate later changes and a reviewable commit history. The "
|
|
27
|
+
"working tree includes tests; run them and verify your work. Do not "
|
|
28
|
+
"delete or reinitialize the repository, and do not merely add a revert "
|
|
29
|
+
"or remove the credential in a new commit. Work directly in the "
|
|
30
|
+
"repository using Pi's stock tools.",
|
|
31
|
+
),
|
|
32
|
+
(
|
|
33
|
+
"bisect-the-regression",
|
|
34
|
+
"A regression is hidden among roughly forty commits in this Git "
|
|
35
|
+
"repository. Identify the first offending commit, write its full SHA "
|
|
36
|
+
"to offending-commit.txt, and fix the underlying bug while preserving "
|
|
37
|
+
"the existing tests and history. Use the repository's tests to guide "
|
|
38
|
+
"the investigation and verify the final state. Do not weaken, replace, "
|
|
39
|
+
"or hardcode the tests.",
|
|
40
|
+
),
|
|
41
|
+
(
|
|
42
|
+
"split-the-mega-commit",
|
|
43
|
+
"The top commit combines an invoice-rounding bugfix with an unrelated "
|
|
44
|
+
"invoice-label feature. Split it into exactly two commits, in this "
|
|
45
|
+
"order and with these subjects: 'Fix invoice rounding', then 'Add "
|
|
46
|
+
"invoice labels'. Each resulting commit must have clean boundaries and "
|
|
47
|
+
"its own test suite must pass when checked out. Preserve the final "
|
|
48
|
+
"behavior and do not squash the changes back together.",
|
|
49
|
+
),
|
|
50
|
+
(
|
|
51
|
+
"recover-lost-work",
|
|
52
|
+
"A branch was deleted after a hard reset, leaving an important commit "
|
|
53
|
+
"unreachable but still present in Git's object store. Explore the "
|
|
54
|
+
"repository, recover the original commit object, and restore it as the "
|
|
55
|
+
"tip of a branch named recovered-work. Do not recreate the change by "
|
|
56
|
+
"typing its file contents into a new commit. Verify the recovered work.",
|
|
57
|
+
),
|
|
58
|
+
(
|
|
59
|
+
"rebase-conflict-chain",
|
|
60
|
+
"Rebase the checked-out feature branch onto main. The rebase has three "
|
|
61
|
+
"sequential semantic conflicts. Resolve every conflict so both main's "
|
|
62
|
+
"behavior and the feature behavior survive; blanket --ours or --theirs "
|
|
63
|
+
"will lose functionality. Preserve the three feature commits and their "
|
|
64
|
+
"order, then run the tests and verify the completed rebase.",
|
|
65
|
+
),
|
|
66
|
+
(
|
|
67
|
+
"backport-release-stack",
|
|
68
|
+
"Backport the fixes for BK-101, BK-104, BK-107, BK-112 and BK-115 from "
|
|
69
|
+
"release-2.x onto the checked-out maint-1.x branch. A fix is a commit "
|
|
70
|
+
"whose message carries the trailer 'Fixes: BK-NNN' for that exact "
|
|
71
|
+
"issue; if a fix was reverted and redone on release-2.x, backport only "
|
|
72
|
+
"the version still in effect there. Cherry-pick with -x so every "
|
|
73
|
+
"backport records its source commit, apply them in the order they "
|
|
74
|
+
"landed on release-2.x, and do not duplicate a fix that maint-1.x "
|
|
75
|
+
"already contains. Resolve conflicts for maint-1.x's code; do not merge "
|
|
76
|
+
"release-2.x or bring over any other release-2.x change. Every new "
|
|
77
|
+
"commit on maint-1.x must pass the test suite (python3 -m unittest "
|
|
78
|
+
"discover -v). Leave release-2.x and maint-1.x's existing commits "
|
|
79
|
+
"untouched.",
|
|
80
|
+
),
|
|
81
|
+
(
|
|
82
|
+
"revert-merge-with-followups",
|
|
83
|
+
"The tests on main fail because one of its merges introduced a "
|
|
84
|
+
"regression. Find that merge and undo the regression with a single "
|
|
85
|
+
"'git revert -m' of the merge commit on top of main, choosing the "
|
|
86
|
+
"mainline parent correctly. Resolve the revert so that it removes only "
|
|
87
|
+
"the regression: every other change either side of the merge brought "
|
|
88
|
+
"in, and every later commit on main, must keep working. Do not reset, "
|
|
89
|
+
"rebase or otherwise rewrite main, and do not edit the tests. Run the "
|
|
90
|
+
"tests and verify the result.",
|
|
91
|
+
),
|
|
92
|
+
(
|
|
93
|
+
"recover-complex-stash",
|
|
94
|
+
"A stash holding an in-progress rates migration was dropped, along "
|
|
95
|
+
"with other stashes and stray commits that are still in the object "
|
|
96
|
+
"store. Recover the stash that renamed legacy_rates.py to rates.py as a "
|
|
97
|
+
"staged change and also created an untracked fixtures/eur.json. "
|
|
98
|
+
"Re-apply its work on top of the current main branch as exactly two "
|
|
99
|
+
"commits: first 'Migrate rates module', containing exactly the changes "
|
|
100
|
+
"that were staged in that stash, then 'Finish rates migration', "
|
|
101
|
+
"containing its unstaged edits and its untracked files. Where main has "
|
|
102
|
+
"changed since the stash was made, keep main's changes as well. Recover "
|
|
103
|
+
"the contents from Git's object store rather than retyping them, and "
|
|
104
|
+
"run the tests on the result.",
|
|
105
|
+
),
|
|
106
|
+
(
|
|
107
|
+
"repair-force-pushed-remote",
|
|
108
|
+
"This workspace holds an offline bare remote, origin.git, and two "
|
|
109
|
+
"clones of it, alice and bob. Someone force-pushed origin's release "
|
|
110
|
+
"branch and discarded published work. Restore origin's release branch "
|
|
111
|
+
"so it is the release history exactly as it was published before the "
|
|
112
|
+
"force-push, followed by the one legitimate commit that was pushed "
|
|
113
|
+
"after the force-push, re-applied on top as a single new commit. Drop "
|
|
114
|
+
"the rewritten commit the force-push introduced. Do not change origin's "
|
|
115
|
+
"other branches or any tag, leave no extra refs behind on origin, do "
|
|
116
|
+
"not delete, re-create or replace origin.git, and run the tests on the "
|
|
117
|
+
"restored branch.",
|
|
118
|
+
),
|
|
119
|
+
(
|
|
120
|
+
"untangle-nested-submodules",
|
|
121
|
+
"app is a superproject whose submodule libs/engine has its own nested "
|
|
122
|
+
"submodule vendor/codec; their remotes live in remotes/. app's top "
|
|
123
|
+
"commit points libs/engine at a commit that exists only in this "
|
|
124
|
+
"checkout, so a fresh recursive clone fails. Amend that top commit so "
|
|
125
|
+
"libs/engine points at the commit published on engine's main branch "
|
|
126
|
+
"with the same content, keeping the commit's message and its other "
|
|
127
|
+
"changes. The nested codec checkout also has uncommitted work that "
|
|
128
|
+
"belongs upstream: commit it in codec as 'Fix codec frame whitespace' "
|
|
129
|
+
"on top of codec's main and push it, then commit 'Pick up codec "
|
|
130
|
+
"whitespace fix' in engine on top of engine's main pointing "
|
|
131
|
+
"vendor/codec at it and push that, then record a new app commit, also "
|
|
132
|
+
"'Pick up codec whitespace fix', that moves libs/engine to the new "
|
|
133
|
+
"engine commit. Keep .gitmodules unchanged, keep both modules as real "
|
|
134
|
+
"submodules, and make sure a fresh clone of app with "
|
|
135
|
+
"--recurse-submodules checks out and passes its tests.",
|
|
136
|
+
),
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _command(call: dict) -> str:
|
|
141
|
+
arguments = call.get("arguments")
|
|
142
|
+
if not isinstance(arguments, dict):
|
|
143
|
+
return ""
|
|
144
|
+
return str(arguments.get("command") or "")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _is_error(call: dict) -> bool:
|
|
148
|
+
output = str(call.get("output") or "")
|
|
149
|
+
return bool(call.get("is_error")) or bool(
|
|
150
|
+
re.search(
|
|
151
|
+
r"(?:"
|
|
152
|
+
r"(?:exited with (?:code|status)|exit code)\s+[1-9]\d*"
|
|
153
|
+
r"|(?:^|\n)\s*(?:fatal|error):"
|
|
154
|
+
r"|index filter failed"
|
|
155
|
+
r"|unknown (?:option|switch)"
|
|
156
|
+
r"|usage:\s+git\b"
|
|
157
|
+
r")",
|
|
158
|
+
output,
|
|
159
|
+
re.I,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _valid_tool_arguments(call: dict) -> bool:
|
|
165
|
+
arguments = call.get("arguments")
|
|
166
|
+
if not isinstance(arguments, dict):
|
|
167
|
+
return False
|
|
168
|
+
required: dict[str, tuple[str, ...]] = {
|
|
169
|
+
"bash": ("command",),
|
|
170
|
+
"read": ("path",),
|
|
171
|
+
"write": ("path", "content"),
|
|
172
|
+
"edit": ("path", "oldText", "newText"),
|
|
173
|
+
}
|
|
174
|
+
fields = required.get(str(call.get("name") or ""))
|
|
175
|
+
if fields is None:
|
|
176
|
+
return bool(arguments)
|
|
177
|
+
return all(isinstance(arguments.get(field), str) for field in fields)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def agentic_metrics(tool_trace: list[dict]) -> dict:
|
|
181
|
+
"""Derive deterministic agent behavior metrics from Pi's native trace."""
|
|
182
|
+
calls = [call for call in tool_trace if isinstance(call, dict)]
|
|
183
|
+
valid = sum(_valid_tool_arguments(call) for call in calls)
|
|
184
|
+
invalid = len(calls) - valid
|
|
185
|
+
errored = sum(_is_error(call) for call in calls)
|
|
186
|
+
redundant = 0
|
|
187
|
+
recoveries = 0
|
|
188
|
+
previous_key = None
|
|
189
|
+
for index, call in enumerate(calls):
|
|
190
|
+
key = (
|
|
191
|
+
str(call.get("name") or ""),
|
|
192
|
+
json.dumps(call.get("arguments"), sort_keys=True, default=str),
|
|
193
|
+
)
|
|
194
|
+
if key == previous_key:
|
|
195
|
+
redundant += 1
|
|
196
|
+
previous_key = key
|
|
197
|
+
if _is_error(call) and index + 1 < len(calls):
|
|
198
|
+
next_call = calls[index + 1]
|
|
199
|
+
next_key = (
|
|
200
|
+
str(next_call.get("name") or ""),
|
|
201
|
+
json.dumps(next_call.get("arguments"), sort_keys=True, default=str),
|
|
202
|
+
)
|
|
203
|
+
recoveries += int(next_key != key and not _is_error(next_call))
|
|
204
|
+
destructive = sum(
|
|
205
|
+
bool(
|
|
206
|
+
re.search(
|
|
207
|
+
r"(?:rm\s+-[^\n]*r[^\n]*\s+\.git(?=$|[\s;&|])|git\s+init\b)",
|
|
208
|
+
_command(call),
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
for call in calls
|
|
212
|
+
)
|
|
213
|
+
total = len(calls)
|
|
214
|
+
return {
|
|
215
|
+
"tool_schema_valid_calls": valid,
|
|
216
|
+
"tool_schema_invalid_calls": invalid,
|
|
217
|
+
"tool_schema_validity_rate": round(valid / total, 4) if total else 1.0,
|
|
218
|
+
"errored_tool_calls": errored,
|
|
219
|
+
"post_error_recoveries": recoveries,
|
|
220
|
+
"post_error_recovery_rate": (
|
|
221
|
+
round(recoveries / errored, 4) if errored else 1.0
|
|
222
|
+
),
|
|
223
|
+
"redundant_tool_calls": redundant,
|
|
224
|
+
"redundant_action_rate": round(redundant / total, 4) if total else 0.0,
|
|
225
|
+
"destructive_action_count": destructive,
|
|
226
|
+
"destructive_action_rate": round(destructive / total, 4) if total else 0.0,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class GitSurgery:
|
|
231
|
+
"""Small agentic benchmark over real, stateful Git repositories."""
|
|
232
|
+
|
|
233
|
+
name = "git-surgery"
|
|
234
|
+
task_count = len(_TASKS)
|
|
235
|
+
workspace_task = True
|
|
236
|
+
evaluation_activity = "checking Git history with plumbing commands"
|
|
237
|
+
list_note = f"{len(_TASKS)} agentic Git tasks · requires Pi"
|
|
238
|
+
|
|
239
|
+
def load_tasks(self) -> list[Task]:
|
|
240
|
+
return [
|
|
241
|
+
Task(id=task_id, prompt="", metadata={"seed": _SEED})
|
|
242
|
+
for task_id, _prompt in _TASKS
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
def build_prompt(self, task: Task) -> str:
|
|
246
|
+
return dict(_TASKS)[task.id]
|
|
247
|
+
|
|
248
|
+
def evaluate(self, _task: Task, _response: str) -> bool:
|
|
249
|
+
raise RuntimeError("Git Surgery must be evaluated inside its workspace")
|
|
250
|
+
|
|
251
|
+
def pi_image(self):
|
|
252
|
+
return git_surgery_pi_image()
|
|
253
|
+
|
|
254
|
+
def prepare_workspace(self, task: Task, environment: DockerTaskEnvironment) -> None:
|
|
255
|
+
workspace = f"/workspace/{task.id}"
|
|
256
|
+
environment.workdir = workspace
|
|
257
|
+
environment.exec(["mkdir", "-p", workspace])
|
|
258
|
+
environment.exec(
|
|
259
|
+
[
|
|
260
|
+
"bash",
|
|
261
|
+
f"/opt/git-surgery/{task.id}/setup.sh",
|
|
262
|
+
str(task.metadata["seed"]),
|
|
263
|
+
workspace,
|
|
264
|
+
],
|
|
265
|
+
timeout=30,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def verify_workspace(
|
|
269
|
+
self,
|
|
270
|
+
task: Task,
|
|
271
|
+
environment: DockerTaskEnvironment,
|
|
272
|
+
tool_trace: list[dict] | None = None,
|
|
273
|
+
) -> EvaluationResult:
|
|
274
|
+
workspace = f"/workspace/{task.id}"
|
|
275
|
+
trace = list(tool_trace or [])
|
|
276
|
+
completed = environment.exec(
|
|
277
|
+
[
|
|
278
|
+
"bash",
|
|
279
|
+
f"/opt/git-surgery/{task.id}/verify.sh",
|
|
280
|
+
str(task.metadata["seed"]),
|
|
281
|
+
workspace,
|
|
282
|
+
],
|
|
283
|
+
timeout=60,
|
|
284
|
+
check=False,
|
|
285
|
+
)
|
|
286
|
+
state: dict[str, tuple[bool, str]] = {}
|
|
287
|
+
for line in completed.stdout.splitlines():
|
|
288
|
+
parts = line.split("\t", 2)
|
|
289
|
+
if len(parts) == 3:
|
|
290
|
+
state[parts[0]] = (parts[1] == "1", parts[2])
|
|
291
|
+
|
|
292
|
+
if task.id != "secret-in-history":
|
|
293
|
+
return self._evaluate_generic(task, environment, trace, completed, state)
|
|
294
|
+
|
|
295
|
+
commands = [_command(call) for call in trace]
|
|
296
|
+
searched = any(_SEARCH_RE.search(command) for command in commands)
|
|
297
|
+
rewrote = any(_REWRITE_RE.search(command) for command in commands)
|
|
298
|
+
history_preserved = state.get("history_preserved", (False, "missing"))
|
|
299
|
+
secret_absent = state.get("secret_absent", (False, "missing"))
|
|
300
|
+
checkpoint_specs = [
|
|
301
|
+
(
|
|
302
|
+
"located_offending_commit",
|
|
303
|
+
1,
|
|
304
|
+
searched,
|
|
305
|
+
"history-search command observed",
|
|
306
|
+
),
|
|
307
|
+
("history_rewrite_path", 1, rewrote, "history-rewrite command observed"),
|
|
308
|
+
(
|
|
309
|
+
"history_rewrite_preserved_changes",
|
|
310
|
+
2,
|
|
311
|
+
rewrote and history_preserved[0],
|
|
312
|
+
history_preserved[1],
|
|
313
|
+
),
|
|
314
|
+
(
|
|
315
|
+
"secret_absent_reachable",
|
|
316
|
+
2,
|
|
317
|
+
secret_absent[0],
|
|
318
|
+
secret_absent[1],
|
|
319
|
+
),
|
|
320
|
+
(
|
|
321
|
+
"tests_pass",
|
|
322
|
+
2,
|
|
323
|
+
state.get("tests_pass", (False, "missing"))[0],
|
|
324
|
+
state.get("tests_pass", (False, "missing"))[1],
|
|
325
|
+
),
|
|
326
|
+
]
|
|
327
|
+
destructive = state.get("destructive_reinit", (True, "missing"))[0]
|
|
328
|
+
checkpoints = [
|
|
329
|
+
{
|
|
330
|
+
"id": checkpoint_id,
|
|
331
|
+
"weight": weight,
|
|
332
|
+
"passed": passed,
|
|
333
|
+
"awarded": weight if passed else 0,
|
|
334
|
+
"evidence": evidence,
|
|
335
|
+
}
|
|
336
|
+
for checkpoint_id, weight, passed, evidence in checkpoint_specs
|
|
337
|
+
]
|
|
338
|
+
checkpoints.append(
|
|
339
|
+
{
|
|
340
|
+
"id": "destructive_reinitialization",
|
|
341
|
+
"weight": -4,
|
|
342
|
+
"passed": destructive,
|
|
343
|
+
"awarded": -4 if destructive else 0,
|
|
344
|
+
"evidence": state.get("destructive_reinit", (True, "missing"))[1],
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
positive = sum(item["awarded"] for item in checkpoints if item["weight"] > 0)
|
|
348
|
+
penalty = -sum(item["awarded"] for item in checkpoints if item["weight"] < 0)
|
|
349
|
+
score = max(0, positive - penalty) / 8
|
|
350
|
+
metrics = agentic_metrics(trace)
|
|
351
|
+
details = {
|
|
352
|
+
"checkpoints": checkpoints,
|
|
353
|
+
"positive_points": positive,
|
|
354
|
+
"penalty_points": penalty,
|
|
355
|
+
"max_points": 8,
|
|
356
|
+
"trap_fired": destructive,
|
|
357
|
+
"verifier_exit_code": completed.returncode,
|
|
358
|
+
"verifier_stderr": completed.stderr[-4000:],
|
|
359
|
+
"git_version": environment.exec(["git", "--version"]).stdout.strip(),
|
|
360
|
+
"agentic_metrics": metrics,
|
|
361
|
+
}
|
|
362
|
+
return EvaluationResult(score=score, details=details)
|
|
363
|
+
|
|
364
|
+
def _evaluate_generic(
|
|
365
|
+
self,
|
|
366
|
+
task: Task,
|
|
367
|
+
environment: DockerTaskEnvironment,
|
|
368
|
+
trace: list[dict],
|
|
369
|
+
completed,
|
|
370
|
+
state: dict[str, tuple[bool, str]],
|
|
371
|
+
) -> EvaluationResult:
|
|
372
|
+
specs: dict[str, list[tuple[str, int]]] = {
|
|
373
|
+
"bisect-the-regression": [
|
|
374
|
+
("used_bisect", 1),
|
|
375
|
+
("identified_commit", 2),
|
|
376
|
+
("bug_fixed", 2),
|
|
377
|
+
("tests_pass", 2),
|
|
378
|
+
("history_preserved", 1),
|
|
379
|
+
],
|
|
380
|
+
"split-the-mega-commit": [
|
|
381
|
+
("two_commits", 1),
|
|
382
|
+
("ordered_boundaries", 3),
|
|
383
|
+
("each_commit_tests", 2),
|
|
384
|
+
("final_tree", 2),
|
|
385
|
+
],
|
|
386
|
+
"recover-lost-work": [
|
|
387
|
+
("explored_objects", 1),
|
|
388
|
+
("branch_restored", 1),
|
|
389
|
+
("original_object", 3),
|
|
390
|
+
("recovered_tests", 2),
|
|
391
|
+
("history_preserved", 1),
|
|
392
|
+
],
|
|
393
|
+
"rebase-conflict-chain": [
|
|
394
|
+
("started_rebase", 1),
|
|
395
|
+
("three_commits_ordered", 2),
|
|
396
|
+
("both_sides_preserved", 3),
|
|
397
|
+
("tests_pass", 2),
|
|
398
|
+
],
|
|
399
|
+
"backport-release-stack": [
|
|
400
|
+
("selected_fixes", 2),
|
|
401
|
+
("skipped_equivalent", 1),
|
|
402
|
+
("intermediate_tests", 2),
|
|
403
|
+
("final_behavior", 1),
|
|
404
|
+
("no_release_leak", 2),
|
|
405
|
+
],
|
|
406
|
+
"revert-merge-with-followups": [
|
|
407
|
+
("revert_commit", 2),
|
|
408
|
+
("topology_preserved", 1),
|
|
409
|
+
("regression_removed", 2),
|
|
410
|
+
("both_parents_kept", 2),
|
|
411
|
+
("tests_pass", 1),
|
|
412
|
+
],
|
|
413
|
+
"recover-complex-stash": [
|
|
414
|
+
("explored_objects", 1),
|
|
415
|
+
("two_commits", 1),
|
|
416
|
+
("staged_commit_exact", 2),
|
|
417
|
+
("remaining_commit_exact", 2),
|
|
418
|
+
("main_changes_kept", 1),
|
|
419
|
+
("tests_pass", 1),
|
|
420
|
+
],
|
|
421
|
+
"repair-force-pushed-remote": [
|
|
422
|
+
("remote_intact", 1),
|
|
423
|
+
("lineage_restored", 2),
|
|
424
|
+
("followup_retained", 2),
|
|
425
|
+
("healthy_refs_untouched", 2),
|
|
426
|
+
("tests_pass", 1),
|
|
427
|
+
],
|
|
428
|
+
"untangle-nested-submodules": [
|
|
429
|
+
("codec_commit", 2),
|
|
430
|
+
("engine_commit", 1),
|
|
431
|
+
("top_commit_repaired", 2),
|
|
432
|
+
("superproject_update", 1),
|
|
433
|
+
("recursive_checkout", 2),
|
|
434
|
+
],
|
|
435
|
+
}
|
|
436
|
+
command_text = "\n".join(_command(call) for call in trace)
|
|
437
|
+
trace_checks = {
|
|
438
|
+
"used_bisect": bool(re.search(r"\bgit\s+bisect\b", command_text)),
|
|
439
|
+
"explored_objects": bool(
|
|
440
|
+
re.search(r"\bgit\s+(?:reflog|fsck|cat-file)\b", command_text)
|
|
441
|
+
),
|
|
442
|
+
"started_rebase": bool(re.search(r"\bgit\s+rebase\b", command_text)),
|
|
443
|
+
"revert_commit": bool(re.search(r"\bgit\s+revert\b", command_text)),
|
|
444
|
+
}
|
|
445
|
+
checkpoints = []
|
|
446
|
+
for checkpoint_id, weight in specs[task.id]:
|
|
447
|
+
passed, evidence = state.get(checkpoint_id, (False, "missing"))
|
|
448
|
+
if checkpoint_id in trace_checks:
|
|
449
|
+
passed = passed and trace_checks[checkpoint_id]
|
|
450
|
+
checkpoints.append(
|
|
451
|
+
{
|
|
452
|
+
"id": checkpoint_id,
|
|
453
|
+
"weight": weight,
|
|
454
|
+
"passed": passed,
|
|
455
|
+
"awarded": weight if passed else 0,
|
|
456
|
+
"evidence": evidence,
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
trap = state.get("trap", (True, "missing"))
|
|
460
|
+
checkpoints.append(
|
|
461
|
+
{
|
|
462
|
+
"id": "destructive_shortcut",
|
|
463
|
+
"weight": -4,
|
|
464
|
+
"passed": trap[0],
|
|
465
|
+
"awarded": -4 if trap[0] else 0,
|
|
466
|
+
"evidence": trap[1],
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
positive = sum(item["awarded"] for item in checkpoints if item["weight"] > 0)
|
|
470
|
+
penalty = -sum(item["awarded"] for item in checkpoints if item["weight"] < 0)
|
|
471
|
+
metrics = agentic_metrics(trace)
|
|
472
|
+
return EvaluationResult(
|
|
473
|
+
score=max(0, positive - penalty) / 8,
|
|
474
|
+
details={
|
|
475
|
+
"checkpoints": checkpoints,
|
|
476
|
+
"positive_points": positive,
|
|
477
|
+
"penalty_points": penalty,
|
|
478
|
+
"max_points": 8,
|
|
479
|
+
"trap_fired": trap[0],
|
|
480
|
+
"verifier_exit_code": completed.returncode,
|
|
481
|
+
"verifier_stderr": completed.stderr[-4000:],
|
|
482
|
+
"git_version": environment.exec(["git", "--version"]).stdout.strip(),
|
|
483
|
+
"agentic_metrics": metrics,
|
|
484
|
+
},
|
|
485
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""GPQA Diamond benchmark - graduate-level science 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" / "gpqa_diamond.jsonl"
|
|
11
|
+
|
|
12
|
+
SYSTEM = (
|
|
13
|
+
"Answer the following graduate-level science 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 GPQA:
|
|
25
|
+
name = "gpqa"
|
|
26
|
+
decision_instructions = (
|
|
27
|
+
"Which option correctly answers this graduate-level science question?"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def load_tasks(self) -> list[Task]:
|
|
31
|
+
tasks = []
|
|
32
|
+
with open(DATASET) as f:
|
|
33
|
+
for line in f:
|
|
34
|
+
d = json.loads(line)
|
|
35
|
+
tasks.append(
|
|
36
|
+
Task(
|
|
37
|
+
id=d["task_id"],
|
|
38
|
+
prompt=d["question"],
|
|
39
|
+
metadata={
|
|
40
|
+
"choices": d["choices"],
|
|
41
|
+
"answer": d["answer"],
|
|
42
|
+
"domain": d["domain"],
|
|
43
|
+
"subdomain": d["subdomain"],
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return tasks
|
|
48
|
+
|
|
49
|
+
def build_prompt(self, task: Task) -> str:
|
|
50
|
+
choices = _format_choices(task.metadata["choices"])
|
|
51
|
+
return f"{SYSTEM}\n\n{task.prompt}\n\n{choices}"
|
|
52
|
+
|
|
53
|
+
def evaluate(self, task: Task, response: str) -> bool:
|
|
54
|
+
text = strip_think_tags(response)
|
|
55
|
+
choice = extract_choice(text, task.metadata["choices"])
|
|
56
|
+
return choice == task.metadata["answer"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""GSM8K benchmark - grade school math word problems."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from benchkit.benchmarks.base import Task
|
|
7
|
+
from benchkit.benchmarks.utils import strip_think_tags
|
|
8
|
+
|
|
9
|
+
DATASET = Path(__file__).parent.parent / "datasets" / "gsm8k.jsonl"
|
|
10
|
+
|
|
11
|
+
SYSTEM = (
|
|
12
|
+
"Solve the following math problem step by step. At the very end, write your "
|
|
13
|
+
'final answer as a single number after "####".'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _extract_leading_number(text: str) -> str | None:
|
|
18
|
+
token = []
|
|
19
|
+
started = False
|
|
20
|
+
|
|
21
|
+
for ch in text.strip():
|
|
22
|
+
if ch in "+-0123456789,.":
|
|
23
|
+
token.append(ch)
|
|
24
|
+
started = True
|
|
25
|
+
continue
|
|
26
|
+
if started:
|
|
27
|
+
break
|
|
28
|
+
|
|
29
|
+
value = "".join(token).strip(".,")
|
|
30
|
+
return value if any(ch.isdigit() for ch in value) else None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extract_last_number(text: str) -> str | None:
|
|
34
|
+
numbers = []
|
|
35
|
+
current = []
|
|
36
|
+
|
|
37
|
+
for ch in text:
|
|
38
|
+
if ch in "+-0123456789,.":
|
|
39
|
+
current.append(ch)
|
|
40
|
+
continue
|
|
41
|
+
if current:
|
|
42
|
+
token = "".join(current).strip(".,")
|
|
43
|
+
if any(c.isdigit() for c in token):
|
|
44
|
+
numbers.append(token)
|
|
45
|
+
current = []
|
|
46
|
+
|
|
47
|
+
if current:
|
|
48
|
+
token = "".join(current).strip(".,")
|
|
49
|
+
if any(c.isdigit() for c in token):
|
|
50
|
+
numbers.append(token)
|
|
51
|
+
|
|
52
|
+
return numbers[-1] if numbers else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_number(text: str | None) -> float | None:
|
|
56
|
+
if not text:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
cleaned = text.replace(",", "").strip()
|
|
60
|
+
if not cleaned:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
return float(cleaned)
|
|
65
|
+
except ValueError:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class GSM8K:
|
|
70
|
+
name = "gsm8k"
|
|
71
|
+
|
|
72
|
+
def load_tasks(self) -> list[Task]:
|
|
73
|
+
tasks = []
|
|
74
|
+
with open(DATASET) as f:
|
|
75
|
+
for line in f:
|
|
76
|
+
d = json.loads(line)
|
|
77
|
+
tasks.append(
|
|
78
|
+
Task(
|
|
79
|
+
id=d["task_id"],
|
|
80
|
+
prompt=d["question"],
|
|
81
|
+
metadata={"answer": d["answer"]},
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
return tasks
|
|
85
|
+
|
|
86
|
+
def build_prompt(self, task: Task) -> str:
|
|
87
|
+
return f"{SYSTEM}\n\n{task.prompt}"
|
|
88
|
+
|
|
89
|
+
def evaluate(self, task: Task, response: str) -> bool:
|
|
90
|
+
text = strip_think_tags(response)
|
|
91
|
+
|
|
92
|
+
if "####" in text:
|
|
93
|
+
predicted = _extract_leading_number(text.rsplit("####", 1)[1])
|
|
94
|
+
else:
|
|
95
|
+
predicted = _extract_last_number(text)
|
|
96
|
+
|
|
97
|
+
return _parse_number(predicted) == _parse_number(task.metadata["answer"])
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""HellaSwag benchmark - commonsense sentence completion."""
|
|
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" / "hellaswag.jsonl"
|
|
11
|
+
|
|
12
|
+
SYSTEM = "Complete the following sentence. Reply with ONLY the letter (A, B, C, or D)."
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _format_choices(choices: list[str]) -> str:
|
|
16
|
+
return "\n".join(
|
|
17
|
+
f"{letter}) {choice}" for letter, choice in zip("ABCD", choices, strict=False)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HellaSwag:
|
|
22
|
+
name = "hellaswag"
|
|
23
|
+
decision_instructions = "Which option is the most plausible continuation?"
|
|
24
|
+
|
|
25
|
+
def load_tasks(self) -> list[Task]:
|
|
26
|
+
tasks = []
|
|
27
|
+
with open(DATASET) as f:
|
|
28
|
+
for line in f:
|
|
29
|
+
d = json.loads(line)
|
|
30
|
+
tasks.append(
|
|
31
|
+
Task(
|
|
32
|
+
id=d["task_id"],
|
|
33
|
+
prompt=d["question"],
|
|
34
|
+
metadata={
|
|
35
|
+
"choices": d["choices"],
|
|
36
|
+
"answer": d["answer"],
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
return tasks
|
|
41
|
+
|
|
42
|
+
def build_prompt(self, task: Task) -> str:
|
|
43
|
+
choices = _format_choices(task.metadata["choices"])
|
|
44
|
+
return f"{SYSTEM}\n\n{task.prompt}\n\n{choices}"
|
|
45
|
+
|
|
46
|
+
def evaluate(self, task: Task, response: str) -> bool:
|
|
47
|
+
text = strip_think_tags(response)
|
|
48
|
+
choice = extract_choice(text, task.metadata["choices"])
|
|
49
|
+
return choice == task.metadata["answer"]
|