tuieval 0.2.0.dev123__tar.gz → 0.2.0.dev125__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.dev123 → tuieval-0.2.0.dev125}/CHANGELOG.md +1 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/PKG-INFO +4 -2
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/README.md +3 -1
- tuieval-0.2.0.dev125/docs/agent-packs.md +142 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/models.md +1 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/writing-packs.md +2 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/_version.py +2 -2
- tuieval-0.2.0.dev125/src/tuieval/agents.py +353 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/compare.py +33 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/engine.py +202 -94
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/packs.py +98 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/run_evals.py +7 -2
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/selftest.py +39 -6
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/tui.py +37 -4
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/tests/test_tuieval.py +269 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/.gitignore +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/LICENSE +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/dates.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/insights.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/logs.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/pta.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev123 → tuieval-0.2.0.dev125}/tests/mock_server.py +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Agent packs (`kind = "agent"`): each test is a small project with a task, worked on by a command-line agent you define in `[agents]`, using the model tuieval serves, and graded by the task's visible and hidden tests. Results show the agent's stats, diff and transcript; `tuieval compare` adds an Agent tasks table; `tuieval selftest` checks each task's reference and wrong fixes. See docs/agent-packs.md.
|
|
5
6
|
- Answers that used more tokens than the `max_tokens` sent (a provider that didn't stop them) are marked over budget: `(n over budget)` in the PTA index, an **over budget** column, `over` in Per question and a note in the answer view. `[defaults] over_budget = "fail"` fails them instead. Works on stored results.
|
|
6
7
|
- Fix: Results → Per question opened the answers of the model one column to the right of the cell picked, and crashed on the last model's column.
|
|
7
8
|
- Fix: `v` on the run screen switches to the last model still running when the stream shows one that has finished.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev125
|
|
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
|
|
@@ -111,6 +111,8 @@ packs/support-bot/
|
|
|
111
111
|
|
|
112
112
|
Built-in graders: `answer` (a number or word on an `ANSWER:` line, or a correct refusal when the data isn't there), `rag` (grounded answers with citations), `reply` (free-form replies against rules), `tool_call` (the right tool with the right arguments, or rightly none) and `code` (Python run against hidden tests). Your own grader is one Python file in the workspace's `graders/` folder.
|
|
113
113
|
|
|
114
|
+
**Agent packs** test models inside a coding agent: each test is a small project with a task. An agent you choose works on it, using the model tuieval serves, and the project's tests grade the result. See [docs/agent-packs.md](docs/agent-packs.md).
|
|
115
|
+
|
|
114
116
|
`tuieval new-pack <name> --grader <grader>` creates a pack with working examples for any grader. **See [docs/writing-packs.md](docs/writing-packs.md)** for every field, gates, critical failures, difficulty labels, custom graders and how to write tests that separate good models from weak ones.
|
|
115
117
|
|
|
116
118
|
## The workspace
|
|
@@ -186,7 +188,7 @@ tuieval help
|
|
|
186
188
|
|
|
187
189
|
## Safety
|
|
188
190
|
|
|
189
|
-
⚠️ The `code` grader executes model-written code on your machine. Run code packs inside a container or VM with no credentials in the environment.
|
|
191
|
+
⚠️ The `code` grader executes model-written code on your machine, and an agent pack's agent runs commands as you. Run code and agent packs inside a container or VM with no credentials in the environment.
|
|
190
192
|
|
|
191
193
|
## Development
|
|
192
194
|
|
|
@@ -85,6 +85,8 @@ packs/support-bot/
|
|
|
85
85
|
|
|
86
86
|
Built-in graders: `answer` (a number or word on an `ANSWER:` line, or a correct refusal when the data isn't there), `rag` (grounded answers with citations), `reply` (free-form replies against rules), `tool_call` (the right tool with the right arguments, or rightly none) and `code` (Python run against hidden tests). Your own grader is one Python file in the workspace's `graders/` folder.
|
|
87
87
|
|
|
88
|
+
**Agent packs** test models inside a coding agent: each test is a small project with a task. An agent you choose works on it, using the model tuieval serves, and the project's tests grade the result. See [docs/agent-packs.md](docs/agent-packs.md).
|
|
89
|
+
|
|
88
90
|
`tuieval new-pack <name> --grader <grader>` creates a pack with working examples for any grader. **See [docs/writing-packs.md](docs/writing-packs.md)** for every field, gates, critical failures, difficulty labels, custom graders and how to write tests that separate good models from weak ones.
|
|
89
91
|
|
|
90
92
|
## The workspace
|
|
@@ -160,7 +162,7 @@ tuieval help
|
|
|
160
162
|
|
|
161
163
|
## Safety
|
|
162
164
|
|
|
163
|
-
⚠️ The `code` grader executes model-written code on your machine. Run code packs inside a container or VM with no credentials in the environment.
|
|
165
|
+
⚠️ The `code` grader executes model-written code on your machine, and an agent pack's agent runs commands as you. Run code and agent packs inside a container or VM with no credentials in the environment.
|
|
164
166
|
|
|
165
167
|
## Development
|
|
166
168
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Agent packs
|
|
2
|
+
|
|
3
|
+
An ordinary pack sends one prompt and grades one answer. That ranks models fairly, but it can't show how a model does inside a coding agent: reading files, editing them, running the tests, reacting to failures and deciding when it's done. An **agent pack** tests that.
|
|
4
|
+
|
|
5
|
+
Each test is a small project with a task. tuieval serves the model as for any other pack, hands a scratch copy of the project to a command-line agent you choose, and grades what the agent leaves behind with tests. Results show up beside your other packs.
|
|
6
|
+
|
|
7
|
+
## How a task runs
|
|
8
|
+
|
|
9
|
+
1. **Copy the project.** The task's `repo/` is copied to a scratch folder and committed to git, so the agent's changes can be shown as a diff.
|
|
10
|
+
2. **Run the agent.** Its command runs in that folder. Every line it prints shows live in the run screen.
|
|
11
|
+
3. **Stop it at the time limit.** The agent is stopped at the task's time limit (`minutes`). It keeps to the turn limit (`max_turns`) itself.
|
|
12
|
+
4. **Grade the folder** with pytest. Two sets of tests run:
|
|
13
|
+
- the task's **original** visible tests. If the agent edits or deletes them, it gains nothing, and the edit is recorded;
|
|
14
|
+
- its **hidden** tests, which the agent never sees.
|
|
15
|
+
|
|
16
|
+
The task passes when every test passes. Its score is the share of hidden tests passed, so a near miss is visible.
|
|
17
|
+
|
|
18
|
+
Server failures never count against the model. If the model's server stops, or the agent reports that the server failed, the task is asked again. If the server keeps failing, the pack stops, like any pack whose server keeps failing.
|
|
19
|
+
|
|
20
|
+
## The pack
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
packs/agentic-coding/
|
|
24
|
+
pack.toml
|
|
25
|
+
tasks/
|
|
26
|
+
fix-fees/
|
|
27
|
+
task.md the instruction the agent gets
|
|
28
|
+
task.toml optional: description, category, difficulty, minutes, max_turns, visible_tests
|
|
29
|
+
repo/ the project the agent works on, with its visible tests (tests/ by default)
|
|
30
|
+
hidden/ tests the agent never sees, copied in only for grading
|
|
31
|
+
solution/ optional: a reference fix, copied over repo/ (a .delete file lists files to remove)
|
|
32
|
+
wrong/<name>/ optional: plausible wrong fixes that must fail
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```toml
|
|
36
|
+
# pack.toml
|
|
37
|
+
label = "Agentic coding"
|
|
38
|
+
group = "Coding"
|
|
39
|
+
kind = "agent" # task folders run by an agent
|
|
40
|
+
agent = "my-agent" # optional: which [agents.<name>]; default: [defaults] agent
|
|
41
|
+
python = "agentic/.venv/bin/python" # optional: the Python the tasks run with (relative to the workspace)
|
|
42
|
+
minutes = 15 # optional: default time limit per task
|
|
43
|
+
max_turns = 40 # optional: default turn limit per task
|
|
44
|
+
min_context = 32768 # optional: context a model needs for this pack (default 32768)
|
|
45
|
+
|
|
46
|
+
[certify]
|
|
47
|
+
repeat = 1
|
|
48
|
+
[gate]
|
|
49
|
+
min_accuracy = 0.0
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```toml
|
|
53
|
+
# tasks/fix-fees/task.toml
|
|
54
|
+
description = "Fees counted twice"
|
|
55
|
+
category = "bug fix"
|
|
56
|
+
difficulty = "medium"
|
|
57
|
+
minutes = 15
|
|
58
|
+
max_turns = 40
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **Python:** the pack's Python must have pytest installed, plus whatever the tasks import. Without a `python` setting, tuieval's own interpreter is used (`pipx inject tuieval pytest` adds pytest to a pipx install).
|
|
62
|
+
- **Fingerprint:** it covers each task's `task.md`, its limits and every file under `repo/` and `hidden/`. Editing any of them reruns that pack. The description, category and difficulty are labels only.
|
|
63
|
+
- **Selftest:** `tuieval selftest <pack>` checks three things for every task: the untouched `repo/` fails, `solution/` passes, and every `wrong/<name>/` fails. A task that passes untouched, or a wrong fix that passes, can't tell good agents from bad ones.
|
|
64
|
+
|
|
65
|
+
## The agent
|
|
66
|
+
|
|
67
|
+
Define the agent in `models.toml`. Its command is any program; `{root}` is your workspace.
|
|
68
|
+
|
|
69
|
+
```toml
|
|
70
|
+
[agents.my-agent]
|
|
71
|
+
cmd = ["python3", "{root}/agents/my_agent.py"]
|
|
72
|
+
|
|
73
|
+
[defaults]
|
|
74
|
+
agent = "my-agent"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The agent's name and command are part of an agent pack's settings, so changing the command reruns the pack. The agent's version isn't: each answer records it, and `tuieval compare` marks a pack that ran on more than one version as MIXED.
|
|
78
|
+
|
|
79
|
+
Most agents need a small **adapter**: a script that reads what tuieval provides, starts the agent with it, and writes the stats file. pi, Aider, Codex CLI, OpenCode and similar agents can all be wrapped this way. The adapter gets everything from its environment:
|
|
80
|
+
|
|
81
|
+
| Variable | Contents |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `TUIEVAL_BASE_URL` | OpenAI-compatible URL of the model (ends in `/v1`) |
|
|
84
|
+
| `TUIEVAL_MODEL_ID` | the model name to send |
|
|
85
|
+
| `TUIEVAL_API_KEY` | the API key for hosted models; empty for local ones |
|
|
86
|
+
| `TUIEVAL_PROMPT_FILE` | the task's instruction |
|
|
87
|
+
| `TUIEVAL_SAMPLING` | the model's sampling settings as JSON (temperature, top_p, max_tokens, reasoning_effort, …) |
|
|
88
|
+
| `TUIEVAL_CONTEXT` | the model's context size in tokens, if known |
|
|
89
|
+
| `TUIEVAL_MAX_TURNS` | the task's turn limit |
|
|
90
|
+
| `TUIEVAL_TASK` | the task's id |
|
|
91
|
+
| `TUIEVAL_STATS_FILE` | where to write the stats (below) |
|
|
92
|
+
|
|
93
|
+
- **Working folder:** the scratch copy of the task.
|
|
94
|
+
- **PATH:** starts with the pack's Python, so `python` and `pytest` work.
|
|
95
|
+
- **Exit code:** ignored. Only the tests decide.
|
|
96
|
+
|
|
97
|
+
**Stats file.** It's optional; without it, tuieval records only the time taken. Any of these keys may be written:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{"agent": "my-agent 1.4.2", "turns": 14, "input_tokens": 182340, "output_tokens": 9120,
|
|
101
|
+
"tool_calls": 22, "tool_errors": 1, "ran_tests": true, "stop": "done",
|
|
102
|
+
"final_message": "Fixed: the ledger subtracted the fee twice."}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- **`tool_errors`:** count failed edits and malformed tool calls, not commands that exit nonzero.
|
|
106
|
+
- **`stop`:** `done`, `turn limit` or `error`.
|
|
107
|
+
- **`server_error`:** if the model's server failed rather than the model, write this key with the error text. The task isn't counted and is asked again.
|
|
108
|
+
|
|
109
|
+
A minimal adapter:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
#!/usr/bin/env python3
|
|
113
|
+
import json, os, subprocess
|
|
114
|
+
|
|
115
|
+
prompt = open(os.environ["TUIEVAL_PROMPT_FILE"]).read()
|
|
116
|
+
proc = subprocess.run(["my-agent", "--base-url", os.environ["TUIEVAL_BASE_URL"],
|
|
117
|
+
"--model", os.environ["TUIEVAL_MODEL_ID"], "--max-turns", os.environ["TUIEVAL_MAX_TURNS"],
|
|
118
|
+
"--print", prompt], capture_output=True, text=True)
|
|
119
|
+
print(proc.stdout) # shows in the run screen and the transcript
|
|
120
|
+
with open(os.environ["TUIEVAL_STATS_FILE"], "w") as f:
|
|
121
|
+
json.dump({"agent": "my-agent", "stop": "done" if proc.returncode == 0 else "error",
|
|
122
|
+
"final_message": proc.stdout.strip().splitlines()[-1] if proc.stdout.strip() else ""}, f)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For a live view in the run screen, print a line per turn and tool call as they happen rather than at the end.
|
|
126
|
+
|
|
127
|
+
## Results
|
|
128
|
+
|
|
129
|
+
Agent tasks appear wherever other answers do: Per question, Failures, the scorecard and the PTA index.
|
|
130
|
+
|
|
131
|
+
- **Answer view:**
|
|
132
|
+
- the agent's stats: version, turns, tokens, tool errors, and whether it ran the tests;
|
|
133
|
+
- its final message and its changes as a diff;
|
|
134
|
+
- the full transcript, with the test output;
|
|
135
|
+
- under **What's checked**, the visible and hidden tests.
|
|
136
|
+
- **`tuieval compare`:** adds an **Agent tasks** table per model and pack. It shows tasks passed, median minutes and turns, output tokens, tool errors, how often the tests were run, limits hit, tests edited, and the agent version.
|
|
137
|
+
|
|
138
|
+
`tuieval regrade` leaves agent results alone: they're graded when they run, so retest the pack instead.
|
|
139
|
+
|
|
140
|
+
## Safety
|
|
141
|
+
|
|
142
|
+
⚠️ The agent runs commands as you, with your permissions. It works in a scratch folder, but nothing stops it from reaching outside that folder. Run agent packs in a container or VM with no credentials in the environment. The hidden tests and reference fixes live in your pack folder, so an agent that searched your disk could find them. The transcript would show it.
|
|
@@ -210,6 +210,7 @@ request_timeout_ms = 1800000 # per request; long thinking runs need it
|
|
|
210
210
|
stall_timeout_ms = 300000 # no tokens this long once answering began: the server stalled (retried, not counted)
|
|
211
211
|
over_budget = "flag" # answers past the max_tokens sent (a provider that didn't stop them): "flag" or "fail"
|
|
212
212
|
# reference = "<label>" # the model Results → Insights compares against by default
|
|
213
|
+
# agent = "<name>" # the [agents.<name>] that runs agent packs (see docs/agent-packs.md)
|
|
213
214
|
ready_timeout_s = 1200 # how long a model may take to load before giving up
|
|
214
215
|
model_dirs = ["~/models"] # where "Scan for models" looks for new GGUFs
|
|
215
216
|
|
|
@@ -15,6 +15,8 @@ Two shortcuts to a first set of real questions:
|
|
|
15
15
|
- **From a spreadsheet:** `tuieval new-pack quiz --from questions.csv` makes one test per row from a `question` and an `answer` column (optional: `id`, `category`, `difficulty`, `tolerance`, and `wrong` with known-wrong answers separated by `|`). Numbers are checked with the tolerance, words case-insensitively, and `NOT_AVAILABLE` means the right answer is "the question doesn't say".
|
|
16
16
|
- **Drafted by a model:** `tuieval new-pack support-bot --grader reply --about "refund questions for a shoe store"` also writes `DRAFT-PROMPT.md`. Paste it into the strongest model you have, save the YAML it writes as `tests.yaml`, and run `tuieval selftest`: it catches drafted tests whose expected answer is wrong or whose grader can't tell a mistake from the right answer. Read the questions anyway; you know your domain better than the model.
|
|
17
17
|
|
|
18
|
+
An **agent pack** (`kind = "agent"`) has task folders instead of questions: a coding agent works on each one, and tests grade the result. See [agent-packs.md](agent-packs.md).
|
|
19
|
+
|
|
18
20
|
To try a different set of questions, copy a pack (`cp -r packs/support-bot packs/support-bot-v2`), edit the copy, and pick whichever you want in the TUI.
|
|
19
21
|
|
|
20
22
|
## Fingerprints
|
|
@@ -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.dev125'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev125')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Agent packs: coding tasks a command-line agent works on, graded by tests.
|
|
2
|
+
|
|
3
|
+
models.toml says how to start each agent (any program; {root} is the workspace):
|
|
4
|
+
|
|
5
|
+
[agents.my-agent]
|
|
6
|
+
cmd = ["python3", "{root}/agents/my_agent.py"]
|
|
7
|
+
|
|
8
|
+
[defaults]
|
|
9
|
+
agent = "my-agent" # for agent packs that don't name one (pack.toml agent = "...")
|
|
10
|
+
|
|
11
|
+
tuieval serves the model as for any pack, then for each task:
|
|
12
|
+
|
|
13
|
+
1. copies the task's repo/ to a scratch folder, a git repository, so the agent's changes show as a diff
|
|
14
|
+
2. runs the agent's command there; every line it prints shows live in the run screen
|
|
15
|
+
3. stops it at the task's time limit (task.toml minutes); the agent keeps to max_turns itself
|
|
16
|
+
4. grades the folder with pytest: the task's original visible tests (editing or deleting them gains
|
|
17
|
+
the agent nothing, and is recorded) plus its hidden tests, which the agent never sees
|
|
18
|
+
|
|
19
|
+
The agent learns the rest from its environment:
|
|
20
|
+
|
|
21
|
+
TUIEVAL_BASE_URL OpenAI-compatible URL of the model (…/v1)
|
|
22
|
+
TUIEVAL_MODEL_ID the model name to send
|
|
23
|
+
TUIEVAL_API_KEY the API key for hosted models ("" for local ones)
|
|
24
|
+
TUIEVAL_PROMPT_FILE the task's instruction
|
|
25
|
+
TUIEVAL_SAMPLING the model's sampling settings as JSON (temperature, max_tokens, reasoning_effort, …)
|
|
26
|
+
TUIEVAL_CONTEXT the model's context size (tokens), if known
|
|
27
|
+
TUIEVAL_MAX_TURNS the task's turn limit
|
|
28
|
+
TUIEVAL_STATS_FILE where it may write its stats as JSON when it finishes (all optional):
|
|
29
|
+
agent ("name version"), turns, input_tokens, output_tokens, tool_calls,
|
|
30
|
+
tool_errors (failed edits, bad tool calls; not commands that exit nonzero),
|
|
31
|
+
ran_tests, stop ("done", "turn limit" or "error"), final_message, and
|
|
32
|
+
server_error: the model's server failed, not the model (the task isn't counted
|
|
33
|
+
and is asked again)
|
|
34
|
+
|
|
35
|
+
PATH starts with the pack's Python (pack.toml python, else tuieval's), so `python` runs the tasks'
|
|
36
|
+
code and tests. The agent's exit code decides nothing; the tests do.
|
|
37
|
+
|
|
38
|
+
The agent runs as you, with your permissions, in the scratch folder: there is no sandbox.
|
|
39
|
+
"""
|
|
40
|
+
import filecmp
|
|
41
|
+
import json
|
|
42
|
+
import os
|
|
43
|
+
import queue
|
|
44
|
+
import re
|
|
45
|
+
import shutil
|
|
46
|
+
import signal
|
|
47
|
+
import subprocess
|
|
48
|
+
import sys
|
|
49
|
+
import tempfile
|
|
50
|
+
import threading
|
|
51
|
+
import time
|
|
52
|
+
import xml.etree.ElementTree as ET
|
|
53
|
+
|
|
54
|
+
PROMPT_TAIL = "\n\nYou're working in a project in the current directory. Run Python with `python`.\n"
|
|
55
|
+
GRADE_TIMEOUT_S = 300
|
|
56
|
+
DIFF_KEEP = 20_000 # characters of the diff kept in the result (the whole diff is in the log)
|
|
57
|
+
HIDDEN_RE = re.compile(r"\.?_hidden\b") # junit names of the hidden tests (a collection error starts with ".")
|
|
58
|
+
STATS_KEYS = ("agent", "turns", "input_tokens", "output_tokens", "tool_calls", "tool_errors", "ran_tests", "stop",
|
|
59
|
+
"final_message", "server_error")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AgentError(Exception):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ServerGone(Exception):
|
|
67
|
+
"""The model's server stopped while the agent worked: not the agent's fault."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def agent_for(cfg, pack):
|
|
71
|
+
"""(name, [agents.<name>] table or None) for an agent pack."""
|
|
72
|
+
name = pack.agent or cfg.get("defaults", {}).get("agent", "")
|
|
73
|
+
return name, cfg.get("agents", {}).get(name) if name else None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def identity(cfg, pack):
|
|
77
|
+
"""What a result's settings record about the agent: its name and command (a different command
|
|
78
|
+
makes earlier results outdated; a new version of the same agent doesn't, it's shown per answer)."""
|
|
79
|
+
name, a = agent_for(cfg, pack)
|
|
80
|
+
return {"name": name, "cmd": list((a or {}).get("cmd", []))}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def python_for(pack, root):
|
|
84
|
+
"""The Python an agent pack's tasks and tests run with."""
|
|
85
|
+
if not pack.python:
|
|
86
|
+
return sys.executable
|
|
87
|
+
p = os.path.expanduser(pack.python)
|
|
88
|
+
return p if os.path.isabs(p) else os.path.join(root, p)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
_PYTEST = {}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def problem(cfg, pack, root):
|
|
95
|
+
"""Why an agent pack can't run here (a note for a skipped job), or None."""
|
|
96
|
+
name, a = agent_for(cfg, pack)
|
|
97
|
+
if not name:
|
|
98
|
+
return 'no agent: add [agents.<name>] to models.toml and [defaults] agent = "<name>"'
|
|
99
|
+
if not a or not a.get("cmd"):
|
|
100
|
+
return f"agent {name!r} isn't defined: add [agents.{name}] with a cmd to models.toml"
|
|
101
|
+
py = python_for(pack, root)
|
|
102
|
+
if py not in _PYTEST:
|
|
103
|
+
try:
|
|
104
|
+
_PYTEST[py] = subprocess.run([py, "-c", "import pytest"], capture_output=True, timeout=60).returncode == 0
|
|
105
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
106
|
+
_PYTEST[py] = False
|
|
107
|
+
if not _PYTEST[py]:
|
|
108
|
+
return f"pytest isn't installed in {py} (pack.toml python), which grades the tasks"
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def env_for(python, extra):
|
|
113
|
+
"""The environment an agent and its tests run in: the pack's Python first on PATH."""
|
|
114
|
+
env = {k: v for k, v in os.environ.items() if not k.startswith(("PYTHON", "VIRTUAL_ENV", "CONDA_PREFIX"))}
|
|
115
|
+
env["PATH"] = os.path.dirname(python) + os.pathsep + env.get("PATH", "/usr/bin:/bin")
|
|
116
|
+
venv = os.path.dirname(os.path.dirname(python))
|
|
117
|
+
if os.path.isfile(os.path.join(venv, "pyvenv.cfg")):
|
|
118
|
+
env["VIRTUAL_ENV"] = venv
|
|
119
|
+
env["MPLBACKEND"] = "Agg"
|
|
120
|
+
env.update(extra)
|
|
121
|
+
return env
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _git(work, *args):
|
|
125
|
+
try:
|
|
126
|
+
return subprocess.run(["git", "-c", "user.name=tuieval", "-c", "user.email=tuieval@localhost",
|
|
127
|
+
"-c", "commit.gpgsign=false", *args], cwd=work, capture_output=True, text=True,
|
|
128
|
+
timeout=60)
|
|
129
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def prepare(test, parent):
|
|
134
|
+
"""Copy a task's repo/ into parent/work as a fresh git repository; returns the folder."""
|
|
135
|
+
work = os.path.join(parent, "work")
|
|
136
|
+
shutil.copytree(os.path.join(test["task_dir"], "repo"), work,
|
|
137
|
+
ignore=shutil.ignore_patterns("__pycache__", ".pytest_cache", ".git"))
|
|
138
|
+
if _git(work, "init", "-q") is not None:
|
|
139
|
+
with open(os.path.join(work, ".git", "info", "exclude"), "a") as f:
|
|
140
|
+
f.write("__pycache__/\n.pytest_cache/\n")
|
|
141
|
+
_git(work, "add", "-A")
|
|
142
|
+
_git(work, "commit", "-qm", "task")
|
|
143
|
+
return work
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def diff(work):
|
|
147
|
+
"""The agent's changes as a patch (empty without git)."""
|
|
148
|
+
if not os.path.isdir(os.path.join(work, ".git")):
|
|
149
|
+
return ""
|
|
150
|
+
_git(work, "add", "-A")
|
|
151
|
+
out = _git(work, "diff", "--cached", "--stat", "-p")
|
|
152
|
+
return out.stdout if out and out.returncode == 0 else ""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _kill(proc):
|
|
156
|
+
for sig in (signal.SIGTERM, signal.SIGKILL):
|
|
157
|
+
try:
|
|
158
|
+
os.killpg(proc.pid, sig)
|
|
159
|
+
except (ProcessLookupError, PermissionError):
|
|
160
|
+
return
|
|
161
|
+
try:
|
|
162
|
+
proc.wait(timeout=5)
|
|
163
|
+
return
|
|
164
|
+
except subprocess.TimeoutExpired:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def run(cmd, work, env, limit_s, on_line, check, alive, log):
|
|
169
|
+
"""Run the agent in work until it exits or limit_s passes. Every output line goes to on_line and
|
|
170
|
+
the log file. check() may raise (a cancel): the agent is stopped first. Returns (stop, seconds,
|
|
171
|
+
exit code); stop is "exited" or "time limit". Raises ServerGone when alive() turns False."""
|
|
172
|
+
t0 = time.monotonic()
|
|
173
|
+
proc = subprocess.Popen(cmd, cwd=work, env=env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
|
174
|
+
stderr=subprocess.STDOUT, start_new_session=True)
|
|
175
|
+
lines = queue.Queue()
|
|
176
|
+
|
|
177
|
+
def read():
|
|
178
|
+
for raw in iter(proc.stdout.readline, b""):
|
|
179
|
+
lines.put(raw.decode("utf-8", "replace"))
|
|
180
|
+
lines.put(None)
|
|
181
|
+
threading.Thread(target=read, daemon=True).start()
|
|
182
|
+
stop = "exited"
|
|
183
|
+
try:
|
|
184
|
+
while True:
|
|
185
|
+
check()
|
|
186
|
+
if not alive():
|
|
187
|
+
raise ServerGone("the model's server stopped while the agent worked")
|
|
188
|
+
if time.monotonic() - t0 > limit_s:
|
|
189
|
+
stop = "time limit"
|
|
190
|
+
break
|
|
191
|
+
try:
|
|
192
|
+
line = lines.get(timeout=0.5)
|
|
193
|
+
except queue.Empty:
|
|
194
|
+
continue
|
|
195
|
+
if line is None:
|
|
196
|
+
break
|
|
197
|
+
log.write(line)
|
|
198
|
+
on_line(line)
|
|
199
|
+
finally:
|
|
200
|
+
if proc.poll() is None:
|
|
201
|
+
_kill(proc)
|
|
202
|
+
else: # exited: its own children may still run
|
|
203
|
+
try:
|
|
204
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
205
|
+
except (ProcessLookupError, PermissionError):
|
|
206
|
+
pass
|
|
207
|
+
return stop, round(time.monotonic() - t0, 1), proc.wait()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def read_stats(path):
|
|
211
|
+
"""The stats the agent wrote (only the known keys), or {}."""
|
|
212
|
+
try:
|
|
213
|
+
with open(path) as f:
|
|
214
|
+
data = json.load(f)
|
|
215
|
+
except (OSError, ValueError):
|
|
216
|
+
return {}
|
|
217
|
+
return {k: data[k] for k in STATS_KEYS if k in data} if isinstance(data, dict) else {}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def tampered(test, work):
|
|
221
|
+
"""Visible test files the agent changed or removed."""
|
|
222
|
+
src = os.path.join(test["task_dir"], "repo", test["visible_tests"])
|
|
223
|
+
out = []
|
|
224
|
+
for dirpath, _, files in os.walk(src):
|
|
225
|
+
for fn in sorted(files):
|
|
226
|
+
if fn.endswith(".py"):
|
|
227
|
+
a = os.path.join(dirpath, fn)
|
|
228
|
+
rel = os.path.relpath(a, os.path.join(test["task_dir"], "repo"))
|
|
229
|
+
b = os.path.join(work, rel)
|
|
230
|
+
if not os.path.isfile(b) or not filecmp.cmp(a, b, shallow=False):
|
|
231
|
+
out.append(rel)
|
|
232
|
+
return sorted(out)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _cases(xml_path):
|
|
236
|
+
"""{test name: "pass" | "fail" | "skip"} from a pytest junit file."""
|
|
237
|
+
try:
|
|
238
|
+
root = ET.parse(xml_path).getroot()
|
|
239
|
+
except (OSError, ET.ParseError):
|
|
240
|
+
return {}
|
|
241
|
+
out = {}
|
|
242
|
+
for case in root.iter("testcase"):
|
|
243
|
+
name = f'{case.get("classname")}.{case.get("name")}'
|
|
244
|
+
bad = case.find("failure") is not None or case.find("error") is not None
|
|
245
|
+
out[name] = "skip" if case.find("skipped") is not None else "fail" if bad else "pass"
|
|
246
|
+
return out
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def grade(test, work, python):
|
|
250
|
+
"""Run the task's original visible tests and its hidden tests on a copy of work.
|
|
251
|
+
Returns {pass, score, visible, hidden, failed, output}; score is the share of hidden tests passed."""
|
|
252
|
+
with tempfile.TemporaryDirectory(prefix="tuieval-grade-") as tmp:
|
|
253
|
+
g = os.path.join(tmp, "work")
|
|
254
|
+
shutil.copytree(work, g, ignore=shutil.ignore_patterns(".git", "__pycache__", ".pytest_cache"))
|
|
255
|
+
vis_src = os.path.join(test["task_dir"], "repo", test["visible_tests"])
|
|
256
|
+
vis = os.path.join(g, test["visible_tests"])
|
|
257
|
+
has_visible = os.path.isdir(vis_src)
|
|
258
|
+
if os.path.isdir(vis):
|
|
259
|
+
shutil.rmtree(vis)
|
|
260
|
+
if has_visible:
|
|
261
|
+
shutil.copytree(vis_src, vis)
|
|
262
|
+
shutil.copytree(os.path.join(test["task_dir"], "hidden"), os.path.join(g, "_hidden"))
|
|
263
|
+
xml = os.path.join(tmp, "junit.xml")
|
|
264
|
+
cmd = [python, "-m", "pytest", "-q", "-p", "no:cacheprovider", "-o", "addopts=",
|
|
265
|
+
"--continue-on-collection-errors", f"--junitxml={xml}",
|
|
266
|
+
*([test["visible_tests"]] if has_visible else []), "_hidden"]
|
|
267
|
+
try:
|
|
268
|
+
p = subprocess.run(cmd, cwd=g, env=env_for(python, {}), capture_output=True, text=True,
|
|
269
|
+
timeout=GRADE_TIMEOUT_S)
|
|
270
|
+
except subprocess.TimeoutExpired:
|
|
271
|
+
return {"pass": False, "score": 0.0, "visible": "0/0", "hidden": "0/0", "failed": [],
|
|
272
|
+
"output": f"the tests ran over {GRADE_TIMEOUT_S} s"}
|
|
273
|
+
cases = _cases(xml)
|
|
274
|
+
hidden = {k: v for k, v in cases.items() if HIDDEN_RE.match(k)}
|
|
275
|
+
visible = {k: v for k, v in cases.items() if not HIDDEN_RE.match(k)}
|
|
276
|
+
failed = [k for k, v in cases.items() if v == "fail"]
|
|
277
|
+
passed_hidden = sum(v == "pass" for v in hidden.values())
|
|
278
|
+
|
|
279
|
+
def frac(c):
|
|
280
|
+
return f'{sum(v == "pass" for v in c.values())}/{len(c)}'
|
|
281
|
+
# a module that no longer imports shows as a collection error: a nonzero exit, never a pass
|
|
282
|
+
ok = bool(hidden) and not failed and p.returncode == 0
|
|
283
|
+
return {"pass": ok, "score": round(passed_hidden / len(hidden), 4) if hidden else 0.0,
|
|
284
|
+
"visible": frac(visible), "hidden": frac(hidden), "failed": failed,
|
|
285
|
+
"output": (p.stdout[-6000:] + p.stderr[-2000:]).strip()}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def reason(g, stop, edited, exit_code, stats):
|
|
289
|
+
"""One line about a graded task, for the answer's reason."""
|
|
290
|
+
parts = [f"hidden tests {g['hidden']}, visible {g['visible']}"]
|
|
291
|
+
if g["failed"]:
|
|
292
|
+
names = [re.sub(r"^\.?(_hidden|tests)\.", "", f) for f in g["failed"]]
|
|
293
|
+
parts.append("failed: " + ", ".join(names[:6]) + (f" and {len(names) - 6} more" if len(names) > 6 else ""))
|
|
294
|
+
elif not g["pass"] and g["output"]:
|
|
295
|
+
parts.append(g["output"].splitlines()[-1][:200])
|
|
296
|
+
if stop == "time limit":
|
|
297
|
+
parts.append("stopped at the time limit")
|
|
298
|
+
elif stats.get("stop") == "turn limit":
|
|
299
|
+
parts.append("stopped at the turn limit")
|
|
300
|
+
elif stats.get("stop") == "error":
|
|
301
|
+
parts.append("the agent stopped with an error")
|
|
302
|
+
elif exit_code not in (0, None) and not stats:
|
|
303
|
+
parts.append(f"the agent exited with code {exit_code}")
|
|
304
|
+
if edited:
|
|
305
|
+
parts.append("edited the tests: " + ", ".join(edited))
|
|
306
|
+
return "; ".join(parts)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def check_task(test, python):
|
|
310
|
+
"""Selftest errors for one task: the untouched repo must fail, solution/ must pass, each
|
|
311
|
+
wrong/<name>/ must fail. (A task without solution/ is only checked to fail untouched.)"""
|
|
312
|
+
errors = []
|
|
313
|
+
with tempfile.TemporaryDirectory(prefix="tuieval-selftest-") as tmp:
|
|
314
|
+
def variant(name, overlay_dir=None):
|
|
315
|
+
work = os.path.join(tmp, name)
|
|
316
|
+
shutil.copytree(os.path.join(test["task_dir"], "repo"), work,
|
|
317
|
+
ignore=shutil.ignore_patterns("__pycache__", ".pytest_cache", ".git"))
|
|
318
|
+
if overlay_dir:
|
|
319
|
+
overlay(overlay_dir, work)
|
|
320
|
+
return grade(test, work, python)
|
|
321
|
+
if variant("base")["pass"]:
|
|
322
|
+
errors.append(f"{test['id']}: the untouched repo already passes its tests")
|
|
323
|
+
sol = os.path.join(test["task_dir"], "solution")
|
|
324
|
+
if os.path.isdir(sol):
|
|
325
|
+
g = variant("solution", sol)
|
|
326
|
+
if not g["pass"]:
|
|
327
|
+
errors.append(f"{test['id']}: solution/ fails: {g['failed'][:4] or g['output'][-300:]}")
|
|
328
|
+
wrong = os.path.join(test["task_dir"], "wrong")
|
|
329
|
+
for w in sorted(os.listdir(wrong)) if os.path.isdir(wrong) else []:
|
|
330
|
+
if os.path.isdir(os.path.join(wrong, w)) and variant(f"wrong-{w}", os.path.join(wrong, w))["pass"]:
|
|
331
|
+
errors.append(f"{test['id']}: wrong/{w} passes, so the tests can't catch that mistake")
|
|
332
|
+
return errors
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def overlay(src, dst):
|
|
336
|
+
"""Copy src's files over dst; a .delete file in src lists paths (one per line) to remove from dst."""
|
|
337
|
+
for dirpath, dirnames, files in os.walk(src):
|
|
338
|
+
dirnames[:] = [d for d in dirnames if d != "__pycache__"]
|
|
339
|
+
for fn in files:
|
|
340
|
+
if fn == ".delete":
|
|
341
|
+
continue
|
|
342
|
+
rel = os.path.relpath(os.path.join(dirpath, fn), src)
|
|
343
|
+
os.makedirs(os.path.dirname(os.path.join(dst, rel)), exist_ok=True)
|
|
344
|
+
shutil.copy2(os.path.join(dirpath, fn), os.path.join(dst, rel))
|
|
345
|
+
listing = os.path.join(src, ".delete")
|
|
346
|
+
if os.path.isfile(listing):
|
|
347
|
+
with open(listing) as f:
|
|
348
|
+
for line in f.read().split():
|
|
349
|
+
target = os.path.join(dst, line)
|
|
350
|
+
if os.path.isdir(target):
|
|
351
|
+
shutil.rmtree(target)
|
|
352
|
+
elif os.path.exists(target):
|
|
353
|
+
os.remove(target)
|
|
@@ -63,6 +63,7 @@ def _load_native(data):
|
|
|
63
63
|
"machine": r.get("machine") or run.get("machine"), "prompt_tokens": r.get("prompt_tokens"),
|
|
64
64
|
# other models served at the same time (parallel_models): its speed was shared with them
|
|
65
65
|
"alongside": r.get("ran_alongside") or [],
|
|
66
|
+
"agent": r.get("agent"), # an agent pack's task: the agent's stats (see agents.py)
|
|
66
67
|
})
|
|
67
68
|
return rows, info
|
|
68
69
|
|
|
@@ -573,6 +574,34 @@ def items(rows):
|
|
|
573
574
|
return out
|
|
574
575
|
|
|
575
576
|
|
|
577
|
+
def agent_table(rows):
|
|
578
|
+
"""(header, table) summing up agent-pack tasks per model and pack, or None without any. A pack run
|
|
579
|
+
by more than one agent version is marked, since a new version can change how models do."""
|
|
580
|
+
groups = collections.defaultdict(list)
|
|
581
|
+
for r in rows:
|
|
582
|
+
if r.get("agent"):
|
|
583
|
+
groups[(r["model"], r["suite"])].append(r)
|
|
584
|
+
if not groups:
|
|
585
|
+
return None
|
|
586
|
+
header = ["model", "pack", "passed", "med min", "med turns", "out tokens", "tool errors", "ran tests",
|
|
587
|
+
"limits hit", "tests edited", "agent"]
|
|
588
|
+
table = []
|
|
589
|
+
for (model, suite), rs in sorted(groups.items()):
|
|
590
|
+
a = [r["agent"] for r in rs]
|
|
591
|
+
turns = [x["turns"] for x in a if x.get("turns") is not None]
|
|
592
|
+
out_tokens = sum(x.get("output_tokens") or 0 for x in a)
|
|
593
|
+
versions = sorted({x.get("version") or "?" for x in a})
|
|
594
|
+
table.append([model, suite, f"{sum(r['ok'] for r in rs)}/{len(rs)}",
|
|
595
|
+
f"{median([r['latency'] / 60000 for r in rs]):.1f}", f"{median(turns):.0f}" if turns else "-",
|
|
596
|
+
f"{out_tokens:,}" if out_tokens else "-",
|
|
597
|
+
str(sum(x.get("tool_errors") or 0 for x in a)),
|
|
598
|
+
f"{sum(bool(x.get('ran_tests')) for x in a)}/{len(a)}",
|
|
599
|
+
str(sum(x.get("stop") in ("time limit", "turn limit") for x in a)),
|
|
600
|
+
str(sum(bool(x.get("tests_edited")) for x in a)),
|
|
601
|
+
" + ".join(versions) + (" (MIXED)" if len(versions) > 1 else "")])
|
|
602
|
+
return header, table
|
|
603
|
+
|
|
604
|
+
|
|
576
605
|
def print_table(header, table):
|
|
577
606
|
widths = [max(len(str(x)) for x in col) for col in zip(header, *table)]
|
|
578
607
|
fmt = " ".join("{:<%d}" % w for w in widths)
|
|
@@ -629,6 +658,10 @@ def main(argv):
|
|
|
629
658
|
if any(r.get("difficulty") in LEVELS for r in rows):
|
|
630
659
|
print("\nPass rate by difficulty:")
|
|
631
660
|
print_table(header, table)
|
|
661
|
+
agents_summary = agent_table(rows)
|
|
662
|
+
if agents_summary:
|
|
663
|
+
print("\nAgent tasks (agent packs; MIXED = answers from more than one agent version):")
|
|
664
|
+
print_table(*agents_summary)
|
|
632
665
|
machine = argv[argv.index("--machine") + 1] if "--machine" in argv else None
|
|
633
666
|
if "--speed" in flags or machine:
|
|
634
667
|
speeds = {}
|