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
253 lines
9.1 KiB
Python
Executable File
253 lines
9.1 KiB
Python
Executable File
"""progress_report.py — render a self-contained HTML chart of a training
|
|
run's progress curve from the JSONL files written by `eval_loop.sh` and
|
|
`checkpoint_sync_loop.sh`.
|
|
|
|
This is the standalone CLI fallback for when the Eliza Cloud UI isn't
|
|
running. The cloud UI reads the same `_progress.jsonl` and `_pull-log.jsonl`
|
|
files, so this script and the cloud view stay in sync without either side
|
|
having to import from the other.
|
|
|
|
Args:
|
|
--run-name <name> (required) — must match the run name passed to
|
|
checkpoint_sync_loop.sh / eval_loop.sh.
|
|
--out <path> Default: training/checkpoints/<run-name>/_progress.html.
|
|
|
|
Output: a single HTML file. Plotly.js is loaded from the official CDN so we
|
|
don't have to vendor it (per AGENTS spec: no new heavy deps). When
|
|
`_progress.jsonl` is missing or empty the page renders a "no progress data
|
|
yet" placeholder so an operator opening the file early in a run sees a
|
|
sensible message rather than a stack trace.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import html
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
PLOTLY_CDN = "https://cdn.plot.ly/plotly-2.35.2.min.js"
|
|
|
|
|
|
def load_jsonl(path: Path) -> list[dict]:
|
|
"""Tolerant JSONL loader — skips blank lines and malformed records.
|
|
|
|
A malformed record gets reported on stderr but doesn't abort the
|
|
report; partial visibility is more useful than a crash.
|
|
"""
|
|
if not path.is_file():
|
|
return []
|
|
out: list[dict] = []
|
|
with path.open("r", encoding="utf-8") as f:
|
|
for line_num, raw in enumerate(f, start=1):
|
|
raw = raw.strip()
|
|
if not raw:
|
|
continue
|
|
try:
|
|
out.append(json.loads(raw))
|
|
except json.JSONDecodeError as exc:
|
|
print(f"warn: {path}:{line_num} skipped — {exc}", file=sys.stderr)
|
|
return out
|
|
|
|
|
|
def merge_progress_with_pulls(progress: list[dict], pulls: list[dict]) -> list[dict]:
|
|
"""Join `_pull-log.jsonl` (pulled_at, size_mb) onto `_progress.jsonl`
|
|
(step, format_ok, content_ok, ...) by step. Pull entries that don't
|
|
yet have a corresponding eval are dropped from the joined view (they
|
|
show up only in the chart's "pending" annotation count). Eval entries
|
|
without a pull row keep working — pulled_at falls back to evaluated_at.
|
|
"""
|
|
pulls_by_step: dict[int, dict] = {}
|
|
for p in pulls:
|
|
try:
|
|
step = int(p.get("step"))
|
|
except (TypeError, ValueError):
|
|
continue
|
|
# If the same step shows up multiple times (refresh pull), keep the
|
|
# earliest pulled_at — that's "first time we saw this step".
|
|
if step not in pulls_by_step:
|
|
pulls_by_step[step] = p
|
|
|
|
rows: list[dict] = []
|
|
for ev in progress:
|
|
try:
|
|
step = int(ev.get("step"))
|
|
except (TypeError, ValueError):
|
|
continue
|
|
pull = pulls_by_step.get(step) or {}
|
|
rows.append({
|
|
"step": step,
|
|
"format_ok": float(ev.get("format_ok") or 0.0),
|
|
"content_ok": float(ev.get("content_ok") or 0.0),
|
|
"tokens_per_sec": float(ev.get("tokens_per_sec") or 0.0),
|
|
"peak_vram_mb": int(ev.get("peak_vram_mb") or 0),
|
|
"evaluated_at": str(ev.get("evaluated_at") or ""),
|
|
"pulled_at": str(pull.get("pulled_at") or ev.get("evaluated_at") or ""),
|
|
"size_mb": int(pull.get("size_mb") or 0),
|
|
"registry_key": str(ev.get("registry_key") or ""),
|
|
})
|
|
rows.sort(key=lambda r: r["step"])
|
|
return rows
|
|
|
|
|
|
def render_empty_html(run_name: str, progress_path: Path, pulls_path: Path) -> str:
|
|
return f"""<!doctype html>
|
|
<html lang="en"><head>
|
|
<meta charset="utf-8">
|
|
<title>training progress — {html.escape(run_name)}</title>
|
|
<style>
|
|
body {{ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
max-width: 720px; margin: 4em auto; color: #222; padding: 0 1em; }}
|
|
code {{ background: #f4f4f4; padding: 0.1em 0.4em; border-radius: 3px; }}
|
|
</style>
|
|
</head><body>
|
|
<h1>training progress — {html.escape(run_name)}</h1>
|
|
<p>No progress data yet for <code>{html.escape(run_name)}</code>.</p>
|
|
<p>This page reads:</p>
|
|
<ul>
|
|
<li><code>{html.escape(str(progress_path))}</code> — appended by <code>eval_loop.sh</code></li>
|
|
<li><code>{html.escape(str(pulls_path))}</code> — appended by <code>checkpoint_sync_loop.sh</code></li>
|
|
</ul>
|
|
<p>Start the sync + eval loops in a separate terminal:</p>
|
|
<pre>bash scripts/checkpoint_sync_loop.sh --run-name {html.escape(run_name)} &
|
|
bash scripts/eval_loop.sh --run-name {html.escape(run_name)} --registry-key <k> &</pre>
|
|
<p>Then re-run <code>python scripts/progress_report.py --run-name {html.escape(run_name)}</code>.</p>
|
|
</body></html>
|
|
"""
|
|
|
|
|
|
def render_html(run_name: str, rows: list[dict], pulls_pending: int) -> str:
|
|
steps = [r["step"] for r in rows]
|
|
format_ok = [r["format_ok"] for r in rows]
|
|
content_ok = [r["content_ok"] for r in rows]
|
|
|
|
# Build the table HTML. We escape every cell value defensively because
|
|
# the registry-key / dir paths originate from user-supplied flags.
|
|
table_rows = []
|
|
for r in rows:
|
|
table_rows.append(
|
|
"<tr>"
|
|
f"<td>{html.escape(r['pulled_at'])}</td>"
|
|
f"<td>{r['step']}</td>"
|
|
f"<td>{r['format_ok']:.3f}</td>"
|
|
f"<td>{r['content_ok']:.3f}</td>"
|
|
f"<td>{r['tokens_per_sec']:.1f}</td>"
|
|
f"<td>{r['peak_vram_mb']}</td>"
|
|
"</tr>"
|
|
)
|
|
|
|
chart_data = {
|
|
"steps": steps,
|
|
"format_ok": format_ok,
|
|
"content_ok": content_ok,
|
|
}
|
|
|
|
# `json.dumps` -> safe JS literal (no XSS surface; numbers + ascii keys).
|
|
chart_data_json = json.dumps(chart_data)
|
|
|
|
pending_note = (
|
|
f"<p><em>{pulls_pending} checkpoint(s) pulled but not yet evaluated — "
|
|
f"the eval loop will pick them up on its next sweep.</em></p>"
|
|
if pulls_pending else ""
|
|
)
|
|
|
|
return f"""<!doctype html>
|
|
<html lang="en"><head>
|
|
<meta charset="utf-8">
|
|
<title>training progress — {html.escape(run_name)}</title>
|
|
<script src="{PLOTLY_CDN}"></script>
|
|
<style>
|
|
body {{ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
max-width: 1080px; margin: 2em auto; color: #222; padding: 0 1em; }}
|
|
h1 {{ margin-bottom: 0.2em; }}
|
|
h1 + .subtitle {{ color: #666; margin-top: 0; margin-bottom: 1.5em; }}
|
|
#chart {{ width: 100%; height: 480px; }}
|
|
table {{ border-collapse: collapse; width: 100%; margin-top: 2em; font-size: 0.9em; }}
|
|
th, td {{ border-bottom: 1px solid #eee; padding: 0.4em 0.6em; text-align: left; }}
|
|
th {{ background: #fafafa; }}
|
|
td:nth-child(2), td:nth-child(3), td:nth-child(4),
|
|
td:nth-child(5), td:nth-child(6) {{ font-variant-numeric: tabular-nums; text-align: right; }}
|
|
</style>
|
|
</head><body>
|
|
<h1>training progress — {html.escape(run_name)}</h1>
|
|
<p class="subtitle">{len(rows)} evaluated checkpoint(s)</p>
|
|
{pending_note}
|
|
<div id="chart"></div>
|
|
<table>
|
|
<thead><tr>
|
|
<th>pulled_at (UTC)</th><th>step</th>
|
|
<th>format_ok</th><th>content_ok</th>
|
|
<th>tok/s</th><th>peak vram (MB)</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
{''.join(table_rows)}
|
|
</tbody>
|
|
</table>
|
|
<script>
|
|
(function() {{
|
|
const data = {chart_data_json};
|
|
const traces = [
|
|
{{
|
|
x: data.steps, y: data.format_ok,
|
|
mode: "lines+markers", name: "format_ok",
|
|
line: {{ color: "#1f77b4" }},
|
|
}},
|
|
{{
|
|
x: data.steps, y: data.content_ok,
|
|
mode: "lines+markers", name: "content_ok",
|
|
line: {{ color: "#d62728" }},
|
|
}},
|
|
];
|
|
const layout = {{
|
|
margin: {{ t: 24, r: 24, b: 48, l: 48 }},
|
|
xaxis: {{ title: "training step" }},
|
|
yaxis: {{ title: "score (0..1)", range: [0, 1] }},
|
|
legend: {{ orientation: "h", y: -0.2 }},
|
|
}};
|
|
Plotly.newPlot("chart", traces, layout, {{ responsive: true, displaylogo: false }});
|
|
}})();
|
|
</script>
|
|
</body></html>
|
|
"""
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--run-name", required=True)
|
|
ap.add_argument("--out", default=None,
|
|
help="Output HTML path. Default: "
|
|
"training/checkpoints/<run-name>/_progress.html")
|
|
args = ap.parse_args()
|
|
|
|
run_dir = ROOT / "checkpoints" / args.run_name
|
|
progress_path = run_dir / "_progress.jsonl"
|
|
pulls_path = run_dir / "_pull-log.jsonl"
|
|
out_path = Path(args.out) if args.out else run_dir / "_progress.html"
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
progress = load_jsonl(progress_path)
|
|
pulls = load_jsonl(pulls_path)
|
|
|
|
if not progress:
|
|
out_path.write_text(render_empty_html(args.run_name, progress_path, pulls_path))
|
|
print(f"wrote empty-state report to {out_path}")
|
|
return 0
|
|
|
|
rows = merge_progress_with_pulls(progress, pulls)
|
|
evaluated_steps = {r["step"] for r in rows}
|
|
pulls_pending = sum(
|
|
1 for p in pulls
|
|
if isinstance(p.get("step"), int) and p["step"] not in evaluated_steps
|
|
)
|
|
|
|
out_path.write_text(render_html(args.run_name, rows, pulls_pending))
|
|
print(f"wrote {len(rows)}-row progress report to {out_path}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|