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:
parent
4ea7801bcf
commit
e539122314
|
|
@ -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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 就绪阶段测试"""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue