tuieval 0.2.0.dev55__py3-none-any.whl → 0.2.0.dev62__py3-none-any.whl

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/_version.py CHANGED
@@ -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.dev55'
22
- __version_tuple__ = version_tuple = (0, 2, 0, 'dev55')
21
+ __version__ = version = '0.2.0.dev62'
22
+ __version_tuple__ = version_tuple = (0, 2, 0, 'dev62')
23
23
 
24
24
  __commit_id__ = commit_id = None
tuieval/cli.py CHANGED
@@ -14,7 +14,7 @@ Start here
14
14
 
15
15
  Day to day
16
16
  tuieval compare [...] scorecards (--speed, --pairwise, --failures)
17
- tuieval pta the PTA index: parsimony, speed and accuracy per model, as bars
17
+ tuieval pta the PTA index: parsimony (tokens), time and accuracy per model, as bars
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
tuieval/compare.py CHANGED
@@ -121,6 +121,12 @@ def p90(values):
121
121
  return statistics.quantiles(values, n=10)[-1] if len(values) > 1 else (values[0] if values else None)
122
122
 
123
123
 
124
+ def pct(fraction):
125
+ """A share as a percentage: "100%" and "0%" only when exact, else one decimal ("99.6%"), so a model
126
+ with a wrong answer never shows as perfect."""
127
+ return "100%" if fraction >= 1 else "0%" if fraction <= 0 else f"{100 * fraction:.1f}%"
128
+
129
+
124
130
  def median(values):
125
131
  values = [v for v in values if v is not None]
126
132
  return statistics.median(values) if values else None
@@ -154,10 +160,19 @@ def settings_notes(infos):
154
160
  per_model.setdefault(i["label"], i["settings"])
155
161
  keys = sorted({k for s in per_model.values() for k in s})
156
162
  differing = [k for k in keys if len({json.dumps(s.get(k)) for s in per_model.values()}) > 1]
157
- if differing:
158
- detail = "; ".join(f"{k}: " + ", ".join(f"{m}={per_model[m].get(k)}" for m in sorted(per_model))
159
- for k in differing)
160
- notes.append(f"Settings differ between models ({detail}). Compare those models knowing that.")
163
+ parts = []
164
+ for k in differing: # only the models that differ from the most common value, named when few
165
+ groups = collections.defaultdict(list)
166
+ for m in sorted(per_model):
167
+ groups[json.dumps(per_model[m].get(k))].append(m)
168
+ common = max(groups, key=lambda v: len(groups[v]))
169
+ for value, ms in sorted(groups.items()):
170
+ if value != common:
171
+ who = ", ".join(ms) if len(ms) <= 3 else f"{len(ms)} models"
172
+ shown = "unset" if value == "null" else json.loads(value)
173
+ parts.append(f"{k} {shown} on {who}")
174
+ if parts:
175
+ notes.append("Settings differ: " + "; ".join(parts) + ".")
161
176
  fps = collections.defaultdict(set)
162
177
  for i in infos:
163
178
  if i.get("pack"):
@@ -412,7 +427,7 @@ def by_difficulty(rows):
412
427
  cells = []
413
428
  for lvl in LEVELS:
414
429
  x = [r["ok"] for r in sub if r.get("difficulty") == lvl]
415
- cells.append(f"{sum(x)}/{len(x)} ({100 * sum(x) / len(x):.0f}%)" if x else "-")
430
+ cells.append(f"{sum(x)}/{len(x)} ({pct(sum(x) / len(x))})" if x else "-")
416
431
  table.append([m, s] + cells)
417
432
  return header, table
418
433
 
tuieval/engine.py CHANGED
@@ -987,16 +987,16 @@ class Engine:
987
987
  del per_pack[name]
988
988
  return {l: p for l, p in out.items() if p}
989
989
 
990
- def estimate_seconds(self, jobs):
991
- """(seconds, notes): expected run time for the waiting jobs, estimated per pack.
990
+ def seconds_per_answer(self, jobs, waiting_only=True):
991
+ """({job key: expected seconds per answer}, borrowed, guessed, thin) for the waiting jobs.
992
992
  A pack the model has answered uses its own median time there. Otherwise the median of
993
993
  other models' times on that pack, scaled by how much slower or faster this model is than
994
- them on packs both have answered. notes lists what made the estimate rough ([] = solid)."""
994
+ them on packs both have answered. The sets say which estimates are rough (see estimate_seconds)."""
995
995
  times = self.answer_times()
996
996
  med = {l: {p: statistics.median(ts) for p, ts in pp.items()} for l, pp in times.items()}
997
- total, borrowed, guessed, thin = 0.0, set(), set(), set()
997
+ spr_of, borrowed, guessed, thin = {}, set(), set(), set()
998
998
  for j in jobs:
999
- if j.status != "waiting":
999
+ if j.status in ("done", "skipped", "failed") or (waiting_only and j.status != "waiting"):
1000
1000
  continue
1001
1001
  own = med.get(j.label, {})
