| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583 |
- # -*- coding: utf-8 -*-
- """
- Tests fuer die Intent-Dekoratoren (@intent, @pattern, @example).
- Testet Stackbarkeit, Slot-Erkennung aus Funktionssignatur,
- Zusammenspiel mit @intent und discover_intent_handlers.
- """
- import asyncio
- import inspect
- import pytest
- from typing import Any
- from trixy_core.nlp.decorators import (
- intent,
- pattern,
- example,
- get_intent_metadata,
- is_intent_handler,
- discover_intent_handlers,
- _extract_slots_from_signature,
- INTENT_METADATA_ATTR,
- _PATTERN_LIST_ATTR,
- _EXAMPLE_LIST_ATTR,
- )
- from trixy_core.nlp.intent_registry import IntentSlot
- # ============================================================================
- # @pattern Dekorator
- # ============================================================================
- class TestPatternDecorator:
- """Tests fuer den @pattern Dekorator."""
- def test_einzelnes_pattern(self):
- """Ein einzelnes Pattern sollte gespeichert werden."""
- @pattern("test muster")
- async def handler(data):
- pass
- patterns = getattr(handler, _PATTERN_LIST_ATTR, [])
- assert len(patterns) == 1
- assert patterns[0] == "test muster"
- def test_mehrere_patterns_stackbar(self):
- """Mehrere @pattern Dekoratoren sollten gestackt werden."""
- @pattern("(wie ist|wie wird) wetter")
- @pattern("wetter [in] {city}")
- @pattern("temperatur [draussen]")
- async def handler(data):
- pass
- patterns = getattr(handler, _PATTERN_LIST_ATTR, [])
- assert len(patterns) == 3
- def test_pattern_behaelt_funktion(self):
- """@pattern sollte die Funktion nicht veraendern."""
- @pattern("test")
- async def handler(data):
- return "result"
- assert asyncio.iscoroutinefunction(handler)
- def test_pattern_kein_intent_handler(self):
- """@pattern allein macht keine Funktion zum Intent-Handler."""
- @pattern("test")
- async def handler(data):
- pass
- assert not is_intent_handler(handler)
- # ============================================================================
- # @example Dekorator
- # ============================================================================
- class TestExampleDecorator:
- """Tests fuer den @example Dekorator."""
- def test_einzelnes_example(self):
- """Ein einzelnes Example sollte gespeichert werden."""
- @example("Wie ist das Wetter?")
- async def handler(data):
- pass
- examples = getattr(handler, _EXAMPLE_LIST_ATTR, [])
- assert len(examples) == 1
- assert examples[0] == "Wie ist das Wetter?"
- def test_mehrere_examples_in_einem_aufruf(self):
- """Mehrere Examples in einem @example sollten gespeichert werden."""
- @example("Hallo", "Hi", "Guten Tag")
- async def handler(data):
- pass
- examples = getattr(handler, _EXAMPLE_LIST_ATTR, [])
- assert len(examples) == 3
- def test_mehrere_example_dekoratoren_stackbar(self):
- """Mehrere @example Dekoratoren sollten gestackt werden."""
- @example("Wie ist das Wetter?")
- @example("Wie warm ist es?")
- async def handler(data):
- pass
- examples = getattr(handler, _EXAMPLE_LIST_ATTR, [])
- assert len(examples) == 2
- def test_example_behaelt_funktion(self):
- """@example sollte die Funktion nicht veraendern."""
- @example("Test")
- async def handler(data):
- return "result"
- assert asyncio.iscoroutinefunction(handler)
- def test_example_kein_intent_handler(self):
- """@example allein macht keine Funktion zum Intent-Handler."""
- @example("Test")
- async def handler(data):
- pass
- assert not is_intent_handler(handler)
- # ============================================================================
- # @intent Dekorator
- # ============================================================================
- class TestIntentDecorator:
- """Tests fuer den @intent Dekorator."""
- def test_grundlegend(self):
- """@intent sollte Metadaten hinzufuegen."""
- @intent("test_intent")
- async def handler(data):
- pass
- assert is_intent_handler(handler)
- metadata = get_intent_metadata(handler)
- assert metadata is not None
- assert metadata["name"] == "test_intent"
- def test_mit_description(self):
- """@intent sollte Description speichern."""
- @intent("test", description="Testbeschreibung")
- async def handler(data):
- pass
- metadata = get_intent_metadata(handler)
- assert metadata["description"] == "Testbeschreibung"
- def test_mit_priority(self):
- """@intent sollte Priority speichern."""
- @intent("test", priority=5)
- async def handler(data):
- pass
- metadata = get_intent_metadata(handler)
- assert metadata["priority"] == 5
- def test_mit_expliziten_examples(self):
- """@intent sollte explizite Examples speichern."""
- @intent("test", examples=["Hallo", "Hi"])
- async def handler(data):
- pass
- metadata = get_intent_metadata(handler)
- assert "Hallo" in metadata["examples"]
- assert "Hi" in metadata["examples"]
- def test_mit_expliziten_patterns(self):
- """@intent sollte explizite Patterns speichern."""
- @intent("test", patterns=["muster eins", "muster zwei"])
- async def handler(data):
- pass
- metadata = get_intent_metadata(handler)
- assert "muster eins" in metadata["patterns"]
- assert "muster zwei" in metadata["patterns"]
- def test_mit_expliziten_slots_type(self):
- """@intent sollte Typ-basierte Slots normalisieren."""
- @intent("test", slots={"device": str, "count": int})
- async def handler(data):
- pass
- metadata = get_intent_metadata(handler)
- assert "device" in metadata["slots"]
- assert isinstance(metadata["slots"]["device"], IntentSlot)
- assert metadata["slots"]["device"].slot_type is str
- assert metadata["slots"]["count"].slot_type is int
- def test_mit_expliziten_slots_intentslot(self):
- """@intent sollte IntentSlot-Objekte direkt akzeptieren."""
- @intent("test", slots={
- "temp": IntentSlot(name="temp", slot_type=float, required=True),
- })
- async def handler(data):
- pass
- metadata = get_intent_metadata(handler)
- assert metadata["slots"]["temp"].slot_type is float
- assert metadata["slots"]["temp"].required is True
- def test_wrapper_ist_async(self):
- """Der Wrapper sollte async sein."""
- @intent("test")
- async def handler(data):
- return "result"
- assert asyncio.iscoroutinefunction(handler)
- # ============================================================================
- # Zusammenspiel @intent + @pattern + @example
- # ============================================================================
- class TestDekoratorZusammenspiel:
- """Tests fuer das Zusammenspiel der Dekoratoren."""
- def test_intent_sammelt_pattern(self):
- """@intent sollte @pattern-Dekoratoren sammeln."""
- @intent("weather")
- @pattern("(wie ist|wie wird) wetter")
- @pattern("wetter [in] {city}")
- async def handler(data):
- pass
- metadata = get_intent_metadata(handler)
- assert len(metadata["patterns"]) == 2
- assert "(wie ist|wie wird) wetter" in metadata["patterns"]
- assert "wetter [in] {city}" in metadata["patterns"]
- def test_intent_sammelt_example(self):
- """@intent sollte @example-Dekoratoren sammeln."""
- @intent("weather")
- @example("Wie ist das Wetter?")
- @example("Wetter in Berlin", "Wie warm ist es?")
- async def handler(data):
- pass
- metadata = get_intent_metadata(handler)
- assert len(metadata["examples"]) == 3
- def test_intent_merged_explizite_und_dekorator_patterns(self):
- """Explizite Patterns und @pattern-Dekoratoren sollten gemergt werden."""
- @intent("test", patterns=["explizit"])
- @pattern("dekorator")
- async def handler(data):
- pass
- metadata = get_intent_metadata(handler)
- assert "explizit" in metadata["patterns"]
- assert "dekorator" in metadata["patterns"]
- def test_intent_merged_explizite_und_dekorator_examples(self):
- """Explizite Examples und @example-Dekoratoren sollten gemergt werden."""
- @intent("test", examples=["Explizit"])
- @example("Dekorator")
- async def handler(data):
- pass
- metadata = get_intent_metadata(handler)
- assert "Explizit" in metadata["examples"]
- assert "Dekorator" in metadata["examples"]
- def test_vollstaendiges_setup(self):
- """Komplettes Setup mit allen Dekoratoren."""
- @intent("current_weather", description="Wetter abfragen")
- @pattern("(wie ist|wie wird) [denn] wetter")
- @pattern("wetter [in] {city}")
- @example("Wie ist das Wetter?", "Wie warm ist es?")
- @example("Wetter in Berlin")
- async def handle_weather(self, data, city: str = ""):
- pass
- metadata = get_intent_metadata(handle_weather)
- assert metadata["name"] == "current_weather"
- assert metadata["description"] == "Wetter abfragen"
- assert len(metadata["patterns"]) == 2
- assert len(metadata["examples"]) == 3
- assert "city" in metadata["slots"]
- assert metadata["slots"]["city"].required is False
- # ============================================================================
- # Slot-Erkennung aus Signatur
- # ============================================================================
- class TestSlotExtractionFromSignature:
- """Tests fuer _extract_slots_from_signature()."""
- def test_optionaler_slot(self):
- """Parameter mit Default-Wert sollte optionaler Slot sein."""
- async def handler(self, data, city: str = ""):
- pass
- slots = _extract_slots_from_signature(handler)
- assert "city" in slots
- assert slots["city"].required is False
- assert slots["city"].slot_type is str
- def test_erforderlicher_slot(self):
- """Parameter ohne Default-Wert sollte erforderlicher Slot sein."""
- async def handler(self, data, device: str):
- pass
- slots = _extract_slots_from_signature(handler)
- assert "device" in slots
- assert slots["device"].required is True
- assert slots["device"].slot_type is str
- def test_verschiedene_typen(self):
- """Verschiedene Typ-Annotationen sollten erkannt werden."""
- async def handler(self, data, count: int = 0, temp: float = 0.0, name: str = ""):
- pass
- slots = _extract_slots_from_signature(handler)
- assert slots["count"].slot_type is int
- assert slots["temp"].slot_type is float
- assert slots["name"].slot_type is str
- def test_skip_self_und_data(self):
- """'self' und 'data' sollten uebersprungen werden."""
- async def handler(self, data, city: str = ""):
- pass
- slots = _extract_slots_from_signature(handler)
- assert "self" not in slots
- assert "data" not in slots
- assert "city" in slots
- def test_skip_args_kwargs(self):
- """*args und **kwargs sollten uebersprungen werden."""
- async def handler(self, data, *args, city: str = "", **kwargs):
- pass
- slots = _extract_slots_from_signature(handler)
- assert "args" not in slots
- assert "kwargs" not in slots
- assert "city" in slots
- def test_ohne_annotation(self):
- """Parameter ohne Annotation sollte als str behandelt werden."""
- async def handler(self, data, city=""):
- pass
- slots = _extract_slots_from_signature(handler)
- assert "city" in slots
- assert slots["city"].slot_type is str
- def test_keine_slots(self):
- """Funktion ohne Slot-Parameter sollte leeres Dict ergeben."""
- async def handler(self, data):
- pass
- slots = _extract_slots_from_signature(handler)
- assert len(slots) == 0
- def test_intent_erkennt_signatur_slots(self):
- """@intent sollte Slots automatisch aus der Signatur erkennen."""
- @intent("test")
- async def handler(self, data, city: str = "", count: int = 0):
- pass
- metadata = get_intent_metadata(handler)
- assert "city" in metadata["slots"]
- assert "count" in metadata["slots"]
- assert metadata["slots"]["city"].required is False
- assert metadata["slots"]["count"].slot_type is int
- def test_explizite_slots_ueberschreiben_signatur(self):
- """Explizite Slots sollten Signatur-Slots ueberschreiben."""
- @intent("test", slots={
- "city": IntentSlot(name="city", slot_type=str, required=True),
- })
- async def handler(self, data, city: str = ""):
- pass
- metadata = get_intent_metadata(handler)
- # Explizit als required gesetzt, obwohl Signatur Default hat
- assert metadata["slots"]["city"].required is True
- # ============================================================================
- # discover_intent_handlers
- # ============================================================================
- class TestDiscoverIntentHandlers:
- """Tests fuer discover_intent_handlers()."""
- def test_findet_handler(self):
- """Sollte alle @intent-dekorierten Methoden finden."""
- class TestPlugin:
- @intent("intent1")
- async def handler1(self, data):
- pass
- @intent("intent2")
- async def handler2(self, data):
- pass
- def normal_method(self):
- pass
- plugin = TestPlugin()
- handlers = discover_intent_handlers(plugin)
- assert len(handlers) == 2
- names = [h[0] for h in handlers]
- assert "intent1" in names
- assert "intent2" in names
- def test_ueberspringe_private_methoden(self):
- """Private Methoden (mit _) sollten uebersprungen werden."""
- class TestPlugin:
- @intent("public")
- async def public_handler(self, data):
- pass
- @intent("private")
- async def _private_handler(self, data):
- pass
- plugin = TestPlugin()
- handlers = discover_intent_handlers(plugin)
- names = [h[0] for h in handlers]
- assert "public" in names
- assert "private" not in names
- def test_handler_tupel_struktur(self):
- """Jeder Handler sollte (name, func, metadata) Tupel sein."""
- class TestPlugin:
- @intent("test", description="Testbeschreibung")
- @pattern("test muster")
- @example("Test Beispiel")
- async def handler(self, data, city: str = ""):
- pass
- plugin = TestPlugin()
- handlers = discover_intent_handlers(plugin)
- assert len(handlers) == 1
- name, func, metadata = handlers[0]
- assert name == "test"
- assert callable(func)
- assert metadata["description"] == "Testbeschreibung"
- assert len(metadata["patterns"]) == 1
- assert len(metadata["examples"]) == 1
- assert "city" in metadata["slots"]
- def test_leeres_objekt(self):
- """Objekt ohne Handler sollte leere Liste ergeben."""
- class EmptyPlugin:
- def normal_method(self):
- pass
- plugin = EmptyPlugin()
- handlers = discover_intent_handlers(plugin)
- assert handlers == []
- def test_mit_pattern_und_example_dekoratoren(self):
- """Handler mit @pattern und @example sollten korrekt gefunden werden."""
- class TestPlugin:
- @intent("weather")
- @pattern("(wie ist|wie wird) wetter")
- @pattern("wetter [in] {city}")
- @example("Wie ist das Wetter?")
- async def handle_weather(self, data, city: str = ""):
- pass
- plugin = TestPlugin()
- handlers = discover_intent_handlers(plugin)
- assert len(handlers) == 1
- _, _, metadata = handlers[0]
- assert len(metadata["patterns"]) == 2
- assert len(metadata["examples"]) == 1
- assert "city" in metadata["slots"]
- assert metadata["slots"]["city"].required is False
- # ============================================================================
- # Hilfsfunktionen
- # ============================================================================
- class TestHilfsfunktionen:
- """Tests fuer get_intent_metadata und is_intent_handler."""
- def test_get_intent_metadata_handler(self):
- """Sollte Metadaten fuer Intent-Handler zurueckgeben."""
- @intent("test")
- async def handler(data):
- pass
- metadata = get_intent_metadata(handler)
- assert metadata is not None
- assert metadata["name"] == "test"
- def test_get_intent_metadata_kein_handler(self):
- """Sollte None fuer Nicht-Handler zurueckgeben."""
- async def handler(data):
- pass
- metadata = get_intent_metadata(handler)
- assert metadata is None
- def test_is_intent_handler_true(self):
- """Sollte True fuer Intent-Handler zurueckgeben."""
- @intent("test")
- async def handler(data):
- pass
- assert is_intent_handler(handler) is True
- def test_is_intent_handler_false(self):
- """Sollte False fuer Nicht-Handler zurueckgeben."""
- async def handler(data):
- pass
- assert is_intent_handler(handler) is False
- def test_is_intent_handler_mit_pattern_only(self):
- """@pattern allein sollte kein Intent-Handler sein."""
- @pattern("test")
- async def handler(data):
- pass
- assert is_intent_handler(handler) is False
- def test_is_intent_handler_mit_example_only(self):
- """@example allein sollte kein Intent-Handler sein."""
- @example("Test")
- async def handler(data):
- pass
- assert is_intent_handler(handler) is False
|