tuieval 0.2.0.dev104__tar.gz → 0.2.0.dev107__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 (79) hide show
  1. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/CHANGELOG.md +2 -1
  2. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/PKG-INFO +1 -1
  3. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/_version.py +2 -2
  4. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/graders/__init__.py +1 -1
  5. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/graders/code.py +18 -2
  6. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/tui.py +29 -7
  7. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/tests/test_tuieval.py +24 -1
  8. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/.gitignore +0 -0
  9. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/LICENSE +0 -0
  10. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/README.md +0 -0
  11. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/RELEASING.md +0 -0
  12. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/README.md +0 -0
  13. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/favicon.ico +0 -0
  14. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/readme-header.png +0 -0
  15. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/social-preview.png +0 -0
  16. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/tuieval-icon-1024.png +0 -0
  17. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/tuieval-icon-128.png +0 -0
  18. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/tuieval-icon-16.png +0 -0
  19. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/tuieval-icon-16.svg +0 -0
  20. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/tuieval-icon-256.png +0 -0
  21. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/tuieval-icon-32.png +0 -0
  22. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/tuieval-icon-48.png +0 -0
  23. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/tuieval-icon-512.png +0 -0
  24. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/tuieval-icon-64.png +0 -0
  25. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/tuieval-icon-animated.svg +0 -0
  26. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/brand/tuieval-icon.svg +0 -0
  27. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/tui-answer-reasoning.png +0 -0
  28. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/tui-run-reasoning.png +0 -0
  29. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/images/tui-setup.png +0 -0
  30. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/models.md +0 -0
  31. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/docs/writing-packs.md +0 -0
  32. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/pyproject.toml +0 -0
  33. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/__init__.py +0 -0
  34. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/__main__.py +0 -0
  35. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/builtin.toml +0 -0
  36. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/cli.py +0 -0
  37. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/client.py +0 -0
  38. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/compare.py +0 -0
  39. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/doctor.py +0 -0
  40. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/engine.py +0 -0
  41. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/export.py +0 -0
  42. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/graders/answer.py +0 -0
  43. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/graders/rag.py +0 -0
  44. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/graders/reply.py +0 -0
  45. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/graders/tool_call.py +0 -0
  46. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/insights.py +0 -0
  47. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/logs.py +0 -0
  48. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/machines.py +0 -0
  49. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/onboard.py +0 -0
  50. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/packs.py +0 -0
  51. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/profiles.py +0 -0
  52. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/pta.py +0 -0
  53. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/remove.py +0 -0
  54. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/run_evals.py +0 -0
  55. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/scaffold.py +0 -0
  56. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/selftest.py +0 -0
  57. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/models.toml +0 -0
  58. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/answer/pack.toml +0 -0
  59. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/answer/system.txt +0 -0
  60. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
  61. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/code/pack.toml +0 -0
  62. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/code/system.txt +0 -0
  63. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/code/tests.yaml +0 -0
  64. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/rag/pack.toml +0 -0
  65. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/rag/system.txt +0 -0
  66. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
  67. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/reply/pack.toml +0 -0
  68. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/reply/system.txt +0 -0
  69. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
  70. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
  71. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
  72. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
  73. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
  74. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/tune.py +0 -0
  75. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/verdict.py +0 -0
  76. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/watch_proxy.py +0 -0
  77. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/workspace.py +0 -0
  78. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/src/tuieval/yamlout.py +0 -0
  79. {tuieval-0.2.0.dev104 → tuieval-0.2.0.dev107}/tests/mock_server.py +0 -0
