tuieval 0.2.0.dev43__py3-none-any.whl → 0.2.0.dev50__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.dev43'
22
- __version_tuple__ = version_tuple = (0, 2, 0, 'dev43')
21
+ __version__ = version = '0.2.0.dev50'
22
+ __version_tuple__ = version_tuple = (0, 2, 0, 'dev50')
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: privacy, time and accuracy per model, as a triangle
17
+ tuieval pta the PTA index: parsimony, speed 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/pta.py CHANGED
@@ -1,145 +1,133 @@
1
- """PTA index: privacy, time and accuracy per model, 0-100 each, drawn as a radar triangle.
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.
2
3
 
3
- P privacy 100 when prompts stay on machines you control: tuieval starts the server, its URL
4
- is this machine, or the server is marked private = true in models.toml. 0 otherwise
5
- (a hosted API: there's no partial privacy once prompts leave).
6
- T time the total time to answer every question compared (each question at its median over
7
- repeats), relative to the fastest model: 100 x fastest total / this model's total.
8
- A accuracy the share of answers to those questions that passed.
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.
9
11
 
10
12
  Models are compared on the questions all of them answered, so one that skipped some never looks
11
- faster. There's no combined number: the three stay side by side, and verdicts and critical
12
- failures stay where they are (verdict.py).
13
+ faster. Server and connection errors aren't answers. By default a model with fewer than half as many
14
+ answers as the most-answered model is left out (listed, so it can be picked), or it would shrink the
15
+ shared questions for everyone. There's no combined number: the three stay side by side, and verdicts
16
+ and critical failures stay where they are (verdict.py).
13
17
  """
14
- import urllib.parse
18
+ import math
19
+
20
+ from rich.markup import escape as rich_escape
15
21
 
16
22
  from . import compare
17
23
 
18
- LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1", "0.0.0.0"}
19
- # one colour per model on the triangle; more models than colours are listed but not drawn
20
- COLORS = ["cyan", "magenta", "yellow", "green", "red", "blue", "bright_white", "bright_cyan"]
24
+ MIN_SHARE = 0.5 # fewer answers than this share of the most-answered model's: left out by default
25
+ POINTS_PER_DOUBLING = 15
21
26
 
22
27
 
23
- def privacy(cfg, label):
24
- """100 if a model's prompts stay on machines you control, else 0 (see the module docstring);
25
- None for a model that's no longer in models.toml (where it ran isn't known)."""
26
- m = next((m for m in cfg["models"] if m["label"] == label), None)
27
- server = cfg["servers"].get(m["server"], {}) if m else {}
28
- if not server:
28
+ def score(total, best):
29
+ """P and T: 100 for the best (lowest) total, POINTS_PER_DOUBLING less for every doubling, never below 0."""
30
+ if not total or not best:
29
31
  return None
30
- if server.get("cmd") or server.get("private"):
31
- return 100
32
- host = urllib.parse.urlparse(server.get("url", "")).hostname or ""
33
- return 100 if host.lower() in LOCAL_HOSTS else 0
32
+ return max(0.0, 100 - POINTS_PER_DOUBLING * math.log2(total / best))
34
33
 
35
34
 
36
- def index(rows, privacy_of, models=None):
37
- """{"questions": n shared, "models": [{model, P, T, A, total_s, passed, answers, alongside}]},
38
- best accuracy first. models: the ones to compare (default: every model in rows)."""
35
+ def index(rows, models=None):
36
+ """{"questions": n shared, "models": [{model, P, T, A, tokens, total_s, passed, answers, alongside,
37
+ fewest, fastest}],
38
+ "left_out": [(model, questions answered)], "no_answers": [model], "most": questions answered by the
39
+ 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)."""
41
+ no_answers = sorted({r["model"] for r in rows} - {r["model"] for r in rows if not r.get("error")})
42
+ rows = [r for r in rows if not r.get("error")] # a server or connection error isn't an answer
39
43
  tests = compare.per_question(rows)
