whileai 0.56__tar.gz → 0.58__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.
Files changed (89) hide show
  1. {whileai-0.56 → whileai-0.58}/PKG-INFO +39 -6
  2. whileai-0.56/whileai.egg-info/PKG-INFO → whileai-0.58/README.md +37 -40
  3. {whileai-0.56 → whileai-0.58}/pyproject.toml +2 -1
  4. {whileai-0.56 → whileai-0.58}/whileai/simulations/__init__.py +4 -0
  5. {whileai-0.56 → whileai-0.58}/whileai/simulations/data.py +1 -0
  6. {whileai-0.56 → whileai-0.58}/whileai/simulations/ingest/traces.py +17 -11
  7. {whileai-0.56 → whileai-0.58}/whileai/simulations/run/engine.py +21 -0
  8. whileai-0.58/whileai/simulations/score/audit.py +296 -0
  9. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/delta.py +15 -0
  10. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/hygiene.py +70 -0
  11. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/judging.py +36 -1
  12. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/optimize.py +18 -0
  13. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/stats.py +34 -9
  14. {whileai-0.56 → whileai-0.58}/whileai/simulations/simulation.py +54 -0
  15. whileai-0.56/README.md → whileai-0.58/whileai.egg-info/PKG-INFO +73 -4
  16. {whileai-0.56 → whileai-0.58}/whileai.egg-info/SOURCES.txt +1 -0
  17. {whileai-0.56 → whileai-0.58}/LICENSE +0 -0
  18. {whileai-0.56 → whileai-0.58}/MANIFEST.in +0 -0
  19. {whileai-0.56 → whileai-0.58}/setup.cfg +0 -0
  20. {whileai-0.56 → whileai-0.58}/whileai/__init__.py +0 -0
  21. {whileai-0.56 → whileai-0.58}/whileai/_env.py +0 -0
  22. {whileai-0.56 → whileai-0.58}/whileai/auth.py +0 -0
  23. {whileai-0.56 → whileai-0.58}/whileai/cli.py +0 -0
  24. {whileai-0.56 → whileai-0.58}/whileai/ingest.py +0 -0
  25. {whileai-0.56 → whileai-0.58}/whileai/py.typed +0 -0
  26. {whileai-0.56 → whileai-0.58}/whileai/simulations/__main__.py +0 -0
  27. {whileai-0.56 → whileai-0.58}/whileai/simulations/environment.py +0 -0
  28. {whileai-0.56 → whileai-0.58}/whileai/simulations/export.py +0 -0
  29. {whileai-0.56 → whileai-0.58}/whileai/simulations/generate/__init__.py +0 -0
  30. {whileai-0.56 → whileai-0.58}/whileai/simulations/generate/actionspace.py +0 -0
  31. {whileai-0.56 → whileai-0.58}/whileai/simulations/generate/adapters.py +0 -0
  32. {whileai-0.56 → whileai-0.58}/whileai/simulations/generate/agents.py +0 -0
  33. {whileai-0.56 → whileai-0.58}/whileai/simulations/generate/coverage.py +0 -0
  34. {whileai-0.56 → whileai-0.58}/whileai/simulations/generate/diversity.py +0 -0
  35. {whileai-0.56 → whileai-0.58}/whileai/simulations/generate/embeddings.py +0 -0
  36. {whileai-0.56 → whileai-0.58}/whileai/simulations/generate/explore.py +0 -0
  37. {whileai-0.56 → whileai-0.58}/whileai/simulations/generate/generator.py +0 -0
  38. {whileai-0.56 → whileai-0.58}/whileai/simulations/generate/offline_agent.py +0 -0
  39. {whileai-0.56 → whileai-0.58}/whileai/simulations/generate/scenarios.py +0 -0
  40. {whileai-0.56 → whileai-0.58}/whileai/simulations/generate/usage_meter.py +0 -0
  41. {whileai-0.56 → whileai-0.58}/whileai/simulations/ingest/__init__.py +0 -0
  42. {whileai-0.56 → whileai-0.58}/whileai/simulations/ingest/otel.py +0 -0
  43. {whileai-0.56 → whileai-0.58}/whileai/simulations/ingest/platform.py +0 -0
  44. {whileai-0.56 → whileai-0.58}/whileai/simulations/monitor.py +0 -0
  45. {whileai-0.56 → whileai-0.58}/whileai/simulations/py.typed +0 -0
  46. {whileai-0.56 → whileai-0.58}/whileai/simulations/run/__init__.py +0 -0
  47. {whileai-0.56 → whileai-0.58}/whileai/simulations/run/config.py +0 -0
  48. {whileai-0.56 → whileai-0.58}/whileai/simulations/run/rows.py +0 -0
  49. {whileai-0.56 → whileai-0.58}/whileai/simulations/run/spec.py +0 -0
  50. {whileai-0.56 → whileai-0.58}/whileai/simulations/schema.py +0 -0
  51. {whileai-0.56 → whileai-0.58}/whileai/simulations/schemas/row-v1.json +0 -0
  52. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/__init__.py +0 -0
  53. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/agreement.py +0 -0
  54. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/checklist.py +0 -0
  55. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/curriculum.py +0 -0
  56. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/grade_llm.py +0 -0
  57. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/grading.py +0 -0
  58. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/grounding.py +0 -0
  59. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/hack_scan.py +0 -0
  60. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/judge_trust.py +0 -0
  61. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/labels.py +0 -0
  62. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/llm_judge.py +0 -0
  63. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/logprobs.py +0 -0
  64. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/markers.py +0 -0
  65. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/pairwise.py +0 -0
  66. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/passat.py +0 -0
  67. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/preflight.py +0 -0
  68. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/privileged.py +0 -0
  69. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/publish_gate.py +0 -0
  70. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/quality.py +0 -0
  71. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/reference.py +0 -0
  72. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/rubric.py +0 -0
  73. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/spec.py +0 -0
  74. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/stage.py +0 -0
  75. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/style.py +0 -0
  76. {whileai-0.56 → whileai-0.58}/whileai/simulations/score/trace.py +0 -0
  77. {whileai-0.56 → whileai-0.58}/whileai/simulations/training.py +0 -0
  78. {whileai-0.56 → whileai-0.58}/whileai/simulations/verify/__init__.py +0 -0
  79. {whileai-0.56 → whileai-0.58}/whileai/simulations/verify/base.py +0 -0
  80. {whileai-0.56 → whileai-0.58}/whileai/simulations/verify/code.py +0 -0
  81. {whileai-0.56 → whileai-0.58}/whileai/simulations/verify/math.py +0 -0
  82. {whileai-0.56 → whileai-0.58}/whileai/simulations/verify/structured.py +0 -0
  83. {whileai-0.56 → whileai-0.58}/whileai/simulations/verify/text.py +0 -0
  84. {whileai-0.56 → whileai-0.58}/whileai/simulations/world/__init__.py +0 -0
  85. {whileai-0.56 → whileai-0.58}/whileai/simulations/world/sandbox.py +0 -0
  86. {whileai-0.56 → whileai-0.58}/whileai.egg-info/dependency_links.txt +0 -0
  87. {whileai-0.56 → whileai-0.58}/whileai.egg-info/entry_points.txt +0 -0
  88. {whileai-0.56 → whileai-0.58}/whileai.egg-info/requires.txt +0 -0
  89. {whileai-0.56 → whileai-0.58}/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.56
