tuieval 0.2.0.dev99__tar.gz → 0.2.0.dev103__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.dev99 → tuieval-0.2.0.dev103}/CHANGELOG.md +3 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/PKG-INFO +2 -2
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/README.md +1 -1
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/models.md +1 -1
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/compare.py +29 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/engine.py +66 -19
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/tui.py +82 -10
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/tests/test_tuieval.py +166 -4
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/.gitignore +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/LICENSE +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/insights.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/logs.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/pta.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/run_evals.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/tests/mock_server.py +0 -0
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Run screen: `x` removes the highlighted pack from the run going if it hasn't started (the reverse of adding it); a model left with nothing isn't started.
|
|
6
|
+
- `k` (skip model) asks first, naming the model. Yes/no dialogs start on No; `y` answers yes, `n` or Esc no.
|
|
7
|
+
- Fix: hosted models no longer count as running alongside local ones (they don't share the machine), so a one-model-at-a-time setup no longer gets the "ran alongside other models" note. Older results are read the same way.
|
|
5
8
|
- Results, Per question: the model summary shows every picked model without scrolling, and fits the window: shorter column names, and long model names cut in the middle (both ends kept). The PTA index table fits the same way.
|
|
6
9
|
- PTA index: the accuracy bar shows answers cut off at max_tokens in orange, with `(n% truncated)` beside it and a **truncated** column in the table, so overthinking stands apart from wrong answers.
|
|
7
10
|
- Results → Insights (also `tuieval insights`): charts with what to do next: every model against a reference (95% CI), the best model per use case, fine-tunes against their base (`base` in models.toml), why models fail, accuracy against tokens or time, question health, and the Certify runs that would settle the strongest models' verdicts. `[defaults] reference` sets the default reference.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev103
|
|
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
|
|
@@ -143,7 +143,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
|
|
|
143
143
|
|
|
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
|
-
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.
|
|
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 (after a yes), `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. `x` takes a pack that hasn't started out of the run; `w` lists queued runs (`del` removes one).
|
|
147
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.
|
|
@@ -117,7 +117,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
|
|
|
117
117
|
|
|
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
|
-
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.
|
|
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 (after a yes), `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. `x` takes a pack that hasn't started out of the run; `w` lists queued runs (`del` removes one).
|
|
121
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.
|
|
@@ -94,7 +94,7 @@ By default a run serves one model at a time. On a machine with room for more, se
|
|
|
94
94
|
- Models start in queue order, each in its own lane. The next one waits while its fit-check memory estimate wouldn't fit next to the ones running (without one, e.g. a server that sizes its own memory, the setting decides). Contexts are never shrunk to make room.
|
|
95
95
|
- A second model on the same server gets a free port, so a server's `cmd` must take `{port}`.
|
|
96
96
|
- A server with `before_start` (which may stop other servers) always runs on its own. Tuning is always one model at a time.
|
|
97
|
-
- Answers are judged exactly as in a run of one model. Each answer records the models served while it ran (`ran_alongside`), and a load time measured next to others is marked (`loaded_alongside`), since models side by side share the machine's GPU and memory bandwidth. The speed table and the answer view show it.
|
|
97
|
+
- Answers are judged exactly as in a run of one model. Each answer records the models served while it ran (`ran_alongside`), and a load time measured next to others is marked (`loaded_alongside`), since models side by side share the machine's GPU and memory bandwidth. Only local models count: hosted ones share nothing of the machine. The speed table and the answer view show it.
|
|
98
98
|
- In the TUI, one model's answers stream at a time: `v` switches to the next, `k` skips the one streaming. `tuieval run` prints one line per answer instead of streaming.
|
|
99
99
|
- Only one run uses the machine at a time across tuieval windows; models at a time applies within a run.
|
|
100
100
|
|
|
@@ -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.dev103'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev103')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -41,6 +41,7 @@ def _load_native(data):
|
|
|
41
41
|
"pack": pack["name"], "pack_fp": pack["fingerprint"],
|
|
42
42
|
# hosted (OpenRouter) models: the provider endpoint and its declared quantization
|
|
43
43
|
"endpoint": run.get("endpoint"), "quantization": run.get("quantization"),
|
|
44
|
+
"hosted": bool(run.get("hosted") or run.get("endpoint")),
|
|
44
45
|
"loaded_alongside": run.get("loaded_alongside") or []}
|
|
45
46
|
rows = []
|
|
46
47
|
for r in data["results"]:
|
|
@@ -78,9 +79,37 @@ def load(paths):
|
|
|
78
79
|
row.update(raw=rec, path=path, pack_fp=data["pack"]["fingerprint"])
|
|
79
80
|
rows += r
|
|
80
81
|
infos.append(i)
|
|
82
|
+
# older results also noted hosted models alongside, which share nothing of this machine: a hosted
|
|
83
|
+
# model's rows note no one, and no one notes it
|
|
84
|
+
labels, prefixes = configured_hosted(paths)
|
|
85
|
+
labels |= {i["label"] for i in infos if i["hosted"]}
|
|
86
|
+
|
|
87
|
+
def local(along):
|
|
88
|
+
return [l for l in along if l not in labels and not l.startswith(tuple(prefixes))]
|
|
89
|
+
for row in rows:
|
|
90
|
+
row["alongside"] = [] if row["model"] in labels else local(row["alongside"])
|
|
91
|
+
for i in infos:
|
|
92
|
+
i["loaded_alongside"] = [] if i["label"] in labels else local(i["loaded_alongside"])
|
|
81
93
|
return rows, infos
|
|
82
94
|
|
|
83
95
|
|
|
96
|
+
def configured_hosted(paths):
|
|
97
|
+
"""(labels, label prefixes) models.toml says are hosted, in the workspaces these results/<label>/
|
|
98
|
+
<pack>.json are from: its hosted models, and the prefix of models named at run time on a hosted
|
|
99
|
+
server (e.g. "or-"). Covers hosted models whose results aren't loaded."""
|
|
100
|
+
from . import engine
|
|
101
|
+
labels, prefixes = set(), set()
|
|
102
|
+
for root in {os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(p)))) for p in paths}:
|
|
103
|
+
try:
|
|
104
|
+
cfg = engine.load_config(os.path.join(root, "models.toml"))
|
|
105
|
+
except Exception:
|
|
106
|
+
continue # no readable models.toml there: the results' own marks still apply
|
|
107
|
+
labels |= {m["label"] for m in cfg["models"] if engine.hosted(cfg, m)}
|
|
108
|
+
prefixes |= {engine.remote_label(n, s, "") for n, s in cfg["servers"].items()
|
|
109
|
+
if s.get("any_model") and engine.hosted(cfg, {"server": n})}
|
|
110
|
+
return labels, prefixes
|
|
111
|
+
|
|
112
|
+
|
|
84
113
|
def default_paths(results_dir=None):
|
|
85
114
|
"""results/<label>/<pack>.json, excluding smoke runs and the archive."""
|
|
86
115
|
results_dir = results_dir or workspace.path("results")
|
|
@@ -83,6 +83,21 @@ def builtin_config():
|
|
|
83
83
|
return tomllib.load(f)
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
def hosted(cfg, m):
|
|
87
|
+
"""A model served by an API elsewhere: tuieval doesn't start its server and its url isn't this
|
|
88
|
+
machine (or its server sets hosted = true). A server you started yourself on localhost still
|
|
89
|
+
counts as local (it uses the GPU)."""
|
|
90
|
+
if m.get("remote"):
|
|
91
|
+
return True
|
|
92
|
+
server = cfg["servers"][m["server"]]
|
|
93
|
+
if "hosted" in server: # say so for a server the url doesn't give away (e.g. a local proxy to an API)
|
|
94
|
+
return bool(server["hosted"])
|
|
95
|
+
if server.get("cmd"):
|
|
96
|
+
return False
|
|
97
|
+
host = urllib.parse.urlparse(server.get("url") or "").hostname or ""
|
|
98
|
+
return bool(host) and host not in ("localhost", "127.0.0.1", "::1", "0.0.0.0")
|
|
99
|
+
|
|
100
|
+
|
|
86
101
|
def load_config(path):
|
|
87
102
|
"""Read models.toml, fill in the built-ins it leaves out (builtin.toml) and each model's defaults.
|
|
88
103
|
[defaults] are filled key by key; [sampling] only when models.toml has none (sampling is part of
|
|
@@ -442,6 +457,7 @@ class Job:
|
|
|
442
457
|
earlier: list = dataclasses.field(default_factory=list) # earlier sittings (see Engine._sittings)
|
|
443
458
|
stop_early: bool = False # stop once the pack's FAIL is certain (verdict.fail_certain)
|
|
444
459
|
first: list = dataclasses.field(default_factory=list) # test ids round 1 asks first (most failed)
|
|
460
|
+
claimed: bool = False # its model's server took it on: it can no longer be removed from the run
|
|
445
461
|
|
|
446
462
|
@property
|
|
447
463
|
def label(self):
|
|
@@ -675,8 +691,9 @@ class Lane:
|
|
|
675
691
|
"""One model being run: its own request stream, stall reason and skip switch, so models run
|
|
676
692
|
side by side (parallel_models) stop independently. Runs and tunes in one lane use the engine's
|
|
677
693
|
main lane."""
|
|
678
|
-
def __init__(self, label=None):
|
|
694
|
+
def __init__(self, label=None, hosted=False):
|
|
679
695
|
self.label = label
|
|
696
|
+
self.hosted = hosted # a hosted model shares nothing of this machine, so it's never noted alongside
|
|
680
697
|
self.stream = None
|
|
681
698
|
self.stall = None # set by the stall watcher: why this model's server stalled
|
|
682
699
|
self.skip = threading.Event()
|
|
@@ -1282,18 +1299,7 @@ class Engine:
|
|
|
1282
1299
|
+ min(parallel_hosted or self.parallel_hosted(), hosted))
|
|
1283
1300
|
|
|
1284
1301
|
def hosted(self, m):
|
|
1285
|
-
|
|
1286
|
-
machine (or its server sets hosted = true). A server you started yourself on localhost still
|
|
1287
|
-
counts as local (it uses the GPU)."""
|
|
1288
|
-
if m.get("remote"):
|
|
1289
|
-
return True
|
|
1290
|
-
server = self.cfg["servers"][m["server"]]
|
|
1291
|
-
if "hosted" in server: # say so for a server the url doesn't give away (e.g. a local proxy to an API)
|
|
1292
|
-
return bool(server["hosted"])
|
|
1293
|
-
if server.get("cmd"):
|
|
1294
|
-
return False
|
|
1295
|
-
host = urllib.parse.urlparse(server.get("url") or "").hostname or ""
|
|
1296
|
-
return bool(host) and host not in ("localhost", "127.0.0.1", "::1", "0.0.0.0")
|
|
1302
|
+
return hosted(self.cfg, m)
|
|
1297
1303
|
|
|
1298
1304
|
def memory_need_gb(self, m):
|
|
1299
1305
|
"""GPU memory a model takes while served here: 0 for one we don't start (a hosted API or a
|
|
@@ -1723,6 +1729,33 @@ class Engine:
|
|
|
1723
1729
|
r["cond"].notify_all()
|
|
1724
1730
|
return added, None if added else "everything picked is already in the run"
|
|
1725
1731
|
|
|
1732
|
+
def removable(self, job):
|
|
1733
|
+
"""Why a job of the run going can't be removed (None: it can): only a pack not yet started."""
|
|
1734
|
+
if job.status in ("done", "failed", "skipped"):
|
|
1735
|
+
return "it has finished"
|
|
1736
|
+
if job.claimed or job.status == "running":
|
|
1737
|
+
return "it's running (k skips the model)"
|
|
1738
|
+
return None
|
|
1739
|
+
|
|
1740
|
+
def remove_jobs(self, keys):
|
|
1741
|
+
"""Take jobs not yet started (removable) out of the run going, the reverse of add_jobs: they never
|
|
1742
|
+
run, and a model left with none isn't started. Returns the jobs removed."""
|
|
1743
|
+
removed = []
|
|
1744
|
+
with self._run_lock:
|
|
1745
|
+
r = self._run
|
|
1746
|
+
if not r or not r["open"]:
|
|
1747
|
+
return removed
|
|
1748
|
+
for j in [j for j in r["jobs"] if j.key in keys and self.removable(j) is None]:
|
|
1749
|
+
j.status, j.note = "removed", "removed from the run"
|
|
1750
|
+
r["jobs"].remove(j)
|
|
1751
|
+
for _, group in r["pending"]:
|
|
1752
|
+
if j in group:
|
|
1753
|
+
group.remove(j)
|
|
1754
|
+
removed.append(j)
|
|
1755
|
+
r["pending"][:] = [g for g in r["pending"] if g[1]] # the lanes hold this very list
|
|
1756
|
+
r["cond"].notify_all()
|
|
1757
|
+
return removed
|
|
1758
|
+
|
|
1726
1759
|
def _run_lanes(self, parallel, parallel_hosted=4):
|
|
1727
1760
|
"""Run each model's jobs in its own lane: up to `parallel` local models at a time, in order (the
|
|
1728
1761
|
next one starts once a lane is free, nothing running stops it (runs_alone), and its memory fits
|
|
@@ -1817,15 +1850,23 @@ class Engine:
|
|
|
1817
1850
|
for t in threads:
|
|
1818
1851
|
t.join()
|
|
1819
1852
|
|
|
1853
|
+
def _local_lanes(self, lane, exclude=None):
|
|
1854
|
+
"""Labels of the local models served now (call with _lanes_lock held); none for a hosted lane."""
|
|
1855
|
+
if lane.hosted:
|
|
1856
|
+
return set()
|
|
1857
|
+
return {l for l, o in self._lanes.items() if not o.hosted} - {exclude}
|
|
1858
|
+
|
|
1820
1859
|
def _run_lane(self, label, mine):
|
|
1821
1860
|
"""One model's jobs, in this thread's own lane. Other models served meanwhile are noted on
|
|
1822
|
-
each answer (ran_alongside), since they share the machine's GPU and memory bandwidth.
|
|
1823
|
-
|
|
1861
|
+
each answer (ran_alongside), since they share the machine's GPU and memory bandwidth. Hosted
|
|
1862
|
+
models share neither, so they note nothing and nobody notes them."""
|
|
1824
1863
|
m = mine[0].model
|
|
1864
|
+
lane = Lane(label, self.hosted(m))
|
|
1825
1865
|
with self._lanes_lock:
|
|
1826
|
-
|
|
1827
|
-
other.
|
|
1828
|
-
|
|
1866
|
+
if not lane.hosted:
|
|
1867
|
+
for other in self._lanes.values():
|
|
1868
|
+
other.seen.add(label)
|
|
1869
|
+
lane.seen = self._local_lanes(lane)
|
|
1829
1870
|
if self.cfg["servers"][m["server"]].get("cmd"):
|
|
1830
1871
|
lane.port = self._lane_port(m)
|
|
1831
1872
|
self._lanes[label] = lane
|
|
@@ -1987,6 +2028,8 @@ class Engine:
|
|
|
1987
2028
|
"kv_type": sv.kv_type, "perf_args": sv.perf_args if local else None,
|
|
1988
2029
|
"perf_source": sv.perf_source, "server_version": self.server_version(m) if local else None}
|
|
1989
2030
|
ep = self.endpoint(m)
|
|
2031
|
+
if self.hosted(m):
|
|
2032
|
+
serving_info["hosted"] = True
|
|
1990
2033
|
if ep:
|
|
1991
2034
|
serving_info.update(endpoint=ep["tag"], provider=ep.get("provider"), quantization=ep.get("quantization"))
|
|
1992
2035
|
for j in jobs:
|
|
@@ -2006,6 +2049,10 @@ class Engine:
|
|
|
2006
2049
|
failed_in_row = 0
|
|
2007
2050
|
for j in jobs:
|
|
2008
2051
|
self._check()
|
|
2052
|
+
with self._run_lock:
|
|
2053
|
+
if j.status == "removed": # taken out of the run while its model ran another pack
|
|
2054
|
+
continue
|
|
2055
|
+
j.claimed = True
|
|
2009
2056
|
try:
|
|
2010
2057
|
self._run_job(j, base_url, run_info, alive=info["alive"])
|
|
2011
2058
|
except BaseException as ex:
|
|
@@ -2244,7 +2291,7 @@ class Engine:
|
|
|
2244
2291
|
self._check()
|
|
2245
2292
|
lane = self._lane()
|
|
2246
2293
|
with self._lanes_lock:
|
|
2247
|
-
lane.seen =
|
|
2294
|
+
lane.seen = self._local_lanes(lane, job.label)
|
|
2248
2295
|
self.emit("request_started", job=job, test=test["description"], test_id=test["id"],
|
|
2249
2296
|
has_image=bool(test.get("image")), repeat=rep, difficulty=test.get("difficulty", "unrated"))
|
|
2250
2297
|
waits = list(self.SERVER_RETRY_WAITS_S)
|
|
@@ -1828,16 +1828,33 @@ class RetestScreen(ModalScreen):
|
|
|
1828
1828
|
|
|
1829
1829
|
|
|
1830
1830
|
class ConfirmScreen(ModalScreen):
|
|
1831
|
-
|
|
1831
|
+
"""Yes or no; No has the focus, so Enter by reflex changes nothing. y: yes; n or Esc: no
|
|
1832
|
+
(and no_keys, e.g. the key that opened it, pressed again)."""
|
|
1833
|
+
BINDINGS = [Binding("y", "answer(True)", "Yes", show=False), Binding("n", "answer(False)", "No", show=False),
|
|
1834
|
+
Binding("escape", "answer(False)", "No", show=False)]
|
|
1835
|
+
|
|
1836
|
+
def __init__(self, question, no_keys=()):
|
|
1832
1837
|
super().__init__()
|
|
1833
1838
|
self.question = question
|
|
1839
|
+
self.no_keys = no_keys
|
|
1834
1840
|
|
|
1835
1841
|
def compose(self) -> ComposeResult:
|
|
1836
1842
|
with Vertical(id="dialog"):
|
|
1837
1843
|
yield Label(self.question)
|
|
1838
1844
|
with Horizontal(classes="dialog-buttons"):
|
|
1839
|
-
yield Button("Yes", id="yes", variant="error")
|
|
1840
|
-
yield Button("No", id="no")
|
|
1845
|
+
yield Button("Yes (y)", id="yes", variant="error")
|
|
1846
|
+
yield Button("No (n)", id="no")
|
|
1847
|
+
|
|
1848
|
+
def on_mount(self):
|
|
1849
|
+
self.query_one("#no", Button).focus()
|
|
1850
|
+
|
|
1851
|
+
def on_key(self, event):
|
|
1852
|
+
if event.key in self.no_keys:
|
|
1853
|
+
event.stop()
|
|
1854
|
+
self.dismiss(False)
|
|
1855
|
+
|
|
1856
|
+
def action_answer(self, yes):
|
|
1857
|
+
self.dismiss(yes)
|
|
1841
1858
|
|
|
1842
1859
|
@on(Button.Pressed)
|
|
1843
1860
|
def answer(self, event):
|
|
@@ -2114,6 +2131,7 @@ class RunScreen(Screen):
|
|
|
2114
2131
|
# priority: these work whichever pane has focus (the text panes would otherwise swallow keys)
|
|
2115
2132
|
BINDINGS = [
|
|
2116
2133
|
Binding("k", "skip_model", "Skip model", priority=True),
|
|
2134
|
+
Binding("x", "remove_job", "Remove from run", priority=True),
|
|
2117
2135
|
Binding("c", "cancel_all", "Cancel all", priority=True),
|
|
2118
2136
|
Binding("f", "toggle_follow", "Pause scroll", priority=True),
|
|
2119
2137
|
Binding("v", "watch_next", "Next model", priority=True),
|
|
@@ -2124,7 +2142,7 @@ class RunScreen(Screen):
|
|
|
2124
2142
|
Binding("q", "back", "Back", show=False, priority=True),
|
|
2125
2143
|
]
|
|
2126
2144
|
kind = "Run"
|
|
2127
|
-
LIVE_ONLY = {"skip_model", "cancel_all", "toggle_follow"}
|
|
2145
|
+
LIVE_ONLY = {"skip_model", "remove_job", "cancel_all", "toggle_follow"}
|
|
2128
2146
|
DONE_ONLY = set()
|
|
2129
2147
|
|
|
2130
2148
|
def __init__(self, jobs, parallel=None):
|
|
@@ -2180,6 +2198,8 @@ class RunScreen(Screen):
|
|
|
2180
2198
|
def check_action(self, action, parameters):
|
|
2181
2199
|
if action == "watch_next":
|
|
2182
2200
|
return self.running and self.side_by_side
|
|
2201
|
+
if action == "remove_job": # only while something is left to remove
|
|
2202
|
+
return self.running and any(self.app.engine.removable(j) is None for j in self.jobs)
|
|
2183
2203
|
if action in self.LIVE_ONLY:
|
|
2184
2204
|
return self.running
|
|
2185
2205
|
if action in self.DONE_ONLY:
|
|
@@ -2219,7 +2239,7 @@ class RunScreen(Screen):
|
|
|
2219
2239
|
with Vertical(id="top"):
|
|
2220
2240
|
yield Static(id="overall")
|
|
2221
2241
|
yield ProgressBar(id="bar", show_eta=False)
|
|
2222
|
-
yield DataTable(id="queue", cursor_type="
|
|
2242
|
+
yield DataTable(id="queue", cursor_type="row", zebra_stripes=True)
|
|
2223
2243
|
yield Static(id="current")
|
|
2224
2244
|
with Horizontal(id="done-bar"):
|
|
2225
2245
|
yield Button("New run [n]", id="done-new", variant="success")
|
|
@@ -2365,10 +2385,12 @@ class RunScreen(Screen):
|
|
|
2365
2385
|
self.total += j.total
|
|
2366
2386
|
self.query_one("#bar", ProgressBar).update(total=max(self.total, 1))
|
|
2367
2387
|
self.event(f"added to this run: {', '.join(j.key for j in d['jobs'])}")
|
|
2388
|
+
self.refresh_bindings()
|
|
2368
2389
|
self.update_overall()
|
|
2369
2390
|
elif kind in ("job_started", "job_update", "job_done"):
|
|
2370
2391
|
job = d["job"]
|
|
2371
2392
|
self.update_job(job)
|
|
2393
|
+
self.refresh_bindings() # x shows while a pack can still be removed
|
|
2372
2394
|
if kind == "job_started" and d.get("resumed"):
|
|
2373
2395
|
earlier = (f" ({fmt_secs(d['earlier_s'])} over {d['sittings']} earlier sitting(s))"
|
|
2374
2396
|
if d.get("sittings") else "")
|
|
@@ -2611,13 +2633,63 @@ class RunScreen(Screen):
|
|
|
2611
2633
|
|
|
2612
2634
|
# -- actions
|
|
2613
2635
|
def action_skip_model(self):
|
|
2614
|
-
if self.running:
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2636
|
+
if not self.running:
|
|
2637
|
+
return
|
|
2638
|
+
one = self.watch if self.side_by_side and self.watch else None
|
|
2639
|
+
names = [one] if one else self.active()
|
|
2640
|
+
if not names:
|
|
2641
|
+
return
|
|
2642
|
+
|
|
2643
|
+
def skip(yes):
|
|
2644
|
+
if not yes or not self.running:
|
|
2645
|
+
return
|
|
2646
|
+
if one:
|
|
2647
|
+
self.app.engine.skip_model(one)
|
|
2648
|
+
self.notify(f"Skipping {one}; the others keep going…")
|
|
2618
2649
|
else:
|
|
2619
2650
|
self.app.engine.skip_model()
|
|
2620
2651
|
self.notify("Skipping the current model…")
|
|
2652
|
+
self.app.push_screen(ConfirmScreen(
|
|
2653
|
+
f"Skip {', '.join(names)}? Answers so far are kept (a later run picks up from them); the rest "
|
|
2654
|
+
f"of {'its' if len(names) == 1 else 'their'} questions in this run are skipped.", no_keys=("k",)), skip)
|
|
2655
|
+
|
|
2656
|
+
def action_remove_job(self):
|
|
2657
|
+
"""Take the highlighted pack out of the run if it hasn't started (the reverse of adding it)."""
|
|
2658
|
+
if not self.running:
|
|
2659
|
+
return
|
|
2660
|
+
q = self.query_one("#queue", DataTable)
|
|
2661
|
+
try:
|
|
2662
|
+
key = q.coordinate_to_cell_key(q.cursor_coordinate).row_key.value
|
|
2663
|
+
except Exception:
|
|
2664
|
+
key = None
|
|
2665
|
+
job = next((j for j in self.jobs if j.key == key), None)
|
|
2666
|
+
if job is None:
|
|
2667
|
+
self.notify("Highlight a pack in the queue (click it or use ↑/↓ there), then press x.")
|
|
2668
|
+
return
|
|
2669
|
+
why = self.app.engine.removable(job)
|
|
2670
|
+
if why:
|
|
2671
|
+
self.notify(f"{job.label} · {job.pack.label} can't be removed: {why}.")
|
|
2672
|
+
return
|
|
2673
|
+
|
|
2674
|
+
def remove(yes):
|
|
2675
|
+
if not yes:
|
|
2676
|
+
return
|
|
2677
|
+
removed = self.app.engine.remove_jobs({job.key})
|
|
2678
|
+
if not removed:
|
|
2679
|
+
self.notify(f"{job.label} · {job.pack.label} started meanwhile, so it stays (k skips the model).")
|
|
2680
|
+
return
|
|
2681
|
+
for j in removed:
|
|
2682
|
+
q.remove_row(j.key)
|
|
2683
|
+
self.total -= j.total
|
|
2684
|
+
self.query_one("#bar", ProgressBar).update(total=max(self.total, 1))
|
|
2685
|
+
self.event(f"removed from this run: {', '.join(j.key for j in removed)}")
|
|
2686
|
+
self.update_overall()
|
|
2687
|
+
self.refresh_bindings()
|
|
2688
|
+
self.app.refresh_live()
|
|
2689
|
+
self.notify(f"Removed from the run: {job.label} · {job.pack.label}.")
|
|
2690
|
+
self.app.push_screen(ConfirmScreen(
|
|
2691
|
+
f"Remove {job.label} · {job.pack.label} from this run? It hasn't started, so nothing is lost; "
|
|
2692
|
+
"Start can add it again later.", no_keys=("x",)), remove)
|
|
2621
2693
|
|
|
2622
2694
|
def action_watch_next(self):
|
|
2623
2695
|
"""Stream the next model running alongside (from its next answer)."""
|
|
@@ -3489,7 +3561,7 @@ class EvalsApp(App):
|
|
|
3489
3561
|
return
|
|
3490
3562
|
run.handle("jobs_added", {"jobs": added})
|
|
3491
3563
|
models = sorted({j.label for j in added})
|
|
3492
|
-
self.notify(f"Added {len(added)} pack run(s) to the run going: {', '.join(models)}.
|
|
3564
|
+
self.notify(f"Added {len(added)} pack run(s) to the run going: {', '.join(models)}. x removes a pack not started yet.", timeout=10)
|
|
3493
3565
|
self.refresh_live()
|
|
3494
3566
|
|
|
3495
3567
|
def enqueue(self, item):
|
|
@@ -893,15 +893,42 @@ class HostedBeside(unittest.TestCase):
|
|
|
893
893
|
|
|
894
894
|
def test_hosted_run_beside_local_with_one_slot(self):
|
|
895
895
|
from tuieval import engine
|
|
896
|
-
|
|
896
|
+
together = set()
|
|
897
|
+
|
|
898
|
+
def on_event(kind, **d):
|
|
899
|
+
if kind == "request_started":
|
|
900
|
+
together.add(frozenset(e._lanes))
|
|
901
|
+
e = engine.Engine(on_event, root=self.ws)
|
|
897
902
|
self.assertEqual([e.hosted(e.model(l)) for l in ("loc", "h1", "h2")], [False, True, True])
|
|
898
903
|
self.assertEqual(e.models_at_once({"loc", "h1", "h2"}, 1, 4), 3)
|
|
899
904
|
jobs = e.plan(["loc", "h1", "h2"], ["apps"], None, "certify", False, {})
|
|
900
905
|
e.run(jobs, parallel=1)
|
|
901
906
|
self.assertEqual([j.status for j in jobs], ["done"] * 3, [(j.key, j.note) for j in jobs])
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
907
|
+
self.assertIn(frozenset({"loc", "h1", "h2"}), together) # neither hosted model waited for the local one
|
|
908
|
+
# hosted models share nothing of this machine: no one notes them alongside, and they note no one
|
|
909
|
+
for label in ("loc", "h1", "h2"):
|
|
910
|
+
self.assertFalse(any("ran_alongside" in r for r in self.results(label)), label)
|
|
911
|
+
data = json.loads(read(os.path.join(self.ws, "results", "h1", "apps.json")))
|
|
912
|
+
self.assertTrue(data["run"]["hosted"])
|
|
913
|
+
self.assertNotIn("loaded_alongside", json.loads(read(os.path.join(self.ws, "results", "loc", "apps.json")))["run"])
|
|
914
|
+
|
|
915
|
+
def test_older_results_drop_hosted_models_alongside(self):
|
|
916
|
+
from tuieval import compare
|
|
917
|
+
paths = []
|
|
918
|
+
# h1's results say it's hosted; or-gone (named at run time, results deleted) by its server's prefix
|
|
919
|
+
for label, run, along in (("loc", {}, ["h1", "loc2", "or-gone"]), ("h1", {"endpoint": "x/fp8"}, ["loc"])):
|
|
920
|
+
data = {"format": "evals/1", "model": {"label": label}, "pack": {"name": "p", "fingerprint": "f"},
|
|
921
|
+
"run": {**run, "loaded_alongside": along}, "settings": {},
|
|
922
|
+
"results": [{"test": "t", "description": "d", "pass": True, "score": 1.0, "reason": "",
|
|
923
|
+
"ran_alongside": along}]}
|
|
924
|
+
path = os.path.join(self.ws, "results", label, "p.json")
|
|
925
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
926
|
+
with open(path, "w") as f:
|
|
927
|
+
json.dump(data, f)
|
|
928
|
+
paths.append(path)
|
|
929
|
+
rows, infos = compare.load(paths)
|
|
930
|
+
self.assertEqual({r["model"]: r["alongside"] for r in rows}, {"loc": ["loc2"], "h1": []})
|
|
931
|
+
self.assertEqual({i["label"]: i["loaded_alongside"] for i in infos}, {"loc": ["loc2"], "h1": []})
|
|
905
932
|
|
|
906
933
|
def test_local_model_joins_a_hosted_only_run(self):
|
|
907
934
|
from tuieval import engine
|
|
@@ -1003,6 +1030,141 @@ class AddToRun(unittest.TestCase):
|
|
|
1003
1030
|
loads = [d["label"] for k, d in events if k == "model_loading"]
|
|
1004
1031
|
self.assertEqual(loads.count("a"), 2) # a's new pack: after it ended
|
|
1005
1032
|
|
|
1033
|
+
def test_engine_removes_jobs_not_started(self):
|
|
1034
|
+
from tuieval import engine
|
|
1035
|
+
events = []
|
|
1036
|
+
e = engine.Engine(lambda kind, **d: events.append((kind, d)), root=self.ws)
|
|
1037
|
+
jobs = e.plan(["a"], ["apps", "other"], None, "certify", False, {})
|
|
1038
|
+
t = threading.Thread(target=e.run, args=(jobs, 1))
|
|
1039
|
+
t.start()
|
|
1040
|
+
deadline = time.time() + 180 # CI machines can take long to start servers
|
|
1041
|
+
while not any(k == "request_done" for k, _ in events) and time.time() < deadline:
|
|
1042
|
+
time.sleep(0.05)
|
|
1043
|
+
added, _ = e.add_jobs(e.plan(["b"], ["apps"], None, "certify", False, {}))
|
|
1044
|
+
self.assertEqual([j.key for j in added], ["b/apps"])
|
|
1045
|
+
self.assertEqual(e.remove_jobs({"a/apps"}), []) # answering: k skips it instead
|
|
1046
|
+
# a's next pack (its server is up) and b (never started) both go
|
|
1047
|
+
self.assertEqual(sorted(j.key for j in e.remove_jobs({"a/other", "b/apps"})), ["a/other", "b/apps"])
|
|
1048
|
+
self.finish(e, t)
|
|
1049
|
+
self.assertEqual([(j.key, j.status) for j in jobs], [("a/apps", "done")])
|
|
1050
|
+
self.assertNotIn(("model_loading", "b"), [(k, d.get("label")) for k, d in events])
|
|
1051
|
+
self.assertFalse(os.path.exists(os.path.join(self.ws, "results", "a", "other.json")))
|
|
1052
|
+
self.assertEqual(e.remove_jobs({"a/apps"}), []) # the run is over
|
|
1053
|
+
|
|
1054
|
+
def test_tui_removes_a_pack_from_the_run(self):
|
|
1055
|
+
path = os.path.join(self.ws, "models.toml")
|
|
1056
|
+
write(path, read(path).replace('"--delay", "0.2"', '"--delay", "3"')) # still running while it's asked
|
|
1057
|
+
code = textwrap.dedent("""
|
|
1058
|
+
import asyncio, json, time
|
|
1059
|
+
from tuieval.tui import EvalsApp, RunScreen, ConfirmScreen
|
|
1060
|
+
from textual.widgets import SelectionList, DataTable
|
|
1061
|
+
async def go():
|
|
1062
|
+
app = EvalsApp({})
|
|
1063
|
+
async with app.run_test(size=(160, 50)) as pilot:
|
|
1064
|
+
await pilot.pause()
|
|
1065
|
+
s = app.screen
|
|
1066
|
+
s.query_one("#suites", SelectionList).select("apps")
|
|
1067
|
+
s.query_one("#suites", SelectionList).select("other")
|
|
1068
|
+
s.selected_models = {"a", "b"}
|
|
1069
|
+
s.refresh_models()
|
|
1070
|
+
s.query_one("#tier-certify").value = True
|
|
1071
|
+
await pilot.pause()
|
|
1072
|
+
s.action_start()
|
|
1073
|
+
deadline = time.time() + 180 # CI machines can take long to start servers
|
|
1074
|
+
while time.time() < deadline:
|
|
1075
|
+
await pilot.pause(0.1)
|
|
1076
|
+
if isinstance(app.screen, RunScreen) and app.screen.counts()[0]:
|
|
1077
|
+
break
|
|
1078
|
+
run, out = app.screen, {}
|
|
1079
|
+
q = run.query_one("#queue", DataTable)
|
|
1080
|
+
total = run.total
|
|
1081
|
+
q.move_cursor(row=q.get_row_index("b/apps"))
|
|
1082
|
+
await pilot.press("x")
|
|
1083
|
+
await pilot.pause()
|
|
1084
|
+
out["asked"] = isinstance(app.screen, ConfirmScreen) and app.screen.focused.id == "no"
|
|
1085
|
+
await pilot.press("n")
|
|
1086
|
+
await pilot.pause()
|
|
1087
|
+
out["kept"] = [j.key for j in run.jobs]
|
|
1088
|
+
q.move_cursor(row=q.get_row_index("b/apps"))
|
|
1089
|
+
await pilot.press("x")
|
|
1090
|
+
await pilot.pause()
|
|
1091
|
+
await pilot.press("y")
|
|
1092
|
+
await pilot.pause()
|
|
1093
|
+
out["after"] = [[j.key for j in run.jobs], list(map(str, (k.value for k in q.rows))),
|
|
1094
|
+
total - run.total]
|
|
1095
|
+
q.move_cursor(row=q.get_row_index("a/apps")) # running: stays, nothing asked
|
|
1096
|
+
await pilot.press("x")
|
|
1097
|
+
await pilot.pause()
|
|
1098
|
+
out["running"] = [isinstance(app.screen, ConfirmScreen), "a/apps" in [j.key for j in run.jobs]]
|
|
1099
|
+
app.engine.cancel()
|
|
1100
|
+
deadline = time.time() + 60
|
|
1101
|
+
while time.time() < deadline and run.running:
|
|
1102
|
+
await pilot.pause(0.2)
|
|
1103
|
+
print(json.dumps(out))
|
|
1104
|
+
asyncio.run(go())
|
|
1105
|
+
""")
|
|
1106
|
+
p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=600,
|
|
1107
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""))
|
|
1108
|
+
out = json.loads(p.stdout.strip().splitlines()[-1]) if p.stdout.strip() else p.stderr
|
|
1109
|
+
self.assertIsInstance(out, dict, out)
|
|
1110
|
+
self.assertTrue(out["asked"])
|
|
1111
|
+
self.assertIn("b/apps", out["kept"])
|
|
1112
|
+
jobs, rows, fewer = out["after"]
|
|
1113
|
+
self.assertNotIn("b/apps", jobs)
|
|
1114
|
+
self.assertNotIn("b/apps", rows)
|
|
1115
|
+
self.assertEqual(fewer, 4) # its 4 questions are no longer counted
|
|
1116
|
+
self.assertEqual(out["running"], [False, True])
|
|
1117
|
+
|
|
1118
|
+
def test_tui_asks_before_skipping_a_model(self):
|
|
1119
|
+
path = os.path.join(self.ws, "models.toml")
|
|
1120
|
+
write(path, read(path).replace('"--delay", "0.2"', '"--delay", "3"')) # still running while it's asked
|
|
1121
|
+
code = textwrap.dedent("""
|
|
1122
|
+
import asyncio, json, time
|
|
1123
|
+
from tuieval.tui import EvalsApp, RunScreen, ConfirmScreen
|
|
1124
|
+
from textual.widgets import SelectionList
|
|
1125
|
+
async def go():
|
|
1126
|
+
app = EvalsApp({})
|
|
1127
|
+
async with app.run_test(size=(160, 50)) as pilot:
|
|
1128
|
+
await pilot.pause()
|
|
1129
|
+
s = app.screen
|
|
1130
|
+
s.query_one("#suites", SelectionList).select("apps")
|
|
1131
|
+
s.selected_models = {"a"}
|
|
1132
|
+
s.refresh_models()
|
|
1133
|
+
s.query_one("#tier-certify").value = True
|
|
1134
|
+
await pilot.pause()
|
|
1135
|
+
s.action_start()
|
|
1136
|
+
deadline = time.time() + 180 # CI machines can take long to start servers
|
|
1137
|
+
while time.time() < deadline:
|
|
1138
|
+
await pilot.pause(0.1)
|
|
1139
|
+
if isinstance(app.screen, RunScreen) and app.screen.counts()[0]:
|
|
1140
|
+
break
|
|
1141
|
+
run, out = app.screen, {}
|
|
1142
|
+
for keys in (["k", "enter"], ["k", "k"], ["k", "n"], ["k", "escape"]): # each one: no
|
|
1143
|
+
await pilot.press(keys[0])
|
|
1144
|
+
await pilot.pause()
|
|
1145
|
+
out.setdefault("asked", []).append(isinstance(app.screen, ConfirmScreen)
|
|
1146
|
+
and app.screen.focused.id == "no")
|
|
1147
|
+
await pilot.press(keys[1])
|
|
1148
|
+
await pilot.pause()
|
|
1149
|
+
out["kept"] = [run.running, app.screen is run, [j.status for j in run.jobs]]
|
|
1150
|
+
await pilot.press("k")
|
|
1151
|
+
await pilot.pause()
|
|
1152
|
+
await pilot.press("y")
|
|
1153
|
+
deadline = time.time() + 120
|
|
1154
|
+
while time.time() < deadline and run.running:
|
|
1155
|
+
await pilot.pause(0.2)
|
|
1156
|
+
out["skipped"] = [j.status for j in run.jobs]
|
|
1157
|
+
print(json.dumps(out))
|
|
1158
|
+
asyncio.run(go())
|
|
1159
|
+
""")
|
|
1160
|
+
p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=600,
|
|
1161
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""))
|
|
1162
|
+
out = json.loads(p.stdout.strip().splitlines()[-1]) if p.stdout.strip() else p.stderr
|
|
1163
|
+
self.assertIsInstance(out, dict, out)
|
|
1164
|
+
self.assertEqual(out["asked"], [True] * 4)
|
|
1165
|
+
self.assertEqual(out["kept"], [True, True, ["running"]])
|
|
1166
|
+
self.assertEqual(out["skipped"], ["skipped"])
|
|
1167
|
+
|
|
1006
1168
|
def test_tui_adds_or_queues_while_running(self):
|
|
1007
1169
|
code = textwrap.dedent("""
|
|
1008
1170
|
import asyncio, json, time
|
|
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.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/pack.toml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev99 → tuieval-0.2.0.dev103}/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
|