- Move intent imports to TYPE_CHECKING block in graph.py (TC001) - Rename test classes to CapWords convention (N801) - Fix line length violations across test files (E501) - Auto-fix import sorting (I001)
178 lines
6.4 KiB
Python
178 lines
6.4 KiB
Python
"""Tests for app.intent module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from app.intent import (
|
|
AMBIGUITY_THRESHOLD,
|
|
ClassificationResult,
|
|
IntentTarget,
|
|
LLMIntentClassifier,
|
|
_build_agent_list,
|
|
)
|
|
from app.registry import AgentConfig
|
|
|
|
|
|
def _make_agent(name: str, desc: str = "test", perm: str = "read") -> AgentConfig:
|
|
return AgentConfig(
|
|
name=name, description=desc, permission=perm, tools=["fallback_respond"],
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestIntentModels:
|
|
def test_intent_target_frozen(self) -> None:
|
|
target = IntentTarget(agent_name="order_lookup", confidence=0.9, reasoning="order query")
|
|
with pytest.raises(Exception):
|
|
target.agent_name = "other" # type: ignore[misc]
|
|
|
|
def test_classification_result_frozen(self) -> None:
|
|
result = ClassificationResult(
|
|
intents=(IntentTarget(agent_name="a", confidence=0.9, reasoning="r"),),
|
|
)
|
|
assert not result.is_ambiguous
|
|
assert result.clarification_question is None
|
|
|
|
def test_classification_result_ambiguous(self) -> None:
|
|
result = ClassificationResult(
|
|
intents=(),
|
|
is_ambiguous=True,
|
|
clarification_question="What do you mean?",
|
|
)
|
|
assert result.is_ambiguous
|
|
|
|
def test_multi_intent(self) -> None:
|
|
result = ClassificationResult(
|
|
intents=(
|
|
IntentTarget(agent_name="order_actions", confidence=0.85, reasoning="cancel"),
|
|
IntentTarget(agent_name="discount", confidence=0.8, reasoning="discount"),
|
|
),
|
|
)
|
|
assert len(result.intents) == 2
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestBuildAgentList:
|
|
def test_formats_agents(self) -> None:
|
|
agents = (
|
|
_make_agent("order_lookup", "Looks up orders", "read"),
|
|
_make_agent("order_actions", "Modifies orders", "write"),
|
|
)
|
|
text = _build_agent_list(agents)
|
|
assert "order_lookup" in text
|
|
assert "order_actions" in text
|
|
assert "read" in text
|
|
assert "write" in text
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestLLMIntentClassifier:
|
|
@pytest.mark.asyncio
|
|
async def test_single_intent_classification(self) -> None:
|
|
expected = ClassificationResult(
|
|
intents=(IntentTarget(agent_name="order_lookup", confidence=0.95, reasoning="query"),),
|
|
)
|
|
mock_structured = MagicMock()
|
|
mock_structured.ainvoke = AsyncMock(return_value=expected)
|
|
mock_llm = MagicMock()
|
|
mock_llm.with_structured_output = MagicMock(return_value=mock_structured)
|
|
|
|
classifier = LLMIntentClassifier(mock_llm)
|
|
agents = (_make_agent("order_lookup"), _make_agent("fallback"))
|
|
|
|
result = await classifier.classify("What is order 1042 status?", agents)
|
|
assert len(result.intents) == 1
|
|
assert result.intents[0].agent_name == "order_lookup"
|
|
assert not result.is_ambiguous
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multi_intent_classification(self) -> None:
|
|
expected = ClassificationResult(
|
|
intents=(
|
|
IntentTarget(agent_name="order_actions", confidence=0.9, reasoning="cancel"),
|
|
IntentTarget(agent_name="discount", confidence=0.85, reasoning="discount"),
|
|
),
|
|
)
|
|
mock_structured = MagicMock()
|
|
mock_structured.ainvoke = AsyncMock(return_value=expected)
|
|
mock_llm = MagicMock()
|
|
mock_llm.with_structured_output = MagicMock(return_value=mock_structured)
|
|
|
|
classifier = LLMIntentClassifier(mock_llm)
|
|
agents = (_make_agent("order_actions"), _make_agent("discount"), _make_agent("fallback"))
|
|
|
|
result = await classifier.classify("Cancel order 1042 and give me 10% off", agents)
|
|
assert len(result.intents) == 2
|
|
assert not result.is_ambiguous
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ambiguous_classification(self) -> None:
|
|
expected = ClassificationResult(
|
|
intents=(IntentTarget(agent_name="fallback", confidence=0.3, reasoning="unclear"),),
|
|
is_ambiguous=False,
|
|
)
|
|
mock_structured = MagicMock()
|
|
mock_structured.ainvoke = AsyncMock(return_value=expected)
|
|
mock_llm = MagicMock()
|
|
mock_llm.with_structured_output = MagicMock(return_value=mock_structured)
|
|
|
|
classifier = LLMIntentClassifier(mock_llm)
|
|
agents = (_make_agent("order_lookup"), _make_agent("fallback"))
|
|
|
|
result = await classifier.classify("hmm", agents)
|
|
# Low confidence triggers ambiguity
|
|
assert result.is_ambiguous
|
|
assert result.clarification_question is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_llm_error_returns_ambiguous(self) -> None:
|
|
mock_structured = MagicMock()
|
|
mock_structured.ainvoke = AsyncMock(side_effect=RuntimeError("LLM error"))
|
|
mock_llm = MagicMock()
|
|
mock_llm.with_structured_output = MagicMock(return_value=mock_structured)
|
|
|
|
classifier = LLMIntentClassifier(mock_llm)
|
|
agents = (_make_agent("fallback"),)
|
|
|
|
result = await classifier.classify("test", agents)
|
|
assert result.is_ambiguous
|
|
assert result.clarification_question is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_result_type_returns_ambiguous(self) -> None:
|
|
mock_structured = MagicMock()
|
|
mock_structured.ainvoke = AsyncMock(return_value="not a ClassificationResult")
|
|
mock_llm = MagicMock()
|
|
mock_llm.with_structured_output = MagicMock(return_value=mock_structured)
|
|
|
|
classifier = LLMIntentClassifier(mock_llm)
|
|
agents = (_make_agent("fallback"),)
|
|
|
|
result = await classifier.classify("test", agents)
|
|
assert result.is_ambiguous
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_high_confidence_not_ambiguous(self) -> None:
|
|
expected = ClassificationResult(
|
|
intents=(
|
|
IntentTarget(
|
|
agent_name="order_lookup",
|
|
confidence=AMBIGUITY_THRESHOLD + 0.1,
|
|
reasoning="clear",
|
|
),
|
|
),
|
|
)
|
|
mock_structured = MagicMock()
|
|
mock_structured.ainvoke = AsyncMock(return_value=expected)
|
|
mock_llm = MagicMock()
|
|
mock_llm.with_structured_output = MagicMock(return_value=mock_structured)
|
|
|
|
classifier = LLMIntentClassifier(mock_llm)
|
|
agents = (_make_agent("order_lookup"),)
|
|
|
|
result = await classifier.classify("order status 1042", agents)
|
|
assert not result.is_ambiguous
|