1002
1002
  ref = {p: statistics.median(v[p] for l, v in med.items() if p in v and l != j.label)
@@ -1013,8 +1013,18 @@ class Engine:
1013
1013
  else:
1014
1014
  spr = statistics.median(own.values()) if own else DEFAULT_SECONDS_PER_REQUEST
1015
1015
  guessed.add((j.key, bool(own)))
1016
- done = len(self._done_keys(j)) if j.merge else 0
1017
- total += max(0, j.total - done) * spr
1016
+ spr_of[j.key] = spr
1017
+ return spr_of, borrowed, guessed, thin
1018
+
1019
+ def estimate_seconds(self, jobs):
1020
+ """(seconds, notes): expected run time for the waiting jobs, estimated per pack
1021
+ (seconds_per_answer). notes lists what made the estimate rough ([] = solid)."""
1022
+ spr_of, borrowed, guessed, thin = self.seconds_per_answer(jobs)
1023
+ total = 0.0
1024
+ for j in jobs:
1025
+ if j.key in spr_of:
1026
+ done = len(self._done_keys(j)) if j.merge else 0
1027
+ total += max(0, j.total - done) * spr_of[j.key]
1018
1028
  notes = []
1019
1029
  if borrowed:
1020
1030
  factors = sorted({f for _, f, _ in borrowed if f is not None})
tuieval/pta.py CHANGED
@@ -1,13 +1,14 @@
1
- """PTA index: parsimony, speed (time) and accuracy per model, 0-100 each and higher is better, shown
2
- as a bar per score for each model.
1
+ """PTA index: parsimony (tokens), time and accuracy per model, a bar each (longer = better).
3
2
 
4
- P parsimony a score out of 100, from the tokens (reasoning and answer) used on every question
5
- compared (each question at its median over repeats): 100 for the model that used the
6
- fewest, POINTS_PER_DOUBLING points less for every doubling (0 at about 100 times more).
7
- Unlike T it doesn't depend on the machine: it's how much a model says to get there.
8
- T speed a score out of 100, the same from the total time to answer those questions: 100 for
9
- the fastest model. Log scales, so models that are all far behind the best still differ.
10
- A accuracy a percentage: the share of answers to those questions that passed.
3
+ P parsimony (tokens) the tokens (reasoning and answer) used on every question compared (each
4
+ question at its median over repeats), shown as how many times the leanest
5
+ model's ("2.4×"). Unlike T it doesn't depend on the machine.
6
+ T time the total time to answer those questions, as how many times the fastest
7
+ model's ("28×").
8
+ A accuracy the share of answers to those questions that passed (%).
9
+
10
+ Behind the P and T bars is a 0-100 score: 100 for the best, POINTS_PER_DOUBLING points less for every
11
+ doubling (0 at about 100 times more), so models far behind the best still get a visible bar.
11
12
 
12
13
  Models are compared on the questions all of them answered, so one that skipped some never looks
13
14
  faster. Server and connection errors aren't answers. By default a model with fewer than half as many
@@ -32,12 +33,23 @@ def score(total, best):
32
33
  return max(0.0, 100 - POINTS_PER_DOUBLING * math.log2(total / best))
33
34
 
34
35
 
35
- def index(rows, models=None):
36
+ def memory_gb(infos):
37
+ """{model: the most memory its server held in any run (GB)}, for models tuieval served (hosted: none).
38
+ On a discrete GPU that's its peak VRAM (vram_peak_gb), where the model lives; otherwise its resident memory."""
39
+ out = {}
40
+ for i in infos:
41
+ gb = i.get("vram_peak_gb") or (i["peak_rss_mb"] / 1024 if i.get("peak_rss_mb") else None)
42
+ if gb:
43
+ out[i["label"]] = max(out.get(i["label"], 0), gb)
44
+ return out
45
+
46
+
47
+ def index(rows, models=None, memory=None):
36
48
  """{"questions": n shared, "models": [{model, P, T, A, tokens, total_s, passed, answers, alongside,
37
- fewest, fastest}],
49
+ fewest, fastest, tps, ttft, memory_gb}],
38
50
  "left_out": [(model, questions answered)], "no_answers": [model], "most": questions answered by the
39
51
  most-answered model}, best accuracy first. models: the ones to compare (default: every model in rows
40
- with enough answers; models named here are compared whatever their count)."""
52
+ with enough answers; models named here are compared whatever their count). memory: memory_gb()."""
41
53
  no_answers = sorted({r["model"] for r in rows} - {r["model"] for r in rows if not r.get("error")})
42
54
  rows = [r for r in rows if not r.get("error")] # a server or connection error isn't an answer
43
55
  tests = compare.per_question(rows)
@@ -61,12 +73,19 @@ def index(rows, models=None):
61
73
  "total_s": sum(c["secs"] or 0 for c in cells),
62
74
  "passed": sum(c["passed"] for c in cells), "answers": answers,
63
75
  "A": 100 * sum(c["passed"] for c in cells) / answers if answers else None,
76
+ # generation speed and time to first token over the same answers (medians)
77
+ "tps": compare.median(r.get("gen_tps") for c in cells for r in c["rows"]),
78
+ "ttft": compare.median(r.get("ttft") for c in cells for r in c["rows"]),
79
+ "memory_gb": (memory or {}).get(m),
64
80
  # answers that ran while other models were served shared the machine's speed
65
81
  "alongside": any(r.get("alongside") for c in cells for r in c["rows"])})
66
82
  fastest = min((x["total_s"] for x in out if x["total_s"] > 0), default=None)
67
83
  fewest = min((x["tokens"] for x in out if x["tokens"] > 0), default=None)
68
84
  for x in out:
69
85
  x["P"], x["T"] = score(x["tokens"], fewest), score(x["total_s"], fastest)
86
+ # shown on the bars: how many times the leanest / fastest model's total (1.0 = the best)
87
+ x["tokens_x"] = x["tokens"] / fewest if fewest and x["tokens"] else None
88
+ x["time_x"] = x["total_s"] / fastest if fastest and x["total_s"] else None
70
89
  x["fewest"] = bool(fewest) and x["tokens"] == fewest
71
90
  x["fastest"] = bool(fastest) and x["total_s"] == fastest
72
91
  out.sort(key=lambda x: (-(x["A"] or 0), -(x["T"] or 0), x["model"]))
@@ -74,29 +93,40 @@ def index(rows, models=None):
74
93
  "most": most}
75
94
 
76
95
 
77
- LETTERS = (("P", "parsimony", "/100", "cyan"), ("T", "speed", "/100", "magenta"), ("A", "accuracy", "%", "green"))
96
+ # (letter, name, what the number beside the bar shows, colour); the bar itself is the 0-100 score
97
+ LETTERS = (("P", "parsimony (tokens)", "tokens_x", "cyan"), ("T", "time", "time_x", "magenta"),
98
+ ("A", "accuracy", "A", "green"))
99
+
100
+
101
+ def times(v):
102
+ """A multiple of the best: "1.0×", "2.4×", "28×"."""
103
+ return "-" if v is None else f"{v:.1f}×" if v < 9.95 else f"{v:.0f}×"
78
104
 
79
105
 
80
106
  def bars(result, width=20, markup=True):
81
- """Lines with a row per model and a bar per score (P, T, A), longer = better, each with its number:
82
- the at-a-glance view. markup=False gives plain text (the report)."""
107
+ """Lines with a row per model and a bar per score (P, T, A), longer = better: the at-a-glance view.
108
+ Beside P and T, how many times the leanest / fastest model's tokens / time; beside A, the percentage.
109
+ markup=False gives plain text (the report)."""
83
110
  if not result["models"]:
84
111
  return []
85
112
  name_w = max(len(x["model"]) for x in result["models"])
86
- cell_w = width + 6 # the bar, a space and "100%" or " 100"
113
+ # each column: the bar, a space, "100%" or "9.8×" (5), then a gap; at least as wide as its heading
114
+ cell_w = max(width + 9, *(len(f"{k} {name}") + 2 for k, name, _, _ in LETTERS))
87
115
  head = " " * (name_w + 2) + "".join(f"{f'{k} {name}':<{cell_w}}" for k, name, _, _ in LETTERS)
88
116
  lines = [f"[b]{head.rstrip()}[/b]" if markup else head.rstrip()]
89
117
  for x in result["models"]:
90
118
  cells = []
91
- for k, _, unit, color in LETTERS:
119
+ for k, _, shown, color in LETTERS:
92
120
  v = x[k]
93
121
  full = 0 if v is None else round(width * max(0, min(100, v)) / 100)
122
+ if v is not None and v < 100: # full only when perfect (accuracy) or the best (P, T)
123
+ full = min(full, width - 1)
94
124
  bar, rest = "█" * full, "░" * (width - full)
95
- num = "-" if v is None else f"{v:.0f}{'%' if unit == '%' else ''}"
125
+ num = (("-" if v is None else compare.pct(v / 100)) if shown == "A" else times(x.get(shown)))
96
126
  bar = f"[{color}]{bar}[/][grey37]{rest}[/]" if markup else bar + rest
97
- cells.append(f"{bar} {num:>4}")
127
+ cells.append(f"{bar} {num:>6}" + " " * (cell_w - width - 7))
98
128
  name = f"{x['model']:<{name_w}}"
99
- lines.append(f"{rich_escape(name) if markup else name} " + " ".join(cells))
129
+ lines.append((f"{rich_escape(name) if markup else name} " + "".join(cells)).rstrip())
100
130
  return lines
101
131
 
102
132
 
@@ -116,22 +146,28 @@ def left_out_lines(result):
116
146
  return out
117
147
 
118
148
 
119
- def _score(v, unit=""):
120
- """A is a percentage (%), P and T scores out of 100 (/100: on a log scale, not percentages)."""
121
- return "-" if v is None else f"{v:.0f}{unit}"
122
-
123
-
124
149
  def table(result):
125
150
  """(header, rows) for a plain table of the index."""
126
- header = ["model", "P parsimony /100", "T speed /100", "A accuracy %", "tokens", "total time",
127
- "answers right"]
128
- rows = [[x["model"], _score(x["P"]), _score(x["T"]), _score(x["A"], "%"),
129
- f"{round(x['tokens']):,}" + (" ★" if x.get("fewest") else ""),
130
- fmt_total(x["total_s"]) + (" ★" if x.get("fastest") else ""),
131
- f"{x['passed']}/{x['answers']}"] for x in result["models"]]
151
+ n = result["questions"] or 1
152
+ header = ["model", "tokens/answer", "time/answer", "answers right", "tok/s", "TTFT s", "memory GB"]
153
+ rows = [[x["model"], f"{round(x['tokens'] / n):,}" + (" ★" if x.get("fewest") else ""),
154
+ fmt_secs(x["total_s"] / n) + (" ★" if x.get("fastest") else ""),
155
+ f"{x['passed']}/{x['answers']}", _num(x.get("tps"), ".0f"), _num(x.get("ttft"), ".1f"),
156
+ _num(x.get("memory_gb"), ".1f")] for x in result["models"]]
132
157
  return header, rows
133
158
 
134
159
 
160
+ def fmt_secs(secs):
161
+ """Time per answer: "2.5s", "69s", "4m10s"."""
162
+ if not secs:
163
+ return "-"
164
+ return f"{secs:.1f}s" if secs < 10 else f"{secs:.0f}s" if secs < 60 else fmt_total(secs)
165
+
166
+
167
+ def _num(v, fmt):
168
+ return "-" if v is None else format(v, fmt)
169
+
170
+
135
171
  def fmt_total(secs):
136
172
  if not secs:
137
173
  return "-"
@@ -147,19 +183,18 @@ def scope_note(result, models):
147
183
  if not result["questions"]:
148
184
  return "These models have no question in common yet, so there's nothing to compare."
149
185
  return (f"Compared on the {result['questions']} questions all {len(result['models'])} model(s) answered. "
150
- f"A is a percentage. P parsimony and T speed are scores out of 100, not percentages: 100 = the fewest "
151
- f"tokens / the fastest total time (★), {POINTS_PER_DOUBLING} points less for each doubling; a better "
152
- "model added lowers the others.")
186
+ "Longer bars are better. P parsimony (tokens) and T time: how many times the leanest / fastest "
187
+ "model's (1.0×, ★ in the table). A accuracy: answers right.")
153
188
 
154
189
 
155
190
  def for_engine(e, labels=None, packs=None, results_dir=None):
156
191
  """(result, models) from a workspace's finished results (results on their way out excluded)."""
