334 lines
13 KiB
Python
334 lines
13 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_with_rounds_option(self):
|
|
"""@board:expert1,expert2 rounds=N 主题 → 解析最大轮次"""
|
|
router = BoardRouter(template_registry=_make_registry_with_experts())
|
|
result = router.resolve("@board:elon_musk,jeff_bezos rounds=3 SpaceX上市问题")
|
|
|
|
assert result.matched is True
|
|
assert result.max_rounds == 3
|
|
assert result.topic == "SpaceX上市问题"
|
|
assert result.specified_experts == ["elon_musk", "jeff_bezos"]
|
|
|
|
def test_resolve_rounds_option_clamped(self):
|
|
"""rounds 超出范围会被限制"""
|
|
router = BoardRouter(template_registry=_make_registry_with_experts())
|
|
result = router.resolve("@board rounds=999 讨论主题")
|
|
|
|
assert result.matched is True
|
|
assert result.max_rounds == 50
|
|
assert result.topic == "讨论主题"
|
|
|
|
def test_resolve_without_rounds_option(self):
|
|
"""未指定 rounds 时 max_rounds 为 None"""
|
|
router = BoardRouter(template_registry=_make_registry_with_experts())
|
|
result = router.resolve("@board:elon_musk 讨论主题")
|
|
|
|
assert result.matched is True
|
|
assert result.max_rounds is None
|
|
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 == ""
|
|
assert result.max_rounds is None
|
|
|
|
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",
|
|
max_rounds=7,
|
|
)
|
|
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"
|