tuieval 0.2.0.dev116__tar.gz → 0.2.0.dev119__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 (80) hide show
  1. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/CHANGELOG.md +2 -0
  2. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/PKG-INFO +1 -1
  3. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/_version.py +2 -2
  4. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/tui.py +16 -7
  5. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/tune.py +33 -0
  6. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/tests/test_tuieval.py +57 -0
  7. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/.gitignore +0 -0
  8. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/LICENSE +0 -0
  9. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/README.md +0 -0
  10. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/RELEASING.md +0 -0
  11. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/README.md +0 -0
  12. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/favicon.ico +0 -0
  13. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/readme-header.png +0 -0
  14. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/social-preview.png +0 -0
  15. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/tuieval-icon-1024.png +0 -0
  16. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/tuieval-icon-128.png +0 -0
  17. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/tuieval-icon-16.png +0 -0
  18. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/tuieval-icon-16.svg +0 -0
  19. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/tuieval-icon-256.png +0 -0
  20. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/tuieval-icon-32.png +0 -0
  21. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/tuieval-icon-48.png +0 -0
  22. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/tuieval-icon-512.png +0 -0
  23. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/tuieval-icon-64.png +0 -0
  24. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/tuieval-icon-animated.svg +0 -0
  25. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/brand/tuieval-icon.svg +0 -0
  26. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/tui-answer-reasoning.png +0 -0
  27. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/tui-run-reasoning.png +0 -0
  28. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/images/tui-setup.png +0 -0
  29. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/models.md +0 -0
  30. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/docs/writing-packs.md +0 -0
  31. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/pyproject.toml +0 -0
  32. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/__init__.py +0 -0
  33. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/__main__.py +0 -0
  34. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/builtin.toml +0 -0
  35. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/cli.py +0 -0
  36. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/client.py +0 -0
  37. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/compare.py +0 -0
  38. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/dates.py +0 -0
  39. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/doctor.py +0 -0
  40. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/engine.py +0 -0
  41. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/export.py +0 -0
  42. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/graders/__init__.py +0 -0
  43. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/graders/answer.py +0 -0
  44. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/graders/code.py +0 -0
  45. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/graders/rag.py +0 -0
  46. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/graders/reply.py +0 -0
  47. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/graders/tool_call.py +0 -0
  48. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/insights.py +0 -0
  49. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/logs.py +0 -0
  50. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/machines.py +0 -0
  51. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/onboard.py +0 -0
  52. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/packs.py +0 -0
  53. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/profiles.py +0 -0
  54. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/pta.py +0 -0
  55. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/remove.py +0 -0
  56. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/run_evals.py +0 -0
  57. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/scaffold.py +0 -0
  58. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/selftest.py +0 -0
  59. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/models.toml +0 -0
  60. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/answer/pack.toml +0 -0
  61. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/answer/system.txt +0 -0
  62. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
  63. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/code/pack.toml +0 -0
  64. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/code/system.txt +0 -0
  65. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/code/tests.yaml +0 -0
  66. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/rag/pack.toml +0 -0
  67. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/rag/system.txt +0 -0
  68. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
  69. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/reply/pack.toml +0 -0
  70. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/reply/system.txt +0 -0
  71. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
  72. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
  73. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
  74. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
  75. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
  76. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/verdict.py +0 -0
  77. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/watch_proxy.py +0 -0
  78. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/workspace.py +0 -0
  79. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/src/tuieval/yamlout.py +0 -0
  80. {tuieval-0.2.0.dev116 → tuieval-0.2.0.dev119}/tests/mock_server.py +0 -0
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Setup opens with no models ticked: at launch, after New run, and after Back from a run or tune. Packs and options are still remembered.
6
+ - Tuning: a server that refuses the `ignore_eos` request field is asked again without it, so strict servers can be tuned; the profile notes that workload answers may end early.
5
7
  - Fix: a server that crashes mid-request (the stream ends with no finish reason) was graded as an empty or cut-short answer; it's a server error now, never counted against the model.
