- Add 2 new team templates: 教育培训团队 (4 roles) and 天工平台工程团队 (5 roles) - Fix orchestrator to support multi-template workflow types (remove hardcoded PM planner) - Add _resolve_planner_role() to auto-detect planner based on team.config.workflow - Add frontend buttons and API clients for new templates - Merge 14 preset roles from 3 template families in get_preset_roles() - Add creation guide and platform engineering usage docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
639 lines
26 KiB
Python
639 lines
26 KiB
Python
"""
|
||
团队编排引擎 — PM 规划 → 顺序阶段执行 → QA 审查 → 交付物
|
||
|
||
用法:
|
||
orchestrator = TeamOrchestrator(db, team_id, user_id)
|
||
result = await orchestrator.execute("做一个个人博客")
|
||
async for event in orchestrator.execute_stream("做一个个人博客"):
|
||
...
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
import logging
|
||
from pathlib import Path
|
||
from typing import Any, AsyncGenerator, Dict, List, Optional
|
||
from datetime import datetime
|
||
|
||
from sqlalchemy.orm import Session
|
||
from app.models.team import Team, TeamMember
|
||
from app.models.agent import Agent
|
||
from app.agent_runtime.core import AgentRuntime
|
||
from app.agent_runtime.schemas import (
|
||
AgentConfig,
|
||
AgentLLMConfig,
|
||
AgentToolConfig,
|
||
AgentMemoryConfig,
|
||
AgentResult,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _build_agent_config(agent: Agent, auto_approve_files: bool = True) -> AgentConfig:
|
||
"""从 Agent 的 workflow_config 构建 AgentConfig,供 AgentRuntime 使用。"""
|
||
wf = agent.workflow_config or {}
|
||
nodes = wf.get("nodes", [])
|
||
llm_node = None
|
||
for n in nodes:
|
||
if n.get("type") == "llm":
|
||
llm_node = n
|
||
break
|
||
|
||
llm_data = llm_node.get("data", {}) if llm_node else {}
|
||
|
||
tools = llm_data.get("selected_tools") or llm_data.get("tools") or []
|
||
system_prompt = llm_data.get("prompt") or ""
|
||
model = llm_data.get("model", "deepseek-v4-pro")
|
||
provider = llm_data.get("provider", "deepseek")
|
||
temperature = float(llm_data.get("temperature", 0.4))
|
||
max_iterations = int(llm_data.get("max_iterations", 10))
|
||
|
||
perm = "acceptEdits" if auto_approve_files else "default"
|
||
logger.info("_build_agent_config: agent=%s tools=%d permission_level=%s", agent.name, len(tools), perm)
|
||
return AgentConfig(
|
||
name=agent.name,
|
||
system_prompt=system_prompt,
|
||
user_id=agent.user_id,
|
||
memory_scope_id=f"team_{agent.id}",
|
||
llm=AgentLLMConfig(
|
||
provider=provider,
|
||
model=model,
|
||
temperature=temperature,
|
||
max_iterations=max_iterations,
|
||
),
|
||
tools=AgentToolConfig(include_tools=tools, permission_level=perm),
|
||
memory=AgentMemoryConfig(
|
||
enabled=True,
|
||
max_history_messages=30,
|
||
persist_to_db=True,
|
||
learning_enabled=True,
|
||
),
|
||
)
|
||
|
||
|
||
def _parse_plan_json(raw_output: str) -> Dict[str, Any]:
|
||
"""从 LLM 原始输出中提取 JSON 计划。"""
|
||
text = raw_output.strip()
|
||
# 尝试直接解析
|
||
try:
|
||
return json.loads(text)
|
||
except json.JSONDecodeError:
|
||
pass
|
||
# 从 ```json ... ``` 中提取
|
||
import re
|
||
m = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', text)
|
||
if m:
|
||
try:
|
||
return json.loads(m.group(1))
|
||
except json.JSONDecodeError:
|
||
pass
|
||
# 从 { ... } 中提取
|
||
m = re.search(r'\{[\s\S]*"phases"[\s\S]*\}', text)
|
||
if m:
|
||
try:
|
||
return json.loads(m.group())
|
||
except json.JSONDecodeError:
|
||
pass
|
||
logger.warning("无法从输出中解析 JSON 计划,原始输出: %.500s", text)
|
||
return {}
|
||
|
||
|
||
def _resolve_workspace_root() -> Path:
|
||
"""获取文件工具的工作区根目录(与 builtin_tools.py 逻辑一致)。"""
|
||
from app.core.config import settings
|
||
raw = (getattr(settings, "LOCAL_FILE_TOOLS_ROOT", None) or "").strip()
|
||
if raw:
|
||
return Path(raw).expanduser().resolve()
|
||
# fallback: 4 级父目录 (builtin_tools.py → app/services → app → backend → repo root)
|
||
return Path(__file__).resolve().parent.parent.parent.parent
|
||
|
||
|
||
def _safe_project_name(description: str) -> str:
|
||
"""从项目描述中提取安全的目录名(英文/拼音/数字/连字符)。"""
|
||
# 取前 40 个字符,只保留英文、数字、中文转拼音首字母的近似
|
||
cleaned = description.strip().replace(" ", "-").replace("/", "-").replace("\\", "-")
|
||
# 去掉非 ASCII 和非基本标点
|
||
safe = re.sub(r'[^\w\-]', '', cleaned, flags=re.ASCII)
|
||
if not safe:
|
||
# 如果全是中文,用时间戳
|
||
from datetime import datetime
|
||
safe = f"project-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
||
return safe[:40].strip("-")
|
||
|
||
|
||
def _scan_directory_files(directory: Path) -> List[str]:
|
||
"""扫描目录下所有文件的绝对路径列表(递归)。"""
|
||
files: List[str] = []
|
||
if not directory.exists():
|
||
return files
|
||
for p in directory.rglob("*"):
|
||
if p.is_file():
|
||
files.append(str(p))
|
||
return sorted(files)
|
||
|
||
|
||
def _extract_written_files(result: AgentResult) -> List[str]:
|
||
"""从 AgentResult.steps 中提取 file_write 成功产出的绝对路径列表。"""
|
||
files: List[str] = []
|
||
seen = set()
|
||
for step in result.steps:
|
||
if step.tool_name == "file_write":
|
||
# 优先从 tool_result JSON 中取绝对路径(仅当写入成功时)
|
||
if step.tool_result:
|
||
try:
|
||
data = json.loads(step.tool_result)
|
||
# 跳过失败的调用(权限不足 / 需确认)
|
||
if data.get("error") or data.get("requires_confirmation"):
|
||
logger.warning("file_write 未成功(%s),跳过文件追踪", data.get("error", "requires_confirmation"))
|
||
continue
|
||
fp = data.get("file_path")
|
||
if fp and fp not in seen:
|
||
files.append(fp)
|
||
seen.add(fp)
|
||
continue
|
||
except (json.JSONDecodeError, TypeError):
|
||
pass
|
||
# 回退:从 tool_input 取相对路径,再拼接工作区根(仅当 tool_result 为空时)
|
||
if step.tool_input:
|
||
fp = step.tool_input.get("file_path")
|
||
if fp:
|
||
abs_path = str(_resolve_workspace_root() / fp)
|
||
if abs_path not in seen:
|
||
files.append(abs_path)
|
||
seen.add(abs_path)
|
||
return files
|
||
|
||
|
||
class TeamOrchestrator:
|
||
"""虚拟团队编排器 — 按角色分工顺序执行软件项目。"""
|
||
|
||
def __init__(
|
||
self,
|
||
db: Session,
|
||
team_id: str,
|
||
user_id: str,
|
||
auto_approve_files: bool = True,
|
||
):
|
||
self.db = db
|
||
self.team_id = team_id
|
||
self.user_id = user_id
|
||
self.auto_approve_files = auto_approve_files
|
||
|
||
def _load_team(self) -> Team:
|
||
team = self.db.query(Team).filter(Team.id == self.team_id).first()
|
||
if not team:
|
||
raise ValueError(f"团队不存在: {self.team_id}")
|
||
return team
|
||
|
||
def _load_members(self) -> List[TeamMember]:
|
||
return (
|
||
self.db.query(TeamMember)
|
||
.filter(TeamMember.team_id == self.team_id)
|
||
.order_by(TeamMember.position)
|
||
.all()
|
||
)
|
||
|
||
def _get_agent_by_role(self, members: List[TeamMember], role: str) -> Optional[Agent]:
|
||
"""按角色查找 Agent。"""
|
||
for m in members:
|
||
if m.role == role:
|
||
return m.agent
|
||
return None
|
||
|
||
def _resolve_planner_role(self, members: List[TeamMember], workflow: str) -> str:
|
||
"""根据团队 workflow 类型解析规划者角色。
|
||
|
||
- software_company → pm
|
||
- education_training → curriculum_designer
|
||
- platform_engineering → product_lead (产品负责人做规划)
|
||
- 回退: 第一个 is_lead=True 的成员
|
||
- 最终回退: 第一个成员
|
||
"""
|
||
workflow_planner_map = {
|
||
"software_company": "pm",
|
||
"education_training": "curriculum_designer",
|
||
"platform_engineering": "product_lead",
|
||
}
|
||
role = workflow_planner_map.get(workflow)
|
||
if role and self._get_agent_by_role(members, role):
|
||
return role
|
||
|
||
# 回退:找第一个 Leader
|
||
for m in sorted(members, key=lambda x: x.position):
|
||
if m.is_lead:
|
||
return m.role
|
||
|
||
# 最终回退:第一个成员
|
||
if members:
|
||
return members[0].role
|
||
return "pm"
|
||
|
||
async def execute(self, project_description: str, auto_approve_files: bool = True) -> Dict[str, Any]:
|
||
"""执行团队项目(非流式)。
|
||
|
||
Returns:
|
||
{
|
||
"project_name": str,
|
||
"plan": {...},
|
||
"phases": [{phase, role, agent_name, output, steps, success, error}],
|
||
"qa_review": {...} | None,
|
||
"final_deliverable": str,
|
||
"success": bool,
|
||
}
|
||
"""
|
||
self.auto_approve_files = auto_approve_files
|
||
team = self._load_team()
|
||
members = self._load_members()
|
||
if not members:
|
||
raise ValueError("团队没有成员,请先为角色分配 Agent")
|
||
|
||
# ─── 创建项目目录 ───
|
||
workspace_root = _resolve_workspace_root()
|
||
safe_name = _safe_project_name(project_description)
|
||
project_path = workspace_root / "team_projects" / self.team_id / safe_name
|
||
project_path.mkdir(parents=True, exist_ok=True)
|
||
logger.info("团队编排 [%s]: 项目目录 %s", self.team_id, project_path)
|
||
|
||
prev_files = set(_scan_directory_files(project_path))
|
||
all_files: List[str] = []
|
||
|
||
results: Dict[str, Any] = {
|
||
"team_id": self.team_id,
|
||
"team_name": team.name,
|
||
"project_description": project_description,
|
||
"project_path": str(project_path),
|
||
"files": [],
|
||
"started_at": datetime.now().isoformat(),
|
||
"plan": None,
|
||
"phases": [],
|
||
"qa_review": None,
|
||
"final_deliverable": "",
|
||
"success": True,
|
||
"error": None,
|
||
}
|
||
|
||
# ─── Phase 0: 规划阶段(由 Leader Agent 担任) ───
|
||
# 选择规划者:优先用 team.config 中指定的 workflow,否则回退兼容
|
||
workflow = (team.config or {}).get("workflow", "software_company")
|
||
planner_role = self._resolve_planner_role(members, workflow)
|
||
planner_agent = self._get_agent_by_role(members, planner_role)
|
||
if not planner_agent:
|
||
raise ValueError(
|
||
f"团队缺少 Leader 角色(需要 role='{planner_role}'),无法分解项目。"
|
||
f"可用角色: {[m.role for m in members]}"
|
||
)
|
||
|
||
# 动态构建可用的角色列表(来自团队成员实际角色)
|
||
available_roles = [m.role for m in members]
|
||
roles_hint = "/".join(available_roles)
|
||
|
||
logger.info("团队编排 [%s]: 规划开始 (workflow=%s, planner=%s, role=%s)",
|
||
self.team_id, workflow, planner_agent.name, planner_role)
|
||
|
||
planner_config = _build_agent_config(planner_agent, self.auto_approve_files)
|
||
planner_runtime = AgentRuntime(planner_config)
|
||
planner_input = (
|
||
f"请分析以下需求,按 JSON 格式输出执行计划:\n\n"
|
||
f"需求描述:{project_description}\n"
|
||
f"输出文件目录:{project_path}\n\n"
|
||
f"你的团队有以下角色(每个 phase 必须使用这些角色之一):{roles_hint}\n"
|
||
f"请输出包含 phases 数组的完整 JSON 计划,每个 phase 需指定 role 为上述角色之一。"
|
||
)
|
||
planner_result = await planner_runtime.run(planner_input)
|
||
plan = _parse_plan_json(planner_result.content) if planner_result.success else {}
|
||
results["plan"] = plan
|
||
|
||
if not plan or not plan.get("phases"):
|
||
results["success"] = False
|
||
results["error"] = f"{planner_role} 未能生成有效计划"
|
||
results["final_deliverable"] = planner_result.content
|
||
return results
|
||
|
||
logger.info("团队编排 [%s]: PM 生成 %d 个阶段", self.team_id, len(plan["phases"]))
|
||
|
||
# ─── Phase 1..N: 顺序执行 ───
|
||
all_outputs: List[str] = []
|
||
context_doc = f"# 项目: {plan.get('project_name', '未命名')}\n\n## 需求分析\n{plan.get('analysis', '')}\n\n"
|
||
|
||
for i, phase in enumerate(plan["phases"]):
|
||
phase_num = phase.get("phase", i + 1)
|
||
role = phase.get("role", "developer")
|
||
phase_name = phase.get("name", f"阶段 {phase_num}")
|
||
phase_desc = phase.get("description", "")
|
||
expected = phase.get("expected_output", "")
|
||
|
||
agent = self._get_agent_by_role(members, role)
|
||
if not agent:
|
||
logger.warning("团队编排 [%s]: 阶段 %s 缺少角色 '%s',跳过", self.team_id, phase_num, role)
|
||
all_outputs.append(f"[跳过] 阶段 {phase_num} ({phase_name}): 无 {role} 角色")
|
||
results["phases"].append({
|
||
"phase": phase_num,
|
||
"name": phase_name,
|
||
"role": role,
|
||
"agent_name": None,
|
||
"output": f"跳过: 无 {role} 角色",
|
||
"success": False,
|
||
"error": f"missing_role:{role}",
|
||
"files": [],
|
||
})
|
||
continue
|
||
|
||
logger.info("团队编排 [%s]: 执行阶段 %s — %s (角色: %s, Agent: %s)",
|
||
self.team_id, phase_num, phase_name, role, agent.name)
|
||
|
||
try:
|
||
agent_config = _build_agent_config(agent, self.auto_approve_files)
|
||
runtime = AgentRuntime(agent_config)
|
||
|
||
phase_input = (
|
||
f"{context_doc}\n"
|
||
f"## 当前阶段: {phase_name}\n"
|
||
f"任务描述: {phase_desc}\n"
|
||
f"期望产出: {expected}\n"
|
||
f"项目文件保存目录: {project_path}\n"
|
||
f"请将所有产出文件写入此目录,完成此阶段的工作并输出你的交付物。"
|
||
)
|
||
phase_result = await runtime.run(phase_input)
|
||
|
||
output_text = phase_result.content if phase_result.success else (
|
||
phase_result.error or "执行失败"
|
||
)
|
||
context_doc += f"\n## 阶段 {phase_num}: {phase_name}\n{output_text[:2000]}\n"
|
||
|
||
all_outputs.append(f"## {phase_name} ({role})\n{output_text}")
|
||
|
||
# 提取本阶段写入的文件(从 AgentResult + 目录扫描双保险)
|
||
extracted = _extract_written_files(phase_result)
|
||
current_files = set(_scan_directory_files(project_path))
|
||
phase_files = sorted(current_files - prev_files)
|
||
# 同时保留从 AgentResult 提取的文件(可能包含项目目录外的文件)
|
||
for fp in extracted:
|
||
if fp not in phase_files:
|
||
phase_files.append(fp)
|
||
prev_files = current_files
|
||
all_files.extend(phase_files)
|
||
|
||
results["phases"].append({
|
||
"phase": phase_num,
|
||
"name": phase_name,
|
||
"role": role,
|
||
"agent_name": agent.name,
|
||
"output": output_text,
|
||
"success": phase_result.success,
|
||
"iterations": phase_result.iterations_used,
|
||
"tool_calls": phase_result.tool_calls_made,
|
||
"error": phase_result.error if not phase_result.success else None,
|
||
"files": phase_files,
|
||
})
|
||
except Exception as e:
|
||
logger.error("团队编排 [%s]: 阶段 %s 异常: %s", self.team_id, phase_num, e)
|
||
results["phases"].append({
|
||
"phase": phase_num,
|
||
"name": phase_name,
|
||
"role": role,
|
||
"agent_name": agent.name if agent else None,
|
||
"output": f"执行异常: {e}",
|
||
"success": False,
|
||
"error": str(e),
|
||
"files": [],
|
||
})
|
||
results["success"] = False
|
||
|
||
# ─── QA 审查 ───
|
||
qa_agent = self._get_agent_by_role(members, "qa")
|
||
if qa_agent and all_outputs:
|
||
logger.info("团队编排 [%s]: QA 审查开始", self.team_id)
|
||
try:
|
||
qa_config = _build_agent_config(qa_agent, self.auto_approve_files)
|
||
qa_runtime = AgentRuntime(qa_config)
|
||
deliverables = "\n\n---\n\n".join(all_outputs)
|
||
qa_input = (
|
||
f"请审查以下项目交付物:\n\n"
|
||
f"项目: {project_description}\n\n"
|
||
f"{deliverables[:8000]}\n\n"
|
||
f"请按 JSON 格式输出审查结果,包含 pass/score/issues/overall_assessment。"
|
||
)
|
||
qa_result = await qa_runtime.run(qa_input)
|
||
qa_review = _parse_plan_json(qa_result.content) if qa_result.success else {}
|
||
results["qa_review"] = {
|
||
"output": qa_result.content,
|
||
"review": qa_review,
|
||
"success": qa_result.success,
|
||
}
|
||
except Exception as e:
|
||
logger.error("团队编排 [%s]: QA 审查异常: %s", self.team_id, e)
|
||
results["qa_review"] = {"error": str(e)}
|
||
|
||
# ─── 汇总最终交付物 ───
|
||
results["final_deliverable"] = "\n\n---\n\n".join(all_outputs)
|
||
# 最终文件列表以目录扫描为准,合并从 AgentResult 提取的
|
||
final_scan = set(_scan_directory_files(project_path))
|
||
for fp in all_files:
|
||
final_scan.add(fp)
|
||
results["files"] = sorted(final_scan)
|
||
results["completed_at"] = datetime.now().isoformat()
|
||
return results
|
||
|
||
async def execute_stream(self, project_description: str, auto_approve_files: bool = True) -> AsyncGenerator[Dict[str, Any], None]:
|
||
"""流式执行团队项目,yield SSE 事件。
|
||
|
||
事件类型:
|
||
- plan_start / plan / plan_done — PM 规划阶段
|
||
- phase_start / phase_output / phase_done — 各执行阶段
|
||
- qa_start / qa_review / qa_done — QA 审查
|
||
- final — 全部完成
|
||
- error — 错误
|
||
"""
|
||
self.auto_approve_files = auto_approve_files
|
||
try:
|
||
team = self._load_team()
|
||
members = self._load_members()
|
||
except Exception as e:
|
||
yield {"type": "error", "content": str(e)}
|
||
return
|
||
|
||
if not members:
|
||
yield {"type": "error", "content": "团队没有成员"}
|
||
return
|
||
|
||
# ─── 创建项目目录 ───
|
||
workspace_root = _resolve_workspace_root()
|
||
safe_name = _safe_project_name(project_description)
|
||
project_path = workspace_root / "team_projects" / self.team_id / safe_name
|
||
project_path.mkdir(parents=True, exist_ok=True)
|
||
prev_files = set(_scan_directory_files(project_path))
|
||
all_files: List[str] = []
|
||
|
||
yield {
|
||
"type": "plan_start",
|
||
"team_name": team.name,
|
||
"member_count": len(members),
|
||
"project_path": str(project_path),
|
||
}
|
||
|
||
all_outputs: List[str] = []
|
||
|
||
# ─── 规划阶段(由 Leader Agent 担任) ───
|
||
workflow = (team.config or {}).get("workflow", "software_company")
|
||
planner_role = self._resolve_planner_role(members, workflow)
|
||
planner_agent = self._get_agent_by_role(members, planner_role)
|
||
if not planner_agent:
|
||
yield {"type": "error", "content": f"团队缺少 Leader 角色(需要 role='{planner_role}'),可用角色: {[m.role for m in members]}"}
|
||
return
|
||
|
||
try:
|
||
available_roles = [m.role for m in members]
|
||
roles_hint = "/".join(available_roles)
|
||
|
||
planner_config = _build_agent_config(planner_agent, self.auto_approve_files)
|
||
planner_runtime = AgentRuntime(planner_config)
|
||
planner_input = (
|
||
f"请分析以下需求,按 JSON 格式输出执行计划:\n\n"
|
||
f"需求描述:{project_description}\n"
|
||
f"输出文件目录:{project_path}\n"
|
||
f"你的团队有以下角色(每个 phase 必须使用这些角色之一):{roles_hint}\n"
|
||
f"请输出包含 phases 数组的完整 JSON 计划,每个 phase 需指定 role 为上述角色之一。"
|
||
)
|
||
|
||
yield {"type": "plan", "status": "running", "agent": planner_agent.name}
|
||
planner_result = await planner_runtime.run(planner_input)
|
||
plan = _parse_plan_json(planner_result.content) if planner_result.success else {}
|
||
|
||
yield {
|
||
"type": "plan_done",
|
||
"plan": plan,
|
||
"raw_output": planner_result.content[:2000],
|
||
"success": planner_result.success,
|
||
}
|
||
except Exception as e:
|
||
yield {"type": "error", "content": f"{planner_role} 规划失败: {e}"}
|
||
return
|
||
|
||
if not plan or not plan.get("phases"):
|
||
yield {"type": "error", "content": f"{planner_role} 未能生成有效计划"}
|
||
return
|
||
|
||
phases = plan["phases"]
|
||
context_doc = f"# 项目: {plan.get('project_name', '未命名')}\n\n## 需求分析\n{plan.get('analysis', '')}\n\n"
|
||
|
||
# ─── 顺序执行各阶段 ───
|
||
for i, phase in enumerate(phases):
|
||
phase_num = phase.get("phase", i + 1)
|
||
role = phase.get("role", "developer")
|
||
phase_name = phase.get("name", f"阶段 {phase_num}")
|
||
phase_desc = phase.get("description", "")
|
||
expected = phase.get("expected_output", "")
|
||
|
||
agent = self._get_agent_by_role(members, role)
|
||
if not agent:
|
||
msg = f"阶段 {phase_num} ({phase_name}) 跳过: 无 {role} 角色"
|
||
yield {"type": "phase_done", "phase": phase_num, "name": phase_name,
|
||
"role": role, "success": False, "error": f"missing_role:{role}"}
|
||
all_outputs.append(f"[跳过] {msg}")
|
||
continue
|
||
|
||
yield {
|
||
"type": "phase_start",
|
||
"phase": phase_num,
|
||
"name": phase_name,
|
||
"role": role,
|
||
"agent": agent.name,
|
||
}
|
||
|
||
try:
|
||
agent_config = _build_agent_config(agent, self.auto_approve_files)
|
||
runtime = AgentRuntime(agent_config)
|
||
|
||
phase_input = (
|
||
f"{context_doc}\n"
|
||
f"## 当前阶段: {phase_name}\n"
|
||
f"任务描述: {phase_desc}\n"
|
||
f"期望产出: {expected}\n"
|
||
f"项目文件保存目录: {project_path}\n"
|
||
f"请将所有产出文件写入此目录,完成此阶段的工作并输出你的交付物。"
|
||
)
|
||
phase_result = await runtime.run(phase_input)
|
||
|
||
output_text = phase_result.content if phase_result.success else (
|
||
phase_result.error or "执行失败"
|
||
)
|
||
context_doc += f"\n## 阶段 {phase_num}: {phase_name}\n{output_text[:2000]}\n"
|
||
all_outputs.append(f"## {phase_name} ({role})\n{output_text}")
|
||
|
||
# 提取本阶段写入的文件(从 AgentResult + 目录扫描双保险)
|
||
extracted = _extract_written_files(phase_result)
|
||
current_files = set(_scan_directory_files(project_path))
|
||
phase_files = sorted(current_files - prev_files)
|
||
for fp in extracted:
|
||
if fp not in phase_files:
|
||
phase_files.append(fp)
|
||
prev_files = current_files
|
||
all_files.extend(phase_files)
|
||
|
||
yield {
|
||
"type": "phase_done",
|
||
"phase": phase_num,
|
||
"name": phase_name,
|
||
"role": role,
|
||
"agent": agent.name,
|
||
"output": output_text,
|
||
"success": phase_result.success,
|
||
"iterations": phase_result.iterations_used,
|
||
"tool_calls": phase_result.tool_calls_made,
|
||
"error": phase_result.error if not phase_result.success else None,
|
||
"files": phase_files,
|
||
}
|
||
except Exception as e:
|
||
logger.error("团队编排: 阶段 %s 异常: %s", phase_num, e)
|
||
yield {
|
||
"type": "phase_done",
|
||
"phase": phase_num,
|
||
"name": phase_name,
|
||
"role": role,
|
||
"agent": agent.name,
|
||
"success": False,
|
||
"error": str(e),
|
||
}
|
||
|
||
# ─── QA 审查 ───
|
||
qa_agent = self._get_agent_by_role(members, "qa")
|
||
if qa_agent and all_outputs:
|
||
yield {"type": "qa_start", "agent": qa_agent.name}
|
||
try:
|
||
qa_config = _build_agent_config(qa_agent, self.auto_approve_files)
|
||
qa_runtime = AgentRuntime(qa_config)
|
||
deliverables = "\n\n---\n\n".join(all_outputs)
|
||
qa_input = (
|
||
f"请审查以下项目交付物:\n\n"
|
||
f"项目: {project_description}\n\n"
|
||
f"{deliverables[:8000]}\n\n"
|
||
f"请按 JSON 格式输出审查结果,包含 pass/score/issues/overall_assessment。"
|
||
)
|
||
qa_result = await qa_runtime.run(qa_input)
|
||
qa_review_data = _parse_plan_json(qa_result.content) if qa_result.success else {}
|
||
yield {
|
||
"type": "qa_done",
|
||
"output": qa_result.content,
|
||
"review": qa_review_data,
|
||
"success": qa_result.success,
|
||
}
|
||
except Exception as e:
|
||
logger.error("团队编排: QA 审查异常: %s", e)
|
||
yield {"type": "qa_done", "error": str(e)}
|
||
|
||
# ─── 最终交付物 ───
|
||
final = "\n\n---\n\n".join(all_outputs)
|
||
final_scan = set(_scan_directory_files(project_path))
|
||
for fp in all_files:
|
||
final_scan.add(fp)
|
||
yield {
|
||
"type": "final",
|
||
"deliverable": final,
|
||
"phase_count": len(phases),
|
||
"team_name": team.name,
|
||
"project_path": str(project_path),
|
||
"files": sorted(final_scan),
|
||
}
|