tuieval 0.2.0.dev127__tar.gz → 0.2.0.dev131__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.dev127 → tuieval-0.2.0.dev131}/CHANGELOG.md +2 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/PKG-INFO +1 -1
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/models.md +2 -1
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/compare.py +28 -3
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/insights.py +12 -6
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/pta.py +23 -9
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/run_evals.py +3 -1
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/tui.py +92 -17
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/tests/test_tuieval.py +155 -2
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/.gitignore +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/LICENSE +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/README.md +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/agent-packs.md +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/agents.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/dates.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/engine.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/logs.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/tests/mock_server.py +0 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Partial credit: the PTA index shows each model's score (e.g. 5 of 6 checks passed is 83%) beside accuracy, and can be ordered by it (By score, `tuieval pta --sort score`). Why models fail splits failures into near miss, partly right and wrong; Failures shows each one's kind and checks and filters by kind; Per question cells show `✗ 5/6`. Verdicts are unchanged.
|
|
6
|
+
- Results: picking a pack (PTA index or Per question) compares the models that answered all of it; the Models button can change that, and All packs goes back to the earlier pick.
|
|
5
7
|
- Fix: a request that runs out of time says `time limit: timed out after N s`, not `connection error`, and `tuieval compare` counts it against the model like the run does (it was left out as an error).
|
|
6
8
|
- Agent packs (`kind = "agent"`): each test is a small project with a task, worked on by a command-line agent you define in `[agents]`, using the model tuieval serves, and graded by the task's visible and hidden tests. Results show the agent's stats, diff and transcript; `tuieval compare` adds an Agent tasks table; `tuieval selftest` checks each task's reference and wrong fixes. See docs/agent-packs.md.
|
|
7
9
|
- Answers that used more tokens than the `max_tokens` sent (a provider that didn't stop them) are marked over budget: `(n over budget)` in the PTA index, an **over budget** column, `over` in Per question and a note in the answer view. `[defaults] over_budget = "fail"` fails them instead. Works on stored results.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev131
|
|
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
|
|
@@ -105,6 +105,7 @@ The **PTA index** puts parsimony (tokens), time and accuracy side by side, as a
|
|
|
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
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% truncated)` beside the number and a **truncated** 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
|
+
- **Score**: partial credit, averaged over the same answers. Graders that can tell how close an answer came give it: the code grader the share of checks passed (5 of 6 is 83%, where accuracy counts it wrong), agent packs the share of hidden tests, the tool-call and RAG graders part marks. It shows beside the accuracy when the two differ, and in a **score** column. Order the models by it with the **By score** dropdown in the tab or `tuieval pta --sort score`; verdicts still use accuracy only.
|
|
108
109
|
- **Over budget**: some providers don't stop a model at `max_tokens` (thinking goes on past it), so the answer isn't cut off but the model had more room than the others. Answers that used more tokens than were sent as `max_tokens` show as `(n over budget)` beside the accuracy, an **over budget** column, `over` in Results → Per question and a note in the answer view. They're only flagged; `over_budget = "fail"` in `[defaults]` counts them as failed instead (stored results aren't changed, so switching back restores them).
|
|
109
110
|
|
|
110
111
|
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.
|
|
@@ -118,7 +119,7 @@ Results → **Insights** (also `tuieval insights`) turns the results into charts
|
|
|
118
119
|
1. **Who's worth keeping**: every model against a reference model, paired on the questions both answered: the difference in accuracy points with its 95% CI, and better, tie or worse. Pick the reference in the tab or with `--reference`; set a default with `reference = "<label>"` under `[defaults]` in `models.toml`. Without one, the most accurate model is the reference.
|
|
119
120
|
2. **Best model per use case**: a heatmap of accuracy per model and use case (pack group); ★ marks the best among models that answered most of it.
|
|
120
121
|
3. **Fine-tunes against their base model**: for each model with `base = "<label>"` in its `[[models]]` block, the difference per pack and the question categories (a test's `category`) it gained or lost most.
|
|
121
|
-
4. **Why models fail**: each model's answers split into right, wrong, cut off at max_tokens, no answer or bad format, code that crashed or hung, and critical failures.
|
|
122
|
+
4. **Why models fail**: each model's answers split into right, near miss (a score of 80% or more, e.g. 5 of 6 checks), partly right, wrong (a score of 0), cut off at max_tokens or the time limit, no answer or bad format, code that crashed or hung, and critical failures. The Failures tab shows each failure's kind (with its checks passed) and filters by it.
|
|
122
123
|
5. **Accuracy against cost**: accuracy against median tokens (or seconds) per question, over the questions every model answered; green models are the ones no other model beats on both. With hosted and local models together, the best local trade-offs get their own line.
|
|
123
124
|
6. **Question health**: questions every model fails (check them), every model passes (they don't separate models), and the ones that split models most.
|
|
124
125
|
7. **Next runs**: the Certify runs that would settle the open verdicts of the strongest models, with their time and command.
|
|
@@ -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.dev131'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev131')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -351,10 +351,22 @@ def speed(rows, infos, machine=None, speeds=None):
|
|
|
351
351
|
return header, table, frontier
|
|
352
352
|
|
|
353
353
|
|
|
354
|
+
NEAR_MISS = 0.8 # a failed answer scoring at least this (e.g. 5 of 6 checks) is a near miss
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def checks_passed(r):
|
|
358
|
+
"""(passed, total) checks of a failed code answer (from its stored checks, or its reason), else None."""
|
|
359
|
+
checks = (r.get("raw") or {}).get("checks")
|
|
360
|
+
if checks:
|
|
361
|
+
return sum(v is None for v in checks.values()), len(checks)
|
|
362
|
+
m = re.match(r"Failed (\d+)/(\d+) checks", r.get("reason") or "")
|
|
363
|
+
return (int(m[2]) - int(m[1]), int(m[2])) if m else None
|
|
364
|
+
|
|
365
|
+
|
|
354
366
|
def per_question(rows):
|
|
355
367
|
"""[{test, name, suite, difficulty, cells: {model: cell}, spread}] with one cell per model that
|
|
356
|
-
answered: n answers, passes, median tokens / seconds / tok/s over repeats,
|
|
357
|
-
partial (some answers from a pack still running), rows (the answers, for the detail view).
|
|
368
|
+
answered: n answers, passes, mean score (partial credit), median tokens / seconds / tok/s over repeats,
|
|
369
|
+
truncated count, partial (some answers from a pack still running), rows (the answers, for the detail view).
|
|
358
370
|
spread = {"tokens": .., "secs": ..}: largest / smallest model median (1 if one model)."""
|
|
359
371
|
tests = {}
|
|
360
372
|
for r in rows:
|
|
@@ -369,6 +381,7 @@ def per_question(rows):
|
|
|
369
381
|
c.update(rows=v, n=len(v), passed=sum(r["ok"] for r in v), truncated=sum(r["truncated"] for r in v),
|
|
370
382
|
over=sum(bool(r.get("over_budget")) for r in v),
|
|
371
383
|
partial=any(r.get("partial") for r in v),
|
|
384
|
+
score=statistics.fmean(r.get("score", r["ok"]) for r in v),
|
|
372
385
|
tokens=median(r["tokens"] or None for r in v), secs=median(r["latency"] / 1000 or None for r in v),
|
|
373
386
|
tps=median(r["gen_tps"] for r in v))
|
|
374
387
|
t["spread"] = _spread(t["cells"])
|
|
@@ -390,10 +403,21 @@ def with_models(test, models):
|
|
|
390
403
|
return dict(test, cells=cells, spread=_spread(cells))
|
|
391
404
|
|
|
392
405
|
|
|
406
|
+
def complete_models(rows, suite, test_ids=None):
|
|
407
|
+
"""The models whose rows answer every question of a pack: test_ids (the pack's current tests),
|
|
408
|
+
or every question of it seen in the rows. Partial rows (a pack still running) don't count."""
|
|
409
|
+
rows = [r for r in rows if r["suite"] == suite and not r.get("partial")]
|
|
410
|
+
wanted = {f"{suite}: {t}" for t in test_ids} if test_ids is not None else {r["test"] for r in rows}
|
|
411
|
+
answered = {}
|
|
412
|
+
for r in rows:
|
|
413
|
+
answered.setdefault(r["model"], set()).add(r["test"])
|
|
414
|
+
return sorted(m for m, tests in answered.items() if wanted and wanted <= tests)
|
|
415
|
+
|
|
416
|
+
|
|
393
417
|
def compare_models(tests, models):
|
|
394
418
|
"""Per model over per_question entries that all `models` answered: passes, token and time
|
|
395
419
|
totals/medians (totals add each question's median over repeats, so repeats don't inflate them), questions where it used the fewest tokens / was fastest (among passing
|
|
396
|
-
answers for tokens; ties count for each), answers cut off."""
|
|
420
|
+
answers for tokens; ties count for each), answers cut off, mean score (partial credit)."""
|
|
397
421
|
out = []
|
|
398
422
|
for m in models:
|
|
399
423
|
cs = [t["cells"][m] for t in tests]
|
|
@@ -410,6 +434,7 @@ def compare_models(tests, models):
|
|
|
410
434
|
fastest += 1
|
|
411
435
|
rs = [r for c in cs for r in c["rows"]]
|
|
412
436
|
out.append({"model": m, "n": len(cs), "passed": passed, "total_tokens": total,
|
|
437
|
+
"score": statistics.fmean(c["score"] for c in cs) if cs else None,
|
|
413
438
|
"avg_pp_tps": _rate([(r.get("prompt_tokens"), r["prompt_tps"]) for r in rs]),
|
|
414
439
|
"avg_gen_tps": _rate([(r["tokens"], r["gen_tps"]) for r in rs]),
|
|
415
440
|
"med_tokens": median(c["tokens"] for c in cs), "med_secs": median(c["secs"] for c in cs),
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
2 use cases accuracy per model and use case (pack group) as a heatmap; the best marked
|
|
7
7
|
3 vs base a fine-tune against its base model (models.toml `base`): per pack, and the question
|
|
8
8
|
categories it gained or lost most
|
|
9
|
-
4 why they fail each model's answers split into right,
|
|
10
|
-
crashed or hung, and
|
|
9
|
+
4 why they fail each model's answers split into right, near miss (most checks passed), partly right,
|
|
10
|
+
wrong, cut off (or out of time), no answer / bad format, code crashed or hung, and
|
|
11
|
+
critical failures
|
|
11
12
|
5 accuracy / cost accuracy against median tokens (or seconds) per question; the models no other beats
|
|
12
13
|
on both
|
|
13
14
|
6 questions questions every model fails (check them), every model passes (no signal), and the
|
|
@@ -267,7 +268,8 @@ def categories(rows, m, base, min_q=2):
|
|
|
267
268
|
|
|
268
269
|
|
|
269
270
|
# ---------------------------------------------------------------- 4 why they fail
|
|
270
|
-
KINDS = (("right", "green", "█"), ("
|
|
271
|
+
KINDS = (("right", "green", "█"), ("near miss", "#87af5f", "█"), ("partly right", "#af875f", "█"),
|
|
272
|
+
("wrong", "red", "█"), ("cut off", "yellow", "█"),
|
|
271
273
|
("no answer / format", "magenta", "█"), ("code crashed or hung", "cyan", "█"), ("critical", "#ff5faf", "▓"))
|
|
272
274
|
FORMAT = ("No 'ANSWER:' line", "Empty answer", "No code", "no tool call")
|
|
273
275
|
CRASHED = ("Timed out after", "Harness crashed", "grader error")
|
|
@@ -278,13 +280,15 @@ def kind(r):
|
|
|
278
280
|
return "right"
|
|
279
281
|
if r.get("severity") == "critical":
|
|
280
282
|
return "critical"
|
|
281
|
-
if r["truncated"] or r["reason"].startswith(("TRUNCATED", "OVER BUDGET")):
|
|
283
|
+
if r["truncated"] or r["reason"].startswith(("TRUNCATED", "OVER BUDGET", "time limit")):
|
|
282
284
|
return "cut off"
|
|
283
285
|
if r["reason"].startswith(FORMAT):
|
|
284
286
|
return "no answer / format"
|
|
285
287
|
if r["reason"].startswith(CRASHED):
|
|
286
288
|
return "code crashed or hung"
|
|
287
|
-
|
|
289
|
+
# partial credit (e.g. checks passed): close isn't the same as all wrong
|
|
290
|
+
score = r.get("score") or 0
|
|
291
|
+
return "near miss" if score >= compare.NEAR_MISS else "partly right" if score > 0 else "wrong"
|
|
288
292
|
|
|
289
293
|
|
|
290
294
|
def _split(counts, total, width):
|
|
@@ -322,7 +326,9 @@ def failure_mix(rows, models, width=36):
|
|
|
322
326
|
top, n = max(((k, c[k]) for k, *_ in KINDS[1:-1]), key=lambda x: x[1])
|
|
323
327
|
if n / fails >= 0.4 and top != "wrong":
|
|
324
328
|
actions.append(f"{m}: {n} of its {fails} failures are {top}: " + {
|
|
325
|
-
"cut off": "it thinks too long for max_tokens, rather than reasoning wrong.",
|
|
329
|
+
"cut off": "it thinks too long for max_tokens (or the time limit), rather than reasoning wrong.",
|
|
330
|
+
"near miss": "they pass most of their checks: it's closer than its accuracy shows.",
|
|
331
|
+
"partly right": "they pass some of their checks, not none.",
|
|
326
332
|
"no answer / format": "it doesn't follow the answer format (check its chat template and the "
|
|
327
333
|
"pack's instructions).",
|
|
328
334
|
"code crashed or hung": "its code crashes or never finishes."}[top])
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
model's ("28×").
|
|
8
8
|
A accuracy the share of answers to those questions that passed (%).
|
|
9
9
|
|
|
10
|
+
Beside A, the score: partial credit averaged over the same answers (e.g. a code answer passing 5 of 6
|
|
11
|
+
checks scores 83%, where A counts it wrong). Shown when it differs from A; models can be ordered by it.
|
|
12
|
+
|
|
10
13
|
The P and T bars are as long as the tokens and time used (the model that used the most fills its bar,
|
|
11
14
|
the others in proportion, at least one block), so shorter is better; the A bar: longer is better.
|
|
12
15
|
P and T also carry a 0-100 score (100 for the best, POINTS_PER_DOUBLING points less for every
|
|
@@ -46,11 +49,15 @@ def memory_gb(infos):
|
|
|
46
49
|
return out
|
|
47
50
|
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
SORTS = ("accuracy", "score")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def index(rows, models=None, memory=None, sort="accuracy"):
|
|
56
|
+
"""{"questions": n shared, "models": [{model, P, T, A, S, cut, cut_pct, tokens, total_s, passed, answers, alongside,
|
|
51
57
|
fewest, fastest, tps, ttft, memory_gb}],
|
|
52
58
|
"left_out": [(model, questions answered)], "no_answers": [model], "most": questions answered by the
|
|
53
|
-
most-answered model}, best accuracy
|
|
59
|
+
most-answered model}, best accuracy (sort="accuracy") or score (sort="score") first. S: the mean
|
|
60
|
+
score (partial credit) in %. models: the ones to compare (default: every model in rows
|
|
54
61
|
with enough answers; models named here are compared whatever their count). memory: memory_gb()."""
|
|
55
62
|
no_answers = sorted({r["model"] for r in rows} - {r["model"] for r in rows if not r.get("error")})
|
|
56
63
|
rows = [r for r in rows if not r.get("error")] # a server or connection error isn't an answer
|
|
@@ -75,6 +82,7 @@ def index(rows, models=None, memory=None):
|
|
|
75
82
|
"total_s": sum(c["secs"] or 0 for c in cells),
|
|
76
83
|
"passed": sum(c["passed"] for c in cells), "answers": answers,
|
|
77
84
|
"A": 100 * sum(c["passed"] for c in cells) / answers if answers else None,
|
|
85
|
+
"S": 100 * sum(c["score"] * c["n"] for c in cells) / answers if answers else None,
|
|
78
86
|
# failed only because max_tokens ran out (overthinking, not wrong yet)
|
|
79
87
|
"cut": sum(c["truncated"] for c in cells),
|
|
80
88
|
"cut_pct": 100 * sum(c["truncated"] for c in cells) / answers if answers else None,
|
|
@@ -95,7 +103,8 @@ def index(rows, models=None, memory=None):
|
|
|
95
103
|
x["time_x"] = x["total_s"] / fastest if fastest and x["total_s"] else None
|
|
96
104
|
x["fewest"] = bool(fewest) and x["tokens"] == fewest
|
|
97
105
|
x["fastest"] = bool(fastest) and x["total_s"] == fastest
|
|
98
|
-
|
|
106
|
+
first, then = ("S", "A") if sort == "score" else ("A", "S")
|
|
107
|
+
out.sort(key=lambda x: (-(x[first] or 0), -(x[then] or 0), -(x["T"] or 0), x["model"]))
|
|
99
108
|
return {"questions": len(shared), "models": out, "left_out": left_out, "no_answers": no_answers,
|
|
100
109
|
"most": most}
|
|
101
110
|
|
|
@@ -145,6 +154,10 @@ def bars(result, width=20, markup=True, click=None):
|
|
|
145
154
|
if v is not None and v < 100: # full only when every answer passed
|
|
146
155
|
full = min(full, width - 1)
|
|
147
156
|
num = "-" if v is None else compare.pct(v / 100)
|
|
157
|
+
score = x.get("S")
|
|
158
|
+
if score is not None and v is not None and compare.pct(score / 100) != num:
|
|
159
|
+
note = f"score {compare.pct(score / 100)}"
|
|
160
|
+
num = f"{num:>6} " + (f"[dim]{note}[/dim]" if markup else note)
|
|
148
161
|
cut = x.get("cut_pct") or 0
|
|
149
162
|
part = min(width - full, max(1, round(width * cut / 100))) if cut else 0
|
|
150
163
|
if cut: # after the accuracy, which stays aligned with the other rows'
|
|
@@ -189,11 +202,12 @@ def table(result):
|
|
|
189
202
|
"""(header, rows) for a plain table of the index."""
|
|
190
203
|
n = result["questions"] or 1
|
|
191
204
|
over = any(x.get("over") for x in result["models"]) # a column only when some answer ran past max_tokens
|
|
192
|
-
header = ["model", "tokens/answer", "time/answer", "answers right", "truncated", *(["over budget"] if over else []),
|
|
205
|
+
header = ["model", "tokens/answer", "time/answer", "answers right", "score", "truncated", *(["over budget"] if over else []),
|
|
193
206
|
"tok/s", "TTFT s", "memory GB"]
|
|
194
207
|
rows = [[x["model"], f"{round(x['tokens'] / n):,}" + (" ★" if x.get("fewest") else ""),
|
|
195
208
|
fmt_secs(x["total_s"] / n) + (" ★" if x.get("fastest") else ""),
|
|
196
|
-
f"{x['passed']}/{x['answers']}",
|
|
209
|
+
f"{x['passed']}/{x['answers']}", "-" if x.get("S") is None else compare.pct(x["S"] / 100),
|
|
210
|
+
f"{x.get('cut') or 0}/{x['answers']}",
|
|
197
211
|
*([f"{x.get('over') or 0}/{x['answers']}"] if over else []),
|
|
198
212
|
_num(x.get("tps"), ".0f"), _num(x.get("ttft"), ".1f"),
|
|
199
213
|
_num(x.get("memory_gb"), ".1f")] for x in result["models"]]
|
|
@@ -228,7 +242,7 @@ def scope_note(result, models):
|
|
|
228
242
|
return (f"Compared on the {result['questions']} questions all {len(result['models'])} model(s) answered. "
|
|
229
243
|
"P parsimony (tokens) and T time: bars as long as the tokens / time used (shorter is better), "
|
|
230
244
|
"and how many times the leanest / fastest model's (1.0×, ★ in the table). A accuracy: answers right "
|
|
231
|
-
"(longer is better)"
|
|
245
|
+
"(longer is better); score: partial credit, e.g. 5 of 6 checks passed is 83% (shown when it differs)"
|
|
232
246
|
+ ("; orange, (n% truncated): answers cut off at max_tokens. They count as wrong, but the model ran "
|
|
233
247
|
"out of tokens rather than answering wrong." if any(x.get("cut") for x in result["models"]) else ".")
|
|
234
248
|
+ (" Over budget: answers that used more tokens than the max_tokens sent, because the provider didn't "
|
|
@@ -236,14 +250,14 @@ def scope_note(result, models):
|
|
|
236
250
|
if any(x.get("over") for x in result["models"]) else ""))
|
|
237
251
|
|
|
238
252
|
|
|
239
|
-
def for_engine(e, labels=None, packs=None, results_dir=None):
|
|
253
|
+
def for_engine(e, labels=None, packs=None, results_dir=None, sort="accuracy"):
|
|
240
254
|
"""(result, models) from a workspace's finished results (results on their way out excluded)."""
|
|
241
255
|
paths = [p for p in compare.default_paths(results_dir or e.results_dir) if not e.superseded(p)]
|
|
242
256
|
rows, infos = compare.load(paths) if paths else ([], [])
|
|
243
257
|
if packs:
|
|
244
258
|
rows = [r for r in rows if r["suite"] in packs]
|
|
245
259
|
models = sorted({r["model"] for r in rows} & set(labels) if labels else {r["model"] for r in rows})
|
|
246
|
-
return index(rows, models if labels else None, memory_gb(infos)), models
|
|
260
|
+
return index(rows, models if labels else None, memory_gb(infos), sort), models
|
|
247
261
|
|
|
248
262
|
|
|
249
263
|
def markdown(result, models):
|
|
@@ -402,6 +402,8 @@ def cmd_pta(argv):
|
|
|
402
402
|
p.add_argument("--only", help="comma-separated model labels (default: every model with results)")
|
|
403
403
|
p.add_argument("--packs", help="comma-separated packs (default: all)")
|
|
404
404
|
p.add_argument("--width", type=int, default=20, help="bar width in characters (default 20)")
|
|
405
|
+
p.add_argument("--sort", choices=("accuracy", "score"), default="accuracy",
|
|
406
|
+
help="order models by answers right (default) or by score (partial credit)")
|
|
405
407
|
p.add_argument("--models")
|
|
406
408
|
p.add_argument("--results-dir")
|
|
407
409
|
a = p.parse_args(argv)
|
|
@@ -410,7 +412,7 @@ def cmd_pta(argv):
|
|
|
410
412
|
e = engine.Engine(models_path=a.models, results_dir=a.results_dir)
|
|
411
413
|
labels = [x.strip() for x in a.only.split(",")] if a.only else None
|
|
412
414
|
packs = [x.strip() for x in a.packs.split(",")] if a.packs else None
|
|
413
|
-
result, models = pta.for_engine(e, labels, packs)
|
|
415
|
+
result, models = pta.for_engine(e, labels, packs, sort=a.sort)
|
|
414
416
|
out = Console(highlight=False)
|
|
415
417
|
if not models:
|
|
416
418
|
sys.exit("no finished results to compare" + (" for those models or packs" if labels or packs else ""))
|
|
@@ -2892,6 +2892,10 @@ class ResultsScreen(Screen):
|
|
|
2892
2892
|
def action_runs(self):
|
|
2893
2893
|
self.app.open_sessions()
|
|
2894
2894
|
|
|
2895
|
+
FAIL_KINDS = [("Every kind", ""), ("Near misses", "near miss"), ("Partly right", "partly right"),
|
|
2896
|
+
("Wrong", "wrong"), ("Cut off", "cut off"), ("No answer / format", "no answer / format"),
|
|
2897
|
+
("Code crashed or hung", "code crashed or hung"), ("Critical", "critical")]
|
|
2898
|
+
|
|
2895
2899
|
READINESS_HELP = ("[dim]PASS needs a Certify run with zero critical failures over enough trials and an "
|
|
2896
2900
|
"accuracy lower bound that clears the pack's gate (pack.toml). Screening can only FAIL "
|
|
2897
2901
|
"or look promising.[/dim]")
|
|
@@ -2913,6 +2917,8 @@ class ResultsScreen(Screen):
|
|
|
2913
2917
|
with TabPane("PTA index", id="tab-pta"):
|
|
2914
2918
|
with Horizontal(id="pta-controls"):
|
|
2915
2919
|
yield Select([("All packs", "")], id="pta-pack", allow_blank=False, value="")
|
|
2920
|
+
yield Select([("By accuracy", "accuracy"), ("By score", "score")], id="pta-sort",
|
|
2921
|
+
allow_blank=False, value="accuracy")
|
|
2916
2922
|
yield Button("Models: all", id="pta-models")
|
|
2917
2923
|
yield Static("[dim]P parsimony (tokens): tokens used, reasoning and answer. T time: total time. "
|
|
2918
2924
|
"A accuracy: answers right. Verdicts and critical failures are in Production "
|
|
@@ -2954,6 +2960,7 @@ class ResultsScreen(Screen):
|
|
|
2954
2960
|
with TabPane("Failures", id="tab-failures"):
|
|
2955
2961
|
with Horizontal(id="fail-controls"):
|
|
2956
2962
|
yield Button("Models: all", id="fail-models")
|
|
2963
|
+
yield Select(self.FAIL_KINDS, id="fail-kind", allow_blank=False, value="")
|
|
2957
2964
|
yield Static(id="fail-count")
|
|
2958
2965
|
yield Static("[dim]Enter on a row shows the whole answer, its reasoning and what was checked.[/dim]")
|
|
2959
2966
|
yield DataTable(id="failures", zebra_stripes=True, cursor_type="row")
|
|
@@ -2963,6 +2970,9 @@ class ResultsScreen(Screen):
|
|
|
2963
2970
|
self.rows, self.infos, self.partial_rows, self.superseded = [], [], [], set()
|
|
2964
2971
|
if not hasattr(self.app, "pq_models"):
|
|
2965
2972
|
self.app.pq_models = set()
|
|
2973
|
+
if getattr(self.app, "pack_pick", None): # the pack lists open on All packs again
|
|
2974
|
+
self.app.pq_models = self.app.pack_pick["before"]
|
|
2975
|
+
self.app.pack_pick = None
|
|
2966
2976
|
if not hasattr(self.app, "result_sorts"):
|
|
2967
2977
|
self.app.result_sorts = {} # table id -> (column name, descending), kept for this session
|
|
2968
2978
|
self.fill()
|
|
@@ -3042,24 +3052,40 @@ class ResultsScreen(Screen):
|
|
|
3042
3052
|
self.sort_tables()
|
|
3043
3053
|
|
|
3044
3054
|
def fill_failures(self):
|
|
3045
|
-
"""Every wrong answer, of the models picked (Models: shared with the other tabs)
|
|
3055
|
+
"""Every wrong answer, of the models picked (Models: shared with the other tabs), of the kind picked
|
|
3056
|
+
(insights.kind: near miss, partly right, wrong, cut off, …)."""
|
|
3057
|
+
from . import insights
|
|
3046
3058
|
f = self.query_one("#failures", DataTable)
|
|
3047
3059
|
f.clear(columns=True)
|
|
3048
|
-
f.add_columns("Model", "Level", "Test", "Answered", "Reason")
|
|
3060
|
+
f.add_columns("Model", "Level", "Test", "Answered", "Kind", "Reason")
|
|
3049
3061
|
every = {r["model"] for r in self.rows}
|
|
3050
3062
|
chosen = {m for m in self.app.pq_models if m in every}
|
|
3051
|
-
|
|
3063
|
+
wanted = self.query_one("#fail-kind", Select).value or ""
|
|
3064
|
+
rows = [r for r in self.rows if (not chosen or r["model"] in chosen)
|
|
3065
|
+
and (r["ok"] or not wanted or insights.kind(r) == wanted)]
|
|
3052
3066
|
# compare.failures lists the failed rows in order, so they line up with fail_rows
|
|
3053
3067
|
self.fail_rows = [r for r in rows if not r["ok"]]
|
|
3068
|
+
colors = {k: c for k, c, _ in insights.KINDS}
|
|
3054
3069
|
for i, (model, test, reason, level) in enumerate(compare.failures(rows)):
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3070
|
+
r = self.fail_rows[i]
|
|
3071
|
+
when = dates.day(dates.answered_at(r.get("raw") or {}))
|
|
3072
|
+
k = insights.kind(r)
|
|
3073
|
+
checks = compare.checks_passed(r)
|
|
3074
|
+
label = f"{k} {checks[0]}/{checks[1]}" if checks and k in ("near miss", "partly right", "wrong") else k
|
|
3075
|
+
f.add_row(model, diff_badge(level), rich_escape(test), when, f"[{colors[k]}]{label}[/]",
|
|
3076
|
+
rich_escape(reason[:130]), key=str(i))
|
|
3077
|
+
self.query_one("#fail-models", Button).label = self.models_label(chosen)
|
|
3058
3078
|
n = len(self.fail_rows)
|
|
3059
3079
|
self.query_one("#fail-count", Static).update(
|
|
3060
|
-
f"{n}
|
|
3080
|
+
f"{n} {'' if not wanted else wanted + ' '}failure{'s' * (n != 1)}"
|
|
3081
|
+
+ (f" of {len(chosen)} model{'s' * (len(chosen) != 1)}" if chosen else ""))
|
|
3061
3082
|
self.sort_table(f)
|
|
3062
3083
|
|
|
3084
|
+
@on(Select.Changed, "#fail-kind")
|
|
3085
|
+
def fail_kind_changed(self):
|
|
3086
|
+
if getattr(self, "rows", None) is not None:
|
|
3087
|
+
self.fill_failures()
|
|
3088
|
+
|
|
3063
3089
|
@on(DataTable.RowSelected, "#failures")
|
|
3064
3090
|
def failure_selected(self, event):
|
|
3065
3091
|
# in the order shown, so [ and ] step through them as sorted
|
|
@@ -3072,7 +3098,6 @@ class ResultsScreen(Screen):
|
|
|
3072
3098
|
self.app.push_screen(AnswerScreen(keep, keep.index(entries[i]), what="failure"))
|
|
3073
3099
|
|
|
3074
3100
|
# -- per question: tokens and time for every model on the same question
|
|
3075
|
-
@on(Select.Changed, "#pq-pack")
|
|
3076
3101
|
@on(Select.Changed, "#pq-level")
|
|
3077
3102
|
@on(Checkbox.Changed, "#pq-partial")
|
|
3078
3103
|
@on(Checkbox.Changed, "#pq-shared")
|
|
@@ -3214,18 +3239,62 @@ class ResultsScreen(Screen):
|
|
|
3214
3239
|
finally:
|
|
3215
3240
|
self.filling_insights = False
|
|
3216
3241
|
chosen = {m for m in self.app.pq_models if m in every}
|
|
3217
|
-
self.query_one("#ins-models", Button).label =
|
|
3242
|
+
self.query_one("#ins-models", Button).label = self.models_label(chosen)
|
|
3218
3243
|
main = os.path.abspath(self.results_dir or e.results_dir) == os.path.abspath(e.results_dir)
|
|
3219
3244
|
bases = {m["label"]: m["base"] for m in e.cfg["models"] if m.get("base")}
|
|
3220
3245
|
by = self.query_one("#ins-cost", Select).value or "tokens"
|
|
3221
3246
|
out = insights.panels(rows, models, e.packs, ref, bases, by, engine=e if main else None)
|
|
3222
3247
|
body.update("\n\n".join(insights.render(p, i) for i, p in enumerate(out, 1)))
|
|
3223
3248
|
|
|
3224
|
-
@on(Select.Changed, "#pta-
|
|
3225
|
-
def
|
|
3249
|
+
@on(Select.Changed, "#pta-sort")
|
|
3250
|
+
def pta_sort_changed(self):
|
|
3251
|
+
"""The Sort dropdown orders the bars and the table, taking over from a column sorted by a click."""
|
|
3252
|
+
self.app.result_sorts.pop("pta", None)
|
|
3226
3253
|
if getattr(self, "rows", None) is not None:
|
|
3227
3254
|
self.fill_pta()
|
|
3228
3255
|
|
|
3256
|
+
@on(Select.Changed, "#pta-pack")
|
|
3257
|
+
@on(Select.Changed, "#pq-pack")
|
|
3258
|
+
def pack_changed(self, event):
|
|
3259
|
+
"""A pack picked (PTA index or Per question, kept the same in both): compare the models that
|
|
3260
|
+
answered every question of it (the Models button can change that); All packs: back to the
|
|
3261
|
+
models picked before."""
|
|
3262
|
+
if getattr(self, "rows", None) is None:
|
|
3263
|
+
return
|
|
3264
|
+
e, pack = self.app.engine, event.value or ""
|
|
3265
|
+
for other, values in (("#pta-pack", "pta_pack_values"), ("#pq-pack", "pq_pack_values")):
|
|
3266
|
+
sel = self.query_one(other, Select)
|
|
3267
|
+
if sel is not event.select and sel.value != pack and pack in getattr(self, values, [""]):
|
|
3268
|
+
with sel.prevent(Select.Changed):
|
|
3269
|
+
sel.value = pack
|
|
3270
|
+
shared = self.query_one("#pq-shared", Checkbox)
|
|
3271
|
+
p = self.app.pack_pick or {"before": set(self.app.pq_models), "shared": shared.value}
|
|
3272
|
+
before = p["before"]
|
|
3273
|
+
if not pack:
|
|
3274
|
+
self.app.pack_pick = None
|
|
3275
|
+
with shared.prevent(Checkbox.Changed): # set_models may have ticked it
|
|
3276
|
+
shared.value = p["shared"]
|
|
3277
|
+
self.set_models(before)
|
|
3278
|
+
return
|
|
3279
|
+
rows = [r for r in self.rows if r.get("path") not in self.superseded]
|
|
3280
|
+
complete = set(compare.complete_models(rows, pack, [t["id"] for t in e.packs[pack].tests]
|
|
3281
|
+
if pack in e.packs else None))
|
|
3282
|
+
self.app.pack_pick = {"pack": pack, "models": complete, "before": before, "shared": p["shared"]}
|
|
3283
|
+
if not complete:
|
|
3284
|
+
self.notify(f"No model has answered every question of {self.pack_label(pack)} yet: "
|
|
3285
|
+
"the models picked before are kept.", severity="warning", timeout=8)
|
|
3286
|
+
self.set_models(complete or before)
|
|
3287
|
+
|
|
3288
|
+
def pack_label(self, pack):
|
|
3289
|
+
return self.app.engine.packs[pack].label if pack in self.app.engine.packs else pack
|
|
3290
|
+
|
|
3291
|
+
def models_label(self, chosen):
|
|
3292
|
+
"""The Models buttons' text: the models a pack pick chose say so."""
|
|
3293
|
+
p = self.app.pack_pick
|
|
3294
|
+
if chosen and p and chosen == p["models"]:
|
|
3295
|
+
return f"Models: {len(chosen)} complete on {self.pack_label(p['pack'])}"
|
|
3296
|
+
return f"Models: {len(chosen)} picked" if chosen else "Models: all"
|
|
3297
|
+
|
|
3229
3298
|
def fill_pta(self):
|
|
3230
3299
|
"""The PTA index (pta.py) of the picked models (Models button, shared with Per question) on
|
|
3231
3300
|
the finished results, over the questions all of them answered."""
|
|
@@ -3245,7 +3314,7 @@ class ResultsScreen(Screen):
|
|
|
3245
3314
|
rows = [r for r in rows if r["suite"] == sel.value]
|
|
3246
3315
|
present = {r["model"] for r in rows}
|
|
3247
3316
|
chosen = {m for m in self.app.pq_models if m in present}
|
|
3248
|
-
self.query_one("#pta-models", Button).label =
|
|
3317
|
+
self.query_one("#pta-models", Button).label = self.models_label(chosen)
|
|
3249
3318
|
t = self.query_one("#pta", DataTable)
|
|
3250
3319
|
t.clear(columns=True)
|
|
3251
3320
|
if not present:
|
|
@@ -3254,7 +3323,8 @@ class ResultsScreen(Screen):
|
|
|
3254
3323
|
self.query_one("#pta-left-out", Static).update("")
|
|
3255
3324
|
return
|
|
3256
3325
|
# none picked: every model with enough answers (pta.index leaves out the rest, and says so)
|
|
3257
|
-
result = pta.index(rows, sorted(chosen) or None, pta.memory_gb(self.infos)
|
|
3326
|
+
result = pta.index(rows, sorted(chosen) or None, pta.memory_gb(self.infos),
|
|
3327
|
+
self.query_one("#pta-sort", Select).value or "accuracy")
|
|
3258
3328
|
self.query_one("#pta-note", Static).update(pta.scope_note(result, sorted(present)))
|
|
3259
3329
|
self.pta_result = result
|
|
3260
3330
|
removed = [m for m in self.pta_removed_now() if m in present]
|
|
@@ -3300,8 +3370,7 @@ class ResultsScreen(Screen):
|
|
|
3300
3370
|
all_tests = compare.per_question(rows)
|
|
3301
3371
|
self.pq_all_tests = all_tests
|
|
3302
3372
|
chosen = {m for m in self.app.pq_models if any(m in t["cells"] for t in all_tests)}
|
|
3303
|
-
self.query_one("#pq-models", Button).label = (
|
|
3304
|
-
f"Models: {len(chosen)} picked" if chosen else "Models: all")
|
|
3373
|
+
self.query_one("#pq-models", Button).label = self.models_label(chosen)
|
|
3305
3374
|
tests = []
|
|
3306
3375
|
for t in all_tests:
|
|
3307
3376
|
if (pack and t["suite"] != pack) or (level and t["difficulty"] != level):
|
|
@@ -3401,7 +3470,8 @@ class ResultsScreen(Screen):
|
|
|
3401
3470
|
tt = r["total_secs"]
|
|
3402
3471
|
cells.append([r["model"],
|
|
3403
3472
|
hi(tt, ("-" if not tt else f"{tt:.1f}s" if tt < 60 else fmt_secs(tt)), "total_secs", "min"),
|
|
3404
|
-
hi(r["passed"], f"{r['passed']}/{r['n']}", "passed", "max")
|
|
3473
|
+
hi(r["passed"], f"{r['passed']}/{r['n']}", "passed", "max")
|
|
3474
|
+
+ (f" [dim]{r['score']:.0%}[/dim]" if r["passed"] < r["n"] and r["score"] else ""),
|
|
3405
3475
|
hi(r["med_tokens"], f"{r['med_tokens']:,.0f}" if r["med_tokens"] else "-", "med_tokens", "min"),
|
|
3406
3476
|
hi(r["total_tokens"], f"{r['total_tokens']:,.0f}", "total_tokens", "min"),
|
|
3407
3477
|
hi(r["tokens_per_pass"], f"{r['tokens_per_pass']:,.0f}" if r["tokens_per_pass"] else "-",
|
|
@@ -3431,7 +3501,7 @@ class ResultsScreen(Screen):
|
|
|
3431
3501
|
"shared the machine's speed: compare their times with care.[/yellow]")
|
|
3432
3502
|
title.update(line + " [dim]Time / Tokens: each question's median, added up. Med tok / Med s: median "
|
|
3433
3503
|
"per answer. Tok/✓: tokens per answer right. Prompt t/s / Gen t/s: all tokens over all the time "
|
|
3434
|
-
"they took (cached prompts left out). Fewest / Fastest: questions won; ties count for both. "
|
|
3504
|
+
"they took (cached prompts left out). Passed: questions right on every repeat, then the score (partial credit, e.g. checks passed) when some failed. Fewest / Fastest: questions won; ties count for both. "
|
|
3435
3505
|
"Cut off: answers that hit max_tokens.[/dim]")
|
|
3436
3506
|
|
|
3437
3507
|
@staticmethod
|
|
@@ -3440,6 +3510,9 @@ class ResultsScreen(Screen):
|
|
|
3440
3510
|
else f"[yellow]{c['passed']}/{c['n']}[/yellow]")
|
|
3441
3511
|
if c["n"] > 1 and c["passed"] in (0, c["n"]):
|
|
3442
3512
|
mark += f"[dim]{c['n']}[/dim]"
|
|
3513
|
+
if c["passed"] < c["n"] and c.get("score"): # partial credit: how close it came
|
|
3514
|
+
checks = compare.checks_passed(c["rows"][0]) if c["n"] == 1 else None
|
|
3515
|
+
mark += f" [yellow]{checks[0]}/{checks[1]}[/yellow]" if checks else f" [yellow]{c['score']:.0%}[/yellow]"
|
|
3443
3516
|
tok = c["tokens"]
|
|
3444
3517
|
tok_s = "-" if not tok else (f"{tok / 1000:.1f}k" if tok >= 1000 else f"{tok:.0f}")
|
|
3445
3518
|
if tok and best and tok == best and c["passed"]:
|
|
@@ -3586,6 +3659,7 @@ class EvalsApp(App):
|
|
|
3586
3659
|
#ins-controls #ins-cost { width: 20; }
|
|
3587
3660
|
#ins-body { height: 1fr; }
|
|
3588
3661
|
#pta-controls Select { width: 28; margin-right: 1; }
|
|
3662
|
+
#pta-controls #pta-sort { width: 18; }
|
|
3589
3663
|
#pta-bars { height: auto; padding: 1 0 0 0; }
|
|
3590
3664
|
#pta { width: 1fr; height: auto; margin-top: 1; }
|
|
3591
3665
|
#pta-left-out { height: auto; padding: 1 0 0 0; }
|
|
@@ -3594,6 +3668,7 @@ class EvalsApp(App):
|
|
|
3594
3668
|
#pq-models { margin-right: 1; min-width: 20; }
|
|
3595
3669
|
#fail-controls { height: 3; }
|
|
3596
3670
|
#fail-models { margin-right: 1; min-width: 20; }
|
|
3671
|
+
#fail-kind { width: 28; margin-right: 1; }
|
|
3597
3672
|
#fail-count { width: auto; padding: 1 1; }
|
|
3598
3673
|
#pq-compare-title { padding: 0 1; height: auto; }
|
|
3599
3674
|
#pq-summary { height: auto; } /* every picked model, no scrolling: the tab scrolls instead */
|
|
@@ -1618,8 +1618,8 @@ class PTAIndex(unittest.TestCase):
|
|
|
1618
1618
|
self.assertEqual(bars["plain"][-2:], ["██████░░░░", "60.0%"]) # none truncated: nothing beside it
|
|
1619
1619
|
self.assertIn("[#ff8c00]██[/]", pta.bars(res, 10)[2] + pta.bars(res, 10)[3])
|
|
1620
1620
|
header, table = pta.table(res)
|
|
1621
|
-
self.assertEqual(header[4], "truncated")
|
|
1622
|
-
self.assertEqual({r[0]: r[
|
|
1621
|
+
self.assertEqual(header[4:6], ["score", "truncated"])
|
|
1622
|
+
self.assertEqual({r[0]: r[5] for r in table}, {"thinker": "2/10", "plain": "0/10"})
|
|
1623
1623
|
self.assertIn("truncated", pta.scope_note(res, ["thinker", "plain"]))
|
|
1624
1624
|
self.assertNotIn("truncated", pta.scope_note(pta.index(rows[10:]), ["plain"]))
|
|
1625
1625
|
|
|
@@ -1656,6 +1656,159 @@ class PTAIndex(unittest.TestCase):
|
|
|
1656
1656
|
self.assertEqual(sorted(cells, key=sort_key), ["10/20", "3/4", "4/4", "abc", "-"])
|
|
1657
1657
|
self.assertEqual(sorted(cells, key=lambda c: sort_key(c, True)), ["4/4", "3/4", "10/20", "abc", "-"])
|
|
1658
1658
|
|
|
1659
|
+
def test_partial_credit_score(self):
|
|
1660
|
+
from tuieval import compare, insights, pta
|
|
1661
|
+
|
|
1662
|
+
def row(model, test, ok, score, reason="Failed", **kw):
|
|
1663
|
+
return {"model": model, "suite": "p", "test": f"p: {test}", "ok": ok, "score": score, "latency": 1000,
|
|
1664
|
+
"tokens": 10, "truncated": False, "gen_tps": None, "repeat": 0, "reason": reason, **kw}
|
|
1665
|
+
rows = ([row("close", f"q{i}", False, 5 / 6, "Failed 1/6 checks: x") for i in range(2)]
|
|
1666
|
+
+ [row("far", f"q{i}", False, 0.0, "Failed 6/6 checks: x") for i in range(2)]
|
|
1667
|
+
+ [row("half", "q0", True, 1.0, "ok"), row("half", "q1", False, 0.0, "Failed 6/6 checks: x")])
|
|
1668
|
+
res = pta.index(rows)
|
|
1669
|
+
self.assertEqual([(x["model"], x["A"], round(x["S"])) for x in res["models"]],
|
|
1670
|
+
[("half", 50, 50), ("close", 0, 83), ("far", 0, 0)]) # accuracy first, then score
|
|
1671
|
+
self.assertEqual([x["model"] for x in pta.index(rows, sort="score")["models"]], ["close", "half", "far"])
|
|
1672
|
+
bars = {l.split()[0]: l for l in pta.bars(res, 10, markup=False)[2:]}
|
|
1673
|
+
self.assertIn("0% score 83.3%", bars["close"])
|
|
1674
|
+
self.assertNotIn("score", bars["half"] + bars["far"]) # shown only when it differs
|
|
1675
|
+
header, table = pta.table(res)
|
|
1676
|
+
self.assertEqual({r[0]: r[header.index("score")] for r in table}, {"half": "50.0%", "close": "83.3%", "far": "0%"})
|
|
1677
|
+
self.assertEqual([compare.checks_passed(r) for r in (rows[0], rows[2], rows[4])], [(5, 6), (0, 6), None])
|
|
1678
|
+
self.assertEqual(compare.checks_passed({"reason": "x", "raw": {"checks": {"a": None, "b": "err"}}}), (1, 2))
|
|
1679
|
+
self.assertEqual([insights.kind(r) for r in (rows[0], rows[2], rows[4])], ["near miss", "wrong", "right"])
|
|
1680
|
+
self.assertEqual(insights.kind(row("m", "q", False, 0.5)), "partly right")
|
|
1681
|
+
self.assertEqual(insights.kind(row("m", "q", False, 0.0, "time limit: timed out after 600 s in all")), "cut off")
|
|
1682
|
+
self.assertEqual(insights.kind(row("m", "q", False, 0.9, severity="critical")), "critical")
|
|
1683
|
+
p = insights.failure_mix(rows, ["close", "far", "half"])
|
|
1684
|
+
self.assertIn("0% right (2 answers; 2 near miss)", re.sub(r"\[[^\]]*\]", "", "\n".join(p["lines"])))
|
|
1685
|
+
|
|
1686
|
+
def test_results_show_partial_credit(self):
|
|
1687
|
+
code = textwrap.dedent("""
|
|
1688
|
+
import asyncio
|
|
1689
|
+
from tuieval.tui import EvalsApp, ResultsScreen
|
|
1690
|
+
from textual.widgets import DataTable, Select, Static, TabbedContent
|
|
1691
|
+
def row(model, test, ok, score, reason):
|
|
1692
|
+
return {"model": model, "suite": "p", "test": f"p: {test}", "name": f"p: {test}", "ok": ok,
|
|
1693
|
+
"score": score, "reason": reason, "error": False, "truncated": False, "output": "",
|
|
1694
|
+
"tokens": 10, "latency": 1000, "gen_tps": None, "prompt_tps": None, "ttft": None,
|
|
1695
|
+
"repeat": 0, "difficulty": "hard", "raw": {}, "path": "x", "alongside": []}
|
|
1696
|
+
rows = ([row("close", f"q{i}", False, 5 / 6, "Failed 1/6 checks: x") for i in range(2)]
|
|
1697
|
+
+ [row("half", "q0", True, 1.0, "ok"), row("half", "q1", False, 0.0, "Failed 6/6 checks: x")])
|
|
1698
|
+
async def go():
|
|
1699
|
+
app = EvalsApp({})
|
|
1700
|
+
async with app.run_test(size=(180, 50)) as pilot:
|
|
1701
|
+
await pilot.pause()
|
|
1702
|
+
app.push_screen(ResultsScreen())
|
|
1703
|
+
await pilot.pause(0.5)
|
|
1704
|
+
s = app.screen
|
|
1705
|
+
s.rows, s.infos = rows, []
|
|
1706
|
+
s.fill_pta()
|
|
1707
|
+
await pilot.pause(0.2)
|
|
1708
|
+
first = s.pta_result["models"][0]["model"]
|
|
1709
|
+
s.query_one("#pta-sort", Select).value = "score"
|
|
1710
|
+
await pilot.pause(0.3)
|
|
1711
|
+
print(first, s.pta_result["models"][0]["model"])
|
|
1712
|
+
s.fill_failures()
|
|
1713
|
+
t = s.query_one("#failures", DataTable)
|
|
1714
|
+
print(sorted(str(t.get_row_at(i)[4]) for i in range(t.row_count)))
|
|
1715
|
+
s.query_one("#fail-kind", Select).value = "near miss"
|
|
1716
|
+
await pilot.pause(0.3)
|
|
1717
|
+
print(t.row_count, str(s.query_one("#fail-count", Static).render()))
|
|
1718
|
+
s.fill_per_question()
|
|
1719
|
+
pq = s.query_one("#per-question", DataTable)
|
|
1720
|
+
cols = [str(c.label) for c in pq.ordered_columns]
|
|
1721
|
+
print(sorted(str(pq.get_row_at(i)[cols.index("close")]).split()[1] for i in range(pq.row_count)))
|
|
1722
|
+
asyncio.run(go())
|
|
1723
|
+
""")
|
|
1724
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1725
|
+
ws = os.path.join(tmp, "ws")
|
|
1726
|
+
tuieval(tmp, "init", ws)
|
|
1727
|
+
out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True,
|
|
1728
|
+
env=dict(os.environ, TUIEVAL_HOME=ws, TUIEVAL_DETECT_PORTS=""), timeout=60)
|
|
1729
|
+
self.assertEqual(out.stdout.strip().splitlines(), [
|
|
1730
|
+
"half close", # by accuracy, then by score
|
|
1731
|
+
"['[#87af5f]near miss 5/6[/]', '[#87af5f]near miss 5/6[/]', '[red]wrong 0/6[/]']",
|
|
1732
|
+
"2 2 near miss failures",
|
|
1733
|
+
"['5/6', '5/6']"], out.stderr[-2000:])
|
|
1734
|
+
|
|
1735
|
+
def test_complete_models(self):
|
|
1736
|
+
from tuieval import compare
|
|
1737
|
+
rows = [{"model": m, "suite": s, "test": f"{s}: {t}", **({"partial": True} if m == "running" else {})}
|
|
1738
|
+
for m, s, ts in (("full", "p", "abc"), ("half", "p", "ab"), ("running", "p", "abc"), ("full", "o", "a"))
|
|
1739
|
+
for t in ts]
|
|
1740
|
+
self.assertEqual(compare.complete_models(rows, "p", ["a", "b", "c"]), ["full"]) # half misses c; running is ◐
|
|
1741
|
+
self.assertEqual(compare.complete_models(rows, "p", ["a", "b"]), ["full", "half"])
|
|
1742
|
+
self.assertEqual(compare.complete_models(rows, "p", ["a", "b", "c", "d"]), []) # a question added since
|
|
1743
|
+
self.assertEqual(compare.complete_models(rows, "p"), ["full"]) # pack gone: questions seen
|
|
1744
|
+
self.assertEqual(compare.complete_models(rows, "x", []), [])
|
|
1745
|
+
|
|
1746
|
+
def test_results_pack_picks_the_models_that_answered_all_of_it(self):
|
|
1747
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1748
|
+
ws = os.path.join(tmp, "ws")
|
|
1749
|
+
tuieval(tmp, "init", ws)
|
|
1750
|
+
for name, ids in (("apps", "abcd"), ("other", "ab")):
|
|
1751
|
+
d = os.path.join(ws, "packs", name)
|
|
1752
|
+
os.makedirs(d)
|
|
1753
|
+
write(os.path.join(d, "pack.toml"), f'label = "{name.title()}"\n[certify]\nrepeat = 1\n')
|
|
1754
|
+
write(os.path.join(d, "tests.yaml"), "".join(
|
|
1755
|
+
f"- {{id: {t}, input: question {t}, expected: 1, reference: 'ANSWER: 1', wrong: ['ANSWER: 2'], "
|
|
1756
|
+
"difficulty: easy}\n" for t in ids))
|
|
1757
|
+
mock = Mock(os.path.join(ws, "packs"), "oracle")
|
|
1758
|
+
try:
|
|
1759
|
+
with open(os.path.join(ws, "models.toml"), "a") as f:
|
|
1760
|
+
f.write(f'\n[servers.here]\nurl = "http://127.0.0.1:{mock.port}"\n')
|
|
1761
|
+
for label in ("full", "half", "solo"):
|
|
1762
|
+
tuieval(ws, "add", "mock", "--server", "here", "--label", label)
|
|
1763
|
+
tuieval(ws, "run", "--tier", "certify", "--only", "full")
|
|
1764
|
+
tuieval(ws, "run", "--tier", "certify", "--only", "half", "--packs", "apps", "--tests", "a,b")
|
|
1765
|
+
tuieval(ws, "run", "--tier", "certify", "--only", "solo", "--packs", "other")
|
|
1766
|
+
finally:
|
|
1767
|
+
mock.stop()
|
|
1768
|
+
code = textwrap.dedent("""
|
|
1769
|
+
import asyncio
|
|
1770
|
+
from tuieval.tui import EvalsApp, ResultsScreen
|
|
1771
|
+
from textual.widgets import Button, DataTable, Select
|
|
1772
|
+
async def go():
|
|
1773
|
+
app = EvalsApp({})
|
|
1774
|
+
async with app.run_test(size=(180, 50)) as pilot:
|
|
1775
|
+
await pilot.pause()
|
|
1776
|
+
app.pq_models = {"half"} # picked by hand before
|
|
1777
|
+
app.push_screen(ResultsScreen())
|
|
1778
|
+
await pilot.pause(0.5)
|
|
1779
|
+
s = app.screen
|
|
1780
|
+
def state():
|
|
1781
|
+
pq = s.query_one("#per-question", DataTable)
|
|
1782
|
+
cols = [str(c.label) for c in pq.ordered_columns][len(s.PQ_FIXED):]
|
|
1783
|
+
return (sorted(app.pq_models), s.query_one("#pta-pack", Select).value,
|
|
1784
|
+
str(s.query_one("#pq-models", Button).label), cols, s.query_one("#pq-shared").value)
|
|
1785
|
+
s.query_one("#pq-pack", Select).value = "apps"
|
|
1786
|
+
await pilot.pause(0.3)
|
|
1787
|
+
print(state())
|
|
1788
|
+
s.query_one("#pta-pack", Select).value = "other"
|
|
1789
|
+
await pilot.pause(0.3)
|
|
1790
|
+
print(state())
|
|
1791
|
+
s.query_one("#pta-pack", Select).value = ""
|
|
1792
|
+
await pilot.pause(0.3)
|
|
1793
|
+
print(state())
|
|
1794
|
+
app.pq_models = set() # none picked: All packs gives every model again
|
|
1795
|
+
s.set_models(set())
|
|
1796
|
+
await pilot.pause(0.3)
|
|
1797
|
+
for pack in ("apps", ""):
|
|
1798
|
+
s.query_one("#pq-pack", Select).value = pack
|
|
1799
|
+
await pilot.pause(0.3)
|
|
1800
|
+
print(state())
|
|
1801
|
+
asyncio.run(go())
|
|
1802
|
+
""")
|
|
1803
|
+
out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True,
|
|
1804
|
+
env=dict(os.environ, TUIEVAL_HOME=ws, TUIEVAL_DETECT_PORTS=""), timeout=60)
|
|
1805
|
+
self.assertEqual(out.stdout.strip().splitlines(), [
|
|
1806
|
+
"(['full'], 'apps', 'Models: 1 complete on Apps', ['full'], False)",
|
|
1807
|
+
"(['full', 'solo'], 'other', 'Models: 2 complete on Other', ['full', 'solo'], False)",
|
|
1808
|
+
"(['half'], '', 'Models: 1 picked', ['half'], False)",
|
|
1809
|
+
"(['full'], 'apps', 'Models: 1 complete on Apps', ['full'], True)", # none picked before: ticked
|
|
1810
|
+
"([], '', 'Models: all', ['full', 'half', 'solo'], False)"], out.stderr[-2000:])
|
|
1811
|
+
|
|
1659
1812
|
def test_cli_report_and_tui(self):
|
|
1660
1813
|
with tempfile.TemporaryDirectory() as tmp:
|
|
1661
1814
|
ws = os.path.join(tmp, "ws")
|
|
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.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/tool_call/pack.toml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev127 → tuieval-0.2.0.dev131}/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
|