tuieval 0.2.0.dev100__tar.gz → 0.2.0.dev103__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.dev100 → tuieval-0.2.0.dev103}/CHANGELOG.md +2 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/PKG-INFO +2 -2
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/README.md +1 -1
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/engine.py +32 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/tui.py +82 -10
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/tests/test_tuieval.py +135 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/.gitignore +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/LICENSE +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/models.md +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/compare.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/insights.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/logs.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/pta.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/run_evals.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/tests/mock_server.py +0 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Run screen: `x` removes the highlighted pack from the run going if it hasn't started (the reverse of adding it); a model left with nothing isn't started.
|
|
6
|
+
- `k` (skip model) asks first, naming the model. Yes/no dialogs start on No; `y` answers yes, `n` or Esc no.
|
|
5
7
|
- Fix: hosted models no longer count as running alongside local ones (they don't share the machine), so a one-model-at-a-time setup no longer gets the "ran alongside other models" note. Older results are read the same way.
|
|
6
8
|
- Results, Per question: the model summary shows every picked model without scrolling, and fits the window: shorter column names, and long model names cut in the middle (both ends kept). The PTA index table fits the same way.
|
|
7
9
|
- PTA index: the accuracy bar shows answers cut off at max_tokens in orange, with `(n% truncated)` beside it and a **truncated** column in the table, so overthinking stands apart from wrong answers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev103
|
|
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
|
|
@@ -143,7 +143,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
|
|
|
143
143
|
|
|
144
144
|
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.
|
|
145
145
|
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.
|
|
146
|
-
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.
|
|
146
|
+
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 (after a yes), `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. `x` takes a pack that hasn't started out of the run; `w` lists queued runs (`del` removes one).
|
|
147
147
|
4. **Press `r` for results:** the PTA index (parsimony (tokens), time and accuracy as bars), production readiness, per question, and failures; click a column name to sort. `tuieval compare`, `tuieval history` and `tuieval items` have the rest: whether a difference is real, verdict history and test quality.
|
|
148
148
|
|
|
149
149
|
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.
|
|
@@ -117,7 +117,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
|
|
|
117
117
|
|
|
118
118
|
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.
|
|
119
119
|
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.
|
|
120
|
-
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.
|
|
120
|
+
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 (after a yes), `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. `x` takes a pack that hasn't started out of the run; `w` lists queued runs (`del` removes one).
|
|
121
121
|
4. **Press `r` for results:** the PTA index (parsimony (tokens), time and accuracy as bars), production readiness, per question, and failures; click a column name to sort. `tuieval compare`, `tuieval history` and `tuieval items` have the rest: whether a difference is real, verdict history and test quality.
|
|
122
122
|
|
|
123
123
|
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.dev103'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev103')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -457,6 +457,7 @@ class Job:
|
|
|
457
457
|
earlier: list = dataclasses.field(default_factory=list) # earlier sittings (see Engine._sittings)
|
|
458
458
|
stop_early: bool = False # stop once the pack's FAIL is certain (verdict.fail_certain)
|
|
459
459
|
first: list = dataclasses.field(default_factory=list) # test ids round 1 asks first (most failed)
|
|
460
|
+
claimed: bool = False # its model's server took it on: it can no longer be removed from the run
|
|
460
461
|
|
|
461
462
|
@property
|
|
462
463
|
def label(self):
|
|
@@ -1728,6 +1729,33 @@ class Engine:
|
|
|
1728
1729
|
r["cond"].notify_all()
|
|
1729
1730
|
return added, None if added else "everything picked is already in the run"
|
|
1730
1731
|
|
|
1732
|
+
def removable(self, job):
|
|
1733
|
+
"""Why a job of the run going can't be removed (None: it can): only a pack not yet started."""
|
|
1734
|
+
if job.status in ("done", "failed", "skipped"):
|
|
1735
|
+
return "it has finished"
|
|
1736
|
+
if job.claimed or job.status == "running":
|
|
1737
|
+
return "it's running (k skips the model)"
|
|
1738
|
+
return None
|
|
1739
|
+
|
|
1740
|
+
def remove_jobs(self, keys):
|
|
1741
|
+
"""Take jobs not yet started (removable) out of the run going, the reverse of add_jobs: they never
|
|
1742
|
+
run, and a model left with none isn't started. Returns the jobs removed."""
|
|
1743
|
+
removed = []
|
|
1744
|
+
with self._run_lock:
|
|
1745
|
+
r = self._run
|
|
1746
|
+
if not r or not r["open"]:
|
|
1747
|
+
return removed
|
|
1748
|
+
for j in [j for j in r["jobs"] if j.key in keys and self.removable(j) is None]:
|
|
1749
|
+
j.status, j.note = "removed", "removed from the run"
|
|
1750
|
+
r["jobs"].remove(j)
|
|
1751
|
+
for _, group in r["pending"]:
|
|
1752
|
+
if j in group:
|
|
1753
|
+
group.remove(j)
|
|
1754
|
+
removed.append(j)
|
|
1755
|
+
r["pending"][:] = [g for g in r["pending"] if g[1]] # the lanes hold this very list
|
|
1756
|
+
r["cond"].notify_all()
|
|
1757
|
+
return removed
|
|
1758
|
+
|
|
1731
1759
|
def _run_lanes(self, parallel, parallel_hosted=4):
|
|
1732
1760
|
"""Run each model's jobs in its own lane: up to `parallel` local models at a time, in order (the
|
|
1733
1761
|
next one starts once a lane is free, nothing running stops it (runs_alone), and its memory fits
|
|
@@ -2021,6 +2049,10 @@ class Engine:
|
|
|
2021
2049
|
failed_in_row = 0
|
|
2022
2050
|
for j in jobs:
|
|
2023
2051
|
self._check()
|
|
2052
|
+
with self._run_lock:
|
|
2053
|
+
if j.status == "removed": # taken out of the run while its model ran another pack
|
|
2054
|
+
continue
|
|
2055
|
+
j.claimed = True
|
|
2024
2056
|
try:
|
|
2025
2057
|
self._run_job(j, base_url, run_info, alive=info["alive"])
|
|
2026
2058
|
except BaseException as ex:
|
|
@@ -1828,16 +1828,33 @@ class RetestScreen(ModalScreen):
|
|
|
1828
1828
|
|
|
1829
1829
|
|
|
1830
1830
|
class ConfirmScreen(ModalScreen):
|
|
1831
|
-
|
|
1831
|
+
"""Yes or no; No has the focus, so Enter by reflex changes nothing. y: yes; n or Esc: no
|
|
1832
|
+
(and no_keys, e.g. the key that opened it, pressed again)."""
|
|
1833
|
+
BINDINGS = [Binding("y", "answer(True)", "Yes", show=False), Binding("n", "answer(False)", "No", show=False),
|
|
1834
|
+
Binding("escape", "answer(False)", "No", show=False)]
|
|
1835
|
+
|
|
1836
|
+
def __init__(self, question, no_keys=()):
|
|
1832
1837
|
super().__init__()
|
|
1833
1838
|
self.question = question
|
|
1839
|
+
self.no_keys = no_keys
|
|
1834
1840
|
|
|
1835
1841
|
def compose(self) -> ComposeResult:
|
|
1836
1842
|
with Vertical(id="dialog"):
|
|
1837
1843
|
yield Label(self.question)
|
|
1838
1844
|
with Horizontal(classes="dialog-buttons"):
|
|
1839
|
-
yield Button("Yes", id="yes", variant="error")
|
|
1840
|
-
yield Button("No", id="no")
|
|
1845
|
+
yield Button("Yes (y)", id="yes", variant="error")
|
|
1846
|
+
yield Button("No (n)", id="no")
|
|
1847
|
+
|
|
1848
|
+
def on_mount(self):
|
|
1849
|
+
self.query_one("#no", Button).focus()
|
|
1850
|
+
|
|
1851
|
+
def on_key(self, event):
|
|
1852
|
+
if event.key in self.no_keys:
|
|
1853
|
+
event.stop()
|
|
1854
|
+
self.dismiss(False)
|
|
1855
|
+
|
|
1856
|
+
def action_answer(self, yes):
|
|
1857
|
+
self.dismiss(yes)
|
|
1841
1858
|
|
|
1842
1859
|
@on(Button.Pressed)
|
|
1843
1860
|
def answer(self, event):
|
|
@@ -2114,6 +2131,7 @@ class RunScreen(Screen):
|
|
|
2114
2131
|
# priority: these work whichever pane has focus (the text panes would otherwise swallow keys)
|
|
2115
2132
|
BINDINGS = [
|
|
2116
2133
|
Binding("k", "skip_model", "Skip model", priority=True),
|
|
2134
|
+
Binding("x", "remove_job", "Remove from run", priority=True),
|
|
2117
2135
|
Binding("c", "cancel_all", "Cancel all", priority=True),
|
|
2118
2136
|
Binding("f", "toggle_follow", "Pause scroll", priority=True),
|
|
2119
2137
|
Binding("v", "watch_next", "Next model", priority=True),
|
|
@@ -2124,7 +2142,7 @@ class RunScreen(Screen):
|
|
|
2124
2142
|
Binding("q", "back", "Back", show=False, priority=True),
|
|
2125
2143
|
]
|
|
2126
2144
|
kind = "Run"
|
|
2127
|
-
LIVE_ONLY = {"skip_model", "cancel_all", "toggle_follow"}
|
|
2145
|
+
LIVE_ONLY = {"skip_model", "remove_job", "cancel_all", "toggle_follow"}
|
|
2128
2146
|
DONE_ONLY = set()
|
|
2129
2147
|
|
|
2130
2148
|
def __init__(self, jobs, parallel=None):
|
|
@@ -2180,6 +2198,8 @@ class RunScreen(Screen):
|
|
|
2180
2198
|
def check_action(self, action, parameters):
|
|
2181
2199
|
if action == "watch_next":
|
|
2182
2200
|
return self.running and self.side_by_side
|
|
2201
|
+
if action == "remove_job": # only while something is left to remove
|
|
2202
|
+
return self.running and any(self.app.engine.removable(j) is None for j in self.jobs)
|
|
2183
2203
|
if action in self.LIVE_ONLY:
|
|
2184
2204
|
return self.running
|
|
2185
2205
|
if action in self.DONE_ONLY:
|
|
@@ -2219,7 +2239,7 @@ class RunScreen(Screen):
|
|
|
2219
2239
|
with Vertical(id="top"):
|
|
2220
2240
|
yield Static(id="overall")
|
|
2221
2241
|
yield ProgressBar(id="bar", show_eta=False)
|
|
2222
|
-
yield DataTable(id="queue", cursor_type="
|
|
2242
|
+
yield DataTable(id="queue", cursor_type="row", zebra_stripes=True)
|
|
2223
2243
|
yield Static(id="current")
|
|
2224
2244
|
with Horizontal(id="done-bar"):
|
|
2225
2245
|
yield Button("New run [n]", id="done-new", variant="success")
|
|
@@ -2365,10 +2385,12 @@ class RunScreen(Screen):
|
|
|
2365
2385
|
self.total += j.total
|
|
2366
2386
|
self.query_one("#bar", ProgressBar).update(total=max(self.total, 1))
|
|
2367
2387
|
self.event(f"added to this run: {', '.join(j.key for j in d['jobs'])}")
|
|
2388
|
+
self.refresh_bindings()
|
|
2368
2389
|
self.update_overall()
|
|
2369
2390
|
elif kind in ("job_started", "job_update", "job_done"):
|
|
2370
2391
|
job = d["job"]
|
|
2371
2392
|
self.update_job(job)
|
|
2393
|
+
self.refresh_bindings() # x shows while a pack can still be removed
|
|
2372
2394
|
if kind == "job_started" and d.get("resumed"):
|
|
2373
2395
|
earlier = (f" ({fmt_secs(d['earlier_s'])} over {d['sittings']} earlier sitting(s))"
|
|
2374
2396
|
if d.get("sittings") else "")
|
|
@@ -2611,13 +2633,63 @@ class RunScreen(Screen):
|
|
|
2611
2633
|
|
|
2612
2634
|
# -- actions
|
|
2613
2635
|
def action_skip_model(self):
|
|
2614
|
-
if self.running:
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2636
|
+
if not self.running:
|
|
2637
|
+
return
|
|
2638
|
+
one = self.watch if self.side_by_side and self.watch else None
|
|
2639
|
+
names = [one] if one else self.active()
|
|
2640
|
+
if not names:
|
|
2641
|
+
return
|
|
2642
|
+
|
|
2643
|
+
def skip(yes):
|
|
2644
|
+
if not yes or not self.running:
|
|
2645
|
+
return
|
|
2646
|
+
if one:
|
|
2647
|
+
self.app.engine.skip_model(one)
|
|
2648
|
+
self.notify(f"Skipping {one}; the others keep going…")
|
|
2618
2649
|
else:
|
|
2619
2650
|
self.app.engine.skip_model()
|
|
2620
2651
|
self.notify("Skipping the current model…")
|
|
2652
|
+
self.app.push_screen(ConfirmScreen(
|
|
2653
|
+
f"Skip {', '.join(names)}? Answers so far are kept (a later run picks up from them); the rest "
|
|
2654
|
+
f"of {'its' if len(names) == 1 else 'their'} questions in this run are skipped.", no_keys=("k",)), skip)
|
|
2655
|
+
|
|
2656
|
+
def action_remove_job(self):
|
|
2657
|
+
"""Take the highlighted pack out of the run if it hasn't started (the reverse of adding it)."""
|
|
2658
|
+
if not self.running:
|
|
2659
|
+
return
|
|
2660
|
+
q = self.query_one("#queue", DataTable)
|
|
2661
|
+
try:
|
|
2662
|
+
key = q.coordinate_to_cell_key(q.cursor_coordinate).row_key.value
|
|
2663
|
+
except Exception:
|
|
2664
|
+
key = None
|
|
2665
|
+
job = next((j for j in self.jobs if j.key == key), None)
|
|
2666
|
+
if job is None:
|
|
2667
|
+
self.notify("Highlight a pack in the queue (click it or use ↑/↓ there), then press x.")
|
|
2668
|
+
return
|
|
2669
|
+
why = self.app.engine.removable(job)
|
|
2670
|
+
if why:
|
|
2671
|
+
self.notify(f"{job.label} · {job.pack.label} can't be removed: {why}.")
|
|
2672
|
+
return
|
|
2673
|
+
|
|
2674
|
+
def remove(yes):
|
|
2675
|
+
if not yes:
|
|
2676
|
+
return
|
|
2677
|
+
removed = self.app.engine.remove_jobs({job.key})
|
|
2678
|
+
if not removed:
|
|
2679
|
+
self.notify(f"{job.label} · {job.pack.label} started meanwhile, so it stays (k skips the model).")
|
|
2680
|
+
return
|
|
2681
|
+
for j in removed:
|
|
2682
|
+
q.remove_row(j.key)
|
|
2683
|
+
self.total -= j.total
|
|
2684
|
+
self.query_one("#bar", ProgressBar).update(total=max(self.total, 1))
|
|
2685
|
+
self.event(f"removed from this run: {', '.join(j.key for j in removed)}")
|
|
2686
|
+
self.update_overall()
|
|
2687
|
+
self.refresh_bindings()
|
|
2688
|
+
self.app.refresh_live()
|
|
2689
|
+
self.notify(f"Removed from the run: {job.label} · {job.pack.label}.")
|
|
2690
|
+
self.app.push_screen(ConfirmScreen(
|
|
2691
|
+
f"Remove {job.label} · {job.pack.label} from this run? It hasn't started, so nothing is lost; "
|
|
2692
|
+
"Start can add it again later.", no_keys=("x",)), remove)
|
|
2621
2693
|
|
|
2622
2694
|
def action_watch_next(self):
|
|
2623
2695
|
"""Stream the next model running alongside (from its next answer)."""
|
|
@@ -3489,7 +3561,7 @@ class EvalsApp(App):
|
|
|
3489
3561
|
return
|
|
3490
3562
|
run.handle("jobs_added", {"jobs": added})
|
|
3491
3563
|
models = sorted({j.label for j in added})
|
|
3492
|
-
self.notify(f"Added {len(added)} pack run(s) to the run going: {', '.join(models)}.
|
|
3564
|
+
self.notify(f"Added {len(added)} pack run(s) to the run going: {', '.join(models)}. x removes a pack not started yet.", timeout=10)
|
|
3493
3565
|
self.refresh_live()
|
|
3494
3566
|
|
|
3495
3567
|
def enqueue(self, item):
|
|
@@ -1030,6 +1030,141 @@ class AddToRun(unittest.TestCase):
|
|
|
1030
1030
|
loads = [d["label"] for k, d in events if k == "model_loading"]
|
|
1031
1031
|
self.assertEqual(loads.count("a"), 2) # a's new pack: after it ended
|
|
1032
1032
|
|
|
1033
|
+
def test_engine_removes_jobs_not_started(self):
|
|
1034
|
+
from tuieval import engine
|
|
1035
|
+
events = []
|
|
1036
|
+
e = engine.Engine(lambda kind, **d: events.append((kind, d)), root=self.ws)
|
|
1037
|
+
jobs = e.plan(["a"], ["apps", "other"], None, "certify", False, {})
|
|
1038
|
+
t = threading.Thread(target=e.run, args=(jobs, 1))
|
|
1039
|
+
t.start()
|
|
1040
|
+
deadline = time.time() + 180 # CI machines can take long to start servers
|
|
1041
|
+
while not any(k == "request_done" for k, _ in events) and time.time() < deadline:
|
|
1042
|
+
time.sleep(0.05)
|
|
1043
|
+
added, _ = e.add_jobs(e.plan(["b"], ["apps"], None, "certify", False, {}))
|
|
1044
|
+
self.assertEqual([j.key for j in added], ["b/apps"])
|
|
1045
|
+
self.assertEqual(e.remove_jobs({"a/apps"}), []) # answering: k skips it instead
|
|
1046
|
+
# a's next pack (its server is up) and b (never started) both go
|
|
1047
|
+
self.assertEqual(sorted(j.key for j in e.remove_jobs({"a/other", "b/apps"})), ["a/other", "b/apps"])
|
|
1048
|
+
self.finish(e, t)
|
|
1049
|
+
self.assertEqual([(j.key, j.status) for j in jobs], [("a/apps", "done")])
|
|
1050
|
+
self.assertNotIn(("model_loading", "b"), [(k, d.get("label")) for k, d in events])
|
|
1051
|
+
self.assertFalse(os.path.exists(os.path.join(self.ws, "results", "a", "other.json")))
|
|
1052
|
+
self.assertEqual(e.remove_jobs({"a/apps"}), []) # the run is over
|
|
1053
|
+
|
|
1054
|
+
def test_tui_removes_a_pack_from_the_run(self):
|
|
1055
|
+
path = os.path.join(self.ws, "models.toml")
|
|
1056
|
+
write(path, read(path).replace('"--delay", "0.2"', '"--delay", "3"')) # still running while it's asked
|
|
1057
|
+
code = textwrap.dedent("""
|
|
1058
|
+
import asyncio, json, time
|
|
1059
|
+
from tuieval.tui import EvalsApp, RunScreen, ConfirmScreen
|
|
1060
|
+
from textual.widgets import SelectionList, DataTable
|
|
1061
|
+
async def go():
|
|
1062
|
+
app = EvalsApp({})
|
|
1063
|
+
async with app.run_test(size=(160, 50)) as pilot:
|
|
1064
|
+
await pilot.pause()
|
|
1065
|
+
s = app.screen
|
|
1066
|
+
s.query_one("#suites", SelectionList).select("apps")
|
|
1067
|
+
s.query_one("#suites", SelectionList).select("other")
|
|
1068
|
+
s.selected_models = {"a", "b"}
|
|
1069
|
+
s.refresh_models()
|
|
1070
|
+
s.query_one("#tier-certify").value = True
|
|
1071
|
+
await pilot.pause()
|
|
1072
|
+
s.action_start()
|
|
1073
|
+
deadline = time.time() + 180 # CI machines can take long to start servers
|
|
1074
|
+
while time.time() < deadline:
|
|
1075
|
+
await pilot.pause(0.1)
|
|
1076
|
+
if isinstance(app.screen, RunScreen) and app.screen.counts()[0]:
|
|
1077
|
+
break
|
|
1078
|
+
run, out = app.screen, {}
|
|
1079
|
+
q = run.query_one("#queue", DataTable)
|
|
1080
|
+
total = run.total
|
|
1081
|
+
q.move_cursor(row=q.get_row_index("b/apps"))
|
|
1082
|
+
await pilot.press("x")
|
|
1083
|
+
await pilot.pause()
|
|
1084
|
+
out["asked"] = isinstance(app.screen, ConfirmScreen) and app.screen.focused.id == "no"
|
|
1085
|
+
await pilot.press("n")
|
|
1086
|
+
await pilot.pause()
|
|
1087
|
+
out["kept"] = [j.key for j in run.jobs]
|
|
1088
|
+
q.move_cursor(row=q.get_row_index("b/apps"))
|
|
1089
|
+
await pilot.press("x")
|
|
1090
|
+
await pilot.pause()
|
|
1091
|
+
await pilot.press("y")
|
|
1092
|
+
await pilot.pause()
|
|
1093
|
+
out["after"] = [[j.key for j in run.jobs], list(map(str, (k.value for k in q.rows))),
|
|
1094
|
+
total - run.total]
|
|
1095
|
+
q.move_cursor(row=q.get_row_index("a/apps")) # running: stays, nothing asked
|
|
1096
|
+
await pilot.press("x")
|
|
1097
|
+
await pilot.pause()
|
|
1098
|
+
out["running"] = [isinstance(app.screen, ConfirmScreen), "a/apps" in [j.key for j in run.jobs]]
|
|
1099
|
+
app.engine.cancel()
|
|
1100
|
+
deadline = time.time() + 60
|
|
1101
|
+
while time.time() < deadline and run.running:
|
|
1102
|
+
await pilot.pause(0.2)
|
|
1103
|
+
print(json.dumps(out))
|
|
1104
|
+
asyncio.run(go())
|
|
1105
|
+
""")
|
|
1106
|
+
p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=600,
|
|
1107
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""))
|
|
1108
|
+
out = json.loads(p.stdout.strip().splitlines()[-1]) if p.stdout.strip() else p.stderr
|
|
1109
|
+
self.assertIsInstance(out, dict, out)
|
|
1110
|
+
self.assertTrue(out["asked"])
|
|
1111
|
+
self.assertIn("b/apps", out["kept"])
|
|
1112
|
+
jobs, rows, fewer = out["after"]
|
|
1113
|
+
self.assertNotIn("b/apps", jobs)
|
|
1114
|
+
self.assertNotIn("b/apps", rows)
|
|
1115
|
+
self.assertEqual(fewer, 4) # its 4 questions are no longer counted
|
|
1116
|
+
self.assertEqual(out["running"], [False, True])
|
|
1117
|
+
|
|
1118
|
+
def test_tui_asks_before_skipping_a_model(self):
|
|
1119
|
+
path = os.path.join(self.ws, "models.toml")
|
|
1120
|
+
write(path, read(path).replace('"--delay", "0.2"', '"--delay", "3"')) # still running while it's asked
|
|
1121
|
+
code = textwrap.dedent("""
|
|
1122
|
+
import asyncio, json, time
|
|
1123
|
+
from tuieval.tui import EvalsApp, RunScreen, ConfirmScreen
|
|
1124
|
+
from textual.widgets import SelectionList
|
|
1125
|
+
async def go():
|
|
1126
|
+
app = EvalsApp({})
|
|
1127
|
+
async with app.run_test(size=(160, 50)) as pilot:
|
|
1128
|
+
await pilot.pause()
|
|
1129
|
+
s = app.screen
|
|
1130
|
+
s.query_one("#suites", SelectionList).select("apps")
|
|
1131
|
+
s.selected_models = {"a"}
|
|
1132
|
+
s.refresh_models()
|
|
1133
|
+
s.query_one("#tier-certify").value = True
|
|
1134
|
+
await pilot.pause()
|
|
1135
|
+
s.action_start()
|
|
1136
|
+
deadline = time.time() + 180 # CI machines can take long to start servers
|
|
1137
|
+
while time.time() < deadline:
|
|
1138
|
+
await pilot.pause(0.1)
|
|
1139
|
+
if isinstance(app.screen, RunScreen) and app.screen.counts()[0]:
|
|
1140
|
+
break
|
|
1141
|
+
run, out = app.screen, {}
|
|
1142
|
+
for keys in (["k", "enter"], ["k", "k"], ["k", "n"], ["k", "escape"]): # each one: no
|
|
1143
|
+
await pilot.press(keys[0])
|
|
1144
|
+
await pilot.pause()
|
|
1145
|
+
out.setdefault("asked", []).append(isinstance(app.screen, ConfirmScreen)
|
|
1146
|
+
and app.screen.focused.id == "no")
|
|
1147
|
+
await pilot.press(keys[1])
|
|
1148
|
+
await pilot.pause()
|
|
1149
|
+
out["kept"] = [run.running, app.screen is run, [j.status for j in run.jobs]]
|
|
1150
|
+
await pilot.press("k")
|
|
1151
|
+
await pilot.pause()
|
|
1152
|
+
await pilot.press("y")
|
|
1153
|
+
deadline = time.time() + 120
|
|
1154
|
+
while time.time() < deadline and run.running:
|
|
1155
|
+
await pilot.pause(0.2)
|
|
1156
|
+
out["skipped"] = [j.status for j in run.jobs]
|
|
1157
|
+
print(json.dumps(out))
|
|
1158
|
+
asyncio.run(go())
|
|
1159
|
+
""")
|
|
1160
|
+
p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=600,
|
|
1161
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""))
|
|
1162
|
+
out = json.loads(p.stdout.strip().splitlines()[-1]) if p.stdout.strip() else p.stderr
|
|
1163
|
+
self.assertIsInstance(out, dict, out)
|
|
1164
|
+
self.assertEqual(out["asked"], [True] * 4)
|
|
1165
|
+
self.assertEqual(out["kept"], [True, True, ["running"]])
|
|
1166
|
+
self.assertEqual(out["skipped"], ["skipped"])
|
|
1167
|
+
|
|
1033
1168
|
def test_tui_adds_or_queues_while_running(self):
|
|
1034
1169
|
code = textwrap.dedent("""
|
|
1035
1170
|
import asyncio, json, time
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/pack.toml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev100 → tuieval-0.2.0.dev103}/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
|