Skip to content

Commit fc9e0ba

Browse files
authored
Merge pull request #9475 from RasaHQ/fix-featurised-slots-in-rules
Fix #9302: Featurised slots sometimes get embedded as conditions in slot-agnostic rules.
2 parents 9b4d181 + 17c64be commit fc9e0ba

File tree

4 files changed

+86
-39
lines changed

4 files changed

+86
-39
lines changed

changelog/9302.bugfix.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix rules not being applied when a featurised categorical slot has as one of its allowed
2+
values `none`, `NoNe`, `None` or a similar value.

rasa/shared/core/domain.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -1155,19 +1155,25 @@ def _get_slots_sub_state(
11551155
omit_unset_slots: If `True` do not include the initial values of slots.
11561156
11571157
Returns:
1158-
a dictionary mapping slot names to their featurization
1158+
a mapping of slot names to their featurization
11591159
"""
11601160
slots = {}
11611161
for slot_name, slot in tracker.slots.items():
1162+
# If the slot doesn't influence conversations, slot.as_feature() will return
1163+
# a result that evaluates to False, meaning that the slot shouldn't be
1164+
# included in featurised sub-states.
1165+
# Note that this condition checks if the slot itself is None. An unset slot
1166+
# will be a Slot object and its `value` attribute will be None.
11621167
if slot is not None and slot.as_feature():
11631168
if omit_unset_slots and not slot.has_been_set:
11641169
continue
11651170
if slot.value == rasa.shared.core.constants.SHOULD_NOT_BE_SET:
11661171
slots[slot_name] = rasa.shared.core.constants.SHOULD_NOT_BE_SET
11671172
elif any(slot.as_feature()):
1168-
# only add slot if some of the features are not zero
1173+
# Only include slot in featurised sub-state if the slot is not
1174+
# unset, i.e. is set to some actual value and has been successfully
1175+
# featurized, and hence has at least one non-zero feature.
11691176
slots[slot_name] = tuple(slot.as_feature())
1170-
11711177
return slots
11721178

11731179
@staticmethod

rasa/shared/core/slots.py

+39-19
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,24 @@ def __init__(
343343
super().__init__(
344344
name, initial_value, value_reset_delay, auto_fill, influence_conversation
345345
)
346-
self.values = [str(v).lower() for v in values] if values else []
346+
if values and None in values:
347+
rasa.shared.utils.io.raise_warning(
348+
f"Categorical slot '{self.name}' has `null` listed as a possible value"
349+
f" in the domain file, which translates to `None` in Python. This value"
350+
f" is reserved for when the slot is not set, and should not be listed"
351+
f" as a value in the slot's definition."
352+
f" Rasa will ignore `null` as a possible value for the '{self.name}'"
353+
f" slot. Consider changing this value in your domain file to, for"
354+
f" example, `unset`, or provide the value explicitly as a string by"
355+
f' using quotation marks: "null".',
356+
category=UserWarning,
357+
)
358+
self.values = (
359+
[str(v).lower() for v in values if v is not None] if values else []
360+
)
347361

348362
def add_default_value(self) -> None:
363+
"""Adds the special default value to the list of possible values."""
349364
values = set(self.values)
350365
if rasa.shared.core.constants.DEFAULT_CATEGORICAL_SLOT_VALUE not in values:
351366
self.values.append(
@@ -367,31 +382,36 @@ def persistence_info(self) -> Dict[Text, Any]:
367382
def _as_feature(self) -> List[float]:
368383
r = [0.0] * self.feature_dimensionality()
369384

385+
# Return the zero-filled array if the slot is unset (i.e. set to None).
386+
# Conceptually, this is similar to the case when the featurisation process
387+
# fails, hence the returned features here are the same as for that case.
388+
if self.value is None:
389+
return r
390+
370391
try:
371392
for i, v in enumerate(self.values):
372393
if v == str(self.value).lower():
373394
r[i] = 1.0
374395
break
375396
else:
376-
if self.value is not None:
377-
if (
397+
if (
398+
rasa.shared.core.constants.DEFAULT_CATEGORICAL_SLOT_VALUE
399+
in self.values
400+
):
401+
i = self.values.index(
378402
rasa.shared.core.constants.DEFAULT_CATEGORICAL_SLOT_VALUE
379-
in self.values
380-
):
381-
i = self.values.index(
382-
rasa.shared.core.constants.DEFAULT_CATEGORICAL_SLOT_VALUE
383-
)
384-
r[i] = 1.0
385-
else:
386-
rasa.shared.utils.io.raise_warning(
387-
f"Categorical slot '{self.name}' is set to a value "
388-
f"('{self.value}') "
389-
"that is not specified in the domain. "
390-
"Value will be ignored and the slot will "
391-
"behave as if no value is set. "
392-
"Make sure to add all values a categorical "
393-
"slot should store to the domain."
394-
)
403+
)
404+
r[i] = 1.0
405+
else:
406+
rasa.shared.utils.io.raise_warning(
407+
f"Categorical slot '{self.name}' is set to a value "
408+
f"('{self.value}') "
409+
"that is not specified in the domain. "
410+
"Value will be ignored and the slot will "
411+
"behave as if no value is set. "
412+
"Make sure to add all values a categorical "
413+
"slot should store to the domain."
414+
)
395415
except (TypeError, ValueError):
396416
logger.exception("Failed to featurize categorical slot.")
397417
return r

tests/shared/core/test_slots.py

+36-17
Original file line numberDiff line numberDiff line change
@@ -239,24 +239,28 @@ class TestCategoricalSlot(SlotTestCollection):
239239
def create_slot(self, influence_conversation: bool) -> Slot:
240240
return CategoricalSlot(
241241
"test",
242-
values=[1, "two", "小于", {"three": 3}, None],
242+
values=[1, "two", "小于", {"three": 3}, "nOnE", "None", "null"],
243243
influence_conversation=influence_conversation,
244244
)
245245

246-
@pytest.fixture(params=[{"a": "b"}, 2, True, "asd", "🌴"])
246+
# None is a special value reserved for unset slots.
247+
@pytest.fixture(params=[{"a": "b"}, 2, True, "asd", "🌴", None])
247248
def invalid_value(self, request: SubRequest) -> Any:
248249
return request.param
249250

250251
@pytest.fixture(
251252
params=[
252-
(None, [0, 0, 0, 0, 1]),
253-
(1, [1, 0, 0, 0, 0]),
254-
("two", [0, 1, 0, 0, 0]),
255-
("小于", [0, 0, 1, 0, 0]),
256-
({"three": 3}, [0, 0, 0, 1, 0]),
253+
(None, [0, 0, 0, 0, 0, 0, 0]), # slot is unset
254+
(1, [1, 0, 0, 0, 0, 0, 0]),
255+
("two", [0, 1, 0, 0, 0, 0, 0]),
256+
("小于", [0, 0, 1, 0, 0, 0, 0]),
257+
({"three": 3}, [0, 0, 0, 1, 0, 0, 0]),
258+
("nOnE", [0, 0, 0, 0, 1, 0, 0]),
259+
("None", [0, 0, 0, 0, 1, 0, 0]), # same as for 'nOnE' (case insensivity)
260+
("null", [0, 0, 0, 0, 0, 0, 1]),
257261
(
258262
rasa.shared.core.constants.DEFAULT_CATEGORICAL_SLOT_VALUE,
259-
[0, 0, 0, 0, 0],
263+
[0, 0, 0, 0, 0, 0, 0],
260264
),
261265
]
262266
)
@@ -268,28 +272,32 @@ class TestCategoricalSlotDefaultValue(SlotTestCollection):
268272
def create_slot(self, influence_conversation: bool) -> Slot:
269273
slot = CategoricalSlot(
270274
"test",
271-
values=[1, "two", "小于", {"three": 3}, None],
275+
values=[1, "two", "小于", {"three": 3}, "nOnE", "None", "null"],
272276
influence_conversation=influence_conversation,
273277
)
274278
slot.add_default_value()
275279
return slot
276280

277-
@pytest.fixture(params=[{"a": "b"}, 2, True, "asd", "🌴"])
281+
# None is a special value reserved for unset slots.
282+
@pytest.fixture(params=[{"a": "b"}, 2, True, "asd", "🌴", None])
278283
def invalid_value(self, request: SubRequest) -> Any:
279284
return request.param
280285

281286
@pytest.fixture(
282287
params=[
283-
(None, [0, 0, 0, 0, 1, 0]),
284-
(1, [1, 0, 0, 0, 0, 0]),
285-
("two", [0, 1, 0, 0, 0, 0]),
286-
("小于", [0, 0, 1, 0, 0, 0]),
287-
({"three": 3}, [0, 0, 0, 1, 0, 0]),
288+
(None, [0, 0, 0, 0, 0, 0, 0, 0]), # slot is unset
289+
(1, [1, 0, 0, 0, 0, 0, 0, 0]),
290+
("two", [0, 1, 0, 0, 0, 0, 0, 0]),
291+
("小于", [0, 0, 1, 0, 0, 0, 0, 0]),
292+
({"three": 3}, [0, 0, 0, 1, 0, 0, 0, 0]),
293+
("nOnE", [0, 0, 0, 0, 1, 0, 0, 0]),
294+
("None", [0, 0, 0, 0, 1, 0, 0, 0]), # same as for 'nOnE' (case insensivity)
295+
("null", [0, 0, 0, 0, 0, 0, 1, 0]),
288296
(
289297
rasa.shared.core.constants.DEFAULT_CATEGORICAL_SLOT_VALUE,
290-
[0, 0, 0, 0, 0, 1],
298+
[0, 0, 0, 0, 0, 0, 0, 1],
291299
),
292-
("unseen value", [0, 0, 0, 0, 0, 1]),
300+
("unseen value", [0, 0, 0, 0, 0, 0, 0, 1]),
293301
]
294302
)
295303
def value_feature_pair(self, request: SubRequest) -> Tuple[Any, List[float]]:
@@ -323,3 +331,14 @@ def test_exception_if_featurized(self):
323331
def test_raises_on_invalid_slot_type():
324332
with pytest.raises(InvalidSlotTypeException):
325333
Slot.resolve_by_type("foobar")
334+
335+
336+
def test_categorical_slot_ignores_none_value():
337+
"""Checks that None can't be added as a possible value for categorical slots."""
338+
with pytest.warns(UserWarning) as records:
339+
slot = CategoricalSlot(name="branch", values=["Berlin", None, "San Francisco"])
340+
341+
assert not ("none" in slot.values)
342+
343+
message_text = "Rasa will ignore `null` as a possible value for the 'branch' slot."
344+
assert any(message_text in record.message.args[0] for record in records)

0 commit comments

Comments
 (0)