40
- models = sorted(models or {m for t in tests for m in t["cells"]})
44
+ counts = {}
45
+ for t in tests:
46
+ for m in t["cells"]:
47
+ counts[m] = counts.get(m, 0) + 1
48
+ most = max(counts.values(), default=0)
49
+ if models:
50
+ no_answers = [m for m in no_answers if m in models]
51
+ models, left_out = sorted(m for m in models if m in counts), []
52
+ else:
53
+ models = sorted(m for m in counts if counts[m] >= MIN_SHARE * most)
54
+ left_out = sorted(((m, n) for m, n in counts.items() if m not in models), key=lambda x: (-x[1], x[0]))
41
55
  shared = [t for t in tests if models and all(m in t["cells"] for m in models)]
42
56
  out = []
43
57
  for m in models:
44
58
  cells = [t["cells"][m] for t in shared]
45
59
  answers = sum(c["n"] for c in cells)
46
- out.append({"model": m, "P": privacy_of(m), "total_s": sum(c["secs"] or 0 for c in cells),
60
+ out.append({"model": m, "tokens": sum(c["tokens"] or 0 for c in cells),
61
+ "total_s": sum(c["secs"] or 0 for c in cells),
47
62
  "passed": sum(c["passed"] for c in cells), "answers": answers,
48
63
  "A": 100 * sum(c["passed"] for c in cells) / answers if answers else None,
49
64
  # answers that ran while other models were served shared the machine's speed
50
65
  "alongside": any(r.get("alongside") for c in cells for r in c["rows"])})
51
66
  fastest = min((x["total_s"] for x in out if x["total_s"] > 0), default=None)
67
+ fewest = min((x["tokens"] for x in out if x["tokens"] > 0), default=None)
52
68
  for x in out:
53
- x["T"] = 100 * fastest / x["total_s"] if fastest and x["total_s"] > 0 else None
69
+ x["P"], x["T"] = score(x["tokens"], fewest), score(x["total_s"], fastest)
70
+ x["fewest"] = bool(fewest) and x["tokens"] == fewest
71
+ x["fastest"] = bool(fastest) and x["total_s"] == fastest
54
72
  out.sort(key=lambda x: (-(x["A"] or 0), -(x["T"] or 0), x["model"]))
55
- return {"questions": len(shared), "models": out}
56
-
57
-
58
- # ---------------------------------------------------------------- the triangle (braille, no dependencies)
59
- _DOTS = ((0x01, 0x08), (0x02, 0x10), (0x04, 0x20), (0x40, 0x80)) # braille dot bits by (row, column)
60
-
61
-
62
- class _Canvas:
63
- """Braille cells, 2 dots wide and 4 tall each; every cell takes the colour of the last line through it."""
64
- def __init__(self, cols, rows):
65
- self.cols, self.rows = cols, rows
66
- self.bits = [[0] * cols for _ in range(rows)]
67
- self.color = [[None] * cols for _ in range(rows)]
68
-
69
- def dot(self, x, y, color):
70
- cx, cy = int(x) // 2, int(y) // 4
71
- if 0 <= cx < self.cols and 0 <= cy < self.rows:
72
- self.bits[cy][cx] |= _DOTS[int(y) % 4][int(x) % 2]
73
- self.color[cy][cx] = color
74
-
75
- def line(self, a, b, color, dotted=False):
76
- steps = int(max(abs(b[0] - a[0]), abs(b[1] - a[1]))) or 1
77
- for i in range(steps + 1):
78
- if dotted and i % 3:
79
- continue
80
- self.dot(a[0] + (b[0] - a[0]) * i / steps, a[1] + (b[1] - a[1]) * i / steps, color)
81
-
82
- def text(self):
83
- lines = []
84
- for bits, colors in zip(self.bits, self.color):
85
- out = []
86
- for b, c in zip(bits, colors):
87
- ch = chr(0x2800 + b) if b else " "
88
- out.append(f"[{c}]{ch}[/]" if b and c else ch)
89
- lines.append("".join(out).rstrip())
90
- return lines
73
+ return {"questions": len(shared), "models": out, "left_out": left_out, "no_answers": no_answers,
74
+ "most": most}
75
+
76
+
77
+ LETTERS = (("P", "parsimony", "/100", "cyan"), ("T", "speed", "/100", "magenta"), ("A", "accuracy", "%", "green"))
78
+
79
+
80
+ 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)."""
83
+ if not result["models"]:
84
+ return []
85
+ name_w = max(len(x["model"]) for x in result["models"])
86
+ cell_w = width + 6 # the bar, a space and "100%" or " 100"
87
+ head = " " * (name_w + 2) + "".join(f"{f'{k} {name}':<{cell_w}}" for k, name, _, _ in LETTERS)
88
+ lines = [f"[b]{head.rstrip()}[/b]" if markup else head.rstrip()]
89
+ for x in result["models"]:
90
+ cells = []
91
+ for k, _, unit, color in LETTERS:
92
+ v = x[k]
93
+ full = 0 if v is None else round(width * max(0, min(100, v)) / 100)
94
+ bar, rest = "█" * full, "░" * (width - full)
95
+ num = "-" if v is None else f"{v:.0f}{'%' if unit == '%' else ''}"
96
+ bar = f"[{color}]{bar}[/][grey37]{rest}[/]" if markup else bar + rest
97
+ cells.append(f"{bar} {num:>4}")
98
+ name = f"{x['model']:<{name_w}}"
99
+ lines.append(f"{rich_escape(name) if markup else name} " + " ".join(cells))
100
+ return lines
91
101
 
92
102
 
93
- def triangle(result, width=60):
94
- """The radar triangle as Rich markup lines: P at the top, T bottom left, A bottom right; a
95
- model's triangle reaches each corner as far as its score (bigger = better all round)."""
96
- cols = max(20, width)
97
- w = cols * 2 - 2 # dots across
98
- h = int(w * 0.866) # an equilateral triangle (braille dots are about square)
99
- rows = h // 4 + 1
100
- top, left, right = (w / 2, 0), (0, h), (w, h)
101
- center = (w / 2, h * 2 / 3)
102
- c = _Canvas(cols, rows)
103
-
104
- def at(corner, score):
105
- return (center[0] + (corner[0] - center[0]) * score / 100, center[1] + (corner[1] - center[1]) * score / 100)
106
-
107
- for corner in (top, left, right): # axes, the 50% ring, then the frame
108
- c.line(center, corner, "grey37", dotted=True)
109
- half = [at(k, 50) for k in (top, left, right)]
110
- for a, b in zip(half, half[1:] + half[:1]):
111
- c.line(a, b, "grey37", dotted=True)
112
- for a, b in ((top, left), (left, right), (right, top)):
113
- c.line(a, b, "grey70")
114
- for i, x in enumerate(result["models"][:len(COLORS)][::-1]): # the best ends on top
115
- color = COLORS[len(result["models"][:len(COLORS)]) - 1 - i]
116
- pts = [at(k, s or 0) for k, s in ((top, x["P"]), (left, x["T"]), (right, x["A"]))]
117
- for a, b in zip(pts, pts[1:] + pts[:1]):
118
- c.line(a, b, color)
119
- lines = c.text()
120
- pad = " " * max(0, cols // 2 - 5)
121
- return ([f"{pad}[b]P[/b] privacy"] + lines
122
- + [f"[b]T[/b] time{' ' * max(1, cols - 15)}[b]A[/b] accuracy"])
123
-
124
-
125
- def legend(result):
126
- """One Rich markup line per model: its colour on the triangle and its three scores."""
103
+ def left_out_lines(result):
104
+ """Plain sentences on the models not compared (too few answers, or only server errors) and on
105
+ answers that shared the machine with other models."""
127
106
  out = []
128
- for i, x in enumerate(result["models"]):
129
- mark = f"[{COLORS[i]}]■[/]" if i < len(COLORS) else " "
130
- out.append(f"{mark} {x['model']} P {_score(x['P'])} · T {_score(x['T'])} · A {_score(x['A'])}"
131
- + (" [yellow](some answers ran alongside other models)[/yellow]" if x["alongside"] else ""))
107
+ if result["left_out"]:
108
+ out.append("Left out (fewer than half as many answers as the most-answered model; pick them under "
109
+ "Models or --only to include them): "
110
+ + ", ".join(f"{m} ({n} of {result['most']})" for m, n in result["left_out"]))
111
+ if result["no_answers"]:
112
+ out.append("No answers, only server errors: " + ", ".join(result["no_answers"]))
113
+ shared = [x["model"] for x in result["models"] if x["alongside"]]
114
+ if shared:
115
+ out.append("Some answers ran alongside other models, which shared the machine's speed: " + ", ".join(shared))
132
116
  return out
133
117
 
134
118
 
135
- def _score(v):
136
- return "-" if v is None else f"{v:.0f}"
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}"
137
122
 
138
123
 
139
124
  def table(result):
140
125
  """(header, rows) for a plain table of the index."""
141
- header = ["model", "P privacy", "T time", "A accuracy", "total time", "answers right"]
142
- rows = [[x["model"], _score(x["P"]), _score(x["T"]), _score(x["A"]), fmt_total(x["total_s"]),
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 ""),
143
131
  f"{x['passed']}/{x['answers']}"] for x in result["models"]]
