"""ExpertTeamRouter 单元测试""" from __future__ import annotations from agentkit.experts.config import ExpertConfig, ExpertTemplate from agentkit.experts.registry import ExpertTemplateRegistry from agentkit.experts.router import ( ExpertTeamRouter, ExpertTeamRoutingResult, TEAM_PREFIX_PATTERN, ) # ── 辅助函数 ────────────────────────────────────────────── def _make_template( name: str = "test_template", persona: str = "测试专家", bound_skills: list[str] | None = None, ) -> ExpertTemplate: """创建测试用 ExpertTemplate 实例""" config = ExpertConfig( name=name, agent_type="expert", persona=persona, thinking_style="analytical", bound_skills=bound_skills or [], task_mode="llm_generate", prompt={"identity": persona}, ) return ExpertTemplate( name=name, config=config, is_builtin=True, description=f"{name} 模板", ) def _make_registry_with_templates() -> ExpertTemplateRegistry: """创建包含预注册模板的注册中心""" registry = ExpertTemplateRegistry() registry.register(_make_template("analyst", persona="数据分析师", bound_skills=["data_query"])) registry.register(_make_template("strategist", persona="策略专家", bound_skills=["planning"])) registry.register(_make_template("reviewer", persona="代码审查员", bound_skills=["code_review"])) return registry # ── TEAM_PREFIX_PATTERN 正则测试 ────────────────────────── class TestTeamPrefixPattern: """TEAM_PREFIX_PATTERN 正则匹配测试""" def test_match_team_only(self): """@team 前缀匹配""" match = TEAM_PREFIX_PATTERN.match("@team 分析这个数据") assert match is not None assert match.group(1) is None assert match.group(2) == "分析这个数据" def test_match_team_with_experts(self): """@team:expert1,expert2 前缀匹配""" match = TEAM_PREFIX_PATTERN.match("@team:analyst,strategist 分析这个数据") assert match is not None assert match.group(1) == "analyst,strategist" assert match.group(2) == "分析这个数据" def test_match_team_single_expert(self): """@team:expert 前缀匹配""" match = TEAM_PREFIX_PATTERN.match("@team:analyst 分析数据") assert match is not None assert match.group(1) == "analyst" assert match.group(2) == "分析数据" def test_match_team_no_task(self): """@team 无后续任务内容""" match = TEAM_PREFIX_PATTERN.match("@team") assert match is not None assert match.group(1) is None assert match.group(2) == "" def test_no_match_without_prefix(self): """无 @team 前缀不匹配""" match = TEAM_PREFIX_PATTERN.match("分析这个数据") assert match is None def test_no_match_team_in_middle(self): """@team 在中间不匹配(必须开头)""" match = TEAM_PREFIX_PATTERN.match("请 @team 分析数据") assert match is None def test_match_team_with_leading_whitespace(self): """@team 前有空白字符时正则不匹配(由 resolve() 中 strip() 处理)""" match = TEAM_PREFIX_PATTERN.match(" @team:analyst 任务内容") # 正则使用 ^ 锚定,前导空白不匹配 assert match is None # ── ExpertTeamRoutingResult 默认值测试 ──────────────────── class TestExpertTeamRoutingResult: """ExpertTeamRoutingResult 数据类测试""" def test_default_values(self): """默认值验证""" result = ExpertTeamRoutingResult() assert result.matched is False assert result.team_mode is False assert result.specified_experts == [] assert result.task_content == "" assert result.auto_compose is False assert result.match_method == "" # ── ExpertTeamRouter.resolve 测试 ────────────────────────── class TestExpertTeamRouterResolve: """ExpertTeamRouter.resolve 方法测试""" def test_team_prefix_triggers_team_mode(self): """@team 前缀触发团队模式""" router = ExpertTeamRouter() result = router.resolve("@team 分析这个数据") assert result.matched is True assert result.team_mode is True assert result.match_method == "explicit_team" assert result.task_content == "分析这个数据" def test_team_with_experts_specifies_members(self): """@team:analyst,strategist 指定专家成员""" router = ExpertTeamRouter() result = router.resolve("@team:analyst,strategist 分析数据") assert result.matched is True assert result.team_mode is True assert result.specified_experts == ["analyst", "strategist"] assert result.auto_compose is False assert result.match_method == "explicit_team" def test_team_no_experts_auto_compose(self): """@team 无指定专家时 auto_compose=True""" router = ExpertTeamRouter() result = router.resolve("@team 分析数据") assert result.matched is True assert result.team_mode is True assert result.specified_experts == [] assert result.auto_compose is True def test_team_with_expert_extracts_task(self): """@team:analyst 正确提取任务内容""" router = ExpertTeamRouter() result = router.resolve("@team:analyst 请分析这份报告") assert result.matched is True assert result.specified_experts == ["analyst"] assert result.task_content == "请分析这份报告" def test_no_team_prefix_no_team_mode(self): """无 @team 前缀不触发团队模式""" router = ExpertTeamRouter() result = router.resolve("普通问题") assert result.matched is False assert result.team_mode is False assert result.task_content == "普通问题" assert result.match_method == "" def test_nonexistent_expert_still_included(self): """指定不存在的专家名仍包含在列表中""" router = ExpertTeamRouter() result = router.resolve("@team:analyst,nonexistent 任务") assert result.specified_experts == ["analyst", "nonexistent"] def test_team_with_empty_task_uses_full_content(self): """@team 无任务内容时 task_content 使用原始内容""" router = ExpertTeamRouter() result = router.resolve("@team") assert result.task_content == "@team" assert result.auto_compose is True def test_resolve_strips_leading_whitespace(self): """resolve() 对前导空白做 strip()""" router = ExpertTeamRouter() result = router.resolve(" @team:analyst 任务内容") assert result.matched is True assert result.team_mode is True assert result.specified_experts == ["analyst"] assert result.task_content == "任务内容" def test_invalid_expert_names_rejected(self): """无效专家名被过滤掉""" router = ExpertTeamRouter() # 包含特殊字符的名称应被过滤 result = router.resolve("@team:analyst,inva!id,bad/name 任务") # 仅保留合法名称 assert "analyst" in result.specified_experts assert "inva!id" not in result.specified_experts assert "bad/name" not in result.specified_experts def test_max_experts_limit(self): """指定超过 MAX_EXPERTS 数量的专家时截断""" router = ExpertTeamRouter() # 构造 15 个专家名 names = ",".join(f"expert{i}" for i in range(15)) result = router.resolve(f"@team:{names} 任务") from agentkit.experts.router import MAX_EXPERTS assert len(result.specified_experts) == MAX_EXPERTS # ── ExpertTeamRouter.resolve_expert_configs 测试 ─────────── class TestExpertTeamRouterResolveExpertConfigs: """ExpertTeamRouter.resolve_expert_configs 方法测试""" def test_resolve_existing_templates(self): """解析已注册模板返回对应 ExpertConfig""" registry = _make_registry_with_templates() router = ExpertTeamRouter(template_registry=registry) configs = router.resolve_expert_configs(["analyst", "strategist"]) assert len(configs) == 2 assert configs[0].name == "analyst" assert configs[0].persona == "数据分析师" assert configs[1].name == "strategist" assert configs[1].persona == "策略专家" def test_resolve_nonexistent_creates_dynamic_config(self): """解析不存在的名称创建动态 ExpertConfig""" registry = _make_registry_with_templates() router = ExpertTeamRouter(template_registry=registry) configs = router.resolve_expert_configs(["analyst", "unknown_expert"]) assert len(configs) == 2 assert configs[0].name == "analyst" assert configs[0].persona == "数据分析师" # 动态生成的配置 assert configs[1].name == "unknown_expert" assert configs[1].persona == "Expert in unknown_expert" assert configs[1].agent_type == "expert" assert configs[1].thinking_style == "analytical" assert configs[1].bound_skills == [] assert configs[1].is_lead is False assert configs[1].task_mode == "llm_generate" def test_resolve_all_nonexistent(self): """所有名称都不存在时全部动态生成""" router = ExpertTeamRouter() configs = router.resolve_expert_configs(["role_a", "role_b"]) assert len(configs) == 2 assert configs[0].name == "role_a" assert configs[0].persona == "Expert in role_a" assert configs[1].name == "role_b" assert configs[1].persona == "Expert in role_b" def test_resolve_empty_list(self): """空列表返回空结果""" router = ExpertTeamRouter() configs = router.resolve_expert_configs([]) assert configs == [] def test_resolve_preserves_template_skills(self): """解析已注册模板保留 bound_skills""" registry = _make_registry_with_templates() router = ExpertTeamRouter(template_registry=registry) configs = router.resolve_expert_configs(["analyst"]) assert configs[0].bound_skills == ["data_query"] def test_resolve_first_expert_is_lead(self): """第一个专家被指定为 lead""" registry = _make_registry_with_templates() router = ExpertTeamRouter(template_registry=registry) configs = router.resolve_expert_configs(["analyst", "strategist", "reviewer"]) assert configs[0].is_lead is True assert configs[1].is_lead is False assert configs[2].is_lead is False def test_resolve_skips_invalid_names(self): """跳过无效的专家名""" router = ExpertTeamRouter() # 包含无效字符的名称应被跳过 configs = router.resolve_expert_configs(["valid_name", "inva!id", "also_valid"]) assert len(configs) == 2 assert configs[0].name == "valid_name" assert configs[1].name == "also_valid" # ── ExpertTeamRouter 构造测试 ───────────────────────────── class TestExpertTeamRouterInit: """ExpertTeamRouter 构造函数测试""" def test_default_registry(self): """无参构造创建默认注册中心""" router = ExpertTeamRouter() assert router._registry is not None def test_custom_registry(self): """传入自定义注册中心""" registry = ExpertTemplateRegistry() router = ExpertTeamRouter(template_registry=registry) assert router._registry is registry # ── ExpertTeamRouter.can_handle 测试 ────────────────────── class TestExpertTeamRouterCanHandle: """ExpertTeamRouter.can_handle 方法测试""" def test_can_handle_with_matching_template_name(self): """模板名出现在内容中时返回 True""" registry = _make_registry_with_templates() router = ExpertTeamRouter(template_registry=registry) assert router.can_handle("请 analyst 帮我分析") is True def test_can_handle_with_matching_description(self): """模板描述词出现在内容中时返回 True""" registry = _make_registry_with_templates() router = ExpertTeamRouter(template_registry=registry) # 描述为 "analyst 模板" 等,"模板" 长度 > 2 应匹配 assert router.can_handle("我需要一个模板来参考") is True def test_can_handle_no_templates(self): """无注册模板时返回 False""" router = ExpertTeamRouter() # 默认注册中心可能为空 # 强制清空以测试 router._registry._templates.clear() assert router.can_handle("任何内容") is False def test_can_handle_with_templates_no_match(self): """有模板但内容不匹配时仍返回 True(auto-compose 可组建团队)""" registry = _make_registry_with_templates() router = ExpertTeamRouter(template_registry=registry) # 内容与任何模板名/描述都不匹配,但有模板存在 → auto-compose 可用 assert router.can_handle("完全无关的内容 xyz123") is True