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,191 @@
|
|
|
1
|
+
"""Connection screen: pick a host/provider and discover models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from textual import events, work
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
11
|
+
from textual.screen import Screen
|
|
12
|
+
from textual.widgets import Button, Footer, Header, Input, Label, Select, Static
|
|
13
|
+
|
|
14
|
+
from benchkit.client import DEFAULT_HOST, InferenceClient
|
|
15
|
+
from benchkit.demo import DemoClient
|
|
16
|
+
from benchkit.tui.widgets import Banner
|
|
17
|
+
|
|
18
|
+
PROVIDERS = [
|
|
19
|
+
("auto detect", "auto"),
|
|
20
|
+
("OpenAI compatible", "openai"),
|
|
21
|
+
("Ollama", "ollama"),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
HINT = (
|
|
25
|
+
"Point BenchKit at any OpenAI-compatible server (llama.cpp, llama-swap, vLLM, "
|
|
26
|
+
"LM Studio) or a native Ollama host."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConnectScreen(Screen[None]):
|
|
31
|
+
"""First screen: configure the endpoint and connect."""
|
|
32
|
+
|
|
33
|
+
BINDINGS = [
|
|
34
|
+
Binding("ctrl+r", "connect", "Connect"),
|
|
35
|
+
Binding("f5", "connect", "Connect", show=False),
|
|
36
|
+
Binding("f3", "demo", "Demo mode"),
|
|
37
|
+
Binding("escape", "app.quit", "Quit", show=False),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def compose(self) -> ComposeResult:
|
|
41
|
+
yield Header(show_clock=True)
|
|
42
|
+
with VerticalScroll(id="connect-root"):
|
|
43
|
+
yield Banner(id="banner")
|
|
44
|
+
yield Static(
|
|
45
|
+
"Benchmark your local LLMs — not vibes, actual scores.", id="tagline"
|
|
46
|
+
)
|
|
47
|
+
with Vertical(id="connect-card"):
|
|
48
|
+
yield Static("Connection", classes="card-title")
|
|
49
|
+
with Vertical(id="connect-form"):
|
|
50
|
+
with Horizontal(classes="field"):
|
|
51
|
+
yield Label("Host", classes="field-label")
|
|
52
|
+
yield Input(placeholder=DEFAULT_HOST, id="host", compact=True)
|
|
53
|
+
with Horizontal(classes="field"):
|
|
54
|
+
yield Label("Provider", classes="field-label")
|
|
55
|
+
yield Select(
|
|
56
|
+
PROVIDERS,
|
|
57
|
+
value="auto",
|
|
58
|
+
allow_blank=False,
|
|
59
|
+
id="provider",
|
|
60
|
+
compact=True,
|
|
61
|
+
)
|
|
62
|
+
with Horizontal(classes="field"):
|
|
63
|
+
yield Label("API key", classes="field-label")
|
|
64
|
+
yield Input(
|
|
65
|
+
placeholder="optional",
|
|
66
|
+
password=True,
|
|
67
|
+
id="api-key",
|
|
68
|
+
compact=True,
|
|
69
|
+
)
|
|
70
|
+
with Horizontal(classes="field"):
|
|
71
|
+
yield Label("Task timeout", classes="field-label")
|
|
72
|
+
yield Input(placeholder="300", id="timeout", compact=True)
|
|
73
|
+
with Horizontal(id="connect-actions"):
|
|
74
|
+
yield Button("Connect", id="connect", variant="primary")
|
|
75
|
+
yield Button("Quit", id="quit", variant="error")
|
|
76
|
+
yield Static(HINT, id="connect-status")
|
|
77
|
+
yield Footer()
|
|
78
|
+
|
|
79
|
+
def on_resize(self, event: events.Resize) -> None:
|
|
80
|
+
# The wordmark is 62 cells wide, so it goes first on a small terminal.
|
|
81
|
+
size = event.size
|
|
82
|
+
self.set_class(size.height < 30 or size.width < 66, "compact")
|
|
83
|
+
|
|
84
|
+
def on_mount(self) -> None:
|
|
85
|
+
defaults = _env_defaults()
|
|
86
|
+
host = self.app.host_override or defaults["host"]
|
|
87
|
+
self.query_one("#host", Input).value = host
|
|
88
|
+
self.query_one("#provider", Select).value = defaults["provider"]
|
|
89
|
+
self.query_one("#api-key", Input).value = defaults["api_key"]
|
|
90
|
+
self.query_one("#timeout", Input).value = defaults["timeout"]
|
|
91
|
+
|
|
92
|
+
if self.app.demo:
|
|
93
|
+
self.set_timer(0.1, self.action_demo)
|
|
94
|
+
else:
|
|
95
|
+
self.set_timer(0.1, self.action_connect)
|
|
96
|
+
|
|
97
|
+
# Actions ----------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
def action_connect(self) -> None:
|
|
100
|
+
host = self.query_one("#host", Input).value.strip() or DEFAULT_HOST
|
|
101
|
+
provider = self.query_one("#provider", Select).value
|
|
102
|
+
api_key = self.query_one("#api-key", Input).value.strip() or None
|
|
103
|
+
raw_timeout = self.query_one("#timeout", Input).value.strip() or "300"
|
|
104
|
+
try:
|
|
105
|
+
timeout = float(raw_timeout)
|
|
106
|
+
except ValueError:
|
|
107
|
+
self._status(f"Timeout must be a number, got {raw_timeout!r}", "error")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
self._busy(True)
|
|
111
|
+
self._status(f"Connecting to {host} …", "working")
|
|
112
|
+
self._connect(host.rstrip("/"), str(provider), api_key, timeout)
|
|
113
|
+
|
|
114
|
+
def action_demo(self) -> None:
|
|
115
|
+
client = DemoClient()
|
|
116
|
+
self._status("Demo mode — offline models, no server required.", "ok")
|
|
117
|
+
self.app.show_setup(client, client.list_models())
|
|
118
|
+
|
|
119
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
120
|
+
if event.button.id == "connect":
|
|
121
|
+
self.action_connect()
|
|
122
|
+
elif event.button.id == "quit":
|
|
123
|
+
self.app.exit()
|
|
124
|
+
|
|
125
|
+
def on_input_submitted(self) -> None:
|
|
126
|
+
self.action_connect()
|
|
127
|
+
|
|
128
|
+
# Worker -----------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
@work(thread=True, exclusive=True, group="connect")
|
|
131
|
+
def _connect(
|
|
132
|
+
self, host: str, provider: str, api_key: str | None, timeout: float
|
|
133
|
+
) -> None:
|
|
134
|
+
try:
|
|
135
|
+
client = InferenceClient(host, provider, api_key, timeout)
|
|
136
|
+
models = client.list_models()
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
self.app.call_from_thread(self._failed, f"{type(exc).__name__}: {exc}")
|
|
139
|
+
return
|
|
140
|
+
self.app.call_from_thread(self._connected, client, models)
|
|
141
|
+
|
|
142
|
+
def _connected(self, client: InferenceClient, models: list[dict]) -> None:
|
|
143
|
+
self._busy(False)
|
|
144
|
+
if not models:
|
|
145
|
+
self._status(
|
|
146
|
+
f"Connected to {client.host} but the server reports no models.", "error"
|
|
147
|
+
)
|
|
148
|
+
return
|
|
149
|
+
self._status(f"{len(models)} model(s) via {client.label}", "ok")
|
|
150
|
+
self.app.show_setup(client, models)
|
|
151
|
+
|
|
152
|
+
def _failed(self, message: str) -> None:
|
|
153
|
+
self._busy(False)
|
|
154
|
+
self._status(
|
|
155
|
+
f"{message}\nCheck the host and that the server is running, "
|
|
156
|
+
"or press F3 for demo mode.",
|
|
157
|
+
"error",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Helpers ----------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def _busy(self, busy: bool) -> None:
|
|
163
|
+
self.query_one("#connect", Button).disabled = busy
|
|
164
|
+
self.query_one("#connect-card").set_class(busy, "busy")
|
|
165
|
+
|
|
166
|
+
def _status(self, message: str, kind: str = "") -> None:
|
|
167
|
+
status = self.query_one("#connect-status", Static)
|
|
168
|
+
status.set_classes(f"status {kind}" if kind else "status")
|
|
169
|
+
status.update(message)
|
|
170
|
+
status.scroll_visible()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _env_defaults() -> dict[str, str]:
|
|
174
|
+
provider = os.environ.get("BENCHKIT_PROVIDER", "auto").lower()
|
|
175
|
+
if provider not in {"auto", "openai", "ollama"}:
|
|
176
|
+
provider = "auto"
|
|
177
|
+
host = (
|
|
178
|
+
os.environ.get("BENCHKIT_HOST")
|
|
179
|
+
or os.environ.get("OPENAI_BASE_URL")
|
|
180
|
+
or os.environ.get("OLLAMA_HOST")
|
|
181
|
+
or DEFAULT_HOST
|
|
182
|
+
)
|
|
183
|
+
api_key = (
|
|
184
|
+
os.environ.get("BENCHKIT_API_KEY") or os.environ.get("OPENAI_API_KEY") or ""
|
|
185
|
+
)
|
|
186
|
+
return {
|
|
187
|
+
"host": host.rstrip("/"),
|
|
188
|
+
"provider": provider,
|
|
189
|
+
"api_key": api_key,
|
|
190
|
+
"timeout": os.environ.get("BENCHKIT_TIMEOUT", "300"),
|
|
191
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Per-run drill-down: every task of a single model x benchmark."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
from textual import events
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
from textual.screen import Screen
|
|
11
|
+
from textual.widgets import DataTable, Footer, Header, Input
|
|
12
|
+
|
|
13
|
+
from benchkit.engine import slice_label
|
|
14
|
+
from benchkit.metrics import (
|
|
15
|
+
aggregate_tok_s,
|
|
16
|
+
decision_speed,
|
|
17
|
+
effective_concurrency,
|
|
18
|
+
is_decision,
|
|
19
|
+
latency_text,
|
|
20
|
+
stream_tok_s,
|
|
21
|
+
)
|
|
22
|
+
from benchkit.tui.formatting import (
|
|
23
|
+
fmt_count,
|
|
24
|
+
fmt_duration,
|
|
25
|
+
result_color,
|
|
26
|
+
score_color,
|
|
27
|
+
throughput_stat,
|
|
28
|
+
)
|
|
29
|
+
from benchkit.tui.screens.modals import TaskDetailScreen
|
|
30
|
+
from benchkit.tui.widgets import SectionTitle, StatCard, apply_compact
|
|
31
|
+
|
|
32
|
+
FILTERS = ["all", "failed", "passed"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class JobDetailScreen(Screen[None]):
|
|
36
|
+
"""Task-level view of one benchmark run."""
|
|
37
|
+
|
|
38
|
+
BINDINGS = [
|
|
39
|
+
Binding("f", "cycle_filter", "Filter"),
|
|
40
|
+
Binding("slash", "focus_search", "Search"),
|
|
41
|
+
Binding("escape", "close", "Back"),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
def __init__(self, result: dict) -> None:
|
|
45
|
+
super().__init__()
|
|
46
|
+
self.result = result
|
|
47
|
+
self.filter_index = 0
|
|
48
|
+
|
|
49
|
+
def compose(self) -> ComposeResult:
|
|
50
|
+
yield Header(show_clock=True)
|
|
51
|
+
with Vertical(id="detail-root"):
|
|
52
|
+
with Horizontal(id="stat-row"):
|
|
53
|
+
yield StatCard("Score", "--", id="stat-score")
|
|
54
|
+
yield StatCard("Passed", "--", id="stat-passed")
|
|
55
|
+
yield StatCard("Loop rate", "--", id="stat-loops")
|
|
56
|
+
yield StatCard("Throughput", "--", id="stat-speed")
|
|
57
|
+
yield StatCard("Avg latency", "--", id="stat-latency")
|
|
58
|
+
yield StatCard("Duration", "--", id="stat-duration")
|
|
59
|
+
with Vertical(classes="pane", id="detail-pane"):
|
|
60
|
+
yield SectionTitle("Tasks", "enter to open", id="detail-title")
|
|
61
|
+
yield Input(placeholder="Search task id or response…", id="task-search")
|
|
62
|
+
yield DataTable(id="task-table", cursor_type="row", zebra_stripes=True)
|
|
63
|
+
yield Footer()
|
|
64
|
+
|
|
65
|
+
def on_mount(self) -> None:
|
|
66
|
+
result = self.result
|
|
67
|
+
benchmark_label = result.get("benchmark_label", result["benchmark"])
|
|
68
|
+
harness = result.get("harness_label", "Direct")
|
|
69
|
+
self.title = f"{benchmark_label} · {result['model']} · {harness}"
|
|
70
|
+
self.sub_title = f"{result['passed']}/{result['total']} passed"
|
|
71
|
+
|
|
72
|
+
table = self.query_one("#task-table", DataTable)
|
|
73
|
+
table.add_column("#", key="index", width=5)
|
|
74
|
+
table.add_column("Task", key="task")
|
|
75
|
+
table.add_column("Result", key="result", width=8)
|
|
76
|
+
table.add_column("Loop", key="loop", width=9)
|
|
77
|
+
table.add_column("Think", key="thinking", width=8)
|
|
78
|
+
table.add_column("Latency", key="latency", width=9)
|
|
79
|
+
table.add_column("Stream tok/s", key="tok_s", width=12)
|
|
80
|
+
|
|
81
|
+
score = result["score"]
|
|
82
|
+
score_hint = slice_label(result.get("slice"))
|
|
83
|
+
if result.get("score_delta_pp") is not None:
|
|
84
|
+
score_hint = (
|
|
85
|
+
f"{score_hint} · Δ {result['score_delta_pp']:+.1f} pp · "
|
|
86
|
+
f"{result.get('regressions', 0)} regression(s) · "
|
|
87
|
+
f"seed {result.get('perturbation_seed', 42)}"
|
|
88
|
+
)
|
|
89
|
+
if result.get("harness_score_delta_pp") is not None:
|
|
90
|
+
score_hint += (
|
|
91
|
+
f" · harness score Δ {result['harness_score_delta_pp']:+.1f} pp"
|
|
92
|
+
f" · loop-kill Δ {result.get('loop_kill_delta_pp', 0):+.1f} pp"
|
|
93
|
+
+ (
|
|
94
|
+
f" (initial {result.get('harness_first_score_delta_pp', 0):+.1f})"
|
|
95
|
+
if result.get("repair_attempts")
|
|
96
|
+
else ""
|
|
97
|
+
)
|
|
98
|
+
+ " · "
|
|
99
|
+
f"{result.get('harness_gains', 0)} gain(s) · "
|
|
100
|
+
f"{result.get('harness_regressions', 0)} regression(s)"
|
|
101
|
+
)
|
|
102
|
+
if result.get("repair_attempts"):
|
|
103
|
+
score_hint += (
|
|
104
|
+
f" · repair Δ {result.get('repair_delta_pp', 0):+.1f} pp · "
|
|
105
|
+
f"{result.get('repair_successes', 0)}/"
|
|
106
|
+
f"{result.get('repair_attempted', 0)} fixed"
|
|
107
|
+
)
|
|
108
|
+
self.query_one("#stat-score", StatCard).set_state(f"{score:.1f}%", score_hint)
|
|
109
|
+
self.query_one("#stat-passed", StatCard).set_state(
|
|
110
|
+
f"{result['passed']}/{result['total']}",
|
|
111
|
+
f"{result.get('errors', 0)} error(s)",
|
|
112
|
+
)
|
|
113
|
+
self.query_one("#stat-loops", StatCard).set_state(
|
|
114
|
+
f"{result.get('loop_rate', 0):.1f}%",
|
|
115
|
+
f"{result.get('loops', 0)} loop · "
|
|
116
|
+
f"{result.get('suspected_loops', 0)} suspect · "
|
|
117
|
+
f"{result.get('recovered_loops', 0)} recovered · "
|
|
118
|
+
f"{result.get('loop_kills', 0)} killed",
|
|
119
|
+
)
|
|
120
|
+
speed = self.query_one("#stat-speed", StatCard)
|
|
121
|
+
speed.set_state(
|
|
122
|
+
*(
|
|
123
|
+
decision_speed(result)
|
|
124
|
+
if is_decision(result)
|
|
125
|
+
else throughput_stat(
|
|
126
|
+
concurrency=result.get("concurrency", 1),
|
|
127
|
+
aggregate=aggregate_tok_s(result),
|
|
128
|
+
stream=stream_tok_s(result),
|
|
129
|
+
effective=effective_concurrency(result),
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
self.query_one("#stat-latency", StatCard).set_state(latency_text(result))
|
|
134
|
+
self.query_one("#stat-duration", StatCard).set_state(
|
|
135
|
+
fmt_duration(result["total_time"])
|
|
136
|
+
)
|
|
137
|
+
self._restyle_score()
|
|
138
|
+
|
|
139
|
+
self._fill()
|
|
140
|
+
self.watch(self.app, "theme", self._theme_changed, init=False)
|
|
141
|
+
table.focus()
|
|
142
|
+
|
|
143
|
+
def _restyle_score(self) -> None:
|
|
144
|
+
color = score_color(self.result["score"], self.app.current_theme.dark)
|
|
145
|
+
self.query_one("#stat-score", StatCard).styles.color = color
|
|
146
|
+
|
|
147
|
+
def _theme_changed(self, _theme: str) -> None:
|
|
148
|
+
self._restyle_score()
|
|
149
|
+
self._fill()
|
|
150
|
+
|
|
151
|
+
def on_resize(self, event: events.Resize) -> None:
|
|
152
|
+
apply_compact(self, event.size.height)
|
|
153
|
+
|
|
154
|
+
# Rendering --------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
def _visible_tasks(self) -> list[tuple[int, dict]]:
|
|
157
|
+
mode = FILTERS[self.filter_index]
|
|
158
|
+
needle = self.query_one("#task-search", Input).value.strip().lower()
|
|
159
|
+
rows = []
|
|
160
|
+
for index, task in enumerate(self.result["tasks"]):
|
|
161
|
+
if mode == "failed" and task["passed"]:
|
|
162
|
+
continue
|
|
163
|
+
if mode == "passed" and not task["passed"]:
|
|
164
|
+
continue
|
|
165
|
+
if (
|
|
166
|
+
needle
|
|
167
|
+
and needle not in task["task_id"].lower()
|
|
168
|
+
and needle not in (task.get("response", "").lower())
|
|
169
|
+
and needle not in (task.get("thinking", "").lower())
|
|
170
|
+
):
|
|
171
|
+
continue
|
|
172
|
+
rows.append((index, task))
|
|
173
|
+
return rows
|
|
174
|
+
|
|
175
|
+
def _fill(self) -> None:
|
|
176
|
+
table = self.query_one("#task-table", DataTable)
|
|
177
|
+
table.clear()
|
|
178
|
+
rows = self._visible_tasks()
|
|
179
|
+
for index, task in rows:
|
|
180
|
+
entry = task.get("entry_point")
|
|
181
|
+
label = f"{task['task_id']} ({entry})" if entry else task["task_id"]
|
|
182
|
+
table.add_row(
|
|
183
|
+
str(index + 1),
|
|
184
|
+
label,
|
|
185
|
+
_result_cell(task, self.app.current_theme.dark),
|
|
186
|
+
_loop_cell(task),
|
|
187
|
+
(
|
|
188
|
+
fmt_duration(task.get("thinking_time_s", 0))
|
|
189
|
+
if task.get("thinking")
|
|
190
|
+
else "—"
|
|
191
|
+
),
|
|
192
|
+
fmt_duration(task.get("response_time_s", 0)),
|
|
193
|
+
f"{task.get('tok_s', 0):.1f}",
|
|
194
|
+
key=str(index),
|
|
195
|
+
)
|
|
196
|
+
self.query_one("#detail-title", SectionTitle).set_detail(
|
|
197
|
+
f"{FILTERS[self.filter_index]} · {fmt_count(len(rows))} shown · enter to open"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Events -----------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def on_input_changed(self) -> None:
|
|
203
|
+
self._fill()
|
|
204
|
+
|
|
205
|
+
def on_input_submitted(self) -> None:
|
|
206
|
+
self.query_one("#task-table", DataTable).focus()
|
|
207
|
+
|
|
208
|
+
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
209
|
+
index = int(str(event.row_key.value))
|
|
210
|
+
task = self.result["tasks"][index]
|
|
211
|
+
benchmark_label = self.result.get("benchmark_label", self.result["benchmark"])
|
|
212
|
+
title = (
|
|
213
|
+
f"{benchmark_label} · {self.result['model']} · "
|
|
214
|
+
f"{self.result.get('harness_label', 'Direct')}"
|
|
215
|
+
)
|
|
216
|
+
self.app.push_screen(TaskDetailScreen(title, task))
|
|
217
|
+
|
|
218
|
+
# Actions ----------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
def action_cycle_filter(self) -> None:
|
|
221
|
+
self.filter_index = (self.filter_index + 1) % len(FILTERS)
|
|
222
|
+
self._fill()
|
|
223
|
+
|
|
224
|
+
def action_focus_search(self) -> None:
|
|
225
|
+
self.query_one("#task-search", Input).focus()
|
|
226
|
+
|
|
227
|
+
def action_close(self) -> None:
|
|
228
|
+
self.app.pop_screen()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _result_cell(task: dict, dark: bool = True) -> Text:
|
|
232
|
+
error = bool(task.get("error"))
|
|
233
|
+
passed = bool(task.get("passed"))
|
|
234
|
+
label = (
|
|
235
|
+
"LOOPKILL"
|
|
236
|
+
if task.get("loop_killed")
|
|
237
|
+
else "TIMEOUT"
|
|
238
|
+
if task.get("timed_out")
|
|
239
|
+
else "LENGTH"
|
|
240
|
+
if task.get("length_exceeded")
|
|
241
|
+
else "ERROR"
|
|
242
|
+
if error
|
|
243
|
+
else "FIXED"
|
|
244
|
+
if task.get("repaired")
|
|
245
|
+
else "PASS"
|
|
246
|
+
if passed
|
|
247
|
+
else f"{float(task.get('score', 0)):.0f}%"
|
|
248
|
+
if float(task.get("score", 0)) > 0
|
|
249
|
+
else "FAIL"
|
|
250
|
+
)
|
|
251
|
+
color = (
|
|
252
|
+
score_color(float(task.get("score", 0)), dark)
|
|
253
|
+
if not passed and float(task.get("score", 0)) > 0 and not error
|
|
254
|
+
else result_color(passed, error, dark)
|
|
255
|
+
)
|
|
256
|
+
return Text(label, style=f"bold {color}")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _loop_cell(task: dict) -> Text:
|
|
260
|
+
state = task.get("loop_state", "unavailable")
|
|
261
|
+
source = task.get("loop_source", "none")
|
|
262
|
+
label = (
|
|
263
|
+
"RECOVERED"
|
|
264
|
+
if task.get("recovered_cycle")
|
|
265
|
+
else "OUTPUT"
|
|
266
|
+
if state == "looping" and source == "answer"
|
|
267
|
+
else "LOOPING"
|
|
268
|
+
if state == "looping"
|
|
269
|
+
else "SUSPECT"
|
|
270
|
+
if state == "suspected"
|
|
271
|
+
else "NO TRACE"
|
|
272
|
+
if state == "clear" and source == "answer"
|
|
273
|
+
else "CLEAR"
|
|
274
|
+
if state == "clear"
|
|
275
|
+
else "NO TRACE"
|
|
276
|
+
)
|
|
277
|
+
style = (
|
|
278
|
+
"bold yellow"
|
|
279
|
+
if task.get("recovered_cycle")
|
|
280
|
+
else "bold red"
|
|
281
|
+
if state == "looping"
|
|
282
|
+
else "bold yellow"
|
|
283
|
+
if state == "suspected"
|
|
284
|
+
else "green"
|
|
285
|
+
if state == "clear" and source != "answer"
|
|
286
|
+
else "dim"
|
|
287
|
+
)
|
|
288
|
+
return Text(label, style=style)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Keyboard reference."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.containers import VerticalScroll
|
|
8
|
+
from textual.screen import ModalScreen
|
|
9
|
+
from textual.widgets import Footer, Markdown, Static
|
|
10
|
+
|
|
11
|
+
HELP = """
|
|
12
|
+
### Everywhere
|
|
13
|
+
|
|
14
|
+
| Key | Action |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `?` / `F1` | This help |
|
|
17
|
+
| `Ctrl+P` | Command palette |
|
|
18
|
+
| `F2` | Dogi light / dark theme |
|
|
19
|
+
| `Ctrl+Q` | Quit |
|
|
20
|
+
| `Tab` / `Shift+Tab` | Move between panes |
|
|
21
|
+
|
|
22
|
+
### Connect
|
|
23
|
+
|
|
24
|
+
| Key | Action |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `Ctrl+R` | Connect / retry |
|
|
27
|
+
| `F3` | Demo mode (no server needed) |
|
|
28
|
+
| `Enter` | Connect from any field |
|
|
29
|
+
|
|
30
|
+
### Setup
|
|
31
|
+
|
|
32
|
+
| Key | Action |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `Space` / `Enter` | Toggle the highlighted row |
|
|
35
|
+
| `a` / `n` / `i` | Select all / clear / invert the focused list |
|
|
36
|
+
| `l` | Task limit for the highlighted benchmark |
|
|
37
|
+
| `p` | Toggle the paired choice-order run |
|
|
38
|
+
| `v` | Check chat templates (selected models, or all) |
|
|
39
|
+
| `d` | Datasets: see what is installed and install what is missing |
|
|
40
|
+
| `/` | Jump to the filter box |
|
|
41
|
+
| `s` / `F5` | Start the run |
|
|
42
|
+
| `Esc` | Back to the connection screen |
|
|
43
|
+
|
|
44
|
+
Limits accept `20` (first 20), `-20` (last 20) and `40-80` (a range).
|
|
45
|
+
Choice-order runs each selected task clean and with deterministically shuffled options.
|
|
46
|
+
|
|
47
|
+
### Run
|
|
48
|
+
|
|
49
|
+
| Key | Action |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `p` | Pause / resume between tasks |
|
|
52
|
+
| `k` | Skip the current model × benchmark |
|
|
53
|
+
| `x` | Stop the run and keep what finished |
|
|
54
|
+
| `f` | Show failures only |
|
|
55
|
+
| `t` | Follow the newest task (auto-scroll) |
|
|
56
|
+
| `Enter` | Inspect the highlighted task |
|
|
57
|
+
|
|
58
|
+
### Results
|
|
59
|
+
|
|
60
|
+
| Key | Action |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `Enter` | Drill into a run's tasks |
|
|
63
|
+
| `s` / `m` / `t` | Sort by score / model / duration |
|
|
64
|
+
| `c` | Copy the results folder path |
|
|
65
|
+
| `r` | Configure another run |
|
|
66
|
+
| `Esc` | Back to setup |
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class HelpScreen(ModalScreen[None]):
|
|
71
|
+
BINDINGS = [
|
|
72
|
+
Binding("escape", "close", "Close"),
|
|
73
|
+
Binding("question_mark", "close", "Close", show=False),
|
|
74
|
+
Binding("q", "close", "Close", show=False),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
def compose(self) -> ComposeResult:
|
|
78
|
+
with VerticalScroll(id="help-dialog", classes="dialog"):
|
|
79
|
+
yield Static("Keyboard reference", classes="card-title")
|
|
80
|
+
yield Markdown(HELP)
|
|
81
|
+
yield Footer()
|
|
82
|
+
|
|
83
|
+
def action_close(self) -> None:
|
|
84
|
+
self.dismiss(None)
|