144
132
  return header, rows
145
133
 
@@ -158,8 +146,10 @@ def scope_note(result, models):
158
146
  """What the scores compare, in a sentence."""
159
147
  if not result["questions"]:
160
148
  return "These models have no question in common yet, so there's nothing to compare."
161
- return (f"Compared on the {result['questions']} questions all {len(models)} model(s) answered. "
162
- "T is relative to the fastest total, so adding a faster model lowers the others.")
149
+ 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.")
163
153
 
164
154
 
165
155
  def for_engine(e, labels=None, packs=None, results_dir=None):
@@ -169,7 +159,7 @@ def for_engine(e, labels=None, packs=None, results_dir=None):
169
159
  if packs:
170
160
  rows = [r for r in rows if r["suite"] in packs]
171
161
  models = sorted({r["model"] for r in rows} & set(labels) if labels else {r["model"] for r in rows})
172
- return index(rows, lambda m: privacy(e.cfg, m), models), models
162
+ return index(rows, models if labels else None), models
173
163
 
174
164
 
175
165
  def markdown(result, models):
@@ -177,9 +167,10 @@ def markdown(result, models):
177
167
  lines = ["## PTA index", "", scope_note(result, models), ""]
178
168
  if not result["questions"]:
179
169
  return lines
170
+ lines += ["```"] + bars(result, markup=False) + ["```", ""]
180
171
  header, rows = table(result)
