benchkit-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. benchkit/__init__.py +1 -0
  2. benchkit/_pi_proxy.py +219 -0
  3. benchkit/answer_key_guard.ts +49 -0
  4. benchkit/artifacts.py +87 -0
  5. benchkit/benchmarks/__init__.py +105 -0
  6. benchkit/benchmarks/aider_polyglot.py +405 -0
  7. benchkit/benchmarks/arc.py +52 -0
  8. benchkit/benchmarks/banking77.py +149 -0
  9. benchkit/benchmarks/base.py +19 -0
  10. benchkit/benchmarks/boolq.py +49 -0
  11. benchkit/benchmarks/chartqa.py +133 -0
  12. benchkit/benchmarks/code_verifier.py +77 -0
  13. benchkit/benchmarks/downloads.py +166 -0
  14. benchkit/benchmarks/evalplus.py +288 -0
  15. benchkit/benchmarks/git_surgery.py +485 -0
  16. benchkit/benchmarks/gpqa.py +56 -0
  17. benchkit/benchmarks/gsm8k.py +97 -0
  18. benchkit/benchmarks/hellaswag.py +49 -0
  19. benchkit/benchmarks/humaneval.py +103 -0
  20. benchkit/benchmarks/ifeval.py +56 -0
  21. benchkit/benchmarks/ifeval_instructions.py +477 -0
  22. benchkit/benchmarks/mbpp.py +60 -0
  23. benchkit/benchmarks/mc_arena.py +479 -0
  24. benchkit/benchmarks/mcq.py +94 -0
  25. benchkit/benchmarks/medfailbench.py +537 -0
  26. benchkit/benchmarks/mmlu.py +54 -0
  27. benchkit/benchmarks/mmlu_pro.py +62 -0
  28. benchkit/benchmarks/mmvp.py +115 -0
  29. benchkit/benchmarks/ocrbench.py +96 -0
  30. benchkit/benchmarks/openbookqa.py +52 -0
  31. benchkit/benchmarks/patcheval.py +929 -0
  32. benchkit/benchmarks/piqa.py +52 -0
  33. benchkit/benchmarks/ruler.py +172 -0
  34. benchkit/benchmarks/ruler_generator.py +535 -0
  35. benchkit/benchmarks/sanity.py +174 -0
  36. benchkit/benchmarks/treejs_arena.py +268 -0
  37. benchkit/benchmarks/truthfulqa.py +52 -0
  38. benchkit/benchmarks/utils.py +25 -0
  39. benchkit/benchmarks/vision.py +230 -0
  40. benchkit/benchmarks/vstar.py +108 -0
  41. benchkit/benchmarks/winogrande.py +52 -0
  42. benchkit/benchmarks/xstest.py +116 -0
  43. benchkit/browser.py +429 -0
  44. benchkit/catalog.py +121 -0
  45. benchkit/cli.py +1000 -0
  46. benchkit/client.py +1413 -0
  47. benchkit/controls.py +87 -0
  48. benchkit/datasets/MMLU_LICENSE.txt +21 -0
  49. benchkit/datasets/README.md +142 -0
  50. benchkit/datasets/arc_challenge.jsonl +1172 -0
  51. benchkit/datasets/banking77.jsonl +3080 -0
  52. benchkit/datasets/banking77_intents.json +79 -0
  53. benchkit/datasets/boolq.jsonl +3270 -0
  54. benchkit/datasets/code_verifier.jsonl +500 -0
  55. benchkit/datasets/gpqa_diamond.jsonl +198 -0
  56. benchkit/datasets/gsm8k.jsonl +1319 -0
  57. benchkit/datasets/hellaswag.jsonl +1000 -0
  58. benchkit/datasets/humaneval.jsonl +164 -0
  59. benchkit/datasets/ifeval.jsonl +541 -0
  60. benchkit/datasets/mbpp.jsonl +500 -0
  61. benchkit/datasets/mc_blocks_1_20_1.jsonl +1003 -0
  62. benchkit/datasets/medfailbench_hard30.jsonl +30 -0
  63. benchkit/datasets/medfailbench_hard30.md +13 -0
  64. benchkit/datasets/openbookqa.jsonl +500 -0
  65. benchkit/datasets/piqa.jsonl +1838 -0
  66. benchkit/datasets/sanity_code.jsonl +6 -0
  67. benchkit/datasets/truthfulqa.jsonl +817 -0
  68. benchkit/datasets/winogrande.jsonl +1267 -0
  69. benchkit/datasets/xstest.csv +451 -0
  70. benchkit/decision.py +194 -0
  71. benchkit/demo.py +451 -0
  72. benchkit/dockerfiles.py +247 -0
  73. benchkit/engine.py +1270 -0
  74. benchkit/evaluation.py +195 -0
  75. benchkit/events.py +141 -0
  76. benchkit/executor.py +96 -0
  77. benchkit/git_surgery/backport-release-stack/setup.sh +328 -0
  78. benchkit/git_surgery/backport-release-stack/verify.sh +119 -0
  79. benchkit/git_surgery/bisect-the-regression/setup.sh +49 -0
  80. benchkit/git_surgery/bisect-the-regression/verify.sh +34 -0
  81. benchkit/git_surgery/rebase-conflict-chain/setup.sh +65 -0
  82. benchkit/git_surgery/rebase-conflict-chain/verify.sh +43 -0
  83. benchkit/git_surgery/recover-complex-stash/setup.sh +152 -0
  84. benchkit/git_surgery/recover-complex-stash/verify.sh +95 -0
  85. benchkit/git_surgery/recover-lost-work/setup.sh +40 -0
  86. benchkit/git_surgery/recover-lost-work/verify.sh +36 -0
  87. benchkit/git_surgery/repair-force-pushed-remote/setup.sh +171 -0
  88. benchkit/git_surgery/repair-force-pushed-remote/verify.sh +83 -0
  89. benchkit/git_surgery/revert-merge-with-followups/setup.sh +198 -0
  90. benchkit/git_surgery/revert-merge-with-followups/verify.sh +98 -0
  91. benchkit/git_surgery/secret-in-history/README.md +27 -0
  92. benchkit/git_surgery/secret-in-history/setup.sh +107 -0
  93. benchkit/git_surgery/secret-in-history/verify.sh +106 -0
  94. benchkit/git_surgery/split-the-mega-commit/setup.sh +50 -0
  95. benchkit/git_surgery/split-the-mega-commit/verify.sh +48 -0
  96. benchkit/git_surgery/untangle-nested-submodules/setup.sh +193 -0
  97. benchkit/git_surgery/untangle-nested-submodules/verify.sh +90 -0
  98. benchkit/history.py +474 -0
  99. benchkit/jobs.py +332 -0
  100. benchkit/leakage.py +51 -0
  101. benchkit/looping.py +873 -0
  102. benchkit/mc_render.py +274 -0
  103. benchkit/mc_viewer/dist/atlas.png +0 -0
  104. benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
  105. benchkit/mc_viewer/dist/version.json +4 -0
  106. benchkit/mc_viewer/dist/viewer.html +23 -0
  107. benchkit/mc_viewer/dist/viewer.js +2 -0
  108. benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +25 -0
  109. benchkit/mc_viewer/dist/worker.js +2 -0
  110. benchkit/mc_viewer/dist/worker.js.LICENSE.txt +19 -0
  111. benchkit/metrics.py +100 -0
  112. benchkit/ollama.py +60 -0
  113. benchkit/perf.py +353 -0
  114. benchkit/perf_report.py +207 -0
  115. benchkit/perturbations.py +208 -0
  116. benchkit/pi_agent.py +476 -0
  117. benchkit/pi_package/package-lock.json +1827 -0
  118. benchkit/pi_package/package.json +8 -0
  119. benchkit/report.py +142 -0
  120. benchkit/report_markdown.py +645 -0
  121. benchkit/results.py +688 -0
  122. benchkit/retry.py +163 -0
  123. benchkit/runner.py +769 -0
  124. benchkit/sandbox.py +1167 -0
  125. benchkit/template_check.py +291 -0
  126. benchkit/templates/arena.html +504 -0
  127. benchkit/templates/history.html +58 -0
  128. benchkit/templates/perf.html +509 -0
  129. benchkit/templates/report.html +769 -0
  130. benchkit/tui/__init__.py +5 -0
  131. benchkit/tui/app.py +87 -0
  132. benchkit/tui/app.tcss +512 -0
  133. benchkit/tui/formatting.py +84 -0
  134. benchkit/tui/screens/__init__.py +1 -0
  135. benchkit/tui/screens/connect.py +191 -0
  136. benchkit/tui/screens/detail.py +288 -0
  137. benchkit/tui/screens/help.py +84 -0
  138. benchkit/tui/screens/modals.py +544 -0
  139. benchkit/tui/screens/results.py +361 -0
  140. benchkit/tui/screens/run.py +988 -0
  141. benchkit/tui/screens/setup.py +733 -0
  142. benchkit/tui/theme.py +135 -0
  143. benchkit/tui/widgets.py +120 -0
  144. benchkit_cli-0.1.0.dist-info/METADATA +294 -0
  145. benchkit_cli-0.1.0.dist-info/RECORD +149 -0
  146. benchkit_cli-0.1.0.dist-info/WHEEL +5 -0
  147. benchkit_cli-0.1.0.dist-info/entry_points.txt +3 -0
  148. benchkit_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  149. benchkit_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,361 @@