6
8
  - Dates: answers record `answered_at` (older ones read it from their log name); tests get an added date (an `added` field, or the day tuieval first saw them, in `test-dates.json`). Shown in the answer view, Pick tests, Per question, Failures and Production readiness.
7
9
  - Copy reasoning: `y` in the answer view copies its full reasoning, `a` the answer; `y` on the run screen copies the reasoning streamed so far. Uses pbcopy, wl-copy, xclip, xsel or clip, else the terminal (OSC 52).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tuieval
3
- Version: 0.2.0.dev116
3
+ Version: 0.2.0.dev119
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.dev116'
22
- __version_tuple__ = version_tuple = (0, 2, 0, 'dev116')
21
+ __version__ = version = '0.2.0.dev119'
22
+ __version_tuple__ = version_tuple = (0, 2, 0, 'dev119')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -751,11 +751,17 @@ class SetupScreen(Screen):
751
751
  self.refresh_packs()
752
752
  self.refresh_models()
753
753
  state = self.app.load_state()
754
- self.apply_selection(state, quiet=True)
755
- if not state:
754
+ if state: # packs and options carry over; models start unticked every time
755
+ self.apply_selection({k: v for k, v in state.items() if k != "models"}, quiet=True)
756
+ else:
756
757
  self.first_time_defaults()
757
758
  self.query_one("#suites", SelectionList).focus()
758
759
 
760
+ def clear_models(self):
761
+ """Untick every model (Setup shown again after a run: each run picks its models afresh)."""
762
+ self.selected_models = set()
763
+ self.refresh_models()
764
+
759
765
  def first_time_defaults(self):
760
766
  """With nothing chosen before: tick the only pack and the only local model, and start on
761
767
  Smoke until something has run (a 3-question check of the setup)."""
@@ -1155,7 +1161,7 @@ class SetupScreen(Screen):
1155
1161
  reasons = sorted({j.note for j in jobs if j.status == "skipped"})
1156
1162
  self.notify(f"Nothing selected can run: {'; '.join(reasons)}", severity="warning")
1157
1163
  return
1158
- self.app.save_state(sel)
1164
+ self.app.save_state({k: v for k, v in sel.items() if k != "models"}) # models aren't remembered
1159
1165
  # Packs whose earlier answers a start would set aside (Retest, everything already done, or
1160
1166
  # other questions or settings): the user picks per pack before anything moves.
1161
1167
  rows, rerun = retest_rows(e, labels, suites, repeat, tier, picks, force)
@@ -1349,7 +1355,7 @@ class TuneScreen(Screen):
1349
1355
  def action_back(self):
1350
1356
  if self.running:
1351
1357
  self.notify("Tuning keeps going; the header shows its progress. Press w to come back.")
1352
- self.app.back_to_setup()
1358
+ self.app.back_to_setup(clear_models=True)
1353
1359
 
1354
1360
 
1355
1361
  # ------------------------------------------------------------------ add model
@@ -2825,7 +2831,7 @@ class RunScreen(Screen):
2825
2831
  if self.running:
2826
2832
  self.notify("Pick models and packs, then Start: add them to this run or queue them after it.",
2827
2833
  timeout=8)
2828
- self.app.back_to_setup() # Setup still holds the last selection
2834
+ self.app.back_to_setup(clear_models=True) # packs and options stay; models are picked afresh
2829
2835
  self.app.screen.query_one("#models").focus()
2830
2836
 
2831
2837
  @on(Button.Pressed, "#done-runs")
@@ -2835,7 +2841,7 @@ class RunScreen(Screen):
2835
2841
  def action_back(self):
2836
2842
  if self.running:
2837
2843
  self.notify("The run keeps going; the header shows its progress. Press w to come back.")
2838
- self.app.back_to_setup()
2844
+ self.app.back_to_setup(clear_models=True)
2839
2845
 
2840
2846
 
2841
2847
  # ------------------------------------------------------------------ results
@@ -3789,9 +3795,12 @@ class EvalsApp(App):
3789
3795
  return (f"\n\nThe {len(self.queue)} queued item(s) are dropped (the queue isn't kept after quitting): "
3790
3796
  + "; ".join(i.title for i in self.queue) + ".")
3791
3797
 
