tuieval 0.2.0.dev10__tar.gz → 0.2.0.dev14__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.dev10 → tuieval-0.2.0.dev14}/CHANGELOG.md +1 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/PKG-INFO +4 -3
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/README.md +3 -2
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/engine.py +39 -18
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/packs.py +9 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/run_evals.py +34 -4
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/tui.py +212 -19
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/tests/test_tuieval.py +109 -2
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/.gitignore +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/LICENSE +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/models.md +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/compare.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/tests/mock_server.py +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Run only some tests of a pack: `e` in the TUI, or `tuieval run --tests`. Runs add up; `--force` reruns just the picked tests. Presets keep the pick.
|
|
5
6
|
- `tuieval tune` scores options on projected full-length answers (`[tune] answer_tokens`, default 1024), so generation speed counts as in real runs.
|
|
6
7
|
- `tuieval tune` reports an option's memory cost instead of rejecting it; it rejects only on critical memory pressure or over `[tune] swap_limit_mb`.
|
|
7
8
|
- Warm-start tuning recognises fine-tunes whose GGUF describes the MTP draft layer differently (e.g. listing KV heads per layer) as the same model shape, so they start from an already-tuned sibling instead of tuning in full.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev14
|
|
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
|
|
@@ -32,7 +32,7 @@ Run the same eval packs against local models (llama.cpp GGUFs, LM Studio, Ollama
|
|
|
32
32
|
|
|
33
33
|
tuieval measures **accuracy, speed and token use** together, and gives a **PASS / FAIL / INCONCLUSIVE** verdict per use case. Grading is automatic; there's no LLM judge.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
You know your workload better than anyone. tuieval ships with **no built-in benchmark**.
|
|
36
36
|
Instead you build *eval packs* from what you actually do: questions with checkable answers, in your domain, with your rules
|
|
37
37
|
Then you get an answer to the questions that matter:
|
|
38
38
|
|
|
@@ -125,7 +125,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
|
|
|
125
125
|
|
|
126
126
|
## The TUI
|
|
127
127
|
|
|
128
|
-
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).
|
|
128
|
+
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.
|
|
129
129
|
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.
|
|
130
130
|
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). Runs started while one is going are queued.
|
|
131
131
|
4. **Press `r` for results:** production readiness, verdict history, scorecard, speed & tokens (★ marks models nothing beats on both accuracy and time), per question, is the difference real?, tests that separate models, by difficulty, failures, and test quality.
|
|
@@ -137,6 +137,7 @@ Other keys: `t` tunes the ticked models' speed flags, `a` adds a model, `m` scan
|
|
|
137
137
|
```bash
|
|
138
138
|
tuieval run # Screen every model on every pack
|
|
139
139
|
tuieval run --tier certify --only a,b --packs support-bot,coding
|
|
140
|
+
tuieval run --packs coding --tests parse-dates,fix-bug # only these tests (or --tests coding:parse-dates)
|
|
140
141
|
tuieval run --dry-run # the plan and server commands
|
|
141
142
|
tuieval verdict # PASS / FAIL / INCONCLUSIVE per model and use case
|
|
142
143
|
tuieval report # the same with evidence, as a markdown file
|
|
@@ -6,7 +6,7 @@ Run the same eval packs against local models (llama.cpp GGUFs, LM Studio, Ollama
|
|
|
6
6
|
|
|
7
7
|
tuieval measures **accuracy, speed and token use** together, and gives a **PASS / FAIL / INCONCLUSIVE** verdict per use case. Grading is automatic; there's no LLM judge.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
You know your workload better than anyone. tuieval ships with **no built-in benchmark**.
|
|
10
10
|
Instead you build *eval packs* from what you actually do: questions with checkable answers, in your domain, with your rules
|
|
11
11
|
Then you get an answer to the questions that matter:
|
|
12
12
|
|
|
@@ -99,7 +99,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
|
|
|
99
99
|
|
|
100
100
|
## The TUI
|
|
101
101
|
|
|
102
|
-
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).
|
|
102
|
+
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.
|
|
103
103
|
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.
|
|
104
104
|
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). Runs started while one is going are queued.
|
|
105
105
|
4. **Press `r` for results:** production readiness, verdict history, scorecard, speed & tokens (★ marks models nothing beats on both accuracy and time), per question, is the difference real?, tests that separate models, by difficulty, failures, and test quality.
|
|
@@ -111,6 +111,7 @@ Other keys: `t` tunes the ticked models' speed flags, `a` adds a model, `m` scan
|
|
|
111
111
|
```bash
|
|
112
112
|
tuieval run # Screen every model on every pack
|
|
113
113
|
tuieval run --tier certify --only a,b --packs support-bot,coding
|
|
114
|
+
tuieval run --packs coding --tests parse-dates,fix-bug # only these tests (or --tests coding:parse-dates)
|
|
114
115
|
tuieval run --dry-run # the plan and server commands
|
|
115
116
|
tuieval verdict # PASS / FAIL / INCONCLUSIVE per model and use case
|
|
116
117
|
tuieval report # the same with evidence, as a markdown file
|
|
@@ -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.dev14'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev14')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -38,6 +38,7 @@ import mimetypes
|
|
|
38
38
|
import os
|
|
39
39
|
import random
|
|
40
40
|
import re
|
|
41
|
+
import shutil
|
|
41
42
|
import signal
|
|
42
43
|
import socket
|
|
43
44
|
import statistics
|
|
@@ -358,6 +359,7 @@ def load_presets(path):
|
|
|
358
359
|
tags = ["moe"] # models with any of these tags …
|
|
359
360
|
models = ["foo", "bar"] # … and/or these labels (omit both for every model)
|
|
360
361
|
repeat = 3
|
|
362
|
+
tests = { coding = ["parse-dates"] } # optional: only these tests of a pack
|
|
361
363
|
"""
|
|
362
364
|
if not os.path.isfile(path):
|
|
363
365
|
return {}
|
|
@@ -366,19 +368,24 @@ def load_presets(path):
|
|
|
366
368
|
|
|
367
369
|
|
|
368
370
|
def resolve_preset(cfg, preset, pack_names):
|
|
369
|
-
"""(labels, packs, repeat) for a preset; unknown names are dropped.
|
|
371
|
+
"""(labels, packs, repeat, tests) for a preset; unknown names are dropped. tests: {pack: [test
|
|
372
|
+
ids]} for packs where only some tests run."""
|
|
370
373
|
labels = [m["label"] for m in cfg["models"]]
|
|
371
374
|
if preset.get("models") or preset.get("tags"):
|
|
372
375
|
want, tags = set(preset.get("models", [])), set(preset.get("tags", []))
|
|
373
376
|
labels = [m["label"] for m in cfg["models"] if m["label"] in want or tags & set(m["tags"])]
|
|
374
377
|
packs = [p for p in preset.get("packs", pack_names) if p in pack_names]
|
|
375
|
-
|
|
378
|
+
tests = {p: list(ids) for p, ids in (preset.get("tests") or {}).items() if p in packs and ids}
|
|
379
|
+
return labels, packs, preset.get("repeat"), tests
|
|
376
380
|
|
|
377
381
|
|
|
378
|
-
def save_preset(path, name, labels, packs, repeat):
|
|
382
|
+
def save_preset(path, name, labels, packs, repeat, tests=None):
|
|
379
383
|
"""Add or replace one preset, keeping the rest of the file as written."""
|
|
384
|
+
tests = {p: ids for p, ids in (tests or {}).items() if p in packs and ids}
|
|
380
385
|
block = (f"[{name}]\nmodels = [{', '.join(json.dumps(l) for l in labels)}]\n"
|
|
381
|
-
f"packs = [{', '.join(json.dumps(p) for p in packs)}]\n" + (f"repeat = {int(repeat)}\n" if repeat else "")
|
|
386
|
+
f"packs = [{', '.join(json.dumps(p) for p in packs)}]\n" + (f"repeat = {int(repeat)}\n" if repeat else "")
|
|
387
|
+
+ ("tests = { " + ", ".join(f"{json.dumps(p)} = [{', '.join(json.dumps(i) for i in ids)}]"
|
|
388
|
+
for p, ids in tests.items()) + " }\n" if tests else ""))
|
|
382
389
|
text = open(path).read() if os.path.isfile(path) else \
|
|
383
390
|
"# Saved selections for the TUI (p) and tuieval run --preset <name>. See engine.load_presets.\n"
|
|
384
391
|
pattern = re.compile(rf"^\[{re.escape(name)}\]\n(?:(?!\[).*\n?)*", re.MULTILINE)
|
|
@@ -394,12 +401,13 @@ class Job:
|
|
|
394
401
|
model: dict
|
|
395
402
|
pack: packs_mod.Pack
|
|
396
403
|
repeat: int
|
|
397
|
-
tests: list # the tests this tier runs
|
|
404
|
+
tests: list # the tests this tier runs (or the ones picked)
|
|
398
405
|
out_path: str
|
|
399
406
|
sampling: dict
|
|
400
407
|
tier: str = "certify"
|
|
401
408
|
settings: dict = dataclasses.field(default_factory=dict) # sampling + serving identity (fingerprinted)
|
|
402
409
|
merge: bool = True # keep matching earlier results and only run what's missing
|
|
410
|
+
redo: bool = False # rerun the picked tests, keeping the pack's other answers
|
|
403
411
|
status: str = "waiting" # waiting, loading, running, done, failed, skipped
|
|
404
412
|
note: str = ""
|
|
405
413
|
discards: list = dataclasses.field(default_factory=list) # [(what, answers, why)] set aside on start
|
|
@@ -868,7 +876,7 @@ class Engine:
|
|
|
868
876
|
if job.merge:
|
|
869
877
|
data = self._result(job.out_path)
|
|
870
878
|
if data and data["pack"]["fingerprint"] == job.pack.fingerprint and settings_match(data, job.settings):
|
|
871
|
-
kept = data["results"]
|
|
879
|
+
kept = self._kept(job, data["results"])
|
|
872
880
|
if kept:
|
|
873
881
|
earlier = self._file_sittings(data["run"])
|
|
874
882
|
head, new = self._partial_lines(job.out_path + ".partial.jsonl")
|
|
@@ -877,6 +885,14 @@ class Engine:
|
|
|
877
885
|
saved = self._saved_sittings(job)
|
|
878
886
|
return kept, new, earlier + saved + self._rebuild_sittings(new, saved + earlier)
|
|
879
887
|
|
|
888
|
+
@staticmethod
|
|
889
|
+
def _kept(job, rows):
|
|
890
|
+
"""A finished file's rows that stay: all of them, less the picked tests' when redoing them."""
|
|
891
|
+
if not job.redo:
|
|
892
|
+
return rows
|
|
893
|
+
redo = {t["id"] for t in job.tests}
|
|
894
|
+
return [r for r in rows if r["test"] not in redo]
|
|
895
|
+
|
|
880
896
|
def _fill_progress(self, job):
|
|
881
897
|
"""Set a job's done/passed/failed and earlier sittings from what's stored."""
|
|
882
898
|
kept, new, job.earlier = self._stored(job)
|
|
@@ -927,11 +943,13 @@ class Engine:
|
|
|
927
943
|
return not current and header_matches(head, pack.fingerprint, settings)
|
|
928
944
|
|
|
929
945
|
# ---- planning
|
|
930
|
-
def plan(self, labels, pack_names, repeat=None, tier="screen", force=False):
|
|
946
|
+
def plan(self, labels, pack_names, repeat=None, tier="screen", force=False, tests=None):
|
|
931
947
|
"""Jobs for models x packs. tier: smoke (3 tests), screen (a spread sample), certify (all
|
|
932
948
|
tests). repeat: times each question is asked; None = the tier default (1 for smoke and
|
|
933
|
-
screen, the pack's certification repeats for certify).
|
|
934
|
-
|
|
949
|
+
screen, the pack's certification repeats for certify). tests: {pack: [test ids]} runs just
|
|
950
|
+
those tests of a pack instead of the tier's sample (unknown ids raise PackError). Earlier
|
|
951
|
+
matching results are kept and only missing answers are run, unless force; force with
|
|
952
|
+
picked tests reruns those and keeps the pack's other answers."""
|
|
935
953
|
if tier not in TIERS:
|
|
936
954
|
raise ValueError(f"tier must be one of {TIERS}")
|
|
937
955
|
jobs = []
|
|
@@ -940,8 +958,10 @@ class Engine:
|
|
|
940
958
|
for name in pack_names:
|
|
941
959
|
pack = self.packs[name]
|
|
942
960
|
reps = repeat or self.default_repeat(pack, tier)
|
|
943
|
-
|
|
944
|
-
|
|
961
|
+
picked = (tests or {}).get(name)
|
|
962
|
+
job = Job(m, pack, reps, pack.pick(picked) if picked else pack.select(tier),
|
|
963
|
+
self.result_path(label, name, tier), effective_sampling(self.cfg, m), tier=tier,
|
|
964
|
+
merge=not force or bool(picked), redo=force and bool(picked), settings=self.settings(m))
|
|
945
965
|
sv = self.serving(m)
|
|
946
966
|
status = self.result_status(label, name, tier)
|
|
947
967
|
job.discards = self._discards(job)
|
|
@@ -958,7 +978,7 @@ class Engine:
|
|
|
958
978
|
elif missing:
|
|
959
979
|
job.status, job.note = "skipped", f"{', '.join(missing)} not installed in tuieval's Python"
|
|
960
980
|
elif status and status.startswith("outdated"):
|
|
961
|
-
job.merge, job.note = False, status.split(": ", 1)[-1] + ", rerunning"
|
|
981
|
+
job.merge, job.redo, job.note = False, False, status.split(": ", 1)[-1] + ", rerunning"
|
|
962
982
|
if not force: # a rerun under the new settings already under way resumes
|
|
963
983
|
self._fill_progress(job)
|
|
964
984
|
if job.done:
|
|
@@ -1656,7 +1676,7 @@ class Engine:
|
|
|
1656
1676
|
data = self._result(job.out_path)
|
|
1657
1677
|
if data and data["pack"]["fingerprint"] == job.pack.fingerprint and \
|
|
1658
1678
|
settings_match(data, job.settings):
|
|
1659
|
-
kept = data["results"]
|
|
1679
|
+
kept = self._kept(job, data["results"])
|
|
1660
1680
|
if kept:
|
|
1661
1681
|
job.earlier = self._file_sittings(data["run"])
|
|
1662
1682
|
new = []
|
|
@@ -1789,18 +1809,19 @@ class Engine:
|
|
|
1789
1809
|
job.note = f"{job.passed}/{job.done} passed"
|
|
1790
1810
|
self.emit("job_done", job=job)
|
|
1791
1811
|
|
|
1792
|
-
def _archive(self, job):
|
|
1793
|
-
"""Keep a result file that is about to be replaced (rerun or outdated) in history
|
|
1812
|
+
def _archive(self, job, keep=False):
|
|
1813
|
+
"""Keep a result file that is about to be replaced (rerun or outdated) in history/; keep:
|
|
1814
|
+
copy it (some of its answers stay in the new file)."""
|
|
1794
1815
|
if job.tier == "smoke" or not os.path.isfile(job.out_path):
|
|
1795
1816
|
return
|
|
1796
1817
|
hist = os.path.join(os.path.dirname(job.out_path), "history")
|
|
1797
1818
|
os.makedirs(hist, exist_ok=True)
|
|
1798
1819
|
stamp = time.strftime("%Y%m%d-%H%M%S", time.localtime(os.path.getmtime(job.out_path)))
|
|
1799
|
-
os.replace(job.out_path, os.path.join(hist, f"{job.pack.name}-{stamp}.json"))
|
|
1820
|
+
(shutil.copy2 if keep else os.replace)(job.out_path, os.path.join(hist, f"{job.pack.name}-{stamp}.json"))
|
|
1800
1821
|
|
|
1801
1822
|
def _write_result(self, job, records, run_info):
|
|
1802
|
-
if not job.merge:
|
|
1803
|
-
self._archive(job)
|
|
1823
|
+
if not job.merge or job.redo:
|
|
1824
|
+
self._archive(job, keep=job.redo)
|
|
1804
1825
|
m = job.model
|
|
1805
1826
|
data = {
|
|
1806
1827
|
"format": RESULT_FORMAT,
|
|
@@ -55,6 +55,15 @@ class Pack:
|
|
|
55
55
|
"""Python modules the pack needs (the `needs` entries that aren't capabilities)."""
|
|
56
56
|
return [n for n in self.needs if n not in CAPABILITIES]
|
|
57
57
|
|
|
58
|
+
def pick(self, ids):
|
|
59
|
+
"""The tests with these ids, in the pack's order. Unknown ids raise PackError."""
|
|
60
|
+
known = {t["id"] for t in self.tests}
|
|
61
|
+
bad = [i for i in ids if i not in known]
|
|
62
|
+
if bad:
|
|
63
|
+
raise PackError(f"pack {self.name!r} has no test {', '.join(map(repr, bad))}; "
|
|
64
|
+
f"its tests are: {', '.join(t['id'] for t in self.tests)}")
|
|
65
|
+
return [t for t in self.tests if t["id"] in set(ids)]
|
|
66
|
+
|
|
58
67
|
def select(self, tier):
|
|
59
68
|
"""The tests a tier runs: smoke = first 3; screen = `screen` tests spread evenly over
|
|
60
69
|
categories (one variant per group); certify = all."""
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
tuieval capture logs/live/<file> --pack X # turn a real failure into a new test
|
|
11
11
|
tuieval run --only my-model --packs my-pack,other-pack
|
|
12
12
|
tuieval run --tags moe --packs my-pack # models tagged "moe"
|
|
13
|
+
tuieval run --packs my-pack --tests a,b # only these tests of a pack (or --tests my-pack:a,other:b)
|
|
13
14
|
tuieval run --preset nightly # a selection saved in the TUI (presets.toml)
|
|
14
15
|
tuieval run --tier smoke --only my-model # 3 tests per pack, 1 repeat: check paths and flags
|
|
15
16
|
tuieval run --tier smoke --only openrouter:qwen/qwen3-32b # any OpenRouter model, no models.toml edit
|
|
@@ -36,6 +37,7 @@ import sys
|
|
|
36
37
|
import threading
|
|
37
38
|
|
|
38
39
|
from . import engine
|
|
40
|
+
from . import packs as packs_mod
|
|
39
41
|
from . import workspace
|
|
40
42
|
|
|
41
43
|
BOLD, DIM, RED, GREEN, CYAN, YELLOW, RESET = "\033[1m", "\033[2m", "\033[31m", "\033[32m", "\033[36m", "\033[33m", "\033[0m"
|
|
@@ -605,6 +607,27 @@ COMMANDS = {"add": cmd_add, "list": cmd_list, "scan": cmd_scan, "regrade": cmd_r
|
|
|
605
607
|
"export": cmd_export, "remove": cmd_remove}
|
|
606
608
|
|
|
607
609
|
|
|
610
|
+
def parse_tests(text, pack_names, known):
|
|
611
|
+
"""({pack: [test ids]}, packs to run) from --tests. A bare id needs exactly one pack chosen;
|
|
612
|
+
pack:id names its pack, and without --packs the packs named are the ones that run."""
|
|
613
|
+
tests = {}
|
|
614
|
+
for item in filter(None, (x.strip() for x in text.split(","))):
|
|
615
|
+
pack, _, tid = item.rpartition(":")
|
|
616
|
+
if not pack:
|
|
617
|
+
if not pack_names or len(pack_names) != 1:
|
|
618
|
+
sys.exit(f"--tests {item}: say which pack, as pack:{item} (or choose one pack with --packs)")
|
|
619
|
+
pack = pack_names[0]
|
|
620
|
+
if pack not in known:
|
|
621
|
+
sys.exit(f"--tests {item}: unknown pack {pack!r}; packs are {', '.join(known)}")
|
|
622
|
+
tests.setdefault(pack, []).append(tid)
|
|
623
|
+
if pack_names is None:
|
|
624
|
+
pack_names = list(tests)
|
|
625
|
+
missing = [p for p in tests if p not in pack_names]
|
|
626
|
+
if missing:
|
|
627
|
+
sys.exit(f"--tests names pack(s) {missing} that aren't in --packs")
|
|
628
|
+
return tests, pack_names
|
|
629
|
+
|
|
630
|
+
|
|
608
631
|
def main(argv=None):
|
|
609
632
|
"""tuieval run [options] (argv without the "run")."""
|
|
610
633
|
argv = sys.argv[1:] if argv is None else argv
|
|
@@ -615,7 +638,9 @@ def main(argv=None):
|
|
|
615
638
|
p.add_argument("--tags", help="comma-separated tags; models having any of them")
|
|
616
639
|
p.add_argument("--preset", help="a saved selection from presets.toml")
|
|
617
640
|
p.add_argument("--packs", help="comma-separated packs (default: all)")
|
|
618
|
-
p.add_argument("--
|
|
641
|
+
p.add_argument("--tests", help="comma-separated test ids to run instead of the tier's sample: id with one "
|
|
642
|
+
"pack in --packs, else pack:id (and --packs defaults to those packs)")
|
|
643
|
+
p.add_argument("--force", action="store_true", help="rerun packs (or the --tests) that already have current results")
|
|
619
644
|
p.add_argument("--repeat", type=int, help="times each question is asked, any tier (default: 1 for smoke and "
|
|
620
645
|
"screen, the pack's certification count for certify)")
|
|
621
646
|
p.add_argument("--tier", choices=engine.TIERS, default="screen",
|
|
@@ -642,7 +667,7 @@ def main(argv=None):
|
|
|
642
667
|
labels = [l for l in labels if want & set(e.model(l)["tags"])]
|
|
643
668
|
if not labels:
|
|
644
669
|
sys.exit(f"no models tagged {sorted(want)}")
|
|
645
|
-
pack_names = list(e.packs)
|
|
670
|
+
pack_names, tests = list(e.packs), {}
|
|
646
671
|
if not pack_names:
|
|
647
672
|
sys.exit("no packs in this workspace yet: tuieval new-pack <name> creates one (see docs/writing-packs.md)")
|
|
648
673
|
if args.preset:
|
|
@@ -650,14 +675,19 @@ def main(argv=None):
|
|
|
650
675
|
"presets.toml"))
|
|
651
676
|
if args.preset not in presets:
|
|
652
677
|
sys.exit(f"unknown preset {args.preset!r}; presets: {', '.join(presets) or 'none (save one in the TUI with p)'}")
|
|
653
|
-
labels, pack_names, preset_repeat = engine.resolve_preset(e.cfg, presets[args.preset], list(e.packs))
|
|
678
|
+
labels, pack_names, preset_repeat, tests = engine.resolve_preset(e.cfg, presets[args.preset], list(e.packs))
|
|
654
679
|
args.repeat = args.repeat or preset_repeat
|
|
655
680
|
if args.packs:
|
|
656
681
|
pack_names = [x.strip() for x in args.packs.split(",")]
|
|
657
682
|
bad = [x for x in pack_names if x not in e.packs]
|
|
658
683
|
if bad:
|
|
659
684
|
sys.exit(f"unknown pack(s) {bad}; packs are {', '.join(e.packs)}")
|
|
660
|
-
|
|
685
|
+
if args.tests:
|
|
686
|
+
tests, pack_names = parse_tests(args.tests, pack_names if (args.packs or args.preset) else None, e.packs)
|
|
687
|
+
try:
|
|
688
|
+
jobs = e.plan(labels, pack_names, args.repeat, args.tier, args.force, tests)
|
|
689
|
+
except packs_mod.PackError as err:
|
|
690
|
+
sys.exit(str(err))
|
|
661
691
|
|
|
662
692
|
if args.dry_run:
|
|
663
693
|
seen = set()
|
|
@@ -21,6 +21,7 @@ import argparse
|
|
|
21
21
|
import glob
|
|
22
22
|
import json
|
|
23
23
|
import os
|
|
24
|
+
import re
|
|
24
25
|
import threading
|
|
25
26
|
import time
|
|
26
27
|
|
|
@@ -398,6 +399,7 @@ class SetupScreen(Screen):
|
|
|
398
399
|
Binding("a", "add_model", "Add model"),
|
|
399
400
|
Binding("m", "scan", "Scan for models"),
|
|
400
401
|
Binding("p", "presets", "Presets"),
|
|
402
|
+
Binding("e", "pick_tests", "Pick tests"),
|
|
401
403
|
Binding("t", "tune", "Tune speed"),
|
|
402
404
|
Binding("r", "results", "Results"),
|
|
403
405
|
Binding("x", "hide_model", "Hide/unhide"),
|
|
@@ -426,7 +428,7 @@ class SetupScreen(Screen):
|
|
|
426
428
|
yield Static(id="machine")
|
|
427
429
|
with Horizontal(id="pickers"):
|
|
428
430
|
with Vertical(classes="picker"):
|
|
429
|
-
yield Label("[b]Eval packs[/b] (space to toggle)")
|
|
431
|
+
yield Label("[b]Eval packs[/b] (space to toggle · e picks tests)")
|
|
430
432
|
yield SelectionList(id="suites")
|
|
431
433
|
with Vertical(classes="picker"):
|
|
432
434
|
yield Label(id="models-legend")
|
|
@@ -455,6 +457,7 @@ class SetupScreen(Screen):
|
|
|
455
457
|
|
|
456
458
|
def on_mount(self):
|
|
457
459
|
self.selected_models = set()
|
|
460
|
+
self.picks = {} # pack -> test ids to run instead of the tier's usual tests
|
|
458
461
|
self.refresh_machine()
|
|
459
462
|
self.refresh_packs()
|
|
460
463
|
self.refresh_models()
|
|
@@ -471,14 +474,19 @@ class SetupScreen(Screen):
|
|
|
471
474
|
def refresh_packs(self):
|
|
472
475
|
e = self.app.engine
|
|
473
476
|
packs = self.query_one("#suites", SelectionList)
|
|
474
|
-
keep = set(packs.selected)
|
|
477
|
+
keep, at = set(packs.selected), packs.highlighted
|
|
475
478
|
packs.clear_options()
|
|
476
479
|
for name, pk in e.packs.items():
|
|
480
|
+
picked = self.current_picks().get(name)
|
|
481
|
+
count = (f"[b cyan]{len(picked)}/{len(pk.tests)} tests picked[/]" if picked
|
|
482
|
+
else f"[dim]{len(pk.tests)} tests[/dim]")
|
|
477
483
|
needs = f" [magenta]{', '.join(pk.needs)}[/magenta]" if pk.needs else ""
|
|
478
484
|
mix = {lvl: sum(t.get("difficulty") == lvl for t in pk.tests) for lvl in ("easy", "medium", "hard")}
|
|
479
485
|
mixed = " ".join(f"[{DIFF_STYLE[l]}]{n}{l[0].upper()}[/]" for l, n in mix.items() if n)
|
|
480
|
-
packs.add_option(Selection(f"[dim]{pk.group} ·[/dim] {pk.label}
|
|
486
|
+
packs.add_option(Selection(f"[dim]{pk.group} ·[/dim] {pk.label} {count} {mixed}{needs}",
|
|
481
487
|
name, name in keep))
|
|
488
|
+
if at is not None and at < packs.option_count:
|
|
489
|
+
packs.highlighted = at
|
|
482
490
|
if not e.packs:
|
|
483
491
|
for i, line in enumerate(NO_PACKS):
|
|
484
492
|
packs.add_option(Selection(line, f"\x00nopacks:{i}", disabled=True))
|
|
@@ -637,6 +645,38 @@ class SetupScreen(Screen):
|
|
|
637
645
|
list(self.query_one("#suites", SelectionList).selected), repeat,
|
|
638
646
|
self.tier(), self.query_one("#force", Checkbox).value)
|
|
639
647
|
|
|
648
|
+
def current_picks(self, suites=None):
|
|
649
|
+
"""{pack: picked test ids} for packs that still have those tests (and, given suites, are ticked)."""
|
|
650
|
+
e = self.app.engine
|
|
651
|
+
out = {}
|
|
652
|
+
for name, ids in getattr(self, "picks", {}).items():
|
|
653
|
+
pk = e.packs.get(name)
|
|
654
|
+
known = [i for i in ids if pk and any(t["id"] == i for t in pk.tests)]
|
|
655
|
+
if known and (suites is None or name in suites):
|
|
656
|
+
out[name] = known
|
|
657
|
+
return out
|
|
658
|
+
|
|
659
|
+
def action_pick_tests(self):
|
|
660
|
+
"""Choose which tests of the highlighted pack run (none ticked: the tier's usual tests)."""
|
|
661
|
+
packs = self.query_one("#suites", SelectionList)
|
|
662
|
+
opt = packs.highlighted_option
|
|
663
|
+
if opt is None or opt.value not in self.app.engine.packs:
|
|
664
|
+
self.notify("Highlight a pack first.", severity="warning")
|
|
665
|
+
return
|
|
666
|
+
name = opt.value
|
|
667
|
+
|
|
668
|
+
def done(ids):
|
|
669
|
+
if ids is None:
|
|
670
|
+
return
|
|
671
|
+
if ids:
|
|
672
|
+
self.picks[name] = ids
|
|
673
|
+
packs.select(name)
|
|
674
|
+
else:
|
|
675
|
+
self.picks.pop(name, None)
|
|
676
|
+
self.refresh_packs()
|
|
677
|
+
self.update_estimate()
|
|
678
|
+
self.app.push_screen(PickTestsScreen(self.app.engine.packs[name], self.current_picks().get(name, [])), done)
|
|
679
|
+
|
|
640
680
|
@on(SelectionList.SelectedChanged)
|
|
641
681
|
@on(Input.Changed)
|
|
642
682
|
@on(Checkbox.Changed)
|
|
@@ -658,10 +698,11 @@ class SetupScreen(Screen):
|
|
|
658
698
|
if not labels or not suites:
|
|
659
699
|
est.update("[dim]Select at least one eval pack and one model.[/dim]" + self.queue_hint())
|
|
660
700
|
return
|
|
661
|
-
|
|
701
|
+
picks = self.current_picks(suites)
|
|
702
|
+
jobs = self.app.engine.plan(labels, suites, repeat, tier, force, picks)
|
|
662
703
|
rerun = not any(j.status == "waiting" for j in jobs)
|
|
663
704
|
if rerun: # everything has results: Start runs it again, so estimate that
|
|
664
|
-
jobs = self.app.engine.plan(labels, suites, repeat, tier,
|
|
705
|
+
jobs = self.app.engine.plan(labels, suites, repeat, tier, True, picks)
|
|
665
706
|
run = [j for j in jobs if j.status == "waiting"]
|
|
666
707
|
skipped = [j for j in jobs if j.status == "skipped"]
|
|
667
708
|
secs, notes = self.app.engine.estimate_seconds(jobs)
|
|
@@ -672,7 +713,10 @@ class SetupScreen(Screen):
|
|
|
672
713
|
f"[b]{reps_text}[/b] repeat(s) = "
|
|
673
714
|
f"[b]{todo:,}[/b] answers · about [b]{fmt_secs(secs)}[/b]"
|
|
674
715
|
+ (f" [dim](rough: {'; '.join(notes)})[/dim]" if notes else ""))
|
|
675
|
-
if
|
|
716
|
+
if picks:
|
|
717
|
+
text += f"\n[cyan]Picked tests only in {', '.join(e.packs[p].label for p in picks)}[/cyan]" \
|
|
718
|
+
"[dim] (e changes; a pack with picked tests can't PASS until all its tests have run)[/dim]"
|
|
719
|
+
elif tier == "screen":
|
|
676
720
|
text += "\n[dim]Screening can only say FAIL or promising; PASS needs Certify.[/dim]"
|
|
677
721
|
finished = [j for j in jobs if j.status == "done"]
|
|
678
722
|
if finished:
|
|
@@ -708,6 +752,8 @@ class SetupScreen(Screen):
|
|
|
708
752
|
return
|
|
709
753
|
e = self.app.engine
|
|
710
754
|
self.selected_models = {l for l in sel.get("models", []) if any(m["label"] == l for m in e.cfg["models"])}
|
|
755
|
+
self.picks = {p: list(ids) for p, ids in (sel.get("tests") or {}).items()}
|
|
756
|
+
self.refresh_packs()
|
|
711
757
|
packs = self.query_one("#suites", SelectionList)
|
|
712
758
|
packs.deselect_all()
|
|
713
759
|
for name in sel.get("packs", []):
|
|
@@ -726,13 +772,14 @@ class SetupScreen(Screen):
|
|
|
726
772
|
|
|
727
773
|
def action_presets(self):
|
|
728
774
|
labels, packs, repeat, _, _ = self.settings()
|
|
775
|
+
tests = self.current_picks(packs)
|
|
729
776
|
|
|
730
777
|
def done(result):
|
|
731
778
|
if result and result[0] == "load":
|
|
732
779
|
self.apply_selection(result[1])
|
|
733
780
|
elif result and result[0] == "saved":
|
|
734
781
|
self.notify(f"Saved preset '{result[1]}'. Also usable as tuieval run --preset {result[1]}")
|
|
735
|
-
self.app.push_screen(PresetsScreen(labels, packs, repeat), done)
|
|
782
|
+
self.app.push_screen(PresetsScreen(labels, packs, repeat, tests), done)
|
|
736
783
|
|
|
737
784
|
def action_scan(self):
|
|
738
785
|
def done(added):
|
|
@@ -750,13 +797,14 @@ class SetupScreen(Screen):
|
|
|
750
797
|
if not labels or not suites:
|
|
751
798
|
self.notify("Select at least one eval pack and one model.", severity="warning")
|
|
752
799
|
return
|
|
753
|
-
|
|
754
|
-
|
|
800
|
+
picks = self.current_picks(suites)
|
|
801
|
+
sel = {"models": labels, "packs": suites, "repeat": repeat, "tier": tier, "force": force, "tests": picks}
|
|
802
|
+
jobs = self.app.engine.plan(labels, suites, repeat, tier, force, picks)
|
|
755
803
|
rerun = False
|
|
756
804
|
if not any(j.status == "waiting" for j in jobs):
|
|
757
805
|
# Everything selected already has results: Start means run it again (earlier results
|
|
758
806
|
# are kept in history/). Packs skipped for another reason (no vision, …) stay skipped.
|
|
759
|
-
jobs = self.app.engine.plan(labels, suites, repeat, tier,
|
|
807
|
+
jobs = self.app.engine.plan(labels, suites, repeat, tier, True, picks)
|
|
760
808
|
if not any(j.status == "waiting" for j in jobs):
|
|
761
809
|
reasons = sorted({j.note for j in jobs if j.status == "skipped"})
|
|
762
810
|
self.notify(f"Nothing selected can run: {'; '.join(reasons)}", severity="warning")
|
|
@@ -1102,9 +1150,9 @@ class PresetsScreen(ModalScreen):
|
|
|
1102
1150
|
"""Load a saved selection, or save the current one (presets.toml)."""
|
|
1103
1151
|
BINDINGS = [Binding("escape", "cancel", "Cancel")]
|
|
1104
1152
|
|
|
1105
|
-
def __init__(self, labels, packs, repeat):
|
|
1153
|
+
def __init__(self, labels, packs, repeat, tests=None):
|
|
1106
1154
|
super().__init__()
|
|
1107
|
-
self.current = (labels, packs, repeat)
|
|
1155
|
+
self.current = (labels, packs, repeat, tests or {})
|
|
1108
1156
|
|
|
1109
1157
|
def compose(self) -> ComposeResult:
|
|
1110
1158
|
with Vertical(id="dialog"):
|
|
@@ -1123,7 +1171,8 @@ class PresetsScreen(ModalScreen):
|
|
|
1123
1171
|
for name, pr in self.presets.items():
|
|
1124
1172
|
who = ", ".join(pr.get("models", [])) or ("tags " + ", ".join(pr["tags"]) if pr.get("tags") else "all models")
|
|
1125
1173
|
opts.add_option(f"[b]{name}[/b] [dim]{', '.join(pr.get('packs', ['all packs']))} · {who}"
|
|
1126
|
-
f"{' · repeat ' + str(pr['repeat']) if pr.get('repeat') else ''}
|
|
1174
|
+
f"{' · repeat ' + str(pr['repeat']) if pr.get('repeat') else ''}"
|
|
1175
|
+
f"{' · picked tests in ' + ', '.join(pr['tests']) if pr.get('tests') else ''}[/dim]")
|
|
1127
1176
|
if not self.presets:
|
|
1128
1177
|
opts.add_option("[dim]No presets yet: select models and packs, then save them here.[/dim]")
|
|
1129
1178
|
opts.disabled = True
|
|
@@ -1131,14 +1180,15 @@ class PresetsScreen(ModalScreen):
|
|
|
1131
1180
|
@on(OptionList.OptionSelected, "#presets")
|
|
1132
1181
|
def load(self, event):
|
|
1133
1182
|
name = list(self.presets)[event.option_index]
|
|
1134
|
-
labels, packs, repeat = engine.resolve_preset(self.app.engine.cfg, self.presets[name],
|
|
1135
|
-
|
|
1183
|
+
labels, packs, repeat, tests = engine.resolve_preset(self.app.engine.cfg, self.presets[name],
|
|
1184
|
+
list(self.app.engine.packs))
|
|
1185
|
+
self.dismiss(("load", {"models": labels, "packs": packs, "repeat": repeat, "tests": tests}))
|
|
1136
1186
|
|
|
1137
1187
|
@on(Button.Pressed, "#save")
|
|
1138
1188
|
@on(Input.Submitted, "#name")
|
|
1139
1189
|
def save(self):
|
|
1140
1190
|
name = self.query_one("#name", Input).value.strip()
|
|
1141
|
-
labels, packs, repeat = self.current
|
|
1191
|
+
labels, packs, repeat, tests = self.current
|
|
1142
1192
|
err = self.query_one("#error", Static)
|
|
1143
1193
|
if not name or not engine.LABEL_RE.match(name):
|
|
1144
1194
|
err.update("[red]Use lowercase letters, digits, '.', '_' or '-'.[/red]")
|
|
@@ -1146,7 +1196,7 @@ class PresetsScreen(ModalScreen):
|
|
|
1146
1196
|
if not labels or not packs:
|
|
1147
1197
|
err.update("[red]Select at least one model and one pack first.[/red]")
|
|
1148
1198
|
return
|
|
1149
|
-
engine.save_preset(self.app.presets_path, name, labels, packs, repeat)
|
|
1199
|
+
engine.save_preset(self.app.presets_path, name, labels, packs, repeat, tests)
|
|
1150
1200
|
self.dismiss(("saved", name))
|
|
1151
1201
|
|
|
1152
1202
|
@on(Button.Pressed, "#cancel")
|
|
@@ -1215,13 +1265,24 @@ class QueuedRun:
|
|
|
1215
1265
|
self.jobs = jobs
|
|
1216
1266
|
self.est = eng.estimate_seconds(jobs)[0]
|
|
1217
1267
|
|
|
1268
|
+
def tests(self, eng):
|
|
1269
|
+
"""The picked tests that are still in their packs (a pack edited while queued drops the rest)."""
|
|
1270
|
+
out = {}
|
|
1271
|
+
for p, ids in (self.sel.get("tests") or {}).items():
|
|
1272
|
+
pk = eng.packs.get(p)
|
|
1273
|
+
known = [i for i in ids if pk and any(t["id"] == i for t in pk.tests)]
|
|
1274
|
+
if known:
|
|
1275
|
+
out[p] = known
|
|
1276
|
+
return out
|
|
1277
|
+
|
|
1218
1278
|
def line(self):
|
|
1219
1279
|
return f"{self.title} [dim]~{fmt_secs(self.est)} · queued {time.strftime('%H:%M', time.localtime(self.queued))}[/dim]"
|
|
1220
1280
|
|
|
1221
1281
|
def make(self, eng):
|
|
1222
1282
|
"""(RunScreen, None), or (None, why it can't start) when its turn comes."""
|
|
1223
1283
|
sel = self.sel
|
|
1224
|
-
jobs = eng.plan(sel["models"], sel["packs"], sel["repeat"], sel["tier"], sel["force"] or self.rerun
|
|
1284
|
+
jobs = eng.plan(sel["models"], sel["packs"], sel["repeat"], sel["tier"], sel["force"] or self.rerun,
|
|
1285
|
+
self.tests(eng))
|
|
1225
1286
|
waiting = [j for j in jobs if j.status == "waiting"]
|
|
1226
1287
|
if not waiting:
|
|
1227
1288
|
return None, "nothing left to run (earlier runs finished it)"
|
|
@@ -1287,6 +1348,138 @@ class ConfirmScreen(ModalScreen):
|
|
|
1287
1348
|
self.dismiss(event.button.id == "yes")
|
|
1288
1349
|
|
|
1289
1350
|
|
|
1351
|
+
def _words(text):
|
|
1352
|
+
return re.findall(r"[a-z0-9]+", str(text).lower())
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def test_note(t):
|
|
1356
|
+
"""What a test's row shows after its id: the description's words the id doesn't already say,
|
|
1357
|
+
and the category unless the id or description already names it."""
|
|
1358
|
+
desc, ids = str(t.get("description") or ""), [w for w in _words(t["id"]) if not re.fullmatch(r"v\d+", w)]
|
|
1359
|
+
note = desc
|
|
1360
|
+
tokens = list(re.finditer(r"[a-z0-9]+", desc.lower()))
|
|
1361
|
+
if set(w.group() for w in tokens) <= set(_words(t["id"])):
|
|
1362
|
+
note = "" # the id in other words
|
|
1363
|
+
elif len(ids) > 1: # "normal entry #1, pretty JSON" for normal-entry-1-v1
|
|
1364
|
+
n = 0
|
|
1365
|
+
while n < len(tokens) and n < len(ids) and tokens[n].group() == ids[n]:
|
|
1366
|
+
n += 1
|
|
1367
|
+
if n == len(ids):
|
|
1368
|
+
note = desc[tokens[n - 1].end():].strip(" ,;:·-#")
|
|
1369
|
+
cat = str(t.get("category") or "")
|
|
1370
|
+
said = " ".join(_words(t["id"]) + _words(note))
|
|
1371
|
+
if cat and " ".join(_words(cat)) not in said:
|
|
1372
|
+
note = f"{note} · {cat}" if note else cat
|
|
1373
|
+
return note
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
class PickTestsScreen(ModalScreen):
|
|
1377
|
+
"""Pick which tests of one pack run. Dismisses with the ticked ids in pack order (empty = the
|
|
1378
|
+
tier's usual tests), or None to keep the current pick."""
|
|
1379
|
+
BINDINGS = [Binding("escape", "cancel", "Cancel"),
|
|
1380
|
+
Binding("space", "toggle", "Tick", show=False),
|
|
1381
|
+
Binding("a", "toggle_shown", "Tick all shown")]
|
|
1382
|
+
|
|
1383
|
+
def __init__(self, pack, chosen):
|
|
1384
|
+
super().__init__()
|
|
1385
|
+
self.pack, self.chosen, self.shown = pack, set(chosen), []
|
|
1386
|
+
|
|
1387
|
+
def compose(self) -> ComposeResult:
|
|
1388
|
+
with Vertical(id="dialog", classes="wide"):
|
|
1389
|
+
yield Label(f"[b]Pick tests: {rich_escape(self.pack.label)}[/b]\n[dim]space ticks · a ticks all shown · "
|
|
1390
|
+
"type to filter, enter to go to the list · none ticked = the tier's usual tests[/dim]")
|
|
1391
|
+
yield Input(placeholder="filter by id, description or category", id="pick-filter")
|
|
1392
|
+
yield OptionList(id="pick-tests")
|
|
1393
|
+
yield Static(id="pick-count")
|
|
1394
|
+
with Horizontal(classes="dialog-buttons"):
|
|
1395
|
+
yield Button("Use these", id="pick-apply", variant="success")
|
|
1396
|
+
yield Button("Clear (usual tests)", id="pick-clear")
|
|
1397
|
+
yield Button("Cancel", id="pick-cancel")
|
|
1398
|
+
|
|
1399
|
+
def on_mount(self):
|
|
1400
|
+
self.fill("")
|
|
1401
|
+
self.query_one("#pick-tests").focus()
|
|
1402
|
+
|
|
1403
|
+
def row(self, t):
|
|
1404
|
+
on = t["id"] in self.chosen
|
|
1405
|
+
mark = "[b green]\\[✓][/]" if on else "[dim]\\[ ][/dim]"
|
|
1406
|
+
tid = f"[b]{rich_escape(t['id'])}[/b]" if on else rich_escape(t["id"])
|
|
1407
|
+
note = test_note(t)
|
|
1408
|
+
return f"{mark} {diff_badge(t.get('difficulty', 'unrated'))} {tid}" + (f" [dim]{rich_escape(note)}[/dim]" if note else "")
|
|
1409
|
+
|
|
1410
|
+
def fill(self, text):
|
|
1411
|
+
ol = self.query_one("#pick-tests", OptionList)
|
|
1412
|
+
ol.clear_options()
|
|
1413
|
+
text = text.lower()
|
|
1414
|
+
self.shown = [t for t in self.pack.tests
|
|
1415
|
+
if text in " ".join(str(t.get(k, "")) for k in ("id", "description", "category")).lower()]
|
|
1416
|
+
ol.add_options([Option(self.row(t), id=t["id"]) for t in self.shown])
|
|
1417
|
+
if self.shown:
|
|
1418
|
+
ol.highlighted = 0
|
|
1419
|
+
self.count()
|
|
1420
|
+
|
|
1421
|
+
def count(self):
|
|
1422
|
+
shown = f" [dim]({len(self.shown)} shown)[/dim]" if len(self.shown) != len(self.pack.tests) else ""
|
|
1423
|
+
self.query_one("#pick-count", Static).update(
|
|
1424
|
+
(f"[b]{len(self.chosen)}[/b] of {len(self.pack.tests)} ticked" if self.chosen
|
|
1425
|
+
else "[dim]None ticked: the tier's usual tests run.[/dim]") + shown)
|
|
1426
|
+
|
|
1427
|
+
def redraw(self, indexes):
|
|
1428
|
+
ol = self.query_one("#pick-tests", OptionList)
|
|
1429
|
+
for i in indexes:
|
|
1430
|
+
ol.replace_option_prompt_at_index(i, self.row(self.shown[i]))
|
|
1431
|
+
self.count()
|
|
1432
|
+
|
|
1433
|
+
def action_toggle(self):
|
|
1434
|
+
ol = self.query_one("#pick-tests", OptionList)
|
|
1435
|
+
if not ol.has_focus or ol.highlighted is None:
|
|
1436
|
+
return
|
|
1437
|
+
self.chosen.symmetric_difference_update({self.shown[ol.highlighted]["id"]})
|
|
1438
|
+
self.redraw([ol.highlighted])
|
|
1439
|
+
|
|
1440
|
+
@on(OptionList.OptionSelected, "#pick-tests")
|
|
1441
|
+
def selected(self, event): # enter on a row ticks it too
|
|
1442
|
+
self.chosen.symmetric_difference_update({event.option.id})
|
|
1443
|
+
self.redraw([event.option_index])
|
|
1444
|
+
|
|
1445
|
+
def action_toggle_shown(self):
|
|
1446
|
+
"""Tick every shown test, or untick them all when they're all ticked."""
|
|
1447
|
+
if self.query_one("#pick-filter", Input).has_focus:
|
|
1448
|
+
return
|
|
1449
|
+
ids = {t["id"] for t in self.shown}
|
|
1450
|
+
if ids <= self.chosen:
|
|
1451
|
+
self.chosen -= ids
|
|
1452
|
+
else:
|
|
1453
|
+
self.chosen |= ids
|
|
1454
|
+
self.redraw(range(len(self.shown)))
|
|
1455
|
+
|
|
1456
|
+
def check_action(self, action, parameters):
|
|
1457
|
+
# while typing in the filter, space and a are text
|
|
1458
|
+
if action in ("toggle", "toggle_shown") and self.query_one("#pick-filter", Input).has_focus:
|
|
1459
|
+
return None
|
|
1460
|
+
return True
|
|
1461
|
+
|
|
1462
|
+
@on(Input.Changed, "#pick-filter")
|
|
1463
|
+
def filter_changed(self, event):
|
|
1464
|
+
self.fill(event.value)
|
|
1465
|
+
|
|
1466
|
+
@on(Input.Submitted, "#pick-filter")
|
|
1467
|
+
def to_list(self):
|
|
1468
|
+
self.query_one("#pick-tests").focus()
|
|
1469
|
+
|
|
1470
|
+
@on(Button.Pressed, "#pick-apply")
|
|
1471
|
+
def apply(self):
|
|
1472
|
+
self.dismiss([t["id"] for t in self.pack.tests if t["id"] in self.chosen])
|
|
1473
|
+
|
|
1474
|
+
@on(Button.Pressed, "#pick-clear")
|
|
1475
|
+
def clear(self):
|
|
1476
|
+
self.dismiss([])
|
|
1477
|
+
|
|
1478
|
+
@on(Button.Pressed, "#pick-cancel")
|
|
1479
|
+
def action_cancel(self):
|
|
1480
|
+
self.dismiss(None)
|
|
1481
|
+
|
|
1482
|
+
|
|
1290
1483
|
class ModelPickScreen(ModalScreen):
|
|
1291
1484
|
"""Pick which models Per question compares. Dismisses with a set of labels (empty = all) or None."""
|
|
1292
1485
|
BINDINGS = [Binding("escape", "cancel", "Cancel")]
|
|
@@ -2331,7 +2524,7 @@ class EvalsApp(App):
|
|
|
2331
2524
|
#dialog { width: 80; height: auto; max-height: 90%; padding: 1 2; border: thick $primary; background: $surface; }
|
|
2332
2525
|
#dialog.wide { width: 120; }
|
|
2333
2526
|
#dialog SelectionList, #dialog OptionList { height: auto; max-height: 20; }
|
|
2334
|
-
PresetsScreen, ScanScreen, SessionsScreen, ChoiceScreen { align: center middle; }
|
|
2527
|
+
PresetsScreen, ScanScreen, SessionsScreen, ChoiceScreen, PickTestsScreen { align: center middle; }
|
|
2335
2528
|
#dialog Input, #dialog Select { margin-bottom: 1; }
|
|
2336
2529
|
.dialog-buttons { height: 3; margin-top: 1; }
|
|
2337
2530
|
.dialog-buttons Button { margin-right: 2; }
|
|
@@ -43,8 +43,8 @@ def free_port():
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class Mock:
|
|
46
|
-
def __init__(self, packs, mode):
|
|
47
|
-
self.port = free_port()
|
|
46
|
+
def __init__(self, packs, mode, port=None):
|
|
47
|
+
self.port = port or free_port()
|
|
48
48
|
self.proc = subprocess.Popen([sys.executable, os.path.join(HERE, "mock_server.py"), "--port", str(self.port),
|
|
49
49
|
"--packs", packs, "--mode", mode], stdout=subprocess.DEVNULL)
|
|
50
50
|
for _ in range(100):
|
|
@@ -209,6 +209,13 @@ class Units(unittest.TestCase):
|
|
|
209
209
|
write(log, "ok\nerror: runtime bootstrap failed [x]: context does not fit\n")
|
|
210
210
|
self.assertEqual(engine.server_error(log, 0), "context does not fit")
|
|
211
211
|
|
|
212
|
+
def test_test_note_skips_what_the_id_says(self):
|
|
213
|
+
from tuieval.tui import test_note
|
|
214
|
+
self.assertEqual(test_note({"id": "normal-entry-1-v2", "category": "normal entry",
|
|
215
|
+
"description": "normal entry #1, compact JSON, shuffled keys"}), "compact JSON, shuffled keys")
|
|
216
|
+
self.assertEqual(test_note({"id": "stale-quote", "description": "stale quote", "category": "data"}), "data")
|
|
217
|
+
self.assertEqual(test_note({"id": "todo", "description": "To-do list app", "category": ""}), "To-do list app")
|
|
218
|
+
|
|
212
219
|
def test_short_note(self):
|
|
213
220
|
from tuieval import tui
|
|
214
221
|
self.assertEqual(tui.short_note("server exited with code 1 while loading: bad file"), "didn't start: bad file")
|
|
@@ -225,6 +232,106 @@ class Units(unittest.TestCase):
|
|
|
225
232
|
self.assertEqual(p.modules, ["some_missing_module"])
|
|
226
233
|
|
|
227
234
|
|
|
235
|
+
class PickTests(unittest.TestCase):
|
|
236
|
+
"""Running only some tests of a pack: partial runs add up, --force redoes just the picked ones."""
|
|
237
|
+
|
|
238
|
+
def setUp(self):
|
|
239
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
240
|
+
self.ws = os.path.join(self.tmp.name, "ws")
|
|
241
|
+
tuieval(self.tmp.name, "init", self.ws)
|
|
242
|
+
for name in ("apps", "other"):
|
|
243
|
+
d = os.path.join(self.ws, "packs", name)
|
|
244
|
+
os.makedirs(d)
|
|
245
|
+
write(os.path.join(d, "pack.toml"), f'label = "{name}"\nscreen = 1\n[certify]\nrepeat = 1\n'
|
|
246
|
+
'[gate]\nmin_accuracy = 0.1\n')
|
|
247
|
+
write(os.path.join(d, "tests.yaml"), "".join(
|
|
248
|
+
f"- {{id: {t}, input: {name} question {t}, expected: 1, reference: 'ANSWER: 1', wrong: ['ANSWER: 2'], "
|
|
249
|
+
"difficulty: easy}\n" for t in ("a", "b", "c", "d")))
|
|
250
|
+
self.mock, self.port = None, free_port()
|
|
251
|
+
with open(os.path.join(self.ws, "models.toml"), "a") as f:
|
|
252
|
+
f.write(f'\n[servers.m]\nurl = "http://127.0.0.1:{self.port}"\n')
|
|
253
|
+
|
|
254
|
+
def tearDown(self):
|
|
255
|
+
if self.mock:
|
|
256
|
+
self.mock.stop()
|
|
257
|
+
self.tmp.cleanup()
|
|
258
|
+
|
|
259
|
+
def server(self, mode):
|
|
260
|
+
"""(Re)start the mock on the same port: oracle answers right, wrong answers wrong."""
|
|
261
|
+
first = self.mock is None
|
|
262
|
+
if self.mock:
|
|
263
|
+
self.mock.stop()
|
|
264
|
+
self.mock = Mock(os.path.join(self.ws, "packs"), mode, port=self.port)
|
|
265
|
+
if first:
|
|
266
|
+
tuieval(self.ws, "add", "mock", "--server", "m", "--label", "m")
|
|
267
|
+
|
|
268
|
+
def rows(self, pack="apps"):
|
|
269
|
+
path = os.path.join(self.ws, "results", "m", f"{pack}.json")
|
|
270
|
+
if not os.path.isfile(path):
|
|
271
|
+
return {}
|
|
272
|
+
return {r["test"]: r["pass"] for r in json.loads(read(path))["results"]}
|
|
273
|
+
|
|
274
|
+
def status(self, pack="apps"):
|
|
275
|
+
code = f"from tuieval import engine; print(engine.Engine().result_status('m', '{pack}'))"
|
|
276
|
+
return subprocess.run([sys.executable, "-c", code], capture_output=True, text=True,
|
|
277
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws)).stdout.strip()
|
|
278
|
+
|
|
279
|
+
def test_pick(self):
|
|
280
|
+
from tuieval import packs
|
|
281
|
+
p = packs.load_pack(os.path.join(self.ws, "packs", "apps"))
|
|
282
|
+
self.assertEqual([t["id"] for t in p.pick(["c", "a"])], ["a", "c"]) # the pack's order
|
|
283
|
+
with self.assertRaises(packs.PackError) as cm:
|
|
284
|
+
p.pick(["a", "zz"])
|
|
285
|
+
self.assertIn("'zz'", str(cm.exception))
|
|
286
|
+
self.assertIn("a, b, c, d", str(cm.exception))
|
|
287
|
+
|
|
288
|
+
def test_partial_runs_add_up_and_force_redoes_only_the_picked(self):
|
|
289
|
+
self.server("oracle")
|
|
290
|
+
tuieval(self.ws, "run", "--packs", "apps", "--tests", "b")
|
|
291
|
+
self.assertEqual(self.rows(), {"b": True})
|
|
292
|
+
self.assertEqual(self.rows("other"), {})
|
|
293
|
+
tuieval(self.ws, "run", "--tests", "apps:a,apps:c", "--tier", "certify") # --packs from --tests
|
|
294
|
+
self.assertEqual(self.rows(), {"a": True, "b": True, "c": True})
|
|
295
|
+
self.assertEqual(self.status(), "screened")
|
|
296
|
+
out = tuieval(self.ws, "run", "--tests", "apps:b").stdout
|
|
297
|
+
self.assertIn("done earlier", out) # nothing missing: not re-asked
|
|
298
|
+
tuieval(self.ws, "run", "--tests", "apps:d", "--tier", "certify")
|
|
299
|
+
self.assertEqual(self.status(), "certified")
|
|
300
|
+
|
|
301
|
+
self.server("wrong")
|
|
302
|
+
tuieval(self.ws, "run", "--tests", "apps:b", "--force")
|
|
303
|
+
self.assertEqual(self.rows(), {"a": True, "b": False, "c": True, "d": True})
|
|
304
|
+
hist = os.listdir(os.path.join(self.ws, "results", "m", "history"))
|
|
305
|
+
self.assertTrue(any(f.startswith("apps-") for f in hist), hist)
|
|
306
|
+
self.assertEqual(self.status(), "certified")
|
|
307
|
+
|
|
308
|
+
def test_cli_errors(self):
|
|
309
|
+
self.server("oracle")
|
|
310
|
+
for args, msg in ((["--tests", "a"], "say which pack"),
|
|
311
|
+
(["--packs", "apps,other", "--tests", "a"], "say which pack"),
|
|
312
|
+
(["--tests", "nope:a"], "unknown pack 'nope'"),
|
|
313
|
+
(["--packs", "apps", "--tests", "zz"], "has no test 'zz'"),
|
|
314
|
+
(["--packs", "other", "--tests", "apps:a"], "aren't in --packs")):
|
|
315
|
+
p = tuieval(self.ws, "run", *args, check=False)
|
|
316
|
+
self.assertNotEqual(p.returncode, 0, args)
|
|
317
|
+
self.assertIn(msg, p.stderr, args)
|
|
318
|
+
self.assertEqual(self.rows(), {})
|
|
319
|
+
|
|
320
|
+
def test_presets_keep_picked_tests(self):
|
|
321
|
+
from tuieval import engine
|
|
322
|
+
path = os.path.join(self.ws, "presets.toml")
|
|
323
|
+
engine.save_preset(path, "quick", ["m"], ["apps", "other"], None, {"apps": ["b", "d"], "gone": ["x"]})
|
|
324
|
+
presets = engine.load_presets(path)
|
|
325
|
+
self.assertEqual(presets["quick"]["tests"], {"apps": ["b", "d"]})
|
|
326
|
+
cfg = {"models": [{"label": "m", "tags": []}]}
|
|
327
|
+
self.assertEqual(engine.resolve_preset(cfg, presets["quick"], ["apps", "other"]),
|
|
328
|
+
(["m"], ["apps", "other"], None, {"apps": ["b", "d"]}))
|
|
329
|
+
self.server("oracle")
|
|
330
|
+
tuieval(self.ws, "run", "--preset", "quick")
|
|
331
|
+
self.assertEqual(self.rows(), {"b": True, "d": True})
|
|
332
|
+
self.assertEqual(len(self.rows("other")), 1) # no pick: the Screen sample (screen = 1)
|
|
333
|
+
|
|
334
|
+
|
|
228
335
|
def fake_gguf(path, embedding=5120, size=0, kv_heads=4):
|
|
229
336
|
"""A GGUF header with just the keys machines.read_gguf reads, padded to size bytes. kv_heads: a
|
|
230
337
|
number, or one per layer (a list, as some GGUFs store it)."""
|
|
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.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev10 → tuieval-0.2.0.dev14}/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
|