301 lines
11 KiB
Python
301 lines
11 KiB
Python
"""U8 GEO 适配层集成测试
|
||
|
||
验证 YAML 配置文件加载、ConfigDrivenAgent 创建、Custom Handler 路由等。
|
||
使用 agentkit 自带的 example_skill.yaml 和内联配置,不依赖 GEO 项目路径。
|
||
"""
|
||
|
||
import pytest
|
||
import yaml
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
from unittest.mock import patch
|
||
|
||
from agentkit.core.config_driven import AgentConfig, ConfigDrivenAgent
|
||
from agentkit.core.protocol import TaskMessage, TaskStatus
|
||
from agentkit.skills.base import SkillConfig
|
||
from agentkit.tools.function_tool import FunctionTool
|
||
from agentkit.tools.registry import ToolRegistry
|
||
|
||
|
||
# Use agentkit's own skills directory
|
||
SKILLS_DIR = Path(__file__).parent.parent.parent / "configs" / "skills"
|
||
|
||
|
||
def _make_llm_generate_config() -> dict:
|
||
"""Inline config for llm_generate mode agent."""
|
||
return {
|
||
"name": "test_llm_agent",
|
||
"agent_type": "test_llm",
|
||
"task_mode": "llm_generate",
|
||
"supported_tasks": ["test_llm_task"],
|
||
"prompt": {
|
||
"identity": "You are a test assistant.",
|
||
"instruction": "Respond to the user's request.",
|
||
},
|
||
}
|
||
|
||
|
||
def _make_tool_call_config() -> dict:
|
||
"""Inline config for tool_call mode agent."""
|
||
return {
|
||
"name": "test_tool_agent",
|
||
"agent_type": "test_tool",
|
||
"task_mode": "tool_call",
|
||
"supported_tasks": ["test_tool_task"],
|
||
"tools": ["mock_tool_a", "mock_tool_b"],
|
||
}
|
||
|
||
|
||
def _make_custom_config() -> dict:
|
||
"""Inline config for custom mode agent."""
|
||
return {
|
||
"name": "test_custom_agent",
|
||
"agent_type": "test_custom",
|
||
"task_mode": "custom",
|
||
"supported_tasks": ["test_custom_task"],
|
||
"custom_handler": "test.handlers.mock_handler",
|
||
}
|
||
|
||
|
||
class TestYAMLConfigLoading:
|
||
"""测试 YAML 配置文件加载(使用内联配置,不依赖 GEO)"""
|
||
|
||
def test_llm_generate_config_structure(self):
|
||
config = AgentConfig.from_dict(_make_llm_generate_config())
|
||
assert config.name == "test_llm_agent"
|
||
assert config.agent_type == "test_llm"
|
||
assert config.task_mode == "llm_generate"
|
||
assert len(config.supported_tasks) > 0
|
||
assert config.prompt is not None
|
||
|
||
def test_tool_call_config_structure(self):
|
||
config = AgentConfig.from_dict(_make_tool_call_config())
|
||
assert config.name == "test_tool_agent"
|
||
assert config.task_mode == "tool_call"
|
||
assert len(config.tools) == 2
|
||
|
||
def test_custom_config_structure(self):
|
||
config = AgentConfig.from_dict(_make_custom_config())
|
||
assert config.name == "test_custom_agent"
|
||
assert config.task_mode == "custom"
|
||
assert config.custom_handler == "test.handlers.mock_handler"
|
||
|
||
def test_llm_generate_agents_have_prompt(self):
|
||
config = AgentConfig.from_dict(_make_llm_generate_config())
|
||
assert config.prompt, "llm_generate mode requires prompt"
|
||
assert "identity" in config.prompt
|
||
|
||
def test_custom_agents_have_handler(self):
|
||
config = AgentConfig.from_dict(_make_custom_config())
|
||
assert config.custom_handler, "custom mode requires custom_handler"
|
||
|
||
def test_tool_call_agents_have_tools(self):
|
||
config = AgentConfig.from_dict(_make_tool_call_config())
|
||
assert config.tools, "tool_call mode requires tools list"
|
||
|
||
def test_example_skill_yaml_if_exists(self):
|
||
"""Test loading example_skill.yaml if it exists in configs/skills/."""
|
||
example_path = SKILLS_DIR / "example_skill.yaml"
|
||
if not example_path.exists():
|
||
pytest.skip("example_skill.yaml not found in configs/skills/")
|
||
config = SkillConfig.from_yaml(str(example_path))
|
||
assert config.name
|
||
assert config.agent_type
|
||
|
||
|
||
class TestConfigDrivenAgentCreation:
|
||
"""测试从配置创建 ConfigDrivenAgent"""
|
||
|
||
def test_create_llm_generate_agent(self):
|
||
config = AgentConfig.from_dict(_make_llm_generate_config())
|
||
tool_registry = ToolRegistry()
|
||
agent = ConfigDrivenAgent(config=config, tool_registry=tool_registry)
|
||
assert agent.name == "test_llm_agent"
|
||
assert agent.agent_type == "test_llm"
|
||
assert agent.prompt_template is not None
|
||
|
||
def test_create_tool_call_agent(self):
|
||
config = AgentConfig.from_dict(_make_tool_call_config())
|
||
tool_registry = ToolRegistry()
|
||
|
||
async def mock_func(**kwargs):
|
||
return {"result": "mock"}
|
||
|
||
tool_registry.register(
|
||
FunctionTool(name="mock_tool_a", description="mock", func=mock_func)
|
||
)
|
||
tool_registry.register(
|
||
FunctionTool(name="mock_tool_b", description="mock", func=mock_func)
|
||
)
|
||
|
||
agent = ConfigDrivenAgent(config=config, tool_registry=tool_registry)
|
||
assert agent.name == "test_tool_agent"
|
||
assert len(agent._tools) == 2
|
||
|
||
def test_create_custom_agent(self):
|
||
config = AgentConfig.from_dict(_make_custom_config())
|
||
|
||
async def mock_handler(task):
|
||
return {"mock": True}
|
||
|
||
custom_handlers = {
|
||
"test.handlers.mock_handler": mock_handler,
|
||
}
|
||
|
||
agent = ConfigDrivenAgent(config=config, custom_handlers=custom_handlers)
|
||
assert agent.name == "test_custom_agent"
|
||
|
||
def test_create_all_mode_agents(self):
|
||
"""验证三种模式的 Agent 都能成功创建"""
|
||
configs = [_make_llm_generate_config(), _make_tool_call_config(), _make_custom_config()]
|
||
|
||
for cfg_dict in configs:
|
||
config = AgentConfig.from_dict(cfg_dict)
|
||
tool_registry = ToolRegistry()
|
||
|
||
# 为 tool_call 模式注册 mock 工具
|
||
for tool_name in config.tools:
|
||
async def mock_func(**kwargs):
|
||
return {"mock": True}
|
||
tool_registry.register(
|
||
FunctionTool(name=tool_name, description=f"Mock {tool_name}", func=mock_func)
|
||
)
|
||
|
||
# 为 custom 模式提供 mock handler
|
||
custom_handlers = {}
|
||
if config.custom_handler:
|
||
async def mock_handler(task):
|
||
return {"mock": True}
|
||
custom_handlers[config.custom_handler] = mock_handler
|
||
|
||
agent = ConfigDrivenAgent(
|
||
config=config,
|
||
tool_registry=tool_registry,
|
||
custom_handlers=custom_handlers,
|
||
)
|
||
assert agent.name == config.name
|
||
|
||
|
||
class TestCustomHandlerRouting:
|
||
"""测试 Custom Handler 路由"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_custom_handler_routing(self):
|
||
config = AgentConfig.from_dict(_make_custom_config())
|
||
|
||
call_log = []
|
||
|
||
async def mock_handler(task):
|
||
call_log.append(task.task_type)
|
||
return {"mock": True, "task_type": task.task_type}
|
||
|
||
custom_handlers = {
|
||
"test.handlers.mock_handler": mock_handler,
|
||
}
|
||
|
||
agent = ConfigDrivenAgent(config=config, custom_handlers=custom_handlers)
|
||
task = TaskMessage(
|
||
task_id="test-1",
|
||
agent_name="test_custom_agent",
|
||
task_type="test_custom_task",
|
||
priority=0,
|
||
input_data={"query_id": "test-qid"},
|
||
callback_url=None,
|
||
created_at=datetime.now(timezone.utc),
|
||
)
|
||
result = await agent.execute(task)
|
||
assert result.status == TaskStatus.COMPLETED
|
||
assert "test_custom_task" in call_log
|
||
|
||
|
||
class TestSkillConfigV2:
|
||
"""测试 SkillConfig v2 字段"""
|
||
|
||
def test_skill_config_from_dict(self):
|
||
data = {
|
||
"name": "test_skill",
|
||
"agent_type": "test",
|
||
"task_mode": "llm_generate",
|
||
"supported_tasks": ["test"],
|
||
"prompt": {"identity": "test"},
|
||
"intent": {
|
||
"keywords": ["test", "demo"],
|
||
"description": "A test skill",
|
||
},
|
||
"quality_gate": {
|
||
"required_fields": ["output"],
|
||
"min_word_count": 10,
|
||
},
|
||
"execution_mode": "react",
|
||
"max_steps": 3,
|
||
"evolution": {
|
||
"enabled": True,
|
||
"reflect_on_failure": False,
|
||
},
|
||
}
|
||
config = SkillConfig.from_dict(data)
|
||
assert config.name == "test_skill"
|
||
assert config.intent.keywords == ["test", "demo"]
|
||
assert config.quality_gate.required_fields == ["output"]
|
||
assert config.execution_mode == "react"
|
||
assert config.max_steps == 3
|
||
assert config.evolution.enabled is True
|
||
|
||
def test_skill_config_backward_compatible(self):
|
||
"""旧 YAML 无 v2 字段时自动填充默认值"""
|
||
data = {
|
||
"name": "legacy_skill",
|
||
"agent_type": "legacy",
|
||
"task_mode": "llm_generate",
|
||
"supported_tasks": ["legacy"],
|
||
"prompt": {"identity": "Legacy skill"},
|
||
}
|
||
config = SkillConfig.from_dict(data)
|
||
assert config.name == "legacy_skill"
|
||
assert config.intent.keywords == []
|
||
assert config.quality_gate.required_fields == []
|
||
assert config.execution_mode == "react" # default
|
||
assert config.evolution.enabled is False # default
|
||
|
||
|
||
class TestToolRegistration:
|
||
"""测试 Tool 注册完整性"""
|
||
|
||
def test_all_referenced_tools_registered(self):
|
||
registry = ToolRegistry()
|
||
all_tool_names = {"mock_tool_a", "mock_tool_b", "mock_tool_c"}
|
||
|
||
for tool_name in all_tool_names:
|
||
async def mock_func(**kwargs):
|
||
return {"mock": True}
|
||
registry.register(
|
||
FunctionTool(name=tool_name, description=f"Mock {tool_name}", func=mock_func)
|
||
)
|
||
|
||
for tool_name in all_tool_names:
|
||
assert registry.has_tool(tool_name), f"Tool '{tool_name}' not registered"
|
||
|
||
|
||
class TestAdapterCompatibility:
|
||
"""测试适配层兼容性"""
|
||
|
||
def test_all_agent_names_unique(self):
|
||
configs = [_make_llm_generate_config(), _make_tool_call_config(), _make_custom_config()]
|
||
names = [AgentConfig.from_dict(c).name for c in configs]
|
||
assert len(names) == len(set(names)), f"Duplicate agent names: {names}"
|
||
|
||
def test_all_agent_types_unique(self):
|
||
configs = [_make_llm_generate_config(), _make_tool_call_config(), _make_custom_config()]
|
||
types = [AgentConfig.from_dict(c).agent_type for c in configs]
|
||
assert len(types) == len(set(types)), f"Duplicate agent types: {types}"
|
||
|
||
def test_supported_tasks_no_overlap(self):
|
||
configs = [_make_llm_generate_config(), _make_tool_call_config(), _make_custom_config()]
|
||
all_tasks = {}
|
||
for cfg_dict in configs:
|
||
config = AgentConfig.from_dict(cfg_dict)
|
||
for task in config.supported_tasks:
|
||
if task in all_tasks:
|
||
assert False, f"Task '{task}' defined in both '{all_tasks[task]}' and '{config.name}'"
|
||
all_tasks[task] = config.name
|