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

3021 lines
87 KiB
Python

import logging
import textwrap
from datetime import datetime
from typing import List, Text, Any, Dict, Optional
from unittest.mock import Mock
import pytest
from pytest import MonkeyPatch
from _pytest.logging import LogCaptureFixture
from aioresponses import aioresponses
from jsonschema import ValidationError
import rasa.core
from rasa.core.actions import action
from rasa.core.actions.action import (
ActionBack,
ActionDefaultAskAffirmation,
ActionDefaultAskRephrase,
ActionDefaultFallback,
ActionExecutionRejection,
ActionRestart,
ActionBotResponse,
ActionRetrieveResponse,
RemoteAction,
ActionSessionStart,
ActionEndToEndResponse,
ActionExtractSlots,
)
from rasa.core.actions.forms import FormAction
from rasa.core.channels import CollectingOutputChannel, OutputChannel
from rasa.core.channels.slack import SlackBot
from rasa.core.constants import COMPRESS_ACTION_SERVER_REQUEST_ENV_NAME
from rasa.core.nlg import NaturalLanguageGenerator
from rasa.shared.constants import (
LATEST_TRAINING_DATA_FORMAT_VERSION,
UTTER_PREFIX,
REQUIRED_SLOTS_KEY,
)
from rasa.shared.core.domain import (
ActionNotFoundException,
SessionConfig,
Domain,
KEY_E2E_ACTIONS,
)
from rasa.shared.core.events import (
Restarted,
SlotSet,
UserUtteranceReverted,
BotUttered,
ActiveLoop,
SessionStarted,
ActionExecuted,
Event,
UserUttered,
EntitiesAdded,
DefinePrevUserUtteredFeaturization,
AllSlotsReset,
ReminderScheduled,
ReminderCancelled,
ActionReverted,
StoryExported,
FollowupAction,
ConversationPaused,
ConversationResumed,
AgentUttered,
LoopInterrupted,
ActionExecutionRejected,
LegacyFormValidation,
LegacyForm,
)
import rasa.shared.utils.common
from rasa.core.nlg.response import TemplatedNaturalLanguageGenerator
from rasa.shared.core.constants import (
USER_INTENT_SESSION_START,
ACTION_LISTEN_NAME,
ACTION_RESTART_NAME,
ACTION_SESSION_START_NAME,
ACTION_DEFAULT_FALLBACK_NAME,
ACTION_DEACTIVATE_LOOP_NAME,
ACTION_REVERT_FALLBACK_EVENTS_NAME,
ACTION_DEFAULT_ASK_AFFIRMATION_NAME,
ACTION_DEFAULT_ASK_REPHRASE_NAME,
ACTION_BACK_NAME,
ACTION_TWO_STAGE_FALLBACK_NAME,
ACTION_UNLIKELY_INTENT_NAME,
RULE_SNIPPET_ACTION_NAME,
ACTIVE_LOOP,
FOLLOWUP_ACTION,
REQUESTED_SLOT,
SESSION_START_METADATA_SLOT,
ACTION_EXTRACT_SLOTS,
)
from rasa.shared.core.trackers import DialogueStateTracker
from rasa.shared.exceptions import RasaException
from rasa.utils.endpoints import ClientResponseError, EndpointConfig
from tests.utilities import json_of_latest_request, latest_request
@pytest.fixture(scope="module")
def template_nlg() -> TemplatedNaturalLanguageGenerator:
responses = {
"utter_ask_rephrase": [{"text": "can you rephrase that?"}],
"utter_restart": [{"text": "congrats, you've restarted me!"}],
"utter_back": [{"text": "backing up..."}],
"utter_invalid": [{"text": "a response referencing an invalid {variable}."}],
"utter_buttons": [
{
"text": "button message",
"buttons": [
{"payload": "button1", "title": "button1"},
{"payload": "button2", "title": "button2"},
],
}
],
}
return TemplatedNaturalLanguageGenerator(responses)
@pytest.fixture(scope="module")
def template_sender_tracker(domain_path: Text):
domain = Domain.load(domain_path)
return DialogueStateTracker("template-sender", domain.slots)
def test_domain_action_instantiation():
domain = Domain(
intents=[{"chitchat": {"is_retrieval_intent": True}}],
entities=[],
slots=[],
responses={},
action_names=["my_module.ActionTest", "utter_test", "utter_chitchat"],
forms={},
data={},
)
instantiated_actions = [
action.action_for_name_or_text(action_name, domain, None)
for action_name in domain.action_names_or_texts
]
assert len(instantiated_actions) == 16
assert instantiated_actions[0].name() == ACTION_LISTEN_NAME
assert instantiated_actions[1].name() == ACTION_RESTART_NAME
assert instantiated_actions[2].name() == ACTION_SESSION_START_NAME
assert instantiated_actions[3].name() == ACTION_DEFAULT_FALLBACK_NAME
assert instantiated_actions[4].name() == ACTION_DEACTIVATE_LOOP_NAME
assert instantiated_actions[5].name() == ACTION_REVERT_FALLBACK_EVENTS_NAME
assert instantiated_actions[6].name() == ACTION_DEFAULT_ASK_AFFIRMATION_NAME
assert instantiated_actions[7].name() == ACTION_DEFAULT_ASK_REPHRASE_NAME
assert instantiated_actions[8].name() == ACTION_TWO_STAGE_FALLBACK_NAME
assert instantiated_actions[9].name() == ACTION_UNLIKELY_INTENT_NAME
assert instantiated_actions[10].name() == ACTION_BACK_NAME
assert instantiated_actions[11].name() == RULE_SNIPPET_ACTION_NAME
assert instantiated_actions[12].name() == ACTION_EXTRACT_SLOTS
assert instantiated_actions[13].name() == "my_module.ActionTest"
assert instantiated_actions[14].name() == "utter_test"
assert instantiated_actions[15].name() == "utter_chitchat"
@pytest.mark.parametrize(
"is_compression_enabled, expected_compress_argument",
[
("True", True),
("False", False),
],
)
async def test_remote_actions_are_compressed(
is_compression_enabled: str,
expected_compress_argument: bool,
default_channel: OutputChannel,
default_nlg: NaturalLanguageGenerator,
default_tracker: DialogueStateTracker,
domain: Domain,
monkeypatch: MonkeyPatch,
):
endpoint = EndpointConfig("https://example.com/webhooks/actions")
remote_action = action.RemoteAction("my_action", endpoint)
monkeypatch.setenv(COMPRESS_ACTION_SERVER_REQUEST_ENV_NAME, is_compression_enabled)
with aioresponses() as mocked:
mocked.post(
"https://example.com/webhooks/actions",
payload={"events": [], "responses": []},
)
await remote_action.run(default_channel, default_nlg, default_tracker, domain)
r = latest_request(mocked, "post", "https://example.com/webhooks/actions")
assert r
assert r[-1].kwargs["compress"] is expected_compress_argument
async def test_remote_action_runs(
default_channel: OutputChannel,
default_nlg: NaturalLanguageGenerator,
default_tracker: DialogueStateTracker,
domain: Domain,
):
endpoint = EndpointConfig("https://example.com/webhooks/actions")
remote_action = action.RemoteAction("my_action", endpoint)
with aioresponses() as mocked:
mocked.post(
"https://example.com/webhooks/actions",
payload={"events": [], "responses": []},
)
await remote_action.run(default_channel, default_nlg, default_tracker, domain)
r = latest_request(mocked, "post", "https://example.com/webhooks/actions")
assert r
assert json_of_latest_request(r) == {
"domain": domain.as_dict(),
"next_action": "my_action",
"sender_id": "my-sender",
"version": rasa.__version__,
"tracker": {
"latest_message": {
"entities": [],
"intent": {},
"text": None,
"message_id": None,
"metadata": {},
},
ACTIVE_LOOP: {},
"latest_action": {},
"latest_action_name": None,
"sender_id": "my-sender",
"paused": False,
"latest_event_time": None,
FOLLOWUP_ACTION: "action_listen",
"slots": {
"name": None,
REQUESTED_SLOT: None,
SESSION_START_METADATA_SLOT: None,
},
"events": [],
"latest_input_channel": None,
},
}
async def test_remote_action_logs_events(
default_channel: OutputChannel,
default_nlg: NaturalLanguageGenerator,
default_tracker: DialogueStateTracker,
domain: Domain,
):
endpoint = EndpointConfig("https://example.com/webhooks/actions")
remote_action = action.RemoteAction("my_action", endpoint)
response = {
"events": [{"event": "slot", "value": "rasa", "name": "name"}],
"responses": [
{
"text": "test text",
"response": None,
"buttons": [{"title": "cheap", "payload": "cheap"}],
},
{"response": "utter_greet"},
],
}
with aioresponses() as mocked:
mocked.post("https://example.com/webhooks/actions", payload=response)
events = await remote_action.run(
default_channel, default_nlg, default_tracker, domain
)
r = latest_request(mocked, "post", "https://example.com/webhooks/actions")
assert r
assert json_of_latest_request(r) == {
"domain": domain.as_dict(),
"next_action": "my_action",
"sender_id": "my-sender",
"version": rasa.__version__,
"tracker": {
"latest_message": {
"entities": [],
"intent": {},
"text": None,
"message_id": None,
"metadata": {},
},
ACTIVE_LOOP: {},
"latest_action": {},
"latest_action_name": None,
"sender_id": "my-sender",
"paused": False,
FOLLOWUP_ACTION: ACTION_LISTEN_NAME,
"latest_event_time": None,
"slots": {
"name": None,
REQUESTED_SLOT: None,
SESSION_START_METADATA_SLOT: None,
},
"events": [],
"latest_input_channel": None,
},
}
assert len(events) == 3 # first two events are bot utterances
assert events[0] == BotUttered(
"test text", {"buttons": [{"title": "cheap", "payload": "cheap"}]}
)
assert events[1] == BotUttered(
"hey there None!", metadata={"utter_action": "utter_greet"}
)
assert events[2] == SlotSet("name", "rasa")
async def test_remote_action_utterances_with_none_values(
default_channel: OutputChannel,
default_tracker: DialogueStateTracker,
domain: Domain,
):
endpoint = EndpointConfig("https://example.com/webhooks/actions")
remote_action = action.RemoteAction("my_action", endpoint)
response = {
"events": [
{"event": "form", "name": "restaurant_form", "timestamp": None},
{
"event": "slot",
"timestamp": None,
"name": "requested_slot",
"value": "cuisine",
},
],
"responses": [
{
"text": None,
"buttons": None,
"elements": [],
"custom": None,
"response": "utter_ask_cuisine",
"image": None,
"attachment": None,
}
],
}
nlg = TemplatedNaturalLanguageGenerator(
{"utter_ask_cuisine": [{"text": "what dou want to eat?"}]}
)
with aioresponses() as mocked:
mocked.post("https://example.com/webhooks/actions", payload=response)
events = await remote_action.run(default_channel, nlg, default_tracker, domain)
assert events == [
BotUttered(
"what dou want to eat?", metadata={"utter_action": "utter_ask_cuisine"}
),
ActiveLoop("restaurant_form"),
SlotSet("requested_slot", "cuisine"),
]
@pytest.mark.parametrize(
"event",
(
EntitiesAdded(
entities=[
{"entity": "city", "value": "London"},
{"entity": "count", "value": 1},
],
timestamp=None,
),
EntitiesAdded(entities=[]),
EntitiesAdded(
entities=[
{"entity": "name", "value": "John", "role": "contact", "group": "test"}
]
),
DefinePrevUserUtteredFeaturization(
use_text_for_featurization=False, timestamp=None, metadata=None
),
ReminderCancelled(timestamp=1621590172.3872123),
ReminderScheduled(
timestamp=None, trigger_date_time=datetime.now(), intent="greet"
),
ActionExecutionRejected(action_name="my_action"),
LegacyFormValidation(validate=True, timestamp=None),
LoopInterrupted(timestamp=None, is_interrupted=False),
ActiveLoop(name="loop"),
LegacyForm(name="my_form"),
AllSlotsReset(),
SlotSet(key="my_slot", value={}),
SlotSet(key="my slot", value=[]),
SlotSet(key="test", value=1),
SlotSet(key="test", value="text"),
ConversationResumed(),
ConversationPaused(),
FollowupAction(name="test"),
StoryExported(),
Restarted(),
ActionReverted(),
UserUtteranceReverted(),
BotUttered(text="Test bot utterance"),
UserUttered(
parse_data={
"entities": [],
"response_selector": {
"all_retrieval_intents": [],
"chitchat/ask_weather": {"response": {}, "ranking": []},
},
}
),
UserUttered(
text="hello",
parse_data={
"intent": {"name": "greet", "confidence": 0.9604260921478271},
"entities": [
{"entity": "city", "value": "London"},
{"entity": "count", "value": 1},
],
"text": "hi",
"message_id": "3f4c04602a4947098c574b107d3ccc50",
"metadata": {},
"intent_ranking": [
{"name": "greet", "confidence": 0.9604260921478271},
{"name": "goodbye", "confidence": 0.01835782080888748},
{"name": "deny", "confidence": 0.011255578137934208},
{"name": "bot_challenge", "confidence": 0.004019865766167641},
{"name": "affirm", "confidence": 0.002524246694520116},
{"name": "mood_great", "confidence": 0.002214624546468258},
{"name": "chitchat", "confidence": 0.0009614597074687481},
{"name": "mood_unhappy", "confidence": 0.00024030178610701114},
],
"response_selector": {
"all_retrieval_intents": [],
"default": {
"response": {
"id": -226546773594344189,
"responses": [{"text": "chitchat/ask_name"}],
"response_templates": [{"text": "chitchat/ask_name"}],
"confidence": 0.9618658423423767,
"intent_response_key": "chitchat/ask_name",
"utter_action": "utter_chitchat/ask_name",
"template_name": "utter_chitchat/ask_name",
},
"ranking": [
{
"id": -226546773594344189,
"confidence": 0.9618658423423767,
"intent_response_key": "chitchat/ask_name",
},
{
"id": 8392727822750416828,
"confidence": 0.03813415765762329,
"intent_response_key": "chitchat/ask_weather",
},
],
},
},
},
),
SessionStarted(),
ActionExecuted(action_name="action_listen"),
AgentUttered(),
),
)
async def test_remote_action_valid_payload_all_events(
default_channel: OutputChannel,
default_nlg: NaturalLanguageGenerator,
default_tracker: DialogueStateTracker,
domain: Domain,
event: Event,
):
endpoint = EndpointConfig("https://example.com/webhooks/actions")
remote_action = action.RemoteAction("my_action", endpoint)
events = [event.as_dict()]
response = {"events": events, "responses": []}
with aioresponses() as mocked:
mocked.post("https://example.com/webhooks/actions", payload=response)
events = await remote_action.run(
default_channel, default_nlg, default_tracker, domain
)
assert len(events) == 1
@pytest.mark.parametrize(
"event",
(
{
"event": "user",
"timestamp": 1621590172.3872123,
"parse_data": {"entities": {}},
},
{"event": "entities", "timestamp": 1621604905.647361, "entities": {}},
),
)
async def test_remote_action_invalid_entities_payload(
default_channel: OutputChannel,
default_nlg: NaturalLanguageGenerator,
default_tracker: DialogueStateTracker,
domain: Domain,
event: Event,
):
endpoint = EndpointConfig("https://example.com/webhooks/actions")
remote_action = action.RemoteAction("my_action", endpoint)
response = {"events": [event], "responses": []}
with aioresponses() as mocked:
mocked.post("https://example.com/webhooks/actions", payload=response)
with pytest.raises(ValidationError) as e:
await remote_action.run(
default_channel, default_nlg, default_tracker, domain
)
assert "Failed to validate Action server response from API" in str(e.value)
async def test_remote_action_multiple_events_payload(
default_channel: OutputChannel,
default_nlg: NaturalLanguageGenerator,
default_tracker: DialogueStateTracker,
domain: Domain,
):
endpoint = EndpointConfig("https://example.com/webhooks/actions")
remote_action = action.RemoteAction("my_action", endpoint)
response = {
"events": [
{
"event": "action",
"name": "action_listen",
"policy": None,
"confidence": None,
"timestamp": None,
},
{"event": "slot", "name": "name", "value": None, "timestamp": None},
{
"event": "user",
"timestamp": None,
"text": "hello",
"parse_data": {
"intent": {"name": "greet", "confidence": 0.99},
"entities": [],
},
},
],
"responses": [],
}
with aioresponses() as mocked:
mocked.post("https://example.com/webhooks/actions", payload=response)
events = await remote_action.run(
default_channel, default_nlg, default_tracker, domain
)
assert isinstance(events[0], ActionExecuted)
assert events[0].as_dict().get("name") == "action_listen"
assert isinstance(events[1], SlotSet)
assert events[1].as_dict().get("name") == "name"
assert isinstance(events[2], UserUttered)
assert events[2].as_dict().get("text") == "hello"
async def test_remote_action_without_endpoint(
default_channel, default_nlg, default_tracker, domain: Domain
):
remote_action = action.RemoteAction("my_action", None)
with pytest.raises(Exception) as execinfo:
await remote_action.run(default_channel, default_nlg, default_tracker, domain)
assert "Failed to execute custom action" in str(execinfo.value)
async def test_remote_action_endpoint_not_running(
default_channel, default_nlg, default_tracker, domain: Domain
):
endpoint = EndpointConfig("https://example.com/webhooks/actions")
remote_action = action.RemoteAction("my_action", endpoint)
with pytest.raises(Exception) as execinfo:
await remote_action.run(default_channel, default_nlg, default_tracker, domain)
assert "Failed to execute custom action" in str(execinfo.value)
async def test_remote_action_endpoint_responds_500(
default_channel, default_nlg, default_tracker, domain: Domain
):
endpoint = EndpointConfig("https://example.com/webhooks/actions")
remote_action = action.RemoteAction("my_action", endpoint)
with aioresponses() as mocked:
mocked.post("https://example.com/webhooks/actions", status=500)
with pytest.raises(Exception) as execinfo:
await remote_action.run(
default_channel, default_nlg, default_tracker, domain
)
assert "Failed to execute custom action" in str(execinfo.value)
async def test_remote_action_endpoint_responds_400(
default_channel, default_nlg, default_tracker, domain: Domain
):
endpoint = EndpointConfig("https://example.com/webhooks/actions")
remote_action = action.RemoteAction("my_action", endpoint)
with aioresponses() as mocked:
# noinspection PyTypeChecker
mocked.post(
"https://example.com/webhooks/actions",
exception=ClientResponseError(400, None, '{"action_name": "my_action"}'),
)
with pytest.raises(Exception) as execinfo:
await remote_action.run(
default_channel, default_nlg, default_tracker, domain
)
assert execinfo.type == ActionExecutionRejection
assert "Custom action 'my_action' rejected to run" in str(execinfo.value)
async def test_action_utter_retrieved_response(
default_channel, default_nlg, default_tracker, domain: Domain
):
from rasa.core.channels.channel import UserMessage
action_name = "utter_chitchat"
default_tracker.latest_message = UserMessage(
"Who are you?",
parse_data={
"response_selector": {
"chitchat": {
"response": {
"intent_response_key": "chitchat/ask_name",
"responses": [{"text": "I am a bot."}],
"utter_action": "utter_chitchat/ask_name",
}
}
}
},
)
domain.responses.update({"utter_chitchat/ask_name": [{"text": "I am a bot."}]})
events = await ActionRetrieveResponse(action_name).run(
default_channel, default_nlg, default_tracker, domain
)
assert events[0].as_dict().get("text") == BotUttered("I am a bot.").as_dict().get(
"text"
)
assert (
events[0].as_dict().get("metadata").get("utter_action")
== "utter_chitchat/ask_name"
)
async def test_action_utter_default_retrieved_response(
default_channel, default_nlg, default_tracker, domain: Domain
):
from rasa.core.channels.channel import UserMessage
action_name = "utter_chitchat"
default_tracker.latest_message = UserMessage(
"Who are you?",
parse_data={
"response_selector": {
"default": {
"response": {
"intent_response_key": "chitchat/ask_name",
"responses": [{"text": "I am a bot."}],
"utter_action": "utter_chitchat/ask_name",
}
}
}
},
)
domain.responses.update({"utter_chitchat/ask_name": [{"text": "I am a bot."}]})
events = await ActionRetrieveResponse(action_name).run(
default_channel, default_nlg, default_tracker, domain
)
assert events[0].as_dict().get("text") == BotUttered("I am a bot.").as_dict().get(
"text"
)
assert (
events[0].as_dict().get("metadata").get("utter_action")
== "utter_chitchat/ask_name"
)
async def test_action_utter_retrieved_empty_response(
default_channel, default_nlg, default_tracker, domain: Domain
):
from rasa.core.channels.channel import UserMessage
action_name = "utter_chitchat"
default_tracker.latest_message = UserMessage(
"Who are you?",
parse_data={
"response_selector": {
"dummy": {
"response": {
"intent_response_key": "chitchat/ask_name",
"responses": [{"text": "I am a bot."}],
"utter_action": "utter_chitchat/ask_name",
}
}
}
},
)
domain.responses.update({"utter_chitchat/ask_name": [{"text": "I am a bot."}]})
events = await ActionRetrieveResponse(action_name).run(
default_channel, default_nlg, default_tracker, domain
)
assert events == []
async def test_response(default_channel, default_nlg, default_tracker, domain: Domain):
events = await ActionBotResponse("utter_channel").run(
default_channel, default_nlg, default_tracker, domain
)
assert events == [
BotUttered(
"this is a default channel", metadata={"utter_action": "utter_channel"}
)
]
async def test_response_unknown_response(
default_channel, default_nlg, default_tracker, domain: Domain
):
events = await ActionBotResponse("utter_unknown").run(
default_channel, default_nlg, default_tracker, domain
)
assert events == []
async def test_response_with_buttons(
default_channel, template_nlg, template_sender_tracker, domain: Domain
):
events = await ActionBotResponse("utter_buttons").run(
default_channel, template_nlg, template_sender_tracker, domain
)
assert events == [
BotUttered(
"button message",
{
"buttons": [
{"payload": "button1", "title": "button1"},
{"payload": "button2", "title": "button2"},
]
},
metadata={"utter_action": "utter_buttons"},
)
]
async def test_response_invalid_response(
default_channel, template_nlg, template_sender_tracker, domain: Domain
):
events = await ActionBotResponse("utter_invalid").run(
default_channel, template_nlg, template_sender_tracker, domain
)
assert len(events) == 1
assert isinstance(events[0], BotUttered)
assert events[0].text.startswith("a response referencing an invalid {variable}.")
async def test_response_channel_specific(default_nlg, default_tracker, domain: Domain):
output_channel = SlackBot("DummyToken", "General")
events = await ActionBotResponse("utter_channel").run(
output_channel, default_nlg, default_tracker, domain
)
assert events == [
BotUttered(
"you're talking to me on slack!",
metadata={"channel": "slack", "utter_action": "utter_channel"},
)
]
@pytest.fixture
def domain_with_response_ids() -> Domain:
domain_yaml = """
responses:
utter_one_id:
- text: test
id: '1'
utter_multiple_ids:
- text: test
id: '2'
- text: test
id: '3'
utter_no_id:
- text: test
"""
domain = Domain.from_yaml(domain_yaml)
return domain
async def test_response_with_response_id(
default_channel, domain_with_response_ids: Domain
) -> None:
nlg = TemplatedNaturalLanguageGenerator(domain_with_response_ids.responses)
events = await ActionBotResponse("utter_one_id").run(
default_channel,
nlg,
DialogueStateTracker("response_id", slots=[]),
domain_with_response_ids,
)
assert events == [
BotUttered(
"test",
metadata={"id": "1", "utter_action": "utter_one_id"},
)
]
async def test_action_back(
default_channel, template_nlg, template_sender_tracker, domain: Domain
):
events = await ActionBack().run(
default_channel, template_nlg, template_sender_tracker, domain
)
assert events == [
BotUttered("backing up...", metadata={"utter_action": "utter_back"}),
UserUtteranceReverted(),
UserUtteranceReverted(),
]
async def test_action_restart(
default_channel, template_nlg, template_sender_tracker, domain: Domain
):
events = await ActionRestart().run(
default_channel, template_nlg, template_sender_tracker, domain
)
assert events == [
BotUttered(
"congrats, you've restarted me!", metadata={"utter_action": "utter_restart"}
),
Restarted(),
]
async def test_action_session_start_without_slots(
default_channel: CollectingOutputChannel,
template_nlg: TemplatedNaturalLanguageGenerator,
template_sender_tracker: DialogueStateTracker,
domain: Domain,
):
events = await ActionSessionStart().run(
default_channel, template_nlg, template_sender_tracker, domain
)
assert events == [SessionStarted(), ActionExecuted(ACTION_LISTEN_NAME)]
@pytest.mark.parametrize(
"session_config, expected_events",
[
(
SessionConfig(123, True),
[
SessionStarted(),
SlotSet("my_slot", "value"),
SlotSet("another-slot", "value2"),
ActionExecuted(action_name=ACTION_LISTEN_NAME),
],
),
(
SessionConfig(123, False),
[SessionStarted(), ActionExecuted(action_name=ACTION_LISTEN_NAME)],
),
],
)
async def test_action_session_start_with_slots(
default_channel: CollectingOutputChannel,
template_nlg: TemplatedNaturalLanguageGenerator,
template_sender_tracker: DialogueStateTracker,
domain: Domain,
session_config: SessionConfig,
expected_events: List[Event],
):
# set a few slots on tracker
slot_set_event_1 = SlotSet("my_slot", "value")
slot_set_event_2 = SlotSet("another-slot", "value2")
for event in [slot_set_event_1, slot_set_event_2]:
template_sender_tracker.update(event)
domain.session_config = session_config
events = await ActionSessionStart().run(
default_channel, template_nlg, template_sender_tracker, domain
)
assert events == expected_events
# make sure that the list of events has ascending timestamps
assert sorted(events, key=lambda x: x.timestamp) == events
async def test_applied_events_after_action_session_start(
default_channel: CollectingOutputChannel,
template_nlg: TemplatedNaturalLanguageGenerator,
):
slot_set = SlotSet("my_slot", "value")
events = [
slot_set,
ActionExecuted(ACTION_LISTEN_NAME),
# User triggers a restart manually by triggering the intent
UserUttered(
text=f"/{USER_INTENT_SESSION_START}",
intent={"name": USER_INTENT_SESSION_START},
),
]
tracker = DialogueStateTracker.from_events("🕵️‍♀️", events)
# Mapping Policy kicks in and runs the session restart action
events = await ActionSessionStart().run(
default_channel, template_nlg, tracker, Domain.empty()
)
for event in events:
tracker.update(event)
assert tracker.applied_events() == [slot_set, ActionExecuted(ACTION_LISTEN_NAME)]
async def test_action_default_fallback(
default_channel, default_nlg, default_tracker, domain: Domain
):
events = await ActionDefaultFallback().run(
default_channel, default_nlg, default_tracker, domain
)
assert events == [
BotUttered(
"sorry, I didn't get that, can you rephrase it?",
metadata={"utter_action": "utter_default"},
),
UserUtteranceReverted(),
]
async def test_action_default_ask_affirmation(
default_channel, default_nlg, domain: Domain
):
initial_events = [
ActionExecuted(ACTION_LISTEN_NAME),
# User triggers a restart manually by triggering the intent
UserUttered(
text="/foobar",
intent={"name": "foobar"},
parse_data={
"intent_ranking": [
{"confidence": 0.9, "name": "foobar"},
{"confidence": 0.1, "name": "baz"},
]
},
),
]
tracker = DialogueStateTracker.from_events("🕵️‍♀️", initial_events)
events = await ActionDefaultAskAffirmation().run(
default_channel, default_nlg, tracker, domain
)
assert events == [
BotUttered(
"Did you mean 'foobar'?",
{
"buttons": [
{"title": "Yes", "payload": "/foobar"},
{"title": "No", "payload": "/out_of_scope"},
]
},
{"utter_action": "action_default_ask_affirmation"},
)
]
async def test_action_default_ask_affirmation_on_empty_conversation(
default_channel, default_nlg, default_tracker, domain: Domain
):
events = await ActionDefaultAskAffirmation().run(
default_channel, default_nlg, default_tracker, domain
)
assert events == [
BotUttered(
"Did you mean 'None'?",
{
"buttons": [
{"title": "Yes", "payload": "/None"},
{"title": "No", "payload": "/out_of_scope"},
]
},
{"utter_action": "action_default_ask_affirmation"},
)
]
async def test_action_default_ask_rephrase(
default_channel, template_nlg, template_sender_tracker, domain: Domain
):
events = await ActionDefaultAskRephrase().run(
default_channel, template_nlg, template_sender_tracker, domain
)
assert events == [
BotUttered(
"can you rephrase that?", metadata={"utter_action": "utter_ask_rephrase"}
)
]
@pytest.mark.parametrize(
"slot",
[
"""- my_slot
""",
"[]",
],
)
def test_get_form_action(slot: Text):
form_action_name = "my_business_logic"
domain = Domain.from_yaml(
textwrap.dedent(
f"""
slots:
my_slot:
type: text
mappings:
- type: from_text
actions:
- my_action
forms:
{form_action_name}:
{REQUIRED_SLOTS_KEY}:
{slot}
"""
)
)
actual = action.action_for_name_or_text(form_action_name, domain, None)
assert isinstance(actual, FormAction)
def test_overridden_form_action():
form_action_name = "my_business_logic"
domain = Domain.from_yaml(
textwrap.dedent(
f"""
actions:
- my_action
- {form_action_name}
forms:
{form_action_name}:
{REQUIRED_SLOTS_KEY}: []
"""
)
)
actual = action.action_for_name_or_text(form_action_name, domain, None)
assert isinstance(actual, RemoteAction)
def test_get_form_action_if_not_in_forms():
form_action_name = "my_business_logic"
domain = Domain.from_yaml(
textwrap.dedent(
"""
actions:
- my_action
"""
)
)
with pytest.raises(ActionNotFoundException):
assert not action.action_for_name_or_text(form_action_name, domain, None)
@pytest.mark.parametrize(
"end_to_end_utterance", ["Hi", f"{UTTER_PREFIX} is a dangerous start"]
)
def test_get_end_to_end_utterance_action(end_to_end_utterance: Text):
domain = Domain.from_yaml(
textwrap.dedent(
f"""
actions:
- my_action
{KEY_E2E_ACTIONS}:
- {end_to_end_utterance}
- Bye Bye
"""
)
)
actual = action.action_for_name_or_text(end_to_end_utterance, domain, None)
assert isinstance(actual, ActionEndToEndResponse)
assert actual.name() == end_to_end_utterance
async def test_run_end_to_end_utterance_action():
end_to_end_utterance = "Hi"
domain = Domain.from_yaml(
textwrap.dedent(
f"""
actions:
- my_action
{KEY_E2E_ACTIONS}:
- {end_to_end_utterance}
- Bye Bye
"""
)
)
e2e_action = action.action_for_name_or_text("Hi", domain, None)
events = await e2e_action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
DialogueStateTracker.from_events("sender", evts=[]),
domain,
)
assert events == [
BotUttered(
end_to_end_utterance,
{
"elements": None,
"quick_replies": None,
"buttons": None,
"attachment": None,
"image": None,
"custom": None,
},
{},
)
]
@pytest.mark.parametrize(
"user, slot_name, slot_value, new_user, updated_value",
[
(
UserUttered(
intent={"name": "inform"},
entities=[{"entity": "city", "value": "London"}],
),
"location",
"London",
UserUttered(
intent={"name": "inform"},
entities=[{"entity": "city", "value": "Berlin"}],
),
"Berlin",
),
(
UserUttered(text="test@example.com", intent={"name": "inform"}),
"email",
"test@example.com",
UserUttered(text="updated_test@example.com", intent={"name": "inform"}),
"updated_test@example.com",
),
(
UserUttered(intent={"name": "affirm"}),
"cancel_booking",
True,
UserUttered(intent={"name": "deny"}),
False,
),
(
UserUttered(intent={"name": "deny"}),
"cancel_booking",
False,
UserUttered(intent={"name": "affirm"}),
True,
),
(
UserUttered(
intent={"name": "inform"},
entities=[
{"entity": "name", "value": "Bob"},
{"entity": "name", "value": "Mary"},
],
),
"guest_names",
["Bob", "Mary"],
UserUttered(
intent={"name": "inform"},
entities=[{"entity": "name", "value": "John"}],
),
["John"],
),
],
)
async def test_action_extract_slots_predefined_mappings(
user: Event, slot_name: Text, slot_value: Any, new_user: Event, updated_value: Any
):
domain = Domain.from_yaml(
textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intents:
- inform
- greet
- affirm
- deny
entities:
- city
- name
slots:
location:
type: text
influence_conversation: false
mappings:
- type: from_entity
entity: city
email:
type: text
influence_conversation: false
mappings:
- type: from_text
intent: inform
not_intent: greet
cancel_booking:
type: bool
influence_conversation: false
mappings:
- type: from_intent
intent: affirm
value: true
- type: from_intent
intent: deny
value: false
guest_names:
type: list
influence_conversation: false
mappings:
- type: from_entity
entity: name"""
)
)
action_extract_slots = ActionExtractSlots(action_endpoint=None)
tracker = DialogueStateTracker.from_events("sender", evts=[user])
with pytest.warns(None):
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == [SlotSet(slot_name, slot_value)]
events.extend([user])
tracker.update_with_events(events, domain)
new_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert new_events == [SlotSet(slot_name, slot_value)]
new_events.extend([BotUttered(), ActionExecuted("action_listen"), new_user])
tracker.update_with_events(new_events, domain)
updated_evts = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert updated_evts == [SlotSet(slot_name, updated_value)]
async def test_action_extract_slots_with_from_trigger_mappings():
domain = Domain.from_yaml(
textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intents:
- greet
- inform
- register
slots:
email:
type: text
influence_conversation: false
mappings:
- type: from_text
intent: inform
not_intent: greet
existing_customer:
type: bool
influence_conversation: false
mappings:
- type: from_trigger_intent
intent: register
value: false
forms:
registration_form:
required_slots:
- email"""
)
)
action_extract_slots = ActionExtractSlots(action_endpoint=None)
user_event = UserUttered(text="I'd like to register", intent={"name": "register"})
tracker = DialogueStateTracker.from_events(
"sender", evts=[user_event, ActiveLoop("registration_form")]
)
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == [SlotSet("existing_customer", False)]
@pytest.mark.parametrize(
"slot_mapping, expected_value",
[
(
{"type": "from_entity", "entity": "some_slot", "intent": "greet"},
"some_value",
),
(
{"type": "from_intent", "intent": "greet", "value": "other_value"},
"other_value",
),
({"type": "from_text"}, "bla"),
({"type": "from_text", "intent": "greet"}, "bla"),
({"type": "from_text", "not_intent": "other"}, "bla"),
],
)
async def test_action_extract_slots_when_mapping_applies(
slot_mapping: Dict, expected_value: Text
):
form_name = "test_form"
entity_name = "some_slot"
domain = Domain.from_dict(
{
"intents": ["greet"],
"entities": ["some_slot"],
"slots": {entity_name: {"type": "text", "mappings": [slot_mapping]}},
"forms": {form_name: {REQUIRED_SLOTS_KEY: [entity_name]}},
}
)
tracker = DialogueStateTracker.from_events(
"default",
[
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, "some_slot"),
UserUttered(
"bla",
intent={"name": "greet", "confidence": 1.0},
entities=[{"entity": entity_name, "value": "some_value"}],
),
],
)
action_extract_slots = ActionExtractSlots(action_endpoint=None)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
# check that the value was extracted for correct intent
assert slot_events == [SlotSet("some_slot", expected_value)]
@pytest.mark.parametrize(
"entities, expected_slot_values",
[
# Two entities were extracted for `ListSlot`
(
[
{"entity": "topping", "value": "mushrooms"},
{"entity": "topping", "value": "kebab"},
],
["mushrooms", "kebab"],
),
# Only one entity was extracted for `ListSlot`
([{"entity": "topping", "value": "kebab"}], ["kebab"]),
],
)
async def test_action_extract_slots_with_list_slot(
entities: List[Dict[Text, Any]], expected_slot_values: List[Text]
):
form_name = "order_form"
slot_name = "toppings"
domain = Domain.from_yaml(
textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
entities:
- topping
slots:
{slot_name}:
type: list
influence_conversation: false
mappings:
- type: from_entity
entity: topping
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
"""
)
)
tracker = DialogueStateTracker.from_events(
"default",
[
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, slot_name),
UserUttered(
"bla", intent={"name": "greet", "confidence": 1.0}, entities=entities
),
ActionExecuted(ACTION_LISTEN_NAME),
],
slots=domain.slots,
)
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, expected_slot_values)]
@pytest.mark.parametrize(
"slot_mapping",
[
{"type": "from_entity", "entity": "some_slot", "intent": "some_intent"},
{"type": "from_intent", "intent": "some_intent", "value": "some_value"},
{"type": "from_intent", "intent": "greeted", "value": "some_value"},
{"type": "from_text", "intent": "other"},
{"type": "from_text", "not_intent": "greet"},
{"type": "from_trigger_intent", "intent": "some_intent", "value": "value"},
],
)
async def test_action_extract_slots_mapping_does_not_apply(slot_mapping: Dict):
form_name = "some_form"
entity_name = "some_slot"
domain = Domain.from_dict(
{
"slots": {entity_name: {"type": "text", "mappings": [slot_mapping]}},
"forms": {form_name: {REQUIRED_SLOTS_KEY: [entity_name]}},
}
)
tracker = DialogueStateTracker.from_events(
"default",
[
UserUttered(
"bla",
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,
)
# check that the value was not extracted for incorrect intent
assert slot_events == []
async def test_action_extract_slots_with_matched_mapping_condition():
form_name = "some_form"
domain = Domain.from_yaml(
textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intent:
- greet
- inform
slots:
name:
type: text
influence_conversation: true
mappings:
- type: from_text
conditions:
- active_loop: some_form
requested_slot: name
- active_loop: other_form
forms:
{form_name}:
required_slots:
- name
other_form:
required_slots:
- name
"""
)
)
tracker = DialogueStateTracker.from_events(
"default",
[
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, "name"),
UserUttered(
"Emily", intent={"name": "inform", "confidence": 1.0}, entities=[]
),
],
)
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")]
async def test_action_extract_slots_no_matched_mapping_conditions():
form_name = "some_form"
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:
{form_name}:
required_slots:
- email
- name
"""
)
)
tracker = DialogueStateTracker.from_events(
"default",
[
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, "name"),
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 == []
@pytest.mark.parametrize(
"mapping_not_intent, mapping_intent, mapping_role, "
"mapping_group, entities, intent, expected_slot_events",
[
(
"some_intent",
None,
None,
None,
[{"entity": "some_entity", "value": "some_value"}],
"some_intent",
[],
),
(
None,
"some_intent",
None,
None,
[{"entity": "some_entity", "value": "some_value"}],
"some_intent",
[SlotSet("some_slot", "some_value")],
),
(
"some_intent",
None,
None,
None,
[{"entity": "some_entity", "value": "some_value"}],
"some_other_intent",
[SlotSet("some_slot", "some_value")],
),
(
None,
None,
"some_role",
None,
[{"entity": "some_entity", "value": "some_value"}],
"some_intent",
[],
),
(
None,
None,
"some_role",
None,
[{"entity": "some_entity", "value": "some_value", "role": "some_role"}],
"some_intent",
[SlotSet("some_slot", "some_value")],
),
(
None,
None,
None,
"some_group",
[{"entity": "some_entity", "value": "some_value"}],
"some_intent",
[],
),
(
None,
None,
None,
"some_group",
[{"entity": "some_entity", "value": "some_value", "group": "some_group"}],
"some_intent",
[SlotSet("some_slot", "some_value")],
),
(
None,
None,
"some_role",
"some_group",
[
{
"entity": "some_entity",
"value": "some_value",
"group": "some_group",
"role": "some_role",
}
],
"some_intent",
[SlotSet("some_slot", "some_value")],
),
(
None,
None,
"some_role",
"some_group",
[{"entity": "some_entity", "value": "some_value", "role": "some_role"}],
"some_intent",
[],
),
(
None,
None,
None,
None,
[
{
"entity": "some_entity",
"value": "some_value",
"group": "some_group",
"role": "some_role",
}
],
"some_intent",
# nothing should be extracted, because entity contain role and group
# but mapping expects them to be None
[],
),
],
)
async def test_action_extract_slots_from_entity(
mapping_not_intent: Optional[Text],
mapping_intent: Optional[Text],
mapping_role: Optional[Text],
mapping_group: Optional[Text],
entities: List[Dict[Text, Any]],
intent: Text,
expected_slot_events: List[SlotSet],
):
"""Test extraction of a slot value from entity with the different restrictions."""
form_name = "some form"
form = FormAction(form_name, None)
mapping = form.from_entity(
entity="some_entity",
role=mapping_role,
group=mapping_group,
intent=mapping_intent,
not_intent=mapping_not_intent,
)
domain = Domain.from_dict(
{
"entities": ["some_entity"],
"slots": {"some_slot": {"type": "any", "mappings": [mapping]}},
"forms": {form_name: {REQUIRED_SLOTS_KEY: ["some_slot"]}},
}
)
tracker = DialogueStateTracker.from_events(
"default",
[
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, "some_slot"),
UserUttered(
"bla", intent={"name": intent, "confidence": 1.0}, entities=entities
),
],
)
action_extract_slots = ActionExtractSlots(action_endpoint=None)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert slot_events == expected_slot_events
@pytest.mark.parametrize(
"entities, expected_slot_values",
[
# Two entities were extracted for `ListSlot`
(
[
{"entity": "topping", "value": "mushrooms"},
{"entity": "topping", "value": "kebab"},
],
["mushrooms", "kebab"],
),
# Only one entity was extracted for `ListSlot`
([{"entity": "topping", "value": "kebab"}], ["kebab"]),
],
)
async def test_extract_other_list_slot_from_entity(
entities: List[Dict[Text, Any]], expected_slot_values: List[Text]
):
form_name = "some_form"
slot_name = "toppings"
domain = Domain.from_yaml(
textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
entities:
- topping
- some_slot
intents:
- some_intent
- greeted
slots:
{slot_name}:
type: list
influence_conversation: false
mappings:
- type: from_entity
entity: topping
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
"""
)
)
tracker = DialogueStateTracker.from_events(
"default",
[
SlotSet(REQUESTED_SLOT, "some slot"),
UserUttered(
"bla", intent={"name": "greet", "confidence": 1.0}, entities=entities
),
ActionExecuted(ACTION_LISTEN_NAME),
],
slots=domain.slots,
)
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, expected_slot_values)]
@pytest.mark.parametrize(
"trigger_slot_mapping, expected_value",
[
({"type": "from_trigger_intent", "intent": "greet", "value": "ten"}, "ten"),
(
{
"type": "from_trigger_intent",
"intent": ["bye", "greet"],
"value": "tada",
},
"tada",
),
],
)
async def test_trigger_slot_mapping_applies(
trigger_slot_mapping: Dict, expected_value: Text
):
form_name = "some_form"
entity_name = "some_slot"
slot_filled_by_trigger_mapping = "other_slot"
domain = Domain.from_dict(
{
"slots": {
entity_name: {
"type": "text",
"mappings": [
{
"type": "from_entity",
"entity": entity_name,
"intent": "some_intent",
}
],
},
slot_filled_by_trigger_mapping: {
"type": "text",
"mappings": [trigger_slot_mapping],
},
},
"forms": {form_name: {REQUIRED_SLOTS_KEY: [entity_name]}},
}
)
tracker = DialogueStateTracker.from_events(
"default",
[
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, "some_slot"),
UserUttered(
"bla",
intent={"name": "greet", "confidence": 1.0},
entities=[{"entity": entity_name, "value": "some_value"}],
),
],
)
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_filled_by_trigger_mapping, expected_value)]
@pytest.mark.parametrize(
"trigger_slot_mapping",
[
({"type": "from_trigger_intent", "intent": "bye", "value": "ten"}),
({"type": "from_trigger_intent", "not_intent": ["greet"], "value": "tada"}),
],
)
async def test_trigger_slot_mapping_does_not_apply(trigger_slot_mapping: Dict):
form_name = "some_form"
entity_name = "some_slot"
slot_filled_by_trigger_mapping = "other_slot"
domain = Domain.from_dict(
{
"slots": {
entity_name: {
"type": "text",
"mappings": [
{
"type": "from_entity",
"entity": entity_name,
"intent": "some_intent",
}
],
},
slot_filled_by_trigger_mapping: {
"type": "text",
"mappings": [trigger_slot_mapping],
},
},
"forms": {
form_name: {
REQUIRED_SLOTS_KEY: [entity_name, slot_filled_by_trigger_mapping]
}
},
}
)
tracker = DialogueStateTracker.from_events(
"default",
[
SlotSet(REQUESTED_SLOT, "some_slot"),
UserUttered(
"bla",
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,
)
assert slot_events == []
@pytest.mark.parametrize(
"event, validate_return_events, expected_events",
[
(
UserUttered(
intent={"name": "inform"},
entities=[{"entity": "city", "value": "london"}],
),
[{"event": "slot", "name": "from_entity_slot", "value": "London"}],
[SlotSet("from_entity_slot", "London")],
),
(
UserUttered("hi", intent={"name": "greet"}),
[{"event": "slot", "name": "from_text_slot", "value": "Hi"}],
[SlotSet("from_text_slot", "Hi")],
),
(
UserUttered(intent={"name": "affirm"}),
[{"event": "slot", "name": "from_intent_slot", "value": True}],
[SlotSet("from_intent_slot", True)],
),
(
UserUttered(intent={"name": "chitchat"}),
[{"event": "slot", "name": "custom_slot", "value": True}],
[SlotSet("custom_slot", True)],
),
(UserUttered("bla"), [], []),
],
)
async def test_action_extract_slots_execute_validation_action(
event: Event,
validate_return_events: List[Dict[Text, Any]],
expected_events: List[Event],
):
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intents:
- greet
- inform
- affirm
- chitchat
entities:
- city
slots:
from_entity_slot:
type: text
influence_conversation: false
mappings:
- type: from_entity
entity: city
from_text_slot:
type: text
influence_conversation: false
mappings:
- type: from_text
intent: greet
from_intent_slot:
type: text
influence_conversation: false
mappings:
- type: from_intent
intent: affirm
value: True
custom_slot:
type: text
influence_conversation: false
mappings:
- type: custom
actions:
- action_validate_slot_mappings
"""
)
domain = Domain.from_yaml(domain_yaml)
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=[event])
action_server_url = "http:/my-action-server:5055/webhook"
with aioresponses() as mocked:
mocked.post(action_server_url, payload={"events": validate_return_events})
action_server = EndpointConfig(action_server_url)
action_extract_slots = ActionExtractSlots(action_server)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert slot_events == expected_events
async def test_action_extract_slots_custom_action_and_predefined_slot_validation():
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intents:
- inform
entities:
- city
slots:
location:
type: text
influence_conversation: false
mappings:
- type: from_entity
entity: city
custom_slot:
type: text
influence_conversation: false
mappings:
- type: custom
action: action_test
actions:
- action_validate_slot_mappings
- action_test
"""
)
domain = Domain.from_yaml(domain_yaml)
event = UserUttered(
intent={"name": "inform"}, entities=[{"entity": "city", "value": "london"}]
)
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=[event])
action_server_url = "http:/my-action-server:5055/webhook"
with aioresponses() as mocked:
mocked.post(
action_server_url,
payload={
"events": [{"event": "slot", "name": "custom_slot", "value": "test"}]
},
)
mocked.post(
action_server_url,
payload={
"events": [{"event": "slot", "name": "location", "value": "London"}]
},
)
action_server = EndpointConfig(action_server_url)
action_extract_slots = ActionExtractSlots(action_server)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert slot_events == [
SlotSet("location", "London"),
SlotSet("custom_slot", "test"),
]
async def test_action_extract_slots_with_duplicate_custom_actions():
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intents:
- inform
entities:
- city
slots:
custom_slot_one:
type: float
influence_conversation: false
mappings:
- type: custom
action: action_test
custom_slot_two:
type: float
influence_conversation: false
mappings:
- type: custom
action: action_test
actions:
- action_test
"""
)
domain = Domain.from_yaml(domain_yaml)
event = UserUttered("Hi")
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=[event])
action_server_url = "http:/my-action-server:5055/webhook"
with aioresponses() as mocked:
mocked.post(
action_server_url,
payload={
"events": [
{"event": "slot", "name": "custom_slot_one", "value": 1},
{"event": "slot", "name": "custom_slot_two", "value": 2},
]
},
)
mocked.post(
action_server_url,
payload={
"events": [
{"event": "slot", "name": "custom_slot_one", "value": 1},
{"event": "slot", "name": "custom_slot_two", "value": 2},
]
},
)
action_server = EndpointConfig(action_server_url)
action_extract_slots = ActionExtractSlots(action_server)
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert len(mocked.requests) == 1
assert len(slot_events) == 2
assert SlotSet("custom_slot_two", 2) in slot_events
assert SlotSet("custom_slot_one", 1) in slot_events
async def test_action_extract_slots_disallowed_events(caplog: LogCaptureFixture):
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
slots:
custom_slot_one:
type: float
influence_conversation: false
mappings:
- type: custom
action: action_test
actions:
- action_test
"""
)
domain = Domain.from_yaml(domain_yaml)
event = UserUttered("Hi")
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=[event])
action_server_url = "http:/my-action-server:5055/webhook"
with aioresponses() as mocked:
mocked.post(
action_server_url,
payload={
"events": [
{"event": "slot", "name": "custom_slot_one", "value": 1},
{"event": "reset_slots"},
]
},
)
action_server = EndpointConfig(action_server_url)
action_extract_slots = ActionExtractSlots(action_server)
with caplog.at_level(logging.INFO):
slot_events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
caplog_info_records = list(
filter(lambda x: x[1] == logging.INFO, caplog.record_tuples)
)
assert all(
[
"Running custom action 'action_test' has resulted "
"in an event of type 'reset_slots'." in record[2]
for record in caplog_info_records
]
)
assert slot_events == [SlotSet("custom_slot_one", 1)]
@pytest.mark.parametrize(
"exception",
[
RasaException("Test"),
ClientResponseError(400, "Test", '{"action_name": "action_test"}'),
],
)
async def test_action_extract_slots_warns_custom_action_exceptions(
caplog: LogCaptureFixture, exception: Exception
):
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
slots:
custom_slot_one:
type: float
influence_conversation: false
mappings:
- type: custom
action: action_test
actions:
- action_test
"""
)
domain = Domain.from_yaml(domain_yaml)
event = UserUttered("Hi")
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=[event])
action_server_url = "http:/my-action-server:5055/webhook"
with aioresponses() as mocked:
mocked.post(action_server_url, exception=exception)
action_server = EndpointConfig(action_server_url)
action_extract_slots = ActionExtractSlots(action_server)
with caplog.at_level(logging.WARNING):
await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert any(
[
"The default action 'action_extract_slots' failed to fill "
"slots with custom mappings." in message
for message in caplog.messages
]
)
async def test_action_extract_slots_with_empty_conditions():
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
entities:
- city
slots:
location:
type: float
influence_conversation: false
mappings:
- type: from_entity
entity: city
conditions: []
"""
)
domain = Domain.from_yaml(domain_yaml)
event = UserUttered("Hi", entities=[{"entity": "city", "value": "Berlin"}])
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=[event])
action_extract_slots = ActionExtractSlots(None)
with pytest.warns(None):
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == [SlotSet("location", "Berlin")]
async def test_action_extract_slots_with_not_existing_entity():
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
entities:
- city
slots:
location:
type: float
influence_conversation: false
mappings:
- type: from_entity
entity: city2
"""
)
domain = Domain.from_yaml(domain_yaml)
event = UserUttered("Hi", entities=[{"entity": "city", "value": "Berlin"}])
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=[event])
action_extract_slots = ActionExtractSlots(None)
with pytest.warns(
None,
match="Slot 'location' uses a `from_entity` mapping for "
"a non-existent entity 'city2'. "
"Skipping slot extraction because of invalid mapping.",
):
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == []
async def test_action_extract_slots_with_not_existing_intent():
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intents:
- greet
slots:
location:
type: text
influence_conversation: false
mappings:
- type: from_intent
intent: affirm
value: some_value
"""
)
domain = Domain.from_yaml(domain_yaml)
event = UserUttered("Hi", entities=[{"entity": "city", "value": "Berlin"}])
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=[event])
action_extract_slots = ActionExtractSlots(None)
with pytest.warns(
UserWarning,
match=r"Slot 'location' uses a 'from_intent' mapping for "
r"a non-existent intent 'affirm'. "
r"Skipping slot extraction because of invalid mapping.",
):
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == []
async def test_action_extract_slots_with_none_value_predefined_mapping():
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
entities:
- some_entity
slots:
some_slot:
type: text
influence_conversation: false
mappings:
- type: from_entity
entity: some_entity
custom_slot:
type: text
influence_conversation: false
mappings:
- type: custom
actions:
- action_validate_slot_mappings
"""
)
domain = Domain.from_yaml(domain_yaml)
event = UserUttered("Hi", entities=[{"entity": "some_entity", "value": None}])
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=[event])
action_extract_slots = ActionExtractSlots(None)
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == []
async def test_action_extract_slots_with_none_value_custom_mapping():
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
slots:
custom_slot:
type: text
influence_conversation: false
mappings:
- type: custom
actions:
- action_validate_slot_mappings
"""
)
domain = Domain.from_yaml(domain_yaml)
event = UserUttered("Hi")
tracker = DialogueStateTracker.from_events(
sender_id="test_id", evts=[event], slots=domain.slots
)
action_server_url = "http:/my-action-server:5055/webhook"
with aioresponses() as mocked:
mocked.post(
action_server_url,
payload={
"events": [{"event": "slot", "name": "custom_slot", "value": None}]
},
)
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 == [SlotSet("custom_slot", None)]
async def test_action_extract_slots_returns_bot_uttered():
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
slots:
custom_slot:
type: text
influence_conversation: false
mappings:
- type: custom
actions:
- action_validate_slot_mappings
"""
)
domain = Domain.from_yaml(domain_yaml)
event = UserUttered("Hi")
tracker = DialogueStateTracker.from_events(
sender_id="test_id", evts=[event], slots=domain.slots
)
action_server_url = "http:/my-action-server:5055/webhook"
with aioresponses() as mocked:
mocked.post(
action_server_url,
payload={
"events": [
{"event": "slot", "name": "custom_slot", "value": "test"},
{"event": "bot", "text": "Information recorded."},
]
},
)
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 all([isinstance(event, (SlotSet, BotUttered)) for event in events])
async def test_action_extract_slots_does_not_raise_disallowed_warning_for_slot_events(
caplog: LogCaptureFixture,
):
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
slots:
custom_slot_a:
type: text
influence_conversation: false
mappings:
- type: custom
action: custom_extract_action
custom_slot_b:
type: text
influence_conversation: false
mappings:
- type: custom
actions:
- custom_extract_action
- action_validate_slot_mappings
"""
)
domain = Domain.from_yaml(domain_yaml)
event = UserUttered("Hi")
tracker = DialogueStateTracker.from_events(
sender_id="test_id", evts=[event], slots=domain.slots
)
action_server_url = "http:/my-action-server:5055/webhook"
with aioresponses() as mocked:
mocked.post(
action_server_url,
payload={
"events": [
{"event": "slot", "name": "custom_slot_a", "value": "test_A"}
]
},
)
mocked.post(
action_server_url,
payload={
"events": [
{"event": "slot", "name": "custom_slot_b", "value": "test_B"}
]
},
)
action_server = EndpointConfig(action_server_url)
action_extract_slots = ActionExtractSlots(action_server)
with caplog.at_level(logging.INFO):
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
caplog_info_records = list(
filter(lambda x: x[1] == logging.INFO, caplog.record_tuples)
)
assert len(caplog_info_records) == 0
assert events == [
SlotSet("custom_slot_b", "test_B"),
SlotSet("custom_slot_a", "test_A"),
]
async def test_action_extract_slots_non_required_form_slot_with_from_entity_mapping():
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intents:
- form_start
- intent1
- intent2
entities:
- form1_info1
- form1_slot1
- form1_slot2
slots:
form1_info1:
type: text
mappings:
- type: from_entity
entity: form1_info1
form1_slot1:
type: text
influence_conversation: false
mappings:
- type: from_intent
value: Filled
intent: intent1
conditions:
- active_loop: form1
requested_slot: form1_slot1
form1_slot2:
type: text
influence_conversation: false
mappings:
- type: from_intent
value: Filled
intent: intent2
conditions:
- active_loop: form1
requested_slot: form1_slot2
forms:
form1:
required_slots:
- form1_slot1
- form1_slot2
"""
)
domain = Domain.from_yaml(domain_yaml)
initial_events = [
UserUttered("Start form."),
ActiveLoop("form1"),
SlotSet(REQUESTED_SLOT, "form1_slot1"),
UserUttered(
"Hi",
intent={"name": "intent1"},
entities=[{"entity": "form1_info1", "value": "info1"}],
),
]
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=initial_events)
action_extract_slots = ActionExtractSlots(None)
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == [SlotSet("form1_info1", "info1"), SlotSet("form1_slot1", "Filled")]
@pytest.mark.parametrize(
"starting_value, value_to_set, expect_event",
[
("value", None, True),
(None, "value", True),
("value", "value", True),
(None, None, False),
],
)
async def test_action_extract_slots_emits_necessary_slot_set_events(
starting_value: Text, value_to_set: Text, expect_event: bool
):
entity_name = "entity"
intent_name = "intent_with_entity"
domain = textwrap.dedent(
f"""
intents:
- {intent_name}
entities:
- {entity_name}
slots:
{entity_name}:
type: text
mappings:
- type: from_entity
entity: {entity_name}
"""
)
domain = Domain.from_yaml(domain)
tracker = DialogueStateTracker.from_events(
"some-sender",
evts=[
SlotSet(entity_name, starting_value),
],
)
tracker.update_with_events(
new_events=[
UserUttered(
text="I am a text",
intent={"name": intent_name},
entities=[{"entity": entity_name, "value": value_to_set}],
)
],
domain=domain,
)
action = ActionExtractSlots(None)
events = await action.run(
output_channel=CollectingOutputChannel(),
nlg=Mock(),
tracker=tracker,
domain=domain,
)
if expect_event:
assert len(events) == 1
assert type(events[0]) == SlotSet
assert events[0].key == entity_name
assert events[0].value == value_to_set
else:
assert len(events) == 0
async def test_action_extract_slots_priority_of_slot_mappings():
slot_name = "location_slot"
entity_name = "location"
entity_value = "Berlin"
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intents:
- inform
entities:
- {entity_name}
slots:
{slot_name}:
type: text
influence_conversation: false
mappings:
- type: from_entity
entity: {entity_name}
- type: from_intent
value: 42
intent: inform
responses:
utter_ask_location:
- text: "where are you located?"
"""
)
domain = Domain.from_yaml(domain_yaml)
initial_events = [
UserUttered(
"I am located in Berlin",
intent={"name": "inform"},
entities=[{"entity": entity_name, "value": entity_value}],
),
]
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=initial_events)
action_extract_slots = ActionExtractSlots(None)
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
tracker.update_with_events(events, domain=domain)
assert tracker.get_slot("location_slot") == entity_value
async def test_action_extract_slots_allows_slotset_for_same_value(
caplog: LogCaptureFixture,
):
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
slots:
custom_slot_a:
type: text
influence_conversation: false
mappings:
- type: custom
action: custom_extract_action
actions:
- custom_extract_action
- action_validate_slot_mappings
"""
)
domain = Domain.from_yaml(domain_yaml)
event = UserUttered("Hi")
tracker = DialogueStateTracker.from_events(
sender_id="test_id", evts=[event], slots=domain.slots
)
# Set the value of the slot in the tracker manually
tracker.update(SlotSet("custom_slot_a", "test_A"))
action_server_url = "https://my-action-server:5055/webhook"
with aioresponses() as mocked:
mocked.post(
action_server_url,
payload={
"events": [
{"event": "slot", "name": "custom_slot_a", "value": "test_A"}
]
},
)
action_server = EndpointConfig(action_server_url)
action_extract_slots = ActionExtractSlots(action_server)
with caplog.at_level(logging.INFO):
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
caplog_info_records = list(
filter(lambda x: x[1] == logging.INFO, caplog.record_tuples)
)
assert len(caplog_info_records) == 0
assert events == [SlotSet("custom_slot_a", "test_A")]
async def test_action_extract_slots_active_loop_none_in_mapping_condition():
entity = "name"
entity_value = "Julia"
slot = "user_name"
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intents:
- greet
entities:
- {entity}
slots:
{slot}:
type: text
mappings:
- type: from_entity
entity: {entity}
conditions:
- active_loop: null
"""
)
domain = Domain.from_yaml(domain_yaml)
initial_events = [
UserUttered(
"Hi, I'm Julia.",
intent={"name": "greet"},
entities=[{"entity": entity, "value": entity_value}],
),
]
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=initial_events)
action_extract_slots = ActionExtractSlots(None)
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == [SlotSet(slot, entity_value)]
async def test_action_extract_slots_active_loop_none_does_not_set_slot_in_form():
entity = "name"
entity_value = "Julia"
slot = "user_name"
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intents:
- greet
entities:
- {entity}
slots:
{slot}:
type: text
mappings:
- type: from_entity
entity: {entity}
conditions:
- active_loop: null
forms:
my_form:
required_slots: []
"""
)
domain = Domain.from_yaml(domain_yaml)
initial_events = [
ActiveLoop("my_form"),
UserUttered(
"Hi, I'm Julia.",
intent={"name": "greet"},
entities=[{"entity": entity, "value": entity_value}],
),
]
tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=initial_events)
action_extract_slots = ActionExtractSlots(None)
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == []