feat(experts): add PhaseType enum and debate_config to PlanPhase

U1: Data model foundation for structured debate collaboration.
- Add PhaseType enum (EXECUTION | DEBATE)
- Add phase_type and debate_config fields to PlanPhase
- Update to_dict/from_dict for serialization with backward compatibility
- Add tests for PhaseType, debate phase creation, serialization, and
  mixed EXECUTION+DEBATE topological sort
This commit is contained in:
chiguyong 2026-06-24 10:42:11 +08:00
parent 4ea7801bcf
commit e539122314
2 changed files with 156 additions and 0 deletions

View File

@ -55,6 +55,17 @@ class PhaseStatus(str, enum.Enum):
FAILED = "failed" FAILED = "failed"
class PhaseType(str, enum.Enum):
"""阶段类型
EXECUTION: 标准执行阶段专家独立完成分配的任务
DEBATE: 辩论阶段Lead 主导指定专家就分歧点交锋Lead 裁决
"""
EXECUTION = "execution"
DEBATE = "debate"
@dataclass @dataclass
class SubTask: class SubTask:
"""Lead Expert 分解出的子任务hub-and-spoke 模式,向后兼容) """Lead Expert 分解出的子任务hub-and-spoke 模式,向后兼容)
@ -110,6 +121,12 @@ class PlanPhase:
depends_on: 前置阶段 ID 列表空列表表示无依赖 depends_on: 前置阶段 ID 列表空列表表示无依赖
status: 当前状态 status: 当前状态
result: 阶段输出结果 result: 阶段输出结果
phase_type: 阶段类型EXECUTION DEBATE
debate_config: 辩论阶段配置 DEBATE 类型使用
- topic: 辩论主题
- participants: 参与专家名称列表
- max_rounds: 最大辩论轮次默认 2硬上限 4
- skip: 是否跳过辩论逃生舱
""" """
id: str = field(default_factory=lambda: str(uuid.uuid4())) id: str = field(default_factory=lambda: str(uuid.uuid4()))
@ -119,6 +136,8 @@ class PlanPhase:
depends_on: list[str] = field(default_factory=list) depends_on: list[str] = field(default_factory=list)
status: PhaseStatus = PhaseStatus.PENDING status: PhaseStatus = PhaseStatus.PENDING
result: dict[str, Any] | None = None result: dict[str, Any] | None = None
phase_type: PhaseType = PhaseType.EXECUTION
debate_config: dict[str, Any] | None = None
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
"""序列化为字典""" """序列化为字典"""
@ -137,6 +156,8 @@ class PlanPhase:
"depends_on": list(self.depends_on), "depends_on": list(self.depends_on),
"status": self.status.value, "status": self.status.value,
"result": result_str, "result": result_str,
"phase_type": self.phase_type.value,
"debate_config": self.debate_config,
} }
@classmethod @classmethod
@ -150,6 +171,8 @@ class PlanPhase:
depends_on=list(data.get("depends_on", [])), depends_on=list(data.get("depends_on", [])),
status=PhaseStatus(data.get("status", PhaseStatus.PENDING.value)), status=PhaseStatus(data.get("status", PhaseStatus.PENDING.value)),
result=data.get("result"), result=data.get("result"),
phase_type=PhaseType(data.get("phase_type", PhaseType.EXECUTION.value)),
debate_config=data.get("debate_config"),
) )

View File

