workflow.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. """需求澄清多智能体工作流。"""
  2. from __future__ import annotations
  3. import json
  4. from dataclasses import dataclass
  5. from html import escape
  6. from pathlib import Path
  7. from hello_agents.tools import ToolRegistry
  8. from .agents import AgentLike, AgentTeam
  9. MAX_REQUIREMENT_LENGTH = 50_000
  10. class WorkflowExecutionError(RuntimeError):
  11. """工作流输入或某个智能体阶段执行失败。"""
  12. @dataclass(frozen=True)
  13. class WorkflowResult:
  14. """保留全部中间产物,便于追踪和测试。"""
  15. requirement: str
  16. audit: dict[str, object]
  17. analysis: str
  18. architecture: str
  19. risk_review: str
  20. report: str
  21. quality: dict[str, object]
  22. class RequirementClarifierWorkflow:
  23. """协调四个 HelloAgents 智能体完成顺序协作。"""
  24. def __init__(self, team: AgentTeam, tool_registry: ToolRegistry) -> None:
  25. self.team = team
  26. self.tool_registry = tool_registry
  27. def run(self, requirement: str) -> WorkflowResult:
  28. """执行确定性初检、三阶段分析、报告整合和结构质检。"""
  29. requirement = self._validate_requirement(requirement)
  30. self._clear_agent_histories()
  31. try:
  32. return self._run_validated(requirement)
  33. finally:
  34. self._clear_agent_histories()
  35. def _run_validated(self, requirement: str) -> WorkflowResult:
  36. """处理已校验的单次需求,调用方负责清理 Agent 历史。"""
  37. audit = self._run_tool(
  38. "requirement_audit", {"requirement_text": requirement}, "需求初检"
  39. )
  40. analysis = self._run_agent(
  41. "需求分析",
  42. self.team.analyst,
  43. "请分析以下原始需求,并参考确定性初检结果。\n\n"
  44. f"{self._tagged('requirement', requirement)}\n\n"
  45. f"{self._tagged('audit', json.dumps(audit, ensure_ascii=False, indent=2))}",
  46. )
  47. architecture = self._run_agent(
  48. "方案设计",
  49. self.team.architect,
  50. "请根据原始需求和需求分析提出可交付的 MVP 技术方案。\n\n"
  51. f"{self._tagged('requirement', requirement)}\n\n"
  52. f"{self._tagged('analysis', analysis)}",
  53. )
  54. risk_review = self._run_agent(
  55. "风险审查",
  56. self.team.reviewer,
  57. "请独立审查以下需求分析和技术方案。\n\n"
  58. f"{self._tagged('requirement', requirement)}\n\n"
  59. f"{self._tagged('analysis', analysis)}\n\n"
  60. f"{self._tagged('architecture', architecture)}",
  61. )
  62. report = self._run_agent(
  63. "报告整合",
  64. self.team.synthesizer,
  65. "请把以下材料整合为最终需求澄清与技术方案报告。\n\n"
  66. f"{self._tagged('requirement', requirement)}\n\n"
  67. f"{self._tagged('audit', json.dumps(audit, ensure_ascii=False, indent=2))}\n\n"
  68. f"{self._tagged('analysis', analysis)}\n\n"
  69. f"{self._tagged('architecture', architecture)}\n\n"
  70. f"{self._tagged('risk_review', risk_review)}",
  71. )
  72. quality = self._run_tool(
  73. "report_quality_check", {"report_text": report}, "报告质检"
  74. )
  75. return WorkflowResult(
  76. requirement=requirement,
  77. audit=audit,
  78. analysis=analysis,
  79. architecture=architecture,
  80. risk_review=risk_review,
  81. report=report,
  82. quality=quality,
  83. )
  84. @staticmethod
  85. def save_report(result: WorkflowResult, output_path: str | Path) -> Path:
  86. """以 UTF-8 保存最终 Markdown 报告。"""
  87. path = Path(output_path)
  88. path.parent.mkdir(parents=True, exist_ok=True)
  89. path.write_text(result.report.rstrip() + "\n", encoding="utf-8")
  90. return path
  91. @staticmethod
  92. def _validate_requirement(requirement: str) -> str:
  93. if not isinstance(requirement, str):
  94. raise WorkflowExecutionError("需求必须是字符串")
  95. requirement = requirement.strip()
  96. if not requirement:
  97. raise WorkflowExecutionError("需求不能为空")
  98. if len(requirement) > MAX_REQUIREMENT_LENGTH:
  99. raise WorkflowExecutionError(
  100. f"需求文本不能超过 {MAX_REQUIREMENT_LENGTH} 个字符"
  101. )
  102. return requirement
  103. @staticmethod
  104. def _run_agent(stage: str, agent: AgentLike, prompt: str) -> str:
  105. try:
  106. response = agent.run(prompt)
  107. except Exception as exc:
  108. raise WorkflowExecutionError(f"{stage}阶段执行失败:{exc}") from exc
  109. if not isinstance(response, str) or not response.strip():
  110. raise WorkflowExecutionError(f"{stage}阶段返回了空结果")
  111. return response.strip()
  112. def _run_tool(
  113. self, name: str, parameters: dict[str, object], stage: str
  114. ) -> dict[str, object]:
  115. """通过官方 ToolRegistry 获取工具并解析其字符串协议。"""
  116. tool = self.tool_registry.get_tool(name)
  117. if tool is None:
  118. raise WorkflowExecutionError(f"{stage}失败:工具 {name} 未注册")
  119. try:
  120. raw_result = tool.run(parameters)
  121. except Exception as exc:
  122. raise WorkflowExecutionError(f"{stage}失败:工具执行异常:{exc}") from exc
  123. try:
  124. payload = json.loads(raw_result)
  125. except (TypeError, ValueError) as exc:
  126. raise WorkflowExecutionError(f"{stage}失败:工具返回的不是有效 JSON") from exc
  127. if not isinstance(payload, dict):
  128. raise WorkflowExecutionError(f"{stage}失败:工具结果必须是 JSON 对象")
  129. if not payload.get("ok"):
  130. raise WorkflowExecutionError(
  131. f"{stage}失败:{payload.get('message', '未知工具错误')}"
  132. )
  133. return payload
  134. def _clear_agent_histories(self) -> None:
  135. """避免多次运行时把上一条需求带入下一条需求。"""
  136. for agent in (
  137. self.team.analyst,
  138. self.team.architect,
  139. self.team.reviewer,
  140. self.team.synthesizer,
  141. ):
  142. clear_history = getattr(agent, "clear_history", None)
  143. if callable(clear_history):
  144. clear_history()
  145. @staticmethod
  146. def _tagged(tag: str, content: str) -> str:
  147. """转义不可信内容,防止内容伪造工作流边界标签。"""
  148. return f"<{tag}>\n{escape(content, quote=False)}\n</{tag}>"