File: test_fuzzy.py

package info (click to toggle)
hassil 3.5.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 536 kB
  • sloc: python: 6,474; makefile: 2
file content (290 lines) | stat: -rw-r--r-- 11,036 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
"""Tests for fuzzy matching."""

from pathlib import Path

import pytest
from home_assistant_intents import get_fuzzy_config, get_fuzzy_language, get_intents
from yaml import safe_load

from hassil import Intents, TextSlotList
from hassil.fuzzy import FuzzyNgramMatcher, FuzzySlotValue, SlotCombinationInfo
from hassil.ngram import Sqlite3NgramModel

_TESTS_DIR = Path(__file__).parent


@pytest.fixture(name="matcher", scope="session")
def matcher_fixture() -> FuzzyNgramMatcher:
    """Load fuzzy matcher for English."""
    intents_dict = get_intents("en")
    assert intents_dict
    intents = Intents.from_dict(intents_dict)

    with open(
        _TESTS_DIR / "test_fixtures.yaml", "r", encoding="utf-8"
    ) as fixtures_file:
        lists_dict = safe_load(fixtures_file)["lists"]

    intents.slot_lists["name"] = TextSlotList.from_tuples(
        (name_info["in"], name_info["out"], name_info["context"])
        for name_info in lists_dict["name"]["values"]
    )
    intents.slot_lists["area"] = TextSlotList.from_strings(lists_dict["area"]["values"])
    intents.slot_lists["floor"] = TextSlotList.from_strings(
        lists_dict["floor"]["values"]
    )

    fuzzy_config = get_fuzzy_config()
    lang_config = get_fuzzy_language("en")
    assert lang_config is not None

    matcher = FuzzyNgramMatcher(
        intents=intents,
        intent_models={
            intent_name: Sqlite3NgramModel(
                order=fuzzy_model.order,
                words={
                    word: str(word_id) for word, word_id in fuzzy_model.words.items()
                },
                database_path=fuzzy_model.database_path,
            )
            for intent_name, fuzzy_model in lang_config.ngram_models.items()
        },
        intent_slot_list_names=fuzzy_config.slot_list_names,
        slot_combinations={
            intent_name: {
                combo_key: SlotCombinationInfo(
                    context_area=combo_info.context_area,
                    name_domains=(
                        set(combo_info.name_domains)
                        if combo_info.name_domains
                        else None
                    ),
                )
                for combo_key, combo_info in intent_combos.items()
            }
            for intent_name, intent_combos in fuzzy_config.slot_combinations.items()
        },
        domain_keywords=lang_config.domain_keywords,
        stop_words=lang_config.stop_words,
    )

    return matcher


