whileai 0.63__tar.gz → 0.65__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.63/whileai.egg-info → whileai-0.65}/PKG-INFO +69 -4
- {whileai-0.63 → whileai-0.65}/README.md +67 -3
- {whileai-0.63 → whileai-0.65}/pyproject.toml +2 -2
- {whileai-0.63 → whileai-0.65}/whileai/ingest.py +13 -6
- whileai-0.65/whileai/platform.py +916 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/__init__.py +2 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/data.py +19 -11
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/adapters.py +2 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/agents.py +187 -13
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/generator.py +9 -1
- {whileai-0.63 → whileai-0.65}/whileai/simulations/ingest/platform.py +58 -1
- {whileai-0.63 → whileai-0.65}/whileai/simulations/ingest/traces.py +67 -6
- {whileai-0.63 → whileai-0.65}/whileai/simulations/run/config.py +25 -1
- {whileai-0.63 → whileai-0.65}/whileai/simulations/run/engine.py +175 -29
- {whileai-0.63 → whileai-0.65}/whileai/simulations/run/rows.py +38 -5
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/delta.py +388 -37
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/grade_llm.py +49 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/judging.py +16 -6
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/passat.py +61 -6
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/stats.py +294 -41
- {whileai-0.63 → whileai-0.65}/whileai/simulations/simulation.py +41 -1
- {whileai-0.63 → whileai-0.65}/whileai/simulations/world/sandbox.py +24 -1
- {whileai-0.63 → whileai-0.65/whileai.egg-info}/PKG-INFO +69 -4
- {whileai-0.63 → whileai-0.65}/whileai.egg-info/SOURCES.txt +1 -0
- {whileai-0.63 → whileai-0.65}/whileai.egg-info/requires.txt +1 -0
- {whileai-0.63 → whileai-0.65}/LICENSE +0 -0
- {whileai-0.63 → whileai-0.65}/MANIFEST.in +0 -0
- {whileai-0.63 → whileai-0.65}/setup.cfg +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/__init__.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/_env.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/auth.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/cli.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/init_evals.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/py.typed +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/__main__.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/environment.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/export.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/__init__.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/actionspace.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/anthropic_backend.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/coverage.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/diversity.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/embeddings.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/explore.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/offline_agent.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/scenarios.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/generate/usage_meter.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/ingest/__init__.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/ingest/otel.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/monitor.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/py.typed +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/run/__init__.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/run/spec.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/schema.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/schemas/row-v1.json +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/__init__.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/agreement.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/audit.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/checklist.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/curriculum.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/grading.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/grounding.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/hack_scan.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/hygiene.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/judge_trust.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/labels.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/llm_judge.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/logprobs.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/markers.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/optimize.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/pairwise.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/preflight.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/privileged.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/publish_gate.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/quality.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/reference.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/rubric.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/spec.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/stage.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/style.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/score/trace.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/text.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/training.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/verify/__init__.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/verify/base.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/verify/code.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/verify/math.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/verify/structured.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/verify/text.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/simulations/world/__init__.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/templates/__init__.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai/templates/evals.py +0 -0
- {whileai-0.63 → whileai-0.65}/whileai.egg-info/dependency_links.txt +0 -0
- {whileai-0.63 → whileai-0.65}/whileai.egg-info/entry_points.txt +0 -0
- {whileai-0.63 → whileai-0.65}/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.65
|
|
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
|
|
@@ -24,6 +24,7 @@ Requires-Python: >=3.10
|
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Requires-Dist: requests>=2.25.0
|
|
27
|
+
Requires-Dist: pydantic>=2.0
|
|
27
28
|
Provides-Extra: dev
|
|
28
29
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
30
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
@@ -709,6 +710,22 @@ rows, report = wai.drop_leaky_rows(data.trajectories, prod)
|
|
|
709
710
|
And the loop closes on itself: `evaluate(rollouts, judge).failed_traces()`
|
|
710
711
|
hands the failures straight back to `simulate(traces=...)`.
|
|
711
712
|
|
|
713
|
+
**What traces can and cannot aim at.** Traces reproduce situations: the
|
|
714
|
+
tools, faults and world states the deployed agent met. A failure that has
|
|
715
|
+
a world-visible trigger (a tool timed out and the agent did not say so, a
|
|
716
|
+
stale record was presented as current) is reproduced. A failure that lives
|
|
717
|
+
in how the reply is worded (an unsupported claim, an estimate not labelled
|
|
718
|
+
as one, two questions where one was asked for) has no trigger in the world,
|
|
719
|
+
so traces alone cannot aim at it: measured on a 12-rule grader, every rule
|
|
720
|
+
with a tool-result trigger was reproduced and every rule about the reply's
|
|
721
|
+
wording was not (#285). For those, put the grader in the loop:
|
|
722
|
+
with `simulate(..., grader=judge)` a row the grader fails is re-rolled and
|
|
723
|
+
its ask mutated like a tool fault, and `data.search["mutation_aims"]` says
|
|
724
|
+
how many parents and mutated rows each aim (`world_fault`,
|
|
725
|
+
`graded_failure`) produced. The grader is the switch; to grade beside the
|
|
726
|
+
loop and still steer by tool faults alone, pass
|
|
727
|
+
`advanced={"mutate_graded_failures": False}`.
|
|
728
|
+
|
|
712
729
|
If your traces are already on the platform, `wai.cut(agent="my-agent")`
|
|
713
730
|
does the whole cut in one line — see
|
|
714
731
|
[Training data out of traces](#training-data-out-of-traces).
|
|
@@ -903,7 +920,8 @@ The platform's "Make training data" button, as one line:
|
|
|
903
920
|
|
|
904
921
|
```python
|
|
905
922
|
wai.send_score("4bf92f3577b34da6", 1.0) # this run passed
|
|
906
|
-
wai.cuts(agent="my-agent") # what a cut would hold
|
|
923
|
+
summary = wai.cuts(agent="my-agent") # what a cut would hold
|
|
924
|
+
print(wai.format_cuts(summary, agent="my-agent")) # the traces page's sentence
|
|
907
925
|
made = wai.cut(agent="my-agent", kind="rl") # make it
|
|
908
926
|
wai.pull(made["train"]["datasetId"], "train.jsonl")
|
|
909
927
|
made["holdout"]["datasetId"] # measure on this, never train on it
|
|
@@ -947,6 +965,13 @@ wai.style_markers(rows) # no_boilerplate, no_hedging, no_apology, no_sycophancy
|
|
|
947
965
|
wai.style_report(rows)["warnings"] # "reward pays for hedging (corr +0.41 ...)"
|
|
948
966
|
wai.refusal_report(benign_rows) # over-refusal rate with a Wilson interval
|
|
949
967
|
wai.compare_runs(run_a, run_b) # paired delta with a 95% interval
|
|
968
|
+
wai.holdout_size(
|
|
969
|
+
0.05, base=0.6, k=4
|
|
970
|
+
) # tasks to prove a 5-point gain; n_tasks_concentrated beside it
|
|
971
|
+
wai.holdout_size(
|
|
972
|
+
0.05, before=before, after=after
|
|
973
|
+
) # the paired sd measured off a previous eval, no model
|
|
974
|
+
wai.holdout_size(0.05, task_std=0.38) # or the sd read off a delta_report interval
|
|
950
975
|
wai.delta_report(before, after, target="pass_at_1", must_not_regress=["honest_after_fault"])
|
|
951
976
|
wai.delta_report(before, after, target="pass_at_1", by="category") # the target per kind of prompt
|
|
952
977
|
before = wai.simulate(agent, tools=TOOLS, tasks=base, runs=3) # the same eval three times
|
|
@@ -979,7 +1004,9 @@ rerun = wai.simulate(
|
|
|
979
1004
|
assert base.rollouts_per_request == rerun.rollouts_per_request # cheap guard
|
|
980
1005
|
```
|
|
981
1006
|
|
|
982
|
-
**
|
|
1007
|
+
**Size before you run.** Most evals are too small to see the effects they produce. Per-prompt paired spread is stable for agent rubrics, about 0.38 across five lanes, and at that spread a 50-prompt eval only detects a 10-point gain; a 5-point gain needs about 220 prompts and a 3-point gain about 600. One lane read the same adapter as "barely helps" at 45 and 85 prompts (both straddled zero) and cleared at 131 (+0.13 [+0.07, +0.19]); the effect was real the whole time and a GPU round went to fixing a data problem that was a measurement problem. The effective sample is prompts, not rollouts: raising k sharpens each prompt's estimate but does not narrow a bootstrap over prompts, so spend eval budget on prompts first (k still matters for preference and grouped methods, which need mixed groups). And "straddles zero" means the eval cannot tell, not that the model did not improve; say which. `holdout_size(effect, base=, k=)` says how many paired tasks prove a gain at 80% power, and `detectable_effect(n_tasks, ...)` is the same solved for the gain. Its binomial model assumes the gain is spread evenly across tasks and the two arms are independent draws, and says so in `notes`; when a trait is only exercised by some prompts most tasks are ties, the paired differences spread far wider, and the model under-sizes by several times (a voice lane at 0 -> 0.127 needed 54 tasks where the model said 14), so the model path also returns `n_tasks_concentrated`. On a holdout whose tasks differ in difficulty the model errs the other way, asking for `1 / (1 - Var(p_i) / (p(1-p)))` times the tasks pairing needs (1.19x at spread 0.2 around 0.5, 2.78x at 0.4); `before=` alone reports the spread and that ratio. The honest paths measure: `before=before, after=after` (the same two row lists `delta_report` takes) reads the per-task paired sd off both arms of a previous eval on the same tasks, with the covariance pairing buys in it, and `task_std=` (the per-task sibling of `run_std`) takes the number you read off a `delta_report` (`(hi - lo) * sqrt(n_paired_tasks) / 3.92` from `target_ci95`). `push(purpose="holdout")` warns when the set is too small to prove a 5-point gain.
|
|
1008
|
+
|
|
1009
|
+
**Before and after.** `delta_report` runs `compare_runs` on pass@1 and every marker both row sets share. `target=` names the metric the training was meant to move and gives the headline; `must_not_regress=` names the behaviors whose significant drop fails the report; any other significant drop is a warning. `format_delta_report(report)` prints one line per metric. `eval_variance(run_1, run_2, run_3)` is the eval's own re-run standard deviation (three or more evaluations of the same model); passing it as `run_std=` makes any delta inside twice that band `within_noise`, and a target there reads `within_eval_noise` rather than moved, since re-running the eval moves it that much on its own (rlhf-book ch. 16). `by=` names a row key, a marker, or a callable that groups rows (a prompt category, a tool, a persona); the report then carries `groups`, the target compared within each group, and `groups_down` for any group whose target dropped significantly while the headline moved. A headline over one dominant kind of prompt cannot hide the other kinds that way. Both sides should have the same rollouts per task; when a run lost some (`data.report()["rollouts_lost"]`, with the reasons in `rollouts_lost_by`) and one arm sits at k=4 while the other is at k=2, the report warns next to the sizing line and names both. Unequal k is a precision issue, not a bias: rows lost at random leave the paired delta unbiased and only widen its interval; rows lost for a reason (a timeout on the hard runs) bias it, and only re-running the short arm on its short tasks fixes that. `balance_rollouts=True` (off by default) trims every paired task to the rows both sides have (drawn by `seed=`) so pass^k and pass@k share one k; it costs precision, removes no bias, and `balanced` says how many rows each side gave up.
|
|
983
1010
|
|
|
984
1011
|
**Run the eval three times.** One evaluation is a draw, not a number: the same model on the same tasks lands somewhere else next time, and most post-training gains are inside that spread (rlhf-book ch. 16, appendix C). `wai.simulate(agent, tasks=base, runs=3)` replays the task set three times in one call, same tasks, faults and world, and stamps `lineage.eval_run` on every row. Feed both sides to `delta_report` and it works out `run_std` from the repeats itself. The verdict words: `moved` is a change the interval and the re-run band both support; `moved_unreplicated` is a change seen once, which could be noise, and the warning tells you the `runs=3` call that settles it; `within_eval_noise` is a delta smaller than what re-running the eval does on its own, so equivalence, not a win; `no_change_detected` is an interval that covers zero. `ceiling=True` means the before run already passes most of its tasks (0.9 or more, or too few paired tasks left with room), so there is little improvement the eval could show; use harder situations before training again.
|
|
985
1012
|
|
|
@@ -1092,6 +1119,42 @@ run.holdout(before=0.42, after=0.58) # did it work? the run page opens with thi
|
|
|
1092
1119
|
|
|
1093
1120
|
A run's page opens with one word — **Better**, **Worse**, **About the same** — over the held-out pass rate before and after. The platform's trainer measures it; a run on your own hardware says it with `run.holdout(before, after)`, or `wai.attach_holdout(run_id, before=..., after=...)` once the run has finished. Pass rates are 0 to 1, so 58% is `0.58`; `metric="loss"` sends held-out loss instead (SFT), where lower is better. `run.delta(...)` and `wai.attach_delta(...)` already measure both sides, so they fill the two numbers in themselves, and add `summary["holdout"]` (also `run.holdout_summary`): each side's pass rate with `n_tasks`, `k` and a `ci95`, plus the delta report's verdict word (`moved`, `moved_unreplicated`, `within_eval_noise`, `no_change_detected`). A hosted run read back with `run.refresh()` has the same block with the interval fields `None` and a note that the platform only returned two numbers.
|
|
1094
1121
|
|
|
1122
|
+
### Report a run so a person can decide
|
|
1123
|
+
|
|
1124
|
+
The platform draws one screen per tracked agent at [while.ai/platform/runs](https://while.ai/platform/runs): the held-out score by version with the frontier model as the line to beat, the training curve, what moved on the behaviors you did not train, the judge checks, live traffic on the served version, and cost. A coding agent fills it with `whileai.platform`; the person reads it and presses Promote. Your agent framework stays yours: `track` takes the agent object you already have (OpenAI Agents SDK, Pydantic AI, LangGraph, Claude Agent SDK) and reads the model, the instructions and the tools off it, or you describe it by hand.
|
|
1125
|
+
|
|
1126
|
+
```python
|
|
1127
|
+
from whileai.platform import Behavior, Frontier, Harness, Judge, track
|
|
1128
|
+
|
|
1129
|
+
tracked = track(
|
|
1130
|
+
"refund-bot", # or track(my_agent): name, model, prompt and tools come from the object
|
|
1131
|
+
model="Qwen/Qwen3-4B",
|
|
1132
|
+
harness=Harness(instructions=SYSTEM_PROMPT, tools=["lookup_order", "issue_refund"]),
|
|
1133
|
+
frontier=Frontier(name="Sonnet 5", score=81, cost_per_1k=18.0),
|
|
1134
|
+
)
|
|
1135
|
+
tracked.behavior(
|
|
1136
|
+
Behavior(
|
|
1137
|
+
name="refunds",
|
|
1138
|
+
test_version="v2",
|
|
1139
|
+
n=240,
|
|
1140
|
+
judge=Judge(agreement=0.86, human_n=60, length_bias=0.08),
|
|
1141
|
+
noise_floor=2.4,
|
|
1142
|
+
contamination=0,
|
|
1143
|
+
reward_is_judge=False,
|
|
1144
|
+
)
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
run = tracked.run("v4", method="GRPO", targets=["refunds"], trained_on=["refunds-grpo"])
|
|
1148
|
+
run.log(10, reward=0.41, kl=0.01) # or trainer.add_callback(wai.TrainerCallback(run))
|
|
1149
|
+
run.score("refunds", 83, ci=2.7, n=240) # every behavior, not only the targets
|
|
1150
|
+
run.score("length", 76, ci=2.8, n=120)
|
|
1151
|
+
run.finish(hours=2.1, gpu="1xH100", cost_usd=31)
|
|
1152
|
+
|
|
1153
|
+
str(tracked.verdict()) # "refunds: v4 beats v3 by 5 (interval excludes zero); 1 regression"
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
Every object is a pydantic model that validates before it leaves the process, and each one's docstring names the rlhfbook.com chapter it comes from. A *harness* is the instructions, tools and model name around the weights; its fingerprint is its version, so a prompt edit shows up as a new version without anyone naming it (Evaluation: a score is only comparable with its setup held constant). A *behavior* has its own frozen held-out test (`test_version`), a `noise_floor` measured by scoring the same model twice, and a judge checked against people (`agreement` over `human_n`) and for `length_bias`. A *run* is scored on every behavior: `targets` are the claim, the rest are the check (Over-Optimization: verbosity, sycophancy and refusals are what moves when the reward is gamed). `ci` is the half-width of the 95% interval; the verdict is whether the candidate's and the served version's intervals overlap. `tracked.live(day, version=, replies=, flagged=)` reports a day of traffic when you serve the model yourself. Logging buffers and never raises into the training loop. Worked example: `recipes/04-train/report-run/`.
|
|
1157
|
+
|
|
1095
1158
|
### Is it hacking the reward right now?
|
|
1096
1159
|
|
|
1097
1160
|
```python
|
|
@@ -1191,6 +1254,8 @@ Measured at `avg_turns=4`. The default is now `12`, so a row carries more turns
|
|
|
1191
1254
|
| `embedder` | `"hash"` | Prompt selection |
|
|
1192
1255
|
| `seed` | `0` | Reproducible draws. Bit-for-bit at `concurrency: 1` or with `reproducible=True`, within a process and across processes; otherwise which rows land before the cap depends on thread timing |
|
|
1193
1256
|
| `avg_turns` | `12` | Target conversation length in turns. The person speaks at most `avg_turns // 2` times; `12` leaves room to verify, look up, confirm, and write. |
|
|
1257
|
+
| `mutate_graded_failures` | on with `grader=` | `False` grades beside the loop without steering by the verdict: only tool faults make mutation parents. A row the grader fails (reward under 0.5) is otherwise re-rolled and its ask mutated the way a tool fault's is; `search["mutation_aims"]` counts each aim. `True` without `grader=` is an error |
|
|
1258
|
+
| `patience` | `"normal"` | How long the person keeps answering the agent's questions. The first question is always attempted; from the second on the person may walk away (`normal`: 35% then 60%; `short`: 60% then 90%; `endless`: never, the pre-knob behaviour). At any question the person may also leave when it asks for something they could not know. A row the person left ends on the agent's question and carries `ended_by="user_left"`; `search["ended_on_question"]` counts them. |
|
|
1194
1259
|
|
|
1195
1260
|
Aliases: `phrasings=` / `n=` → `requests_per_situation`; `repeats=` → `rollouts_per_request`; `unique=` → `unique_situations`; `policy=` → `system_prompt`; `risk=` → `fault_rate`.
|
|
1196
1261
|
|
|
@@ -1198,7 +1263,7 @@ Aliases: `phrasings=` / `n=` → `requests_per_situation`; `repeats=` → `rollo
|
|
|
1198
1263
|
|
|
1199
1264
|
Each row, in `data.trajectories` and on disk: `prompt`, `messages`, `steps`, `final_text`, `scenario_id`. Optional `world_state`, `faults`, `reward`, `reason`. `llm_grade=True` adds `llm_reward`. `wai.rank(path)` adds `quality` without changing `reward`. Every row also says how it was sampled: `sampling` is `{"temperature", "max_tokens", "model"}` as the model backend resolved them, or what you passed as `simulate(sampling=...)` for your own callable agent (`None` when you passed nothing, since only you know how it samples). `policy_version` names the model and the system prompt it ran under.
|
|
1200
1265
|
|
|
1201
|
-
Three models can take part in a run, and by default they are one: the agent answers, and the same model writes the situations and plays the user in follow-up turns (only the judge is a different model). Every row now says who did which job, next to `model_version` for the agent: `writer_model` (the writer's model, or `template`, `seed`, `pinned` when no model wrote the prompt), `user_model` (absent when the agent took a single message), and `judge_meta.model` once graded; `data.metadata` and the `.meta.json` sidecar carry the same three. When the agent model also wrote the situations or played the user, `data.degraded` holds `same_model` and `data.warnings` says so in one sentence, with the fix: pass `simulator=` for the writer and `user_model=` for the user to put those jobs on a different model.
|
|
1266
|
+
Three models can take part in a run, and by default they are one: the agent answers, and the same model writes the situations and plays the user in follow-up turns (only the judge is a different model). Every row now says who did which job, next to `model_version` for the agent: `writer_model` (the writer's model, or `template`, `seed`, `pinned` when no model wrote the prompt), `user_model` (absent when the agent took a single message), and `judge_meta.model` once graded; `data.metadata` and the `.meta.json` sidecar carry the same three. Every row also names the deploy prompt it was generated under: `lineage.system_prompt_sha` (the hash `policy_version` carries after `@`) , `lineage.system_prompt_head` (its first 120 chars) and `lineage.system_prompt_chars`, with the full text once per run in `data.system_prompts[<sha>]`, so a base rate measured under a full policy is never mistaken for one measured under a bare prompt; `delta_report` warns when its two arms differ on that hash. When the agent model also wrote the situations or played the user, `data.degraded` holds `same_model` and `data.warnings` says so in one sentence, with the fix: pass `simulator=` for the writer and `user_model=` for the user to put those jobs on a different model.
|
|
1202
1267
|
|
|
1203
1268
|
What goes to disk is the whole row, not a summary of it: `data.rows` (the same list `output=` and `save()` write, callable as `data.rows()` too) carries everything the trajectory carries, so a saved run can still prove its own provenance. That includes how the situation was drawn (`scenario_dimensions`, `arm`, `selection_reason`, `behavior_signature`, `seed`), who graded it and how that went (`judge_name`, `judge_status`, `judge_meta`, `lineage`, `label_source`), and what was measured on it (`markers`, read by `marker_summary` and `delta_report`). Two things never ship, at any depth of the row: the teacher-only `privileged` block and its `principle` / `hidden_state` / `reference` / `rubric` fields, which would put the answer key one step from a training file, and `vector`, the raw embedding the diversity search keeps in memory for the length of the run. A privileged block nested inside a carried field or a tool result is dropped the same way, before `messages` is rebuilt from the steps.
|
|
1204
1269
|
|
|
@@ -673,6 +673,22 @@ rows, report = wai.drop_leaky_rows(data.trajectories, prod)
|
|
|
673
673
|
And the loop closes on itself: `evaluate(rollouts, judge).failed_traces()`
|
|
674
674
|
hands the failures straight back to `simulate(traces=...)`.
|
|
675
675
|
|
|
676
|
+
**What traces can and cannot aim at.** Traces reproduce situations: the
|
|
677
|
+
tools, faults and world states the deployed agent met. A failure that has
|
|
678
|
+
a world-visible trigger (a tool timed out and the agent did not say so, a
|
|
679
|
+
stale record was presented as current) is reproduced. A failure that lives
|
|
680
|
+
in how the reply is worded (an unsupported claim, an estimate not labelled
|
|
681
|
+
as one, two questions where one was asked for) has no trigger in the world,
|
|
682
|
+
so traces alone cannot aim at it: measured on a 12-rule grader, every rule
|
|
683
|
+
with a tool-result trigger was reproduced and every rule about the reply's
|
|
684
|
+
wording was not (#285). For those, put the grader in the loop:
|
|
685
|
+
with `simulate(..., grader=judge)` a row the grader fails is re-rolled and
|
|
686
|
+
its ask mutated like a tool fault, and `data.search["mutation_aims"]` says
|
|
687
|
+
how many parents and mutated rows each aim (`world_fault`,
|
|
688
|
+
`graded_failure`) produced. The grader is the switch; to grade beside the
|
|
689
|
+
loop and still steer by tool faults alone, pass
|
|
690
|
+
`advanced={"mutate_graded_failures": False}`.
|
|
691
|
+
|
|
676
692
|
If your traces are already on the platform, `wai.cut(agent="my-agent")`
|
|
677
693
|
does the whole cut in one line — see
|
|
678
694
|
[Training data out of traces](#training-data-out-of-traces).
|
|
@@ -867,7 +883,8 @@ The platform's "Make training data" button, as one line:
|
|
|
867
883
|
|
|
868
884
|
```python
|
|
869
885
|
wai.send_score("4bf92f3577b34da6", 1.0) # this run passed
|
|
870
|
-
wai.cuts(agent="my-agent") # what a cut would hold
|
|
886
|
+
summary = wai.cuts(agent="my-agent") # what a cut would hold
|
|
887
|
+
print(wai.format_cuts(summary, agent="my-agent")) # the traces page's sentence
|
|
871
888
|
made = wai.cut(agent="my-agent", kind="rl") # make it
|
|
872
889
|
wai.pull(made["train"]["datasetId"], "train.jsonl")
|
|
873
890
|
made["holdout"]["datasetId"] # measure on this, never train on it
|
|
@@ -911,6 +928,13 @@ wai.style_markers(rows) # no_boilerplate, no_hedging, no_apology, no_sycophancy
|
|
|
911
928
|
wai.style_report(rows)["warnings"] # "reward pays for hedging (corr +0.41 ...)"
|
|
912
929
|
wai.refusal_report(benign_rows) # over-refusal rate with a Wilson interval
|
|
913
930
|
wai.compare_runs(run_a, run_b) # paired delta with a 95% interval
|
|
931
|
+
wai.holdout_size(
|
|
932
|
+
0.05, base=0.6, k=4
|
|
933
|
+
) # tasks to prove a 5-point gain; n_tasks_concentrated beside it
|
|
934
|
+
wai.holdout_size(
|
|
935
|
+
0.05, before=before, after=after
|
|
936
|
+
) # the paired sd measured off a previous eval, no model
|
|
937
|
+
wai.holdout_size(0.05, task_std=0.38) # or the sd read off a delta_report interval
|
|
914
938
|
wai.delta_report(before, after, target="pass_at_1", must_not_regress=["honest_after_fault"])
|
|
915
939
|
wai.delta_report(before, after, target="pass_at_1", by="category") # the target per kind of prompt
|
|
916
940
|
before = wai.simulate(agent, tools=TOOLS, tasks=base, runs=3) # the same eval three times
|
|
@@ -943,7 +967,9 @@ rerun = wai.simulate(
|
|
|
943
967
|
assert base.rollouts_per_request == rerun.rollouts_per_request # cheap guard
|
|
944
968
|
```
|
|
945
969
|
|
|
946
|
-
**
|
|
970
|
+
**Size before you run.** Most evals are too small to see the effects they produce. Per-prompt paired spread is stable for agent rubrics, about 0.38 across five lanes, and at that spread a 50-prompt eval only detects a 10-point gain; a 5-point gain needs about 220 prompts and a 3-point gain about 600. One lane read the same adapter as "barely helps" at 45 and 85 prompts (both straddled zero) and cleared at 131 (+0.13 [+0.07, +0.19]); the effect was real the whole time and a GPU round went to fixing a data problem that was a measurement problem. The effective sample is prompts, not rollouts: raising k sharpens each prompt's estimate but does not narrow a bootstrap over prompts, so spend eval budget on prompts first (k still matters for preference and grouped methods, which need mixed groups). And "straddles zero" means the eval cannot tell, not that the model did not improve; say which. `holdout_size(effect, base=, k=)` says how many paired tasks prove a gain at 80% power, and `detectable_effect(n_tasks, ...)` is the same solved for the gain. Its binomial model assumes the gain is spread evenly across tasks and the two arms are independent draws, and says so in `notes`; when a trait is only exercised by some prompts most tasks are ties, the paired differences spread far wider, and the model under-sizes by several times (a voice lane at 0 -> 0.127 needed 54 tasks where the model said 14), so the model path also returns `n_tasks_concentrated`. On a holdout whose tasks differ in difficulty the model errs the other way, asking for `1 / (1 - Var(p_i) / (p(1-p)))` times the tasks pairing needs (1.19x at spread 0.2 around 0.5, 2.78x at 0.4); `before=` alone reports the spread and that ratio. The honest paths measure: `before=before, after=after` (the same two row lists `delta_report` takes) reads the per-task paired sd off both arms of a previous eval on the same tasks, with the covariance pairing buys in it, and `task_std=` (the per-task sibling of `run_std`) takes the number you read off a `delta_report` (`(hi - lo) * sqrt(n_paired_tasks) / 3.92` from `target_ci95`). `push(purpose="holdout")` warns when the set is too small to prove a 5-point gain.
|
|
971
|
+
|
|
972
|
+
**Before and after.** `delta_report` runs `compare_runs` on pass@1 and every marker both row sets share. `target=` names the metric the training was meant to move and gives the headline; `must_not_regress=` names the behaviors whose significant drop fails the report; any other significant drop is a warning. `format_delta_report(report)` prints one line per metric. `eval_variance(run_1, run_2, run_3)` is the eval's own re-run standard deviation (three or more evaluations of the same model); passing it as `run_std=` makes any delta inside twice that band `within_noise`, and a target there reads `within_eval_noise` rather than moved, since re-running the eval moves it that much on its own (rlhf-book ch. 16). `by=` names a row key, a marker, or a callable that groups rows (a prompt category, a tool, a persona); the report then carries `groups`, the target compared within each group, and `groups_down` for any group whose target dropped significantly while the headline moved. A headline over one dominant kind of prompt cannot hide the other kinds that way. Both sides should have the same rollouts per task; when a run lost some (`data.report()["rollouts_lost"]`, with the reasons in `rollouts_lost_by`) and one arm sits at k=4 while the other is at k=2, the report warns next to the sizing line and names both. Unequal k is a precision issue, not a bias: rows lost at random leave the paired delta unbiased and only widen its interval; rows lost for a reason (a timeout on the hard runs) bias it, and only re-running the short arm on its short tasks fixes that. `balance_rollouts=True` (off by default) trims every paired task to the rows both sides have (drawn by `seed=`) so pass^k and pass@k share one k; it costs precision, removes no bias, and `balanced` says how many rows each side gave up.
|
|
947
973
|
|
|
948
974
|
**Run the eval three times.** One evaluation is a draw, not a number: the same model on the same tasks lands somewhere else next time, and most post-training gains are inside that spread (rlhf-book ch. 16, appendix C). `wai.simulate(agent, tasks=base, runs=3)` replays the task set three times in one call, same tasks, faults and world, and stamps `lineage.eval_run` on every row. Feed both sides to `delta_report` and it works out `run_std` from the repeats itself. The verdict words: `moved` is a change the interval and the re-run band both support; `moved_unreplicated` is a change seen once, which could be noise, and the warning tells you the `runs=3` call that settles it; `within_eval_noise` is a delta smaller than what re-running the eval does on its own, so equivalence, not a win; `no_change_detected` is an interval that covers zero. `ceiling=True` means the before run already passes most of its tasks (0.9 or more, or too few paired tasks left with room), so there is little improvement the eval could show; use harder situations before training again.
|
|
949
975
|
|
|
@@ -1056,6 +1082,42 @@ run.holdout(before=0.42, after=0.58) # did it work? the run page opens with thi
|
|
|
1056
1082
|
|
|
1057
1083
|
A run's page opens with one word — **Better**, **Worse**, **About the same** — over the held-out pass rate before and after. The platform's trainer measures it; a run on your own hardware says it with `run.holdout(before, after)`, or `wai.attach_holdout(run_id, before=..., after=...)` once the run has finished. Pass rates are 0 to 1, so 58% is `0.58`; `metric="loss"` sends held-out loss instead (SFT), where lower is better. `run.delta(...)` and `wai.attach_delta(...)` already measure both sides, so they fill the two numbers in themselves, and add `summary["holdout"]` (also `run.holdout_summary`): each side's pass rate with `n_tasks`, `k` and a `ci95`, plus the delta report's verdict word (`moved`, `moved_unreplicated`, `within_eval_noise`, `no_change_detected`). A hosted run read back with `run.refresh()` has the same block with the interval fields `None` and a note that the platform only returned two numbers.
|
|
1058
1084
|
|
|
1085
|
+
### Report a run so a person can decide
|
|
1086
|
+
|
|
1087
|
+
The platform draws one screen per tracked agent at [while.ai/platform/runs](https://while.ai/platform/runs): the held-out score by version with the frontier model as the line to beat, the training curve, what moved on the behaviors you did not train, the judge checks, live traffic on the served version, and cost. A coding agent fills it with `whileai.platform`; the person reads it and presses Promote. Your agent framework stays yours: `track` takes the agent object you already have (OpenAI Agents SDK, Pydantic AI, LangGraph, Claude Agent SDK) and reads the model, the instructions and the tools off it, or you describe it by hand.
|
|
1088
|
+
|
|
1089
|
+
```python
|
|
1090
|
+
from whileai.platform import Behavior, Frontier, Harness, Judge, track
|
|
1091
|
+
|
|
1092
|
+
tracked = track(
|
|
1093
|
+
"refund-bot", # or track(my_agent): name, model, prompt and tools come from the object
|
|
1094
|
+
model="Qwen/Qwen3-4B",
|
|
1095
|
+
harness=Harness(instructions=SYSTEM_PROMPT, tools=["lookup_order", "issue_refund"]),
|
|
1096
|
+
frontier=Frontier(name="Sonnet 5", score=81, cost_per_1k=18.0),
|
|
1097
|
+
)
|
|
1098
|
+
tracked.behavior(
|
|
1099
|
+
Behavior(
|
|
1100
|
+
name="refunds",
|
|
1101
|
+
test_version="v2",
|
|
1102
|
+
n=240,
|
|
1103
|
+
judge=Judge(agreement=0.86, human_n=60, length_bias=0.08),
|
|
1104
|
+
noise_floor=2.4,
|
|
1105
|
+
contamination=0,
|
|
1106
|
+
reward_is_judge=False,
|
|
1107
|
+
)
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
run = tracked.run("v4", method="GRPO", targets=["refunds"], trained_on=["refunds-grpo"])
|
|
1111
|
+
run.log(10, reward=0.41, kl=0.01) # or trainer.add_callback(wai.TrainerCallback(run))
|
|
1112
|
+
run.score("refunds", 83, ci=2.7, n=240) # every behavior, not only the targets
|
|
1113
|
+
run.score("length", 76, ci=2.8, n=120)
|
|
1114
|
+
run.finish(hours=2.1, gpu="1xH100", cost_usd=31)
|
|
1115
|
+
|
|
1116
|
+
str(tracked.verdict()) # "refunds: v4 beats v3 by 5 (interval excludes zero); 1 regression"
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
Every object is a pydantic model that validates before it leaves the process, and each one's docstring names the rlhfbook.com chapter it comes from. A *harness* is the instructions, tools and model name around the weights; its fingerprint is its version, so a prompt edit shows up as a new version without anyone naming it (Evaluation: a score is only comparable with its setup held constant). A *behavior* has its own frozen held-out test (`test_version`), a `noise_floor` measured by scoring the same model twice, and a judge checked against people (`agreement` over `human_n`) and for `length_bias`. A *run* is scored on every behavior: `targets` are the claim, the rest are the check (Over-Optimization: verbosity, sycophancy and refusals are what moves when the reward is gamed). `ci` is the half-width of the 95% interval; the verdict is whether the candidate's and the served version's intervals overlap. `tracked.live(day, version=, replies=, flagged=)` reports a day of traffic when you serve the model yourself. Logging buffers and never raises into the training loop. Worked example: `recipes/04-train/report-run/`.
|
|
1120
|
+
|
|
1059
1121
|
### Is it hacking the reward right now?
|
|
1060
1122
|
|
|
1061
1123
|
```python
|
|
@@ -1155,6 +1217,8 @@ Measured at `avg_turns=4`. The default is now `12`, so a row carries more turns
|
|
|
1155
1217
|
| `embedder` | `"hash"` | Prompt selection |
|
|
1156
1218
|
| `seed` | `0` | Reproducible draws. Bit-for-bit at `concurrency: 1` or with `reproducible=True`, within a process and across processes; otherwise which rows land before the cap depends on thread timing |
|
|
1157
1219
|
| `avg_turns` | `12` | Target conversation length in turns. The person speaks at most `avg_turns // 2` times; `12` leaves room to verify, look up, confirm, and write. |
|
|
1220
|
+
| `mutate_graded_failures` | on with `grader=` | `False` grades beside the loop without steering by the verdict: only tool faults make mutation parents. A row the grader fails (reward under 0.5) is otherwise re-rolled and its ask mutated the way a tool fault's is; `search["mutation_aims"]` counts each aim. `True` without `grader=` is an error |
|
|
1221
|
+
| `patience` | `"normal"` | How long the person keeps answering the agent's questions. The first question is always attempted; from the second on the person may walk away (`normal`: 35% then 60%; `short`: 60% then 90%; `endless`: never, the pre-knob behaviour). At any question the person may also leave when it asks for something they could not know. A row the person left ends on the agent's question and carries `ended_by="user_left"`; `search["ended_on_question"]` counts them. |
|
|
1158
1222
|
|
|
1159
1223
|
Aliases: `phrasings=` / `n=` → `requests_per_situation`; `repeats=` → `rollouts_per_request`; `unique=` → `unique_situations`; `policy=` → `system_prompt`; `risk=` → `fault_rate`.
|
|
1160
1224
|
|
|
@@ -1162,7 +1226,7 @@ Aliases: `phrasings=` / `n=` → `requests_per_situation`; `repeats=` → `rollo
|
|
|
1162
1226
|
|
|
1163
1227
|
Each row, in `data.trajectories` and on disk: `prompt`, `messages`, `steps`, `final_text`, `scenario_id`. Optional `world_state`, `faults`, `reward`, `reason`. `llm_grade=True` adds `llm_reward`. `wai.rank(path)` adds `quality` without changing `reward`. Every row also says how it was sampled: `sampling` is `{"temperature", "max_tokens", "model"}` as the model backend resolved them, or what you passed as `simulate(sampling=...)` for your own callable agent (`None` when you passed nothing, since only you know how it samples). `policy_version` names the model and the system prompt it ran under.
|
|
1164
1228
|
|
|
1165
|
-
Three models can take part in a run, and by default they are one: the agent answers, and the same model writes the situations and plays the user in follow-up turns (only the judge is a different model). Every row now says who did which job, next to `model_version` for the agent: `writer_model` (the writer's model, or `template`, `seed`, `pinned` when no model wrote the prompt), `user_model` (absent when the agent took a single message), and `judge_meta.model` once graded; `data.metadata` and the `.meta.json` sidecar carry the same three. When the agent model also wrote the situations or played the user, `data.degraded` holds `same_model` and `data.warnings` says so in one sentence, with the fix: pass `simulator=` for the writer and `user_model=` for the user to put those jobs on a different model.
|
|
1229
|
+
Three models can take part in a run, and by default they are one: the agent answers, and the same model writes the situations and plays the user in follow-up turns (only the judge is a different model). Every row now says who did which job, next to `model_version` for the agent: `writer_model` (the writer's model, or `template`, `seed`, `pinned` when no model wrote the prompt), `user_model` (absent when the agent took a single message), and `judge_meta.model` once graded; `data.metadata` and the `.meta.json` sidecar carry the same three. Every row also names the deploy prompt it was generated under: `lineage.system_prompt_sha` (the hash `policy_version` carries after `@`) , `lineage.system_prompt_head` (its first 120 chars) and `lineage.system_prompt_chars`, with the full text once per run in `data.system_prompts[<sha>]`, so a base rate measured under a full policy is never mistaken for one measured under a bare prompt; `delta_report` warns when its two arms differ on that hash. When the agent model also wrote the situations or played the user, `data.degraded` holds `same_model` and `data.warnings` says so in one sentence, with the fix: pass `simulator=` for the writer and `user_model=` for the user to put those jobs on a different model.
|
|
1166
1230
|
|
|
1167
1231
|
What goes to disk is the whole row, not a summary of it: `data.rows` (the same list `output=` and `save()` write, callable as `data.rows()` too) carries everything the trajectory carries, so a saved run can still prove its own provenance. That includes how the situation was drawn (`scenario_dimensions`, `arm`, `selection_reason`, `behavior_signature`, `seed`), who graded it and how that went (`judge_name`, `judge_status`, `judge_meta`, `lineage`, `label_source`), and what was measured on it (`markers`, read by `marker_summary` and `delta_report`). Two things never ship, at any depth of the row: the teacher-only `privileged` block and its `principle` / `hidden_state` / `reference` / `rubric` fields, which would put the answer key one step from a training file, and `vector`, the raw embedding the diversity search keeps in memory for the length of the run. A privileged block nested inside a carried field or a tool result is dropped the same way, before `messages` is rebuilt from the steps.
|
|
1168
1232
|
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "whileai"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.65"
|
|
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"}
|
|
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
|
|