feat(workbench): persist operational workflows and events
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Import one historical synthetic investigation without rewriting Agent history."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import NAMESPACE_URL, uuid5
|
||||
|
||||
import httpx
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from app.domain.schemas.operational_task import AgentArtifactsCommand
|
||||
|
||||
try:
|
||||
from scripts.run_operational_worker import CredentialProvider, WorkerSettings
|
||||
except ModuleNotFoundError:
|
||||
from run_operational_worker import CredentialProvider, WorkerSettings
|
||||
|
||||
|
||||
def convert_plan(snapshot: dict) -> tuple[dict, list[dict], dict]:
|
||||
evidence, nodes = {}, []
|
||||
for node in snapshot["nodes"]:
|
||||
refs = []
|
||||
for text in node.get("evidence", []):
|
||||
evidence_id = "legacy-synthetic:" + hashlib.sha256(text.encode()).hexdigest()[:24]
|
||||
refs.append(evidence_id)
|
||||
evidence[evidence_id] = {
|
||||
"evidence_id": evidence_id, "kind": "synthetic", "summary": text,
|
||||
"source_label": "历史水质投诉合成调查输入", "source_ref": snapshot["session_id"],
|
||||
"observed_at": snapshot["updated_at"],
|
||||
}
|
||||
nodes.append({"id": node["id"], "title": node["title"], "owner": node["owner"],
|
||||
"status": node["status"], "detail": node["detail"], "evidence_refs": refs})
|
||||
plan = {"schema": "execution-plan@1", "title": snapshot["title"],
|
||||
"reason": "从原调查会话导入已保存的计划,未重新执行调查", "nodes": nodes,
|
||||
"edges": [{"from": edge["from"], "to": edge["to"],
|
||||
**({"condition": edge["label"]} if edge.get("label") else {})}
|
||||
for edge in snapshot["edges"]]}
|
||||
judgment = next((node for node in nodes if node["id"] == "STAGE_JUDGMENT"), None)
|
||||
report = {"title": "历史水质投诉调查报告", "current_conclusion": judgment["detail"] if judgment else "历史调查尚无阶段判断",
|
||||
"conclusion_boundary": "合成演练的历史结果,未重新查询当前数据;现场工作以实际反馈为准",
|
||||
"evidence_refs": judgment["evidence_refs"] if judgment else [],
|
||||
"pending_items": [node["detail"] for node in nodes if node["status"] in {"waiting", "pending"}]}
|
||||
return plan, list(evidence.values()), report
|
||||
|
||||
|
||||
async def run(args):
|
||||
if not re.fullmatch(r"ses_[A-Za-z0-9_-]+", args.session_id):
|
||||
raise ValueError("invalid session id")
|
||||
filename = args.session_id.lower() + ".json"
|
||||
metadata = json.loads((args.agent_data / "session-metadata" / filename).read_text())
|
||||
state = json.loads((args.agent_data / "session-ui-states" / filename).read_text())
|
||||
if metadata.get("triggerWorkflow") != "water_quality_complaint" or metadata.get("triggerSource") != "peripheral_sample":
|
||||
raise ValueError("only explicitly synthetic peripheral water investigations can be imported")
|
||||
snapshots = state.get("executionPlans", [])
|
||||
if not snapshots:
|
||||
raise ValueError("session has no saved plan; no progress will be fabricated")
|
||||
snapshot = max(snapshots, key=lambda item: item["revision"])
|
||||
plan, evidence, report = convert_plan(snapshot)
|
||||
# Validate the complete payload before reporting readiness or creating a
|
||||
# backend task. A dry run must catch incompatible historical plan states.
|
||||
artifacts = AgentArtifactsCommand.model_validate({
|
||||
"request_id": str(uuid5(NAMESPACE_URL, f"legacy-artifacts:{metadata.get('projectId')}:{args.session_id}:{snapshot['revision']}")),
|
||||
"source_session_id": args.session_id, "plan": plan, "evidence": evidence, "report": report,
|
||||
}).model_dump(mode="json", by_alias=True)
|
||||
if args.dry_run:
|
||||
print(f"Ready to import {len(plan['nodes'])} nodes and {len(evidence)} synthetic evidence records; no writes")
|
||||
return
|
||||
settings = WorkerSettings.from_environment()
|
||||
if metadata.get("projectId") != settings.project_id:
|
||||
raise ValueError("session does not belong to the worker project")
|
||||
async with httpx.AsyncClient(timeout=30, follow_redirects=False, trust_env=False) as client:
|
||||
headers = await CredentialProvider(settings).headers(client)
|
||||
base = settings.server_url.rstrip("/") + "/api/v1/operational-tasks"
|
||||
response = await client.post(base, headers=headers, json={
|
||||
"request_id": str(uuid5(NAMESPACE_URL, f"legacy-investigation:{settings.project_id}:{args.session_id}")),
|
||||
"task_type": "event_investigation", "workflow": "water_quality_complaint",
|
||||
"title": metadata.get("title") or "水质投诉调查", "summary": "从历史合成调查导入,原会话完整保留",
|
||||
"source": {"kind": "external_system", "external_id": metadata["triggerSubjectId"], "event_kind": "water_quality_complaint"},
|
||||
"trigger": {"kind": "event", "occurred_at": metadata["triggerOccurredAt"], "detail": "历史合成外围投诉事件"},
|
||||
})
|
||||
response.raise_for_status()
|
||||
task = response.json()
|
||||
if task["current_plan_revision"] is not None:
|
||||
print("Task already has a plan; preserved without overwrite")
|
||||
return
|
||||
run = task.get("active_run")
|
||||
if not run:
|
||||
raise RuntimeError("task has no active Agent run")
|
||||
if run.get("agent_session_id") != args.session_id or run["status"] != "running":
|
||||
response = await client.post(
|
||||
f"{base}/{task['task_id']}/runs/{run['run_id']}/claim",
|
||||
headers=headers,
|
||||
json={
|
||||
"request_id": run["execution_request_id"],
|
||||
"expected_revision": run["revision"],
|
||||
"source_session_id": args.session_id,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
run = response.json()
|
||||
artifacts["run_id"] = run["run_id"]
|
||||
response = await client.post(f"{base}/{task['task_id']}/agent-artifacts", headers=headers, json=artifacts)
|
||||
response.raise_for_status()
|
||||
run = response.json()["task"]["active_run"]
|
||||
response = await client.post(
|
||||
f"{base}/{task['task_id']}/runs/{run['run_id']}/complete",
|
||||
headers=headers,
|
||||
json={
|
||||
"request_id": str(uuid5(NAMESPACE_URL, f"legacy-complete:{settings.project_id}:{args.session_id}:{snapshot['revision']}")),
|
||||
"expected_revision": run["revision"],
|
||||
"source_session_id": args.session_id,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
print(f"Imported task {task['task_id']}; original Agent records unchanged")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("session_id")
|
||||
parser.add_argument("--agent-data", type=Path, default=Path(__file__).resolve().parents[2] / "next-tjwater-agent/data")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
asyncio.run(run(parser.parse_args()))
|
||||
Reference in New Issue
Block a user