whileai 0.57__tar.gz → 0.59__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {whileai-0.57/whileai.egg-info → whileai-0.59}/PKG-INFO +6 -2
  2. {whileai-0.57 → whileai-0.59}/README.md +5 -1
  3. {whileai-0.57 → whileai-0.59}/pyproject.toml +1 -1
  4. {whileai-0.57 → whileai-0.59}/whileai/simulations/ingest/traces.py +17 -11
  5. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/delta.py +15 -0
  6. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/judge_trust.py +26 -5
  7. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/stats.py +34 -9
  8. {whileai-0.57 → whileai-0.59/whileai.egg-info}/PKG-INFO +6 -2
  9. {whileai-0.57 → whileai-0.59}/LICENSE +0 -0
  10. {whileai-0.57 → whileai-0.59}/MANIFEST.in +0 -0
  11. {whileai-0.57 → whileai-0.59}/setup.cfg +0 -0
  12. {whileai-0.57 → whileai-0.59}/whileai/__init__.py +0 -0
  13. {whileai-0.57 → whileai-0.59}/whileai/_env.py +0 -0
  14. {whileai-0.57 → whileai-0.59}/whileai/auth.py +0 -0
  15. {whileai-0.57 → whileai-0.59}/whileai/cli.py +0 -0
  16. {whileai-0.57 → whileai-0.59}/whileai/ingest.py +0 -0
  17. {whileai-0.57 → whileai-0.59}/whileai/py.typed +0 -0
  18. {whileai-0.57 → whileai-0.59}/whileai/simulations/__init__.py +0 -0
  19. {whileai-0.57 → whileai-0.59}/whileai/simulations/__main__.py +0 -0
  20. {whileai-0.57 → whileai-0.59}/whileai/simulations/data.py +0 -0
  21. {whileai-0.57 → whileai-0.59}/whileai/simulations/environment.py +0 -0
  22. {whileai-0.57 → whileai-0.59}/whileai/simulations/export.py +0 -0
  23. {whileai-0.57 → whileai-0.59}/whileai/simulations/generate/__init__.py +0 -0
  24. {whileai-0.57 → whileai-0.59}/whileai/simulations/generate/actionspace.py +0 -0
  25. {whileai-0.57 → whileai-0.59}/whileai/simulations/generate/adapters.py +0 -0
  26. {whileai-0.57 → whileai-0.59}/whileai/simulations/generate/agents.py +0 -0
  27. {whileai-0.57 → whileai-0.59}/whileai/simulations/generate/coverage.py +0 -0
  28. {whileai-0.57 → whileai-0.59}/whileai/simulations/generate/diversity.py +0 -0
  29. {whileai-0.57 → whileai-0.59}/whileai/simulations/generate/embeddings.py +0 -0
  30. {whileai-0.57 → whileai-0.59}/whileai/simulations/generate/explore.py +0 -0
  31. {whileai-0.57 → whileai-0.59}/whileai/simulations/generate/generator.py +0 -0
  32. {whileai-0.57 → whileai-0.59}/whileai/simulations/generate/offline_agent.py +0 -0
  33. {whileai-0.57 → whileai-0.59}/whileai/simulations/generate/scenarios.py +0 -0
  34. {whileai-0.57 → whileai-0.59}/whileai/simulations/generate/usage_meter.py +0 -0
  35. {whileai-0.57 → whileai-0.59}/whileai/simulations/ingest/__init__.py +0 -0
  36. {whileai-0.57 → whileai-0.59}/whileai/simulations/ingest/otel.py +0 -0
  37. {whileai-0.57 → whileai-0.59}/whileai/simulations/ingest/platform.py +0 -0
  38. {whileai-0.57 → whileai-0.59}/whileai/simulations/monitor.py +0 -0
  39. {whileai-0.57 → whileai-0.59}/whileai/simulations/py.typed +0 -0
  40. {whileai-0.57 → whileai-0.59}/whileai/simulations/run/__init__.py +0 -0
  41. {whileai-0.57 → whileai-0.59}/whileai/simulations/run/config.py +0 -0
  42. {whileai-0.57 → whileai-0.59}/whileai/simulations/run/engine.py +0 -0
  43. {whileai-0.57 → whileai-0.59}/whileai/simulations/run/rows.py +0 -0
  44. {whileai-0.57 → whileai-0.59}/whileai/simulations/run/spec.py +0 -0
  45. {whileai-0.57 → whileai-0.59}/whileai/simulations/schema.py +0 -0
  46. {whileai-0.57 → whileai-0.59}/whileai/simulations/schemas/row-v1.json +0 -0
  47. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/__init__.py +0 -0
  48. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/agreement.py +0 -0
  49. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/audit.py +0 -0
  50. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/checklist.py +0 -0
  51. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/curriculum.py +0 -0
  52. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/grade_llm.py +0 -0
  53. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/grading.py +0 -0
  54. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/grounding.py +0 -0
  55. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/hack_scan.py +0 -0
  56. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/hygiene.py +0 -0
  57. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/judging.py +0 -0
  58. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/labels.py +0 -0
  59. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/llm_judge.py +0 -0
  60. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/logprobs.py +0 -0
  61. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/markers.py +0 -0
  62. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/optimize.py +0 -0
  63. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/pairwise.py +0 -0
  64. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/passat.py +0 -0
  65. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/preflight.py +0 -0
  66. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/privileged.py +0 -0
  67. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/publish_gate.py +0 -0
  68. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/quality.py +0 -0
  69. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/reference.py +0 -0
  70. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/rubric.py +0 -0
  71. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/spec.py +0 -0
  72. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/stage.py +0 -0
  73. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/style.py +0 -0
  74. {whileai-0.57 → whileai-0.59}/whileai/simulations/score/trace.py +0 -0
  75. {whileai-0.57 → whileai-0.59}/whileai/simulations/simulation.py +0 -0
  76. {whileai-0.57 → whileai-0.59}/whileai/simulations/training.py +0 -0
  77. {whileai-0.57 → whileai-0.59}/whileai/simulations/verify/__init__.py +0 -0
  78. {whileai-0.57 → whileai-0.59}/whileai/simulations/verify/base.py +0 -0
  79. {whileai-0.57 → whileai-0.59}/whileai/simulations/verify/code.py +0 -0
  80. {whileai-0.57 → whileai-0.59}/whileai/simulations/verify/math.py +0 -0
  81. {whileai-0.57 → whileai-0.59}/whileai/simulations/verify/structured.py +0 -0
  82. {whileai-0.57 → whileai-0.59}/whileai/simulations/verify/text.py +0 -0
  83. {whileai-0.57 → whileai-0.59}/whileai/simulations/world/__init__.py +0 -0
  84. {whileai-0.57 → whileai-0.59}/whileai/simulations/world/sandbox.py +0 -0
  85. {whileai-0.57 → whileai-0.59}/whileai.egg-info/SOURCES.txt +0 -0
  86. {whileai-0.57 → whileai-0.59}/whileai.egg-info/dependency_links.txt +0 -0
  87. {whileai-0.57 → whileai-0.59}/whileai.egg-info/entry_points.txt +0 -0
  88. {whileai-0.57 → whileai-0.59}/whileai.egg-info/requires.txt +0 -0
  89. {whileai-0.57 → whileai-0.59}/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.57