181
172
  lines += ["| " + " | ".join(header) + " |", "|" + "---|" * len(header)]
182
173
  lines += ["| " + " | ".join(r) + " |" for r in rows]
183
- if any(x["alongside"] for x in result["models"]):
184
- lines += ["", "Some answers ran alongside other models, which shared the machine's speed."]
174
+ for line in left_out_lines(result):
175
+ lines += ["", line]
185
176
  return lines
tuieval/run_evals.py CHANGED
@@ -389,11 +389,11 @@ def report_markdown(e, labels=None):
389
389
 
390
390
  def cmd_pta(argv):
391
391
  p = argparse.ArgumentParser(prog="tuieval pta",
392
- description="The PTA index: privacy, time and accuracy per model (0-100 each), as a "
393
- "triangle and a table, over the questions all the models answered.")
392
+ description="The PTA index: parsimony (tokens), speed and accuracy per model (0-100, higher is better), as a "
393
+ "bar per score for each model and a table, over the questions all the models answered.")
394
394
  p.add_argument("--only", help="comma-separated model labels (default: every model with results)")
395
395
  p.add_argument("--packs", help="comma-separated packs (default: all)")
396
- p.add_argument("--width", type=int, default=48, help="triangle width in characters (default 48)")
396
+ p.add_argument("--width", type=int, default=20, help="bar width in characters (default 20)")
397
397
  p.add_argument("--models")
398
398
  p.add_argument("--results-dir")
399
399
  a = p.parse_args(argv)
@@ -408,15 +408,20 @@ def cmd_pta(argv):
408
408
  sys.exit("no finished results to compare" + (" for those models or packs" if labels or packs else ""))
409
409
  out.print(pta.scope_note(result, models))
410
410
  if not result["questions"]:
411
+ for line in pta.left_out_lines(result):
412
+ out.print(line, markup=False)
411
413
  return
412
414
  out.print()
413
- for line in pta.triangle(result, a.width) + [""] + pta.legend(result):
415
+ for line in pta.bars(result, a.width):
414
416
  out.print(line)
415
417
  header, rows = pta.table(result)
416
418
  widths = [max(len(h), *(len(r[i]) for r in rows)) for i, h in enumerate(header)]
417
419
  out.print()
418
420
  for line in [header] + rows:
