tuieval 0.2.0.dev137__tar.gz → 0.2.0.dev141__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.dev137 → tuieval-0.2.0.dev141}/CHANGELOG.md +2 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/PKG-INFO +1 -1
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/models.md +1 -1
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/compare.py +3 -1
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/engine.py +23 -3
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/run_evals.py +9 -4
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/tui.py +35 -18
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/tests/test_tuieval.py +157 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/.gitignore +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/LICENSE +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/README.md +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/agent-packs.md +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/agents.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/dates.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/insights.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/logs.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/pta.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/tests/mock_server.py +0 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Per question → Comparison: a **Pass rate** column beside Passed, the share of answers right with repeats included (shown when some model has repeats).
|
|
6
|
+
- Repeats (Setup, `tuieval run --repeat N`) add N rounds to the saved answers: ×1 run with 2 gives ×3, not ×2. Retest, a blank count and presets still mean the total.
|
|
5
7
|
- Answers stuck in a loop (one short unit repeated over 2000 chars, in the reasoning or the answer) are stopped as they stream and failed as `LOOP`, instead of running to max_tokens. `[defaults] loop_stop_chars` sets the length; 0 turns it off.
|
|
6
8
|
- Repeats: the PTA index, Per question, `tuieval compare` and Setup show how many times each model answered each question (`×3`, `×1-3`), so single-run results stand out.
|
|
7
9
|
- Partial credit: the PTA index shows each model's score (e.g. 5 of 6 checks passed is 83%) beside accuracy, and can be ordered by it (By score, `tuieval pta --sort score`). Why models fail splits failures into near miss, partly right and wrong; Failures shows each one's kind and checks and filters by kind; Per question cells show `✗ 5/6`. Verdicts are unchanged.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev141
|
|
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
|
|
@@ -106,7 +106,7 @@ The **PTA index** puts parsimony (tokens), time and accuracy side by side, as a
|
|
|
106
106
|
- **T time**: the total time to answer those questions, as how many times the fastest model's, e.g. `28×` (★ in **time/answer**).
|
|
107
107
|
- **A accuracy**: the share of answers to those questions that passed. Answers cut off at `max_tokens` count as wrong, but show in orange after the green, with `(n% truncated)` beside the number and a **truncated** column in the table: those failures are a model out of tokens (overthinking), not a wrong answer yet. A pack's own `max_tokens` gives it more room.
|
|
108
108
|
- **Score**: partial credit, averaged over the same answers. Graders that can tell how close an answer came give it: the code grader the share of checks passed (5 of 6 is 83%, where accuracy counts it wrong), agent packs the share of hidden tests, the tool-call and RAG graders part marks. It shows beside the accuracy when the two differ, and in a **score** column. Order the models by it with the **By score** dropdown in the tab or `tuieval pta --sort score`; verdicts still use accuracy only.
|
|
109
|
-
- **Repeats**: beside each name, how many times the model answered each question compared (`×3`, or `×1-3` when it varies), also a **repeats** column here and in `tuieval compare`. A `×1` model (in yellow) answered each question once, so its accuracy is rougher: one answer more or less right moves it by 100 / questions points. Setup shows the same beside each pack in a model's results (`✓coding×3`).
|
|
109
|
+
- **Repeats**: beside each name, how many times the model answered each question compared (`×3`, or `×1-3` when it varies), also a **repeats** column here and in `tuieval compare`. A `×1` model (in yellow) answered each question once, so its accuracy is rougher: one answer more or less right moves it by 100 / questions points. Setup shows the same beside each pack in a model's results (`✓coding×3`). To steady it, set **Repeats** in Setup (or `tuieval run --repeat N`): N adds that many rounds to the answers already saved, so a `×1` model run with 2 ends up `×3` (with Retest, N in all; left blank, the tier's default count in all). A preset's repeat count is in all too, until you type another.
|
|
110
110
|
- **Over budget**: some providers don't stop a model at `max_tokens` (thinking goes on past it), so the answer isn't cut off but the model had more room than the others. Answers that used more tokens than were sent as `max_tokens` show as `(n over budget)` beside the accuracy, an **over budget** column, `over` in Results → Per question and a note in the answer view. They're only flagged; `over_budget = "fail"` in `[defaults]` counts them as failed instead (stored results aren't changed, so switching back restores them).
|
|
111
111
|
|
|
112
112
|
The P and T bars are as long as the tokens and time used: the model that used the most fills its bar, the others in proportion (at least one block), so **shorter is better**. The A bar is the share of answers right, so **longer is better**; it's full only when every answer passed. Each heading says which way is better.
|
|
@@ -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.dev141'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev141')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -420,7 +420,8 @@ def complete_models(rows, suite, test_ids=None):
|
|
|
420
420
|
def compare_models(tests, models):
|
|
421
421
|
"""Per model over per_question entries that all `models` answered: passes, token and time
|
|
422
422
|
totals/medians (totals add each question's median over repeats, so repeats don't inflate them), questions where it used the fewest tokens / was fastest (among passing
|
|
423
|
-
answers for tokens; ties count for each), answers cut off, mean score (partial credit)
|
|
423
|
+
answers for tokens; ties count for each), answers cut off, mean score (partial credit),
|
|
424
|
+
pass rate (answers right with repeats, each question weighing the same)."""
|
|
424
425
|
out = []
|
|
425
426
|
for m in models:
|
|
426
427
|
cs = [t["cells"][m] for t in tests]
|
|
@@ -438,6 +439,7 @@ def compare_models(tests, models):
|
|
|
438
439
|
rs = [r for c in cs for r in c["rows"]]
|
|
439
440
|
out.append({"model": m, "n": len(cs), "passed": passed, "total_tokens": total,
|
|
440
441
|
"score": statistics.fmean(c["score"] for c in cs) if cs else None,
|
|
442
|
+
"pass_rate": statistics.fmean(c["passed"] / c["n"] for c in cs) if cs else None,
|
|
441
443
|
"avg_pp_tps": _rate([(r.get("prompt_tokens"), r["prompt_tps"]) for r in rs]),
|
|
442
444
|
"avg_gen_tps": _rate([(r["tokens"], r["gen_tps"]) for r in rs]),
|
|
443
445
|
"med_tokens": median(c["tokens"] for c in cs), "med_secs": median(c["secs"] for c in cs),
|
|
@@ -443,7 +443,7 @@ def save_preset(path, name, labels, packs, repeat, tests=None):
|
|
|
443
443
|
class Job:
|
|
444
444
|
model: dict
|
|
445
445
|
pack: packs_mod.Pack
|
|
446
|
-
repeat: int
|
|
446
|
+
repeat: int # rounds each question has once it's done (earlier ones included)
|
|
447
447
|
tests: list # the tests this tier runs (or the ones picked)
|
|
448
448
|
out_path: str
|
|
449
449
|
sampling: dict
|
|
@@ -465,6 +465,7 @@ class Job:
|
|
|
465
465
|
stop_early: bool = False # stop once the pack's FAIL is certain (verdict.fail_certain)
|
|
466
466
|
first: list = dataclasses.field(default_factory=list) # test ids round 1 asks first (most failed)
|
|
467
467
|
claimed: bool = False # its model's server took it on: it can no longer be removed from the run
|
|
468
|
+
base: int = 0 # rounds already in its result file that a chosen repeat count adds to
|
|
468
469
|
|
|
469
470
|
@property
|
|
470
471
|
def label(self):
|
|
@@ -1148,6 +1149,17 @@ class Engine:
|
|
|
1148
1149
|
redo = {t["id"] for t in job.tests}
|
|
1149
1150
|
return [r for r in rows if r["test"] not in redo]
|
|
1150
1151
|
|
|
1152
|
+
def saved_rounds(self, job):
|
|
1153
|
+
"""The most rounds any of the job's questions has in its finished result file (0 when the file
|
|
1154
|
+
doesn't match the job's questions and settings). An unfinished run's answers don't count, so
|
|
1155
|
+
resuming one doesn't add more rounds."""
|
|
1156
|
+
data = self._result(job.out_path)
|
|
1157
|
+
if not data or data["pack"]["fingerprint"] != job.pack.fingerprint or not settings_match(data, job.settings):
|
|
1158
|
+
return 0
|
|
1159
|
+
ids = {t["id"] for t in job.tests}
|
|
1160
|
+
rounds = collections.Counter(t for t, _ in {(r["test"], r["repeat"]) for r in data["results"] if r["test"] in ids})
|
|
1161
|
+
return max(rounds.values(), default=0)
|
|
1162
|
+
|
|
1151
1163
|
def _fill_progress(self, job):
|
|
1152
1164
|
"""Set a job's done/passed/failed and earlier sittings from what's stored."""
|
|
1153
1165
|
kept, new, job.earlier = self._stored(job)
|
|
@@ -1212,10 +1224,13 @@ class Engine:
|
|
|
1212
1224
|
return not current and header_matches(head, pack.fingerprint, settings)
|
|
1213
1225
|
|
|
1214
1226
|
# ---- planning
|
|
1215
|
-
def plan(self, labels, pack_names, repeat=None, tier="screen", force=False, tests=None, stop_early=None
|
|
1227
|
+
def plan(self, labels, pack_names, repeat=None, tier="screen", force=False, tests=None, stop_early=None,
|
|
1228
|
+
total=False):
|
|
1216
1229
|
"""Jobs for models x packs. tier: smoke (3 tests), screen (a spread sample), certify (all
|
|
1217
1230
|
tests). repeat: times each question is asked; None = the tier default (1 for smoke and
|
|
1218
|
-
screen, the pack's certification repeats for certify)
|
|
1231
|
+
screen, the pack's certification repeats for certify), counted in total; a number adds that
|
|
1232
|
+
many rounds to the ones already in the result file (a retest starts from none), or with total
|
|
1233
|
+
(a preset's count) is the rounds in all. tests: {pack: [test ids]} runs just
|
|
1219
1234
|
those tests of a pack instead of the tier's sample (unknown ids raise PackError). Earlier
|
|
1220
1235
|
matching results are kept and only missing answers are run, unless force; force with
|
|
1221
1236
|
picked tests reruns those and keeps the pack's other answers. force is True, False, or the
|
|
@@ -1240,6 +1255,9 @@ class Engine:
|
|
|
1240
1255
|
job.first = self.most_failed_first(pack)
|
|
1241
1256
|
sv = self.serving(m)
|
|
1242
1257
|
status = self.result_status(label, name, tier)
|
|
1258
|
+
if repeat and not total and job.merge and not job.redo and not (status or "").startswith("outdated"):
|
|
1259
|
+
job.base = self.saved_rounds(job)
|
|
1260
|
+
job.repeat = job.base + repeat
|
|
1243
1261
|
job.discards = self._discards(job)
|
|
1244
1262
|
missing = [n for n in pack.modules if not importlib.util.find_spec(n)]
|
|
1245
1263
|
no_agent = agents.problem(self.cfg, pack, self.root) if pack.kind == "agent" else None
|
|
@@ -1273,6 +1291,8 @@ class Engine:
|
|
|
1273
1291
|
job.note = f"{job.done} of {job.total} done earlier, running the rest"
|
|
1274
1292
|
if job.status != "waiting":
|
|
1275
1293
|
job.discards = []
|
|
1294
|
+
elif job.base:
|
|
1295
|
+
job.note = "; ".join(filter(None, [job.note, f"×{job.base} → ×{job.repeat}"]))
|
|
1276
1296
|
for what, n, why in job.discards:
|
|
1277
1297
|
if what == "unfinished": # (a finished file's reason is already the note)
|
|
1278
1298
|
job.note = "; ".join(filter(None, [job.note, f"{n} unfinished answers set aside ({why})"]))
|
|
@@ -101,7 +101,8 @@ class Printer:
|
|
|
101
101
|
"so the server may be returning cached answers rather than fresh ones", RED)
|
|
102
102
|
|
|
103
103
|
def on_job_started(self, job, resumed, earlier_s=0, sittings=0):
|
|
104
|
-
|
|
104
|
+
reps = f"{job.repeat - job.base} more repeats (×{job.base} → ×{job.repeat})" if job.base else f"{job.repeat} repeats"
|
|
105
|
+
say(f"{job.key}: {job.count} tests x {reps}" + (f", resuming after {resumed}" if resumed else "")
|
|
105
106
|
+ (f" ({earlier_s / 60:.0f} min over {sittings} earlier sitting(s))" if sittings else ""))
|
|
106
107
|
|
|
107
108
|
def on_request_started(self, job, test, test_id, has_image, repeat, difficulty="unrated"):
|
|
@@ -792,8 +793,9 @@ def main(argv=None):
|
|
|
792
793
|
p.add_argument("--stop-early", action="store_true",
|
|
793
794
|
help="stop each pack once its FAIL is certain (a critical failure, or a bar out of reach), "
|
|
794
795
|
"asking first what models fail most (default: each pack's stop_early)")
|
|
795
|
-
p.add_argument("--repeat", type=int, help="
|
|
796
|
-
"
|
|
796
|
+
p.add_argument("--repeat", type=int, help="rounds to add: each question is asked N more times on top of its "
|
|
797
|
+
"saved answers (with --retest, N in all). Default: up to 1 for "
|
|
798
|
+
"smoke and screen, the pack's certification count for certify")
|
|
797
799
|
p.add_argument("--tier", choices=engine.TIERS, default="screen",
|
|
798
800
|
help="smoke: 3 tests; screen (default): a spread sample; certify: everything, with repeats")
|
|
799
801
|
p.add_argument("--smoke", action="store_const", const="smoke", dest="tier", help="same as --tier smoke")
|
|
@@ -838,12 +840,14 @@ def main(argv=None):
|
|
|
838
840
|
sys.exit("no models to run yet: tuieval add <GGUF path or model id>, or name a hosted one with "
|
|
839
841
|
"--only openrouter:<model id>" if not models else
|
|
840
842
|
"no local models; hosted ones (they cost money) run only when named: --only <label>")
|
|
843
|
+
repeat_total = False
|
|
841
844
|
if args.preset:
|
|
842
845
|
presets = engine.load_presets(os.path.join(os.path.dirname(os.path.abspath(models_file(args.models))),
|
|
843
846
|
"presets.toml"))
|
|
844
847
|
if args.preset not in presets:
|
|
845
848
|
sys.exit(f"unknown preset {args.preset!r}; presets: {', '.join(presets) or 'none (save one in the TUI with p)'}")
|
|
846
849
|
labels, pack_names, preset_repeat, tests = engine.resolve_preset(e.cfg, presets[args.preset], list(e.packs))
|
|
850
|
+
repeat_total = not args.repeat and bool(preset_repeat) # a preset's count is the rounds in all
|
|
847
851
|
args.repeat = args.repeat or preset_repeat
|
|
848
852
|
if args.packs:
|
|
849
853
|
pack_names = [x.strip() for x in args.packs.split(",")]
|
|
@@ -853,7 +857,8 @@ def main(argv=None):
|
|
|
853
857
|
if args.tests:
|
|
854
858
|
tests, pack_names = parse_tests(args.tests, pack_names if (args.packs or args.preset) else None, e.packs)
|
|
855
859
|
try:
|
|
856
|
-
jobs = e.plan(labels, pack_names, args.repeat, args.tier, args.force, tests, args.stop_early or None
|
|
860
|
+
jobs = e.plan(labels, pack_names, args.repeat, args.tier, args.force, tests, args.stop_early or None,
|
|
861
|
+
repeat_total)
|
|
857
862
|
except packs_mod.PackError as err:
|
|
858
863
|
sys.exit(str(err))
|
|
859
864
|
|
|
@@ -1025,6 +1025,11 @@ class SetupScreen(Screen):
|
|
|
1025
1025
|
list(self.query_one("#suites", SelectionList).selected), repeat,
|
|
1026
1026
|
self.tier(), self.query_one("#force", Checkbox).value)
|
|
1027
1027
|
|
|
1028
|
+
def repeat_total(self):
|
|
1029
|
+
"""Is the Repeats count the rounds in all? Yes while it's a loaded preset's; a typed one adds rounds."""
|
|
1030
|
+
value = self.query_one("#repeat", Input).value
|
|
1031
|
+
return bool(value) and value == getattr(self, "preset_repeat", None)
|
|
1032
|
+
|
|
1028
1033
|
def parallel(self):
|
|
1029
1034
|
"""Models at a time for the next run: the field, or blank for this machine's parallel_models."""
|
|
1030
1035
|
try:
|
|
@@ -1088,16 +1093,21 @@ class SetupScreen(Screen):
|
|
|
1088
1093
|
"[b]s[/b] to start." + self.queue_hint())
|
|
1089
1094
|
return
|
|
1090
1095
|
picks = self.current_picks(suites)
|
|
1091
|
-
|
|
1096
|
+
total = self.repeat_total()
|
|
1097
|
+
jobs = self.app.engine.plan(labels, suites, repeat, tier, force, picks, total=total)
|
|
1092
1098
|
rerun = not any(j.status == "waiting" for j in jobs)
|
|
1093
1099
|
if rerun: # everything has results: Start runs it again, so estimate that
|
|
1094
|
-
jobs = self.app.engine.plan(labels, suites, repeat, tier, True, picks)
|
|
1100
|
+
jobs = self.app.engine.plan(labels, suites, repeat, tier, True, picks, total=total)
|
|
1095
1101
|
run = [j for j in jobs if j.status == "waiting"]
|
|
1096
1102
|
skipped = [j for j in jobs if j.status == "skipped"]
|
|
1097
1103
|
secs, notes = self.app.engine.estimate_seconds(jobs)
|
|
1098
|
-
todo = sum(j.total for j in run)
|
|
1099
|
-
reps = sorted({j.repeat for j in jobs})
|
|
1100
|
-
reps_text = "/".join(map(str, reps)) + (" (pack default)" if repeat is None and len(reps) > 1 else "")
|
|
1104
|
+
todo = sum(j.total - j.done for j in run)
|
|
1105
|
+
reps = sorted({j.repeat - j.base for j in jobs})
|
|
1106
|
+
reps_text = "/".join(map(str, reps)) + (" (pack default)" if repeat is None and len(reps) > 1 else "") \
|
|
1107
|
+
+ (" in all (preset)" if total else "")
|
|
1108
|
+
bases = sorted({j.base for j in run if j.base})
|
|
1109
|
+
if bases: # a chosen count adds to the rounds already answered
|
|
1110
|
+
reps_text += " more" + " [dim](" + ", ".join(f"×{b} → ×{b + repeat}" for b in bases) + ")[/dim]"
|
|
1101
1111
|
text = (f"[b]{tier.title()}[/b]: [b]{len(labels)}[/b] model(s) × [b]{len(suites)}[/b] pack(s) × "
|
|
1102
1112
|
f"[b]{reps_text}[/b] repeat(s) = "
|
|
1103
1113
|
f"[b]{todo:,}[/b] answers · about [b]{fmt_secs(secs)}[/b]"
|
|
@@ -1158,6 +1168,7 @@ class SetupScreen(Screen):
|
|
|
1158
1168
|
for name in sel.get("packs", []):
|
|
1159
1169
|
if name in e.packs:
|
|
1160
1170
|
packs.select(name)
|
|
1171
|
+
self.preset_repeat = str(sel["repeat"]) if sel.get("repeat") and sel.get("repeat_total") else None
|
|
1161
1172
|
self.query_one("#repeat", Input).value = str(sel["repeat"]) if sel.get("repeat") else ""
|
|
1162
1173
|
if "parallel" in sel:
|
|
1163
1174
|
self.query_one("#parallel", Input).value = str(sel["parallel"]) if sel["parallel"] else ""
|
|
@@ -1201,10 +1212,11 @@ class SetupScreen(Screen):
|
|
|
1201
1212
|
self.notify("Select at least one eval pack and one model.", severity="warning")
|
|
1202
1213
|
return
|
|
1203
1214
|
picks = self.current_picks(suites)
|
|
1204
|
-
|
|
1205
|
-
|
|
1215
|
+
total = self.repeat_total()
|
|
1216
|
+
sel = {"models": labels, "packs": suites, "repeat": repeat, "repeat_total": total, "tier": tier, "force": force,
|
|
1217
|
+
"tests": picks, "parallel": self.parallel(), "stop_early": self.query_one("#stop-early", Checkbox).value}
|
|
1206
1218
|
e = self.app.engine
|
|
1207
|
-
jobs = e.plan(labels, suites, repeat, tier, True, picks)
|
|
1219
|
+
jobs = e.plan(labels, suites, repeat, tier, True, picks, total=total)
|
|
1208
1220
|
if not any(j.status == "waiting" for j in jobs):
|
|
1209
1221
|
reasons = sorted({j.note for j in jobs if j.status == "skipped"})
|
|
1210
1222
|
self.notify(f"Nothing selected can run: {'; '.join(reasons)}", severity="warning")
|
|
@@ -1212,7 +1224,7 @@ class SetupScreen(Screen):
|
|
|
1212
1224
|
self.app.save_state({k: v for k, v in sel.items() if k != "models"}) # models aren't remembered
|
|
1213
1225
|
# Packs whose earlier answers a start would set aside (Retest, everything already done, or
|
|
1214
1226
|
# other questions or settings): the user picks per pack before anything moves.
|
|
1215
|
-
rows, rerun = retest_rows(e, labels, suites, repeat, tier, picks, force)
|
|
1227
|
+
rows, rerun = retest_rows(e, labels, suites, repeat, tier, picks, force, total)
|
|
1216
1228
|
|
|
1217
1229
|
def go(choices):
|
|
1218
1230
|
if choices is None:
|
|
@@ -1640,7 +1652,7 @@ class PresetsScreen(ModalScreen):
|
|
|
1640
1652
|
name = list(self.presets)[event.option_index]
|
|
1641
1653
|
labels, packs, repeat, tests = engine.resolve_preset(self.app.engine.cfg, self.presets[name],
|
|
1642
1654
|
list(self.app.engine.packs))
|
|
1643
|
-
self.dismiss(("load", {"models": labels, "packs": packs, "repeat": repeat, "tests": tests}))
|
|
1655
|
+
self.dismiss(("load", {"models": labels, "packs": packs, "repeat": repeat, "repeat_total": True, "tests": tests}))
|
|
1644
1656
|
|
|
1645
1657
|
@on(Button.Pressed, "#save")
|
|
1646
1658
|
@on(Input.Submitted, "#name")
|
|
@@ -1765,7 +1777,8 @@ class QueuedRun:
|
|
|
1765
1777
|
"""(jobs, None), or (None, why it can't run) now."""
|
|
1766
1778
|
sel = self.sel
|
|
1767
1779
|
jobs = [j for j in eng.plan(sel["models"], sel["packs"], sel["repeat"], sel["tier"], self.retest,
|
|
1768
|
-
self.tests(eng), sel.get("stop_early") or None
|
|
1780
|
+
self.tests(eng), sel.get("stop_early") or None, sel.get("repeat_total", False))
|
|
1781
|
+
if j.key not in self.leave]
|
|
1769
1782
|
waiting = [j for j in jobs if j.status == "waiting"]
|
|
1770
1783
|
if not waiting:
|
|
1771
1784
|
return None, "every pack left is already done"
|
|
@@ -1850,13 +1863,13 @@ class RetestRow:
|
|
|
1850
1863
|
CHOICE_TEXT = {"zero": "[b red]Zero[/b red]", "continue": "[b green]Continue[/b green]", "leave": "[dim]Leave out[/dim]"}
|
|
1851
1864
|
|
|
1852
1865
|
|
|
1853
|
-
def retest_rows(eng, labels, packs, repeat, tier, picks, force):
|
|
1866
|
+
def retest_rows(eng, labels, packs, repeat, tier, picks, force, total=False):
|
|
1854
1867
|
"""(rows, rerun): the packs Start would set earlier answers aside for, or could. rerun: Retest is
|
|
1855
1868
|
off but everything selected is done, so Start runs it again. A pack whose questions or settings
|
|
1856
1869
|
changed can't continue (Zero or Leave out); with Retest every pack with answers starts at Zero,
|
|
1857
1870
|
without it a pack that can resume continues."""
|
|
1858
|
-
base = eng.plan(labels, packs, repeat, tier, False, picks)
|
|
1859
|
-
zero = {j.key: j for j in eng.plan(labels, packs, repeat, tier, True, picks)}
|
|
1871
|
+
base = eng.plan(labels, packs, repeat, tier, False, picks, total=total)
|
|
1872
|
+
zero = {j.key: j for j in eng.plan(labels, packs, repeat, tier, True, picks, total=total)}
|
|
1860
1873
|
rerun = not force and not any(j.status == "waiting" for j in base)
|
|
1861
1874
|
rows = []
|
|
1862
1875
|
for b in base:
|
|
@@ -3474,10 +3487,10 @@ class ResultsScreen(Screen):
|
|
|
3474
3487
|
self.pq_summary_args = (tests, models) # to refit on resize
|
|
3475
3488
|
rows = compare.compare_models(shared, models)
|
|
3476
3489
|
# short headings so every column fits; the line above the table says what each one is
|
|
3477
|
-
header = ("Model", "Time", "Passed", "Med tok", "Tokens", "Tok/✓", "Med s", "Prompt t/s",
|
|
3478
|
-
"Fewest", "Fastest", "Cut off")
|
|
3490
|
+
header = ("Model", "Time", "Passed", "Pass rate", "Med tok", "Tokens", "Tok/✓", "Med s", "Prompt t/s",
|
|
3491
|
+
"Gen t/s", "Fewest", "Fastest", "Cut off")
|
|
3479
3492
|
best = {k: max((r[k] for r in rows if r[k] is not None), default=None)
|
|
3480
|
-
for k in ("passed", "fewest", "fastest", "avg_pp_tps", "avg_gen_tps")}
|
|
3493
|
+
for k in ("passed", "pass_rate", "fewest", "fastest", "avg_pp_tps", "avg_gen_tps")}
|
|
3481
3494
|
low = {k: min(r[k] for r in rows if r[k] is not None) if any(r[k] is not None for r in rows) else None
|
|
3482
3495
|
for k in ("med_tokens", "total_tokens", "tokens_per_pass", "med_secs", "total_secs")}
|
|
3483
3496
|
|
|
@@ -3492,6 +3505,7 @@ class ResultsScreen(Screen):
|
|
|
3492
3505
|
hi(tt, ("-" if not tt else f"{tt:.1f}s" if tt < 60 else fmt_secs(tt)), "total_secs", "min"),
|
|
3493
3506
|
hi(r["passed"], f"{r['passed']}/{r['n']}", "passed", "max")
|
|
3494
3507
|
+ (f" [dim]{r['score']:.0%}[/dim]" if r["passed"] < r["n"] and r["score"] else ""),
|
|
3508
|
+
hi(r["pass_rate"], f"{r['pass_rate']:.0%}", "pass_rate", "max"),
|
|
3495
3509
|
hi(r["med_tokens"], f"{r['med_tokens']:,.0f}" if r["med_tokens"] else "-", "med_tokens", "min"),
|
|
3496
3510
|
hi(r["total_tokens"], f"{r['total_tokens']:,.0f}", "total_tokens", "min"),
|
|
3497
3511
|
hi(r["tokens_per_pass"], f"{r['tokens_per_pass']:,.0f}" if r["tokens_per_pass"] else "-",
|
|
@@ -3504,6 +3518,9 @@ class ResultsScreen(Screen):
|
|
|
3504
3518
|
hi(r["fastest"], str(r["fastest"]), "fastest", "max"),
|
|
3505
3519
|
(f"[red]{r['cut']}[/red]" if r["cut"] else "0")
|
|
3506
3520
|
+ (f" [yellow]+{r['over']} over[/yellow]" if r.get("over") else "")])
|
|
3521
|
+
if all(t["cells"][m]["n"] == 1 for t in shared for m in models): # one answer each: Passed says it all
|
|
3522
|
+
i = header.index("Pass rate")
|
|
3523
|
+
header, cells = header[:i] + header[i + 1:], [c[:i] + c[i + 1:] for c in cells]
|
|
3507
3524
|
table.add_columns(*header)
|
|
3508
3525
|
tag_w = max(Text.from_markup(tag).cell_len + 1 for tag in tags.values())
|
|
3509
3526
|
cells = fit_first_column(header, cells, (self.size.width or self.app.size.width) - 3 - tag_w,
|
|
@@ -3524,7 +3541,7 @@ class ResultsScreen(Screen):
|
|
|
3524
3541
|
"shared the machine's speed: compare their times with care.[/yellow]")
|
|
3525
3542
|
title.update(line + " [dim]Time / Tokens: each question's median, added up. Med tok / Med s: median "
|
|
3526
3543
|
"per answer. Tok/✓: tokens per answer right. Prompt t/s / Gen t/s: all tokens over all the time "
|
|
3527
|
-
"they took (cached prompts left out). Passed: questions right on every repeat, then the score (partial credit, e.g. checks passed) when some failed. Fewest / Fastest: questions won; ties count for both. "
|
|
3544
|
+
"they took (cached prompts left out). Passed: questions right on every repeat, then the score (partial credit, e.g. checks passed) when some failed. Pass rate (with repeats): answers right, repeats included (each question weighs the same). Fewest / Fastest: questions won; ties count for both. "
|
|
3528
3545
|
"Cut off: answers that hit max_tokens. ×n beside a name: answers per question.[/dim]")
|
|
3529
3546
|
|
|
3530
3547
|
@staticmethod
|
|
@@ -711,6 +711,117 @@ class Retest(unittest.TestCase):
|
|
|
711
711
|
self.assertEqual(self.passes("apps"), dict.fromkeys("abcd", True))
|
|
712
712
|
|
|
713
713
|
|
|
714
|
+
class AddRepeats(unittest.TestCase):
|
|
715
|
+
"""A chosen repeat count adds rounds to the saved answers (×1 + 2 = ×3); a retest, blank and a
|
|
716
|
+
resumed run don't add more."""
|
|
717
|
+
|
|
718
|
+
def setUp(self):
|
|
719
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
720
|
+
self.ws = os.path.join(self.tmp.name, "ws")
|
|
721
|
+
tuieval(self.tmp.name, "init", self.ws)
|
|
722
|
+
d = os.path.join(self.ws, "packs", "apps")
|
|
723
|
+
os.makedirs(d)
|
|
724
|
+
write(os.path.join(d, "pack.toml"), 'label = "apps"\nscreen = 4\n[certify]\nrepeat = 1\n'
|
|
725
|
+
'[gate]\nmin_accuracy = 0.1\n')
|
|
726
|
+
write(os.path.join(d, "tests.yaml"), "".join(
|
|
727
|
+
f"- {{id: {t}, input: question {t}, expected: 1, reference: 'ANSWER: 1', wrong: ['ANSWER: 2'], "
|
|
728
|
+
"difficulty: easy}\n" for t in "abcd"))
|
|
729
|
+
port = free_port()
|
|
730
|
+
with open(os.path.join(self.ws, "models.toml"), "a") as f:
|
|
731
|
+
f.write(f'\n[servers.m]\nurl = "http://127.0.0.1:{port}"\n')
|
|
732
|
+
self.mock = Mock(os.path.join(self.ws, "packs"), "oracle", port=port)
|
|
733
|
+
tuieval(self.ws, "add", "mock", "--server", "m", "--label", "m")
|
|
734
|
+
tuieval(self.ws, "run", "--tier", "certify")
|
|
735
|
+
self.path = os.path.join(self.ws, "results", "m", "apps.json")
|
|
736
|
+
|
|
737
|
+
def tearDown(self):
|
|
738
|
+
self.mock.stop()
|
|
739
|
+
self.tmp.cleanup()
|
|
740
|
+
|
|
741
|
+
def rounds(self):
|
|
742
|
+
out = {}
|
|
743
|
+
for r in json.loads(read(self.path))["results"]:
|
|
744
|
+
out.setdefault(r["test"], set()).add(r["repeat"])
|
|
745
|
+
return {t: sorted(v) for t, v in out.items()}
|
|
746
|
+
|
|
747
|
+
def test_cli_adds_rounds(self):
|
|
748
|
+
out = tuieval(self.ws, "run", "--tier", "certify", "--repeat", "2").stdout
|
|
749
|
+
self.assertIn("×1 → ×3", out)
|
|
750
|
+
self.assertEqual(self.rounds(), dict.fromkeys("abcd", [0, 1, 2]))
|
|
751
|
+
tuieval(self.ws, "run", "--tier", "certify") # blank: the pack's 1, done
|
|
752
|
+
self.assertEqual(self.rounds(), dict.fromkeys("abcd", [0, 1, 2]))
|
|
753
|
+
|
|
754
|
+
def test_retest_counts_from_none(self):
|
|
755
|
+
tuieval(self.ws, "run", "--tier", "certify", "--repeat", "2", "--retest")
|
|
756
|
+
self.assertEqual(self.rounds(), dict.fromkeys("abcd", [0, 1]))
|
|
757
|
+
|
|
758
|
+
def test_picked_tests_then_top_up(self):
|
|
759
|
+
tuieval(self.ws, "run", "--tier", "certify", "--tests", "apps:a", "--repeat", "1")
|
|
760
|
+
self.assertEqual(self.rounds(), {"a": [0, 1], "b": [0], "c": [0], "d": [0]})
|
|
761
|
+
tuieval(self.ws, "run", "--tier", "certify", "--repeat", "1") # the most rounds (2) + 1, for all
|
|
762
|
+
self.assertEqual(self.rounds(), dict.fromkeys("abcd", [0, 1, 2]))
|
|
763
|
+
|
|
764
|
+
def test_resumed_run_adds_no_more(self):
|
|
765
|
+
from tuieval import engine
|
|
766
|
+
e = engine.Engine(root=self.ws)
|
|
767
|
+
j = e.plan(["m"], ["apps"], 2, "certify", False, {})[0]
|
|
768
|
+
self.assertEqual((j.base, j.repeat, j.done), (1, 3, 4))
|
|
769
|
+
rows = [dict(r, repeat=1) for r in json.loads(read(self.path))["results"] if r["test"] in "ab"]
|
|
770
|
+
write(self.path + ".partial.jsonl", "".join(json.dumps(l) + "\n" for l in [e._partial_header(j)] + rows))
|
|
771
|
+
out = tuieval(self.ws, "run", "--tier", "certify", "--repeat", "2").stdout # the same +2, resumed
|
|
772
|
+
self.assertIn("resuming after 6", out)
|
|
773
|
+
self.assertEqual(self.rounds(), dict.fromkeys("abcd", [0, 1, 2]))
|
|
774
|
+
|
|
775
|
+
def test_preset_repeat_is_a_total(self):
|
|
776
|
+
from tuieval import engine
|
|
777
|
+
engine.save_preset(os.path.join(self.ws, "presets.toml"), "two", ["m"], ["apps"], 2)
|
|
778
|
+
tuieval(self.ws, "run", "--tier", "certify", "--preset", "two")
|
|
779
|
+
self.assertEqual(self.rounds(), dict.fromkeys("abcd", [0, 1])) # ×1 topped up to 2
|
|
780
|
+
tuieval(self.ws, "run", "--tier", "certify", "--preset", "two") # already 2: nothing new
|
|
781
|
+
self.assertEqual(self.rounds(), dict.fromkeys("abcd", [0, 1]))
|
|
782
|
+
tuieval(self.ws, "run", "--tier", "certify", "--preset", "two", "--repeat", "1") # typed: adds
|
|
783
|
+
self.assertEqual(self.rounds(), dict.fromkeys("abcd", [0, 1, 2]))
|
|
784
|
+
|
|
785
|
+
def estimate(self, steps):
|
|
786
|
+
code = textwrap.dedent("""
|
|
787
|
+
import asyncio
|
|
788
|
+
from tuieval.tui import EvalsApp
|
|
789
|
+
from textual.widgets import SelectionList, Input
|
|
790
|
+
async def go():
|
|
791
|
+
app = EvalsApp({})
|
|
792
|
+
async with app.run_test(size=(160, 50)) as pilot:
|
|
793
|
+
await pilot.pause()
|
|
794
|
+
s = app.screen
|
|
795
|
+
s.query_one("#suites", SelectionList).select("apps")
|
|
796
|
+
s.selected_models = {"m"}
|
|
797
|
+
s.refresh_models()
|
|
798
|
+
s.query_one("#tier-certify").value = True
|
|
799
|
+
%s
|
|
800
|
+
await pilot.pause()
|
|
801
|
+
print(str(s.query_one("#estimate").render()))
|
|
802
|
+
asyncio.run(go())
|
|
803
|
+
""") % textwrap.indent(textwrap.dedent(steps), " " * 8)
|
|
804
|
+
p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=300,
|
|
805
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""))
|
|
806
|
+
return p.stdout + p.stderr
|
|
807
|
+
|
|
808
|
+
def test_tui_estimate_says_more(self):
|
|
809
|
+
self.assertIn("2 more (×1 → ×3) repeat(s) = 8 answers", self.estimate("""
|
|
810
|
+
s.query_one("#repeat", Input).value = "2"
|
|
811
|
+
"""))
|
|
812
|
+
|
|
813
|
+
def test_tui_preset_repeat_is_a_total(self):
|
|
814
|
+
out = self.estimate("""
|
|
815
|
+
s.apply_selection({"models": ["m"], "packs": ["apps"], "repeat": 2, "repeat_total": True}, quiet=True)
|
|
816
|
+
s.query_one("#tier-certify").value = True
|
|
817
|
+
await pilot.pause()
|
|
818
|
+
print(str(s.query_one("#estimate").render()))
|
|
819
|
+
s.query_one("#repeat", Input).value = "3" # typed over: adds again
|
|
820
|
+
""")
|
|
821
|
+
self.assertIn("2 in all (preset) repeat(s) = 4 answers", out)
|
|
822
|
+
self.assertIn("3 more (×1 → ×4) repeat(s) = 12 answers", out)
|
|
823
|
+
|
|
824
|
+
|
|
714
825
|
class Parallel(unittest.TestCase):
|
|
715
826
|
"""Several models at a time (parallel_models / --parallel): each runs in its own lane on its own
|
|
716
827
|
port, answers note the models they ran alongside, and the scheduler keeps the memory and
|
|
@@ -2340,6 +2451,52 @@ class FirstRun(unittest.TestCase):
|
|
|
2340
2451
|
"['once [yellow]×1[/]', 'thrice [dim]×3[/]']", # Comparison: only the questions both answered
|
|
2341
2452
|
], out.stderr[-2000:])
|
|
2342
2453
|
|
|
2454
|
+
def test_pass_rate_counts_every_answer(self):
|
|
2455
|
+
from tuieval import compare
|
|
2456
|
+
row = {"suite": "p", "latency": 1000, "tokens": 10, "truncated": False, "gen_tps": 20.0, "prompt_tps": None}
|
|
2457
|
+
oks = {"a": {"q0": [True, True], "q1": [True, False], "q2": [False, False, False, True]}, # ×2, ×2, ×4
|
|
2458
|
+
"b": {"q0": [True], "q1": [False], "q2": [False]},
|
|
2459
|
+
"c": {"q0": [False], "q1": [False], "q2": [False]}}
|
|
2460
|
+
rows = [dict(row, model=m, test=f"p: {q}", ok=ok, repeat=r)
|
|
2461
|
+
for m, qs in oks.items() for q, v in qs.items() for r, ok in enumerate(v)]
|
|
2462
|
+
out = {r["model"]: r for r in compare.compare_models(compare.per_question(rows), list(oks))}
|
|
2463
|
+
self.assertEqual(out["a"]["passed"], 1) # strict: right on every repeat
|
|
2464
|
+
self.assertAlmostEqual(out["a"]["pass_rate"], (1 + 0.5 + 0.25) / 3) # each question weighs the same
|
|
2465
|
+
self.assertAlmostEqual(out["b"]["pass_rate"], 1 / 3) # ×1: the same as passed / questions
|
|
2466
|
+
self.assertEqual(out["c"]["pass_rate"], 0)
|
|
2467
|
+
|
|
2468
|
+
def test_comparison_shows_pass_rate(self):
|
|
2469
|
+
tuieval(self.ws, "add", "my-model", "--server", "local")
|
|
2470
|
+
code = textwrap.dedent("""
|
|
2471
|
+
import asyncio
|
|
2472
|
+
from rich.text import Text
|
|
2473
|
+
from tuieval import compare
|
|
2474
|
+
from tuieval.tui import EvalsApp, ResultsScreen
|
|
2475
|
+
from textual.widgets import DataTable, TabbedContent
|
|
2476
|
+
oks = {"steady": [[True, True], [True, True]], "shaky": [[True, True], [True, False]]}
|
|
2477
|
+
rows = [{"model": m, "suite": "p", "test": f"p: q{q}", "ok": ok, "latency": 1000, "tokens": 10,
|
|
2478
|
+
"truncated": False, "gen_tps": 20.0, "prompt_tps": None, "repeat": r}
|
|
2479
|
+
for m, qs in oks.items() for q, v in enumerate(qs) for r, ok in enumerate(v)]
|
|
2480
|
+
async def go():
|
|
2481
|
+
app = EvalsApp({})
|
|
2482
|
+
async with app.run_test(size=(160, 40)) as pilot:
|
|
2483
|
+
await pilot.pause()
|
|
2484
|
+
app.push_screen(ResultsScreen())
|
|
2485
|
+
await pilot.pause(0.5)
|
|
2486
|
+
app.screen.query_one(TabbedContent).active = "tab-per-question"
|
|
2487
|
+
await pilot.pause(0.3)
|
|
2488
|
+
app.screen.fill_pq_summary(compare.per_question(rows), list(oks))
|
|
2489
|
+
await pilot.pause(0.3)
|
|
2490
|
+
t = app.screen.query_one("#pq-summary", DataTable)
|
|
2491
|
+
i = [str(c.label) for c in t.ordered_columns].index("Pass rate")
|
|
2492
|
+
print(str(t.ordered_columns[i - 1].label), [t.get_row_at(n)[i] for n in range(t.row_count)])
|
|
2493
|
+
asyncio.run(go())
|
|
2494
|
+
""")
|
|
2495
|
+
out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True,
|
|
2496
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""), timeout=60)
|
|
2497
|
+
# right after Passed; the best in bold green
|
|
2498
|
+
self.assertEqual(out.stdout.strip(), "Passed ['[bold green]100%[/bold green]', '75%']", out.stderr[-2000:])
|
|
2499
|
+
|
|
2343
2500
|
def test_results_open_on_pta_index(self):
|
|
2344
2501
|
tuieval(self.ws, "add", "my-model", "--server", "local")
|
|
2345
2502
|
code = textwrap.dedent("""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/tool_call/pack.toml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev137 → tuieval-0.2.0.dev141}/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
|