| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839 |
- # -*- coding: utf-8 -*-
- """
- Tests fuer den KeywordIntentMatcher.
- Testet Pattern-Parsing, Pattern-Matching, Fuzzy-Matching,
- Sentiment-Erkennung, Slot-Extraktion, Example-Matching
- und Confidence-Berechnung.
- """
- import pytest
- from trixy_core.nlp.keyword_matcher import (
- KeywordIntentMatcher,
- MatchResult,
- PatternToken,
- CompiledPattern,
- POLITE_WORDS,
- RUDE_WORDS,
- URGENT_WORDS,
- )
- # ============================================================================
- # Fixtures
- # ============================================================================
- @pytest.fixture
- def matcher():
- """Erstellt einen KeywordIntentMatcher mit Standard-Config."""
- return KeywordIntentMatcher()
- @pytest.fixture
- def matcher_low_threshold():
- """Matcher mit niedrigerer Schwelle fuer Grenzfall-Tests."""
- return KeywordIntentMatcher({"confidence_threshold": 0.5, "fuzzy_threshold": 0.7})
- @pytest.fixture
- def weather_intents():
- """Wetter-Intent-Definitionen fuer Tests."""
- return [
- {
- "name": "current_weather",
- "patterns": [
- "(wie ist|wie wird) [denn] [das] wetter [draussen|gerade|heute]",
- "wetter [in] {city}",
- "(wie warm|wie kalt) ist [es] [draussen|gerade]",
- "temperatur [draussen]",
- ],
- "examples": [
- "Wie ist das Wetter?",
- "Wie ist das Wetter in Berlin?",
- "Wie warm ist es?",
- "Wetter in Hamburg",
- ],
- },
- {
- "name": "weather_forecast",
- "patterns": [
- "wie wird [das] wetter {date}",
- "vorhersage [fuer] {date}",
- "brauche ich {date} [einen] regenschirm",
- ],
- "examples": [
- "Wie wird das Wetter morgen?",
- "Brauche ich morgen einen Regenschirm?",
- ],
- },
- ]
- @pytest.fixture
- def system_intents():
- """System-Intent-Definitionen fuer Tests."""
- return [
- {
- "name": "stop",
- "patterns": ["stopp", "halt", "alles stoppen", "sei still", "ruhe"],
- "examples": ["Stopp", "Halt", "Alles stoppen"],
- },
- {
- "name": "pause",
- "patterns": ["pause", "pausieren"],
- "examples": ["Pause", "Pausieren"],
- },
- {
- "name": "resume",
- "patterns": ["weiterspielen", "fortsetzen", "weiter [machen]", "play"],
- "examples": ["Weiterspielen", "Fortsetzen", "Play"],
- },
- {
- "name": "volume_set",
- "patterns": [
- "lautstaerke [auf] {level}",
- "(setze|stelle) [die] lautstaerke [auf] {level}",
- ],
- "examples": ["Lautstärke auf 50"],
- },
- {
- "name": "help",
- "patterns": ["hilfe", "was kannst [du]", "befehle"],
- "examples": ["Hilfe", "Was kannst du?"],
- },
- ]
- @pytest.fixture
- def timer_intents():
- """Timer-Intent-Definitionen fuer Tests."""
- return [
- {
- "name": "set_timer",
- "patterns": [
- "(setze|stelle|erstelle|starte) [einen|den] timer [auf|fuer] {duration}",
- "timer {duration}",
- "weck [mich] [in] {duration}",
- ],
- "examples": [
- "Setze Timer auf 5 Minuten",
- "Timer 30 Sekunden",
- ],
- },
- ]
- # ============================================================================
- # Tokenisierung
- # ============================================================================
- class TestTokenize:
- """Tests fuer die Tokenisierung."""
- def test_einfacher_text(self, matcher):
- """Einfacher Text sollte in Kleinbuchstaben tokenisiert werden."""
- tokens = matcher._tokenize("Wie ist das Wetter")
- assert tokens == ["wie", "ist", "das", "wetter"]
- def test_interpunktion_entfernt(self, matcher):
- """Interpunktion sollte entfernt werden."""
- tokens = matcher._tokenize("Wie spät ist es?")
- assert "?" not in " ".join(tokens)
- assert tokens[0] == "wie"
- def test_umlaute_erhalten(self, matcher):
- """Umlaute sollten erhalten bleiben."""
- tokens = matcher._tokenize("Wie spät ist es draußen?")
- assert "spät" in tokens or "spaet" in tokens or "spät" in tokens
- def test_leerer_text(self, matcher):
- """Leerer Text sollte leere Liste ergeben."""
- tokens = matcher._tokenize("")
- assert tokens == []
- def test_nur_leerzeichen(self, matcher):
- """Nur Leerzeichen sollte leere Liste ergeben."""
- tokens = matcher._tokenize(" ")
- assert tokens == []
- # ============================================================================
- # Pattern-Parsing
- # ============================================================================
- class TestParsePattern:
- """Tests fuer das Pattern-Parsing."""
- def test_literal_keyword(self, matcher):
- """Einfaches Wort sollte als Literal geparst werden."""
- tokens = matcher._parse_pattern("wetter")
- assert len(tokens) == 1
- assert tokens[0].kind == "literal"
- assert tokens[0].values == ["wetter"]
- assert tokens[0].required is True
- def test_pflicht_alternativen(self, matcher):
- """(a|b) sollte als Pflicht-Alternativen geparst werden."""
- tokens = matcher._parse_pattern("(wie ist|wie wird)")
- assert len(tokens) == 1
- assert tokens[0].kind == "alternatives"
- assert "wie ist" in tokens[0].values
- assert "wie wird" in tokens[0].values
- assert tokens[0].required is True
- def test_optionales_wort(self, matcher):
- """[wort] sollte als optional geparst werden."""
- tokens = matcher._parse_pattern("[denn]")
- assert len(tokens) == 1
- assert tokens[0].kind == "optional"
- assert tokens[0].values == ["denn"]
- assert tokens[0].required is False
- def test_optionale_alternativen(self, matcher):
- """[a|b|c] sollte als optionale Alternativen geparst werden."""
- tokens = matcher._parse_pattern("[draussen|gerade|heute]")
- assert len(tokens) == 1
- assert tokens[0].kind == "optional"
- assert "draussen" in tokens[0].values
- assert "gerade" in tokens[0].values
- assert "heute" in tokens[0].values
- assert tokens[0].required is False
- def test_slot(self, matcher):
- """{{slot_name}} sollte als Slot geparst werden."""
- tokens = matcher._parse_pattern("{city}")
- assert len(tokens) == 1
- assert tokens[0].kind == "slot"
- assert tokens[0].slot_name == "city"
- assert tokens[0].entity_type == ""
- assert tokens[0].required is False
- def test_slot_mit_entity_typ(self, matcher):
- """{{name:typ}} sollte Slot mit Entity-Typ ergeben."""
- tokens = matcher._parse_pattern("{date:datum}")
- assert len(tokens) == 1
- assert tokens[0].kind == "slot"
- assert tokens[0].slot_name == "date"
- assert tokens[0].entity_type == "datum"
- def test_slot_mit_entity_typ_komplex(self, matcher):
- """Komplexes Pattern mit Entity-Typ-Slots."""
- tokens = matcher._parse_pattern("wetter [am] {date:datum} [in] {city:stadt}")
- assert len(tokens) == 5
- # {date:datum}
- assert tokens[2].kind == "slot"
- assert tokens[2].slot_name == "date"
- assert tokens[2].entity_type == "datum"
- # {city:stadt}
- assert tokens[4].kind == "slot"
- assert tokens[4].slot_name == "city"
- assert tokens[4].entity_type == "stadt"
- def test_inline_alternativen_mit_leer(self, matcher):
- """{a|b|} sollte als optional geparst werden (letztes leer)."""
- tokens = matcher._parse_pattern("{das|}")
- assert len(tokens) == 1
- assert tokens[0].kind == "optional"
- assert "das" in tokens[0].values
- assert tokens[0].required is False
- def test_inline_alternativen_ohne_leer(self, matcher):
- """{a|b} sollte als Pflicht-Alternativen geparst werden."""
- tokens = matcher._parse_pattern("{warm|kalt}")
- assert len(tokens) == 1
- assert tokens[0].kind == "alternatives"
- assert "warm" in tokens[0].values
- assert "kalt" in tokens[0].values
- assert tokens[0].required is True
- def test_komplexes_pattern(self, matcher):
- """Komplexes Pattern mit mehreren Elementen."""
- tokens = matcher._parse_pattern("(wie ist|wie wird) [denn] wetter [in] {city}")
- assert len(tokens) == 5
- # (wie ist|wie wird)
- assert tokens[0].kind == "alternatives"
- assert tokens[0].required is True
- # [denn]
- assert tokens[1].kind == "optional"
- # wetter
- assert tokens[2].kind == "literal"
- assert tokens[2].required is True
- # [in]
- assert tokens[3].kind == "optional"
- # {city}
- assert tokens[4].kind == "slot"
- assert tokens[4].slot_name == "city"
- def test_leeres_pattern(self, matcher):
- """Leeres Pattern sollte leere Liste ergeben."""
- tokens = matcher._parse_pattern("")
- assert tokens == []
- # ============================================================================
- # Pattern-Matching
- # ============================================================================
- class TestPatternMatching:
- """Tests fuer das Pattern-Matching."""
- def test_exakter_match(self, matcher, system_intents):
- """Exakter Befehl sollte matchen."""
- matcher.compile_intents(system_intents)
- result = matcher.match("Stopp")
- assert result is not None
- assert result.intent == "stop"
- assert result.confidence >= 0.82
- def test_einwort_befehle(self, matcher, system_intents):
- """Einwort-Befehle sollten korrekt matchen."""
- matcher.compile_intents(system_intents)
- for text, expected_intent in [
- ("Stopp", "stop"),
- ("Halt", "stop"),
- ("Pause", "pause"),
- ("Pausieren", "pause"),
- ("Hilfe", "help"),
- ("Play", "resume"),
- ]:
- result = matcher.match(text)
- assert result is not None, f"'{text}' sollte matchen"
- assert result.intent == expected_intent, (
- f"'{text}' sollte '{expected_intent}' sein, war '{result.intent}'"
- )
- def test_mehrwort_befehl(self, matcher, system_intents):
- """Mehrwort-Befehle sollten matchen."""
- matcher.compile_intents(system_intents)
- result = matcher.match("Alles stoppen")
- assert result is not None
- assert result.intent == "stop"
- def test_optionale_woerter(self, matcher, system_intents):
- """Optionale Woerter sollten korrekt behandelt werden."""
- matcher.compile_intents(system_intents)
- # "weiter [machen]" — ohne optionales Wort
- result = matcher.match("Weiter")
- assert result is not None
- assert result.intent == "resume"
- # "weiter [machen]" — mit optionalem Wort
- result = matcher.match("Weiter machen")
- assert result is not None
- assert result.intent == "resume"
- def test_pflicht_alternativen(self, matcher, weather_intents):
- """Pflicht-Alternativen sollten alle matchen."""
- matcher.compile_intents(weather_intents)
- result1 = matcher.match("Wie ist das Wetter")
- assert result1 is not None
- assert result1.intent == "current_weather"
- result2 = matcher.match("Wie wird das Wetter")
- assert result2 is not None
- def test_slot_extraktion(self, matcher, weather_intents):
- """Slots sollten korrekt extrahiert werden."""
- matcher.compile_intents(weather_intents)
- result = matcher.match("Wetter in Berlin")
- assert result is not None
- assert result.intent == "current_weather"
- assert "city" in result.slots
- assert result.slots["city"] == "berlin"
- def test_slot_mehrwort(self, matcher, weather_intents):
- """Slots sollten Mehrwort-Werte aufnehmen koennen."""
- matcher.compile_intents(weather_intents)
- result = matcher.match("Wetter in New York")
- assert result is not None
- assert "city" in result.slots
- assert "new york" in result.slots["city"]
- def test_slot_am_ende(self, matcher, timer_intents):
- """Slot am Ende sollte restliche Tokens aufnehmen."""
- matcher.compile_intents(timer_intents)
- result = matcher.match("Timer 5 Minuten")
- assert result is not None
- assert result.intent == "set_timer"
- assert "duration" in result.slots
- assert "5 minuten" in result.slots["duration"]
- def test_kein_match(self, matcher, system_intents):
- """Unbekannter Text sollte nicht matchen."""
- matcher.compile_intents(system_intents)
- result = matcher.match("Wer war Beethoven?")
- assert result is None
- def test_kein_match_leerer_text(self, matcher, system_intents):
- """Leerer Text sollte nicht matchen."""
- matcher.compile_intents(system_intents)
- assert matcher.match("") is None
- assert matcher.match(" ") is None
- assert matcher.match(None) is None # type: ignore
- def test_confidence_ueber_schwelle(self, matcher, system_intents):
- """Confidence sollte ueber der konfigurierten Schwelle liegen."""
- matcher.compile_intents(system_intents)
- result = matcher.match("Stopp")
- assert result is not None
- assert result.confidence >= 0.82
- def test_matched_by_pattern(self, matcher, system_intents):
- """matched_by sollte 'pattern' fuer Pattern-Matches sein."""
- matcher.compile_intents(system_intents)
- result = matcher.match("Stopp")
- assert result is not None
- assert result.matched_by == "pattern"
- # ============================================================================
- # Fuzzy-Matching
- # ============================================================================
- class TestFuzzyMatching:
- """Tests fuer das Fuzzy-Matching (STT-Fehlertoleranz)."""
- def test_fuzzy_compare_exakt(self, matcher):
- """Exakte Matches sollten 1.0 ergeben."""
- assert matcher._fuzzy_compare("wetter", "wetter") == 1.0
- assert matcher._fuzzy_compare("stopp", "stopp") == 1.0
- def test_fuzzy_compare_aehnlich(self, matcher):
- """Aehnliche Woerter sollten hohen Score ergeben."""
- score = matcher._fuzzy_compare("weter", "wetter")
- assert score >= 0.8
- def test_fuzzy_compare_verschieden(self, matcher):
- """Verschiedene Woerter sollten niedrigen Score ergeben."""
- score = matcher._fuzzy_compare("hund", "katze")
- assert score < 0.5
- def test_fuzzy_pattern_match(self, matcher, weather_intents):
- """Leichte STT-Fehler sollten toleriert werden."""
- matcher.compile_intents(weather_intents)
- # "weter" statt "wetter" — ein Buchstabe fehlt
- result = matcher.match("weter in Berlin")
- assert result is not None
- assert result.intent == "current_weather"
- def test_fuzzy_example_match(self, matcher, system_intents):
- """Fuzzy sollte auch bei Example-Matching funktionieren."""
- matcher.compile_intents(system_intents)
- # "Hilf" statt "Hilfe" — aehnlich genug
- result = matcher.match("Hilf")
- # Kann per Pattern oder Example matchen
- if result is not None:
- assert result.intent == "help"
- # ============================================================================
- # Example-Matching
- # ============================================================================
- class TestExampleMatching:
- """Tests fuer das Example-basierte Matching."""
- def test_exakter_example_match(self, matcher):
- """Exakte Uebereinstimmung mit Example sollte hohen Score ergeben."""
- score = matcher._score_example_match(
- ["wie", "ist", "das", "wetter"],
- ["wie", "ist", "das", "wetter"],
- )
- assert score == 0.98 # Exakter Match
- def test_teilweise_uebereinstimmung(self, matcher):
- """Teilweise Uebereinstimmung sollte Score proportional ergeben."""
- score = matcher._score_example_match(
- ["wie", "ist", "wetter"],
- ["wie", "ist", "das", "wetter"],
- )
- assert score > 0.5
- def test_leerer_input(self, matcher):
- """Leerer Input sollte 0.0 ergeben."""
- assert matcher._score_example_match([], ["test"]) == 0.0
- assert matcher._score_example_match(["test"], []) == 0.0
- assert matcher._score_example_match([], []) == 0.0
- def test_example_nur_intents(self, matcher):
- """Intents mit nur Examples (keine Patterns) sollten matchen."""
- intents = [{
- "name": "greet",
- "examples": ["Hallo", "Guten Tag", "Hi"],
- }]
- matcher.compile_intents(intents)
- result = matcher.match("Hallo")
- assert result is not None
- assert result.intent == "greet"
- assert result.matched_by == "example"
- def test_example_match_confidence(self, matcher):
- """Example-Matches sollten realistische Confidence haben."""
- intents = [{
- "name": "greet",
- "examples": ["Hallo wie geht es dir"],
- }]
- matcher.compile_intents(intents)
- result = matcher.match("Hallo wie geht es dir")
- assert result is not None
- assert result.confidence >= 0.82
- # ============================================================================
- # Sentiment-Erkennung
- # ============================================================================
- class TestSentiment:
- """Tests fuer die Sentiment-Erkennung."""
- def test_neutral(self, matcher):
- """Text ohne Floskel sollte neutral sein."""
- sentiment = matcher._extract_sentiment(["wie", "ist", "das", "wetter"])
- assert sentiment["tone"] == "neutral"
- assert sentiment["is_urgent"] is False
- assert sentiment["markers"] == []
- def test_polite(self, matcher):
- """'bitte' sollte als hoeflich erkannt werden."""
- sentiment = matcher._extract_sentiment(["bitte", "wie", "ist", "das", "wetter"])
- assert sentiment["tone"] == "polite"
- assert "bitte" in sentiment["markers"]
- def test_rude(self, matcher):
- """Schimpfwoerter sollten als unhöflich erkannt werden."""
- sentiment = matcher._extract_sentiment(["verdammt", "wie", "ist", "das", "wetter"])
- assert sentiment["tone"] == "rude"
- assert "verdammt" in sentiment["markers"]
- def test_rude_ueberwiegt_polite(self, matcher):
- """Rude sollte polite ueberschreiben."""
- sentiment = matcher._extract_sentiment(["bitte", "verdammt", "stopp"])
- assert sentiment["tone"] == "rude"
- def test_urgent(self, matcher):
- """Dringlichkeitswoerter sollten erkannt werden."""
- sentiment = matcher._extract_sentiment(["sofort", "stopp"])
- assert sentiment["is_urgent"] is True
- assert "sofort" in sentiment["markers"]
- def test_polite_und_urgent(self, matcher):
- """Hoeflich und dringend gleichzeitig."""
- sentiment = matcher._extract_sentiment(["bitte", "schnell", "stopp"])
- assert sentiment["tone"] == "polite"
- assert sentiment["is_urgent"] is True
- def test_sentiment_in_match_result(self, matcher, system_intents):
- """Sentiment sollte im Match-Ergebnis enthalten sein."""
- matcher.compile_intents(system_intents)
- result = matcher.match("bitte stopp")
- assert result is not None
- assert result.sentiment["tone"] == "polite"
- def test_sentiment_woerter_entfernt_beim_matching(self, matcher, weather_intents):
- """Sentiment-Woerter sollten das Matching nicht stoeren."""
- matcher.compile_intents(weather_intents)
- # "bitte wie ist das wetter" — "bitte" soll entfernt werden
- result = matcher.match("bitte wie ist das Wetter")
- assert result is not None
- assert result.intent == "current_weather"
- assert result.sentiment["tone"] == "polite"
- # ============================================================================
- # Compile-Intents
- # ============================================================================
- class TestCompileIntents:
- """Tests fuer das Kompilieren von Intents."""
- def test_compile_mit_patterns(self, matcher):
- """Intents mit Patterns sollten kompiliert werden."""
- intents = [{
- "name": "test_intent",
- "patterns": ["test (eins|zwei)"],
- "examples": ["Test eins"],
- }]
- matcher.compile_intents(intents)
- assert "test_intent" in matcher._compiled
- assert len(matcher._compiled["test_intent"]) == 1
- def test_compile_nur_examples(self, matcher):
- """Intents mit nur Examples sollten in _examples landen."""
- intents = [{
- "name": "test_intent",
- "examples": ["Hallo", "Hi"],
- }]
- matcher.compile_intents(intents)
- assert "test_intent" not in matcher._compiled
- assert "test_intent" in matcher._examples
- assert len(matcher._examples["test_intent"]) == 2
- def test_compile_ohne_name(self, matcher):
- """Intents ohne Name sollten uebersprungen werden."""
- intents = [{"patterns": ["test"]}]
- matcher.compile_intents(intents)
- assert len(matcher._compiled) == 0
- assert len(matcher._examples) == 0
- def test_compile_leere_liste(self, matcher):
- """Leere Intent-Liste sollte keine Fehler verursachen."""
- matcher.compile_intents([])
- assert len(matcher._compiled) == 0
- assert len(matcher._examples) == 0
- def test_compile_loescht_vorherige(self, matcher):
- """Erneutes Kompilieren sollte vorherige Daten loeschen."""
- matcher.compile_intents([{
- "name": "old",
- "patterns": ["alt"],
- }])
- assert "old" in matcher._compiled
- matcher.compile_intents([{
- "name": "new",
- "patterns": ["neu"],
- }])
- assert "old" not in matcher._compiled
- assert "new" in matcher._compiled
- def test_compile_mehrere_patterns_pro_intent(self, matcher):
- """Mehrere Patterns pro Intent sollten kompiliert werden."""
- intents = [{
- "name": "test",
- "patterns": ["muster eins", "(a|b) zwei", "drei [optional]"],
- }]
- matcher.compile_intents(intents)
- assert len(matcher._compiled["test"]) == 3
- # ============================================================================
- # Konfiguration
- # ============================================================================
- class TestKonfiguration:
- """Tests fuer die Konfiguration des Matchers."""
- def test_default_config(self):
- """Standard-Konfiguration sollte gesetzt sein."""
- matcher = KeywordIntentMatcher()
- assert matcher._confidence_threshold == 0.82
- assert matcher._fuzzy_threshold == 0.8
- def test_custom_config(self):
- """Benutzerdefinierte Konfiguration sollte uebernommen werden."""
- matcher = KeywordIntentMatcher({
- "confidence_threshold": 0.9,
- "fuzzy_threshold": 0.85,
- })
- assert matcher._confidence_threshold == 0.9
- assert matcher._fuzzy_threshold == 0.85
- def test_none_config(self):
- """None-Config sollte Standard-Werte verwenden."""
- matcher = KeywordIntentMatcher(None)
- assert matcher._confidence_threshold == 0.82
- def test_leere_config(self):
- """Leere Config sollte Standard-Werte verwenden."""
- matcher = KeywordIntentMatcher({})
- assert matcher._confidence_threshold == 0.82
- # ============================================================================
- # Datenklassen
- # ============================================================================
- class TestDatenklassen:
- """Tests fuer die Datenklassen."""
- def test_pattern_token_defaults(self):
- """PatternToken sollte korrekte Defaults haben."""
- pt = PatternToken(kind="literal")
- assert pt.kind == "literal"
- assert pt.values == []
- assert pt.slot_name == ""
- assert pt.required is True
- def test_compiled_pattern_defaults(self):
- """CompiledPattern sollte korrekte Defaults haben."""
- cp = CompiledPattern()
- assert cp.tokens == []
- assert cp.source == ""
- def test_match_result_defaults(self):
- """MatchResult sollte korrekte Defaults haben."""
- mr = MatchResult()
- assert mr.intent == ""
- assert mr.confidence == 0.0
- assert mr.slots == {}
- assert mr.sentiment == {}
- assert mr.matched_by == ""
- def test_match_result_mit_werten(self):
- """MatchResult sollte Werte korrekt speichern."""
- mr = MatchResult(
- intent="test",
- confidence=0.95,
- slots={"city": "berlin"},
- sentiment={"tone": "polite"},
- matched_by="pattern",
- )
- assert mr.intent == "test"
- assert mr.confidence == 0.95
- assert mr.slots["city"] == "berlin"
- assert mr.sentiment["tone"] == "polite"
- # ============================================================================
- # Sentiment-Wort-Entfernung
- # ============================================================================
- class TestRemoveSentimentWords:
- """Tests fuer das Entfernen von Sentiment-Woertern."""
- def test_entfernt_bitte(self, matcher):
- """'bitte' sollte entfernt werden."""
- result = matcher._remove_sentiment_words(["bitte", "stopp"])
- assert "bitte" not in result
- assert "stopp" in result
- def test_entfernt_fuellwoerter(self, matcher):
- """Fuellwoerter sollten entfernt werden."""
- result = matcher._remove_sentiment_words(["kannst", "du", "mal", "stopp"])
- assert "stopp" in result
- assert "kannst" not in result
- assert "du" not in result
- assert "mal" not in result
- def test_behaelt_original_bei_komplettem_entfernen(self, matcher):
- """Wenn alles entfernt wuerde, Original zurueckgeben."""
- result = matcher._remove_sentiment_words(["bitte", "danke"])
- # Sollte Original zurueckgeben, da sonst leere Liste
- assert len(result) > 0
- # ============================================================================
- # Integrationstests
- # ============================================================================
- class TestIntegration:
- """Integrationstests mit realistischen Szenarien."""
- @pytest.fixture
- def full_matcher(self, weather_intents, system_intents, timer_intents):
- """Matcher mit allen Intents geladen."""
- m = KeywordIntentMatcher()
- all_intents = weather_intents + system_intents + timer_intents
- m.compile_intents(all_intents)
- return m
- def test_wetter_abfrage(self, full_matcher):
- """Wetter-Abfrage sollte korrekt matchen."""
- result = full_matcher.match("Wie ist das Wetter?")
- assert result is not None
- assert result.intent == "current_weather"
- def test_wetter_mit_stadt(self, full_matcher):
- """Wetter-Abfrage mit Stadt sollte Slot extrahieren."""
- result = full_matcher.match("Wetter in München")
- assert result is not None
- assert result.intent == "current_weather"
- assert "city" in result.slots
- def test_stopp_befehl(self, full_matcher):
- """Stopp-Befehl sollte matchen."""
- result = full_matcher.match("Stopp")
- assert result is not None
- assert result.intent == "stop"
- def test_timer_setzen(self, full_matcher):
- """Timer setzen sollte matchen."""
- result = full_matcher.match("Setze Timer auf 5 Minuten")
- assert result is not None
- assert result.intent == "set_timer"
- assert "duration" in result.slots
- def test_unbekannter_befehl(self, full_matcher):
- """Unbekannter Befehl sollte nicht matchen."""
- result = full_matcher.match("Wer war Beethoven?")
- assert result is None
- def test_hoefliche_wetter_abfrage(self, full_matcher):
- """Hoefliche Anfrage sollte Sentiment erkennen."""
- result = full_matcher.match("Bitte wie ist das Wetter?")
- assert result is not None
- assert result.intent == "current_weather"
- assert result.sentiment["tone"] == "polite"
- def test_dringende_stopp_anfrage(self, full_matcher):
- """Dringende Anfrage sollte Urgency erkennen."""
- result = full_matcher.match("Sofort stopp")
- assert result is not None
- assert result.intent == "stop"
- assert result.sentiment["is_urgent"] is True
- def test_lautstaerke_mit_slot(self, full_matcher):
- """Lautstaerke setzen sollte Level-Slot extrahieren."""
- result = full_matcher.match("Lautstärke auf 50")
- assert result is not None
- assert result.intent == "volume_set"
- assert "level" in result.slots
- def test_wie_warm_ist_es(self, full_matcher):
- """'Wie warm ist es' sollte als Wetter matchen."""
- result = full_matcher.match("Wie warm ist es")
- assert result is not None
- assert result.intent == "current_weather"
- def test_pattern_match_vor_example(self, full_matcher):
- """Bei gleicher Confidence sollte der bessere Match gewinnen."""
- result = full_matcher.match("Pause")
- assert result is not None
- assert result.intent == "pause"
- # Pattern-Match sollte bevorzugt werden (hoehere Confidence)
- # oder zumindest korrekt matchen
- def test_performance_einfach(self, full_matcher):
- """Einfacher Match sollte schnell sein (<10ms)."""
- import time
- start = time.perf_counter()
- for _ in range(100):
- full_matcher.match("Stopp")
- elapsed = (time.perf_counter() - start) / 100 * 1000 # ms
- # Sollte weit unter 10ms pro Match sein
- assert elapsed < 10, f"Match dauerte {elapsed:.2f}ms (sollte <10ms sein)"
- def test_performance_komplex(self, full_matcher):
- """Komplexer Match sollte schnell sein (<10ms)."""
- import time
- start = time.perf_counter()
- for _ in range(100):
- full_matcher.match("Bitte wie ist das Wetter in Berlin?")
- elapsed = (time.perf_counter() - start) / 100 * 1000 # ms
- assert elapsed < 10, f"Match dauerte {elapsed:.2f}ms (sollte <10ms sein)"
|