419
421
  out.print(" ".join(c.ljust(w) for c, w in zip(line, widths)).rstrip(), markup=False)
422
+ for line in pta.left_out_lines(result):
423
+ out.print()
424
+ out.print(line, markup=False)
420
425
 
421
426
 
422
427
  def cmd_report(argv):
tuieval/tui.py CHANGED
@@ -10,10 +10,10 @@ 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 Per question, which compares each model's tokens and seconds on the same
14
- question. ctrl+r reloads (it also reloads when a run's pack finishes). Enter on a
15
- Failures row or a Per question cell shows the answer: grading and checks, reasoning,
16
- answer, question.
13
+ Results: opens on the PTA index (parsimony, speed and accuracy per model, as bars). Per
14
+ question compares each model's tokens and seconds on the same question. ctrl+r
15
+ reloads (it also reloads when a run's pack finishes). Enter on a Failures row or a
16
+ Per question cell shows the answer: grading and checks, reasoning, answer, question.
17
17
  Answer detail: [ and ] step to the previous/next answer; esc/q close.
18
18
  Anywhere: esc/q go back (q quits only on Setup), ctrl+q quits, w lists this session's runs and
19
19
  tunes. Leaving a live run or tune keeps it going; the header shows its progress.
@@ -331,11 +331,11 @@ with the model's reasoning and answer as they stream, then recent results with t
331
331
  v with several models at a time: stream the next one (k skips the one streaming)""",
332
332
  "ResultsScreen": """[b]Results[/b]
333
333
 
334
- Opens on [b]Per question[/b]. [b]Production readiness[/b] gives one verdict per model and use case,
335
- and what's missing when it's INCONCLUSIVE. The other tabs are the evidence:
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:
336
337
  Scorecard accuracy, critical failures and truncation per model and pack
337
338
  Speed & tokens time and tokens per answer (★ = nothing beats it on both accuracy and time)
338
- PTA index privacy, total time and accuracy per model (0-100 each), as a triangle
339
339
  Per question every question, model by model
340
340
  Is the difference real? whether one model is really better than another, or it's noise
341
341
  Tests that separate the questions that tell models apart
@@ -2341,7 +2341,18 @@ class ResultsScreen(Screen):
2341
2341
  yield Static(id="results-info")
2342
2342
  if self.smoke_dir:
2343
2343
  yield Button("Show this Smoke run's results", id="toggle-smoke")
2344
- with TabbedContent(initial="tab-per-question"):
2344
+ with TabbedContent(initial="tab-pta"):
2345
+ with TabPane("PTA index", id="tab-pta"):
2346
+ with Horizontal(id="pta-controls"):
2347
+ yield Select([("All packs", "")], id="pta-pack", allow_blank=False, value="")
2348
+ 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]")
2352
+ yield Static(id="pta-note")
2353
+ yield Static(id="pta-bars")
2354
+ yield DataTable(id="pta", zebra_stripes=True, cursor_type="none")
2355
+ yield Static(id="pta-left-out")
2345
2356
  with TabPane("Production readiness"):
2346
2357
  yield Static(id="readiness-short")
2347
2358
  yield Static(self.READINESS_HELP, id="readiness-help")
@@ -2366,19 +2377,6 @@ class ResultsScreen(Screen):
2366
2377
  "On another machine, times are measured there if the model ran there, otherwise "
2367
2378
  "projected from each answer's token counts and that machine's tuned speeds.[/dim]")
2368
2379
  yield DataTable(id="speed", zebra_stripes=True)
2369
- with TabPane("PTA index", id="tab-pta"):
2370
- with Horizontal(id="pta-controls"):
2371
- yield Select([("All packs", "")], id="pta-pack", allow_blank=False, value="")
2372
- yield Button("Models: all", id="pta-models")
2373
- yield Static("[dim]P privacy: 100 when prompts stay on machines you control (local, or a server "
2374
- "marked private = true), 0 for a hosted API. T time: total time for every question "
2375
- "compared, relative to the fastest model (100). A accuracy: answers right. Bigger "
2376
- "triangle = better all round; verdicts and critical failures are in Production "
2377
- "readiness.[/dim]")
2378
- yield Static(id="pta-note")
2379
- with Horizontal(id="pta-body"):
2380
- yield Static(id="pta-triangle")
2381
- yield DataTable(id="pta", zebra_stripes=True, cursor_type="none")
2382
2380
  with TabPane("Per question", id="tab-per-question"):