3792
- def back_to_setup(self):
3798
+ def back_to_setup(self, clear_models=False):
3799
+ """clear_models: Setup is being shown for a new pick (New run, Back), so no model stays ticked."""
3793
3800
  while len(self.screen_stack) > 2 and not isinstance(self.screen, SetupScreen):
3794
3801
  self.pop_screen()
3802
+ if clear_models and isinstance(self.screen, SetupScreen):
3803
+ self.screen.clear_models()
3795
3804
 
3796
3805
  def show_session(self, screen):
3797
3806
  if screen is None or screen is self.screen:
@@ -169,7 +169,37 @@ def _body(m, sampling, msgs, max_tokens=GEN_TOKENS, request=None):
169
169
  **(request or {})}
170
170
 
171
171
 
172
+ def _rejects_ignore_eos(res):
173
+ """True if the server refused the request because it doesn't accept the ignore_eos field."""
174
+ err = res.get("error") or ""
175
+ return err.startswith("server returned HTTP 4") and "ignore_eos" in err
176
+
177
+
172
178
  def _request(eng, base_url, body, name, emit, limit):
179
+ """One streamed request with live progress and a wall-clock limit. A server that refuses
180
+ ignore_eos is asked again without it, and later requests leave it out (see no_ignore_eos)."""
181
+ key = (base_url, body.get("model"))
182
+ rejected = no_ignore_eos(eng)
183
+ if key in rejected:
184
+ body = {k: v for k, v in body.items() if k != "ignore_eos"}
185
+ res = _stream_request(eng, base_url, body, name, emit, limit)
186
+ if body.get("ignore_eos") and _rejects_ignore_eos(res):
187
+ rejected.add(key)
188
+ emit("tune_step", message=" the server doesn't accept ignore_eos: answers may end early "
189
+ "(speeds are per token, so still comparable)")
190
+ res = _stream_request(eng, base_url, {k: v for k, v in body.items() if k != "ignore_eos"},
191
+ name, emit, limit)
192
+ return res
193
+
194
+
195
+ def no_ignore_eos(eng):
196
+ """(base_url, served model) pairs whose server refused ignore_eos in this tuning session."""
197
+ if not hasattr(eng, "_tune_no_ignore_eos"):
198
+ eng._tune_no_ignore_eos = set()
199
+ return eng._tune_no_ignore_eos
200
+
201
+
202
+ def _stream_request(eng, base_url, body, name, emit, limit):
173
203
  """One streamed request with live progress (every PROGRESS_EVERY_S: waiting for the first
174
204
  token, or ~tokens so far and tok/s) and a wall-clock limit. Returns client.stream_chat's result."""
175
205
  st, t0 = client.Stream(), time.time()
@@ -681,6 +711,9 @@ def tune(eng, label, emit=lambda *a, **k: None, max_starts=16, min_gain=0.03, us
681
711
  if warm_info:
682
712
  notes.insert(0, f"warm start from {warm_info['from']}: inherited "
683
713
  f"{', '.join(warm_info['inherited']) or 'nothing'}")
714
+ if any(model == m["served_name"] for _, model in no_ignore_eos(eng)):
715
+ notes.insert(0, f"the server doesn't accept ignore_eos, so workload answers could end before "
716
+ f"{gen_tokens} tokens (speeds are per token)")
684
717
  if best_r.facts.get("extra_swap_mb") and best_r is not base:
685
718
  warnings.append(f"the chosen flags use more memory than the defaults: macOS swapped "
686
719
  f"~{best_r.facts['extra_swap_mb']} MB more of other apps' memory; close apps during runs, "
@@ -2016,6 +2016,32 @@ class FirstRun(unittest.TestCase):
2016
2016
  env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""), timeout=60).stdout.strip()
2017
2017
  self.assertEqual(out, "['first'] ['my-model'] smoke False") # tuning shows once something has run
2018
2018
 
