196 lines
7.4 KiB
Python
196 lines
7.4 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from app.schemas.debug import IntentCandidate, MatcherStageDebug
|
|
from app.schemas.intent import IntentDefinition
|
|
from app.services.intent_registry import IntentRegistry
|
|
from app.services.router import IntentMatchResult, MultiStageIntentMatcher
|
|
|
|
|
|
class _FakeMatcher:
|
|
def __init__(self, stage_debug: MatcherStageDebug) -> None:
|
|
self._stage_debug = stage_debug
|
|
|
|
def match(self, text: str) -> IntentMatchResult:
|
|
_ = text
|
|
return IntentMatchResult(intent=None, stage_debug=self._stage_debug)
|
|
|
|
|
|
def _intent(intent_id: str) -> IntentDefinition:
|
|
return IntentDefinition(
|
|
intent_id=intent_id,
|
|
plugin_id=f"mock.{intent_id}",
|
|
domain="test",
|
|
keywords=[],
|
|
examples=[],
|
|
)
|
|
|
|
|
|
class RouterDecisionTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.registry = IntentRegistry([_intent("alpha"), _intent("beta"), _intent("gamma")])
|
|
|
|
def test_execute_when_bert_classifier_is_clear(self) -> None:
|
|
matcher = MultiStageIntentMatcher(
|
|
registry=self.registry,
|
|
matchers=[
|
|
_FakeMatcher(
|
|
MatcherStageDebug(
|
|
stage="classifier",
|
|
accepted=True,
|
|
selected_intent="alpha",
|
|
score=0.92,
|
|
reason="classifier selected best candidate",
|
|
backend="joint-bert-local",
|
|
candidates=[
|
|
IntentCandidate(intent_id="alpha", score=0.92, reason="classifier", model_name="joint-bert-local"),
|
|
IntentCandidate(intent_id="beta", score=0.21, reason="classifier", model_name="joint-bert-local"),
|
|
],
|
|
)
|
|
),
|
|
],
|
|
)
|
|
|
|
result = matcher.match("alpha")
|
|
|
|
self.assertEqual(result.debug.decision, "execute")
|
|
self.assertEqual(result.intent.intent_id if result.intent else None, "alpha")
|
|
|
|
def test_clarify_when_bert_top_candidates_are_too_close(self) -> None:
|
|
matcher = MultiStageIntentMatcher(
|
|
registry=self.registry,
|
|
matchers=[
|
|
_FakeMatcher(
|
|
MatcherStageDebug(
|
|
stage="classifier",
|
|
accepted=True,
|
|
selected_intent="alpha",
|
|
score=0.22,
|
|
reason="classifier selected best candidate",
|
|
backend="bert-local",
|
|
metadata={"threshold": 0.2},
|
|
candidates=[
|
|
IntentCandidate(intent_id="alpha", score=0.31, reason="classifier", model_name="bert-local"),
|
|
IntentCandidate(intent_id="beta", score=0.28, reason="classifier", model_name="bert-local"),
|
|
],
|
|
)
|
|
),
|
|
],
|
|
route_to_cloud_threshold=0.2,
|
|
)
|
|
|
|
result = matcher.match("ambiguous request")
|
|
|
|
self.assertEqual(result.debug.decision, "clarify")
|
|
self.assertIsNone(result.intent)
|
|
self.assertEqual(result.debug.confidence_grade, "medium")
|
|
|
|
def test_route_to_cloud_when_bert_signal_is_weak_but_known(self) -> None:
|
|
matcher = MultiStageIntentMatcher(
|
|
registry=self.registry,
|
|
matchers=[
|
|
_FakeMatcher(
|
|
MatcherStageDebug(
|
|
stage="classifier",
|
|
accepted=False,
|
|
selected_intent="alpha",
|
|
score=0.29,
|
|
reason="classifier below execute threshold",
|
|
backend="joint-bert-local",
|
|
candidates=[
|
|
IntentCandidate(intent_id="alpha", score=0.29, reason="classifier", model_name="joint-bert-local"),
|
|
IntentCandidate(intent_id="beta", score=0.14, reason="classifier", model_name="joint-bert-local"),
|
|
],
|
|
)
|
|
),
|
|
],
|
|
)
|
|
|
|
result = matcher.match("weak symbolic request")
|
|
|
|
self.assertEqual(result.debug.decision, "route_to_cloud")
|
|
self.assertIsNone(result.intent)
|
|
|
|
def test_reject_when_no_branch_has_usable_signal(self) -> None:
|
|
matcher = MultiStageIntentMatcher(
|
|
registry=self.registry,
|
|
matchers=[
|
|
_FakeMatcher(
|
|
MatcherStageDebug(
|
|
stage="classifier",
|
|
accepted=False,
|
|
score=0.12,
|
|
reason="classifier below threshold",
|
|
backend="bert-local",
|
|
metadata={"threshold": 0.2},
|
|
candidates=[],
|
|
)
|
|
),
|
|
],
|
|
)
|
|
|
|
result = matcher.match("unknown request")
|
|
|
|
self.assertEqual(result.debug.decision, "reject")
|
|
self.assertTrue(result.debug.unknown_detected)
|
|
self.assertIsNone(result.intent)
|
|
|
|
def test_route_to_cloud_for_low_confidence_classifier_only_bert_signal(self) -> None:
|
|
matcher = MultiStageIntentMatcher(
|
|
registry=self.registry,
|
|
matchers=[
|
|
_FakeMatcher(
|
|
MatcherStageDebug(
|
|
stage="classifier",
|
|
accepted=True,
|
|
selected_intent="alpha",
|
|
score=0.31,
|
|
reason="classifier selected best candidate",
|
|
backend="bert-local",
|
|
metadata={"threshold": 0.0, "top_margin": 0.04},
|
|
candidates=[
|
|
IntentCandidate(intent_id="alpha", score=0.31, reason="classifier", model_name="bert-local"),
|
|
IntentCandidate(intent_id="beta", score=0.27, reason="classifier", model_name="bert-local"),
|
|
],
|
|
)
|
|
),
|
|
],
|
|
)
|
|
|
|
result = matcher.match("bert only weak request")
|
|
|
|
self.assertEqual(result.debug.decision, "route_to_cloud")
|
|
self.assertIsNone(result.intent)
|
|
|
|
def test_execute_for_high_confidence_classifier_only_bert_signal(self) -> None:
|
|
matcher = MultiStageIntentMatcher(
|
|
registry=self.registry,
|
|
matchers=[
|
|
_FakeMatcher(
|
|
MatcherStageDebug(
|
|
stage="classifier",
|
|
accepted=True,
|
|
selected_intent="alpha",
|
|
score=0.92,
|
|
reason="classifier selected best candidate",
|
|
backend="bert-local",
|
|
metadata={"threshold": 0.0, "top_margin": 0.63},
|
|
candidates=[
|
|
IntentCandidate(intent_id="alpha", score=0.92, reason="classifier", model_name="bert-local"),
|
|
IntentCandidate(intent_id="beta", score=0.29, reason="classifier", model_name="bert-local"),
|
|
],
|
|
)
|
|
),
|
|
],
|
|
)
|
|
|
|
result = matcher.match("bert only strong request")
|
|
|
|
self.assertEqual(result.debug.decision, "execute")
|
|
self.assertEqual(result.intent.intent_id if result.intent else None, "alpha")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|