Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

475 lines
19 KiB
Python

#!/usr/bin/env python3
"""Cap per-action and per-source representation in the packed train corpus.
Runs AFTER `pack_dataset.py`. While `pack_dataset.py` applies per-source
*tier* weighting (DATASET_REVIEW.md Tier S/A/B/...), it does not bound
the resulting *action* distribution: `TASK_CALL` alone can dominate the
final mix even after tier caps because Tier B/C tool corpora collapse
into one action. This transform fixes that with three composable gates:
1. per-source-dataset cap (default 100,000)
2. per-primary-action cap (default 50,000)
3. non-eliza fraction gate (default 50% non-eliza after the above)
Eliza-tier sources (Tier S + Tier A from DATASET_REVIEW.md) are never
downsampled by gate (3). Gate (3) downsamples uniformly across non-eliza
sources only.
The transform is deterministic — same seed in, byte-identical output.
Usage:
uv run python scripts/transform_cap_distribution.py \\
--input data/final/train.jsonl \\
--output data/intermediate/train_capped.jsonl \\
--config config/corpus_caps.yaml \\
--report data/synthesized/review/cap_distribution_applied.json \\
[--dry-run]
"""
from __future__ import annotations
import argparse
import json
import logging
import random
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any
import yaml
# Source-of-truth: DATASET_REVIEW.md Tier S + Tier A.
# Keep this in sync with `eliza_tier_whitelist` in
# config/corpus_caps.yaml.
ELIZA_TIER_WHITELIST: frozenset[str] = frozenset({
"nubilio-trajectories", # Tier S — real eliza coding-agent loop
"scambench", # Tier A — real scam-defense scenarios
"scam-defense-corpus", # Tier A — augmented v2 trajectories
})
# Action-name extractor for legacy text-encoded `actions:` blocks. Prefer
# native JSON/function-call records upstream; this fallback only lifts the first
# action name for cap accounting when older intermediate rows are encountered.
_NATIVE_JSON_ACTION_NAME_RE = re.compile(
r"actions(?:\[\d+\])?\s*\{[^}]*?\bname\s*:\s*([A-Z_][A-Z0-9_]*)",
re.DOTALL,
)
_NATIVE_JSON_FIRST_NAME_RE = re.compile(
r"^\s*-\s*name\s*:\s*([A-Z_][A-Z0-9_]*)\s*$",
re.MULTILINE,
)
# Common eliza action shape: actions: [N] { name: TASK_CALL ...
_NATIVE_JSON_ACTION_HEADER_RE = re.compile(
r"\bname\s*:\s*([A-Z_][A-Z0-9_]+)",
)
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger("cap-distribution")
# ─────────────────────────── extraction ────────────────────────────
def primary_action(rec: dict[str, Any]) -> str:
"""Best-effort primary action name for a record.
Routing-style tasks (`reply`, `should_respond_with_context`, …)
surface their action via `availableActions[0]`. Tool-call / shell
tasks surface it via the native JSON `tool_calls[0].name` field of
`expectedResponse`. We try both, in order, and fall back to a
canonical sentinel so cap logic always has a key.
"""
md = rec.get("metadata") or {}
task_type = (md.get("task_type") or "").lower()
# Tool-call / shell tasks: extract from native JSON expectedResponse.
if task_type in ("tool_call", "shell_command", "agent_trace"):
er = rec.get("expectedResponse")
if isinstance(er, str) and er:
m = _NATIVE_JSON_ACTION_HEADER_RE.search(er)
if m:
return m.group(1)
# Routing / reply tasks: first availableActions entry.
aa = rec.get("availableActions")
if isinstance(aa, list) and aa:
first = aa[0]
if isinstance(first, str) and first:
return first
# Fall back to expectedResponse extraction even when task_type
# didn't claim to be tool_call (some adapters mis-label).
er = rec.get("expectedResponse")
if isinstance(er, str) and er:
m = _NATIVE_JSON_ACTION_HEADER_RE.search(er)
if m:
return m.group(1)
return "_UNKNOWN_"
def source_of(rec: dict[str, Any]) -> str:
md = rec.get("metadata") or {}
return str(md.get("source_dataset") or "_unknown_")
def task_type_of(rec: dict[str, Any]) -> str:
md = rec.get("metadata") or {}
return str(md.get("task_type") or "_unknown_")
# ─────────────────────────── pass 1: scan ──────────────────────────
def scan_corpus(input_path: Path) -> tuple[
list[tuple[str, str, str]], # records: (source, primary_action, task_type)
Counter, # by_source
Counter, # by_action
Counter, # by_task_type
Counter, # by (source, task_type) tuple
]:
"""Single pass over the corpus collecting per-record routing keys.
We keep one tuple-per-record in memory, not the full record. At
~30 bytes/tuple a 10M-row corpus costs ~300 MB — well within the
machine budget. The actual records are re-streamed in pass 2 by
line index so we never hold the JSON payload.
"""
records: list[tuple[str, str, str]] = []
by_source: Counter = Counter()
by_action: Counter = Counter()
by_task_type: Counter = Counter()
by_source_task: Counter = Counter()
with input_path.open("r", encoding="utf-8", errors="replace") as f:
for line in f:
line = line.rstrip("\n")
if not line:
records.append(("_blank_", "_blank_", "_blank_"))
continue
try:
rec = json.loads(line)
except json.JSONDecodeError:
records.append(("_malformed_", "_malformed_", "_malformed_"))
continue
src = source_of(rec)
act = primary_action(rec)
tt = task_type_of(rec)
records.append((src, act, tt))
by_source[src] += 1
by_action[act] += 1
by_task_type[tt] += 1
by_source_task[(src, tt)] += 1
return records, by_source, by_action, by_task_type, by_source_task
# ─────────────────────────── cap engine ────────────────────────────
def apply_caps(
records: list[tuple[str, str, str]],
*,
max_per_source: int,
max_per_action: int,
max_non_eliza_fraction: float,
eliza_whitelist: frozenset[str],
seed: int,
) -> tuple[set[int], dict[str, int]]:
"""Return (kept_indices, drop_reasons_counter).
Three gates applied in order. Each gate computes which indices to
drop and removes them from the working set. The final `kept` is the
intersection of all gates' survivors.
"""
n = len(records)
drops: Counter = Counter()
drop_reason: dict[int, str] = {}
def _rng(*labels: str) -> random.Random:
"""Per-bucket RNG. Mixing the seed with a label string ensures the
per-source / per-action / fraction-gate decisions are independent
and reproducible across runs."""
return random.Random(f"{seed}|" + "|".join(labels))
# ── gate 1: per-source cap ─────────────────────────────────────
# Group indices by source, downsample any over-cap source uniformly.
if max_per_source > 0:
by_src: dict[str, list[int]] = defaultdict(list)
for i, (src, _, _) in enumerate(records):
by_src[src].append(i)
for src, idxs in by_src.items():
if len(idxs) > max_per_source:
rng = _rng("source", src)
# rng.sample requires a list; idxs is one. Sort first
# so the seed alone (not iteration order) drives selection.
idxs_sorted = sorted(idxs)
kept = set(rng.sample(idxs_sorted, max_per_source))
for i in idxs_sorted:
if i not in kept:
drop_reason[i] = "too-many-of-source"
drops["too-many-of-source"] += 1
# ── gate 2: per-action cap ─────────────────────────────────────
if max_per_action > 0:
by_act: dict[str, list[int]] = defaultdict(list)
for i, (_, act, _) in enumerate(records):
if i in drop_reason:
continue
by_act[act].append(i)
for act, idxs in by_act.items():
if len(idxs) > max_per_action:
rng = _rng("action", act)
idxs_sorted = sorted(idxs)
kept = set(rng.sample(idxs_sorted, max_per_action))
for i in idxs_sorted:
if i not in kept:
drop_reason[i] = "too-many-of-action"
drops["too-many-of-action"] += 1
# ── gate 3: non-eliza fraction gate ────────────────────────────
# Pool together the surviving non-eliza records. If they exceed
# `max_non_eliza_fraction` of the total survivors, downsample
# uniformly across the entire non-eliza pool (NOT per-source) so
# the cut bites proportionally to current size.
if 0.0 < max_non_eliza_fraction < 1.0:
survivors = [i for i in range(n) if i not in drop_reason]
eliza_idxs = [i for i in survivors if records[i][0] in eliza_whitelist]
non_eliza_idxs = [i for i in survivors if records[i][0] not in eliza_whitelist]
e = len(eliza_idxs)
ne = len(non_eliza_idxs)
total = e + ne
if total > 0:
current_ne_frac = ne / total
if current_ne_frac > max_non_eliza_fraction:
# Solve for ne' such that ne' / (e + ne') = cap.
# ne' = e * cap / (1 - cap)
target_ne = int(e * max_non_eliza_fraction / (1.0 - max_non_eliza_fraction))
target_ne = min(target_ne, ne)
rng = _rng("non-eliza-fraction")
non_eliza_sorted = sorted(non_eliza_idxs)
kept = set(rng.sample(non_eliza_sorted, target_ne))
for i in non_eliza_sorted:
if i not in kept:
drop_reason[i] = "non-eliza-fraction-exceeded"
drops["non-eliza-fraction-exceeded"] += 1
kept_indices = {i for i in range(n) if i not in drop_reason}
return kept_indices, dict(drops)
# ─────────────────────────── reporting ─────────────────────────────
def build_report(
*,
records: list[tuple[str, str, str]],
kept: set[int],
drops: dict[str, int],
by_source_before: Counter,
by_action_before: Counter,
by_task_type_before: Counter,
by_source_task_before: Counter,
eliza_whitelist: frozenset[str],
config_used: dict[str, Any],
input_path: Path,
output_path: Path,
) -> dict[str, Any]:
after_source: Counter = Counter()
after_action: Counter = Counter()
after_task_type: Counter = Counter()
after_source_task: Counter = Counter()
for i in kept:
src, act, tt = records[i]
after_source[src] += 1
after_action[act] += 1
after_task_type[tt] += 1
after_source_task[(src, tt)] += 1
eliza_after = sum(c for s, c in after_source.items() if s in eliza_whitelist)
non_eliza_after = sum(c for s, c in after_source.items() if s not in eliza_whitelist)
total_after = eliza_after + non_eliza_after
eliza_before = sum(c for s, c in by_source_before.items() if s in eliza_whitelist)
non_eliza_before = sum(c for s, c in by_source_before.items() if s not in eliza_whitelist)
total_before = eliza_before + non_eliza_before
def _ratio(num: int, den: int) -> float:
return float(num) / float(den) if den > 0 else 0.0
return {
"input": str(input_path),
"output": str(output_path),
"config": config_used,
"totals": {
"before": total_before,
"after": total_after,
"dropped": total_before - total_after,
},
"drop_reasons": drops,
"eliza_fraction": {
"before": _ratio(eliza_before, total_before),
"after": _ratio(eliza_after, total_after),
},
"non_eliza_fraction": {
"before": _ratio(non_eliza_before, total_before),
"after": _ratio(non_eliza_after, total_after),
},
"balance_ratio_eliza_to_non_eliza": {
"before": _ratio(eliza_before, non_eliza_before),
"after": _ratio(eliza_after, non_eliza_after),
},
"by_source": {
"before": dict(by_source_before.most_common()),
"after": dict(after_source.most_common()),
},
"by_action": {
"before": dict(by_action_before.most_common()),
"after": dict(after_action.most_common()),
},
"by_task_type": {
"before": dict(by_task_type_before.most_common()),
"after": dict(after_task_type.most_common()),
},
"by_source_task_type": {
"before": {f"{s}::{t}": c for (s, t), c in by_source_task_before.most_common()},
"after": {f"{s}::{t}": c for (s, t), c in after_source_task.most_common()},
},
"eliza_tier_whitelist": sorted(eliza_whitelist),
}
# ─────────────────────────── pass 2: write ─────────────────────────
def write_kept(input_path: Path, output_path: Path, kept: set[int]) -> int:
"""Stream input and write kept indices to output. Returns count written."""
output_path.parent.mkdir(parents=True, exist_ok=True)
written = 0
with input_path.open("r", encoding="utf-8", errors="replace") as fin, \
output_path.open("w", encoding="utf-8") as fout:
for idx, line in enumerate(fin):
if idx in kept:
if not line.endswith("\n"):
line += "\n"
fout.write(line)
written += 1
return written
# ─────────────────────────── config loader ─────────────────────────
def load_config(config_path: Path) -> dict[str, Any]:
if not config_path.exists():
raise FileNotFoundError(f"config not found: {config_path}")
with config_path.open("r", encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
out = {
"seed": int(cfg.get("seed", 42)),
"max_per_source": int(cfg.get("max_per_source", 100_000)),
"max_per_action": int(cfg.get("max_per_action", 50_000)),
"max_non_eliza_fraction": float(cfg.get("max_non_eliza_fraction", 0.5)),
"eliza_tier_whitelist": frozenset(cfg.get("eliza_tier_whitelist") or ELIZA_TIER_WHITELIST),
}
if not (0.0 <= out["max_non_eliza_fraction"] <= 1.0):
raise ValueError(
f"max_non_eliza_fraction must be in [0,1], got {out['max_non_eliza_fraction']}"
)
return out
# ─────────────────────────── CLI ───────────────────────────────────
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--input", type=Path, required=True,
help="packed train.jsonl from pack_dataset.py")
ap.add_argument("--output", type=Path, required=True,
help="path for the capped output JSONL")
ap.add_argument("--config", type=Path, required=True,
help="YAML config (config/corpus_caps.yaml)")
ap.add_argument("--report", type=Path, required=True,
help="where to write the per-cap distribution report (JSON)")
ap.add_argument("--dry-run", action="store_true",
help="compute caps + report without writing the output JSONL")
args = ap.parse_args()
if not args.input.exists():
log.error("input does not exist: %s", args.input)
return 2
cfg = load_config(args.config)
log.info("config: seed=%d per_source=%d per_action=%d non_eliza_max=%.2f whitelist=%s",
cfg["seed"], cfg["max_per_source"], cfg["max_per_action"],
cfg["max_non_eliza_fraction"], sorted(cfg["eliza_tier_whitelist"]))
log.info("scanning %s", args.input)
records, by_src, by_act, by_tt, by_src_tt = scan_corpus(args.input)
log.info("scanned %d records / %d sources / %d distinct actions / %d task_types",
len(records), len(by_src), len(by_act), len(by_tt))
kept, drops = apply_caps(
records,
max_per_source=cfg["max_per_source"],
max_per_action=cfg["max_per_action"],
max_non_eliza_fraction=cfg["max_non_eliza_fraction"],
eliza_whitelist=cfg["eliza_tier_whitelist"],
seed=cfg["seed"],
)
log.info("kept %d / %d (drops: %s)", len(kept), len(records), drops)
config_used = {
"seed": cfg["seed"],
"max_per_source": cfg["max_per_source"],
"max_per_action": cfg["max_per_action"],
"max_non_eliza_fraction": cfg["max_non_eliza_fraction"],
}
report = build_report(
records=records,
kept=kept,
drops=drops,
by_source_before=by_src,
by_action_before=by_act,
by_task_type_before=by_tt,
by_source_task_before=by_src_tt,
eliza_whitelist=cfg["eliza_tier_whitelist"],
config_used=config_used,
input_path=args.input,
output_path=args.output,
)
if args.dry_run:
report["dry_run"] = True
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, indent=2, sort_keys=False), encoding="utf-8")
log.info("wrote report %s", args.report)
if args.dry_run:
log.info("--dry-run: skipping output write")
# Print a one-line JSON summary so callers piping stdout see the answer.
summary = {
"dry_run": True,
"before": report["totals"]["before"],
"after": report["totals"]["after"],
"dropped": report["totals"]["dropped"],
"drop_reasons": report["drop_reasons"],
"eliza_fraction_after": report["eliza_fraction"]["after"],
"non_eliza_fraction_after": report["non_eliza_fraction"]["after"],
}
print(json.dumps(summary))
return 0
written = write_kept(args.input, args.output, kept)
log.info("wrote %d records to %s", written, args.output)
if written != len(kept):
log.error("write count mismatch: kept=%d written=%d", len(kept), written)
return 3
return 0
if __name__ == "__main__":
sys.exit(main())