2019
+ def test_setup_never_starts_with_models_ticked(self):
2020
+ tuieval(self.ws, "add", "my-model", "--server", "local")
2021
+ os.makedirs(os.path.join(self.ws, "logs"), exist_ok=True)
2022
+ write(os.path.join(self.ws, "logs", "tui_state.json"),
2023
+ json.dumps({"models": ["my-model"], "packs": ["first"], "tier": "certify", "repeat": 2}))
2024
+ code = textwrap.dedent("""
2025
+ import asyncio
2026
+ from tuieval.tui import EvalsApp
2027
+ from textual.widgets import SelectionList
2028
+ async def go():
2029
+ app = EvalsApp({})
2030
+ async with app.run_test(size=(140, 40)) as pilot:
2031
+ await pilot.pause()
2032
+ s = app.screen
2033
+ print(s.query_one("#suites", SelectionList).selected, sorted(s.selected_models), s.tier())
2034
+ s.selected_models = {"my-model"}
2035
+ s.refresh_models()
2036
+ app.back_to_setup(clear_models=True) # what New run and Back do
2037
+ await pilot.pause()
2038
+ print(s.query_one("#suites", SelectionList).selected, sorted(s.selected_models))
2039
+ asyncio.run(go())
2040
+ """)
2041
+ out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True,
2042
+ env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""), timeout=60).stdout
2043
+ self.assertEqual(out.strip().splitlines(), ["['first'] [] certify", "['first'] []"])
2044
+
2019
2045
  def test_trimmed_names_keep_both_ends(self):
2020
2046
  from tuieval.tui import fit_first_column, mid_trim, trim_names
2021
2047
  name = "some-author--some-long-model-name-q4_k_m-vllm"
@@ -2512,6 +2538,37 @@ class WarmTune(unittest.TestCase):
2512
2538
  self.assertEqual(out.score("projected"), 66.0)
2513
2539
  self.assertEqual(out.score("total"), 10.0)
2514
2540
 
2541
+ def test_server_refusing_ignore_eos_is_asked_without_it(self):
2542
+ sent = []
2543
+
2544
+ def server(eng, base_url, body, *a): # refuses unknown fields, like a strict server
2545
+ sent.append(dict(body))
2546
+ if "ignore_eos" in body:
2547
+ return {"error": 'server returned HTTP 400: {"error":{"message":"unsupported request field: '
2548
+ 'ignore_eos"}}', "answer": "", "reasoning": ""}
2549
+ return {"error": None, "answer": "ok", "reasoning": ""}
2550
+ orig = self.tune._stream_request
2551
+ self.tune._stream_request = server
2552
+ self.addCleanup(setattr, self.tune, "_stream_request", orig)
2553
+ body = {"model": "new", "messages": [], "ignore_eos": True}
2554
+ res = self.tune._request(self.eng, "http://x", body, "p", lambda *a, **k: None, 60)
2555
+ self.assertIsNone(res["error"])
2556
+ self.assertEqual(["ignore_eos" in b for b in sent], [True, False]) # retried without it
2557
+ self.tune._request(self.eng, "http://x", body, "p", lambda *a, **k: None, 60)
2558
+ self.assertEqual(len(sent), 3) # later requests leave it out at once
2559
+ self.assertNotIn("ignore_eos", sent[-1])
2560
+ self.assertIn(("http://x", "new"), self.tune.no_ignore_eos(self.eng))
2561
+
2562
+ def test_other_request_errors_are_not_retried(self):
2563
+ sent = []
2564
+ orig = self.tune._stream_request
2565
+ self.tune._stream_request = lambda eng, url, body, *a: sent.append(body) or {
2566
+ "error": "server returned HTTP 400: bad messages", "answer": "", "reasoning": ""}
2567
+ self.addCleanup(setattr, self.tune, "_stream_request", orig)
2568
+ res = self.tune._request(self.eng, "http://x", {"model": "new", "ignore_eos": True}, "p",
2569
+ lambda *a, **k: None, 60)
2570
+ self.assertEqual((len(sent), res["error"]), (1, "server returned HTTP 400: bad messages"))
2571
+
2515
2572
  def test_family_ignores_the_mtp_layer(self):
2516
2573
  # one GGUF lists KV heads per layer, none on the MTP (last) layer; the other gives one number
2517
2574
  d = self.tmp.name
File without changes
File without changes