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
benchkit/evaluation.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Verifier results and answer-safe repair prompts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from numbers import Real
|
|
7
|
+
|
|
8
|
+
from benchkit.benchmarks.base import Task
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class EvaluationResult:
|
|
13
|
+
"""Normalized benchmark score plus feedback safe to return to a model."""
|
|
14
|
+
|
|
15
|
+
score: float
|
|
16
|
+
feedback: str = ""
|
|
17
|
+
error: str = ""
|
|
18
|
+
details: dict = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def passed(self) -> bool:
|
|
22
|
+
return self.score >= 1.0 and not self.error
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _score(value: object) -> float:
|
|
26
|
+
if isinstance(value, bool):
|
|
27
|
+
return 1.0 if value else 0.0
|
|
28
|
+
if isinstance(value, Real):
|
|
29
|
+
return min(1.0, max(0.0, float(value)))
|
|
30
|
+
return 1.0 if bool(value) else 0.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def evaluate_response(
|
|
34
|
+
bench: object, task: Task, response: str, *, decision: bool = False
|
|
35
|
+
) -> EvaluationResult:
|
|
36
|
+
"""Evaluate one response and add generic feedback when needed.
|
|
37
|
+
|
|
38
|
+
A decision model's response is the chosen option key, not text. Most
|
|
39
|
+
benchmarks score that with their ordinary ``evaluate()``; one whose
|
|
40
|
+
evaluator reads prose (XSTest's refusal matcher) supplies
|
|
41
|
+
``evaluate_decision`` instead.
|
|
42
|
+
"""
|
|
43
|
+
detailed = getattr(bench, "evaluate_with_feedback", None)
|
|
44
|
+
decide = getattr(bench, "evaluate_decision", None) if decision else None
|
|
45
|
+
raw = (
|
|
46
|
+
decide(task, response)
|
|
47
|
+
if callable(decide)
|
|
48
|
+
else detailed(task, response)
|
|
49
|
+
if callable(detailed)
|
|
50
|
+
else bench.evaluate(task, response)
|
|
51
|
+
)
|
|
52
|
+
if isinstance(raw, EvaluationResult):
|
|
53
|
+
result = EvaluationResult(
|
|
54
|
+
score=_score(raw.score),
|
|
55
|
+
feedback=raw.feedback.strip(),
|
|
56
|
+
error=raw.error.strip(),
|
|
57
|
+
details=dict(raw.details),
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
result = EvaluationResult(score=_score(raw))
|
|
61
|
+
|
|
62
|
+
if result.passed or result.error or result.feedback:
|
|
63
|
+
return result
|
|
64
|
+
if result.score > 0:
|
|
65
|
+
feedback = (
|
|
66
|
+
f"The verifier awarded {result.score * 100:.1f}% partial credit. "
|
|
67
|
+
"Re-check the requirements and replace the answer with a corrected one."
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
feedback = (
|
|
71
|
+
"The verifier marked the previous answer incorrect. Re-check the "
|
|
72
|
+
"reasoning and replace it with a corrected answer."
|
|
73
|
+
)
|
|
74
|
+
return EvaluationResult(
|
|
75
|
+
score=result.score,
|
|
76
|
+
feedback=feedback,
|
|
77
|
+
details=dict(result.details),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def repair_message(feedback: str, attempt: int, total: int) -> str:
|
|
82
|
+
"""Build one follow-up user turn without exposing verifier secrets."""
|
|
83
|
+
remaining = total - attempt
|
|
84
|
+
suffix = (
|
|
85
|
+
"This is the only repair attempt."
|
|
86
|
+
if total == 1
|
|
87
|
+
else f"You have {remaining} repair attempt(s) remaining after this one."
|
|
88
|
+
)
|
|
89
|
+
return (
|
|
90
|
+
"BenchKit verifier feedback:\n"
|
|
91
|
+
f"{feedback.strip()}\n\n"
|
|
92
|
+
f"{suffix} Use the available tools if helpful, then return a complete "
|
|
93
|
+
"replacement answer for the original task."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def standalone_repair_prompt(
|
|
98
|
+
original_prompt: str,
|
|
99
|
+
previous_response: str,
|
|
100
|
+
feedback: str,
|
|
101
|
+
attempt: int,
|
|
102
|
+
total: int,
|
|
103
|
+
) -> str:
|
|
104
|
+
"""Render a follow-up transcript for stateless direct inference APIs."""
|
|
105
|
+
return (
|
|
106
|
+
"Continue the following benchmark exchange.\n\n"
|
|
107
|
+
"Original user request:\n"
|
|
108
|
+
f"{original_prompt}\n\n"
|
|
109
|
+
"Previous assistant answer:\n"
|
|
110
|
+
f"{previous_response}\n\n"
|
|
111
|
+
f"{repair_message(feedback, attempt, total)}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def combine_attempts(
|
|
116
|
+
attempts: list[tuple[dict, EvaluationResult]],
|
|
117
|
+
feedback_sent: list[str],
|
|
118
|
+
) -> dict:
|
|
119
|
+
"""Combine multiple model calls into one final generation result."""
|
|
120
|
+
if not attempts:
|
|
121
|
+
raise ValueError("at least one generation attempt is required")
|
|
122
|
+
|
|
123
|
+
final = dict(attempts[-1][0])
|
|
124
|
+
total_output = sum(int(gen.get("eval_count") or 0) for gen, _ in attempts)
|
|
125
|
+
total_input = sum(int(gen.get("input_tokens") or 0) for gen, _ in attempts)
|
|
126
|
+
generation_ns = sum(int(gen.get("eval_duration_ns") or 0) for gen, _ in attempts)
|
|
127
|
+
# Tokens recovered from an untimed stream have no decode time to divide by.
|
|
128
|
+
timed_output = sum(
|
|
129
|
+
int(gen.get("eval_count") or 0)
|
|
130
|
+
for gen, _ in attempts
|
|
131
|
+
if int(gen.get("eval_duration_ns") or 0) > 0
|
|
132
|
+
)
|
|
133
|
+
response_time = sum(float(gen.get("response_time_s") or 0.0) for gen, _ in attempts)
|
|
134
|
+
traces: list[dict] = []
|
|
135
|
+
for number, (gen, _) in enumerate(attempts, start=1):
|
|
136
|
+
traces.extend(
|
|
137
|
+
{**call, "attempt": number} for call in gen.get("tool_trace") or []
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
first_evaluation = attempts[0][1]
|
|
141
|
+
final_evaluation = attempts[-1][1]
|
|
142
|
+
attempt_records = []
|
|
143
|
+
for number, (gen, evaluation) in enumerate(attempts, start=1):
|
|
144
|
+
attempt_records.append(
|
|
145
|
+
{
|
|
146
|
+
"attempt": number,
|
|
147
|
+
"response": str(gen.get("response") or ""),
|
|
148
|
+
"thinking": str(gen.get("thinking") or ""),
|
|
149
|
+
"score": round(evaluation.score * 100, 1),
|
|
150
|
+
"passed": evaluation.passed,
|
|
151
|
+
"feedback": evaluation.feedback,
|
|
152
|
+
"error": evaluation.error,
|
|
153
|
+
"input_tokens": int(gen.get("input_tokens") or 0),
|
|
154
|
+
"output_tokens": int(gen.get("eval_count") or 0),
|
|
155
|
+
"response_time_s": round(float(gen.get("response_time_s") or 0.0), 3),
|
|
156
|
+
"model_turns": int(gen.get("model_turns") or 1),
|
|
157
|
+
"tool_calls": int(gen.get("tool_calls") or 0),
|
|
158
|
+
"done_reason": str(gen.get("done_reason") or ""),
|
|
159
|
+
"length_exceeded": bool(gen.get("length_exceeded")),
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
final.update(
|
|
164
|
+
tok_s=(
|
|
165
|
+
timed_output / (generation_ns / 1e9)
|
|
166
|
+
if generation_ns > 0
|
|
167
|
+
else total_output / response_time
|
|
168
|
+
if response_time > 0
|
|
169
|
+
else 0.0
|
|
170
|
+
),
|
|
171
|
+
eval_count=total_output,
|
|
172
|
+
eval_duration_ns=generation_ns,
|
|
173
|
+
response_time_s=response_time,
|
|
174
|
+
input_tokens=total_input,
|
|
175
|
+
model_turns=sum(int(gen.get("model_turns") or 1) for gen, _ in attempts),
|
|
176
|
+
tool_calls=sum(int(gen.get("tool_calls") or 0) for gen, _ in attempts),
|
|
177
|
+
tool_trace=traces,
|
|
178
|
+
attempts=attempt_records,
|
|
179
|
+
repair_attempts_used=len(attempts) - 1,
|
|
180
|
+
repair_feedback=list(feedback_sent),
|
|
181
|
+
first_attempt_score=first_evaluation.score,
|
|
182
|
+
evaluation_score=final_evaluation.score,
|
|
183
|
+
evaluation_feedback=final_evaluation.feedback,
|
|
184
|
+
evaluation_error=final_evaluation.error,
|
|
185
|
+
evaluation_details=dict(final_evaluation.details),
|
|
186
|
+
repaired=(not first_evaluation.passed and final_evaluation.passed),
|
|
187
|
+
trace_status=(
|
|
188
|
+
"observed"
|
|
189
|
+
if any(gen.get("trace_status") == "observed" for gen, _ in attempts)
|
|
190
|
+
else "available_empty"
|
|
191
|
+
if any(gen.get("trace_status") == "available_empty" for gen, _ in attempts)
|
|
192
|
+
else str(final.get("trace_status") or "unavailable")
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
return final
|
benchkit/events.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Events the engine emits while it runs.
|
|
2
|
+
|
|
3
|
+
Pure data shared by the TUI and the headless runner. ``JobSpec`` and
|
|
4
|
+
``TaskRecord`` are only needed for annotations, so this module imports nothing
|
|
5
|
+
at runtime.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import TYPE_CHECKING, Literal
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from benchkit.jobs import JobSpec
|
|
16
|
+
from benchkit.results import TaskRecord
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RunStarted:
|
|
21
|
+
jobs: list[JobSpec]
|
|
22
|
+
total_tasks: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class JobStarted:
|
|
27
|
+
index: int
|
|
28
|
+
job: JobSpec
|
|
29
|
+
total: int
|
|
30
|
+
overall_total: int
|
|
31
|
+
concurrency: int = 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class TaskPhase:
|
|
36
|
+
"""Current phase of a task before its terminal result is available."""
|
|
37
|
+
|
|
38
|
+
index: int
|
|
39
|
+
job: JobSpec
|
|
40
|
+
position: int
|
|
41
|
+
total: int
|
|
42
|
+
task_id: str
|
|
43
|
+
entry_point: str
|
|
44
|
+
phase: Literal[
|
|
45
|
+
"generating",
|
|
46
|
+
"evaluating",
|
|
47
|
+
"timed_out",
|
|
48
|
+
"loop_killed",
|
|
49
|
+
"length_exceeded",
|
|
50
|
+
"harness_error",
|
|
51
|
+
]
|
|
52
|
+
activity: str
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def label(self) -> str:
|
|
56
|
+
return (
|
|
57
|
+
f"{self.task_id} ({self.entry_point})" if self.entry_point else self.task_id
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class GenerationProgress:
|
|
63
|
+
"""Throttled live state from an in-flight model generation."""
|
|
64
|
+
|
|
65
|
+
index: int
|
|
66
|
+
job: JobSpec
|
|
67
|
+
position: int
|
|
68
|
+
total: int
|
|
69
|
+
task_id: str
|
|
70
|
+
entry_point: str
|
|
71
|
+
phase: Literal["waiting", "thinking", "answering"]
|
|
72
|
+
elapsed_s: float
|
|
73
|
+
thinking_chars: int
|
|
74
|
+
response_chars: int
|
|
75
|
+
trace_status: str
|
|
76
|
+
loop_state: str
|
|
77
|
+
loop_score: float
|
|
78
|
+
loop_source: str
|
|
79
|
+
loop_kill_remaining_s: float | None
|
|
80
|
+
prompt: str
|
|
81
|
+
thinking: str
|
|
82
|
+
response: str
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def label(self) -> str:
|
|
86
|
+
return (
|
|
87
|
+
f"{self.task_id} ({self.entry_point})" if self.entry_point else self.task_id
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class TaskCompleted:
|
|
93
|
+
index: int
|
|
94
|
+
job: JobSpec
|
|
95
|
+
record: TaskRecord
|
|
96
|
+
passed: int
|
|
97
|
+
completed: int
|
|
98
|
+
score_points: float | None = None
|
|
99
|
+
scored_total: int | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class JobCompleted:
|
|
104
|
+
index: int
|
|
105
|
+
job: JobSpec
|
|
106
|
+
result: dict
|
|
107
|
+
skipped: bool
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class RunCompleted:
|
|
112
|
+
results: list[dict]
|
|
113
|
+
stopped: bool
|
|
114
|
+
elapsed: float
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class ModelUnloaded:
|
|
119
|
+
"""A force-unload attempt for a model that finished its last job."""
|
|
120
|
+
|
|
121
|
+
model: str
|
|
122
|
+
error: str = ""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class RunFailed:
|
|
127
|
+
message: str
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
EngineEvent = (
|
|
131
|
+
RunStarted
|
|
132
|
+
| JobStarted
|
|
133
|
+
| TaskPhase
|
|
134
|
+
| GenerationProgress
|
|
135
|
+
| TaskCompleted
|
|
136
|
+
| JobCompleted
|
|
137
|
+
| ModelUnloaded
|
|
138
|
+
| RunCompleted
|
|
139
|
+
| RunFailed
|
|
140
|
+
)
|
|
141
|
+
Sink = Callable[[EngineEvent], None]
|
benchkit/executor.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Sandboxed code execution via subprocess."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
BLOCKED_MODULES = {
|
|
10
|
+
"subprocess",
|
|
11
|
+
"shutil",
|
|
12
|
+
"socket",
|
|
13
|
+
"http",
|
|
14
|
+
"urllib",
|
|
15
|
+
"requests",
|
|
16
|
+
"signal",
|
|
17
|
+
"ctypes",
|
|
18
|
+
"multiprocessing",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
SANDBOX_HEADER = f"""
|
|
22
|
+
import builtins as _b
|
|
23
|
+
_orig = _b.__import__
|
|
24
|
+
_blocked = {BLOCKED_MODULES!r}
|
|
25
|
+
|
|
26
|
+
def _safe(name, *a, **kw):
|
|
27
|
+
if name.split(".")[0] in _blocked:
|
|
28
|
+
raise ImportError(f"blocked: {{name}}")
|
|
29
|
+
return _orig(name, *a, **kw)
|
|
30
|
+
|
|
31
|
+
_b.__import__ = _safe
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class ExecutionResult:
|
|
37
|
+
"""Code-verifier result with answer-safe diagnostic feedback."""
|
|
38
|
+
|
|
39
|
+
passed: bool
|
|
40
|
+
feedback: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _failure_feedback(stderr: str) -> str:
|
|
44
|
+
"""Describe the failure category without exposing hidden test source."""
|
|
45
|
+
if "SyntaxError" in stderr:
|
|
46
|
+
return "The submitted Python failed to parse with SyntaxError."
|
|
47
|
+
if "IndentationError" in stderr:
|
|
48
|
+
return "The submitted Python failed to parse with IndentationError."
|
|
49
|
+
if "AssertionError" in stderr:
|
|
50
|
+
return "The submitted Python ran, but a verifier assertion failed."
|
|
51
|
+
for name in (
|
|
52
|
+
"NameError",
|
|
53
|
+
"TypeError",
|
|
54
|
+
"ValueError",
|
|
55
|
+
"IndexError",
|
|
56
|
+
"KeyError",
|
|
57
|
+
"AttributeError",
|
|
58
|
+
"ZeroDivisionError",
|
|
59
|
+
"RecursionError",
|
|
60
|
+
"ImportError",
|
|
61
|
+
):
|
|
62
|
+
if name in stderr:
|
|
63
|
+
return f"The submitted Python failed during verification with {name}."
|
|
64
|
+
return "The submitted Python exited unsuccessfully during verification."
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def execute_with_feedback(code: str, timeout: int = 10) -> ExecutionResult:
|
|
68
|
+
"""Run code and retain only a sanitized failure category."""
|
|
69
|
+
full = SANDBOX_HEADER + "\n" + code
|
|
70
|
+
|
|
71
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
72
|
+
f.write(full)
|
|
73
|
+
path = Path(f.name)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
[sys.executable, str(path)],
|
|
78
|
+
capture_output=True,
|
|
79
|
+
text=True,
|
|
80
|
+
timeout=timeout,
|
|
81
|
+
)
|
|
82
|
+
if result.returncode == 0:
|
|
83
|
+
return ExecutionResult(True)
|
|
84
|
+
return ExecutionResult(False, _failure_feedback(result.stderr or ""))
|
|
85
|
+
except subprocess.TimeoutExpired:
|
|
86
|
+
return ExecutionResult(
|
|
87
|
+
False,
|
|
88
|
+
f"The submitted Python exceeded the {timeout}s verifier timeout.",
|
|
89
|
+
)
|
|
90
|
+
finally:
|
|
91
|
+
path.unlink(missing_ok=True)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def execute(code: str, timeout: int = 10) -> bool:
|
|
95
|
+
"""Run code in a subprocess. Returns True if exit code is 0."""
|
|
96
|
+
return execute_with_feedback(code, timeout).passed
|