run_global_data.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  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. ) -> dict[str, Any]:
  88. """创建或恢复一个持久化 Global Data Run;终态重启不再执行。"""
  89. source = input_path.resolve()
  90. if not source.is_file():
  91. raise FileNotFoundError(f"输入文件不存在:{source}")
  92. input_sha256 = file_sha256(source)
  93. run_dir = run_directory(output_root.resolve(), thread_id).resolve()
  94. register_thread(
  95. registry_dir=(
  96. registry_dir
  97. or Path(__file__).resolve().parent / ".run_registry"
  98. ),
  99. thread_id=thread_id,
  100. protocol_version=PROTOCOL_VERSION,
  101. input_path=source,
  102. input_sha256=input_sha256,
  103. run_dir=run_dir,
  104. )
  105. config = {
  106. "configurable": {"thread_id": thread_id},
  107. "recursion_limit": 256,
  108. }
  109. started = perf_counter()
  110. graph_invoked = False
  111. with acquire_run_lock(run_dir):
  112. with create_sqlite_checkpointer(run_dir) as checkpointer:
  113. graph = create_global_data_graph(
  114. checkpointer=checkpointer,
  115. planner_model=planner_model,
  116. executor_model=executor_model,
  117. validator_model=validator_model,
  118. )
  119. saved = dict(graph.get_state(config).values)
  120. execution_mode = (
  121. (
  122. "completed_noop"
  123. if saved.get("status") == "COMPLETED"
  124. else "failed_noop"
  125. )
  126. if saved and saved.get("status") in TERMINAL_STATUSES
  127. else ("resume" if saved else "fresh")
  128. )
  129. with GlobalDataObservation(
  130. project_root=Path(__file__).resolve().parent,
  131. thread_id=thread_id,
  132. input_sha256=input_sha256,
  133. protocol_version=PROTOCOL_VERSION,
  134. execution_mode=execution_mode,
  135. graph=graph,
  136. ) as observation:
  137. if saved:
  138. validate_resume_identity(
  139. saved,
  140. thread_id=thread_id,
  141. protocol_version=PROTOCOL_VERSION,
  142. input_path=source,
  143. input_sha256=input_sha256,
  144. run_dir=run_dir,
  145. )
  146. if saved.get("status") in TERMINAL_STATUSES:
  147. if saved.get("status") == "COMPLETED":
  148. validate_completed_run(saved)
  149. observation.finish(saved, graph_invoked=False)
  150. return saved
  151. graph_invoked = True
  152. result = dict(graph.invoke(None, config))
  153. else:
  154. graph_invoked = True
  155. result = dict(
  156. graph.invoke(
  157. _initial_state(
  158. thread_id=thread_id,
  159. input_path=source,
  160. input_sha256=input_sha256,
  161. run_dir=run_dir,
  162. ),
  163. config,
  164. )
  165. )
  166. observation.finish(result, graph_invoked=graph_invoked)
  167. if graph_invoked:
  168. try:
  169. record_run_invocation(
  170. run_dir,
  171. run_id=thread_id,
  172. duration_ms=max(
  173. 0,
  174. round((perf_counter() - started) * 1000),
  175. ),
  176. status=str(result.get("status") or "UNKNOWN"),
  177. )
  178. except (KeyError, OSError, TypeError, ValueError):
  179. pass
  180. return result
  181. def main() -> None:
  182. args = _build_parser().parse_args()
  183. thread_id = args.thread_id or f"demo-{uuid4()}"
  184. result = run_global_data(
  185. args.input,
  186. output_root=args.output_dir,
  187. thread_id=thread_id,
  188. )
  189. print(json.dumps(result, ensure_ascii=False, indent=2))
  190. if result.get("status") != "COMPLETED":
  191. raise SystemExit(1)
  192. if __name__ == "__main__":
  193. main()