2383
2381
  with Horizontal(id="pq-controls"):
2384
2382
  yield Select([("All packs", "")], id="pq-pack", allow_blank=False, value="")
@@ -2586,21 +2584,22 @@ class ResultsScreen(Screen):
2586
2584
  present = {r["model"] for r in rows}
2587
2585
  chosen = {m for m in self.app.pq_models if m in present}
2588
2586
  self.query_one("#pta-models", Button).label = f"Models: {len(chosen)} picked" if chosen else "Models: all"
2589
- models = sorted(chosen or present)
2590
2587
  t = self.query_one("#pta", DataTable)
2591
2588
  t.clear(columns=True)
2592
- if not models:
2589
+ if not present:
2593
2590
  self.query_one("#pta-note", Static).update("No finished results to compare yet.")
2594
- self.query_one("#pta-triangle", Static).update("")
2591
+ self.query_one("#pta-bars", Static).update("")
2592
+ self.query_one("#pta-left-out", Static).update("")
2595
2593
  return
2596
- result = pta.index(rows, lambda m: pta.privacy(e.cfg, m), models)
2597
- extra = (f" [yellow]{len(pta.COLORS)} models are drawn at most; pick fewer with Models.[/yellow]"
2598
- if len(models) > len(pta.COLORS) else "")
2599
- self.query_one("#pta-note", Static).update(pta.scope_note(result, models) + extra)
2600
- self.query_one("#pta-triangle", Static).update(
2601
- "\n".join(pta.triangle(result, 48) + [""] + pta.legend(result)) if result["questions"] else "")
2594
+ # 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)
2596
+ self.query_one("#pta-note", Static).update(pta.scope_note(result, sorted(present)))
2597
+ self.query_one("#pta-bars", Static).update("\n".join(pta.bars(result)) if result["questions"] else "")
2598
+ self.query_one("#pta-left-out", Static).update(
2599
+ "\n".join(f"[dim]{rich_escape(line)}[/dim]" for line in pta.left_out_lines(result)))
2602
2600
  header, table = pta.table(result)
2603
- t.add_columns(*header)
2601
+ for i, h in enumerate(header): # sized to the contents: added before the rows, a column keeps its header's width
2602
+ t.add_column(h, width=max(len(h), *(len(r[i]) for r in table)) if table else None)
2604
2603
  t.add_rows(table)
2605
2604
 
2606
2605
  def fill_per_question(self):
@@ -2908,9 +2907,9 @@ class EvalsApp(App):
2908
2907
  #pq-controls { height: 3; }
2909
2908
  #pta-controls { height: 3; }
2910
2909
  #pta-controls Select { width: 28; margin-right: 1; }
2911
- #pta-body { height: auto; }
2912
- #pta-triangle { width: auto; padding: 1 2 0 0; }
2910
+ #pta-bars { height: auto; padding: 1 0 0 0; }
2913
2911
  #pta { width: 1fr; height: auto; margin-top: 1; }
2912
+ #pta-left-out { height: auto; padding: 1 0 0 0; }
2914
2913
  #pq-controls Checkbox { width: auto; }
2915
2914
  #pq-controls Select { width: 28; margin-right: 1; }
2916
2915
  #pq-models { margin-right: 1; min-width: 20; }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tuieval
3
- Version: 0.2.0.dev43
3
+ Version: 0.2.0.dev50
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
@@ -1,8 +1,8 @@
1
1
  tuieval/__init__.py,sha256=myVXVX7htbwezW6k3x_FuKgn1RSZ2WcTaDEMByGn6Wg,312
2
2
  tuieval/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
3
- tuieval/_version.py,sha256=5PoXfeeUc_RJHZEcbrdVeWQpyK7qC0e8oom2QPT307Y,535
3
+ tuieval/_version.py,sha256=cUlv5rRBNcBZ7a8L_s4uRBMTmFx7dza4nVKsHqXS0Pg,535
4
4
  tuieval/builtin.toml,sha256=Kxw22RZCJhURUyu5Wz8tj5Y1nM85A8hFVRbA4ocoPUg,4293
