tuieval 0.2.0.dev81__tar.gz → 0.2.0.dev87__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.dev81 → tuieval-0.2.0.dev87}/CHANGELOG.md +3 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/PKG-INFO +1 -1
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/tui.py +77 -5
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/tests/test_tuieval.py +141 -2
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/.gitignore +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/LICENSE +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/README.md +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/models.md +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/compare.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/engine.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/logs.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/pta.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/run_evals.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/tests/mock_server.py +0 -0
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- The run screen's queue keeps the running model in view, unless you scroll it yourself (scrolling back to it follows again).
|
|
6
|
+
- Start while a run is going skips models and packs already running or queued (it says which); picking only those adds nothing. Retest and picked questions still go through.
|
|
7
|
+
- Fix: something queued just as a run ended (while its server was stopping) now starts instead of waiting forever.
|
|
5
8
|
- Hosted models (OpenRouter and other APIs) run beside local ones instead of waiting for a slot: up to `parallel_hosted` at a time (default 4, `--parallel-hosted N`). Local models can be added to a hosted-only run.
|
|
6
9
|
- Early stop: `tuieval run --stop-early`, **Stop a pack once it fails** in Setup, or `stop_early = true` in a pack.toml ends a pack once its FAIL is certain (a critical failure, or an accuracy or truncation bar out of reach), asking first the questions models fail most. Verdicts also say FAIL when a partial run can no longer pass.
|
|
7
10
|
- `tuieval compare --reference <model>`: every other model paired against one on the questions both answered, per pack and overall: the difference with a 95% CI, better/tie/worse, critical failures and tokens vs the reference. `--packs` limits any compare to some packs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev87
|
|
4
4
|
Summary: Evaluate local and frontier LLMs on your own questions: accuracy, speed, tokens and PASS/FAIL verdicts, in the terminal.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ashe-wb/tuieval
|
|
6
6
|
Project-URL: Issues, https://github.com/ashe-wb/tuieval/issues
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2, 0, '
|
|
21
|
+
__version__ = version = '0.2.0.dev87'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev87')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1550,6 +1550,13 @@ def run_title(tier, models, packs):
|
|
|
1550
1550
|
f"{', '.join(packs) if len(packs) <= 2 else f'{len(packs)} packs'}")
|
|
1551
1551
|
|
|
1552
1552
|
|
|
1553
|
+
def short_keys(keys, n=4):
|
|
1554
|
+
"""A few "model/pack" pairs as "model · pack", the rest counted."""
|
|
1555
|
+
keys = sorted(keys)
|
|
1556
|
+
shown = ", ".join(k.replace("/", " · ", 1) for k in keys[:n])
|
|
1557
|
+
return rich_escape(shown + (f" and {len(keys) - n} more" if len(keys) > n else ""))
|
|
1558
|
+
|
|
1559
|
+
|
|
1553
1560
|
class QueuedRun:
|
|
1554
1561
|
"""A run waiting its turn. It's planned again when it starts, so it picks up what the runs before
|
|
1555
1562
|
it finished; it never sets aside answers that weren't confirmed when it was queued. retest: the
|
|
@@ -1557,13 +1564,29 @@ class QueuedRun:
|
|
|
1557
1564
|
kind = "Run"
|
|
1558
1565
|
|
|
1559
1566
|
def __init__(self, sel, retest, leave, accepted, eng):
|
|
1560
|
-
self.sel, self.retest, self.leave, self.accepted = sel, retest, leave, accepted
|
|
1567
|
+
self.sel, self.retest, self.leave, self.accepted = sel, retest, set(leave), accepted
|
|
1561
1568
|
self.queued = time.time()
|
|
1569
|
+
self.replan(eng)
|
|
1570
|
+
|
|
1571
|
+
def replan(self, eng):
|
|
1562
1572
|
self.jobs, self.problem = self.plan(eng)
|
|
1563
1573
|
jobs = self.jobs or []
|
|
1564
|
-
self.title = run_title(sel["tier"], {j.label for j in jobs}, {j.pack.label for j in jobs})
|
|
1574
|
+
self.title = run_title(self.sel["tier"], {j.label for j in jobs}, {j.pack.label for j in jobs})
|
|
1565
1575
|
self.est = eng.estimate_seconds(jobs)[0]
|
|
1566
1576
|
|
|
1577
|
+
def keys(self):
|
|
1578
|
+
"""The "model/pack" pairs it covers."""
|
|
1579
|
+
return {f"{m}/{p}" for m in self.sel["models"] for p in self.sel["packs"]} - self.leave
|
|
1580
|
+
|
|
1581
|
+
def deliberate(self):
|
|
1582
|
+
"""Retest or picked questions: asked again on purpose, never treated as a duplicate."""
|
|
1583
|
+
return bool(self.sel.get("force") or self.sel.get("tests"))
|
|
1584
|
+
|
|
1585
|
+
def drop(self, keys, eng):
|
|
1586
|
+
"""Leave these pairs out (they're already running or queued)."""
|
|
1587
|
+
self.leave |= set(keys)
|
|
1588
|
+
self.replan(eng)
|
|
1589
|
+
|
|
1567
1590
|
def tests(self, eng):
|
|
1568
1591
|
"""The picked tests that are still in their packs (a pack edited while queued drops the rest)."""
|
|
1569
1592
|
out = {}
|
|
@@ -2183,6 +2206,7 @@ class RunScreen(Screen):
|
|
|
2183
2206
|
yield Footer()
|
|
2184
2207
|
|
|
2185
2208
|
def on_mount(self):
|
|
2209
|
+
self._queue_follow, self._queue_y = True, None # follow_queue
|
|
2186
2210
|
q = self.query_one("#queue", DataTable)
|
|
2187
2211
|
for label, key in (("Model", "model"), ("Eval", "suite"), ("Status", "status"), ("Progress", "progress"),
|
|
2188
2212
|
("✓", "pass"), ("✗", "fail"), ("Time", "time"), ("Note", "note")):
|
|
@@ -2500,6 +2524,25 @@ class RunScreen(Screen):
|
|
|
2500
2524
|
self.update_job(j)
|
|
2501
2525
|
self.update_overall()
|
|
2502
2526
|
self.refresh_current()
|
|
2527
|
+
self.follow_queue()
|
|
2528
|
+
|
|
2529
|
+
def follow_queue(self):
|
|
2530
|
+
"""Keep the running model's row in view in the queue, unless you scrolled the queue yourself;
|
|
2531
|
+
scrolling back to where the running row shows picks the following up again."""
|
|
2532
|
+
q = self.query_one("#queue", DataTable)
|
|
2533
|
+
keys = [r.key.value for r in q.ordered_rows]
|
|
2534
|
+
now = next((i for i, k in enumerate(keys)
|
|
2535
|
+
if any(j.key == k and j.status in ("loading", "running") for j in self.jobs)), None)
|
|
2536
|
+
if now is None:
|
|
2537
|
+
return
|
|
2538
|
+
if self._queue_y is not None and q.scroll_y != self._queue_y: # you scrolled since we last did
|
|
2539
|
+
seen = q.scroll_y <= now < q.scroll_y + max(1, q.scrollable_content_region.height - 1)
|
|
2540
|
+
self._queue_follow = seen
|
|
2541
|
+
if self._queue_follow:
|
|
2542
|
+
q.scroll_to(y=max(0, now - 1), animate=False)
|
|
2543
|
+
self._queue_y = max(0, min(now - 1, q.max_scroll_y))
|
|
2544
|
+
else:
|
|
2545
|
+
self._queue_y = q.scroll_y
|
|
2503
2546
|
|
|
2504
2547
|
# -- actions
|
|
2505
2548
|
def action_skip_model(self):
|
|
@@ -3236,8 +3279,32 @@ class EvalsApp(App):
|
|
|
3236
3279
|
what = s.live_text() if s else "The last run is still stopping its server"
|
|
3237
3280
|
return what + (f" (+{len(self.queue)} queued)" if self.queue else "")
|
|
3238
3281
|
|
|
3282
|
+
def taken(self, tier):
|
|
3283
|
+
"""The "model/pack" pairs of this tier already in the run going (not finished) or in the queue."""
|
|
3284
|
+
out = set()
|
|
3285
|
+
run = self.active_session
|
|
3286
|
+
if isinstance(run, RunScreen) and run.running and run.jobs and run.jobs[0].tier == tier:
|
|
3287
|
+
out |= {j.key for j in run.jobs if j.status in ("waiting", "loading", "running")}
|
|
3288
|
+
for q in self.queue:
|
|
3289
|
+
if q.kind == "Run" and q.sel["tier"] == tier:
|
|
3290
|
+
out |= q.keys()
|
|
3291
|
+
return out
|
|
3292
|
+
|
|
3239
3293
|
# -- the queue: Start or t while something runs adds to it; each item starts when the one before ends
|
|
3240
3294
|
def start_or_queue(self, item):
|
|
3295
|
+
skipped = set()
|
|
3296
|
+
if self.is_busy() and item.kind == "Run" and not item.deliberate():
|
|
3297
|
+
mine = item.keys()
|
|
3298
|
+
skipped = mine & self.taken(item.sel["tier"])
|
|
3299
|
+
if skipped == mine:
|
|
3300
|
+
self.notify(f"Already running or queued, so nothing was added: {short_keys(skipped)}.", timeout=10)
|
|
3301
|
+
return
|
|
3302
|
+
if skipped:
|
|
3303
|
+
item.drop(skipped, self.engine)
|
|
3304
|
+
if not item.jobs:
|
|
3305
|
+
self.notify(f"Nothing new to run: {item.problem}. Already running or queued: "
|
|
3306
|
+
f"{short_keys(skipped)}.", timeout=10)
|
|
3307
|
+
return
|
|
3241
3308
|
if not self.is_busy():
|
|
3242
3309
|
screen, problem = item.make(self.engine)
|
|
3243
3310
|
if problem:
|
|
@@ -3256,11 +3323,11 @@ class EvalsApp(App):
|
|
|
3256
3323
|
self.add_to_run(run, item)
|
|
3257
3324
|
elif choice == "queue":
|
|
3258
3325
|
self.enqueue(item)
|
|
3259
|
-
what =
|
|
3260
|
-
|
|
3326
|
+
what = short_keys(item.keys(), 6)
|
|
3327
|
+
skip = (f"\n\nLeft out, already running or queued: {short_keys(skipped)}." if skipped else "")
|
|
3261
3328
|
self.push_screen(ChoiceScreen(
|
|
3262
3329
|
f"A run is going: {run.live_text()}.\n\nAdd {what} to it? Its models run after the ones "
|
|
3263
|
-
"already in it.\nOr queue it as a separate run that starts when this one ends.",
|
|
3330
|
+
f"already in it.\nOr queue it as a separate run that starts when this one ends.{skip}",
|
|
3264
3331
|
[("add", "Add to this run", "success"), ("queue", f"Queue it as #{len(self.queue) + 1}", "primary"),
|
|
3265
3332
|
("keep", "Cancel", "default")]), picked)
|
|
3266
3333
|
return
|
|
@@ -3270,6 +3337,9 @@ class EvalsApp(App):
|
|
|
3270
3337
|
jobs, problem = item.plan(self.engine)
|
|
3271
3338
|
added, why = self.engine.add_jobs(jobs) if jobs else ([], problem)
|
|
3272
3339
|
if not added:
|
|
3340
|
+
if why and why.startswith("everything picked is already in the run"):
|
|
3341
|
+
self.notify(f"Not added: {why}.", timeout=10) # queueing it again would only duplicate it
|
|
3342
|
+
return
|
|
3273
3343
|
self.notify(f"Not added: {why}. Queued it instead.", severity="warning", timeout=10)
|
|
3274
3344
|
self.enqueue(item)
|
|
3275
3345
|
return
|
|
@@ -3281,6 +3351,8 @@ class EvalsApp(App):
|
|
|
3281
3351
|
def enqueue(self, item):
|
|
3282
3352
|
ahead = self.busy_text()
|
|
3283
3353
|
self.queue.append(item)
|
|
3354
|
+
if not self.active_session: # the last run already ended (its server may still be stopping):
|
|
3355
|
+
self.set_timer(0.3, self.start_next) # nothing else would start the queue
|
|
3284
3356
|
self.notify(f"Queued #{len(self.queue)}: {item.title}. It starts by itself after: {ahead}. "
|
|
3285
3357
|
"w shows the queue.", timeout=10)
|
|
3286
3358
|
self.refresh_live()
|
|
@@ -1007,6 +1007,10 @@ class AddToRun(unittest.TestCase):
|
|
|
1007
1007
|
import asyncio, json, time
|
|
1008
1008
|
from tuieval.tui import EvalsApp, RunScreen, SetupScreen, ChoiceScreen
|
|
1009
1009
|
from textual.widgets import SelectionList
|
|
1010
|
+
async def setup(app, pilot): # a dialog closing can take a moment on a slow machine
|
|
1011
|
+
deadline = time.time() + 30
|
|
1012
|
+
while not isinstance(app.screen, SetupScreen) and time.time() < deadline:
|
|
1013
|
+
await pilot.pause(0.1)
|
|
1010
1014
|
async def go():
|
|
1011
1015
|
app = EvalsApp({})
|
|
1012
1016
|
async with app.run_test(size=(160, 50)) as pilot:
|
|
@@ -1033,7 +1037,7 @@ class AddToRun(unittest.TestCase):
|
|
|
1033
1037
|
await pilot.pause()
|
|
1034
1038
|
asked = isinstance(app.screen, ChoiceScreen)
|
|
1035
1039
|
app.screen.dismiss("add")
|
|
1036
|
-
await pilot
|
|
1040
|
+
await setup(app, pilot)
|
|
1037
1041
|
app.screen.query_one("#suites", SelectionList).deselect("apps")
|
|
1038
1042
|
app.screen.query_one("#suites", SelectionList).select("other")
|
|
1039
1043
|
app.screen.selected_models = {"a"}
|
|
@@ -1041,7 +1045,7 @@ class AddToRun(unittest.TestCase):
|
|
|
1041
1045
|
app.screen.action_start()
|
|
1042
1046
|
await pilot.pause()
|
|
1043
1047
|
app.screen.dismiss("queue")
|
|
1044
|
-
await pilot
|
|
1048
|
+
await setup(app, pilot)
|
|
1045
1049
|
app.screen.query_one("#tier-smoke").value = True # another tier can't join: queued
|
|
1046
1050
|
await pilot.pause()
|
|
1047
1051
|
app.screen.action_start()
|
|
@@ -1074,6 +1078,141 @@ class AddToRun(unittest.TestCase):
|
|
|
1074
1078
|
self.assertEqual(out["second"], [["a/other", "done"]]) # queued: its own run after
|
|
1075
1079
|
|
|
1076
1080
|
|
|
1081
|
+
def test_tui_never_queues_the_same_thing_twice(self):
|
|
1082
|
+
"""Start while a run is going never adds or queues a model and pack already running or queued."""
|
|
1083
|
+
code = textwrap.dedent("""
|
|
1084
|
+
import asyncio, json, time
|
|
1085
|
+
from tuieval.tui import EvalsApp, RunScreen, SetupScreen, ChoiceScreen
|
|
1086
|
+
from textual.widgets import SelectionList
|
|
1087
|
+
async def pick(app, pilot, models, packs=("apps",)):
|
|
1088
|
+
deadline = time.time() + 30
|
|
1089
|
+
while not isinstance(app.screen, SetupScreen) and time.time() < deadline:
|
|
1090
|
+
await pilot.pause(0.1)
|
|
1091
|
+
s = app.screen
|
|
1092
|
+
for p in ("apps", "other"):
|
|
1093
|
+
(s.query_one("#suites", SelectionList).select if p in packs
|
|
1094
|
+
else s.query_one("#suites", SelectionList).deselect)(p)
|
|
1095
|
+
s.selected_models = set(models)
|
|
1096
|
+
s.refresh_models()
|
|
1097
|
+
await pilot.pause()
|
|
1098
|
+
s.action_start()
|
|
1099
|
+
await pilot.pause()
|
|
1100
|
+
return app.screen
|
|
1101
|
+
async def go():
|
|
1102
|
+
app = EvalsApp({})
|
|
1103
|
+
async with app.run_test(size=(160, 50)) as pilot:
|
|
1104
|
+
await pilot.pause()
|
|
1105
|
+
app.screen.query_one("#tier-certify").value = True
|
|
1106
|
+
await pick(app, pilot, ["a"])
|
|
1107
|
+
deadline = time.time() + 180
|
|
1108
|
+
while time.time() < deadline:
|
|
1109
|
+
await pilot.pause(0.1)
|
|
1110
|
+
if isinstance(app.screen, RunScreen) and app.screen.counts()[0]:
|
|
1111
|
+
break
|
|
1112
|
+
run = app.screen
|
|
1113
|
+
await pilot.press("n")
|
|
1114
|
+
out = {}
|
|
1115
|
+
s = await pick(app, pilot, ["a"]) # the same again: nothing happens
|
|
1116
|
+
out["same"] = [isinstance(s, ChoiceScreen), len(app.queue)]
|
|
1117
|
+
s = await pick(app, pilot, ["a", "b"]) # partly new: only b is offered
|
|
1118
|
+
out["partly"] = [isinstance(s, ChoiceScreen), "Left out" in str(getattr(s, "question", ""))]
|
|
1119
|
+
s.dismiss("queue")
|
|
1120
|
+
await pilot.pause()
|
|
1121
|
+
out["queued"] = [sorted(q.keys()) for q in app.queue]
|
|
1122
|
+
s = await pick(app, pilot, ["b"]) # already queued: nothing happens
|
|
1123
|
+
out["again"] = [isinstance(s, ChoiceScreen), len(app.queue)]
|
|
1124
|
+
s = await pick(app, pilot, ["a"], ("other",)) # new: offered, queued or started
|
|
1125
|
+
out["new"] = (isinstance(s, ChoiceScreen) or any("a/other" in q.keys() for q in app.queue)
|
|
1126
|
+
or any(j.key == "a/other" for x in app.sessions for j in x.jobs))
|
|
1127
|
+
if isinstance(s, ChoiceScreen):
|
|
1128
|
+
s.dismiss("queue")
|
|
1129
|
+
await pilot.pause()
|
|
1130
|
+
deadline = time.time() + 300 # the queued runs start one after another: wait for all three
|
|
1131
|
+
while time.time() < deadline:
|
|
1132
|
+
await pilot.pause(0.2)
|
|
1133
|
+
if len(app.sessions) == 3 and not app.queue and not any(x.running for x in app.sessions):
|
|
1134
|
+
break
|
|
1135
|
+
out["sessions"] = sorted((j.key, j.status) for x in app.sessions for j in x.jobs)
|
|
1136
|
+
print(json.dumps(out))
|
|
1137
|
+
asyncio.run(go())
|
|
1138
|
+
""")
|
|
1139
|
+
p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=600,
|
|
1140
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""))
|
|
1141
|
+
out = json.loads(p.stdout.strip().splitlines()[-1]) if p.stdout.strip() else p.stderr
|
|
1142
|
+
self.assertIsInstance(out, dict, out)
|
|
1143
|
+
self.assertEqual(out["same"], [False, 0])
|
|
1144
|
+
self.assertEqual(out["partly"], [True, True])
|
|
1145
|
+
self.assertEqual(out["queued"], [["b/apps"]])
|
|
1146
|
+
self.assertEqual(out["again"], [False, 1])
|
|
1147
|
+
self.assertTrue(out["new"])
|
|
1148
|
+
self.assertEqual(out["sessions"], [["a/apps", "done"], ["a/other", "done"], ["b/apps", "done"]])
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
class QueueFollows(unittest.TestCase):
|
|
1152
|
+
"""The run screen's queue keeps the running model in view unless you scroll it yourself."""
|
|
1153
|
+
|
|
1154
|
+
def test_queue_follows_the_running_row(self):
|
|
1155
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1156
|
+
ws = os.path.join(tmp, "ws")
|
|
1157
|
+
tuieval(tmp, "init", ws)
|
|
1158
|
+
for i in range(15):
|
|
1159
|
+
d = os.path.join(ws, "packs", f"p{i:02}")
|
|
1160
|
+
os.makedirs(d)
|
|
1161
|
+
write(os.path.join(d, "pack.toml"), f'label = "p{i:02}"\norder = {i}\n[certify]\nrepeat = 1\n[gate]\nmin_accuracy = 0.1\n')
|
|
1162
|
+
write(os.path.join(d, "tests.yaml"), f"- {{id: a, input: q{i} a, expected: 1, reference: 'ANSWER: 1', difficulty: easy}}\n"
|
|
1163
|
+
f"- {{id: b, input: q{i} b, expected: 1, reference: 'ANSWER: 1', difficulty: easy}}\n")
|
|
1164
|
+
mock = [sys.executable, os.path.join(HERE, "mock_server.py"), "--port", "{port}", "--packs",
|
|
1165
|
+
os.path.join(ws, "packs"), "--model", "{served_name}", "--delay", "0.4"]
|
|
1166
|
+
with open(os.path.join(ws, "models.toml"), "a") as f:
|
|
1167
|
+
f.write(f'\n[servers.mk]\ncmd = {json.dumps(mock)}\nport = {free_port()}\n'
|
|
1168
|
+
'\n[[models]]\nlabel = "m"\nserver = "mk"\nmodel = "m"\n')
|
|
1169
|
+
code = textwrap.dedent("""
|
|
1170
|
+
import asyncio, json, time
|
|
1171
|
+
from tuieval.tui import EvalsApp, RunScreen
|
|
1172
|
+
from textual.widgets import SelectionList, DataTable
|
|
1173
|
+
async def go():
|
|
1174
|
+
app = EvalsApp({})
|
|
1175
|
+
async with app.run_test(size=(160, 50)) as pilot:
|
|
1176
|
+
await pilot.pause()
|
|
1177
|
+
s = app.screen
|
|
1178
|
+
for i in range(15):
|
|
1179
|
+
s.query_one("#suites", SelectionList).select(f"p{i:02}")
|
|
1180
|
+
s.selected_models = {"m"}
|
|
1181
|
+
s.refresh_models()
|
|
1182
|
+
s.query_one("#tier-certify").value = True
|
|
1183
|
+
await pilot.pause()
|
|
1184
|
+
s.action_start()
|
|
1185
|
+
def at(run, n):
|
|
1186
|
+
return sum(j.status == "done" for j in run.jobs) >= n
|
|
1187
|
+
deadline = time.time() + 240
|
|
1188
|
+
while time.time() < deadline:
|
|
1189
|
+
await pilot.pause(0.2)
|
|
1190
|
+
if isinstance(app.screen, RunScreen) and at(app.screen, 12):
|
|
1191
|
+
break
|
|
1192
|
+
run = app.screen
|
|
1193
|
+
q = run.query_one("#queue", DataTable)
|
|
1194
|
+
await pilot.pause(1.5)
|
|
1195
|
+
followed = q.scroll_y
|
|
1196
|
+
q.scroll_to(y=0, animate=False) # you scroll up
|
|
1197
|
+
await pilot.pause(2.5)
|
|
1198
|
+
stayed = q.scroll_y
|
|
1199
|
+
q.scroll_to(y=q.max_scroll_y, animate=False) # back to where it runs
|
|
1200
|
+
await pilot.pause(2.5)
|
|
1201
|
+
again = q.scroll_y
|
|
1202
|
+
while run.running and time.time() < deadline:
|
|
1203
|
+
await pilot.pause(0.2)
|
|
1204
|
+
print(json.dumps({"followed": followed, "stayed": stayed, "again": again}))
|
|
1205
|
+
asyncio.run(go())
|
|
1206
|
+
""")
|
|
1207
|
+
p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=480,
|
|
1208
|
+
env=dict(os.environ, TUIEVAL_HOME=ws, TUIEVAL_DETECT_PORTS=""))
|
|
1209
|
+
out = json.loads(p.stdout.strip().splitlines()[-1]) if p.stdout.strip() else p.stderr
|
|
1210
|
+
self.assertIsInstance(out, dict, out)
|
|
1211
|
+
self.assertGreater(out["followed"], 0, out) # scrolled down to the running row by itself
|
|
1212
|
+
self.assertEqual(out["stayed"], 0, out) # your scroll wins
|
|
1213
|
+
self.assertGreater(out["again"], 0, out) # and following resumes once you're back
|
|
1214
|
+
|
|
1215
|
+
|
|
1077
1216
|
class PTAIndex(unittest.TestCase):
|
|
1078
1217
|
"""The PTA index: parsimony from tokens and speed from time, each relative to the best on a log
|
|
1079
1218
|
scale, and accuracy, all over the questions every compared model answered."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev81 → tuieval-0.2.0.dev87}/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
|