Files
wehub-resource-sync dc6079821b
Docs Tests / Check for file changes (push) Has been cancelled
Docs Tests / Test Documentation (push) Has been cancelled
Docs Tests / Documentation Linting Checks (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.9) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.9) (push) Has been cancelled
Continuous Integration / Check for file changes (push) Has been cancelled
Continuous Integration / Wait for docs tests (push) Has been cancelled
Continuous Integration / Code Quality (push) Has been cancelled
Continuous Integration / Check for changelog (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Upload coverage reports to codeclimate (push) Has been cancelled
Continuous Integration / Run Non-Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Run Broker Integration Tests (push) Has been cancelled
Continuous Integration / Run Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Build Docker base images and setup environment (push) Has been cancelled
Continuous Integration / Build Docker (default) (push) Has been cancelled
Continuous Integration / Build Docker (full) (push) Has been cancelled
Continuous Integration / Build Docker (mitie-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-de) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-it) (push) Has been cancelled
Continuous Integration / Deploy to PyPI (push) Has been cancelled
Continuous Integration / Notify Slack & Publish Release Notes (push) Has been cancelled
Publish Documentation / Evaluate release tag (push) Has been cancelled
Publish Documentation / Prebuild Docs (push) Has been cancelled
Publish Documentation / Preview Docs (push) Has been cancelled
Publish Documentation / Check for file changes (push) Has been cancelled
Publish Documentation / Publish Docs (push) Has been cancelled
Automatic PR Merger / mergepal (push) Has been cancelled
CI Github Actions / Run Tests (push) Has been cancelled
Semgrep / Semgrep Workflow Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:47 +08:00

2148 lines
61 KiB
Python

