whileai 0.53__tar.gz → 0.55__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.53/whileai.egg-info → whileai-0.55}/PKG-INFO +8 -8
- {whileai-0.53 → whileai-0.55}/README.md +7 -7
- {whileai-0.53 → whileai-0.55}/pyproject.toml +1 -1
- {whileai-0.53 → whileai-0.55}/whileai/simulations/__init__.py +8 -3
- {whileai-0.53 → whileai-0.55}/whileai/simulations/data.py +8 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/environment.py +4 -4
- {whileai-0.53 → whileai-0.55}/whileai/simulations/export.py +46 -3
- {whileai-0.53 → whileai-0.55}/whileai/simulations/generate/agents.py +34 -2
- {whileai-0.53 → whileai-0.55}/whileai/simulations/run/engine.py +27 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/run/spec.py +2 -2
- {whileai-0.53 → whileai-0.55}/whileai/simulations/schema.py +26 -2
- {whileai-0.53 → whileai-0.55}/whileai/simulations/schemas/row-v1.json +1 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/delta.py +7 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/grading.py +11 -2
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/judging.py +52 -2
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/passat.py +11 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/privileged.py +8 -2
- {whileai-0.53 → whileai-0.55}/whileai/simulations/training.py +77 -8
- {whileai-0.53 → whileai-0.55/whileai.egg-info}/PKG-INFO +8 -8
- {whileai-0.53 → whileai-0.55}/LICENSE +0 -0
- {whileai-0.53 → whileai-0.55}/MANIFEST.in +0 -0
- {whileai-0.53 → whileai-0.55}/setup.cfg +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/__init__.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/_env.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/auth.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/cli.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/ingest.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/py.typed +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/__main__.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/generate/__init__.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/generate/actionspace.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/generate/adapters.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/generate/coverage.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/generate/diversity.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/generate/embeddings.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/generate/explore.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/generate/generator.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/generate/offline_agent.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/generate/scenarios.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/generate/usage_meter.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/ingest/__init__.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/ingest/otel.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/ingest/platform.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/ingest/traces.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/monitor.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/py.typed +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/run/__init__.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/run/config.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/run/rows.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/__init__.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/agreement.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/checklist.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/curriculum.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/grade_llm.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/grounding.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/hack_scan.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/hygiene.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/judge_trust.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/labels.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/llm_judge.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/logprobs.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/markers.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/optimize.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/pairwise.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/preflight.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/publish_gate.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/quality.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/reference.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/rubric.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/spec.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/stage.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/stats.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/style.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/score/trace.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/simulation.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/verify/__init__.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/verify/base.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/verify/code.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/verify/math.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/verify/structured.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/verify/text.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/world/__init__.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai/simulations/world/sandbox.py +0 -0
- {whileai-0.53 → whileai-0.55}/whileai.egg-info/SOURCES.txt +0 -0
- {whileai-0.53 → whileai-0.55}/whileai.egg-info/dependency_links.txt +0 -0
- {whileai-0.53 → whileai-0.55}/whileai.egg-info/entry_points.txt +0 -0
- {whileai-0.53 → whileai-0.55}/whileai.egg-info/requires.txt +0 -0
- {whileai-0.53 → whileai-0.55}/whileai.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: whileai
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.55
|
|
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
|
|
@@ -51,7 +51,7 @@ Two ways in, one engine. Give it the agent's tools and system prompt and it samp
|
|
|
51
51
|
|
|
52
52
|

