180 lines
5.6 KiB
Python
180 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Export OpenClaw incumbent replay JSONL from existing AWOOOI audit tables.
|
|
|
|
This script is read-only: it queries agent_sessions, auto_repair_executions, and
|
|
incident_evidence, then writes candidate_id=openclaw_incumbent records that can
|
|
be scored by scripts/ai-agent-replay-scorecard.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import sys
|
|
from datetime import timedelta
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy import and_, func, select
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
API_SRC = ROOT / "apps" / "api"
|
|
sys.path.insert(0, str(API_SRC))
|
|
|
|
from src.db.base import get_db_context # noqa: E402
|
|
from src.db.models import AgentSession, AutoRepairExecution, IncidentEvidence # noqa: E402
|
|
from src.services.agent_replacement_evaluator import ( # noqa: E402
|
|
build_openclaw_incumbent_record,
|
|
)
|
|
from src.utils.timezone import now_taipei # noqa: E402
|
|
|
|
|
|
async def main_async() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Export OpenClaw incumbent replay JSONL from DB."
|
|
)
|
|
parser.add_argument("--output", required=True, help="Output JSONL path")
|
|
parser.add_argument("--limit", type=int, default=100, help="Max incidents")
|
|
parser.add_argument("--days", type=int, default=30, help="Lookback days")
|
|
parser.add_argument(
|
|
"--run-id",
|
|
default=f"openclaw-incumbent-{now_taipei().strftime('%Y%m%d%H%M%S')}",
|
|
help="Replay run id",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
cutoff = now_taipei() - timedelta(days=args.days)
|
|
records = []
|
|
try:
|
|
async with get_db_context() as db:
|
|
incident_ids = await _incident_ids(db, cutoff=cutoff, limit=args.limit)
|
|
for incident_id in incident_ids:
|
|
coordinator = await _latest_coordinator(db, incident_id)
|
|
if coordinator is None:
|
|
continue
|
|
execution = await _latest_execution(db, incident_id)
|
|
evidence = await _latest_evidence(db, incident_id)
|
|
turn_count = await _agent_turn_count(db, incident_id)
|
|
records.append(
|
|
build_openclaw_incumbent_record(
|
|
run_id=args.run_id,
|
|
incident_id=incident_id,
|
|
coordinator_output=coordinator.output_json,
|
|
execution_success=(
|
|
execution.success if execution is not None else None
|
|
),
|
|
verification_result=(
|
|
evidence.verification_result if evidence is not None else None
|
|
),
|
|
audit_trace_complete=turn_count >= 3,
|
|
latency_ms=float(coordinator.latency_ms or 0),
|
|
coordinator_degraded=bool(coordinator.degraded),
|
|
)
|
|
)
|
|
except Exception as exc:
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"error": "openclaw_incumbent_export_failed",
|
|
"detail": str(exc),
|
|
"output": args.output,
|
|
"run_id": args.run_id,
|
|
},
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
return 2
|
|
|
|
output = Path(args.output)
|
|
with output.open("w", encoding="utf-8") as handle:
|
|
for record in records:
|
|
handle.write(json.dumps(record.__dict__, ensure_ascii=False, sort_keys=True))
|
|
handle.write("\n")
|
|
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"output": str(output),
|
|
"records": len(records),
|
|
"run_id": args.run_id,
|
|
},
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
async def _incident_ids(db, *, cutoff, limit: int) -> list[str]:
|
|
stmt = (
|
|
select(AgentSession.incident_id)
|
|
.where(
|
|
and_(
|
|
AgentSession.agent_role == "coordinator",
|
|
AgentSession.created_at >= cutoff,
|
|
)
|
|
)
|
|
.distinct()
|
|
.order_by(AgentSession.incident_id.desc())
|
|
.limit(limit)
|
|
)
|
|
result = await db.execute(stmt)
|
|
return [str(row[0]) for row in result.all()]
|
|
|
|
|
|
async def _latest_coordinator(db, incident_id: str):
|
|
stmt = (
|
|
select(AgentSession)
|
|
.where(
|
|
and_(
|
|
AgentSession.incident_id == incident_id,
|
|
AgentSession.agent_role == "coordinator",
|
|
)
|
|
)
|
|
.order_by(AgentSession.created_at.desc())
|
|
.limit(1)
|
|
)
|
|
result = await db.execute(stmt)
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
async def _latest_execution(db, incident_id: str):
|
|
stmt = (
|
|
select(AutoRepairExecution)
|
|
.where(AutoRepairExecution.incident_id == incident_id)
|
|
.order_by(AutoRepairExecution.created_at.desc())
|
|
.limit(1)
|
|
)
|
|
result = await db.execute(stmt)
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
async def _latest_evidence(db, incident_id: str):
|
|
stmt = (
|
|
select(IncidentEvidence)
|
|
.where(IncidentEvidence.incident_id == incident_id)
|
|
.order_by(IncidentEvidence.collected_at.desc())
|
|
.limit(1)
|
|
)
|
|
result = await db.execute(stmt)
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
async def _agent_turn_count(db, incident_id: str) -> int:
|
|
stmt = select(func.count()).select_from(AgentSession).where(
|
|
AgentSession.incident_id == incident_id
|
|
)
|
|
result = await db.execute(stmt)
|
|
return int(result.scalar() or 0)
|
|
|
|
|
|
def main() -> int:
|
|
return asyncio.run(main_async())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|