141 lines
7.4 KiB
Python
141 lines
7.4 KiB
Python
#!/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, InvestigationReport
|
|
|
|
|
|
class HistoricalArtifacts(AgentArtifactsCommand):
|
|
"""Validate history for inspection without inventing a current outcome."""
|
|
|
|
report: InvestigationReport
|
|
|
|
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 = HistoricalArtifacts.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"Validated {len(plan['nodes'])} nodes and {len(evidence)} synthetic evidence records; no writes. "
|
|
"Historical outcomes are unrecorded; automatic publication as a new report is unavailable.")
|
|
return
|
|
# A historical read is not a new investigation result. Reject before task
|
|
# creation, rather than inventing concluded/insufficient_data from old prose.
|
|
artifacts = AgentArtifactsCommand.model_validate(artifacts).model_dump(mode="json", by_alias=True)
|
|
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()))
|