whileai 0.55__tar.gz → 0.57__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.55 → whileai-0.57}/PKG-INFO +34 -5
  2. whileai-0.55/whileai.egg-info/PKG-INFO → whileai-0.57/README.md +32 -39
  3. {whileai-0.55 → whileai-0.57}/pyproject.toml +2 -1
  4. {whileai-0.55 → whileai-0.57}/whileai/simulations/__init__.py +10 -0
  5. {whileai-0.55 → whileai-0.57}/whileai/simulations/data.py +1 -0
  6. {whileai-0.55 → whileai-0.57}/whileai/simulations/ingest/platform.py +33 -0
  7. {whileai-0.55 → whileai-0.57}/whileai/simulations/run/engine.py +21 -0
  8. whileai-0.57/whileai/simulations/score/audit.py +296 -0
  9. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/delta.py +37 -1
  10. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/hygiene.py +70 -0
  11. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/judging.py +36 -1
  12. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/optimize.py +145 -0
  13. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/stats.py +110 -0
  14. {whileai-0.55 → whileai-0.57}/whileai/simulations/simulation.py +54 -0
  15. whileai-0.55/README.md → whileai-0.57/whileai.egg-info/PKG-INFO +68 -3
  16. {whileai-0.55 → whileai-0.57}/whileai.egg-info/SOURCES.txt +1 -0
  17. {whileai-0.55 → whileai-0.57}/LICENSE +0 -0
  18. {whileai-0.55 → whileai-0.57}/MANIFEST.in +0 -0
  19. {whileai-0.55 → whileai-0.57}/setup.cfg +0 -0
  20. {whileai-0.55 → whileai-0.57}/whileai/__init__.py +0 -0
  21. {whileai-0.55 → whileai-0.57}/whileai/_env.py +0 -0
  22. {whileai-0.55 → whileai-0.57}/whileai/auth.py +0 -0
  23. {whileai-0.55 → whileai-0.57}/whileai/cli.py +0 -0
  24. {whileai-0.55 → whileai-0.57}/whileai/ingest.py +0 -0
  25. {whileai-0.55 → whileai-0.57}/whileai/py.typed +0 -0
  26. {whileai-0.55 → whileai-0.57}/whileai/simulations/__main__.py +0 -0
  27. {whileai-0.55 → whileai-0.57}/whileai/simulations/environment.py +0 -0
  28. {whileai-0.55 → whileai-0.57}/whileai/simulations/export.py +0 -0
  29. {whileai-0.55 → whileai-0.57}/whileai/simulations/generate/__init__.py +0 -0
  30. {whileai-0.55 → whileai-0.57}/whileai/simulations/generate/actionspace.py +0 -0
  31. {whileai-0.55 → whileai-0.57}/whileai/simulations/generate/adapters.py +0 -0
  32. {whileai-0.55 → whileai-0.57}/whileai/simulations/generate/agents.py +0 -0
  33. {whileai-0.55 → whileai-0.57}/whileai/simulations/generate/coverage.py +0 -0
  34. {whileai-0.55 → whileai-0.57}/whileai/simulations/generate/diversity.py +0 -0
  35. {whileai-0.55 → whileai-0.57}/whileai/simulations/generate/embeddings.py +0 -0
  36. {whileai-0.55 → whileai-0.57}/whileai/simulations/generate/explore.py +0 -0
  37. {whileai-0.55 → whileai-0.57}/whileai/simulations/generate/generator.py +0 -0
  38. {whileai-0.55 → whileai-0.57}/whileai/simulations/generate/offline_agent.py +0 -0
  39. {whileai-0.55 → whileai-0.57}/whileai/simulations/generate/scenarios.py +0 -0
  40. {whileai-0.55 → whileai-0.57}/whileai/simulations/generate/usage_meter.py +0 -0
  41. {whileai-0.55 → whileai-0.57}/whileai/simulations/ingest/__init__.py +0 -0
  42. {whileai-0.55 → whileai-0.57}/whileai/simulations/ingest/otel.py +0 -0
  43. {whileai-0.55 → whileai-0.57}/whileai/simulations/ingest/traces.py +0 -0
  44. {whileai-0.55 → whileai-0.57}/whileai/simulations/monitor.py +0 -0
  45. {whileai-0.55 → whileai-0.57}/whileai/simulations/py.typed +0 -0
  46. {whileai-0.55 → whileai-0.57}/whileai/simulations/run/__init__.py +0 -0
  47. {whileai-0.55 → whileai-0.57}/whileai/simulations/run/config.py +0 -0
  48. {whileai-0.55 → whileai-0.57}/whileai/simulations/run/rows.py +0 -0
  49. {whileai-0.55 → whileai-0.57}/whileai/simulations/run/spec.py +0 -0
  50. {whileai-0.55 → whileai-0.57}/whileai/simulations/schema.py +0 -0
  51. {whileai-0.55 → whileai-0.57}/whileai/simulations/schemas/row-v1.json +0 -0
  52. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/__init__.py +0 -0
  53. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/agreement.py +0 -0
  54. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/checklist.py +0 -0
  55. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/curriculum.py +0 -0
  56. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/grade_llm.py +0 -0
  57. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/grading.py +0 -0
  58. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/grounding.py +0 -0
  59. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/hack_scan.py +0 -0
  60. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/judge_trust.py +0 -0
  61. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/labels.py +0 -0
  62. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/llm_judge.py +0 -0
  63. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/logprobs.py +0 -0
  64. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/markers.py +0 -0
  65. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/pairwise.py +0 -0
  66. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/passat.py +0 -0
  67. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/preflight.py +0 -0
  68. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/privileged.py +0 -0
  69. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/publish_gate.py +0 -0
  70. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/quality.py +0 -0
  71. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/reference.py +0 -0
  72. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/rubric.py +0 -0
  73. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/spec.py +0 -0
  74. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/stage.py +0 -0
  75. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/style.py +0 -0
  76. {whileai-0.55 → whileai-0.57}/whileai/simulations/score/trace.py +0 -0
  77. {whileai-0.55 → whileai-0.57}/whileai/simulations/training.py +0 -0
  78. {whileai-0.55 → whileai-0.57}/whileai/simulations/verify/__init__.py +0 -0
  79. {whileai-0.55 → whileai-0.57}/whileai/simulations/verify/base.py +0 -0
  80. {whileai-0.55 → whileai-0.57}/whileai/simulations/verify/code.py +0 -0
  81. {whileai-0.55 → whileai-0.57}/whileai/simulations/verify/math.py +0 -0
  82. {whileai-0.55 → whileai-0.57}/whileai/simulations/verify/structured.py +0 -0
  83. {whileai-0.55 → whileai-0.57}/whileai/simulations/verify/text.py +0 -0
  84. {whileai-0.55 → whileai-0.57}/whileai/simulations/world/__init__.py +0 -0
  85. {whileai-0.55 → whileai-0.57}/whileai/simulations/world/sandbox.py +0 -0
  86. {whileai-0.55 → whileai-0.57}/whileai.egg-info/dependency_links.txt +0 -0
  87. {whileai-0.55 → whileai-0.57}/whileai.egg-info/entry_points.txt +0 -0
  88. {whileai-0.55 → whileai-0.57}/whileai.egg-info/requires.txt +0 -0
  89. {whileai-0.55 → whileai-0.57}/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.55