157
192
  paths = [p for p in compare.default_paths(results_dir or e.results_dir) if not e.superseded(p)]
158
- rows, _ = compare.load(paths) if paths else ([], [])
193
+ rows, infos = compare.load(paths) if paths else ([], [])
159
194
  if packs:
160
195
  rows = [r for r in rows if r["suite"] in packs]
161
196
  models = sorted({r["model"] for r in rows} & set(labels) if labels else {r["model"] for r in rows})
162
- return index(rows, models if labels else None), models
197
+ return index(rows, models if labels else None, memory_gb(infos)), models
163
198
 
164
199
 
165
200
  def markdown(result, models):
tuieval/run_evals.py CHANGED
@@ -373,11 +373,12 @@ def report_markdown(e, labels=None):
373
373
  + f"; truncated {100 * ev['truncation']:.1f}%"
374
374
  + (f"; consistency {100 * ev['consistency']:.0f}% of {ev['groups']} groups" if ev.get("consistency") is not None else ""))
375
375
  recs = e.records(label, name)
376
+ from .compare import pct
376
377
  levels = []
377
378
  for lvl in ("easy", "medium", "hard"):
378
379
  x = [r["pass"] for r in recs if r.get("difficulty") == lvl]
379
380
  if x:
380
- levels.append(f"{lvl} {sum(x)}/{len(x)} ({100 * sum(x) / len(x):.0f}%)")
381
+ levels.append(f"{lvl} {sum(x)}/{len(x)} ({pct(sum(x) / len(x))})")
381
382
  if levels:
382
383
  lines.append(" - by difficulty: " + ", ".join(levels))
383
384
  for ex in ev.get("critical_examples", []):
@@ -389,7 +390,7 @@ def report_markdown(e, labels=None):
389
390
 
390
391
  def cmd_pta(argv):
391
392
  p = argparse.ArgumentParser(prog="tuieval pta",
392
- description="The PTA index: parsimony (tokens), speed and accuracy per model (0-100, higher is better), as a "
393
+ description="The PTA index: parsimony (tokens), time and accuracy per model, as a "
393
394
  "bar per score for each model and a table, over the questions all the models answered.")
394
395
  p.add_argument("--only", help="comma-separated model labels (default: every model with results)")
395
396
  p.add_argument("--packs", help="comma-separated packs (default: all)")
@@ -413,7 +414,7 @@ def cmd_pta(argv):
413
414
  return
414
415
  out.print()
415
416
  for line in pta.bars(result, a.width):
416
- out.print(line)
417
+ out.print(line, soft_wrap=True) # a bar row stays one line (the terminal wraps it if it must)
417
418
  header, rows = pta.table(result)
418
419
  widths = [max(len(h), *(len(r[i]) for r in rows)) for i, h in enumerate(header)]
419
420
  out.print()
