tuieval 0.2.0.dev94__tar.gz → 0.2.0.dev98__tar.gz
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.
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/CHANGELOG.md +2 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/PKG-INFO +1 -1
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/models.md +1 -1
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/pta.py +21 -8
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/tui.py +57 -8
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/tests/test_tuieval.py +68 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/.gitignore +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/LICENSE +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/README.md +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/compare.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/engine.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/insights.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/logs.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/run_evals.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/tests/mock_server.py +0 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Results, Per question: the model summary shows every picked model without scrolling, and fits the window: shorter column names, and long model names cut in the middle (both ends kept). The PTA index table fits the same way.
|
|
6
|
+
- PTA index: the accuracy bar shows answers cut off at max_tokens in orange, with `✂ n%` beside it and a **cut off** column in the table, so overthinking stands apart from wrong answers.
|
|
5
7
|
- Results → Insights (also `tuieval insights`): charts with what to do next: every model against a reference (95% CI), the best model per use case, fine-tunes against their base (`base` in models.toml), why models fail, accuracy against tokens or time, question health, and the Certify runs that would settle the strongest models' verdicts. `[defaults] reference` sets the default reference.
|
|
6
8
|
- A server that stops sending tokens mid-answer for 5 minutes (`stall_timeout_ms`) is a server error: retried, never counted against the model. Keep-alive pings no longer stretch a request past `request_timeout_ms`.
|
|
7
9
|
- The run screen's queue shows what each running model is doing: waiting, thinking or answering with ~tokens so far, and how long it's been quiet, so a model you aren't streaming doesn't look stuck.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev98
|
|
4
4
|
Summary: Evaluate local and frontier LLMs on your own questions: accuracy, speed, tokens and PASS/FAIL verdicts, in the terminal.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ashe-wb/tuieval
|
|
6
6
|
Project-URL: Issues, https://github.com/ashe-wb/tuieval/issues
|
|
@@ -104,7 +104,7 @@ The **PTA index** puts parsimony (tokens), time and accuracy side by side, as a
|
|
|
104
104
|
|
|
105
105
|
- **P parsimony (tokens)**: the tokens used, reasoning and answer, on every question compared (each question at its median over repeats). Beside the bar: how many times the leanest model's, e.g. `2.4×` (`1.0×` is the leanest, ★ in the table's **tokens/answer**). Unlike time it doesn't depend on your machine: it's how much a model says to get to an answer, which is what you pay for on an API and what fills its context.
|
|
106
106
|
- **T time**: the total time to answer those questions, as how many times the fastest model's, e.g. `28×` (★ in **time/answer**).
|
|
107
|
-
- **A accuracy**: the share of answers to those questions that passed.
|
|
107
|
+
- **A accuracy**: the share of answers to those questions that passed. Answers cut off at `max_tokens` count as wrong, but show in orange after the green, with `✂ n%` beside the number and a **cut off** column in the table: those failures are a model out of tokens (overthinking), not a wrong answer yet. A pack's own `max_tokens` gives it more room.
|
|
108
108
|
|
|
109
109
|
The P and T bars are as long as the tokens and time used: the model that used the most fills its bar, the others in proportion (at least one block), so **shorter is better**. The A bar is the share of answers right, so **longer is better**; it's full only when every answer passed. Each heading says which way is better.
|
|
110
110
|
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2, 0, '
|
|
21
|
+
__version__ = version = '0.2.0.dev98'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev98')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -47,7 +47,7 @@ def memory_gb(infos):
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def index(rows, models=None, memory=None):
|
|
50
|
-
"""{"questions": n shared, "models": [{model, P, T, A, tokens, total_s, passed, answers, alongside,
|
|
50
|
+
"""{"questions": n shared, "models": [{model, P, T, A, cut, cut_pct, tokens, total_s, passed, answers, alongside,
|
|
51
51
|
fewest, fastest, tps, ttft, memory_gb}],
|
|
52
52
|
"left_out": [(model, questions answered)], "no_answers": [model], "most": questions answered by the
|
|
53
53
|
most-answered model}, best accuracy first. models: the ones to compare (default: every model in rows
|
|
@@ -75,6 +75,9 @@ def index(rows, models=None, memory=None):
|
|
|
75
75
|
"total_s": sum(c["secs"] or 0 for c in cells),
|
|
76
76
|
"passed": sum(c["passed"] for c in cells), "answers": answers,
|
|
77
77
|
"A": 100 * sum(c["passed"] for c in cells) / answers if answers else None,
|
|
78
|
+
# failed only because max_tokens ran out (overthinking, not wrong yet)
|
|
79
|
+
"cut": sum(c["truncated"] for c in cells),
|
|
80
|
+
"cut_pct": 100 * sum(c["truncated"] for c in cells) / answers if answers else None,
|
|
78
81
|
# generation speed and time to first token over the same answers (medians)
|
|
79
82
|
"tps": compare.median(r.get("gen_tps") for c in cells for r in c["rows"]),
|
|
80
83
|
"ttft": compare.median(r.get("ttft") for c in cells for r in c["rows"]),
|
|
@@ -118,7 +121,8 @@ def bars(result, width=20, markup=True):
|
|
|
118
121
|
"""Lines with a row per model and a bar each for P, T and A: the at-a-glance view. The P and T bars
|
|
119
122
|
are proportional to tokens and time (the longest is the model that used most; shorter is better),
|
|
120
123
|
with how many times the leanest / fastest model's beside them; the A bar is the share of answers
|
|
121
|
-
right (longer is better, full only when every answer passed)
|
|
124
|
+
right (longer is better, full only when every answer passed), then the share cut off at max_tokens in
|
|
125
|
+
orange (▓ in plain text) and "✂ n%" beside it. markup=False gives plain text."""
|
|
122
126
|
if not result["models"]:
|
|
123
127
|
return []
|
|
124
128
|
name_w = max(len(x["model"]) for x in result["models"])
|
|
@@ -138,12 +142,18 @@ def bars(result, width=20, markup=True):
|
|
|
138
142
|
if v is not None and v < 100: # full only when every answer passed
|
|
139
143
|
full = min(full, width - 1)
|
|
140
144
|
num = "-" if v is None else compare.pct(v / 100)
|
|
145
|
+
cut = x.get("cut_pct") or 0
|
|
146
|
+
part = min(width - full, max(1, round(width * cut / 100))) if cut else 0
|
|
147
|
+
if cut: # after the accuracy, which stays aligned with the other rows'
|
|
148
|
+
num = f"{num:>6} ✂ {compare.pct(cut / 100)}"
|
|
141
149
|
else:
|
|
142
150
|
full = _blocks(width, x.get(measure), most[measure])
|
|
143
151
|
num = times(x.get(shown))
|
|
144
|
-
|
|
145
|
-
bar =
|
|
146
|
-
|
|
152
|
+
part = 0
|
|
153
|
+
bar, cut_bar, rest = "█" * full, "▓" * part, "░" * (width - full - part)
|
|
154
|
+
bar = (f"[{color}]{bar}[/]" + (f"[#ff8c00]{'█' * part}[/]" if part else "") + f"[grey37]{rest}[/]"
|
|
155
|
+
if markup else bar + cut_bar + rest)
|
|
156
|
+
cells.append(f"{bar} {num:>6}" + " " * max(0, cell_w - width - 1 - max(6, len(num))))
|
|
147
157
|
name = f"{x['model']:<{name_w}}"
|
|
148
158
|
lines.append((f"{rich_escape(name) if markup else name} " + "".join(cells)).rstrip())
|
|
149
159
|
return lines
|
|
@@ -168,10 +178,11 @@ def left_out_lines(result):
|
|
|
168
178
|
def table(result):
|
|
169
179
|
"""(header, rows) for a plain table of the index."""
|
|
170
180
|
n = result["questions"] or 1
|
|
171
|
-
header = ["model", "tokens/answer", "time/answer", "answers right", "tok/s", "TTFT s", "memory GB"]
|
|
181
|
+
header = ["model", "tokens/answer", "time/answer", "answers right", "cut off", "tok/s", "TTFT s", "memory GB"]
|
|
172
182
|
rows = [[x["model"], f"{round(x['tokens'] / n):,}" + (" ★" if x.get("fewest") else ""),
|
|
173
183
|
fmt_secs(x["total_s"] / n) + (" ★" if x.get("fastest") else ""),
|
|
174
|
-
f"{x['passed']}/{x['answers']}",
|
|
184
|
+
f"{x['passed']}/{x['answers']}", f"{x.get('cut') or 0}/{x['answers']}",
|
|
185
|
+
_num(x.get("tps"), ".0f"), _num(x.get("ttft"), ".1f"),
|
|
175
186
|
_num(x.get("memory_gb"), ".1f")] for x in result["models"]]
|
|
176
187
|
return header, rows
|
|
177
188
|
|
|
@@ -204,7 +215,9 @@ def scope_note(result, models):
|
|
|
204
215
|
return (f"Compared on the {result['questions']} questions all {len(result['models'])} model(s) answered. "
|
|
205
216
|
"P parsimony (tokens) and T time: bars as long as the tokens / time used (shorter is better), "
|
|
206
217
|
"and how many times the leanest / fastest model's (1.0×, ★ in the table). A accuracy: answers right "
|
|
207
|
-
"(longer is better)
|
|
218
|
+
"(longer is better)"
|
|
219
|
+
+ ("; orange ✂: answers cut off at max_tokens, out of tokens rather than wrong (they count as wrong)."
|
|
220
|
+
if any(x.get("cut") for x in result["models"]) else "."))
|
|
208
221
|
|
|
209
222
|
|
|
210
223
|
def for_engine(e, labels=None, packs=None, results_dir=None):
|
|
@@ -355,6 +355,38 @@ def rich_escape(text):
|
|
|
355
355
|
return text.replace("[", r"\[")
|
|
356
356
|
|
|
357
357
|
|
|
358
|
+
def mid_trim(text, width, head=None):
|
|
359
|
+
"""text cut to width cells with … in the middle, so both ends of a model name show (its family and
|
|
360
|
+
its server suffix, e.g. -llama / -vllm). head: how many leading characters to keep (default: half)."""
|
|
361
|
+
if cell_len(text) <= width or width < 5:
|
|
362
|
+
return text
|
|
363
|
+
head = width - 1 - width // 2 if head is None else min(head, width - 2)
|
|
364
|
+
return text[:head] + "…" + text[head - width + 1:]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def trim_names(names, width):
|
|
368
|
+
"""names cut to width (mid_trim), still telling them apart: when cutting the middle would make two
|
|
369
|
+
alike, the cut moves into the start they all share; if that's not enough either, names stay whole."""
|
|
370
|
+
trimmed = [mid_trim(n, width) for n in names]
|
|
371
|
+
if len(set(trimmed)) == len(set(names)):
|
|
372
|
+
return trimmed
|
|
373
|
+
shared = len(os.path.commonprefix(list(names)))
|
|
374
|
+
head = width - 1 - max(len(n) - shared for n in names) # keep every name's end from where they differ
|
|
375
|
+
if head < 1:
|
|
376
|
+
return list(names)
|
|
377
|
+
trimmed = [mid_trim(n, width, head=head) for n in names]
|
|
378
|
+
return trimmed if len(set(trimmed)) == len(set(names)) else list(names)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def fit_first_column(header, rows, room, floor=24):
|
|
382
|
+
"""rows with the first cell trimmed in the middle (mid_trim) so a table of these columns fits room
|
|
383
|
+
cells across, but never below floor cells. Cells are Rich markup; each column adds 2 cells of padding."""
|
|
384
|
+
rest = sum(2 + max(cell_len(h), *(Text.from_markup(r[i]).cell_len for r in rows))
|
|
385
|
+
for i, h in enumerate(header) if i)
|
|
386
|
+
names = trim_names([r[0] for r in rows], max(floor, room - rest - 2))
|
|
387
|
+
return [[n] + r[1:] for n, r in zip(names, rows)]
|
|
388
|
+
|
|
389
|
+
|
|
358
390
|
class BlockDumper(yaml.SafeDumper):
|
|
359
391
|
"""Multi-line strings (reference code, expected text) as readable | blocks, not escaped \\n."""
|
|
360
392
|
|
|
@@ -2981,6 +3013,9 @@ class ResultsScreen(Screen):
|
|
|
2981
3013
|
self.query_one("#pta-left-out", Static).update(
|
|
2982
3014
|
"\n".join(f"[dim]{rich_escape(line)}[/dim]" for line in pta.left_out_lines(result)))
|
|
2983
3015
|
header, table = pta.table(result)
|
|
3016
|
+
self.pta_shown = bool(table) # to refit on resize
|
|
3017
|
+
if table:
|
|
3018
|
+
table = fit_first_column(header, table, (self.size.width or self.app.size.width) - 3)
|
|
2984
3019
|
for i, h in enumerate(header): # sized to the contents: added before the rows, a column keeps its header's width
|
|
2985
3020
|
t.add_column(h, width=max(len(h), *(len(r[i]) for r in table)) if table else None)
|
|
2986
3021
|
t.add_rows(table)
|
|
@@ -3065,6 +3100,14 @@ class ResultsScreen(Screen):
|
|
|
3065
3100
|
f"{os.path.basename(os.path.dirname(p))} · {os.path.basename(p)[:-5]}" for p in sorted(self.superseded))
|
|
3066
3101
|
+ ". Only the new run's answers (◐) are shown for these.[/yellow]" if self.superseded else ""))
|
|
3067
3102
|
|
|
3103
|
+
def on_resize(self, event):
|
|
3104
|
+
"""Refit the tables whose model names are trimmed to the window's width."""
|
|
3105
|
+
if getattr(self, "pq_summary_args", None):
|
|
3106
|
+
self.fill_pq_summary(*self.pq_summary_args)
|
|
3107
|
+
self.sort_table(self.query_one("#pq-summary", DataTable))
|
|
3108
|
+
if getattr(self, "pta_shown", False):
|
|
3109
|
+
self.fill_pta()
|
|
3110
|
+
|
|
3068
3111
|
def fill_pq_summary(self, tests, models):
|
|
3069
3112
|
"""One row per shown model over the questions they ALL answered, so totals compare fairly;
|
|
3070
3113
|
with two models, a head-to-head line."""
|
|
@@ -3081,9 +3124,11 @@ class ResultsScreen(Screen):
|
|
|
3081
3124
|
title.update("[yellow]These models have no question in common (with the filters above).[/yellow]")
|
|
3082
3125
|
table.display = False
|
|
3083
3126
|
return
|
|
3127
|
+
self.pq_summary_args = (tests, models) # to refit on resize
|
|
3084
3128
|
rows = compare.compare_models(shared, models)
|
|
3085
|
-
|
|
3086
|
-
|
|
3129
|
+
# short headings so every column fits; the line above the table says what each one is
|
|
3130
|
+
header = ("Model", "Time", "Passed", "Med tok", "Tokens", "Tok/✓", "Med s", "Prompt t/s", "Gen t/s",
|
|
3131
|
+
"Fewest", "Fastest", "Cut off")
|
|
3087
3132
|
best = {k: max((r[k] for r in rows if r[k] is not None), default=None)
|
|
3088
3133
|
for k in ("passed", "fewest", "fastest", "avg_pp_tps", "avg_gen_tps")}
|
|
3089
3134
|
low = {k: min(r[k] for r in rows if r[k] is not None) if any(r[k] is not None for r in rows) else None
|
|
@@ -3092,9 +3137,10 @@ class ResultsScreen(Screen):
|
|
|
3092
3137
|
def hi(v, text, key, good):
|
|
3093
3138
|
ref = best.get(key) if good == "max" else low.get(key)
|
|
3094
3139
|
return f"[bold green]{text}[/bold green]" if v is not None and v == ref and len(rows) > 1 else text
|
|
3140
|
+
cells = []
|
|
3095
3141
|
for r in rows:
|
|
3096
3142
|
tt = r["total_secs"]
|
|
3097
|
-
|
|
3143
|
+
cells.append([r["model"],
|
|
3098
3144
|
hi(tt, ("-" if not tt else f"{tt:.1f}s" if tt < 60 else fmt_secs(tt)), "total_secs", "min"),
|
|
3099
3145
|
hi(r["passed"], f"{r['passed']}/{r['n']}", "passed", "max"),
|
|
3100
3146
|
hi(r["med_tokens"], f"{r['med_tokens']:,.0f}" if r["med_tokens"] else "-", "med_tokens", "min"),
|
|
@@ -3107,7 +3153,9 @@ class ResultsScreen(Screen):
|
|
|
3107
3153
|
"max"),
|
|
3108
3154
|
hi(r["fewest"], str(r["fewest"]), "fewest", "max"),
|
|
3109
3155
|
hi(r["fastest"], str(r["fastest"]), "fastest", "max"),
|
|
3110
|
-
f"[red]{r['cut']}[/red]" if r["cut"] else "0")
|
|
3156
|
+
f"[red]{r['cut']}[/red]" if r["cut"] else "0"])
|
|
3157
|
+
table.add_columns(*header)
|
|
3158
|
+
table.add_rows(fit_first_column(header, cells, (self.size.width or self.app.size.width) - 3))
|
|
3111
3159
|
line = f"[b]Comparison[/b] over the {len(shared)} questions all {len(models)} answered"
|
|
3112
3160
|
if len(models) == 2:
|
|
3113
3161
|
h = compare.head_to_head(shared, *models)
|
|
@@ -3121,9 +3169,10 @@ class ResultsScreen(Screen):
|
|
|
3121
3169
|
if shared_speed:
|
|
3122
3170
|
line += (f"\n[yellow]Some answers of {', '.join(shared_speed)} ran alongside other models, which "
|
|
3123
3171
|
"shared the machine's speed: compare their times with care.[/yellow]")
|
|
3124
|
-
title.update(line + " [dim]
|
|
3125
|
-
"
|
|
3126
|
-
"(cached prompts left out).
|
|
3172
|
+
title.update(line + " [dim]Time / Tokens: each question's median, added up. Med tok / Med s: median "
|
|
3173
|
+
"per answer. Tok/✓: tokens per answer right. Prompt t/s / Gen t/s: all tokens over all the time "
|
|
3174
|
+
"they took (cached prompts left out). Fewest / Fastest: questions won; ties count for both. "
|
|
3175
|
+
"Cut off: answers that hit max_tokens.[/dim]")
|
|
3127
3176
|
|
|
3128
3177
|
@staticmethod
|
|
3129
3178
|
def pq_cell(c, best, here):
|
|
@@ -3277,7 +3326,7 @@ class EvalsApp(App):
|
|
|
3277
3326
|
#pq-controls Select { width: 22; margin-right: 1; }
|
|
3278
3327
|
#pq-models { margin-right: 1; min-width: 20; }
|
|
3279
3328
|
#pq-compare-title { padding: 0 1; height: auto; }
|
|
3280
|
-
#pq-summary { height: auto;
|
|
3329
|
+
#pq-summary { height: auto; } /* every picked model, no scrolling: the tab scrolls instead */
|
|
3281
3330
|
#pq-legend { padding: 0 1; height: auto; }
|
|
3282
3331
|
/* Tabs must read as tabs, not as a line of text: each tab is a button-like chip on its own band,
|
|
3283
3332
|
the active one filled with the accent colour. (Textual's default: dimmed words on the background.) */
|
|
@@ -1264,6 +1264,29 @@ class PTAIndex(unittest.TestCase):
|
|
|
1264
1264
|
self.assertEqual((two_wrong[-2], two_wrong[-1]), ("█████████░", "99.6%")) # not a full bar
|
|
1265
1265
|
self.assertEqual((perfect[-2], perfect[-1]), ("██████████", "100%"))
|
|
1266
1266
|
|
|
1267
|
+
def test_cut_off_answers_show_in_the_accuracy_bar(self):
|
|
1268
|
+
from tuieval import pta
|
|
1269
|
+
|
|
1270
|
+
def row(model, test, ok, truncated=False):
|
|
1271
|
+
return {"model": model, "suite": "p", "test": f"p: {test}", "ok": ok, "latency": 1000,
|
|
1272
|
+
"tokens": 10, "truncated": truncated, "gen_tps": None, "repeat": 0}
|
|
1273
|
+
rows = ([row("thinker", f"q{i}", i < 6) for i in range(8)]
|
|
1274
|
+
+ [row("thinker", f"q{i}", False, truncated=True) for i in (8, 9)] # out of tokens
|
|
1275
|
+
+ [row("plain", f"q{i}", i < 6) for i in range(10)])
|
|
1276
|
+
res = pta.index(rows)
|
|
1277
|
+
by = {x["model"]: x for x in res["models"]}
|
|
1278
|
+
self.assertEqual((by["thinker"]["A"], by["thinker"]["cut"], by["thinker"]["cut_pct"]), (60, 2, 20))
|
|
1279
|
+
self.assertEqual((by["plain"]["cut"], by["plain"]["cut_pct"]), (0, 0))
|
|
1280
|
+
bars = {line.split()[0]: line.split() for line in pta.bars(res, 10, markup=False)[2:]}
|
|
1281
|
+
self.assertEqual(bars["thinker"][-4:], ["██████▓▓░░", "60.0%", "✂", "20.0%"]) # right, cut off, wrong
|
|
1282
|
+
self.assertEqual(bars["plain"][-2:], ["██████░░░░", "60.0%"]) # no cut-offs: no ✂
|
|
1283
|
+
self.assertIn("[#ff8c00]██[/]", pta.bars(res, 10)[2] + pta.bars(res, 10)[3])
|
|
1284
|
+
header, table = pta.table(res)
|
|
1285
|
+
self.assertEqual(header[4], "cut off")
|
|
1286
|
+
self.assertEqual({r[0]: r[4] for r in table}, {"thinker": "2/10", "plain": "0/10"})
|
|
1287
|
+
self.assertIn("cut off at max_tokens", pta.scope_note(res, ["thinker", "plain"]))
|
|
1288
|
+
self.assertNotIn("cut off", pta.scope_note(pta.index(rows[10:]), ["plain"]))
|
|
1289
|
+
|
|
1267
1290
|
def test_server_errors_and_models_with_few_answers(self):
|
|
1268
1291
|
from tuieval import pta
|
|
1269
1292
|
|
|
@@ -1637,6 +1660,51 @@ class FirstRun(unittest.TestCase):
|
|
|
1637
1660
|
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""), timeout=60).stdout.strip()
|
|
1638
1661
|
self.assertEqual(out, "['first'] ['my-model'] smoke False") # tuning shows once something has run
|
|
1639
1662
|
|
|
1663
|
+
def test_trimmed_names_keep_both_ends(self):
|
|
1664
|
+
from tuieval.tui import fit_first_column, mid_trim, trim_names
|
|
1665
|
+
name = "some-author--some-long-model-name-q4_k_m-vllm"
|
|
1666
|
+
self.assertEqual(mid_trim(name, 20), "some-auth…4_k_m-vllm")
|
|
1667
|
+
self.assertEqual(mid_trim("short", 20), "short")
|
|
1668
|
+
rows = [[name, "[green]1/2[/green]", "10"], ["b", "2/2", "1,000"]]
|
|
1669
|
+
fitted = fit_first_column(["Model", "Passed", "Tokens"], rows, 45) # 2 + 8 + 8 cells for the rest
|
|
1670
|
+
self.assertEqual([len(r[0]) for r in fitted], [27, 1])
|
|
1671
|
+
self.assertEqual(fitted[0][1:], rows[0][1:])
|
|
1672
|
+
self.assertEqual(len(fit_first_column(["Model", "Passed"], rows, 10)[0][0]), 24) # never below the floor
|
|
1673
|
+
twins = [f"author--model-number-{i}-and-a-long-tail" for i in range(3)] # alike but in the middle
|
|
1674
|
+
self.assertEqual(trim_names(twins, 20), ["au…0-and-a-long-tail", "au…1-and-a-long-tail",
|
|
1675
|
+
"au…2-and-a-long-tail"])
|
|
1676
|
+
self.assertEqual(trim_names(twins, 10), twins) # can't keep them apart that short: whole
|
|
1677
|
+
|
|
1678
|
+
def test_per_question_summary_fits_every_model(self):
|
|
1679
|
+
tuieval(self.ws, "add", "my-model", "--server", "local")
|
|
1680
|
+
code = textwrap.dedent("""
|
|
1681
|
+
import asyncio
|
|
1682
|
+
from tuieval import compare
|
|
1683
|
+
from tuieval.tui import EvalsApp, ResultsScreen
|
|
1684
|
+
from textual.widgets import DataTable, TabbedContent
|
|
1685
|
+
models = [f"some-author--a-rather-long-model-name-number-{i:02d}-q4_k_m-vllm" for i in range(12)]
|
|
1686
|
+
rows = [{"model": m, "suite": "p", "test": f"p: q{q}", "ok": q % 2 == 0, "latency": 1000, "tokens": 10,
|
|
1687
|
+
"truncated": False, "gen_tps": 20.0, "prompt_tps": None, "repeat": 0} for m in models for q in range(3)]
|
|
1688
|
+
async def go():
|
|
1689
|
+
app = EvalsApp({})
|
|
1690
|
+
async with app.run_test(size=(120, 40)) as pilot:
|
|
1691
|
+
await pilot.pause()
|
|
1692
|
+
app.push_screen(ResultsScreen())
|
|
1693
|
+
await pilot.pause(0.5)
|
|
1694
|
+
app.screen.query_one(TabbedContent).active = "tab-per-question"
|
|
1695
|
+
await pilot.pause(0.3)
|
|
1696
|
+
app.screen.fill_pq_summary(compare.per_question(rows), models)
|
|
1697
|
+
await pilot.pause(0.3)
|
|
1698
|
+
t = app.screen.query_one("#pq-summary", DataTable)
|
|
1699
|
+
names = [str(t.get_row_at(i)[0]) for i in range(t.row_count)]
|
|
1700
|
+
print(t.row_count, t.region.height, t.virtual_size.width <= t.region.width,
|
|
1701
|
+
all(n.endswith("-q4_k_m-vllm") and "…" in n for n in names), len(set(names)))
|
|
1702
|
+
asyncio.run(go())
|
|
1703
|
+
""")
|
|
1704
|
+
out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True,
|
|
1705
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""), timeout=60)
|
|
1706
|
+
self.assertEqual(out.stdout.strip(), "12 13 True True 12", out.stderr[-2000:]) # every row, no scroll
|
|
1707
|
+
|
|
1640
1708
|
def test_results_open_on_pta_index(self):
|
|
1641
1709
|
tuieval(self.ws, "add", "my-model", "--server", "local")
|
|
1642
1710
|
code = textwrap.dedent("""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev94 → tuieval-0.2.0.dev98}/src/tuieval/templates/packs/tool_call/tools.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|