whileai 0.64__tar.gz → 0.66__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.66/PKG-INFO +276 -0
- whileai-0.66/README.md +239 -0
- {whileai-0.64 → whileai-0.66}/pyproject.toml +3 -3
- {whileai-0.64 → whileai-0.66}/whileai/ingest.py +13 -6
- whileai-0.66/whileai/platform.py +1013 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/__init__.py +2 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/data.py +11 -11
- {whileai-0.64 → whileai-0.66}/whileai/simulations/ingest/platform.py +58 -1
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/judging.py +16 -6
- whileai-0.66/whileai.egg-info/PKG-INFO +276 -0
- {whileai-0.64 → whileai-0.66}/whileai.egg-info/SOURCES.txt +1 -0
- {whileai-0.64 → whileai-0.66}/whileai.egg-info/requires.txt +1 -0
- whileai-0.64/PKG-INFO +0 -1315
- whileai-0.64/README.md +0 -1279
- whileai-0.64/whileai.egg-info/PKG-INFO +0 -1315
- {whileai-0.64 → whileai-0.66}/LICENSE +0 -0
- {whileai-0.64 → whileai-0.66}/MANIFEST.in +0 -0
- {whileai-0.64 → whileai-0.66}/setup.cfg +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/__init__.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/_env.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/auth.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/cli.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/init_evals.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/py.typed +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/__main__.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/environment.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/export.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/__init__.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/actionspace.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/adapters.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/agents.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/anthropic_backend.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/coverage.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/diversity.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/embeddings.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/explore.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/generator.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/offline_agent.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/scenarios.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/generate/usage_meter.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/ingest/__init__.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/ingest/otel.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/ingest/traces.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/monitor.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/py.typed +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/run/__init__.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/run/config.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/run/engine.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/run/rows.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/run/spec.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/schema.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/schemas/row-v1.json +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/__init__.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/agreement.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/audit.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/checklist.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/curriculum.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/delta.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/grade_llm.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/grading.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/grounding.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/hack_scan.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/hygiene.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/judge_trust.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/labels.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/llm_judge.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/logprobs.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/markers.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/optimize.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/pairwise.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/passat.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/preflight.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/privileged.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/publish_gate.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/quality.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/reference.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/rubric.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/spec.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/stage.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/stats.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/style.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/score/trace.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/simulation.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/text.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/training.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/verify/__init__.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/verify/base.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/verify/code.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/verify/math.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/verify/structured.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/verify/text.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/world/__init__.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/simulations/world/sandbox.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/templates/__init__.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai/templates/evals.py +0 -0
- {whileai-0.64 → whileai-0.66}/whileai.egg-info/dependency_links.txt +0 -0
- {whileai-0.64 → whileai-0.66}/whileai.egg-info/entry_points.txt +0 -0
- {whileai-0.64 → whileai-0.66}/whileai.egg-info/top_level.txt +0 -0
whileai-0.66/PKG-INFO
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: whileai
|
|
3
|
+
Version: 0.66
|
|
4
|
+
Summary: Post-training data and evaluation for tool-using LLM agents: simulate, grade, measure, select, train, export, with the checks from rlhfbook.com.
|
|
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: zp,zeroproof,while,evals,agent-evals,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
|
+
Requires-Dist: pydantic>=2.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.12; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.16; extra == "dev"
|
|
33
|
+
Requires-Dist: types-requests; extra == "dev"
|
|
34
|
+
Provides-Extra: rl
|
|
35
|
+
Requires-Dist: verifiers>=0.3; (python_version >= "3.11" and python_version < "3.14") and extra == "rl"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# whileai
|
|
39
|
+
|
|
40
|
+
[](https://github.com/whilehq/whileai-sdk/actions/workflows/ci.yml)
|
|
41
|
+
[](https://pypi.org/project/whileai/)
|
|
42
|
+
[](https://pypi.org/project/whileai/)
|
|
43
|
+
[](https://pypistats.org/packages/whileai)
|
|
44
|
+
[](.github/workflows/ci.yml)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+
|
|
47
|
+
Post-training data and evaluation for tool-using language-model agents.
|
|
48
|
+
|
|
49
|
+
`whileai` simulates the situations an agent can meet, rolls the agent
|
|
50
|
+
through them against a world that fails on schedule, grades every rollout
|
|
51
|
+
under one judge contract, and turns graded rows into SFT, preference and RL
|
|
52
|
+
data with the checks the literature says to run: pass@k with intervals,
|
|
53
|
+
difficulty bands, judge validation, decontamination, reward-hacking scans.
|
|
54
|
+
Every method names the chapter of [rlhfbook.com](https://rlhfbook.com)
|
|
55
|
+
(Lambert, *RLHF and LLM Post-Training*) or the paper it implements, and
|
|
56
|
+
every recipe reports a paired delta with a 95% interval on a held-out set,
|
|
57
|
+
never a mean alone.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install whileai # or: uv add whileai
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
One runtime dependency (`requests`), Python 3.10 to 3.13, typed. Formerly
|
|
64
|
+
`zeroproof`; that name still installs this package.
|
|
65
|
+
|
|
66
|
+
## Sixty seconds, offline
|
|
67
|
+
|
|
68
|
+
No key, no network. The seeded agent answers honestly and, on a labeled
|
|
69
|
+
fraction of rollouts, does one wrong thing on purpose, so a judge that
|
|
70
|
+
catches exactly those rows is a judge that works.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import whileai.simulations as wai
|
|
74
|
+
|
|
75
|
+
TOOLS = [
|
|
76
|
+
{
|
|
77
|
+
"type": "function",
|
|
78
|
+
"function": {
|
|
79
|
+
"name": "get_order",
|
|
80
|
+
"description": "Look up an order by id.",
|
|
81
|
+
"parameters": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {"order_id": {"type": "string"}},
|
|
84
|
+
"required": ["order_id"],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
data = wai.simulate(
|
|
91
|
+
wai.seeded_agent(TOOLS),
|
|
92
|
+
tools=TOOLS,
|
|
93
|
+
system_prompt="Help customers with orders.",
|
|
94
|
+
simulator=False, # template writer: no model
|
|
95
|
+
mode="rl", # k rollouts per prompt
|
|
96
|
+
repeats=4,
|
|
97
|
+
repeat_policy="fixed",
|
|
98
|
+
budget=64,
|
|
99
|
+
)
|
|
100
|
+
scored = data.grade(judge=lambda row: {"reward": int(not row["seeded"])})
|
|
101
|
+
print(scored.pass_at) # pass@1 [95% CI over tasks] | pass^k | pass@k | headroom
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
pass@1 0.67 [0.55..0.78] | pass^4 (pass_pow_k) 0.19 [0.00..0.38] | pass@4 1.00 [1.00..1.00] | headroom 0.33 (16 groups, k=4)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Swap in your agent as `agent(message) -> {"steps": [...], "final_text": ...}`,
|
|
109
|
+
a model spec such as `"openai:gpt-4.1-mini"`, or a model you serve. The
|
|
110
|
+
judge is any callable `row -> {"reward": 0..1}`, a verifier such as
|
|
111
|
+
`CodeExec`, or the hosted judge.
|
|
112
|
+
|
|
113
|
+
## The loop
|
|
114
|
+
|
|
115
|
+
| Step | Call | What it computes | Source |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| Simulate | `simulate(agent, tools=, system_prompt=, mode="rl", repeats=k)` | a covering grid over tools, world state and user stance; k rollouts per prompt; tool faults on a schedule | ch. 12, 13 |
|
|
118
|
+
| Grade | `data.grade(judge=)`, `verify.MathEqual`, `verify.CodeExec` | reward per rollout under one contract; verifiable rewards where the answer is checkable | ch. 5, 7 |
|
|
119
|
+
| Validate the judge | `judge_trust`, `judge_probes` | agreement and Cohen's kappa against human gold; length bias; exploit probes (filler, rubric echo, unbacked success claim) | ch. 5, 14 |
|
|
120
|
+
| Measure | `pass_at`, `delta_report`, `eval_variance`, `holdout_size` | pass@1, pass^k, pass@k with bootstrap intervals over tasks; paired before/after with a permutation p-value; re-run noise band; power | ch. 16, app. C |
|
|
121
|
+
| Select | `optimize(mode="rl"\|"sft")`, `build_preference_pairs`, `curriculum` | 20 to 80% difficulty band, unanimous-group drop, within-task dedupe, rejection sampling, length-matched pairs, easy-to-hard schedule | ch. 6, 7, 9, 11 |
|
|
122
|
+
| Guard | `decontaminate`, `hack_scan`, `trace_markers`, `HackMonitor` | 8-gram and semantic overlap with the eval set; within-task reward-feature correlation against a shuffle floor; trajectory lies (claimed tests, phantom edits) | ch. 14, 16 |
|
|
123
|
+
| Train and export | `export_dataset`, `export_environment`, `train`, `serve` | loss masks and unrolled turns; a `verifiers` environment for GRPO; hosted LoRA SFT, GRPO, DPO and reward-model runs | ch. 4, 6, 8 |
|
|
124
|
+
|
|
125
|
+
Chapters: 04 instruction tuning, 05 reward models, 06 policy gradients, 07
|
|
126
|
+
reasoning, 08 direct alignment, 09 rejection sampling, 11 preference data,
|
|
127
|
+
12 synthetic data, 13 tools, 14 over-optimization, 15 regularization, 16
|
|
128
|
+
evaluation, 17 character, appendix C practice.
|
|
129
|
+
|
|
130
|
+
## What the science looks like here
|
|
131
|
+
|
|
132
|
+
**Supervised fine-tuning.** `optimize(mode="sft")` is rejection sampling:
|
|
133
|
+
the highest-reward completion per prompt above `min_reward`, with random
|
|
134
|
+
selectors as the chance control (ch. 9). Exported rows carry a per-message
|
|
135
|
+
`loss_mask` (loss on agent turns only, never on tool output), `unroll=True`
|
|
136
|
+
turns an N-turn conversation into N samples each trained on the context it
|
|
137
|
+
had, and `format="trl"` is the shape `SFTTrainer` loads (ch. 4).
|
|
138
|
+
|
|
139
|
+
**RL with verifiable rewards.** A reward is a program where it can be:
|
|
140
|
+
`MathEqual`, `CodeExec` against hidden tests, `JSONSchema`, composed with
|
|
141
|
+
`All` and `Weighted` (ch. 7, 13). `mode="rl"` allocates rollouts
|
|
142
|
+
successively: two per prompt as a probe, filled to k only where the group
|
|
143
|
+
splits, because a unanimous group carries zero advantage (DAPO dynamic
|
|
144
|
+
sampling, ch. 6). `optimize(mode="rl")` keeps the 20 to 80% pass-rate band
|
|
145
|
+
with the interval on each task's rate, handles overlong rollouts by policy,
|
|
146
|
+
and `export_environment` writes the task set, world and reward as a
|
|
147
|
+
`verifiers` package for Prime Intellect or TRL. Rows carry sampled
|
|
148
|
+
logprobs for the importance ratio, `staleness_report` flags off-policy
|
|
149
|
+
rows, and `mean_kl` reads the drift from a reference (ch. 6, 15).
|
|
150
|
+
|
|
151
|
+
**Character training.** A constitution is a versioned object: `load_spec`
|
|
152
|
+
hashes its principles into `spec.version`, `stamp_spec` tags the rows a run
|
|
153
|
+
targeted, and the judge is checked against the spec's own labels before it
|
|
154
|
+
grades. Pairs are length-matched so the update learns the trait and not
|
|
155
|
+
the word count, and `delta_report(must_not_regress=spec.behaviors())` fails
|
|
156
|
+
the run that traded one trait for another (ch. 17).
|
|
157
|
+
[docs/character-training.md](docs/character-training.md).
|
|
158
|
+
|
|
159
|
+
**Evaluation.** Every pass@1 is a bootstrap over tasks, not rollouts;
|
|
160
|
+
`simulate(tasks=base, runs=3)` replays the same eval three times and
|
|
161
|
+
`delta_report` refuses a verdict inside twice the re-run standard
|
|
162
|
+
deviation. `holdout_size(effect, before=, after=)` reads the per-task paired
|
|
163
|
+
spread off a previous eval and says how many prompts prove a gain at 80%
|
|
164
|
+
power. `decontaminate` applies the Llama 2 8-gram rule plus task identity
|
|
165
|
+
and an optional embedding pass (ch. 16, app. C).
|
|
166
|
+
[docs/evals.md](docs/evals.md).
|
|
167
|
+
|
|
168
|
+
**Over-optimization.** `hack_scan` centers reward and every candidate
|
|
169
|
+
feature within task, ranks by correlation, and floors it against a
|
|
170
|
+
within-task shuffle, so it finds the delimiter or phrase the judge pays for.
|
|
171
|
+
`judge_probes` tries the exploits a policy finds first. `delta_report(proxy=,
|
|
172
|
+
target=)` fails when the training reward rose and the target did not.
|
|
173
|
+
`HackMonitor` runs the same scan inside a TRL loop and can stop the run
|
|
174
|
+
(ch. 14). [docs/reward-hacking.md](docs/reward-hacking.md).
|
|
175
|
+
|
|
176
|
+
## Recipes
|
|
177
|
+
|
|
178
|
+
One post-training run as five steps, each a runnable script with what you
|
|
179
|
+
learn, what it needs, and how long it takes. Offline recipes take seconds;
|
|
180
|
+
CI runs every one on every pull request.
|
|
181
|
+
|
|
182
|
+
| Step | Recipes |
|
|
183
|
+
|---|---|
|
|
184
|
+
| [01-simulate](recipes/01-simulate) | bring your own agent, verifiers, a coding agent traced to the platform |
|
|
185
|
+
| [02-measure](recipes/02-measure) | eval your agent, pass@k, is your eval any good, reward hacking, safety evals |
|
|
186
|
+
| [03-select](recipes/03-select) | the row schema, GRPO data with a gradient gate, character training |
|
|
187
|
+
| [04-train](recipes/04-train) | hosted loop, identity SFT, GRPO and DPO on Modal, text-to-SQL hill climb |
|
|
188
|
+
| [05-export](recipes/05-export) | Hugging Face datasets and adapters |
|
|
189
|
+
| [papers](recipes/papers) | one recent paper per recipe, one change to a step, the number it moved with its interval |
|
|
190
|
+
|
|
191
|
+
Index: [recipes/README.md](recipes/README.md). `whileai init-evals` scaffolds
|
|
192
|
+
the eval recipe around the agent it finds in your project.
|
|
193
|
+
|
|
194
|
+
## Models
|
|
195
|
+
|
|
196
|
+
Any OpenAI-compatible chat endpoint that returns tool calls can play the
|
|
197
|
+
agent, write the situations, play the user, or judge. A spec names the
|
|
198
|
+
backend and the model and works wherever one is accepted.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
data = wai.simulate(
|
|
202
|
+
agent="anthropic:claude-haiku-4-5", # or openai:<model>, vllm:<model>@<url>, ollama:<model>
|
|
203
|
+
simulator="anthropic:claude-sonnet-5", # the situation writer
|
|
204
|
+
tools=TOOLS,
|
|
205
|
+
system_prompt=POLICY,
|
|
206
|
+
output="rollout.jsonl",
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
With no `agent=`, the run uses While-hosted Qwen on your account key
|
|
211
|
+
(`whileai login`), with Phi-4 as the judge so the judge is never the policy.
|
|
212
|
+
|
|
213
|
+
## Platform
|
|
214
|
+
|
|
215
|
+
Datasets, training runs and served adapters on [zeroproofai.com](https://zeroproofai.com/platform),
|
|
216
|
+
from the same objects.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
v1 = data.push("refunds-v1", holdout=0.2, gate=True) # gated: refuses gradient-free RL data
|
|
220
|
+
run = wai.train(v1["datasetId"], method="grpo", steps=200) # sft | grpo | dpo | rm
|
|
221
|
+
run.wait()
|
|
222
|
+
model = wai.serve("refunds-v2", run) # OpenAI-compatible endpoint
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Your own trainer reports into the same run page through `wai.TrainerCallback`
|
|
226
|
+
or `wai.training_run(...)`. Traces in production come back as `traces=`,
|
|
227
|
+
which aims the next run's grid at the situations that failed.
|
|
228
|
+
|
|
229
|
+
## Documentation
|
|
230
|
+
|
|
231
|
+
| Page | What it covers |
|
|
232
|
+
|---|---|
|
|
233
|
+
| [docs/reference.md](docs/reference.md) | every call, knob, report and gate, in the order a run happens |
|
|
234
|
+
| [docs/engine.md](docs/engine.md) | how a row is made: the draw, the grid, the search arms, the rollout, the split |
|
|
235
|
+
| [docs/simulations.md](docs/simulations.md) | why the engine is shaped this way |
|
|
236
|
+
| [docs/evals.md](docs/evals.md) | pass rates with intervals and a CI gate for an agent you already have |
|
|
237
|
+
| [docs/reward-hacking.md](docs/reward-hacking.md) | before, during and after training |
|
|
238
|
+
| [docs/safety-evals.md](docs/safety-evals.md) | injection, exfiltration, unauthorized writes, over-refusal controls |
|
|
239
|
+
| [docs/character-training.md](docs/character-training.md) | constitution to graded rows to length-matched pairs |
|
|
240
|
+
| [CHANGELOG.md](CHANGELOG.md) | one entry per release, with the pull request |
|
|
241
|
+
|
|
242
|
+
The row schema is `Task`, `Rollout`, `Judgment`, `Marker`
|
|
243
|
+
(`whileai/simulations/schemas/row-v1.json`); every training target is a
|
|
244
|
+
projection of those four.
|
|
245
|
+
|
|
246
|
+
## Development
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
uv sync --extra dev
|
|
250
|
+
uv run pytest # about two minutes, no network
|
|
251
|
+
uv run ruff check . && uv run ruff format --check .
|
|
252
|
+
uv run mypy
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
CI runs the suite on Python 3.10 through 3.13, lint and types, line
|
|
256
|
+
coverage gated at 90%, every recipe's `smoke.sh`, a plain-pip install of
|
|
257
|
+
the built wheel into a clean venv, and the version gate. Contributions:
|
|
258
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
259
|
+
|
|
260
|
+
## Cite
|
|
261
|
+
|
|
262
|
+
```bibtex
|
|
263
|
+
@software{whileai,
|
|
264
|
+
title = {whileai: post-training data and evaluation for tool-using agents},
|
|
265
|
+
author = {{While}},
|
|
266
|
+
year = {2026},
|
|
267
|
+
url = {https://github.com/whilehq/whileai-sdk}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The methods follow Lambert, N. (2025). *Reinforcement Learning from Human
|
|
272
|
+
Feedback*. arXiv:2504.12501. [rlhfbook.com](https://rlhfbook.com).
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
Apache-2.0
|
whileai-0.66/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# whileai
|
|
2
|
+
|
|
3
|
+
[](https://github.com/whilehq/whileai-sdk/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/whileai/)
|
|
5
|
+
[](https://pypi.org/project/whileai/)
|
|
6
|
+
[](https://pypistats.org/packages/whileai)
|
|
7
|
+
[](.github/workflows/ci.yml)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
Post-training data and evaluation for tool-using language-model agents.
|
|
11
|
+
|
|
12
|
+
`whileai` simulates the situations an agent can meet, rolls the agent
|
|
13
|
+
through them against a world that fails on schedule, grades every rollout
|
|
14
|
+
under one judge contract, and turns graded rows into SFT, preference and RL
|
|
15
|
+
data with the checks the literature says to run: pass@k with intervals,
|
|
16
|
+
difficulty bands, judge validation, decontamination, reward-hacking scans.
|
|
17
|
+
Every method names the chapter of [rlhfbook.com](https://rlhfbook.com)
|
|
18
|
+
(Lambert, *RLHF and LLM Post-Training*) or the paper it implements, and
|
|
19
|
+
every recipe reports a paired delta with a 95% interval on a held-out set,
|
|
20
|
+
never a mean alone.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install whileai # or: uv add whileai
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
One runtime dependency (`requests`), Python 3.10 to 3.13, typed. Formerly
|
|
27
|
+
`zeroproof`; that name still installs this package.
|
|
28
|
+
|
|
29
|
+
## Sixty seconds, offline
|
|
30
|
+
|
|
31
|
+
No key, no network. The seeded agent answers honestly and, on a labeled
|
|
32
|
+
fraction of rollouts, does one wrong thing on purpose, so a judge that
|
|
33
|
+
catches exactly those rows is a judge that works.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import whileai.simulations as wai
|
|
37
|
+
|
|
38
|
+
TOOLS = [
|
|
39
|
+
{
|
|
40
|
+
"type": "function",
|
|
41
|
+
"function": {
|
|
42
|
+
"name": "get_order",
|
|
43
|
+
"description": "Look up an order by id.",
|
|
44
|
+
"parameters": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"properties": {"order_id": {"type": "string"}},
|
|
47
|
+
"required": ["order_id"],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
data = wai.simulate(
|
|
54
|
+
wai.seeded_agent(TOOLS),
|
|
55
|
+
tools=TOOLS,
|
|
56
|
+
system_prompt="Help customers with orders.",
|
|
57
|
+
simulator=False, # template writer: no model
|
|
58
|
+
mode="rl", # k rollouts per prompt
|
|
59
|
+
repeats=4,
|
|
60
|
+
repeat_policy="fixed",
|
|
61
|
+
budget=64,
|
|
62
|
+
)
|
|
63
|
+
scored = data.grade(judge=lambda row: {"reward": int(not row["seeded"])})
|
|
64
|
+
print(scored.pass_at) # pass@1 [95% CI over tasks] | pass^k | pass@k | headroom
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
pass@1 0.67 [0.55..0.78] | pass^4 (pass_pow_k) 0.19 [0.00..0.38] | pass@4 1.00 [1.00..1.00] | headroom 0.33 (16 groups, k=4)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Swap in your agent as `agent(message) -> {"steps": [...], "final_text": ...}`,
|
|
72
|
+
a model spec such as `"openai:gpt-4.1-mini"`, or a model you serve. The
|
|
73
|
+
judge is any callable `row -> {"reward": 0..1}`, a verifier such as
|
|
74
|
+
`CodeExec`, or the hosted judge.
|
|
75
|
+
|
|
76
|
+
## The loop
|
|
77
|
+
|
|
78
|
+
| Step | Call | What it computes | Source |
|
|
79
|
+
|---|---|---|---|
|
|
80
|
+
| Simulate | `simulate(agent, tools=, system_prompt=, mode="rl", repeats=k)` | a covering grid over tools, world state and user stance; k rollouts per prompt; tool faults on a schedule | ch. 12, 13 |
|
|
81
|
+
| Grade | `data.grade(judge=)`, `verify.MathEqual`, `verify.CodeExec` | reward per rollout under one contract; verifiable rewards where the answer is checkable | ch. 5, 7 |
|
|
82
|
+
| Validate the judge | `judge_trust`, `judge_probes` | agreement and Cohen's kappa against human gold; length bias; exploit probes (filler, rubric echo, unbacked success claim) | ch. 5, 14 |
|
|
83
|
+
| Measure | `pass_at`, `delta_report`, `eval_variance`, `holdout_size` | pass@1, pass^k, pass@k with bootstrap intervals over tasks; paired before/after with a permutation p-value; re-run noise band; power | ch. 16, app. C |
|
|
84
|
+
| Select | `optimize(mode="rl"\|"sft")`, `build_preference_pairs`, `curriculum` | 20 to 80% difficulty band, unanimous-group drop, within-task dedupe, rejection sampling, length-matched pairs, easy-to-hard schedule | ch. 6, 7, 9, 11 |
|
|
85
|
+
| Guard | `decontaminate`, `hack_scan`, `trace_markers`, `HackMonitor` | 8-gram and semantic overlap with the eval set; within-task reward-feature correlation against a shuffle floor; trajectory lies (claimed tests, phantom edits) | ch. 14, 16 |
|
|
86
|
+
| Train and export | `export_dataset`, `export_environment`, `train`, `serve` | loss masks and unrolled turns; a `verifiers` environment for GRPO; hosted LoRA SFT, GRPO, DPO and reward-model runs | ch. 4, 6, 8 |
|
|
87
|
+
|
|
88
|
+
Chapters: 04 instruction tuning, 05 reward models, 06 policy gradients, 07
|
|
89
|
+
reasoning, 08 direct alignment, 09 rejection sampling, 11 preference data,
|
|
90
|
+
12 synthetic data, 13 tools, 14 over-optimization, 15 regularization, 16
|
|
91
|
+
evaluation, 17 character, appendix C practice.
|
|
92
|
+
|
|
93
|
+
## What the science looks like here
|
|
94
|
+
|
|
95
|
+
**Supervised fine-tuning.** `optimize(mode="sft")` is rejection sampling:
|
|
96
|
+
the highest-reward completion per prompt above `min_reward`, with random
|
|
97
|
+
selectors as the chance control (ch. 9). Exported rows carry a per-message
|
|
98
|
+
`loss_mask` (loss on agent turns only, never on tool output), `unroll=True`
|
|
99
|
+
turns an N-turn conversation into N samples each trained on the context it
|
|
100
|
+
had, and `format="trl"` is the shape `SFTTrainer` loads (ch. 4).
|
|
101
|
+
|
|
102
|
+
**RL with verifiable rewards.** A reward is a program where it can be:
|
|
103
|
+
`MathEqual`, `CodeExec` against hidden tests, `JSONSchema`, composed with
|
|
104
|
+
`All` and `Weighted` (ch. 7, 13). `mode="rl"` allocates rollouts
|
|
105
|
+
successively: two per prompt as a probe, filled to k only where the group
|
|
106
|
+
splits, because a unanimous group carries zero advantage (DAPO dynamic
|
|
107
|
+
sampling, ch. 6). `optimize(mode="rl")` keeps the 20 to 80% pass-rate band
|
|
108
|
+
with the interval on each task's rate, handles overlong rollouts by policy,
|
|
109
|
+
and `export_environment` writes the task set, world and reward as a
|
|
110
|
+
`verifiers` package for Prime Intellect or TRL. Rows carry sampled
|
|
111
|
+
logprobs for the importance ratio, `staleness_report` flags off-policy
|
|
112
|
+
rows, and `mean_kl` reads the drift from a reference (ch. 6, 15).
|
|
113
|
+
|
|
114
|
+
**Character training.** A constitution is a versioned object: `load_spec`
|
|
115
|
+
hashes its principles into `spec.version`, `stamp_spec` tags the rows a run
|
|
116
|
+
targeted, and the judge is checked against the spec's own labels before it
|
|
117
|
+
grades. Pairs are length-matched so the update learns the trait and not
|
|
118
|
+
the word count, and `delta_report(must_not_regress=spec.behaviors())` fails
|
|
119
|
+
the run that traded one trait for another (ch. 17).
|
|
120
|
+
[docs/character-training.md](docs/character-training.md).
|
|
121
|
+
|
|
122
|
+
**Evaluation.** Every pass@1 is a bootstrap over tasks, not rollouts;
|
|
123
|
+
`simulate(tasks=base, runs=3)` replays the same eval three times and
|
|
124
|
+
`delta_report` refuses a verdict inside twice the re-run standard
|
|
125
|
+
deviation. `holdout_size(effect, before=, after=)` reads the per-task paired
|
|
126
|
+
spread off a previous eval and says how many prompts prove a gain at 80%
|
|
127
|
+
power. `decontaminate` applies the Llama 2 8-gram rule plus task identity
|
|
128
|
+
and an optional embedding pass (ch. 16, app. C).
|
|
129
|
+
[docs/evals.md](docs/evals.md).
|
|
130
|
+
|
|
131
|
+
**Over-optimization.** `hack_scan` centers reward and every candidate
|
|
132
|
+
feature within task, ranks by correlation, and floors it against a
|
|
133
|
+
within-task shuffle, so it finds the delimiter or phrase the judge pays for.
|
|
134
|
+
`judge_probes` tries the exploits a policy finds first. `delta_report(proxy=,
|
|
135
|
+
target=)` fails when the training reward rose and the target did not.
|
|
136
|
+
`HackMonitor` runs the same scan inside a TRL loop and can stop the run
|
|
137
|
+
(ch. 14). [docs/reward-hacking.md](docs/reward-hacking.md).
|
|
138
|
+
|
|
139
|
+
## Recipes
|
|
140
|
+
|
|
141
|
+
One post-training run as five steps, each a runnable script with what you
|
|
142
|
+
learn, what it needs, and how long it takes. Offline recipes take seconds;
|
|
143
|
+
CI runs every one on every pull request.
|
|
144
|
+
|
|
145
|
+
| Step | Recipes |
|
|
146
|
+
|---|---|
|
|
147
|
+
| [01-simulate](recipes/01-simulate) | bring your own agent, verifiers, a coding agent traced to the platform |
|
|
148
|
+
| [02-measure](recipes/02-measure) | eval your agent, pass@k, is your eval any good, reward hacking, safety evals |
|
|
149
|
+
| [03-select](recipes/03-select) | the row schema, GRPO data with a gradient gate, character training |
|
|
150
|
+
| [04-train](recipes/04-train) | hosted loop, identity SFT, GRPO and DPO on Modal, text-to-SQL hill climb |
|
|
151
|
+
| [05-export](recipes/05-export) | Hugging Face datasets and adapters |
|
|
152
|
+
| [papers](recipes/papers) | one recent paper per recipe, one change to a step, the number it moved with its interval |
|
|
153
|
+
|
|
154
|
+
Index: [recipes/README.md](recipes/README.md). `whileai init-evals` scaffolds
|
|
155
|
+
the eval recipe around the agent it finds in your project.
|
|
156
|
+
|
|
157
|
+
## Models
|
|
158
|
+
|
|
159
|
+
Any OpenAI-compatible chat endpoint that returns tool calls can play the
|
|
160
|
+
agent, write the situations, play the user, or judge. A spec names the
|
|
161
|
+
backend and the model and works wherever one is accepted.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
data = wai.simulate(
|
|
165
|
+
agent="anthropic:claude-haiku-4-5", # or openai:<model>, vllm:<model>@<url>, ollama:<model>
|
|
166
|
+
simulator="anthropic:claude-sonnet-5", # the situation writer
|
|
167
|
+
tools=TOOLS,
|
|
168
|
+
system_prompt=POLICY,
|
|
169
|
+
output="rollout.jsonl",
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
With no `agent=`, the run uses While-hosted Qwen on your account key
|
|
174
|
+
(`whileai login`), with Phi-4 as the judge so the judge is never the policy.
|
|
175
|
+
|
|
176
|
+
## Platform
|
|
177
|
+
|
|
178
|
+
Datasets, training runs and served adapters on [zeroproofai.com](https://zeroproofai.com/platform),
|
|
179
|
+
from the same objects.
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
v1 = data.push("refunds-v1", holdout=0.2, gate=True) # gated: refuses gradient-free RL data
|
|
183
|
+
run = wai.train(v1["datasetId"], method="grpo", steps=200) # sft | grpo | dpo | rm
|
|
184
|
+
run.wait()
|
|
185
|
+
model = wai.serve("refunds-v2", run) # OpenAI-compatible endpoint
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Your own trainer reports into the same run page through `wai.TrainerCallback`
|
|
189
|
+
or `wai.training_run(...)`. Traces in production come back as `traces=`,
|
|
190
|
+
which aims the next run's grid at the situations that failed.
|
|
191
|
+
|
|
192
|
+
## Documentation
|
|
193
|
+
|
|
194
|
+
| Page | What it covers |
|
|
195
|
+
|---|---|
|
|
196
|
+
| [docs/reference.md](docs/reference.md) | every call, knob, report and gate, in the order a run happens |
|
|
197
|
+
| [docs/engine.md](docs/engine.md) | how a row is made: the draw, the grid, the search arms, the rollout, the split |
|
|
198
|
+
| [docs/simulations.md](docs/simulations.md) | why the engine is shaped this way |
|
|
199
|
+
| [docs/evals.md](docs/evals.md) | pass rates with intervals and a CI gate for an agent you already have |
|
|
200
|
+
| [docs/reward-hacking.md](docs/reward-hacking.md) | before, during and after training |
|
|
201
|
+
| [docs/safety-evals.md](docs/safety-evals.md) | injection, exfiltration, unauthorized writes, over-refusal controls |
|
|
202
|
+
| [docs/character-training.md](docs/character-training.md) | constitution to graded rows to length-matched pairs |
|
|
203
|
+
| [CHANGELOG.md](CHANGELOG.md) | one entry per release, with the pull request |
|
|
204
|
+
|
|
205
|
+
The row schema is `Task`, `Rollout`, `Judgment`, `Marker`
|
|
206
|
+
(`whileai/simulations/schemas/row-v1.json`); every training target is a
|
|
207
|
+
projection of those four.
|
|
208
|
+
|
|
209
|
+
## Development
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
uv sync --extra dev
|
|
213
|
+
uv run pytest # about two minutes, no network
|
|
214
|
+
uv run ruff check . && uv run ruff format --check .
|
|
215
|
+
uv run mypy
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
CI runs the suite on Python 3.10 through 3.13, lint and types, line
|
|
219
|
+
coverage gated at 90%, every recipe's `smoke.sh`, a plain-pip install of
|
|
220
|
+
the built wheel into a clean venv, and the version gate. Contributions:
|
|
221
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
222
|
+
|
|
223
|
+
## Cite
|
|
224
|
+
|
|
225
|
+
```bibtex
|
|
226
|
+
@software{whileai,
|
|
227
|
+
title = {whileai: post-training data and evaluation for tool-using agents},
|
|
228
|
+
author = {{While}},
|
|
229
|
+
year = {2026},
|
|
230
|
+
url = {https://github.com/whilehq/whileai-sdk}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The methods follow Lambert, N. (2025). *Reinforcement Learning from Human
|
|
235
|
+
Feedback*. arXiv:2504.12501. [rlhfbook.com](https://rlhfbook.com).
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
Apache-2.0
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "whileai"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.66"
|
|
8
|
+
description = "Post-training data and evaluation for tool-using LLM agents: simulate, grade, measure, select, train, export, with the checks from rlhfbook.com."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
|
-
dependencies = ["requests>=2.25.0"]
|
|
12
|
+
dependencies = ["requests>=2.25.0", "pydantic>=2.0"]
|
|
13
13
|
authors = [{name = "While"}]
|
|
14
14
|
keywords = [
|
|
15
15
|
"zp", "zeroproof", "while", "evals", "agent-evals",
|
|
@@ -39,6 +39,13 @@ from whileai._env import getenv
|
|
|
39
39
|
_DEFAULT_TRACE_URL = "https://api.zeroproofai.com"
|
|
40
40
|
_GZIP_MAGIC = b"\x1f\x8b"
|
|
41
41
|
|
|
42
|
+
# The resource attribute that names the dataset. The gate reads
|
|
43
|
+
# `zeroproof.dataset` and nothing else, so sending only the whileai spelling
|
|
44
|
+
# lands every batch in a dataset called `traces` whatever you asked for, with
|
|
45
|
+
# a 202 that says so too late to notice. Both are written: the second costs
|
|
46
|
+
# one attribute and means the rename needs no release.
|
|
47
|
+
_DATASET_KEYS = ("zeroproof.dataset", "whileai.dataset")
|
|
48
|
+
|
|
42
49
|
|
|
43
50
|
class WhileIngestError(Exception):
|
|
44
51
|
"""Raised when the gate rejects a trace batch."""
|
|
@@ -67,7 +74,7 @@ def otel_env(api_key: str, dataset: str = "traces", base_url: str | None = None)
|
|
|
67
74
|
"OTEL_EXPORTER_OTLP_HEADERS": "x-api-key=" + api_key,
|
|
68
75
|
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/json",
|
|
69
76
|
# Resource attribute the gate reads to name the dataset.
|
|
70
|
-
"OTEL_RESOURCE_ATTRIBUTES": "
|
|
77
|
+
"OTEL_RESOURCE_ATTRIBUTES": ",".join(k + "=" + dataset for k in _DATASET_KEYS),
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
|
|
@@ -133,9 +140,9 @@ def ingest_traces(
|
|
|
133
140
|
Push a local OTLP batch file end to end and return ``{datasetId, dataset,
|
|
134
141
|
rows}``.
|
|
135
142
|
|
|
136
|
-
``dataset`` overrides the dataset name by setting the
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
``dataset`` overrides the dataset name by setting the dataset resource
|
|
144
|
+
attribute on every resourceSpan, which requires reading the batch; leave
|
|
145
|
+
it unset to send the bytes untouched.
|
|
139
146
|
"""
|
|
140
147
|
with open(file, "rb") as fh:
|
|
141
148
|
body = fh.read()
|
|
@@ -148,9 +155,9 @@ def ingest_traces(
|
|
|
148
155
|
for resource_span in batch.get("resourceSpans", []):
|
|
149
156
|
resource = resource_span.setdefault("resource", {})
|
|
150
157
|
attributes = [
|
|
151
|
-
a for a in resource.get("attributes", []) if a.get("key")
|
|
158
|
+
a for a in resource.get("attributes", []) if a.get("key") not in _DATASET_KEYS
|
|
152
159
|
]
|
|
153
|
-
attributes
|
|
160
|
+
attributes += [{"key": k, "value": {"stringValue": dataset}} for k in _DATASET_KEYS]
|
|
154
161
|
resource["attributes"] = attributes
|
|
155
162
|
body = json.dumps(batch).encode("utf-8")
|
|
156
163
|
|