tuieval 0.2.0.dev70__tar.gz → 0.2.0.dev74__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.dev70 → tuieval-0.2.0.dev74}/CHANGELOG.md +2 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/PKG-INFO +1 -1
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/engine.py +48 -10
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/run_evals.py +7 -1
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/tui.py +177 -35
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/tests/test_tuieval.py +157 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/.gitignore +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/LICENSE +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/README.md +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/models.md +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/compare.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/logs.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/pta.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/tests/mock_server.py +0 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Retest asks per pack before it starts: from zero, continue or leave out. It now really starts from zero, and replaced smoke results go to history/.
|
|
6
|
+
- Retest: the setup checkbox "Rerun existing results" is now "Retest (ask again, even if already answered)", and `tuieval run --retest` is the same (`--force` still works).
|
|
5
7
|
- `tuieval logs` follows the server log of the model running now and moves on with the run; `logs live` shows each answer as it finishes, `--list` every log.
|
|
6
8
|
- Results tables sort by a click on a column name; a second click reverses. Numbers, times and levels sort by value; empty cells stay last.
|
|
7
9
|
- Nvidia and AMD GPUs on Linux: VRAM from `nvidia-smi`, `amd-smi` or `rocm-smi` sizes models (`gpu_memory_gb` overrides it), runs record peak VRAM, and `tuieval doctor` shows the GPU.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev74
|
|
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
|
|
@@ -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.dev74'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev74')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -427,6 +427,7 @@ class Job:
|
|
|
427
427
|
settings: dict = dataclasses.field(default_factory=dict) # sampling + serving identity (fingerprinted)
|
|
428
428
|
merge: bool = True # keep matching earlier results and only run what's missing
|
|
429
429
|
redo: bool = False # rerun the picked tests, keeping the pack's other answers
|
|
430
|
+
fresh: bool = False # retest: an unfinished run's matching answers are set aside too, not resumed
|
|
430
431
|
status: str = "waiting" # waiting, loading, running, done, failed, skipped
|
|
431
432
|
note: str = ""
|
|
432
433
|
discards: list = dataclasses.field(default_factory=list) # [(what, answers, why)] set aside on start
|
|
@@ -1059,6 +1060,8 @@ class Engine:
|
|
|
1059
1060
|
lines = [json.loads(l) for l in f if l.strip()]
|
|
1060
1061
|
if lines and self._header_ok(lines[0], job):
|
|
1061
1062
|
keys |= {(r["test"], r["repeat"]) for r in lines[1:] if not client.server_error_row(r)}
|
|
1063
|
+
if job.fresh: # a retest asks these again
|
|
1064
|
+
keys = {k for k in keys if k[0] not in self._redo_ids(job)}
|
|
1062
1065
|
return keys & wanted
|
|
1063
1066
|
|
|
1064
1067
|
def _stored(self, job):
|
|
@@ -1119,6 +1122,20 @@ class Engine:
|
|
|
1119
1122
|
out.append(("unfinished", len(rows), why))
|
|
1120
1123
|
return out
|
|
1121
1124
|
|
|
1125
|
+
def set_aside(self, job):
|
|
1126
|
+
"""(finished, unfinished): earlier answers a retest of this job asks again, so they move to
|
|
1127
|
+
history/ and stop counting: in its result file and its unfinished run, under the job's
|
|
1128
|
+
questions and settings (only the picked tests' when it redoes those)."""
|
|
1129
|
+
redo = self._redo_ids(job)
|
|
1130
|
+
data = self._result(job.out_path)
|
|
1131
|
+
finished = 0
|
|
1132
|
+
if data and data["pack"]["fingerprint"] == job.pack.fingerprint and settings_match(data, job.settings):
|
|
1133
|
+
finished = sum(r["test"] in redo for r in data["results"])
|
|
1134
|
+
head, rows = self._partial_lines(job.out_path + ".partial.jsonl")
|
|
1135
|
+
unfinished = sum(r["test"] in redo and not client.server_error_row(r) for r in rows) \
|
|
1136
|
+
if self._header_ok(head, job) else 0
|
|
1137
|
+
return finished, unfinished
|
|
1138
|
+
|
|
1122
1139
|
def superseded(self, path):
|
|
1123
1140
|
"""Is this result file about to be replaced? True when it no longer matches the current
|
|
1124
1141
|
questions or settings and an unfinished run of the same pack does."""
|
|
@@ -1141,7 +1158,8 @@ class Engine:
|
|
|
1141
1158
|
screen, the pack's certification repeats for certify). tests: {pack: [test ids]} runs just
|
|
1142
1159
|
those tests of a pack instead of the tier's sample (unknown ids raise PackError). Earlier
|
|
1143
1160
|
matching results are kept and only missing answers are run, unless force; force with
|
|
1144
|
-
picked tests reruns those and keeps the pack's other answers.
|
|
1161
|
+
picked tests reruns those and keeps the pack's other answers. force is True, False, or the
|
|
1162
|
+
keys ("label/pack") of the jobs to retest, the others resuming."""
|
|
1145
1163
|
if tier not in TIERS:
|
|
1146
1164
|
raise ValueError(f"tier must be one of {TIERS}")
|
|
1147
1165
|
jobs = []
|
|
@@ -1151,9 +1169,11 @@ class Engine:
|
|
|
1151
1169
|
pack = self.packs[name]
|
|
1152
1170
|
reps = repeat or self.default_repeat(pack, tier)
|
|
1153
1171
|
picked = (tests or {}).get(name)
|
|
1172
|
+
retest = force if isinstance(force, bool) else f"{label}/{name}" in force
|
|
1154
1173
|
job = Job(m, pack, reps, pack.pick(picked) if picked else pack.select(tier),
|
|
1155
1174
|
self.result_path(label, name, tier), effective_sampling(self.cfg, m), tier=tier,
|
|
1156
|
-
merge=not
|
|
1175
|
+
merge=not retest or bool(picked), redo=retest and bool(picked), fresh=retest,
|
|
1176
|
+
settings=self.settings(m))
|
|
1157
1177
|
sv = self.serving(m)
|
|
1158
1178
|
status = self.result_status(label, name, tier)
|
|
1159
1179
|
job.discards = self._discards(job)
|
|
@@ -1171,11 +1191,11 @@ class Engine:
|
|
|
1171
1191
|
job.status, job.note = "skipped", f"{', '.join(missing)} not installed in tuieval's Python"
|
|
1172
1192
|
elif status and status.startswith("outdated"):
|
|
1173
1193
|
job.merge, job.redo, job.note = False, False, status.split(": ", 1)[-1] + ", rerunning"
|
|
1174
|
-
if not
|
|
1194
|
+
if not retest: # a rerun under the new settings already under way resumes
|
|
1175
1195
|
self._fill_progress(job)
|
|
1176
1196
|
if job.done:
|
|
1177
1197
|
job.note += f" ({job.done} of {job.total} done)"
|
|
1178
|
-
elif not
|
|
1198
|
+
elif not retest:
|
|
1179
1199
|
self._fill_progress(job)
|
|
1180
1200
|
if job.done >= job.total:
|
|
1181
1201
|
job.status, job.note = "done", "done earlier"
|
|
@@ -2092,11 +2112,16 @@ class Engine:
|
|
|
2092
2112
|
lines = [json.loads(l) for l in f if l.strip()]
|
|
2093
2113
|
if lines and self._header_ok(lines[0], job):
|
|
2094
2114
|
new = [r for r in lines[1:] if not client.server_error_row(r)]
|
|
2115
|
+
if job.fresh: # a retest asks again what the unfinished run already answered
|
|
2116
|
+
stay = [r for r in new if r["test"] not in self._redo_ids(job)]
|
|
2117
|
+
if len(stay) < len(new):
|
|
2118
|
+
self._partial_to_history(job, keep=bool(stay))
|
|
2119
|
+
new = stay
|
|
2120
|
+
if stay:
|
|
2121
|
+
with open(partial, "w") as f:
|
|
2122
|
+
f.write("".join(json.dumps(l) + "\n" for l in [lines[0]] + stay))
|
|
2095
2123
|
else: # other questions or settings: kept in history/, not deleted
|
|
2096
|
-
|
|
2097
|
-
os.makedirs(hist, exist_ok=True)
|
|
2098
|
-
stamp = time.strftime("%Y%m%d-%H%M%S", time.localtime(os.path.getmtime(partial)))
|
|
2099
|
-
os.replace(partial, os.path.join(hist, f"{job.pack.name}-{stamp}.partial.jsonl"))
|
|
2124
|
+
self._partial_to_history(job)
|
|
2100
2125
|
if not os.path.isfile(partial):
|
|
2101
2126
|
with contextlib.suppress(OSError): # a fresh start: sittings of a discarded partial don't count
|
|
2102
2127
|
os.remove(self._sittings_path(job))
|
|
@@ -2224,10 +2249,23 @@ class Engine:
|
|
|
2224
2249
|
job.note = f"{job.passed}/{job.done} passed"
|
|
2225
2250
|
self.emit("job_done", job=job)
|
|
2226
2251
|
|
|
2252
|
+
def _partial_to_history(self, job, keep=False):
|
|
2253
|
+
"""Keep an unfinished run that is set aside in history/; keep: copy it (some of its answers stay)."""
|
|
2254
|
+
partial = job.out_path + ".partial.jsonl"
|
|
2255
|
+
hist = os.path.join(os.path.dirname(job.out_path), "history")
|
|
2256
|
+
os.makedirs(hist, exist_ok=True)
|
|
2257
|
+
stamp = time.strftime("%Y%m%d-%H%M%S", time.localtime(os.path.getmtime(partial)))
|
|
2258
|
+
(shutil.copy2 if keep else os.replace)(partial, os.path.join(hist, f"{job.pack.name}-{stamp}.partial.jsonl"))
|
|
2259
|
+
|
|
2260
|
+
@staticmethod
|
|
2261
|
+
def _redo_ids(job):
|
|
2262
|
+
"""The tests whose earlier answers a retest replaces: the picked ones, or all of them."""
|
|
2263
|
+
return {t["id"] for t in job.tests} if job.redo else {t["id"] for t in job.pack.tests}
|
|
2264
|
+
|
|
2227
2265
|
def _archive(self, job, keep=False):
|
|
2228
2266
|
"""Keep a result file that is about to be replaced (rerun or outdated) in history/; keep:
|
|
2229
|
-
copy it (some of its answers stay in the new file)."""
|
|
2230
|
-
if
|
|
2267
|
+
copy it (some of its answers stay in the new file). Smoke results too: nothing is deleted."""
|
|
2268
|
+
if not os.path.isfile(job.out_path):
|
|
2231
2269
|
return
|
|
2232
2270
|
hist = os.path.join(os.path.dirname(job.out_path), "history")
|
|
2233
2271
|
os.makedirs(hist, exist_ok=True)
|
|
@@ -11,6 +11,7 @@
|
|
|
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
13
|
tuieval run --packs my-pack --tests a,b # only these tests of a pack (or --tests my-pack:a,other:b)
|
|
14
|
+
tuieval run --only my-model --packs my-pack --tests a --retest # ask a question again
|
|
14
15
|
tuieval run --preset nightly # a selection saved in the TUI (presets.toml)
|
|
15
16
|
tuieval run --tier smoke --only my-model # 3 tests per pack, 1 repeat: check paths and flags
|
|
16
17
|
tuieval run --tier smoke --only openrouter:qwen/qwen3-32b # any OpenRouter model, no models.toml edit
|
|
@@ -742,7 +743,8 @@ def main(argv=None):
|
|
|
742
743
|
p.add_argument("--packs", help="comma-separated packs (default: all)")
|
|
743
744
|
p.add_argument("--tests", help="comma-separated test ids to run instead of the tier's sample: id with one "
|
|
744
745
|
"pack in --packs, else pack:id (and --packs defaults to those packs)")
|
|
745
|
-
p.add_argument("--force", action="store_true",
|
|
746
|
+
p.add_argument("--retest", "--force", action="store_true", dest="force",
|
|
747
|
+
help="ask again even if already answered: the packs, or just the --tests (earlier answers go to history/)")
|
|
746
748
|
p.add_argument("--repeat", type=int, help="times each question is asked, any tier (default: 1 for smoke and "
|
|
747
749
|
"screen, the pack's certification count for certify)")
|
|
748
750
|
p.add_argument("--tier", choices=engine.TIERS, default="screen",
|
|
@@ -821,6 +823,10 @@ def main(argv=None):
|
|
|
821
823
|
say(f"{j.key}: skipping ({j.note})")
|
|
822
824
|
elif j.status == "done":
|
|
823
825
|
say(f"{j.key}: done earlier ({j.passed}/{j.done} passed)")
|
|
826
|
+
elif j.fresh and any(e.set_aside(j)):
|
|
827
|
+
finished, unfinished = e.set_aside(j)
|
|
828
|
+
what = " and ".join(f"{n} {w}" for n, w in ((finished, "finished"), (unfinished, "unfinished")) if n)
|
|
829
|
+
say(f"{j.key}: retest from zero; its {what} answers move to history/")
|
|
824
830
|
|
|
825
831
|
def stop(*_):
|
|
826
832
|
say("stopping…", RED)
|
|
@@ -385,6 +385,11 @@ HELP = {
|
|
|
385
385
|
[b]Models at a time[/b]: how many of the ticked models run side by side (blank: this machine's
|
|
386
386
|
parallel_models in models.toml, default 1). Answers note the models they ran alongside.
|
|
387
387
|
|
|
388
|
+
[b]Retest[/b]: ask the ticked questions again even if they're already answered (only the tests
|
|
389
|
+
picked with e, if any). Without it, answered questions are skipped. Before anything moves, Start lists
|
|
390
|
+
the packs with earlier answers and lets you choose for each: Zero (ask everything again), Continue
|
|
391
|
+
(keep what's answered, ask the rest) or Leave out. Replaced answers go to history/, never deleted.
|
|
392
|
+
|
|
388
393
|
[b]Keys[/b]
|
|
389
394
|
s start (with a run going: add to it, or queue it) r results
|
|
390
395
|
a add a model (a GGUF, a model id, openrouter:<id>) m scan model folders for new GGUFs
|
|
@@ -646,7 +651,7 @@ class SetupScreen(Screen):
|
|
|
646
651
|
yield Input("", id="repeat", type="integer", max_length=2, placeholder="1")
|
|
647
652
|
yield Label("Models at a time")
|
|
648
653
|
yield Input("", id="parallel", type="integer", max_length=1, placeholder="1")
|
|
649
|
-
yield Checkbox("
|
|
654
|
+
yield Checkbox("Retest (ask again, even if already answered)", id="force")
|
|
650
655
|
yield Static(id="estimate")
|
|
651
656
|
with Horizontal(id="buttons"):
|
|
652
657
|
yield Button("Start [s]", id="start", variant="success")
|
|
@@ -958,7 +963,9 @@ class SetupScreen(Screen):
|
|
|
958
963
|
if not run:
|
|
959
964
|
text += "\n[yellow]Nothing selected can run (see skipping).[/yellow]"
|
|
960
965
|
elif rerun:
|
|
961
|
-
text += "\n[yellow]Everything selected has results; Start
|
|
966
|
+
text += "\n[yellow]Everything selected has results; Start asks which packs to ask again.[/yellow]"
|
|
967
|
+
if force:
|
|
968
|
+
text += "\n[yellow]Retest: Start lists the packs that would start from zero and lets you choose.[/yellow]"
|
|
962
969
|
resumed = [j for j in run if "done earlier" in j.note]
|
|
963
970
|
if resumed:
|
|
964
971
|
text += f" [yellow]{len(resumed)} pack(s) continue from earlier results[/yellow]"
|
|
@@ -1037,32 +1044,32 @@ class SetupScreen(Screen):
|
|
|
1037
1044
|
picks = self.current_picks(suites)
|
|
1038
1045
|
sel = {"models": labels, "packs": suites, "repeat": repeat, "tier": tier, "force": force, "tests": picks,
|
|
1039
1046
|
"parallel": self.parallel()}
|
|
1040
|
-
|
|
1041
|
-
|
|
1047
|
+
e = self.app.engine
|
|
1048
|
+
jobs = e.plan(labels, suites, repeat, tier, True, picks)
|
|
1042
1049
|
if not any(j.status == "waiting" for j in jobs):
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
jobs = self.app.engine.plan(labels, suites, repeat, tier, True, picks)
|
|
1046
|
-
if not any(j.status == "waiting" for j in jobs):
|
|
1047
|
-
reasons = sorted({j.note for j in jobs if j.status == "skipped"})
|
|
1048
|
-
self.notify(f"Nothing selected can run: {'; '.join(reasons)}", severity="warning")
|
|
1049
|
-
return
|
|
1050
|
-
rerun = True
|
|
1051
|
-
self.app.save_state(sel)
|
|
1052
|
-
lost = [(j, what, n, why) for j in jobs if j.status == "waiting" for what, n, why in j.discards]
|
|
1053
|
-
item = QueuedRun(sel, rerun, {(j.key, what) for j, what, _, _ in lost}, jobs, self.app.engine)
|
|
1054
|
-
if not lost:
|
|
1055
|
-
self.app.start_or_queue(item)
|
|
1050
|
+
reasons = sorted({j.note for j in jobs if j.status == "skipped"})
|
|
1051
|
+
self.notify(f"Nothing selected can run: {'; '.join(reasons)}", severity="warning")
|
|
1056
1052
|
return
|
|
1053
|
+
self.app.save_state(sel)
|
|
1054
|
+
# Packs whose earlier answers a start would set aside (Retest, everything already done, or
|
|
1055
|
+
# other questions or settings): the user picks per pack before anything moves.
|
|
1056
|
+
rows, rerun = retest_rows(e, labels, suites, repeat, tier, picks, force)
|
|
1057
1057
|
|
|
1058
|
-
def
|
|
1059
|
-
if
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1058
|
+
def go(choices):
|
|
1059
|
+
if choices is None:
|
|
1060
|
+
return
|
|
1061
|
+
item = QueuedRun(sel, {k for k, c in choices.items() if c == "zero"},
|
|
1062
|
+
{k for k, c in choices.items() if c == "leave"},
|
|
1063
|
+
{(r.key, what) for r in rows if choices.get(r.key) == "zero" for what, _, _ in r.base.discards},
|
|
1064
|
+
e)
|
|
1065
|
+
if not item.jobs:
|
|
1066
|
+
self.notify(f"Nothing to run: {item.problem}.", severity="warning")
|
|
1067
|
+
return
|
|
1068
|
+
self.app.start_or_queue(item)
|
|
1069
|
+
if rows:
|
|
1070
|
+
self.app.push_screen(RetestScreen(rows, force, rerun), go)
|
|
1071
|
+
else:
|
|
1072
|
+
go({})
|
|
1066
1073
|
|
|
1067
1074
|
def action_tune(self):
|
|
1068
1075
|
e = self.app.engine
|
|
@@ -1537,14 +1544,16 @@ def run_title(tier, models, packs):
|
|
|
1537
1544
|
|
|
1538
1545
|
class QueuedRun:
|
|
1539
1546
|
"""A run waiting its turn. It's planned again when it starts, so it picks up what the runs before
|
|
1540
|
-
it finished; it never sets aside answers that weren't confirmed when it was queued.
|
|
1547
|
+
it finished; it never sets aside answers that weren't confirmed when it was queued. retest: the
|
|
1548
|
+
job keys to ask again from zero; leave: the ones left out."""
|
|
1541
1549
|
kind = "Run"
|
|
1542
1550
|
|
|
1543
|
-
def __init__(self, sel,
|
|
1544
|
-
self.sel, self.
|
|
1551
|
+
def __init__(self, sel, retest, leave, accepted, eng):
|
|
1552
|
+
self.sel, self.retest, self.leave, self.accepted = sel, retest, leave, accepted
|
|
1545
1553
|
self.queued = time.time()
|
|
1554
|
+
self.jobs, self.problem = self.plan(eng)
|
|
1555
|
+
jobs = self.jobs or []
|
|
1546
1556
|
self.title = run_title(sel["tier"], {j.label for j in jobs}, {j.pack.label for j in jobs})
|
|
1547
|
-
self.jobs = jobs
|
|
1548
1557
|
self.est = eng.estimate_seconds(jobs)[0]
|
|
1549
1558
|
|
|
1550
1559
|
def tests(self, eng):
|
|
@@ -1563,11 +1572,11 @@ class QueuedRun:
|
|
|
1563
1572
|
def plan(self, eng):
|
|
1564
1573
|
"""(jobs, None), or (None, why it can't run) now."""
|
|
1565
1574
|
sel = self.sel
|
|
1566
|
-
jobs = eng.plan(sel["models"], sel["packs"], sel["repeat"], sel["tier"],
|
|
1567
|
-
|
|
1575
|
+
jobs = [j for j in eng.plan(sel["models"], sel["packs"], sel["repeat"], sel["tier"], self.retest,
|
|
1576
|
+
self.tests(eng)) if j.key not in self.leave]
|
|
1568
1577
|
waiting = [j for j in jobs if j.status == "waiting"]
|
|
1569
1578
|
if not waiting:
|
|
1570
|
-
return None, "
|
|
1579
|
+
return None, "every pack left is already done"
|
|
1571
1580
|
new = sorted({j.key for j in waiting for what, _, _ in j.discards if (j.key, what) not in self.accepted})
|
|
1572
1581
|
if new:
|
|
1573
1582
|
return None, (f"it would now start over {', '.join(new)}, which wasn't confirmed when it was queued. "
|
|
@@ -1618,6 +1627,140 @@ class ChoiceScreen(ModalScreen):
|
|
|
1618
1627
|
self.dismiss(None)
|
|
1619
1628
|
|
|
1620
1629
|
|
|
1630
|
+
class RetestRow:
|
|
1631
|
+
"""A pack whose earlier answers a start could set aside. base: its job resuming; zero: from zero."""
|
|
1632
|
+
|
|
1633
|
+
def __init__(self, base, zero, finished, unfinished, choices, choice):
|
|
1634
|
+
self.base, self.zero, self.finished, self.unfinished = base, zero, finished, unfinished
|
|
1635
|
+
self.choices, self.choice = choices, choice
|
|
1636
|
+
self.key = base.key
|
|
1637
|
+
|
|
1638
|
+
def earlier(self):
|
|
1639
|
+
if self.base.discards:
|
|
1640
|
+
return ", ".join(f"{n} {what}" for what, n, _ in self.base.discards)
|
|
1641
|
+
parts = [f"{self.finished} finished"] * bool(self.finished) + \
|
|
1642
|
+
[f"[yellow]{self.unfinished} unfinished[/yellow]"] * bool(self.unfinished)
|
|
1643
|
+
return ", ".join(parts)
|
|
1644
|
+
|
|
1645
|
+
def happens(self):
|
|
1646
|
+
b = self.base
|
|
1647
|
+
if self.choice == "leave":
|
|
1648
|
+
return "not run now"
|
|
1649
|
+
if self.choice == "continue":
|
|
1650
|
+
return "skipped: already done" if b.status == "done" else f"keeps {b.done}, asks the other {b.total - b.done}"
|
|
1651
|
+
if b.discards:
|
|
1652
|
+
why = "; ".join(sorted({why for _, _, why in b.discards}))
|
|
1653
|
+
return f"[yellow]{why}[/yellow]; {sum(n for _, n, _ in b.discards)} → history/"
|
|
1654
|
+
what = "picked tests asked again" if self.zero.redo else "asked again"
|
|
1655
|
+
return f"{what}; {self.finished + self.unfinished} → history/"
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
CHOICE_TEXT = {"zero": "[b red]Zero[/b red]", "continue": "[b green]Continue[/b green]", "leave": "[dim]Leave out[/dim]"}
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
def retest_rows(eng, labels, packs, repeat, tier, picks, force):
|
|
1662
|
+
"""(rows, rerun): the packs Start would set earlier answers aside for, or could. rerun: Retest is
|
|
1663
|
+
off but everything selected is done, so Start runs it again. A pack whose questions or settings
|
|
1664
|
+
changed can't continue (Zero or Leave out); with Retest every pack with answers starts at Zero,
|
|
1665
|
+
without it a pack that can resume continues."""
|
|
1666
|
+
base = eng.plan(labels, packs, repeat, tier, False, picks)
|
|
1667
|
+
zero = {j.key: j for j in eng.plan(labels, packs, repeat, tier, True, picks)}
|
|
1668
|
+
rerun = not force and not any(j.status == "waiting" for j in base)
|
|
1669
|
+
rows = []
|
|
1670
|
+
for b in base:
|
|
1671
|
+
z = zero[b.key]
|
|
1672
|
+
if z.status != "waiting":
|
|
1673
|
+
continue
|
|
1674
|
+
finished, unfinished = eng.set_aside(z)
|
|
1675
|
+
if b.discards:
|
|
1676
|
+
rows.append(RetestRow(b, z, finished, unfinished, ["zero", "leave"], "zero"))
|
|
1677
|
+
elif (force or rerun) and (finished or unfinished):
|
|
1678
|
+
rows.append(RetestRow(b, z, finished, unfinished, ["zero", "continue"],
|
|
1679
|
+
"zero" if force or b.status == "done" else "continue"))
|
|
1680
|
+
return rows, rerun
|
|
1681
|
+
|
|
1682
|
+
|
|
1683
|
+
class RetestScreen(ModalScreen):
|
|
1684
|
+
"""Start's confirm when earlier answers would be set aside: a choice per model · pack. Dismisses
|
|
1685
|
+
with {job key: "zero" | "continue" | "leave"}, or None on cancel."""
|
|
1686
|
+
BINDINGS = [Binding("space", "toggle", "Toggle"), Binding("a", "all('zero')", "All from zero"),
|
|
1687
|
+
Binding("c", "all('continue')", "All continue"), Binding("escape", "cancel", "Cancel")]
|
|
1688
|
+
|
|
1689
|
+
def __init__(self, rows, force, rerun):
|
|
1690
|
+
super().__init__()
|
|
1691
|
+
self.rows, self.force, self.rerun = rows, force, rerun
|
|
1692
|
+
|
|
1693
|
+
def compose(self) -> ComposeResult:
|
|
1694
|
+
if self.force:
|
|
1695
|
+
title = ("[b]Retest: choose what each pack does.[/b] Zero asks every question again; Continue keeps "
|
|
1696
|
+
"what's answered and asks the rest.")
|
|
1697
|
+
elif self.rerun:
|
|
1698
|
+
title = ("[b]Everything selected has results.[/b] Choose which packs to ask again (Zero) and which "
|
|
1699
|
+
"to keep (Continue).")
|
|
1700
|
+
else:
|
|
1701
|
+
title = ("[b]These packs can't continue:[/b] their earlier answers ran with other questions or "
|
|
1702
|
+
"settings. Start them from zero, or leave them out (put the setting back in models.toml "
|
|
1703
|
+
"to resume).")
|
|
1704
|
+
with Vertical(id="dialog", classes="wide"):
|
|
1705
|
+
yield Static(title)
|
|
1706
|
+
yield DataTable(id="choices", cursor_type="row", zebra_stripes=True)
|
|
1707
|
+
yield Static("[dim]space toggles the highlighted row · a all from zero · c all continue · "
|
|
1708
|
+
"Zero: earlier answers go to results/<model>/history/ (kept, not counted)[/dim]")
|
|
1709
|
+
with Horizontal(classes="dialog-buttons"):
|
|
1710
|
+
yield Button(self.start_label(), id="start", variant="primary")
|
|
1711
|
+
yield Button("Cancel", id="cancel")
|
|
1712
|
+
|
|
1713
|
+
def on_mount(self):
|
|
1714
|
+
t = self.query_one(DataTable)
|
|
1715
|
+
t.add_column("Choice", key="choice", width=10)
|
|
1716
|
+
t.add_column("Model", key="model")
|
|
1717
|
+
t.add_column("Pack", key="pack")
|
|
1718
|
+
t.add_column("Earlier answers", key="earlier")
|
|
1719
|
+
t.add_column("What happens", key="happens")
|
|
1720
|
+
for r in self.rows:
|
|
1721
|
+
t.add_row(CHOICE_TEXT[r.choice], r.base.label, r.base.pack.label, r.earlier(), r.happens(), key=r.key)
|
|
1722
|
+
self.update_start()
|
|
1723
|
+
t.focus()
|
|
1724
|
+
|
|
1725
|
+
def redraw(self, r):
|
|
1726
|
+
t = self.query_one(DataTable)
|
|
1727
|
+
t.update_cell(r.key, "choice", CHOICE_TEXT[r.choice])
|
|
1728
|
+
t.update_cell(r.key, "happens", r.happens(), update_width=True)
|
|
1729
|
+
self.update_start()
|
|
1730
|
+
|
|
1731
|
+
def start_label(self):
|
|
1732
|
+
n = collections.Counter(r.choice for r in self.rows)
|
|
1733
|
+
parts = [f"{n[c]} {w}" for c, w in (("zero", "from zero"), ("continue", "continue"), ("leave", "left out"))
|
|
1734
|
+
if n[c]]
|
|
1735
|
+
return f"Start ({', '.join(parts)})"
|
|
1736
|
+
|
|
1737
|
+
def update_start(self):
|
|
1738
|
+
button = self.query_one("#start", Button)
|
|
1739
|
+
button.label = self.start_label()
|
|
1740
|
+
button.refresh(layout=True)
|
|
1741
|
+
|
|
1742
|
+
def action_toggle(self):
|
|
1743
|
+
t = self.query_one(DataTable)
|
|
1744
|
+
if not self.rows:
|
|
1745
|
+
return
|
|
1746
|
+
r = self.rows[t.cursor_row]
|
|
1747
|
+
r.choice = r.choices[(r.choices.index(r.choice) + 1) % len(r.choices)]
|
|
1748
|
+
self.redraw(r)
|
|
1749
|
+
|
|
1750
|
+
def action_all(self, choice):
|
|
1751
|
+
for r in self.rows:
|
|
1752
|
+
if choice in r.choices:
|
|
1753
|
+
r.choice = choice
|
|
1754
|
+
self.redraw(r)
|
|
1755
|
+
|
|
1756
|
+
def action_cancel(self):
|
|
1757
|
+
self.dismiss(None)
|
|
1758
|
+
|
|
1759
|
+
@on(Button.Pressed)
|
|
1760
|
+
def answer(self, event):
|
|
1761
|
+
self.dismiss({r.key: r.choice for r in self.rows} if event.button.id == "start" else None)
|
|
1762
|
+
|
|
1763
|
+
|
|
1621
1764
|
class ConfirmScreen(ModalScreen):
|
|
1622
1765
|
def __init__(self, question):
|
|
1623
1766
|
super().__init__()
|
|
@@ -2952,7 +3095,8 @@ class EvalsApp(App):
|
|
|
2952
3095
|
#reasoning { color: $text-muted; }
|
|
2953
3096
|
StreamView { height: 1fr; border: none; padding: 0; }
|
|
2954
3097
|
#tabs { height: 12; }
|
|
2955
|
-
AddModelScreen, ConfirmScreen { align: center middle; }
|
|
3098
|
+
AddModelScreen, ConfirmScreen, RetestScreen { align: center middle; }
|
|
3099
|
+
RetestScreen #start { width: auto; }
|
|
2956
3100
|
SelectionList > .selection-list--button, SelectionList > .selection-list--button-highlighted {
|
|
2957
3101
|
color: $panel; background: $panel;
|
|
2958
3102
|
}
|
|
@@ -3091,8 +3235,6 @@ class EvalsApp(App):
|
|
|
3091
3235
|
if problem:
|
|
3092
3236
|
self.notify(f"Can't start {item.title}: {problem}", severity="warning", timeout=15)
|
|
3093
3237
|
return
|
|
3094
|
-
if item.kind == "Run" and item.rerun:
|
|
3095
|
-
self.notify("Everything selected had results; running it again.")
|
|
3096
3238
|
self.add_session(screen)
|
|
3097
3239
|
return
|
|
3098
3240
|
run = self.active_session
|
|
@@ -380,6 +380,9 @@ class PickTests(unittest.TestCase):
|
|
|
380
380
|
hist = os.listdir(os.path.join(self.ws, "results", "m", "history"))
|
|
381
381
|
self.assertTrue(any(f.startswith("apps-") for f in hist), hist)
|
|
382
382
|
self.assertEqual(self.status(), "certified")
|
|
383
|
+
self.server("oracle")
|
|
384
|
+
tuieval(self.ws, "run", "--tests", "apps:b", "--retest") # --retest = --force
|
|
385
|
+
self.assertEqual(self.rows(), {"a": True, "b": True, "c": True, "d": True})
|
|
383
386
|
|
|
384
387
|
def test_cli_errors(self):
|
|
385
388
|
self.server("oracle")
|
|
@@ -408,6 +411,160 @@ class PickTests(unittest.TestCase):
|
|
|
408
411
|
self.assertEqual(len(self.rows("other")), 1) # no pick: the Screen sample (screen = 1)
|
|
409
412
|
|
|
410
413
|
|
|
414
|
+
class Retest(unittest.TestCase):
|
|
415
|
+
"""Retest starts from zero (an unfinished run's answers too), keeps every replaced answer in
|
|
416
|
+
history/ (smoke too), and the TUI asks per pack before anything moves."""
|
|
417
|
+
|
|
418
|
+
def setUp(self):
|
|
419
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
420
|
+
self.ws = os.path.join(self.tmp.name, "ws")
|
|
421
|
+
tuieval(self.tmp.name, "init", self.ws)
|
|
422
|
+
for name in ("apps", "other"):
|
|
423
|
+
d = os.path.join(self.ws, "packs", name)
|
|
424
|
+
os.makedirs(d)
|
|
425
|
+
write(os.path.join(d, "pack.toml"), f'label = "{name}"\nscreen = 4\n[certify]\nrepeat = 1\n'
|
|
426
|
+
'[gate]\nmin_accuracy = 0.1\n')
|
|
427
|
+
write(os.path.join(d, "tests.yaml"), "".join(
|
|
428
|
+
f"- {{id: {t}, input: {name} question {t}, expected: 1, reference: 'ANSWER: 1', wrong: ['ANSWER: 2'], "
|
|
429
|
+
"difficulty: easy}\n" for t in "abcd"))
|
|
430
|
+
self.port = free_port()
|
|
431
|
+
with open(os.path.join(self.ws, "models.toml"), "a") as f:
|
|
432
|
+
f.write(f'\n[servers.m]\nurl = "http://127.0.0.1:{self.port}"\n')
|
|
433
|
+
self.mock = Mock(os.path.join(self.ws, "packs"), "oracle", port=self.port)
|
|
434
|
+
tuieval(self.ws, "add", "mock", "--server", "m", "--label", "m")
|
|
435
|
+
tuieval(self.ws, "run", "--tier", "certify")
|
|
436
|
+
self.mock.stop()
|
|
437
|
+
self.mock = Mock(os.path.join(self.ws, "packs"), "wrong", port=self.port) # retested answers fail
|
|
438
|
+
|
|
439
|
+
def tearDown(self):
|
|
440
|
+
self.mock.stop()
|
|
441
|
+
self.tmp.cleanup()
|
|
442
|
+
|
|
443
|
+
def path(self, pack, smoke=False):
|
|
444
|
+
return os.path.join(self.ws, "results", *(["smoke"] if smoke else []), "m", f"{pack}.json")
|
|
445
|
+
|
|
446
|
+
def passes(self, pack):
|
|
447
|
+
return {r["test"]: r["pass"] for r in json.loads(read(self.path(pack)))["results"]}
|
|
448
|
+
|
|
449
|
+
def unfinished(self, pack, tests):
|
|
450
|
+
"""Turn a pack's finished answers for these tests into an unfinished run of the same settings."""
|
|
451
|
+
from tuieval import engine
|
|
452
|
+
e = engine.Engine(root=self.ws)
|
|
453
|
+
j = e.plan(["m"], [pack], None, "certify", False, {})[0]
|
|
454
|
+
rows = [r for r in json.loads(read(j.out_path))["results"] if r["test"] in tests]
|
|
455
|
+
write(j.out_path + ".partial.jsonl", "".join(json.dumps(l) + "\n" for l in [e._partial_header(j)] + rows))
|
|
456
|
+
os.remove(j.out_path)
|
|
457
|
+
|
|
458
|
+
def history(self, smoke=False):
|
|
459
|
+
d = os.path.join(self.ws, "results", *(["smoke"] if smoke else []), "m", "history")
|
|
460
|
+
return sorted(os.listdir(d)) if os.path.isdir(d) else []
|
|
461
|
+
|
|
462
|
+
def test_cli_retest_starts_from_zero(self):
|
|
463
|
+
self.unfinished("apps", "ab")
|
|
464
|
+
out = tuieval(self.ws, "run", "--tier", "certify", "--packs", "apps", "--retest").stdout
|
|
465
|
+
self.assertIn("m/apps: retest from zero; its 2 unfinished answers move to history/", out)
|
|
466
|
+
self.assertNotIn("resuming", out)
|
|
467
|
+
self.assertEqual(self.passes("apps"), dict.fromkeys("abcd", False)) # all asked again
|
|
468
|
+
self.assertTrue(any(f.endswith(".partial.jsonl") for f in self.history()))
|
|
469
|
+
|
|
470
|
+
def test_picked_retest_keeps_the_rest_of_an_unfinished_run(self):
|
|
471
|
+
self.unfinished("apps", "abc")
|
|
472
|
+
tuieval(self.ws, "run", "--tier", "certify", "--tests", "apps:b", "--retest")
|
|
473
|
+
partial = self.path("apps") + ".partial.jsonl"
|
|
474
|
+
self.assertFalse(os.path.exists(partial))
|
|
475
|
+
self.assertEqual(self.passes("apps"), {"a": True, "b": False, "c": True}) # only b asked again
|
|
476
|
+
self.assertTrue(any(f.endswith(".partial.jsonl") for f in self.history())) # a copy, b included
|
|
477
|
+
|
|
478
|
+
def test_smoke_retest_keeps_history(self):
|
|
479
|
+
tuieval(self.ws, "run", "--tier", "smoke", "--packs", "apps")
|
|
480
|
+
tuieval(self.ws, "run", "--tier", "smoke", "--packs", "apps", "--retest")
|
|
481
|
+
self.assertEqual(len(self.history(smoke=True)), 1)
|
|
482
|
+
self.assertEqual(tuieval(self.ws, "list").returncode, 0)
|
|
483
|
+
|
|
484
|
+
def tui(self, steps):
|
|
485
|
+
"""Drive Setup with apps and other ticked for model m (Certify); steps runs against the screen."""
|
|
486
|
+
code = textwrap.dedent("""
|
|
487
|
+
import asyncio, json, time
|
|
488
|
+
from tuieval.tui import EvalsApp, RetestScreen, RunScreen
|
|
489
|
+
from textual.widgets import SelectionList, Checkbox
|
|
490
|
+
async def go():
|
|
491
|
+
app = EvalsApp({})
|
|
492
|
+
async with app.run_test(size=(160, 50)) as pilot:
|
|
493
|
+
await pilot.pause()
|
|
494
|
+
s = app.screen
|
|
495
|
+
for p in ("apps", "other"):
|
|
496
|
+
s.query_one("#suites", SelectionList).select(p)
|
|
497
|
+
s.selected_models = {"m"}
|
|
498
|
+
s.refresh_models()
|
|
499
|
+
s.query_one("#tier-certify").value = True
|
|
500
|
+
out = {}
|
|
501
|
+
%s
|
|
502
|
+
deadline = time.time() + 120
|
|
503
|
+
while time.time() < deadline and (app.queue or (isinstance(app.active_session, RunScreen)
|
|
504
|
+
and app.active_session.running)):
|
|
505
|
+
await pilot.pause(0.2)
|
|
506
|
+
out["sessions"] = len(app.sessions)
|
|
507
|
+
print(json.dumps(out))
|
|
508
|
+
asyncio.run(go())
|
|
509
|
+
""") % textwrap.indent(textwrap.dedent(steps), " " * 8)
|
|
510
|
+
p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=300,
|
|
511
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""))
|
|
512
|
+
out = json.loads(p.stdout.strip().splitlines()[-1]) if p.stdout.strip() else p.stderr
|
|
513
|
+
self.assertIsInstance(out, dict, out)
|
|
514
|
+
return out
|
|
515
|
+
|
|
516
|
+
def test_tui_asks_per_pack(self):
|
|
517
|
+
self.unfinished("other", "ab")
|
|
518
|
+
out = self.tui("""
|
|
519
|
+
s.query_one("#force", Checkbox).value = True
|
|
520
|
+
await pilot.pause()
|
|
521
|
+
s.action_start()
|
|
522
|
+
await pilot.pause()
|
|
523
|
+
d = app.screen
|
|
524
|
+
out["asked"] = isinstance(d, RetestScreen)
|
|
525
|
+
out["rows"] = [(r.key, r.choice, r.finished, r.unfinished) for r in d.rows]
|
|
526
|
+
await pilot.press("down", "space") # other: Continue
|
|
527
|
+
out["button"] = str(d.query_one("#start").label)
|
|
528
|
+
await pilot.press("escape") # cancel: nothing runs
|
|
529
|
+
await pilot.pause()
|
|
530
|
+
out["after_cancel"] = len(app.sessions)
|
|
531
|
+
s.action_start()
|
|
532
|
+
await pilot.pause()
|
|
533
|
+
await pilot.press("down", "space")
|
|
534
|
+
await pilot.pause()
|
|
535
|
+
app.screen.query_one("#start").press()
|
|
536
|
+
await pilot.pause()
|
|
537
|
+
""")
|
|
538
|
+
self.assertTrue(out["asked"])
|
|
539
|
+
self.assertEqual(out["rows"], [["m/apps", "zero", 4, 0], ["m/other", "zero", 0, 2]])
|
|
540
|
+
self.assertEqual(out["button"], "Start (1 from zero, 1 continue)")
|
|
541
|
+
self.assertEqual(out["after_cancel"], 0)
|
|
542
|
+
self.assertEqual(out["sessions"], 1)
|
|
543
|
+
self.assertEqual(self.passes("apps"), dict.fromkeys("abcd", False)) # from zero
|
|
544
|
+
self.assertEqual(self.passes("other"), {"a": True, "b": True, "c": False, "d": False}) # continued
|
|
545
|
+
|
|
546
|
+
def test_tui_everything_done_or_settings_changed(self):
|
|
547
|
+
models = os.path.join(self.ws, "models.toml")
|
|
548
|
+
write(models, read(models).replace("temperature = 0.7", "temperature = 0.6"))
|
|
549
|
+
out = self.tui("""
|
|
550
|
+
s.query_one("#suites", SelectionList).deselect("other")
|
|
551
|
+
await pilot.pause()
|
|
552
|
+
s.action_start()
|
|
553
|
+
await pilot.pause()
|
|
554
|
+
d = app.screen
|
|
555
|
+
out["rows"] = [(r.key, r.choices, r.choice) for r in d.rows]
|
|
556
|
+
await pilot.press("space") # Leave out
|
|
557
|
+
await pilot.pause()
|
|
558
|
+
out["happens"] = d.rows[0].happens()
|
|
559
|
+
d.query_one("#start").press()
|
|
560
|
+
await pilot.pause()
|
|
561
|
+
""")
|
|
562
|
+
self.assertEqual(out["rows"], [["m/apps", ["zero", "leave"], "zero"]])
|
|
563
|
+
self.assertEqual(out["happens"], "not run now")
|
|
564
|
+
self.assertEqual(out["sessions"], 0) # nothing ran
|
|
565
|
+
self.assertEqual(self.passes("apps"), dict.fromkeys("abcd", True))
|
|
566
|
+
|
|
567
|
+
|
|
411
568
|
class Parallel(unittest.TestCase):
|
|
412
569
|
"""Several models at a time (parallel_models / --parallel): each runs in its own lane on its own
|
|
413
570
|
port, answers note the models they ran alongside, and the scheduler keeps the memory and
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev70 → tuieval-0.2.0.dev74}/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
|