@@ -2,7 +2,8 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- - Fix: an answer followed by a stray `</think>` was graded as empty; the answer before the tag is graded now.
5
+ - Results → Failures: a Models button filters the list (the same pick as the other tabs), with the number of failures shown.
6
+ - Fix: an answer followed by a stray `</think>` (or a stray code fence after it) was graded as empty; the answer before the tag is graded now. The code grader also reads a doubled ```` ```python ```` fence as one.
6
7
  - Run screen: `x` removes the highlighted pack from the run going if it hasn't started (the reverse of adding it); a model left with nothing isn't started.
7
8
  - `k` (skip model) asks first, naming the model. Yes/no dialogs start on No; `y` answers yes, `n` or Esc no.
8
9
  - Fix: hosted models no longer count as running alongside local ones (they don't share the machine), so a one-model-at-a-time setup no longer gets the "ran alongside other models" note. Older results are read the same way.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tuieval
3
- Version: 0.2.0.dev104
3
+ Version: 0.2.0.dev107
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.dev104'
22
- __version_tuple__ = version_tuple = (0, 2, 0, 'dev104')
21
+ __version__ = version = '0.2.0.dev107'
22
+ __version_tuple__ = version_tuple = (0, 2, 0, 'dev107')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -124,7 +124,7 @@ def strip_thinking(text):
124
124
  text = THINK_RE.sub("", text or "")
125
125
  if "</think>" in text:
126
126
  before, after = text.rsplit("</think>", 1)
127
- text = after if after.strip() else before # a stray closing tag after the answer
127
+ text = after if after.strip(" \t\r\n`") else before # a stray closing tag (or fence) after the answer
128
128
  return text.replace("<think>", "").replace("</think>", "").strip()
129
129
 
130
130
 
@@ -15,8 +15,24 @@ from . import grader, result
15
15
 
16
16
 
17
17
  def _extract_code(text):
18
- blocks = re.findall(r"```(?:python|py)?[ \t]*\n(.*?)```", text, re.DOTALL)
19
- return blocks[-1] if blocks else text # last block = the final answer, not a draft
18
+ """The last python (or untagged) fenced block: the final answer, not a draft. A doubled
19
+ opening fence is one fence, and a stray fence after the last block is ignored."""
20
+ blocks, lang, cur = [], None, None
21
+ for line in text.splitlines(keepends=True):
22
+ fence = line.strip()
23
+ if not fence.startswith("```"):
24
+ if cur is not None:
25
+ cur.append(line)
26
+ elif cur is None:
27
+ lang, cur = fence[3:].strip().lower(), []
28
+ elif fence[3:].strip() and not "".join(cur).strip():
29
+ lang = fence[3:].strip().lower() # ```python twice
30
+ else:
31
+ if lang in ("", "python", "py", "python3"):
32
+ blocks.append("".join(cur))
33
+ cur = None
34
+ blocks = [b for b in blocks if b.strip()]
35
+ return blocks[-1] if blocks else text
20
36
 
21
37
 
22
38
  CHECK_RE = re.compile(r"^# (SETUP|CHECK: *(.+?)) *$")
@@ -454,7 +454,8 @@ with the model's reasoning and answer as they stream, then recent results with t
454
454
  Results open here.
455
455
  Production readiness one verdict per model and use case; the table says why and what to run next
456
456
  Per question every question, model by model (filters: pack, level, Models disagree)
457
- Failures every wrong answer with the grader's reason; enter opens it in full
457
+ Failures every wrong answer with the grader's reason (filter: Models); enter opens it
458
+ in full
458
459
 
459
460
  Click a column name to sort a table by it; click it again to reverse. Empty cells stay last.
460
461
 
@@ -2825,6 +2826,9 @@ class ResultsScreen(Screen):
2825
2826
  yield Static(id="pq-legend")
2826
2827
  yield DataTable(id="per-question", zebra_stripes=True, cursor_type="cell")
2827
2828
  with TabPane("Failures", id="tab-failures"):
2829
+ with Horizontal(id="fail-controls"):
2830
+ yield Button("Models: all", id="fail-models")
2831
+ yield Static(id="fail-count")
2828
2832
  yield Static("[dim]Enter on a row shows the whole answer, its reasoning and what was checked.[/dim]")
2829
2833
  yield DataTable(id="failures", zebra_stripes=True, cursor_type="row")
2830
2834
  yield Footer()
@@ -2902,12 +2906,7 @@ class ResultsScreen(Screen):
2902
2906
  info.update(f"{len(paths)} {'Smoke ' if smoke else ''}result files in {os.path.relpath(results_dir)}/" + banner
2903
2907
  + "".join(f"\n[yellow]⚠ {n}[/yellow]" for n in notes))