@ -7,6 +7,7 @@ import pytest
from agentkit.experts.plan import ( from agentkit.experts.plan import (
MergeStrategy, MergeStrategy,
PhaseStatus, PhaseStatus,
PhaseType,
PlanPhase, PlanPhase,
PlanStatus, PlanStatus,
SubTask, SubTask,
@ -356,6 +357,19 @@ class TestPhaseStatus:
assert PhaseStatus.FAILED == "failed" assert PhaseStatus.FAILED == "failed"
class TestPhaseType:
"""PhaseType 枚举测试"""
def test_types_exist(self):
"""阶段类型都存在"""
assert PhaseType.EXECUTION == "execution"
assert PhaseType.DEBATE == "debate"
def test_only_two_types(self):
"""只有 EXECUTION 和 DEBATE 两种类型"""
assert len(list(PhaseType)) == 2
class TestPlanPhase: class TestPlanPhase:
"""PlanPhase 数据模型测试""" """PlanPhase 数据模型测试"""
@ -438,6 +452,79 @@ class TestPlanPhase:
# result is serialized to string to match frontend ITeamPlanPhase.result type # result is serialized to string to match frontend ITeamPlanPhase.result type
assert d["result"] == "phase output data" assert d["result"] == "phase output data"
def test_default_phase_type_is_execution(self):
"""默认 phase_type 为 EXECUTION"""
phase = PlanPhase(name="测试阶段")
assert phase.phase_type == PhaseType.EXECUTION
assert phase.debate_config is None
def test_debate_phase_creation(self):
"""创建 DEBATE 类型阶段"""
debate_config = {
"topic": "前端框架选型React vs Vue",
"participants": ["frontend_engineer", "tech_lead"],
"max_rounds": 2,
}
phase = PlanPhase(
name="框架选型辩论",
assigned_expert="tech_lead",
task_description="就前端框架选型进行辩论",
phase_type=PhaseType.DEBATE,
debate_config=debate_config,
)
assert phase.phase_type == PhaseType.DEBATE
assert phase.debate_config == debate_config
assert phase.debate_config["topic"] == "前端框架选型React vs Vue"
assert phase.debate_config["participants"] == ["frontend_engineer", "tech_lead"]
assert phase.debate_config["max_rounds"] == 2
def test_debate_phase_serialization_roundtrip(self):
"""DEBATE 阶段序列化往返"""
debate_config = {
"topic": "微服务 vs 单体",
"participants": ["backend_engineer", "tech_lead"],
"max_rounds": 3,
}
phase = PlanPhase(
id="debate_1",
name="架构辩论",
assigned_expert="tech_lead",
task_description="架构选型辩论",
phase_type=PhaseType.DEBATE,
debate_config=debate_config,
)
d = phase.to_dict()
assert d["phase_type"] == "debate"
assert d["debate_config"] == debate_config
restored = PlanPhase.from_dict(d)
assert restored.phase_type == PhaseType.DEBATE
assert restored.debate_config == debate_config
assert restored.debate_config["topic"] == "微服务 vs 单体"
def test_backward_compatibility_no_phase_type(self):
"""向后兼容:不带 phase_type 的旧 dict 默认为 EXECUTION"""
old_dict = {
"id": "old_phase",
"name": "旧阶段",
"assigned_expert": "dev",
"task_description": "旧任务",
"depends_on": [],
"status": "pending",
"result": None,
}
phase = PlanPhase.from_dict(old_dict)
assert phase.phase_type == PhaseType.EXECUTION
assert phase.debate_config is None
def test_debate_config_none_for_execution(self):
"""EXECUTION 阶段的 debate_config 为 None"""
phase = PlanPhase(name="执行阶段", phase_type=PhaseType.EXECUTION)
assert phase.debate_config is None
d = phase.to_dict()
assert d["phase_type"] == "execution"
assert d["debate_config"] is None
class TestTeamPlanPhases: class TestTeamPlanPhases:
"""TeamPlan 流水线模式phases测试""" """TeamPlan 流水线模式phases测试"""
@ -633,6 +720,52 @@ class TestTopologicalSort:
with pytest.raises(ValueError, match="non-existent phase"): with pytest.raises(ValueError, match="non-existent phase"):
plan.topological_sort() plan.topological_sort()
def test_mixed_execution_and_debate_phases(self):
"""混合 EXECUTION + DEBATE 阶段的拓扑排序
结构:
Layer 0: [规划] (EXECUTION)
Layer 1: [前端, 后端] (EXECUTION, 依赖规划)
Layer 2: [架构辩论] (DEBATE, 依赖前端+后端)
Layer 3: [QA] (EXECUTION, 依赖架构辩论)
"""
plan = TeamPlan(
task="混合模式任务",
phases=[
PlanPhase(id="p1", name="规划", assigned_expert="tech_lead", depends_on=[]),
PlanPhase(
id="p2", name="前端", assigned_expert="frontend", depends_on=["p1"]
),
PlanPhase(
id="p3", name="后端", assigned_expert="backend", depends_on=["p1"]
),
PlanPhase(
id="d1",
name="架构辩论",
assigned_expert="tech_lead",
depends_on=["p2", "p3"],
phase_type=PhaseType.DEBATE,
debate_config={
"topic": "前后端接口设计",
"participants": ["frontend", "backend"],
"max_rounds": 2,
},
),
PlanPhase(id="p4", name="QA", assigned_expert="qa", depends_on=["d1"]),
],
)
layers = plan.topological_sort()
assert len(layers) == 4
assert [ph.id for ph in layers[0]] == ["p1"]
assert set(ph.id for ph in layers[1]) == {"p2", "p3"}
assert [ph.id for ph in layers[2]] == ["d1"]
assert [ph.id for ph in layers[3]] == ["p4"]
# Verify the debate phase is correctly typed
debate_phase = plan.get_phase("d1")
assert debate_phase is not None
assert debate_phase.phase_type == PhaseType.DEBATE
assert debate_phase.debate_config is not None
class TestGetReadyPhases: class TestGetReadyPhases:
"""get_ready_phases 就绪阶段测试""" """get_ready_phases 就绪阶段测试"""