fischer-agentkit/tests/integration/test_geo_e2e.py

559 lines
21 KiB
Python
Raw Permalink 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.

"""GEO Skill 工具绑定与端到端验证 — U4 集成测试
验证:
- SkillConfig with tools 字段加载正确
- ConfigDrivenAgent 从 ToolRegistry 注册声明的工具
- citation_detector 绑定 search + crawl 工具
- competitor_analyzer 绑定 search + crawl 工具
- geo_optimizer 绑定 schema_generate 工具
- schema_advisor 绑定 extract + generate 工具
- Tool 不在 ToolRegistry 中时优雅降级log warning, skip
- GEO Pipeline 配置加载正确
"""
import os
from unittest.mock import AsyncMock
import pytest
import yaml
from agentkit.core.config_driven import AgentConfig, ConfigDrivenAgent
from agentkit.skills.base import Skill, SkillConfig
from agentkit.skills.loader import SkillLoader
from agentkit.skills.registry import SkillRegistry
from agentkit.tools.baidu_search import BaiduSearchTool
from agentkit.tools.base import Tool
from agentkit.tools.function_tool import FunctionTool
from agentkit.tools.registry import ToolRegistry
from agentkit.tools.schema_tools import SchemaExtractTool, SchemaGenerateTool
from agentkit.tools.web_crawl import WebCrawlTool
# ── Fixtures ────────────────────────────────────────────────
CONFIGS_DIR = os.path.join(
os.path.dirname(__file__), "..", "..", "configs"
)
SKILLS_DIR = os.path.join(CONFIGS_DIR, "skills")
PIPELINES_DIR = os.path.join(CONFIGS_DIR, "pipelines")
@pytest.fixture
def tool_registry_with_infra_tools():
"""创建包含基础设施工具的 ToolRegistry"""
registry = ToolRegistry()
registry.register(BaiduSearchTool())
registry.register(WebCrawlTool())
registry.register(SchemaExtractTool())
registry.register(SchemaGenerateTool())
return registry
@pytest.fixture
def tool_registry_empty():
"""创建空的 ToolRegistry用于测试工具不可用时的降级"""
return ToolRegistry()
# ── Test: SkillConfig tools 字段加载 ────────────────────────
class TestSkillConfigToolsField:
"""验证 SkillConfig 的 tools 字段正确加载"""
def test_citation_detector_tools_loaded(self):
"""citation_detector YAML 加载后 tools 包含 baidu_search + web_crawl"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "citation_detector.yaml")
)
assert "baidu_search" in config.tools
assert "web_crawl" in config.tools
# 原有业务工具也保留
assert "execute_single_platform" in config.tools
assert "get_or_create_task" in config.tools
def test_competitor_analyzer_tools_loaded(self):
"""competitor_analyzer YAML 加载后 tools 包含 baidu_search + web_crawl"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "competitor_analyzer.yaml")
)
assert "baidu_search" in config.tools
assert "web_crawl" in config.tools
assert "competitor_analyze" in config.tools
def test_geo_optimizer_tools_loaded(self):
"""geo_optimizer YAML 加载后 tools 包含 schema_generate"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "geo_optimizer.yaml")
)
assert "schema_generate" in config.tools
def test_schema_advisor_tools_loaded(self):
"""schema_advisor YAML 加载后 tools 包含 schema_extract + schema_generate"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "schema_advisor.yaml")
)
assert "schema_extract" in config.tools
assert "schema_generate" in config.tools
assert "fill_schema_with_llm" in config.tools
def test_monitor_tools_loaded(self):
"""monitor YAML 加载后 tools 包含 baidu_search"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "monitor.yaml")
)
assert "baidu_search" in config.tools
assert "monitor_check_and_compare" in config.tools
def test_trend_agent_tools_loaded(self):
"""trend_agent YAML 加载后 tools 包含 baidu_search + web_crawl"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "trend_agent.yaml")
)
assert "baidu_search" in config.tools
assert "web_crawl" in config.tools
def test_content_generator_tools_loaded(self):
"""content_generator YAML 加载后 tools 包含 baidu_search"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "content_generator.yaml")
)
assert "baidu_search" in config.tools
assert "retrieve_knowledge" in config.tools
def test_deai_agent_tools_loaded(self):
"""deai_agent YAML 加载后 tools 包含 detect_ai_patterns"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "deai_agent.yaml")
)
assert "detect_ai_patterns" in config.tools
def test_all_skills_load_without_error(self):
"""所有 GEO Skill YAML 都能成功加载"""
yaml_files = [
"citation_detector.yaml",
"competitor_analyzer.yaml",
"geo_optimizer.yaml",
"monitor.yaml",
"schema_advisor.yaml",
"trend_agent.yaml",
"content_generator.yaml",
"deai_agent.yaml",
]
for filename in yaml_files:
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, filename)
)
assert config.name, f"{filename} should have a name"
assert config.tools is not None, f"{filename} should have tools field"
# ── Test: ConfigDrivenAgent 工具绑定 ────────────────────────
class TestConfigDrivenAgentToolBinding:
"""验证 ConfigDrivenAgent 从 ToolRegistry 注册声明的工具"""
def test_citation_detector_binds_search_and_crawl(
self, tool_registry_with_infra_tools
):
"""citation_detector 绑定 baidu_search + web_crawl 工具"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "citation_detector.yaml")
)
agent = ConfigDrivenAgent(
config=config,
tool_registry=tool_registry_with_infra_tools,
)
tool_names = [t.name for t in agent.get_tools()]
assert "baidu_search" in tool_names
assert "web_crawl" in tool_names
def test_competitor_analyzer_binds_search_and_crawl(
self, tool_registry_with_infra_tools
):
"""competitor_analyzer 绑定 baidu_search + web_crawl 工具"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "competitor_analyzer.yaml")
)
agent = ConfigDrivenAgent(
config=config,
tool_registry=tool_registry_with_infra_tools,
)
tool_names = [t.name for t in agent.get_tools()]
assert "baidu_search" in tool_names
assert "web_crawl" in tool_names
def test_geo_optimizer_binds_schema_generate(
self, tool_registry_with_infra_tools
):
"""geo_optimizer 绑定 schema_generate 工具"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "geo_optimizer.yaml")
)
agent = ConfigDrivenAgent(
config=config,
tool_registry=tool_registry_with_infra_tools,
)
tool_names = [t.name for t in agent.get_tools()]
assert "schema_generate" in tool_names
def test_schema_advisor_binds_extract_and_generate(
self, tool_registry_with_infra_tools
):
"""schema_advisor 绑定 schema_extract + schema_generate 工具"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "schema_advisor.yaml")
)
agent = ConfigDrivenAgent(
config=config,
tool_registry=tool_registry_with_infra_tools,
)
tool_names = [t.name for t in agent.get_tools()]
assert "schema_extract" in tool_names
assert "schema_generate" in tool_names
def test_monitor_binds_search(
self, tool_registry_with_infra_tools
):
"""monitor 绑定 baidu_search 工具"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "monitor.yaml")
)
agent = ConfigDrivenAgent(
config=config,
tool_registry=tool_registry_with_infra_tools,
)
tool_names = [t.name for t in agent.get_tools()]
assert "baidu_search" in tool_names
def test_trend_agent_binds_search_and_crawl(
self, tool_registry_with_infra_tools
):
"""trend_agent 绑定 baidu_search + web_crawl 工具"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "trend_agent.yaml")
)
agent = ConfigDrivenAgent(
config=config,
tool_registry=tool_registry_with_infra_tools,
)
tool_names = [t.name for t in agent.get_tools()]
assert "baidu_search" in tool_names
assert "web_crawl" in tool_names
# ── Test: 工具不可用时优雅降级 ──────────────────────────────
class TestToolNotFoundGracefulDegradation:
"""验证 Tool 不在 ToolRegistry 中时优雅降级"""
def test_missing_tool_does_not_crash(self, tool_registry_empty):
"""Tool 不在 ToolRegistry 中时 Agent 不会崩溃"""
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "citation_detector.yaml")
)
# citation_detector 声明了 baidu_search, web_crawl, execute_single_platform, get_or_create_task
# 这些工具都不在空 registry 中
agent = ConfigDrivenAgent(
config=config,
tool_registry=tool_registry_empty,
)
# Agent 应该成功创建,只是没有绑定任何工具
assert agent is not None
assert len(agent.get_tools()) == 0
def test_partial_tool_binding(self):
"""部分工具在 Registry 中时,只绑定可用的工具"""
registry = ToolRegistry()
registry.register(BaiduSearchTool())
# 只注册了 baidu_search没有 web_crawl
config = SkillConfig.from_yaml(
os.path.join(SKILLS_DIR, "citation_detector.yaml")
)
agent = ConfigDrivenAgent(
config=config,
tool_registry=registry,
)
tool_names = [t.name for t in agent.get_tools()]
# baidu_search 应该绑定成功
assert "baidu_search" in tool_names
# web_crawl 不在 registry 中,不应该绑定
assert "web_crawl" not in tool_names
# ── Test: SkillLoader 批量加载 ──────────────────────────────
class TestSkillLoaderBatchLoad:
"""验证 SkillLoader 从目录批量加载 Skill 并绑定工具"""
def test_load_all_geo_skills(self, tool_registry_with_infra_tools):
"""从 skills 目录加载所有 GEO Skill"""
skill_registry = SkillRegistry()
loader = SkillLoader(
skill_registry=skill_registry,
tool_registry=tool_registry_with_infra_tools,
)
skills = loader.load_from_directory(SKILLS_DIR)
# 验证所有 Skill 都加载成功
skill_names = [s.name for s in skills]
assert "citation_detector" in skill_names
assert "competitor_analyzer" in skill_names
assert "geo_optimizer" in skill_names
assert "monitor" in skill_names
assert "schema_advisor" in skill_names
assert "trend_agent" in skill_names
assert "content_generator" in skill_names
assert "deai_agent" in skill_names
def test_citation_detector_skill_has_tools(
self, tool_registry_with_infra_tools
):
"""citation_detector Skill 绑定了 search + crawl 工具"""
skill_registry = SkillRegistry()
loader = SkillLoader(
skill_registry=skill_registry,
tool_registry=tool_registry_with_infra_tools,
)
loader.load_from_directory(SKILLS_DIR)
skill = skill_registry.get("citation_detector")
tool_names = [t.name for t in skill.tools]
assert "baidu_search" in tool_names
assert "web_crawl" in tool_names
def test_schema_advisor_skill_has_tools(
self, tool_registry_with_infra_tools
):
"""schema_advisor Skill 绑定了 extract + generate 工具"""
skill_registry = SkillRegistry()
loader = SkillLoader(
skill_registry=skill_registry,
tool_registry=tool_registry_with_infra_tools,
)
loader.load_from_directory(SKILLS_DIR)
skill = skill_registry.get("schema_advisor")
tool_names = [t.name for t in skill.tools]
assert "schema_extract" in tool_names
assert "schema_generate" in tool_names
def test_geo_optimizer_skill_has_schema_generate(
self, tool_registry_with_infra_tools
):
"""geo_optimizer Skill 绑定了 schema_generate 工具"""
skill_registry = SkillRegistry()
loader = SkillLoader(
skill_registry=skill_registry,
tool_registry=tool_registry_with_infra_tools,
)
loader.load_from_directory(SKILLS_DIR)
skill = skill_registry.get("geo_optimizer")
tool_names = [t.name for t in skill.tools]
assert "schema_generate" in tool_names
# ── Test: GEO Pipeline 配置加载 ──────────────────────────────
class TestGEOPipelineConfig:
"""验证 GEO Pipeline 配置加载正确"""
def test_pipeline_config_loads(self):
"""geo_full_pipeline.yaml 能成功加载"""
with open(os.path.join(PIPELINES_DIR, "geo_full_pipeline.yaml"), "r") as f:
config = yaml.safe_load(f)
assert config["name"] == "geo_full_pipeline"
assert len(config["steps"]) > 0
def test_pipeline_has_all_steps(self):
"""Pipeline 包含所有 GEO 步骤"""
with open(os.path.join(PIPELINES_DIR, "geo_full_pipeline.yaml"), "r") as f:
config = yaml.safe_load(f)
step_names = [s["name"] for s in config["steps"]]
# 核心步骤
assert "detect" in step_names
assert "analyze_competitor" in step_names
assert "analyze_trend" in step_names
assert "optimize" in step_names
assert "schema" in step_names
assert "monitor" in step_names
# 新增步骤
assert "generate_content" in step_names
assert "deai" in step_names
def test_pipeline_step_skills_match_yaml_names(self):
"""Pipeline 步骤的 skill 字段与 YAML 文件中的 name 一致"""
with open(os.path.join(PIPELINES_DIR, "geo_full_pipeline.yaml"), "r") as f:
config = yaml.safe_load(f)
for step in config["steps"]:
skill_name = step["skill"]
yaml_path = os.path.join(SKILLS_DIR, f"{skill_name}.yaml")
assert os.path.exists(yaml_path), (
f"Pipeline step '{step['name']}' references skill "
f"'{skill_name}' but {yaml_path} does not exist"
)
def test_pipeline_dependency_graph_is_valid(self):
"""Pipeline 依赖关系有效(无循环依赖)"""
with open(os.path.join(PIPELINES_DIR, "geo_full_pipeline.yaml"), "r") as f:
config = yaml.safe_load(f)
step_map = {s["name"]: s.get("depends_on", []) for s in config["steps"]}
# 拓扑排序检测循环依赖
visited = set()
in_stack = set()
def dfs(name):
if name in in_stack:
return False # 循环依赖
if name in visited:
return True
in_stack.add(name)
for dep in step_map.get(name, []):
if not dfs(dep):
return False
in_stack.discard(name)
visited.add(name)
return True
for name in step_map:
assert dfs(name), f"Circular dependency detected involving '{name}'"
def test_pipeline_from_config_creates_pipeline(self):
"""GEOPipeline.from_config 能从 YAML 配置创建 Pipeline"""
from agentkit.skills.geo_pipeline import GEOPipeline
with open(os.path.join(PIPELINES_DIR, "geo_full_pipeline.yaml"), "r") as f:
config = yaml.safe_load(f)
pipeline = GEOPipeline.from_config(config)
assert pipeline.name == "geo_full_pipeline"
assert len(pipeline._steps) == len(config["steps"])
def test_pipeline_execution_order_respects_dependencies(self):
"""Pipeline 执行顺序尊重依赖关系"""
from agentkit.skills.geo_pipeline import GEOPipeline
with open(os.path.join(PIPELINES_DIR, "geo_full_pipeline.yaml"), "r") as f:
config = yaml.safe_load(f)
pipeline = GEOPipeline.from_config(config)
groups = pipeline._build_execution_groups()
# 展平执行顺序
executed = set()
for group in groups:
for step_name in group:
step = pipeline._step_map[step_name]
# 所有依赖必须已经执行
for dep in step.depends_on:
assert dep in executed, (
f"Step '{step_name}' depends on '{dep}' "
f"but '{dep}' hasn't been executed yet"
)
executed.add(step_name)
# 所有步骤都应该被执行
assert executed == set(pipeline._step_map.keys())
# ── Test: 基础设施工具实例化 ──────────────────────────────────
class TestInfrastructureToolsInstantiation:
"""验证基础设施工具能正确实例化"""
def test_baidu_search_tool_instantiation(self):
"""BaiduSearchTool 能正确实例化"""
tool = BaiduSearchTool()
assert tool.name == "baidu_search"
assert "search" in tool.tags
def test_web_crawl_tool_instantiation(self):
"""WebCrawlTool 能正确实例化"""
tool = WebCrawlTool()
assert tool.name == "web_crawl"
assert "crawl" in tool.tags
def test_schema_extract_tool_instantiation(self):
"""SchemaExtractTool 能正确实例化"""
tool = SchemaExtractTool()
assert tool.name == "schema_extract"
assert "extraction" in tool.tags
def test_schema_generate_tool_instantiation(self):
"""SchemaGenerateTool 能正确实例化"""
tool = SchemaGenerateTool()
assert tool.name == "schema_generate"
assert "generation" in tool.tags
def test_all_infra_tools_registered_in_registry(self):
"""所有基础设施工具都能注册到 ToolRegistry"""
registry = ToolRegistry()
registry.register(BaiduSearchTool())
registry.register(WebCrawlTool())
registry.register(SchemaExtractTool())
registry.register(SchemaGenerateTool())
assert registry.has_tool("baidu_search")
assert registry.has_tool("web_crawl")
assert registry.has_tool("schema_extract")
assert registry.has_tool("schema_generate")
# ── Test: AgentConfig tools 字段向后兼容 ──────────────────────
class TestAgentConfigToolsBackwardCompat:
"""验证 AgentConfig 的 tools 字段向后兼容"""
def test_agent_config_with_tools_list(self):
"""AgentConfig 接受 tools 列表"""
config = AgentConfig(
name="test",
agent_type="test",
task_mode="tool_call",
tools=["baidu_search", "web_crawl"],
)
assert config.tools == ["baidu_search", "web_crawl"]
def test_agent_config_without_tools(self):
"""AgentConfig 不提供 tools 时默认为空列表"""
config = AgentConfig(
name="test",
agent_type="test",
task_mode="llm_generate",
prompt={"identity": "test", "instructions": "test"},
)
assert config.tools == []
def test_skill_config_inherits_tools(self):
"""SkillConfig 继承 AgentConfig 的 tools 字段"""
config = SkillConfig(
name="test",
agent_type="test",
task_mode="tool_call",
tools=["baidu_search"],
)
assert config.tools == ["baidu_search"]
def test_skill_config_from_dict_with_tools(self):
"""SkillConfig.from_dict 正确解析 tools 字段"""
data = {
"name": "test",
"agent_type": "test",
"task_mode": "tool_call",
"tools": ["baidu_search", "web_crawl", "schema_generate"],
}
config = SkillConfig.from_dict(data)
assert config.tools == ["baidu_search", "web_crawl", "schema_generate"]