def test_domain_only(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match(
        "turn on the lights in this room", context_area="Living Room"
    )
    assert result is not None
    assert result.intent_name == "HassTurnOn"
    assert result.slots.keys() == {"domain", "area"}
    assert result.slots["domain"] == FuzzySlotValue(value="light", text="lights")
    assert result.slots["area"] == FuzzySlotValue(value="Living Room", text="")


def test_name_only(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("turn off that tv right now")
    assert result is not None
    assert result.intent_name == "HassTurnOff"
    assert result.slots.keys() == {"name"}
    assert result.slots["name"] == FuzzySlotValue(
        value="Family Room Google TV", text="TV"
    )
    assert result.name_domain == "media_player"


def test_name_area(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("kitchen A.C. on")
    assert result is not None
    assert result.intent_name == "HassTurnOn"
    assert result.slots.keys() == {"name", "area"}
    assert result.slots["name"] == FuzzySlotValue(value="A.C.", text="A.C.")
    assert result.slots["area"] == FuzzySlotValue(value="Kitchen", text="Kitchen")
    assert result.name_domain == "switch"


def test_domain_area_device_class(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("open up all of the windows in the kitchen area please")
    assert result is not None
    assert result.intent_name == "HassTurnOn"
    assert result.slots.keys() == {"domain", "area", "device_class"}
    assert result.slots["domain"] == FuzzySlotValue(value="cover", text="windows")
    assert result.slots["device_class"] == FuzzySlotValue(
        value="window", text="windows"
    )
    assert result.slots["area"] == FuzzySlotValue(value="Kitchen", text="Kitchen")


def test_brightness(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("overhead light 50% brightness")
    assert result is not None
    assert result.intent_name == "HassLightSet"
    assert result.slots.keys() == {"name", "brightness"}
    assert result.slots["name"] == FuzzySlotValue(
        value="Overhead light", text="Overhead light"
    )
    assert result.slots["brightness"] == FuzzySlotValue(value=50, text="50%")
    assert result.name_domain == "light"


def test_temperature(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("how is the temperature", context_area="Living Room")
    assert result is not None
    assert result.intent_name == "HassClimateGetTemperature"
    assert result.slots.keys() == {"area"}
    assert result.slots["area"] == FuzzySlotValue(value="Living Room", text="")


def test_temperature_name(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("ecobee temp")
    assert result is not None
    assert result.intent_name == "HassClimateGetTemperature"
    assert result.slots.keys() == {"name"}
    assert result.slots["name"] == FuzzySlotValue(value="Ecobee", text="Ecobee")
    assert result.name_domain == "climate"


def test_get_time(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("time now")
    assert result is not None
    assert result.intent_name == "HassGetCurrentTime"
    assert not result.slots


def test_get_date(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("date today")
    assert result is not None
    assert result.intent_name == "HassGetCurrentDate"
    assert not result.slots


def test_weather(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("weather")
    assert result is not None
    assert result.intent_name == "HassGetWeather"
    assert not result.slots


def test_weather_name(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("how about demo weather south")
    assert result is not None
    assert result.intent_name == "HassGetWeather"
    assert result.slots.keys() == {"name"}
    assert result.slots["name"] == FuzzySlotValue(
        value="Demo Weather South", text="Demo Weather South"
    )
    assert result.name_domain == "weather"


def test_set_temperature(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("make it 72 degrees", context_area="Living Room")
    assert result is not None
    assert result.intent_name == "HassClimateSetTemperature"
    assert result.slots.keys() == {"temperature", "area"}
    assert result.slots["temperature"] == FuzzySlotValue(value=72, text="72")
    assert result.slots["area"] == FuzzySlotValue(value="Living Room", text="")


def test_set_color(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("red bedroom")
    assert result is not None
    assert result.intent_name == "HassLightSet"
    assert result.slots.keys() == {"area", "color"}
    assert result.slots["area"] == FuzzySlotValue(value="Bedroom", text="Bedroom")
    assert result.slots["color"] == FuzzySlotValue(value="red", text="red")


def test_set_volume(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("set TV 50")
    assert result is not None
    assert result.intent_name == "HassSetVolume"
    assert result.slots.keys() == {"name", "volume_level"}
    assert result.slots["name"] == FuzzySlotValue(
        value="Family Room Google TV", text="TV"
    )
    assert result.slots["volume_level"] == FuzzySlotValue(value=50, text="50")
    assert result.name_domain == "media_player"


def test_set_position(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("hall window 50")
    assert result is not None
    assert result.intent_name == "HassSetPosition"
    assert result.slots.keys() == {"name", "position"}
    assert result.slots["name"] == FuzzySlotValue(
        value="Hall Window", text="Hall Window"
    )
    assert result.slots["position"] == FuzzySlotValue(value=50, text="50")
    assert result.name_domain == "cover"


def test_degrees(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("set 72°", context_area="Living Room")
    assert result is not None
    assert result.intent_name == "HassClimateSetTemperature"
    assert result.slots.keys() == {"temperature", "area"}
    assert result.slots["temperature"] == FuzzySlotValue(value=72, text="72°")
    assert result.slots["area"] == FuzzySlotValue(value="Living Room", text="")


def test_nevermind(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("oh nevermind")
    assert result is not None
    assert result.intent_name == "HassNevermind"
    assert not result.slots.keys()


def test_scene(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("party time, excellent")
    assert result is not None
    assert result.intent_name == "HassTurnOn"
    assert result.slots.keys() == {"name"}
    assert result.slots["name"] == FuzzySlotValue(value="party time", text="party time")
    assert result.name_domain == "scene"


def test_timer_status(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match("what about my timers")
    assert result is not None
    assert result.intent_name == "HassTimerStatus"
    assert not result.slots


def test_wrong_vocab(matcher: FuzzyNgramMatcher) -> None:
    assert not matcher.match("open office lights")
    assert not matcher.match("close A.C.")
    assert not matcher.match("garage door off")

    # Front Door is a lock, not a cover
    assert not matcher.match("close front door")


def test_stop_words(matcher: FuzzyNgramMatcher) -> None:
    result = matcher.match(
        "hi there, so pls could you turn the A.C. off just now lol ok"
    )
    assert result is not None
    assert result.intent_name == "HassTurnOff"
    assert result.slots.keys() == {"name"}
    assert result.slots["name"] == FuzzySlotValue(value="A.C.", text="A.C.")


def test_misspelled_area(matcher: FuzzyNgramMatcher) -> None:
    """Test that context area is used when domain-only sentence is matched."""
    result = matcher.match(
        "turn off lights in the spaceship", context_area="Living Room"
    )
    assert result is not None
    assert result.intent_name == "HassTurnOff"
    assert result.slots.keys() == {"domain", "area"}
    assert result.slots["domain"] == FuzzySlotValue(value="light", text="lights")
    assert result.slots["area"] == FuzzySlotValue(value="Living Room", text="")


@pytest.mark.parametrize(
    "text", ["lights", "front door", "living room", "first floor", "10", "tv"]
)
def test_no_list_name_only(matcher: FuzzyNgramMatcher, text: str) -> None:
    """Test that fuzzy matching doesn't work for list names only."""
    assert not matcher.match(text), text