Files
cluster-knaster/core/tests/test_router.py
T

134 lines
4.8 KiB
Python

import pytest
from knaster.problems import Signal
from knaster.router import Router, RouterConfig
RECHNUNG = "Rechnung auslesen: Rechnungsnummer und Betrag extrahieren. Rechnung 4711 über 99 EUR"
@pytest.fixture
def router(registry, library, embedder):
return Router(registry, library, embedder)
def test_routes_to_weak_slot_with_adapter(router):
d = router.route(RECHNUNG)
assert d.status == "routed"
assert (d.stage, d.slot, d.skill) == (1, "tier1-general", "rechnung-extrahieren")
assert d.model == "tier1-general/rechnungen-v1" and d.adapter == "rechnungen-v1"
assert d.prompt_variant == "full"
def test_unknown_request_goes_to_strong(router):
d = router.route("Wie ist das Wetter morgen in Berlin?")
assert (d.stage, d.slot, d.skill) == (0, "tier0-strong", None)
assert Signal.NO_SKILL in d.signals
def test_draft_skill_not_routed(router):
d = router.route("Gedicht über Rechnung schreiben")
assert d.skill != "entwurf"
def test_demoted_skill_runs_on_strong_with_hint(router):
d = router.route("Vertrag prüfen auf Kündigungsfrist")
assert (d.stage, d.skill, d.prompt_variant) == (0, "vertrag-pruefen", "full")
def test_classifier_and_program_stages(router):
assert router.route("Ticket: Drucker druckt nicht").stage == 3
d = router.route("Text in Großbuchstaben: hallo welt")
assert (d.stage, d.slot) == (2, None)
def test_unavailable_slot_uses_fallback(router, registry):
registry.set_status("tier1-general", "degraded")
d = router.route(RECHNUNG)
assert d.slot == "tier1-flex"
assert Signal.SLOT_UNAVAILABLE in d.signals
def test_all_weak_slots_unavailable_goes_strong(router, registry):
registry.set_status("tier1-general", "unavailable")
registry.set_status("tier1-flex", "unavailable")
d = router.route(RECHNUNG)
assert d.stage == 0 and d.skill == "rechnung-extrahieren"
def test_long_input_uses_compact_then_larger_slot(router):
# Eingabe ~1585 Tokens: mit Beispielen (64) zu groß für 2000, kompakt (38) passt es
filler = "x" * (3 * 1585 - len(RECHNUNG) - 1)
d = router.route(RECHNUNG + " " + filler)
assert d.slot == "tier1-general" and d.prompt_variant == "compact"
assert Signal.COMPACT_PROMPT in d.signals
d = router.route(RECHNUNG + " " + "x" * (3 * 5000))
assert d.slot == "tier1-flex"
assert Signal.LARGER_SLOT in d.signals
def test_too_large_for_everything(router):
d = router.route(RECHNUNG + " " + "x" * (3 * 120000))
assert d.status == "too_large"
assert Signal.NEEDS_SPLIT in d.signals
def test_context_exceeded_goes_to_strong(router):
d = router.route(RECHNUNG + " " + "x" * (3 * 30000))
assert d.stage == 0 and Signal.CONTEXT_EXCEEDED in d.signals
def test_hint_skill(router):
d = router.route("irgendwas", hint_skill="rechnung-extrahieren")
assert d.skill == "rechnung-extrahieren" and d.similarity is None
with pytest.raises(KeyError):
router.route("x", hint_skill="entwurf")
def test_escalation_quality_goes_to_strong(router):
d = router.route(RECHNUNG)
e = router.escalate(d, Signal.VERIFIER_FAILED, RECHNUNG)
assert (e.stage, e.slot, e.attempt) == (0, "tier0-strong", 1)
f = router.escalate(e, Signal.VERIFIER_FAILED, RECHNUNG)
assert f.status == "failed"
def test_escalation_infra_uses_fallback_slot(router):
d = router.route(RECHNUNG)
e = router.escalate(d, Signal.ENDPOINT_ERROR, RECHNUNG)
assert (e.stage, e.slot) == (1, "tier1-flex")
f = router.escalate(e, Signal.ENDPOINT_ERROR, RECHNUNG)
assert f.stage == 0
def test_escalation_from_program_to_weak(router):
d = router.route("Text in Großbuchstaben: hallo welt")
e = router.escalate(d, Signal.PROGRAM_ERROR, "Text in Großbuchstaben: hallo welt")
assert e.stage == 1
def test_max_escalations(registry, library, embedder):
r = Router(registry, library, embedder, config=RouterConfig(max_escalations=1))
d = r.route(RECHNUNG)
e = r.escalate(d, Signal.ENDPOINT_ERROR, RECHNUNG)
assert r.escalate(e, Signal.ENDPOINT_ERROR, RECHNUNG).status == "failed"
class PreferRechnung:
def rerank(self, query, documents):
return [1.0 if "Rechnung" in d else 0.0 for d in documents]
def test_conflict_without_reranker_goes_strong(registry, library, embedder):
r = Router(registry, library, embedder, config=RouterConfig(conflict_margin=1.0))
d = r.route(RECHNUNG)
assert d.stage == 0 and Signal.SKILL_CONFLICT in d.signals
def test_conflict_with_reranker_trusts_reranker(registry, library, embedder):
r = Router(registry, library, embedder, reranker=PreferRechnung(), config=RouterConfig(conflict_margin=1.0))
d = r.route("Vertrag prüfen Kündigungsfrist. Rechnung auslesen Rechnungsnummer Betrag")
assert Signal.SKILL_CONFLICT in d.signals
assert "Reranking aktiv" in d.reasons
assert d.skill == "rechnung-extrahieren" and d.stage == 1