tuieval 0.2.0.dev55__tar.gz → 0.2.0.dev56__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.
Files changed (77) hide show
  1. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/CHANGELOG.md +1 -0
  2. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/PKG-INFO +1 -1
  3. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/_version.py +2 -2
  4. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/engine.py +17 -7
  5. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/tui.py +35 -3
  6. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/tests/test_tuieval.py +21 -0
  7. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/.gitignore +0 -0
  8. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/LICENSE +0 -0
  9. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/README.md +0 -0
  10. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/RELEASING.md +0 -0
  11. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/README.md +0 -0
  12. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/favicon.ico +0 -0
  13. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/readme-header.png +0 -0
  14. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/social-preview.png +0 -0
  15. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/tuieval-icon-1024.png +0 -0
  16. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/tuieval-icon-128.png +0 -0
  17. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/tuieval-icon-16.png +0 -0
  18. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/tuieval-icon-16.svg +0 -0
  19. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/tuieval-icon-256.png +0 -0
  20. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/tuieval-icon-32.png +0 -0
  21. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/tuieval-icon-48.png +0 -0
  22. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/tuieval-icon-512.png +0 -0
  23. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/tuieval-icon-64.png +0 -0
  24. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/tuieval-icon-animated.svg +0 -0
  25. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/brand/tuieval-icon.svg +0 -0
  26. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/tui-answer-reasoning.png +0 -0
  27. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/tui-run-reasoning.png +0 -0
  28. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/images/tui-setup.png +0 -0
  29. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/models.md +0 -0
  30. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/docs/writing-packs.md +0 -0
  31. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/pyproject.toml +0 -0
  32. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/__init__.py +0 -0
  33. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/__main__.py +0 -0
  34. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/builtin.toml +0 -0
  35. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/cli.py +0 -0
  36. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/client.py +0 -0
  37. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/compare.py +0 -0
  38. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/doctor.py +0 -0
  39. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/export.py +0 -0
  40. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/graders/__init__.py +0 -0
  41. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/graders/answer.py +0 -0
  42. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/graders/code.py +0 -0
  43. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/graders/rag.py +0 -0
  44. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/graders/reply.py +0 -0
  45. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/graders/tool_call.py +0 -0
  46. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/machines.py +0 -0
  47. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/onboard.py +0 -0
  48. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/packs.py +0 -0
  49. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/profiles.py +0 -0
  50. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/pta.py +0 -0
  51. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/remove.py +0 -0
  52. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/run_evals.py +0 -0
  53. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/scaffold.py +0 -0
  54. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/selftest.py +0 -0
  55. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/models.toml +0 -0
  56. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/answer/pack.toml +0 -0
  57. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/answer/system.txt +0 -0
  58. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
  59. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/code/pack.toml +0 -0
  60. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/code/system.txt +0 -0
  61. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/code/tests.yaml +0 -0
  62. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/rag/pack.toml +0 -0
  63. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/rag/system.txt +0 -0
  64. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
  65. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/reply/pack.toml +0 -0
  66. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/reply/system.txt +0 -0
  67. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
  68. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
  69. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
  70. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
  71. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
  72. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/tune.py +0 -0
  73. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/verdict.py +0 -0
  74. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/watch_proxy.py +0 -0
  75. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/workspace.py +0 -0
  76. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/src/tuieval/yamlout.py +0 -0
  77. {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev56}/tests/mock_server.py +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - The run's ETA adds up each pack's answers left × that model's time per answer there (its history, then its own times once it has 5 answers), instead of extrapolating from the first few answers.
5
6
  - Faster start on Macs: the GPU core count comes from `ioreg` instead of `system_profiler`, which took seconds on some Macs.
6
7
  - A model a server won't serve (HTTP 401-404, e.g. OpenRouter's "No endpoints found") is never judged on it: those answers don't count, and the model stops at once instead of failing every question.
7
8
  - PTA index: parsimony (tokens used) and speed (time), each 100 for the best and 15 points less per doubling, and accuracy per model, as bars: the first Results tab (where `r` lands), `tuieval pta` and the report. Server errors aren't answers, and models with few answers are left out unless picked.
@@ -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.dev56
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
@@ -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.dev56'
22
+ __version_tuple__ = version_tuple = (0, 2, 0, 'dev56')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -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})
@@ -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,9 +2226,9 @@ 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))}"
2229
+ if self.running and self.total > done:
2230
+ left, rough = self.eta_seconds()
2231
+ eta = f" · ETA {'~' if rough else ''}{fmt_secs(left)}"
2200
2232
  pct = f"{100 * passed / (passed + failed):.0f}%" 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")]
@@ -12,6 +12,7 @@ import sys
12
12
  import tempfile
13
13
  import textwrap
14
14
  import threading
15
+ import types
15
16
  import time
16
17
  import unittest
17
18
  import urllib.request
@@ -199,6 +200,26 @@ class WorkspaceGraders(unittest.TestCase):
199
200
 
200
201
 
201
202
  class Units(unittest.TestCase):
203
+ def test_eta_goes_pack_by_pack(self):
204
+ from tuieval.tui import RunScreen
205
+ job = lambda key, label, total, done=0, status="waiting": types.SimpleNamespace(
206
+ key=key, label=label, total=total, done=done, status=status)
207
+ run = RunScreen.__new__(RunScreen)
208
+ run.parallel, run.rough = 1, {"local/new"}
209
+ run.jobs = [job("hosted/a", "hosted", 100, 100, "done"), job("local/a", "local", 200, 4, "running"),
210
+ job("local/new", "local", 50)]
211
+ run.spr = {"local/a": 60.0, "local/new": 30.0} # from history
212
+ run.live = {"local/a": [900.0, 800.0, 700.0, 1000.0]} # its first answers are the slow ones
213
+ left, rough = run.eta_seconds()
214
+ self.assertEqual((left, rough), (196 * 60 + 50 * 30, True)) # history, not 4 slow answers: ~3.7h, not ~47h
215
+ run.live["local/a"].append(600.0) # 5 answers: its own average takes over
216
+ run.jobs[1].done = 5
217
+ self.assertEqual(run.eta_seconds()[0], 195 * 800 + 50 * 30)
218
+ run.parallel = 2
219
+ run.jobs.append(job("other/a", "other", 10))
220
+ run.spr["other/a"] = 10.0
221
+ self.assertEqual(run.eta_seconds()[0], (195 * 800 + 50 * 30 + 100) / 2) # two models at a time
222
+
202
223
  def test_server_errors_never_judge_the_model(self):
203
224
  from tuieval import client
204
225
  no_endpoints = 'server returned HTTP 404: {"error":{"message":"No endpoints found for some/model."}}'
File without changes
File without changes
File without changes