3
+ Version: 0.59
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
@@ -219,7 +219,11 @@ rollout touched, a marker that fired on no row. A 1.00 on a run like that is not
219
219
 
220
220
  Bring your own model. Any OpenAI-compatible chat endpoint that returns tool
221
221
  calls works; it writes the situations and plays the agent, so both run on
222
- your key:
222
+ your key. To put a number on a model you serve (`wai.serve`, or your own
223
+ vLLM), make it the agent: `wai.simulate(tasks=pinned,
224
+ agent=wai.local_model(endpoint, name, tools=TOOLS, system=POLICY,
225
+ thinking=False))`, and run both arms of a before/after through that same
226
+ call so the only difference is the weights.
223
227
 
224
228
  ```bash
225
229
  export OPENAI_API_KEY=...
@@ -183,7 +183,11 @@ rollout touched, a marker that fired on no row. A 1.00 on a run like that is not
183
183
 
184
184
  Bring your own model. Any OpenAI-compatible chat endpoint that returns tool
185
185
  calls works; it writes the situations and plays the agent, so both run on
186
- your key:
186
+ your key. To put a number on a model you serve (`wai.serve`, or your own
187
+ vLLM), make it the agent: `wai.simulate(tasks=pinned,
188
+ agent=wai.local_model(endpoint, name, tools=TOOLS, system=POLICY,
189
+ thinking=False))`, and run both arms of a before/after through that same
190
+ call so the only difference is the weights.
187
191
 
188
192
  ```bash
