whileai 0.62__tar.gz → 0.63__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.63}/PKG-INFO +44 -4
- {whileai-0.62 → whileai-0.63}/README.md +43 -3
- {whileai-0.62 → whileai-0.63}/pyproject.toml +1 -1
- {whileai-0.62 → whileai-0.63}/whileai/simulations/__init__.py +8 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/export.py +18 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/agents.py +149 -27
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/diversity.py +29 -7
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/generator.py +29 -3
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/scenarios.py +68 -3
- {whileai-0.62 → whileai-0.63}/whileai/simulations/ingest/traces.py +32 -18
- {whileai-0.62 → whileai-0.63}/whileai/simulations/run/config.py +47 -3
- {whileai-0.62 → whileai-0.63}/whileai/simulations/run/engine.py +147 -1
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/delta.py +113 -1
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/grade_llm.py +91 -2
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/grading.py +121 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/hygiene.py +9 -1
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/passat.py +51 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/preflight.py +59 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/stats.py +204 -14
- {whileai-0.62 → whileai-0.63}/whileai/simulations/simulation.py +8 -0
- whileai-0.63/whileai/simulations/text.py +39 -0
- {whileai-0.62 → whileai-0.63/whileai.egg-info}/PKG-INFO +44 -4
- {whileai-0.62 → whileai-0.63}/whileai.egg-info/SOURCES.txt +1 -0
- {whileai-0.62 → whileai-0.63}/LICENSE +0 -0
- {whileai-0.62 → whileai-0.63}/MANIFEST.in +0 -0
- {whileai-0.62 → whileai-0.63}/setup.cfg +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/__init__.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/_env.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/auth.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/cli.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/ingest.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/init_evals.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/py.typed +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/__main__.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/data.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/environment.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/__init__.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/actionspace.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/adapters.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/anthropic_backend.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/coverage.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/embeddings.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/explore.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/offline_agent.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/generate/usage_meter.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/ingest/__init__.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/ingest/otel.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/ingest/platform.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/monitor.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/py.typed +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/run/__init__.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/run/rows.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/run/spec.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/schema.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/schemas/row-v1.json +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/__init__.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/agreement.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/audit.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/checklist.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/curriculum.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/grounding.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/hack_scan.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/judge_trust.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/judging.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/labels.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/llm_judge.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/logprobs.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/markers.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/optimize.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/pairwise.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/privileged.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/publish_gate.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/quality.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/reference.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/rubric.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/spec.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/stage.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/style.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/score/trace.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/training.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/verify/__init__.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/verify/base.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/verify/code.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/verify/math.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/verify/structured.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/verify/text.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/world/__init__.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/simulations/world/sandbox.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/templates/__init__.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai/templates/evals.py +0 -0
- {whileai-0.62 → whileai-0.63}/whileai.egg-info/dependency_links.txt +0 -0
- {whileai-0.62 → whileai-0.63}/whileai.egg-info/entry_points.txt +0 -0
- {whileai-0.62 → whileai-0.63}/whileai.egg-info/requires.txt +0 -0
- {whileai-0.62 → whileai-0.63}/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.63
|
|
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 |
|
|
@@ -902,7 +939,10 @@ rows, report = wai.write_rubrics(rows, domain="refunds") # per-prompt criteria
|
|
|
902
939
|
scored = wai.run_judge(rows, wai.rubric_judge()) # a verdict per criterion; markers rubric:<item>
|
|
903
940
|
clean, report = wai.decontaminate(
|
|
904
941
|
train_rows, against=[eval_rows]
|
|
905
|
-
) # 8-gram overlap with the eval set
|
|
942
|
+
) # same task id, verbatim, or 8-gram overlap with the eval set
|
|
943
|
+
clean, report = wai.decontaminate(
|
|
944
|
+
train_rows, against=[eval_rows], embedder=embed, similarity=0.85
|
|
945
|
+
) # plus a semantic pass: n_semantic counted apart, a flag means "reads alike"
|
|
906
946
|
wai.style_markers(rows) # no_boilerplate, no_hedging, no_apology, no_sycophancy, answered
|
|
907
947
|
wai.style_report(rows)["warnings"] # "reward pays for hedging (corr +0.41 ...)"
|
|
908
948
|
wai.refusal_report(benign_rows) # over-refusal rate with a Wilson interval
|
|
@@ -923,7 +963,7 @@ wai.grounding_report(rows) # grounded rate, and the invented values by tool and
|
|
|
923
963
|
|
|
924
964
|
**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
965
|
|
|
926
|
-
**Decontamination.**
|
|
966
|
+
**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
967
|
|
|
928
968
|
**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
969
|
|
|
@@ -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 |
|
|
@@ -866,7 +903,10 @@ rows, report = wai.write_rubrics(rows, domain="refunds") # per-prompt criteria
|
|
|
866
903
|
scored = wai.run_judge(rows, wai.rubric_judge()) # a verdict per criterion; markers rubric:<item>
|
|
867
904
|
clean, report = wai.decontaminate(
|
|
868
905
|
train_rows, against=[eval_rows]
|
|
869
|
-
) # 8-gram overlap with the eval set
|
|
906
|
+
) # same task id, verbatim, or 8-gram overlap with the eval set
|
|
907
|
+
clean, report = wai.decontaminate(
|
|
908
|
+
train_rows, against=[eval_rows], embedder=embed, similarity=0.85
|
|
909
|
+
) # plus a semantic pass: n_semantic counted apart, a flag means "reads alike"
|
|
870
910
|
wai.style_markers(rows) # no_boilerplate, no_hedging, no_apology, no_sycophancy, answered
|
|
871
911
|
wai.style_report(rows)["warnings"] # "reward pays for hedging (corr +0.41 ...)"
|
|
872
912
|
wai.refusal_report(benign_rows) # over-refusal rate with a Wilson interval
|
|
@@ -887,7 +927,7 @@ wai.grounding_report(rows) # grounded rate, and the invented values by tool and
|
|
|
887
927
|
|
|
888
928
|
**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
929
|
|
|
890
|
-
**Decontamination.**
|
|
930
|
+
**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
931
|
|
|
892
932
|
**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
933
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "whileai"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.63"
|
|
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",
|
|
@@ -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
|
|
@@ -17,6 +17,7 @@ from urllib.parse import urlparse
|
|
|
17
17
|
from whileai._env import getenv
|
|
18
18
|
from whileai.auth import SIGN_IN_URL
|
|
19
19
|
|
|
20
|
+
from ..text import split_reasoning
|
|
20
21
|
from ..world.sandbox import MockEnvironment
|
|
21
22
|
from .anthropic_backend import ANTHROPIC_BASE_URL, is_anthropic_url
|
|
22
23
|
from .anthropic_backend import DEFAULT_MODEL as ANTHROPIC_DEFAULT_MODEL
|
|
@@ -780,8 +781,13 @@ def _strip_tool_markup(text: str) -> str:
|
|
|
780
781
|
return cleaned.strip()
|
|
781
782
|
|
|
782
783
|
|
|
783
|
-
|
|
784
|
-
|
|
784
|
+
#: A simulated-user turn that carried reasoning and left fewer spoken
|
|
785
|
+
#: characters than this was reasoning with no spoken line: the writer
|
|
786
|
+
#: thought and never typed, so the turn is retried, never emitted as a
|
|
787
|
+
#: fragment. The floor applies only to turns that carried reasoning; a
|
|
788
|
+
#: bare ``yes`` or ``order 4821`` with no ``<think>`` is a real user turn
|
|
789
|
+
#: and passes untouched (#284).
|
|
790
|
+
_MIN_SPOKEN = 25
|
|
785
791
|
|
|
786
792
|
|
|
787
793
|
def _strip_think(text: str) -> str:
|
|
@@ -789,8 +795,23 @@ def _strip_think(text: str) -> str:
|
|
|
789
795
|
an unclosed ``<think>`` (the token cap landed inside it) goes to the
|
|
790
796
|
end. What is left is the reply, which is what a grader, a marker and
|
|
791
797
|
the next turn's history should see (#264)."""
|
|
792
|
-
|
|
793
|
-
|
|
798
|
+
return split_reasoning(text)[0]
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _note_user_turn(turn_stats: dict | None, closed: int, unclosed: bool) -> None:
|
|
802
|
+
"""One simulated-user reply came back: count the turn, and count it
|
|
803
|
+
again as stripped when it carried reasoning, and as unclosed when the
|
|
804
|
+
reasoning was cut off. The run reports counts and shares per arm
|
|
805
|
+
(#284: 18 unclosed on one arm, 0 on the other)."""
|
|
806
|
+
if not turn_stats:
|
|
807
|
+
return
|
|
808
|
+
lock = turn_stats.get("lock")
|
|
809
|
+
with lock if lock is not None else contextlib.nullcontext():
|
|
810
|
+
turn_stats["user_turns"] = turn_stats.get("user_turns", 0) + 1
|
|
811
|
+
if closed or unclosed:
|
|
812
|
+
turn_stats["user_think_stripped"] = turn_stats.get("user_think_stripped", 0) + 1
|
|
813
|
+
if unclosed:
|
|
814
|
+
turn_stats["user_think_unclosed"] = turn_stats.get("user_think_unclosed", 0) + 1
|
|
794
815
|
|
|
795
816
|
|
|
796
817
|
def _spoken_text(reply: dict) -> str:
|
|
@@ -897,9 +918,21 @@ def _want_followup(
|
|
|
897
918
|
|
|
898
919
|
A question or refusal earns an answer while the turn budget has
|
|
899
920
|
room: the depth cap is ``budget // 2`` user turns (avg_turns=12
|
|
900
|
-
allows 6, avg_turns=4 allows 2).
|
|
901
|
-
|
|
902
|
-
budget
|
|
921
|
+
allows 6, avg_turns=4 allows 2). Otherwise the thread continues with
|
|
922
|
+
probability ``1 - 1/cap``, which makes the mean depth track the cap.
|
|
923
|
+
A budget under 4 still ends on the agent.
|
|
924
|
+
|
|
925
|
+
Before this, any reply that was not a question, a refusal, or a
|
|
926
|
+
recognised success ended the thread, and a completed action only
|
|
927
|
+
continued on a fixed coin flip. "Your reservation has been cancelled"
|
|
928
|
+
matches none of those, so threads died at one user turn and the mean
|
|
929
|
+
sat near 1.5 whatever ``avg_turns`` said: measured 1.54 at avg_turns=6
|
|
930
|
+
and 1.52 at avg_turns=10 on the same spec. That silently caps every
|
|
931
|
+
behaviour that needs three turns to happen at all. Confirm-before-
|
|
932
|
+
acting is the clearest case: the user asks, the agent names the action
|
|
933
|
+
and asks, the user says yes, the agent acts. At 1.5 user turns most
|
|
934
|
+
rollouts never reach the write, so the rule is never exercised and the
|
|
935
|
+
training set cannot demonstrate it.
|
|
903
936
|
"""
|
|
904
937
|
cap = max(2, int(budget) // 2)
|
|
905
938
|
if int(user_turns) >= cap:
|
|
@@ -909,10 +942,15 @@ def _want_followup(
|
|
|
909
942
|
return True
|
|
910
943
|
if _AGENT_REFUSAL.search(text):
|
|
911
944
|
return True
|
|
912
|
-
if int(budget)
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
945
|
+
if int(budget) < 4:
|
|
946
|
+
# A short thread still ends on the agent: with room for one user line
|
|
947
|
+
# there is nothing a second one could be for.
|
|
948
|
+
return False
|
|
949
|
+
# Geometric with p = 1 - 1/cap: a thread of cap turns in expectation,
|
|
950
|
+
# deterministic in the message and turn so a seeded run reproduces.
|
|
951
|
+
digest = hashlib.sha256(f"{message}:{turn_i}:react".encode()).hexdigest()
|
|
952
|
+
draw = int(digest[:8], 16) / float(1 << 32)
|
|
953
|
+
return draw < (1.0 - 1.0 / float(cap))
|
|
916
954
|
|
|
917
955
|
|
|
918
956
|
# Follow-up user turns only. Opener temperature lives on the writer (0.45–1.05).
|
|
@@ -1052,11 +1090,6 @@ _CONFIRM_ONLY = re.compile(
|
|
|
1052
1090
|
re.I,
|
|
1053
1091
|
)
|
|
1054
1092
|
_AGENT_QUESTION = re.compile(r"\?\s*$|\b(which|what|who|where|can you|could you)\b", re.I)
|
|
1055
|
-
_AGENT_SUCCESS = re.compile(
|
|
1056
|
-
r"\b(done|created|opened|fixed|all set|i (have|'ve|just)|"
|
|
1057
|
-
r"successfully|completed)\b",
|
|
1058
|
-
re.I,
|
|
1059
|
-
)
|
|
1060
1093
|
_AGENT_REFUSAL = re.compile(
|
|
1061
1094
|
r"\b(can't|cannot|won't|unable|not allowed|against (the )?(policy|rules?))\b",
|
|
1062
1095
|
re.I,
|
|
@@ -1220,7 +1253,16 @@ def _user_followup(
|
|
|
1220
1253
|
tools: list | None = None,
|
|
1221
1254
|
force: bool = False,
|
|
1222
1255
|
persona_tags: dict | None = None,
|
|
1256
|
+
extra: Mapping[str, Any] | None = None,
|
|
1257
|
+
turn_stats: dict | None = None,
|
|
1223
1258
|
) -> str:
|
|
1259
|
+
"""The simulated user's next line, or ``""`` when the writer produced
|
|
1260
|
+
none worth keeping. ``extra`` carries the agent's request fields
|
|
1261
|
+
(``thinking=False`` on ``local_model``) so the user model is asked not
|
|
1262
|
+
to reason either; whatever it still emits as ``<think>`` is stripped
|
|
1263
|
+
before the words become a user turn, an unclosed block is dropped
|
|
1264
|
+
whole, and a turn that was reasoning with no spoken line is retried
|
|
1265
|
+
rather than emitted as a fragment (#284)."""
|
|
1224
1266
|
from .generator import _realize_typed_message, _strip_directive_phrases, clean_user_message
|
|
1225
1267
|
|
|
1226
1268
|
trace = _render_user_trace(messages, steps)
|
|
@@ -1275,10 +1317,14 @@ def _user_followup(
|
|
|
1275
1317
|
# Floor, not ceiling: a 5s wait on a busy endpoint silently killed
|
|
1276
1318
|
# every follow-up and collapsed whole datasets to single-turn.
|
|
1277
1319
|
wait = max(8.0 if asked else 5.0, min(30.0, float(timeout or 30) / 2))
|
|
1278
|
-
attempts = (
|
|
1320
|
+
attempts = list(
|
|
1279
1321
|
(body, retry_body, retry_body) if force else ((body, retry_body) if asked else (body,))
|
|
1280
1322
|
)
|
|
1281
|
-
|
|
1323
|
+
retried_for_reasoning = False
|
|
1324
|
+
attempt = 0
|
|
1325
|
+
while attempt < len(attempts):
|
|
1326
|
+
content = attempts[attempt]
|
|
1327
|
+
attempt += 1
|
|
1282
1328
|
try:
|
|
1283
1329
|
reply = complete(
|
|
1284
1330
|
base_url,
|
|
@@ -1290,15 +1336,23 @@ def _user_followup(
|
|
|
1290
1336
|
tools=None,
|
|
1291
1337
|
api_key=api_key,
|
|
1292
1338
|
temperature=(_RESPONSE_TEMP_LO + _RESPONSE_TEMP_HI) / 2,
|
|
1293
|
-
max_tokens=120 if attempt else 180,
|
|
1339
|
+
max_tokens=120 if attempt > 1 else 180,
|
|
1294
1340
|
timeout=wait,
|
|
1341
|
+
extra=extra,
|
|
1295
1342
|
)
|
|
1296
1343
|
except Exception:
|
|
1297
1344
|
continue
|
|
1345
|
+
spoken, closed, unclosed = split_reasoning(reply.get("content") or "")
|
|
1346
|
+
_note_user_turn(turn_stats, closed, unclosed)
|
|
1347
|
+
if (closed or unclosed) and len(spoken.strip()) < _MIN_SPOKEN:
|
|
1348
|
+
# Reasoning and no spoken line. One more try with the short
|
|
1349
|
+
# prompt; never a fragment, never empty user speech.
|
|
1350
|
+
if not retried_for_reasoning:
|
|
1351
|
+
retried_for_reasoning = True
|
|
1352
|
+
attempts.append(retry_body)
|
|
1353
|
+
continue
|
|
1298
1354
|
text = _scrub_ai_traces(
|
|
1299
|
-
_realize_typed_message(
|
|
1300
|
-
_strip_directive_phrases(clean_user_message(reply.get("content") or "")), tags
|
|
1301
|
-
)
|
|
1355
|
+
_realize_typed_message(_strip_directive_phrases(clean_user_message(spoken)), tags)
|
|
1302
1356
|
)
|
|
1303
1357
|
if _accept_followup(text, prior, agent_text) and not _repeats_user_history(text, messages):
|
|
1304
1358
|
return text
|
|
@@ -1350,6 +1404,8 @@ def _human_answer(
|
|
|
1350
1404
|
api_key: str | None,
|
|
1351
1405
|
timeout: float,
|
|
1352
1406
|
stance: str = "",
|
|
1407
|
+
extra: Mapping[str, Any] | None = None,
|
|
1408
|
+
turn_stats: dict | None = None,
|
|
1353
1409
|
) -> str:
|
|
1354
1410
|
"""The simulated user answers the agent's question, in character.
|
|
1355
1411
|
|
|
@@ -1389,9 +1445,20 @@ def _human_answer(
|
|
|
1389
1445
|
},
|
|
1390
1446
|
]
|
|
1391
1447
|
reply = complete(
|
|
1392
|
-
base_url,
|
|
1448
|
+
base_url,
|
|
1449
|
+
model,
|
|
1450
|
+
msgs,
|
|
1451
|
+
api_key=api_key,
|
|
1452
|
+
temperature=0.9,
|
|
1453
|
+
timeout=timeout,
|
|
1454
|
+
max_tokens=120,
|
|
1455
|
+
extra=extra,
|
|
1393
1456
|
)
|
|
1394
|
-
|
|
1457
|
+
spoken, closed, unclosed = split_reasoning(reply.get("content") or "")
|
|
1458
|
+
_note_user_turn(turn_stats, closed, unclosed)
|
|
1459
|
+
if (closed or unclosed) and len(spoken.strip()) < _MIN_SPOKEN:
|
|
1460
|
+
return ""
|
|
1461
|
+
return _strip_tool_markup(spoken).strip()
|
|
1395
1462
|
|
|
1396
1463
|
|
|
1397
1464
|
def _answer_tool_call(env: Any, execute: Callable | None, tool: str, arguments: dict) -> dict:
|
|
@@ -1421,6 +1488,12 @@ def _answer_tool_call(env: Any, execute: Callable | None, tool: str, arguments:
|
|
|
1421
1488
|
#: says otherwise. Recorded on every row under ``sampling`` (rlhf-book ch. 9:
|
|
1422
1489
|
#: rejection sampling is run at 0.7 to 1.0; the row has to say what it was).
|
|
1423
1490
|
LOCAL_MODEL_TEMPERATURE = 0.8
|
|
1491
|
+
# Seconds one completion may take. A served model that scaled to zero
|
|
1492
|
+
# takes two to three minutes to answer its first request (113 s measured
|
|
1493
|
+
# on the account's own endpoint, #302; the hosted judge is the same
|
|
1494
|
+
# shape), and the old 60 s dropped every rollout of the first pass and
|
|
1495
|
+
# returned an empty run that looked finished.
|
|
1496
|
+
LOCAL_MODEL_TIMEOUT = 300.0
|
|
1424
1497
|
|
|
1425
1498
|
|
|
1426
1499
|
def reply_budget(max_tokens: int | None = None) -> int:
|
|
@@ -1451,7 +1524,7 @@ def local_model(
|
|
|
1451
1524
|
opening_rate: float = 0.0,
|
|
1452
1525
|
human_tools: set | None = None,
|
|
1453
1526
|
execute: Callable | None = None,
|
|
1454
|
-
timeout: float =
|
|
1527
|
+
timeout: float = LOCAL_MODEL_TIMEOUT,
|
|
1455
1528
|
max_tokens: int | None = None,
|
|
1456
1529
|
user_model: str | None = None,
|
|
1457
1530
|
thinking: bool | None = None,
|
|
@@ -1463,8 +1536,50 @@ def local_model(
|
|
|
1463
1536
|
``chat_template_kwargs={"enable_thinking": False}`` so the reply is
|
|
1464
1537
|
the answer, not the reasoning, the way the hosted Qwen path already
|
|
1465
1538
|
does; ``True`` asks for it; ``None`` (the default) sends nothing and
|
|
1466
|
-
leaves the server's default.
|
|
1467
|
-
|
|
1539
|
+
leaves the server's default. The same field goes to the simulated
|
|
1540
|
+
user when the agent's own model plays it (the default) or
|
|
1541
|
+
``user_model`` sits on the same endpoint, so the customer is asked
|
|
1542
|
+
not to reason either; a ``user_model`` on another endpoint keeps
|
|
1543
|
+
that server's default. Either way ``<think>`` markup never reaches
|
|
1544
|
+
``step["text"]``, ``final_text``, or a user turn (``step["user"]`` and
|
|
1545
|
+
the ``messages`` history): what the user model still emits as
|
|
1546
|
+
reasoning is stripped before it becomes speech, and a turn that was
|
|
1547
|
+
reasoning with no spoken line is retried, then dropped (#284). The run
|
|
1548
|
+
reports those under ``search["user_think"]``: ``user_turns``,
|
|
1549
|
+
``stripped`` and ``unclosed`` as counts, ``stripped_share`` and
|
|
1550
|
+
``unclosed_share`` as shares of the user turns, zeros when none.
|
|
1551
|
+
|
|
1552
|
+
``result_shapes`` pins what a tool returns: ``{tool_name: example
|
|
1553
|
+
result dict}``. The sandbox fills the example on every call instead
|
|
1554
|
+
of inventing a record, so a policy branch that only exists for some
|
|
1555
|
+
tool results (a credit over $200 must be escalated) is reached on
|
|
1556
|
+
purpose rather than by luck. Field names and free text stay as
|
|
1557
|
+
written; ids, dates and people are re-drawn per call, and a number
|
|
1558
|
+
moves by up to about a third of itself (``900.0`` lands in roughly
|
|
1559
|
+
600 to 1200, ``90.0`` in 60 to 120), so pick a template value whose
|
|
1560
|
+
whole range sits on the side of the threshold you want. An argument
|
|
1561
|
+
that shares a key with the template is echoed back (``invoice_id``
|
|
1562
|
+
in, same ``invoice_id`` out). To measure a branch, run the same
|
|
1563
|
+
pinned tasks under two shapes, one per side of the rule. Without it
|
|
1564
|
+
the situation writer drafts an example per tool (``write_result_shapes``)
|
|
1565
|
+
and the branch is exercised at random.
|
|
1566
|
+
|
|
1567
|
+
``fault_plans`` schedules faults per ask: ``{message: {tool_name:
|
|
1568
|
+
{"mode": "timeout", "rate": 1.0}}}``, keyed by the exact user message,
|
|
1569
|
+
with ``mode`` one of ``timeout``, ``malformed``, ``stale`` or
|
|
1570
|
+
``permission_denied`` and ``rate`` the chance the fault fires on a
|
|
1571
|
+
call. The plan may also carry ``world_state``, ``stance``, ``tone``
|
|
1572
|
+
and ``texture``, which are popped off and shape the world and the
|
|
1573
|
+
simulated user for that ask. ``simulate()`` writes these itself from
|
|
1574
|
+
``fault_rate=``; pass your own only to replay a known plan (``tasks=``
|
|
1575
|
+
does this for you).
|
|
1576
|
+
|
|
1577
|
+
``timeout`` is seconds per completion, ``LOCAL_MODEL_TIMEOUT`` (300)
|
|
1578
|
+
by default: a served model that scaled to zero takes two to three
|
|
1579
|
+
minutes to answer its first request, and a timeout under that drops
|
|
1580
|
+
every rollout of the first pass. When a call still times out the run
|
|
1581
|
+
says so in ``data.warnings`` with the fix (raise ``timeout=``, or send
|
|
1582
|
+
one throwaway request first so the endpoint is warm).
|
|
1468
1583
|
"""
|
|
1469
1584
|
local = threading.local()
|
|
1470
1585
|
plans = fault_plans if fault_plans is not None else {}
|
|
@@ -1479,6 +1594,9 @@ def local_model(
|
|
|
1479
1594
|
user_key: str | None = None
|
|
1480
1595
|
else:
|
|
1481
1596
|
user_url, user_name, user_key = base_url, model, api_key
|
|
1597
|
+
# thinking= reaches the user simulator on the agent's own endpoint.
|
|
1598
|
+
# Another server has its own template fields, so it keeps its default.
|
|
1599
|
+
user_extras = extras if str(user_url).rstrip("/") == str(base_url).rstrip("/") else None
|
|
1482
1600
|
shapes = result_shapes if result_shapes is not None else {}
|
|
1483
1601
|
cap = default_max_turns(n_tools=len(tools)) if max_turns is None else max(1, int(max_turns))
|
|
1484
1602
|
min_users = max(1, min(int(min_user_turns), max(1, cap // 2)))
|
|
@@ -1590,6 +1708,8 @@ def local_model(
|
|
|
1590
1708
|
api_key=user_key,
|
|
1591
1709
|
timeout=timeout,
|
|
1592
1710
|
stance=stance,
|
|
1711
|
+
extra=user_extras,
|
|
1712
|
+
turn_stats=turn_stats,
|
|
1593
1713
|
)
|
|
1594
1714
|
# an empty answer used to default to "go ahead", which
|
|
1595
1715
|
# silently taught the agent that asking always clears
|
|
@@ -1682,6 +1802,8 @@ def local_model(
|
|
|
1682
1802
|
persona_tags=persona_tags,
|
|
1683
1803
|
tools=tools,
|
|
1684
1804
|
force=force_followup,
|
|
1805
|
+
extra=user_extras,
|
|
1806
|
+
turn_stats=turn_stats,
|
|
1685
1807
|
)
|
|
1686
1808
|
if follow:
|
|
1687
1809
|
last_user = follow
|