3
+ Version: 0.58
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
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/whilehq/whileai-sdk
8
8
  Project-URL: Repository, https://github.com/whilehq/whileai-sdk
9
9
  Project-URL: Issues, https://github.com/whilehq/whileai-sdk/issues
10
10
  Project-URL: Datasets, https://huggingface.co/datasets/zero-proof-ai/agent-simulations
11
- Keywords: rl,reinforcement-learning,grpo,synthetic-data,agents,tool-use,evaluation,simulation,llm
11
+ Keywords: zp,zeroproof,while,evals,agent-evals,rl,reinforcement-learning,grpo,synthetic-data,agents,tool-use,evaluation,simulation,llm
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Intended Audience :: Science/Research
@@ -41,7 +41,7 @@ The While Python SDK. One package, two importable modules:
41
41
  - `whileai`: the platform client. OTLP trace ingest and trace-dataset listing against the token gate.
42
42
  - `whileai.simulations`: post-training data for an agent. Give it the agent's traces, or its tools and system prompt; it simulates the situations, the people, and the world, plays the agent through multi-turn tool-calling conversations, and returns rows for your grader.
43
43
 
44
- **Renamed.** This SDK was `zeroproof` (ZeroProof is now While). `pip install zeroproof` still works: it installs `whileai`, and `import zeroproof` (or the older `zeroproof_simulations`) resolves to the same modules with a deprecation warning. `ZEROPROOF_*` environment variables and a saved `~/.zeroproof/credentials.json` are still read. Change the import when you can; new releases land under `whileai`.
44
+ **Renamed.** This SDK was `zeroproof` (ZeroProof is now While). `pip install zeroproof` still works: it installs `whileai`, and `import zeroproof` (or the older `zeroproof_simulations`) resolves to the same modules with a deprecation warning. `ZEROPROOF_*` environment variables and a saved `~/.zeroproof/credentials.json` are still read. Change the import when you can; new releases land under `whileai`. zp, ZeroProof and While all name this one product: the package is `whileai`, the import is `whileai.simulations`, keys start with `zp_`. A machine with the old package still picks up `~/.zeroproof/credentials.json`; set `WHILEAI_HOME` to a fresh directory to isolate a new account from it.
45
45
 
46
46
  Releases of `whileai` before 0.3 were an unrelated encrypted agent-to-agent messaging client. That code was removed in 0.04; pin `whileai<0.3` if you still depend on it.
47
47
 
@@ -187,11 +187,43 @@ and runs in seconds, but the situations are less varied than a model writes,
187
187
  so it is for wiring up your agent and grader, not for a training set — for
188
188
  that, bring a model below.
189
189
 
190
+ ### Evals for the agent you already have
191
+
192
+ Not training anything yet? The shortest path is an eval: wrap your agent
193
+ as `agent(message) -> {steps, final_text}`, write the policy as a judge
194
+ that reads the trajectory, run the asks `k` times each, and read pass@1
195
+ with its interval. Offline first, then the hosted writer. The how-to is
196
+ [docs/evals.md](docs/evals.md); the runnable version is
197
+ [`recipes/02-measure/eval-your-agent`](recipes/02-measure/eval-your-agent),
198
+ which ends at a CI gate, not a push.
199
+
200
+ ```python
201
+ data = wai.simulate(
202
+ agent,
203
+ tools=TOOLS,
204
+ system_prompt=POLICY,
205
+ seeds=SEEDS,
206
+ simulator=False,
207
+ mode="rl",
208
+ repeats=4,
209
+ repeat_policy="fixed",
210
+ )
211
+ scored = wai.evaluate(data, judge) # eval lineage: never the reward
212
+ print(wai.pass_at(scored.rows), *scored.warnings) # a hollow run says so here
213
+ ```
214
+
215
+ `scored.warnings` is new: no rollout called a tool, a declared tool no
216
+ 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.
217
+
190
218
  ### Bring a model
191
219
 
