447 lines
16 KiB
Python
447 lines
16 KiB
Python
"""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 <specific_files>`.
|
||
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
|