financial_audit_env / scripts /run_hackathon_demo.py
balloonmann's picture
Round2 hardening, campaign fixes, training quickstart, and CI gate
92ead25
#!/usr/bin/env python3
"""Run a deterministic no-LLM campaign demo and write a summary artifact."""
import argparse
import json
import time
from pathlib import Path
from typing import Any, Dict, List
import requests
ROLES = [
"expense_specialist",
"invoice_specialist",
"gst_specialist",
"fraud_specialist",
]
def _headers(api_key: str) -> Dict[str, str]:
headers = {"Content-Type": "application/json", "User-Agent": "hackathon-demo-runner/1.0"}
if api_key:
headers["X-API-Key"] = api_key
return headers
def _post(session: requests.Session, url: str, payload: Dict[str, Any], headers: Dict[str, str]) -> Dict[str, Any]:
resp = session.post(url, json=payload, headers=headers, timeout=60)
resp.raise_for_status()
return resp.json()
def run_demo(env_url: str, seed: int, total_periods: int, api_key: str) -> Dict[str, Any]:
started_at = time.time()
session = requests.Session()
headers = _headers(api_key)
start_payload = {"seed": seed, "total_periods": total_periods}
start_data = _post(session, f"{env_url}/campaign/start", start_payload, headers)
campaign_id = start_data["campaign_id"]
periods: List[Dict[str, Any]] = []
for period in range(1, total_periods + 1):
period_row: Dict[str, Any] = {
"period": period,
"roles": {},
"overseer_decisions": 0,
"regulatory_shocks_seen": 0,
"schema_version": None,
"policy_version": None,
}
for role in ROLES:
task_start = _post(
session,
f"{env_url}/campaign/task/start",
{"campaign_id": campaign_id, "role": role},
headers,
)
observation = task_start.get("observation", {})
world_state = task_start.get("world_state", {})
period_row["schema_version"] = world_state.get("schema_version")
period_row["policy_version"] = world_state.get("policy_version")
submit = _post(
session,
f"{env_url}/campaign/task/submit",
{
"campaign_id": campaign_id,
"role": role,
"action": {"findings": [], "submit_final": True},
},
headers,
)
submit_obs = submit.get("observation", {})
shocks = submit_obs.get("pending_regulatory_shocks", [])
feedback = (submit_obs.get("period_observation") or {}).get("feedback", "")
period_row["roles"][role] = {
"task_id": observation.get("period_observation", {}).get("task_id"),
"reward": observation.get("period_observation", {}).get("reward"),
"shock_count": len(shocks),
"final_deferred": "Final submission was deferred" in feedback,
}
period_row["regulatory_shocks_seen"] += len(shocks)
review = _post(
session,
f"{env_url}/overseer/review",
{
"campaign_id": campaign_id,
"action": {
"audit_trail_id": f"demo-{campaign_id}-p{period}",
"decisions": [],
"conflicts_resolved": [],
"task_reassignments": {},
},
},
headers,
)
period_row["overseer_decisions"] = len(review.get("result", {}).get("decisions", []))
if period < total_periods:
_post(
session,
f"{env_url}/campaign/period/advance",
{"campaign_id": campaign_id},
headers,
)
periods.append(period_row)
state_resp = session.get(
f"{env_url}/campaign/state",
params={"campaign_id": campaign_id},
headers=headers,
timeout=60,
)
state_resp.raise_for_status()
final_state = state_resp.json()
improve = _post(
session,
f"{env_url}/self-improve",
{
"campaign_id": campaign_id,
"train_seeds": [42, 43, 44, 45, 46],
"held_out_seeds": [100, 101, 102],
},
headers,
)
ended_at = time.time()
return {
"campaign_id": campaign_id,
"seed": seed,
"total_periods": total_periods,
"duration_seconds": round(ended_at - started_at, 3),
"periods": periods,
"final_state": final_state,
"self_improve": improve,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Run deterministic hackathon campaign demo")
parser.add_argument("--env-url", default="http://localhost:8000", help="API base URL")
parser.add_argument("--seed", type=int, default=42, help="Campaign seed")
parser.add_argument("--periods", type=int, default=5, help="Campaign periods")
parser.add_argument("--api-key", default="", help="Optional X-API-Key")
parser.add_argument(
"--output",
default="artifacts/hackathon_demo_summary.json",
help="Output artifact path",
)
args = parser.parse_args()
summary = run_demo(args.env_url, args.seed, args.periods, args.api_key)
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(summary, indent=2), encoding="utf-8")
print(json.dumps({
"status": "ok",
"campaign_id": summary["campaign_id"],
"output": str(output_path),
"duration_seconds": summary["duration_seconds"],
}, indent=2))
if __name__ == "__main__":
main()