fischer-agentkit/tests/unit/test_skill_config.py

347 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""SkillConfig 单元测试"""
import os
import tempfile
import pytest
import yaml
from agentkit.core.exceptions import ConfigValidationError
from agentkit.skills.base import IntentConfig, QualityGateConfig, SkillConfig, Skill
# ── IntentConfig 测试 ──────────────────────────────────────
class TestIntentConfig:
"""IntentConfig 数据类测试"""
def test_default_values(self):
intent = IntentConfig()
assert intent.keywords == []
assert intent.description == ""
assert intent.examples == []
def test_from_dict_with_all_fields(self):
data = {
"keywords": ["生成", "写作"],
"description": "内容生成意图",
"examples": ["帮我写一篇文章", "生成一段文案"],
}
intent = IntentConfig(**data)
assert intent.keywords == ["生成", "写作"]
assert intent.description == "内容生成意图"
assert intent.examples == ["帮我写一篇文章", "生成一段文案"]
def test_empty_keywords_is_valid(self):
intent = IntentConfig(keywords=[])
assert intent.keywords == []
# ── QualityGateConfig 测试 ─────────────────────────────────
class TestQualityGateConfig:
"""QualityGateConfig 数据类测试"""
def test_default_values(self):
gate = QualityGateConfig()
assert gate.required_fields == []
assert gate.min_word_count == 0
assert gate.max_retries == 0
assert gate.custom_validator is None
def test_from_dict_with_all_fields(self):
data = {
"required_fields": ["title", "body"],
"min_word_count": 100,
"max_retries": 3,
"custom_validator": "validators.check_quality",
}
gate = QualityGateConfig(**data)
assert gate.required_fields == ["title", "body"]
assert gate.min_word_count == 100
assert gate.max_retries == 3
assert gate.custom_validator == "validators.check_quality"
def test_max_retries_defaults_to_zero(self):
gate = QualityGateConfig()
assert gate.max_retries == 0
# ── SkillConfig 测试 ───────────────────────────────────────
class TestSkillConfig:
"""SkillConfig 继承 AgentConfig 并扩展 v2 字段"""
def test_from_dict_with_intent_and_quality_gate(self):
data = {
"name": "content_gen",
"agent_type": "content_generation",
"task_mode": "llm_generate",
"prompt": {"identity": "你是内容生成助手"},
"intent": {
"keywords": ["生成", "写作"],
"description": "内容生成意图",
"examples": ["帮我写文章"],
},
"quality_gate": {
"required_fields": ["title", "body"],
"min_word_count": 100,
"max_retries": 3,
},
"execution_mode": "react",
"max_steps": 10,
}
config = SkillConfig.from_dict(data)
assert config.name == "content_gen"
assert config.intent.keywords == ["生成", "写作"]
assert config.intent.description == "内容生成意图"
assert config.quality_gate.required_fields == ["title", "body"]
assert config.quality_gate.max_retries == 3
assert config.execution_mode == "react"
assert config.max_steps == 10
def test_from_old_agent_config_dict_auto_fills_defaults(self):
"""旧 AgentConfig 字典(无 intent/quality_gate应自动填充默认值"""
data = {
"name": "geo_writer",
"agent_type": "geo_writing",
"task_mode": "llm_generate",
"prompt": {"identity": "你是 GEO 写作助手"},
}
config = SkillConfig.from_dict(data)
assert config.name == "geo_writer"
assert isinstance(config.intent, IntentConfig)
assert config.intent.keywords == []
assert config.intent.description == ""
assert config.intent.examples == []
assert isinstance(config.quality_gate, QualityGateConfig)
assert config.quality_gate.required_fields == []
assert config.quality_gate.max_retries == 0
def test_execution_mode_defaults_to_react(self):
data = {
"name": "test_skill",
"agent_type": "test",
"task_mode": "llm_generate",
"prompt": {"identity": "test"},
}
config = SkillConfig.from_dict(data)
assert config.execution_mode == "react"
def test_max_steps_defaults_to_five(self):
data = {
"name": "test_skill",
"agent_type": "test",
"task_mode": "llm_generate",
"prompt": {"identity": "test"},
}
config = SkillConfig.from_dict(data)
assert config.max_steps == 5
def test_backward_compat_old_yaml_without_intent(self):
"""旧 YAML 无 intent 字段 → intent 默认为空 IntentConfig"""
yaml_content = yaml.dump({
"name": "legacy_skill",
"agent_type": "legacy",
"task_mode": "llm_generate",
"prompt": {"identity": "旧技能"},
})
with tempfile.NamedTemporaryFile(
mode="w", suffix=".yaml", delete=False, encoding="utf-8"
) as f:
f.write(yaml_content)
path = f.name
try:
config = SkillConfig.from_yaml(path)
assert config.name == "legacy_skill"
assert isinstance(config.intent, IntentConfig)
assert config.intent.keywords == []
assert isinstance(config.quality_gate, QualityGateConfig)
assert config.quality_gate.max_retries == 0
assert config.execution_mode == "react"
finally:
os.unlink(path)
def test_from_yaml_loads_correctly(self):
yaml_content = yaml.dump({
"name": "yaml_skill",
"agent_type": "yaml_type",
"task_mode": "llm_generate",
"prompt": {"identity": "YAML 技能"},
"intent": {"keywords": ["yaml"], "description": "YAML 加载测试"},
"quality_gate": {"required_fields": ["result"], "max_retries": 2},
"execution_mode": "direct",
"max_steps": 3,
})
with tempfile.NamedTemporaryFile(
mode="w", suffix=".yaml", delete=False, encoding="utf-8"
) as f:
f.write(yaml_content)
path = f.name
try:
config = SkillConfig.from_yaml(path)
assert config.name == "yaml_skill"
assert config.intent.keywords == ["yaml"]
assert config.quality_gate.max_retries == 2
assert config.execution_mode == "direct"
assert config.max_steps == 3
finally:
os.unlink(path)
def test_to_dict_includes_v2_fields(self):
data = {
"name": "dict_skill",
"agent_type": "dict_type",
"task_mode": "llm_generate",
"prompt": {"identity": "字典技能"},
"intent": {"keywords": ["dict"]},
"quality_gate": {"required_fields": ["output"]},
"execution_mode": "custom",
"max_steps": 7,
}
config = SkillConfig.from_dict(data)
result = config.to_dict()
assert "intent" in result
assert result["intent"]["keywords"] == ["dict"]
assert "quality_gate" in result
assert result["quality_gate"]["required_fields"] == ["output"]
assert result["execution_mode"] == "custom"
assert result["max_steps"] == 7
def test_to_dict_includes_v2_defaults_when_not_provided(self):
data = {
"name": "minimal_skill",
"agent_type": "minimal",
"task_mode": "llm_generate",
"prompt": {"identity": "最小技能"},
}
config = SkillConfig.from_dict(data)
result = config.to_dict()
assert "intent" in result
assert result["intent"]["keywords"] == []
assert "quality_gate" in result
assert result["quality_gate"]["max_retries"] == 0
assert result["execution_mode"] == "react"
assert result["max_steps"] == 5
def test_invalid_execution_mode_raises_config_validation_error(self):
data = {
"name": "bad_mode",
"agent_type": "bad",
"task_mode": "llm_generate",
"prompt": {"identity": "坏模式"},
"execution_mode": "invalid_mode",
}
with pytest.raises(ConfigValidationError):
SkillConfig.from_dict(data)
def test_direct_execution_mode(self):
data = {
"name": "direct_skill",
"agent_type": "direct",
"task_mode": "tool_call",
"tools": ["some_tool"],
"execution_mode": "direct",
}
config = SkillConfig.from_dict(data)
assert config.execution_mode == "direct"
def test_custom_execution_mode(self):
data = {
"name": "custom_skill",
"agent_type": "custom",
"task_mode": "custom",
"custom_handler": "handlers.custom",
"execution_mode": "custom",
}
config = SkillConfig.from_dict(data)
assert config.execution_mode == "custom"
# ── Skill 测试 ─────────────────────────────────────────────
class TestSkill:
"""Skill 类测试"""
def _make_config(self, name: str = "test_skill") -> SkillConfig:
return SkillConfig.from_dict({
"name": name,
"agent_type": "test",
"task_mode": "llm_generate",
"prompt": {"identity": "测试技能"},
})
def test_skill_name_property(self):
config = self._make_config("my_skill")
skill = Skill(config)
assert skill.name == "my_skill"
def test_skill_config_property(self):
config = self._make_config()
skill = Skill(config)
assert skill.config is config
def test_skill_tools_default_empty(self):
config = self._make_config()
skill = Skill(config)
assert skill.tools == []
def test_skill_bind_tool(self):
from agentkit.tools.base import Tool
class DummyTool(Tool):
async def execute(self, **kwargs):
return {}
config = self._make_config()
skill = Skill(config)
tool = DummyTool(name="t1", description="test tool")
skill.bind_tool(tool)
assert len(skill.tools) == 1
assert skill.tools[0].name == "t1"
def test_skill_unbind_tool(self):
from agentkit.tools.base import Tool
class DummyTool(Tool):
async def execute(self, **kwargs):
return {}
config = self._make_config()
skill = Skill(config)
tool = DummyTool(name="t1", description="test tool")
skill.bind_tool(tool)
skill.unbind_tool("t1")
assert skill.tools == []
def test_skill_unbind_nonexistent_tool_no_error(self):
config = self._make_config()
skill = Skill(config)
skill.unbind_tool("nonexistent") # 不应抛异常
assert skill.tools == []
def test_skill_to_dict(self):
config = self._make_config()
skill = Skill(config)
d = skill.to_dict()
assert "config" in d
assert d["config"]["name"] == "test_skill"
assert "tools" in d
assert d["tools"] == []
def test_skill_with_tools_in_constructor(self):
from agentkit.tools.base import Tool
class DummyTool(Tool):
async def execute(self, **kwargs):
return {}
config = self._make_config()
tool = DummyTool(name="t1", description="test tool")
skill = Skill(config, tools=[tool])
assert len(skill.tools) == 1