import logging
import textwrap
from typing import Dict, Text, List, Any, Union
from unittest.mock import Mock
import pytest
from _pytest.logging import LogCaptureFixture
from _pytest.monkeypatch import MonkeyPatch
from aioresponses import aioresponses
from rasa.core.agent import Agent
from rasa.core.policies.policy import PolicyPrediction
from rasa.core.actions import action
from rasa.core.actions.action import ActionExecutionRejection, ActionExtractSlots
from rasa.shared.constants import (
LATEST_TRAINING_DATA_FORMAT_VERSION,
REQUIRED_SLOTS_KEY,
IGNORED_INTENTS,
)
from rasa.shared.core.constants import ACTION_LISTEN_NAME, REQUESTED_SLOT
from rasa.core.actions.forms import FormAction
from rasa.core.channels import CollectingOutputChannel
from rasa.shared.core.domain import Domain
from rasa.shared.core.events import (
ActiveLoop,
SlotSet,
UserUttered,
ActionExecuted,
BotUttered,
Restarted,
Event,
ActionExecutionRejected,
DefinePrevUserUtteredFeaturization,
)
from rasa.core.nlg import TemplatedNaturalLanguageGenerator
from rasa.shared.core.trackers import DialogueStateTracker
from rasa.utils.endpoints import EndpointConfig
ACTION_SERVER_URL = "http://my-action-server:5055/webhook"
async def test_activate():
tracker = DialogueStateTracker.from_events(sender_id="bla", evts=[])
form_name = "my form"
action = FormAction(form_name, None)
slot_name = "num_people"
domain = textwrap.dedent(
f"""
slots:
{slot_name}:
type: float
mappings:
- type: from_entity
entity: number
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
responses:
utter_ask_num_people:
- text: "How many people?"
"""
)
domain = Domain.from_yaml(domain)
events = await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert isinstance(events[-1], BotUttered)
assert events[:-1] == [ActiveLoop(form_name), SlotSet(REQUESTED_SLOT, slot_name)]
async def test_activate_with_custom_slot_mapping():
tracker = DialogueStateTracker.from_events(sender_id="bla", evts=[])
form_name = "my_form"
action_server = EndpointConfig(ACTION_SERVER_URL)
action = FormAction(form_name, action_server)
domain_required_slot_name = "num_people"
slot_set_by_remote_custom_extraction_method = "some_slot"
slot_value_set_by_remote_custom_extraction_method = "anything"
domain = textwrap.dedent(
f"""
slots:
{domain_required_slot_name}:
type: float
mappings:
- type: from_entity
entity: number
{slot_set_by_remote_custom_extraction_method}:
type: any
mappings:
- type: custom
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {domain_required_slot_name}
responses:
utter_ask_num_people:
- text: "How many people?"
actions:
- validate_{form_name}
"""
)
domain = Domain.from_yaml(domain)
form_validation_events = [
{
"event": "slot",
"timestamp": None,
"name": slot_set_by_remote_custom_extraction_method,
"value": slot_value_set_by_remote_custom_extraction_method,
}
]
with aioresponses() as mocked:
mocked.post(
ACTION_SERVER_URL,
payload={"events": form_validation_events},
)
events = await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events[:-1] == [
ActiveLoop(form_name),
SlotSet(
slot_set_by_remote_custom_extraction_method,
slot_value_set_by_remote_custom_extraction_method,
),
SlotSet(REQUESTED_SLOT, domain_required_slot_name),
]
assert isinstance(events[-1], BotUttered)
async def test_activate_with_mapping_conditions_slot():
tracker = DialogueStateTracker.from_events(sender_id="bla", evts=[])
form_name = "my form"
action = FormAction(form_name, None)
slot_name = "num_people"
domain = textwrap.dedent(
f"""
slots:
{slot_name}:
type: float
mappings:
- type: from_entity
entity: number
conditions:
- active_loop: {form_name}
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
responses:
utter_ask_num_people:
- text: "How many people?"
"""
)
domain = Domain.from_yaml(domain)
events = await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events[:-1] == [ActiveLoop(form_name), SlotSet(REQUESTED_SLOT, slot_name)]
assert isinstance(events[-1], BotUttered)
async def test_activate_with_prefilled_slot():
slot_name = "num_people"
slot_value = 5
tracker = DialogueStateTracker.from_events(
sender_id="bla", evts=[SlotSet(slot_name, slot_value)]
)
form_name = "my_form"
action = FormAction(form_name, None)
next_slot_to_request = "next slot to request"
domain = f"""
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
- {next_slot_to_request}
slots:
{slot_name}:
type: any
mappings:
- type: from_entity
entity: {slot_name}
{next_slot_to_request}:
type: text
mappings:
- type: from_text
"""
domain = Domain.from_yaml(domain)
events = await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == [
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, next_slot_to_request),
]
async def test_activate_with_prefilled_slot_with_mapping_conditions():
slot_name = "num_people"
slot_value = 5
tracker = DialogueStateTracker.from_events(
sender_id="bla", evts=[SlotSet(slot_name, slot_value)]
)
form_name = "my form"
action = FormAction(form_name, None)
next_slot_to_request = "next slot to request"
domain = f"""
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
- {next_slot_to_request}
slots:
{slot_name}:
type: any
mappings:
- type: from_entity
entity: {slot_name}
conditions:
- active_loop: {form_name}
{next_slot_to_request}:
type: text
mappings:
- type: from_text
conditions:
- active_loop: {form_name}
"""
domain = Domain.from_yaml(domain)
events = await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == [
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, next_slot_to_request),
]
async def test_switch_forms_with_same_slot(default_agent: Agent):
"""Tests switching of forms, where the first slot is the same in both forms.
Tests the fix for issue 7710"""
# Define two forms in the domain, with same first slot
slot_a = "my_slot_a"
form_1 = "my_form_1"
utter_ask_form_1 = f"Please provide the value for {slot_a} of form 1"
form_2 = "my_form_2"
utter_ask_form_2 = f"Please provide the value for {slot_a} of form 2"
domain = f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
nlu:
- intent: order_status
examples: |
- check status of my order
- when are my shoes coming in
- intent: return
examples: |
- start a return
- I don't want my shoes anymore
slots:
{slot_a}:
type: float
mappings:
- type: from_entity
entity: number
forms:
{form_1}:
{REQUIRED_SLOTS_KEY}:
- {slot_a}
{form_2}:
{REQUIRED_SLOTS_KEY}:
- {slot_a}
responses:
utter_ask_{form_1}_{slot_a}:
- text: {utter_ask_form_1}
utter_ask_{form_2}_{slot_a}:
- text: {utter_ask_form_2}
"""
domain = Domain.from_yaml(domain)
# Driving it like rasa/core/processor
processor = default_agent.processor
processor.domain = domain
# activate the first form
tracker = DialogueStateTracker.from_events(
"some-sender",
evts=[
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered("order status", {"name": "form_1", "confidence": 1.0}),
DefinePrevUserUtteredFeaturization(False),
],
)
# rasa/core/processor.predict_next_action
prediction = PolicyPrediction([], "some_policy")
action_1 = FormAction(form_1, None)
await processor._run_action(
action_1,
tracker,
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
prediction,
)
events_expected = [
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered("order status", {"name": "form_1", "confidence": 1.0}),
DefinePrevUserUtteredFeaturization(False),
ActionExecuted(form_1),
ActiveLoop(form_1),
SlotSet(REQUESTED_SLOT, slot_a),
BotUttered(
text=utter_ask_form_1,
metadata={"utter_action": f"utter_ask_{form_1}_{slot_a}"},
),
]
assert tracker.applied_events() == events_expected
next_events = [
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered("return my shoes", {"name": "form_2", "confidence": 1.0}),
DefinePrevUserUtteredFeaturization(False),
]
tracker.update_with_events(next_events, domain)
events_expected.extend(next_events)
# form_1 is still active, and bot will first validate if the user utterance
# provides valid data for the requested slot, which is rejected
await processor._run_action(
action_1,
tracker,
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
prediction,
)
events_expected.extend([ActionExecutionRejected(action_name=form_1)])
assert tracker.applied_events() == events_expected
# Next, bot predicts form_2
action_2 = FormAction(form_2, None)
await processor._run_action(
action_2,
tracker,
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
prediction,
)
events_expected.extend(
[
ActionExecuted(form_2),
ActiveLoop(form_2),
SlotSet(REQUESTED_SLOT, slot_a),
BotUttered(
text=utter_ask_form_2,
metadata={"utter_action": f"utter_ask_{form_2}_{slot_a}"},
),
]
)
assert tracker.applied_events() == events_expected
async def test_activate_and_immediate_deactivate():
slot_name = "num_people"
slot_value = 5
tracker = DialogueStateTracker.from_events(
sender_id="bla",
evts=[
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(
"haha",
{"name": "greet"},
entities=[{"entity": slot_name, "value": slot_value}],
),
SlotSet(slot_name, slot_value),
],
)
form_name = "my form"
action = FormAction(form_name, None)
domain = f"""
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
slots:
{slot_name}:
type: any
mappings:
- type: from_entity
entity: {slot_name}
"""
domain = Domain.from_yaml(domain)
events = await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == [
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
]
async def test_set_slot_and_deactivate():
form_name = "my form"
slot_name = "num_people"
slot_value = "dasdasdfasdf"
events = [
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, slot_name),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(slot_value),
SlotSet(slot_name, slot_value),
]
tracker = DialogueStateTracker.from_events(sender_id="bla", evts=events)
domain = f"""
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
slots:
{slot_name}:
type: text
influence_conversation: false
mappings:
- type: from_text
"""
domain = Domain.from_yaml(domain)
form_action = FormAction(form_name, None)
events = await form_action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == [SlotSet(REQUESTED_SLOT, None), ActiveLoop(None)]
async def test_action_rejection():
form_name = "my form"
slot_to_fill = "some slot"
tracker = DialogueStateTracker.from_events(
sender_id="bla",
evts=[
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, slot_to_fill),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered("haha", {"name": "greet"}),
],
)
form_name = "my form"
action = FormAction(form_name, None)
domain = f"""
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_to_fill}
slots:
{slot_to_fill}:
type: any
mappings:
- type: from_entity
entity: some_entity
"""
domain = Domain.from_yaml(domain)
with pytest.raises(ActionExecutionRejection):
await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
@pytest.mark.parametrize(
"validate_return_events, expected_events",
[
# Validate function returns SlotSet events for every slot to fill
(
[
{"event": "slot", "name": "num_people", "value": "so_clean"},
{"event": "slot", "name": "num_tables", "value": 5},
],
[
SlotSet("num_people", "so_clean"),
SlotSet("num_tables", 5),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
),
# Validate function returns extra Slot Event
(
[
{"event": "slot", "name": "num_people", "value": "so_clean"},
{"event": "slot", "name": "some_other_slot", "value": 2},
],
[
SlotSet("num_people", "so_clean"),
SlotSet("some_other_slot", 2),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
),
# Validate function only validates one of the candidates
(
[{"event": "slot", "name": "num_people", "value": "so_clean"}],
[
SlotSet("num_people", "so_clean"),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
),
# Validate function says slot is invalid
(
[{"event": "slot", "name": "num_people", "value": None}],
[SlotSet("num_people", None), SlotSet(REQUESTED_SLOT, "num_people")],
),
# Validate function decides to request a slot which is not part of the default
# slot mapping
(
[{"event": "slot", "name": "requested_slot", "value": "is_outside"}],
[SlotSet(REQUESTED_SLOT, "is_outside")],
),
# Validate function decides that no more slots should be requested
(
[
{"event": "slot", "name": "num_people", "value": None},
{"event": "slot", "name": REQUESTED_SLOT, "value": None},
],
[
SlotSet("num_people", None),
SlotSet(REQUESTED_SLOT, None),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
),
# Validate function deactivates loop
(
[
{"event": "slot", "name": "num_people", "value": None},
{"event": "active_loop", "name": None},
],
[
SlotSet("num_people", None),
ActiveLoop(None),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
),
# User rejected manually
(
[{"event": "action_execution_rejected", "name": "my form"}],
[ActionExecutionRejected("my form")],
),
],
)
async def test_validate_slots(
validate_return_events: List[Dict], expected_events: List[Event]
):
form_name = "my form"
slot_name = "num_people"
slot_value = "hi"
events = [
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, slot_name),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(slot_value, entities=[{"entity": "num_tables", "value": 5}]),
]
tracker = DialogueStateTracker.from_events(sender_id="bla", evts=events)
domain = f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
entities:
- num_tables
- some_entity
- some_other_entity
- some_slot
slots:
{slot_name}:
type: any
mappings:
- type: from_text
num_tables:
type: any
mappings:
- type: from_entity
entity: num_tables
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
- num_tables
actions:
- validate_{form_name}
"""
domain = Domain.from_yaml(textwrap.dedent(domain))
action_extract_slots = ActionExtractSlots(action_endpoint=None)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert slot_events == [SlotSet(slot_name, slot_value), SlotSet("num_tables", 5)]
tracker.update_with_events(slot_events, domain)
with aioresponses() as mocked:
mocked.post(ACTION_SERVER_URL, payload={"events": validate_return_events})
action_server = EndpointConfig(ACTION_SERVER_URL)
action = FormAction(form_name, action_server)
events = await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == expected_events
async def test_request_correct_slots_after_unhappy_path_with_custom_required_slots():
form_name = "some_form"
slot_name_1 = "slot_1"
slot_name_2 = "slot_2"
domain = f"""
slots:
{slot_name_1}:
type: any
mappings:
- type: from_intent
intent: some_intent
value: some_value
{slot_name_2}:
type: any
mappings:
- type: from_intent
intent: some_intent
value: some_value
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name_1}
- {slot_name_2}
actions:
- validate_{form_name}
"""
domain = Domain.from_yaml(domain)
tracker = DialogueStateTracker.from_events(
"default",
[
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, "slot_2"),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered("hello", intent={"name": "greet", "confidence": 1.0}),
ActionExecutionRejected(form_name),
ActionExecuted("utter_greet"),
],
)
# Custom form validation action changes the order of the requested slots
validate_return_events = [
{"event": "slot", "name": REQUESTED_SLOT, "value": slot_name_2}
]
# The form should ask the same slot again when coming back after unhappy path
expected_events = [SlotSet(REQUESTED_SLOT, slot_name_2)]
with aioresponses() as mocked:
mocked.post(ACTION_SERVER_URL, payload={"events": validate_return_events})
action_server = EndpointConfig(ACTION_SERVER_URL)
action = FormAction(form_name, action_server)
events = await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == expected_events
@pytest.mark.parametrize(
"custom_events",
[
# Custom action returned no events
[],
# Custom action returned events but no `SlotSet` events
[BotUttered("some text").as_dict()],
# Custom action returned only `SlotSet` event for `required_slot`
[SlotSet(REQUESTED_SLOT, "some value").as_dict()],
],
)
async def test_no_slots_extracted_with_custom_slot_mappings(custom_events: List[Event]):
form_name = "my form"
events = [
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, "num_tables"),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered("off topic"),
]
tracker = DialogueStateTracker.from_events(sender_id="bla", evts=events)
domain = f"""
slots:
num_tables:
type: any
mappings:
- type: from_entity
entity: num_tables
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- num_tables
actions:
- validate_{form_name}
"""
domain = Domain.from_yaml(domain)
with aioresponses() as mocked:
mocked.post(ACTION_SERVER_URL, payload={"events": custom_events})
action_server = EndpointConfig(ACTION_SERVER_URL)
action = FormAction(form_name, action_server)
with pytest.raises(ActionExecutionRejection):
await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
async def test_validate_slots_on_activation_with_other_action_after_user_utterance():
form_name = "my form"
slot_name = "num_people"
slot_value = "hi"
events = [
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(slot_value, entities=[{"entity": "num_tables", "value": 5}]),
ActionExecuted("action_in_between"),
]
tracker = DialogueStateTracker.from_events(sender_id="bla", evts=events)
domain = f"""
slots:
{slot_name}:
type: any
mappings:
- type: from_text
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
actions:
- validate_{form_name}
"""
domain = Domain.from_yaml(domain)
expected_slot_value = "✅"
with aioresponses() as mocked:
mocked.post(
ACTION_SERVER_URL,
payload={
"events": [
{"event": "slot", "name": slot_name, "value": expected_slot_value}
]
},
)
action_server = EndpointConfig(ACTION_SERVER_URL)
action_extract_slots = ActionExtractSlots(action_endpoint=None)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
tracker.update_with_events(slot_events, domain)
form_action = FormAction(form_name, action_server)
mocked.post(
ACTION_SERVER_URL,
payload={"events": []},
)
events = await form_action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == [
ActiveLoop(form_name),
SlotSet(slot_name, expected_slot_value),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
]
@pytest.mark.parametrize(
"utterance_name", ["utter_ask_my_form_num_people", "utter_ask_num_people"]
)
def test_name_of_utterance(utterance_name: Text):
form_name = "my_form"
slot_name = "num_people"
domain = f"""
slots:
{slot_name}:
type: any
mappings:
- type: from_text
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
responses:
{utterance_name}:
- text: "How many people?"
"""
domain = Domain.from_yaml(domain)
action = FormAction(form_name, None)
assert action._name_of_utterance(domain, slot_name) == utterance_name
def test_temporary_tracker():
extra_slot = "some_slot"
sender_id = "test"
domain = Domain.from_yaml(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
slots:
{extra_slot}:
type: any
mappings:
- type: from_text
"""
)
previous_events = [ActionExecuted(ACTION_LISTEN_NAME)]
old_tracker = DialogueStateTracker.from_events(
sender_id, previous_events, slots=domain.slots
)
new_events = [Restarted()]
form_action = FormAction("some name", None)
temp_tracker = form_action._temporary_tracker(old_tracker, new_events, domain)
assert extra_slot in temp_tracker.slots.keys()
assert list(temp_tracker.events) == [
*previous_events,
SlotSet(REQUESTED_SLOT),
ActionExecuted(form_action.name()),
*new_events,
]
async def test_extract_requested_slot_default():
"""Test default extraction of a slot value from entity with the same name."""
form_name = "some_form"
form = FormAction(form_name, None)
domain = Domain.from_dict(
{
"slots": {
"some_slot": {
"type": "text",
"mappings": [
{
"type": "from_entity",
"entity": "some_slot",
"value": "some_value",
}
],
}
},
"forms": {form_name: {REQUIRED_SLOTS_KEY: ["some_slot"]}},
}
)
tracker = DialogueStateTracker.from_events(
"default",
[
ActiveLoop("some form"),
SlotSet(REQUESTED_SLOT, "some_slot"),
UserUttered(
"bla", entities=[{"entity": "some_slot", "value": "some_value"}]
),
SlotSet("some_slot", "some_value"),
ActionExecuted(ACTION_LISTEN_NAME),
],
)
slot_values = await form.validate(
tracker,
domain,
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
)
assert slot_values == []
@pytest.mark.parametrize(
"some_other_slot_mapping, some_slot_mapping, entities, "
"intent, expected_slot_events",
[
(
[
{
"type": "from_entity",
"intent": "some_intent",
"entity": "some_entity",
"role": "some_role",
}
],
[{"type": "from_entity", "intent": "some_intent", "entity": "some_entity"}],
[
{
"entity": "some_entity",
"value": "some_value",
"role": "some_other_role",
}
],
"some_intent",
[],
),
(
[
{
"type": "from_entity",
"intent": "some_intent",
"entity": "some_entity",
"role": "some_role",
}
],
[{"type": "from_entity", "intent": "some_intent", "entity": "some_entity"}],
[{"entity": "some_entity", "value": "some_value", "role": "some_role"}],
"some_intent",
[SlotSet("some_other_slot", "some_value")],
),
(
[
{
"type": "from_entity",
"intent": "some_intent",
"entity": "some_entity",
"group": "some_group",
}
],
[{"type": "from_entity", "intent": "some_intent", "entity": "some_entity"}],
[
{
"entity": "some_entity",
"value": "some_value",
"group": "some_other_group",
}
],
"some_intent",
[],
),
(
[
{
"type": "from_entity",
"intent": "some_intent",
"entity": "some_entity",
"group": "some_group",
}
],
[{"type": "from_entity", "intent": "some_intent", "entity": "some_entity"}],
[{"entity": "some_entity", "value": "some_value", "group": "some_group"}],
"some_intent",
[SlotSet("some_other_slot", "some_value")],
),
(
[
{
"type": "from_entity",
"intent": "some_intent",
"entity": "some_entity",
"group": "some_group",
"role": "some_role",
}
],
[{"type": "from_entity", "intent": "some_intent", "entity": "some_entity"}],
[
{
"entity": "some_entity",
"value": "some_value",
"role": "some_role",
"group": "some_group",
}
],
"some_intent",
[SlotSet("some_other_slot", "some_value")],
),
(
[{"type": "from_entity", "intent": "some_intent", "entity": "some_entity"}],
[
{
"type": "from_entity",
"intent": "some_intent",
"entity": "some_other_entity",
}
],
[{"entity": "some_entity", "value": "some_value"}],
"some_intent",
# other slot should be extracted because slot mapping is unique
[SlotSet("some_other_slot", "some_value")],
),
(
[
{
"type": "from_entity",
"intent": "some_intent",
"entity": "some_entity",
"role": "some_role",
}
],
[
{
"type": "from_entity",
"intent": "some_intent",
"entity": "some_other_entity",
}
],
[{"entity": "some_entity", "value": "some_value", "role": "some_role"}],
"some_intent",
# other slot should be extracted because slot mapping is unique
[SlotSet("some_other_slot", "some_value")],
),
(
[{"type": "from_entity", "intent": "some_intent", "entity": "some_entity"}],
[
{
"type": "from_entity",
"intent": "some_intent",
"entity": "some_other_entity",
}
],
[{"entity": "some_entity", "value": "some_value", "role": "some_role"}],
"some_intent",
# other slot should not be extracted
# because even though slot mapping is unique it doesn't contain the role
[],
),
(
[{"type": "from_entity", "intent": "some_intent", "entity": "some_entity"}],
[{"type": "from_entity", "intent": "some_intent", "entity": "some_entity"}],
[{"entity": "some_entity", "value": "some_value"}],
"some_intent",
# other slot should not be extracted because slot mapping is not unique
[SlotSet("some_slot", "some_value")],
),
],
)
async def test_extract_other_slots_with_entity(
some_other_slot_mapping: List[Dict[Text, Any]],
some_slot_mapping: List[Dict[Text, Any]],
entities: List[Dict[Text, Any]],
intent: Text,
expected_slot_events: List[SlotSet],
):
"""Test extraction of other not requested slots values from entities."""
form_name = "some_form"
form = FormAction(form_name, None)
domain = Domain.from_dict(
{
"slots": {
"some_other_slot": {"type": "any", "mappings": some_other_slot_mapping},
"some_slot": {"type": "any", "mappings": some_slot_mapping},
},
"forms": {
form_name: {REQUIRED_SLOTS_KEY: ["some_other_slot", "some_slot"]}
},
}
)
tracker = DialogueStateTracker.from_events(
"default",
[
ActiveLoop("some_form"),
SlotSet(REQUESTED_SLOT, "some_slot"),
UserUttered(
"bla", intent={"name": intent, "confidence": 1.0}, entities=entities
),
ActionExecuted(ACTION_LISTEN_NAME),
],
)
action_extract_slots = ActionExtractSlots(action_endpoint=None)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
tracker.update_with_events(slot_events, domain)
if slot_events:
slot_values = await form.validate(
tracker,
domain,
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
)
assert slot_values == expected_slot_events
else:
with pytest.raises(ActionExecutionRejection):
await form.validate(
tracker,
domain,
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
)
@pytest.mark.parametrize(
"domain_dict, expected_action",
[
(
{
"actions": ["action_ask_my_form_sun", "action_ask_sun"],
"responses": {"utter_ask_my_form_sun": [{"text": "ask"}]},
},
"action_ask_my_form_sun",
),
(
{
"actions": ["action_ask_sun"],
"responses": {"utter_ask_my_form_sun": [{"text": "ask"}]},
},
"utter_ask_my_form_sun",
),
(
{
"actions": ["action_ask_sun"],
"responses": {"utter_ask_sun": [{"text": "hi"}]},
},
"action_ask_sun",
),
(
{
"actions": ["action_ask_my_form_sun"],
"responses": {"utter_ask_my_form_sun": [{"text": "hi"}]},
},
"action_ask_my_form_sun",
),
],
)
async def test_ask_for_slot(
domain_dict: Dict,
expected_action: Text,
monkeypatch: MonkeyPatch,
default_nlg: TemplatedNaturalLanguageGenerator,
):
slot_name = "sun"
action_from_name = Mock(return_value=action.ActionListen())
endpoint_config = Mock()
monkeypatch.setattr(
action, action.action_for_name_or_text.__name__, action_from_name
)
form = FormAction("my_form", endpoint_config)
domain = Domain.from_dict(domain_dict)
await form._ask_for_slot(
domain,
default_nlg,
CollectingOutputChannel(),
slot_name,
DialogueStateTracker.from_events("dasd", []),
)
action_from_name.assert_called_once_with(expected_action, domain, endpoint_config)
async def test_ask_for_slot_if_not_utter_ask(
monkeypatch: MonkeyPatch, default_nlg: TemplatedNaturalLanguageGenerator
):
action_from_name = Mock(return_value=action.ActionListen())
endpoint_config = Mock()
monkeypatch.setattr(
action, action.action_for_name_or_text.__name__, action_from_name
)
form = FormAction("my_form", endpoint_config)
events = await form._ask_for_slot(
Domain.empty(),
default_nlg,
CollectingOutputChannel(),
"some slot",
DialogueStateTracker.from_events("dasd", []),
)
assert not events
action_from_name.assert_not_called()
@pytest.mark.parametrize(
"ignored_intents, slot_not_intent",
[
# for entity_type -> from_entity
(
# `ignored_intents` as a string and slot's not_intent as an empty list.
"greet",
[],
),
(
# `ignored_intents` as an empty list and slot's not_intent has a value.
[],
["greet"],
),
(
# `ignored_intents` as a list of 2 values and slot's not_intent has one
# value different than the ones in `ignored_intents`.
["chitchat", "greet"],
["inform"],
),
(
# `ignored_intents` as a list of 2 values and slot's not_intent has one
# value that is included also in `ignored_intents`.
["chitchat", "greet"],
["chitchat"],
),
],
)
async def test_ignored_intents_with_slot_type_from_entity(
ignored_intents: Union[Text, List[Text]], slot_not_intent: Union[Text, List[Text]]
):
form_name = "some_form"
entity_name = "some_slot"
form = FormAction(form_name, None)
domain = Domain.from_dict(
{
"slots": {
entity_name: {
"type": "any",
"mappings": [
{
"type": "from_entity",
"entity": entity_name,
"not_intent": slot_not_intent,
}
],
}
},
"forms": {
form_name: {
IGNORED_INTENTS: ignored_intents,
REQUIRED_SLOTS_KEY: [entity_name],
}
},
}
)
tracker = DialogueStateTracker.from_events(
"default",
[
ActiveLoop("some_form"),
SlotSet(REQUESTED_SLOT, "some_slot"),
UserUttered(
"hello",
intent={"name": "greet", "confidence": 1.0},
entities=[{"entity": entity_name, "value": "some_value"}],
),
ActionExecuted(ACTION_LISTEN_NAME),
],
)
action_extract_slots = ActionExtractSlots(action_endpoint=None)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
tracker.update_with_events(slot_events, domain)
assert slot_events == []
with pytest.raises(ActionExecutionRejection):
await form.validate(
tracker,
domain,
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
)
@pytest.mark.parametrize(
"ignored_intents, slot_not_intent",
[
# same examples for entity_type -> from_text
(
# `ignored_intents` as a string and slot's not_intent as an empty list.
"greet",
[],
),
(
# `ignored_intents` as an empty list and slot's not_intent has a value.
[],
["greet"],
),
(
# `ignored_intents` as a list of 2 values and slot's not_intent has one
# value different than the ones in `ignored_intents`.
["chitchat", "greet"],
["inform"],
),
(
# `ignored_intents` as a list of 2 values and slot's not_intent has one
# value that is included also in `ignored_intents`.
["chitchat", "greet"],
["chitchat"],
),
],
)
async def test_ignored_intents_with_slot_type_from_text(
ignored_intents: Union[Text, List[Text]], slot_not_intent: Union[Text, List[Text]]
):
form_name = "some_form"
entity_name = "some_slot"
form = FormAction(form_name, None)
domain = Domain.from_dict(
{
"slots": {
entity_name: {
"type": "any",
"mappings": [
{
"type": "from_text",
"intent": "some_intent",
"not_intent": slot_not_intent,
}
],
}
},
"forms": {
form_name: {
IGNORED_INTENTS: ignored_intents,
REQUIRED_SLOTS_KEY: [entity_name],
}
},
}
)
tracker = DialogueStateTracker.from_events(
"default",
[
SlotSet(REQUESTED_SLOT, "some_slot"),
UserUttered(
"hello",
intent={"name": "greet", "confidence": 1.0},
entities=[{"entity": entity_name, "value": "some_value"}],
),
ActionExecuted(ACTION_LISTEN_NAME),
],
)
action_extract_slots = ActionExtractSlots(action_endpoint=None)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
tracker.update_with_events(slot_events, domain)
assert slot_events == []
form_slot_events = await form.validate(
tracker,
domain,
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
)
assert form_slot_events == []
@pytest.mark.parametrize(
"ignored_intents, slot_not_intent, entity_type",
[
# same examples for entity_type -> from_intent
(
# `ignored_intents` as a string and slot's not_intent as an empty list.
"greet",
[],
"from_intent",
),
(
# `ignored_intents` as an empty list and slot's not_intent has a value.
[],
["greet"],
"from_intent",
),
(
# `ignored_intents` as a list of 2 values and slot's not_intent has one
# value different than the ones in `ignored_intents`.
["chitchat", "greet"],
["inform"],
"from_intent",
),
(
# `ignored_intents` as a list of 2 values and slot's not_intent has one
# value that is included also in `ignored_intents`.
["chitchat", "greet"],
["chitchat"],
"from_intent",
),
# same examples for entity_type -> from_trigger_intent
(
# `ignored_intents` as a string and slot's not_intent as an empty list.
"greet",
[],
"from_trigger_intent",
),
(
# `ignored_intents` as an empty list and slot's not_intent has a value.
[],
["greet"],
"from_trigger_intent",
),
(
# `ignored_intents` as a list of 2 values and slot's not_intent has one
# value different than the ones in `ignored_intents`.
["chitchat", "greet"],
["inform"],
"from_trigger_intent",
),
(
# `ignored_intents` as a list of 2 values and slot's not_intent has one
# value that is included also in `ignored_intents`.
["chitchat", "greet"],
["chitchat"],
"from_trigger_intent",
),
],
)
async def test_ignored_intents_with_other_type_of_slots(
ignored_intents: Union[Text, List[Text]],
slot_not_intent: Union[Text, List[Text]],
entity_type: Text,
):
form_name = "some_form"
entity_name = "some_slot"
form = FormAction(form_name, None)
domain = Domain.from_dict(
{
"slots": {
entity_name: {
"type": "any",
"mappings": [
{
"type": entity_type,
"value": "affirm",
"intent": "true",
"not_intent": slot_not_intent,
}
],
}
},
"forms": {
form_name: {
IGNORED_INTENTS: ignored_intents,
REQUIRED_SLOTS_KEY: [entity_name],
}
},
}
)
tracker = DialogueStateTracker.from_events(
"default",
[
SlotSet(REQUESTED_SLOT, "some_slot"),
UserUttered(
"hello",
intent={"name": "greet", "confidence": 1.0},
entities=[{"entity": entity_name, "value": "some_value"}],
),
ActionExecuted(ACTION_LISTEN_NAME),
],
)
action_extract_slots = ActionExtractSlots(action_endpoint=None)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
tracker.update_with_events(slot_events, domain)
assert slot_events == []
form_slot_events = await form.validate(
tracker,
domain,
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
)
assert form_slot_events == []
async def test_extract_other_slots_with_matched_mapping_conditions():
form_name = "some_form"
form = FormAction(form_name, None)
domain = Domain.from_yaml(
textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intent:
- greet
- inform
entities:
- email
- name
slots:
name:
type: text
influence_conversation: false
mappings:
- type: from_entity
entity: name
conditions:
- active_loop: some_form
requested_slot: email
email:
type: text
influence_conversation: false
mappings:
- type: from_entity
entity: email
forms:
some_form:
required_slots:
- email
- name
"""
)
)
tracker = DialogueStateTracker.from_events(
"default",
[
ActiveLoop("some_form"),
SlotSet(REQUESTED_SLOT, "email"),
UserUttered(
"My name is Emily.",
intent={"name": "inform", "confidence": 1.0},
entities=[{"entity": "name", "value": "Emily"}],
),
ActionExecuted(ACTION_LISTEN_NAME),
],
)
action_extract_slots = ActionExtractSlots(action_endpoint=None)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert slot_events == [SlotSet("name", "Emily")]
tracker.update_with_events(slot_events, domain)
form_slot_events = await form.validate(
tracker,
domain,
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
)
assert form_slot_events == []
async def test_extract_other_slots_raises_no_matched_conditions():
form_name = "some_form"
form = FormAction(form_name, None)
domain = Domain.from_yaml(
textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intent:
- greet
- inform
entities:
- email
- name
slots:
name:
type: text
influence_conversation: false
mappings:
- type: from_entity
entity: name
conditions:
- active_loop: some_form
requested_slot: name
email:
type: text
influence_conversation: false
mappings:
- type: from_entity
entity: email
forms:
some_form:
required_slots:
- email
- name
"""
)
)
tracker = DialogueStateTracker.from_events(
"default",
[
ActiveLoop("some_form"),
SlotSet(REQUESTED_SLOT, "email"),
UserUttered(
"My name is Emily.",
intent={"name": "inform", "confidence": 1.0},
entities=[{"entity": "name", "value": "Emily"}],
),
ActionExecuted(ACTION_LISTEN_NAME),
],
)
action_extract_slots = ActionExtractSlots(action_endpoint=None)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
tracker.update_with_events(slot_events, domain)
assert slot_events == []
with pytest.raises(ActionExecutionRejection):
await form.validate(
tracker,
domain,
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
)
async def test_action_extract_slots_custom_mapping_with_condition():
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
slots:
custom_slot:
type: text
influence_conversation: false
mappings:
- type: custom
conditions:
- active_loop: some_form
forms:
some_form:
required_slots:
- custom_slot
actions:
- validate_some_form
"""
)
domain = Domain.from_yaml(domain_yaml)
events = [ActiveLoop("some_form"), UserUttered("Hi")]
tracker = DialogueStateTracker.from_events(
sender_id="test_id", evts=events, slots=domain.slots
)
with aioresponses() as mocked:
mocked.post(
ACTION_SERVER_URL,
payload={
"events": [{"event": "slot", "name": "custom_slot", "value": "test"}]
},
)
action_server = EndpointConfig(ACTION_SERVER_URL)
action_extract_slots = ActionExtractSlots(action_server)
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == []
form = FormAction("some_form", action_server)
form_events = await form.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert form_events == [
SlotSet("custom_slot", "test"),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
]
async def test_form_slots_empty_with_restart():
domain = Domain.from_yaml(
textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intent:
- greet
- inform
entities:
- name
slots:
name:
type: text
influence_conversation: false
mappings:
- type: from_entity
entity: name
conditions:
- active_loop: some_form
forms:
some_form:
required_slots:
- name
"""
)
)
tracker = DialogueStateTracker.from_events(
"default",
[
UserUttered("hi", intent={"name": "greet", "confidence": 1.0}),
ActiveLoop("some_form"),
SlotSet(REQUESTED_SLOT, "name"),
UserUttered(
"My name is Emily.",
intent={"name": "inform", "confidence": 1.0},
entities=[{"entity": "name", "value": "Emily"}],
),
SlotSet("name", "emily"),
Restarted(),
ActiveLoop("some_form"),
SlotSet(REQUESTED_SLOT, "name"),
UserUttered("hi", intent={"name": "greet", "confidence": 1.0}),
ActionExecuted(ACTION_LISTEN_NAME),
],
)
action = FormAction("some_form", None)
# FormAction execution is rejected because a slot was requested but none
# were extracted (events before restart are not considered).
with pytest.raises(ActionExecutionRejection):
await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
async def test_extract_slots_with_mapping_conditions_during_form_activation():
slot_name = "city"
entity_value = "London"
entity_name = "location"
form_name = "test_form"
domain = Domain.from_yaml(
f"""
entities:
- {entity_name}
slots:
{slot_name}:
type: text
mappings:
- type: from_entity
entity: {entity_name}
conditions:
- active_loop: {form_name}
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
"""
)
events = [
ActionExecuted("action_listen"),
UserUttered(
"I live in London",
entities=[{"entity": entity_name, "value": entity_value}],
),
]
tracker = DialogueStateTracker.from_events(
sender_id="test", evts=events, domain=domain, slots=domain.slots
)
assert tracker.active_loop_name is None
action_extract_slots = ActionExtractSlots(None)
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == []
expected_events = [
ActiveLoop(form_name),
SlotSet(slot_name, entity_value),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
]
form_action = FormAction(form_name, None)
form_events = await form_action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert form_events == expected_events
async def test_form_validation_happens_once(caplog: LogCaptureFixture):
"""
Tests if form validation happens once instead of twice.
Solves the bug presented in https://rasahq.atlassian.net/browse/ENG-117
"""
tracker = DialogueStateTracker.from_events(sender_id="bla", evts=[])
form_name = "my_form"
action_server = EndpointConfig(ACTION_SERVER_URL)
action = FormAction(form_name, action_server)
slot_name = "num_people"
domain = textwrap.dedent(
f"""
slots:
{slot_name}:
type: float
mappings:
- type: from_entity
entity: number
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
responses:
utter_ask_num_people:
- text: "How many people?"
actions:
- validate_{form_name}
"""
)
domain = Domain.from_yaml(domain)
form_validation_events = [
{
"event": "slot",
"timestamp": None,
"name": "num_people",
"value": 5,
}
]
with aioresponses() as mocked, caplog.at_level(logging.DEBUG):
mocked.post(
ACTION_SERVER_URL,
payload={"events": form_validation_events},
)
_ = await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert (
sum(
[
1
for message in caplog.messages
if f"Calling action endpoint to run action 'validate_{form_name}'."
in message
]
)
== 1
)
async def test_form_validation_happens_at_form_activation(caplog: LogCaptureFixture):
"""Test if form validation happens at form activation.
The particular case is when a non-required slot for the form is also filled
at form activation.
Fixes the bug in https://rasahq.atlassian.net/browse/ATO-1104.
"""
form = "help_form"
action_server = EndpointConfig(ACTION_SERVER_URL)
form_action = FormAction(form, action_server)
entity_a = "device"
entity_b = "account_type"
entity_b_value = "bronze"
context_slot = "device_type"
required_slot = "send_sms"
global_slot = "membership"
domain = textwrap.dedent(
f"""
intents:
- start_form
entities:
- {entity_a}
- {entity_b}
slots:
{context_slot}:
type: categorical
influence_conversation: false
initial_value: "mobile"
values:
- mobile
- desktop
- other
mappings:
- type: from_entity
entity: {entity_a}
{required_slot}:
type: float
influence_conversation: false
mappings:
- type: custom
{global_slot}:
type: text
influence_conversation: false
mappings:
- type: from_entity
entity: {entity_b}
forms:
{form}:
{REQUIRED_SLOTS_KEY}:
- {required_slot}
responses:
utter_ask_{required_slot}:
- text: "Would you like to receive an SMS?"
actions:
- validate_{form}
"""
)
domain = Domain.from_yaml(domain)
tracker = DialogueStateTracker.from_events(
sender_id="test",
evts=[
UserUttered(
"Can you help me with my bronze account ",
intent={"name": "start_form", "confidence": 1.0},
entities=[{"entity": entity_b, "value": entity_b_value}],
),
],
)
# Mock the custom validation action to update form required_slots to empty list
form_validation_events = [
{
"event": "slot",
"timestamp": None,
"name": "requested_slot",
"value": None,
}
]
with aioresponses() as mocked, caplog.at_level(logging.DEBUG):
mocked.post(
ACTION_SERVER_URL,
payload={"events": form_validation_events},
)
events = await form_action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert (
sum(
[
1
for message in caplog.messages
if f"Calling action endpoint to run action 'validate_{form}'."
in message
]
)
== 1
)
assert events == [
ActiveLoop(form),
SlotSet(REQUESTED_SLOT, None),
SlotSet(global_slot, entity_b_value),
ActiveLoop(None),
]
async def test_form_validation_happens_at_form_activation_with_empty_required_slots(
caplog: LogCaptureFixture,
):
"""Test if required_slots get updated dynamically at form activation.
The particular case is when a form has an empty required_slots list.
"""
form = "booking_form"
action_server = EndpointConfig(ACTION_SERVER_URL)
form_action = FormAction(form, action_server)
dynamic_slot = "customer_name"
bot_utterance = "What is your name?"
domain = textwrap.dedent(
f"""
intents:
- start_form
slots:
{dynamic_slot}:
type: text
influence_conversation: false
mappings:
- type: custom
forms:
{form}:
{REQUIRED_SLOTS_KEY}: []
responses:
utter_ask_{dynamic_slot}:
- text: "{bot_utterance}"
actions:
- validate_{form}
"""
)
domain = Domain.from_yaml(domain)
tracker = DialogueStateTracker.from_events(
sender_id="test",
evts=[
UserUttered(
"I'd like to make a booking. ",
intent={"name": "start_form", "confidence": 1.0},
),
],
)
# Mock the custom validation action to update form required_slots
form_validation_events = [
{
"event": "slot",
"timestamp": None,
"name": "requested_slot",
"value": dynamic_slot,
}
]
with aioresponses() as mocked, caplog.at_level(logging.DEBUG):
mocked.post(
ACTION_SERVER_URL,
payload={"events": form_validation_events},
)
events = await form_action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert (
sum(
[
1
for message in caplog.messages
if f"Calling action endpoint to run action 'validate_{form}'."
in message
]
)
== 1
)
assert len(events) == 3
assert events[0] == ActiveLoop(form)
assert events[1] == SlotSet(REQUESTED_SLOT, dynamic_slot)
assert isinstance(events[2], BotUttered) is True
assert events[2].text == bot_utterance