Files
cloudinit-bot a8ec7b80de feat(scripts+workflows+docs): explain_run, reclassify, replay, dashboard, daily_digest, operator-tutorial (REQ-INB-42 + -44 + -45 + -50 + -47 + -53)
---ci---
project: inboxos
phase: 1
milestone: v1.4
status: execute
wave: 2
tasks: [T3, T4, T5, T6, T7, T8]
---/ci---
2026-06-09 14:19:49 +00:00

101 lines
3.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Explain a run: structured-log → human summary.
v1.4 REQ-INB-42.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from datetime import datetime
ARTIFACTS_DIR = os.environ.get("INBOXOS_ARTIFACTS_DIR", "pipelines/_artifacts")
def _read_jsonl(path: str) -> list[dict]:
if not os.path.exists(path):
return []
with open(path) as f:
return [json.loads(line) for line in f if line.strip()]
def _read_last_run() -> dict:
path = os.path.join(ARTIFACTS_DIR, "last_run.json")
if not os.path.exists(path):
return {}
with open(path) as f:
return json.load(f)
def _duration(start: str, end: str) -> str:
try:
s = datetime.fromisoformat(start.replace("Z", "+00:00"))
e = datetime.fromisoformat(end.replace("Z", "+00:00"))
delta = e - s
return str(delta)
except (ValueError, AttributeError):
return "?"
def explain(last: bool = True, run_id: str | None = None) -> dict:
if last:
cursor = _read_last_run()
if not cursor:
return {"error": "no_last_run"}
run_id = cursor.get("run_id", "last")
events = _read_jsonl(os.path.join(ARTIFACTS_DIR, f"{run_id}.jsonl"))
error_events = [e for e in events if e.get("event", "").endswith("_error")][:3]
return {
"run_id": run_id,
"run_type": cursor.get("run_type", "?"),
"started_at": cursor.get("started_at", "?"),
"finished_at": cursor.get("finished_at", "?"),
"duration": _duration(cursor.get("started_at", ""), cursor.get("finished_at", "")),
"mids_total": cursor.get("mids_total", 0),
"mids_upserted": cursor.get("mids_upserted", 0),
"mids_skipped": cursor.get("mids_skipped", 0),
"mids_errored": cursor.get("mids_errored", 0),
"modules": cursor.get("modules", {}),
"top_errors": [
{"event": e.get("event"), "message": e.get("message", "")}
for e in error_events
],
}
def main() -> int:
p = argparse.ArgumentParser(description="Explain an InboxOS run")
g = p.add_mutually_exclusive_group()
g.add_argument("--last", action="store_true", default=True, help="Read last_run.json")
g.add_argument("--run-id", help="Specific run ID to explain")
p.add_argument("--json", action="store_true", help="Emit JSON instead of human summary")
args = p.parse_args()
result = explain(last=not args.run_id, run_id=args.run_id)
if "error" in result and result["error"] == "no_last_run":
sys.stderr.write("No last_run.json found. Run a pipeline first.\n")
return 1
if args.json:
print(json.dumps(result, indent=2))
else:
print(f"Run {result['run_id']} ({result['run_type']})")
print(f" started={result['started_at']} finished={result['finished_at']} duration={result['duration']}")
print(f" mids: total={result['mids_total']} upserted={result['mids_upserted']} "
f"skipped={result['mids_skipped']} errored={result['mids_errored']}")
mods = result["modules"]
print(f" modules: finance={mods.get('finance', 0)} crm={mods.get('crm', 0)} "
f"intel={mods.get('intel', 0)} promotions={mods.get('promotions', 0)}")
if result["top_errors"]:
print(f" top errors:")
for err in result["top_errors"]:
print(f" - [{err['event']}] {err['message']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())