tuieval/tui.py CHANGED
@@ -10,7 +10,7 @@ Run screen: live reasoning and answer, per-test ✓/✗, TTFT and tokens/s, p
10
10
  k skips the current model, c cancels everything, f pauses auto-scroll.
11
11
  r shows results at any time (also mid-run); n starts a new run when it ends.
12
12
  Enter on a Recent results row shows that answer in full.
13
- Results: opens on the PTA index (parsimony, speed and accuracy per model, as bars). Per
13
+ Results: opens on the PTA index (parsimony, time and accuracy per model, as bars). Per
14
14
  question compares each model's tokens and seconds on the same question. ctrl+r
15
15
  reloads (it also reloads when a run's pack finishes). Enter on a Failures row or a
16
16
  Per question cell shows the answer: grading and checks, reasoning, answer, question.
@@ -19,6 +19,7 @@ Anywhere: esc/q go back (q quits only on Setup), ctrl+q quits, w lists this
19
19
  tunes. Leaving a live run or tune keeps it going; the header shows its progress.
20
20
  """
21
21
  import argparse
22
+ import collections
22
23
  import glob
23
24
  import json
24
25
  import os
@@ -331,16 +332,15 @@ with the model's reasoning and answer as they stream, then recent results with t
331
332
  v with several models at a time: stream the next one (k skips the one streaming)""",
332
333
  "ResultsScreen": """[b]Results[/b]
333
334
 
334
- Opens on the [b]PTA index[/b]: parsimony (tokens), speed and accuracy per model, a bar each (0-100,
335
- longer = better). [b]Production readiness[/b] gives one verdict per model and use case, and what's
336
- missing when it's INCONCLUSIVE. The other tabs are the evidence:
337
- Scorecard accuracy, critical failures and truncation per model and pack
338
- Speed & tokens time and tokens per answer (★ = nothing beats it on both accuracy and time)
339
- Per question every question, model by model
340
- Is the difference real? whether one model is really better than another, or it's noise
341
- Tests that separate the questions that tell models apart
335
+ PTA index parsimony (tokens), time and accuracy per model, a bar each (longer = better;
336
+ 2.4× = 2.4 times the best), with per-answer numbers, tok/s, TTFT and memory in
337
+ the table. Results open here.
338
+ Production readiness one verdict per model and use case; the table says why and what to run next
339
+ Per question every question, model by model (filters: pack, level, Models disagree)
342
340
  Failures every wrong answer with the grader's reason; enter opens it in full
343
- Test quality tests nobody fails, everybody fails, or that look broken
341
+
342
+ More in the terminal: tuieval compare (--pairwise: is a difference real), tuieval history,
343
+ tuieval items (test quality).
344
344
 
345
345
  [b]Keys[/b] esc back · ctrl+r refresh · w runs""",
346
346
  "TuneScreen": """[b]Tuning speed flags[/b]
@@ -1855,6 +1855,8 @@ class RunScreen(Screen):
1855
1855
  self.current = ""
1856
1856
  self.req_first = None
1857
1857
  self.stats = []
1858
+ self.spr, self.rough = None, set() # job key -> expected seconds per answer (ETA), rough ones
1859
+ self.live = {} # job key -> seconds per answer in this session
1858
1860
  self.mem = {}
1859
1861
  self.outcome = "" # "Finished" or "Cancelled" once the queue is done
1860
1862
  self.ended = None
@@ -2037,6 +2039,8 @@ class RunScreen(Screen):
2037
2039
  + (f" [red]copy of repeat {r['identical_to_repeat'] + 1}[/red]"
2038
2040
  if r.get("identical_to_repeat") is not None else ""))
2039
2041
  self.stats.append(r)
2042
+ if r.get("total_s"):
2043
+ self.live.setdefault(job.key, []).append(r["total_s"])
2040
2044
  self.recent_entries[row_key] = {"model": job.label, "pack": job.pack.name, "record": r,
2041
2045
  "pack_fp": job.pack.fingerprint}
2042
2046
  if recent.row_count > 500:
@@ -2187,6 +2191,34 @@ class RunScreen(Screen):
2187
2191
  ("note", short_note(j.note))):
2188
2192
  q.update_cell(j.key, col, value, update_width=True)
2189
2193
 
2194
+ LIVE_ANSWERS = 5 # a pack's own times this session take over from its history after this many
2195
+
2196
+ def eta_seconds(self):
2197
+ """(seconds, rough): time left, pack by pack: answers left × seconds per answer. A pack's own
2198
+ average this session once it has LIVE_ANSWERS of them, before that its history (Engine.
2199
+ seconds_per_answer: this model's past times there, or other models' scaled). Divided by the
2200
+ models running side by side. rough: some packs have no history of their own yet."""
2201
+ new = [j for j in self.jobs if self.spr is None or (j.key not in self.spr and j.status not in
2202
+ ("done", "skipped", "failed"))]
2203
+ if new: # at the start, and for evals added to the run
2204
+ spr, borrowed, guessed, _ = self.app.engine.seconds_per_answer(new, waiting_only=False)
2205
+ self.spr = {**(self.spr or {}), **spr}
2206
+ self.rough |= {k for k, *_ in borrowed} | {k for k, _ in guessed}
2207
+ left, rough, models = 0.0, False, set()
2208
+ for j in self.jobs:
2209
+ if j.status in ("done", "skipped", "failed") or j.done >= j.total:
2210
+ continue
2211
+ live = self.live.get(j.key, [])
2212
+ if len(live) >= self.LIVE_ANSWERS:
2213
+ spr = sum(live) / len(live)
2214
+ else:
2215
+ spr = self.spr.get(j.key, engine.DEFAULT_SECONDS_PER_REQUEST)
2216
+ rough |= j.key in self.rough
2217
+ left += (j.total - j.done) * spr
2218
+ models.add(j.label)
2219
+ lanes = max(1, min(self.parallel or self.app.engine.parallel_models(), len(models)))
2220
+ return left / lanes, rough
2221
+
2190
2222
  def update_overall(self):
2191
2223
  done = sum(j.done for j in self.jobs)
2192
2224
  passed = sum(j.passed for j in self.jobs)
@@ -2194,10 +2226,10 @@ class RunScreen(Screen):
2194
2226
  self.query_one("#bar", ProgressBar).update(progress=done)
2195
2227
  elapsed = time.time() - self.started
2196
2228
  eta = ""
2197
- done_now = sum(j.done - j.resumed for j in self.jobs if j.started) # answers from earlier sittings
2198
- if self.running and done_now > 0 and self.total > done: # took no time in this one
2199
- eta = f" · ETA {fmt_secs(elapsed / done_now * (self.total - done))}"
2200
- pct = f"{100 * passed / (passed + failed):.0f}%" if passed + failed else "-"
2229
+ if self.running and self.total > done:
2230
+ left, rough = self.eta_seconds()
2231
+ eta = f" · ETA {'~' if rough else ''}{fmt_secs(left)}"
2232
+ pct = compare.pct(passed / (passed + failed)) if passed + failed else "-"
2201
2233
  recent = self.stats[-50:]
2202
2234
  tps = [r["gen_tps"] for r in recent if r.get("gen_tps")]
2203
2235
  ttfts = [r["ttft_s"] for r in recent if r.get("ttft_s") is not None and not r.get("cached_tokens")]
@@ -2315,8 +2347,8 @@ class RunScreen(Screen):
2315
2347
  class ResultsScreen(Screen):
2316
2348
  BINDINGS = [Binding("escape", "app.pop_screen", "Back"), Binding("q", "app.pop_screen", "Back", show=False),
2317
2349
  Binding("w", "runs", "Runs"), Binding("ctrl+r", "refresh", "Refresh")]
2318
- SORTS = [("Sort: token gap", "tokens"), ("Sort: time gap", "secs"),
2319
- ("Sort: most tokens", "most"), ("Sort: pack order", "order")]
2350
+ SORTS = [("By token gap", "tokens"), ("By time gap", "secs"),
2351
+ ("By most tokens", "most"), ("By pack order", "order")]
2320
2352
 
2321
2353
  def check_action(self, action, parameters):
2322
2354
  return bool(self.app.sessions) if action == "runs" else True
@@ -2346,9 +2378,9 @@ class ResultsScreen(Screen):
2346
2378
  with Horizontal(id="pta-controls"):
2347
2379
  yield Select([("All packs", "")], id="pta-pack", allow_blank=False, value="")
2348
2380
  yield Button("Models: all", id="pta-models")
2349
- yield Static("[dim]P parsimony: tokens used (reasoning and answer). T speed: total time. A accuracy: "
2350
- "answers right. Longer bars are better for all three. Verdicts and critical failures "
2351
- "are in Production readiness.[/dim]")
2381
+ yield Static("[dim]P parsimony (tokens): tokens used, reasoning and answer. T time: total time. "
2382
+ "A accuracy: answers right. Verdicts and critical failures are in Production "
2383
+ "readiness.[/dim]")
2352
2384
  yield Static(id="pta-note")
2353
2385
  yield Static(id="pta-bars")
2354
2386
  yield DataTable(id="pta", zebra_stripes=True, cursor_type="none")
@@ -2361,22 +2393,6 @@ class ResultsScreen(Screen):
2361
2393
  "measured there, or projected from token counts and that machine's tuned speeds.[/dim]")
2362
2394
  yield DataTable(id="latency", zebra_stripes=True)
2363
2395
  yield DataTable(id="readiness-detail", zebra_stripes=True, cursor_type="row")
2364
- with TabPane("Verdict history"):
2365
- yield Static("[dim]Every change in a model's verdict, newest first. Kept even after results are rerun "
2366
- "or questions change; the evidence behind older verdicts is in results/<model>/history/.[/dim]")
2367
- yield DataTable(id="history", zebra_stripes=True, cursor_type="row")
2368
- with TabPane("Scorecard"):
2369
- yield DataTable(id="scorecard", zebra_stripes=True)
2370
- with TabPane("Speed & tokens"):
2371
- yield Select([("As measured (all machines)", "")] +
2372
- [(f"{mc.id}{' (this machine)' if i == 0 else ''}", mc.id)
2373
- for i, mc in enumerate(self.app.engine.machines())],
2374
- id="speed-machine", allow_blank=False, value="")
2375
- yield Static("[dim]★ = no other model is both more accurate and faster per answer. "
2376
- "Memory is the server's resident size (includes GPU-mapped weights on Apple Silicon). "
2377
- "On another machine, times are measured there if the model ran there, otherwise "
2378
- "projected from each answer's token counts and that machine's tuned speeds.[/dim]")
2379
- yield DataTable(id="speed", zebra_stripes=True)
2380
2396
  with TabPane("Per question", id="tab-per-question"):
2381
2397
  with Horizontal(id="pq-controls"):
2382
2398
  yield Select([("All packs", "")], id="pq-pack", allow_blank=False, value="")
@@ -2384,28 +2400,16 @@ class ResultsScreen(Screen):
2384
2400
  id="pq-level", allow_blank=False, value="")
2385
2401
  yield Select(self.SORTS, id="pq-sort", allow_blank=False, value="tokens")
2386
2402
  yield Button("Models: all", id="pq-models")
2387
- yield Checkbox("Only questions all answered", False, id="pq-shared")
2388
- yield Checkbox("Include unfinished", True, id="pq-partial")
2403
+ yield Checkbox("Answered by all", False, id="pq-shared")
2404
+ yield Checkbox("Models disagree", False, id="pq-disagree")
2405
+ yield Checkbox("In progress", True, id="pq-partial")
2389
2406
  yield Static(id="pq-compare-title")
2390
2407
  yield DataTable(id="pq-summary", zebra_stripes=True, cursor_type="none")
2391
2408
  yield Static(id="pq-legend")
2392
2409
  yield DataTable(id="per-question", zebra_stripes=True, cursor_type="cell")
2393
- with TabPane("Is the difference real?"):
2394
- yield DataTable(id="pairwise", zebra_stripes=True)
2395
- with TabPane("Tests that separate models"):
2396
- yield DataTable(id="separating", zebra_stripes=True)
2397
- with TabPane("By difficulty"):
2398
- yield Static("[dim]Pass rate per difficulty level. The level is your label only; models never see it. "
2399
- "A strong model should hold up on HARD, not just EASY.[/dim]")
2400
- yield DataTable(id="difficulty", zebra_stripes=True)
2401
2410
  with TabPane("Failures"):
2402
2411
  yield Static("[dim]Enter on a row shows the whole answer, its reasoning and what was checked.[/dim]")
2403
2412
  yield DataTable(id="failures", zebra_stripes=True, cursor_type="row")
2404
- with TabPane("Test quality"):
2405
- yield Static("[dim]no signal: every model passed · suspicious: every model failed (check the test) · "
2406
- "inverted: the weakest model beat the strongest · flaky: same model, different results "
2407
- "across repeats[/dim]")
2408
- yield DataTable(id="quality", zebra_stripes=True)
2409
2413
  yield Footer()
2410
2414
 
2411
2415
  def on_mount(self):
@@ -2462,7 +2466,6 @@ class ResultsScreen(Screen):
2462
2466
 
2463
2467
  def fill(self):
2464
2468
  results_dir = self.results_dir or self.app.engine.results_dir
2465
- self.fill_history()
2466
2469
  paths = compare.default_paths(results_dir)
2467
2470
  self.partial_rows = compare.load_partials(results_dir)
2468
2471
  self.superseded = {p for p in compare.default_paths(results_dir) if self.app.engine.superseded(p)}
@@ -2479,40 +2482,13 @@ class ResultsScreen(Screen):
2479
2482
  smoke = os.path.abspath(results_dir) != os.path.abspath(self.app.engine.results_dir)
2480
2483
  info.update(f"{len(paths)} {'Smoke ' if smoke else ''}result files in {os.path.relpath(results_dir)}/" + banner
2481
2484
  + "".join(f"\n[yellow]⚠ {n}[/yellow]" for n in notes))
2482
- header, table = compare.scorecard(rows)
2483
- t = self.query_one("#scorecard", DataTable)
2484
- t.add_columns(*header)
2485
- t.add_rows(table)
2486
2485
  self.rows, self.infos = rows, infos
2487
- self.fill_speed(self.query_one("#speed-machine", Select).value or "")
2488
- p = self.query_one("#pairwise", DataTable)
2489
- p.add_columns("Model A", "Model B", "A − B", "95% CI", "Verdict", "Shared tests")
2490
- for a, b, diff, lo, hi, call, n in compare.pairwise(rows):
2491
- color = "green" if call == "clear" else "dim"
2492
- p.add_row(a, b, f"{100 * diff:+.1f} pts", f"[{100 * lo:+.1f}, {100 * hi:+.1f}]",
2493
- f"[{color}]{call}[/{color}]", str(n))
2494
- split, _ = compare.separating(rows)
2495
- sep = self.query_one("#separating", DataTable)
2496
- models = sorted({r["model"] for r in rows})
2497
- sep.add_columns("Test", *models)
2498
- for test, d in split:
2499
- sep.add_row(test, *[f"{sum(d[m])}/{len(d[m])}" if m in d else "-" for m in models])
2500
2486
  f = self.query_one("#failures", DataTable)
2501
2487
  f.add_columns("Model", "Level", "Test", "Reason")
2502
2488
  # compare.failures lists the failed rows in order, so they line up with fail_rows
2503
2489
  self.fail_rows = [r for r in rows if not r["ok"]]
2504
2490
  for model, test, reason, level in compare.failures(rows):
2505
2491
  f.add_row(model, diff_badge(level), test, rich_escape(reason[:130]))
2506
- dh, dt = compare.by_difficulty(rows)
2507
- dtab = self.query_one("#difficulty", DataTable)
2508
- dtab.add_columns("Model", "Pack", *[diff_badge(l) for l in compare.LEVELS])
2509
- for line in dt:
2510
- dtab.add_row(*line)
2511
- qt = self.query_one("#quality", DataTable)
2512
- qt.add_columns("Flags", "Test", *models)
2513
- if len(models) >= 2:
2514
- for test, flags, rates in compare.items(rows):
2515
- qt.add_row(", ".join(flags), test, *[f"{100 * rates[m]:.0f}%" if m in rates else "-" for m in models])
2516
2492
  self.fill_per_question()
2517
2493
  self.fill_pta()
2518
2494
  self.fill_readiness(results_dir)
@@ -2533,6 +2509,7 @@ class ResultsScreen(Screen):
2533
2509
  @on(Select.Changed, "#pq-sort")
2534
2510
  @on(Checkbox.Changed, "#pq-partial")
2535
2511
  @on(Checkbox.Changed, "#pq-shared")
2512
+ @on(Checkbox.Changed, "#pq-disagree")
2536
2513
  def per_question_changed(self):
2537
2514
  if getattr(self, "rows", None) is not None:
2538
2515
  self.fill_per_question()
@@ -2592,7 +2569,7 @@ class ResultsScreen(Screen):
2592
2569
  self.query_one("#pta-left-out", Static).update("")
2593
2570
  return
2594
2571
  # none picked: every model with enough answers (pta.index leaves out the rest, and says so)
2595
- result = pta.index(rows, sorted(chosen) or None)
2572
+ result = pta.index(rows, sorted(chosen) or None, pta.memory_gb(self.infos))
2596
2573
  self.query_one("#pta-note", Static).update(pta.scope_note(result, sorted(present)))
2597
2574
  self.query_one("#pta-bars", Static).update("\n".join(pta.bars(result)) if result["questions"] else "")
2598
2575
  self.query_one("#pta-left-out", Static).update(
@@ -2639,6 +2616,8 @@ class ResultsScreen(Screen):
2639
2616
  shown = sorted(chosen) if chosen else sorted({m for t in tests for m in t["cells"]})
2640
2617
  if self.query_one("#pq-shared", Checkbox).value:
2641
2618
  tests = [t for t in tests if all(m in t["cells"] for m in shown)]
2619
+ if self.query_one("#pq-disagree", Checkbox).value: # some models got it right, others didn't
2620
+ tests = [t for t in tests if len({c["passed"] == c["n"] for c in t["cells"].values()}) > 1]
2642
2621
  self.fill_pq_summary(tests, shown)
2643
2622
  order = {}
2644
2623
  for name, pk in self.app.engine.packs.items():
@@ -2769,38 +2748,6 @@ class ResultsScreen(Screen):
2769
2748
  return
2770
2749
  self.app.push_screen(AnswerScreen(entries, what="repeat"))
2771
2750
 
2772
- @on(Select.Changed, "#speed-machine")
2773
- def machine_changed(self, event):
2774
- if getattr(self, "rows", None) is not None:
2775
- self.fill_speed(event.value or "")
2776
-
2777
- def fill_speed(self, machine_id):
2778
- e = self.app.engine
2779
- speeds = {}
2780
- if machine_id:
2781
- mc = next(x for x in e.machines() if x.id == machine_id)
2782
- speeds = {m["label"]: (e.serving(m, mc).profile or {}).get("measured") for m in e.cfg["models"]}
2783
- header, table, _ = compare.speed(self.rows, self.infos, machine_id or None, speeds)
2784
- sp = self.query_one("#speed", DataTable)
2785
- sp.clear(columns=True)
2786
- sp.add_columns(*header)
2787
- for line in table:
2788
- sp.add_row(*([f"[yellow]{line[0]}[/yellow]"] + line[1:]))
2789
-
2790
- def fill_history(self):
2791
- e = self.app.engine
2792
- t = self.query_one("#history", DataTable)
2793
- t.add_columns("When", "Model", "Use case", "Change", "Questions", "Why")
2794
- rank = {"FAIL": 0, "INCONCLUSIVE": 1, "NO DATA": 1, "PASS": 2}
2795
- for h in reversed(verdict.load_history(e)):
2796
- prev, now = h.get("previous"), h["status"]
2797
- change = f"[{VERDICT_STYLE[now]}]{now}[/]" if not prev else \
2798
- f"{prev} → [{VERDICT_STYLE[now]}]{now}[/]"
2799
- if prev and rank.get(now, 1) < rank.get(prev, 1):
2800
- change += " [bold red]⚠ worse[/bold red]"
2801
- qs = "current" if verdict.is_current(h, e) else "[yellow]changed since[/yellow]"
2802
- t.add_row(h["time"].replace("T", " "), h["model"], h["use_case"], change, qs, "; ".join(h["reasons"])[:120])
2803
-
2804
2751
  def fill_readiness(self, results_dir):
2805
2752
  style = {"PASS": "bold green", "FAIL": "bold red", "INCONCLUSIVE": "yellow", "NO DATA": "dim"}
2806
2753
  matrix = self.query_one("#readiness", DataTable)
@@ -2812,17 +2759,18 @@ class ResultsScreen(Screen):
2812
2759
  return
2813
2760
  help_text.update(self.READINESS_HELP)
2814
2761
  table = verdict.readiness(self.app.engine)
2815
- short = ["[b]In short[/b]"]
2762
+ short = ["[b]In short[/b]"] # one line per model; what to run next is in the table below
2816
2763
  for label, sentence, todo in verdict.plain_summary(table):
2817
2764
  short.append(f" [b]{rich_escape(label)}[/b]: {rich_escape(sentence)}")
2818
- short += [f" → {rich_escape(g)}: {rich_escape(what)}" for g, what, _ in todo]
2819
2765
  if any("Certify" in what for _, _, todo in verdict.plain_summary(table) for _, what, _ in todo):
2820
2766
  short.append(" [dim]To run Certify: in Setup tick the model and pack, choose Certify, press s.[/dim]")
2821
2767
  self.query_one("#readiness-short", Static).update("\n".join(short) if table else
2822
2768
  "[dim]No verdicts yet: run something from Setup.[/dim]")
2823
2769
  groups = sorted({g for t in table.values() for g in t})
2824
2770
  matrix.add_columns("Model", *groups)
2825
- detail.add_columns("Model", "Use case", "Pack", "Verdict", "Why")
2771
+ detail.add_columns("Model", "Use case", "Pack", "Verdict", "Answers right", "Why")
2772
+ right = collections.Counter((r["model"], r["suite"]) for r in self.rows if r["ok"])
2773
+ answered = collections.Counter((r["model"], r["suite"]) for r in self.rows)
2826
2774
  lat = verdict.latency_table(self.app.engine, None, table)
2827
2775
  machine_ids = [mc.id for mc in self.app.engine.machines()]
2828
2776
  lt = self.query_one("#latency", DataTable)
@@ -2839,7 +2787,9 @@ class ResultsScreen(Screen):
2839
2787
  for g, (_, packs) in gs.items():
2840
2788
  for name, pv in packs.items():
2841
2789
  if pv.status != "NO DATA":
2842
- detail.add_row(label, g, name, f"[{style[pv.status]}]{pv.status}[/]", "; ".join(pv.reasons)[:160])
2790
+ n = answered[(label, name)]
2791
+ detail.add_row(label, g, name, f"[{style[pv.status]}]{pv.status}[/]",
2792
+ f"{right[(label, name)]}/{n}" if n else "-", "; ".join(pv.reasons)[:160])
2843
2793
 
2844
2794
 
2845
2795
  # ------------------------------------------------------------------ app
@@ -2900,7 +2850,6 @@ class EvalsApp(App):
2900
2850
  #machine { padding: 0 1; height: auto; }
2901
2851
  #tune-status { padding: 0 1; height: auto; min-height: 2; background: $boost; }
2902
2852
  #tune-log { height: 1fr; }
2903
- #speed-machine { width: 60; margin: 0 1; }
2904
2853
  #results-top { height: auto; }
2905
2854
  #results-info { width: 1fr; height: auto; }
2906
2855
  #toggle-smoke { width: auto; min-width: 32; }
@@ -2911,7 +2860,7 @@ class EvalsApp(App):
2911
2860
  #pta { width: 1fr; height: auto; margin-top: 1; }
2912
2861
  #pta-left-out { height: auto; padding: 1 0 0 0; }
2913
2862
  #pq-controls Checkbox { width: auto; }
2914
- #pq-controls Select { width: 28; margin-right: 1; }
2863
+ #pq-controls Select { width: 22; margin-right: 1; }
2915
2864
  #pq-models { margin-right: 1; min-width: 20; }
2916
2865
  #pq-compare-title { padding: 0 1; height: auto; }
2917
2866
  #pq-summary { height: auto; max-height: 10; }
tuieval/verdict.py CHANGED
@@ -259,7 +259,8 @@ def _plain_fail(v):
259
259
  if "cut off" in text:
260
260
  return "too many answers cut off by max_tokens"
261
261
  if "accuracy" in text and ev.get("trials"):
262
- return f"{100 * ev['accuracy']:.0f}% right, under the bar"
262
+ from .compare import pct # local: compare imports nothing from here, but keep verdict light
263
+ return f"{pct(ev['accuracy'])} right, under the bar"
263
264
  return text.split(" (")[0]
264
265
 
265
266
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tuieval
3
- Version: 0.2.0.dev55
3
+ Version: 0.2.0.dev62
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
@@ -144,7 +144,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
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
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.
147
- 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.
147
+ 4. **Press `r` for results:** the PTA index (parsimony (tokens), time and accuracy as bars), production readiness, per question, and failures. `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.
150
150
 
@@ -176,7 +176,7 @@ tuieval help
176
176
 
177
177
  - **Start with Production readiness.** A single critical failure already means FAIL, whatever the accuracy: a model that breaks a hard rule once in 300 answers will do it in production. The Failures tab and `tuieval report` list exactly which answers failed.
178
178
  - **INCONCLUSIVE is not "nearly passed".** It says what's missing (usually a Certify run, or more trials).
179
- - **Trust "Is the difference real?" over raw percentages.** Differences of one or two tests are usually noise.
179
+ - **Trust `tuieval compare --pairwise` over raw percentages.** Differences of one or two tests are usually noise.
180
180
  - **Read `trunc` before accuracy.** A model that runs out of tokens isn't wrong; it's thinking too long for the budget.
181
181
  - **Watch TTFT for interactive use.** A model that is 5% more accurate but takes 3 s longer to start answering may be the worse choice.
182
182
 
@@ -1,25 +1,25 @@
1
1
  tuieval/__init__.py,sha256=myVXVX7htbwezW6k3x_FuKgn1RSZ2WcTaDEMByGn6Wg,312
2
2
  tuieval/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
3
- tuieval/_version.py,sha256=1ZPMt5NERZZN86PFadmP7kAdrSCtNEk8dYfg9p2vGXg,535
3
+ tuieval/_version.py,sha256=Q5t2lKtGGZuhOAPSq1URLDDN-gL2EG-f55AiFxUzLv0,535
4
4
  tuieval/builtin.toml,sha256=Kxw22RZCJhURUyu5Wz8tj5Y1nM85A8hFVRbA4ocoPUg,4293
5
- tuieval/cli.py,sha256=gzGBzhqvR0WKJQQNHB8CudpgViIeIyawrNWa54whmoI,4502
5
+ tuieval/cli.py,sha256=fn6z0cupM-aW3fY5fFgnS-HmOQmMzlDg-U76Y6LgL3o,4510
6
6
  tuieval/client.py,sha256=kpsyLEZKfxCsvIGBHBbBy83TpI97CvnGCDq-4AbJS4Y,9986
7
- tuieval/compare.py,sha256=SHN_o_nlppFcHMBqBZRITIkeQDPaR_JxhUZItQLMBwo,26270
7
+ tuieval/compare.py,sha256=V_K34LFnDvd7xhGDS4zZDCguaIMU05f5mCbPx0VpUOA,26944
8
8
  tuieval/doctor.py,sha256=y1C_ggjd8oikXTrovgTBf26wjThP8EoVkfTVeO_n5dQ,6070
9
- tuieval/engine.py,sha256=w3KsgxzFAMcRyVzFGQXhTgN3d9qYRtAZZxNLXZOgnrQ,114222
9
+ tuieval/engine.py,sha256=0PuHFus1k02qAY8nlsI6z5iQo7IT82bMwmHDZT6fGtU,114784
10
10
  tuieval/export.py,sha256=dbHQgL_ubWtIPM1sLzAew_gETnrDJyd0zgqVA5vkpXM,9657
11
11
  tuieval/machines.py,sha256=tdRrn3WNOmCHwfcxcWz1NuAuOJdI7WnJFR1y6xgF89c,13512
12
12
  tuieval/onboard.py,sha256=IYETVDWCCo9vF0Dtj4_rr9VanYeCky8PwP3-2g5A_l0,6215
13
13
  tuieval/packs.py,sha256=tiLmxsrX_M5EUMuR4Yvb7pLBLEI15wamHSjflD6wjoY,11299
14
14
  tuieval/profiles.py,sha256=soeLxjdB-l-OxSY8jZ4yjwfdSnsBV-QICcwKxRJS7nQ,3663
15
- tuieval/pta.py,sha256=KkrOxE1hTDwu1435IrzMzIysrgVUvFuKVo786VlRyK4,9056
15
+ tuieval/pta.py,sha256=6HwPMvTTJFoZjO-2ZurucJrp1itIzpDTKL1xvYP9W0w,10888
16
16
  tuieval/remove.py,sha256=Vwh43nm9lVb9U9aN-h6CMYbkTwdR3UWfZObwweF72n0,5157
17
- tuieval/run_evals.py,sha256=5bQEqU533XYiLze9dYcl72c5GSIpohMj1eqJrePRwnc,46450
17
+ tuieval/run_evals.py,sha256=1zNkCdFt744cqpebqiyj-sZrbOI22c6IYBcXk9koz20,46538
18
18
  tuieval/scaffold.py,sha256=YBOqniJpf_GtKKAFF3_OHxxMGbmTWdFjcClGcOnrrKE,12130
19
19
  tuieval/selftest.py,sha256=nExkrOidgymlmvkxImEBfrQG5iTUN5WuRMIKtqusCCs,9835
20
- tuieval/tui.py,sha256=21Re8fM5rf3zbHIKvK9laMV-mL2HjcGfMReoRg0DRLw,164268
20
+ tuieval/tui.py,sha256=lrIv4z21s9OG809706Z-lE2-TZ6gg8z0AjaJ_zW7zkw,161021
21
21
  tuieval/tune.py,sha256=jXwYuSjfdqbLCE3WonP2xaUHwk_FnhI67twB1BaHggI,38992
22
- tuieval/verdict.py,sha256=Muo1a05mCv781MlEJsXwDsGaKbRi0JS4Lv8HdvK_Aco,19054
22
+ tuieval/verdict.py,sha256=VB-0LTxR9y8bJHT9RicxtE9K7cUHBdCu40hOCrsbmA4,19150
23
23
  tuieval/watch_proxy.py,sha256=bnDO1i6n6yUs1hODoUWcMppJMGPavoQqs6aNYrIxpT8,11416
24
24
  tuieval/workspace.py,sha256=V_HhkaN1odr0OpjNsPYradRWLpzDldM7SwpvxohBL4U,895
25
25
  tuieval/yamlout.py,sha256=r1IokjLqW-Ztpg1fSn33ZW3VK00v1VvxymmU5Qy7GEw,606
@@ -46,8 +46,8 @@ tuieval/templates/packs/tool_call/pack.toml,sha256=n4bx0lGJHSmW41uVqhfIXJ2BWZpmT
46
46
  tuieval/templates/packs/tool_call/system.txt,sha256=mKxu-lB-0YjLHXdgeFJa8x2C180OvcNz5cHv2N5Bg3U,254
47
47
  tuieval/templates/packs/tool_call/tests.yaml,sha256=Nv7c4syrzJ9cU8EBKSK_1ZSFx9V02aCjW4-2TBCPT7s,1859
48
48
  tuieval/templates/packs/tool_call/tools.yaml,sha256=HgxCkh0On2VwbHSsjAGM8tNCK7ZtKLGrs7lMCn3Fprs,1048
49
- tuieval-0.2.0.dev55.dist-info/METADATA,sha256=DGIO8VwyZC4CMfIgLwiY7gpg24S1xfuPDakBj_W90qo,13881
50
- tuieval-0.2.0.dev55.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
51
- tuieval-0.2.0.dev55.dist-info/entry_points.txt,sha256=7eIhi1GpFxBP2OLJV02Nn8LwsNuO512eKOGt1IBbj04,45
52
- tuieval-0.2.0.dev55.dist-info/licenses/LICENSE,sha256=7wfRgGtBGH1aXhZkIn47oUznqKC18kEPMvUVgol7Ffc,1077
53
- tuieval-0.2.0.dev55.dist-info/RECORD,,
49
+ tuieval-0.2.0.dev62.dist-info/METADATA,sha256=X2v6zWEn3E0Lkm-W2x-pjAHgG6qjjzFw_BqeSvyyIxI,13898
50
+ tuieval-0.2.0.dev62.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
51
+ tuieval-0.2.0.dev62.dist-info/entry_points.txt,sha256=7eIhi1GpFxBP2OLJV02Nn8LwsNuO512eKOGt1IBbj04,45
52
+ tuieval-0.2.0.dev62.dist-info/licenses/LICENSE,sha256=7wfRgGtBGH1aXhZkIn47oUznqKC18kEPMvUVgol7Ffc,1077
53
+ tuieval-0.2.0.dev62.dist-info/RECORD,,