"""Expert Team 计划数据模型 - 流水线模式 支持两种模式: 1. hub-and-spoke(向后兼容):Lead Expert 分解为并行子任务(SubTask) 2. pipeline(新):Lead Expert 分解为阶段(PlanPhase),阶段间有依赖关系 流水线模式核心: - Lead Expert 分解任务为阶段(phases),每个阶段有 assigned_expert 和 depends_on - 执行时按依赖拓扑排序,无依赖的阶段并行,有依赖的串行 - 阶段间数据通过 SharedWorkspace 传递 """ from __future__ import annotations import enum import uuid from dataclasses import dataclass, field from typing import Any class MergeStrategy(str, enum.Enum): """合并策略 - Lead Expert 用于选择最佳结果 仅保留 BEST 策略:Lead Expert 从所有结果中选择或综合出最佳结果。 """ BEST = "best" class PlanStatus(str, enum.Enum): """计划状态""" DRAFT = "draft" EXECUTING = "executing" COMPLETED = "completed" FAILED = "failed" FALLBACK = "fallback" class SubTaskStatus(str, enum.Enum): """子任务状态(hub-and-spoke 模式)""" PENDING = "pending" RUNNING = "running" COMPLETED = "completed" FAILED = "failed" class PhaseStatus(str, enum.Enum): """阶段状态(流水线模式)""" PENDING = "pending" RUNNING = "in_progress" COMPLETED = "completed" FAILED = "failed" class PhaseType(str, enum.Enum): """阶段类型 EXECUTION: 标准执行阶段,专家独立完成分配的任务 DEBATE: 辩论阶段,Lead 主导指定专家就分歧点交锋,Lead 裁决 """ EXECUTION = "execution" DEBATE = "debate" @dataclass class SubTask: """Lead Expert 分解出的子任务(hub-and-spoke 模式,向后兼容) Attributes: id: 子任务标识符 description: 子任务描述 assigned_expert: 分配的 Expert 名称 status: 当前状态 result: 子任务输出结果 """ id: str = field(default_factory=lambda: str(uuid.uuid4())) description: str = "" assigned_expert: str = "" status: SubTaskStatus = SubTaskStatus.PENDING result: dict[str, Any] | None = None def to_dict(self) -> dict[str, Any]: """序列化为字典""" return { "id": self.id, "description": self.description, "assigned_expert": self.assigned_expert, "status": self.status.value, "result": self.result, } @classmethod def from_dict(cls, data: dict[str, Any]) -> SubTask: """从字典创建 SubTask""" return cls( id=data.get("id", str(uuid.uuid4())), description=data.get("description", ""), assigned_expert=data.get("assigned_expert", ""), status=SubTaskStatus(data.get("status", SubTaskStatus.PENDING.value)), result=data.get("result"), ) @dataclass class CollaborationContract: """协作契约 — 定义专家间的协作关系 Lead 在分解任务时为每个阶段定义协作契约,明确哪些专家需要协作、协作内容是什么。 Attributes: from_expert: 提供协作内容的专家名称 to_expert: 接收协作内容的专家名称 content_description: 协作内容描述(如"API 定义"、"数据模型") status: 契约状态(pending/delivered/received) """ from_expert: str = "" to_expert: str = "" content_description: str = "" status: str = "pending" def to_dict(self) -> dict[str, Any]: """序列化为字典""" return { "from_expert": self.from_expert, "to_expert": self.to_expert, "content_description": self.content_description, "status": self.status, } @classmethod def from_dict(cls, data: dict[str, Any]) -> CollaborationContract: """从字典创建 CollaborationContract""" return cls( from_expert=data.get("from_expert", ""), to_expert=data.get("to_expert", ""), content_description=data.get("content_description", ""), status=data.get("status", "pending"), ) @dataclass class PlanPhase: """流水线模式中的执行阶段 Lead Expert 将任务分解为多个阶段,阶段间通过 depends_on 建立依赖关系。 执行时按拓扑排序,同层无依赖阶段并行,层间串行。 Attributes: id: 阶段标识符(用于 depends_on 引用) name: 阶段名称(如"规划"、"前端"、"后端"、"QA"、"评审") assigned_expert: 分配的 Expert 名称 task_description: 阶段任务描述 depends_on: 前置阶段 ID 列表(空列表表示无依赖) status: 当前状态 result: 阶段输出结果 phase_type: 阶段类型(EXECUTION 或 DEBATE) debate_config: 辩论阶段配置(仅 DEBATE 类型使用): - topic: 辩论主题 - participants: 参与专家名称列表 - max_rounds: 最大辩论轮次(默认 2,硬上限 4) - skip: 是否跳过辩论(逃生舱) collaboration_contracts: 协作契约列表,定义该阶段涉及的专家协作关系 rework_count: 返工次数(Lead 验收不合格后重新执行的次数) review_feedback: Lead 验收反馈(不合格时的修改要求) """ id: str = field(default_factory=lambda: str(uuid.uuid4())) name: str = "" assigned_expert: str = "" task_description: str = "" depends_on: list[str] = field(default_factory=list) status: PhaseStatus = PhaseStatus.PENDING result: dict[str, Any] | None = None phase_type: PhaseType = PhaseType.EXECUTION debate_config: dict[str, Any] | None = None collaboration_contracts: list[CollaborationContract] = field(default_factory=list) rework_count: int = 0 review_feedback: str | None = None # G9/U4: opt-in rollback fields. When unset, no rollback executes (KTD6). # validation_command runs first; if it fails, rollback_command runs. # canonical rollback pattern: `git checkout `. validation_command: str | None = None rollback_command: str | None = None def to_dict(self) -> dict[str, Any]: """序列化为字典""" # Serialize result to string to match frontend ITeamPlanPhase.result type result_str: str | None = None if self.result is not None: if isinstance(self.result, dict): result_str = self.result.get("content", str(self.result)) else: result_str = str(self.result) out: dict[str, Any] = { "id": self.id, "name": self.name, "assigned_expert": self.assigned_expert, "task_description": self.task_description, "depends_on": list(self.depends_on), "status": self.status.value, "result": result_str, "phase_type": self.phase_type.value, "debate_config": self.debate_config, "collaboration_contracts": [c.to_dict() for c in self.collaboration_contracts], "rework_count": self.rework_count, "review_feedback": self.review_feedback, } # G9/U4: only include new keys when set, to preserve pre-change dict shape (KTD6). if self.validation_command is not None: out["validation_command"] = self.validation_command if self.rollback_command is not None: out["rollback_command"] = self.rollback_command return out @classmethod def from_dict(cls, data: dict[str, Any]) -> PlanPhase: """从字典创建 PlanPhase""" contracts_data = data.get("collaboration_contracts", []) if not isinstance(contracts_data, list): contracts_data = [] contracts = [ CollaborationContract.from_dict(c) if isinstance(c, dict) else CollaborationContract() for c in contracts_data ] return cls( id=data.get("id", str(uuid.uuid4())), name=data.get("name", ""), assigned_expert=data.get("assigned_expert", ""), task_description=data.get("task_description", ""), depends_on=list(data.get("depends_on", [])), status=PhaseStatus(data.get("status", PhaseStatus.PENDING.value)), result=data.get("result"), phase_type=PhaseType(data.get("phase_type", PhaseType.EXECUTION.value)), debate_config=data.get("debate_config"), collaboration_contracts=contracts, rework_count=data.get("rework_count", 0), review_feedback=data.get("review_feedback"), validation_command=data.get("validation_command"), rollback_command=data.get("rollback_command"), ) @dataclass class TeamPlan: """Expert Team 执行计划 支持两种模式: 1. hub-and-spoke(向后兼容):使用 subtasks,并行执行无依赖 2. pipeline(新):使用 phases,按依赖拓扑排序执行 新代码应优先使用 phases 字段。subtasks 保留用于向后兼容。 Attributes: id: 计划标识符 task: 原始任务描述 subtasks: 子任务列表(hub-and-spoke 模式,向后兼容) phases: 阶段列表(流水线模式) status: 计划状态 lead_expert: 主导 Expert 名称 """ id: str = field(default_factory=lambda: str(uuid.uuid4())) task: str = "" subtasks: list[SubTask] = field(default_factory=list) phases: list[PlanPhase] = field(default_factory=list) status: PlanStatus = PlanStatus.DRAFT lead_expert: str = "" def to_dict(self) -> dict[str, Any]: """序列化为字典""" return { "id": self.id, "task": self.task, "subtasks": [st.to_dict() for st in self.subtasks], "phases": [ph.to_dict() for ph in self.phases], "status": self.status.value, "lead_expert": self.lead_expert, } @classmethod def from_dict(cls, data: dict[str, Any]) -> TeamPlan: """从字典创建 TeamPlan""" subtasks = [SubTask.from_dict(st) for st in data.get("subtasks", [])] phases = [PlanPhase.from_dict(ph) for ph in data.get("phases", [])] return cls( id=data.get("id", str(uuid.uuid4())), task=data.get("task", ""), subtasks=subtasks, phases=phases, status=PlanStatus(data.get("status", PlanStatus.DRAFT.value)), lead_expert=data.get("lead_expert", ""), ) # ── SubTask 方法(hub-and-spoke 模式,向后兼容)── def get_subtask(self, subtask_id: str) -> SubTask | None: """根据 ID 获取子任务,不存在则返回 None""" for st in self.subtasks: if st.id == subtask_id: return st return None def update_subtask_status( self, subtask_id: str, status: SubTaskStatus, result: dict[str, Any] | None = None ) -> None: """更新子任务状态和可选的结果""" st = self.get_subtask(subtask_id) if st is not None: st.status = status if result is not None: st.result = result @property def completed_subtasks(self) -> list[SubTask]: """已完成的子任务列表""" return [st for st in self.subtasks if st.status == SubTaskStatus.COMPLETED] @property def failed_subtasks(self) -> list[SubTask]: """失败的子任务列表""" return [st for st in self.subtasks if st.status == SubTaskStatus.FAILED] @property def all_done(self) -> bool: """所有子任务是否都已完成(成功或失败)""" return all( st.status in (SubTaskStatus.COMPLETED, SubTaskStatus.FAILED) for st in self.subtasks ) # ── PlanPhase 方法(流水线模式)── def get_phase(self, phase_id: str) -> PlanPhase | None: """根据 ID 获取阶段,不存在则返回 None""" for ph in self.phases: if ph.id == phase_id: return ph return None def update_phase_status( self, phase_id: str, status: PhaseStatus, result: dict[str, Any] | None = None ) -> None: """更新阶段状态和可选的结果""" ph = self.get_phase(phase_id) if ph is not None: ph.status = status if result is not None: ph.result = result @property def completed_phases(self) -> list[PlanPhase]: """已完成的阶段列表""" return [ph for ph in self.phases if ph.status == PhaseStatus.COMPLETED] @property def failed_phases(self) -> list[PlanPhase]: """失败的阶段列表""" return [ph for ph in self.phases if ph.status == PhaseStatus.FAILED] @property def all_phases_done(self) -> bool: """所有阶段是否都已完成(成功或失败)""" return all(ph.status in (PhaseStatus.COMPLETED, PhaseStatus.FAILED) for ph in self.phases) def get_ready_phases(self) -> list[PlanPhase]: """返回当前可执行的阶段(状态为 PENDING 且所有依赖已完成) Returns: 可执行的阶段列表 """ completed_ids = {ph.id for ph in self.completed_phases} ready = [] for ph in self.phases: if ph.status != PhaseStatus.PENDING: continue # Check if all dependencies are completed if all(dep_id in completed_ids for dep_id in ph.depends_on): ready.append(ph) return ready def topological_sort(self) -> list[list[PlanPhase]]: """按依赖关系拓扑排序阶段,返回执行层列表 同层阶段无依赖关系,可并行执行;层间有依赖,需串行等待。 Returns: 执行层列表,每层包含可并行执行的阶段 Raises: ValueError: 如果存在循环依赖 """ if not self.phases: return [] # Build dependency graph phase_map = {ph.id: ph for ph in self.phases} all_ids = set(phase_map.keys()) # Validate depends_on references for ph in self.phases: for dep_id in ph.depends_on: if dep_id not in all_ids: raise ValueError( f"Phase '{ph.id}' ({ph.name}) depends on non-existent phase '{dep_id}'" ) # Kahn's algorithm for topological sort # Compute in-degree (number of dependencies) for each phase in_degree: dict[str, int] = {ph.id: len(ph.depends_on) for ph in self.phases} # Build reverse dependency map (which phases depend on this one) dependents: dict[str, list[str]] = {ph.id: [] for ph in self.phases} for ph in self.phases: for dep_id in ph.depends_on: dependents[dep_id].append(ph.id) layers: list[list[PlanPhase]] = [] processed: set[str] = set() while len(processed) < len(self.phases): # Find all phases with in_degree 0 that haven't been processed current_layer_ids = [ ph_id for ph_id in in_degree if ph_id not in processed and in_degree[ph_id] == 0 ] if not current_layer_ids: # No progress — cycle detected remaining = [ph_id for ph_id in in_degree if ph_id not in processed] raise ValueError(f"Circular dependency detected among phases: {remaining}") # Add current layer current_layer = [phase_map[ph_id] for ph_id in current_layer_ids] layers.append(current_layer) # Mark as processed and reduce in_degree for dependents for ph_id in current_layer_ids: processed.add(ph_id) for dep_id in dependents[ph_id]: in_degree[dep_id] -= 1 return layers