fischer-agentkit/src/agentkit/experts/plan.py

447 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Expert Team 计划数据模型 - 流水线模式
支持两种模式:
1. hub-and-spoke向后兼容Lead Expert 分解为并行子任务SubTask
2. pipelineLead 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