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
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