|
|
53
53
|
|
|
54
|
-
A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The
|
|
54
|
+
A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [while.ai/docs/engine](https://while.ai/docs/engine).
|
|
55
55
|
|
|
56
56
|
## Overview
|
|
57
57
|
|
|
@@ -230,7 +230,7 @@ data.grade(rubric=RUBRIC) # 2 grade against the task rubric: reward 0/1 on ever
|
|
|
230
230
|
print(data.pass_at)
|
|
231
231
|
wai.judge_trust(data.trajectories) # 3 trust the numbers
|
|
232
232
|
rows, report = wai.optimize(data, mode="rl") # 4 prune to what carries gradient
|
|
233
|
-
entry = wai.push_rows(rows, "
|
|
233
|
+
entry = wai.push_rows(rows, "my-agent-rl-v1", gate=True, mode="rl") # 5 publish, gated
|
|
234
234
|
```
|
|
235
235
|
|
|
236
236
|
`situations=200, repeats=8` is a guess. `wai.recommend(tools=TOOLS, system_prompt=POLICY, mode="rl")` replaces it with numbers from this agent's own grid: [How much to run](#how-much-to-run).
|
|
@@ -391,9 +391,9 @@ installable `verifiers` package, the shape Prime Intellect and TRL read.
|
|
|
391
391
|
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY, mode="rl", repeats=8)
|
|
392
392
|
data.grade()
|
|
393
393
|
# reward and world must import by name in the trainer: a module-level function or "module:attr"
|
|
394
|
-
wai.export_environment(data, "envs/
|
|
395
|
-
# pip install -e envs/
|
|
396
|
-
# vf-eval
|
|
394
|
+
wai.export_environment(data, "envs/my-agent", reward=my_verifier)
|
|
395
|
+
# pip install -e envs/my-agent
|
|
396
|
+
# vf-eval my_agent -a '{"split": "holdout"}' -m <policy> -b <base url> -k <key var>
|
|
397
397
|
```
|
|
398
398
|
|
|
399
399
|
The package holds `spec.json` (system prompt, the tool schemas verbatim,
|
|
@@ -681,10 +681,10 @@ login`.
|
|
|
681
681
|
# export WHILEAI_DELEGATED_CREDENTIAL=credential["credential"]
|
|
682
682
|
|
|
683
683
|
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY)
|
|
684
|
-
v1 = data.push("
|
|
684
|
+
v1 = data.push("my-agent-explore-v1") # -> {"datasetId": "ds_...", ...}
|
|
685
685
|
|
|
686
686
|
# iterate, then push the next version with lineage
|
|
687
|
-
v2 = data.push("
|
|
687
|
+
v2 = data.push("my-agent-explore-v2", parent=v1["datasetId"])
|
|
688
688
|
|
|
689
689
|
wai.datasets() # list yours + storage used
|
|
690
690
|
rows = wai.pull(v1["datasetId"]) # rows, or pass path= for a file
|
|
@@ -15,7 +15,7 @@ Two ways in, one engine. Give it the agent's tools and system prompt and it samp
|
|
|
15
15
|
|
|
16
16
|

|
|
17
17
|
|
|
18
|
-
A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The
|
|
18
|
+
A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [while.ai/docs/engine](https://while.ai/docs/engine).
|
|
19
19
|
|
|
20
20
|
## Overview
|
|
21
21
|
|
|
@@ -194,7 +194,7 @@ data.grade(rubric=RUBRIC) # 2 grade against the task rubric: reward 0/1 on ever
|
|
|
194
194
|
print(data.pass_at)
|
|
195
195
|
wai.judge_trust(data.trajectories) # 3 trust the numbers
|
|
196
196
|
rows, report = wai.optimize(data, mode="rl") # 4 prune to what carries gradient
|
|
197
|
-
entry = wai.push_rows(rows, "
|
|
197
|
+
entry = wai.push_rows(rows, "my-agent-rl-v1", gate=True, mode="rl") # 5 publish, gated
|
|
198
198
|
```
|
|
199
199
|
|
|
200
200
|
`situations=200, repeats=8` is a guess. `wai.recommend(tools=TOOLS, system_prompt=POLICY, mode="rl")` replaces it with numbers from this agent's own grid: [How much to run](#how-much-to-run).
|
|
@@ -355,9 +355,9 @@ installable `verifiers` package, the shape Prime Intellect and TRL read.
|
|
|
355
355
|
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY, mode="rl", repeats=8)
|
|
356
356
|
data.grade()
|
|
357
357
|
# reward and world must import by name in the trainer: a module-level function or "module:attr"
|
|
358
|
-
wai.export_environment(data, "envs/
|
|
359
|
-
# pip install -e envs/
|
|
360
|
-
# vf-eval
|
|
358
|
+
wai.export_environment(data, "envs/my-agent", reward=my_verifier)
|
|
359
|
+
# pip install -e envs/my-agent
|
|
360
|
+
# vf-eval my_agent -a '{"split": "holdout"}' -m <policy> -b <base url> -k <key var>
|
|
361
361
|
```
|
|
362
362
|
|
|
363
363
|
The package holds `spec.json` (system prompt, the tool schemas verbatim,
|
|
@@ -645,10 +645,10 @@ login`.
|
|
|
645
645
|
# export WHILEAI_DELEGATED_CREDENTIAL=credential["credential"]
|
|
646
646
|
|
|
647
647
|
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY)
|
|
648
|
-
v1 = data.push("
|
|
648
|
+
v1 = data.push("my-agent-explore-v1") # -> {"datasetId": "ds_...", ...}
|
|
649
649
|
|
|
650
650
|
# iterate, then push the next version with lineage
|
|
651
|
-
v2 = data.push("
|
|
651
|
+
v2 = data.push("my-agent-explore-v2", parent=v1["datasetId"])
|
|
652
652
|
|
|
653
653
|
wai.datasets() # list yours + storage used
|
|
654
654
|
rows = wai.pull(v1["datasetId"]) # rows, or pass path= for a file
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "whileai"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.55"
|
|
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"}
|
|
@@ -7,12 +7,13 @@ malicious), and rolls the agent. Grade 0/1 later. Optimize for post-training.
|
|
|
7
7
|
Five calls, spec to gated dataset:
|
|
8
8
|
|
|
9
9
|
import whileai.simulations as wai
|
|
10
|
-
data = wai.simulate(agent="openai:gpt-4.1-mini",
|
|
11
|
-
mode="rl",
|
|
10
|
+
data = wai.simulate(agent="openai:gpt-4.1-mini", tools=my_tools,
|
|
11
|
+
system_prompt=my_policy, mode="rl",
|
|
12
|
+
situations=200, repeats=8) # generate
|
|
12
13
|
scored = data.grade(judge=my_judge) # grade 0/1
|
|
13
14
|
print(scored.pass_at); wai.judge_trust(scored.rows, judge=my_judge) # trust
|
|
14
15
|
rows, report = wai.optimize(scored, mode="rl") # prune
|
|
15
|
-
wai.push_rows(rows, "
|
|
16
|
+
wai.push_rows(rows, "my-agent-rl-v1", gate=True, mode="rl") # publish, gated
|
|
16
17
|
|
|
17
18
|
Everything else exported here is one layer down from those five.
|
|
18
19
|
"""
|
|
@@ -174,6 +175,7 @@ from .training import (
|
|
|
174
175
|
TrainingRun,
|
|
175
176
|
attach_delta,
|
|
176
177
|
attach_holdout,
|
|
178
|
+
delete_model,
|
|
177
179
|
delete_run,
|
|
178
180
|
get_run,
|
|
179
181
|
list_runs,
|
|
@@ -182,6 +184,7 @@ from .training import (
|
|
|
182
184
|
serve,
|
|
183
185
|
train,
|
|
184
186
|
training_run,
|
|
187
|
+
unserve,
|
|
185
188
|
)
|
|
186
189
|
from .verify import Verifier, verifier
|
|
187
190
|
from .world.sandbox import MockEnvironment
|
|
@@ -252,6 +255,7 @@ __all__ = [
|
|
|
252
255
|
"dedupe_groups",
|
|
253
256
|
"delete_dataset",
|
|
254
257
|
"delete_empty_datasets",
|
|
258
|
+
"delete_model",
|
|
255
259
|
"delete_run",
|
|
256
260
|
"delta_report",
|
|
257
261
|
"dimensions_from_traces",
|
|
@@ -381,6 +385,7 @@ __all__ = [
|
|
|
381
385
|
"trim_unanimous_groups",
|
|
382
386
|
"ungrounded_arguments",
|
|
383
387
|
"unpublish",
|
|
388
|
+
"unserve",
|
|
384
389
|
"update_dataset",
|
|
385
390
|
"validate",
|
|
386
391
|
"verifier",
|
|
@@ -652,6 +652,14 @@ class SimulationData:
|
|
|
652
652
|
report["selection"] = self.search.get("selection")
|
|
653
653
|
return report
|
|
654
654
|
|
|
655
|
+
def leak_report(self, *, min_len: int = 12) -> dict[str, Any]:
|
|
656
|
+
"""Did any reply quote its own ``privileged`` block? Reads the
|
|
657
|
+
trajectories, which still carry the block; ``rows()`` is scrubbed
|
|
658
|
+
and would check nothing. Same report as ``leak_report``."""
|
|
659
|
+
from .score.privileged import leak_report
|
|
660
|
+
|
|
661
|
+
return leak_report(self.trajectories, min_len=min_len)
|
|
662
|
+
|
|
655
663
|
@property
|
|
656
664
|
def rows(self) -> RowList:
|
|
657
665
|
"""The exported rows: exactly what ``save()`` and ``output=`` write.
|
|
@@ -10,13 +10,13 @@ reward). ``export_environment`` writes those three as an installable
|
|
|
10
10
|
``verifiers`` package, the shape Prime Intellect and TRL consume::
|
|
11
11
|
|
|
12
12
|
import whileai.simulations as wai
|
|
13
|
-
data = wai.simulate(
|
|
13
|
+
data = wai.simulate(tools=my_tools, system_prompt=my_policy, mode="rl", repeats=8)
|
|
14
14
|
scored = data.grade()
|
|
15
|
-
wai.export_environment(scored, "envs/
|
|
15
|
+
wai.export_environment(scored, "envs/my-agent", reward=my_verifier)
|
|
16
16
|
|
|
17
17
|
# then, with verifiers installed:
|
|
18
|
-
# pip install -e envs/
|
|
19
|
-
# vf-eval
|
|
18
|
+
# pip install -e envs/my-agent
|
|
19
|
+
# vf-eval my_agent -a '{"split": "holdout"}' -m <policy> ...
|
|
20
20
|
|
|
21
21
|
What goes in the package:
|
|
22
22
|
|
|
@@ -50,6 +50,7 @@ from pathlib import Path
|
|
|
50
50
|
from typing import Any
|
|
51
51
|
|
|
52
52
|
from .schema import check, stamp
|
|
53
|
+
from .score.privileged import leak_report
|
|
53
54
|
from .score.quality import load_jsonl, write_jsonl
|
|
54
55
|
from .score.stats import task_key
|
|
55
56
|
|
|
@@ -601,8 +602,14 @@ def export_training(
|
|
|
601
602
|
|
|
602
603
|
With a path source and no ``output``, writes ``<name>.train.jsonl``
|
|
603
604
|
next to it. ``validate=True`` refuses to write a dataset whose tool
|
|
604
|
-
calls do not round-trip to structured arguments
|
|
605
|
-
|
|
605
|
+
calls do not round-trip to structured arguments, or whose assistant
|
|
606
|
+
turns quote the row's own ``privileged`` block (the export scrubs the
|
|
607
|
+
key, not the reply that recited it); pass ``validate=False`` to export
|
|
608
|
+
anyway and read the report instead. The leak check reads the source
|
|
609
|
+
before the scrub, so pass the ``SimulationData`` or its
|
|
610
|
+
``trajectories``; rows that already came through ``rows()``, ``save()``
|
|
611
|
+
or a file carry nothing to check, and ``report["privileged_leaks"]``
|
|
612
|
+
says so.
|
|
606
613
|
|
|
607
614
|
``format="openai"`` (the default) writes the OpenAI chat-completions
|
|
608
615
|
wire row: the full ``messages`` list, ``function.arguments`` as a JSON
|
|
@@ -638,7 +645,20 @@ def export_training(
|
|
|
638
645
|
"them teaches string-wrapped arguments. Fix the rows or pass "
|
|
639
646
|
"validate=False."
|
|
640
647
|
)
|
|
641
|
-
|
|
648
|
+
raw, _, _, src = _resolve(source)
|
|
649
|
+
# The scrub drops the ``privileged`` key at any depth and copies the
|
|
650
|
+
# assistant's reply through verbatim, so a reply that recited the block
|
|
651
|
+
# still recites it in the training file. Check the unscrubbed side,
|
|
652
|
+
# which is the only place the needles still exist (#249).
|
|
653
|
+
leaks = leak_report(raw)
|
|
654
|
+
if validate and leaks["n_leaked"]:
|
|
655
|
+
raise ValueError(
|
|
656
|
+
f"privileged_leak: {leaks['n_leaked']} of {leaks['n_checked']} rows quote "
|
|
657
|
+
"their own privileged context (reference, principle or hidden state) in "
|
|
658
|
+
"an assistant turn; the export scrubs the key, not the reply, so training "
|
|
659
|
+
"on them teaches the model to say what only the grader was told. Drop "
|
|
660
|
+
"those rows (leak_report(...)['leaked'] names them) or pass validate=False."
|
|
661
|
+
)
|
|
642
662
|
dest = output
|
|
643
663
|
if not dest and src:
|
|
644
664
|
path = Path(src)
|
|
@@ -659,6 +679,9 @@ def export_training(
|
|
|
659
679
|
"tool_output_chars_cut": sum(int(r.get("tool_output_chars_cut") or 0) for r in rows),
|
|
660
680
|
"trained_messages": sum(sum(r["loss_mask"]) for r in rows),
|
|
661
681
|
"masked_messages": sum(len(r["loss_mask"]) - sum(r["loss_mask"]) for r in rows),
|
|
682
|
+
"privileged_leaks": {
|
|
683
|
+
k: leaks[k] for k in ("checked", "n_checked", "n_leaked", "leaked", "summary")
|
|
684
|
+
},
|
|
662
685
|
}
|
|
663
686
|
# SFT clones every row it is given. A failed rollout in the file
|
|
664
687
|
# teaches the failure, so say how many there are instead of leaving
|
|
@@ -673,6 +696,19 @@ def export_training(
|
|
|
673
696
|
"n_ungraded": len(rows) - n_pass - n_fail,
|
|
674
697
|
}
|
|
675
698
|
warnings: list[str] = []
|
|
699
|
+
if leaks["n_leaked"]:
|
|
700
|
+
warnings.append(
|
|
701
|
+
f"{leaks['n_leaked']} of {leaks['n_checked']} rows quote their own privileged "
|
|
702
|
+
"context in an assistant turn and are exported anyway (validate=False); "
|
|
703
|
+
"report['privileged_leaks']['leaked'] names them."
|
|
704
|
+
)
|
|
705
|
+
n_cut = sum(1 for r in raw if isinstance(r, dict) and r.get("finish_reason") == "length")
|
|
706
|
+
if n_cut:
|
|
707
|
+
warnings.append(
|
|
708
|
+
f"{n_cut} of {len(raw)} rows were cut by the reply token cap (finish_reason "
|
|
709
|
+
"'length') and are exported as SFT targets; a model trained on them learns to "
|
|
710
|
+
"stop mid-thought. Filter on finish_reason == 'stop' or raise agent_max_tokens=."
|
|
711
|
+
)
|
|
676
712
|
if n_fail:
|
|
677
713
|
warnings.append(
|
|
678
714
|
f"{n_fail} of {len(rows)} rows have reward below 0.5 and are exported as "
|
|
@@ -698,6 +734,7 @@ export_dataset = export_training
|
|
|
698
734
|
_PAIR_KEYS = (
|
|
699
735
|
"tie",
|
|
700
736
|
"pairwise",
|
|
737
|
+
"first_turn_differs",
|
|
701
738
|
"chosen_score",
|
|
702
739
|
"rejected_score",
|
|
703
740
|
"margin",
|
|
@@ -817,6 +854,12 @@ def export_preference(
|
|
|
817
854
|
length_note = length_confound_warning(chosen_longer, len(deltas))
|
|
818
855
|
if length_note:
|
|
819
856
|
report["warnings"] = [length_note]
|
|
857
|
+
identical = sum(1 for r in out_rows if r.get("first_turn_differs") is False)
|
|
858
|
+
if identical:
|
|
859
|
+
from .score.judging import first_turn_note
|
|
860
|
+
|
|
861
|
+
report["first_turn_identical"] = identical
|
|
862
|
+
report.setdefault("warnings", []).append(first_turn_note(identical, len(out_rows)))
|
|
820
863
|
margins = [r["margin"] for r in out_rows if isinstance(r.get("margin"), (int, float))]
|
|
821
864
|
if margins:
|
|
822
865
|
report["mean_margin"] = round(sum(margins) / len(margins), 4)
|
|
@@ -10,7 +10,7 @@ import os
|
|
|
10
10
|
import re
|
|
11
11
|
import threading
|
|
12
12
|
import time
|
|
13
|
-
from collections.abc import Callable
|
|
13
|
+
from collections.abc import Callable, Mapping
|
|
14
14
|
from typing import Any
|
|
15
15
|
from urllib.parse import urlparse
|
|
16
16
|
|
|
@@ -531,6 +531,7 @@ def complete(
|
|
|
531
531
|
timeout: float = 60,
|
|
532
532
|
n: int = 1,
|
|
533
533
|
logprobs: bool | str = False,
|
|
534
|
+
extra: Mapping[str, Any] | None = None,
|
|
534
535
|
) -> dict:
|
|
535
536
|
"""POST /chat/completions. Reuses a thread-local keep-alive connection.
|
|
536
537
|
|
|
@@ -577,6 +578,8 @@ def complete(
|
|
|
577
578
|
if tools:
|
|
578
579
|
payload["tools"] = _wire_tools(tools)
|
|
579
580
|
payload.update(_request_extras(base_url, model))
|
|
581
|
+
if extra:
|
|
582
|
+
payload.update(dict(extra))
|
|
580
583
|
headers = {"Content-Type": "application/json", "Connection": "keep-alive"}
|
|
581
584
|
if key:
|
|
582
585
|
headers["Authorization"] = f"Bearer {key}"
|
|
@@ -730,8 +733,21 @@ def _strip_tool_markup(text: str) -> str:
|
|
|
730
733
|
return cleaned.strip()
|
|
731
734
|
|
|
732
735
|
|
|
736
|
+
_THINK_BLOCK = re.compile(r"<think>.*?</think>\s*", re.S | re.I)
|
|
737
|
+
_THINK_OPEN = re.compile(r"<think>.*\Z", re.S | re.I)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _strip_think(text: str) -> str:
|
|
741
|
+
"""Drop a thinking model's reasoning markup. A closed block goes whole;
|
|
742
|
+
an unclosed ``<think>`` (the token cap landed inside it) goes to the
|
|
743
|
+
end. What is left is the reply, which is what a grader, a marker and
|
|
744
|
+
the next turn's history should see (#264)."""
|
|
745
|
+
text = _THINK_BLOCK.sub("", text)
|
|
746
|
+
return _THINK_OPEN.sub("", text)
|
|
747
|
+
|
|
748
|
+
|
|
733
749
|
def _spoken_text(reply: dict) -> str:
|
|
734
|
-
return _strip_tool_markup(str(reply.get("content") or ""))
|
|
750
|
+
return _strip_tool_markup(_strip_think(str(reply.get("content") or "")))
|
|
735
751
|
|
|
736
752
|
|
|
737
753
|
def _calls_from_reply(reply: dict) -> tuple[list[dict], dict]:
|
|
@@ -1391,9 +1407,23 @@ def local_model(
|
|
|
1391
1407
|
timeout: float = 60,
|
|
1392
1408
|
max_tokens: int | None = None,
|
|
1393
1409
|
user_model: str | None = None,
|
|
1410
|
+
thinking: bool | None = None,
|
|
1394
1411
|
) -> Callable:
|
|
1412
|
+
"""An agent that talks to an OpenAI-compatible endpoint (a served
|
|
1413
|
+
adapter, a local vLLM, any chat server) for ``simulate(agent=...)``.
|
|
1414
|
+
|
|
1415
|
+
``thinking`` is for reasoning bases such as Qwen3: ``False`` sends
|
|
1416
|
+
``chat_template_kwargs={"enable_thinking": False}`` so the reply is
|
|
1417
|
+
the answer, not the reasoning, the way the hosted Qwen path already
|
|
1418
|
+
does; ``True`` asks for it; ``None`` (the default) sends nothing and
|
|
1419
|
+
leaves the server's default. Either way ``<think>`` markup never
|
|
1420
|
+
reaches ``step["text"]`` or ``final_text``.
|
|
1421
|
+
"""
|
|
1395
1422
|
local = threading.local()
|
|
1396
1423
|
plans = fault_plans if fault_plans is not None else {}
|
|
1424
|
+
extras: dict[str, Any] | None = (
|
|
1425
|
+
None if thinking is None else {"chat_template_kwargs": {"enable_thinking": bool(thinking)}}
|
|
1426
|
+
)
|
|
1397
1427
|
# The simulated user's model. None means the agent's own model plays
|
|
1398
1428
|
# the user (the default); a backend spec moves that role to another
|
|
1399
1429
|
# model, with the key resolved for that endpoint.
|
|
@@ -1438,6 +1468,7 @@ def local_model(
|
|
|
1438
1468
|
temperature=temperature,
|
|
1439
1469
|
timeout=timeout,
|
|
1440
1470
|
max_tokens=120,
|
|
1471
|
+
extra=extras,
|
|
1441
1472
|
)
|
|
1442
1473
|
opener_text = (_spoken_text(greet) or "").strip()
|
|
1443
1474
|
if opener_text:
|
|
@@ -1477,6 +1508,7 @@ def local_model(
|
|
|
1477
1508
|
timeout=timeout,
|
|
1478
1509
|
max_tokens=reply_budget(max_tokens),
|
|
1479
1510
|
logprobs=logprobs,
|
|
1511
|
+
extra=extras,
|
|
1480
1512
|
)
|
|
1481
1513
|
calls, assistant = _calls_from_reply(reply)
|
|
1482
1514
|
# One agent turn, one set of sampling facts, on its first step.
|
|
@@ -160,6 +160,32 @@ def _agent_error_text(exc: BaseException) -> str:
|
|
|
160
160
|
return f"<agent error: {type(exc).__name__}: {public_llm_error(exc)}>"
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
FINISH_REASONS = ("stop", "length", "tool", "error")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _finish_reason(raw: dict, steps: list, final_text: str) -> str:
|
|
167
|
+
"""Why the rollout ended, on the row where a trainer can read it.
|
|
168
|
+
|
|
169
|
+
``length``: a turn was cut by the reply token cap (the backend said
|
|
170
|
+
so). ``error``: the agent raised. ``tool``: the last thing the agent
|
|
171
|
+
did was call a tool and no final reply followed, so the turn budget
|
|
172
|
+
ran out. ``stop``: the agent finished on its own. A callable agent may
|
|
173
|
+
say it outright with ``finish_reason`` in what it returns. A length
|
|
174
|
+
cut scored 0 teaches the cheapest fix, shorter thinking, before it
|
|
175
|
+
teaches the task (#253), so the trainer masks these by default.
|
|
176
|
+
"""
|
|
177
|
+
told = raw.get("finish_reason")
|
|
178
|
+
if isinstance(told, str) and told in FINISH_REASONS:
|
|
179
|
+
return told
|
|
180
|
+
if final_text.startswith("<agent error:"):
|
|
181
|
+
return "error"
|
|
182
|
+
if any(isinstance(s, dict) and s.get("truncated") for s in steps):
|
|
183
|
+
return "length"
|
|
184
|
+
if not final_text.strip() and steps and isinstance(steps[-1], dict) and steps[-1].get("tool"):
|
|
185
|
+
return "tool"
|
|
186
|
+
return "stop"
|
|
187
|
+
|
|
188
|
+
|
|
163
189
|
def _hit_length_cap(row: dict) -> bool:
|
|
164
190
|
"""A step the backend flagged as cut by its token cap, or a reply that
|
|
165
191
|
ends mid-sentence by the hygiene rule."""
|
|
@@ -802,6 +828,7 @@ class Run:
|
|
|
802
828
|
t["seeded"] = [str(x) for x in seeded]
|
|
803
829
|
t.update(_row_conversation(meta, prompt, c.seed))
|
|
804
830
|
t["behavior_signature"] = behavior_signature(t)
|
|
831
|
+
t["finish_reason"] = _finish_reason(raw, t["steps"], t["final_text"])
|
|
805
832
|
# Sampling facts roll up from the agent turns: the summed logprob
|
|
806
833
|
# and token count a trainer needs for an importance ratio or a KL.
|
|
807
834
|
lp_steps = [
|
|
@@ -69,8 +69,8 @@ RUBRIC_FILES = ("rubric.md", "rubric.txt")
|
|
|
69
69
|
def spec_rubric(spec: Any) -> str | None:
|
|
70
70
|
"""The rubric shipped with a spec: what doing the job means, in prose,
|
|
71
71
|
for the judge. ``rubric.md`` (or ``.txt``) next to the spec file the
|
|
72
|
-
loader resolves ``spec`` to (so ``spec="
|
|
73
|
-
``specs/
|
|
72
|
+
loader resolves ``spec`` to (so ``spec="my-agent"`` reads
|
|
73
|
+
``specs/my-agent/rubric.md``, the same shorthand ``simulate`` accepts),
|
|
74
74
|
or a ``rubric`` key in the spec dict. None when the spec carries none."""
|
|
75
75
|
if isinstance(spec, dict):
|
|
76
76
|
text = str(spec.get("rubric") or "").strip()
|
|
@@ -561,6 +561,9 @@ def attach(row: dict, judgment: Judgment) -> dict:
|
|
|
561
561
|
meta = dict(judgment.evidence)
|
|
562
562
|
if judgment.scorer.version:
|
|
563
563
|
meta["version"] = judgment.scorer.version
|
|
564
|
+
if judgment.scorer.kind != "judge":
|
|
565
|
+
# A named scorer reads back as "judge" unless the row says otherwise.
|
|
566
|
+
meta["scorer_kind"] = judgment.scorer.kind
|
|
564
567
|
if meta:
|
|
565
568
|
row["judge_meta"] = meta
|
|
566
569
|
return row
|
|
@@ -576,6 +579,19 @@ def _scorer_version(row: dict) -> str | None:
|
|
|
576
579
|
return None
|
|
577
580
|
|
|
578
581
|
|
|
582
|
+
def _scorer_kind(row: dict) -> str | None:
|
|
583
|
+
"""The kind the grading run stamped, if any (``judge_meta.scorer_kind``)."""
|
|
584
|
+
meta = row.get("judge_meta")
|
|
585
|
+
if isinstance(meta, dict) and meta.get("scorer_kind") in (
|
|
586
|
+
"rule",
|
|
587
|
+
"judge",
|
|
588
|
+
"reward_model",
|
|
589
|
+
"human",
|
|
590
|
+
):
|
|
591
|
+
return str(meta["scorer_kind"])
|
|
592
|
+
return None
|
|
593
|
+
|
|
594
|
+
|
|
579
595
|
def _judgments(row: dict, rollout_id: str) -> list[Judgment]:
|
|
580
596
|
out: list[Judgment] = []
|
|
581
597
|
has_primary = (
|
|
@@ -588,7 +604,11 @@ def _judgments(row: dict, rollout_id: str) -> list[Judgment]:
|
|
|
588
604
|
judge = row.get("judge_name")
|
|
589
605
|
label = row.get("label_source")
|
|
590
606
|
name = judge or label or "unlabeled"
|
|
591
|
-
kind
|
|
607
|
+
# The stamped kind wins; without one, a named judge is a model judge
|
|
608
|
+
# and a bare label is a rule. A Verifier run through ``run_judge``
|
|
609
|
+
# carries ``judge_name`` too, so the inference alone called every
|
|
610
|
+
# verifier a judge (#250).
|
|
611
|
+
kind: Any = _scorer_kind(row) or ("judge" if judge else "rule")
|
|
592
612
|
reward = _number(row.get("reward"))
|
|
593
613
|
status: Any = row.get("judge_status") or "ok"
|
|
594
614
|
evidence: dict = {}
|
|
@@ -802,7 +822,11 @@ def to_row(
|
|
|
802
822
|
if primary.reason:
|
|
803
823
|
row["reason"] = primary.reason
|
|
804
824
|
if primary.scorer.name != "unlabeled":
|
|
805
|
-
|
|
825
|
+
# ``judge_name`` is what a grading run called itself, whatever
|
|
826
|
+
# its kind; a stamped kind proves a run named it. ``label_source``
|
|
827
|
+
# is the engine's own rule label.
|
|
828
|
+
stamped = (primary.evidence.get("judge_meta") or {}).get("scorer_kind")
|
|
829
|
+
if primary.scorer.kind != "rule" or stamped:
|
|
806
830
|
row["judge_name"] = primary.scorer.name
|
|
807
831
|
if primary.evidence.get("label_source"):
|
|
808
832
|
row["label_source"] = primary.evidence["label_source"]
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"token_logprobs": {"type": "array", "items": {"type": "number"}, "$comment": "per-token log-probabilities in generation order across the agent's turns; simulate(logprobs=\"tokens\")"},
|
|
45
45
|
"sampling": {"type": ["object", "null"], "properties": {"temperature": {"type": "number"}, "max_tokens": {"type": "integer"}, "model": {"type": "string"}}, "$comment": "how the row was sampled: a model backend records its resolved temperature, reply budget and model; a callable agent records simulate(sampling=) verbatim, else null"},
|
|
46
46
|
"policy_version": {"type": "string", "$comment": "Rollout.policy.version: <model_version>@<sha256 of the system policy>[:16]"},
|
|
47
|
+
"finish_reason": {"type": "string", "enum": ["stop", "length", "tool", "error"], "$comment": "why the rollout ended: on its own, cut by the reply token cap, turn budget ran out on a tool call, or the agent raised"},
|
|
47
48
|
"writer_model": {"type": "string", "$comment": "who wrote the prompt: the situation writer's model tag, or template / seed / pinned when no model wrote it"},
|
|
48
49
|
"user_model": {"type": "string", "$comment": "who played the simulated user; absent when the agent took a single message"},
|
|
49
50
|
"calibration": {
|
|
@@ -370,6 +370,13 @@ def delta_report(
|
|
|
370
370
|
f"Before allowed {cfg_a['max_tokens']} reply tokens and after {cfg_b['max_tokens']}; "
|
|
371
371
|
"re-run one side so both use the same agent_max_tokens=."
|
|
372
372
|
)
|
|
373
|
+
if _both("truncated_share") and abs(cfg_a["truncated_share"] - cfg_b["truncated_share"]) > 0.05:
|
|
374
|
+
warnings.append(
|
|
375
|
+
f"The token cap cut {cfg_a['truncated_share']:.0%} of before rows and "
|
|
376
|
+
f"{cfg_b['truncated_share']:.0%} of after rows; a side that is cut more often is "
|
|
377
|
+
"not the same eval. Raise agent_max_tokens= on both sides or read the delta with "
|
|
378
|
+
"that in mind."
|
|
379
|
+
)
|
|
373
380
|
if _both("policy_version") and cfg_a["policy_version"] == cfg_b["policy_version"]:
|
|
374
381
|
warnings.append(
|
|
375
382
|
"Before and after are the same policy version; this compares a model to itself."
|
|
@@ -40,6 +40,15 @@ _FAULT_ALIASES = {
|
|
|
40
40
|
"entity already acted on": "already_done",
|
|
41
41
|
}
|
|
42
42
|
_CLEAN_FAULT = {"", "ok", "created", "deleted", "success", "exit_0", "no fault", "clean"}
|
|
43
|
+
#: Statuses a tool result can carry that mean the call failed. A ``status``
|
|
44
|
+
#: key with any other value (``paid``, ``open``, ``completed``) is the
|
|
45
|
+
#: tool's own vocabulary, not a fault (#261).
|
|
46
|
+
_KNOWN_FAULTS = (
|
|
47
|
+
_BAD_STATUS
|
|
48
|
+
| set(_FAULT_ALIASES)
|
|
49
|
+
| set(_FAULT_ALIASES.values())
|
|
50
|
+
| {"stale", "malformed", "already_done", "not_found", "deny", "garbled"}
|
|
51
|
+
)
|
|
43
52
|
_HTTP_FAIL = re.compile(r"^[45]\d\d$")
|
|
44
53
|
_INFRA_STUB = re.compile(
|
|
45
54
|
r"^<agent error:|returned\s+[45]\d\d\b|^(https?://\S+\s+)?[45]\d\d(\s|$)", re.I
|
|
@@ -426,10 +435,10 @@ def _fault_from_result(result) -> str:
|
|
|
426
435
|
if status in {"already_done", "already_acted_on"} or reason == "already_acted_on":
|
|
427
436
|
return "already_done"
|
|
428
437
|
name = normalize_fault_name(status)
|
|
429
|
-
if name:
|
|
438
|
+
if name and (name in _KNOWN_FAULTS or status in _KNOWN_FAULTS):
|
|
430
439
|
return name
|
|
431
440
|
if _step_faulted(result):
|
|
432
|
-
return
|
|
441
|
+
return name or "error"
|
|
433
442
|
return ""
|
|
434
443
|
|
|
435
444
|
|
|
@@ -402,6 +402,11 @@ def run_judge(
|
|
|
402
402
|
name = judge_name or getattr(judge, "__name__", "") or _instance_name(judge) or "judge"
|
|
403
403
|
if name == "<lambda>":
|
|
404
404
|
name = "lambda_judge"
|
|
405
|
+
# A Verifier says what it is (``kind="rule"``); a function judge does
|
|
406
|
+
# not, and the schema then infers "judge" from the name. Stamp the
|
|
407
|
+
# declared kind so a verifier does not read back as a model judge (#250).
|
|
408
|
+
kind = getattr(judge, "kind", None)
|
|
409
|
+
scorer_kind = kind if kind in ("rule", "reward_model", "human") else None
|
|
405
410
|
verdicts: list[dict[str, Any]]
|
|
406
411
|
if concurrency > 1 and len(src_rows) > 1:
|
|
407
412
|
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as pool:
|
|
@@ -429,8 +434,11 @@ def run_judge(
|
|
|
429
434
|
out["reason"] = verdict["reason"]
|
|
430
435
|
out["judge_status"] = verdict["judge_status"]
|
|
431
436
|
out["judge_name"] = name
|
|
432
|
-
|
|
433
|
-
|
|
437
|
+
meta = dict(verdict["judge_meta"] or {})
|
|
438
|
+
if scorer_kind:
|
|
439
|
+
meta["scorer_kind"] = scorer_kind
|
|
440
|
+
if meta:
|
|
441
|
+
out["judge_meta"] = meta
|
|
434
442
|
fc = (verdict["judge_meta"] or {}).get("failure_class")
|
|
435
443
|
if fc:
|
|
436
444
|
out["failure_class"] = str(fc)
|
|
@@ -555,6 +563,42 @@ def length_confound_warning(chosen_longer: int, n: int) -> str | None:
|
|
|
555
563
|
return None
|
|
556
564
|
|
|
557
565
|
|
|
566
|
+
def _first_turn(row: dict) -> str:
|
|
567
|
+
"""What the policy emitted first, read the way the hosted DPO trainer
|
|
568
|
+
reads it: the first tool step as its call, else the first assistant
|
|
569
|
+
text, else ``final_text``."""
|
|
570
|
+
steps = [s for s in (row.get("steps") or []) if isinstance(s, dict)]
|
|
571
|
+
for step in steps:
|
|
572
|
+
if step.get("tool"):
|
|
573
|
+
args = step.get("arguments")
|
|
574
|
+
if args is None:
|
|
575
|
+
args = step.get("args")
|
|
576
|
+
return json.dumps({"name": step["tool"], "arguments": args or {}}, sort_keys=True)
|
|
577
|
+
for step in steps:
|
|
578
|
+
if str(step.get("text") or "").strip():
|
|
579
|
+
return str(step["text"]).strip()
|
|
580
|
+
for message in row.get("messages") or []:
|
|
581
|
+
if isinstance(message, dict) and message.get("role") == "assistant":
|
|
582
|
+
text = str(message.get("content") or "").strip()
|
|
583
|
+
if text:
|
|
584
|
+
return text
|
|
585
|
+
return str(row.get("final_text") or "").strip()
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def first_turn_note(identical: int, n: int) -> str:
|
|
589
|
+
"""The warning for pairs whose first assistant turns read the same."""
|
|
590
|
+
if not identical:
|
|
591
|
+
return ""
|
|
592
|
+
left = n - identical
|
|
593
|
+
return (
|
|
594
|
+
f"{identical}/{n} pairs have identical first assistant turns (same opening tool call "
|
|
595
|
+
"or line); their contrast is later in the rollout. The hosted DPO trainer compares "
|
|
596
|
+
f"first turns only and will drop them, leaving {left} (it needs at least 8). Keep "
|
|
597
|
+
"[p for p in pairs if p['first_turn_differs']] to see what it will train on, or "
|
|
598
|
+
"export_preference(pairs) for a trainer that reads whole conversations."
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
558
602
|
def build_preference_pairs(
|
|
559
603
|
rows: Sequence[dict],
|
|
560
604
|
*,
|
|
@@ -643,6 +687,7 @@ def build_preference_pairs(
|
|
|
643
687
|
"rejected_model": r_model,
|
|
644
688
|
"same_policy": (c_model == r_model) if c_model and r_model else None,
|
|
645
689
|
"length_delta": reply_length(chosen) - reply_length(rejected),
|
|
690
|
+
"first_turn_differs": _first_turn(chosen) != _first_turn(rejected),
|
|
646
691
|
"chosen_reason": str(chosen.get("reason") or ""),
|
|
647
692
|
"rejected_reason": str(rejected.get("reason") or ""),
|
|
648
693
|
"rejected_failure_class": rejected.get("failure_class"),
|
|
@@ -665,10 +710,13 @@ def build_preference_pairs(
|
|
|
665
710
|
for p in pairs
|
|
666
711
|
if p["chosen_score"] not in (0.0, 1.0) or p["rejected_score"] not in (0.0, 1.0)
|
|
667
712
|
)
|
|
713
|
+
identical = sum(1 for p in pairs if not p["first_turn_differs"])
|
|
668
714
|
warnings: list[str] = []
|
|
669
715
|
length_note = length_confound_warning(chosen_longer, n)
|
|
670
716
|
if length_note:
|
|
671
717
|
warnings.append(length_note)
|
|
718
|
+
if identical:
|
|
719
|
+
warnings.append(first_turn_note(identical, n))
|
|
672
720
|
if mixed_policy:
|
|
673
721
|
warnings.append(
|
|
674
722
|
f"{mixed_policy}/{n} pairs mix policies (chosen and rejected from different "
|
|
@@ -686,6 +734,8 @@ def build_preference_pairs(
|
|
|
686
734
|
"min_margin": min_margin,
|
|
687
735
|
"mean_margin": round(sum(p["margin"] for p in pairs) / n, 4) if n else None,
|
|
688
736
|
"partial_score_pairs": partial,
|
|
737
|
+
"first_turn_identical": identical,
|
|
738
|
+
"trainer_pairs": n - identical,
|
|
689
739
|
"same_policy_pairs": same_policy,
|
|
690
740
|
"mixed_policy_pairs": mixed_policy,
|
|
691
741
|
"eval_sourced": eval_pairs,
|
|
@@ -114,6 +114,14 @@ def run_config(
|
|
|
114
114
|
mixed.append(key)
|
|
115
115
|
out[key] = next(iter(values)) if len(values) == 1 else None
|
|
116
116
|
out["mixed"] = mixed
|
|
117
|
+
# The share of rows the token cap cut. A side that was cut more often
|
|
118
|
+
# is not the same eval; ``delta_report`` warns when the two differ.
|
|
119
|
+
# ``None`` when no row says how it finished (rows from before 0.54).
|
|
120
|
+
reasons = [r.get("finish_reason") for r in rows if isinstance(r, dict)]
|
|
121
|
+
known = [x for x in reasons if isinstance(x, str)]
|
|
122
|
+
out["truncated_share"] = (
|
|
123
|
+
round(sum(1 for x in known if x == "length") / len(known), 4) if known else None
|
|
124
|
+
)
|
|
117
125
|
return out
|
|
118
126
|
|
|
119
127
|
|
|
@@ -190,6 +198,9 @@ class PassAt:
|
|
|
190
198
|
f"headroom {fmt(self.headroom)}"
|
|
191
199
|
)
|
|
192
200
|
tail = f"({self.n_groups} groups, k={self.k}"
|
|
201
|
+
cut = self.config.get("truncated_share") if self.config else None
|
|
202
|
+
if cut:
|
|
203
|
+
tail += f"; {cut:.0%} of rows cut by the token cap"
|
|
193
204
|
if self.note:
|
|
194
205
|
tail += f"; {self.note}"
|
|
195
206
|
return f"{head} {tail})"
|
|
@@ -15,7 +15,6 @@ it is vacuous. The report says so instead of passing.
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import re
|
|
18
|
-
from collections.abc import Sequence
|
|
19
18
|
from typing import Any
|
|
20
19
|
|
|
21
20
|
from .style import assistant_text
|
|
@@ -52,9 +51,14 @@ def _needles(privileged: Any, *, min_len: int) -> list[tuple[str, str]]:
|
|
|
52
51
|
return out
|
|
53
52
|
|
|
54
53
|
|
|
55
|
-
def leak_report(rows:
|
|
54
|
+
def leak_report(rows: Any, *, min_len: int = 12) -> dict[str, Any]:
|
|
56
55
|
"""Which rows quote their own ``privileged`` block in the agent's text.
|
|
57
56
|
|
|
57
|
+
Takes the ``SimulationData`` itself, ``data.trajectories``, or any list
|
|
58
|
+
of rows. Given the data object it reads the trajectories, which still
|
|
59
|
+
carry the block; ``data.rows()`` is the scrubbed export and checks
|
|
60
|
+
nothing (the report says so).
|
|
61
|
+
|
|
58
62
|
Checks every row that carries ``privileged`` (``reference``,
|
|
59
63
|
``principle``, and every string in ``hidden_state`` at least
|
|
60
64
|
``min_len`` characters long) against the final reply and every
|
|
@@ -69,6 +73,8 @@ def leak_report(rows: Sequence[dict], *, min_len: int = 12) -> dict[str, Any]:
|
|
|
69
73
|
and the report is vacuous. When it can tell the rows came through the
|
|
70
74
|
export, ``summary`` says so and names the accessor to use instead.
|
|
71
75
|
"""
|
|
76
|
+
if hasattr(rows, "trajectories") and not isinstance(rows, (list, tuple)):
|
|
77
|
+
rows = rows.trajectories
|
|
72
78
|
n_rows = 0
|
|
73
79
|
n_checked = 0
|
|
74
80
|
exported = False
|
|
@@ -474,6 +474,11 @@ class TrainingRun:
|
|
|
474
474
|
else:
|
|
475
475
|
self.finish("done")
|
|
476
476
|
|
|
477
|
+
@property
|
|
478
|
+
def id(self) -> str:
|
|
479
|
+
"""The run id, the handle ``get_run``, ``serve`` and ``delete_run`` take."""
|
|
480
|
+
return self.run_id
|
|
481
|
+
|
|
477
482
|
def __repr__(self) -> str:
|
|
478
483
|
return (
|
|
479
484
|
f"TrainingRun({self.run_id!r}, {self.name!r}, status={self.status!r}, step={self.step})"
|
|
@@ -558,6 +563,10 @@ def _measured_temperature(
|
|
|
558
563
|
return None
|
|
559
564
|
|
|
560
565
|
|
|
566
|
+
#: what GRPO does with a sampled reply the token cap cut
|
|
567
|
+
TRUNCATED = ("mask", "zero")
|
|
568
|
+
|
|
569
|
+
|
|
561
570
|
def train(
|
|
562
571
|
dataset: str,
|
|
563
572
|
*,
|
|
@@ -573,6 +582,7 @@ def train(
|
|
|
573
582
|
max_completion_length: int | None = None,
|
|
574
583
|
loss_type: str | None = None,
|
|
575
584
|
temperature: float | None = None,
|
|
585
|
+
truncated: str | None = None,
|
|
576
586
|
config: Mapping[str, Any] | None = None,
|
|
577
587
|
wait: bool = False,
|
|
578
588
|
timeout: float | None = None,
|
|
@@ -612,10 +622,15 @@ def train(
|
|
|
612
622
|
sampling temperature the trainer rolls out at (GRPO); the dataset's
|
|
613
623
|
rows say what they were measured at under ``sampling.temperature``,
|
|
614
624
|
and ``train`` says so when the two differ, since a before/after
|
|
615
|
-
comparison across temperatures is not like for like.
|
|
625
|
+
comparison across temperatures is not like for like. ``truncated``
|
|
626
|
+
says what GRPO does with a sampled reply the token cap cut:
|
|
627
|
+
``"mask"`` (the default) gives it no gradient, ``"zero"`` scores it 0
|
|
628
|
+
the old way. A cut reply scored 0 teaches shorter thinking before it
|
|
629
|
+
teaches the task, so ``"zero"`` is the knob to reach for only when the
|
|
630
|
+
cap itself is the behavior under training (#253). Each has a
|
|
616
631
|
trainer default when left ``None``. ``config`` passes further host
|
|
617
632
|
keys as given
|
|
618
|
-
(``epsilonHigh``, ``scaleRewards``, ``
|
|
633
|
+
(``epsilonHigh``, ``scaleRewards``, ``balance``).
|
|
619
634
|
Every knob lands on the run's ``config`` so the run page shows it.
|
|
620
635
|
|
|
621
636
|
A dataset already training answers with that run instead of a second.
|
|
@@ -682,6 +697,13 @@ def train(
|
|
|
682
697
|
if not 0 < float(temperature) <= 2:
|
|
683
698
|
raise ValueError("temperature: above 0 and at most 2")
|
|
684
699
|
body["temperature"] = float(temperature)
|
|
700
|
+
if truncated is not None:
|
|
701
|
+
if method != "grpo":
|
|
702
|
+
raise ValueError("truncated= says what GRPO does with a token-capped reply; grpo only")
|
|
703
|
+
if truncated not in TRUNCATED:
|
|
704
|
+
raise ValueError(f"truncated must be one of {', '.join(TRUNCATED)}; got {truncated!r}")
|
|
705
|
+
if method == "grpo":
|
|
706
|
+
body["maskTruncated"] = (truncated or "mask") == "mask"
|
|
685
707
|
for key, value in dict(config or {}).items():
|
|
686
708
|
if key in body:
|
|
687
709
|
raise ValueError(f"config[{key!r}] collides with a named argument")
|
|
@@ -808,11 +830,40 @@ def models(*, api_key: str | None = None) -> list[dict[str, Any]]:
|
|
|
808
830
|
"""The account's hosted models: ``name``, ``baseModel``, ``adapter``,
|
|
809
831
|
``adapterRunId``, ``version``, ``endpoint`` (an OpenAI-compatible
|
|
810
832
|
base URL; send the account key as the bearer and ``name`` as the
|
|
811
|
-
model).
|
|
833
|
+
model).
|
|
834
|
+
|
|
835
|
+
A row here is a registry entry, not a running GPU: the endpoint
|
|
836
|
+
behind it idles to zero on its own and an unused model costs nothing.
|
|
837
|
+
The row stays until ``unserve(name)`` removes it; serving the same
|
|
838
|
+
name again bumps its ``version`` rather than adding a row."""
|
|
812
839
|
out = _call("GET", "/models", api_key)
|
|
813
840
|
return list(out.get("models") or []) if isinstance(out, dict) else []
|
|
814
841
|
|
|
815
842
|
|
|
843
|
+
def unserve(
|
|
844
|
+
name: str,
|
|
845
|
+
*,
|
|
846
|
+
api_key: str | None = None,
|
|
847
|
+
transport: Callable[..., Any] | None = None,
|
|
848
|
+
) -> dict[str, Any]:
|
|
849
|
+
"""Stop hosting ``name``: removes the model row from the account, so
|
|
850
|
+
``models()`` no longer lists it and its endpoint stops answering for
|
|
851
|
+
that name. The inverse of ``serve``, the way ``delete_dataset`` is the
|
|
852
|
+
inverse of ``push``. The adapter weights and the training run stay;
|
|
853
|
+
``serve`` the run again to bring it back (at version 1).
|
|
854
|
+
Returns ``{"name": ..., "deleted": True}``."""
|
|
855
|
+
call = transport or _call
|
|
856
|
+
key = str(name).strip().lower()
|
|
857
|
+
if not key:
|
|
858
|
+
raise ValueError("unserve: name is the hosted model's name, as models() lists it")
|
|
859
|
+
out = call("DELETE", f"/models/{key}", api_key)
|
|
860
|
+
return dict(out) if isinstance(out, dict) else {"name": key, "deleted": True}
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
#: Same call, the other spelling: symmetric with ``delete_dataset``.
|
|
864
|
+
delete_model = unserve
|
|
865
|
+
|
|
866
|
+
|
|
816
867
|
def serve(
|
|
817
868
|
name: str,
|
|
818
869
|
run: TrainingRun | str | None = None,
|
|
@@ -825,18 +876,36 @@ def serve(
|
|
|
825
876
|
row; ``endpoint`` is the OpenAI-compatible base URL and ``name`` the
|
|
826
877
|
model id to send. Posting an existing name bumps ``version``.
|
|
827
878
|
|
|
828
|
-
``run`` is a ``TrainingRun
|
|
829
|
-
|
|
830
|
-
serves the bare base
|
|
879
|
+
``run`` is a ``TrainingRun``, the record ``get_run`` returns, or the
|
|
880
|
+
run id; the adapter and base model come from the run record unless
|
|
881
|
+
``base_model`` is given. No ``run`` serves the bare base
|
|
882
|
+
(``base_model`` required). ``unserve`` is the inverse.
|
|
831
883
|
"""
|
|
832
884
|
call = transport or _call
|
|
833
885
|
adapter: str | None = None
|
|
834
886
|
base = base_model
|
|
887
|
+
run_id: str | None
|
|
835
888
|
if isinstance(run, TrainingRun):
|
|
836
889
|
adapter = run.adapter
|
|
837
|
-
run_id
|
|
890
|
+
run_id = run.run_id
|
|
891
|
+
elif isinstance(run, dict):
|
|
892
|
+
# The record ``get_run`` returns. Reading the id here is what lets
|
|
893
|
+
# train in one process and serve in the next compose (#262).
|
|
894
|
+
run_id = str(run.get("runId") or run.get("run_id") or run.get("id") or "").strip() or None
|
|
895
|
+
if not run_id:
|
|
896
|
+
raise TypeError(
|
|
897
|
+
"run is a dict with no runId; pass the record wai.get_run(run_id) returns, "
|
|
898
|
+
"a TrainingRun, or the run id string"
|
|
899
|
+
)
|
|
900
|
+
adapter = str(run.get("adapter") or "").strip() or None
|
|
901
|
+
base = base or run.get("baseModel") or run.get("base_model")
|
|
902
|
+
elif run is None or isinstance(run, str):
|
|
903
|
+
run_id = (str(run).strip() or None) if run else None
|
|
838
904
|
else:
|
|
839
|
-
|
|
905
|
+
raise TypeError(
|
|
906
|
+
"run must be a TrainingRun, the record wai.get_run(run_id) returns, or the run "
|
|
907
|
+
f"id string; got {type(run).__name__}"
|
|
908
|
+
)
|
|
840
909
|
if run_id and (adapter is None or base is None):
|
|
841
910
|
meta = call("GET", f"/runs/{run_id}", api_key)
|
|
842
911
|
meta = meta if isinstance(meta, dict) else {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: whileai
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.55
|
|
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
|
|
@@ -51,7 +51,7 @@ Two ways in, one engine. Give it the agent's tools and system prompt and it samp
|
|
|
51
51
|
|
|
52
52
|

|
|
53
53
|
|
|
54
|
-
A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The
|
|
54
|
+
A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [while.ai/docs/engine](https://while.ai/docs/engine).
|
|
55
55
|
|
|
56
56
|
## Overview
|
|
57
57
|
|
|
@@ -230,7 +230,7 @@ data.grade(rubric=RUBRIC) # 2 grade against the task rubric: reward 0/1 on ever
|
|
|
230
230
|
print(data.pass_at)
|
|
231
231
|
wai.judge_trust(data.trajectories) # 3 trust the numbers
|
|
232
232
|
rows, report = wai.optimize(data, mode="rl") # 4 prune to what carries gradient
|
|
233
|
-
entry = wai.push_rows(rows, "
|
|
233
|
+
entry = wai.push_rows(rows, "my-agent-rl-v1", gate=True, mode="rl") # 5 publish, gated
|
|
234
234
|
```
|
|
235
235
|
|
|
236
236
|
`situations=200, repeats=8` is a guess. `wai.recommend(tools=TOOLS, system_prompt=POLICY, mode="rl")` replaces it with numbers from this agent's own grid: [How much to run](#how-much-to-run).
|
|
@@ -391,9 +391,9 @@ installable `verifiers` package, the shape Prime Intellect and TRL read.
|
|
|
391
391
|
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY, mode="rl", repeats=8)
|
|
392
392
|
data.grade()
|
|
393
393
|
# reward and world must import by name in the trainer: a module-level function or "module:attr"
|
|
394
|
-
wai.export_environment(data, "envs/
|
|
395
|
-
# pip install -e envs/
|
|
396
|
-
# vf-eval
|
|
394
|
+
wai.export_environment(data, "envs/my-agent", reward=my_verifier)
|
|
395
|
+
# pip install -e envs/my-agent
|
|
396
|
+
# vf-eval my_agent -a '{"split": "holdout"}' -m <policy> -b <base url> -k <key var>
|
|
397
397
|
```
|
|
398
398
|
|
|
399
399
|
The package holds `spec.json` (system prompt, the tool schemas verbatim,
|
|
@@ -681,10 +681,10 @@ login`.
|
|
|
681
681
|
# export WHILEAI_DELEGATED_CREDENTIAL=credential["credential"]
|
|
682
682
|
|
|
683
683
|
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY)
|
|
684
|
-
v1 = data.push("
|
|
684
|
+
v1 = data.push("my-agent-explore-v1") # -> {"datasetId": "ds_...", ...}
|
|
685
685
|
|
|
686
686
|
# iterate, then push the next version with lineage
|
|
687
|
-
v2 = data.push("
|
|
687
|
+
v2 = data.push("my-agent-explore-v2", parent=v1["datasetId"])
|
|
688
688
|
|
|
689
689
|
wai.datasets() # list yours + storage used
|
|
690
690
|
rows = wai.pull(v1["datasetId"]) # rows, or pass path= for a file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|