189
193
  export OPENAI_API_KEY=...
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "whileai"
7
- version = "0.57"
7
+ version = "0.59"
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"}
@@ -278,15 +278,20 @@ def dimensions_from_traces(
278
278
  def _task_key(row: dict, index: int) -> tuple[str, object]:
279
279
  """What makes two rows the same task for splitting purposes.
280
280
 
281
- ``prompt`` first, because that is what a train/eval split has to keep
282
- disjoint and what ``leakage_report`` measures. ``scenario_id`` covers
283
- rows that carry no prompt. A row with neither is its own task, so rows
284
- that merely lack both are not swept onto one side together.
281
+ The unit every report counts in: ``task_key`` (``scenario_id`` when
282
+ the row has one, else ``task_id``, else the prompt), so repeats and
283
+ rephrasings of one situation land on the same side and the held-out
284
+ slice is disjoint from train in the unit ``pass_at``, ``compare_runs``
285
+ and ``delta_report`` group by (#268). Splitting on the prompt alone
286
+ left 16 of 28 held-out situations in train, and ``decontaminate``
287
+ cannot see that because it compares prompts. A row with no key is its
288
+ own task, so rows that merely lack one are not swept onto one side.
285
289
  """
286
- for field in ("prompt", "scenario_id"):
287
- value = row.get(field)
288
- if value is not None and str(value).strip():
289
- return (field, str(value))
290
+ from ..score.stats import task_key
291
+
292
+ key = task_key(row) if isinstance(row, dict) else ""
293
+ if key and str(key).strip():
294
+ return ("task", str(key))
290
295
  return ("index", index)
291
296
 
292
297
 
@@ -295,9 +300,10 @@ def split_pseudo_production(
295
300
  ) -> tuple[list[dict], list[dict]]:
296
301
  """Set aside a pseudo-production slice; the rest stays for training.
297
302
 
298
- The split is by task, not by row: every row sharing a ``prompt`` (or a
299
- ``scenario_id``, for rows without a prompt) lands on the same side, so
300
- the held-out slice is prompt-disjoint from the training side. Splitting
303
+ The split is by task, not by row: every row sharing a ``task_key``
304
+ (the ``scenario_id``, else the prompt) lands on the same side, so the
305
+ held-out slice is disjoint from the training side in the unit every
306
+ report groups by, not just prompt-disjoint. Splitting
301
307
  by row is not enough — under ``mode="rl"`` with ``repeats=k`` each
302
308
  prompt has k rows, and scattering siblings across the two sides trains
303
309
  the student on every prompt it is then evaluated on.
@@ -33,6 +33,7 @@ from .stats import (
33
33
  eval_variance,
34
34
  holdout_size,
35
35
  marker_names,
36
+ metric_summary,
36
37
  task_means,
37
38
  )
38
39
 
@@ -214,6 +215,13 @@ def delta_report(
214
215
 
215
216
  guarded = {_key(m) for m in must_not_regress}
216
217
  target_key = _key(target) if target else None
218
+ degenerate_guards: list[str] = []
219
+ for m in sorted(guarded):
220
+ if m not in results:
221
+ continue
222
+ sides = [metric_summary(rows, m, n_boot=10) for rows in (before, after)]
223
+ if all(s.get("degenerate") for s in sides):
224
+ degenerate_guards.append(m)
217
225
  eval_runs = {"before": len(_eval_runs(before)), "after": len(_eval_runs(after))}
218
226
  run_std_source = "given" if run_std is not None else None
219
227
  if run_std is None and min(eval_runs.values()) >= 2:
@@ -255,6 +263,12 @@ def delta_report(
255
263
  f"One eval run on {where}, so this could be noise. Run each side three times with "
256
264
  "simulate(tasks=..., runs=3) and the report will say."
257
265
  )
266
+ for m in degenerate_guards:
267
+ warnings.append(
268
+ f"must_not_regress {m} is degenerate on both sides (every applicable row scored the "
269
+ "same value): this guard cannot fail, so it catches nothing. Check that the marker "
270
+ "fires at all."
271
+ )
258
272
  if run_std_source == "eval_run" and min(eval_runs.values()) < 3:
259
273
  warnings.append(
260
274
  "Two eval runs on a side is a difference, not a distribution, so run_std is rough; "
@@ -434,6 +448,7 @@ def delta_report(
434
448
  "ceiling": ceiling,
435
449
  "detectable_effect": can_prove,
436
450
  "tasks_needed": tasks_needed,
451
+ "degenerate_guards": degenerate_guards,
437
452
  "proxy": proxy_key,
438
453
  "proxy_verdict": proxy_verdict,
439
454
  "proxy_delta": proxy_result["delta"] if proxy_result else None,
@@ -526,11 +526,32 @@ def judge_trust(
526
526
  if trusted and agree["n"]:
527
527
  low = agree["ci95"][0]
528
528
  if low < min_agreement:
529
- warnings.append(
530
- f"Judge agreement with human labels is {low:.2f} (lower bound), under the "
531
- f"{min_agreement:.2f} floor. Change the judge prompt or the judge model, then "
532
- "run judge_trust again."
533
- )
529
+ point = float(agree.get("agreement") or 0.0)
530
+ need = int(agree["n"])
531
+ if point > min_agreement:
532
+ while need < 1000:
533
+ ci = wilson_interval(round(point * need), need)
534
+ if ci is not None and ci[0] >= min_agreement:
535
+ break
536
+ need += 1
537
+ if point > min_agreement and need <= 200:
538
+ # The judge agrees often enough; the sample is what is short.
539
+ # Say how many labels the bound needs at this agreement rate,
540
+ # or a perfect judge on 14 labels reads as "change the judge". At
541
+ # the floor exactly, or when the count is out of reach, the old
542
+ # advice stands: the judge is what to change.
543
+ warnings.append(
544
+ f"Judge agreement with human labels is {point:.2f} on {agree['n']} labels, "
545
+ f"but the lower bound is {low:.2f}, under the {min_agreement:.2f} floor. "
546
+ f"The judge is not the problem; the sample is. Label about "
547
+ f'{need} rows (attach_labels(kind="human")) and run judge_trust again.'
548
+ )
549
+ else:
550
+ warnings.append(
551
+ f"Judge agreement with human labels is {low:.2f} (lower bound), under the "
552
+ f"{min_agreement:.2f} floor. Change the judge prompt or the judge model, then "
553
+ "run judge_trust again."
554
+ )
534
555
  kappa = agree["kappa"]
535
556
  if not degenerate_gold and kappa is not None and kappa < min_kappa:
536
557
  warnings.append(
@@ -250,21 +250,46 @@ def task_means(rows: Sequence[dict], metric: str = "pass_at_1") -> dict[str, flo
250
250
  def metric_summary(
251
251
  rows: Sequence[dict], metric: str = "pass_at_1", *, n_boot: int = DEFAULT_BOOT, seed: int = 0
252
252
  ) -> dict[str, Any]:
253
- """Mean over tasks with a task-bootstrap 95% interval."""
253
+ """Mean over tasks with a task-bootstrap 95% interval.
254
+
255
+ ``degenerate`` is set when every applicable row scored the same
256
+ value: the metric has not been shown to be able to come out any
257
+ other way, so ``ci95`` is ``None`` (the way ``pass_at`` returns
258
+ ``None`` below three groups) and ``warning`` says so. A marker that
259
+ is silently unfireable (a key-name mismatch) and one that is
260
+ genuinely always true look identical otherwise, and either one passed
261
+ to ``must_not_regress`` is a guard that cannot fail (#270).
262
+ ``n_rows_at_1`` and ``n_rows_at_0`` put the row-level split next to
263
+ the mean.
264
+ """
254
265
  means = task_means(rows, metric)
255
266
  values = list(means.values())
256
- return {
267
+ per_task = _by_task(
268
+ rows, _binary if metric == "pass_at_1" else _marker(metric.split(":", 1)[1])
269
+ )
270
+ row_values = [v for vs in per_task.values() for v in vs]
271
+ distinct = {round(float(v), 9) for v in row_values}
272
+ degenerate = len(row_values) > 0 and len(distinct) == 1
273
+ out: dict[str, Any] = {
257
274
  "metric": metric,
258
275
  "n_tasks": len(values),
259
- "n_rows": sum(
260
- len(v)
261
- for v in _by_task(
262
- rows, _binary if metric == "pass_at_1" else _marker(metric.split(":", 1)[1])
263
- ).values()
264
- ),
276
+ "n_rows": len(row_values),
277
+ "n_rows_at_1": sum(1 for v in row_values if float(v) == 1.0),
278
+ "n_rows_at_0": sum(1 for v in row_values if float(v) == 0.0),
265
279
  "mean": _mean(values) if values else None,
266
- "ci95": bootstrap_ci(values, n_boot=n_boot, seed=seed),
280
+ "ci95": None if degenerate else bootstrap_ci(values, n_boot=n_boot, seed=seed),
281
+ "degenerate": degenerate,
267
282
  }
283
+ if degenerate:
284
+ only = next(iter(distinct))
285
+ name = metric.split(":", 1)[1] if metric.startswith("marker:") else metric
286
+ out["warning"] = (
287
+ f"all {len(row_values)} applicable rows scored {only:g}; {name} has not been shown "
288
+ "to be able to come out any other way. Check the marker fires at all (a key-name "
289
+ "mismatch looks exactly like this) before reading the mean, and do not put it in "
290
+ "must_not_regress: a guard that cannot fail catches nothing."
291
+ )
292
+ return out
268
293
 
269
294
 
270
295
  def marker_names(rows: Sequence[dict]) -> list[str]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whileai
3
- Version: 0.57
3
+ Version: 0.59
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
@@ -219,7 +219,11 @@ rollout touched, a marker that fired on no row. A 1.00 on a run like that is not
219
219
 
220
220
  Bring your own model. Any OpenAI-compatible chat endpoint that returns tool
221
221
  calls works; it writes the situations and plays the agent, so both run on
222
- your key:
222
+ your key. To put a number on a model you serve (`wai.serve`, or your own
223
+ vLLM), make it the agent: `wai.simulate(tasks=pinned,
224
+ agent=wai.local_model(endpoint, name, tools=TOOLS, system=POLICY,
225
+ thinking=False))`, and run both arms of a before/after through that same
226
+ call so the only difference is the weights.
223
227
 
224
228
  ```bash
225
229
  export OPENAI_API_KEY=...
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