feat(server): U6 新增 _execute_team_collab 集成 @team 流水线到 WebSocket
This commit is contained in:
parent
ee6d16345c
commit
1e818b507d
|
|
@ -307,6 +307,148 @@ async def _execute_board_meeting(
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def _execute_team_collab(
|
||||||
|
websocket: WebSocket,
|
||||||
|
session_id: str,
|
||||||
|
content: str,
|
||||||
|
sm: SessionManager,
|
||||||
|
) -> bool:
|
||||||
|
"""Intercept @team prefix and execute a pipeline team collaboration.
|
||||||
|
|
||||||
|
Returns True if the input was handled as a team collaboration (caller should return),
|
||||||
|
False if the input should continue through the normal chat pipeline.
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Resolve @team routing via ExpertTeamRouter
|
||||||
|
2. Create ExpertTeam with lead + member configs
|
||||||
|
3. Register handoff_transport handler to relay events to WebSocket
|
||||||
|
4. Execute TeamOrchestrator (pipeline mode)
|
||||||
|
5. Send final synthesis as final_answer
|
||||||
|
6. Persist user task + final result to session history
|
||||||
|
"""
|
||||||
|
from agentkit.experts.router import ExpertTeamRouter
|
||||||
|
from agentkit.experts.team import ExpertTeam
|
||||||
|
from agentkit.experts.orchestrator import TeamOrchestrator
|
||||||
|
|
||||||
|
app_state = websocket.app.state
|
||||||
|
|
||||||
|
# Resolve ExpertTemplateRegistry from app.state (loaded at startup)
|
||||||
|
template_registry = getattr(app_state, "expert_template_registry", None)
|
||||||
|
if template_registry is None:
|
||||||
|
from agentkit.experts.registry import ExpertTemplateRegistry
|
||||||
|
|
||||||
|
template_registry = ExpertTemplateRegistry()
|
||||||
|
|
||||||
|
team_router = ExpertTeamRouter(template_registry=template_registry)
|
||||||
|
routing_result = team_router.resolve(content)
|
||||||
|
|
||||||
|
if not routing_result.matched:
|
||||||
|
return False # Not a @team input, continue normal pipeline
|
||||||
|
|
||||||
|
if not routing_result.task_content:
|
||||||
|
await websocket.send_json(
|
||||||
|
{"type": "error", "data": {"message": "团队任务需要一个描述,例如:@team 开发用户登录功能"}}
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Resolve expert configs from specified experts or default dev_team template
|
||||||
|
expert_configs = team_router.resolve_expert_configs(routing_result.specified_experts)
|
||||||
|
if not expert_configs:
|
||||||
|
await websocket.send_json(
|
||||||
|
{"type": "error", "data": {"message": "无法解析团队成员,请检查专家名称或模板配置"}}
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Split configs: first is lead, rest are members (V2 verification)
|
||||||
|
lead_config = expert_configs[0]
|
||||||
|
member_configs = expert_configs[1:] if len(expert_configs) > 1 else []
|
||||||
|
|
||||||
|
# Create ExpertTeam
|
||||||
|
team = ExpertTeam(
|
||||||
|
pool=app_state.agent_pool,
|
||||||
|
template_registry=template_registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register handoff_transport handler to relay team events to WebSocket
|
||||||
|
async def _relay_team_event(message: dict) -> None:
|
||||||
|
msg_type = message.get("type")
|
||||||
|
if not msg_type:
|
||||||
|
return
|
||||||
|
# Strip internal fields, keep only event data
|
||||||
|
event_data = {k: v for k, v in message.items() if k != "type"}
|
||||||
|
await emit_team_event(websocket, msg_type, event_data)
|
||||||
|
|
||||||
|
team.handoff_transport.register_handler(team.team_channel, _relay_team_event)
|
||||||
|
|
||||||
|
# Append user task to session history
|
||||||
|
await sm.append_message(
|
||||||
|
session_id=session_id,
|
||||||
|
role=MessageRole.USER,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await team.create_team(lead_config=lead_config, member_configs=member_configs)
|
||||||
|
orchestrator = TeamOrchestrator(team=team)
|
||||||
|
result = await orchestrator.execute(routing_result.task_content)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Team collaboration failed for session {session_id}: {e}", exc_info=True)
|
||||||
|
await websocket.send_json(
|
||||||
|
{"type": "error", "data": {"message": f"团队协作执行失败: {str(e)[:200]}"}}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await team.dissolve()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
# Always remove handler to avoid leaks
|
||||||
|
try:
|
||||||
|
team.handoff_transport._handlers.pop(team.team_channel, None)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Build final answer text from synthesis result
|
||||||
|
final_result = result.get("result") or {}
|
||||||
|
final_content = final_result.get("content", "") if isinstance(final_result, dict) else str(final_result)
|
||||||
|
|
||||||
|
if not final_content:
|
||||||
|
# Fallback: use phase results if synthesis is empty
|
||||||
|
phase_results = result.get("phase_results") or {}
|
||||||
|
if phase_results:
|
||||||
|
parts = []
|
||||||
|
for phase_id, pr in phase_results.items():
|
||||||
|
if isinstance(pr, dict) and "content" in pr:
|
||||||
|
parts.append(f"### {pr.get('phase_name', phase_id)}\n\n{pr['content']}")
|
||||||
|
final_content = "\n\n".join(parts) if parts else "团队执行完成,但未生成最终结果。"
|
||||||
|
else:
|
||||||
|
final_content = "团队执行完成,但未生成最终结果。"
|
||||||
|
|
||||||
|
await websocket.send_json(
|
||||||
|
{
|
||||||
|
"type": "final_answer",
|
||||||
|
"content": final_content,
|
||||||
|
"is_final": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Persist final synthesis as assistant message
|
||||||
|
await sm.append_message(
|
||||||
|
session_id=session_id,
|
||||||
|
role=MessageRole.ASSISTANT,
|
||||||
|
content=final_content,
|
||||||
|
agent_name="team_collab",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dissolve the team to release expert agents
|
||||||
|
try:
|
||||||
|
await team.dissolve()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Team dissolve failed: {e}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _session_to_response(session) -> SessionResponse:
|
def _session_to_response(session) -> SessionResponse:
|
||||||
return SessionResponse(
|
return SessionResponse(
|
||||||
session_id=session.session_id,
|
session_id=session.session_id,
|
||||||
|
|
@ -637,6 +779,9 @@ async def _handle_chat_message(
|
||||||
|
|
||||||
Board Meeting mode: @board prefix is intercepted before RequestPreprocessor
|
Board Meeting mode: @board prefix is intercepted before RequestPreprocessor
|
||||||
and routed to BoardOrchestrator for multi-round group discussion.
|
and routed to BoardOrchestrator for multi-round group discussion.
|
||||||
|
|
||||||
|
Team Collaboration mode: @team prefix is intercepted before RequestPreprocessor
|
||||||
|
and routed to TeamOrchestrator for pipeline-based expert collaboration.
|
||||||
"""
|
"""
|
||||||
from agentkit.chat.request_preprocessor import RequestPreprocessor
|
from agentkit.chat.request_preprocessor import RequestPreprocessor
|
||||||
|
|
||||||
|
|
@ -644,6 +789,10 @@ async def _handle_chat_message(
|
||||||
if await _execute_board_meeting(websocket, session_id, content, sm):
|
if await _execute_board_meeting(websocket, session_id, content, sm):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Team Collaboration mode: intercept @team prefix before any other preprocessing
|
||||||
|
if await _execute_team_collab(websocket, session_id, content, sm):
|
||||||
|
return
|
||||||
|
|
||||||
# Resolve Agent first (needed for default tools/prompt)
|
# Resolve Agent first (needed for default tools/prompt)
|
||||||
pool = websocket.app.state.agent_pool
|
pool = websocket.app.state.agent_pool
|
||||||
session = await sm.get_session(session_id)
|
session = await sm.get_session(session_id)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,491 @@
|
||||||
|
"""Tests for _execute_team_collab in chat.py (U6).
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- @team prefix triggers team collaboration (returns True)
|
||||||
|
- Non-@team input does not trigger (returns False)
|
||||||
|
- @team without task content sends error
|
||||||
|
- Team events are relayed to WebSocket via emit_team_event
|
||||||
|
- final_answer is sent after execution
|
||||||
|
- User message and final result are persisted to session history
|
||||||
|
- Team is dissolved after execution
|
||||||
|
- Execution failure sends error and dissolves team
|
||||||
|
- @team and @board do not interfere with each other
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from agentkit.experts.config import ExpertConfig, ExpertTemplate
|
||||||
|
from agentkit.experts.registry import ExpertTemplateRegistry
|
||||||
|
from agentkit.server.routes.chat import _execute_team_collab
|
||||||
|
from agentkit.session.manager import SessionManager
|
||||||
|
from agentkit.session.models import MessageRole
|
||||||
|
from agentkit.session.store import InMemorySessionStore
|
||||||
|
|
||||||
|
|
||||||
|
# ── 辅助函数 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _make_expert_template(
|
||||||
|
name: str,
|
||||||
|
persona: str = "测试专家",
|
||||||
|
is_lead: bool = False,
|
||||||
|
) -> ExpertTemplate:
|
||||||
|
"""创建测试用 ExpertTemplate"""
|
||||||
|
config = ExpertConfig(
|
||||||
|
name=name,
|
||||||
|
agent_type="expert",
|
||||||
|
persona=persona,
|
||||||
|
thinking_style="analytical",
|
||||||
|
bound_skills=[],
|
||||||
|
is_lead=is_lead,
|
||||||
|
task_mode="llm_generate",
|
||||||
|
prompt={"identity": persona},
|
||||||
|
)
|
||||||
|
return ExpertTemplate(
|
||||||
|
name=name,
|
||||||
|
config=config,
|
||||||
|
is_builtin=True,
|
||||||
|
description=f"{name} 模板",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_registry_with_dev_team() -> ExpertTemplateRegistry:
|
||||||
|
"""创建包含 dev_team 模板和成员模板的注册中心"""
|
||||||
|
registry = ExpertTemplateRegistry()
|
||||||
|
registry.register(_make_expert_template("tech_lead", persona="技术负责人"))
|
||||||
|
registry.register(_make_expert_template("frontend_engineer", persona="前端工程师"))
|
||||||
|
registry.register(_make_expert_template("backend_engineer", persona="后端工程师"))
|
||||||
|
# dev_team 模板(bound_skills 存储成员列表)
|
||||||
|
registry.register(
|
||||||
|
ExpertTemplate(
|
||||||
|
name="dev_team",
|
||||||
|
config=ExpertConfig(
|
||||||
|
name="dev_team",
|
||||||
|
agent_type="expert",
|
||||||
|
persona="编程团队",
|
||||||
|
thinking_style="流水线",
|
||||||
|
bound_skills=["tech_lead", "frontend_engineer", "backend_engineer"],
|
||||||
|
task_mode="llm_generate",
|
||||||
|
prompt={"identity": "Dev Team"},
|
||||||
|
),
|
||||||
|
is_builtin=True,
|
||||||
|
description="编程团队模板",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return registry
|
||||||
|
|
||||||
|
|
||||||
|
class FakeWebSocket:
|
||||||
|
"""Minimal WebSocket fake for testing."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.sent: list[dict] = []
|
||||||
|
self.app = MagicMock()
|
||||||
|
self.app.state.agent_pool = MagicMock()
|
||||||
|
self.app.state.expert_template_registry = None # Will be set per-test
|
||||||
|
|
||||||
|
async def send_json(self, data: dict) -> None:
|
||||||
|
self.sent.append(data)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_mock_team() -> MagicMock:
|
||||||
|
"""创建 mock ExpertTeam 实例"""
|
||||||
|
mock_team = MagicMock()
|
||||||
|
mock_team.team_channel = "team:test"
|
||||||
|
mock_team.handoff_transport = MagicMock()
|
||||||
|
mock_team.handoff_transport.register_handler = MagicMock()
|
||||||
|
mock_team.handoff_transport._handlers = {}
|
||||||
|
mock_team.create_team = AsyncMock()
|
||||||
|
mock_team.dissolve = AsyncMock()
|
||||||
|
return mock_team
|
||||||
|
|
||||||
|
|
||||||
|
def _make_mock_orchestrator(result: dict) -> MagicMock:
|
||||||
|
"""创建 mock TeamOrchestrator 实例"""
|
||||||
|
mock_orch = MagicMock()
|
||||||
|
mock_orch.execute = AsyncMock(return_value=result)
|
||||||
|
return mock_orch
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def session_manager() -> SessionManager:
|
||||||
|
sm = SessionManager(store=InMemorySessionStore())
|
||||||
|
session = await sm.create_session(agent_name="test-agent")
|
||||||
|
sm._test_session_id = session.session_id
|
||||||
|
return sm
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def websocket() -> FakeWebSocket:
|
||||||
|
ws = FakeWebSocket()
|
||||||
|
ws.app.state.expert_template_registry = _make_registry_with_dev_team()
|
||||||
|
return ws
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_orchestrator_result() -> dict:
|
||||||
|
"""Mock result from TeamOrchestrator.execute()"""
|
||||||
|
return {
|
||||||
|
"status": "completed",
|
||||||
|
"result": {"content": "## 团队最终结果\n\n用户登录功能已实现"},
|
||||||
|
"phase_results": {
|
||||||
|
"phase-1": {"content": "规划完成", "phase_name": "规划"},
|
||||||
|
"phase-2": {"content": "前端实现完成", "phase_name": "前端"},
|
||||||
|
},
|
||||||
|
"plan": MagicMock(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── 路由匹配测试 ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestTeamCollabRouting:
|
||||||
|
"""_execute_team_collab 路由匹配测试"""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_team_prefix_triggers_collab(
|
||||||
|
self, websocket, session_manager, mock_orchestrator_result
|
||||||
|
):
|
||||||
|
"""@team 前缀触发团队协作,返回 True"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
with patch(
|
||||||
|
"agentkit.experts.team.ExpertTeam"
|
||||||
|
) as mock_team_cls, patch(
|
||||||
|
"agentkit.experts.orchestrator.TeamOrchestrator"
|
||||||
|
) as mock_orch_cls:
|
||||||
|
mock_team_cls.return_value = _make_mock_team()
|
||||||
|
mock_orch_cls.return_value = _make_mock_orchestrator(mock_orchestrator_result)
|
||||||
|
|
||||||
|
result = await _execute_team_collab(
|
||||||
|
websocket, session_id, "@team 开发用户登录功能", session_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_non_team_input_does_not_trigger(self, websocket, session_manager):
|
||||||
|
"""非 @team 输入不触发,返回 False"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
result = await _execute_team_collab(
|
||||||
|
websocket, session_id, "普通问题", session_manager
|
||||||
|
)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_board_prefix_does_not_trigger_team(
|
||||||
|
self, websocket, session_manager
|
||||||
|
):
|
||||||
|
"""@board 前缀不触发 @team 协作"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
result = await _execute_team_collab(
|
||||||
|
websocket, session_id, "@board 讨论主题", session_manager
|
||||||
|
)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_team_with_dev_team_template(
|
||||||
|
self, websocket, session_manager, mock_orchestrator_result
|
||||||
|
):
|
||||||
|
"""@team:dev_team 触发团队协作"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
with patch(
|
||||||
|
"agentkit.experts.team.ExpertTeam"
|
||||||
|
) as mock_team_cls, patch(
|
||||||
|
"agentkit.experts.orchestrator.TeamOrchestrator"
|
||||||
|
) as mock_orch_cls:
|
||||||
|
mock_team_cls.return_value = _make_mock_team()
|
||||||
|
mock_orch_cls.return_value = _make_mock_orchestrator(mock_orchestrator_result)
|
||||||
|
|
||||||
|
result = await _execute_team_collab(
|
||||||
|
websocket, session_id, "@team:dev_team 开发功能", session_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
# ── 错误处理测试 ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestTeamCollabErrorHandling:
|
||||||
|
"""_execute_team_collab 错误处理测试"""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_team_without_task_sends_error(
|
||||||
|
self, websocket, session_manager
|
||||||
|
):
|
||||||
|
"""@team 无任务内容时发送错误(通过 mock router 模拟空 task_content)"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
# Mock ExpertTeamRouter to return empty task_content
|
||||||
|
mock_routing_result = MagicMock()
|
||||||
|
mock_routing_result.matched = True
|
||||||
|
mock_routing_result.task_content = "" # 空 task_content
|
||||||
|
mock_routing_result.specified_experts = ["tech_lead"]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"agentkit.experts.router.ExpertTeamRouter"
|
||||||
|
) as mock_router_cls:
|
||||||
|
mock_router = MagicMock()
|
||||||
|
mock_router.resolve = MagicMock(return_value=mock_routing_result)
|
||||||
|
mock_router_cls.return_value = mock_router
|
||||||
|
|
||||||
|
result = await _execute_team_collab(
|
||||||
|
websocket, session_id, "@team", session_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
# 应该发送了错误消息
|
||||||
|
assert any(
|
||||||
|
msg.get("type") == "error" and "描述" in msg.get("data", {}).get("message", "")
|
||||||
|
for msg in websocket.sent
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_team_execution_failure_sends_error(
|
||||||
|
self, websocket, session_manager
|
||||||
|
):
|
||||||
|
"""团队执行失败时发送错误并清理"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
with patch(
|
||||||
|
"agentkit.experts.team.ExpertTeam"
|
||||||
|
) as mock_team_cls, patch(
|
||||||
|
"agentkit.experts.orchestrator.TeamOrchestrator"
|
||||||
|
) as mock_orch_cls:
|
||||||
|
mock_team = _make_mock_team()
|
||||||
|
mock_team_cls.return_value = mock_team
|
||||||
|
|
||||||
|
mock_orch_cls.return_value = _make_mock_orchestrator_result_failing()
|
||||||
|
|
||||||
|
result = await _execute_team_collab(
|
||||||
|
websocket, session_id, "@team 开发功能", session_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
# 应该发送了错误消息
|
||||||
|
assert any(
|
||||||
|
msg.get("type") == "error"
|
||||||
|
and "团队协作执行失败" in msg.get("data", {}).get("message", "")
|
||||||
|
for msg in websocket.sent
|
||||||
|
)
|
||||||
|
# 应该调用了 dissolve 清理
|
||||||
|
mock_team.dissolve.assert_called()
|
||||||
|
|
||||||
|
|
||||||
|
def _make_mock_orchestrator_result_failing() -> MagicMock:
|
||||||
|
"""创建 mock TeamOrchestrator that raises an exception"""
|
||||||
|
mock_orch = MagicMock()
|
||||||
|
mock_orch.execute = AsyncMock(side_effect=RuntimeError("LLM 不可用"))
|
||||||
|
return mock_orch
|
||||||
|
|
||||||
|
|
||||||
|
# ── 事件中继与持久化测试 ──────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestTeamCollabEventRelay:
|
||||||
|
"""_execute_team_collab 事件中继与持久化测试"""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_final_answer_sent_after_execution(
|
||||||
|
self, websocket, session_manager, mock_orchestrator_result
|
||||||
|
):
|
||||||
|
"""执行完成后发送 final_answer"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
with patch(
|
||||||
|
"agentkit.experts.team.ExpertTeam"
|
||||||
|
) as mock_team_cls, patch(
|
||||||
|
"agentkit.experts.orchestrator.TeamOrchestrator"
|
||||||
|
) as mock_orch_cls:
|
||||||
|
mock_team_cls.return_value = _make_mock_team()
|
||||||
|
mock_orch_cls.return_value = _make_mock_orchestrator(mock_orchestrator_result)
|
||||||
|
|
||||||
|
await _execute_team_collab(
|
||||||
|
websocket, session_id, "@team 开发登录功能", session_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
final_msgs = [msg for msg in websocket.sent if msg.get("type") == "final_answer"]
|
||||||
|
assert len(final_msgs) == 1
|
||||||
|
assert "团队最终结果" in final_msgs[0]["content"]
|
||||||
|
assert final_msgs[0]["is_final"] is True
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_user_message_persisted(
|
||||||
|
self, websocket, session_manager, mock_orchestrator_result
|
||||||
|
):
|
||||||
|
"""用户消息持久化到会话历史"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
with patch(
|
||||||
|
"agentkit.experts.team.ExpertTeam"
|
||||||
|
) as mock_team_cls, patch(
|
||||||
|
"agentkit.experts.orchestrator.TeamOrchestrator"
|
||||||
|
) as mock_orch_cls:
|
||||||
|
mock_team_cls.return_value = _make_mock_team()
|
||||||
|
mock_orch_cls.return_value = _make_mock_orchestrator(mock_orchestrator_result)
|
||||||
|
|
||||||
|
await _execute_team_collab(
|
||||||
|
websocket, session_id, "@team 开发登录功能", session_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = await session_manager.get_messages(session_id)
|
||||||
|
user_msgs = [m for m in messages if m.role == MessageRole.USER]
|
||||||
|
assert len(user_msgs) == 1
|
||||||
|
assert "@team 开发登录功能" in user_msgs[0].content
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_final_result_persisted(
|
||||||
|
self, websocket, session_manager, mock_orchestrator_result
|
||||||
|
):
|
||||||
|
"""最终结果持久化为 assistant 消息"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
with patch(
|
||||||
|
"agentkit.experts.team.ExpertTeam"
|
||||||
|
) as mock_team_cls, patch(
|
||||||
|
"agentkit.experts.orchestrator.TeamOrchestrator"
|
||||||
|
) as mock_orch_cls:
|
||||||
|
mock_team_cls.return_value = _make_mock_team()
|
||||||
|
mock_orch_cls.return_value = _make_mock_orchestrator(mock_orchestrator_result)
|
||||||
|
|
||||||
|
await _execute_team_collab(
|
||||||
|
websocket, session_id, "@team 开发登录功能", session_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = await session_manager.get_messages(session_id)
|
||||||
|
assistant_msgs = [m for m in messages if m.role == MessageRole.ASSISTANT]
|
||||||
|
assert len(assistant_msgs) == 1
|
||||||
|
assert "团队最终结果" in assistant_msgs[0].content
|
||||||
|
assert assistant_msgs[0].agent_name == "team_collab"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_team_dissolved_after_execution(
|
||||||
|
self, websocket, session_manager, mock_orchestrator_result
|
||||||
|
):
|
||||||
|
"""执行后 team.dissolve() 被调用"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
with patch(
|
||||||
|
"agentkit.experts.team.ExpertTeam"
|
||||||
|
) as mock_team_cls, patch(
|
||||||
|
"agentkit.experts.orchestrator.TeamOrchestrator"
|
||||||
|
) as mock_orch_cls:
|
||||||
|
mock_team = _make_mock_team()
|
||||||
|
mock_team_cls.return_value = mock_team
|
||||||
|
mock_orch_cls.return_value = _make_mock_orchestrator(mock_orchestrator_result)
|
||||||
|
|
||||||
|
await _execute_team_collab(
|
||||||
|
websocket, session_id, "@team 开发登录功能", session_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_team.dissolve.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_handoff_handler_registered(
|
||||||
|
self, websocket, session_manager, mock_orchestrator_result
|
||||||
|
):
|
||||||
|
"""handoff_transport handler 被注册用于事件中继"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
with patch(
|
||||||
|
"agentkit.experts.team.ExpertTeam"
|
||||||
|
) as mock_team_cls, patch(
|
||||||
|
"agentkit.experts.orchestrator.TeamOrchestrator"
|
||||||
|
) as mock_orch_cls:
|
||||||
|
mock_team = _make_mock_team()
|
||||||
|
mock_team.team_channel = "team:test-channel"
|
||||||
|
mock_team_cls.return_value = mock_team
|
||||||
|
mock_orch_cls.return_value = _make_mock_orchestrator(mock_orchestrator_result)
|
||||||
|
|
||||||
|
await _execute_team_collab(
|
||||||
|
websocket, session_id, "@team 开发登录功能", session_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_team.handoff_transport.register_handler.assert_called_once()
|
||||||
|
call_args = mock_team.handoff_transport.register_handler.call_args
|
||||||
|
assert call_args[0][0] == "team:test-channel"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_team_called_with_lead_and_members(
|
||||||
|
self, websocket, session_manager, mock_orchestrator_result
|
||||||
|
):
|
||||||
|
"""create_team 以 lead_config 和 member_configs 调用"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
with patch(
|
||||||
|
"agentkit.experts.team.ExpertTeam"
|
||||||
|
) as mock_team_cls, patch(
|
||||||
|
"agentkit.experts.orchestrator.TeamOrchestrator"
|
||||||
|
) as mock_orch_cls:
|
||||||
|
mock_team = _make_mock_team()
|
||||||
|
mock_team_cls.return_value = mock_team
|
||||||
|
mock_orch_cls.return_value = _make_mock_orchestrator(mock_orchestrator_result)
|
||||||
|
|
||||||
|
await _execute_team_collab(
|
||||||
|
websocket, session_id, "@team:dev_team 开发功能", session_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_team.create_team.assert_called_once()
|
||||||
|
call_kwargs = mock_team.create_team.call_args.kwargs
|
||||||
|
# lead_config 应该是第一个专家(tech_lead)
|
||||||
|
assert call_kwargs["lead_config"].name == "tech_lead"
|
||||||
|
# member_configs 应该包含其余专家
|
||||||
|
member_names = [c.name for c in call_kwargs["member_configs"]]
|
||||||
|
assert "frontend_engineer" in member_names
|
||||||
|
assert "backend_engineer" in member_names
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_empty_synthesis_falls_back_to_phase_results(
|
||||||
|
self, websocket, session_manager
|
||||||
|
):
|
||||||
|
"""synthesis 结果为空时 fallback 到 phase_results"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
empty_result = {
|
||||||
|
"status": "completed",
|
||||||
|
"result": {"content": ""}, # 空的 synthesis
|
||||||
|
"phase_results": {
|
||||||
|
"phase-1": {"content": "阶段1结果", "phase_name": "规划"},
|
||||||
|
"phase-2": {"content": "阶段2结果", "phase_name": "执行"},
|
||||||
|
},
|
||||||
|
"plan": MagicMock(),
|
||||||
|
}
|
||||||
|
with patch(
|
||||||
|
"agentkit.experts.team.ExpertTeam"
|
||||||
|
) as mock_team_cls, patch(
|
||||||
|
"agentkit.experts.orchestrator.TeamOrchestrator"
|
||||||
|
) as mock_orch_cls:
|
||||||
|
mock_team_cls.return_value = _make_mock_team()
|
||||||
|
mock_orch_cls.return_value = _make_mock_orchestrator(empty_result)
|
||||||
|
|
||||||
|
await _execute_team_collab(
|
||||||
|
websocket, session_id, "@team 开发功能", session_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
final_msgs = [msg for msg in websocket.sent if msg.get("type") == "final_answer"]
|
||||||
|
assert len(final_msgs) == 1
|
||||||
|
# 应该包含 phase_results 的内容
|
||||||
|
assert "阶段1结果" in final_msgs[0]["content"] or "阶段2结果" in final_msgs[0]["content"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_completely_empty_result(
|
||||||
|
self, websocket, session_manager
|
||||||
|
):
|
||||||
|
"""result 和 phase_results 都为空时发送默认消息"""
|
||||||
|
session_id = session_manager._test_session_id
|
||||||
|
empty_result = {
|
||||||
|
"status": "completed",
|
||||||
|
"result": {},
|
||||||
|
"phase_results": {},
|
||||||
|
"plan": MagicMock(),
|
||||||
|
}
|
||||||
|
with patch(
|
||||||
|
"agentkit.experts.team.ExpertTeam"
|
||||||
|
) as mock_team_cls, patch(
|
||||||
|
"agentkit.experts.orchestrator.TeamOrchestrator"
|
||||||
|
) as mock_orch_cls:
|
||||||
|
mock_team_cls.return_value = _make_mock_team()
|
||||||
|
mock_orch_cls.return_value = _make_mock_orchestrator(empty_result)
|
||||||
|
|
||||||
|
await _execute_team_collab(
|
||||||
|
websocket, session_id, "@team 开发功能", session_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
final_msgs = [msg for msg in websocket.sent if msg.get("type") == "final_answer"]
|
||||||
|
assert len(final_msgs) == 1
|
||||||
|
assert "未生成最终结果" in final_msgs[0]["content"]
|
||||||
Loading…
Reference in New Issue