2904
2908
  self.rows, self.infos = rows, infos
2905
- f = self.query_one("#failures", DataTable)
2906
- f.add_columns("Model", "Level", "Test", "Reason")
2907
- # compare.failures lists the failed rows in order, so they line up with fail_rows
2908
- self.fail_rows = [r for r in rows if not r["ok"]]
2909
- for i, (model, test, reason, level) in enumerate(compare.failures(rows)):
2910
- f.add_row(model, diff_badge(level), rich_escape(test), rich_escape(reason[:130]), key=str(i))
2909
+ self.fill_failures()
2911
2910
  self.fill_per_question()
2912
2911
  self.fill_pta()
2913
2912
  self.fill_readiness(results_dir)
@@ -2916,6 +2915,24 @@ class ResultsScreen(Screen):
2916
2915
  self.fill_insights()
2917
2916
  self.sort_tables()
2918
2917
 
2918
+ def fill_failures(self):
2919
+ """Every wrong answer, of the models picked (Models: shared with the other tabs)."""
2920
+ f = self.query_one("#failures", DataTable)
2921
+ f.clear(columns=True)
2922
+ f.add_columns("Model", "Level", "Test", "Reason")
2923
+ every = {r["model"] for r in self.rows}
2924
+ chosen = {m for m in self.app.pq_models if m in every}
2925
+ rows = [r for r in self.rows if not chosen or r["model"] in chosen]
2926
+ # compare.failures lists the failed rows in order, so they line up with fail_rows
2927
+ self.fail_rows = [r for r in rows if not r["ok"]]
2928
+ for i, (model, test, reason, level) in enumerate(compare.failures(rows)):
2929
+ f.add_row(model, diff_badge(level), rich_escape(test), rich_escape(reason[:130]), key=str(i))
2930
+ self.query_one("#fail-models", Button).label = f"Models: {len(chosen)} picked" if chosen else "Models: all"
2931
+ n = len(self.fail_rows)
2932
+ self.query_one("#fail-count", Static).update(
2933
+ f"{n} failure{'s' * (n != 1)}" + (f" of {len(chosen)} model{'s' * (len(chosen) != 1)}" if chosen else ""))
2934
+ self.sort_table(f)
2935
+
2919
2936
  @on(DataTable.RowSelected, "#failures")
2920
2937
  def failure_selected(self, event):
2921
2938
  # in the order shown, so [ and ] step through them as sorted
@@ -2979,6 +2996,7 @@ class ResultsScreen(Screen):
2979
2996
  @on(Button.Pressed, "#pq-models")
2980
2997
  @on(Button.Pressed, "#pta-models")
2981
2998
  @on(Button.Pressed, "#ins-models")
2999
+ @on(Button.Pressed, "#fail-models")
2982
3000
  def pick_models(self):
2983
3001
  counts = {}
2984
3002
  for t in getattr(self, "pq_all_tests", []):
@@ -2997,6 +3015,7 @@ class ResultsScreen(Screen):
2997
3015
  # after the picker has closed: a table rebuilt under a modal keeps its header-only widths
2998
3016
  self.call_after_refresh(self.fill_per_question)
2999
3017
  self.call_after_refresh(self.fill_pta)
3018
+ self.call_after_refresh(self.fill_failures)
3000
3019
  self.insights_stale = True
3001
3020
  if self.query_one(TabbedContent).active == "tab-insights":
3002
3021
  self.call_after_refresh(self.fill_insights)
@@ -3397,6 +3416,9 @@ class EvalsApp(App):
3397
3416
  #pq-controls Checkbox { width: auto; }
3398
3417
  #pq-controls Select { width: 22; margin-right: 1; }
3399
3418
  #pq-models { margin-right: 1; min-width: 20; }
3419
+ #fail-controls { height: 3; }
3420
+ #fail-models { margin-right: 1; min-width: 20; }
3421
+ #fail-count { width: auto; padding: 1 1; }
3400
3422
  #pq-compare-title { padding: 0 1; height: auto; }
