fischer-agentkit/tests/integration/test_router_engine_chain.py

316 lines
11 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.

"""集成测试 - CostAwareRouter → Engine → AlignmentGuard 全链路"""
from __future__ import annotations
import json
from unittest.mock import AsyncMock, MagicMock
import pytest
from agentkit.chat.skill_routing import CostAwareRouter, SkillRoutingResult
from agentkit.core.react import ReActEngine, ReActResult, ReActStep
from agentkit.llm.protocol import LLMResponse, TokenUsage
from agentkit.org.context import AgentProfile, OrganizationContext
from agentkit.quality.alignment import AlignmentConfig, AlignmentGuard
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_llm_response(content: str) -> LLMResponse:
return LLMResponse(
content=content,
model="test-model",
usage=TokenUsage(prompt_tokens=10, completion_tokens=20),
)
def _make_mock_gateway(responses: list[LLMResponse]) -> MagicMock:
gateway = MagicMock()
gateway.chat = AsyncMock(side_effect=responses)
return gateway
# ---------------------------------------------------------------------------
# Test 1: Router routes to ReAct engine, output passes alignment check
# ---------------------------------------------------------------------------
class TestRouterToEnginePassesAlignment:
"""路由到 ReAct 引擎,输出通过对齐检查"""
@pytest.mark.asyncio
async def test_react_output_passes_alignment(self):
# --- Setup: LLM returns low complexity → default agent (ReAct) ---
gateway = _make_mock_gateway([
_make_llm_response('{"complexity": 0.2}'), # quick_classify
_make_llm_response("你好!有什么可以帮你的?"), # ReAct final answer
])
org_context = OrganizationContext()
alignment_config = AlignmentConfig(
constraints=["password", "secret_key"],
)
guard = AlignmentGuard(config=alignment_config)
router = CostAwareRouter(llm_gateway=gateway, org_context=org_context)
mock_skill_registry = MagicMock()
mock_skill_registry.list_skills.return_value = []
mock_intent_router = AsyncMock()
# Step 1: Route
route_result = await router.route(
content="随便聊聊",
skill_registry=mock_skill_registry,
intent_router=mock_intent_router,
default_tools=[],
default_system_prompt="You are helpful",
default_model="default",
default_agent_name="default",
)
assert route_result.complexity < 0.3
assert route_result.agent_name == "default"
# Step 2: Inject constraints
input_data = {"content": route_result.clean_content}
injected = guard.inject_constraints(input_data)
assert "alignment_constraints" in injected
# Step 3: Simulate engine execution (use real ReActEngine with mock gateway)
react_engine = ReActEngine(llm_gateway=gateway)
engine_result = await react_engine.execute(
messages=[{"role": "user", "content": injected["content"]}],
)
# Step 4: Alignment check
output = {"result": engine_result.output}
check_result = await guard.check_output(output)
assert check_result.passed is True
assert check_result.violations == []
# ---------------------------------------------------------------------------
# Test 2: Router routes to ReAct engine, output fails alignment check
# ---------------------------------------------------------------------------
class TestRouterToEngineFailsAlignment:
"""路由到 ReAct 引擎,输出未通过对齐检查"""
@pytest.mark.asyncio
async def test_react_output_fails_alignment(self):
gateway = _make_mock_gateway([
_make_llm_response('{"complexity": 0.2}'),
_make_llm_response("Your password is 123456"),
])
org_context = OrganizationContext()
alignment_config = AlignmentConfig(
constraints=["password", "secret_key"],
)
guard = AlignmentGuard(config=alignment_config)
router = CostAwareRouter(llm_gateway=gateway, org_context=org_context)
mock_skill_registry = MagicMock()
mock_skill_registry.list_skills.return_value = []
mock_intent_router = AsyncMock()
route_result = await router.route(
content="随便聊聊",
skill_registry=mock_skill_registry,
intent_router=mock_intent_router,
default_tools=[],
default_system_prompt="You are helpful",
default_model="default",
default_agent_name="default",
)
input_data = {"content": route_result.clean_content}
injected = guard.inject_constraints(input_data)
react_engine = ReActEngine(llm_gateway=gateway)
engine_result = await react_engine.execute(
messages=[{"role": "user", "content": injected["content"]}],
)
output = {"result": engine_result.output}
check_result = await guard.check_output(output)
assert check_result.passed is False
assert len(check_result.violations) > 0
assert "password" in check_result.violations
# ---------------------------------------------------------------------------
# Test 3: Router routes based on complexity (low→default, high→org_context)
# ---------------------------------------------------------------------------
class TestRouterComplexityBasedRouting:
"""基于复杂度的路由低复杂度→默认高复杂度→org_context 能力匹配"""
@pytest.mark.asyncio
async def test_low_complexity_routes_to_default(self):
gateway = _make_mock_gateway([
_make_llm_response('{"complexity": 0.15}'),
])
org_context = OrganizationContext()
org_context.register_agent(AgentProfile(
name="analyst",
agent_type="react",
capabilities=["analysis"],
skills=["analysis"],
))
router = CostAwareRouter(llm_gateway=gateway, org_context=org_context)
mock_skill_registry = MagicMock()
mock_skill_registry.list_skills.return_value = []
mock_intent_router = AsyncMock()
result = await router.route(
content="简单问题",
skill_registry=mock_skill_registry,
intent_router=mock_intent_router,
default_tools=[],
default_system_prompt="You are helpful",
default_model="default",
default_agent_name="default",
)
assert result.complexity < 0.3
assert result.agent_name == "default"
assert result.match_method == "low_complexity"
@pytest.mark.asyncio
async def test_high_complexity_routes_via_org_context(self):
gateway = _make_mock_gateway([
_make_llm_response('{"complexity": 0.85}'),
])
org_context = OrganizationContext()
org_context.register_agent(AgentProfile(
name="analyst",
agent_type="react",
capabilities=["分析", "市场", "调研"],
skills=["market_analysis"],
current_load=0,
))
# find_best_agent returns real AgentProfile
org_context.find_best_agent = MagicMock(
return_value=org_context.get_agent_profile("analyst")
)
router = CostAwareRouter(llm_gateway=gateway, org_context=org_context)
mock_skill_registry = MagicMock()
mock_skill_registry.list_skills.return_value = []
mock_intent_router = AsyncMock()
result = await router.route(
content="请对市场趋势进行深度分析并给出投资建议",
skill_registry=mock_skill_registry,
intent_router=mock_intent_router,
default_tools=[],
default_system_prompt="You are helpful",
default_model="default",
default_agent_name="default",
)
assert result.complexity >= 0.7
assert result.match_method == "capability"
assert result.agent_name == "analyst"
assert result.matched is True
# ---------------------------------------------------------------------------
# Test 4: AlignmentGuard injects constraints into input before engine execution
# ---------------------------------------------------------------------------
class TestAlignmentGuardConstraintInjection:
"""AlignmentGuard 在引擎执行前将约束注入到输入中"""
@pytest.mark.asyncio
async def test_constraints_injected_before_engine_execution(self):
gateway = _make_mock_gateway([
_make_llm_response('{"complexity": 0.5}'),
_make_llm_response("Safe answer"),
])
alignment_config = AlignmentConfig(
constraints=["不得泄露用户隐私", "禁止生成有害内容"],
)
guard = AlignmentGuard(config=alignment_config)
org_context = OrganizationContext()
router = CostAwareRouter(llm_gateway=gateway, org_context=org_context)
mock_skill_registry = MagicMock()
mock_skill_registry.list_skills.return_value = []
mock_intent_router = AsyncMock()
# Step 1: Route
route_result = await router.route(
content="请帮我写一篇文章",
skill_registry=mock_skill_registry,
intent_router=mock_intent_router,
default_tools=[],
default_system_prompt="You are helpful",
default_model="default",
default_agent_name="default",
)
# Step 2: Inject constraints
input_data = {"content": route_result.clean_content}
injected = guard.inject_constraints(input_data)
# Verify constraints are present
assert "alignment_constraints" in injected
assert "不得泄露用户隐私" in injected["alignment_constraints"]
assert "禁止生成有害内容" in injected["alignment_constraints"]
# Original data preserved
assert injected["content"] == route_result.clean_content
# Original dict not mutated
assert "alignment_constraints" not in input_data
# Step 3: Engine executes with injected input
react_engine = ReActEngine(llm_gateway=gateway)
engine_result = await react_engine.execute(
messages=[{"role": "user", "content": injected["content"]}],
)
# Step 4: Output passes alignment
output = {"result": engine_result.output}
check_result = await guard.check_output(output)
assert check_result.passed is True
@pytest.mark.asyncio
async def test_constraint_injection_with_cascade_monitoring(self):
"""约束注入 + 级联故障监控的完整链路"""
alignment_config = AlignmentConfig(
constraints=["password"],
cascade_max_interactions=5,
)
guard = AlignmentGuard(config=alignment_config)
# Inject constraints
input_data = {"content": "请帮我重置密码"}
injected = guard.inject_constraints(input_data)
assert "alignment_constraints" in injected
# Simulate safe output
output = {"result": "密码重置链接已发送到您的邮箱。"}
check_result = await guard.check_output(output)
assert check_result.passed is True
# Record interactions — no cascade alert
alert = guard.record_interaction("session-chain-1")
assert alert is None
assert guard.get_interaction_count("session-chain-1") == 1