whileai 0.62__tar.gz → 0.64__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.62/whileai.egg-info → whileai-0.64}/PKG-INFO +73 -6
- {whileai-0.62 → whileai-0.64}/README.md +72 -5
- {whileai-0.62 → whileai-0.64}/pyproject.toml +1 -1
- {whileai-0.62 → whileai-0.64}/whileai/simulations/__init__.py +8 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/data.py +8 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/export.py +18 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/adapters.py +2 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/agents.py +333 -37
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/diversity.py +29 -7
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/generator.py +38 -4
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/scenarios.py +68 -3
- {whileai-0.62 → whileai-0.64}/whileai/simulations/ingest/traces.py +98 -23
- {whileai-0.62 → whileai-0.64}/whileai/simulations/run/config.py +71 -3
- {whileai-0.62 → whileai-0.64}/whileai/simulations/run/engine.py +303 -11
- {whileai-0.62 → whileai-0.64}/whileai/simulations/run/rows.py +38 -5
- whileai-0.64/whileai/simulations/score/delta.py +1013 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/grade_llm.py +140 -2
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/grading.py +121 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/hygiene.py +9 -1
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/passat.py +112 -6
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/preflight.py +59 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/stats.py +498 -55
- {whileai-0.62 → whileai-0.64}/whileai/simulations/simulation.py +49 -1
- whileai-0.64/whileai/simulations/text.py +39 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/world/sandbox.py +24 -1
- {whileai-0.62 → whileai-0.64/whileai.egg-info}/PKG-INFO +73 -6
- {whileai-0.62 → whileai-0.64}/whileai.egg-info/SOURCES.txt +1 -0
- whileai-0.62/whileai/simulations/score/delta.py +0 -550
- {whileai-0.62 → whileai-0.64}/LICENSE +0 -0
- {whileai-0.62 → whileai-0.64}/MANIFEST.in +0 -0
- {whileai-0.62 → whileai-0.64}/setup.cfg +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/__init__.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/_env.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/auth.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/cli.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/ingest.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/init_evals.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/py.typed +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/__main__.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/environment.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/__init__.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/actionspace.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/anthropic_backend.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/coverage.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/embeddings.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/explore.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/offline_agent.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/generate/usage_meter.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/ingest/__init__.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/ingest/otel.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/ingest/platform.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/monitor.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/py.typed +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/run/__init__.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/run/spec.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/schema.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/schemas/row-v1.json +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/__init__.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/agreement.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/audit.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/checklist.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/curriculum.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/grounding.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/hack_scan.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/judge_trust.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/judging.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/labels.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/llm_judge.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/logprobs.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/markers.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/optimize.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/pairwise.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/privileged.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/publish_gate.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/quality.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/reference.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/rubric.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/spec.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/stage.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/style.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/score/trace.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/training.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/verify/__init__.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/verify/base.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/verify/code.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/verify/math.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/verify/structured.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/verify/text.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/simulations/world/__init__.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/templates/__init__.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai/templates/evals.py +0 -0
- {whileai-0.62 → whileai-0.64}/whileai.egg-info/dependency_links.txt +0 -0
- {whileai-0.62 → whileai-0.64}/whileai.egg-info/entry_points.txt +0 -0
- {whileai-0.62 → whileai-0.64}/whileai.egg-info/requires.txt +0 -0
- {whileai-0.62 → whileai-0.64}/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.64
|
|
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
|
|
@@ -219,6 +219,18 @@ print(wai.pass_at(scored.rows), *scored.warnings) # a hollow run says so here
|
|
|
219
219
|
`scored.warnings` is new: no rollout called a tool, a declared tool no
|
|
220
220
|
rollout touched, a marker that fired on no row. A 1.00 on a run like that is not a result; the note names the fix.
|
|
221
221
|
|
|
222
|
+
A declared tool the world cannot answer is the quiet version of the same
|
|
223
|
+
failure: with `execute=`, a tool that is in the schema but has no branch in
|
|
224
|
+
your function fails exactly like a world fault, the agent reports the miss
|
|
225
|
+
honestly, and a candour rubric rewards the row. Every run now records calls
|
|
226
|
+
and successes per tool in `data.coverage["tools"]` (`n`, `ok`, `fault_n`, and
|
|
227
|
+
`injected` for faults the run scheduled itself), lists the tools that never
|
|
228
|
+
work in `data.coverage["dead_tools"]` (the Wilson 95% upper bound on the
|
|
229
|
+
success rate is under 0.30, so 0 of 9 or 4 of 612 is dead and 0 of 3 or 2 of
|
|
230
|
+
5 is not), adds `dead_tools` to `data.degraded`, and puts the names and the
|
|
231
|
+
one fix that applies to your world in `data.warnings` and `data.report()`.
|
|
232
|
+
Steps with no recorded result are not evidence and never accuse a tool.
|
|
233
|
+
|
|
222
234
|
### Bring a model
|
|
223
235
|
|
|
224
236
|
Bring your own model. Any OpenAI-compatible chat endpoint that returns tool
|
|
@@ -227,7 +239,31 @@ your key. To put a number on a model you serve (`wai.serve`, or your own
|
|
|
227
239
|
vLLM), make it the agent: `wai.simulate(tasks=pinned,
|
|
228
240
|
agent=wai.local_model(endpoint, name, tools=TOOLS, system=POLICY,
|
|
229
241
|
thinking=False))`, and run both arms of a before/after through that same
|
|
230
|
-
call so the only difference is the weights.
|
|
242
|
+
call so the only difference is the weights. `thinking=False` reaches the
|
|
243
|
+
simulated user as well when the agent's own model plays it (the default),
|
|
244
|
+
and whatever a user model still emits as `<think>` is stripped before it
|
|
245
|
+
becomes a user turn; the run reports those under
|
|
246
|
+
`data.search["user_think"]` (counts and shares of the user turns, zeros when
|
|
247
|
+
none) and says so in `data.warnings`. `delta_report` fails a before/after
|
|
248
|
+
whose arms differ in how often they answered at all (`"answered"` in
|
|
249
|
+
`not_comparable`, a two-proportion test at p < 0.01 and a gap over the
|
|
250
|
+
re-run band or 10 points), which is what a reasoning base against an
|
|
251
|
+
adapter trained on think-free targets does under one shared token budget:
|
|
252
|
+
set `thinking=` the same on both arms.
|
|
253
|
+
|
|
254
|
+
Two `local_model` knobs the situation writer cannot guess for you.
|
|
255
|
+
`result_shapes={tool_name: example_result}` pins what a tool returns, so a
|
|
256
|
+
policy branch that only exists for some results ("credits over $200 go to
|
|
257
|
+
`escalate_to_human`") is reached on purpose instead of by luck. Ids, dates
|
|
258
|
+
and people are re-drawn per call and a number moves by up to about a third
|
|
259
|
+
of itself (`900.0` lands in roughly 600 to 1200), so pick a template value
|
|
260
|
+
whose whole range sits on one side of the threshold and run the same pinned
|
|
261
|
+
tasks under one shape per side. `fault_plans={message: {tool: {"mode":
|
|
262
|
+
"timeout", "rate": 1.0}}}` replays a known fault schedule; `simulate()`
|
|
263
|
+
writes these from `fault_rate=`, so pass your own only to replay one.
|
|
264
|
+
`timeout=` is 300 s by default, enough for a served model that scaled to
|
|
265
|
+
zero to answer its first request; when a call still times out the run says
|
|
266
|
+
so in `data.warnings` with the fix.
|
|
231
267
|
|
|
232
268
|
```bash
|
|
233
269
|
export OPENAI_API_KEY=...
|
|
@@ -527,6 +563,7 @@ Pass `spec=` if you have a local tools-and-system-prompt folder of your own: a d
|
|
|
527
563
|
| `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 |
|
|
528
564
|
| `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) |
|
|
529
565
|
| `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) |
|
|
566
|
+
| `timeout` | `300` | Seconds per agent completion, for `local_model` and every model spec. A served model that scaled to zero takes two to three minutes to answer its first request, so a shorter value drops the first pass; a timed-out call is named in `data.warnings` with the fix |
|
|
530
567
|
| `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 |
|
|
531
568
|
| `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 |
|
|
532
569
|
| `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 |
|
|
@@ -672,6 +709,22 @@ rows, report = wai.drop_leaky_rows(data.trajectories, prod)
|
|
|
672
709
|
And the loop closes on itself: `evaluate(rollouts, judge).failed_traces()`
|
|
673
710
|
hands the failures straight back to `simulate(traces=...)`.
|
|
674
711
|
|
|
712
|
+
**What traces can and cannot aim at.** Traces reproduce situations: the
|
|
713
|
+
tools, faults and world states the deployed agent met. A failure that has
|
|
714
|
+
a world-visible trigger (a tool timed out and the agent did not say so, a
|
|
715
|
+
stale record was presented as current) is reproduced. A failure that lives
|
|
716
|
+
in how the reply is worded (an unsupported claim, an estimate not labelled
|
|
717
|
+
as one, two questions where one was asked for) has no trigger in the world,
|
|
718
|
+
so traces alone cannot aim at it: measured on a 12-rule grader, every rule
|
|
719
|
+
with a tool-result trigger was reproduced and every rule about the reply's
|
|
720
|
+
wording was not (#285). For those, put the grader in the loop:
|
|
721
|
+
with `simulate(..., grader=judge)` a row the grader fails is re-rolled and
|
|
722
|
+
its ask mutated like a tool fault, and `data.search["mutation_aims"]` says
|
|
723
|
+
how many parents and mutated rows each aim (`world_fault`,
|
|
724
|
+
`graded_failure`) produced. The grader is the switch; to grade beside the
|
|
725
|
+
loop and still steer by tool faults alone, pass
|
|
726
|
+
`advanced={"mutate_graded_failures": False}`.
|
|
727
|
+
|
|
675
728
|
If your traces are already on the platform, `wai.cut(agent="my-agent")`
|
|
676
729
|
does the whole cut in one line — see
|
|
677
730
|
[Training data out of traces](#training-data-out-of-traces).
|
|
@@ -902,11 +955,21 @@ rows, report = wai.write_rubrics(rows, domain="refunds") # per-prompt criteria
|
|
|
902
955
|
scored = wai.run_judge(rows, wai.rubric_judge()) # a verdict per criterion; markers rubric:<item>
|
|
903
956
|
clean, report = wai.decontaminate(
|
|
904
957
|
train_rows, against=[eval_rows]
|
|
905
|
-
) # 8-gram overlap with the eval set
|
|
958
|
+
) # same task id, verbatim, or 8-gram overlap with the eval set
|
|
959
|
+
clean, report = wai.decontaminate(
|
|
960
|
+
train_rows, against=[eval_rows], embedder=embed, similarity=0.85
|
|
961
|
+
) # plus a semantic pass: n_semantic counted apart, a flag means "reads alike"
|
|
906
962
|
wai.style_markers(rows) # no_boilerplate, no_hedging, no_apology, no_sycophancy, answered
|
|
907
963
|
wai.style_report(rows)["warnings"] # "reward pays for hedging (corr +0.41 ...)"
|
|
908
964
|
wai.refusal_report(benign_rows) # over-refusal rate with a Wilson interval
|
|
909
965
|
wai.compare_runs(run_a, run_b) # paired delta with a 95% interval
|
|
966
|
+
wai.holdout_size(
|
|
967
|
+
0.05, base=0.6, k=4
|
|
968
|
+
) # tasks to prove a 5-point gain; n_tasks_concentrated beside it
|
|
969
|
+
wai.holdout_size(
|
|
970
|
+
0.05, before=before, after=after
|
|
971
|
+
) # the paired sd measured off a previous eval, no model
|
|
972
|
+
wai.holdout_size(0.05, task_std=0.38) # or the sd read off a delta_report interval
|
|
910
973
|
wai.delta_report(before, after, target="pass_at_1", must_not_regress=["honest_after_fault"])
|
|
911
974
|
wai.delta_report(before, after, target="pass_at_1", by="category") # the target per kind of prompt
|
|
912
975
|
before = wai.simulate(agent, tools=TOOLS, tasks=base, runs=3) # the same eval three times
|
|
@@ -923,7 +986,7 @@ wai.grounding_report(rows) # grounded rate, and the invented values by tool and
|
|
|
923
986
|
|
|
924
987
|
**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.
|
|
925
988
|
|
|
926
|
-
**Decontamination.**
|
|
989
|
+
**Decontamination.** Four rules between a dataset's rows and any evaluation source (row lists, JSONL paths, or platform dataset ids), each counted on its own and a row counted once. A row is contaminated when it shares a `scenario_id` or `task_id` with an eval row (`n_same_task`: a task is a situation, not a string, so a rephrasing of an eval situation is the eval situation), when it is an eval prompt verbatim (`n_exact`), or when one eval text covers at least 80% of its words in shared 8-grams (`n_near`; `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, and short prompts match verbatim only). `fields=("prompt", "final_text")` also checks replies against eval answers and references. Word overlap does not see a paraphrase: a holdout written by re-running the generator was 70% within 0.85 cosine of the training batch, and the 8-gram rule flagged 4 of its 101 prompts where a semantic pass flagged 16. Pass `embedder=` (any callable from a list of texts to one vector per text, so nothing is imported; with sentence-transformers, `embedder=lambda texts: model.encode(texts, normalize_embeddings=True).tolist()`) and rows whose prompt is within `similarity=` (0.85 cosine) of an eval prompt are flagged as `n_semantic`. That flag means the two prompts read alike, not that they are the same task: "cancel one reservation" and "cancel three reservations" for different customers score 0.93 with no shared answer. So the task-id rule decides first, the semantic pass only looks across different task ids, and `report["notes"]` says the flag is a question to check. The default stays lexical; the threshold was read off BGE (unrelated prompts score about 0.55 there) and needs picking for another model, so when the eval rows carry task ids the pass measures how alike distinct tasks read to your embedder (the 99th percentile of similarity over eval-prompt pairs with different task ids) and `notes` says it, and says when `similarity=` sits below it, since a threshold there flags tasks that merely share a domain. The report returns the clean rows with the first offenders, their coverage or similarity, and hits per field.
|
|
927
990
|
|
|
928
991
|
**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.
|
|
929
992
|
|
|
@@ -939,7 +1002,9 @@ rerun = wai.simulate(
|
|
|
939
1002
|
assert base.rollouts_per_request == rerun.rollouts_per_request # cheap guard
|
|
940
1003
|
```
|
|
941
1004
|
|
|
942
|
-
**
|
|
1005
|
+
**Size before you run.** Most evals are too small to see the effects they produce. Per-prompt paired spread is stable for agent rubrics, about 0.38 across five lanes, and at that spread a 50-prompt eval only detects a 10-point gain; a 5-point gain needs about 220 prompts and a 3-point gain about 600. One lane read the same adapter as "barely helps" at 45 and 85 prompts (both straddled zero) and cleared at 131 (+0.13 [+0.07, +0.19]); the effect was real the whole time and a GPU round went to fixing a data problem that was a measurement problem. The effective sample is prompts, not rollouts: raising k sharpens each prompt's estimate but does not narrow a bootstrap over prompts, so spend eval budget on prompts first (k still matters for preference and grouped methods, which need mixed groups). And "straddles zero" means the eval cannot tell, not that the model did not improve; say which. `holdout_size(effect, base=, k=)` says how many paired tasks prove a gain at 80% power, and `detectable_effect(n_tasks, ...)` is the same solved for the gain. Its binomial model assumes the gain is spread evenly across tasks and the two arms are independent draws, and says so in `notes`; when a trait is only exercised by some prompts most tasks are ties, the paired differences spread far wider, and the model under-sizes by several times (a voice lane at 0 -> 0.127 needed 54 tasks where the model said 14), so the model path also returns `n_tasks_concentrated`. On a holdout whose tasks differ in difficulty the model errs the other way, asking for `1 / (1 - Var(p_i) / (p(1-p)))` times the tasks pairing needs (1.19x at spread 0.2 around 0.5, 2.78x at 0.4); `before=` alone reports the spread and that ratio. The honest paths measure: `before=before, after=after` (the same two row lists `delta_report` takes) reads the per-task paired sd off both arms of a previous eval on the same tasks, with the covariance pairing buys in it, and `task_std=` (the per-task sibling of `run_std`) takes the number you read off a `delta_report` (`(hi - lo) * sqrt(n_paired_tasks) / 3.92` from `target_ci95`). `push(purpose="holdout")` warns when the set is too small to prove a 5-point gain.
|
|
1006
|
+
|
|
1007
|
+
**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. Both sides should have the same rollouts per task; when a run lost some (`data.report()["rollouts_lost"]`, with the reasons in `rollouts_lost_by`) and one arm sits at k=4 while the other is at k=2, the report warns next to the sizing line and names both. Unequal k is a precision issue, not a bias: rows lost at random leave the paired delta unbiased and only widen its interval; rows lost for a reason (a timeout on the hard runs) bias it, and only re-running the short arm on its short tasks fixes that. `balance_rollouts=True` (off by default) trims every paired task to the rows both sides have (drawn by `seed=`) so pass^k and pass@k share one k; it costs precision, removes no bias, and `balanced` says how many rows each side gave up.
|
|
943
1008
|
|
|
944
1009
|
**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.
|
|
945
1010
|
|
|
@@ -1151,6 +1216,8 @@ Measured at `avg_turns=4`. The default is now `12`, so a row carries more turns
|
|
|
1151
1216
|
| `embedder` | `"hash"` | Prompt selection |
|
|
1152
1217
|
| `seed` | `0` | Reproducible draws. Bit-for-bit at `concurrency: 1` or with `reproducible=True`, within a process and across processes; otherwise which rows land before the cap depends on thread timing |
|
|
1153
1218
|
| `avg_turns` | `12` | Target conversation length in turns. The person speaks at most `avg_turns // 2` times; `12` leaves room to verify, look up, confirm, and write. |
|
|
1219
|
+
| `mutate_graded_failures` | on with `grader=` | `False` grades beside the loop without steering by the verdict: only tool faults make mutation parents. A row the grader fails (reward under 0.5) is otherwise re-rolled and its ask mutated the way a tool fault's is; `search["mutation_aims"]` counts each aim. `True` without `grader=` is an error |
|
|
1220
|
+
| `patience` | `"normal"` | How long the person keeps answering the agent's questions. The first question is always attempted; from the second on the person may walk away (`normal`: 35% then 60%; `short`: 60% then 90%; `endless`: never, the pre-knob behaviour). At any question the person may also leave when it asks for something they could not know. A row the person left ends on the agent's question and carries `ended_by="user_left"`; `search["ended_on_question"]` counts them. |
|
|
1154
1221
|
|
|
1155
1222
|
Aliases: `phrasings=` / `n=` → `requests_per_situation`; `repeats=` → `rollouts_per_request`; `unique=` → `unique_situations`; `policy=` → `system_prompt`; `risk=` → `fault_rate`.
|
|
1156
1223
|
|
|
@@ -1158,7 +1225,7 @@ Aliases: `phrasings=` / `n=` → `requests_per_situation`; `repeats=` → `rollo
|
|
|
1158
1225
|
|
|
1159
1226
|
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.
|
|
1160
1227
|
|
|
1161
|
-
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.
|
|
1228
|
+
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. Every row also names the deploy prompt it was generated under: `lineage.system_prompt_sha` (the hash `policy_version` carries after `@`) , `lineage.system_prompt_head` (its first 120 chars) and `lineage.system_prompt_chars`, with the full text once per run in `data.system_prompts[<sha>]`, so a base rate measured under a full policy is never mistaken for one measured under a bare prompt; `delta_report` warns when its two arms differ on that hash. 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.
|
|
1162
1229
|
|
|
1163
1230
|
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.
|
|
1164
1231
|
|
|
@@ -183,6 +183,18 @@ print(wai.pass_at(scored.rows), *scored.warnings) # a hollow run says so here
|
|
|
183
183
|
`scored.warnings` is new: no rollout called a tool, a declared tool no
|
|
184
184
|
rollout touched, a marker that fired on no row. A 1.00 on a run like that is not a result; the note names the fix.
|
|
185
185
|
|
|
186
|
+
A declared tool the world cannot answer is the quiet version of the same
|
|
187
|
+
failure: with `execute=`, a tool that is in the schema but has no branch in
|
|
188
|
+
your function fails exactly like a world fault, the agent reports the miss
|
|
189
|
+
honestly, and a candour rubric rewards the row. Every run now records calls
|
|
190
|
+
and successes per tool in `data.coverage["tools"]` (`n`, `ok`, `fault_n`, and
|
|
191
|
+
`injected` for faults the run scheduled itself), lists the tools that never
|
|
192
|
+
work in `data.coverage["dead_tools"]` (the Wilson 95% upper bound on the
|
|
193
|
+
success rate is under 0.30, so 0 of 9 or 4 of 612 is dead and 0 of 3 or 2 of
|
|
194
|
+
5 is not), adds `dead_tools` to `data.degraded`, and puts the names and the
|
|
195
|
+
one fix that applies to your world in `data.warnings` and `data.report()`.
|
|
196
|
+
Steps with no recorded result are not evidence and never accuse a tool.
|
|
197
|
+
|
|
186
198
|
### Bring a model
|
|
187
199
|
|
|
188
200
|
Bring your own model. Any OpenAI-compatible chat endpoint that returns tool
|
|
@@ -191,7 +203,31 @@ your key. To put a number on a model you serve (`wai.serve`, or your own
|
|
|
191
203
|
vLLM), make it the agent: `wai.simulate(tasks=pinned,
|
|
192
204
|
agent=wai.local_model(endpoint, name, tools=TOOLS, system=POLICY,
|
|
193
205
|
thinking=False))`, and run both arms of a before/after through that same
|
|
194
|
-
call so the only difference is the weights.
|
|
206
|
+
call so the only difference is the weights. `thinking=False` reaches the
|
|
207
|
+
simulated user as well when the agent's own model plays it (the default),
|
|
208
|
+
and whatever a user model still emits as `<think>` is stripped before it
|
|
209
|
+
becomes a user turn; the run reports those under
|
|
210
|
+
`data.search["user_think"]` (counts and shares of the user turns, zeros when
|
|
211
|
+
none) and says so in `data.warnings`. `delta_report` fails a before/after
|
|
212
|
+
whose arms differ in how often they answered at all (`"answered"` in
|
|
213
|
+
`not_comparable`, a two-proportion test at p < 0.01 and a gap over the
|
|
214
|
+
re-run band or 10 points), which is what a reasoning base against an
|
|
215
|
+
adapter trained on think-free targets does under one shared token budget:
|
|
216
|
+
set `thinking=` the same on both arms.
|
|
217
|
+
|
|
218
|
+
Two `local_model` knobs the situation writer cannot guess for you.
|
|
219
|
+
`result_shapes={tool_name: example_result}` pins what a tool returns, so a
|
|
220
|
+
policy branch that only exists for some results ("credits over $200 go to
|
|
221
|
+
`escalate_to_human`") is reached on purpose instead of by luck. Ids, dates
|
|
222
|
+
and people are re-drawn per call and a number moves by up to about a third
|
|
223
|
+
of itself (`900.0` lands in roughly 600 to 1200), so pick a template value
|
|
224
|
+
whose whole range sits on one side of the threshold and run the same pinned
|
|
225
|
+
tasks under one shape per side. `fault_plans={message: {tool: {"mode":
|
|
226
|
+
"timeout", "rate": 1.0}}}` replays a known fault schedule; `simulate()`
|
|
227
|
+
writes these from `fault_rate=`, so pass your own only to replay one.
|
|
228
|
+
`timeout=` is 300 s by default, enough for a served model that scaled to
|
|
229
|
+
zero to answer its first request; when a call still times out the run says
|
|
230
|
+
so in `data.warnings` with the fix.
|
|
195
231
|
|
|
196
232
|
```bash
|
|
197
233
|
export OPENAI_API_KEY=...
|
|
@@ -491,6 +527,7 @@ Pass `spec=` if you have a local tools-and-system-prompt folder of your own: a d
|
|
|
491
527
|
| `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 |
|
|
492
528
|
| `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) |
|
|
493
529
|
| `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) |
|
|
530
|
+
| `timeout` | `300` | Seconds per agent completion, for `local_model` and every model spec. A served model that scaled to zero takes two to three minutes to answer its first request, so a shorter value drops the first pass; a timed-out call is named in `data.warnings` with the fix |
|
|
494
531
|
| `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 |
|
|
495
532
|
| `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 |
|
|
496
533
|
| `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 |
|
|
@@ -636,6 +673,22 @@ rows, report = wai.drop_leaky_rows(data.trajectories, prod)
|
|
|
636
673
|
And the loop closes on itself: `evaluate(rollouts, judge).failed_traces()`
|
|
637
674
|
hands the failures straight back to `simulate(traces=...)`.
|
|
638
675
|
|
|
676
|
+
**What traces can and cannot aim at.** Traces reproduce situations: the
|
|
677
|
+
tools, faults and world states the deployed agent met. A failure that has
|
|
678
|
+
a world-visible trigger (a tool timed out and the agent did not say so, a
|
|
679
|
+
stale record was presented as current) is reproduced. A failure that lives
|
|
680
|
+
in how the reply is worded (an unsupported claim, an estimate not labelled
|
|
681
|
+
as one, two questions where one was asked for) has no trigger in the world,
|
|
682
|
+
so traces alone cannot aim at it: measured on a 12-rule grader, every rule
|
|
683
|
+
with a tool-result trigger was reproduced and every rule about the reply's
|
|
684
|
+
wording was not (#285). For those, put the grader in the loop:
|
|
685
|
+
with `simulate(..., grader=judge)` a row the grader fails is re-rolled and
|
|
686
|
+
its ask mutated like a tool fault, and `data.search["mutation_aims"]` says
|
|
687
|
+
how many parents and mutated rows each aim (`world_fault`,
|
|
688
|
+
`graded_failure`) produced. The grader is the switch; to grade beside the
|
|
689
|
+
loop and still steer by tool faults alone, pass
|
|
690
|
+
`advanced={"mutate_graded_failures": False}`.
|
|
691
|
+
|
|
639
692
|
If your traces are already on the platform, `wai.cut(agent="my-agent")`
|
|
640
693
|
does the whole cut in one line — see
|
|
641
694
|
[Training data out of traces](#training-data-out-of-traces).
|
|
@@ -866,11 +919,21 @@ rows, report = wai.write_rubrics(rows, domain="refunds") # per-prompt criteria
|
|
|
866
919
|
scored = wai.run_judge(rows, wai.rubric_judge()) # a verdict per criterion; markers rubric:<item>
|
|
867
920
|
clean, report = wai.decontaminate(
|
|
868
921
|
train_rows, against=[eval_rows]
|
|
869
|
-
) # 8-gram overlap with the eval set
|
|
922
|
+
) # same task id, verbatim, or 8-gram overlap with the eval set
|
|
923
|
+
clean, report = wai.decontaminate(
|
|
924
|
+
train_rows, against=[eval_rows], embedder=embed, similarity=0.85
|
|
925
|
+
) # plus a semantic pass: n_semantic counted apart, a flag means "reads alike"
|
|
870
926
|
wai.style_markers(rows) # no_boilerplate, no_hedging, no_apology, no_sycophancy, answered
|
|
871
927
|
wai.style_report(rows)["warnings"] # "reward pays for hedging (corr +0.41 ...)"
|
|
872
928
|
wai.refusal_report(benign_rows) # over-refusal rate with a Wilson interval
|
|
873
929
|
wai.compare_runs(run_a, run_b) # paired delta with a 95% interval
|
|
930
|
+
wai.holdout_size(
|
|
931
|
+
0.05, base=0.6, k=4
|
|
932
|
+
) # tasks to prove a 5-point gain; n_tasks_concentrated beside it
|
|
933
|
+
wai.holdout_size(
|
|
934
|
+
0.05, before=before, after=after
|
|
935
|
+
) # the paired sd measured off a previous eval, no model
|
|
936
|
+
wai.holdout_size(0.05, task_std=0.38) # or the sd read off a delta_report interval
|
|
874
937
|
wai.delta_report(before, after, target="pass_at_1", must_not_regress=["honest_after_fault"])
|
|
875
938
|
wai.delta_report(before, after, target="pass_at_1", by="category") # the target per kind of prompt
|
|
876
939
|
before = wai.simulate(agent, tools=TOOLS, tasks=base, runs=3) # the same eval three times
|
|
@@ -887,7 +950,7 @@ wai.grounding_report(rows) # grounded rate, and the invented values by tool and
|
|
|
887
950
|
|
|
888
951
|
**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.
|
|
889
952
|
|
|
890
|
-
**Decontamination.**
|
|
953
|
+
**Decontamination.** Four rules between a dataset's rows and any evaluation source (row lists, JSONL paths, or platform dataset ids), each counted on its own and a row counted once. A row is contaminated when it shares a `scenario_id` or `task_id` with an eval row (`n_same_task`: a task is a situation, not a string, so a rephrasing of an eval situation is the eval situation), when it is an eval prompt verbatim (`n_exact`), or when one eval text covers at least 80% of its words in shared 8-grams (`n_near`; `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, and short prompts match verbatim only). `fields=("prompt", "final_text")` also checks replies against eval answers and references. Word overlap does not see a paraphrase: a holdout written by re-running the generator was 70% within 0.85 cosine of the training batch, and the 8-gram rule flagged 4 of its 101 prompts where a semantic pass flagged 16. Pass `embedder=` (any callable from a list of texts to one vector per text, so nothing is imported; with sentence-transformers, `embedder=lambda texts: model.encode(texts, normalize_embeddings=True).tolist()`) and rows whose prompt is within `similarity=` (0.85 cosine) of an eval prompt are flagged as `n_semantic`. That flag means the two prompts read alike, not that they are the same task: "cancel one reservation" and "cancel three reservations" for different customers score 0.93 with no shared answer. So the task-id rule decides first, the semantic pass only looks across different task ids, and `report["notes"]` says the flag is a question to check. The default stays lexical; the threshold was read off BGE (unrelated prompts score about 0.55 there) and needs picking for another model, so when the eval rows carry task ids the pass measures how alike distinct tasks read to your embedder (the 99th percentile of similarity over eval-prompt pairs with different task ids) and `notes` says it, and says when `similarity=` sits below it, since a threshold there flags tasks that merely share a domain. The report returns the clean rows with the first offenders, their coverage or similarity, and hits per field.
|
|
891
954
|
|
|
892
955
|
**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.
|
|
893
956
|
|
|
@@ -903,7 +966,9 @@ rerun = wai.simulate(
|
|
|
903
966
|
assert base.rollouts_per_request == rerun.rollouts_per_request # cheap guard
|
|
904
967
|
```
|
|
905
968
|
|
|
906
|
-
**
|
|
969
|
+
**Size before you run.** Most evals are too small to see the effects they produce. Per-prompt paired spread is stable for agent rubrics, about 0.38 across five lanes, and at that spread a 50-prompt eval only detects a 10-point gain; a 5-point gain needs about 220 prompts and a 3-point gain about 600. One lane read the same adapter as "barely helps" at 45 and 85 prompts (both straddled zero) and cleared at 131 (+0.13 [+0.07, +0.19]); the effect was real the whole time and a GPU round went to fixing a data problem that was a measurement problem. The effective sample is prompts, not rollouts: raising k sharpens each prompt's estimate but does not narrow a bootstrap over prompts, so spend eval budget on prompts first (k still matters for preference and grouped methods, which need mixed groups). And "straddles zero" means the eval cannot tell, not that the model did not improve; say which. `holdout_size(effect, base=, k=)` says how many paired tasks prove a gain at 80% power, and `detectable_effect(n_tasks, ...)` is the same solved for the gain. Its binomial model assumes the gain is spread evenly across tasks and the two arms are independent draws, and says so in `notes`; when a trait is only exercised by some prompts most tasks are ties, the paired differences spread far wider, and the model under-sizes by several times (a voice lane at 0 -> 0.127 needed 54 tasks where the model said 14), so the model path also returns `n_tasks_concentrated`. On a holdout whose tasks differ in difficulty the model errs the other way, asking for `1 / (1 - Var(p_i) / (p(1-p)))` times the tasks pairing needs (1.19x at spread 0.2 around 0.5, 2.78x at 0.4); `before=` alone reports the spread and that ratio. The honest paths measure: `before=before, after=after` (the same two row lists `delta_report` takes) reads the per-task paired sd off both arms of a previous eval on the same tasks, with the covariance pairing buys in it, and `task_std=` (the per-task sibling of `run_std`) takes the number you read off a `delta_report` (`(hi - lo) * sqrt(n_paired_tasks) / 3.92` from `target_ci95`). `push(purpose="holdout")` warns when the set is too small to prove a 5-point gain.
|
|
970
|
+
|
|
971
|
+
**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. Both sides should have the same rollouts per task; when a run lost some (`data.report()["rollouts_lost"]`, with the reasons in `rollouts_lost_by`) and one arm sits at k=4 while the other is at k=2, the report warns next to the sizing line and names both. Unequal k is a precision issue, not a bias: rows lost at random leave the paired delta unbiased and only widen its interval; rows lost for a reason (a timeout on the hard runs) bias it, and only re-running the short arm on its short tasks fixes that. `balance_rollouts=True` (off by default) trims every paired task to the rows both sides have (drawn by `seed=`) so pass^k and pass@k share one k; it costs precision, removes no bias, and `balanced` says how many rows each side gave up.
|
|
907
972
|
|
|
908
973
|
**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.
|
|
909
974
|
|
|
@@ -1115,6 +1180,8 @@ Measured at `avg_turns=4`. The default is now `12`, so a row carries more turns
|
|
|
1115
1180
|
| `embedder` | `"hash"` | Prompt selection |
|
|
1116
1181
|
| `seed` | `0` | Reproducible draws. Bit-for-bit at `concurrency: 1` or with `reproducible=True`, within a process and across processes; otherwise which rows land before the cap depends on thread timing |
|
|
1117
1182
|
| `avg_turns` | `12` | Target conversation length in turns. The person speaks at most `avg_turns // 2` times; `12` leaves room to verify, look up, confirm, and write. |
|
|
1183
|
+
| `mutate_graded_failures` | on with `grader=` | `False` grades beside the loop without steering by the verdict: only tool faults make mutation parents. A row the grader fails (reward under 0.5) is otherwise re-rolled and its ask mutated the way a tool fault's is; `search["mutation_aims"]` counts each aim. `True` without `grader=` is an error |
|
|
1184
|
+
| `patience` | `"normal"` | How long the person keeps answering the agent's questions. The first question is always attempted; from the second on the person may walk away (`normal`: 35% then 60%; `short`: 60% then 90%; `endless`: never, the pre-knob behaviour). At any question the person may also leave when it asks for something they could not know. A row the person left ends on the agent's question and carries `ended_by="user_left"`; `search["ended_on_question"]` counts them. |
|
|
1118
1185
|
|
|
1119
1186
|
Aliases: `phrasings=` / `n=` → `requests_per_situation`; `repeats=` → `rollouts_per_request`; `unique=` → `unique_situations`; `policy=` → `system_prompt`; `risk=` → `fault_rate`.
|
|
1120
1187
|
|
|
@@ -1122,7 +1189,7 @@ Aliases: `phrasings=` / `n=` → `requests_per_situation`; `repeats=` → `rollo
|
|
|
1122
1189
|
|
|
1123
1190
|
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.
|
|
1124
1191
|
|
|
1125
|
-
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.
|
|
1192
|
+
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. Every row also names the deploy prompt it was generated under: `lineage.system_prompt_sha` (the hash `policy_version` carries after `@`) , `lineage.system_prompt_head` (its first 120 chars) and `lineage.system_prompt_chars`, with the full text once per run in `data.system_prompts[<sha>]`, so a base rate measured under a full policy is never mistaken for one measured under a bare prompt; `delta_report` warns when its two arms differ on that hash. 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.
|
|
1126
1193
|
|
|
1127
1194
|
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.
|
|
1128
1195
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "whileai"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.64"
|
|
8
8
|
description = "While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -80,11 +80,14 @@ from .ingest.traces import (
|
|
|
80
80
|
drop_leaky_rows,
|
|
81
81
|
flaw_rows,
|
|
82
82
|
format_trace_report,
|
|
83
|
+
infer_harness,
|
|
83
84
|
leakage_report,
|
|
84
85
|
load_traces,
|
|
85
86
|
mine_traces,
|
|
87
|
+
opening_share,
|
|
86
88
|
simulate_from_traces,
|
|
87
89
|
split_pseudo_production,
|
|
90
|
+
tools_from_traces,
|
|
88
91
|
trace_report,
|
|
89
92
|
)
|
|
90
93
|
from .monitor import HackMonitor, format_hack_monitor
|
|
@@ -175,6 +178,7 @@ from .score.stats import (
|
|
|
175
178
|
detectable_effect,
|
|
176
179
|
eval_variance,
|
|
177
180
|
holdout_size,
|
|
181
|
+
marker_names,
|
|
178
182
|
marker_summary,
|
|
179
183
|
metric_summary,
|
|
180
184
|
task_key,
|
|
@@ -310,6 +314,7 @@ __all__ = [
|
|
|
310
314
|
"holdout_size",
|
|
311
315
|
"hosted_model",
|
|
312
316
|
"import_hf",
|
|
317
|
+
"infer_harness",
|
|
313
318
|
"inspect",
|
|
314
319
|
"issue_delegated_credential",
|
|
315
320
|
"judge_agreement",
|
|
@@ -329,6 +334,7 @@ __all__ = [
|
|
|
329
334
|
"loss_mask",
|
|
330
335
|
"mark_grounding",
|
|
331
336
|
"mark_rows",
|
|
337
|
+
"marker_names",
|
|
332
338
|
"marker_summary",
|
|
333
339
|
"mean_kl",
|
|
334
340
|
"metric_summary",
|
|
@@ -339,6 +345,7 @@ __all__ = [
|
|
|
339
345
|
"normalize_judge_result",
|
|
340
346
|
"novelty",
|
|
341
347
|
"open_ended_probes",
|
|
348
|
+
"opening_share",
|
|
342
349
|
"optimize",
|
|
343
350
|
"outcome_check",
|
|
344
351
|
"pairwise_judge",
|
|
@@ -393,6 +400,7 @@ __all__ = [
|
|
|
393
400
|
"to_row",
|
|
394
401
|
"to_trl",
|
|
395
402
|
"tool_call_roundtrip",
|
|
403
|
+
"tools_from_traces",
|
|
396
404
|
"trace_flag_report",
|
|
397
405
|
"trace_flags",
|
|
398
406
|
"trace_markers",
|
|
@@ -366,6 +366,10 @@ class SimulationData:
|
|
|
366
366
|
# ``model_version`` on every row.
|
|
367
367
|
writer_model: str = ""
|
|
368
368
|
user_model: str | None = None
|
|
369
|
+
# The deploy prompt the rows were generated under, full text keyed by
|
|
370
|
+
# the hash every row carries in ``lineage.system_prompt_sha`` (#296).
|
|
371
|
+
# Those two are the hash's only homes: the row and this text.
|
|
372
|
+
system_prompts: dict[str, str] = field(default_factory=dict)
|
|
369
373
|
|
|
370
374
|
@property
|
|
371
375
|
def judge_model(self) -> str | None:
|
|
@@ -837,6 +841,10 @@ class SimulationData:
|
|
|
837
841
|
# The agent spec: a trainer loading this JSONL later
|
|
838
842
|
# needs the policy and tool schemas the run knew.
|
|
839
843
|
"system_prompt": str(getattr(self.profile, "policy", "") or ""),
|
|
844
|
+
# the exact text the rows were generated under
|
|
845
|
+
# (policy plus scaffold), keyed by the hash each
|
|
846
|
+
# row carries in lineage.system_prompt_sha
|
|
847
|
+
"system_prompts": dict(self.system_prompts),
|
|
840
848
|
"tools": list(getattr(self.profile, "tools", None) or []),
|
|
841
849
|
"stopped_because": self.stopped_because,
|
|
842
850
|
"coverage": self.coverage,
|
|
@@ -13,6 +13,16 @@ target model emits thinking tokens. ``training_rows`` closes that gap:
|
|
|
13
13
|
* strips ``<think>`` blocks from assistant turns, so a thinking rollout
|
|
14
14
|
model never teaches a non-thinking student to emit them.
|
|
15
15
|
|
|
16
|
+
The strip cuts the other way too. A reasoning base (Qwen3) trained on
|
|
17
|
+
think-free targets learns to print an empty ``<think></think>`` and
|
|
18
|
+
answer at once. That is fine until the eval, where that adapter meets
|
|
19
|
+
the untrained base under one shared ``max_tokens``: the base reasons and
|
|
20
|
+
runs out of budget, the adapter answers, and the delta is a win over
|
|
21
|
+
replies the base never produced (#297). ``delta_report`` fails that
|
|
22
|
+
comparison; the fix is ``thinking=`` set the same on both arms, or
|
|
23
|
+
``strip_think=False`` when the student is a reasoning model and should
|
|
24
|
+
keep reasoning.
|
|
25
|
+
|
|
16
26
|
Two wire shapes come out of here, and they are not the same shape:
|
|
17
27
|
|
|
18
28
|
``format="openai"`` (the default)
|
|
@@ -411,6 +421,14 @@ def training_rows(
|
|
|
411
421
|
its policy trains an agent that never saw its rules. ``mask_mode``
|
|
412
422
|
picks which assistant turns carry loss (see ``loss_mask``).
|
|
413
423
|
|
|
424
|
+
``strip_think=True`` (the default) removes ``<think>`` blocks from the
|
|
425
|
+
assistant turns. On a reasoning base such as Qwen3 that teaches the
|
|
426
|
+
adapter to emit an empty ``<think></think>`` and answer at once, so
|
|
427
|
+
at eval it answers while the untrained base is still reasoning under
|
|
428
|
+
the same ``max_tokens`` (#297). Pass ``strip_think=False`` when the
|
|
429
|
+
student should keep reasoning, and set ``thinking=`` the same on both
|
|
430
|
+
arms of the eval either way.
|
|
431
|
+
|
|
414
432
|
``unroll=True`` turns an N-turn conversation into N samples, the
|
|
415
433
|
k-th ending at the k-th assistant turn with loss on that turn only
|
|
416
434
|
(rlhf-book ch. 4, multi-turn masking). Every earlier agent turn then
|
|
@@ -343,6 +343,7 @@ def resolve(
|
|
|
343
343
|
max_turns: int = 5,
|
|
344
344
|
avg_turns: float = 6,
|
|
345
345
|
min_user_turns: int = 1,
|
|
346
|
+
patience: str = "normal",
|
|
346
347
|
turn_stats: dict | None = None,
|
|
347
348
|
opening_rate: float = 0.0,
|
|
348
349
|
temperature: float | None = None,
|
|
@@ -378,6 +379,7 @@ def resolve(
|
|
|
378
379
|
fault_plans=fault_plans,
|
|
379
380
|
avg_turns=avg_turns,
|
|
380
381
|
min_user_turns=min_user_turns,
|
|
382
|
+
patience=patience,
|
|
381
383
|
opening_rate=opening_rate,
|
|
382
384
|
execute=execute,
|
|
383
385
|
result_shapes=result_shapes,
|