192
220
  Bring your own model. Any OpenAI-compatible chat endpoint that returns tool
193
221
  calls works; it writes the situations and plays the agent, so both run on
194
- your key:
222
+ your key. To put a number on a model you serve (`wai.serve`, or your own
223
+ vLLM), make it the agent: `wai.simulate(tasks=pinned,
224
+ agent=wai.local_model(endpoint, name, tools=TOOLS, system=POLICY,
225
+ thinking=False))`, and run both arms of a before/after through that same
226
+ call so the only difference is the weights.
195
227
 
196
228
  ```bash
197
229
  export OPENAI_API_KEY=...
@@ -421,8 +453,8 @@ When the rows carry none of that metadata the export warns: the reward
421
453
  reduces to `conduct_grade`, a process reward, and a policy trained on it
422
454
  alone learns to call nothing (`recipes/03-select/prime-intellect-rl`). `wai.load_environment(spec)`
423
455
  builds the environment in a process that has `verifiers` (`pip install
424
- 'whileai[rl]'`); `examples/coding-efficiency` is the same shape built by
425
- hand over an executable world with a hidden test suite.
456
+ 'whileai[rl]'`); the [tool-call-efficiency](https://huggingface.co/datasets/zero-proof-ai/tool-call-efficiency)
457
+ dataset is the same shape built by hand over an executable world with a hidden test suite.
426
458
 
427
459
  Training notes, each with the chapter of rlhfbook.com behind it. Calibrate
428
460
  difficulty with 8 to 16 rollouts per task before exporting so the band is
@@ -626,6 +658,7 @@ network.
626
658
  | Simulate and grade | [`recipes/01-simulate/agent-behavior`](recipes/01-simulate/agent-behavior) | Start here if the platform is new to you. Runs a coding agent with bad habits against real tests, streams every turn to While as OTLP spans plus a judge verdict, and fills a dashboard with behaviour worth looking at. Needs a key and a model endpoint; stdlib only. |
627
659
  | Simulate and grade | [`recipes/01-simulate/verifiers`](recipes/01-simulate/verifiers) | Verifiable rewards: math (`MathEqual`), an answer-and-format gate (`All`), code run against hidden tests (`CodeExec`), and a JSON-schema check, each feeding `grade`/`optimize`. Offline. |
628
660
  | Measure | [`recipes/02-measure/pass-at-k`](recipes/02-measure/pass-at-k) | pass@1, pass^k and pass@k with their intervals for one agent, the per-ask histogram the mean hides, and what each number tells you to do next. Offline. |
661
+ | Measure | [`recipes/02-measure/eval-your-agent`](recipes/02-measure/eval-your-agent) | Evals for the agent you already have: the callable wrapper, the policy as a judge that reads the trajectory, pass@1 with an interval and pass^k per policy branch, the coverage warnings that catch a hollow run, and a CI gate. Two scripted refund bots, one careful and one eager, so the eval visibly separates them. Offline, seconds. How-to: [docs/evals.md](docs/evals.md). |
629
662
  | Measure | [`recipes/02-measure/reward-hacking`](recipes/02-measure/reward-hacking) | Reward hacking caught before, during and after training: the within-ask scan, the judge probes, the trajectory flags, and the proxy-vs-target verdict on a scripted agent and two judges. Offline, seconds, no key. How-to: [docs/reward-hacking.md](docs/reward-hacking.md). |
630
663
  | Measure | [`recipes/02-measure/safety-evals`](recipes/02-measure/safety-evals) | Safety evals for a tool-using agent: prompt injection (direct, and planted in a tool result), data exfiltration, secret leakage, unauthorized writes, plus the benign controls that catch over-refusal. Trajectory markers as the judge, pass^k per attack class, the judge checked against hand labels, and a before/after that fails the fix which got safe by refusing. Offline, seconds. How-to: [docs/safety-evals.md](docs/safety-evals.md). |
631
664
  | Measure | [`recipes/02-measure/safety-evals-marketplace`](recipes/02-measure/safety-evals-marketplace) | The same safety eval for a marketplace agent: the injection is planted in user-generated reviews, the private data is per tenant (a competitor's buyer-intent list), one of the writes is a public post, and a flag needs a moderation ticket. Six trajectory markers, pass^k per attack class, the guarded before/after, and `live.py` to run the suite on a real model through Ollama with no key. Offline, seconds. |
@@ -1,39 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: whileai
3
- Version: 0.56
4
- Summary: While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt.
5
- Author: While
6
- License: Apache-2.0
7
- Project-URL: Homepage, https://github.com/whilehq/whileai-sdk
8
- Project-URL: Repository, https://github.com/whilehq/whileai-sdk
9
- Project-URL: Issues, https://github.com/whilehq/whileai-sdk/issues
10
- Project-URL: Datasets, https://huggingface.co/datasets/zero-proof-ai/agent-simulations
11
- Keywords: rl,reinforcement-learning,grpo,synthetic-data,agents,tool-use,evaluation,simulation,llm
12
- Classifier: Development Status :: 3 - Alpha
13
- Classifier: Intended Audience :: Developers
14
- Classifier: Intended Audience :: Science/Research
15
- Classifier: License :: OSI Approved :: Apache Software License
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.13
21
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
- Classifier: Typing :: Typed
23
- Requires-Python: >=3.10
24
- Description-Content-Type: text/markdown
25
- License-File: LICENSE
26
- Requires-Dist: requests>=2.25.0
27
- Provides-Extra: dev
28
- Requires-Dist: pytest>=8.0; extra == "dev"
29
- Requires-Dist: pytest-cov>=5.0; extra == "dev"
30
- Requires-Dist: ruff>=0.12; extra == "dev"
31
- Requires-Dist: mypy>=1.16; extra == "dev"
32
- Requires-Dist: types-requests; extra == "dev"
33
- Provides-Extra: rl
34
- Requires-Dist: verifiers>=0.3; (python_version >= "3.11" and python_version < "3.14") and extra == "rl"
35
- Dynamic: license-file
36
-
37
1
  # whileai
38
2
 
39
3
  The While Python SDK. One package, two importable modules:
@@ -41,7 +5,7 @@ The While Python SDK. One package, two importable modules:
41
5
  - `whileai`: the platform client. OTLP trace ingest and trace-dataset listing against the token gate.
42
6
  - `whileai.simulations`: post-training data for an agent. Give it the agent's traces, or its tools and system prompt; it simulates the situations, the people, and the world, plays the agent through multi-turn tool-calling conversations, and returns rows for your grader.
43
7
 
44
- **Renamed.** This SDK was `zeroproof` (ZeroProof is now While). `pip install zeroproof` still works: it installs `whileai`, and `import zeroproof` (or the older `zeroproof_simulations`) resolves to the same modules with a deprecation warning. `ZEROPROOF_*` environment variables and a saved `~/.zeroproof/credentials.json` are still read. Change the import when you can; new releases land under `whileai`.
8
+ **Renamed.** This SDK was `zeroproof` (ZeroProof is now While). `pip install zeroproof` still works: it installs `whileai`, and `import zeroproof` (or the older `zeroproof_simulations`) resolves to the same modules with a deprecation warning. `ZEROPROOF_*` environment variables and a saved `~/.zeroproof/credentials.json` are still read. Change the import when you can; new releases land under `whileai`. zp, ZeroProof and While all name this one product: the package is `whileai`, the import is `whileai.simulations`, keys start with `zp_`. A machine with the old package still picks up `~/.zeroproof/credentials.json`; set `WHILEAI_HOME` to a fresh directory to isolate a new account from it.
45
9
 
46
10
  Releases of `whileai` before 0.3 were an unrelated encrypted agent-to-agent messaging client. That code was removed in 0.04; pin `whileai<0.3` if you still depend on it.
47
11
 
@@ -187,11 +151,43 @@ and runs in seconds, but the situations are less varied than a model writes,
187
151
  so it is for wiring up your agent and grader, not for a training set — for
188
152
  that, bring a model below.
189
153
 
154
+ ### Evals for the agent you already have
155
+
156
+ Not training anything yet? The shortest path is an eval: wrap your agent
157
+ as `agent(message) -> {steps, final_text}`, write the policy as a judge
158
+ that reads the trajectory, run the asks `k` times each, and read pass@1
159
+ with its interval. Offline first, then the hosted writer. The how-to is
160
+ [docs/evals.md](docs/evals.md); the runnable version is
161
+ [`recipes/02-measure/eval-your-agent`](recipes/02-measure/eval-your-agent),
162
+ which ends at a CI gate, not a push.
163
+
164
+ ```python
165
+ data = wai.simulate(
166
+ agent,
167
+ tools=TOOLS,
168
+ system_prompt=POLICY,
169
+ seeds=SEEDS,
170
+ simulator=False,
171
+ mode="rl",
172
+ repeats=4,
173
+ repeat_policy="fixed",
174
+ )
175
+ scored = wai.evaluate(data, judge) # eval lineage: never the reward
176
+ print(wai.pass_at(scored.rows), *scored.warnings) # a hollow run says so here
177
+ ```
178
+
179
+ `scored.warnings` is new: no rollout called a tool, a declared tool no
180
+ 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.
181
+
190
182
  ### Bring a model
191
183
 
192
184
  Bring your own model. Any OpenAI-compatible chat endpoint that returns tool
193
185
  calls works; it writes the situations and plays the agent, so both run on
194
- your key:
186
+ your key. To put a number on a model you serve (`wai.serve`, or your own
187
+ vLLM), make it the agent: `wai.simulate(tasks=pinned,
188
+ agent=wai.local_model(endpoint, name, tools=TOOLS, system=POLICY,
189
+ thinking=False))`, and run both arms of a before/after through that same
190
+ call so the only difference is the weights.
195
191
 
196
192
  ```bash
197
193
  export OPENAI_API_KEY=...
@@ -421,8 +417,8 @@ When the rows carry none of that metadata the export warns: the reward
421
417
  reduces to `conduct_grade`, a process reward, and a policy trained on it
422
418
  alone learns to call nothing (`recipes/03-select/prime-intellect-rl`). `wai.load_environment(spec)`
423
419
  builds the environment in a process that has `verifiers` (`pip install
424
- 'whileai[rl]'`); `examples/coding-efficiency` is the same shape built by
425
- hand over an executable world with a hidden test suite.
420
+ 'whileai[rl]'`); the [tool-call-efficiency](https://huggingface.co/datasets/zero-proof-ai/tool-call-efficiency)
421
+ dataset is the same shape built by hand over an executable world with a hidden test suite.
426
422
 
427
423
  Training notes, each with the chapter of rlhfbook.com behind it. Calibrate
428
424
  difficulty with 8 to 16 rollouts per task before exporting so the band is
@@ -626,6 +622,7 @@ network.
626
622
  | Simulate and grade | [`recipes/01-simulate/agent-behavior`](recipes/01-simulate/agent-behavior) | Start here if the platform is new to you. Runs a coding agent with bad habits against real tests, streams every turn to While as OTLP spans plus a judge verdict, and fills a dashboard with behaviour worth looking at. Needs a key and a model endpoint; stdlib only. |
627
623
  | Simulate and grade | [`recipes/01-simulate/verifiers`](recipes/01-simulate/verifiers) | Verifiable rewards: math (`MathEqual`), an answer-and-format gate (`All`), code run against hidden tests (`CodeExec`), and a JSON-schema check, each feeding `grade`/`optimize`. Offline. |
628
624
  | Measure | [`recipes/02-measure/pass-at-k`](recipes/02-measure/pass-at-k) | pass@1, pass^k and pass@k with their intervals for one agent, the per-ask histogram the mean hides, and what each number tells you to do next. Offline. |
625
+ | Measure | [`recipes/02-measure/eval-your-agent`](recipes/02-measure/eval-your-agent) | Evals for the agent you already have: the callable wrapper, the policy as a judge that reads the trajectory, pass@1 with an interval and pass^k per policy branch, the coverage warnings that catch a hollow run, and a CI gate. Two scripted refund bots, one careful and one eager, so the eval visibly separates them. Offline, seconds. How-to: [docs/evals.md](docs/evals.md). |
629
626
  | Measure | [`recipes/02-measure/reward-hacking`](recipes/02-measure/reward-hacking) | Reward hacking caught before, during and after training: the within-ask scan, the judge probes, the trajectory flags, and the proxy-vs-target verdict on a scripted agent and two judges. Offline, seconds, no key. How-to: [docs/reward-hacking.md](docs/reward-hacking.md). |
630
627
  | Measure | [`recipes/02-measure/safety-evals`](recipes/02-measure/safety-evals) | Safety evals for a tool-using agent: prompt injection (direct, and planted in a tool result), data exfiltration, secret leakage, unauthorized writes, plus the benign controls that catch over-refusal. Trajectory markers as the judge, pass^k per attack class, the judge checked against hand labels, and a before/after that fails the fix which got safe by refusing. Offline, seconds. How-to: [docs/safety-evals.md](docs/safety-evals.md). |
631
628
  | Measure | [`recipes/02-measure/safety-evals-marketplace`](recipes/02-measure/safety-evals-marketplace) | The same safety eval for a marketplace agent: the injection is planted in user-generated reviews, the private data is per tenant (a competitor's buyer-intent list), one of the writes is a public post, and a flag needs a moderation ticket. Six trajectory markers, pass^k per attack class, the guarded before/after, and `live.py` to run the suite on a real model through Ollama with no key. Offline, seconds. |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "whileai"
7
- version = "0.56"
7
+ version = "0.58"
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"}
@@ -12,6 +12,7 @@ requires-python = ">=3.10"
12
12
  dependencies = ["requests>=2.25.0"]
13
13
  authors = [{name = "While"}]
14
14
  keywords = [
15
+ "zp", "zeroproof", "while", "evals", "agent-evals",
15
16
  "rl", "reinforcement-learning", "grpo", "synthetic-data", "agents",
16
17
  "tool-use", "evaluation", "simulation", "llm",
17
18
  ]
@@ -102,6 +102,7 @@ from .schema import (
102
102
  validate,
103
103
  )
104
104
  from .score.agreement import judge_agreement
105
+ from .score.audit import audit_grades, format_audit
105
106
  from .score.checklist import expected_outcome, outcome_check, privileged_context, task_checklist
106
107
  from .score.curriculum import curriculum, format_curriculum, retire_solved
107
108
  from .score.delta import delta_report, format_delta_report
@@ -115,6 +116,7 @@ from .score.grounding import (
115
116
  from .score.hack_scan import format_hack_scan, format_hack_scan_diff, hack_scan, hack_scan_diff
116
117
  from .score.hygiene import (
117
118
  HACK_THRESHOLD,
119
+ coverage_warnings,
118
120
  dedupe_groups,
119
121
  length_report,
120
122
  near_duplicate_prompts,
@@ -235,6 +237,7 @@ __all__ = [
235
237
  "attach_holdout",
236
238
  "attach_labels",
237
239
  "attach_rubric",
240
+ "audit_grades",
238
241
  "behavior_signature",
239
242
  "behavioral_markers",
240
243
  "build_dimensions",
@@ -273,6 +276,7 @@ __all__ = [
273
276
  "export_training",
274
277
  "filter_rl_rows",
275
278
  "flaw_rows",
279
+ "format_audit",
276
280
  "format_curriculum",
277
281
  "format_delta_report",
278
282
  "format_hack_monitor",
@@ -460,6 +460,7 @@ class SimulationData:
460
460
  source="grade",
461
461
  concurrency=min(int(concurrency), 32),
462
462
  version=version,
463
+ tools=sorted(str(t) for t in self.declared_tools),
463
464
  scale=scale,
464
465
  )
465
466
  note = trust_after_grade(scored.rows, mode=trust)["note"]
@@ -278,15 +278,20 @@ def dimensions_from_traces(
278
278
  def _task_key(row: dict, index: int) -> tuple[str, object]:
279
279
  """What makes two rows the same task for splitting purposes.
280
280
 
281
- ``prompt`` first, because that is what a train/eval split has to keep
282
- disjoint and what ``leakage_report`` measures. ``scenario_id`` covers
283
- rows that carry no prompt. A row with neither is its own task, so rows
284
- that merely lack both are not swept onto one side together.
281
+ The unit every report counts in: ``task_key`` (``scenario_id`` when
282
+ the row has one, else ``task_id``, else the prompt), so repeats and
283
+ rephrasings of one situation land on the same side and the held-out
284
+ slice is disjoint from train in the unit ``pass_at``, ``compare_runs``
285
+ and ``delta_report`` group by (#268). Splitting on the prompt alone
286
+ left 16 of 28 held-out situations in train, and ``decontaminate``
287
+ cannot see that because it compares prompts. A row with no key is its
288
+ own task, so rows that merely lack one are not swept onto one side.
285
289
  """
286
- for field in ("prompt", "scenario_id"):
287
- value = row.get(field)
288
- if value is not None and str(value).strip():
289
- return (field, str(value))
290
+ from ..score.stats import task_key
291
+
292
+ key = task_key(row) if isinstance(row, dict) else ""
293
+ if key and str(key).strip():
294
+ return ("task", str(key))
290
295
  return ("index", index)
291
296
 
292
297
 
@@ -295,9 +300,10 @@ def split_pseudo_production(
295
300
  ) -> tuple[list[dict], list[dict]]:
296
301
  """Set aside a pseudo-production slice; the rest stays for training.
297
302
 
298
- The split is by task, not by row: every row sharing a ``prompt`` (or a
299
- ``scenario_id``, for rows without a prompt) lands on the same side, so
300
- the held-out slice is prompt-disjoint from the training side. Splitting
303
+ The split is by task, not by row: every row sharing a ``task_key``
304
+ (the ``scenario_id``, else the prompt) lands on the same side, so the
305
+ held-out slice is disjoint from the training side in the unit every
306
+ report groups by, not just prompt-disjoint. Splitting
301
307
  by row is not enough — under ``mode="rl"`` with ``repeats=k`` each
302
308
  prompt has k rows, and scattering siblings across the two sides trains
303
309
  the student on every prompt it is then evaluated on.
@@ -2851,6 +2851,27 @@ class Run:
2851
2851
  data.degraded.append("same_model")
2852
2852
  data.warnings.append(note)
2853
2853
  log.warning(note)
2854
+ # A run whose rollouts never called a tool is hollow: the writer
2855
+ # asked about things the world does not have, or the wrapper did
2856
+ # not record steps. Grading it gives a number that means nothing.
2857
+ rows = data.trajectories
2858
+ if rows and data.declared_tools:
2859
+ with_calls = sum(
2860
+ 1
2861
+ for r in rows
2862
+ if any(isinstance(s, dict) and s.get("tool") for s in (r.get("steps") or []))
2863
+ )
2864
+ if with_calls == 0:
2865
+ note = (
2866
+ f"0 of {len(rows)} rollouts called a tool, so this run says nothing "
2867
+ "about tool use. Put the ids your world has (order numbers, account "
2868
+ "names) in the tool descriptions or in seeds=, and check the agent "
2869
+ "wrapper records its steps, before grading it."
2870
+ )
2871
+ if "no_tool_calls" not in data.degraded:
2872
+ data.degraded.append("no_tool_calls")
2873
+ data.warnings.append(note)
2874
+ log.warning(note)
2854
2875
  if c.out_path is not None and data.trajectories:
2855
2876
  data.save(str(c.out_path), meta=True)
2856
2877
  return data
@@ -0,0 +1,296 @@
1
+ """Is the verifier failing answers that are right?
2
+
3
+ A verifier is a rule: execution match, exact match, a regex on the final
4
+ number. Rules fail correct answers for reasons that have nothing to do
5
+ with correctness (rounding, ordering, a column named differently, an
6
+ equivalent row set), and every such false negative halves the
7
+ separation on its task: the policy is told that a right answer was
8
+ wrong. On 570 SEC XBRL tasks, fixing the grader moved mean reward from
9
+ 0.20 to 0.48 with no training at all. The RLHF book treats verifiers as
10
+ solved (ch. 14: "a scoring function that returns a positive reward when
11
+ the answer is correct and 0 otherwise"); this module is our own
12
+ measurement of how often that function is wrong.
13
+
14
+ ``audit_grades`` samples failed rows, asks a judge whether each reply is
15
+ in fact correct given the reference, and reports the false-negative rate
16
+ with a Wilson interval and the verifier reasons that dominate. The judge
17
+ is a second opinion, not ground truth: the book's judge-prompt rules
18
+ apply (ch. 7: length must not sway it, no position bias, temperature 0
19
+ for stable ratings), and a rate near the threshold deserves a check of
20
+ the judge itself on human labels (``judge_agreement``, ``judge_trust``).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import random
26
+ import re
27
+ from collections.abc import Callable, Sequence
28
+ from typing import Any
29
+
30
+ from .judging import run_judge
31
+ from .stats import wilson_interval
32
+
33
+ #: above this share of audited failures, the verifier is the problem to fix first
34
+ FN_WARN = 0.10
35
+
36
+ #: what the judge is asked when the row has no rubric of its own
37
+ AUDIT_QUESTION = (
38
+ "The reference answer is the answer key for this task. Is the reply's final answer "
39
+ "correct, meaning it gives the same result as the reference? Differences in "
40
+ "formatting, rounding, ordering (unless the task asks for an order), column or "
41
+ "field naming, or an equivalent way of expressing the same result do not make it "
42
+ "wrong. A missing, different, or partial result does."
43
+ )
44
+
45
+ _DIGITS = re.compile(r"\d+(\.\d+)?")
46
+
47
+
48
+ def _reason_key(reason: Any, verifier: str | None) -> str:
49
+ """The kind of failure a verifier reason names, without its numbers:
50
+ ``result differs: got 3 rows x 2 cols`` and ``result differs: got 10
51
+ rows x 1 col`` are one reason."""
52
+ text = str(reason or "").strip()
53
+ if verifier and text.lower().startswith(verifier.lower() + ":"):
54
+ text = text[len(verifier) + 1 :].strip()
55
+ head = re.split(r"[:(]", text, maxsplit=1)[0].strip()
56
+ head = _DIGITS.sub("#", head).lower()
57
+ return head or "(no reason)"
58
+
59
+
60
+ def _binary(row: dict) -> int | None:
61
+ value = row.get("reward")
62
+ if value is None or isinstance(value, bool):
63
+ return None
64
+ try:
65
+ f = float(value)
66
+ except (TypeError, ValueError):
67
+ return None
68
+ if f in (0.0, 1.0):
69
+ return int(f)
70
+ return None
71
+
72
+
73
+ def _audit_row(row: dict, question: str) -> dict:
74
+ """The row as the judge sees it: the reply and the reference, the
75
+ audit question as the rubric when the row has none, and the
76
+ verifier's own verdict alongside so a judge that reads it can say
77
+ why the rule was wrong."""
78
+ out = dict(row)
79
+ priv = dict(row.get("privileged") or {})
80
+ if not priv.get("rubric"):
81
+ priv["rubric"] = question
82
+ out["privileged"] = priv
83
+ out["audit"] = {
84
+ "question": question,
85
+ "verifier": row.get("judge_name"),
86
+ "verifier_reason": row.get("reason"),
87
+ "verifier_reward": row.get("reward"),
88
+ }
89
+ for key in ("reward", "reason", "judge_status", "judge_meta", "judge_name", "failure_class"):
90
+ out.pop(key, None)
91
+ return out
92
+
93
+
94
+ def _rate(hits: int, n: int) -> tuple[float | None, tuple[float, float] | None]:
95
+ if not n:
96
+ return None, None
97
+ return round(hits / n, 4), wilson_interval(hits, n)
98
+
99
+
100
+ def audit_grades(
101
+ rows: Sequence[dict],
102
+ *,
103
+ judge: Callable[[dict], Any],
104
+ sample: int = 40,
105
+ passes: int = 0,
106
+ question: str = AUDIT_QUESTION,
107
+ seed: int = 0,
108
+ concurrency: int = 4,
109
+ timeout: float = 120,
110
+ judge_name: str | None = None,
111
+ ) -> dict[str, Any]:
112
+ """Estimate the verifier's false-negative rate from a judged sample.
113
+
114
+ ``rows`` are graded by the verifier (``run_judge(rows, verifier)`` or
115
+ ``data.grade(judge=verifier)``): ``reward`` 0/1, ``reason`` from the
116
+ rule. ``sample`` failed rows (reward 0, judge ok) are drawn with
117
+ ``seed`` and each is put to ``judge`` (any judge in the ``run_judge``
118
+ contract: ``rubric_judge()``, ``grade_llm``, your own callable) with
119
+ the reference in place and ``question`` as the rubric when the row
120
+ carries none. A judge reward at or above 0.5 on a failed row is a
121
+ false negative. ``passes`` samples passed rows the same way for the
122
+ false-positive side.
123
+
124
+ Returns ``fn_rate`` with ``fn_ci95`` (Wilson), ``estimated_wrong_fails``
125
+ (the rate over every failed row), ``reasons`` (the verifier's failure
126
+ kinds in the sample, each with how many the judge overturned), a few
127
+ ``examples``, ``fp_rate`` when ``passes`` > 0, and ``warnings``. Above
128
+ ``FN_WARN`` the summary says to fix the verifier before training;
129
+ ``select_for_rl(audit=report)`` and ``optimize(audit=)`` carry the
130
+ same warning into the selection.
131
+ """
132
+ if sample < 1:
133
+ raise ValueError("sample is how many failed rows to put to the judge; at least 1")
134
+ graded = [r for r in rows if isinstance(r, dict) and _binary(r) is not None]
135
+ failed = [r for r in graded if _binary(r) == 0 and r.get("judge_status", "ok") == "ok"]
136
+ passed = [r for r in graded if _binary(r) == 1 and r.get("judge_status", "ok") == "ok"]
137
+ verifier = next((str(r.get("judge_name")) for r in graded if r.get("judge_name")), None)
138
+ rng = random.Random(seed)
139
+ take_f = failed if len(failed) <= sample else rng.sample(failed, sample)
140
+ take_p = (
141
+ [] if passes <= 0 else (passed if len(passed) <= passes else rng.sample(passed, passes))
142
+ )
143
+
144
+ def _judge(batch: list[dict]) -> list[dict]:
145
+ if not batch:
146
+ return []
147
+ audit_rows = [_audit_row(r, question) for r in batch]
148
+ scored = run_judge(
149
+ audit_rows,
150
+ judge,
151
+ judge_name=judge_name,
152
+ concurrency=concurrency,
153
+ timeout=timeout,
154
+ source="audit",
155
+ )
156
+ return list(scored.rows)
157
+
158
+ judged_f = _judge(list(take_f))
159
+ judged_p = _judge(list(take_p))
160
+ judge_label = next((str(r.get("judge_name")) for r in judged_f + judged_p), judge_name)
161
+
162
+ checked_f = [r for r in judged_f if r.get("judge_status") == "ok" and _num(r) is not None]
163
+ overturned = [r for r in checked_f if float(_num(r) or 0.0) >= 0.5]
164
+ checked_p = [r for r in judged_p if r.get("judge_status") == "ok" and _num(r) is not None]
165
+ fp_rows = [r for r in checked_p if float(_num(r) or 0.0) < 0.5]
166
+ errors = sum(1 for r in judged_f + judged_p if r.get("judge_status") != "ok")
167
+
168
+ fn_rate, fn_ci = _rate(len(overturned), len(checked_f))
169
+ fp_rate, fp_ci = _rate(len(fp_rows), len(checked_p))
170
+
171
+ reasons: dict[str, dict[str, int]] = {}
172
+ for r in checked_f:
173
+ key = _reason_key((r.get("audit") or {}).get("verifier_reason"), verifier)
174
+ slot = reasons.setdefault(key, {"n": 0, "fn": 0})
175
+ slot["n"] += 1
176
+ if float(_num(r) or 0.0) >= 0.5:
177
+ slot["fn"] += 1
178
+ reasons = dict(sorted(reasons.items(), key=lambda kv: (-kv[1]["fn"], -kv[1]["n"], kv[0])))
179
+
180
+ examples = [
181
+ {
182
+ "scenario_id": r.get("scenario_id"),
183
+ "rollout_index": r.get("rollout_index"),
184
+ "verifier_reason": (r.get("audit") or {}).get("verifier_reason"),
185
+ "judge_reason": r.get("reason"),
186
+ }
187
+ for r in overturned[:5]
188
+ ]
189
+ estimated = round(float(fn_rate) * len(failed)) if fn_rate is not None else None
190
+
191
+ warnings: list[str] = []
192
+ if fn_rate is None:
193
+ summary = (
194
+ f"no failed row could be audited ({len(failed)} failed, {errors} judge errors); "
195
+ "nothing to say about the verifier"
196
+ )
197
+ warnings.append(summary)
198
+ else:
199
+ lo, hi = fn_ci or (0.0, 0.0)
200
+ summary = (
201
+ f"the judge overturned {len(overturned)} of {len(checked_f)} audited failures: "
202
+ f"false-negative rate {fn_rate:.0%} (95% {lo:.0%}..{hi:.0%}), about {estimated} of "
203
+ f"{len(failed)} failed rows are right answers the verifier rejected"
204
+ )
205
+ if fn_rate > FN_WARN:
206
+ top = next(iter(reasons), None)
207
+ warnings.append(
208
+ f"VERIFIER: {summary}. Fix the verifier before training: each false negative "
209
+ "halves the separation on its task, and a grader fix has moved mean reward "
210
+ "0.20 -> 0.48 with no training"
211
+ + (f". The reason the judge overturns most: {top!r}" if top else "")
212
+ + "."
213
+ )
214
+ if len(checked_f) < 20:
215
+ warnings.append(
216
+ f"{len(checked_f)} audited failures is a small sample; the interval is wide. "
217
+ "Raise sample= for a rate worth acting on."
218
+ )
219
+ if fp_rate is not None and fp_rate > FN_WARN:
220
+ warnings.append(
221
+ f"the judge also disagreed with {len(fp_rows)} of {len(checked_p)} audited passes "
222
+ f"(false-positive rate {fp_rate:.0%}); the rule may be too loose as well"
223
+ )
224
+ if errors:
225
+ warnings.append(f"{errors} audit rows were not judged (judge error or timeout)")
226
+ if fn_rate is not None:
227
+ warnings.append(
228
+ "The judge is a second opinion, not ground truth; check it on human labels "
229
+ "(judge_agreement, judge_trust) before acting on a rate near the threshold."
230
+ )
231
+ return {
232
+ "n_rows": len(graded),
233
+ "n_failed": len(failed),
234
+ "n_passed": len(passed),
235
+ "n_sampled": len(take_f),
236
+ "n_checked": len(checked_f),
237
+ "false_negatives": len(overturned),
238
+ "fn_rate": fn_rate,
239
+ "fn_ci95": fn_ci,
240
+ "estimated_wrong_fails": estimated,
241
+ "n_passes_sampled": len(take_p),
242
+ "n_passes_checked": len(checked_p),
243
+ "false_positives": len(fp_rows),
244
+ "fp_rate": fp_rate,
245
+ "fp_ci95": fp_ci,
246
+ "judge_errors": errors,
247
+ "reasons": reasons,
248
+ "examples": examples,
249
+ "verifier": verifier,
250
+ "judge": judge_label,
251
+ "question": question,
252
+ "warnings": warnings,
253
+ "summary": summary,
254
+ }
255
+
256
+
257
+ def _num(row: dict) -> float | None:
258
+ value = row.get("reward")
259
+ if value is None or isinstance(value, bool):
260
+ return None
261
+ try:
262
+ return float(value)
263
+ except (TypeError, ValueError):
264
+ return None
265
+
266
+
267
+ def audit_warning(audit: dict[str, Any] | None) -> str | None:
268
+ """The one line a selection report carries when an audit found the
269
+ verifier wrong too often; ``None`` otherwise."""
270
+ if not isinstance(audit, dict):
271
+ return None
272
+ rate = audit.get("fn_rate")
273
+ if rate is None or float(rate) <= FN_WARN:
274
+ return None
275
+ ci = audit.get("fn_ci95") or (0.0, 0.0)
276
+ return (
277
+ f"verifier false-negative rate {float(rate):.0%} (95% {ci[0]:.0%}..{ci[1]:.0%}, "
278
+ f"{audit.get('n_checked')} audited): right answers are being scored 0. Fix the "
279
+ "verifier before training on this selection (audit_grades)."
280
+ )
281
+
282
+
283
+ def format_audit(report: dict[str, Any]) -> str:
284
+ """The block a person reads: the summary, then the reasons."""
285
+ lines = [str(report.get("summary") or "")]
286
+ for key, slot in (report.get("reasons") or {}).items():
287
+ lines.append(f" {key}: {slot['fn']} of {slot['n']} overturned")
288
+ for ex in report.get("examples") or []:
289
+ lines.append(
290
+ f" {ex.get('scenario_id')} r{ex.get('rollout_index')}: verifier said "
291
+ f"{ex.get('verifier_reason')!r}; judge: {ex.get('judge_reason')!r}"
292
+ )
293
+ return "\n".join(lines)
294
+
295
+
296
+ __all__ = ["AUDIT_QUESTION", "FN_WARN", "audit_grades", "audit_warning", "format_audit"]