| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- """需求澄清多智能体工作流。"""
- from __future__ import annotations
- import json
- from dataclasses import dataclass
- from html import escape
- from pathlib import Path
- from hello_agents.tools import ToolRegistry
- from .agents import AgentLike, AgentTeam
- MAX_REQUIREMENT_LENGTH = 50_000
- class WorkflowExecutionError(RuntimeError):
- """工作流输入或某个智能体阶段执行失败。"""
- @dataclass(frozen=True)
- class WorkflowResult:
- """保留全部中间产物,便于追踪和测试。"""
- requirement: str
- audit: dict[str, object]
- analysis: str
- architecture: str
- risk_review: str
- report: str
- quality: dict[str, object]
- class RequirementClarifierWorkflow:
- """协调四个 HelloAgents 智能体完成顺序协作。"""
- def __init__(self, team: AgentTeam, tool_registry: ToolRegistry) -> None:
- self.team = team
- self.tool_registry = tool_registry
- def run(self, requirement: str) -> WorkflowResult:
- """执行确定性初检、三阶段分析、报告整合和结构质检。"""
- requirement = self._validate_requirement(requirement)
- self._clear_agent_histories()
- try:
- return self._run_validated(requirement)
- finally:
- self._clear_agent_histories()
- def _run_validated(self, requirement: str) -> WorkflowResult:
- """处理已校验的单次需求,调用方负责清理 Agent 历史。"""
- audit = self._run_tool(
- "requirement_audit", {"requirement_text": requirement}, "需求初检"
- )
- analysis = self._run_agent(
- "需求分析",
- self.team.analyst,
- "请分析以下原始需求,并参考确定性初检结果。\n\n"
- f"{self._tagged('requirement', requirement)}\n\n"
- f"{self._tagged('audit', json.dumps(audit, ensure_ascii=False, indent=2))}",
- )
- architecture = self._run_agent(
- "方案设计",
- self.team.architect,
- "请根据原始需求和需求分析提出可交付的 MVP 技术方案。\n\n"
- f"{self._tagged('requirement', requirement)}\n\n"
- f"{self._tagged('analysis', analysis)}",
- )
- risk_review = self._run_agent(
- "风险审查",
- self.team.reviewer,
- "请独立审查以下需求分析和技术方案。\n\n"
- f"{self._tagged('requirement', requirement)}\n\n"
- f"{self._tagged('analysis', analysis)}\n\n"
- f"{self._tagged('architecture', architecture)}",
- )
- report = self._run_agent(
- "报告整合",
- self.team.synthesizer,
- "请把以下材料整合为最终需求澄清与技术方案报告。\n\n"
- f"{self._tagged('requirement', requirement)}\n\n"
- f"{self._tagged('audit', json.dumps(audit, ensure_ascii=False, indent=2))}\n\n"
- f"{self._tagged('analysis', analysis)}\n\n"
- f"{self._tagged('architecture', architecture)}\n\n"
- f"{self._tagged('risk_review', risk_review)}",
- )
- quality = self._run_tool(
- "report_quality_check", {"report_text": report}, "报告质检"
- )
- return WorkflowResult(
- requirement=requirement,
- audit=audit,
- analysis=analysis,
- architecture=architecture,
- risk_review=risk_review,
- report=report,
- quality=quality,
- )
- @staticmethod
- def save_report(result: WorkflowResult, output_path: str | Path) -> Path:
- """以 UTF-8 保存最终 Markdown 报告。"""
- path = Path(output_path)
- path.parent.mkdir(parents=True, exist_ok=True)
- path.write_text(result.report.rstrip() + "\n", encoding="utf-8")
- return path
- @staticmethod
- def _validate_requirement(requirement: str) -> str:
- if not isinstance(requirement, str):
- raise WorkflowExecutionError("需求必须是字符串")
- requirement = requirement.strip()
- if not requirement:
- raise WorkflowExecutionError("需求不能为空")
- if len(requirement) > MAX_REQUIREMENT_LENGTH:
- raise WorkflowExecutionError(
- f"需求文本不能超过 {MAX_REQUIREMENT_LENGTH} 个字符"
- )
- return requirement
- @staticmethod
- def _run_agent(stage: str, agent: AgentLike, prompt: str) -> str:
- try:
- response = agent.run(prompt)
- except Exception as exc:
- raise WorkflowExecutionError(f"{stage}阶段执行失败:{exc}") from exc
- if not isinstance(response, str) or not response.strip():
- raise WorkflowExecutionError(f"{stage}阶段返回了空结果")
- return response.strip()
- def _run_tool(
- self, name: str, parameters: dict[str, object], stage: str
- ) -> dict[str, object]:
- """通过官方 ToolRegistry 获取工具并解析其字符串协议。"""
- tool = self.tool_registry.get_tool(name)
- if tool is None:
- raise WorkflowExecutionError(f"{stage}失败:工具 {name} 未注册")
- try:
- raw_result = tool.run(parameters)
- except Exception as exc:
- raise WorkflowExecutionError(f"{stage}失败:工具执行异常:{exc}") from exc
- try:
- payload = json.loads(raw_result)
- except (TypeError, ValueError) as exc:
- raise WorkflowExecutionError(f"{stage}失败:工具返回的不是有效 JSON") from exc
- if not isinstance(payload, dict):
- raise WorkflowExecutionError(f"{stage}失败:工具结果必须是 JSON 对象")
- if not payload.get("ok"):
- raise WorkflowExecutionError(
- f"{stage}失败:{payload.get('message', '未知工具错误')}"
- )
- return payload
- def _clear_agent_histories(self) -> None:
- """避免多次运行时把上一条需求带入下一条需求。"""
- for agent in (
- self.team.analyst,
- self.team.architect,
- self.team.reviewer,
- self.team.synthesizer,
- ):
- clear_history = getattr(agent, "clear_history", None)
- if callable(clear_history):
- clear_history()
- @staticmethod
- def _tagged(tag: str, content: str) -> str:
- """转义不可信内容,防止内容伪造工作流边界标签。"""
- return f"<{tag}>\n{escape(content, quote=False)}\n</{tag}>"
|