tuieval 0.2.0.dev65__tar.gz → 0.2.0.dev68__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.dev65 → tuieval-0.2.0.dev68}/CHANGELOG.md +2 -1
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/PKG-INFO +2 -2
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/README.md +1 -1
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/models.md +2 -2
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/pta.py +41 -21
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/tui.py +134 -10
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/tests/test_tuieval.py +54 -5
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/.gitignore +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/LICENSE +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/compare.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/engine.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/run_evals.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/tests/mock_server.py +0 -0
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Results tables sort by a click on a column name; a second click reverses. Numbers, times and levels sort by value; empty cells stay last.
|
|
5
6
|
- Nvidia and AMD GPUs on Linux: VRAM from `nvidia-smi`, `amd-smi` or `rocm-smi` sizes models (`gpu_memory_gb` overrides it), runs record peak VRAM, and `tuieval doctor` shows the GPU.
|
|
6
7
|
- Results has four tabs: PTA index, Production readiness, Per question and Failures. The PTA table adds tok/s, TTFT and memory; Per question can show only where models disagree; the settings warning is one line. Scorecard, pairwise, history and test quality stay in `tuieval compare`, `history` and `items`.
|
|
7
8
|
- The run's ETA adds up each pack's answers left × that model's time per answer there (its history, then its own times once it has 5 answers), instead of extrapolating from the first few answers.
|
|
8
9
|
- Faster start on Macs: the GPU core count comes from `ioreg` instead of `system_profiler`, which took seconds on some Macs.
|
|
9
10
|
- A model a server won't serve (HTTP 401-404, e.g. OpenRouter's "No endpoints found") is never judged on it: those answers don't count, and the model stops at once instead of failing every question.
|
|
10
|
-
- PTA index: parsimony (tokens), time and accuracy per model as bars, with tokens and time shown as multiples of the best (2.4×): the first Results tab (where `r` lands), `tuieval pta` and the report. Server errors aren't answers, and models with few answers are left out unless picked.
|
|
11
|
+
- PTA index: parsimony (tokens), time and accuracy per model as bars (tokens and time as long as they took, shorter is better; accuracy longer is better), with tokens and time also shown as multiples of the best (2.4×): the first Results tab (where `r` lands), `tuieval pta` and the report. Server errors aren't answers, and models with few answers are left out unless picked.
|
|
11
12
|
- Run several models at a time: `parallel_models` per machine, **Models at a time** in the TUI, or `tuieval run --parallel N` (default 1). Models wait for memory to fit, and answers note what ran alongside them.
|
|
12
13
|
- More evals during a run: `n` opens Setup, and Start adds them to the run going (same tier) or queues them after it.
|
|
13
14
|
- A new workspace's `models.toml` is 23 lines: the llama, local and OpenRouter servers and the defaults are built in (docs/models.md lists them in full; a server defined in `models.toml` replaces the built-in one with that name). Existing workspaces keep their settings; built-in servers they don't define become available, and adding a model still prefers the workspace's own servers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev68
|
|
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
|
|
@@ -144,7 +144,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
|
|
|
144
144
|
1. **Pick packs and models** (space ticks; type to filter models by label or tag). Each model shows a short code per use case, e.g. `C✓ S?` (✓ pass, ✗ fail, ? inconclusive; grey = from earlier results). To run only some tests of a pack, highlight it and press `e`. Runs of different tests add up, and the pack can PASS once all its tests have run.
|
|
145
145
|
2. **Pick a tier** (Smoke to check setup, Screen, Certify) and **press `s`**. The line above the buttons shows how many answers that is and roughly how long it will take. For each model, tuieval starts its server (or uses a running one), checks the right model is loaded, runs every selected pack, then stops it.
|
|
146
146
|
3. **Watch the run:** progress with ETA, live score, tok/s, TTFT and memory, the current test with reasoning and answer side by side, recent results with the grader's reason. `k` skips a model, `c` cancels (finished work is kept and resumes next time). `n` sets up more evals while it runs: add them to this run (same tier) or queue them as the next run.
|
|
147
|
-
4. **Press `r` for results:** the PTA index (parsimony (tokens), time and accuracy as bars), production readiness, per question, and failures. `tuieval compare`, `tuieval history` and `tuieval items` have the rest: whether a difference is real, verdict history and test quality.
|
|
147
|
+
4. **Press `r` for results:** the PTA index (parsimony (tokens), time and accuracy as bars), production readiness, per question, and failures; click a column name to sort. `tuieval compare`, `tuieval history` and `tuieval items` have the rest: whether a difference is real, verdict history and test quality.
|
|
148
148
|
|
|
149
149
|
Other keys: `t` tunes the ticked models' speed flags, `a` adds a model, `m` scans for GGUFs, `p` saves or loads a preset, `x` hides a model. `?` explains any screen.
|
|
150
150
|
|
|
@@ -118,7 +118,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
|
|
|
118
118
|
1. **Pick packs and models** (space ticks; type to filter models by label or tag). Each model shows a short code per use case, e.g. `C✓ S?` (✓ pass, ✗ fail, ? inconclusive; grey = from earlier results). To run only some tests of a pack, highlight it and press `e`. Runs of different tests add up, and the pack can PASS once all its tests have run.
|
|
119
119
|
2. **Pick a tier** (Smoke to check setup, Screen, Certify) and **press `s`**. The line above the buttons shows how many answers that is and roughly how long it will take. For each model, tuieval starts its server (or uses a running one), checks the right model is loaded, runs every selected pack, then stops it.
|
|
120
120
|
3. **Watch the run:** progress with ETA, live score, tok/s, TTFT and memory, the current test with reasoning and answer side by side, recent results with the grader's reason. `k` skips a model, `c` cancels (finished work is kept and resumes next time). `n` sets up more evals while it runs: add them to this run (same tier) or queue them as the next run.
|
|
121
|
-
4. **Press `r` for results:** the PTA index (parsimony (tokens), time and accuracy as bars), production readiness, per question, and failures. `tuieval compare`, `tuieval history` and `tuieval items` have the rest: whether a difference is real, verdict history and test quality.
|
|
121
|
+
4. **Press `r` for results:** the PTA index (parsimony (tokens), time and accuracy as bars), production readiness, per question, and failures; click a column name to sort. `tuieval compare`, `tuieval history` and `tuieval items` have the rest: whether a difference is real, verdict history and test quality.
|
|
122
122
|
|
|
123
123
|
Other keys: `t` tunes the ticked models' speed flags, `a` adds a model, `m` scans for GGUFs, `p` saves or loads a preset, `x` hides a model. `?` explains any screen.
|
|
124
124
|
|
|
@@ -98,13 +98,13 @@ By default a run serves one model at a time. On a machine with room for more, se
|
|
|
98
98
|
|
|
99
99
|
## PTA index
|
|
100
100
|
|
|
101
|
-
The **PTA index** puts parsimony (tokens), time and accuracy side by side, as a bar for each per model
|
|
101
|
+
The **PTA index** puts parsimony (tokens), time and accuracy side by side, as a bar for each per model, with a table underneath. It's the first tab in Results, where `r` opens, and it's also `tuieval pta` and a section in `tuieval report`. There's no combined number; verdicts and critical failures stay in Production readiness.
|
|
102
102
|
|
|
103
103
|
- **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.
|
|
104
104
|
- **T time**: the total time to answer those questions, as how many times the fastest model's, e.g. `28×` (★ in **time/answer**).
|
|
105
105
|
- **A accuracy**: the share of answers to those questions that passed.
|
|
106
106
|
|
|
107
|
-
The P and T bars
|
|
107
|
+
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.
|
|
108
108
|
|
|
109
109
|
Models are compared on the questions all of them answered, so one that skipped some never looks faster. Server and connection errors aren't answers: a model with only those is listed as having none. A model with fewer than half as many answers as the most-answered one is left out (and listed), or it would shrink the shared questions for everyone. Pick the models and pack to compare in the TUI, or use `--only` and `--packs`: models you pick are always compared.
|
|
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.dev68'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev68')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""PTA index: parsimony (tokens), time and accuracy per model, a bar each
|
|
1
|
+
"""PTA index: parsimony (tokens), time and accuracy per model, a bar each.
|
|
2
2
|
|
|
3
3
|
P parsimony (tokens) the tokens (reasoning and answer) used on every question compared (each
|
|
4
4
|
question at its median over repeats), shown as how many times the leanest
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
model's ("28×").
|
|
8
8
|
A accuracy the share of answers to those questions that passed (%).
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
The P and T bars are as long as the tokens and time used (the model that used the most fills its bar,
|
|
11
|
+
the others in proportion, at least one block), so shorter is better; the A bar: longer is better.
|
|
12
|
+
P and T also carry a 0-100 score (100 for the best, POINTS_PER_DOUBLING points less for every
|
|
13
|
+
doubling), used to order models with the same accuracy.
|
|
12
14
|
|
|
13
15
|
Models are compared on the questions all of them answered, so one that skipped some never looks
|
|
14
16
|
faster. Server and connection errors aren't answers. By default a model with fewer than half as many
|
|
@@ -93,9 +95,11 @@ def index(rows, models=None, memory=None):
|
|
|
93
95
|
"most": most}
|
|
94
96
|
|
|
95
97
|
|
|
96
|
-
# (letter, name, what the
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
# (letter, name, which way is better, what the bar measures, the number beside it, colour). P and T bars
|
|
99
|
+
# are the amount itself (tokens, time: shorter is better); the A bar is the share right (longer is better).
|
|
100
|
+
LETTERS = (("P", "parsimony (tokens)", "▼ shorter is better", "tokens", "tokens_x", "cyan"),
|
|
101
|
+
("T", "time", "▼ shorter is better", "total_s", "time_x", "magenta"),
|
|
102
|
+
("A", "accuracy", "▲ longer is better", "A", "A", "green"))
|
|
99
103
|
|
|
100
104
|
|
|
101
105
|
def times(v):
|
|
@@ -103,26 +107,41 @@ def times(v):
|
|
|
103
107
|
return "-" if v is None else f"{v:.1f}×" if v < 9.95 else f"{v:.0f}×"
|
|
104
108
|
|
|
105
109
|
|
|
110
|
+
def _blocks(width, v, most):
|
|
111
|
+
"""How many of width blocks a bar fills: v as a share of most, at least one block when v > 0."""
|
|
112
|
+
if not v or not most:
|
|
113
|
+
return 0
|
|
114
|
+
return max(1, min(width, round(width * v / most)))
|
|
115
|
+
|
|
116
|
+
|
|
106
117
|
def bars(result, width=20, markup=True):
|
|
107
|
-
"""Lines with a row per model and a bar
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
"""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
|
+
are proportional to tokens and time (the longest is the model that used most; shorter is better),
|
|
120
|
+
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). markup=False gives plain text."""
|
|
110
122
|
if not result["models"]:
|
|
111
123
|
return []
|
|
112
124
|
name_w = max(len(x["model"]) for x in result["models"])
|
|
113
|
-
# each column: the bar, a space, "100%" or "9.8×" (
|
|
114
|
-
cell_w = max(width + 9, *(len(
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
# each column: the bar, a space, "100%" or "9.8×" (6), then a gap; at least as wide as its heading
|
|
126
|
+
cell_w = max(width + 9, *(len(t) + 2 for k, name, better, *_ in LETTERS for t in (f"{k} {name}", better)))
|
|
127
|
+
pad = " " * (name_w + 2)
|
|
128
|
+
head = pad + "".join(f"{f'{k} {name}':<{cell_w}}" for k, name, *_ in LETTERS)
|
|
129
|
+
sub = pad + "".join(f"{better:<{cell_w}}" for _, _, better, *_ in LETTERS)
|
|
130
|
+
lines = [f"[b]{head.rstrip()}[/b]" if markup else head.rstrip(), f"[dim]{sub.rstrip()}[/dim]" if markup else sub.rstrip()]
|
|
131
|
+
most = {m: max((x.get(m) or 0 for x in result["models"]), default=0) for _, _, _, m, _, _ in LETTERS}
|
|
117
132
|
for x in result["models"]:
|
|
118
133
|
cells = []
|
|
119
|
-
for k, _, shown, color in LETTERS:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
for k, _, _, measure, shown, color in LETTERS:
|
|
135
|
+
if k == "A":
|
|
136
|
+
v = x["A"]
|
|
137
|
+
full = 0 if v is None else round(width * max(0, min(100, v)) / 100)
|
|
138
|
+
if v is not None and v < 100: # full only when every answer passed
|
|
139
|
+
full = min(full, width - 1)
|
|
140
|
+
num = "-" if v is None else compare.pct(v / 100)
|
|
141
|
+
else:
|
|
142
|
+
full = _blocks(width, x.get(measure), most[measure])
|
|
143
|
+
num = times(x.get(shown))
|
|
124
144
|
bar, rest = "█" * full, "░" * (width - full)
|
|
125
|
-
num = (("-" if v is None else compare.pct(v / 100)) if shown == "A" else times(x.get(shown)))
|
|
126
145
|
bar = f"[{color}]{bar}[/][grey37]{rest}[/]" if markup else bar + rest
|
|
127
146
|
cells.append(f"{bar} {num:>6}" + " " * (cell_w - width - 7))
|
|
128
147
|
name = f"{x['model']:<{name_w}}"
|
|
@@ -183,8 +202,9 @@ def scope_note(result, models):
|
|
|
183
202
|
if not result["questions"]:
|
|
184
203
|
return "These models have no question in common yet, so there's nothing to compare."
|
|
185
204
|
return (f"Compared on the {result['questions']} questions all {len(result['models'])} model(s) answered. "
|
|
186
|
-
"
|
|
187
|
-
"model's (1.0×, ★ in the table). A accuracy: answers right
|
|
205
|
+
"P parsimony (tokens) and T time: bars as long as the tokens / time used (shorter is better), "
|
|
206
|
+
"and how many times the leanest / fastest model's (1.0×, ★ in the table). A accuracy: answers right "
|
|
207
|
+
"(longer is better).")
|
|
188
208
|
|
|
189
209
|
|
|
190
210
|
def for_engine(e, labels=None, packs=None, results_dir=None):
|
|
@@ -39,6 +39,8 @@ from textual.widgets.option_list import Option
|
|
|
39
39
|
from textual.widgets.selection_list import Selection
|
|
40
40
|
|
|
41
41
|
import yaml
|
|
42
|
+
from rich.cells import cell_len
|
|
43
|
+
from rich.text import Text
|
|
42
44
|
|
|
43
45
|
from . import compare
|
|
44
46
|
from . import engine
|
|
@@ -181,6 +183,79 @@ def fmt_secs(secs):
|
|
|
181
183
|
return f"{secs // 60}m{secs % 60:02d}s" if secs >= 60 else f"{secs}s"
|
|
182
184
|
|
|
183
185
|
|
|
186
|
+
# -- sorting Results tables by a click on a column name
|
|
187
|
+
SORT_ARROWS = (" ▲", " ▼")
|
|
188
|
+
SORT_RANKS = {"EASY": 1, "MEDIUM": 2, "HARD": 3, "FAIL": 1, "INCONCLUSIVE": 2, "PASS": 3,
|
|
189
|
+
"DOESN'T FIT": float("inf")}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SortCell(Text):
|
|
193
|
+
"""A table cell that sorts by its `sort_value` instead of what it shows."""
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def sort_cell(markup, value):
|
|
197
|
+
t = Text.from_markup(markup)
|
|
198
|
+
cell = SortCell(t.plain, spans=t.spans)
|
|
199
|
+
cell.sort_value = value
|
|
200
|
+
return cell
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _sort_number(tok):
|
|
204
|
+
"""1,234 · 3.1k · 2.4× · 12/20 · 4m10s · 1h02m · 12.5s as a number, else None."""
|
|
205
|
+
tok = tok.rstrip("★×%").replace(",", "")
|
|
206
|
+
if m := re.fullmatch(r"(\d+)/(\d+)", tok): # answers right: the share, then the count
|
|
207
|
+
a, b = int(m[1]), int(m[2])
|
|
208
|
+
return (a / b if b else 0) + a * 1e-9
|
|
209
|
+
if (m := re.fullmatch(r"(?:(\d+)h)?(?:(\d+)m)?(?:(\d+(?:\.\d+)?)s)?", tok)) and any(m.groups()):
|
|
210
|
+
h, mi, s = m.groups()
|
|
211
|
+
return int(h or 0) * 3600 + int(mi or 0) * 60 + float(s or 0)
|
|
212
|
+
if m := re.fullmatch(r"(-?\d+(?:\.\d+)?)(k?)", tok):
|
|
213
|
+
return float(m[1]) * (1000 if m[2] else 1)
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def sort_value(cell):
|
|
218
|
+
"""What a Results cell sorts by: the number it shows, a rank for levels and verdicts, else its
|
|
219
|
+
text; None (always last) for an empty cell."""
|
|
220
|
+
if isinstance(cell, SortCell):
|
|
221
|
+
return cell.sort_value
|
|
222
|
+
plain = (cell.plain if isinstance(cell, Text) else Text.from_markup(str(cell)).plain).strip()
|
|
223
|
+
if plain.lower() in ("", "-", "no data", "unrated"):
|
|
224
|
+
return None
|
|
225
|
+
if plain.upper() in SORT_RANKS:
|
|
226
|
+
return SORT_RANKS[plain.upper()]
|
|
227
|
+
first = re.sub(r"^(OK|TOO SLOW) ", "", plain).split()[0] # latency: by its p90 seconds
|
|
228
|
+
n = _sort_number(first)
|
|
229
|
+
return plain.casefold() if n is None else n
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class _Desc:
|
|
233
|
+
"""Reverses the order of what it wraps (so empty cells stay last when sorting descending)."""
|
|
234
|
+
__slots__ = ("v",)
|
|
235
|
+
|
|
236
|
+
def __init__(self, v):
|
|
237
|
+
self.v = v
|
|
238
|
+
|
|
239
|
+
def __eq__(self, other):
|
|
240
|
+
return self.v == other.v
|
|
241
|
+
|
|
242
|
+
def __lt__(self, other):
|
|
243
|
+
return other.v < self.v
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def sort_key(cell, reverse=False):
|
|
247
|
+
v = sort_value(cell)
|
|
248
|
+
if v is None:
|
|
249
|
+
return (1,)
|
|
250
|
+
return (0, isinstance(v, str), _Desc(v) if reverse else v)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def column_name(label):
|
|
254
|
+
"""A column's label without the sort arrow."""
|
|
255
|
+
text = label.plain if isinstance(label, Text) else str(label)
|
|
256
|
+
return next((text[:-len(a)] for a in SORT_ARROWS if text.endswith(a)), text)
|
|
257
|
+
|
|
258
|
+
|
|
184
259
|
class StreamView(TextArea):
|
|
185
260
|
"""Read-only, word-wrapped text that grows as tokens stream in. Scroll it with the mouse; it follows
|
|
186
261
|
new text only while scrolled to the bottom."""
|
|
@@ -332,13 +407,16 @@ with the model's reasoning and answer as they stream, then recent results with t
|
|
|
332
407
|
v with several models at a time: stream the next one (k skips the one streaming)""",
|
|
333
408
|
"ResultsScreen": """[b]Results[/b]
|
|
334
409
|
|
|
335
|
-
PTA index parsimony (tokens), time and accuracy per model, a bar each
|
|
336
|
-
2.4× = 2.4 times the best),
|
|
337
|
-
|
|
410
|
+
PTA index parsimony (tokens), time and accuracy per model, a bar each: tokens and time
|
|
411
|
+
as long as they took (shorter is better; 2.4× = 2.4 times the best), accuracy
|
|
412
|
+
longer is better. Per-answer numbers, tok/s, TTFT and memory in the table.
|
|
413
|
+
Results open here.
|
|
338
414
|
Production readiness one verdict per model and use case; the table says why and what to run next
|
|
339
415
|
Per question every question, model by model (filters: pack, level, Models disagree)
|
|
340
416
|
Failures every wrong answer with the grader's reason; enter opens it in full
|
|
341
417
|
|
|
418
|
+
Click a column name to sort a table by it; click it again to reverse. Empty cells stay last.
|
|
419
|
+
|
|
342
420
|
More in the terminal: tuieval compare (--pairwise: is a difference real), tuieval history,
|
|
343
421
|
tuieval items (test quality).
|
|
344
422
|
|
|
@@ -2407,7 +2485,7 @@ class ResultsScreen(Screen):
|
|
|
2407
2485
|
yield DataTable(id="pq-summary", zebra_stripes=True, cursor_type="none")
|
|
2408
2486
|
yield Static(id="pq-legend")
|
|
2409
2487
|
yield DataTable(id="per-question", zebra_stripes=True, cursor_type="cell")
|
|
2410
|
-
with TabPane("Failures"):
|
|
2488
|
+
with TabPane("Failures", id="tab-failures"):
|
|
2411
2489
|
yield Static("[dim]Enter on a row shows the whole answer, its reasoning and what was checked.[/dim]")
|
|
2412
2490
|
yield DataTable(id="failures", zebra_stripes=True, cursor_type="row")
|
|
2413
2491
|
yield Footer()
|
|
@@ -2416,6 +2494,8 @@ class ResultsScreen(Screen):
|
|
|
2416
2494
|
self.rows, self.infos, self.partial_rows, self.superseded = [], [], [], set()
|
|
2417
2495
|
if not hasattr(self.app, "pq_models"):
|
|
2418
2496
|
self.app.pq_models = set()
|
|
2497
|
+
if not hasattr(self.app, "result_sorts"):
|
|
2498
|
+
self.app.result_sorts = {} # table id -> (column name, descending), kept for this session
|
|
2419
2499
|
self.fill()
|
|
2420
2500
|
|
|
2421
2501
|
def on_screen_resume(self):
|
|
@@ -2487,15 +2567,17 @@ class ResultsScreen(Screen):
|
|
|
2487
2567
|
f.add_columns("Model", "Level", "Test", "Reason")
|
|
2488
2568
|
# compare.failures lists the failed rows in order, so they line up with fail_rows
|
|
2489
2569
|
self.fail_rows = [r for r in rows if not r["ok"]]
|
|
2490
|
-
for model, test, reason, level in compare.failures(rows):
|
|
2491
|
-
f.add_row(model, diff_badge(level), test, rich_escape(reason[:130]))
|
|
2570
|
+
for i, (model, test, reason, level) in enumerate(compare.failures(rows)):
|
|
2571
|
+
f.add_row(model, diff_badge(level), test, rich_escape(reason[:130]), key=str(i))
|
|
2492
2572
|
self.fill_per_question()
|
|
2493
2573
|
self.fill_pta()
|
|
2494
2574
|
self.fill_readiness(results_dir)
|
|
2575
|
+
self.sort_tables()
|
|
2495
2576
|
|
|
2496
2577
|
@on(DataTable.RowSelected, "#failures")
|
|
2497
2578
|
def failure_selected(self, event):
|
|
2498
|
-
|
|
2579
|
+
# in the order shown, so [ and ] step through them as sorted
|
|
2580
|
+
entries = [answer_entry(self.fail_rows[int(r.key.value)]) for r in event.data_table.ordered_rows]
|
|
2499
2581
|
i = event.cursor_row
|
|
2500
2582
|
if not entries[i]:
|
|
2501
2583
|
self.notify("This result is in the old format and has no stored answer.", severity="warning")
|
|
@@ -2506,7 +2588,6 @@ class ResultsScreen(Screen):
|
|
|
2506
2588
|
# -- per question: tokens and time for every model on the same question
|
|
2507
2589
|
@on(Select.Changed, "#pq-pack")
|
|
2508
2590
|
@on(Select.Changed, "#pq-level")
|
|
2509
|
-
@on(Select.Changed, "#pq-sort")
|
|
2510
2591
|
@on(Checkbox.Changed, "#pq-partial")
|
|
2511
2592
|
@on(Checkbox.Changed, "#pq-shared")
|
|
2512
2593
|
@on(Checkbox.Changed, "#pq-disagree")
|
|
@@ -2514,6 +2595,45 @@ class ResultsScreen(Screen):
|
|
|
2514
2595
|
if getattr(self, "rows", None) is not None:
|
|
2515
2596
|
self.fill_per_question()
|
|
2516
2597
|
|
|
2598
|
+
@on(Select.Changed, "#pq-sort")
|
|
2599
|
+
def pq_sort_changed(self):
|
|
2600
|
+
"""The Sort dropdown takes over from a column sorted by a click."""
|
|
2601
|
+
self.app.result_sorts.pop("per-question", None)
|
|
2602
|
+
self.per_question_changed()
|
|
2603
|
+
|
|
2604
|
+
@on(DataTable.HeaderSelected)
|
|
2605
|
+
def header_selected(self, event):
|
|
2606
|
+
"""A click on a column name sorts by it; another click on it reverses the order."""
|
|
2607
|
+
t, name = event.data_table, column_name(event.label)
|
|
2608
|
+
prev = self.app.result_sorts.get(t.id)
|
|
2609
|
+
self.app.result_sorts[t.id] = (name, not prev[1] if prev and prev[0] == name else False)
|
|
2610
|
+
self.sort_table(t)
|
|
2611
|
+
|
|
2612
|
+
def sort_tables(self):
|
|
2613
|
+
"""After a table is rebuilt: sort it again by the column picked (if that column is still there)."""
|
|
2614
|
+
for t in self.query(DataTable):
|
|
2615
|
+
self.sort_table(t)
|
|
2616
|
+
|
|
2617
|
+
def sort_table(self, t):
|
|
2618
|
+
name, desc = self.app.result_sorts.get(t.id, (None, False))
|
|
2619
|
+
found = None
|
|
2620
|
+
for col in t.ordered_columns:
|
|
2621
|
+
base = column_name(col.label)
|
|
2622
|
+
label = base + SORT_ARROWS[desc] if base == name else base
|
|
2623
|
+
if col.label.plain != label:
|
|
2624
|
+
col.label = Text(label)
|
|
2625
|
+
w = cell_len(label) # room for the arrow (a column keeps the width it was added with)
|
|
2626
|
+
col.content_width = max(col.content_width, w)
|
|
2627
|
+
if not col.auto_width:
|
|
2628
|
+
col.width = max(col.width, w)
|
|
2629
|
+
t._require_update_dimensions = True
|
|
2630
|
+
if base == name:
|
|
2631
|
+
found = col
|
|
2632
|
+
if found is not None:
|
|
2633
|
+
t.sort(found.key, key=lambda cell: sort_key(cell, desc))
|
|
2634
|
+
else:
|
|
2635
|
+
t.refresh()
|
|
2636
|
+
|
|
2517
2637
|
@on(Button.Pressed, "#pq-models")
|
|
2518
2638
|
@on(Button.Pressed, "#pta-models")
|
|
2519
2639
|
def pick_models(self):
|
|
@@ -2578,6 +2698,7 @@ class ResultsScreen(Screen):
|
|
|
2578
2698
|
for i, h in enumerate(header): # sized to the contents: added before the rows, a column keeps its header's width
|
|
2579
2699
|
t.add_column(h, width=max(len(h), *(len(r[i]) for r in table)) if table else None)
|
|
2580
2700
|
t.add_rows(table)
|
|
2701
|
+
self.sort_table(t)
|
|
2581
2702
|
|
|
2582
2703
|
def fill_per_question(self):
|
|
2583
2704
|
# a pack being rerun under new questions or settings: its old result file is on its way out,
|
|
@@ -2640,18 +2761,21 @@ class ResultsScreen(Screen):
|
|
|
2640
2761
|
best = min((c["tokens"] for c in t["cells"].values() if c["tokens"] and c["passed"]), default=None)
|
|
2641
2762
|
for m in models:
|
|
2642
2763
|
c = t["cells"].get(m)
|
|
2643
|
-
|
|
2764
|
+
# a model's column sorts by its tokens
|
|
2765
|
+
cells.append(sort_cell(self.pq_cell(c, best, here), c["tokens"] or None) if c else "[dim]-[/dim]")
|
|
2644
2766
|
spread = t["spread"]["tokens"]
|
|
2645
2767
|
sp = f"[yellow]{spread:.1f}×[/yellow]" if spread >= 3 else (f"{spread:.1f}×" if len(t["cells"]) > 1 else "")
|
|
2646
2768
|
desc = t["cells"][next(iter(t["cells"]))]["rows"][0].get("raw", {}).get("description") or t["name"]
|
|
2647
2769
|
table.add_row(rich_escape(desc[:48]), t["suite"], diff_badge(t["difficulty"]), sp, *cells, key=t["test"])
|
|
2648
2770
|
self.pq_tests[t["test"]] = t
|
|
2771
|
+
self.sort_table(table)
|
|
2772
|
+
self.sort_table(self.query_one("#pq-summary", DataTable))
|
|
2649
2773
|
self.query_one("#pq-legend", Static).update(
|
|
2650
2774
|
f"[dim]{len(tests)} questions. Each cell: result (passes/answers if repeated), median tokens, median seconds. "
|
|
2651
2775
|
"[green]bold[/green] = fewest tokens among passing answers · [red]cut[/red] = hit max_tokens · "
|
|
2652
2776
|
"◐ = pack unfinished (not in scores) · dim seconds = hosted, or measured on another machine; tokens compare "
|
|
2653
2777
|
"across machines, seconds don't. ×tokens = most / fewest tokens across models. Enter on a cell opens the "
|
|
2654
|
-
"answers.[/dim]" + ("\n[yellow]Left out, being rerun with other questions or settings: " + ", ".join(
|
|
2778
|
+
"answers. Click a column name to sort by it (a model's column: by its tokens).[/dim]" + ("\n[yellow]Left out, being rerun with other questions or settings: " + ", ".join(
|
|
2655
2779
|
f"{os.path.basename(os.path.dirname(p))} · {os.path.basename(p)[:-5]}" for p in sorted(self.superseded))
|
|
2656
2780
|
+ ". Only the new run's answers (◐) are shown for these.[/yellow]" if self.superseded else ""))
|
|
2657
2781
|
|
|
@@ -761,10 +761,14 @@ class PTAIndex(unittest.TestCase):
|
|
|
761
761
|
self.assertEqual((cells["slow"][1], cells["fast"][1:3]), ("10 ★", ["40", "2.0s ★"])) # per answer
|
|
762
762
|
bars = pta.bars(res, 10, markup=False)
|
|
763
763
|
self.assertEqual(bars[0].split(), ["P", "parsimony", "(tokens)", "T", "time", "A", "accuracy"])
|
|
764
|
+
self.assertEqual(bars[1].split(), ["▼", "shorter", "is", "better"] * 2 + ["▲", "longer", "is", "better"])
|
|
765
|
+
slow, fast = bars[2], bars[3]
|
|
764
766
|
self.assertEqual((bars[0].index("P parsimony"), bars[0].index("T time")), # each bar starts under
|
|
765
|
-
(
|
|
766
|
-
#
|
|
767
|
-
|
|
767
|
+
(fast.index("██████████"), fast.index("███░░░░░░░"))) # its heading
|
|
768
|
+
# P and T bars as long as tokens and time (shorter is better): fast used 4x the tokens of slow but
|
|
769
|
+
# took 4 s against 13 s, so it has the longest token bar and the shortest time bar
|
|
770
|
+
self.assertEqual(fast.split(), ["fast", "██████████", "4.0×", "███░░░░░░░", "1.0×", "█████░░░░░", "50.0%"])
|
|
771
|
+
self.assertEqual(slow.split()[1:5], ["██░░░░░░░░", "1.0×", "██████████", "3.2×"])
|
|
768
772
|
self.assertEqual((pta.times(1), pta.times(9.84), pta.times(27.9)), ("1.0×", "9.8×", "28×"))
|
|
769
773
|
self.assertEqual((by["fast"]["A"], by["slow"]["A"]), (50, 100))
|
|
770
774
|
self.assertEqual([x["model"] for x in res["models"]], ["slow", "fast"]) # best accuracy first
|
|
@@ -774,7 +778,7 @@ class PTAIndex(unittest.TestCase):
|
|
|
774
778
|
self.assertEqual([compare.pct(f) for f in (467 / 469, 1, 0, 0.5)], ["99.6%", "100%", "0%", "50.0%"])
|
|
775
779
|
row = lambda model, a: {"model": model, "P": 100, "T": 100, "A": a, "tokens_x": 1, "time_x": 1}
|
|
776
780
|
bars = pta.bars({"models": [row("two-wrong", 100 * 467 / 469), row("perfect", 100.0)]}, 10, markup=False)
|
|
777
|
-
two_wrong, perfect = bars[
|
|
781
|
+
two_wrong, perfect = bars[2].split(), bars[3].split()
|
|
778
782
|
self.assertEqual((two_wrong[-2], two_wrong[-1]), ("█████████░", "99.6%")) # not a full bar
|
|
779
783
|
self.assertEqual((perfect[-2], perfect[-1]), ("██████████", "100%"))
|
|
780
784
|
|
|
@@ -799,6 +803,18 @@ class PTAIndex(unittest.TestCase):
|
|
|
799
803
|
picked = pta.index(rows, ["a", "few"]) # picked by hand: compared anyway
|
|
800
804
|
self.assertEqual((picked["questions"], picked["left_out"]), (1, []))
|
|
801
805
|
|
|
806
|
+
def test_results_sort_values(self):
|
|
807
|
+
from tuieval.tui import diff_badge, sort_cell, sort_key, sort_value
|
|
808
|
+
got = [sort_value(c) for c in ("1,234 ★", "3.1k", "2.4×", "4m10s", "1h02m", "12.5s", diff_badge("hard"),
|
|
809
|
+
"[bold green]PASS[/]", "[green]OK 12s[/] [dim]measured[/dim]",
|
|
810
|
+
"[bold red]doesn't fit[/]", "Top 10 holdings")]
|
|
811
|
+
self.assertEqual(got, [1234, 3100, 2.4, 250, 3720, 12.5, 3, 3, 12, float("inf"), "top 10 holdings"])
|
|
812
|
+
self.assertEqual([sort_value(c) for c in ("-", "[dim]-[/dim]", "[dim]no data[/]", "")], [None] * 4)
|
|
813
|
+
self.assertEqual(sort_value(sort_cell("[green]✓[/green] 3.1k 12s", 3100)), 3100)
|
|
814
|
+
cells = ["3/4", "-", "10/20", "4/4", "abc"] # numbers, then text; empty cells last both ways
|
|
815
|
+
self.assertEqual(sorted(cells, key=sort_key), ["10/20", "3/4", "4/4", "abc", "-"])
|
|
816
|
+
self.assertEqual(sorted(cells, key=lambda c: sort_key(c, True)), ["4/4", "3/4", "10/20", "abc", "-"])
|
|
817
|
+
|
|
802
818
|
def test_cli_report_and_tui(self):
|
|
803
819
|
with tempfile.TemporaryDirectory() as tmp:
|
|
804
820
|
ws = os.path.join(tmp, "ws")
|
|
@@ -853,8 +869,37 @@ class PTAIndex(unittest.TestCase):
|
|
|
853
869
|
app.screen.fill_per_question()
|
|
854
870
|
await pilot.pause(0.3)
|
|
855
871
|
alone = pq.row_count
|
|
872
|
+
tab = app.screen.query_one(TabbedContent).active
|
|
873
|
+
|
|
874
|
+
async def click_header(t, name): # a real click on the column name
|
|
875
|
+
i = next(i for i, c in enumerate(t.ordered_columns) if c.label.plain.startswith(name))
|
|
876
|
+
x = sum(c.get_render_width(t) for c in t.ordered_columns[:i]) + 1
|
|
877
|
+
await pilot.click(t, offset=(x, 0))
|
|
878
|
+
await pilot.pause(0.2)
|
|
879
|
+
return [str(t.get_row_at(r)[0]) for r in range(t.row_count)], \
|
|
880
|
+
[c.label.plain for c in t.ordered_columns]
|
|
881
|
+
sorts = {}
|
|
882
|
+
pta_t = app.screen.query_one("#pta", DataTable)
|
|
883
|
+
sorts["up"], _ = await click_header(pta_t, "answers right")
|
|
884
|
+
sorts["down"], sorts["labels"] = await click_header(pta_t, "answers right")
|
|
885
|
+
app.pq_models = set()
|
|
886
|
+
app.screen.fill_pta() # rebuilt: still sorted
|
|
887
|
+
await pilot.pause(0.2)
|
|
888
|
+
sorts["kept"] = [str(pta_t.get_row_at(r)[0]) for r in range(pta_t.row_count)]
|
|
889
|
+
app.screen.query_one(TabbedContent).active = "tab-failures"
|
|
890
|
+
await pilot.pause(0.3)
|
|
891
|
+
f = app.screen.query_one("#failures", DataTable)
|
|
892
|
+
await click_header(f, "Test")
|
|
893
|
+
sorts["tests"], _ = await click_header(f, "Test")
|
|
894
|
+
sorts["tests"] = [str(f.get_row_at(r)[2]) for r in range(f.row_count)]
|
|
895
|
+
f.focus()
|
|
896
|
+
f.move_cursor(row=0)
|
|
897
|
+
await pilot.press("enter") # opens the row shown, not the first failure
|
|
898
|
+
await pilot.pause(0.3)
|
|
899
|
+
s = app.screen
|
|
900
|
+
sorts["opened"] = s.entries[s.index]["record"]["test"]
|
|
856
901
|
print(json.dumps({"rows": rows, "bars": "P parsimony" in tri, "disagree": [both, alone],
|
|
857
|
-
"tab":
|
|
902
|
+
"tab": tab, "sorts": sorts}))
|
|
858
903
|
asyncio.run(go())
|
|
859
904
|
""")
|
|
860
905
|
p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=300,
|
|
@@ -865,6 +910,10 @@ class PTAIndex(unittest.TestCase):
|
|
|
865
910
|
self.assertEqual(got["disagree"], [4, 0])
|
|
866
911
|
self.assertEqual(got["tab"], "tab-pta") # Results opens on the PTA index
|
|
867
912
|
self.assertEqual([(r[0], r[3]) for r in got["rows"]][0], ("good", "4/4"))
|
|
913
|
+
s = got["sorts"] # a click on a column name sorts by it, a second click reverses
|
|
914
|
+
self.assertEqual((s["up"], s["down"], s["kept"]), (["hosted", "good"], ["good", "hosted"], ["good", "hosted"]))
|
|
915
|
+
self.assertIn("answers right ▼", s["labels"])
|
|
916
|
+
self.assertEqual((s["tests"][0], s["opened"]), ("apps: d", "d"))
|
|
868
917
|
|
|
869
918
|
|
|
870
919
|
class Unavailable(unittest.TestCase):
|
|
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.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev65 → tuieval-0.2.0.dev68}/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
|