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,544 @@
|
|
|
1
|
+
"""Modal dialogs: task limits, task inspection, datasets and confirmations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
from textual import work
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
from textual.content import Content
|
|
11
|
+
from textual.css.query import NoMatches
|
|
12
|
+
from textual.screen import ModalScreen
|
|
13
|
+
from textual.widgets import (
|
|
14
|
+
Button,
|
|
15
|
+
DataTable,
|
|
16
|
+
Footer,
|
|
17
|
+
Input,
|
|
18
|
+
Static,
|
|
19
|
+
TabbedContent,
|
|
20
|
+
TabPane,
|
|
21
|
+
TextArea,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from benchkit import catalog
|
|
25
|
+
from benchkit.benchmarks.downloads import fmt_bytes
|
|
26
|
+
from benchkit.engine import SliceError, parse_slice, slice_label
|
|
27
|
+
from benchkit.template_check import TemplateReport
|
|
28
|
+
from benchkit.tui.formatting import fmt_count, fmt_duration
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LimitScreen(ModalScreen[str | None]):
|
|
32
|
+
"""Ask for a per-benchmark task slice."""
|
|
33
|
+
|
|
34
|
+
BINDINGS = [
|
|
35
|
+
Binding("escape", "cancel", "Cancel"),
|
|
36
|
+
Binding("ctrl+x", "clear", "Run all tasks"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
def __init__(self, benchmark: str, current: str, total: int) -> None:
|
|
40
|
+
super().__init__()
|
|
41
|
+
self.benchmark = benchmark
|
|
42
|
+
self.current = current
|
|
43
|
+
self.total = total
|
|
44
|
+
|
|
45
|
+
def compose(self) -> ComposeResult:
|
|
46
|
+
with Vertical(id="limit-dialog", classes="dialog"):
|
|
47
|
+
yield Static(f"Task limit · {self.benchmark}", classes="card-title")
|
|
48
|
+
yield Static(f"{fmt_count(self.total)} tasks available", classes="hint")
|
|
49
|
+
yield Input(value=self.current, placeholder="all tasks", id="limit-input")
|
|
50
|
+
yield Static(
|
|
51
|
+
"20 → first 20 -20 → last 20 40-80 → tasks 40 through 80",
|
|
52
|
+
classes="hint",
|
|
53
|
+
)
|
|
54
|
+
yield Static("", id="limit-preview", classes="hint")
|
|
55
|
+
with Horizontal(classes="dialog-actions"):
|
|
56
|
+
yield Button("Apply", id="apply", variant="primary")
|
|
57
|
+
yield Button("Run all", id="clear")
|
|
58
|
+
yield Button("Cancel", id="cancel")
|
|
59
|
+
|
|
60
|
+
def on_mount(self) -> None:
|
|
61
|
+
self.query_one("#limit-input", Input).focus()
|
|
62
|
+
self._preview(self.current)
|
|
63
|
+
|
|
64
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
65
|
+
self._preview(event.value)
|
|
66
|
+
|
|
67
|
+
def on_input_submitted(self) -> None:
|
|
68
|
+
self._apply()
|
|
69
|
+
|
|
70
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
71
|
+
if event.button.id == "apply":
|
|
72
|
+
self._apply()
|
|
73
|
+
elif event.button.id == "clear":
|
|
74
|
+
self.action_clear()
|
|
75
|
+
else:
|
|
76
|
+
self.action_cancel()
|
|
77
|
+
|
|
78
|
+
def _preview(self, value: str) -> None:
|
|
79
|
+
preview = self.query_one("#limit-preview", Static)
|
|
80
|
+
spec = value.strip()
|
|
81
|
+
if not spec:
|
|
82
|
+
preview.update("Runs every task.")
|
|
83
|
+
return
|
|
84
|
+
try:
|
|
85
|
+
start, end = parse_slice(spec, self.total or 1)
|
|
86
|
+
except SliceError:
|
|
87
|
+
preview.update("[b]Invalid[/b] — use N, -N or A-B.")
|
|
88
|
+
return
|
|
89
|
+
preview.update(f"{slice_label(spec)} → {fmt_count(max(0, end - start))} tasks")
|
|
90
|
+
|
|
91
|
+
def _apply(self) -> None:
|
|
92
|
+
spec = self.query_one("#limit-input", Input).value.strip()
|
|
93
|
+
if spec:
|
|
94
|
+
try:
|
|
95
|
+
parse_slice(spec, self.total or 1)
|
|
96
|
+
except SliceError:
|
|
97
|
+
self.notify("Invalid slice — use N, -N or A-B.", severity="error")
|
|
98
|
+
return
|
|
99
|
+
self.dismiss(spec or None)
|
|
100
|
+
|
|
101
|
+
def action_clear(self) -> None:
|
|
102
|
+
self.dismiss(None)
|
|
103
|
+
|
|
104
|
+
def action_cancel(self) -> None:
|
|
105
|
+
self.dismiss(self.current or None)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _attempts_text(attempts: list[dict]) -> str:
|
|
109
|
+
"""Render verifier-feedback attempts as a readable transcript."""
|
|
110
|
+
sections = []
|
|
111
|
+
for attempt in attempts:
|
|
112
|
+
number = int(attempt.get("attempt") or len(sections) + 1)
|
|
113
|
+
status = "PASS" if attempt.get("passed") else "FAIL"
|
|
114
|
+
lines = [
|
|
115
|
+
f"ATTEMPT {number} · {status} · {float(attempt.get('score') or 0):.1f}%"
|
|
116
|
+
]
|
|
117
|
+
if attempt.get("feedback"):
|
|
118
|
+
lines.extend(["", "Verifier feedback:", str(attempt["feedback"])])
|
|
119
|
+
if attempt.get("thinking"):
|
|
120
|
+
lines.extend(["", "Thinking:", str(attempt["thinking"])])
|
|
121
|
+
lines.extend(["", "Response:", str(attempt.get("response") or "")])
|
|
122
|
+
sections.append("\n".join(lines))
|
|
123
|
+
return ("\n\n" + "─" * 48 + "\n\n").join(sections)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TaskDetailScreen(ModalScreen[None]):
|
|
127
|
+
"""Prompt, response and error for a single task."""
|
|
128
|
+
|
|
129
|
+
BINDINGS = [
|
|
130
|
+
Binding("escape", "close", "Close"),
|
|
131
|
+
Binding("q", "close", "Close", show=False),
|
|
132
|
+
Binding("c", "copy", "Copy response"),
|
|
133
|
+
Binding("y", "copy_thinking", "Copy thinking"),
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
def __init__(self, context: str, task: dict, *, live: bool = False) -> None:
|
|
137
|
+
super().__init__()
|
|
138
|
+
self.context = context
|
|
139
|
+
self.task_data = task
|
|
140
|
+
self.live = live
|
|
141
|
+
|
|
142
|
+
def _header(self) -> Content:
|
|
143
|
+
live_phase = self.task_data.get("live_phase") if self.live else None
|
|
144
|
+
passed = self.task_data.get("passed")
|
|
145
|
+
error = self.task_data.get("error")
|
|
146
|
+
if live_phase:
|
|
147
|
+
status, style = str(live_phase).upper(), "b blue"
|
|
148
|
+
elif self.task_data.get("loop_killed"):
|
|
149
|
+
status, style = "LOOP KILLED", "b red"
|
|
150
|
+
elif self.task_data.get("timed_out"):
|
|
151
|
+
status, style = "TIMEOUT", "b yellow"
|
|
152
|
+
elif self.task_data.get("length_exceeded"):
|
|
153
|
+
status, style = "LENGTH EXCEEDED", "b yellow"
|
|
154
|
+
elif error:
|
|
155
|
+
status, style = "ERROR", "b yellow"
|
|
156
|
+
elif passed:
|
|
157
|
+
status, style = "PASS", "b green"
|
|
158
|
+
elif float(self.task_data.get("score", 0)) > 0:
|
|
159
|
+
status = f"PARTIAL {float(self.task_data['score']):.0f}%"
|
|
160
|
+
style = "b yellow"
|
|
161
|
+
else:
|
|
162
|
+
status, style = "FAIL", "b red"
|
|
163
|
+
return Content.from_markup(
|
|
164
|
+
"[b]$task[/b] [$style]$status[/$style] [dim]$context[/dim]",
|
|
165
|
+
task=self.task_data.get("task_id", ""),
|
|
166
|
+
status=status,
|
|
167
|
+
style=style,
|
|
168
|
+
context=self.context,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def _metadata(self) -> str:
|
|
172
|
+
loop_state = self.task_data.get("loop_state", "unavailable")
|
|
173
|
+
loop_label = (
|
|
174
|
+
"recovered" if self.task_data.get("recovered_cycle") else loop_state
|
|
175
|
+
)
|
|
176
|
+
loop_score = float(self.task_data.get("loop_score", 0.0))
|
|
177
|
+
trace = self.task_data.get("trace_status", "unavailable")
|
|
178
|
+
pieces = [
|
|
179
|
+
fmt_duration(self.task_data.get("response_time_s", 0)),
|
|
180
|
+
f"{self.task_data.get('tok_s', 0)} stream tok/s",
|
|
181
|
+
f"trace {trace}",
|
|
182
|
+
f"loop {loop_label} ({loop_score:.0%})",
|
|
183
|
+
]
|
|
184
|
+
if self.task_data.get("repair_attempts_used"):
|
|
185
|
+
pieces.append(
|
|
186
|
+
f"repaired after {self.task_data['repair_attempts_used']} feedback turn"
|
|
187
|
+
)
|
|
188
|
+
if float(self.task_data.get("score", 0)) > 0 and not self.task_data.get(
|
|
189
|
+
"passed"
|
|
190
|
+
):
|
|
191
|
+
pieces.insert(0, f"{float(self.task_data['score']):.0f}% task score")
|
|
192
|
+
if self.task_data.get("error"):
|
|
193
|
+
pieces.append(self.task_data["error"])
|
|
194
|
+
elif self.task_data.get("loop_kill_remaining_s") is not None:
|
|
195
|
+
pieces.append(f"kill in {self.task_data['loop_kill_remaining_s']:.1f}s")
|
|
196
|
+
return " · ".join(pieces)
|
|
197
|
+
|
|
198
|
+
def compose(self) -> ComposeResult:
|
|
199
|
+
with Vertical(id="task-dialog", classes="dialog"):
|
|
200
|
+
yield Static(self._header(), id="task-header")
|
|
201
|
+
yield Static(
|
|
202
|
+
self._metadata(),
|
|
203
|
+
classes="hint",
|
|
204
|
+
id="task-meta",
|
|
205
|
+
)
|
|
206
|
+
with TabbedContent(initial="thinking-tab" if self.live else "response-tab"):
|
|
207
|
+
with TabPane("Thinking", id="thinking-tab"):
|
|
208
|
+
yield TextArea(
|
|
209
|
+
self.task_data.get("thinking", "")
|
|
210
|
+
or "(reasoning trace unavailable or empty)",
|
|
211
|
+
read_only=True,
|
|
212
|
+
soft_wrap=True,
|
|
213
|
+
id="thinking-text",
|
|
214
|
+
)
|
|
215
|
+
with TabPane("Response", id="response-tab"):
|
|
216
|
+
yield TextArea(
|
|
217
|
+
self.task_data.get("response", "") or "(empty response)",
|
|
218
|
+
read_only=True,
|
|
219
|
+
soft_wrap=True,
|
|
220
|
+
id="response-text",
|
|
221
|
+
)
|
|
222
|
+
if self.task_data.get("attempts"):
|
|
223
|
+
with TabPane("Attempts", id="attempts-tab"):
|
|
224
|
+
yield TextArea(
|
|
225
|
+
_attempts_text(self.task_data["attempts"]),
|
|
226
|
+
read_only=True,
|
|
227
|
+
soft_wrap=True,
|
|
228
|
+
id="attempts-text",
|
|
229
|
+
)
|
|
230
|
+
with TabPane("Prompt", id="prompt-tab"):
|
|
231
|
+
yield TextArea(
|
|
232
|
+
self.task_data.get("prompt", ""),
|
|
233
|
+
read_only=True,
|
|
234
|
+
soft_wrap=True,
|
|
235
|
+
id="prompt-text",
|
|
236
|
+
)
|
|
237
|
+
yield Footer()
|
|
238
|
+
|
|
239
|
+
def update_live(self, task: dict, *, finished: bool = False) -> None:
|
|
240
|
+
"""Refresh an open task inspector from a streamed snapshot."""
|
|
241
|
+
self.task_data.update(task)
|
|
242
|
+
if finished:
|
|
243
|
+
self.live = False
|
|
244
|
+
self.task_data.pop("live_phase", None)
|
|
245
|
+
if not self.is_mounted:
|
|
246
|
+
return
|
|
247
|
+
try:
|
|
248
|
+
self.query_one("#task-header", Static).update(self._header())
|
|
249
|
+
self.query_one("#task-meta", Static).update(self._metadata())
|
|
250
|
+
_update_live_text(
|
|
251
|
+
self.query_one("#thinking-text", TextArea),
|
|
252
|
+
self.task_data.get("thinking", ""),
|
|
253
|
+
"(reasoning trace unavailable or empty)",
|
|
254
|
+
)
|
|
255
|
+
_update_live_text(
|
|
256
|
+
self.query_one("#response-text", TextArea),
|
|
257
|
+
self.task_data.get("response", ""),
|
|
258
|
+
"(empty response)",
|
|
259
|
+
)
|
|
260
|
+
except NoMatches:
|
|
261
|
+
# A final stream update may race with the modal being dismissed.
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
def action_close(self) -> None:
|
|
265
|
+
self.dismiss(None)
|
|
266
|
+
|
|
267
|
+
def action_copy(self) -> None:
|
|
268
|
+
self.app.copy_to_clipboard(self.task_data.get("response", ""))
|
|
269
|
+
self.notify("Response copied to clipboard", timeout=2)
|
|
270
|
+
|
|
271
|
+
def action_copy_thinking(self) -> None:
|
|
272
|
+
self.app.copy_to_clipboard(self.task_data.get("thinking", ""))
|
|
273
|
+
self.notify("Thinking copied to clipboard", timeout=2)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _update_live_text(area: TextArea, text: str, empty: str) -> None:
|
|
277
|
+
"""Append a live snapshot while preserving the reader's scroll position."""
|
|
278
|
+
value = text or empty
|
|
279
|
+
if value == area.text:
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
follow_tail = area.is_vertical_scroll_end
|
|
283
|
+
scroll_x, scroll_y = area.scroll_offset
|
|
284
|
+
if area.text != empty and value.startswith(area.text):
|
|
285
|
+
area.insert(value[len(area.text) :], area.document.end)
|
|
286
|
+
area.history.clear()
|
|
287
|
+
else:
|
|
288
|
+
# The analyzer retains a bounded tail, so very long streams may
|
|
289
|
+
# eventually drop an old prefix and require a full replacement.
|
|
290
|
+
area.load_text(value)
|
|
291
|
+
|
|
292
|
+
if follow_tail:
|
|
293
|
+
area.scroll_end(animate=False, x_axis=False)
|
|
294
|
+
else:
|
|
295
|
+
area.call_after_refresh(
|
|
296
|
+
lambda: area.scroll_to(
|
|
297
|
+
x=scroll_x,
|
|
298
|
+
y=scroll_y,
|
|
299
|
+
animate=False,
|
|
300
|
+
force=True,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class TemplateCheckScreen(ModalScreen[None]):
|
|
306
|
+
"""Compact results for the setup screen's model template check."""
|
|
307
|
+
|
|
308
|
+
BINDINGS = [
|
|
309
|
+
Binding("escape", "close", "Close"),
|
|
310
|
+
Binding("q", "close", "Close", show=False),
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
def __init__(self, reports: list[TemplateReport]) -> None:
|
|
314
|
+
super().__init__()
|
|
315
|
+
self.reports = reports
|
|
316
|
+
|
|
317
|
+
def compose(self) -> ComposeResult:
|
|
318
|
+
with Vertical(id="template-check-dialog", classes="dialog"):
|
|
319
|
+
yield Static("Chat template checks", classes="card-title")
|
|
320
|
+
yield Static(
|
|
321
|
+
"Reference-free sanity checks using the server's rendered prompt.",
|
|
322
|
+
classes="hint",
|
|
323
|
+
)
|
|
324
|
+
yield DataTable(id="template-check-table", cursor_type="row")
|
|
325
|
+
with Horizontal(classes="dialog-actions"):
|
|
326
|
+
yield Button("Close", id="close", variant="primary")
|
|
327
|
+
yield Footer()
|
|
328
|
+
|
|
329
|
+
def on_mount(self) -> None:
|
|
330
|
+
table = self.query_one("#template-check-table", DataTable)
|
|
331
|
+
table.add_columns("Model", "Status", "Details")
|
|
332
|
+
labels = {
|
|
333
|
+
"pass": Text("PASS", style="green"),
|
|
334
|
+
"warn": Text("WARN", style="yellow"),
|
|
335
|
+
"fail": Text("FAIL", style="red"),
|
|
336
|
+
"unavailable": Text("N/A", style="dim"),
|
|
337
|
+
}
|
|
338
|
+
for report in self.reports:
|
|
339
|
+
table.add_row(report.model, labels[report.status], report.detail)
|
|
340
|
+
table.focus()
|
|
341
|
+
|
|
342
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
343
|
+
if event.button.id == "close":
|
|
344
|
+
self.action_close()
|
|
345
|
+
|
|
346
|
+
def action_close(self) -> None:
|
|
347
|
+
self.dismiss(None)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class DatasetsScreen(ModalScreen[bool]):
|
|
351
|
+
"""Installed and missing benchmark datasets, with one-click installs.
|
|
352
|
+
|
|
353
|
+
Dismisses with True when anything was installed, so the caller can
|
|
354
|
+
refresh whatever it shows about missing data.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
BINDINGS = [
|
|
358
|
+
Binding("escape", "close", "Close"),
|
|
359
|
+
Binding("q", "close", "Close", show=False),
|
|
360
|
+
Binding("a", "install_all", "Install all missing"),
|
|
361
|
+
Binding("enter", "install_selected", "Install selected"),
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
def __init__(self) -> None:
|
|
365
|
+
super().__init__()
|
|
366
|
+
self.installed_any = False
|
|
367
|
+
self.busy = False
|
|
368
|
+
|
|
369
|
+
def compose(self) -> ComposeResult:
|
|
370
|
+
with Vertical(id="datasets-dialog", classes="dialog"):
|
|
371
|
+
yield Static("Datasets", classes="card-title")
|
|
372
|
+
yield Static(
|
|
373
|
+
"Small suites ship with BenchKit. Large ones download once into "
|
|
374
|
+
"a local cache, and a run fetches what it needs automatically.",
|
|
375
|
+
classes="hint",
|
|
376
|
+
)
|
|
377
|
+
yield DataTable(id="datasets-table", cursor_type="row")
|
|
378
|
+
yield Static("", id="datasets-status", classes="hint")
|
|
379
|
+
with Horizontal(classes="dialog-actions"):
|
|
380
|
+
yield Button("Install all missing", id="install-all", variant="primary")
|
|
381
|
+
yield Button("Install selected", id="install-selected")
|
|
382
|
+
yield Button("Close", id="close")
|
|
383
|
+
yield Footer()
|
|
384
|
+
|
|
385
|
+
def on_mount(self) -> None:
|
|
386
|
+
table = self.query_one("#datasets-table", DataTable)
|
|
387
|
+
table.add_column("Benchmark", key="benchmark")
|
|
388
|
+
table.add_column("Status", key="status", width=12)
|
|
389
|
+
table.add_column("Size", key="size", width=10)
|
|
390
|
+
table.add_column("Source", key="source")
|
|
391
|
+
for entry in catalog.statuses():
|
|
392
|
+
table.add_row(
|
|
393
|
+
entry.benchmark,
|
|
394
|
+
self._status_cell(entry),
|
|
395
|
+
catalog.size_label(entry),
|
|
396
|
+
entry.hint
|
|
397
|
+
if entry.kind == "manual" and not entry.installed
|
|
398
|
+
else entry.summary,
|
|
399
|
+
key=entry.benchmark,
|
|
400
|
+
)
|
|
401
|
+
self._sync_actions()
|
|
402
|
+
table.focus()
|
|
403
|
+
|
|
404
|
+
@staticmethod
|
|
405
|
+
def _status_cell(entry: catalog.DatasetStatus) -> Text:
|
|
406
|
+
if entry.kind == "bundled":
|
|
407
|
+
return Text("bundled", style="dim")
|
|
408
|
+
if entry.installed:
|
|
409
|
+
return Text("✓ installed", style="green")
|
|
410
|
+
if entry.kind == "manual":
|
|
411
|
+
return Text("✗ manual", style="red")
|
|
412
|
+
return Text("✗ missing", style="yellow")
|
|
413
|
+
|
|
414
|
+
def _sync_actions(self) -> None:
|
|
415
|
+
missing = catalog.missing()
|
|
416
|
+
self.query_one("#install-all", Button).disabled = self.busy or not missing
|
|
417
|
+
self.query_one("#install-selected", Button).disabled = self.busy
|
|
418
|
+
if not self.busy:
|
|
419
|
+
self.query_one("#datasets-status", Static).update(
|
|
420
|
+
f"{len(missing)} missing: {', '.join(missing)}"
|
|
421
|
+
if missing
|
|
422
|
+
else "Every downloadable dataset is installed."
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
426
|
+
if event.button.id == "install-all":
|
|
427
|
+
self.action_install_all()
|
|
428
|
+
elif event.button.id == "install-selected":
|
|
429
|
+
self.action_install_selected()
|
|
430
|
+
elif event.button.id == "close":
|
|
431
|
+
self.action_close()
|
|
432
|
+
|
|
433
|
+
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
434
|
+
event.stop()
|
|
435
|
+
self.action_install_selected()
|
|
436
|
+
|
|
437
|
+
def action_install_all(self) -> None:
|
|
438
|
+
self._start(catalog.missing())
|
|
439
|
+
|
|
440
|
+
def action_install_selected(self) -> None:
|
|
441
|
+
table = self.query_one("#datasets-table", DataTable)
|
|
442
|
+
if table.row_count == 0:
|
|
443
|
+
return
|
|
444
|
+
key = table.coordinate_to_cell_key(table.cursor_coordinate).row_key.value
|
|
445
|
+
entry = catalog.status(str(key))
|
|
446
|
+
if entry.kind == "bundled":
|
|
447
|
+
self.notify(f"{entry.benchmark} ships with BenchKit.", timeout=3)
|
|
448
|
+
elif entry.installed:
|
|
449
|
+
self.notify(f"{entry.benchmark} is already installed.", timeout=3)
|
|
450
|
+
elif entry.kind == "manual":
|
|
451
|
+
self.notify(entry.hint, severity="warning", timeout=8)
|
|
452
|
+
else:
|
|
453
|
+
self._start([entry.benchmark])
|
|
454
|
+
|
|
455
|
+
def _start(self, keys: list[str]) -> None:
|
|
456
|
+
if self.busy or not keys:
|
|
457
|
+
return
|
|
458
|
+
self.busy = True
|
|
459
|
+
self._sync_actions()
|
|
460
|
+
self._install(keys)
|
|
461
|
+
|
|
462
|
+
@work(thread=True, exclusive=True, group="datasets")
|
|
463
|
+
def _install(self, keys: list[str]) -> None:
|
|
464
|
+
failures = []
|
|
465
|
+
for key in keys:
|
|
466
|
+
self.app.call_from_thread(self._progress, key, 0, None)
|
|
467
|
+
try:
|
|
468
|
+
catalog.install(
|
|
469
|
+
key,
|
|
470
|
+
lambda done, total, key=key: self.app.call_from_thread(
|
|
471
|
+
self._progress, key, done, total
|
|
472
|
+
),
|
|
473
|
+
)
|
|
474
|
+
except Exception as exc:
|
|
475
|
+
failures.append(key)
|
|
476
|
+
self.app.call_from_thread(self._failed, key, str(exc))
|
|
477
|
+
else:
|
|
478
|
+
self.installed_any = True
|
|
479
|
+
self.app.call_from_thread(self._refresh_row, key)
|
|
480
|
+
self.app.call_from_thread(self._finished, keys, failures)
|
|
481
|
+
|
|
482
|
+
def _progress(self, key: str, done: int, total: int | None) -> None:
|
|
483
|
+
detail = f" {fmt_bytes(done)}" if done else "…"
|
|
484
|
+
if done and total:
|
|
485
|
+
detail = f" {fmt_bytes(done)} / {fmt_bytes(total)}"
|
|
486
|
+
self.query_one("#datasets-status", Static).update(f"Installing {key}{detail}")
|
|
487
|
+
|
|
488
|
+
def _refresh_row(self, key: str) -> None:
|
|
489
|
+
table = self.query_one("#datasets-table", DataTable)
|
|
490
|
+
entry = catalog.status(key)
|
|
491
|
+
table.update_cell(key, "status", self._status_cell(entry))
|
|
492
|
+
table.update_cell(key, "size", catalog.size_label(entry))
|
|
493
|
+
|
|
494
|
+
def _failed(self, key: str, message: str) -> None:
|
|
495
|
+
self.notify(f"{key}: {message}", severity="error", timeout=10)
|
|
496
|
+
|
|
497
|
+
def _finished(self, keys: list[str], failures: list[str]) -> None:
|
|
498
|
+
self.busy = False
|
|
499
|
+
self._sync_actions()
|
|
500
|
+
done = len(keys) - len(failures)
|
|
501
|
+
if done:
|
|
502
|
+
self.notify(f"Installed {done} dataset(s).", timeout=4)
|
|
503
|
+
|
|
504
|
+
def action_close(self) -> None:
|
|
505
|
+
if self.busy:
|
|
506
|
+
self.notify("Wait for the install to finish.", timeout=3)
|
|
507
|
+
return
|
|
508
|
+
self.dismiss(self.installed_any)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class ConfirmScreen(ModalScreen[bool]):
|
|
512
|
+
"""Yes/no confirmation."""
|
|
513
|
+
|
|
514
|
+
BINDINGS = [
|
|
515
|
+
Binding("escape", "no", "Cancel"),
|
|
516
|
+
Binding("y", "yes", "Yes"),
|
|
517
|
+
Binding("n", "no", "No"),
|
|
518
|
+
]
|
|
519
|
+
|
|
520
|
+
def __init__(
|
|
521
|
+
self, question: str, detail: str = "", confirm: str = "Confirm"
|
|
522
|
+
) -> None:
|
|
523
|
+
super().__init__()
|
|
524
|
+
self.question = question
|
|
525
|
+
self.detail = detail
|
|
526
|
+
self.confirm_label = confirm
|
|
527
|
+
|
|
528
|
+
def compose(self) -> ComposeResult:
|
|
529
|
+
with Vertical(id="confirm-dialog", classes="dialog"):
|
|
530
|
+
yield Static(self.question, classes="card-title")
|
|
531
|
+
if self.detail:
|
|
532
|
+
yield Static(self.detail, classes="hint")
|
|
533
|
+
with Horizontal(classes="dialog-actions"):
|
|
534
|
+
yield Button(self.confirm_label, id="yes", variant="error")
|
|
535
|
+
yield Button("Cancel", id="no", variant="primary")
|
|
536
|
+
|
|
537
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
538
|
+
self.dismiss(event.button.id == "yes")
|
|
539
|
+
|
|
540
|
+
def action_yes(self) -> None:
|
|
541
|
+
self.dismiss(True)
|
|
542
|
+
|
|
543
|
+
def action_no(self) -> None:
|
|
544
|
+
self.dismiss(False)
|