store.py 68 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811
  1. """把本项目按 Run 落盘的业务文件投影成上游查看器使用的只读视图。"""
  2. from __future__ import annotations
  3. import hashlib
  4. import json
  5. import mimetypes
  6. import re
  7. from dataclasses import dataclass
  8. from datetime import datetime, timezone
  9. from pathlib import Path
  10. from typing import Any, Iterable
  11. PROJECT_ROOT = Path(__file__).resolve().parents[2]
  12. DEFAULT_RUN_ROOT = PROJECT_ROOT / "demo_output"
  13. def _load_json(path: Path, default: Any = None) -> Any:
  14. try:
  15. return json.loads(path.read_text(encoding="utf-8"))
  16. except (FileNotFoundError, json.JSONDecodeError, OSError):
  17. return default
  18. def _stable_id(value: str) -> int:
  19. """生成低于 JavaScript MAX_SAFE_INTEGER 的稳定数字 ID。"""
  20. return int(hashlib.sha256(value.encode("utf-8")).hexdigest()[:13], 16)
  21. def _iso(timestamp: float | None) -> str | None:
  22. if timestamp is None:
  23. return None
  24. return datetime.fromtimestamp(timestamp, tz=timezone.utc).isoformat()
  25. def _clip(value: Any, limit: int = 240) -> str:
  26. if value is None:
  27. return ""
  28. if not isinstance(value, str):
  29. value = json.dumps(value, ensure_ascii=False, default=str)
  30. return value if len(value) <= limit else value[: limit - 1] + "…"
  31. def _viewer_status(value: Any) -> str:
  32. normalized = str(value or "").upper()
  33. if normalized in {"COMPLETED", "PASS", "SUCCESS", "SUCCEEDED"} or (
  34. normalized.endswith("_COMPLETED")
  35. ):
  36. return "success"
  37. if normalized in {"FAILED", "FAIL", "ERROR"}:
  38. return "failed"
  39. return "running"
  40. def _files(root: Path, pattern: str) -> list[Path]:
  41. return sorted(root.glob(pattern))
  42. def _version(path: Path) -> int:
  43. match = re.search(r"\.v(\d+)\.json$", path.name)
  44. return int(match.group(1)) if match else 0
  45. def _latest_by_subject(paths: Iterable[Path]) -> dict[str, Path]:
  46. """按 ``Subject.vN.json`` 的 Subject 选择最新正式版本。"""
  47. result: dict[str, Path] = {}
  48. for path in paths:
  49. subject = re.sub(r"\.v\d+$", "", path.stem)
  50. current = result.get(subject)
  51. if current is None or _version(path) > _version(current):
  52. result[subject] = path
  53. return result
  54. def _mtime_bounds(root: Path) -> tuple[float | None, float | None]:
  55. values = [
  56. path.stat().st_mtime
  57. for path in root.rglob("*")
  58. if path.is_file() and path.name != ".run.lock"
  59. ]
  60. return (min(values), max(values)) if values else (None, None)
  61. def _input_block(
  62. key: str,
  63. title: str,
  64. value: Any,
  65. *,
  66. source: str = "",
  67. images: Iterable[str] = (),
  68. ) -> dict[str, Any]:
  69. return {
  70. "key": key,
  71. "name": key,
  72. "title": title,
  73. "hint": source,
  74. "source": source,
  75. "filled": value not in (None, "", [], {}),
  76. "text": _clip(value, 1600),
  77. "images": list(images),
  78. }
  79. def _node_card(
  80. index: int,
  81. *,
  82. node_type: str,
  83. label: str,
  84. summary: Any,
  85. ok: bool | None = None,
  86. detail: Any = None,
  87. tool_call_id: str = "",
  88. ) -> dict[str, Any]:
  89. return {
  90. "seq": index,
  91. "kind": "node",
  92. "fanout_group": "",
  93. "card": {
  94. "i": index,
  95. "type": node_type,
  96. "label": label,
  97. "summary": _clip(summary, 300),
  98. "ok": ok,
  99. "images": [],
  100. "branch": "",
  101. "node_id": None,
  102. "tool_call_id": tool_call_id,
  103. "io": None,
  104. "calls": [],
  105. "reasoning": "",
  106. "tokens": "",
  107. "detail": detail if detail is not None else summary,
  108. },
  109. }
  110. @dataclass(frozen=True)
  111. class RunRef:
  112. numeric_id: int
  113. key: str
  114. path: Path
  115. kind: str
  116. class FileRunStore:
  117. """扫描 ``demo_output``,不读取或修改数据库、Checkpoint。"""
  118. def __init__(self, run_root: Path | None = None):
  119. self.run_root = (run_root or DEFAULT_RUN_ROOT).resolve()
  120. def discover(self) -> list[RunRef]:
  121. if not self.run_root.is_dir():
  122. return []
  123. refs: list[RunRef] = []
  124. for path in sorted(self.run_root.iterdir()):
  125. if not path.is_dir():
  126. continue
  127. kind = self._kind(path)
  128. if kind is None:
  129. continue
  130. refs.append(
  131. RunRef(
  132. numeric_id=_stable_id(path.name),
  133. key=path.name,
  134. path=path.resolve(),
  135. kind=kind,
  136. )
  137. )
  138. return sorted(
  139. refs,
  140. key=lambda item: _mtime_bounds(item.path)[1] or 0,
  141. reverse=True,
  142. )
  143. def get(self, numeric_id: int) -> RunRef:
  144. for ref in self.discover():
  145. if ref.numeric_id == numeric_id:
  146. return ref
  147. raise KeyError(numeric_id)
  148. @staticmethod
  149. def _kind(path: Path) -> str | None:
  150. if (path / "run_summary.json").is_file() or (
  151. path / "global_data_stage_delivery.json"
  152. ).is_file():
  153. return "global_data"
  154. if _files(path, "production_plans/production_dag.v*.json"):
  155. return "segment"
  156. return None
  157. def brief(self, ref: RunRef) -> dict[str, Any]:
  158. start, end = _mtime_bounds(ref.path)
  159. if ref.kind == "global_data":
  160. summary = _load_json(ref.path / "run_summary.json", {}) or {}
  161. stage_delivery = _load_json(
  162. ref.path / "global_data_stage_delivery.json", {}
  163. ) or {}
  164. metrics = _load_json(ref.path / "run_metrics.json", {}) or {}
  165. totals = metrics.get("model_totals") or {}
  166. invocation = metrics.get("run_invocations") or {}
  167. duration_ms = invocation.get("total_duration_ms")
  168. plan = self._latest_plan(ref.path)
  169. raw_status = (
  170. summary.get("status")
  171. or stage_delivery.get("status")
  172. or "RUNNING"
  173. )
  174. tasks = (plan or {}).get("tasks") or []
  175. plan_version = int(
  176. (plan or {}).get("plan_version") or 1
  177. )
  178. task_reports = [
  179. _load_json(
  180. ref.path
  181. / "validation_results"
  182. / f"{task.get('task_id')}.v{plan_version}.json",
  183. {},
  184. ) or {}
  185. for task in tasks
  186. ]
  187. task_passed = sum(
  188. report.get("verdict") == "PASS"
  189. for report in task_reports
  190. )
  191. task_failed = sum(
  192. report.get("verdict") == "FAIL"
  193. for report in task_reports
  194. )
  195. return {
  196. "id": ref.numeric_id,
  197. "run_key": ref.key,
  198. "run_kind": ref.kind,
  199. "agent_name": "global_data",
  200. "model_name": self._model_names(metrics),
  201. "objective": (plan or {}).get("goal") or ref.key,
  202. "status": _viewer_status(raw_status),
  203. "raw_status": raw_status,
  204. "business_label": f"Global Data · {len(tasks)} 个资料任务",
  205. "business_progress": (
  206. f"{task_passed} 已通过"
  207. + (f" · {task_failed} 未通过" if task_failed else "")
  208. ),
  209. "parent_run_id": None,
  210. "start_time": _iso(start),
  211. "end_time": _iso(end),
  212. "duration_sec": (
  213. round(float(duration_ms) / 1000, 1)
  214. if isinstance(duration_ms, (int, float))
  215. else None
  216. ),
  217. "step_count": len(summary.get("event_log") or []),
  218. "replan_count": summary.get("replan_count") or 0,
  219. "input_tokens": totals.get("input_tokens") or 0,
  220. "output_tokens": totals.get("output_tokens") or 0,
  221. "cost_usd": totals.get("reported_cost_usd") or 0,
  222. "cost_status": (
  223. "reported"
  224. if totals.get("reported_cost_usd") is not None
  225. else "unavailable"
  226. ),
  227. }
  228. plan = self._latest_production_plan(ref.path) or {}
  229. run_summary = _load_json(
  230. ref.path / "production_run_summary.json", {}
  231. ) or {}
  232. reports = _files(ref.path, "segment_validation_results/*.v*.json")
  233. summaries = _files(ref.path, "segment_summaries/*.v*.json")
  234. metrics = _load_json(ref.path / "run_metrics.json", {}) or {}
  235. totals = metrics.get("model_totals") or {}
  236. latest_reports = [
  237. _load_json(path, {}) or {}
  238. for path in _latest_by_subject(reports).values()
  239. ]
  240. segments = plan.get("segments") or []
  241. segment_passed = sum(
  242. report.get("verdict") == "PASS"
  243. for report in latest_reports
  244. )
  245. segment_failed = sum(
  246. report.get("verdict") == "FAIL"
  247. for report in latest_reports
  248. )
  249. raw_status = run_summary.get("status") or "RUNNING"
  250. if not run_summary and reports:
  251. verdicts = [
  252. (_load_json(path, {}) or {}).get("verdict") for path in reports
  253. ]
  254. raw_status = "COMPLETED" if verdicts and all(
  255. value == "PASS" for value in verdicts
  256. ) else "FAILED"
  257. elif not run_summary and summaries:
  258. raw_status = (_load_json(summaries[-1], {}) or {}).get(
  259. "status", "RUNNING"
  260. )
  261. return {
  262. "id": ref.numeric_id,
  263. "run_key": ref.key,
  264. "run_kind": ref.kind,
  265. "agent_name": "segment_production",
  266. "model_name": self._model_names(metrics),
  267. "objective": plan.get("summary") or f"Production Segment · {ref.key}",
  268. "status": _viewer_status(raw_status),
  269. "raw_status": raw_status,
  270. "business_label": f"Production · {len(segments)} 个 Segment",
  271. "business_progress": (
  272. (
  273. f"停在 {run_summary.get('current_segment_id')} · "
  274. f"{run_summary.get('phase')}"
  275. )
  276. if run_summary.get("current_segment_id")
  277. else (
  278. f"{segment_passed} 已通过"
  279. + (
  280. f" · {segment_failed} 未通过"
  281. if segment_failed else ""
  282. )
  283. )
  284. ),
  285. "parent_run_id": None,
  286. "start_time": _iso(start),
  287. "end_time": _iso(end),
  288. "duration_sec": (
  289. round(end - start, 1)
  290. if start is not None and end is not None
  291. else None
  292. ),
  293. "step_count": len(_files(ref.path, "tool_operations/*.json")),
  294. "replan_count": 0,
  295. "input_tokens": totals.get("input_tokens") or 0,
  296. "output_tokens": totals.get("output_tokens") or 0,
  297. "cost_usd": totals.get("reported_cost_usd") or 0,
  298. "cost_status": (
  299. "reported"
  300. if totals.get("reported_cost_usd") is not None
  301. else "unavailable"
  302. ),
  303. }
  304. @staticmethod
  305. def _model_names(metrics: dict[str, Any]) -> str:
  306. names: list[str] = []
  307. for item in (metrics.get("models") or {}).values():
  308. for name in item.get("models") or []:
  309. if name and name not in names:
  310. names.append(str(name))
  311. return " · ".join(names)
  312. @staticmethod
  313. def _latest_plan(run_dir: Path) -> dict[str, Any] | None:
  314. plans = _files(run_dir, "plans/global_data_dag.v*.json")
  315. return _load_json(plans[-1], {}) if plans else None
  316. @staticmethod
  317. def _latest_production_plan(
  318. run_dir: Path,
  319. ) -> dict[str, Any] | None:
  320. plans = _files(
  321. run_dir,
  322. "production_plans/production_dag.v*.json",
  323. )
  324. if not plans:
  325. return None
  326. latest = max(
  327. plans,
  328. key=lambda path: int(
  329. re.search(r"\.v(\d+)\.json$", path.name).group(1)
  330. ),
  331. )
  332. return _load_json(latest, {})
  333. def detail(self, ref: RunRef) -> dict[str, Any]:
  334. result = self.brief(ref)
  335. if ref.kind == "global_data":
  336. summary = _load_json(ref.path / "run_summary.json", {}) or {}
  337. delivery = _load_json(
  338. ref.path / "global_data_stage_delivery.json", {}
  339. ) or {}
  340. result.update(
  341. {
  342. "final_output": delivery.get("summary"),
  343. "error_message": summary.get("error"),
  344. "input_payload": {
  345. "input_path": summary.get("input_path"),
  346. "input_sha256": summary.get("input_sha256"),
  347. "protocol_version": summary.get("protocol_version"),
  348. },
  349. "overview": self._global_overview(ref, summary),
  350. "steps": self._global_steps(summary),
  351. }
  352. )
  353. else:
  354. plan = self._latest_production_plan(ref.path) or {}
  355. summary = _load_json(
  356. ref.path / "production_run_summary.json", {}
  357. ) or {}
  358. result.update(
  359. {
  360. "final_output": self._segment_final_output(ref),
  361. "error_message": summary.get("error"),
  362. "input_payload": plan,
  363. "overview": self._segment_overview(ref),
  364. "steps": self._segment_steps(ref),
  365. }
  366. )
  367. result["business_story"] = self._business_story(ref)
  368. return result
  369. def _global_overview(
  370. self, ref: RunRef, summary: dict[str, Any]
  371. ) -> dict[str, int]:
  372. plans = _files(ref.path, "plans/global_data_dag.v*.json")
  373. latest = _load_json(plans[-1], {}) if plans else {}
  374. tasks = (latest or {}).get("tasks") or []
  375. version = int((latest or {}).get("plan_version") or len(plans) or 1)
  376. reports = [
  377. _load_json(
  378. ref.path
  379. / "validation_results"
  380. / f"{task.get('task_id')}.v{version}.json",
  381. {},
  382. )
  383. or {}
  384. for task in tasks
  385. ]
  386. result_files = _files(ref.path, "executor_results/Task*.v*.json")
  387. distinct_tasks = {
  388. re.sub(r"\.v\d+$", "", path.stem) for path in result_files
  389. }
  390. return {
  391. "plan_rounds": len(plans),
  392. "execute_rounds": int(
  393. summary.get("executor_calls") or len(result_files)
  394. ),
  395. "task_count": len(tasks),
  396. "passed": sum(report.get("verdict") == "PASS" for report in reports),
  397. "failed": sum(report.get("verdict") == "FAIL" for report in reports),
  398. "retries": max(len(result_files) - len(distinct_tasks), 0),
  399. }
  400. def _segment_overview(self, ref: RunRef) -> dict[str, int]:
  401. plan = self._latest_production_plan(ref.path) or {}
  402. packages = _files(ref.path, "segment_packages/*.json")
  403. latest_reports = {
  404. segment_id: _load_json(path, {}) or {}
  405. for segment_id, path in _latest_by_subject(
  406. _files(ref.path, "segment_validation_results/*.v*.json")
  407. ).items()
  408. }
  409. reports = list(latest_reports.values())
  410. return {
  411. "plan_rounds": len(
  412. _files(
  413. ref.path,
  414. "production_plans/production_dag.v*.json",
  415. )
  416. ),
  417. "execute_rounds": len(_files(ref.path, "tool_operations/*.json")),
  418. "task_count": len(plan.get("segments") or packages),
  419. "passed": sum(report.get("verdict") == "PASS" for report in reports),
  420. "failed": sum(report.get("verdict") == "FAIL" for report in reports),
  421. "retries": 0,
  422. }
  423. @staticmethod
  424. def _global_steps(summary: dict[str, Any]) -> list[dict[str, Any]]:
  425. steps = []
  426. for index, message in enumerate(summary.get("event_log") or [], start=1):
  427. lower = message.lower()
  428. if "planner" in lower:
  429. node_type = "plan"
  430. elif "executor" in lower:
  431. node_type = "execute"
  432. elif "validator" in lower or "验收" in message:
  433. node_type = "validate"
  434. elif "预处理" in message:
  435. node_type = "preprocess"
  436. elif "完成" in message:
  437. node_type = "finalize"
  438. else:
  439. node_type = "route"
  440. steps.append(
  441. {
  442. "step_id": index,
  443. "step_index": index,
  444. "node_type": node_type,
  445. "content": message,
  446. "delta": {"event": message},
  447. "input_tokens": 0,
  448. "output_tokens": 0,
  449. "created_at": None,
  450. }
  451. )
  452. return steps
  453. def _segment_steps(self, ref: RunRef) -> list[dict[str, Any]]:
  454. steps: list[dict[str, Any]] = []
  455. package_paths = _files(ref.path, "segment_packages/*.json")
  456. if package_paths:
  457. package = _load_json(package_paths[0], {}) or {}
  458. steps.append(
  459. {
  460. "step_id": 1,
  461. "step_index": 1,
  462. "node_type": "prepare_segment",
  463. "content": f"物化 {package.get('segment_id', 'Segment')} Package",
  464. "delta": package,
  465. "input_tokens": 0,
  466. "output_tokens": 0,
  467. "created_at": _iso(package_paths[0].stat().st_mtime),
  468. }
  469. )
  470. for index, path in enumerate(
  471. _files(ref.path, "tool_operations/*.json"), start=2
  472. ):
  473. operation = _load_json(path, {}) or {}
  474. steps.append(
  475. {
  476. "step_id": index,
  477. "step_index": index,
  478. "node_type": "execute",
  479. "content": (
  480. f"{operation.get('tool_name', 'tool')} · "
  481. f"{operation.get('status', 'UNKNOWN')}"
  482. ),
  483. "delta": operation,
  484. "input_tokens": 0,
  485. "output_tokens": 0,
  486. "created_at": _iso(path.stat().st_mtime),
  487. }
  488. )
  489. return steps
  490. @staticmethod
  491. def _segment_final_output(ref: RunRef) -> Any:
  492. final_delivery = _load_json(
  493. ref.path / "final_production_delivery.json", None
  494. )
  495. if final_delivery:
  496. return final_delivery
  497. summary = _load_json(
  498. ref.path / "production_run_summary.json", {}
  499. ) or {}
  500. reports = _files(ref.path, "segment_validation_results/*.v*.json")
  501. if reports:
  502. return _load_json(reports[-1], {})
  503. if summary.get("status") == "FAILED":
  504. return {
  505. "status": "FAILED",
  506. "phase": summary.get("phase"),
  507. "failure_code": summary.get("failure_code"),
  508. "message": summary.get("error") or summary.get("event"),
  509. }
  510. packages = _files(ref.path, "segment_packages/*.json")
  511. if packages:
  512. package = _load_json(packages[-1], {}) or {}
  513. return {
  514. "status": "RUNNING",
  515. "segment_id": package.get("segment_id"),
  516. "message": "Segment 已物化,尚无终态 ValidationReport",
  517. }
  518. return None
  519. def _business_story(self, ref: RunRef) -> dict[str, Any]:
  520. if ref.kind == "global_data":
  521. return self._global_business_story(ref)
  522. return self._production_business_story(ref)
  523. def _artifact_item(
  524. self, ref: RunRef, artifact: dict[str, Any]
  525. ) -> dict[str, Any]:
  526. uri = artifact.get("uri") or artifact.get("source_uri")
  527. return {
  528. "label": (
  529. artifact.get("description")
  530. or artifact.get("artifact_id")
  531. or artifact.get("artifact_type")
  532. or "正式产物"
  533. ),
  534. "detail": " · ".join(
  535. str(value)
  536. for value in (
  537. artifact.get("artifact_type"),
  538. artifact.get("mime_type"),
  539. )
  540. if value
  541. ),
  542. "source": artifact.get("artifact_id") or "",
  543. "media_url": self.media_url(ref, uri),
  544. }
  545. @staticmethod
  546. def _criteria(report: dict[str, Any]) -> list[dict[str, Any]]:
  547. source = (
  548. report.get("criterion_results")
  549. or report.get("requirement_results")
  550. or report.get("checks")
  551. or []
  552. )
  553. return [
  554. {
  555. "label": (
  556. item.get("expectation_id")
  557. or item.get("requirement_id")
  558. or item.get("name")
  559. or item.get("verification_capability")
  560. or f"判断项 {index}"
  561. ),
  562. "verdict": item.get("verdict") or item.get("status") or "UNKNOWN",
  563. "reason": item.get("reason") or item.get("summary") or "",
  564. "evidence": item.get("evidence") or [],
  565. }
  566. for index, item in enumerate(source, start=1)
  567. if isinstance(item, dict)
  568. ]
  569. def _global_business_story(self, ref: RunRef) -> dict[str, Any]:
  570. summary = _load_json(ref.path / "run_summary.json", {}) or {}
  571. delivery = _load_json(
  572. ref.path / "global_data_stage_delivery.json", {}
  573. ) or {}
  574. plan_paths = _files(ref.path, "plans/global_data_dag.v*.json")
  575. rounds: list[dict[str, Any]] = []
  576. for round_index, plan_path in enumerate(plan_paths, start=1):
  577. plan = _load_json(plan_path, {}) or {}
  578. version = int(plan.get("plan_version") or _version(plan_path) or 1)
  579. steps: list[dict[str, Any]] = []
  580. for task in self._ordered_tasks(ref, plan.get("tasks") or []):
  581. task_id = str(task.get("task_id") or "")
  582. execution = _load_json(
  583. ref.path
  584. / "executor_results"
  585. / f"{task_id}.v{version}.json",
  586. {},
  587. ) or {}
  588. report = _load_json(
  589. ref.path
  590. / "validation_results"
  591. / f"{task_id}.v{version}.json",
  592. {},
  593. ) or {}
  594. verdict = report.get("verdict")
  595. status = (
  596. "success" if verdict == "PASS"
  597. else "failed" if verdict == "FAIL"
  598. else "running" if execution
  599. else "pending"
  600. )
  601. basis = [
  602. {
  603. "label": "业务来源",
  604. "detail": source,
  605. "source": source,
  606. }
  607. for source in task.get("source_paths") or []
  608. ]
  609. basis.extend(
  610. {
  611. "label": "验收目标",
  612. "detail": expectation,
  613. "source": expectation,
  614. }
  615. for expectation in task.get("expectation_ids") or []
  616. )
  617. basis.extend(
  618. {
  619. "label": "前置依赖",
  620. "detail": dependency,
  621. "source": dependency,
  622. }
  623. for dependency in task.get("depends_on") or []
  624. )
  625. outputs = [
  626. self._artifact_item(ref, artifact)
  627. for artifact in execution.get("artifacts") or []
  628. if isinstance(artifact, dict)
  629. ]
  630. steps.append(
  631. {
  632. "id": task_id,
  633. "stage": "GLOBAL DATA",
  634. "title": task.get("objective") or task_id,
  635. "status": status,
  636. "action": (
  637. execution.get("summary")
  638. or "已纳入本轮计划,尚未形成正式执行交付。"
  639. ),
  640. "reason": task.get("reason") or "",
  641. "basis": basis,
  642. "judgment": {
  643. "verdict": verdict or "PENDING",
  644. "summary": (
  645. report.get("summary")
  646. or "尚无独立 Validator 判断。"
  647. ),
  648. "criteria": self._criteria(report),
  649. },
  650. "outputs": outputs,
  651. "tools": [
  652. {
  653. "name": call.get("tool_name") or "tool",
  654. "status": (
  655. "SUCCEEDED"
  656. if call.get("success") is True
  657. else "FAILED"
  658. if call.get("success") is False
  659. else "UNKNOWN"
  660. ),
  661. "detail": call.get("output_excerpt") or "",
  662. }
  663. for call in execution.get("tool_calls") or []
  664. if isinstance(call, dict)
  665. ],
  666. }
  667. )
  668. if round_index == len(plan_paths):
  669. stage_paths = _files(
  670. ref.path, "stage_validation_results/global_data.v*.json"
  671. )
  672. if stage_paths:
  673. stage_report = _load_json(stage_paths[-1], {}) or {}
  674. stage_verdict = stage_report.get("verdict")
  675. steps.append(
  676. {
  677. "id": "GLOBAL_DATA_STAGE",
  678. "stage": "STAGE GATE",
  679. "title": "判断 Global Data 是否可交付给 Production",
  680. "status": (
  681. "success" if stage_verdict == "PASS"
  682. else "failed" if stage_verdict == "FAIL"
  683. else "running"
  684. ),
  685. "action": "汇总所有 Task 的正式产物,检查覆盖、冲突与未解决项。",
  686. "reason": "单个素材通过不等于整套生产输入完整,阶段门负责做跨 Task 的最终判断。",
  687. "basis": [
  688. {
  689. "label": "阶段输入",
  690. "detail": f"{len(plan.get('tasks') or [])} 个 Task 的正式交付与独立验收",
  691. "source": stage_paths[-1].name,
  692. }
  693. ],
  694. "judgment": {
  695. "verdict": stage_verdict or "PENDING",
  696. "summary": stage_report.get("summary") or "",
  697. "criteria": self._criteria(stage_report),
  698. },
  699. "outputs": [
  700. {
  701. "label": "Global Data Stage Delivery",
  702. "detail": delivery.get("summary") or "",
  703. "source": "global_data_stage_delivery.json",
  704. "media_url": None,
  705. }
  706. ] if delivery else [],
  707. "tools": [],
  708. }
  709. )
  710. passed = sum(step["status"] == "success" for step in steps)
  711. failed = sum(step["status"] == "failed" for step in steps)
  712. rounds.append(
  713. {
  714. "index": round_index,
  715. "title": f"Global Data Plan v{version}",
  716. "status": (
  717. "failed" if failed
  718. else "success" if steps and passed == len(steps)
  719. else "running"
  720. ),
  721. "summary": (
  722. plan.get("revision_summary")
  723. or plan.get("goal")
  724. or "Global Data 规划与验收"
  725. ),
  726. "steps": steps,
  727. }
  728. )
  729. raw_status = (
  730. summary.get("status") or delivery.get("status") or "RUNNING"
  731. )
  732. return {
  733. "kind": "global_data",
  734. "protocol_version": (
  735. summary.get("protocol_version")
  736. or (self._latest_plan(ref.path) or {}).get("schema_version")
  737. or ""
  738. ),
  739. "status": _viewer_status(raw_status),
  740. "phase": summary.get("phase") or "GLOBAL_DATA",
  741. "headline": "把 Production 需要的共享素材与约束准备完整",
  742. "conclusion": (
  743. delivery.get("summary")
  744. or summary.get("error")
  745. or "运行尚未形成阶段交付。"
  746. ),
  747. "failure": (
  748. {
  749. "code": summary.get("failure_code") or "GLOBAL_DATA_FAILED",
  750. "message": summary.get("error") or "",
  751. "stage": summary.get("phase") or "GLOBAL_DATA",
  752. }
  753. if _viewer_status(raw_status) == "failed"
  754. else None
  755. ),
  756. "rounds": rounds,
  757. }
  758. @staticmethod
  759. def _operation_segment(operation: dict[str, Any]) -> str:
  760. operation_id = str(
  761. operation.get("operation_scope")
  762. or operation.get("operation_id")
  763. or ""
  764. )
  765. match = re.search(r":(Segment[^:]+):v\d+:", operation_id)
  766. return match.group(1) if match else ""
  767. def _production_business_story(self, ref: RunRef) -> dict[str, Any]:
  768. summary = _load_json(
  769. ref.path / "production_run_summary.json", {}
  770. ) or {}
  771. plan_paths = _files(
  772. ref.path, "production_plans/production_dag.v*.json"
  773. )
  774. all_operations = [
  775. _load_json(path, {}) or {}
  776. for path in _files(ref.path, "tool_operations/*.json")
  777. ]
  778. rounds: list[dict[str, Any]] = []
  779. for round_index, plan_path in enumerate(plan_paths, start=1):
  780. plan = _load_json(plan_path, {}) or {}
  781. version = int(plan.get("plan_version") or _version(plan_path) or 1)
  782. steps: list[dict[str, Any]] = []
  783. for segment in plan.get("segments") or []:
  784. segment_id = str(segment.get("segment_id") or "")
  785. package_path = (
  786. ref.path
  787. / "segment_packages"
  788. / f"{segment_id}.v{version}.json"
  789. )
  790. package = _load_json(package_path, {}) or {}
  791. delivery_paths = _files(
  792. ref.path,
  793. f"segment_deliveries/{segment_id}.v*.json",
  794. )
  795. report_paths = _files(
  796. ref.path,
  797. f"segment_validation_results/{segment_id}.v*.json",
  798. )
  799. delivery = (
  800. _load_json(delivery_paths[-1], {}) or {}
  801. if delivery_paths else {}
  802. )
  803. report = (
  804. _load_json(report_paths[-1], {}) or {}
  805. if report_paths else {}
  806. )
  807. operations = [
  808. item
  809. for item in all_operations
  810. if self._operation_segment(item) == segment_id
  811. ]
  812. is_failed_segment = (
  813. summary.get("status") == "FAILED"
  814. and summary.get("current_segment_id") == segment_id
  815. )
  816. verdict = report.get("verdict")
  817. status = (
  818. "success" if verdict == "PASS"
  819. else "failed" if verdict == "FAIL" or is_failed_segment
  820. else "running" if package or operations
  821. else "pending"
  822. )
  823. artifact_inputs = (
  824. package.get("artifact_inputs")
  825. or segment.get("artifact_inputs")
  826. or []
  827. )
  828. basis = []
  829. for item in artifact_inputs:
  830. if not isinstance(item, dict):
  831. continue
  832. basis.append(
  833. {
  834. "label": (
  835. item.get("usage")
  836. or item.get("artifact_id")
  837. or "正式素材"
  838. ),
  839. "detail": (
  840. item.get("artifact_id")
  841. or item.get("input_id")
  842. or _clip(item.get("value"), 180)
  843. ),
  844. "source": (
  845. item.get("source_path")
  846. or item.get("artifact_id")
  847. or ""
  848. ),
  849. "media_url": self.media_url(
  850. ref, item.get("uri") or item.get("source_uri")
  851. ),
  852. }
  853. )
  854. basis.extend(
  855. {
  856. "label": "前置 Segment",
  857. "detail": dependency,
  858. "source": dependency,
  859. }
  860. for dependency in (
  861. segment.get("depends_on")
  862. or package.get("dependencies")
  863. or []
  864. )
  865. )
  866. outputs = [
  867. self._artifact_item(ref, artifact)
  868. for artifact in delivery.get("artifacts") or []
  869. if isinstance(artifact, dict)
  870. ]
  871. judgment_summary = report.get("summary") or ""
  872. judgment_verdict = verdict or "PENDING"
  873. if is_failed_segment:
  874. judgment_verdict = "FAIL"
  875. judgment_summary = (
  876. summary.get("error")
  877. or summary.get("event")
  878. or "Segment 执行失败。"
  879. )
  880. steps.append(
  881. {
  882. "id": segment_id,
  883. "stage": "PRODUCTION",
  884. "title": (
  885. segment.get("objective")
  886. or package.get("objective")
  887. or f"生产 {segment_id}"
  888. ),
  889. "status": status,
  890. "action": (
  891. delivery.get("summary")
  892. or (
  893. f"执行了 {len(operations)} 次媒体工具操作,"
  894. "但尚未形成合法 Segment Delivery。"
  895. if operations else
  896. "已规划,尚未进入正式生产。"
  897. )
  898. ),
  899. "reason": (
  900. f"完成时间线 {(
  901. package.get('timeline')
  902. or segment.get('timeline')
  903. or {}
  904. ).get('start_ms', '?')}–{(
  905. package.get('timeline')
  906. or segment.get('timeline')
  907. or {}
  908. ).get('end_ms', '?')} ms 的独立可验收片段。"
  909. ),
  910. "basis": basis,
  911. "judgment": {
  912. "verdict": judgment_verdict,
  913. "summary": (
  914. judgment_summary
  915. or "尚无正式 Segment ValidationReport。"
  916. ),
  917. "criteria": self._criteria(report),
  918. },
  919. "outputs": outputs,
  920. "tools": [
  921. {
  922. "name": operation.get("tool_name") or "tool",
  923. "status": operation.get("status") or "UNKNOWN",
  924. "detail": _clip(operation.get("result"), 360),
  925. }
  926. for operation in sorted(
  927. operations,
  928. key=lambda item: item.get("ordinal") or 0,
  929. )
  930. ],
  931. }
  932. )
  933. failed = sum(step["status"] == "failed" for step in steps)
  934. passed = sum(step["status"] == "success" for step in steps)
  935. rounds.append(
  936. {
  937. "index": round_index,
  938. "title": f"Production Plan v{version}",
  939. "status": (
  940. "failed" if failed
  941. else "success" if steps and passed == len(steps)
  942. else "running"
  943. ),
  944. "summary": (
  945. plan.get("revision_summary")
  946. or plan.get("summary")
  947. or f"按 {len(steps)} 个 Segment 组织正式生产"
  948. ),
  949. "steps": steps,
  950. }
  951. )
  952. raw_status = summary.get("status") or "RUNNING"
  953. return {
  954. "kind": "production",
  955. "protocol_version": (
  956. summary.get("schema_version")
  957. or (self._latest_production_plan(ref.path) or {}).get(
  958. "schema_version"
  959. )
  960. or ""
  961. ),
  962. "status": _viewer_status(raw_status),
  963. "phase": summary.get("phase") or "PRODUCTION",
  964. "headline": "按 Segment 生产、独立验收,再决定是否进入最终交付",
  965. "conclusion": (
  966. summary.get("event")
  967. or summary.get("error")
  968. or "运行尚未形成正式 Production 结论。"
  969. ),
  970. "failure": (
  971. {
  972. "code": summary.get("failure_code") or "PRODUCTION_FAILED",
  973. "message": summary.get("error") or summary.get("event") or "",
  974. "stage": summary.get("phase") or "PRODUCTION",
  975. }
  976. if _viewer_status(raw_status) == "failed"
  977. else None
  978. ),
  979. "rounds": rounds,
  980. }
  981. def snapshots(self, ref: RunRef) -> dict[str, Any]:
  982. if ref.kind == "global_data":
  983. snapshots = self._global_snapshots(ref)
  984. else:
  985. snapshots = self._segment_snapshots(ref)
  986. return {
  987. "run_id": ref.numeric_id,
  988. "run_key": ref.key,
  989. "objective": self.brief(ref)["objective"],
  990. "status": self.brief(ref)["status"],
  991. "snapshots": snapshots,
  992. }
  993. def _global_snapshots(self, ref: RunRef) -> list[dict[str, Any]]:
  994. paths = _files(ref.path, "plans/global_data_dag.v*.json")
  995. output: list[dict[str, Any]] = []
  996. previous_ids: set[str] = set()
  997. for index, path in enumerate(paths):
  998. plan = _load_json(path, {}) or {}
  999. version = plan.get("plan_version") or index + 1
  1000. tasks = plan.get("tasks") or []
  1001. current_ids = {str(item.get("task_id")) for item in tasks}
  1002. added = sorted(current_ids - previous_ids)
  1003. removed = sorted(previous_ids - current_ids)
  1004. nodes = [
  1005. self._snapshot_task(ref, task, version) for task in tasks
  1006. ]
  1007. output.append(
  1008. {
  1009. "loop_index": index,
  1010. "kind": "initial" if index == 0 else "replan",
  1011. "title": f"Plan v{version}",
  1012. "step_id": index + 1,
  1013. "step_index": index + 1,
  1014. "is_final": index == len(paths) - 1,
  1015. "created_at": _iso(path.stat().st_mtime),
  1016. "reasoning": plan.get("revision_summary") or plan.get("goal"),
  1017. "tree_changed": bool(added or removed),
  1018. "diff": {
  1019. "added": added,
  1020. "removed": removed,
  1021. "modified": [],
  1022. },
  1023. "lessons_added": [],
  1024. "lessons_cumulative": [],
  1025. "tree": {"nodes": nodes},
  1026. "node_change": {
  1027. **{item: "added" for item in added},
  1028. **{item: "removed" for item in removed},
  1029. },
  1030. "round_execution": None,
  1031. }
  1032. )
  1033. previous_ids = current_ids
  1034. return output
  1035. def _snapshot_task(
  1036. self, ref: RunRef, task: dict[str, Any], version: int
  1037. ) -> dict[str, Any]:
  1038. task_id = str(task.get("task_id") or "")
  1039. delivery = _load_json(
  1040. ref.path / "executor_results" / f"{task_id}.v{version}.json", {}
  1041. ) or {}
  1042. report = _load_json(
  1043. ref.path / "validation_results" / f"{task_id}.v{version}.json", {}
  1044. ) or {}
  1045. verdict = report.get("verdict")
  1046. status = "completed" if verdict == "PASS" else (
  1047. "failed" if verdict == "FAIL" else "pending"
  1048. )
  1049. return {
  1050. "step_id": task_id,
  1051. "task_id": task_id,
  1052. "parent_id": None,
  1053. "name": task.get("objective") or task_id,
  1054. "goal": task.get("reason") or "",
  1055. "status": status,
  1056. "kind": task.get("skill_id"),
  1057. "acceptance": task.get("expectation_ids") or [],
  1058. "inputs": task.get("depends_on") or [],
  1059. "stage": "GLOBAL_DATA",
  1060. "category": task.get("deliverable_type") or "",
  1061. "executing": False,
  1062. "result": (
  1063. {
  1064. "summary": delivery.get("summary") or report.get("summary"),
  1065. "product_url": None,
  1066. "product_text": None,
  1067. "verdict": report,
  1068. "attempts": 1,
  1069. }
  1070. if delivery or report
  1071. else None
  1072. ),
  1073. "exec_step_id": None,
  1074. "tools": [
  1075. item.get("tool_name")
  1076. for item in delivery.get("tool_calls") or []
  1077. if item.get("tool_name")
  1078. ],
  1079. "trace_summary": {
  1080. "llm": 0,
  1081. "tool": len(delivery.get("tool_calls") or []),
  1082. },
  1083. }
  1084. def _segment_snapshots(self, ref: RunRef) -> list[dict[str, Any]]:
  1085. plan = self._latest_production_plan(ref.path) or {}
  1086. packages = [
  1087. _load_json(path, {}) or {}
  1088. for path in _files(ref.path, "segment_packages/*.json")
  1089. ]
  1090. nodes = []
  1091. for package in packages:
  1092. segment_id = package.get("segment_id") or "Segment"
  1093. reports = _files(
  1094. ref.path,
  1095. f"segment_validation_results/{segment_id}.v*.json",
  1096. )
  1097. report = _load_json(reports[-1], {}) if reports else {}
  1098. verdict = (report or {}).get("verdict")
  1099. nodes.append(
  1100. {
  1101. "step_id": segment_id,
  1102. "task_id": segment_id,
  1103. "parent_id": None,
  1104. "name": segment_id,
  1105. "goal": f"生产并验收 {segment_id}",
  1106. "status": (
  1107. "completed" if verdict == "PASS"
  1108. else "failed" if verdict == "FAIL"
  1109. else "running"
  1110. ),
  1111. "kind": "segment-production",
  1112. "acceptance": [],
  1113. "inputs": [
  1114. item.get("input_id")
  1115. for item in package.get("production_inputs") or []
  1116. ],
  1117. "stage": "PRODUCTION",
  1118. "category": "video",
  1119. "executing": verdict is None,
  1120. "result": report or None,
  1121. "exec_step_id": None,
  1122. "tools": [],
  1123. "trace_summary": {"llm": 0, "tool": 0},
  1124. }
  1125. )
  1126. return [
  1127. {
  1128. "loop_index": 0,
  1129. "kind": "initial",
  1130. "title": f"Production Plan v{plan.get('plan_version', 1)}",
  1131. "step_id": 1,
  1132. "step_index": 1,
  1133. "is_final": self.brief(ref)["status"] != "running",
  1134. "created_at": None,
  1135. "reasoning": plan.get("summary") or "",
  1136. "tree_changed": True,
  1137. "diff": {
  1138. "added": [node["step_id"] for node in nodes],
  1139. "removed": [],
  1140. "modified": [],
  1141. },
  1142. "lessons_added": [],
  1143. "lessons_cumulative": [],
  1144. "tree": {"nodes": nodes},
  1145. "node_change": {
  1146. node["step_id"]: "added" for node in nodes
  1147. },
  1148. "round_execution": None,
  1149. }
  1150. ]
  1151. def flat(self, ref: RunRef, round_index: int | None) -> dict[str, Any]:
  1152. if ref.kind == "global_data":
  1153. return self._global_flat(ref, round_index)
  1154. return self._segment_flat(ref, round_index)
  1155. @staticmethod
  1156. def _module_def(
  1157. module_id: int,
  1158. key: str,
  1159. *,
  1160. name: str,
  1161. summary: str,
  1162. tools: Iterable[str] = (),
  1163. inputs: Iterable[tuple[str, str]] = (),
  1164. model: str = "",
  1165. refs: Iterable[tuple[str, str]] = (),
  1166. ) -> dict[str, Any]:
  1167. return {
  1168. "id": module_id,
  1169. "module_key": key,
  1170. "kind": "agent",
  1171. "name": name,
  1172. "summary": summary,
  1173. "spec": {
  1174. "node_kind": "agent",
  1175. "summary": summary,
  1176. "system_prompt": "",
  1177. "input_schema": [
  1178. {
  1179. "key": input_key,
  1180. "name": input_key,
  1181. "title": title,
  1182. "required": True,
  1183. "source": title,
  1184. }
  1185. for input_key, title in inputs
  1186. ],
  1187. "tools": [{"name": value} for value in tools],
  1188. "model": model,
  1189. },
  1190. "view": None,
  1191. "fingerprint": hashlib.sha256(
  1192. f"{key}:{summary}".encode("utf-8")
  1193. ).hexdigest()[:8],
  1194. "refs": [
  1195. {
  1196. "node_key": node_key,
  1197. "to_module_key": target,
  1198. "seq": index,
  1199. "title": target,
  1200. "cardinality": "one",
  1201. "fanout_over": "",
  1202. }
  1203. for index, (node_key, target) in enumerate(refs)
  1204. ],
  1205. }
  1206. @staticmethod
  1207. def _instance(
  1208. instance_id: int,
  1209. module_id: int,
  1210. module_key: str,
  1211. *,
  1212. label: str,
  1213. task_id: str | None,
  1214. ok: bool | None,
  1215. inputs: list[dict[str, Any]],
  1216. output: Any,
  1217. flow: list[dict[str, Any]],
  1218. used_tools: Iterable[str] = (),
  1219. images: Iterable[str] = (),
  1220. ) -> dict[str, Any]:
  1221. return {
  1222. "id": instance_id,
  1223. "module_id": module_id,
  1224. "module_key": module_key,
  1225. "label": label,
  1226. "task_id": task_id,
  1227. "step_id": task_id,
  1228. "step_index": instance_id,
  1229. "branch": "",
  1230. "ok": ok,
  1231. "input": {"blocks": inputs},
  1232. "output": {
  1233. "data": output,
  1234. "ok": ok,
  1235. "images": list(images),
  1236. } if output is not None else None,
  1237. "timing": {"sec": None},
  1238. "tokens": "",
  1239. "caller": None,
  1240. "used_tools": sorted(set(used_tools)),
  1241. "flow": flow,
  1242. }
  1243. def _global_flat(
  1244. self, ref: RunRef, round_index: int | None
  1245. ) -> dict[str, Any]:
  1246. plans = _files(ref.path, "plans/global_data_dag.v*.json")
  1247. metrics = _load_json(ref.path / "run_metrics.json", {}) or {}
  1248. model = self._model_names(metrics)
  1249. module_defs = {
  1250. 1: self._module_def(
  1251. 1, "production.global_data.plan",
  1252. name="Global Data Planner",
  1253. summary="读取 Production Brief,生成或修订完整 Global Data DAG。",
  1254. inputs=(("brief", "Production Brief"),),
  1255. model=model,
  1256. ),
  1257. 2: self._module_def(
  1258. 2, "production.global_data.execute",
  1259. name="Global Data Executor",
  1260. summary="按 TaskPackage 与 Skill 白名单执行一个 Task。",
  1261. inputs=(("task", "TaskPackage"),),
  1262. model=model,
  1263. ),
  1264. 3: self._module_def(
  1265. 3, "production.global_data.validate_task",
  1266. name="Task Validator",
  1267. summary="在独立上下文中逐项验收 Executor Delivery。",
  1268. inputs=(("delivery", "Executor Delivery"),),
  1269. model=model,
  1270. ),
  1271. 4: self._module_def(
  1272. 4, "production.global_data.validate_stage",
  1273. name="Stage Validator",
  1274. summary="全部 Task 通过后检查阶段完整性、冲突和漏项。",
  1275. inputs=(("plan", "Global Data Plan"), ("tasks", "PASS Deliveries")),
  1276. model=model,
  1277. ),
  1278. }
  1279. rounds: list[dict[str, Any]] = []
  1280. for index, path in enumerate(plans):
  1281. if round_index is not None and index != round_index:
  1282. continue
  1283. plan = _load_json(path, {}) or {}
  1284. version = int(plan.get("plan_version") or index + 1)
  1285. instances: list[dict[str, Any]] = []
  1286. next_id = (index + 1) * 1000
  1287. next_id += 1
  1288. planner_id = next_id
  1289. instances.append(
  1290. self._instance(
  1291. planner_id, 1, "production.global_data.plan",
  1292. label=f"Plan v{version}",
  1293. task_id=None,
  1294. ok=True,
  1295. inputs=[
  1296. _input_block(
  1297. "brief", "Production Brief",
  1298. plan.get("goal"), source="production_brief.json",
  1299. )
  1300. ],
  1301. output=plan,
  1302. flow=[
  1303. _node_card(
  1304. 0, node_type="llm", label="规划",
  1305. summary=plan.get("revision_summary") or plan.get("goal"),
  1306. ok=True, detail=plan,
  1307. )
  1308. ],
  1309. )
  1310. )
  1311. entry_ids = [planner_id]
  1312. for task in self._ordered_tasks(ref, plan.get("tasks") or []):
  1313. task_id = str(task.get("task_id"))
  1314. delivery = _load_json(
  1315. ref.path / "executor_results" / f"{task_id}.v{version}.json",
  1316. {},
  1317. ) or {}
  1318. report = _load_json(
  1319. ref.path / "validation_results" / f"{task_id}.v{version}.json",
  1320. {},
  1321. ) or {}
  1322. next_id += 1
  1323. executor_id = next_id
  1324. tools = [
  1325. item.get("tool_name")
  1326. for item in delivery.get("tool_calls") or []
  1327. if item.get("tool_name")
  1328. ]
  1329. flow = [
  1330. _node_card(
  1331. flow_index,
  1332. node_type="tool",
  1333. label=call.get("tool_name") or "tool",
  1334. summary=call.get("output_excerpt"),
  1335. ok=call.get("success"),
  1336. detail={
  1337. "input": call.get("arguments"),
  1338. "output": call.get("output_excerpt"),
  1339. },
  1340. tool_call_id=call.get("tool_call_id") or "",
  1341. )
  1342. for flow_index, call in enumerate(
  1343. delivery.get("tool_calls") or []
  1344. )
  1345. ]
  1346. if not flow and delivery:
  1347. flow = [
  1348. _node_card(
  1349. 0,
  1350. node_type="tool",
  1351. label="确定性交付",
  1352. summary=delivery.get("summary") or "已写入交付结果",
  1353. ok=True,
  1354. detail=delivery,
  1355. )
  1356. ]
  1357. delivery_view = self._delivery_view(ref, delivery)
  1358. image_urls = [
  1359. artifact["browser_uri"]
  1360. for artifact in (delivery_view or {}).get("artifacts") or []
  1361. if str(
  1362. artifact.get("mime_type")
  1363. or artifact.get("media_type")
  1364. or ""
  1365. ).startswith("image/")
  1366. and artifact.get("browser_uri")
  1367. ]
  1368. instances.append(
  1369. self._instance(
  1370. executor_id, 2, "production.global_data.execute",
  1371. label=task.get("objective") or task_id,
  1372. task_id=task_id,
  1373. ok=bool(delivery),
  1374. inputs=[
  1375. _input_block(
  1376. "task", "TaskPackage", task,
  1377. source=f"tasks/{task_id}.v{version}.json",
  1378. )
  1379. ],
  1380. output=delivery_view,
  1381. flow=flow or [
  1382. _node_card(
  1383. 0, node_type="note", label="等待执行",
  1384. summary="尚无 Executor Delivery", ok=None,
  1385. )
  1386. ],
  1387. used_tools=tools,
  1388. images=image_urls,
  1389. )
  1390. )
  1391. entry_ids.append(executor_id)
  1392. next_id += 1
  1393. validator_id = next_id
  1394. instances.append(
  1395. self._instance(
  1396. validator_id, 3,
  1397. "production.global_data.validate_task",
  1398. label=f"{task_id} 验收",
  1399. task_id=task_id,
  1400. ok=report.get("verdict") == "PASS" if report else None,
  1401. inputs=[
  1402. _input_block(
  1403. "delivery", "Executor Delivery",
  1404. delivery.get("summary"),
  1405. source=f"executor_results/{task_id}.v{version}.json",
  1406. )
  1407. ],
  1408. output=report or None,
  1409. flow=[
  1410. _node_card(
  1411. 0, node_type="llm", label="独立验收",
  1412. summary=report.get("summary") or "尚无验收报告",
  1413. ok=(
  1414. report.get("verdict") == "PASS"
  1415. if report else None
  1416. ),
  1417. detail=report or None,
  1418. )
  1419. ],
  1420. )
  1421. )
  1422. entry_ids.append(validator_id)
  1423. stage_paths = _files(
  1424. ref.path,
  1425. f"stage_validation_results/global_data.v{version}.json",
  1426. )
  1427. if stage_paths:
  1428. stage = _load_json(stage_paths[-1], {}) or {}
  1429. next_id += 1
  1430. stage_id = next_id
  1431. instances.append(
  1432. self._instance(
  1433. stage_id, 4, "production.global_data.validate_stage",
  1434. label="Global Data Stage 验收",
  1435. task_id=None,
  1436. ok=stage.get("verdict") == "PASS",
  1437. inputs=[
  1438. _input_block(
  1439. "plan", "Global Data Plan", f"v{version}",
  1440. source=path.name,
  1441. ),
  1442. _input_block(
  1443. "tasks", "PASS Deliveries",
  1444. [item.get("task_id") for item in plan.get("tasks") or []],
  1445. source="executor_results/",
  1446. ),
  1447. ],
  1448. output=stage,
  1449. flow=[
  1450. _node_card(
  1451. 0, node_type="llm", label="阶段总验收",
  1452. summary=stage.get("summary"),
  1453. ok=stage.get("verdict") == "PASS",
  1454. detail=stage,
  1455. )
  1456. ],
  1457. )
  1458. )
  1459. entry_ids.append(stage_id)
  1460. rounds.append(
  1461. {"index": index, "entry_ids": entry_ids, "instances": instances}
  1462. )
  1463. root = {
  1464. "id": 0,
  1465. "module_id": 0,
  1466. "module_key": "production.global_data.workflow",
  1467. "label": "Global Data 0.3",
  1468. "task_id": None,
  1469. "step_id": None,
  1470. "step_index": 0,
  1471. "branch": "",
  1472. "ok": self.brief(ref)["status"] == "success",
  1473. "input": {"blocks": []},
  1474. "output": None,
  1475. "timing": {"sec": self.brief(ref)["duration_sec"]},
  1476. "tokens": "",
  1477. "caller": None,
  1478. "used_tools": [],
  1479. "flow": [],
  1480. "module": {
  1481. "id": 0,
  1482. "module_key": "production.global_data.workflow",
  1483. "kind": "workflow",
  1484. "name": "Global Data Workflow",
  1485. "summary": "Preprocess → Plan → Execute → Validate → Replan/Finalize",
  1486. "spec": {},
  1487. "view": None,
  1488. "fingerprint": "0.3",
  1489. "refs": [],
  1490. },
  1491. }
  1492. return {
  1493. "run_id": ref.numeric_id,
  1494. "run_key": ref.key,
  1495. "root": root,
  1496. "modules": module_defs,
  1497. "round_count": len(plans),
  1498. "rounds": rounds,
  1499. }
  1500. @staticmethod
  1501. def _ordered_tasks(
  1502. ref: RunRef, tasks: list[dict[str, Any]]
  1503. ) -> list[dict[str, Any]]:
  1504. summary = _load_json(ref.path / "run_summary.json", {}) or {}
  1505. order: list[str] = []
  1506. for event in summary.get("event_log") or []:
  1507. match = re.search(r"(Task\d+)", event)
  1508. if match and match.group(1) not in order:
  1509. order.append(match.group(1))
  1510. rank = {task_id: index for index, task_id in enumerate(order)}
  1511. return sorted(
  1512. tasks,
  1513. key=lambda item: (
  1514. rank.get(str(item.get("task_id")), len(rank)),
  1515. str(item.get("task_id")),
  1516. ),
  1517. )
  1518. def _delivery_view(
  1519. self, ref: RunRef, delivery: dict[str, Any]
  1520. ) -> dict[str, Any] | None:
  1521. if not delivery:
  1522. return None
  1523. result = dict(delivery)
  1524. result["artifacts"] = [
  1525. {
  1526. **artifact,
  1527. "browser_uri": self.media_url(ref, artifact.get("uri")),
  1528. }
  1529. for artifact in delivery.get("artifacts") or []
  1530. ]
  1531. return result
  1532. def _segment_flat(
  1533. self, ref: RunRef, round_index: int | None
  1534. ) -> dict[str, Any]:
  1535. plan = self._latest_production_plan(ref.path) or {}
  1536. packages = [
  1537. (path, _load_json(path, {}) or {})
  1538. for path in _files(ref.path, "segment_packages/*.json")
  1539. ]
  1540. modules = {
  1541. 11: self._module_def(
  1542. 11, "production.segment.execute",
  1543. name="Segment Executor",
  1544. summary="按 SegmentPackage 生产镜头、声音、字幕和最终片段。",
  1545. inputs=(("package", "SegmentPackage"),),
  1546. ),
  1547. 12: self._module_def(
  1548. 12, "production.segment.validate",
  1549. name="Segment Validator",
  1550. summary="确定性证据前检后,独立判断六项 Segment 质量。",
  1551. inputs=(("delivery", "Segment Delivery"),),
  1552. ),
  1553. }
  1554. instances: list[dict[str, Any]] = []
  1555. entry_ids: list[int] = []
  1556. all_operations = [
  1557. _load_json(path, {}) or {}
  1558. for path in _files(ref.path, "tool_operations/*.json")
  1559. ]
  1560. for index, (package_path, package) in enumerate(packages):
  1561. segment_id = str(package.get("segment_id") or f"Segment{index + 1}")
  1562. executor_id = 2000 + index * 10 + 1
  1563. operations = [
  1564. operation
  1565. for operation in all_operations
  1566. if self._operation_segment(operation) == segment_id
  1567. ]
  1568. flow = [
  1569. _node_card(
  1570. flow_index,
  1571. node_type="tool",
  1572. label=operation.get("tool_name") or "tool",
  1573. summary=operation.get("status"),
  1574. ok=(
  1575. True
  1576. if operation.get("status") == "SUCCEEDED"
  1577. else False
  1578. if operation.get("status") == "FAILED"
  1579. else None
  1580. ),
  1581. detail=operation,
  1582. )
  1583. for flow_index, operation in enumerate(operations)
  1584. ]
  1585. deliveries = _files(
  1586. ref.path, f"segment_deliveries/{segment_id}.v*.json"
  1587. )
  1588. delivery = _load_json(deliveries[-1], {}) if deliveries else {}
  1589. instances.append(
  1590. self._instance(
  1591. executor_id, 11, "production.segment.execute",
  1592. label=segment_id,
  1593. task_id=segment_id,
  1594. ok=bool(delivery) if deliveries else None,
  1595. inputs=[
  1596. _input_block(
  1597. "package", "SegmentPackage", package,
  1598. source=str(package_path.relative_to(ref.path)),
  1599. )
  1600. ],
  1601. output=delivery or None,
  1602. flow=flow or [
  1603. _node_card(
  1604. 0, node_type="note", label="等待执行",
  1605. summary="尚未形成 Segment Delivery", ok=None,
  1606. )
  1607. ],
  1608. used_tools=[
  1609. item.get("tool_name")
  1610. for item in operations
  1611. if item.get("tool_name")
  1612. ],
  1613. )
  1614. )
  1615. entry_ids.append(executor_id)
  1616. reports = _files(
  1617. ref.path,
  1618. f"segment_validation_results/{segment_id}.v*.json",
  1619. )
  1620. if reports:
  1621. report = _load_json(reports[-1], {}) or {}
  1622. validator_id = executor_id + 1
  1623. instances.append(
  1624. self._instance(
  1625. validator_id, 12, "production.segment.validate",
  1626. label=f"{segment_id} 验收",
  1627. task_id=segment_id,
  1628. ok=report.get("verdict") == "PASS",
  1629. inputs=[
  1630. _input_block(
  1631. "delivery", "Segment Delivery",
  1632. delivery, source=deliveries[-1].name,
  1633. )
  1634. ],
  1635. output=report,
  1636. flow=[
  1637. _node_card(
  1638. 0, node_type="llm", label="六项独立验收",
  1639. summary=report.get("summary"),
  1640. ok=report.get("verdict") == "PASS",
  1641. detail=report,
  1642. )
  1643. ],
  1644. )
  1645. )
  1646. entry_ids.append(validator_id)
  1647. rounds = (
  1648. [{"index": 0, "entry_ids": entry_ids, "instances": instances}]
  1649. if round_index in (None, 0)
  1650. else []
  1651. )
  1652. protocol_version = str(plan.get("schema_version") or "unknown")
  1653. root = {
  1654. "id": 10,
  1655. "module_id": 10,
  1656. "module_key": "production.segment.workflow",
  1657. "label": (
  1658. f"Production {protocol_version} · "
  1659. f"{plan.get('plan_id', ref.key)}"
  1660. ),
  1661. "task_id": None,
  1662. "step_id": None,
  1663. "step_index": 0,
  1664. "branch": "",
  1665. "ok": self.brief(ref)["status"] == "success",
  1666. "input": {"blocks": []},
  1667. "output": None,
  1668. "timing": {"sec": self.brief(ref)["duration_sec"]},
  1669. "tokens": "",
  1670. "caller": None,
  1671. "used_tools": [],
  1672. "flow": [],
  1673. "module": {
  1674. "id": 10,
  1675. "module_key": "production.segment.workflow",
  1676. "kind": "workflow",
  1677. "name": "Segment Workflow",
  1678. "summary": "SegmentPackage → Executor → Validator",
  1679. "spec": {},
  1680. "view": None,
  1681. "fingerprint": protocol_version,
  1682. "refs": [],
  1683. },
  1684. }
  1685. return {
  1686. "run_id": ref.numeric_id,
  1687. "run_key": ref.key,
  1688. "root": root,
  1689. "modules": modules,
  1690. "round_count": 1,
  1691. "rounds": rounds,
  1692. }
  1693. def media_path(self, ref: RunRef, relative: str) -> Path:
  1694. candidate = (ref.path / relative).resolve()
  1695. if not candidate.is_relative_to(ref.path) or not candidate.is_file():
  1696. raise FileNotFoundError(relative)
  1697. return candidate
  1698. def media_url(self, ref: RunRef, uri: Any) -> str | None:
  1699. if not isinstance(uri, str) or not uri:
  1700. return None
  1701. try:
  1702. path = Path(uri).resolve()
  1703. relative = path.relative_to(ref.path)
  1704. except (ValueError, OSError):
  1705. return uri if uri.startswith(("http://", "https://")) else None
  1706. return f"/api/runs/{ref.numeric_id}/media/{relative.as_posix()}"
  1707. def tools(self) -> dict[str, Any]:
  1708. from production_build_agents.tools.registry import (
  1709. create_default_tool_registry,
  1710. )
  1711. # ToolRegistry 没有只读定义接口;从 Skill 白名单和工具 README 返回稳定说明,
  1712. # 不实例化远端客户端,也不触发环境校验。
  1713. names = {
  1714. "read_production_brief",
  1715. "search_tool",
  1716. "inspect_tool",
  1717. "run_tool",
  1718. "probe_media",
  1719. "view_images",
  1720. "extract_frames",
  1721. "publish_media_reference",
  1722. "transcribe_audio",
  1723. "create_ass_subtitles",
  1724. "inspect_ass_subtitles",
  1725. "render_ass_subtitles",
  1726. "audio_trim",
  1727. "mix_audio_tracks",
  1728. "video_trim",
  1729. "video_concat",
  1730. "video_mux_audio",
  1731. }
  1732. _ = create_default_tool_registry # 保留到实现正式定义投影时使用。
  1733. return {
  1734. name: {
  1735. "description": "Production Build 运行时工具",
  1736. "params": [],
  1737. }
  1738. for name in sorted(names)
  1739. }
  1740. def media_type(path: Path) -> str:
  1741. return mimetypes.guess_type(path.name)[0] or "application/octet-stream"