tuieval 0.2.0.dev28__tar.gz → 0.2.0.dev32__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.dev28 → tuieval-0.2.0.dev32}/CHANGELOG.md +1 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/PKG-INFO +2 -2
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/README.md +1 -1
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/engine.py +71 -27
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/tui.py +61 -8
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/tests/test_tuieval.py +154 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/.gitignore +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/LICENSE +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/models.md +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/compare.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/run_evals.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/tests/mock_server.py +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
5
|
- Run several models at a time: `parallel_models` per machine, **Models at a time** in the TUI, or `tuieval run --parallel N` (default 1). Models wait for memory to fit, and answers note what ran alongside them.
|
|
6
|
+
- More evals during a run: `n` opens Setup, and Start adds them to the run going (same tier) or queues them after it.
|
|
6
7
|
- A new workspace's `models.toml` is 23 lines: the llama, local and OpenRouter servers and the defaults are built in (docs/models.md lists them in full; a server defined in `models.toml` replaces the built-in one with that name). Existing workspaces keep their settings; built-in servers they don't define become available, and adding a model still prefers the workspace's own servers.
|
|
7
8
|
- Less to take in at first: until something has run, the setup screen leaves out context sizes, tuning state and the Tune key; Presets and Hide are off the footer (`?` lists every key); `tuieval help` groups commands into Start here, Day to day and Advanced.
|
|
8
9
|
- Results lead with a plain summary per model ("ready for Support; not decided yet for Coding → run Certify to finish"), in the TUI, `tuieval verdict` (with the command to run next) and the report; the statistics follow.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev32
|
|
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
|
|
@@ -137,7 +137,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
|
|
|
137
137
|
|
|
138
138
|
1. **Pick packs and models** (space ticks; type to filter models by label or tag). Each model shows a short code per use case, e.g. `C✓ S?` (✓ pass, ✗ fail, ? inconclusive; grey = from earlier results). To run only some tests of a pack, highlight it and press `e`. Runs of different tests add up, and the pack can PASS once all its tests have run.
|
|
139
139
|
2. **Pick a tier** (Smoke to check setup, Screen, Certify) and **press `s`**. The line above the buttons shows how many answers that is and roughly how long it will take. For each model, tuieval starts its server (or uses a running one), checks the right model is loaded, runs every selected pack, then stops it.
|
|
140
|
-
3. **Watch the run:** progress with ETA, live score, tok/s, TTFT and memory, the current test with reasoning and answer side by side, recent results with the grader's reason. `k` skips a model, `c` cancels (finished work is kept and resumes next time).
|
|
140
|
+
3. **Watch the run:** progress with ETA, live score, tok/s, TTFT and memory, the current test with reasoning and answer side by side, recent results with the grader's reason. `k` skips a model, `c` cancels (finished work is kept and resumes next time). `n` sets up more evals while it runs: add them to this run (same tier) or queue them as the next run.
|
|
141
141
|
4. **Press `r` for results:** production readiness, verdict history, scorecard, speed & tokens (★ marks models nothing beats on both accuracy and time), per question, is the difference real?, tests that separate models, by difficulty, failures, and test quality.
|
|
142
142
|
|
|
143
143
|
Other keys: `t` tunes the ticked models' speed flags, `a` adds a model, `m` scans for GGUFs, `p` saves or loads a preset, `x` hides a model. `?` explains any screen.
|
|
@@ -111,7 +111,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
|
|
|
111
111
|
|
|
112
112
|
1. **Pick packs and models** (space ticks; type to filter models by label or tag). Each model shows a short code per use case, e.g. `C✓ S?` (✓ pass, ✗ fail, ? inconclusive; grey = from earlier results). To run only some tests of a pack, highlight it and press `e`. Runs of different tests add up, and the pack can PASS once all its tests have run.
|
|
113
113
|
2. **Pick a tier** (Smoke to check setup, Screen, Certify) and **press `s`**. The line above the buttons shows how many answers that is and roughly how long it will take. For each model, tuieval starts its server (or uses a running one), checks the right model is loaded, runs every selected pack, then stops it.
|
|
114
|
-
3. **Watch the run:** progress with ETA, live score, tok/s, TTFT and memory, the current test with reasoning and answer side by side, recent results with the grader's reason. `k` skips a model, `c` cancels (finished work is kept and resumes next time).
|
|
114
|
+
3. **Watch the run:** progress with ETA, live score, tok/s, TTFT and memory, the current test with reasoning and answer side by side, recent results with the grader's reason. `k` skips a model, `c` cancels (finished work is kept and resumes next time). `n` sets up more evals while it runs: add them to this run (same tier) or queue them as the next run.
|
|
115
115
|
4. **Press `r` for results:** production readiness, verdict history, scorecard, speed & tokens (★ marks models nothing beats on both accuracy and time), per question, is the difference real?, tests that separate models, by difficulty, failures, and test quality.
|
|
116
116
|
|
|
117
117
|
Other keys: `t` tunes the ticked models' speed flags, `a` adds a model, `m` scans for GGUFs, `p` saves or loads a preset, `x` hides a model. `?` explains any screen.
|
|
@@ -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.dev32'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev32')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -698,6 +698,8 @@ class Engine:
|
|
|
698
698
|
self._lanes_lock = threading.Lock()
|
|
699
699
|
self._procs = []
|
|
700
700
|
self._stop_lock = threading.Lock()
|
|
701
|
+
self._run_lock = threading.Lock()
|
|
702
|
+
self._run = None # the run going: {"jobs", "pending", "open", "machine", "cond"} (add_jobs)
|
|
701
703
|
self._header_cache = {}
|
|
702
704
|
self._serving = {}
|
|
703
705
|
self.tuning_dir = os.path.join(root, "tuning")
|
|
@@ -1552,29 +1554,38 @@ class Engine:
|
|
|
1552
1554
|
# ---- running
|
|
1553
1555
|
def run(self, jobs, parallel=None):
|
|
1554
1556
|
"""Run the jobs (blocking). Jobs of the same model share one server start. parallel: how many
|
|
1555
|
-
models are served at a time (default: parallel_models for this machine).
|
|
1557
|
+
models are served at a time (default: parallel_models for this machine). add_jobs() can add
|
|
1558
|
+
more while it runs; they run after the ones before them."""
|
|
1556
1559
|
self._cancel.clear()
|
|
1557
1560
|
self.emit("queue_started", jobs=jobs)
|
|
1558
1561
|
models = sorted({j.label for j in jobs if j.status == "waiting"})
|
|
1562
|
+
machine = bool(jobs) and self.needs_machine(jobs)
|
|
1559
1563
|
lock = (self.machine_lock(f"{jobs[0].tier} run: {', '.join(models[:3])}"
|
|
1560
1564
|
+ (f" +{len(models) - 3}" if len(models) > 3 else ""))
|
|
1561
|
-
if
|
|
1565
|
+
if machine else contextlib.nullcontext())
|
|
1566
|
+
order = []
|
|
1567
|
+
for j in jobs:
|
|
1568
|
+
if j.label not in order:
|
|
1569
|
+
order.append(j.label)
|
|
1570
|
+
groups = [(label, [j for j in jobs if j.label == label and j.status == "waiting"]) for label in order]
|
|
1571
|
+
with self._run_lock:
|
|
1572
|
+
self._run = {"jobs": jobs, "pending": [g for g in groups if g[1]], "open": True, "machine": machine,
|
|
1573
|
+
"cond": threading.Condition(self._run_lock)}
|
|
1562
1574
|
try:
|
|
1563
1575
|
with lock:
|
|
1564
|
-
|
|
1565
|
-
for j in jobs:
|
|
1566
|
-
if j.label not in order:
|
|
1567
|
-
order.append(j.label)
|
|
1568
|
-
groups = [(label, [j for j in jobs if j.label == label and j.status == "waiting"]) for label in order]
|
|
1569
|
-
self._run_lanes([g for g in groups if g[1]], max(1, int(parallel or self.parallel_models())))
|
|
1576
|
+
self._run_lanes(max(1, int(parallel or self.parallel_models())))
|
|
1570
1577
|
if self._cancel.is_set():
|
|
1571
1578
|
raise Cancelled()
|
|
1572
1579
|
except Cancelled:
|
|
1580
|
+
with self._run_lock:
|
|
1581
|
+
self._run["open"] = False
|
|
1573
1582
|
for j in jobs:
|
|
1574
1583
|
if j.status in ("waiting", "loading", "running"):
|
|
1575
1584
|
j.status, j.note = "skipped", "cancelled (progress kept)" if j.done else "cancelled"
|
|
1576
1585
|
self.emit("job_done", job=j)
|
|
1577
1586
|
finally:
|
|
1587
|
+
with self._run_lock:
|
|
1588
|
+
self._run["open"] = False
|
|
1578
1589
|
changes = []
|
|
1579
1590
|
if any(j.status == "done" and j.started and j.tier != "smoke" for j in jobs):
|
|
1580
1591
|
try:
|
|
@@ -1586,12 +1597,36 @@ class Engine:
|
|
|
1586
1597
|
self.emit("verdicts", changes=changes)
|
|
1587
1598
|
self.emit("queue_done", jobs=jobs, cancelled=self._cancel.is_set())
|
|
1588
1599
|
|
|
1589
|
-
def
|
|
1600
|
+
def add_jobs(self, new):
|
|
1601
|
+
"""Add planned jobs to the run going on, after its other jobs. Returns (added, why not): a
|
|
1602
|
+
pack already waiting or running in it isn't added twice, and nothing is added once the run is
|
|
1603
|
+
finishing or cancelled, or when it holds no machine lock (hosted only) and these need one.
|
|
1604
|
+
Runs on the caller's thread, so the caller shows what was added (no event)."""
|
|
1605
|
+
with self._run_lock:
|
|
1606
|
+
r = self._run
|
|
1607
|
+
if not r or not r["open"] or self._cancel.is_set():
|
|
1608
|
+
return [], "the run is finishing"
|
|
1609
|
+
if not r["machine"] and self.needs_machine(new):
|
|
1610
|
+
return [], "the run going uses only hosted models, and these need this machine"
|
|
1611
|
+
busy = {j.key for j in r["jobs"] if j.status in ("waiting", "loading", "running")}
|
|
1612
|
+
added = [j for j in new if j.status == "waiting" and j.key not in busy]
|
|
1613
|
+
r["jobs"].extend(added)
|
|
1614
|
+
for j in added: # a model still waiting takes them on; otherwise it comes again at the end
|
|
1615
|
+
group = next((g for g in r["pending"] if g[0] == j.label), None)
|
|
1616
|
+
if group:
|
|
1617
|
+
group[1].append(j)
|
|
1618
|
+
else:
|
|
1619
|
+
r["pending"].append((j.label, [j]))
|
|
1620
|
+
r["cond"].notify_all()
|
|
1621
|
+
return added, None if added else "everything picked is already in the run"
|
|
1622
|
+
|
|
1623
|
+
def _run_lanes(self, parallel):
|
|
1590
1624
|
"""Run each model's jobs in its own lane, up to `parallel` at a time, in order: the next
|
|
1591
1625
|
model starts once a lane is free, nothing running stops it (runs_alone), and its memory fits
|
|
1592
|
-
next to the ones running (memory_need_gb; an unknown need is left to the setting).
|
|
1593
|
-
|
|
1594
|
-
|
|
1626
|
+
next to the ones running (memory_need_gb; an unknown need is left to the setting). Lanes
|
|
1627
|
+
wait for jobs added meanwhile (add_jobs) until every model is done."""
|
|
1628
|
+
pending, running, told = self._run["pending"], {}, set()
|
|
1629
|
+
cond = self._run["cond"]
|
|
1595
1630
|
|
|
1596
1631
|
def blocked(label):
|
|
1597
1632
|
"""Why the next model can't start now (None: it can)."""
|
|
@@ -1614,20 +1649,29 @@ class Engine:
|
|
|
1614
1649
|
|
|
1615
1650
|
def lane():
|
|
1616
1651
|
while True:
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1652
|
+
mine, note = None, None
|
|
1653
|
+
with cond: # events go out after the lock is released: the UI may be waiting for it (add_jobs)
|
|
1654
|
+
if self._cancel.is_set() or not pending and not running:
|
|
1655
|
+
self._run["open"] = False # nothing more can be added
|
|
1656
|
+
cond.notify_all()
|
|
1657
|
+
return
|
|
1658
|
+
# in order, except that a model added again while it runs waits for itself to end
|
|
1659
|
+
# without holding up the ones behind it
|
|
1660
|
+
nxt = next((i for i, g in enumerate(pending) if g[0] not in running), None)
|
|
1661
|
+
label = pending[nxt][0] if nxt is not None else None
|
|
1662
|
+
why = blocked(label) if label else "" # none can start: a model still runs, more may come
|
|
1663
|
+
if why is None:
|
|
1664
|
+
label, mine = pending.pop(nxt)
|
|
1665
|
+
running[label] = self.memory_need_gb(self.model(label))
|
|
1666
|
+
elif why and label not in told:
|
|
1667
|
+
told.add(label)
|
|
1668
|
+
note = f"{label} waits: {why}"
|
|
1669
|
+
else:
|
|
1630
1670
|
cond.wait(1.0)
|
|
1671
|
+
if note:
|
|
1672
|
+
self.emit("model_waiting", label=label, message=note)
|
|
1673
|
+
if mine is None:
|
|
1674
|
+
continue
|
|
1631
1675
|
try:
|
|
1632
1676
|
self._run_lane(label, mine)
|
|
1633
1677
|
finally:
|
|
@@ -1635,10 +1679,10 @@ class Engine:
|
|
|
1635
1679
|
running.pop(label, None)
|
|
1636
1680
|
cond.notify_all()
|
|
1637
1681
|
|
|
1638
|
-
if parallel == 1
|
|
1682
|
+
if parallel == 1:
|
|
1639
1683
|
lane()
|
|
1640
1684
|
return
|
|
1641
|
-
threads = [threading.Thread(target=lane, name=f"lane-{i + 1}") for i in range(
|
|
1685
|
+
threads = [threading.Thread(target=lane, name=f"lane-{i + 1}") for i in range(parallel)]
|
|
1642
1686
|
for t in threads:
|
|
1643
1687
|
t.start()
|
|
1644
1688
|
for t in threads:
|
|
@@ -294,7 +294,7 @@ HELP = {
|
|
|
294
294
|
parallel_models in models.toml, default 1). Answers note the models they ran alongside.
|
|
295
295
|
|
|
296
296
|
[b]Keys[/b]
|
|
297
|
-
s start (
|
|
297
|
+
s start (with a run going: add to it, or queue it) r results
|
|
298
298
|
a add a model (a GGUF, a model id, openrouter:<id>) m scan model folders for new GGUFs
|
|
299
299
|
e pick which tests of the highlighted pack run p save or load a selection (preset)
|
|
300
300
|
t tune the ticked models' speed flags here (after x hide or unhide a model
|
|
@@ -310,7 +310,7 @@ with the model's reasoning and answer as they stream, then recent results with t
|
|
|
310
310
|
|
|
311
311
|
[b]Keys[/b]
|
|
312
312
|
k skip the current model c cancel everything (finished answers are kept; it resumes next time)
|
|
313
|
-
f pause or resume auto-scroll r results n
|
|
313
|
+
f pause or resume auto-scroll r results n more evals: add them to this run or queue them
|
|
314
314
|
w runs and queue esc back to Setup (the run keeps going)
|
|
315
315
|
v with several models at a time: stream the next one (k skips the one streaming)""",
|
|
316
316
|
"ResultsScreen": """[b]Results[/b]
|
|
@@ -878,7 +878,8 @@ class SetupScreen(Screen):
|
|
|
878
878
|
"""Says what Start will do when something is already running, here or in another window."""
|
|
879
879
|
app = self.app
|
|
880
880
|
if app.is_busy():
|
|
881
|
-
|
|
881
|
+
add = "add this to the run going or " if isinstance(app.active_session, RunScreen) else ""
|
|
882
|
+
return (f"\n[cyan]{app.busy_text()}: Start lets you {add}queue it as #{len(app.queue) + 1} "
|
|
882
883
|
"(w shows the queue).[/cyan]")
|
|
883
884
|
holder = app.engine.machine_holder()
|
|
884
885
|
if holder:
|
|
@@ -1464,8 +1465,8 @@ class QueuedRun:
|
|
|
1464
1465
|
def line(self):
|
|
1465
1466
|
return f"{self.title} [dim]~{fmt_secs(self.est)} · queued {time.strftime('%H:%M', time.localtime(self.queued))}[/dim]"
|
|
1466
1467
|
|
|
1467
|
-
def
|
|
1468
|
-
"""(
|
|
1468
|
+
def plan(self, eng):
|
|
1469
|
+
"""(jobs, None), or (None, why it can't run) now."""
|
|
1469
1470
|
sel = self.sel
|
|
1470
1471
|
jobs = eng.plan(sel["models"], sel["packs"], sel["repeat"], sel["tier"], sel["force"] or self.rerun,
|
|
1471
1472
|
self.tests(eng))
|
|
@@ -1476,7 +1477,12 @@ class QueuedRun:
|
|
|
1476
1477
|
if new:
|
|
1477
1478
|
return None, (f"it would now start over {', '.join(new)}, which wasn't confirmed when it was queued. "
|
|
1478
1479
|
"Start it again from Setup to see why")
|
|
1479
|
-
return
|
|
1480
|
+
return jobs, None
|
|
1481
|
+
|
|
1482
|
+
def make(self, eng):
|
|
1483
|
+
"""(RunScreen, None), or (None, why it can't start) when its turn comes."""
|
|
1484
|
+
jobs, problem = self.plan(eng)
|
|
1485
|
+
return (RunScreen(jobs, self.sel.get("parallel")), None) if jobs else (None, problem)
|
|
1480
1486
|
|
|
1481
1487
|
|
|
1482
1488
|
class QueuedTune:
|
|
@@ -1807,7 +1813,7 @@ class RunScreen(Screen):
|
|
|
1807
1813
|
Binding("c", "cancel_all", "Cancel all", priority=True),
|
|
1808
1814
|
Binding("f", "toggle_follow", "Pause scroll", priority=True),
|
|
1809
1815
|
Binding("v", "watch_next", "Next model", priority=True),
|
|
1810
|
-
Binding("n", "new_run", "
|
|
1816
|
+
Binding("n", "new_run", "More evals", priority=True),
|
|
1811
1817
|
Binding("r", "results", "Results", priority=True),
|
|
1812
1818
|
Binding("w", "runs", "Runs", priority=True),
|
|
1813
1819
|
Binding("escape", "back", "Setup", priority=True),
|
|
@@ -1815,7 +1821,7 @@ class RunScreen(Screen):
|
|
|
1815
1821
|
]
|
|
1816
1822
|
kind = "Run"
|
|
1817
1823
|
LIVE_ONLY = {"skip_model", "cancel_all", "toggle_follow"}
|
|
1818
|
-
DONE_ONLY =
|
|
1824
|
+
DONE_ONLY = set()
|
|
1819
1825
|
|
|
1820
1826
|
def __init__(self, jobs, parallel=None):
|
|
1821
1827
|
super().__init__()
|
|
@@ -2027,6 +2033,16 @@ class RunScreen(Screen):
|
|
|
2027
2033
|
if self.watched(kind, d):
|
|
2028
2034
|
self.req_started = None
|
|
2029
2035
|
self.update_overall()
|
|
2036
|
+
elif kind == "jobs_added":
|
|
2037
|
+
q = self.query_one("#queue", DataTable)
|
|
2038
|
+
for j in d["jobs"]:
|
|
2039
|
+
if j.key not in q.rows:
|
|
2040
|
+
q.add_row(j.label, j.pack.label, "", "", "", "", "", "", key=j.key)
|
|
2041
|
+
self.update_job(j)
|
|
2042
|
+
self.total += j.total
|
|
2043
|
+
self.query_one("#bar", ProgressBar).update(total=max(self.total, 1))
|
|
2044
|
+
self.event(f"added to this run: {', '.join(j.key for j in d['jobs'])}")
|
|
2045
|
+
self.update_overall()
|
|
2030
2046
|
elif kind in ("job_started", "job_update", "job_done"):
|
|
2031
2047
|
job = d["job"]
|
|
2032
2048
|
self.update_job(job)
|
|
@@ -2262,6 +2278,9 @@ class RunScreen(Screen):
|
|
|
2262
2278
|
|
|
2263
2279
|
@on(Button.Pressed, "#done-new")
|
|
2264
2280
|
def action_new_run(self):
|
|
2281
|
+
if self.running:
|
|
2282
|
+
self.notify("Pick models and packs, then Start: add them to this run or queue them after it.",
|
|
2283
|
+
timeout=8)
|
|
2265
2284
|
self.app.back_to_setup() # Setup still holds the last selection
|
|
2266
2285
|
self.app.screen.query_one("#models").focus()
|
|
2267
2286
|
|
|
@@ -2923,6 +2942,40 @@ class EvalsApp(App):
|
|
|
2923
2942
|
self.notify("Everything selected had results; running it again.")
|
|
2924
2943
|
self.add_session(screen)
|
|
2925
2944
|
return
|
|
2945
|
+
run = self.active_session
|
|
2946
|
+
if item.kind == "Run" and isinstance(run, RunScreen) and run.running \
|
|
2947
|
+
and item.sel["tier"] != run.jobs[0].tier:
|
|
2948
|
+
self.notify(f"The run going is {run.jobs[0].tier.title()} and this is {item.sel['tier'].title()}, "
|
|
2949
|
+
"so it can't join it.", timeout=10)
|
|
2950
|
+
elif item.kind == "Run" and isinstance(run, RunScreen) and run.running:
|
|
2951
|
+
def picked(choice):
|
|
2952
|
+
if choice == "add":
|
|
2953
|
+
self.add_to_run(run, item)
|
|
2954
|
+
elif choice == "queue":
|
|
2955
|
+
self.enqueue(item)
|
|
2956
|
+
what = f"{', '.join(item.sel['models'][:4])}{' …' if len(item.sel['models']) > 4 else ''} · " \
|
|
2957
|
+
f"{', '.join(item.sel['packs'][:4])}{' …' if len(item.sel['packs']) > 4 else ''}"
|
|
2958
|
+
self.push_screen(ChoiceScreen(
|
|
2959
|
+
f"A run is going: {run.live_text()}.\n\nAdd {what} to it? Its models run after the ones "
|
|
2960
|
+
"already in it.\nOr queue it as a separate run that starts when this one ends.",
|
|
2961
|
+
[("add", "Add to this run", "success"), ("queue", f"Queue it as #{len(self.queue) + 1}", "primary"),
|
|
2962
|
+
("keep", "Cancel", "default")]), picked)
|
|
2963
|
+
return
|
|
2964
|
+
self.enqueue(item)
|
|
2965
|
+
|
|
2966
|
+
def add_to_run(self, run, item):
|
|
2967
|
+
jobs, problem = item.plan(self.engine)
|
|
2968
|
+
added, why = self.engine.add_jobs(jobs) if jobs else ([], problem)
|
|
2969
|
+
if not added:
|
|
2970
|
+
self.notify(f"Not added: {why}. Queued it instead.", severity="warning", timeout=10)
|
|
2971
|
+
self.enqueue(item)
|
|
2972
|
+
return
|
|
2973
|
+
run.handle("jobs_added", {"jobs": added})
|
|
2974
|
+
models = sorted({j.label for j in added})
|
|
2975
|
+
self.notify(f"Added {len(added)} pack run(s) to the run going: {', '.join(models)}. w shows it.", timeout=10)
|
|
2976
|
+
self.refresh_live()
|
|
2977
|
+
|
|
2978
|
+
def enqueue(self, item):
|
|
2926
2979
|
ahead = self.busy_text()
|
|
2927
2980
|
self.queue.append(item)
|
|
2928
2981
|
self.notify(f"Queued #{len(self.queue)}: {item.title}. It starts by itself after: {ahead}. "
|
|
@@ -522,6 +522,160 @@ class Parallel(unittest.TestCase):
|
|
|
522
522
|
self.assertEqual(sum(r["pass"] for r in self.results("b")["results"]), 6)
|
|
523
523
|
|
|
524
524
|
|
|
525
|
+
class AddToRun(unittest.TestCase):
|
|
526
|
+
"""More evals while a run is going: added to it (they run after its other jobs) or queued after it."""
|
|
527
|
+
|
|
528
|
+
def setUp(self):
|
|
529
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
530
|
+
self.ws = os.path.join(self.tmp.name, "ws")
|
|
531
|
+
tuieval(self.tmp.name, "init", self.ws)
|
|
532
|
+
for name in ("apps", "other"):
|
|
533
|
+
d = os.path.join(self.ws, "packs", name)
|
|
534
|
+
os.makedirs(d)
|
|
535
|
+
write(os.path.join(d, "pack.toml"), f'label = "{name}"\n[certify]\nrepeat = 1\n[gate]\nmin_accuracy = 0.1\n')
|
|
536
|
+
write(os.path.join(d, "tests.yaml"), "".join(
|
|
537
|
+
f"- {{id: {t}, input: {name} question {t}, expected: 1, reference: 'ANSWER: 1', difficulty: easy}}\n"
|
|
538
|
+
for t in "abcd"))
|
|
539
|
+
mock = [sys.executable, os.path.join(HERE, "mock_server.py"), "--port", "{port}", "--packs",
|
|
540
|
+
os.path.join(self.ws, "packs"), "--model", "{served_name}", "--delay", "0.2"]
|
|
541
|
+
with open(os.path.join(self.ws, "models.toml"), "a") as f:
|
|
542
|
+
f.write(f"\n[servers.mk]\ncmd = {json.dumps(mock)}\nport = {free_port()}\n")
|
|
543
|
+
for label in "ab":
|
|
544
|
+
f.write(f'\n[[models]]\nlabel = "{label}"\nserver = "mk"\nmodel = "{label}"\n')
|
|
545
|
+
|
|
546
|
+
def tearDown(self):
|
|
547
|
+
self.tmp.cleanup()
|
|
548
|
+
|
|
549
|
+
def finish(self, e, t, limit=480):
|
|
550
|
+
"""Wait for a run thread; CI machines can take half a minute per server start. One that's
|
|
551
|
+
still going after `limit` is cancelled, so it can't run into the next test."""
|
|
552
|
+
t.join(limit)
|
|
553
|
+
if t.is_alive():
|
|
554
|
+
e.cancel()
|
|
555
|
+
t.join(120)
|
|
556
|
+
self.fail(f"the run didn't finish in {limit}s")
|
|
557
|
+
|
|
558
|
+
def test_engine_adds_jobs_to_the_run_going(self):
|
|
559
|
+
from tuieval import engine
|
|
560
|
+
events = []
|
|
561
|
+
e = engine.Engine(lambda kind, **d: events.append((kind, d)), root=self.ws)
|
|
562
|
+
jobs = e.plan(["a"], ["apps"], None, "certify", False, {})
|
|
563
|
+
self.assertEqual(e.add_jobs(e.plan(["b"], ["apps"], None, "certify", False, {})),
|
|
564
|
+
([], "the run is finishing")) # no run yet
|
|
565
|
+
t = threading.Thread(target=e.run, args=(jobs,))
|
|
566
|
+
t.start()
|
|
567
|
+
deadline = time.time() + 180 # CI machines can take long to start servers
|
|
568
|
+
while not any(k == "request_done" for k, _ in events) and time.time() < deadline:
|
|
569
|
+
time.sleep(0.05)
|
|
570
|
+
added, why = e.add_jobs(e.plan(["b", "a"], ["apps", "other"], None, "certify", False, {}))
|
|
571
|
+
self.assertIsNone(why)
|
|
572
|
+
self.assertEqual(sorted(j.key for j in added), ["a/other", "b/apps", "b/other"]) # a/apps is running
|
|
573
|
+
self.assertEqual(e.add_jobs(e.plan(["b"], ["apps"], None, "certify", False, {}))[1],
|
|
574
|
+
"everything picked is already in the run")
|
|
575
|
+
self.finish(e, t)
|
|
576
|
+
self.assertEqual([j.status for j in jobs], ["done"] * 4)
|
|
577
|
+
self.assertEqual(jobs[0].key, "a/apps")
|
|
578
|
+
started = [d["job"].key for k, d in events if k == "job_started"]
|
|
579
|
+
self.assertEqual(started[0], "a/apps")
|
|
580
|
+
self.assertEqual(e.add_jobs(e.plan(["b"], ["apps"], None, "certify", True, {}))[1], "the run is finishing")
|
|
581
|
+
queue_done = [d for k, d in events if k == "queue_done"]
|
|
582
|
+
self.assertEqual(len(queue_done[0]["jobs"]), 4)
|
|
583
|
+
|
|
584
|
+
def test_added_models_use_a_free_lane(self):
|
|
585
|
+
from tuieval import engine
|
|
586
|
+
path = os.path.join(self.ws, "models.toml") # slower answers: a is surely still answering when b is added
|
|
587
|
+
write(path, read(path).replace('"--delay", "0.2"', '"--delay", "1.0"'))
|
|
588
|
+
events = []
|
|
589
|
+
e = engine.Engine(lambda kind, **d: events.append((kind, d)), root=self.ws)
|
|
590
|
+
jobs = e.plan(["a"], ["apps"], None, "certify", False, {})
|
|
591
|
+
t = threading.Thread(target=e.run, args=(jobs, 2))
|
|
592
|
+
t.start()
|
|
593
|
+
deadline = time.time() + 180 # CI machines can take long to start servers
|
|
594
|
+
while not any(k == "request_done" for k, _ in events) and time.time() < deadline:
|
|
595
|
+
time.sleep(0.05)
|
|
596
|
+
added, why = e.add_jobs(e.plan(["a", "b"], ["other", "apps"], None, "certify", False, {}))
|
|
597
|
+
self.assertEqual(sorted(j.key for j in added), ["a/other", "b/apps", "b/other"])
|
|
598
|
+
self.finish(e, t)
|
|
599
|
+
self.assertEqual([j.status for j in jobs], ["done"] * 4)
|
|
600
|
+
order = [(k, d.get("label") or d["job"].key) for k, d in events if k in ("model_loading", "job_done")]
|
|
601
|
+
self.assertLess(order.index(("model_loading", "b")), order.index(("job_done", "a/apps")), order) # next to a
|
|
602
|
+
a_other = json.loads(read(os.path.join(self.ws, "results", "a", "other.json")))["results"]
|
|
603
|
+
self.assertFalse(any("a" in (r.get("ran_alongside") or []) for r in a_other))
|
|
604
|
+
loads = [d["label"] for k, d in events if k == "model_loading"]
|
|
605
|
+
self.assertEqual(loads.count("a"), 2) # a's new pack: after it ended
|
|
606
|
+
|
|
607
|
+
def test_tui_adds_or_queues_while_running(self):
|
|
608
|
+
code = textwrap.dedent("""
|
|
609
|
+
import asyncio, json, time
|
|
610
|
+
from tuieval.tui import EvalsApp, RunScreen, SetupScreen, ChoiceScreen
|
|
611
|
+
from textual.widgets import SelectionList
|
|
612
|
+
async def go():
|
|
613
|
+
app = EvalsApp({})
|
|
614
|
+
async with app.run_test(size=(160, 50)) as pilot:
|
|
615
|
+
await pilot.pause()
|
|
616
|
+
s = app.screen
|
|
617
|
+
s.query_one("#suites", SelectionList).select("apps")
|
|
618
|
+
s.selected_models = {"a"}
|
|
619
|
+
s.refresh_models()
|
|
620
|
+
s.query_one("#tier-certify").value = True
|
|
621
|
+
await pilot.pause()
|
|
622
|
+
s.action_start()
|
|
623
|
+
deadline = time.time() + 180 # CI machines can take long to start servers
|
|
624
|
+
while time.time() < deadline:
|
|
625
|
+
await pilot.pause(0.1)
|
|
626
|
+
if isinstance(app.screen, RunScreen) and app.screen.counts()[0]:
|
|
627
|
+
break
|
|
628
|
+
run = app.screen
|
|
629
|
+
await pilot.press("n") # more evals while it runs
|
|
630
|
+
await pilot.pause()
|
|
631
|
+
on_setup = isinstance(app.screen, SetupScreen)
|
|
632
|
+
app.screen.selected_models = {"b"}
|
|
633
|
+
app.screen.refresh_models()
|
|
634
|
+
app.screen.action_start()
|
|
635
|
+
await pilot.pause()
|
|
636
|
+
asked = isinstance(app.screen, ChoiceScreen)
|
|
637
|
+
app.screen.dismiss("add")
|
|
638
|
+
await pilot.pause()
|
|
639
|
+
app.screen.query_one("#suites", SelectionList).deselect("apps")
|
|
640
|
+
app.screen.query_one("#suites", SelectionList).select("other")
|
|
641
|
+
app.screen.selected_models = {"a"}
|
|
642
|
+
app.screen.refresh_models()
|
|
643
|
+
app.screen.action_start()
|
|
644
|
+
await pilot.pause()
|
|
645
|
+
app.screen.dismiss("queue")
|
|
646
|
+
await pilot.pause()
|
|
647
|
+
app.screen.query_one("#tier-smoke").value = True # another tier can't join: queued
|
|
648
|
+
await pilot.pause()
|
|
649
|
+
app.screen.action_start()
|
|
650
|
+
await pilot.pause()
|
|
651
|
+
other_tier = not isinstance(app.screen, ChoiceScreen)
|
|
652
|
+
queued = len(app.queue)
|
|
653
|
+
deadline = time.time() + 300
|
|
654
|
+
while time.time() < deadline:
|
|
655
|
+
await pilot.pause(0.2)
|
|
656
|
+
if not run.running and not app.queue and not app.active_session:
|
|
657
|
+
break
|
|
658
|
+
second = app.sessions[1]
|
|
659
|
+
print(json.dumps({"on_setup": on_setup, "asked": asked, "queued": queued, "other_tier": other_tier,
|
|
660
|
+
"run": [(j.key, j.status) for j in run.jobs], "total": run.total,
|
|
661
|
+
"sessions": len(app.sessions),
|
|
662
|
+
"second": [(j.key, j.status) for j in second.jobs]}))
|
|
663
|
+
asyncio.run(go())
|
|
664
|
+
""")
|
|
665
|
+
p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=600,
|
|
666
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""))
|
|
667
|
+
out = json.loads(p.stdout.strip().splitlines()[-1]) if p.stdout.strip() else p.stderr
|
|
668
|
+
self.assertIsInstance(out, dict, out)
|
|
669
|
+
self.assertTrue(out["on_setup"])
|
|
670
|
+
self.assertTrue(out["asked"])
|
|
671
|
+
self.assertTrue(out["other_tier"])
|
|
672
|
+
self.assertEqual(out["queued"], 2)
|
|
673
|
+
self.assertEqual(out["run"], [["a/apps", "done"], ["b/apps", "done"]]) # added to the run going
|
|
674
|
+
self.assertEqual(out["total"], 8)
|
|
675
|
+
self.assertEqual(out["sessions"], 3)
|
|
676
|
+
self.assertEqual(out["second"], [["a/other", "done"]]) # queued: its own run after
|
|
677
|
+
|
|
678
|
+
|
|
525
679
|
class FirstRun(unittest.TestCase):
|
|
526
680
|
"""A new user's first run fails fast with the fix, never hangs or shows a bare Python error."""
|
|
527
681
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev28 → tuieval-0.2.0.dev32}/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
|