run_global_data.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. """从 production_final.json 运行完整 Global Data 闭环。"""
  2. from __future__ import annotations
  3. import argparse
  4. import json
  5. from pathlib import Path
  6. from time import perf_counter
  7. from typing import Any
  8. from uuid import uuid4
  9. from langchain_core.language_models import BaseChatModel
  10. from production_build_agents.global_data.graph import create_global_data_graph
  11. from production_build_agents.global_data_stage_finalization import (
  12. validate_completed_run,
  13. )
  14. from production_build_agents.observability import GlobalDataObservation
  15. from production_build_agents.run.langgraph_checkpointer import (
  16. create_sqlite_checkpointer,
  17. )
  18. from production_build_agents.run.artifacts import file_sha256
  19. from production_build_agents.run.lock import acquire_run_lock
  20. from production_build_agents.run.layout import run_directory
  21. from production_build_agents.run.metrics import record_run_invocation
  22. from production_build_agents.run.registry import (
  23. register_thread,
  24. validate_resume_identity,
  25. )
  26. TERMINAL_STATUSES = {
  27. "FAILED",
  28. "COMPLETED",
  29. }
  30. PROTOCOL_VERSION = "0.3"
  31. def _build_parser() -> argparse.ArgumentParser:
  32. parser = argparse.ArgumentParser(
  33. description="运行完整 Global Data DAG,直到阶段完成或明确失败"
  34. )
  35. parser.add_argument("input", type=Path, help="原始 production_final.json")
  36. parser.add_argument(
  37. "--output-dir",
  38. type=Path,
  39. default=Path("demo_output"),
  40. help="所有 Run 的根目录;每个 thread_id 使用独立子目录",
  41. )
  42. parser.add_argument("--thread-id", default=None)
  43. return parser
  44. def _initial_state(
  45. *,
  46. thread_id: str,
  47. input_path: Path,
  48. input_sha256: str,
  49. run_dir: Path,
  50. ) -> dict[str, Any]:
  51. return {
  52. "run_id": thread_id,
  53. "protocol_version": PROTOCOL_VERSION,
  54. "input_path": str(input_path),
  55. "input_sha256": input_sha256,
  56. "output_dir": str(run_dir),
  57. "status": "PENDING",
  58. "phase": "PREPROCESS",
  59. "replan_scope": None,
  60. "failure_code": None,
  61. "error": None,
  62. "global_data_plan_path": None,
  63. "plan_history": {},
  64. "task_records": {},
  65. "current_task_path": None,
  66. "current_executor_delivery_path": None,
  67. "current_validation_report_path": None,
  68. "current_stage_candidate_path": None,
  69. "current_stage_validation_report_path": None,
  70. "global_data_delivery_path": None,
  71. "planner_calls": 0,
  72. "executor_calls": 0,
  73. "validator_calls": 0,
  74. "stage_validator_calls": 0,
  75. "replan_count": 0,
  76. "event_log": [],
  77. }
  78. def run_global_data(
  79. input_path: Path,
  80. *,
  81. output_root: Path,
  82. thread_id: str,
  83. planner_model: BaseChatModel | None = None,
  84. executor_model: BaseChatModel | None = None,
  85. validator_model: BaseChatModel | None = None,
  86. registry_dir: Path | None = None,
  87. observation_parent_run_id: str | None = None,
  88. observation_parent_inst_id: str | None = None,
  89. pipeline_round_id: str | None = None,
  90. ) -> dict[str, Any]:
  91. """创建或恢复一个持久化 Global Data Run;终态重启不再执行。"""
  92. source = input_path.resolve()
  93. if not source.is_file():
  94. raise FileNotFoundError(f"输入文件不存在:{source}")
  95. input_sha256 = file_sha256(source)
  96. run_dir = run_directory(output_root.resolve(), thread_id).resolve()
  97. register_thread(
  98. registry_dir=(
  99. registry_dir
  100. or Path(__file__).resolve().parent / ".run_registry"
  101. ),
  102. thread_id=thread_id,
  103. protocol_version=PROTOCOL_VERSION,
  104. input_path=source,
  105. input_sha256=input_sha256,
  106. run_dir=run_dir,
  107. )
  108. config = {
  109. "configurable": {"thread_id": thread_id},
  110. "recursion_limit": 256,
  111. }
  112. started = perf_counter()
  113. graph_invoked = False
  114. with acquire_run_lock(run_dir):
  115. with create_sqlite_checkpointer(run_dir) as checkpointer:
  116. graph = create_global_data_graph(
  117. checkpointer=checkpointer,
  118. planner_model=planner_model,
  119. executor_model=executor_model,
  120. validator_model=validator_model,
  121. )
  122. saved = dict(graph.get_state(config).values)
  123. execution_mode = (
  124. (
  125. "completed_noop"
  126. if saved.get("status") == "COMPLETED"
  127. else "failed_noop"
  128. )
  129. if saved and saved.get("status") in TERMINAL_STATUSES
  130. else ("resume" if saved else "fresh")
  131. )
  132. with GlobalDataObservation(
  133. project_root=Path(__file__).resolve().parent,
  134. thread_id=thread_id,
  135. input_sha256=input_sha256,
  136. protocol_version=PROTOCOL_VERSION,
  137. execution_mode=execution_mode,
  138. graph=graph,
  139. input_path=source,
  140. parent_run_id=observation_parent_run_id,
  141. parent_inst_id=observation_parent_inst_id,
  142. pipeline_round_id=pipeline_round_id,
  143. ) as observation:
  144. if saved:
  145. validate_resume_identity(
  146. saved,
  147. thread_id=thread_id,
  148. protocol_version=PROTOCOL_VERSION,
  149. input_path=source,
  150. input_sha256=input_sha256,
  151. run_dir=run_dir,
  152. )
  153. if saved.get("status") in TERMINAL_STATUSES:
  154. if saved.get("status") == "COMPLETED":
  155. validate_completed_run(saved)
  156. observation.finish(saved, graph_invoked=False)
  157. return saved
  158. graph_invoked = True
  159. result = dict(graph.invoke(None, config))
  160. else:
  161. graph_invoked = True
  162. result = dict(
  163. graph.invoke(
  164. _initial_state(
  165. thread_id=thread_id,
  166. input_path=source,
  167. input_sha256=input_sha256,
  168. run_dir=run_dir,
  169. ),
  170. config,
  171. )
  172. )
  173. observation.finish(result, graph_invoked=graph_invoked)
  174. if graph_invoked:
  175. try:
  176. record_run_invocation(
  177. run_dir,
  178. run_id=thread_id,
  179. duration_ms=max(
  180. 0,
  181. round((perf_counter() - started) * 1000),
  182. ),
  183. status=str(result.get("status") or "UNKNOWN"),
  184. )
  185. except (KeyError, OSError, TypeError, ValueError):
  186. pass
  187. return result
  188. def main() -> None:
  189. args = _build_parser().parse_args()
  190. thread_id = args.thread_id or f"demo-{uuid4()}"
  191. result = run_global_data(
  192. args.input,
  193. output_root=args.output_dir,
  194. thread_id=thread_id,
  195. )
  196. print(json.dumps(result, ensure_ascii=False, indent=2))
  197. if result.get("status") != "COMPLETED":
  198. raise SystemExit(1)
  199. if __name__ == "__main__":
  200. main()