3401
3423
  #pq-summary { height: auto; } /* every picked model, no scrolling: the tab scrolls instead */
3402
3424
  #pq-legend { padding: 0 1; height: auto; }
@@ -254,6 +254,14 @@ class Units(unittest.TestCase):
254
254
  self.assertEqual(strip_thinking("leaked thoughts</think>\n42"), "42")
255
255
  self.assertEqual(strip_thinking("```python\nx = 1\n```\n</think>\n"), "```python\nx = 1\n```")
256
256
  self.assertEqual(strip_thinking("</think>"), "")
257
+ self.assertEqual(strip_thinking("```python\nx = 1\n```\n</think>\n```"), "```python\nx = 1\n```")
258
+
259
+ def test_extract_code_tolerates_doubled_and_stray_fences(self):
260
+ from tuieval.graders.code import _extract_code
261
+ self.assertEqual(_extract_code("```python\n```python\nx = 1\n```\n```"), "x = 1\n")
262
+ self.assertEqual(_extract_code("```py\nold\n```\ntext\n```python\nnew\n```"), "new\n") # last block wins
263
+ self.assertEqual(_extract_code("```python\nx = 1\n```\n```bash\nrun it\n```"), "x = 1\n")
264
+ self.assertEqual(_extract_code("x = 1"), "x = 1")
257
265
 
258
266
  def test_settings_note_names_only_what_differs(self):
259
267
  from tuieval import compare
@@ -1563,6 +1571,17 @@ class PTAIndex(unittest.TestCase):
1563
1571
  app.screen.query_one(TabbedContent).active = "tab-failures"
1564
1572
  await pilot.pause(0.3)
1565
1573
  f = app.screen.query_one("#failures", DataTable)
1574
+ count = lambda: str(app.screen.query_one("#fail-count", Static).render())
1575
+ fails = {"all": [f.row_count, count()]}
1576
+ for m in ("good", "hosted"): # Models: only that model's failures
1577
+ app.pq_models = {m}
1578
+ app.screen.fill_failures()
1579
+ await pilot.pause(0.2)
1580
+ fails[m] = [f.row_count, count(), str(app.screen.query_one("#fail-models").label),
1581
+ sorted({str(f.get_row_at(r)[0]) for r in range(f.row_count)})]
1582
+ app.pq_models = set()
1583
+ app.screen.fill_failures()
1584
+ await pilot.pause(0.2)
1566
1585
  await click_header(f, "Test")
1567
1586
  sorts["tests"], _ = await click_header(f, "Test")
1568
1587
  sorts["tests"] = [str(f.get_row_at(r)[2]) for r in range(f.row_count)]
@@ -1573,7 +1592,7 @@ class PTAIndex(unittest.TestCase):
1573
1592
  s = app.screen
1574
1593
  sorts["opened"] = s.entries[s.index]["record"]["test"]
1575
1594
  print(json.dumps({"rows": rows, "bars": "P parsimony" in tri, "disagree": [both, alone],
1576
- "tab": tab, "sorts": sorts}))
1595
+ "tab": tab, "sorts": sorts, "fails": fails}))
1577
1596
  asyncio.run(go())
1578
1597
  """)
1579
1598
  p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=300,
@@ -1588,6 +1607,10 @@ class PTAIndex(unittest.TestCase):
1588
1607
  self.assertEqual((s["up"], s["down"], s["kept"]), (["hosted", "good"], ["good", "hosted"], ["good", "hosted"]))
1589
1608
  self.assertIn("answers right ▼", s["labels"])
1590
1609
  self.assertEqual((s["tests"][0], s["opened"]), ("apps: d", "d"))
1610
+ fails = got["fails"]
1611
+ self.assertEqual(fails["all"], [4, "4 failures"])
1612
+ self.assertEqual(fails["good"], [0, "0 failures of 1 model", "Models: 1 picked", []])
1613
+ self.assertEqual(fails["hosted"], [4, "4 failures of 1 model", "Models: 1 picked", ["hosted"]])
1591
1614
 
1592
1615
 
1593
1616
  class Unavailable(unittest.TestCase):
File without changes
File without changes