tuieval 0.2.0.dev107__tar.gz → 0.2.0.dev112__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.
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/CHANGELOG.md +3 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/PKG-INFO +2 -2
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/README.md +1 -1
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/pta.py +9 -4
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/tui.py +140 -23
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/tests/test_tuieval.py +127 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/.gitignore +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/LICENSE +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/models.md +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/compare.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/engine.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/insights.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/logs.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/run_evals.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/tests/mock_server.py +0 -0
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- 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).
|
|
6
|
+
- PTA index: click a model's name to remove it from view (every Results tab follows); click it under the bars to bring it back, or show all.
|
|
7
|
+
- The Models heading shows how many are selected, naming any the filter hides; Start asks first when some are hidden.
|
|
5
8
|
- Results → Failures: a Models button filters the list (the same pick as the other tabs), with the number of failures shown.
|
|
6
9
|
- 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.
|
|
7
10
|
- 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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev112
|
|
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
|
|
@@ -66,7 +66,7 @@ While a run goes, you watch each question's reasoning and answer stream in:
|
|
|
66
66
|
|
|
67
67
|

|
|
68
68
|
|
|
69
|
-
Afterwards, every answer keeps its reasoning. Enter on a result opens it with the grade and each check, the full reasoning and the answer; `[` and `]` step through the others:
|
|
69
|
+
Afterwards, every answer keeps its reasoning. Enter on a result opens it with the grade and each check, the full reasoning and the answer; `[` and `]` step through the others, `y` copies the reasoning and `a` the answer (on the run screen, `y` copies the reasoning streamed so far):
|
|
70
70
|
|
|
71
71
|

|
|
72
72
|
|
|
@@ -40,7 +40,7 @@ While a run goes, you watch each question's reasoning and answer stream in:
|
|
|
40
40
|
|
|
41
41
|

|
|
42
42
|
|
|
43
|
-
Afterwards, every answer keeps its reasoning. Enter on a result opens it with the grade and each check, the full reasoning and the answer; `[` and `]` step through the others:
|
|
43
|
+
Afterwards, every answer keeps its reasoning. Enter on a result opens it with the grade and each check, the full reasoning and the answer; `[` and `]` step through the others, `y` copies the reasoning and `a` the answer (on the run screen, `y` copies the reasoning streamed so far):
|
|
44
44
|
|
|
45
45
|

