fischer-agentkit/tests/unit/experts/test_board_router.py

302 lines
12 KiB
Python

"""BoardRouter 单元测试 — @board 前缀路由解析"""
from __future__ import annotations
from agentkit.experts.board_router import (
BOARD_PREFIX_PATTERN,
BoardRouter,
BoardRoutingResult,
MAX_EXPERTS,
)
from agentkit.experts.config import ExpertConfig, ExpertTemplate
from agentkit.experts.registry import ExpertTemplateRegistry
# ── 辅助函数 ──────────────────────────────────────────────
def _make_expert_template(
name: str = "test_expert",
persona: str = "测试专家",
speaking_style: str = "直接",
decision_framework: str = "分析",
) -> ExpertTemplate:
"""创建测试用 ExpertTemplate"""
config = ExpertConfig(
name=name,
agent_type="expert",
persona=persona,
thinking_style="analytical",
speaking_style=speaking_style,
decision_framework=decision_framework,
bound_skills=[],
task_mode="llm_generate",
prompt={"identity": persona},
)
return ExpertTemplate(
name=name,
config=config,
is_builtin=True,
description=f"{name} 测试模板",
)
def _make_registry_with_experts() -> ExpertTemplateRegistry:
"""创建包含预注册专家模板的注册中心"""
registry = ExpertTemplateRegistry()
registry.register(_make_expert_template("elon_musk", persona="Elon Musk"))
registry.register(_make_expert_template("jeff_bezos", persona="Jeff Bezos"))
registry.register(_make_expert_template("allenzhang", persona="张小龙"))
# 注册 private_board 模板(使用 bound_skills 存储成员列表)
board_config = ExpertConfig(
name="private_board",
agent_type="expert",
persona="私董会模板",
bound_skills=["elon_musk", "jeff_bezos", "allenzhang"],
task_mode="llm_generate",
prompt={"identity": "Private Board"},
)
registry.register(ExpertTemplate(
name="private_board",
config=board_config,
is_builtin=True,
description="默认私董会模板",
))
return registry
# ── BOARD_PREFIX_PATTERN 正则测试 ──────────────────────────
class TestBoardPrefixPattern:
"""BOARD_PREFIX_PATTERN 正则匹配测试"""
def test_match_board_only(self):
"""@board 前缀匹配(无专家指定)"""
match = BOARD_PREFIX_PATTERN.match("@board 讨论AI未来")
assert match is not None
assert match.group(1) is None
assert match.group(2) == "讨论AI未来"
def test_match_board_with_experts(self):
"""@board:expert1,expert2 格式匹配"""
match = BOARD_PREFIX_PATTERN.match("@board:elon_musk,jeff_bezos SpaceX上市")
assert match is not None
assert match.group(1) == "elon_musk,jeff_bezos"
assert match.group(2) == "SpaceX上市"
def test_match_board_with_template_name(self):
"""@board:private_board 显式使用模板"""
match = BOARD_PREFIX_PATTERN.match("@board:private_board 讨论主题")
assert match is not None
assert match.group(1) == "private_board"
assert match.group(2) == "讨论主题"
def test_no_match_regular_input(self):
"""普通输入不匹配"""
assert BOARD_PREFIX_PATTERN.match("你好,今天天气怎么样") is None
assert BOARD_PREFIX_PATTERN.match("@team 分析数据") is None
assert BOARD_PREFIX_PATTERN.match("@skill:search 搜索内容") is None
def test_match_board_with_multiline_topic(self):
"""多行主题匹配"""
match = BOARD_PREFIX_PATTERN.match("@board 第一行\n第二行")
assert match is not None
assert "第一行" in match.group(2)
assert "第二行" in match.group(2)
# ── BoardRouter.resolve 测试 ──────────────────────────────
class TestBoardRouterResolve:
"""BoardRouter.resolve 路由解析测试"""
def test_resolve_default_template(self):
"""@board 主题 → 使用默认模板"""
router = BoardRouter(template_registry=_make_registry_with_experts())
result = router.resolve("@board 如何看待AI对教育的影响")
assert result.matched is True
assert result.board_mode is True
assert result.topic == "如何看待AI对教育的影响"
assert result.use_default_template is True
assert result.match_method == "explicit_board"
assert "elon_musk" in result.specified_experts
assert "jeff_bezos" in result.specified_experts
assert "allenzhang" in result.specified_experts
def test_resolve_explicit_template(self):
"""@board:private_board 主题 → 显式使用默认模板"""
router = BoardRouter(template_registry=_make_registry_with_experts())
result = router.resolve("@board:private_board 讨论主题")
assert result.matched is True
assert result.use_default_template is True
assert result.topic == "讨论主题"
assert len(result.specified_experts) == 3
def test_resolve_specified_experts(self):
"""@board:expert1,expert2 主题 → 指定专家"""
router = BoardRouter(template_registry=_make_registry_with_experts())
result = router.resolve("@board:elon_musk,jeff_bezos SpaceX上市问题")
assert result.matched is True
assert result.use_default_template is False
assert result.specified_experts == ["elon_musk", "jeff_bezos"]
assert result.topic == "SpaceX上市问题"
def test_resolve_non_board_input(self):
"""普通输入不匹配"""
router = BoardRouter(template_registry=_make_registry_with_experts())
result = router.resolve("你好,今天天气怎么样")
assert result.matched is False
assert result.board_mode is False
assert result.topic == "你好,今天天气怎么样"
def test_resolve_empty_topic(self):
"""@board 无主题 → 空主题"""
router = BoardRouter(template_registry=_make_registry_with_experts())
result = router.resolve("@board")
assert result.matched is True
assert result.topic == ""
def test_resolve_invalid_expert_names_filtered(self):
"""无效专家名被过滤"""
router = BoardRouter(template_registry=_make_registry_with_experts())
result = router.resolve("@board:elon_musk,invalid@name,jeff_bezos 主题")
assert result.matched is True
assert "elon_musk" in result.specified_experts
assert "jeff_bezos" in result.specified_experts
assert "invalid@name" not in result.specified_experts
def test_resolve_max_experts_limit(self):
"""专家数量超过上限被截断"""
router = BoardRouter(template_registry=_make_registry_with_experts())
# 构造超过 MAX_EXPERTS 个专家名
names = ",".join(f"expert_{i}" for i in range(MAX_EXPERTS + 5))
result = router.resolve(f"@board:{names} 讨论主题")
assert result.matched is True
assert len(result.specified_experts) <= MAX_EXPERTS
def test_resolve_default_template_fallback(self):
"""无注册中心时使用硬编码回退默认成员"""
router = BoardRouter(template_registry=ExpertTemplateRegistry())
result = router.resolve("@board 讨论主题")
assert result.matched is True
assert result.use_default_template is True
# 回退到硬编码列表
assert len(result.specified_experts) > 0
assert "elon_musk" in result.specified_experts
# ── BoardRouter.resolve_expert_configs 测试 ────────────────
class TestBoardRouterResolveConfigs:
"""BoardRouter.resolve_expert_configs 配置解析测试"""
def test_resolve_configs_from_templates(self):
"""从注册模板解析专家配置"""
registry = _make_registry_with_experts()
router = BoardRouter(template_registry=registry)
configs = router.resolve_expert_configs(["elon_musk", "jeff_bezos"])
assert len(configs) == 2
assert configs[0].name == "elon_musk"
assert configs[0].is_lead is True # 第一个为主持人
assert configs[1].name == "jeff_bezos"
assert configs[1].is_lead is False
# 验证 board 模式字段
assert configs[0].speaking_style == "直接"
assert configs[0].decision_framework == "分析"
def test_resolve_configs_dynamic_generation(self):
"""未注册的专家名动态生成配置"""
router = BoardRouter(template_registry=ExpertTemplateRegistry())
configs = router.resolve_expert_configs(["unknown_expert"])
assert len(configs) == 1
assert configs[0].name == "unknown_expert"
assert configs[0].is_lead is True
def test_resolve_configs_first_is_moderator(self):
"""第一个专家自动设为主持人"""
registry = _make_registry_with_experts()
router = BoardRouter(template_registry=registry)
configs = router.resolve_expert_configs(["elon_musk", "jeff_bezos", "allenzhang"])
assert configs[0].is_lead is True
assert configs[1].is_lead is False
assert configs[2].is_lead is False
def test_resolve_configs_empty_list(self):
"""空列表返回空配置"""
router = BoardRouter(template_registry=_make_registry_with_experts())
configs = router.resolve_expert_configs([])
assert len(configs) == 0
def test_resolve_configs_invalid_name_skipped(self):
"""无效专家名被跳过"""
router = BoardRouter(template_registry=_make_registry_with_experts())
configs = router.resolve_expert_configs(["elon_musk", "invalid@name", "jeff_bezos"])
assert len(configs) == 2
assert configs[0].name == "elon_musk"
assert configs[1].name == "jeff_bezos"
def test_resolve_configs_ensure_at_least_one_lead(self):
"""确保至少有一个主持人"""
registry = _make_registry_with_experts()
# 修改模板使 is_lead 全为 False
for name in ["elon_musk", "jeff_bezos"]:
template = registry.get(name)
if template:
template.config.is_lead = False
router = BoardRouter(template_registry=registry)
configs = router.resolve_expert_configs(["elon_musk", "jeff_bezos"])
# 第一个应被强制设为 lead
assert configs[0].is_lead is True
# ── BoardRoutingResult 数据类测试 ──────────────────────────
class TestBoardRoutingResult:
"""BoardRoutingResult 数据类测试"""
def test_default_values(self):
"""默认值"""
result = BoardRoutingResult()
assert result.matched is False
assert result.board_mode is False
assert result.specified_experts == []
assert result.topic == ""
assert result.use_default_template is False
assert result.match_method == ""
def test_custom_values(self):
"""自定义值"""
result = BoardRoutingResult(
matched=True,
board_mode=True,
specified_experts=["a", "b"],
topic="测试主题",
use_default_template=True,
match_method="explicit_board",
)
assert result.matched is True
assert result.board_mode is True
assert result.specified_experts == ["a", "b"]
assert result.topic == "测试主题"
assert result.use_default_template is True
assert result.match_method == "explicit_board"