3
+ Version: 0.57
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,6 +187,34 @@ 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
@@ -421,8 +449,8 @@ When the rows carry none of that metadata the export warns: the reward
421
449
  reduces to `conduct_grade`, a process reward, and a policy trained on it
422
450
  alone learns to call nothing (`recipes/03-select/prime-intellect-rl`). `wai.load_environment(spec)`
423
451
  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.
452
+ 'whileai[rl]'`); the [tool-call-efficiency](https://huggingface.co/datasets/zero-proof-ai/tool-call-efficiency)
453
+ dataset is the same shape built by hand over an executable world with a hidden test suite.
426
454
 
427
455
  Training notes, each with the chapter of rlhfbook.com behind it. Calibrate
428
456
  difficulty with 8 to 16 rollouts per task before exporting so the band is
@@ -626,6 +654,7 @@ network.
626
654
  | 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
655
  | 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
656
  | 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. |
657
+ | 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
658
  | 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
659
  | 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
660
  | 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.55
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,6 +151,34 @@ 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
@@ -421,8 +413,8 @@ When the rows carry none of that metadata the export warns: the reward
421
413
  reduces to `conduct_grade`, a process reward, and a policy trained on it
422
414
  alone learns to call nothing (`recipes/03-select/prime-intellect-rl`). `wai.load_environment(spec)`
423
415
  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.
416
+ 'whileai[rl]'`); the [tool-call-efficiency](https://huggingface.co/datasets/zero-proof-ai/tool-call-efficiency)
417
+ dataset is the same shape built by hand over an executable world with a hidden test suite.
426
418
 
427
419
  Training notes, each with the chapter of rlhfbook.com behind it. Calibrate
428
420
  difficulty with 8 to 16 rollouts per task before exporting so the band is
@@ -626,6 +618,7 @@ network.
626
618
  | 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
619
  | 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
620
  | 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. |
621
+ | 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
622
  | 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
623
  | 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
624
  | 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.55"
7
+ version = "0.57"
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,
@@ -135,6 +137,7 @@ from .score.optimize import (
135
137
  DEFAULT_BAND,
136
138
  filter_rl_rows,
137
139
  group_signal,
140
+ next_round,
138
141
  optimize,
139
142
  recommend,
140
143
  select_for_rl,
@@ -162,7 +165,9 @@ from .score.stage import STAGES, format_stages, stage_of, stage_report, stamp_st
162
165
  from .score.stats import (
163
166
  compare_runs,
164
167
  decontaminate,
168
+ detectable_effect,
165
169
  eval_variance,
170
+ holdout_size,
166
171
  marker_summary,
167
172
  metric_summary,
168
173
  task_key,
@@ -232,6 +237,7 @@ __all__ = [
232
237
  "attach_holdout",
233
238
  "attach_labels",
234
239
  "attach_rubric",
240
+ "audit_grades",
235
241
  "behavior_signature",
236
242
  "behavioral_markers",
237
243
  "build_dimensions",
@@ -258,6 +264,7 @@ __all__ = [
258
264
  "delete_model",
259
265
  "delete_run",
260
266
  "delta_report",
267
+ "detectable_effect",
261
268
  "dimensions_from_traces",
262
269
  "drop_leaky_rows",
263
270
  "eval_variance",
@@ -269,6 +276,7 @@ __all__ = [
269
276
  "export_training",
270
277
  "filter_rl_rows",
271
278
  "flaw_rows",
279
+ "format_audit",
272
280
  "format_curriculum",
273
281
  "format_delta_report",
274
282
  "format_hack_monitor",
@@ -290,6 +298,7 @@ __all__ = [
290
298
  "hf_publish",
291
299
  "hf_publish_run",
292
300
  "hf_status",
301
+ "holdout_size",
293
302
  "hosted_model",
294
303
  "import_hf",
295
304
  "inspect",
@@ -317,6 +326,7 @@ __all__ = [
317
326
  "mine_traces",
318
327
  "models",
319
328
  "near_duplicate_prompts",
329
+ "next_round",
320
330
  "normalize_judge_result",
321
331
  "novelty",
322
332
  "open_ended_probes",
@@ -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"]
@@ -22,6 +22,7 @@ import time
22
22
  import urllib.error
23
23
  import urllib.parse
24
24
  import urllib.request
25
+ import warnings as _warnings
25
26
  from collections.abc import Sequence
26
27
  from typing import Any
27
28
 
@@ -242,6 +243,36 @@ def _meta_body(
242
243
  return body
243
244
 
244
245
 
246
+ HOLDOUT_PROVE_EFFECT = 0.05
247
+
248
+
249
+ def _warn_small_holdout(rows: Sequence[dict]) -> None:
250
+ """A holdout too small to prove a 5-point gain reads every round as
251
+ ``no_change_detected``; say so at push time, not after training (#257)."""
252
+ from ..score.stats import holdout_size, task_key
253
+
254
+ groups: dict[str, int] = {}
255
+ for row in rows:
256
+ if isinstance(row, dict):
257
+ key = task_key(row)
258
+ groups[key] = groups.get(key, 0) + 1
259
+ n_tasks = len(groups)
260
+ if not n_tasks:
261
+ return
262
+ try:
263
+ need = holdout_size(HOLDOUT_PROVE_EFFECT, rows=rows)
264
+ except ValueError:
265
+ need = holdout_size(HOLDOUT_PROVE_EFFECT, k=min(groups.values()))
266
+ if n_tasks < need["n_tasks"]:
267
+ _warnings.warn(
268
+ f"holdout has {n_tasks} tasks at k={need['k']}; proving a "
269
+ f"{HOLDOUT_PROVE_EFFECT:.0%} gain at 80% power needs about {need['n_tasks']} "
270
+ "(holdout_size). A smaller holdout reads a real gain that size as "
271
+ "no_change_detected.",
272
+ stacklevel=3,
273
+ )
274
+
275
+
245
276
  def push_rows(
246
277
  rows: list[dict],
247
278
  name: str,
@@ -279,6 +310,8 @@ def push_rows(
279
310
 
280
311
  gate_report = publish_gate(rows, mode=mode, endorsed=endorsed, strict_hacks=strict_hacks)
281
312
  check(rows, where="push_rows")
313
+ if purpose == "holdout":
314
+ _warn_small_holdout(rows)
282
315
  body: dict = {
283
316
  "name": name,
284
317
  **_meta_body(purpose, mode if mode in MODES else None, agent, description),
@@ -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"]