tuieval 0.2.0.dev135__tar.gz → 0.2.0.dev139__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.dev135 → tuieval-0.2.0.dev139}/CHANGELOG.md +2 -1
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/PKG-INFO +1 -1
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/models.md +1 -1
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/engine.py +23 -3
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/run_evals.py +9 -4
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/tui.py +51 -19
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/tests/test_tuieval.py +160 -6
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/.gitignore +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/LICENSE +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/README.md +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/agent-packs.md +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/agents.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/compare.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/dates.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/insights.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/logs.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/pta.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/tests/mock_server.py +0 -0
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- 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
6
|
- 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
|
-
- Repeats: the PTA index, `tuieval compare` and Setup show how many times each model answered each question (`×3`, `×1-3`), so single-run results stand out.
|
|
7
|
+
- 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
8
|
- 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.
|
|
8
9
|
- Results: picking a pack (PTA index or Per question) compares the models that answered all of it; the Models button can change that, and All packs goes back to the earlier pick.
|
|
9
10
|
- Fix: a request that runs out of time says `time limit: timed out after N s`, not `connection error`, and `tuieval compare` counts it against the model like the run does (it was left out as an error).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev139
|
|
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.dev139'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev139')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -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
|
|
|
@@ -431,6 +431,15 @@ def trim_names(names, width):
|
|
|
431
431
|
return trimmed if len(set(trimmed)) == len(set(names)) else list(names)
|
|
432
432
|
|
|
433
433
|
|
|
434
|
+
def repeats_tag(counts):
|
|
435
|
+
"""A model's answers per question as markup, as the PTA index shows them: "×3", or "×1-3" when it varies;
|
|
436
|
+
yellow when it's ×1 (one answer decides each result, the roughest), else dim. "" for no answers."""
|
|
437
|
+
if not counts:
|
|
438
|
+
return ""
|
|
439
|
+
few, most = min(counts), max(counts)
|
|
440
|
+
return f"[{'yellow' if most == 1 else 'dim'}]×{few}{'' if few == most else f'-{most}'}[/]"
|
|
441
|
+
|
|
442
|
+
|
|
434
443
|
def fit_first_column(header, rows, room, floor=24):
|
|
435
444
|
"""rows with the first cell trimmed in the middle (mid_trim) so a table of these columns fits room
|
|
436
445
|
cells across, but never below floor cells. Cells are Rich markup; each column adds 2 cells of padding."""
|
|
@@ -1016,6 +1025,11 @@ class SetupScreen(Screen):
|
|
|
1016
1025
|
list(self.query_one("#suites", SelectionList).selected), repeat,
|
|
1017
1026
|
self.tier(), self.query_one("#force", Checkbox).value)
|
|
1018
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
|
+
|
|
1019
1033
|
def parallel(self):
|
|
1020
1034
|
"""Models at a time for the next run: the field, or blank for this machine's parallel_models."""
|
|
1021
1035
|
try:
|
|
@@ -1079,16 +1093,21 @@ class SetupScreen(Screen):
|
|
|
1079
1093
|
"[b]s[/b] to start." + self.queue_hint())
|
|
1080
1094
|
return
|
|
1081
1095
|
picks = self.current_picks(suites)
|
|
1082
|
-
|
|
1096
|
+
total = self.repeat_total()
|
|
1097
|
+
jobs = self.app.engine.plan(labels, suites, repeat, tier, force, picks, total=total)
|
|
1083
1098
|
rerun = not any(j.status == "waiting" for j in jobs)
|
|
1084
1099
|
if rerun: # everything has results: Start runs it again, so estimate that
|
|
1085
|
-
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)
|
|
1086
1101
|
run = [j for j in jobs if j.status == "waiting"]
|
|
1087
1102
|
skipped = [j for j in jobs if j.status == "skipped"]
|
|
1088
1103
|
secs, notes = self.app.engine.estimate_seconds(jobs)
|
|
1089
|
-
todo = sum(j.total for j in run)
|
|
1090
|
-
reps = sorted({j.repeat for j in jobs})
|
|
1091
|
-
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]"
|
|
1092
1111
|
text = (f"[b]{tier.title()}[/b]: [b]{len(labels)}[/b] model(s) × [b]{len(suites)}[/b] pack(s) × "
|
|
1093
1112
|
f"[b]{reps_text}[/b] repeat(s) = "
|
|
1094
1113
|
f"[b]{todo:,}[/b] answers · about [b]{fmt_secs(secs)}[/b]"
|
|
@@ -1149,6 +1168,7 @@ class SetupScreen(Screen):
|
|
|
1149
1168
|
for name in sel.get("packs", []):
|
|
1150
1169
|
if name in e.packs:
|
|
1151
1170
|
packs.select(name)
|
|
1171
|
+
self.preset_repeat = str(sel["repeat"]) if sel.get("repeat") and sel.get("repeat_total") else None
|
|
1152
1172
|
self.query_one("#repeat", Input).value = str(sel["repeat"]) if sel.get("repeat") else ""
|
|
1153
1173
|
if "parallel" in sel:
|
|
1154
1174
|
self.query_one("#parallel", Input).value = str(sel["parallel"]) if sel["parallel"] else ""
|
|
@@ -1192,10 +1212,11 @@ class SetupScreen(Screen):
|
|
|
1192
1212
|
self.notify("Select at least one eval pack and one model.", severity="warning")
|
|
1193
1213
|
return
|
|
1194
1214
|
picks = self.current_picks(suites)
|
|
1195
|
-
|
|
1196
|
-
|
|
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}
|
|
1197
1218
|
e = self.app.engine
|
|
1198
|
-
jobs = e.plan(labels, suites, repeat, tier, True, picks)
|
|
1219
|
+
jobs = e.plan(labels, suites, repeat, tier, True, picks, total=total)
|
|
1199
1220
|
if not any(j.status == "waiting" for j in jobs):
|
|
1200
1221
|
reasons = sorted({j.note for j in jobs if j.status == "skipped"})
|
|
1201
1222
|
self.notify(f"Nothing selected can run: {'; '.join(reasons)}", severity="warning")
|
|
@@ -1203,7 +1224,7 @@ class SetupScreen(Screen):
|
|
|
1203
1224
|
self.app.save_state({k: v for k, v in sel.items() if k != "models"}) # models aren't remembered
|
|
1204
1225
|
# Packs whose earlier answers a start would set aside (Retest, everything already done, or
|
|
1205
1226
|
# other questions or settings): the user picks per pack before anything moves.
|
|
1206
|
-
rows, rerun = retest_rows(e, labels, suites, repeat, tier, picks, force)
|
|
1227
|
+
rows, rerun = retest_rows(e, labels, suites, repeat, tier, picks, force, total)
|
|
1207
1228
|
|
|
1208
1229
|
def go(choices):
|
|
1209
1230
|
if choices is None:
|
|
@@ -1631,7 +1652,7 @@ class PresetsScreen(ModalScreen):
|
|
|
1631
1652
|
name = list(self.presets)[event.option_index]
|
|
1632
1653
|
labels, packs, repeat, tests = engine.resolve_preset(self.app.engine.cfg, self.presets[name],
|
|
1633
1654
|
list(self.app.engine.packs))
|
|
1634
|
-
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}))
|
|
1635
1656
|
|
|
1636
1657
|
@on(Button.Pressed, "#save")
|
|
1637
1658
|
@on(Input.Submitted, "#name")
|
|
@@ -1756,7 +1777,8 @@ class QueuedRun:
|
|
|
1756
1777
|
"""(jobs, None), or (None, why it can't run) now."""
|
|
1757
1778
|
sel = self.sel
|
|
1758
1779
|
jobs = [j for j in eng.plan(sel["models"], sel["packs"], sel["repeat"], sel["tier"], self.retest,
|
|
1759
|
-
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]
|
|
1760
1782
|
waiting = [j for j in jobs if j.status == "waiting"]
|
|
1761
1783
|
if not waiting:
|
|
1762
1784
|
return None, "every pack left is already done"
|
|
@@ -1841,13 +1863,13 @@ class RetestRow:
|
|
|
1841
1863
|
CHOICE_TEXT = {"zero": "[b red]Zero[/b red]", "continue": "[b green]Continue[/b green]", "leave": "[dim]Leave out[/dim]"}
|
|
1842
1864
|
|
|
1843
1865
|
|
|
1844
|
-
def retest_rows(eng, labels, packs, repeat, tier, picks, force):
|
|
1866
|
+
def retest_rows(eng, labels, packs, repeat, tier, picks, force, total=False):
|
|
1845
1867
|
"""(rows, rerun): the packs Start would set earlier answers aside for, or could. rerun: Retest is
|
|
1846
1868
|
off but everything selected is done, so Start runs it again. A pack whose questions or settings
|
|
1847
1869
|
changed can't continue (Zero or Leave out); with Retest every pack with answers starts at Zero,
|
|
1848
1870
|
without it a pack that can resume continues."""
|
|
1849
|
-
base = eng.plan(labels, packs, repeat, tier, False, picks)
|
|
1850
|
-
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)}
|
|
1851
1873
|
rerun = not force and not any(j.status == "waiting" for j in base)
|
|
1852
1874
|
rows = []
|
|
1853
1875
|
for b in base:
|
|
@@ -3136,7 +3158,10 @@ class ResultsScreen(Screen):
|
|
|
3136
3158
|
base = column_name(col.label)
|
|
3137
3159
|
label = base + SORT_ARROWS[desc] if base == name else base
|
|
3138
3160
|
if col.label.plain != label:
|
|
3139
|
-
col.label
|
|
3161
|
+
text = col.label.copy() # keeps its styling, e.g. a model's ×n
|
|
3162
|
+
if len(text.plain) > len(base): # right_crop(0) would empty it
|
|
3163
|
+
text.right_crop(len(text.plain) - len(base))
|
|
3164
|
+
col.label = text + SORT_ARROWS[desc] if base == name else text
|
|
3140
3165
|
w = cell_len(label) # room for the arrow (a column keeps the width it was added with)
|
|
3141
3166
|
col.content_width = max(col.content_width, w)
|
|
3142
3167
|
if not col.auto_width:
|
|
@@ -3404,7 +3429,9 @@ class ResultsScreen(Screen):
|
|
|
3404
3429
|
here = self.app.engine.machine().id
|
|
3405
3430
|
table = self.query_one("#per-question", DataTable)
|
|
3406
3431
|
table.clear(columns=True)
|
|
3407
|
-
table.add_columns(*self.PQ_FIXED, *
|
|
3432
|
+
table.add_columns(*self.PQ_FIXED, *(Text.from_markup(
|
|
3433
|
+
f"{rich_escape(m)} {repeats_tag([t['cells'][m]['n'] for t in tests if m in t['cells']])}".rstrip())
|
|
3434
|
+
for m in models))
|
|
3408
3435
|
self.pq_tests, self.pq_models = {}, models
|
|
3409
3436
|
for t in tests:
|
|
3410
3437
|
cells = []
|
|
@@ -3427,7 +3454,8 @@ class ResultsScreen(Screen):
|
|
|
3427
3454
|
"[green]bold[/green] = fewest tokens among passing answers · [red]cut[/red] = hit max_tokens · "
|
|
3428
3455
|
"[yellow]over[/yellow] = ran past max_tokens (the provider didn't stop it) · "
|
|
3429
3456
|
"◐ = pack unfinished (not in scores) · dim seconds = hosted, or measured on another machine; tokens compare "
|
|
3430
|
-
"across machines, seconds don't. ×tokens = most / fewest tokens across models.
|
|
3457
|
+
"across machines, seconds don't. ×tokens = most / fewest tokens across models. ×n beside a model: answers "
|
|
3458
|
+
"per question (×1: one answer decides each result, the roughest). Enter on a cell opens the "
|
|
3431
3459
|
"answers. Click a column name to sort by it (a model's column: by its tokens).[/dim]" + ("\n[yellow]Left out, being rerun with other questions or settings: " + ", ".join(
|
|
3432
3460
|
f"{os.path.basename(os.path.dirname(p))} · {os.path.basename(p)[:-5]}" for p in sorted(self.superseded))
|
|
3433
3461
|
+ ". Only the new run's answers (◐) are shown for these.[/yellow]" if self.superseded else ""))
|
|
@@ -3469,6 +3497,7 @@ class ResultsScreen(Screen):
|
|
|
3469
3497
|
def hi(v, text, key, good):
|
|
3470
3498
|
ref = best.get(key) if good == "max" else low.get(key)
|
|
3471
3499
|
return f"[bold green]{text}[/bold green]" if v is not None and v == ref and len(rows) > 1 else text
|
|
3500
|
+
tags = {m: repeats_tag([t["cells"][m]["n"] for t in shared]) for m in models}
|
|
3472
3501
|
cells = []
|
|
3473
3502
|
for r in rows:
|
|
3474
3503
|
tt = r["total_secs"]
|
|
@@ -3489,7 +3518,10 @@ class ResultsScreen(Screen):
|
|
|
3489
3518
|
(f"[red]{r['cut']}[/red]" if r["cut"] else "0")
|
|
3490
3519
|
+ (f" [yellow]+{r['over']} over[/yellow]" if r.get("over") else "")])
|
|
3491
3520
|
table.add_columns(*header)
|
|
3492
|
-
|
|
3521
|
+
tag_w = max(Text.from_markup(tag).cell_len + 1 for tag in tags.values())
|
|
3522
|
+
cells = fit_first_column(header, cells, (self.size.width or self.app.size.width) - 3 - tag_w,
|
|
3523
|
+
floor=24 - tag_w) # the column's floor counts the ×n too
|
|
3524
|
+
table.add_rows([[f"{rich_escape(c[0])} {tags[r['model']]}"] + c[1:] for c, r in zip(cells, rows)])
|
|
3493
3525
|
line = f"[b]Comparison[/b] over the {len(shared)} questions all {len(models)} answered"
|
|
3494
3526
|
if len(models) == 2:
|
|
3495
3527
|
h = compare.head_to_head(shared, *models)
|
|
@@ -3506,7 +3538,7 @@ class ResultsScreen(Screen):
|
|
|
3506
3538
|
title.update(line + " [dim]Time / Tokens: each question's median, added up. Med tok / Med s: median "
|
|
3507
3539
|
"per answer. Tok/✓: tokens per answer right. Prompt t/s / Gen t/s: all tokens over all the time "
|
|
3508
3540
|
"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. "
|
|
3509
|
-
"Cut off: answers that hit max_tokens.[/dim]")
|
|
3541
|
+
"Cut off: answers that hit max_tokens. ×n beside a name: answers per question.[/dim]")
|
|
3510
3542
|
|
|
3511
3543
|
@staticmethod
|
|
3512
3544
|
def pq_cell(c, best, here):
|
|
@@ -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
|
|
@@ -1739,7 +1850,7 @@ class PTAIndex(unittest.TestCase):
|
|
|
1739
1850
|
print(t.row_count, str(s.query_one("#fail-count", Static).render()))
|
|
1740
1851
|
s.fill_per_question()
|
|
1741
1852
|
pq = s.query_one("#per-question", DataTable)
|
|
1742
|
-
cols = [str(c.label) for c in pq.ordered_columns]
|
|
1853
|
+
cols = [str(c.label).split(" ×")[0] for c in pq.ordered_columns]
|
|
1743
1854
|
print(sorted(str(pq.get_row_at(i)[cols.index("close")]).split()[1] for i in range(pq.row_count)))
|
|
1744
1855
|
asyncio.run(go())
|
|
1745
1856
|
""")
|
|
@@ -1801,7 +1912,7 @@ class PTAIndex(unittest.TestCase):
|
|
|
1801
1912
|
s = app.screen
|
|
1802
1913
|
def state():
|
|
1803
1914
|
pq = s.query_one("#per-question", DataTable)
|
|
1804
|
-
cols = [str(c.label) for c in pq.ordered_columns][len(s.PQ_FIXED):]
|
|
1915
|
+
cols = [str(c.label).split(" ×")[0] for c in pq.ordered_columns][len(s.PQ_FIXED):]
|
|
1805
1916
|
return (sorted(app.pq_models), s.query_one("#pta-pack", Select).value,
|
|
1806
1917
|
str(s.query_one("#pq-models", Button).label), cols, s.query_one("#pq-shared").value)
|
|
1807
1918
|
s.query_one("#pq-pack", Select).value = "apps"
|
|
@@ -1955,7 +2066,7 @@ class PTAIndex(unittest.TestCase):
|
|
|
1955
2066
|
pq.move_cursor(row=0, column=col)
|
|
1956
2067
|
await pilot.press("enter")
|
|
1957
2068
|
await pilot.pause(0.3)
|
|
1958
|
-
opened[
|
|
2069
|
+
opened[pq.ordered_columns[col].label.plain.split(" ×")[0]] = app.screen.entries[0]["model"]
|
|
1959
2070
|
app.screen.dismiss()
|
|
1960
2071
|
await pilot.pause(0.2)
|
|
1961
2072
|
print(json.dumps({"rows": rows, "bars": "P parsimony" in tri, "disagree": [both, alone], "opened": opened,
|
|
@@ -2271,6 +2382,7 @@ class FirstRun(unittest.TestCase):
|
|
|
2271
2382
|
tuieval(self.ws, "add", "my-model", "--server", "local")
|
|
2272
2383
|
code = textwrap.dedent("""
|
|
2273
2384
|
import asyncio
|
|
2385
|
+
from rich.text import Text
|
|
2274
2386
|
from tuieval import compare
|
|
2275
2387
|
from tuieval.tui import EvalsApp, ResultsScreen
|
|
2276
2388
|
from textual.widgets import DataTable, TabbedContent
|
|
@@ -2288,14 +2400,56 @@ class FirstRun(unittest.TestCase):
|
|
|
2288
2400
|
app.screen.fill_pq_summary(compare.per_question(rows), models)
|
|
2289
2401
|
await pilot.pause(0.3)
|
|
2290
2402
|
t = app.screen.query_one("#pq-summary", DataTable)
|
|
2291
|
-
names = [
|
|
2403
|
+
names = [Text.from_markup(t.get_row_at(i)[0]).plain for i in range(t.row_count)]
|
|
2292
2404
|
print(t.row_count, t.region.height, t.virtual_size.width <= t.region.width,
|
|
2293
|
-
all(n.endswith("-q4_k_m-vllm") and "…" in n for n in names), len(set(names)))
|
|
2405
|
+
all(n.endswith("-q4_k_m-vllm ×1") and "…" in n for n in names), len(set(names)))
|
|
2406
|
+
asyncio.run(go())
|
|
2407
|
+
""")
|
|
2408
|
+
out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True,
|
|
2409
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""), timeout=60)
|
|
2410
|
+
# every row, no scroll; a trimmed name keeps its ×n
|
|
2411
|
+
self.assertEqual(out.stdout.strip(), "12 13 True True 12", out.stderr[-2000:])
|
|
2412
|
+
|
|
2413
|
+
def test_per_question_shows_repeats(self):
|
|
2414
|
+
tuieval(self.ws, "add", "my-model", "--server", "local")
|
|
2415
|
+
code = textwrap.dedent("""
|
|
2416
|
+
import asyncio
|
|
2417
|
+
from tuieval.tui import EvalsApp, ResultsScreen
|
|
2418
|
+
from textual.widgets import DataTable, TabbedContent
|
|
2419
|
+
rows = [{"model": m, "suite": "p", "test": f"p: q{q}", "ok": True, "latency": 1000, "tokens": 10,
|
|
2420
|
+
"truncated": False, "gen_tps": 20.0, "prompt_tps": None, "repeat": r, "score": 1.0}
|
|
2421
|
+
for m, reps in (("once", 1), ("thrice", 3)) for q in range(2) for r in range(reps)]
|
|
2422
|
+
rows += [dict(rows[-1], test="p: q2", repeat=0)] # one more question, answered once
|
|
2423
|
+
async def go():
|
|
2424
|
+
app = EvalsApp({})
|
|
2425
|
+
async with app.run_test(size=(160, 40)) as pilot:
|
|
2426
|
+
await pilot.pause()
|
|
2427
|
+
app.push_screen(ResultsScreen())
|
|
2428
|
+
await pilot.pause(0.5)
|
|
2429
|
+
s = app.screen
|
|
2430
|
+
s.rows, s.partial_rows, s.superseded = rows, [], set()
|
|
2431
|
+
s.query_one(TabbedContent).active = "tab-per-question"
|
|
2432
|
+
await pilot.pause(0.3)
|
|
2433
|
+
s.fill_per_question()
|
|
2434
|
+
await pilot.pause(0.3)
|
|
2435
|
+
pq = s.query_one("#per-question", DataTable)
|
|
2436
|
+
heads = [c.label for c in pq.ordered_columns][len(s.PQ_FIXED):]
|
|
2437
|
+
print([h.plain for h in heads], [str(h.spans[0].style) for h in heads])
|
|
2438
|
+
app.result_sorts["per-question"] = ("thrice ×1-3", True) # as a click on its name does
|
|
2439
|
+
s.sort_table(pq)
|
|
2440
|
+
head = pq.ordered_columns[-1].label
|
|
2441
|
+
print(head.plain, str(head.spans[0].style))
|
|
2442
|
+
t = s.query_one("#pq-summary", DataTable)
|
|
2443
|
+
print([t.get_row_at(i)[0] for i in range(t.row_count)])
|
|
2294
2444
|
asyncio.run(go())
|
|
2295
2445
|
""")
|
|
2296
2446
|
out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True,
|
|
2297
2447
|
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""), timeout=60)
|
|
2298
|
-
self.assertEqual(out.stdout.strip()
|
|
2448
|
+
self.assertEqual(out.stdout.strip().splitlines(), [
|
|
2449
|
+
"['once ×1', 'thrice ×1-3'] ['yellow', 'dim']", # over the questions shown
|
|
2450
|
+
"thrice ×1-3 ▼ dim", # sorting keeps the ×n's colour
|
|
2451
|
+
"['once [yellow]×1[/]', 'thrice [dim]×3[/]']", # Comparison: only the questions both answered
|
|
2452
|
+
], out.stderr[-2000:])
|
|
2299
2453
|
|
|
2300
2454
|
def test_results_open_on_pta_index(self):
|
|
2301
2455
|
tuieval(self.ws, "add", "my-model", "--server", "local")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/tool_call/pack.toml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev135 → tuieval-0.2.0.dev139}/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
|