1
+ """Results screen: sortable summary plus drill-down into every task."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from rich.text import Text
8
+ from textual import events
9
+ from textual.app import ComposeResult
10
+ from textual.binding import Binding
11
+ from textual.containers import Horizontal, Vertical
12
+ from textual.screen import Screen
13
+ from textual.widgets import DataTable, Footer, Header, Static
14
+
15
+ from benchkit.metrics import (
16
+ aggregate_tok_s,
17
+ decision_speed,
18
+ effective_concurrency,
19
+ is_decision,
20
+ latency_text,
21
+ stream_tok_s,
22
+ )
23
+ from benchkit.tui.formatting import (
24
+ bar,
25
+ fmt_count,
26
+ fmt_duration,
27
+ score_color,
28
+ throughput_stat,
29
+ )
30
+ from benchkit.tui.screens.detail import JobDetailScreen
31
+ from benchkit.tui.widgets import SectionTitle, StatCard, apply_compact
32
+
33
+
34
+ def _benchmark_label(result: dict) -> str:
35
+ return str(result.get("benchmark_label") or result["benchmark"])
36
+
37
+
38
+ def _is_parallel(result: dict) -> bool:
39
+ return result.get("concurrency", 1) > 1
40
+
41
+
42
+ def _display_tok_s(result: dict) -> float:
43
+ return aggregate_tok_s(result) if _is_parallel(result) else stream_tok_s(result)
44
+
45
+
46
+ def _display_speed(result: dict) -> float:
47
+ if is_decision(result):
48
+ return float(result.get("decisions_per_s") or 0.0)
49
+ return _display_tok_s(result)
50
+
51
+
52
+ def _delta_cell(result: dict) -> Text | str:
53
+ value = result.get("score_delta_pp")
54
+ if value is None:
55
+ return "—"
56
+ delta = float(value)
57
+ style = "green" if delta > 0 else "red" if delta < 0 else "dim"
58
+ return Text(f"{delta:+.1f}", style=style)
59
+
60
+
61
+ def _harness_delta_cell(result: dict) -> Text | str:
62
+ value = result.get("harness_score_delta_pp")
63
+ if value is None:
64
+ return "—"
65
+ delta = float(value)
66
+ style = "green" if delta > 0 else "red" if delta < 0 else "dim"
67
+ return Text(f"{delta:+.1f}", style=style)
68
+
69
+
70
+ def _repair_delta_cell(result: dict) -> Text | str:
71
+ if not result.get("repair_attempts"):
72
+ return "—"
73
+ delta = float(result.get("repair_delta_pp", 0))
74
+ style = "green" if delta > 0 else "red" if delta < 0 else "dim"
75
+ return Text(f"{delta:+.1f}", style=style)
76
+
77
+
78
+ SORT_KEYS = {
79
+ "model": lambda r: (r["model"], r["benchmark"]),
80
+ "benchmark": lambda r: (_benchmark_label(r), -r["score"]),
81
+ "harness": lambda r: (r.get("harness", "direct"), -r["score"]),
82
+ "harness_delta": lambda r: -float(r.get("harness_score_delta_pp", 0)),
83
+ "repair_delta": lambda r: -float(r.get("repair_delta_pp", 0)),
84
+ "score": lambda r: -r["score"],
85
+ "delta": lambda r: -float(r.get("score_delta_pp", 0)),
86
+ "regressions": lambda r: -int(r.get("regressions", 0)),
87
+ "passed": lambda r: -r["passed"],
88
+ "errors": lambda r: -r.get("errors", 0),
89
+ "loops": lambda r: -r.get("loop_rate", 0),
90
+ "parallel": lambda r: -r.get("concurrency", 1),
91
+ "aggregate": lambda r: -aggregate_tok_s(r),
92
+ "stream": lambda r: -stream_tok_s(r),
93
+ "effective": lambda r: -effective_concurrency(r),
94
+ "avg": lambda r: r["avg_response_time"],
95
+ "time": lambda r: r["total_time"],
96
+ }
97
+
98
+
99
+ class ResultsScreen(Screen[None]):
100
+ """What the run produced, and where it was written."""
101
+
102
+ BINDINGS = [
103
+ Binding("s", "sort('score')", "Sort score"),
104
+ Binding("m", "sort('model')", "Sort model"),
105
+ Binding("t", "sort('time')", "Sort time"),
106
+ Binding("r", "again", "New run"),
107
+ Binding("c", "copy_path", "Copy path"),
108
+ Binding("escape", "again", "Back", show=False),
109
+ ]
110
+
111
+ def __init__(self, results: list[dict], output: Path | None) -> None:
112
+ super().__init__()
113
+ self.results = list(results)
114
+ self.has_parallel = any(_is_parallel(result) for result in self.results)
115
+ # Decision rows are measured in dec/s, so a table that mixes them with
116
+ # generating rows names the unit in every cell instead of the header.
117
+ self.has_decisions = any(is_decision(result) for result in self.results)
118
+ self.has_perturbations = any(
119
+ result.get("perturbation") for result in self.results
120
+ )
121
+ self.has_harness_pairs = any(
122
+ result.get("harness_score_delta_pp") is not None for result in self.results
123
+ )
124
+ self.has_repairs = any(
125
+ result.get("repair_attempts", 0) for result in self.results
126
+ )
127
+ self.output = output
128
+ self.sort_key = "score"
129
+
130
+ def compose(self) -> ComposeResult:
131
+ yield Header(show_clock=True)
132
+ with Vertical(id="results-root"):
133
+ with Horizontal(id="stat-row"):
134
+ yield StatCard("Best score", "--", id="stat-best")
135
+ yield StatCard("Runs", "0", id="stat-runs")
136
+ yield StatCard("Tasks", "0", id="stat-tasks")
137
+ yield StatCard("Loops", "0", id="stat-loops")
138
+ yield StatCard("Peak throughput", "--", id="stat-fastest")
139
+ yield StatCard("Total time", "--", id="stat-time")
140
+ with Vertical(classes="pane", id="results-pane"):
141
+ yield SectionTitle(
142
+ "Results",
143
+ "enter to inspect · click a header to sort",
144
+ id="results-title",
145
+ )
146
+ yield DataTable(id="summary", cursor_type="row", zebra_stripes=True)
147
+ yield Static("", id="output-path", classes="hint")
148
+ yield Footer()
149
+
150
+ def on_mount(self) -> None:
151
+ self.sub_title = "run complete"
152
+ table = self.query_one("#summary", DataTable)
153
+ table.add_column("Model", key="model")
154
+ table.add_column("Harness", key="harness", width=18)
155
+ table.add_column("Benchmark", key="benchmark")
156
+ table.add_column("Score", key="score", width=8)
157
+ table.add_column("", key="bar", width=14)
158
+ if self.has_harness_pairs:
159
+ table.add_column("Harness score Δ", key="harness_delta", width=15)
160
+ if self.has_repairs:
161
+ table.add_column("Repair Δ", key="repair_delta", width=9)
162
+ table.add_column("Fixed", key="repair_fixed", width=8)
163
+ if self.has_perturbations:
164
+ table.add_column("Δ pp", key="delta", width=7)
165
+ table.add_column("Regress", key="regressions", width=9)
166
+ table.add_column("Passed", key="passed", width=10)
167
+ table.add_column("Failures F/L/T/Len/H", key="errors", width=21)
168
+ if self.has_parallel:
169
+ table.add_column("Parallel", key="parallel", width=8)
170
+ table.add_column(
171
+ "Agg speed" if self.has_decisions else "Agg tok/s",
172
+ key="aggregate",
173
+ width=14 if self.has_decisions else 10,
174
+ )
175
+ table.add_column("Stream", key="stream", width=8)
176
+ table.add_column("Eff", key="effective", width=7)
177
+ else:
178
+ table.add_column(
179
+ "Speed" if self.has_decisions else "Tok/s",
180
+ key="stream",
181
+ width=14 if self.has_decisions else 9,
182
+ )
183
+ table.add_column("Avg", key="avg", width=8)
184
+ table.add_column("Wall", key="time", width=9)
185
+
186
+ self._fill_table()
187
+ self._fill_stats()
188
+ self.watch(self.app, "theme", self._theme_changed, init=False)
189
+
190
+ if self.output is not None:
191
+ path = self.output.resolve()
192
+ self.query_one("#output-path", Static).update(
193
+ f"Reports → {path} · results.json · csv · md · html"
194
+ )
195
+ else:
196
+ self.query_one("#output-path", Static).update(
197
+ "Nothing was written to disk."
198
+ )
199
+ table.focus()
200
+
201
+ def on_resize(self, event: events.Resize) -> None:
202
+ apply_compact(self, event.size.height)
203
+
204
+ def _theme_changed(self, _theme: str) -> None:
205
+ # Table cell colours are baked in, so redraw them for the new palette.
206
+ self._fill_table()
207
+
208
+ # Rendering --------------------------------------------------------
209
+
210
+ def _sorted(self) -> list[tuple[int, dict]]:
211
+ key = SORT_KEYS.get(self.sort_key, SORT_KEYS["score"])
212
+ return sorted(enumerate(self.results), key=lambda pair: key(pair[1]))
213
+
214
+ def _fill_table(self) -> None:
215
+ table = self.query_one("#summary", DataTable)
216
+ table.clear()
217
+ dark = self.app.current_theme.dark
218
+ for index, result in self._sorted():
219
+ score = result["score"]
220
+ color = score_color(score, dark)
221
+ cells: list[object] = [
222
+ result["model"],
223
+ result.get("harness_label", "Direct"),
224
+ _benchmark_label(result)
225
+ + (f" [{result['slice']}]" if result.get("slice") else ""),
226
+ Text(f"{score:.1f}%", style=f"bold {color}"),
227
+ Text(bar(score / 100, 12), style=color),
228
+ ]
229
+ if self.has_harness_pairs:
230
+ cells.append(_harness_delta_cell(result))
231
+ if self.has_repairs:
232
+ cells.extend(
233
+ [
234
+ _repair_delta_cell(result),
235
+ (
236
+ f"{result.get('repair_successes', 0)}/"
237
+ f"{result.get('repair_attempted', 0)}"
238
+ if result.get("repair_attempts")
239
+ else "—"
240
+ ),
241
+ ]
242
+ )
243
+ if self.has_perturbations:
244
+ cells.extend(
245
+ [
246
+ _delta_cell(result),
247
+ (
248
+ f"{result['regressions']}/{result['paired_total']}"
249
+ if result.get("paired_total") is not None
250
+ else "—"
251
+ ),
252
+ ]
253
+ )
254
+ cells.extend(
255
+ [
256
+ f"{result['passed']}/{result.get('scored_total', result['total'])}",
257
+ f"{result.get('failures', 0)}/{result.get('loop_kills', 0)}/"
258
+ f"{result.get('timeouts', 0)}/"
259
+ f"{result.get('length_exceeded', 0)}/"
260
+ f"{result.get('harness_errors', 0)}",
261
+ ]
262
+ )
263
+ decisions = is_decision(result)
264
+ unit = " tok/s" if self.has_decisions else ""
265
+ if self.has_parallel:
266
+ parallel = _is_parallel(result)
267
+ cells.extend(
268
+ [
269
+ str(result.get("concurrency", 1)),
270
+ decision_speed(result)[0]
271
+ if decisions
272
+ else f"{aggregate_tok_s(result):.1f}{unit}"
273
+ if parallel
274
+ else "—",
275
+ "—" if decisions else f"{stream_tok_s(result):.1f}",
276
+ f"{effective_concurrency(result):.2f}x" if parallel else "—",
277
+ ]
278
+ )
279
+ else:
280
+ cells.append(
281
+ decision_speed(result)[0]
282
+ if decisions
283
+ else f"{stream_tok_s(result):.1f}{unit}"
284
+ )
285
+ cells.extend([latency_text(result), fmt_duration(result["total_time"])])
286
+ table.add_row(
287
+ *cells,
288
+ key=str(index),
289
+ )
290
+
291
+ def _fill_stats(self) -> None:
292
+ if not self.results:
293
+ return
294
+ overall_results = [
295
+ result for result in self.results if result.get("include_in_overall", True)
296
+ ]
297
+ best = max(overall_results or self.results, key=lambda r: r["score"])
298
+ # Tokens/s and decisions/s are different units, so the fastest card
299
+ # compares generating runs and only falls back to decision runs.
300
+ generating = [r for r in self.results if not is_decision(r)]
301
+ fastest = max(generating or self.results, key=_display_speed)
302
+ tasks = sum(r["total"] for r in self.results)
303
+ total_time = sum(r["total_time"] for r in self.results)
304
+ loops = sum(r.get("loops", 0) for r in self.results)
305
+ loop_kills = sum(r.get("loop_kills", 0) for r in self.results)
306
+
307
+ self.query_one("#stat-best", StatCard).set_state(
308
+ f"{best['score']:.1f}%",
309
+ f"{best['model']} · {best.get('harness_label', 'Direct')} · "
310
+ f"{_benchmark_label(best)}",
311
+ )
312
+ self.query_one("#stat-runs", StatCard).set_state(str(len(self.results)))
313
+ self.query_one("#stat-tasks", StatCard).set_state(fmt_count(tasks))
314
+ self.query_one("#stat-loops", StatCard).set_state(
315
+ str(loops),
316
+ (f"{loops / tasks * 100:.1f}% · {loop_kills} killed" if tasks else ""),
317
+ )
318
+ fastest_card = self.query_one("#stat-fastest", StatCard)
319
+ value, hint = (
320
+ decision_speed(fastest)
321
+ if is_decision(fastest)
322
+ else throughput_stat(
323
+ concurrency=fastest.get("concurrency", 1),
324
+ aggregate=aggregate_tok_s(fastest),
325
+ stream=stream_tok_s(fastest),
326
+ effective=effective_concurrency(fastest),
327
+ precision=0,
328
+ )
329
+ )
330
+ fastest_card.set_state(value, f"{fastest['model']} · {hint}")
331
+ self.query_one("#stat-time", StatCard).set_state(fmt_duration(total_time))
332
+
333
+ # Events -----------------------------------------------------------
334
+
335
+ def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
336
+ index = int(str(event.row_key.value))
337
+ self.app.push_screen(JobDetailScreen(self.results[index]))
338
+
339
+ def on_data_table_header_selected(self, event: DataTable.HeaderSelected) -> None:
340
+ key = str(event.column_key.value)
341
+ if key in SORT_KEYS:
342
+ self.action_sort(key)
343
+
344
+ # Actions ----------------------------------------------------------
345
+
346
+ def action_sort(self, key: str) -> None:
347
+ self.sort_key = key
348
+ self._fill_table()
349
+ self.query_one("#results-title", SectionTitle).set_detail(
350
+ f"sorted by {key} · enter to inspect"
351
+ )
352
+
353
+ def action_again(self) -> None:
354
+ self.app.back_to_setup()
355
+
356
+ def action_copy_path(self) -> None:
357
+ if self.output is None:
358
+ return
359
+ path = str(self.output.resolve())
360
+ self.app.copy_to_clipboard(path)
361
+ self.notify(f"Copied {path}", timeout=3)