|
|
46
46
|
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2, 0, '
|
|
21
|
+
__version__ = version = '0.2.0.dev112'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev112')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -117,12 +117,13 @@ def _blocks(width, v, most):
|
|
|
117
117
|
return max(1, min(width, round(width * v / most)))
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
def bars(result, width=20, markup=True):
|
|
120
|
+
def bars(result, width=20, markup=True, click=None):
|
|
121
121
|
"""Lines with a row per model and a bar each for P, T and A: the at-a-glance view. The P and T bars
|
|
122
122
|
are proportional to tokens and time (the longest is the model that used most; shorter is better),
|
|
123
123
|
with how many times the leanest / fastest model's beside them; the A bar is the share of answers
|
|
124
124
|
right (longer is better, full only when every answer passed), then the share cut off at max_tokens in
|
|
125
|
-
orange (▓ in plain text) and "(n% truncated)" beside it. markup=False gives plain text
|
|
125
|
+
orange (▓ in plain text) and "(n% truncated)" beside it. markup=False gives plain text; click (an
|
|
126
|
+
action, e.g. "screen.remove") makes each model's name a link calling it with the row's index."""
|
|
126
127
|
if not result["models"]:
|
|
127
128
|
return []
|
|
128
129
|
name_w = max(len(x["model"]) for x in result["models"])
|
|
@@ -133,7 +134,7 @@ def bars(result, width=20, markup=True):
|
|
|
133
134
|
sub = pad + "".join(f"{better:<{cell_w}}" for _, _, better, *_ in LETTERS)
|
|
134
135
|
lines = [f"[b]{head.rstrip()}[/b]" if markup else head.rstrip(), f"[dim]{sub.rstrip()}[/dim]" if markup else sub.rstrip()]
|
|
135
136
|
most = {m: max((x.get(m) or 0 for x in result["models"]), default=0) for _, _, _, m, _, _ in LETTERS}
|
|
136
|
-
for x in result["models"]:
|
|
137
|
+
for row, x in enumerate(result["models"]):
|
|
137
138
|
cells = []
|
|
138
139
|
for k, _, _, measure, shown, color in LETTERS:
|
|
139
140
|
if k == "A":
|
|
@@ -155,7 +156,11 @@ def bars(result, width=20, markup=True):
|
|
|
155
156
|
if markup else bar + cut_bar + rest)
|
|
156
157
|
cells.append(f"{bar} {num:>6}" + " " * max(0, cell_w - width - 1 - max(6, len(num))))
|
|
157
158
|
name = f"{x['model']:<{name_w}}"
|
|
158
|
-
|
|
159
|
+
if markup:
|
|
160
|
+
pad_w = name_w - len(x["model"])
|
|
161
|
+
name = (f"[@click={click}({row})]{rich_escape(x['model'])}[/]" + " " * pad_w if click
|
|
162
|
+
else rich_escape(name))
|
|
163
|
+
lines.append((f"{name} " + "".join(cells)).rstrip())
|
|
159
164
|
return lines
|
|
160
165
|
|
|
161
166
|
|
|
@@ -24,6 +24,8 @@ import glob
|
|
|
24
24
|
import json
|
|
25
25
|
import os
|
|
26
26
|
import re
|
|
27
|
+
import shutil
|
|
28
|
+
import subprocess
|
|
27
29
|
import threading
|
|
28
30
|
import time
|
|
29
31
|
|
|
@@ -289,6 +291,37 @@ class StreamView(TextArea):
|
|
|
289
291
|
self.call_after_refresh(lambda: self.scroll_to(y=max(0, self.max_scroll_y - from_end), animate=False))
|
|
290
292
|
|
|
291
293
|
|
|
294
|
+
CLIPBOARD_TOOLS = (["pbcopy"], ["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"],
|
|
295
|
+
["clip"])
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def copy_text(app, text):
|
|
299
|
+
"""Put text on the system clipboard with the first tool here that takes it (pbcopy, wl-copy, xclip, xsel,
|
|
300
|
+
clip). Without one, the terminal is asked to (OSC 52), which not every terminal allows. True if a tool did."""
|
|
301
|
+
for cmd in CLIPBOARD_TOOLS:
|
|
302
|
+
if not shutil.which(cmd[0]):
|
|
303
|
+
continue
|
|
304
|
+
try:
|
|
305
|
+
subprocess.run(cmd, input=text.encode("utf-16" if cmd[0] == "clip" else "utf-8"),
|
|
306
|
+
capture_output=True, check=True, timeout=10)
|
|
307
|
+
return True
|
|
308
|
+
except (OSError, subprocess.SubprocessError):
|
|
309
|
+
continue # e.g. xclip without a display: try the next
|
|
310
|
+
app.copy_to_clipboard(text)
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def notify_copied(app, what, text, note=""):
|
|
315
|
+
if not text:
|
|
316
|
+
app.notify(f"No {what} to copy.", severity="warning")
|
|
317
|
+
elif copy_text(app, text):
|
|
318
|
+
app.notify(f"Copied {what}{note}: {len(text):,} chars.")
|
|
319
|
+
else:
|
|
320
|
+
app.notify(f"Sent {what}{note} ({len(text):,} chars) to the terminal's clipboard. If nothing pastes, "
|
|
321
|
+
"the terminal doesn't allow it: install a clipboard tool (wl-copy, xclip or xsel).",
|
|
322
|
+
severity="warning", timeout=10)
|
|
323
|
+
|
|
324
|
+
|
|
292
325
|
# ------------------------------------------------------------------ answer detail
|
|
293
326
|
def answer_entry(row):
|
|
294
327
|
"""An answer for AnswerScreen from a compare row (None when the row has no stored answer)."""
|
|
@@ -451,7 +484,7 @@ with the model's reasoning and answer as they stream, then recent results with t
|
|
|
451
484
|
PTA index parsimony (tokens), time and accuracy per model, a bar each: tokens and time
|
|
452
485
|
as long as they took (shorter is better; 2.4× = 2.4 times the best), accuracy
|
|
453
486
|
longer is better. Per-answer numbers, tok/s, TTFT and memory in the table.
|
|
454
|
-
Results open here.
|
|
487
|
+
Results open here. Click a model's name to remove it from view (every tab).
|
|
455
488
|
Production readiness one verdict per model and use case; the table says why and what to run next
|
|
456
489
|
Per question every question, model by model (filters: pack, level, Models disagree)
|
|
457
490
|
Failures every wrong answer with the grader's reason (filter: Models); enter opens it
|
|
@@ -511,11 +544,14 @@ class AnswerScreen(ModalScreen):
|
|
|
511
544
|
Binding("q", "close", "Close", show=False, priority=True),
|
|
512
545
|
Binding("left_square_bracket", "step(-1)", "Previous", priority=True),
|
|
513
546
|
Binding("right_square_bracket", "step(1)", "Next", priority=True),
|
|
547
|
+
Binding("y", "copy('reasoning')", "Copy reasoning", priority=True),
|
|
548
|
+
Binding("a", "copy('answer')", "Copy answer", priority=True),
|
|
514
549
|
]
|
|
515
550
|
|
|
516
551
|
def __init__(self, entries, index=0, what="answer"):
|
|
517
552
|
super().__init__()
|
|
518
553
|
self.entries, self.index, self.what = entries, index, what
|
|
554
|
+
self.texts = {} # what's shown: "reasoning", "answer" -> text, for the copy keys
|
|
519
555
|
|
|
520
556
|
def check_action(self, action, parameters):
|
|
521
557
|
return len(self.entries) > 1 if action == "step" else True
|
|
@@ -550,6 +586,9 @@ class AnswerScreen(ModalScreen):
|
|
|
550
586
|
self.index = (self.index + delta) % len(self.entries)
|
|
551
587
|
self.show()
|
|
552
588
|
|
|
589
|
+
def action_copy(self, what):
|
|
590
|
+
notify_copied(self.app, what, self.texts.get(what, ""))
|
|
591
|
+
|
|
553
592
|
def show(self):
|
|
554
593
|
e = self.app.engine
|
|
555
594
|
entry = self.entries[self.index]
|
|
@@ -608,6 +647,7 @@ class AnswerScreen(ModalScreen):
|
|
|
608
647
|
answer += "\n\n=== TOOL CALLS ===\n" + json.dumps(rec["tool_calls"], indent=2)
|
|
609
648
|
for wid, text in (("#answer-reasoning", reasoning), ("#answer-answer", answer or "(empty)")):
|
|
610
649
|
self.query_one(wid, TextArea).load_text(text)
|
|
650
|
+
self.texts = {"reasoning": reasoning, "answer": answer}
|
|
611
651
|
|
|
612
652
|
question, checked = "(test not found)", "(test not found)"
|
|
613
653
|
if test:
|
|
@@ -776,10 +816,7 @@ class SetupScreen(Screen):
|
|
|
776
816
|
self.verdicts = {}
|
|
777
817
|
self.app.log(f"verdicts failed: {err!r}")
|
|
778
818
|
codes = group_codes(e)
|
|
779
|
-
self.
|
|
780
|
-
"[b]Models[/b] [dim][green]✓[/green] pass [red]✗[/red] fail [yellow]?[/yellow] inconclusive "
|
|
781
|
-
"grey = earlier · x hides[/dim]\n[dim]"
|
|
782
|
-
+ " ".join(f"{c} {g}" for g, c in codes.items()) + "[/dim]")
|
|
819
|
+
self.legend_codes = codes
|
|
783
820
|
n_hidden = sum(m["label"] in hidden for m in e.cfg["models"])
|
|
784
821
|
self.query_one("#show-hidden", Checkbox).label = f"Show hidden ({n_hidden})"
|
|
785
822
|
sections = {}
|
|
@@ -819,8 +856,27 @@ class SetupScreen(Screen):
|
|
|
819
856
|
at = values.index(keep) if keep in values else min(keep_at or 0, models.option_count - 1)
|
|
820
857
|
models.highlighted = min((i for i in enabled if i >= at), default=enabled[-1])
|
|
821
858
|
self.show_detail()
|
|
859
|
+
self.update_legend()
|
|
822
860
|
self.update_estimate()
|
|
823
861
|
|
|
862
|
+
def unseen_models(self):
|
|
863
|
+
"""Ticked models the list doesn't show right now (the filter leaves them out)."""
|
|
864
|
+
models = self.query_one("#models", SelectionList)
|
|
865
|
+
visible = {models.get_option_at_index(i).value for i in range(models.option_count)}
|
|
866
|
+
return [m["label"] for m in self.app.engine.cfg["models"]
|
|
867
|
+
if m["label"] in self.selected_models and m["label"] not in visible]
|
|
868
|
+
|
|
869
|
+
def update_legend(self):
|
|
870
|
+
"""The Models heading: how many are ticked (and which ones the filter hides), then the key."""
|
|
871
|
+
n, unseen = len(self.selected_models), self.unseen_models()
|
|
872
|
+
count = f"[b]{n}[/b] selected" if n else "[dim]none selected[/dim]"
|
|
873
|
+
if unseen:
|
|
874
|
+
count += f" [yellow]({len(unseen)} not shown: {rich_escape(', '.join(unseen))})[/yellow]"
|
|
875
|
+
self.query_one("#models-legend", Label).update(
|
|
876
|
+
f"[b]Models[/b] {count} [dim][green]✓[/green] pass [red]✗[/red] fail [yellow]?[/yellow] "
|
|
877
|
+
"inconclusive grey = earlier · x hides[/dim]\n[dim]"
|
|
878
|
+
+ " ".join(f"{c} {g}" for g, c in getattr(self, "legend_codes", {}).items()) + "[/dim]")
|
|
879
|
+
|
|
824
880
|
@on(SelectionList.SelectionHighlighted, "#models")
|
|
825
881
|
def show_detail(self, *_):
|
|
826
882
|
"""Everything about the highlighted model: verdicts in full, serving, per-pack results."""
|
|
@@ -894,6 +950,7 @@ class SetupScreen(Screen):
|
|
|
894
950
|
return
|
|
895
951
|
visible = {models.get_option_at_index(i).value for i in range(models.option_count)}
|
|
896
952
|
self.selected_models = (self.selected_models - visible) | set(models.selected)
|
|
953
|
+
self.update_legend()
|
|
897
954
|
self.update_estimate()
|
|
898
955
|
|
|
899
956
|
def tier(self):
|
|
@@ -1110,10 +1167,20 @@ class SetupScreen(Screen):
|
|
|
1110
1167
|
self.notify(f"Nothing to run: {item.problem}.", severity="warning")
|
|
1111
1168
|
return
|
|
1112
1169
|
self.app.start_or_queue(item)
|
|
1113
|
-
|
|
1114
|
-
|
|
1170
|
+
def asked(yes=True):
|
|
1171
|
+
if not yes:
|
|
1172
|
+
return
|
|
1173
|
+
if rows:
|
|
1174
|
+
self.app.push_screen(RetestScreen(rows, force, rerun), go)
|
|
1175
|
+
else:
|
|
1176
|
+
go({})
|
|
1177
|
+
unseen = self.unseen_models()
|
|
1178
|
+
if unseen: # ticks the filter hides are easy to forget: name them before anything starts
|
|
1179
|
+
self.app.push_screen(ConfirmScreen(
|
|
1180
|
+
f"Start {len(labels)} model(s)? {len(unseen)} of them the filter hides:\n "
|
|
1181
|
+
+ rich_escape("\n ".join(unseen) + f"\n\nAll selected: {', '.join(labels)}")), asked)
|
|
1115
1182
|
else:
|
|
1116
|
-
|
|
1183
|
+
asked()
|
|
1117
1184
|
|
|
1118
1185
|
def action_tune(self):
|
|
1119
1186
|
e = self.app.engine
|
|
@@ -2136,6 +2203,7 @@ class RunScreen(Screen):
|
|
|
2136
2203
|
Binding("c", "cancel_all", "Cancel all", priority=True),
|
|
2137
2204
|
Binding("f", "toggle_follow", "Pause scroll", priority=True),
|
|
2138
2205
|
Binding("v", "watch_next", "Next model", priority=True),
|
|
2206
|
+
Binding("y", "copy_reasoning", "Copy reasoning", priority=True),
|
|
2139
2207
|
Binding("n", "new_run", "More evals", priority=True),
|
|
2140
2208
|
Binding("r", "results", "Results", priority=True),
|
|
2141
2209
|
Binding("w", "runs", "Runs", priority=True),
|
|
@@ -2155,6 +2223,8 @@ class RunScreen(Screen):
|
|
|
2155
2223
|
self.follow = True
|
|
2156
2224
|
self.buf = {"reasoning": [], "answer": []}
|
|
2157
2225
|
self.buf_lock = threading.Lock()
|
|
2226
|
+
self.streamed = {"reasoning": [], "answer": []} # the watched answer so far, untrimmed (for y)
|
|
2227
|
+
self.streamed_whole = False # from its start (not joined midway with v)
|
|
2158
2228
|
self.req_live = {} # model label -> its request now: phase, ~tokens, started, last token
|
|
2159
2229
|
self.started = time.time()
|
|
2160
2230
|
self.req_started = None
|
|
@@ -2317,6 +2387,7 @@ class RunScreen(Screen):
|
|
|
2317
2387
|
v.clear()
|
|
2318
2388
|
for stream, text in parts.items():
|
|
2319
2389
|
if text:
|
|
2390
|
+
self.streamed[stream].append(text)
|
|
2320
2391
|
self.query_one(f"#{stream}", StreamView).append(text, self.follow)
|
|
2321
2392
|
self.req_tokens += max(1, len(text) // 4)
|
|
2322
2393
|
if self.req_first is None and self.req_started:
|
|
@@ -2337,6 +2408,7 @@ class RunScreen(Screen):
|
|
|
2337
2408
|
self.flush_streams()
|
|
2338
2409
|
self.query_one("#reasoning", StreamView).clear()
|
|
2339
2410
|
self.query_one("#answer", StreamView).clear()
|
|
2411
|
+
self.streamed, self.streamed_whole = {"reasoning": [], "answer": []}, True
|
|
2340
2412
|
self.req_started, self.req_tokens, self.req_first = time.time(), 0, None
|
|
2341
2413
|
job = d["job"]
|
|
2342
2414
|
rep_note = f" · repeat {d['repeat'] + 1}/{job.repeat}" if job.repeat > 1 else ""
|
|
@@ -2704,6 +2776,7 @@ class RunScreen(Screen):
|
|
|
2704
2776
|
v.clear()
|
|
2705
2777
|
self.query_one("#reasoning", StreamView).clear()
|
|
2706
2778
|
self.query_one("#answer", StreamView).clear()
|
|
2779
|
+
self.streamed, self.streamed_whole = {"reasoning": [], "answer": []}, False
|
|
2707
2780
|
self.req_started = None
|
|
2708
2781
|
self.current = f"Streaming [b]{self.watch}[/b] from its next answer…"
|
|
2709
2782
|
self.refresh_current()
|
|
@@ -2721,6 +2794,12 @@ class RunScreen(Screen):
|
|
|
2721
2794
|
self.follow = not self.follow
|
|
2722
2795
|
self.update_overall()
|
|
2723
2796
|
|
|
2797
|
+
def action_copy_reasoning(self):
|
|
2798
|
+
"""The streaming answer's reasoning so far, all of it (the pane drops the oldest text when it's long)."""
|
|
2799
|
+
self.flush_streams()
|
|
2800
|
+
notify_copied(self.app, "reasoning", "".join(self.streamed["reasoning"]),
|
|
2801
|
+
"" if self.streamed_whole else " (from midway: streaming joined after it began)")
|
|
2802
|
+
|
|
2724
2803
|
@on(DataTable.RowSelected, "#recent")
|
|
2725
2804
|
def recent_selected(self, event):
|
|
2726
2805
|
keys = [k for k in event.data_table.rows if k in self.recent_entries]
|
|
@@ -3005,22 +3084,51 @@ class ResultsScreen(Screen):
|
|
|
3005
3084
|
models = sorted(counts, key=lambda m: (-counts[m], m))
|
|
3006
3085
|
|
|
3007
3086
|
def picked(chosen):
|
|
3008
|
-
if chosen is None:
|
|
3009
|
-
|
|
3010
|
-
first = chosen and not self.app.pq_models
|
|
3011
|
-
self.app.pq_models = chosen # kept for this session, so reopening Results keeps it
|
|
3012
|
-
if first: # a fair comparison needs the same questions: turn it on (you can untick it)
|
|
3013
|
-
with self.query_one("#pq-shared", Checkbox).prevent(Checkbox.Changed):
|
|
3014
|
-
self.query_one("#pq-shared", Checkbox).value = True
|
|
3015
|
-
# after the picker has closed: a table rebuilt under a modal keeps its header-only widths
|
|
3016
|
-
self.call_after_refresh(self.fill_per_question)
|
|
3017
|
-
self.call_after_refresh(self.fill_pta)
|
|
3018
|
-
self.call_after_refresh(self.fill_failures)
|
|
3019
|
-
self.insights_stale = True
|
|
3020
|
-
if self.query_one(TabbedContent).active == "tab-insights":
|
|
3021
|
-
self.call_after_refresh(self.fill_insights)
|
|
3087
|
+
if chosen is not None:
|
|
3088
|
+
self.set_models(chosen)
|
|
3022
3089
|
self.app.push_screen(ModelPickScreen(models, counts, self.app.pq_models), picked)
|
|
3023
3090
|
|
|
3091
|
+
def set_models(self, chosen):
|
|
3092
|
+
"""The models every Results tab compares (none: all of them)."""
|
|
3093
|
+
first = chosen and not self.app.pq_models
|
|
3094
|
+
self.app.pq_models = chosen # kept for this session, so reopening Results keeps it
|
|
3095
|
+
if first: # a fair comparison needs the same questions: turn it on (you can untick it)
|
|
3096
|
+
with self.query_one("#pq-shared", Checkbox).prevent(Checkbox.Changed):
|
|
3097
|
+
self.query_one("#pq-shared", Checkbox).value = True
|
|
3098
|
+
# after a picker has closed: a table rebuilt under a modal keeps its header-only widths
|
|
3099
|
+
self.call_after_refresh(self.fill_per_question)
|
|
3100
|
+
self.call_after_refresh(self.fill_pta)
|
|
3101
|
+
self.call_after_refresh(self.fill_failures)
|
|
3102
|
+
self.insights_stale = True
|
|
3103
|
+
if self.query_one(TabbedContent).active == "tab-insights":
|
|
3104
|
+
self.call_after_refresh(self.fill_insights)
|
|
3105
|
+
|
|
3106
|
+
def action_pta_remove(self, row):
|
|
3107
|
+
"""A click on a model's name in the PTA bars: leave it out of every tab (click it again below to bring it back)."""
|
|
3108
|
+
shown = [x["model"] for x in getattr(self, "pta_result", {}).get("models", [])]
|
|
3109
|
+
if not 0 <= row < len(shown):
|
|
3110
|
+
return
|
|
3111
|
+
if len(shown) == 1:
|
|
3112
|
+
self.notify("That's the last model shown.", severity="warning")
|
|
3113
|
+
return
|
|
3114
|
+
model = shown[row]
|
|
3115
|
+
self.app.pta_removed = [m for m in getattr(self.app, "pta_removed", []) if m != model] + [model]
|
|
3116
|
+
self.set_models((self.app.pq_models or set(shown)) - {model})
|
|
3117
|
+
|
|
3118
|
+
def action_pta_restore(self, row):
|
|
3119
|
+
removed = self.pta_removed_now()
|
|
3120
|
+
if 0 <= row < len(removed):
|
|
3121
|
+
self.app.pta_removed.remove(removed[row])
|
|
3122
|
+
self.set_models(self.app.pq_models | {removed[row]})
|
|
3123
|
+
|
|
3124
|
+
def action_pta_restore_all(self):
|
|
3125
|
+
self.app.pta_removed = []
|
|
3126
|
+
self.set_models(set())
|
|
3127
|
+
|
|
3128
|
+
def pta_removed_now(self):
|
|
3129
|
+
"""Models removed by a click that are still left out (the Models button may have put them back)."""
|
|
3130
|
+
return [m for m in getattr(self.app, "pta_removed", []) if self.app.pq_models and m not in self.app.pq_models]
|
|
3131
|
+
|
|
3024
3132
|
# -- insights: charts with what to do next (insights.py); built when the tab is first shown
|
|
3025
3133
|
@on(TabbedContent.TabActivated)
|
|
3026
3134
|
def tab_activated(self, event):
|
|
@@ -3100,7 +3208,16 @@ class ResultsScreen(Screen):
|
|
|
3100
3208
|
# none picked: every model with enough answers (pta.index leaves out the rest, and says so)
|
|
3101
3209
|
result = pta.index(rows, sorted(chosen) or None, pta.memory_gb(self.infos))
|
|
3102
3210
|
self.query_one("#pta-note", Static).update(pta.scope_note(result, sorted(present)))
|
|
3103
|
-
self.
|
|
3211
|
+
self.pta_result = result
|
|
3212
|
+
removed = [m for m in self.pta_removed_now() if m in present]
|
|
3213
|
+
self.query_one("#pta-bars", Static).update(
|
|
3214
|
+
"\n".join(pta.bars(result, click="screen.pta_remove")
|
|
3215
|
+
+ ([f"\n[dim]Click a name to remove it. Removed:[/dim] "
|
|
3216
|
+
+ " ".join(f"[@click=screen.pta_restore({self.pta_removed_now().index(m)})]{rich_escape(m)}[/]"
|
|
3217
|
+
for m in removed)
|
|
3218
|
+
+ " [dim]·[/dim] [@click=screen.pta_restore_all]show all[/]"] if removed
|
|
3219
|
+
else ["\n[dim]Click a name to remove it from view.[/dim]"]))
|
|
3220
|
+
if result["questions"] else "")
|
|
3104
3221
|
self.query_one("#pta-left-out", Static).update(
|
|
3105
3222
|
"\n".join(f"[dim]{rich_escape(line)}[/dim]" for line in pta.left_out_lines(result)))
|
|
3106
3223
|
header, table = pta.table(result)
|
|
@@ -1130,6 +1130,111 @@ class AddToRun(unittest.TestCase):
|
|
|
1130
1130
|
self.assertEqual(fewer, 4) # its 4 questions are no longer counted
|
|
1131
1131
|
self.assertEqual(out["running"], [False, True])
|
|
1132
1132
|
|
|
1133
|
+
def test_tui_copies_reasoning_and_answer(self):
|
|
1134
|
+
path = os.path.join(self.ws, "models.toml")
|
|
1135
|
+
write(path, read(path).replace('"--delay", "0.2"', '"--delay", "1.5"')) # reasoning first, then a wait
|
|
1136
|
+
fake = os.path.join(self.tmp.name, "bin")
|
|
1137
|
+
os.makedirs(fake)
|
|
1138
|
+
write(os.path.join(fake, "pbcopy"), '#!/bin/sh\ncat > "$CLIP_OUT"\n')
|
|
1139
|
+
os.chmod(os.path.join(fake, "pbcopy"), 0o755)
|
|
1140
|
+
clip = os.path.join(self.tmp.name, "clip.txt")
|
|
1141
|
+
code = textwrap.dedent("""
|
|
1142
|
+
import asyncio, json, os, time
|
|
1143
|
+
from tuieval.tui import AnswerScreen, EvalsApp, RunScreen
|
|
1144
|
+
from textual.widgets import SelectionList
|
|
1145
|
+
def pasted():
|
|
1146
|
+
with open(os.environ["CLIP_OUT"]) as f:
|
|
1147
|
+
text = f.read()
|
|
1148
|
+
os.remove(os.environ["CLIP_OUT"])
|
|
1149
|
+
return text
|
|
1150
|
+
async def go():
|
|
1151
|
+
app = EvalsApp({})
|
|
1152
|
+
async with app.run_test(size=(160, 50)) as pilot:
|
|
1153
|
+
await pilot.pause()
|
|
1154
|
+
s = app.screen
|
|
1155
|
+
s.query_one("#suites", SelectionList).select("apps")
|
|
1156
|
+
s.selected_models = {"a"}
|
|
1157
|
+
s.refresh_models()
|
|
1158
|
+
s.query_one("#tier-certify").value = True
|
|
1159
|
+
await pilot.pause()
|
|
1160
|
+
s.action_start()
|
|
1161
|
+
deadline = time.time() + 180 # CI machines can take long to start servers
|
|
1162
|
+
while time.time() < deadline:
|
|
1163
|
+
await pilot.pause(0.1)
|
|
1164
|
+
if isinstance(app.screen, RunScreen) and app.screen.req_started:
|
|
1165
|
+
break
|
|
1166
|
+
run, out = app.screen, {}
|
|
1167
|
+
await pilot.pause(0.5) # the reasoning has streamed; the answer waits 1.5 s
|
|
1168
|
+
await pilot.press("y")
|
|
1169
|
+
await pilot.pause()
|
|
1170
|
+
out["live"] = pasted()
|
|
1171
|
+
deadline = time.time() + 120
|
|
1172
|
+
while run.running and time.time() < deadline:
|
|
1173
|
+
await pilot.pause(0.2)
|
|
1174
|
+
app.push_screen(AnswerScreen(list(run.recent_entries.values())))
|
|
1175
|
+
await pilot.pause()
|
|
1176
|
+
for key in "ya":
|
|
1177
|
+
await pilot.press(key)
|
|
1178
|
+
await pilot.pause()
|
|
1179
|
+
out[key] = pasted()
|
|
1180
|
+
print(json.dumps(out))
|
|
1181
|
+
asyncio.run(go())
|
|
1182
|
+
""")
|
|
1183
|
+
p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=600,
|
|
1184
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS="", CLIP_OUT=clip,
|
|
1185
|
+
PATH=fake + os.pathsep + os.environ["PATH"]))
|
|
1186
|
+
out = json.loads(p.stdout.strip().splitlines()[-1]) if p.stdout.strip() else p.stderr
|
|
1187
|
+
self.assertEqual(out, {"live": "Thinking it over. ", "y": "Thinking it over. ", "a": "ANSWER: 1"}, out)
|
|
1188
|
+
|
|
1189
|
+
def test_copy_falls_back_to_the_terminal(self):
|
|
1190
|
+
from unittest import mock
|
|
1191
|
+
from tuieval.tui import copy_text
|
|
1192
|
+
app = mock.Mock()
|
|
1193
|
+
with mock.patch.dict(os.environ, {"PATH": self.tmp.name}): # no clipboard tool here
|
|
1194
|
+
self.assertFalse(copy_text(app, "some reasoning"))
|
|
1195
|
+
app.copy_to_clipboard.assert_called_once_with("some reasoning")
|
|
1196
|
+
|
|
1197
|
+
def test_tui_counts_ticked_models_and_names_filtered_ones_before_start(self):
|
|
1198
|
+
code = textwrap.dedent("""
|
|
1199
|
+
import asyncio, json
|
|
1200
|
+
from tuieval.tui import EvalsApp, ConfirmScreen
|
|
1201
|
+
from textual.widgets import SelectionList, Input, Label
|
|
1202
|
+
async def go():
|
|
1203
|
+
app = EvalsApp({})
|
|
1204
|
+
async with app.run_test(size=(160, 50)) as pilot:
|
|
1205
|
+
await pilot.pause()
|
|
1206
|
+
s, out = app.screen, {}
|
|
1207
|
+
legend = lambda: str(s.query_one("#models-legend", Label).render())
|
|
1208
|
+
out["none"] = legend()
|
|
1209
|
+
s.query_one("#suites", SelectionList).select("apps")
|
|
1210
|
+
models = s.query_one("#models", SelectionList)
|
|
1211
|
+
models.select("a")
|
|
1212
|
+
models.select("b")
|
|
1213
|
+
await pilot.pause()
|
|
1214
|
+
out["two"] = legend()
|
|
1215
|
+
s.query_one("#filter", Input).value = "zzz-nothing"
|
|
1216
|
+
await pilot.pause()
|
|
1217
|
+
out["filtered"] = legend()
|
|
1218
|
+
s.action_start()
|
|
1219
|
+
await pilot.pause()
|
|
1220
|
+
out["asked"] = isinstance(app.screen, ConfirmScreen) and str(app.screen.question)
|
|
1221
|
+
await pilot.press("n")
|
|
1222
|
+
await pilot.pause()
|
|
1223
|
+
out["cancelled"] = app.screen is s and not app.is_busy()
|
|
1224
|
+
print(json.dumps(out))
|
|
1225
|
+
asyncio.run(go())
|
|
1226
|
+
""")
|
|
1227
|
+
p = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=600,
|
|
1228
|
+
env=dict(os.environ, TUIEVAL_HOME=self.ws, TUIEVAL_DETECT_PORTS=""))
|
|
1229
|
+
out = json.loads(p.stdout.strip().splitlines()[-1]) if p.stdout.strip() else p.stderr
|
|
1230
|
+
self.assertIsInstance(out, dict, out)
|
|
1231
|
+
self.assertIn("none selected", out["none"])
|
|
1232
|
+
self.assertIn("2 selected", out["two"])
|
|
1233
|
+
self.assertNotIn("not shown", out["two"])
|
|
1234
|
+
self.assertIn("2 not shown: a, b", out["filtered"])
|
|
1235
|
+
self.assertIn("All selected: a, b", out["asked"])
|
|
1236
|
+
self.assertTrue(out["cancelled"])
|
|
1237
|
+
|
|
1133
1238
|
def test_tui_asks_before_skipping_a_model(self):
|
|
1134
1239
|
path = os.path.join(self.ws, "models.toml")
|
|
1135
1240
|
write(path, read(path).replace('"--delay", "0.2"', '"--delay", "3"')) # still running while it's asked
|
|
@@ -1568,6 +1673,23 @@ class PTAIndex(unittest.TestCase):
|
|
|
1568
1673
|
app.screen.fill_pta() # rebuilt: still sorted
|
|
1569
1674
|
await pilot.pause(0.2)
|
|
1570
1675
|
sorts["kept"] = [str(pta_t.get_row_at(r)[0]) for r in range(pta_t.row_count)]
|
|
1676
|
+
bars_w = app.screen.query_one("#pta-bars", Static)
|
|
1677
|
+
shown = [x["model"] for x in app.screen.pta_result["models"]]
|
|
1678
|
+
await pilot.click(bars_w, offset=(1, 1 + 2 + shown.index("hosted"))) # a click on its name
|
|
1679
|
+
await pilot.pause(0.3)
|
|
1680
|
+
clicks = {"removed": [sorted(app.pq_models), [x["model"] for x in app.screen.pta_result["models"]],
|
|
1681
|
+
"Removed: hosted" in str(bars_w.render())]}
|
|
1682
|
+
await pilot.click(bars_w, offset=(1, 1 + 2)) # the last one stays
|
|
1683
|
+
await pilot.pause(0.3)
|
|
1684
|
+
clicks["last"] = sorted(app.pq_models)
|
|
1685
|
+
app.screen.action_pta_restore(0)
|
|
1686
|
+
await pilot.pause(0.3)
|
|
1687
|
+
clicks["restored"] = [sorted(app.pq_models), len(app.screen.pta_result["models"])]
|
|
1688
|
+
app.screen.action_pta_remove(shown.index("hosted"))
|
|
1689
|
+
app.screen.action_pta_restore_all()
|
|
1690
|
+
await pilot.pause(0.3)
|
|
1691
|
+
clicks["all"] = [sorted(app.pq_models), "Removed" in str(bars_w.render())]
|
|
1692
|
+
sorts["clicks"] = clicks
|
|
1571
1693
|
app.screen.query_one(TabbedContent).active = "tab-failures"
|
|
1572
1694
|
await pilot.pause(0.3)
|
|
1573
1695
|
f = app.screen.query_one("#failures", DataTable)
|
|
@@ -1606,6 +1728,11 @@ class PTAIndex(unittest.TestCase):
|
|
|
1606
1728
|
s = got["sorts"] # a click on a column name sorts by it, a second click reverses
|
|
1607
1729
|
self.assertEqual((s["up"], s["down"], s["kept"]), (["hosted", "good"], ["good", "hosted"], ["good", "hosted"]))
|
|
1608
1730
|
self.assertIn("answers right ▼", s["labels"])
|
|
1731
|
+
c = s["clicks"] # a click on a name in the bars leaves it out of every tab; the last one stays
|
|
1732
|
+
self.assertEqual(c["removed"], [["good"], ["good"], True])
|
|
1733
|
+
self.assertEqual(c["last"], ["good"])
|
|
1734
|
+
self.assertEqual(c["restored"], [["good", "hosted"], 2])
|
|
1735
|
+
self.assertEqual(c["all"], [[], False])
|
|
1609
1736
|
self.assertEqual((s["tests"][0], s["opened"]), ("apps: d", "d"))
|
|
1610
1737
|
fails = got["fails"]
|
|
1611
1738
|
self.assertEqual(fails["all"], [4, "4 failures"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/tool_call/pack.toml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev107 → tuieval-0.2.0.dev112}/src/tuieval/templates/packs/tool_call/tools.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|