tuieval 0.2.0.dev68__tar.gz → 0.2.0.dev72__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.dev68 → tuieval-0.2.0.dev72}/CHANGELOG.md +2 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/PKG-INFO +3 -1
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/README.md +2 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/cli.py +4 -0
- tuieval-0.2.0.dev72/src/tuieval/logs.py +220 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/run_evals.py +3 -1
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/tui.py +5 -2
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/tests/test_tuieval.py +86 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/.gitignore +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/LICENSE +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/models.md +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/compare.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/engine.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/pta.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/tests/mock_server.py +0 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Retest: the setup checkbox "Rerun existing results" is now "Retest (ask again, even if already answered)", and `tuieval run --retest` is the same (`--force` still works).
|
|
6
|
+
- `tuieval logs` follows the server log of the model running now and moves on with the run; `logs live` shows each answer as it finishes, `--list` every log.
|
|
5
7
|
- Results tables sort by a click on a column name; a second click reverses. Numbers, times and levels sort by value; empty cells stay last.
|
|
6
8
|
- Nvidia and AMD GPUs on Linux: VRAM from `nvidia-smi`, `amd-smi` or `rocm-smi` sizes models (`gpu_memory_gb` overrides it), runs record peak VRAM, and `tuieval doctor` shows the GPU.
|
|
7
9
|
- Results has four tabs: PTA index, Production readiness, Per question and Failures. The PTA table adds tok/s, TTFT and memory; Per question can show only where models disagree; the settings warning is one line. Scorecard, pairwise, history and test quality stay in `tuieval compare`, `history` and `items`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev72
|
|
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
|
|
@@ -162,6 +162,8 @@ tuieval history # every verdict change over time
|
|
|
162
162
|
tuieval compare --speed --pairwise --failures # scorecards
|
|
163
163
|
tuieval selftest # check every test's reference and wrong answers
|
|
164
164
|
tuieval items # tests that don't separate models or look broken
|
|
165
|
+
tuieval logs # follow the server log of the model running now
|
|
166
|
+
tuieval logs live # each answer as it finishes (--list: every log)
|
|
165
167
|
tuieval capture logs/live/<file> --pack X # turn a real failure into a new test
|
|
166
168
|
tuieval regrade # re-score stored answers after changing a grader
|
|
167
169
|
tuieval regrade --only <model> --packs X # just one model's results (and packs)
|
|
@@ -136,6 +136,8 @@ tuieval history # every verdict change over time
|
|
|
136
136
|
tuieval compare --speed --pairwise --failures # scorecards
|
|
137
137
|
tuieval selftest # check every test's reference and wrong answers
|
|
138
138
|
tuieval items # tests that don't separate models or look broken
|
|
139
|
+
tuieval logs # follow the server log of the model running now
|
|
140
|
+
tuieval logs live # each answer as it finishes (--list: every log)
|
|
139
141
|
tuieval capture logs/live/<file> --pack X # turn a real failure into a new test
|
|
140
142
|
tuieval regrade # re-score stored answers after changing a grader
|
|
141
143
|
tuieval regrade --only <model> --packs X # just one model's results (and packs)
|
|
@@ -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.dev72'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev72')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -18,6 +18,7 @@ Day to day
|
|
|
18
18
|
tuieval report the verdicts with their evidence, as a markdown file
|
|
19
19
|
tuieval history every verdict change, newest first
|
|
20
20
|
tuieval items tests that don't separate models or look broken
|
|
21
|
+
tuieval logs follow the server log of the model running now (live: its answers)
|
|
21
22
|
tuieval capture LOG --pack turn a real failure (logs/live/*.txt) into a new test
|
|
22
23
|
tuieval regrade re-score stored answers after changing a grader (--only, --packs)
|
|
23
24
|
tuieval list the models tuieval knows, hidden ones separately (--packs: the packs)
|
|
@@ -85,6 +86,9 @@ def main(argv=None):
|
|
|
85
86
|
if cmd == "watch":
|
|
86
87
|
from . import watch_proxy
|
|
87
88
|
return watch_proxy.main(rest)
|
|
89
|
+
if cmd == "logs":
|
|
90
|
+
from . import logs
|
|
91
|
+
return logs.main(rest)
|
|
88
92
|
from . import run_evals
|
|
89
93
|
if cmd in run_evals.COMMANDS:
|
|
90
94
|
return run_evals.COMMANDS[cmd](rest)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""tuieval logs: follow the server log of the model running now, without naming it.
|
|
2
|
+
|
|
3
|
+
tuieval logs the newest server log, then every log that grows (a header says
|
|
4
|
+
which model each part is from), so it moves on with the run
|
|
5
|
+
tuieval logs live each answer (reasoning and answer, from logs/live/) as it finishes
|
|
6
|
+
tuieval logs --list every model's server log, newest first
|
|
7
|
+
tuieval logs MODEL one model's log (any unambiguous part of its label)
|
|
8
|
+
|
|
9
|
+
Read-only: it never touches servers or results. Only servers tuieval starts write a log here;
|
|
10
|
+
OpenRouter and servers that were already running don't.
|
|
11
|
+
"""
|
|
12
|
+
import argparse
|
|
13
|
+
import datetime
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
from . import workspace
|
|
20
|
+
|
|
21
|
+
POLL_S = 0.5
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def server_dir():
|
|
25
|
+
return workspace.path("logs", "server")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def live_dir():
|
|
29
|
+
return workspace.path("logs", "live")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _files(d, ext):
|
|
33
|
+
"""{path: (size, mtime)} for the files in d with this extension (none if d doesn't exist)."""
|
|
34
|
+
out = {}
|
|
35
|
+
try:
|
|
36
|
+
names = os.listdir(d)
|
|
37
|
+
except OSError:
|
|
38
|
+
return out
|
|
39
|
+
for n in names:
|
|
40
|
+
if n.endswith(ext):
|
|
41
|
+
p = os.path.join(d, n)
|
|
42
|
+
try:
|
|
43
|
+
st = os.stat(p)
|
|
44
|
+
except OSError:
|
|
45
|
+
continue
|
|
46
|
+
out[p] = (st.st_size, st.st_mtime)
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _label(path):
|
|
51
|
+
return os.path.basename(path)[:-len(".log")]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _tail_offset(path, lines):
|
|
55
|
+
"""Where the last `lines` lines of a file start."""
|
|
56
|
+
with open(path, "rb") as f:
|
|
57
|
+
f.seek(0, os.SEEK_END)
|
|
58
|
+
end = pos = f.tell()
|
|
59
|
+
found, chunk = 0, 8192
|
|
60
|
+
while pos > 0 and found <= lines:
|
|
61
|
+
pos = max(0, pos - chunk)
|
|
62
|
+
f.seek(pos)
|
|
63
|
+
found = f.read(end - pos).count(b"\n")
|
|
64
|
+
if pos == 0 and found <= lines:
|
|
65
|
+
return 0
|
|
66
|
+
f.seek(pos)
|
|
67
|
+
data = f.read(end - pos)
|
|
68
|
+
cut = len(data)
|
|
69
|
+
for _ in range(lines + 1):
|
|
70
|
+
cut = data.rfind(b"\n", 0, cut)
|
|
71
|
+
return pos + cut + 1
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Follower:
|
|
75
|
+
"""Follows server logs. `poll()` returns the text written since the last poll, with a
|
|
76
|
+
`── label ──` header whenever it comes from another log than the text before it.
|
|
77
|
+
only: follow just this log. Otherwise every log in the folder, including new ones."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, folder, lines=40, only=None):
|
|
80
|
+
self.folder, self.only = folder, only
|
|
81
|
+
self.offsets = {p: size for p, (size, _) in _files(folder, ".log").items()}
|
|
82
|
+
self.last = None # the log the previous text came from
|
|
83
|
+
self.held = {} # path -> size at the last poll, for a line still being written
|
|
84
|
+
start = only or max(self.offsets, key=lambda p: os.path.getmtime(p), default=None)
|
|
85
|
+
if start and start in self.offsets:
|
|
86
|
+
self.offsets[start] = _tail_offset(start, lines)
|
|
87
|
+
|
|
88
|
+
def poll(self):
|
|
89
|
+
out = []
|
|
90
|
+
files = _files(self.folder, ".log")
|
|
91
|
+
if self.only:
|
|
92
|
+
files = {p: v for p, v in files.items() if p == self.only}
|
|
93
|
+
for path, (size, _) in sorted(files.items(), key=lambda kv: kv[1][1]): # oldest change first
|
|
94
|
+
off = self.offsets.setdefault(path, 0)
|
|
95
|
+
if size < off: # truncated or replaced: read it from the start
|
|
96
|
+
off = 0
|
|
97
|
+
if size == off:
|
|
98
|
+
continue
|
|
99
|
+
with open(path, "rb") as f:
|
|
100
|
+
f.seek(off)
|
|
101
|
+
data = f.read(size - off)
|
|
102
|
+
cut = data.rfind(b"\n") + 1
|
|
103
|
+
if not cut: # no complete line yet: wait one poll for the rest of it
|
|
104
|
+
if self.held.get(path) != size:
|
|
105
|
+
self.held[path] = size
|
|
106
|
+
continue
|
|
107
|
+
cut = len(data)
|
|
108
|
+
self.held.pop(path, None)
|
|
109
|
+
self.offsets[path] = off + cut
|
|
110
|
+
if path != self.last:
|
|
111
|
+
out.append(f"── {_label(path)} ──\n")
|
|
112
|
+
self.last = path
|
|
113
|
+
out.append(data[:cut].decode("utf-8", "replace"))
|
|
114
|
+
return "".join(out)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
FOOTER = re.compile(r"^=== .*finish=.* ===$", re.M)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class LiveFollower:
|
|
121
|
+
"""New answers in logs/live/ as they're written: each one from its reasoning on (the prompt is
|
|
122
|
+
in the file), under a header with the file name."""
|
|
123
|
+
|
|
124
|
+
def __init__(self, folder, last=1):
|
|
125
|
+
self.folder = folder
|
|
126
|
+
files = sorted(_files(folder, ".txt").items(), key=lambda kv: kv[1][1])
|
|
127
|
+
self.seen = {p for p, _ in files[:max(0, len(files) - last)]}
|
|
128
|
+
|
|
129
|
+
def poll(self):
|
|
130
|
+
out = []
|
|
131
|
+
for path, (_, mtime) in sorted(_files(self.folder, ".txt").items(), key=lambda kv: kv[1][1]):
|
|
132
|
+
if path in self.seen:
|
|
133
|
+
continue
|
|
134
|
+
try:
|
|
135
|
+
with open(path, errors="replace") as f:
|
|
136
|
+
text = f.read()
|
|
137
|
+
except OSError:
|
|
138
|
+
continue
|
|
139
|
+
if not FOOTER.search(text) and time.time() - mtime < 2: # still being written
|
|
140
|
+
continue
|
|
141
|
+
self.seen.add(path)
|
|
142
|
+
i = text.find("=== REASONING ===")
|
|
143
|
+
out.append(f"── {os.path.basename(path)[:-4]} ──\n{text[i:] if i >= 0 else text}\n")
|
|
144
|
+
return "".join(out)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _ago(mtime):
|
|
148
|
+
s = max(0, int(time.time() - mtime))
|
|
149
|
+
if s < 60:
|
|
150
|
+
return f"{s}s ago"
|
|
151
|
+
if s < 3600:
|
|
152
|
+
return f"{s // 60} min ago"
|
|
153
|
+
if s < 86400:
|
|
154
|
+
return f"{s // 3600} h ago"
|
|
155
|
+
return datetime.datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _size(n):
|
|
159
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
160
|
+
if n < 1024 or unit == "GB":
|
|
161
|
+
return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}"
|
|
162
|
+
n /= 1024
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def list_logs(folder):
|
|
166
|
+
files = sorted(_files(folder, ".log").items(), key=lambda kv: -kv[1][1])
|
|
167
|
+
if not files:
|
|
168
|
+
return f"No server logs yet in {os.path.relpath(folder)}/."
|
|
169
|
+
w = max(len(_label(p)) for p, _ in files)
|
|
170
|
+
return "\n".join(f"{_label(p):<{w}} {_size(size):>9} {_ago(mtime)}" for p, (size, mtime) in files)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def find_log(folder, name):
|
|
174
|
+
"""The log whose label is `name`, or the only one containing it. Raises SystemExit otherwise."""
|
|
175
|
+
labels = {_label(p): p for p in _files(folder, ".log")}
|
|
176
|
+
if name in labels:
|
|
177
|
+
return labels[name]
|
|
178
|
+
hits = sorted(l for l in labels if name.lower() in l.lower())
|
|
179
|
+
if len(hits) == 1:
|
|
180
|
+
return labels[hits[0]]
|
|
181
|
+
if not hits:
|
|
182
|
+
sys.exit(f"No server log matches {name!r}; tuieval logs --list shows them.")
|
|
183
|
+
sys.exit(f"{name!r} matches {len(hits)} logs; say which:\n " + "\n ".join(hits))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def follow(follower, waiting):
|
|
187
|
+
"""Print what the follower finds until ctrl-c."""
|
|
188
|
+
said = False
|
|
189
|
+
try:
|
|
190
|
+
while True:
|
|
191
|
+
text = follower.poll()
|
|
192
|
+
if text:
|
|
193
|
+
sys.stdout.write(text)
|
|
194
|
+
sys.stdout.flush()
|
|
195
|
+
said = True
|
|
196
|
+
elif not said:
|
|
197
|
+
print(waiting, flush=True)
|
|
198
|
+
said = True
|
|
199
|
+
time.sleep(POLL_S)
|
|
200
|
+
except KeyboardInterrupt:
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def main(argv):
|
|
205
|
+
p = argparse.ArgumentParser(prog="tuieval logs", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
206
|
+
epilog="ctrl-c stops following (a run going on isn't affected).")
|
|
207
|
+
p.add_argument("what", nargs="?", help="'live' for answers, or a model (any unambiguous part of its label)")
|
|
208
|
+
p.add_argument("--list", action="store_true", help="every model's server log, newest first")
|
|
209
|
+
p.add_argument("-n", "--lines", type=int, default=40, help="lines shown before following (default 40)")
|
|
210
|
+
a = p.parse_args(argv)
|
|
211
|
+
if a.list:
|
|
212
|
+
print(list_logs(server_dir()))
|
|
213
|
+
return 0
|
|
214
|
+
if a.what == "live":
|
|
215
|
+
return follow(LiveFollower(live_dir()),
|
|
216
|
+
f"Waiting for answers in {os.path.relpath(live_dir())}/ (ctrl-c stops)")
|
|
217
|
+
only = find_log(server_dir(), a.what) if a.what else None
|
|
218
|
+
return follow(Follower(server_dir(), a.lines, only),
|
|
219
|
+
f"Waiting for {'that model' if only else 'a server'} to log in "
|
|
220
|
+
f"{os.path.relpath(server_dir())}/ (ctrl-c stops)")
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
tuieval run --only my-model --packs my-pack,other-pack
|
|
12
12
|
tuieval run --tags moe --packs my-pack # models tagged "moe"
|
|
13
13
|
tuieval run --packs my-pack --tests a,b # only these tests of a pack (or --tests my-pack:a,other:b)
|
|
14
|
+
tuieval run --only my-model --packs my-pack --tests a --retest # ask a question again
|
|
14
15
|
tuieval run --preset nightly # a selection saved in the TUI (presets.toml)
|
|
15
16
|
tuieval run --tier smoke --only my-model # 3 tests per pack, 1 repeat: check paths and flags
|
|
16
17
|
tuieval run --tier smoke --only openrouter:qwen/qwen3-32b # any OpenRouter model, no models.toml edit
|
|
@@ -742,7 +743,8 @@ def main(argv=None):
|
|
|
742
743
|
p.add_argument("--packs", help="comma-separated packs (default: all)")
|
|
743
744
|
p.add_argument("--tests", help="comma-separated test ids to run instead of the tier's sample: id with one "
|
|
744
745
|
"pack in --packs, else pack:id (and --packs defaults to those packs)")
|
|
745
|
-
p.add_argument("--force", action="store_true",
|
|
746
|
+
p.add_argument("--retest", "--force", action="store_true", dest="force",
|
|
747
|
+
help="ask again even if already answered: the packs, or just the --tests (earlier answers go to history/)")
|
|
746
748
|
p.add_argument("--repeat", type=int, help="times each question is asked, any tier (default: 1 for smoke and "
|
|
747
749
|
"screen, the pack's certification count for certify)")
|
|
748
750
|
p.add_argument("--tier", choices=engine.TIERS, default="screen",
|
|
@@ -385,6 +385,9 @@ HELP = {
|
|
|
385
385
|
[b]Models at a time[/b]: how many of the ticked models run side by side (blank: this machine's
|
|
386
386
|
parallel_models in models.toml, default 1). Answers note the models they ran alongside.
|
|
387
387
|
|
|
388
|
+
[b]Retest[/b]: ask the ticked questions again even if they're already answered (only the tests
|
|
389
|
+
picked with e, if any). Without it, answered questions are skipped. Replaced answers go to history/.
|
|
390
|
+
|
|
388
391
|
[b]Keys[/b]
|
|
389
392
|
s start (with a run going: add to it, or queue it) r results
|
|
390
393
|
a add a model (a GGUF, a model id, openrouter:<id>) m scan model folders for new GGUFs
|
|
@@ -646,7 +649,7 @@ class SetupScreen(Screen):
|
|
|
646
649
|
yield Input("", id="repeat", type="integer", max_length=2, placeholder="1")
|
|
647
650
|
yield Label("Models at a time")
|
|
648
651
|
yield Input("", id="parallel", type="integer", max_length=1, placeholder="1")
|
|
649
|
-
yield Checkbox("
|
|
652
|
+
yield Checkbox("Retest (ask again, even if already answered)", id="force")
|
|
650
653
|
yield Static(id="estimate")
|
|
651
654
|
with Horizontal(id="buttons"):
|
|
652
655
|
yield Button("Start [s]", id="start", variant="success")
|
|
@@ -958,7 +961,7 @@ class SetupScreen(Screen):
|
|
|
958
961
|
if not run:
|
|
959
962
|
text += "\n[yellow]Nothing selected can run (see skipping).[/yellow]"
|
|
960
963
|
elif rerun:
|
|
961
|
-
text += "\n[yellow]Everything selected has results; Start
|
|
964
|
+
text += "\n[yellow]Everything selected has results; Start retests it.[/yellow]"
|
|
962
965
|
resumed = [j for j in run if "done earlier" in j.note]
|
|
963
966
|
if resumed:
|
|
964
967
|
text += f" [yellow]{len(resumed)} pack(s) continue from earlier results[/yellow]"
|
|
@@ -380,6 +380,9 @@ class PickTests(unittest.TestCase):
|
|
|
380
380
|
hist = os.listdir(os.path.join(self.ws, "results", "m", "history"))
|
|
381
381
|
self.assertTrue(any(f.startswith("apps-") for f in hist), hist)
|
|
382
382
|
self.assertEqual(self.status(), "certified")
|
|
383
|
+
self.server("oracle")
|
|
384
|
+
tuieval(self.ws, "run", "--tests", "apps:b", "--retest") # --retest = --force
|
|
385
|
+
self.assertEqual(self.rows(), {"a": True, "b": True, "c": True, "d": True})
|
|
383
386
|
|
|
384
387
|
def test_cli_errors(self):
|
|
385
388
|
self.server("oracle")
|
|
@@ -1705,5 +1708,88 @@ class Remove(unittest.TestCase):
|
|
|
1705
1708
|
self.assertTrue(os.path.exists(os.path.join(ws, "removed", kept, rel)), rel)
|
|
1706
1709
|
|
|
1707
1710
|
|
|
1711
|
+
|
|
1712
|
+
class Logs(unittest.TestCase):
|
|
1713
|
+
"""tuieval logs: follows whichever model's server log is being written, without naming it."""
|
|
1714
|
+
|
|
1715
|
+
def test_follows_the_log_being_written(self):
|
|
1716
|
+
from tuieval import logs
|
|
1717
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1718
|
+
a, b = os.path.join(d, "model-a.log"), os.path.join(d, "model-b.log")
|
|
1719
|
+
write(a, "a old\n")
|
|
1720
|
+
write(b, "".join(f"b {i}\n" for i in range(50)))
|
|
1721
|
+
os.utime(a, (time.time() - 60,) * 2)
|
|
1722
|
+
f = logs.Follower(d, lines=3)
|
|
1723
|
+
self.assertEqual(f.poll(), "── model-b ──\nb 47\nb 48\nb 49\n") # the newest, its last lines
|
|
1724
|
+
self.assertEqual(f.poll(), "")
|
|
1725
|
+
with open(a, "a") as fh:
|
|
1726
|
+
fh.write("a new\n") # the run moved on to model a
|
|
1727
|
+
self.assertEqual(f.poll(), "── model-a ──\na new\n")
|
|
1728
|
+
with open(b, "a") as fh:
|
|
1729
|
+
fh.write("half a li") # a line still being written
|
|
1730
|
+
self.assertEqual(f.poll(), "")
|
|
1731
|
+
self.assertEqual(f.poll(), "── model-b ──\nhalf a li") # unchanged since: shown anyway
|
|
1732
|
+
write(os.path.join(d, "model-c.log"), "c starts\n") # a model that never ran before
|
|
1733
|
+
self.assertEqual(f.poll(), "── model-c ──\nc starts\n")
|
|
1734
|
+
write(a, "fresh\n") # replaced: read from the start
|
|
1735
|
+
self.assertEqual(f.poll(), "── model-a ──\nfresh\n")
|
|
1736
|
+
only = logs.Follower(d, lines=1, only=b)
|
|
1737
|
+
self.assertEqual(only.poll(), "── model-b ──\nb 49\n") # complete lines first
|
|
1738
|
+
self.assertEqual((only.poll(), only.poll()), ("", "half a li")) # held one poll, as above
|
|
1739
|
+
with open(a, "a") as fh:
|
|
1740
|
+
fh.write("ignored\n")
|
|
1741
|
+
self.assertEqual(only.poll(), "")
|
|
1742
|
+
|
|
1743
|
+
def test_live_answers(self):
|
|
1744
|
+
from tuieval import logs
|
|
1745
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1746
|
+
def answer(name, done=True, age=0):
|
|
1747
|
+
path = os.path.join(d, name + ".txt")
|
|
1748
|
+
write(path, "MODEL: m\nPROMPT: the question\n\n=== REASONING ===\nthink\n\n=== ANSWER ===\n42\n"
|
|
1749
|
+
+ ("\n=== 10 tokens, 1.0s, finish=stop ===\n" if done else ""))
|
|
1750
|
+
os.utime(path, (time.time() - age,) * 2)
|
|
1751
|
+
answer("20260101_100000_m_pack_old", age=20)
|
|
1752
|
+
answer("20260101_100100_m_pack_last", age=10)
|
|
1753
|
+
f = logs.LiveFollower(d)
|
|
1754
|
+
first = f.poll()
|
|
1755
|
+
self.assertIn("── 20260101_100100_m_pack_last ──\n=== REASONING ===", first) # the latest one only
|
|
1756
|
+
self.assertNotIn("_old", first)
|
|
1757
|
+
self.assertNotIn("PROMPT", first)
|
|
1758
|
+
answer("20260101_100200_m_pack_new", done=False) # still being written
|
|
1759
|
+
self.assertEqual(f.poll(), "")
|
|
1760
|
+
answer("20260101_100200_m_pack_new")
|
|
1761
|
+
self.assertIn("=== ANSWER ===\n42", f.poll())
|
|
1762
|
+
|
|
1763
|
+
def test_cli(self):
|
|
1764
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1765
|
+
ws = os.path.join(tmp, "ws")
|
|
1766
|
+
tuieval(tmp, "init", ws)
|
|
1767
|
+
self.assertIn("No server logs yet", tuieval(ws, "logs", "--list").stdout)
|
|
1768
|
+
d = os.path.join(ws, "logs", "server")
|
|
1769
|
+
os.makedirs(d, exist_ok=True)
|
|
1770
|
+
for i, name in enumerate(("coder-q4", "coder-q5", "chat")):
|
|
1771
|
+
write(os.path.join(d, name + ".log"), f"{name} line\n")
|
|
1772
|
+
os.utime(os.path.join(d, name + ".log"), (time.time() - 100 + i,) * 2)
|
|
1773
|
+
listed = tuieval(ws, "logs", "--list").stdout.splitlines()
|
|
1774
|
+
self.assertEqual([l.split()[0] for l in listed], ["chat", "coder-q5", "coder-q4"]) # newest first
|
|
1775
|
+
p = tuieval(ws, "logs", "coder", check=False)
|
|
1776
|
+
self.assertNotEqual(p.returncode, 0)
|
|
1777
|
+
self.assertIn("matches 2 logs", p.stderr)
|
|
1778
|
+
self.assertIn("No server log matches", tuieval(ws, "logs", "nothing", check=False).stderr)
|
|
1779
|
+
# following: the newest log, then whichever one grows; ctrl-c stops it cleanly
|
|
1780
|
+
proc = subprocess.Popen([sys.executable, "-m", "tuieval", "logs", "-n", "1"], stdout=subprocess.PIPE,
|
|
1781
|
+
text=True, env=dict(os.environ, TUIEVAL_HOME=ws))
|
|
1782
|
+
try:
|
|
1783
|
+
time.sleep(1.5)
|
|
1784
|
+
with open(os.path.join(d, "coder-q4.log"), "a") as fh:
|
|
1785
|
+
fh.write("q4 server started\n")
|
|
1786
|
+
time.sleep(1.5)
|
|
1787
|
+
finally:
|
|
1788
|
+
proc.send_signal(__import__("signal").SIGINT)
|
|
1789
|
+
out, _ = proc.communicate(timeout=20)
|
|
1790
|
+
self.assertEqual(proc.returncode, 0)
|
|
1791
|
+
self.assertEqual(out, "── chat ──\nchat line\n── coder-q4 ──\nq4 server started\n")
|
|
1792
|
+
|
|
1793
|
+
|
|
1708
1794
|
if __name__ == "__main__":
|
|
1709
1795
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev68 → tuieval-0.2.0.dev72}/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
|