5
- tuieval/cli.py,sha256=kI1P0a7TMS-daHjZjLDBdBDPOwBCmPw1qjw6VDlY258,4505
5
+ tuieval/cli.py,sha256=gzGBzhqvR0WKJQQNHB8CudpgViIeIyawrNWa54whmoI,4502
6
6
  tuieval/client.py,sha256=kpsyLEZKfxCsvIGBHBbBy83TpI97CvnGCDq-4AbJS4Y,9986
7
7
  tuieval/compare.py,sha256=SHN_o_nlppFcHMBqBZRITIkeQDPaR_JxhUZItQLMBwo,26270
8
8
  tuieval/doctor.py,sha256=y1C_ggjd8oikXTrovgTBf26wjThP8EoVkfTVeO_n5dQ,6070
@@ -12,12 +12,12 @@ tuieval/machines.py,sha256=ImEihNj8QlBRdCHLED4QIdjJUvjVtZgo6DiVU5h6Zzs,13415
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=IUVEy0AEmSfGu8y_HXBtnRxuz-6HJWvEklnp9s4YGcg,8733
15
+ tuieval/pta.py,sha256=KkrOxE1hTDwu1435IrzMzIysrgVUvFuKVo786VlRyK4,9056
16
16
  tuieval/remove.py,sha256=Vwh43nm9lVb9U9aN-h6CMYbkTwdR3UWfZObwweF72n0,5157
17
- tuieval/run_evals.py,sha256=1iht7XHc4-PNdppXC2PbPEkIaRZAem7u_M6Xfe588ik,46250
17
+ tuieval/run_evals.py,sha256=5bQEqU533XYiLze9dYcl72c5GSIpohMj1eqJrePRwnc,46450
18
18
  tuieval/scaffold.py,sha256=YBOqniJpf_GtKKAFF3_OHxxMGbmTWdFjcClGcOnrrKE,12130
19
19
  tuieval/selftest.py,sha256=nExkrOidgymlmvkxImEBfrQG5iTUN5WuRMIKtqusCCs,9835
20
- tuieval/tui.py,sha256=ZuDOJQ65D8UH-JpEvaQ3eIzdGYJTstQb1ElmJki31Ko,164195
20
+ tuieval/tui.py,sha256=21Re8fM5rf3zbHIKvK9laMV-mL2HjcGfMReoRg0DRLw,164268
21
21
  tuieval/tune.py,sha256=jXwYuSjfdqbLCE3WonP2xaUHwk_FnhI67twB1BaHggI,38992
22
22
  tuieval/verdict.py,sha256=Muo1a05mCv781MlEJsXwDsGaKbRi0JS4Lv8HdvK_Aco,19054
23
23
  tuieval/watch_proxy.py,sha256=z916NRhEur04a9RnLm6ZpIEY1SnnUGPiHYbmlD9ktQ0,11035
@@ -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.dev43.dist-info/METADATA,sha256=P6oHoB_GI8wBLV-_DUqU4TggqZdTVGtz5glAfpIn2jU,13881
50
- tuieval-0.2.0.dev43.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
51
- tuieval-0.2.0.dev43.dist-info/entry_points.txt,sha256=7eIhi1GpFxBP2OLJV02Nn8LwsNuO512eKOGt1IBbj04,45
52
- tuieval-0.2.0.dev43.dist-info/licenses/LICENSE,sha256=7wfRgGtBGH1aXhZkIn47oUznqKC18kEPMvUVgol7Ffc,1077
53
- tuieval-0.2.0.dev43.dist-info/RECORD,,
49
+ tuieval-0.2.0.dev50.dist-info/METADATA,sha256=VW1tuB9QtgVUt9irfVVzrDhkF907wql1_WpM7PasfPU,13881
50
+ tuieval-0.2.0.dev50.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
51
+ tuieval-0.2.0.dev50.dist-info/entry_points.txt,sha256=7eIhi1GpFxBP2OLJV02Nn8LwsNuO512eKOGt1IBbj04,45
52
+ tuieval-0.2.0.dev50.dist-info/licenses/LICENSE,sha256=7wfRgGtBGH1aXhZkIn47oUznqKC18kEPMvUVgol7Ffc,1077
53
+ tuieval-0.2.0.dev50.dist-info/RECORD,,