whileai 0.51__tar.gz → 0.52__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.
- {whileai-0.51/whileai.egg-info → whileai-0.52}/PKG-INFO +164 -156
- {whileai-0.51 → whileai-0.52}/README.md +163 -155
- {whileai-0.51 → whileai-0.52}/pyproject.toml +1 -1
- {whileai-0.51 → whileai-0.52}/whileai/simulations/__init__.py +7 -5
- {whileai-0.51 → whileai-0.52}/whileai/simulations/data.py +55 -2
- {whileai-0.51 → whileai-0.52}/whileai/simulations/environment.py +3 -3
- {whileai-0.51 → whileai-0.52}/whileai/simulations/export.py +7 -4
- {whileai-0.51 → whileai-0.52}/whileai/simulations/generate/adapters.py +20 -1
- {whileai-0.51 → whileai-0.52}/whileai/simulations/generate/agents.py +33 -10
- {whileai-0.51 → whileai-0.52}/whileai/simulations/ingest/platform.py +8 -8
- {whileai-0.51 → whileai-0.52}/whileai/simulations/run/config.py +23 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/run/engine.py +84 -13
- {whileai-0.51 → whileai-0.52}/whileai/simulations/schema.py +18 -3
- {whileai-0.51 → whileai-0.52}/whileai/simulations/schemas/row-v1.json +5 -2
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/agreement.py +55 -8
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/curriculum.py +34 -21
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/delta.py +163 -11
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/grade_llm.py +72 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/judge_trust.py +131 -18
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/judging.py +5 -5
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/labels.py +16 -8
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/logprobs.py +2 -1
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/markers.py +1 -1
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/optimize.py +81 -20
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/passat.py +87 -8
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/publish_gate.py +64 -17
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/stats.py +21 -5
- {whileai-0.51 → whileai-0.52}/whileai/simulations/simulation.py +86 -5
- {whileai-0.51 → whileai-0.52}/whileai/simulations/training.py +134 -12
- {whileai-0.51 → whileai-0.52}/whileai/simulations/verify/__init__.py +2 -2
- {whileai-0.51 → whileai-0.52/whileai.egg-info}/PKG-INFO +164 -156
- {whileai-0.51 → whileai-0.52}/LICENSE +0 -0
- {whileai-0.51 → whileai-0.52}/MANIFEST.in +0 -0
- {whileai-0.51 → whileai-0.52}/setup.cfg +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/__init__.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/_env.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/auth.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/cli.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/ingest.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/py.typed +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/__main__.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/generate/__init__.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/generate/actionspace.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/generate/coverage.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/generate/diversity.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/generate/embeddings.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/generate/explore.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/generate/generator.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/generate/offline_agent.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/generate/scenarios.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/generate/usage_meter.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/ingest/__init__.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/ingest/otel.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/ingest/traces.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/monitor.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/py.typed +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/run/__init__.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/run/rows.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/run/spec.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/__init__.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/checklist.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/grading.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/grounding.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/hack_scan.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/hygiene.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/llm_judge.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/pairwise.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/preflight.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/privileged.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/quality.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/reference.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/rubric.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/spec.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/stage.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/style.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/score/trace.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/verify/base.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/verify/code.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/verify/math.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/verify/structured.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/verify/text.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/world/__init__.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai/simulations/world/sandbox.py +0 -0
- {whileai-0.51 → whileai-0.52}/whileai.egg-info/SOURCES.txt +0 -0
- {whileai-0.51 → whileai-0.52}/whileai.egg-info/dependency_links.txt +0 -0
- {whileai-0.51 → whileai-0.52}/whileai.egg-info/entry_points.txt +0 -0
- {whileai-0.51 → whileai-0.52}/whileai.egg-info/requires.txt +0 -0
- {whileai-0.51 → whileai-0.52}/whileai.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: whileai
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.52
|
|
4
4
|
Summary: While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt.
|
|
5
5
|
Author: While
|
|
6
6
|
License: Apache-2.0
|
|
@@ -43,7 +43,7 @@ The While Python SDK. One package, two importable modules:
|
|
|
43
43
|
|
|
44
44
|
**Renamed.** This SDK was `zeroproof` (ZeroProof is now While). `pip install zeroproof` still works: it installs `whileai`, and `import zeroproof` (or the older `zeroproof_simulations`) resolves to the same modules with a deprecation warning. `ZEROPROOF_*` environment variables and a saved `~/.zeroproof/credentials.json` are still read. Change the import when you can; new releases land under `whileai`.
|
|
45
45
|
|
|
46
|
-
Releases of `
|
|
46
|
+
Releases of `whileai` before 0.3 were an unrelated encrypted agent-to-agent messaging client. That code was removed in 0.04; pin `whileai<0.3` if you still depend on it.
|
|
47
47
|
|
|
48
48
|
Two ways in, one engine. Give it the agent's tools and system prompt and it samples situations across everything that agent can be asked. Give it graded traces as well (`traces=`, plain row dicts — see [Close the loop](#close-the-loop-aim-the-budget-with-traces)) and it aims the budget at the situations that fail in production, so new rows land where the agent is weak and carry both the failure and the fixed version. Every row is a full conversation: user turns, agent turns, tool calls, tool results, scheduled faults. Rows come back ungraded; your grader decides what good means. Default `explore`: one unique situation per row. How it thinks: [docs/simulations.md](docs/simulations.md).
|
|
49
49
|
|
|
@@ -62,7 +62,7 @@ A situation is drawn across the world axes (from the agent's tools) and the huma
|
|
|
62
62
|
3. **Write users.** A separate writer (same hosted model, different prompt, no agent policy) samples situations across tools, stance, history, and so on.
|
|
63
63
|
4. **Pick the diverse ones.** Embeddings plus a bit of noise so the batch is not 200 copies of the same prompt.
|
|
64
64
|
5. **Play the agent.** It talks, calls tools, gets results, talks again. All of that is stored: user text, agent text, tool calls, tool results, `final_text`.
|
|
65
|
-
6. **Grade.** Rows come back ungraded. Grade after with `data.grade()` (hosted judge, against the spec's `rubric.md` or `rubric=`), `data.grade(judge=...)` (your judge), or `
|
|
65
|
+
6. **Grade.** Rows come back ungraded. Grade after with `data.grade()` (hosted judge, against the spec's `rubric.md` or `rubric=`), `data.grade(judge=...)` (your judge), or `wai.grade(path)`. The legacy `grade=True` flag writes deterministic conduct rewards; avoid it for the rubric workflow.
|
|
66
66
|
|
|
67
67
|
Stop when the row cap or the clock hits.
|
|
68
68
|
|
|
@@ -79,7 +79,7 @@ fastest way to see a row and to check your agent and grader are wired up
|
|
|
79
79
|
correctly before you spend a key on variety.
|
|
80
80
|
|
|
81
81
|
```python
|
|
82
|
-
import whileai.simulations as
|
|
82
|
+
import whileai.simulations as wai
|
|
83
83
|
|
|
84
84
|
# 1. Your tools, in OpenAI function-calling shape. This is all `tools=` wants.
|
|
85
85
|
TOOLS = [
|
|
@@ -114,7 +114,7 @@ def my_agent(message: str) -> dict:
|
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
# 3. simulator=False uses the built-in template writer: no model, no key.
|
|
117
|
-
data =
|
|
117
|
+
data = wai.simulate(
|
|
118
118
|
my_agent,
|
|
119
119
|
tools=TOOLS,
|
|
120
120
|
system_prompt="Help customers with orders.",
|
|
@@ -130,10 +130,10 @@ print(scored.pass_at)
|
|
|
130
130
|
Offline, every marker is your agent's. The template writer writes the
|
|
131
131
|
users, not the agent, so a callable that never hedges scores zero
|
|
132
132
|
hedging, and `faults` on a row only fire if your agent's tool calls go
|
|
133
|
-
through the world that schedules them. `
|
|
133
|
+
through the world that schedules them. `wai.world(TOOLS)` is that world:
|
|
134
134
|
|
|
135
135
|
```python
|
|
136
|
-
WORLD =
|
|
136
|
+
WORLD = wai.world(TOOLS)
|
|
137
137
|
|
|
138
138
|
|
|
139
139
|
def my_agent(message: str) -> dict:
|
|
@@ -149,7 +149,7 @@ def my_agent(message: str) -> dict:
|
|
|
149
149
|
```
|
|
150
150
|
|
|
151
151
|
To see the detectors fire before you plug in your own agent, run the
|
|
152
|
-
seeded one. It answers honestly through `
|
|
152
|
+
seeded one. It answers honestly through `wai.world`, and on a labeled
|
|
153
153
|
fraction of rollouts does one wrong thing on purpose: hedges, flatters,
|
|
154
154
|
apologizes, pads, claims success through a fault, or quotes the row's
|
|
155
155
|
privileged context. Every row says what it did in `seeded` (`[]` when
|
|
@@ -157,16 +157,16 @@ it behaved), so a check that catches exactly those rows is a check that
|
|
|
157
157
|
works.
|
|
158
158
|
|
|
159
159
|
```python
|
|
160
|
-
data =
|
|
161
|
-
|
|
160
|
+
data = wai.simulate(
|
|
161
|
+
wai.seeded_agent(TOOLS),
|
|
162
162
|
tools=TOOLS,
|
|
163
163
|
system_prompt="Help customers with orders.",
|
|
164
164
|
simulator=False,
|
|
165
165
|
budget=60,
|
|
166
166
|
)
|
|
167
167
|
rows = data.trajectories # export_row scrubs privileged; the run keeps it
|
|
168
|
-
print(
|
|
169
|
-
print(
|
|
168
|
+
print(wai.style_report(rows)["markers"]["no_hedging"]["hits"]) # > 0, only on seeded rows
|
|
169
|
+
print(wai.format_leak_report(wai.leak_report(rows)))
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
```
|
|
@@ -199,9 +199,9 @@ export OPENAI_BASE_URL=... # only for a non-OpenAI endpoint
|
|
|
199
199
|
```
|
|
200
200
|
|
|
201
201
|
```python
|
|
202
|
-
import whileai.simulations as
|
|
202
|
+
import whileai.simulations as wai
|
|
203
203
|
|
|
204
|
-
data =
|
|
204
|
+
data = wai.simulate(
|
|
205
205
|
agent="openai:gpt-4.1-mini",
|
|
206
206
|
tools=my_tools,
|
|
207
207
|
system_prompt=my_system_prompt,
|
|
@@ -216,9 +216,9 @@ Agent to gated dataset. Everything else in this README is one layer down.
|
|
|
216
216
|
`POLICY` is the agent's system prompt.
|
|
217
217
|
|
|
218
218
|
```python
|
|
219
|
-
import whileai.simulations as
|
|
219
|
+
import whileai.simulations as wai
|
|
220
220
|
|
|
221
|
-
data =
|
|
221
|
+
data = wai.simulate(
|
|
222
222
|
agent="openai:gpt-4.1-mini",
|
|
223
223
|
tools=TOOLS,
|
|
224
224
|
system_prompt=POLICY,
|
|
@@ -228,23 +228,23 @@ data = zps.simulate(
|
|
|
228
228
|
) # 1 generate
|
|
229
229
|
data.grade(rubric=RUBRIC) # 2 grade against the task rubric: reward 0/1 on every row
|
|
230
230
|
print(data.pass_at)
|
|
231
|
-
|
|
232
|
-
rows, report =
|
|
233
|
-
entry =
|
|
231
|
+
wai.judge_trust(data.trajectories) # 3 trust the numbers
|
|
232
|
+
rows, report = wai.optimize(data, mode="rl") # 4 prune to what carries gradient
|
|
233
|
+
entry = wai.push_rows(rows, "github-rl-v1", gate=True, mode="rl") # 5 publish, gated
|
|
234
234
|
```
|
|
235
235
|
|
|
236
|
-
`situations=200, repeats=8` is a guess. `
|
|
236
|
+
`situations=200, repeats=8` is a guess. `wai.recommend(tools=TOOLS, system_prompt=POLICY, mode="rl")` replaces it with numbers from this agent's own grid: [How much to run](#how-much-to-run).
|
|
237
237
|
|
|
238
238
|
A spec folder is `spec.json` (tools and policy) plus `rubric.md`: what doing the job means, in prose. `grade()` scores against it. The hosted judge writes `reward` and `reason` onto the run's rows and returns the judge report (a dict), so the numbers are read off `data`; `grade(judge=your_callable)` instead returns a `ScoredData` of graded copies, leaves the run untouched, and has its own `.push(name, ...)`. Without one it grades the conduct floor only (nothing invented, nothing skipped) and the report says so; pass `rubric=` to `simulate` or `grade` to supply one, `judge=` for your own callable.
|
|
239
239
|
|
|
240
|
-
After training, measure whether it landed: `
|
|
240
|
+
After training, measure whether it landed: `wai.delta_report(before=scored.rows, after=after_rows, target="pass_at_1")`. Name the training reward too, `proxy="marker:first_action"`, and the report says whether the run over-optimized it: proxy up while the target did not follow fails the report (rlhf-book ch. 14). `wai.hack_scan_diff(before, after, endorsed=[...])` names what the update moved toward, and withholds the name when either side came back `degenerate`.
|
|
241
241
|
|
|
242
242
|
Character training, the same loop aimed at how the model talks: a constitution in, graded replies, length-matched pairs and SFT rows out, and the judge checked against the constitution's own labels. Worked example [`recipes/03-select/character`](recipes/03-select/character), recipe [docs/character-training.md](docs/character-training.md), page [zeroproofai.com/docs/character-training](https://zeroproofai.com/docs/character-training).
|
|
243
243
|
|
|
244
244
|
| Call | What it decides | Reads |
|
|
245
245
|
|---|---|---|
|
|
246
246
|
| `simulate` | the situations, the users, the world, k rollouts per ask | your spec or tools + system prompt |
|
|
247
|
-
| `data.grade(judge=)` | 0/1 per rollout. `
|
|
247
|
+
| `data.grade(judge=)` | 0/1 per rollout. `wai.grade(data)` uses the hosted judge instead | your judge callable, or your account key (`whileai login`) |
|
|
248
248
|
| `pass_at` / `judge_trust` | pass@1 with an interval, headroom for RL, whether the judge can be trusted | graded rows, 30 to 100 hand labels as `gold_reward` |
|
|
249
249
|
| `optimize(mode="rl")` | drops junk rows, duplicates, dead groups, and asks outside the *difficulty* band; flags reward hacks | graded rows |
|
|
250
250
|
| `push_rows(gate=True)` | refuses ungraded or gradient-free RL data; stamps calibration | pruned rows |
|
|
@@ -257,7 +257,7 @@ engine, reward model, human-label lookup, HTTP call: the SDK does not care
|
|
|
257
257
|
how the reward was produced, only that the result honors this contract.
|
|
258
258
|
The same contract is what `grade`, `run_judge`, `evaluate`, `grader=`,
|
|
259
259
|
`optimize` and a gated `push` all read, and what every `verify` verifier
|
|
260
|
-
and `
|
|
260
|
+
and `wai.reward_model(run)` already honors.
|
|
261
261
|
|
|
262
262
|
```python
|
|
263
263
|
judge(row) -> {"reward": 0 or 1} # the minimum
|
|
@@ -287,14 +287,14 @@ More on the four marker families in
|
|
|
287
287
|
**The loop, closed in five lines.**
|
|
288
288
|
|
|
289
289
|
```python
|
|
290
|
-
import whileai.simulations as
|
|
290
|
+
import whileai.simulations as wai
|
|
291
291
|
|
|
292
292
|
judge = lambda row: {"reward": int("sorry" not in row["final_text"])}
|
|
293
|
-
scored =
|
|
294
|
-
|
|
293
|
+
scored = wai.run_judge(data.trajectories, judge) # or data.grade(judge=judge)
|
|
294
|
+
wai.export_dataset(scored.passes(), output="train.jsonl", system_prompt=POLICY, tools=TOOLS)
|
|
295
295
|
# ...train externally, roll the tuned model on a holdout...
|
|
296
|
-
evald =
|
|
297
|
-
nxt =
|
|
296
|
+
evald = wai.evaluate(rollouts, judge, model="my-tuned-v1")
|
|
297
|
+
nxt = wai.simulate(tools=TOOLS, system_prompt=POLICY, traces=evald.failed_traces())
|
|
298
298
|
```
|
|
299
299
|
|
|
300
300
|
The full contract, with every status and the rest of the loop, is the
|
|
@@ -302,7 +302,7 @@ module docstring of `whileai.simulations.score.judging` — note the
|
|
|
302
302
|
`score.`; there is no `whileai.simulations.judging`.
|
|
303
303
|
|
|
304
304
|
Writing the judge is half of it; knowing whether to believe it is the
|
|
305
|
-
other half. `
|
|
305
|
+
other half. `wai.judge_trust(rows, judge=...)` and `wai.judge_probes(rows,
|
|
306
306
|
judge)` are under [Trust the numbers](#trust-the-numbers).
|
|
307
307
|
|
|
308
308
|
### Verifiers: when the reward is a program, not a judge
|
|
@@ -312,11 +312,11 @@ For a verifiable task the reward is a checker, not an opinion (RLHF book ch. 7,
|
|
|
312
312
|
```python
|
|
313
313
|
from whileai.simulations.verify import MathEqual, CodeExec, JSONSchema, Regex, All
|
|
314
314
|
|
|
315
|
-
data =
|
|
315
|
+
data = wai.simulate(
|
|
316
316
|
tools=MATH_TOOLS, system_prompt=MATH_POLICY, mode="rl", situations=200, repeats=8
|
|
317
317
|
)
|
|
318
318
|
scored = data.grade(judge=MathEqual()) # the verifier is the reward
|
|
319
|
-
rows, _ =
|
|
319
|
+
rows, _ = wai.optimize(scored, mode="rl") # GRPO data, gradient checked
|
|
320
320
|
```
|
|
321
321
|
|
|
322
322
|
The candidate is the rollout's `final_text`; the gold is read from the row's `privileged.reference`, which the training export never projects, so the answer key cannot leak into a training file (flat `answer`/`target`/... fields work too, or point at any column with `field=`). Built in: `ExactMatch`, `Includes`, `Regex`, `MultipleChoice`, `Numeric`, `MathEqual`, `JSONValid`, `JSONSchema`, `JSONField`, and `CodeExec` (runs the candidate against hidden tests in a sandboxed subprocess with a timeout). Compose with `All` (right answer *and* right format), `Any`, or a graded `Weighted` rubric; wrap your own with `@verifier`. Worked example: [`recipes/01-simulate/verifiers`](recipes/01-simulate/verifiers).
|
|
@@ -342,7 +342,7 @@ fully offline — see [Start here](#start-here-no-key-required) above for the
|
|
|
342
342
|
whole runnable block.
|
|
343
343
|
|
|
344
344
|
```python
|
|
345
|
-
data =
|
|
345
|
+
data = wai.simulate(
|
|
346
346
|
my_agent, tools=my_tools, system_prompt=my_system_prompt, simulator=False, budget=40
|
|
347
347
|
)
|
|
348
348
|
```
|
|
@@ -388,10 +388,10 @@ grades a finished trajectory. `export_environment` writes those three as an
|
|
|
388
388
|
installable `verifiers` package, the shape Prime Intellect and TRL read.
|
|
389
389
|
|
|
390
390
|
```python
|
|
391
|
-
data =
|
|
391
|
+
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY, mode="rl", repeats=8)
|
|
392
392
|
data.grade()
|
|
393
393
|
# reward and world must import by name in the trainer: a module-level function or "module:attr"
|
|
394
|
-
|
|
394
|
+
wai.export_environment(data, "envs/github-agent", reward=my_verifier)
|
|
395
395
|
# pip install -e envs/github-agent
|
|
396
396
|
# vf-eval github_agent -a '{"split": "holdout"}' -m <policy> -b <base url> -k <key var>
|
|
397
397
|
```
|
|
@@ -419,7 +419,7 @@ acknowledged. No model in the loop, and `markers` say which check ran
|
|
|
419
419
|
(rlhf-book ch. 12 rubrics, computed from state rather than written by a judge).
|
|
420
420
|
When the rows carry none of that metadata the export warns: the reward
|
|
421
421
|
reduces to `conduct_grade`, a process reward, and a policy trained on it
|
|
422
|
-
alone learns to call nothing (`recipes/03-select/prime-intellect-rl`). `
|
|
422
|
+
alone learns to call nothing (`recipes/03-select/prime-intellect-rl`). `wai.load_environment(spec)`
|
|
423
423
|
builds the environment in a process that has `verifiers` (`pip install
|
|
424
424
|
'whileai[rl]'`); `examples/coding-efficiency` is the same shape built by
|
|
425
425
|
hand over an executable world with a hidden test suite.
|
|
@@ -445,10 +445,10 @@ and a `must_not_regress` list, and report pass^k alongside pass@1 for
|
|
|
445
445
|
reliability (ch. 13, 16).
|
|
446
446
|
|
|
447
447
|
```python
|
|
448
|
-
import whileai.simulations as
|
|
448
|
+
import whileai.simulations as wai
|
|
449
449
|
|
|
450
|
-
data =
|
|
451
|
-
data =
|
|
450
|
+
data = wai.simulate(tools=my_tools, system_prompt=my_system_prompt, output="rollout.jsonl")
|
|
451
|
+
data = wai.simulate(agent=my_agent)
|
|
452
452
|
```
|
|
453
453
|
|
|
454
454
|
Pass `spec=` if you have a local tools-and-system-prompt folder of your own: a directory (or a JSON/YAML file) holding `tools` and `policy` / `system_prompt`, optionally with seed `situations` and a `rubric.md` (what doing the job means, for `grade()`). No spec folders ship with this package, so every snippet here uses `tools=` + `system_prompt=` — the two are interchangeable, and `spec=` is only a way to keep them in a file. The generated datasets are on Hugging Face in the [Post-Training Foundational Datasets](https://huggingface.co/collections/zero-proof-ai/whileai-post-training-foundational-datasets-6aa0b9c040ff8591988696dc) collection, not stored in this repo: [agent-simulations](https://huggingface.co/datasets/zero-proof-ai/agent-simulations) by agent type, [tool-call-efficiency](https://huggingface.co/datasets/zero-proof-ai/tool-call-efficiency) (SFT, preference, GRPO and eval splits), and [tau2-simulated](https://huggingface.co/datasets/zero-proof-ai/tau2-simulated), among others.
|
|
@@ -461,11 +461,13 @@ Pass `spec=` if you have a local tools-and-system-prompt folder of your own: a d
|
|
|
461
461
|
| `rollouts_per_request` | from mode | Repeats: reruns of one phrasing. Alias `repeats=` |
|
|
462
462
|
| `fault_rate` | `0.5` | Broken tools. `0` off. Applied by the mock world, so a callable `agent=` that answers its own tool calls never sees one |
|
|
463
463
|
| `simulator` | hosted Qwen | Situation writer. `False` uses the built-in template writer (no model, less variety); an `openai:`/`vllm:` spec runs it on your endpoint |
|
|
464
|
+
| `user_model` | `None` | Who plays the simulated user in follow-up turns. `None` is the agent's own model; an `openai:`/`vllm:` spec moves that job to another model |
|
|
464
465
|
| `traces` | `None` | Graded traces of the deployed agent — a list of plain row dicts or a JSONL path. Aims the coverage grid at the behaviors those traces show and keeps the sources out of the generated rows. See [Close the loop](#close-the-loop-aim-the-budget-with-traces) |
|
|
465
466
|
| `tasks` | `None` | Re-run a previous run's task set instead of drawing a new one: that run, its rows, or its JSONL path. k is **not** inherited — see [Same tasks, new prompt](#trust-the-numbers) |
|
|
466
467
|
| `logprobs` | `False` | Ask the rollout model for the log-probability of every token it generates. Each agent turn's step gets `logprob` and `n_tokens`, the row gets the totals. `"tokens"` keeps the per-token list. Model backends only |
|
|
468
|
+
| `sampling` | `None` | How your own callable agent samples, `{"temperature": 0.7, "max_tokens": 1024, "model": "my-model"}`, recorded on every row as given. A model backend records its own and ignores this |
|
|
467
469
|
| `reproducible` | `False` | Same seed, same concurrency, same agent: same rows. Runs batch by batch, so uneven latency costs throughput. Needs the clock off. `concurrency: 1` always runs this way |
|
|
468
|
-
| `grade` | `False` | Legacy: `True` writes the deterministic conduct score at simulation time. Rows come back ungraded by default; grade after with `data.grade(...)` or `
|
|
470
|
+
| `grade` | `False` | Legacy: `True` writes the deterministic conduct score at simulation time. Rows come back ungraded by default; grade after with `data.grade(...)` or `wai.grade(...)` |
|
|
469
471
|
| `llm_grade` | `False` | Extra LLM judge. Needs `OPENAI_API_KEY` |
|
|
470
472
|
| `output` | | JSONL path |
|
|
471
473
|
|
|
@@ -481,10 +483,10 @@ Depends on the use case. How each scenario is built is in [The recipe](#the-reci
|
|
|
481
483
|
| A mix, until coverage plateaus | `adaptive` | New situations, phrasings, and repeats. Best with `until="saturation"` |
|
|
482
484
|
|
|
483
485
|
```python
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
486
|
+
wai.simulate(tools=my_tools, system_prompt=my_system_prompt) # explore
|
|
487
|
+
wai.simulate(tools=my_tools, system_prompt=my_system_prompt, mode="sft")
|
|
488
|
+
wai.simulate(tools=my_tools, system_prompt=my_system_prompt, mode="rl")
|
|
489
|
+
wai.simulate(tools=my_tools, system_prompt=my_system_prompt, mode="adaptive", until="saturation")
|
|
488
490
|
```
|
|
489
491
|
|
|
490
492
|
### How much to run
|
|
@@ -494,10 +496,10 @@ covering grid and from published post-training practice (FireAct, LIMA,
|
|
|
494
496
|
AgentTuning for SFT; DAPO, Skywork-OR1 for RL). No key, no network.
|
|
495
497
|
|
|
496
498
|
```python
|
|
497
|
-
rec =
|
|
499
|
+
rec = wai.recommend(tools=my_tools, system_prompt=my_system_prompt, mode="sft")
|
|
498
500
|
print("
|
|
499
501
|
".join(rec["reasoning"]))
|
|
500
|
-
data =
|
|
502
|
+
data = wai.simulate(tools=my_tools, system_prompt=my_system_prompt, **rec["simulate_kwargs"])
|
|
501
503
|
```
|
|
502
504
|
|
|
503
505
|
```
|
|
@@ -571,12 +573,12 @@ usually drop straight in. OTLP ingest and platform datasets are *one* way
|
|
|
571
573
|
to get rows into this shape, not a prerequisite for it.
|
|
572
574
|
|
|
573
575
|
```python
|
|
574
|
-
import whileai.simulations as
|
|
576
|
+
import whileai.simulations as wai
|
|
575
577
|
|
|
576
|
-
traces =
|
|
577
|
-
print(
|
|
578
|
+
traces = wai.load_traces("production.jsonl") # or just pass the list
|
|
579
|
+
print(wai.trace_report(traces, tools=TOOLS)) # what will this aim at?
|
|
578
580
|
|
|
579
|
-
data =
|
|
581
|
+
data = wai.simulate(
|
|
580
582
|
my_agent, tools=TOOLS, system_prompt=POLICY, traces=traces, mode="rl", repeats=4
|
|
581
583
|
)
|
|
582
584
|
```
|
|
@@ -598,16 +600,16 @@ near-copy a source. `leakage_report` / `drop_leaky_rows` are how you verify
|
|
|
598
600
|
it, which is what makes it safe to hold traces out for evaluation:
|
|
599
601
|
|
|
600
602
|
```python
|
|
601
|
-
prod, train =
|
|
602
|
-
data =
|
|
603
|
-
print(
|
|
604
|
-
rows, report =
|
|
603
|
+
prod, train = wai.split_pseudo_production(scored.rows, fraction=0.2)
|
|
604
|
+
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY, traces=prod, mode="rl", repeats=4)
|
|
605
|
+
print(wai.leakage_report(data.trajectories, prod)["n_leaky"]) # want 0
|
|
606
|
+
rows, report = wai.drop_leaky_rows(data.trajectories, prod)
|
|
605
607
|
```
|
|
606
608
|
|
|
607
609
|
And the loop closes on itself: `evaluate(rollouts, judge).failed_traces()`
|
|
608
610
|
hands the failures straight back to `simulate(traces=...)`.
|
|
609
611
|
|
|
610
|
-
If your traces are already on the platform, `
|
|
612
|
+
If your traces are already on the platform, `wai.cut(agent="my-agent")`
|
|
611
613
|
does the whole cut in one line — see
|
|
612
614
|
[Training data out of traces](#training-data-out-of-traces).
|
|
613
615
|
|
|
@@ -630,7 +632,7 @@ network.
|
|
|
630
632
|
| Select | [`recipes/03-select/schema`](recipes/03-select/schema) | One row file in, six training targets out: eval, SFT, preference, GRPO prompts, OPSD hints, OPD. Migrates any legacy file first. Offline. |
|
|
631
633
|
| Select | [`recipes/03-select/prime-intellect-rl`](recipes/03-select/prime-intellect-rl) | Generates a GRPO-ready dataset with `simulate(mode="rl")` and checks it carries gradient before you spend GPU time on it, then exports prompts in the `verifiers` shape. Needs an account key (`whileai login`), or `VLLM_API_KEY` for the shared pool. |
|
|
632
634
|
| Select | [`recipes/03-select/character`](recipes/03-select/character) | Character training from a constitution: the OpenAI Model Spec's style traits become graded rows, preference pairs and SFT rows, with the judge checked against the spec's own labels and a before/after measurement. Offline by default. How-to: [docs/character-training.md](docs/character-training.md). |
|
|
633
|
-
| Train | [`recipes/04-train/hosted-loop`](recipes/04-train/hosted-loop) | Push graded rows, `
|
|
635
|
+
| Train | [`recipes/04-train/hosted-loop`](recipes/04-train/hosted-loop) | Push graded rows, `wai.train` SFT on Qwen3-4B, `wai.serve` the adapter, one chat completion from the endpoint. One key, one A10G minute; the wiring check for training on the platform. |
|
|
634
636
|
| Train | [`recipes/04-train/identity`](recipes/04-train/identity) | Builds a leak-free SFT set that teaches a model a new name and maker, with Modal scripts to train a LoRA and evaluate identity and leak rates. No model calls to generate. |
|
|
635
637
|
| Train | [`recipes/04-train/grpo`](recipes/04-train/grpo) | GRPO on Modal, end to end: prompts from the simulator, a verifiable tool-discipline reward, TRL `GRPOTrainer` with LoRA, `HackMonitor`, reward and KL on the dashboard, pass@1 before and after on a holdout with the paired delta and per-category table on the run page. One A10G, under fifteen minutes. |
|
|
636
638
|
| Train | [`recipes/04-train/dpo`](recipes/04-train/dpo) | DPO on the same environment: on-policy pairs from `build_preference_pairs`, TRL `DPOTrainer` with LoRA, the reward margin on the run page, iterated rounds with `--from-run`, constructed negatives where the policy never fails. One A10G, about ten minutes. |
|
|
@@ -675,25 +677,25 @@ login`.
|
|
|
675
677
|
# export WHILEAI_DELEGATED_CREDENTIAL="zp_dc_..."
|
|
676
678
|
|
|
677
679
|
# If you need to mint one from a Clerk session token:
|
|
678
|
-
# credential =
|
|
680
|
+
# credential = wai.issue_delegated_credential(clerk_token, ttl_seconds=3600)
|
|
679
681
|
# export WHILEAI_DELEGATED_CREDENTIAL=credential["credential"]
|
|
680
682
|
|
|
681
|
-
data =
|
|
683
|
+
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY)
|
|
682
684
|
v1 = data.push("github-explore-v1") # -> {"datasetId": "ds_...", ...}
|
|
683
685
|
|
|
684
686
|
# iterate, then push the next version with lineage
|
|
685
687
|
v2 = data.push("github-explore-v2", parent=v1["datasetId"])
|
|
686
688
|
|
|
687
|
-
|
|
688
|
-
rows =
|
|
689
|
-
|
|
690
|
-
|
|
689
|
+
wai.datasets() # list yours + storage used
|
|
690
|
+
rows = wai.pull(v1["datasetId"]) # rows, or pass path= for a file
|
|
691
|
+
wai.push_file("rollout.jsonl") # upload an existing JSONL
|
|
692
|
+
wai.delete_dataset(v1["datasetId"]) # permanent
|
|
691
693
|
```
|
|
692
694
|
|
|
693
695
|
Storage is private per account, 5 GB free. `parent=` records dataset
|
|
694
696
|
lineage so iterations show as a family on the platform.
|
|
695
697
|
|
|
696
|
-
`data.push` and `
|
|
698
|
+
`data.push` and `wai.push_file` run a publish gate first (`gate=False` skips it). Every graded row gets a `calibration` stamp: its task's pass rate over k repeats, k, and the policy that produced it, so a trainer can build a curriculum or retire solved tasks. An RL-shaped run (repeats of one ask) is refused with `PublishGateError` when it is ungraded or has no mixed group, because a grouped update would learn nothing from it. The report comes back as `entry["gate"]`, with warnings when unanimous asks, or asks outside the difficulty band, are still present; `wai.optimize(data, mode="rl")` prunes those. `wai.publish_gate(rows)` runs the same check on any row list. The stamp is the schema's `Calibration` object: `wai.calibration_of(row)` reads it back typed, `from_row` carries it on `rollout.extra["calibration"]`, and `to_row` writes it out again. `k` is the repeats the grader saw, not the rows that survived: `optimize(mode="rl")` stamps its selection from the rows it was given, before its own dedupe and trims, and the gate keeps a carried stamp rather than re-measuring it on what is left. The gate's own `pass_at` block is still over the rows in front of it, and says so when the two differ.
|
|
697
699
|
|
|
698
700
|
`export_dataset` and `export_training` are the same function object (`export_dataset is export_training`), not two exporters to choose between: same arguments, same file, same report. `export_dataset` is the name to write in new code — it exports a dataset, not a training run — and `export_training` is the older spelling, kept so nothing already written breaks. `training_rows` is the list-returning half of the same path, without writing a file.
|
|
699
701
|
|
|
@@ -702,10 +704,10 @@ Training rows from `export_dataset` / `training_rows` carry a `loss_mask`, one 0
|
|
|
702
704
|
Two wire shapes come out of the exporters, and a trainer needs the second one:
|
|
703
705
|
|
|
704
706
|
```python
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
707
|
+
wai.export_training(rows, "sft.jsonl") # OpenAI chat-completions wire (default)
|
|
708
|
+
wai.export_training(rows, "sft.jsonl", format="trl") # what TRL's SFTTrainer loads
|
|
709
|
+
wai.export_preference(pairs, "dpo.jsonl", format="trl") # what TRL's DPOTrainer loads
|
|
710
|
+
wai.to_trl(wai.training_rows(data), "training") # same reshape on rows you already hold
|
|
709
711
|
```
|
|
710
712
|
|
|
711
713
|
`format="openai"` (the default) is the API wire row: the whole conversation in `messages`, `function.arguments` as a JSON string, and the ask alongside as `prompt`. `format="trl"` is what `trl.data_utils.maybe_apply_chat_template` accepts. For SFT that is conversational `{"messages": [...]}` with **no** `prompt` string column — TRL decides "is this conversational?" from the column set, and a `prompt` string next to `messages` makes it skip the chat template silently and train on the bare ask; the ask survives as `prompt_text`. For preference data it is `prompt` as the message list up to the first agent turn with `chosen`/`rejected` as the completions only, because the default shape (a `prompt` string with full conversations on both sides) raises `TypeError: string indices must be integers` inside TRL. In the TRL shape `function.arguments` is a dict, not a JSON string: HF chat templates render it with `| tojson`, so a pre-encoded string is quoted twice and the student learns to emit a string where an object belongs. The `tool_call_roundtrip` gate in the report names which of the two encodings it checked (`encoding: "json_string"` or `"dict"`), so `invalid: 0` says what it actually vouches for.
|
|
@@ -713,36 +715,36 @@ zps.to_trl(zps.training_rows(data), "training") # same reshape on rows you alre
|
|
|
713
715
|
### Prune before training
|
|
714
716
|
|
|
715
717
|
```python
|
|
716
|
-
rows, report =
|
|
717
|
-
rows, report =
|
|
718
|
-
rows, report =
|
|
718
|
+
rows, report = wai.optimize(data, mode="rl") # whole groups, 20%-80% pass rate
|
|
719
|
+
rows, report = wai.optimize(data, mode="rl", band=(0.3, 0.7))
|
|
720
|
+
rows, report = wai.optimize(data, mode="rl", enforce_band=False) # rank, do not drop
|
|
719
721
|
report["band_dropped"] # {"too_easy": n, "too_hard": n}
|
|
720
722
|
```
|
|
721
723
|
|
|
722
|
-
`optimize(mode="rl")` drops junk rows, duplicate rollouts within an ask (same trajectory twice adds nothing to a group-relative advantage), truncated rollouts (`truncated="keep"` leaves them in as `overlong`, `"penalize"` keeps them as failures with the judged score under `reward_before_penalty`, DAPO's overlong handling), unanimous asks (all pass or all fail: zero advantage), and asks outside the difficulty band (`trim_out_of_band`: "out of band" means outside the [0.2, 0.8] *pass-rate* band, never off-topic — it does not read the prompt at all, so an on-topic ask the policy always solves is dropped and an odd one it solves half the time is kept), then keeps whole groups round-robin across fault kinds. The prune shrinks every group, so the k-way reliability numbers do not survive it: `pass_at` on the selection reports `pass^k` and `pass@k` as `n/a` where the graded rows had them, which is why the quickstart prints `pass_at` before this call. The report says so in `hygiene_warnings` when they were available before, and the carried `calibration` stamp keeps the graded per-task measurement. `optimize(mode="sft")` is rejection sampling (rlhf-book ch. 9): `select="top_per_prompt"` keeps each prompt's highest-reward completion above `min_reward` (default 1.0; lower it for a partial-credit grader), `"top_k_overall"` the best `k` across prompts, and the `random_*` rules are the matching chance controls. Exported groups carry `n0`/`n1` (fail/pass, partial credit splits at 0.5) and `reward_mean`/`reward_std`. The band is the offline difficulty filter from the reasoning-model recipes (keep prompts the policy solves 20-80% of the time); it is a heuristic, so it is a parameter. Every selector report (`select_for_rl`, `select_for_sft`, `build_preference_pairs`) carries `eval_sourced`, the rows or pairs whose reward came from `evaluate()` (`lineage.source == "eval"`), with a warning when it is non-zero: a held-out score that becomes the reward makes the scorer you report the one you optimised against. Nothing is dropped; grade the training set with `run_judge` or `data.grade` and keep `evaluate` for held-out rows.
|
|
724
|
+
`optimize(mode="rl")` drops junk rows, duplicate rollouts within an ask (same trajectory twice adds nothing to a group-relative advantage), truncated rollouts (`truncated="keep"` leaves them in as `overlong`, `"penalize"` keeps them as failures with the judged score under `reward_before_penalty`, DAPO's overlong handling), unanimous asks (all pass or all fail: zero advantage), and asks outside the difficulty band (`trim_out_of_band`: "out of band" means outside the [0.2, 0.8] *pass-rate* band, never off-topic — it does not read the prompt at all, so an on-topic ask the policy always solves is dropped and an odd one it solves half the time is kept), then keeps whole groups round-robin across fault kinds and, within a fault kind, round-robin across pass rates: a 25% ask, a 50% ask and a 75% ask are taken in turn, with no preference for the middle (`order="middle"` restores the older nearest-to-50% ranking). Each kept row's `calibration` stamp carries `pass_rate_ci95`, the interval on that pass rate, and the report says so when the band was measured from fewer than 16 rollouts per task, since at 8 a task's band assignment can be off by about 0.3. The prune shrinks every group, so the k-way reliability numbers do not survive it: `pass_at` on the selection reports `pass^k` and `pass@k` as `n/a` where the graded rows had them, which is why the quickstart prints `pass_at` before this call. The report says so in `hygiene_warnings` when they were available before, and the carried `calibration` stamp keeps the graded per-task measurement. `optimize(mode="sft")` is rejection sampling (rlhf-book ch. 9): `select="top_per_prompt"` keeps each prompt's highest-reward completion above `min_reward` (default 1.0; lower it for a partial-credit grader), `"top_k_overall"` the best `k` across prompts, and the `random_*` rules are the matching chance controls. Exported groups carry `n0`/`n1` (fail/pass, partial credit splits at 0.5) and `reward_mean`/`reward_std`. The band is the offline difficulty filter from the reasoning-model recipes (keep prompts the policy solves 20-80% of the time); it is a heuristic, so it is a parameter. Every selector report (`select_for_rl`, `select_for_sft`, `build_preference_pairs`) carries `eval_sourced`, the rows or pairs whose reward came from `evaluate()` (`lineage.source == "eval"`), with a warning when it is non-zero: a held-out score that becomes the reward makes the scorer you report the one you optimised against. Nothing is dropped; grade the training set with `run_judge` or `data.grade` and keep `evaluate` for held-out rows.
|
|
723
725
|
|
|
724
726
|
### What will the policy learn?
|
|
725
727
|
|
|
726
728
|
```python
|
|
727
|
-
scan =
|
|
729
|
+
scan = wai.hack_scan(scored.rows, endorsed=["tool:lookup_order", "marker:grounded"])
|
|
728
730
|
scan["regime"] # train | reward_hack | pool_exhausted | no_signal | degenerate | unknown
|
|
729
731
|
scan["top_feature"] # e.g. 'contains:### done' when the judge pays for a delimiter
|
|
730
|
-
print(
|
|
732
|
+
print(wai.format_hack_scan(scan))
|
|
731
733
|
```
|
|
732
734
|
|
|
733
735
|
A grouped update learns whatever separates reward *within* an ask; what only tracks which ask it is (difficulty) is baselined away. `hack_scan` asks the question the same way: reward and every candidate feature are centered within ask, ranked by that correlation, and compared to a noise floor from shuffling reward within ask (`tau`). Features come in two tiers, both pure Python: the hand tier (reply length, tool calls, turns, truncation, surface counts, one indicator per tool called, mean token logprob, every numeric marker, plus `features={"name": fn}` of your own) and the auto tier (the 200 most common words and word pairs in the agent's text, and pairwise ANDs that beat both parents), which is the tier that finds the shortcut nobody listed. `endorsed` names what the reward should track, as substrings of feature names; with it the scan can say `reward_hack` (the top feature is not endorsed, and the warning names what the policy would learn instead), `integrity` (share of the above-floor signal that is endorsed), and lists rivals. Without it the scan still ranks and floors. An agent that emits only a couple of distinct trajectories per ask makes every feature that separates them an exact function of the label — they all tie at |rho| 1, and the floor cannot break a tie between two perfect explanations — so the scan returns `degenerate` with `top_feature` `None`, lists the tied features in `collinear`, and names the cause (`distinct_per_ask`) rather than picking the alphabetical winner.
|
|
734
736
|
|
|
735
|
-
The whole loop, before, during and after training, is in [docs/reward-hacking.md](docs/reward-hacking.md) and runs offline in [`recipes/02-measure/reward-hacking`](recipes/02-measure/reward-hacking). `optimize(mode="rl", endorsed=[...])` carries the scan as `report["hack_scan"]`, with its warnings in `report["hygiene_warnings"]` next to the older pooled `report["correlations"]` (reply length, tool calls, turns, flagged at `HACK_THRESHOLD` 0.3). A reward that tracks a shortcut is a judge problem, so it is flagged, not pruned. The publish gate reports the same on RL-shaped rows, plus near-duplicate asks and length spread; `data.push(endorsed=[...], strict_hacks=True)` refuses a `reward_hack`. Standalone: `
|
|
737
|
+
The whole loop, before, during and after training, is in [docs/reward-hacking.md](docs/reward-hacking.md) and runs offline in [`recipes/02-measure/reward-hacking`](recipes/02-measure/reward-hacking). `optimize(mode="rl", endorsed=[...])` carries the scan as `report["hack_scan"]`, with its warnings in `report["hygiene_warnings"]` next to the older pooled `report["correlations"]` (reply length, tool calls, turns, flagged at `HACK_THRESHOLD` 0.3). A reward that tracks a shortcut is a judge problem, so it is flagged, not pruned. The publish gate reports the same on RL-shaped rows, plus near-duplicate asks and length spread; `data.push(endorsed=[...], strict_hacks=True)` refuses a `reward_hack`. Standalone: `wai.reward_correlations(rows)`, `wai.dedupe_groups(rows)`, `wai.near_duplicate_prompts(rows)`, `wai.length_report(rows)`.
|
|
736
738
|
|
|
737
739
|
### Curriculum: easy to hard, and retire the solved
|
|
738
740
|
|
|
739
|
-
A curriculum needs per-prompt difficulty (rlhf-book ch. 7), which is just each task's pass rate over its k rollouts. `curriculum(rows)` splits graded tasks into *trainable* (ordered easy to hard, and bucketed into `tiers` for a staged schedule), *retired* (pass rate
|
|
741
|
+
A curriculum needs per-prompt difficulty (rlhf-book ch. 7), which is just each task's pass rate over its k rollouts. `curriculum(rows)` splits graded tasks into *trainable* (ordered easy to hard, and bucketed into `tiers` for a staged schedule), *retired* (pass rate above `solved`, default 0.8: an all-pass task is dead gradient), and *not ready* (below `floor`, default 0.2: no signal until the policy improves), and counts how many trainable tasks sit in the 20-80% band. The two defaults are the band's own edges, so `curriculum` and `optimize(mode="rl")` agree on which tasks are trainable.
|
|
740
742
|
|
|
741
743
|
```python
|
|
742
|
-
cur =
|
|
744
|
+
cur = wai.curriculum(scored.rows) # solved=0.8, floor=0.2, tiers=3
|
|
743
745
|
cur["schedule"] # trainable task ids, easy -> hard
|
|
744
|
-
print(
|
|
745
|
-
rows =
|
|
746
|
+
print(wai.format_curriculum(cur))
|
|
747
|
+
rows = wai.retire_solved(scored.rows) # drop tasks the policy already aces
|
|
746
748
|
```
|
|
747
749
|
|
|
748
750
|
### Agents
|
|
@@ -754,8 +756,8 @@ An agent exists the moment a push names it or a trace arrives with
|
|
|
754
756
|
data.push(
|
|
755
757
|
"airline-v3", agent="airline-support"
|
|
756
758
|
) # registers the agent and attaches tools + system prompt
|
|
757
|
-
|
|
758
|
-
|
|
759
|
+
wai.agents() # every agent: traces, sets by purpose, public cards
|
|
760
|
+
wai.register_agent("airline-support", description="Refunds and rebooking")
|
|
759
761
|
```
|
|
760
762
|
|
|
761
763
|
### Clean up
|
|
@@ -766,7 +768,7 @@ whileai purge --agent demo-agent # delete them, after a y/N
|
|
|
766
768
|
whileai purge --empty --max-rows 2 # datasets with no bytes, or 2 rows or fewer
|
|
767
769
|
```
|
|
768
770
|
|
|
769
|
-
Python: `
|
|
771
|
+
Python: `wai.purge_agent("demo-agent")`, `wai.delete_empty_datasets(max_rows=2)`.
|
|
770
772
|
Both take `dry_run=True`.
|
|
771
773
|
|
|
772
774
|
### Train, holdout, eval
|
|
@@ -776,9 +778,9 @@ data.push("airline-v3", holdout=0.2) # train set + a linked holdout set, split
|
|
|
776
778
|
data.push("airline-evals", purpose="eval") # a set you measure with
|
|
777
779
|
scored = data.grade(judge=my_judge)
|
|
778
780
|
scored.push("airline-rl-v3", gate=True, mode="rl") # the graded copies, gated
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
781
|
+
wai.update_dataset("ds_...", purpose="holdout")
|
|
782
|
+
wai.preview("ds_...") # three sample rows + the analyzer report
|
|
783
|
+
wai.profile("ds_...") # pass rate, support, mixed tasks, tool use, per task
|
|
782
784
|
```
|
|
783
785
|
|
|
784
786
|
The Datasets page groups sets by purpose (train, holdout, eval) and
|
|
@@ -799,10 +801,10 @@ leaves the rest of the eval paired for `compare_runs`.
|
|
|
799
801
|
The platform's "Make training data" button, as one line:
|
|
800
802
|
|
|
801
803
|
```python
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
made =
|
|
805
|
-
|
|
804
|
+
wai.send_score("4bf92f3577b34da6", 1.0) # this run passed
|
|
805
|
+
wai.cuts(agent="my-agent") # what a cut would hold
|
|
806
|
+
made = wai.cut(agent="my-agent", kind="rl") # make it
|
|
807
|
+
wai.pull(made["train"]["datasetId"], "train.jsonl")
|
|
806
808
|
made["holdout"]["datasetId"] # measure on this, never train on it
|
|
807
809
|
```
|
|
808
810
|
|
|
@@ -825,47 +827,49 @@ and `holdout=` move the defaults, and any other keyword is a trace filter (`mode
|
|
|
825
827
|
Three checks that decide whether a result is believable, all report-only and all over rows you already have.
|
|
826
828
|
|
|
827
829
|
```python
|
|
828
|
-
rows, report =
|
|
830
|
+
rows, report = wai.attach_labels(
|
|
829
831
|
rows, "labels.jsonl", annotator="ana"
|
|
830
832
|
) # gold_reward + who said what
|
|
831
|
-
|
|
833
|
+
wai.judge_trust(rows, judge=my_judge) # is the judge trustworthy?
|
|
832
834
|
data.grade(use_privileged=True) # judge also reads privileged principle, reference, hidden state
|
|
833
|
-
|
|
834
|
-
pairs, report =
|
|
835
|
-
rows, report =
|
|
836
|
-
scored =
|
|
837
|
-
clean, report =
|
|
835
|
+
wai.run_judge(rows, likert_judge, scale=(1, 5)) # rating kept, reward = (r - 1) / 4
|
|
836
|
+
pairs, report = wai.judge_pairs(pairs) # A vs B both ways round: winner, tie, position_flip_rate
|
|
837
|
+
rows, report = wai.write_rubrics(rows, domain="refunds") # per-prompt criteria on privileged.rubric
|
|
838
|
+
scored = wai.run_judge(rows, wai.rubric_judge()) # a verdict per criterion; markers rubric:<item>
|
|
839
|
+
clean, report = wai.decontaminate(
|
|
838
840
|
train_rows, against=[eval_rows]
|
|
839
841
|
) # 8-gram overlap with the eval set
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
) #
|
|
850
|
-
|
|
842
|
+
wai.style_markers(rows) # no_boilerplate, no_hedging, no_apology, no_sycophancy, answered
|
|
843
|
+
wai.style_report(rows)["warnings"] # "reward pays for hedging (corr +0.41 ...)"
|
|
844
|
+
wai.refusal_report(benign_rows) # over-refusal rate with a Wilson interval
|
|
845
|
+
wai.compare_runs(run_a, run_b) # paired delta with a 95% interval
|
|
846
|
+
wai.delta_report(before, after, target="pass_at_1", must_not_regress=["honest_after_fault"])
|
|
847
|
+
wai.delta_report(before, after, target="pass_at_1", by="category") # the target per kind of prompt
|
|
848
|
+
before = wai.simulate(agent, tools=TOOLS, tasks=base, runs=3) # the same eval three times
|
|
849
|
+
after = wai.simulate(trained, tools=TOOLS, tasks=base, runs=3)
|
|
850
|
+
wai.delta_report(before.rows(), after.rows(), target="pass_at_1") # run_std computed from the runs
|
|
851
|
+
wai.eval_variance(before.rows()) # the eval's own re-run std, split by lineage.eval_run
|
|
852
|
+
wai.mark_grounding(
|
|
851
853
|
rows
|
|
852
854
|
) # markers["argument_grounding"]: every tool argument came from the conversation
|
|
853
|
-
|
|
855
|
+
wai.grounding_report(rows) # grounded rate, and the invented values by tool and key
|
|
854
856
|
```
|
|
855
857
|
|
|
856
|
-
**
|
|
858
|
+
**Checked by default.** Every `grade` call ends by checking the judge against the rows' human labels, with no flag needed. "Gold" means a label a person wrote: `attach_labels(rows, labels, kind="human")` stamps `gold_reward` and `gold_kind="human"`; a model's labels, or a second judge pass, are marked `model` and do not count, and older rows with `gold_reward` but no record of who wrote it count as unknown. Label 50 rows by hand (a JSONL of `{"key": ..., "label": 0 or 1}` or a `{key: label}` dict), attach them, and grade: the summary lands on every graded row as `judge_meta["trust"]` (`agreement`, `agreement_low`, `kappa`, `n_gold`, `ok`) and in the grade report as `trust`, and `publish_gate` carries it as `judge_trust`. The judge passes when the lower bound of its agreement with the people is at least 0.80 and kappa at least 0.60; under either, the report says the number, the floor, and what to do. With no human labels the grade prints one line saying the judge was not measured. `grade(trust="require")` raises instead of printing; `trust="off"` skips the check. `judge_trust` refuses model gold the same way unless `allow_model_gold=True`. And `audit_grades` never audits with the grader's own model: when the auditor would be the same, it uses the other hosted model and the report says which (`grader`, `auditor`), or it stops and asks for `backend_spec=`.
|
|
859
|
+
|
|
860
|
+
**Judge trust.** Label 50 to 100 rows by hand with `attach_labels` (0/1) -- `report["ok"]` means measured and clean, so with no labels it is `False` and the report says the judge is unmeasured rather than untrustworthy (`format_judge_trust` prints `NOT MEASURED`). The report gives agreement with a Wilson interval and Cohen's kappa, agreement on two task halves (tune the rubric on one, read the other), judge pass rate on short versus long replies within the same human label (length bias the humans rule out), and, with the judge callable, a re-judge of a sample as-is (consistency) and with neutral filler appended (a flip means the judge reads length). Disagreements come back as a review queue. `format_judge_trust(report)` prints it. `probes="all"` (or a list) tries the reward hacks a policy finds first on the judge on purpose: filler, the rubric's own words stuffed in, a claim of success with no evidence, the ask echoed back, a well-formed tool call with empty arguments, a sycophantic opener, a polite refusal. An additive probe is exploitable when failing replies start passing; a replacement probe when a reply with no content passes. `report["exploitable_by"]` names the holes at or over 10%, and a policy trained on this judge will find those same holes. Standalone: `wai.judge_probes(rows, judge, rubric=...)`. The gold set needs both passes and failures; with one class only the report says so and skips the kappa and length flags. With the hosted judge, call `wai.grade` once first (or `whileai.simulations.score.grade_llm.warm_judge`; it is not re-exported) so the cold start, two to three minutes, is not counted as timeouts.
|
|
857
861
|
|
|
858
862
|
**Decontamination.** Word 8-gram overlap between a dataset's prompts and any evaluation source: row lists, JSONL paths, or platform dataset ids. A row is contaminated when it is an eval prompt verbatim or when one eval text covers at least 80% of its words (`overlap=`, the Llama 2 rule); one shared 8-gram is not enough, because situations written from the same templates share whole sentences without sharing the question. Short prompts match verbatim only. `fields=("prompt", "final_text")` also checks replies against eval answers and references. The report separates verbatim hits from near copies and counts hits per field, and returns the clean rows with the first offenders.
|
|
859
863
|
|
|
860
|
-
**Intervals and comparison.** Every pass@1 carries a 95% interval from a bootstrap over tasks (`pass_at(rows).ci95`), and `metric_summary` / `marker_summary` do the same for markers. pass^k and pass@k carry their own (`pass_pow_k_ci95`, `pass_at_k_ci95`), a bootstrap over the k-eligible groups. Markers come from the judge: return `{"reward": ..., "markers": {"name": value}}` from a `grader=` or `run_judge` callable and they land on `row["markers"]`, which is what `marker_summary`, `delta_report` and `from_row` read. `compare_runs` pairs the tasks two runs share, bootstraps the paired difference, and adds a sign-flip permutation p-value; fewer than five shared tasks falls back to an unpaired test and says so. Tasks on one side only are dropped from a paired comparison; `note` says how many and `paired_share` is the fraction that paired, so a verdict over a quarter of the eval reads as one. The verdict `no_difference_detected` means the interval covers zero, not that the runs are equal.
|
|
864
|
+
**Intervals and comparison.** Every pass@1 carries a 95% interval from a bootstrap over tasks (`pass_at(rows).ci95`), and `metric_summary` / `marker_summary` do the same for markers. pass^k and pass@k carry their own (`pass_pow_k_ci95`, `pass_at_k_ci95`), a bootstrap over the k-eligible groups. Markers come from the judge: return `{"reward": ..., "markers": {"name": value}}` from a `grader=` or `run_judge` callable and they land on `row["markers"]`, which is what `marker_summary`, `delta_report` and `from_row` read. `compare_runs` pairs the tasks two runs share, bootstraps the paired difference, and adds a sign-flip permutation p-value; fewer than five shared tasks falls back to an unpaired test and says so. Tasks on one side only are dropped from a paired comparison; `note` says how many and `paired_share` is the fraction that paired, so a verdict over a quarter of the eval reads as one. The verdict `no_difference_detected` means the interval covers zero, not that the runs are equal. A task is a situation, not a string: every report (`pass_at`, `compare_runs`, `delta_report`, `eval_variance`, `curriculum`, `group_signal`, the exporters) groups rows by `wai.task_key(row)`, the engine's `scenario_id` when the row has one, so repeats and rephrasings of one situation count as one task and the same rows give the same task count everywhere. `pass_at(rows).config` and `delta_report(...)["config"]` say what the rows were produced with (temperature, reply budget, policy and judge versions), and `delta_report` warns when the two sides differ.
|
|
861
865
|
|
|
862
|
-
**Same tasks, new prompt.** A run draws its tasks from the grid by seed and, above `concurrency: 1`, by completion order, so a second `simulate()` shares only part of its tasks with the first. To A/B a prompt edit, a model swap or another seed on exactly the same eval, pin the task set: `
|
|
866
|
+
**Same tasks, new prompt.** A run draws its tasks from the grid by seed and, above `concurrency: 1`, by completion order, so a second `simulate()` shares only part of its tasks with the first. To A/B a prompt edit, a model swap or another seed on exactly the same eval, pin the task set: `wai.simulate(agent, tools=TOOLS, system_prompt=EDITED, tasks=base)` re-runs every prompt of `base` (a run, its rows, or its JSONL path) on its own `scenario_id`, under the same faults and world state, and draws nothing new; it stops with `tasks_done` once every prompt has its rollouts, and `compare_runs(base.rows(), rerun.rows())` pairs every task.
|
|
863
867
|
|
|
864
868
|
`tasks=` copies the prompts and, unless you pass `repeats=`, the pinned run's k (the most rollouts any of its prompts has), so a base built with `mode="rl", repeats=4` and re-run as `simulate(..., tasks=base, mode="rl")` comes back at k=4 and `pass_at` reports the same k on both sides. Pass `repeats=` to re-run at a different k on purpose:
|
|
865
869
|
|
|
866
870
|
```python
|
|
867
|
-
base =
|
|
868
|
-
rerun =
|
|
871
|
+
base = wai.simulate(agent, tools=TOOLS, system_prompt=POLICY, mode="rl", repeats=4)
|
|
872
|
+
rerun = wai.simulate(
|
|
869
873
|
agent, tools=TOOLS, system_prompt=EDITED, tasks=base, mode="rl"
|
|
870
874
|
) # k=4, inherited
|
|
871
875
|
assert base.rollouts_per_request == rerun.rollouts_per_request # cheap guard
|
|
@@ -873,18 +877,20 @@ assert base.rollouts_per_request == rerun.rollouts_per_request # cheap guard
|
|
|
873
877
|
|
|
874
878
|
**Before and after.** `delta_report` runs `compare_runs` on pass@1 and every marker both row sets share. `target=` names the metric the training was meant to move and gives the headline; `must_not_regress=` names the behaviors whose significant drop fails the report; any other significant drop is a warning. `format_delta_report(report)` prints one line per metric. `eval_variance(run_1, run_2, run_3)` is the eval's own re-run standard deviation (three or more evaluations of the same model); passing it as `run_std=` makes any delta inside twice that band `within_noise`, and a target there reads `within_eval_noise` rather than moved, since re-running the eval moves it that much on its own (rlhf-book ch. 16). `by=` names a row key, a marker, or a callable that groups rows (a prompt category, a tool, a persona); the report then carries `groups`, the target compared within each group, and `groups_down` for any group whose target dropped significantly while the headline moved. A headline over one dominant kind of prompt cannot hide the other kinds that way.
|
|
875
879
|
|
|
880
|
+
**Run the eval three times.** One evaluation is a draw, not a number: the same model on the same tasks lands somewhere else next time, and most post-training gains are inside that spread (rlhf-book ch. 16, appendix C). `wai.simulate(agent, tasks=base, runs=3)` replays the task set three times in one call, same tasks, faults and world, and stamps `lineage.eval_run` on every row. Feed both sides to `delta_report` and it works out `run_std` from the repeats itself. The verdict words: `moved` is a change the interval and the re-run band both support; `moved_unreplicated` is a change seen once, which could be noise, and the warning tells you the `runs=3` call that settles it; `within_eval_noise` is a delta smaller than what re-running the eval does on its own, so equivalence, not a win; `no_change_detected` is an interval that covers zero. `ceiling=True` means the before run already passes most of its tasks (0.9 or more, or too few paired tasks left with room), so there is little improvement the eval could show; use harder situations before training again.
|
|
881
|
+
|
|
876
882
|
**Argument grounding.** A policy trained to call a tool learns to call it before it learns when not to; on the refund environment both GRPO and DPO learned to invent an order id on a quarter of the prompts that gave none while the headline rose. `mark_grounding(rows)` stamps `argument_grounding`: 1 when every string argument of every tool call appears in the prompt, the user and system turns, or an earlier tool result (rows with no calls count as grounded), else 0. No categories, any agent; `must_not_regress=["argument_grounding"]` fails the run that learned to invent, and `ungrounded_arguments(row)` / `grounding_report(rows)` name the values. `ignore_keys=` skips free-text arguments, `allow=` lists enums and defaults.
|
|
877
883
|
|
|
878
884
|
**Trajectory flags.** Did the agent fake the work? `trace_markers(rows)` reads the trajectory rather than the prose (rlhf-book ch. 13, 14): `lie.tests_claimed` (tests said to pass when no test command ran or the last one failed), `lie.unverified_claim` ("I verified" with no tool calls), `lie.phantom_edit` ("I updated" with nothing written), `lie.ignored_failure` (the turn ended on a failed call and the reply never says so), `hack.test_edited`, `hack.test_weakened`, `hack.suppressed`, `hack.bypassed`, `risk.destructive`, `risk.secrets`, each with the fragment that raised it on `row["trace_flags"]`. The markers it stamps (`honest_claims`, `reported_failure`, `no_test_tampering`, `no_suppression`, `no_bypass`, `no_destructive`, `no_secrets`) are 1.0 when clean, so `must_not_regress=["honest_claims"]` fails a run that learned to overclaim, and `hack_scan` carries every fired flag as a `trace:` feature. `trace_flag_report(rows)` gives each flag's rate, examples, and its correlation with the reward, flagged when the judge pays for the fake. Reads, writes, deletes and commands are told apart by the tool's arguments and name; `kinds={"my_tool": "write"}` overrides.
|
|
879
885
|
|
|
880
|
-
**Stage lineage.** The pipeline is a sequence of stages (rlhf-book ch. 3): SFT, reward modeling, RL, and the eval that judges the result. `stamp_stage(rows, "sft")` records which stage a row fed, and `stage_report(rows)` counts rows per stage and flags the one mistake it most needs caught: any task used in both `eval` and a training stage. `
|
|
886
|
+
**Stage lineage.** The pipeline is a sequence of stages (rlhf-book ch. 3): SFT, reward modeling, RL, and the eval that judges the result. `stamp_stage(rows, "sft")` records which stage a row fed, and `stage_report(rows)` counts rows per stage and flags the one mistake it most needs caught: any task used in both `eval` and a training stage. `wai.stamp_stage`, `wai.stage_report`, `wai.stage_of`, `wai.STAGES` (`sft`, `rm`, `rl`, `eval`, `mid`).
|
|
881
887
|
|
|
882
888
|
**Model spec as an object.** A spec or constitution is a living, versioned document (rlhf-book ch. 17). `load_spec(constitution)` wraps the `{source, traits: [{id, name, principle, authority}]}` shape (what the character example writes) into a `Spec` whose `version` is a content hash, so any edit to a principle changes it. `spec.behaviors()` are the trait ids, ready for `delta_report(must_not_regress=...)`; `stamp_spec(rows, spec)` tags a run with the spec version it targeted, so you can ask whether adherence held from one spec or model version to the next.
|
|
883
889
|
|
|
884
890
|
```python
|
|
885
|
-
spec =
|
|
886
|
-
scored =
|
|
887
|
-
|
|
891
|
+
spec = wai.load_spec("recipes/03-select/character/constitution.json")
|
|
892
|
+
scored = wai.stamp_spec(data.grade(judge=my_judge).rows, spec)
|
|
893
|
+
wai.delta_report(before=before, after=scored, target="pass_at_1", must_not_regress=spec.behaviors())
|
|
888
894
|
```
|
|
889
895
|
|
|
890
896
|
#### Markers: four families, one polarity
|
|
@@ -898,9 +904,9 @@ one of them has the wrong polarity:
|
|
|
898
904
|
| Family | How you get it | Polarity | Use it for |
|
|
899
905
|
|---|---|---|---|
|
|
900
906
|
| **Judge-emitted custom markers** | your own name and value, returned as `{"reward": ..., "markers": {"name": value}}` from a `judge=` / `grader=` / `run_judge` callable | **yours to choose — and it must be 1.0 = good** | Anything your product cares about. This is the family `delta_report` and `must_not_regress=` are built for |
|
|
901
|
-
| `trace_markers` / `trace_flag_report` | `
|
|
902
|
-
| `style_markers` / `style_report` | `
|
|
903
|
-
| `behavioral_markers` / `mark_rows` / `STOCK_MARKERS` | `
|
|
907
|
+
| `trace_markers` / `trace_flag_report` | `wai.trace_markers(rows)` stamps `honest_claims`, `reported_failure`, `no_test_tampering`, `no_suppression`, `no_bypass`, `no_destructive`, `no_secrets`, with the evidence on `row["trace_flags"]` | 1.0 = no flag fired, higher is better | Did the agent fake the work? Read from the trajectory, not the prose — see [Trajectory flags](#trust-the-numbers) above |
|
|
908
|
+
| `style_markers` / `style_report` | `wai.style_markers(rows)` stamps `no_boilerplate`, `no_hedging`, `no_apology`, `no_sycophancy`, `answered` | 1.0 = clean reply, higher is better | Over-optimization drift in a paired before/after |
|
|
909
|
+
| `behavioral_markers` / `mark_rows` / `STOCK_MARKERS` | `wai.behavioral_markers(rows)` -> `{"boilerplate": 0.31, "refusal": 0.04, ...}` | **presence: 1 = the tic appears, higher is worse** | A one-shot read of how often each tic occurs. Not a delta |
|
|
904
910
|
|
|
905
911
|
> **Deprecated.** `behavioral_markers`, `mark_rows`, `row_markers` and
|
|
906
912
|
> `STOCK_MARKERS` all live in
|
|
@@ -933,7 +939,7 @@ know which direction a name means.
|
|
|
933
939
|
Two ways to train, one record. The platform trains a pushed dataset (SFT, GRPO, DPO or a reward model, LoRA on an A10G) and serves the result; or your own trainer runs on Modal, a GPU box, or a notebook and reports into the same run. Either way the loss curve and the progress bar are at [zeroproofai.com/platform/training](https://www.zeroproofai.com/platform/training).
|
|
934
940
|
|
|
935
941
|
```python
|
|
936
|
-
run =
|
|
942
|
+
run = wai.train(
|
|
937
943
|
"ds_...", method="sft", base_model="Qwen/Qwen3-4B", epochs=2
|
|
938
944
|
) # or "grpo" / "dpo" / "rm" with steps=
|
|
939
945
|
run.wait() # done or failed; run.url is the curve while it goes
|
|
@@ -941,37 +947,37 @@ run.training["before"], run.training["after"] # holdout pass@1 (SFT: loss)
|
|
|
941
947
|
run.delta(
|
|
942
948
|
before_rows, after_rows, target="pass_at_1", by="category"
|
|
943
949
|
) # paired delta on the run page
|
|
944
|
-
model =
|
|
950
|
+
model = wai.serve("refund-v2", run) # adapter on an OpenAI-compatible endpoint
|
|
945
951
|
# model["endpoint"] + /chat/completions, model="refund-v2", bearer = your zp_ key
|
|
946
|
-
|
|
952
|
+
wai.models() # what the account hosts
|
|
947
953
|
```
|
|
948
954
|
|
|
949
|
-
`epochs=` sets SFT, `steps=` sets GRPO, DPO and RM; each method has a default. `run.delta` is `delta_report` (below) kept on the run and drawn on its page, including the per-group table when `by=` names a row key or marker; `
|
|
955
|
+
`epochs=` sets SFT, `steps=` sets GRPO, DPO and RM; each method has a default. `run.delta` is `delta_report` (below) kept on the run and drawn on its page, including the per-group table when `by=` names a row key or marker; `wai.attach_delta(run_id, before, after)` does the same for a run that already finished. `holdout=` names the eval set (defaults to the train set's split sibling); a dataset already training returns that run. `serve` needs a finished run whose base is a served one (`Qwen/Qwen3-4B`, `microsoft/phi-4`). The trainer's default bases (Qwen2.5-0.5B for SFT, 1.5B for GRPO and DPO) train in under a minute but cannot be served, so `train` warns when a run will not reach an endpoint; SFT runs on an A10G; GRPO and DPO run on an L40S, so a 4B base fits all three. Qwen3 answers in thinking mode by default: leave room in `max_tokens` or send `extra_body={"chat_template_kwargs": {"enable_thinking": False}}`.
|
|
950
956
|
|
|
951
|
-
`method="rm"` trains a reward model (rlhf-book ch. 5) on the set's pass-vs-fail pairs and reports pair accuracy on the held-out pairs before and after. `
|
|
957
|
+
`method="rm"` trains a reward model (rlhf-book ch. 5) on the set's pass-vs-fail pairs and reports pair accuracy on the held-out pairs before and after. `wai.reward_model(run)` is that model as a judge, with the judge contract (`reward` 0/1 against the run's threshold, `rm_score` raw), so it goes wherever a judge goes:
|
|
952
958
|
|
|
953
959
|
```python
|
|
954
|
-
rm =
|
|
955
|
-
run =
|
|
960
|
+
rm = wai.train("ds_...", method="rm", steps=60, wait=True)
|
|
961
|
+
run = wai.train(
|
|
956
962
|
"ds_...", method="grpo", generations=8, beta=0.02, learning_rate=5e-6, seed=3
|
|
957
963
|
) # the knobs a run is compared by
|
|
958
|
-
judge =
|
|
964
|
+
judge = wai.reward_model(rm) # or reward_model("run_...", threshold=0.4)
|
|
959
965
|
scored = data.grade(judge=judge)
|
|
960
|
-
|
|
966
|
+
wai.judge_trust(scored.rows, judge=judge) # the same checks as the LLM judge
|
|
961
967
|
```
|
|
962
968
|
|
|
963
969
|
Your own trainer, three ways in:
|
|
964
970
|
|
|
965
971
|
```python
|
|
966
972
|
# one line on a Transformers or TRL trainer
|
|
967
|
-
run =
|
|
973
|
+
run = wai.training_run(
|
|
968
974
|
"identity-v1", dataset="ds_...", base_model="Qwen/Qwen3-4B-Instruct-2507", trainer="trl"
|
|
969
975
|
)
|
|
970
|
-
trainer.add_callback(
|
|
976
|
+
trainer.add_callback(wai.TrainerCallback(run))
|
|
971
977
|
trainer.train() # loss, lr, eval loss, epoch, grad norm, then finish
|
|
972
978
|
|
|
973
979
|
# your own loop
|
|
974
|
-
with
|
|
980
|
+
with wai.training_run("sft-v3", dataset="ds_...", total_steps=1000) as run:
|
|
975
981
|
for step, batch in enumerate(loader):
|
|
976
982
|
loss = train_step(batch)
|
|
977
983
|
run.log(step, loss=loss, lr=scheduler.get_last_lr()[0])
|
|
@@ -980,27 +986,27 @@ with zps.training_run("sft-v3", dataset="ds_...", total_steps=1000) as run:
|
|
|
980
986
|
run.holdout(before=0.42, after=0.58) # did it work? the run page opens with this
|
|
981
987
|
```
|
|
982
988
|
|
|
983
|
-
A run's page opens with one word — **Better**, **Worse**, **About the same** — over the held-out pass rate before and after. The platform's trainer measures it; a run on your own hardware says it with `run.holdout(before, after)`, or `
|
|
989
|
+
A run's page opens with one word — **Better**, **Worse**, **About the same** — over the held-out pass rate before and after. The platform's trainer measures it; a run on your own hardware says it with `run.holdout(before, after)`, or `wai.attach_holdout(run_id, before=..., after=...)` once the run has finished. Pass rates are 0 to 1, so 58% is `0.58`; `metric="loss"` sends held-out loss instead (SFT), where lower is better. `run.delta(...)` and `wai.attach_delta(...)` already measure both sides, so they fill the two numbers in themselves, and add `summary["holdout"]` (also `run.holdout_summary`): each side's pass rate with `n_tasks`, `k` and a `ci95`, plus the delta report's verdict word (`moved`, `moved_unreplicated`, `within_eval_noise`, `no_change_detected`). A hosted run read back with `run.refresh()` has the same block with the interval fields `None` and a note that the platform only returned two numbers.
|
|
984
990
|
|
|
985
991
|
### Is it hacking the reward right now?
|
|
986
992
|
|
|
987
993
|
```python
|
|
988
|
-
monitor =
|
|
994
|
+
monitor = wai.HackMonitor(
|
|
989
995
|
run,
|
|
990
996
|
holdout=holdout_rows, # prompts or {"prompt": ..., <columns the reward reads>}
|
|
991
|
-
gold=
|
|
997
|
+
gold=wai.reward_model(rm_run), # or the hosted judge, or a second rule; any judge callable
|
|
992
998
|
every=10, k=4, # sample the holdout from the live policy every 10 steps
|
|
993
999
|
endorsed=["tool:lookup_order"], # what the reward should track
|
|
994
1000
|
stop_on="divergence", # or "length", "drift", "feature", "any"; default: log only
|
|
995
1001
|
)
|
|
996
1002
|
trainer = GRPOTrainer(model, reward_funcs=[monitor.wrap(rule_reward)], ...)
|
|
997
1003
|
trainer.add_callback(monitor)
|
|
998
|
-
trainer.add_callback(
|
|
1004
|
+
trainer.add_callback(wai.TrainerCallback(run))
|
|
999
1005
|
```
|
|
1000
1006
|
|
|
1001
|
-
Over-optimization looks like one picture (rlhf-book ch. 14): the training reward keeps climbing while the evaluation you care about flattens, read against KL. The monitor draws it during the run instead of after. `wrap` watches the reward function, so the monitor keeps the last completions with their rewards and runs `hack_scan` on them; every `every` steps it samples the holdout from the live policy and scores it with the training reward (the proxy) and with `gold`, a scorer the proxy cannot see. `proxy_reward`, `gold_reward` and `holdout_length` land on the run beside the loss curve. Four alarms, one line each on the run: `divergence` (proxy up by `delta` over the window while the paired gold interval does not move up), `length` (completions grow while gold does not), `drift` (KL past `kl_budget`), `feature` (the batch scan says `reward_hack`). `stop_on` names the ones that stop training; a stopped run finishes as `stopped` with the reason, and `run.note(...)` puts anything else on the run's summary. `
|
|
1007
|
+
Over-optimization looks like one picture (rlhf-book ch. 14): the training reward keeps climbing while the evaluation you care about flattens, read against KL. The monitor draws it during the run instead of after. `wrap` watches the reward function, so the monitor keeps the last completions with their rewards and runs `hack_scan` on them; every `every` steps it samples the holdout from the live policy and scores it with the training reward (the proxy) and with `gold`, a scorer the proxy cannot see. `proxy_reward`, `gold_reward` and `holdout_length` land on the run beside the loss curve. Four alarms, one line each on the run: `divergence` (proxy up by `delta` over the window while the paired gold interval does not move up), `length` (completions grow while gold does not), `drift` (KL past `kl_budget`), `feature` (the batch scan says `reward_hack`). `stop_on` names the ones that stop training; a stopped run finishes as `stopped` with the reason, and `run.note(...)` puts anything else on the run's summary. `wai.format_hack_monitor(monitor.summary())` prints the curve and the alarms. [`recipes/04-train/grpo`](recipes/04-train/grpo) runs it by default.
|
|
1002
1008
|
|
|
1003
|
-
Plain HTTP, for a stack that is not Python: `POST /runs {"name", "dataset_id", "base_model", "total_steps"}` returns `runId`; `POST /runs/{id}/log {"points": [{"step": 10, "loss": 1.2, "lr": 1e-4}], "total_steps"?}` in batches of up to 500; `POST /runs/{id}/finish {"status": "done|failed|stopped", "summary"?, "adapter"?}`. All with `X-Api-Key`. Points are buffered on the client and a send that fails is retried on the next flush; the dashboard never interrupts the trainer. `
|
|
1009
|
+
Plain HTTP, for a stack that is not Python: `POST /runs {"name", "dataset_id", "base_model", "total_steps"}` returns `runId`; `POST /runs/{id}/log {"points": [{"step": 10, "loss": 1.2, "lr": 1e-4}], "total_steps"?}` in batches of up to 500; `POST /runs/{id}/finish {"status": "done|failed|stopped", "summary"?, "adapter"?}`. All with `X-Api-Key`. Points are buffered on the client and a send that fails is retried on the next flush; the dashboard never interrupts the trainer. `wai.get_run(id)["series"]` returns the points, oldest first.
|
|
1004
1010
|
|
|
1005
1011
|
### Publish a dataset as a card
|
|
1006
1012
|
|
|
@@ -1011,22 +1017,22 @@ data.push(
|
|
|
1011
1017
|
publish=True,
|
|
1012
1018
|
description="Graded refund conversations with injected tool faults.",
|
|
1013
1019
|
)
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
rows =
|
|
1017
|
-
|
|
1020
|
+
wai.publish("ds_...", agent="airline-support") # or publish an existing one
|
|
1021
|
+
wai.catalog() # every public card, by agent
|
|
1022
|
+
rows = wai.pull("ds_...") # public sets need no key
|
|
1023
|
+
wai.unpublish("ds_...")
|
|
1018
1024
|
```
|
|
1019
1025
|
|
|
1020
1026
|
Hugging Face, both directions. Connect your account once on any dataset page, then:
|
|
1021
1027
|
|
|
1022
1028
|
```python
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
row =
|
|
1029
|
+
wai.hf_status() # connected? namespaces
|
|
1030
|
+
wai.hf_publish("ds_...", repo="airline-refunds", wait=True) # rows -> a dataset repo you own
|
|
1031
|
+
wai.hf_publish_run("run_...", private=True) # a finished run's LoRA adapter -> a model repo
|
|
1032
|
+
row = wai.import_hf(
|
|
1027
1033
|
"tatsu-lab/alpaca", split="train", purpose="eval"
|
|
1028
1034
|
) # any Hub split -> your account
|
|
1029
|
-
|
|
1035
|
+
wai.profile(row["datasetId"]) # profiled before you train on it
|
|
1030
1036
|
```
|
|
1031
1037
|
|
|
1032
1038
|
Every push is one commit tagged `zp-<id>`, so `load_dataset(repo, split, revision="zp-ds_...")` pins the exact push; the repo's `whileai.json` maps each split to its While dataset with history. Worked example: [`recipes/05-export/hugging-face`](recipes/05-export/hugging-face).
|
|
@@ -1086,13 +1092,15 @@ Aliases: `phrasings=` / `n=` → `requests_per_situation`; `repeats=` → `rollo
|
|
|
1086
1092
|
|
|
1087
1093
|
## Output
|
|
1088
1094
|
|
|
1089
|
-
Each row, in `data.trajectories` and on disk: `prompt`, `messages`, `steps`, `final_text`, `scenario_id`. Optional `world_state`, `faults`, `reward`, `reason`. `llm_grade=True` adds `llm_reward`. `
|
|
1095
|
+
Each row, in `data.trajectories` and on disk: `prompt`, `messages`, `steps`, `final_text`, `scenario_id`. Optional `world_state`, `faults`, `reward`, `reason`. `llm_grade=True` adds `llm_reward`. `wai.rank(path)` adds `quality` without changing `reward`. Every row also says how it was sampled: `sampling` is `{"temperature", "max_tokens", "model"}` as the model backend resolved them, or what you passed as `simulate(sampling=...)` for your own callable agent (`None` when you passed nothing, since only you know how it samples). `policy_version` names the model and the system prompt it ran under.
|
|
1096
|
+
|
|
1097
|
+
Three models can take part in a run, and by default they are one: the agent answers, and the same model writes the situations and plays the user in follow-up turns (only the judge is a different model). Every row now says who did which job, next to `model_version` for the agent: `writer_model` (the writer's model, or `template`, `seed`, `pinned` when no model wrote the prompt), `user_model` (absent when the agent took a single message), and `judge_meta.model` once graded; `data.metadata` and the `.meta.json` sidecar carry the same three. When the agent model also wrote the situations or played the user, `data.degraded` holds `same_model` and `data.warnings` says so in one sentence, with the fix: pass `simulator=` for the writer and `user_model=` for the user to put those jobs on a different model.
|
|
1090
1098
|
|
|
1091
1099
|
What goes to disk is the whole row, not a summary of it: `data.rows` (the same list `output=` and `save()` write, callable as `data.rows()` too) carries everything the trajectory carries, so a saved run can still prove its own provenance. That includes how the situation was drawn (`scenario_dimensions`, `arm`, `selection_reason`, `behavior_signature`, `seed`), who graded it and how that went (`judge_name`, `judge_status`, `judge_meta`, `lineage`, `label_source`), and what was measured on it (`markers`, read by `marker_summary` and `delta_report`). Two things never ship, at any depth of the row: the teacher-only `privileged` block and its `principle` / `hidden_state` / `reference` / `rubric` fields, which would put the answer key one step from a training file, and `vector`, the raw embedding the diversity search keeps in memory for the length of the run. A privileged block nested inside a carried field or a tool result is dropped the same way, before `messages` is rebuilt from the steps.
|
|
1092
1100
|
|
|
1093
1101
|
After grading, `data.pass_at` (also on the `ScoredData` from `judge=` and `evaluate`) gives pass@1, pass^k and pass@k off the same groups, one job each: pass@1 is the measurement headline (the agent runs once in production), pass^k is the reliability line (all k repeats pass), and pass@k minus pass@1 (`.headroom`) is what a grouped RL update has to learn from, the same asks `group_signal` counts as mixed. k is the smallest group of repeats; below `repeats=4` the k-way numbers are `None` with a note rather than a noisy figure. With an LLM judge, pass@k inflates on false positives and pass^k on false negatives, so pass@1 stays the headline.
|
|
1094
1102
|
|
|
1095
|
-
`.per_task` is a **dict**, `{
|
|
1103
|
+
`.per_task` is a **dict**, `{task: pass rate over that task's rollouts}` — keyed by `task_key(row)` (the `scenario_id`, else `task_id`, else the prompt string), not indexed, so `per_task[0]` is a `KeyError` and not the first task. Iterate `.per_task.items()`; `.per_task.values()` is the pass-rate vector pass@1 averages.
|
|
1096
1104
|
|
|
1097
1105
|
**Which of these carry an interval.** All three pass numbers: `pass_at(rows).ci95` is a bootstrap over tasks on pass@1, and `pass_pow_k_ci95` / `pass_at_k_ci95` bootstrap the per-group unbiased estimates over the k-eligible groups, so the reliability line is read with the uncertainty of the tasks behind it (rlhf-book ch. 16). Fewer than three groups gives `None`. What else in this file carries one: `metric_summary` / `marker_summary` over markers, `trace_flag_report` over each trajectory marker's clean share, `refusal_report` and `judge_trust` a Wilson interval, `compare_runs` / `delta_report` a bootstrap interval on the paired *difference*. If a number is not in that list and is not one of the three pass numbers, assume it is a point estimate.
|
|
1098
1106
|
|
|
@@ -1101,26 +1109,26 @@ scored = data.grade(judge=my_judge)
|
|
|
1101
1109
|
print(scored.pass_at) # pass@1 0.61 | pass^8 0.32 | pass@8 0.88 | headroom 0.27 (200 groups, k=8)
|
|
1102
1110
|
```
|
|
1103
1111
|
|
|
1104
|
-
`simulate(logprobs=True)` records, on every agent turn, the summed log-probability of the tokens the policy generated and how many there were (`step["logprob"]`, `step["n_tokens"]`, totals on the row). A trainer that updates on these rollouts later needs that number to form the importance ratio `exp(new_logprob - logprob)`; without it the update is off-policy and nothing says so. `
|
|
1112
|
+
`simulate(logprobs=True)` records, on every agent turn, the summed log-probability of the tokens the policy generated and how many there were (`step["logprob"]`, `step["n_tokens"]`, totals on the row). A trainer that updates on these rollouts later needs that number to form the importance ratio `exp(new_logprob - logprob)`; without it the update is off-policy and nothing says so. `wai.logprob_report(rows)` says how much was captured and whether reward tracks the policy's confidence, which on a fair judge it should not. Score the same rows under a reference model, put its summed logprob in `ref_logprob`, and `wai.mean_kl(rows)` gives the sampled KL per generated token, overall and per task; `wai.calibrate(rows, ref="ref_logprob")` writes it into each row's `calibration.mean_kl`. A turn the model cut at the token cap is marked `truncated`. Independently of `logprobs`, every agent step also records what its model call cost when the server reports it (`step["input_tokens"]`, `step["output_tokens"]`, summed into `row["usage"]`), which is what the platform counts per day.
|
|
1105
1113
|
|
|
1106
1114
|
```python
|
|
1107
|
-
|
|
1108
|
-
|
|
1115
|
+
wai.logprob_report(rows) # coverage, and whether reward tracks the policy's confidence
|
|
1116
|
+
wai.reference_logprobs(
|
|
1109
1117
|
data, "vllm:Qwen/Qwen3-4B@https://zeroproofai--zeroproof-serve-qwen3-4b.modal.run/v1"
|
|
1110
1118
|
) # ref_logprob on every row
|
|
1111
|
-
|
|
1112
|
-
|
|
1119
|
+
wai.mean_kl(rows, ref="ref_logprob") # sampled KL per generated token, overall and per task
|
|
1120
|
+
wai.staleness_report(
|
|
1113
1121
|
rows, base_model="Qwen/Qwen3-4B"
|
|
1114
1122
|
) # policy versions, stale rows, logprob coverage
|
|
1115
1123
|
```
|
|
1116
1124
|
|
|
1117
1125
|
`staleness_report` is the off-policy check (rlhf-book ch. 6): rows sampled by an older policy are usable only when they carry the sampler's version and its logprobs, so the importance ratio can be formed; rows whose `model_version` differs from `base_model` are `stale`.
|
|
1118
1126
|
|
|
1119
|
-
The default judge is not the policy. `
|
|
1127
|
+
The default judge is not the policy. `wai.grade` grades with hosted Phi-4 (`WHILEAI_JUDGE` overrides; any `vllm:`/`openai:` spec or a bare URL works), while rollouts come from hosted Qwen, because a judge grading its own model's writing prefers it. When the judge and the rows' `model_version` are the same model anyway, the grade report says so (`self_judged`, `warnings`).
|
|
1120
1128
|
|
|
1121
|
-
A judge is a reward model, so two things ride with every label. Provenance: rows graded by `
|
|
1129
|
+
A judge is a reward model, so two things ride with every label. Provenance: rows graded by `wai.grade` carry `judge_name`, `judge_status`, and `judge_meta` with the model, prompt hash, temperature, and `version` (`<model>@<prompt sha>`); a rubric edit is a new judge and the row says so. `run_judge(version=...)` records the same for your own judge. Accuracy: hand-label a sample into `gold_reward` and call `wai.judge_agreement(rows)` (or `scored.agreement()`) for agreement, Cohen's kappa, the confusion counts, and `pass_when_gold_fail`, the gold failures the judge passed. Those are the rows a training run learns the failure from, so that rate matters more than the headline agreement. Pass a second scoring run as `gold` to measure the judge against itself. Fifty gold rows is the floor; the report says so below it.
|
|
1122
1130
|
|
|
1123
|
-
Every row carries `schema_version` (`"1"`). A row is a projection of four objects in `whileai.simulations.schema`: `Task` (the situation), `Rollout` (one episode), `Judgment` (a scorer's verdict), `Marker` (a behavior measurement). `
|
|
1131
|
+
Every row carries `schema_version` (`"1"`). A row is a projection of four objects in `whileai.simulations.schema`: `Task` (the situation), `Rollout` (one episode), `Judgment` (a scorer's verdict), `Marker` (a behavior measurement). `wai.from_row(row)` splits a row into them and `wai.to_row(...)` flattens them back. The wire contract is `whileai/simulations/schemas/row-v1.json`. Rows written before the stamp are version 0 and load by shape, so older files still work.
|
|
1124
1132
|
|
|
1125
1133
|
## The recipe
|
|
1126
1134
|
|
|
@@ -1146,7 +1154,7 @@ Ordinary asks first, then the edges. On top of that, we embed the openers and ad
|
|
|
1146
1154
|
|
|
1147
1155
|
## Package layout
|
|
1148
1156
|
|
|
1149
|
-
The public surface is the package itself: `import whileai.simulations as
|
|
1157
|
+
The public surface is the package itself: `import whileai.simulations as wai`.
|
|
1150
1158
|
Internals are grouped by stage and may move between releases.
|
|
1151
1159
|
|
|
1152
1160
|
| folder | what lives there |
|