Files
rasahq--rasa/tests/core/test_processor.py
T
wehub-resource-sync dc6079821b
CI Github Actions / Run Tests (push) Waiting to run
Automatic PR Merger / mergepal (push) Waiting to run
Semgrep / Semgrep Workflow Security Scan (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 13:24:47 +08:00

1993 lines
65 KiB
Python

import asyncio
import datetime
from http import HTTPStatus
import os.path
import shutil
import textwrap
from pathlib import Path
import freezegun
import pytest
from unittest.mock import MagicMock
from rasa.plugin import plugin_manager
import time
import uuid
import json
from _pytest.monkeypatch import MonkeyPatch
from _pytest.logging import LogCaptureFixture
from aioresponses import aioresponses
from typing import Optional, Text, List, Callable, Type, Any
from rasa.core.lock_store import InMemoryLockStore
from rasa.core.policies.ensemble import DefaultPolicyPredictionEnsemble
from rasa.core.tracker_store import InMemoryTrackerStore
import rasa.shared.utils.io
from rasa.core.actions.action import (
ActionBotResponse,
ActionListen,
ActionExecutionRejection,
ActionUnlikelyIntent,
)
from rasa.core.nlg import NaturalLanguageGenerator, TemplatedNaturalLanguageGenerator
from rasa.core.policies.policy import PolicyPrediction
from tests.conftest import (
with_assistant_id,
with_assistant_ids,
with_model_id,
with_model_ids,
)
import tests.utilities
from rasa.core import jobs
from rasa.core.agent import Agent, load_agent
from rasa.core.channels.channel import (
CollectingOutputChannel,
UserMessage,
OutputChannel,
)
from rasa.engine.graph import ExecutionContext
from rasa.engine.storage.storage import ModelStorage
from rasa.exceptions import ActionLimitReached
from rasa.nlu.tokenizers.whitespace_tokenizer import WhitespaceTokenizer
from rasa.shared.constants import ASSISTANT_ID_KEY, LATEST_TRAINING_DATA_FORMAT_VERSION
from rasa.shared.core.domain import SessionConfig, Domain, KEY_ACTIONS
from rasa.shared.core.events import (
ActionExecuted,
ActiveLoop,
BotUttered,
ReminderCancelled,
ReminderScheduled,
Restarted,
UserUttered,
SessionStarted,
Event,
SlotSet,
DefinePrevUserUtteredFeaturization,
ActionExecutionRejected,
LoopInterrupted,
)
from rasa.core.http_interpreter import RasaNLUHttpInterpreter
from rasa.core.processor import MessageProcessor
from rasa.shared.core.trackers import DialogueStateTracker
from rasa.shared.nlu.constants import (
INTENT,
INTENT_NAME_KEY,
FULL_RETRIEVAL_INTENT_NAME_KEY,
METADATA_MODEL_ID,
)
from rasa.shared.nlu.training_data.message import Message
from rasa.utils.endpoints import EndpointConfig
from rasa.shared.core.constants import (
ACTION_EXTRACT_SLOTS,
ACTION_RESTART_NAME,
ACTION_UNLIKELY_INTENT_NAME,
DEFAULT_INTENTS,
ACTION_LISTEN_NAME,
ACTION_SESSION_START_NAME,
EXTERNAL_MESSAGE_PREFIX,
IS_EXTERNAL,
SESSION_START_METADATA_SLOT,
)
import logging
logger = logging.getLogger(__name__)
async def test_message_processor(
default_channel: CollectingOutputChannel, default_processor: MessageProcessor
):
await default_processor.handle_message(
UserMessage('/greet{"name":"Core"}', default_channel)
)
assert default_channel.latest_output() == {
"recipient_id": "default",
"text": "hey there Core!",
}
async def test_message_id_logging(default_processor: MessageProcessor):
message = UserMessage("If Meg was an egg would she still have a leg?")
tracker = DialogueStateTracker("1", [])
await default_processor._handle_message_with_tracker(message, tracker)
logged_event = tracker.events[-1]
assert logged_event.message_id == message.message_id
assert logged_event.message_id is not None
async def test_parsing(default_processor: MessageProcessor):
message = UserMessage('/greet{"name": "boy"}')
parsed = await default_processor.parse_message(message)
assert parsed["intent"][INTENT_NAME_KEY] == "greet"
assert parsed["entities"][0]["entity"] == "name"
async def test_check_for_unseen_feature(default_processor: MessageProcessor):
message = UserMessage('/greet{"name": "Joe"}')
old_domain = default_processor.domain
dict_for_new_domain = old_domain.as_dict()
dict_for_new_domain["intents"] = [
intent for intent in dict_for_new_domain["intents"] if intent != "greet"
]
dict_for_new_domain["entities"] = [
entity for entity in dict_for_new_domain["entities"] if entity != "name"
]
new_domain = Domain.from_dict(dict_for_new_domain)
default_processor.domain = new_domain
parsed = await default_processor.parse_message(message)
with pytest.warns(UserWarning) as record:
default_processor._check_for_unseen_features(parsed)
assert len(record) == 2
assert record[0].message.args[0].startswith("Parsed an intent 'greet'")
assert record[1].message.args[0].startswith("Parsed an entity 'name'")
default_processor.domain = old_domain
@pytest.mark.parametrize("default_intent", DEFAULT_INTENTS)
async def test_default_intent_recognized(
default_processor: MessageProcessor, default_intent: Text
):
message = UserMessage(f"/{default_intent}")
parsed = await default_processor.parse_message(message)
with pytest.warns(None) as record:
default_processor._check_for_unseen_features(parsed)
assert len(record) == 0
async def test_http_parsing(trained_default_agent_model: Text, domain: Domain):
message = UserMessage("lunch?")
endpoint = EndpointConfig("https://interpreter.com")
response_body = {
"intent": {INTENT_NAME_KEY: "some_intent", "confidence": 1.0},
"entities": [],
"text": "lunch?",
}
with aioresponses() as mocked:
mocked.post(
"https://interpreter.com/model/parse",
repeat=True,
status=HTTPStatus.OK,
body=json.dumps(response_body),
)
inter = RasaNLUHttpInterpreter(endpoint_config=endpoint)
processor = MessageProcessor(
trained_default_agent_model,
InMemoryTrackerStore(domain),
InMemoryLockStore(),
NaturalLanguageGenerator(),
http_interpreter=inter,
)
data = await processor.parse_message(message)
r = tests.utilities.latest_request(
mocked, "POST", "https://interpreter.com/model/parse"
)
assert r
assert data == response_body
async def test_http_parsing_default_response(
trained_default_agent_model: Text, domain: Domain
):
message = UserMessage("lunch?")
endpoint = EndpointConfig("https://interpreter.com")
with aioresponses() as mocked:
mocked.post(
"https://interpreter.com/model/parse",
repeat=True,
status=HTTPStatus.OK,
body=None,
)
inter = RasaNLUHttpInterpreter(endpoint_config=endpoint)
processor = MessageProcessor(
trained_default_agent_model,
InMemoryTrackerStore(domain),
InMemoryLockStore(),
NaturalLanguageGenerator(),
http_interpreter=inter,
)
data = await processor.parse_message(message)
r = tests.utilities.latest_request(
mocked, "POST", "https://interpreter.com/model/parse"
)
assert r
assert data == {
"intent": {INTENT_NAME_KEY: "", "confidence": 0.0},
"entities": [],
"text": "",
}
async def test_reminder_scheduled(
default_channel: CollectingOutputChannel, default_processor: MessageProcessor
):
sender_id = uuid.uuid4().hex
reminder = ReminderScheduled("remind", datetime.datetime.now())
tracker = await default_processor.tracker_store.get_or_create_tracker(sender_id)
tracker.update(UserUttered("test"))
tracker.update(ActionExecuted("action_schedule_reminder"))
tracker.update(reminder)
await default_processor.tracker_store.save(tracker)
await default_processor.handle_reminder(reminder, sender_id, default_channel)
# retrieve the updated tracker
t = await default_processor.tracker_store.retrieve(sender_id)
assert t.events[1] == UserUttered("test")
assert t.events[2] == ActionExecuted("action_schedule_reminder")
assert isinstance(t.events[3], ReminderScheduled)
assert t.events[4] == UserUttered(
f"{EXTERNAL_MESSAGE_PREFIX}remind",
intent={INTENT_NAME_KEY: "remind", IS_EXTERNAL: True},
)
async def test_reminder_lock(
default_channel: CollectingOutputChannel,
default_processor: MessageProcessor,
caplog: LogCaptureFixture,
):
caplog.clear()
with caplog.at_level(logging.DEBUG):
sender_id = uuid.uuid4().hex
reminder = ReminderScheduled("remind", datetime.datetime.now())
tracker = await default_processor.tracker_store.get_or_create_tracker(sender_id)
tracker.update(UserUttered("test"))
tracker.update(ActionExecuted("action_schedule_reminder"))
tracker.update(reminder)
await default_processor.tracker_store.save(tracker)
await default_processor.handle_reminder(reminder, sender_id, default_channel)
assert f"Deleted lock for conversation '{sender_id}'." in caplog.text
async def test_trigger_external_latest_input_channel(
default_channel: CollectingOutputChannel, default_processor: MessageProcessor
):
sender_id = uuid.uuid4().hex
tracker = await default_processor.tracker_store.get_or_create_tracker(sender_id)
input_channel = "test_input_channel_external"
tracker.update(UserUttered("test1"))
tracker.update(UserUttered("test2", input_channel=input_channel))
await default_processor.trigger_external_user_uttered(
"test3", None, tracker, default_channel
)
tracker = await default_processor.tracker_store.retrieve(sender_id)
assert tracker.get_latest_input_channel() == input_channel
async def test_reminder_aborted(
default_channel: CollectingOutputChannel, default_processor: MessageProcessor
):
sender_id = uuid.uuid4().hex
reminder = ReminderScheduled(
"utter_greet", datetime.datetime.now(), kill_on_user_message=True
)
tracker = await default_processor.tracker_store.get_or_create_tracker(sender_id)
tracker.update(reminder)
tracker.update(UserUttered("test")) # cancels the reminder
await default_processor.tracker_store.save(tracker)
await default_processor.handle_reminder(reminder, sender_id, default_channel)
# retrieve the updated tracker
t = await default_processor.tracker_store.retrieve(sender_id)
assert len(t.events) == 3 # nothing should have been executed
async def wait_until_all_jobs_were_executed(
timeout_after_seconds: Optional[float] = None,
) -> None:
total_seconds = 0.0
while len((await jobs.scheduler()).get_jobs()) > 0 and (
not timeout_after_seconds or total_seconds < timeout_after_seconds
):
await asyncio.sleep(0.1)
total_seconds += 0.1
if total_seconds >= timeout_after_seconds:
jobs.kill_scheduler()
raise TimeoutError
async def test_reminder_cancelled_multi_user(
default_channel: CollectingOutputChannel, default_processor: MessageProcessor
):
sender_ids = [uuid.uuid4().hex, uuid.uuid4().hex]
trackers = []
for sender_id in sender_ids:
tracker = await default_processor.tracker_store.get_or_create_tracker(sender_id)
tracker.update(UserUttered("test"))
tracker.update(ActionExecuted("action_reminder_reminder"))
tracker.update(
ReminderScheduled(
"greet", datetime.datetime.now(), kill_on_user_message=True
)
)
trackers.append(tracker)
# cancel all reminders (one) for the first user
trackers[0].update(ReminderCancelled())
for tracker in trackers:
await default_processor.tracker_store.save(tracker)
await default_processor._schedule_reminders(
tracker.events, tracker, default_channel
)
# check that the jobs were added
assert len((await jobs.scheduler()).get_jobs()) == 2
for tracker in trackers:
await default_processor._cancel_reminders(tracker.events, tracker)
# check that only one job was removed
assert len((await jobs.scheduler()).get_jobs()) == 1
# execute the jobs
await wait_until_all_jobs_were_executed(timeout_after_seconds=5.0)
tracker_0 = await default_processor.tracker_store.retrieve(sender_ids[0])
# there should be no utter_greet action
assert (
UserUttered(
f"{EXTERNAL_MESSAGE_PREFIX}greet",
intent={INTENT_NAME_KEY: "greet", IS_EXTERNAL: True},
)
not in tracker_0.events
)
tracker_1 = await default_processor.tracker_store.retrieve(sender_ids[1])
# there should be utter_greet action
assert (
UserUttered(
f"{EXTERNAL_MESSAGE_PREFIX}greet",
intent={INTENT_NAME_KEY: "greet", IS_EXTERNAL: True},
)
in tracker_1.events
)
async def test_reminder_cancelled_cancels_job_with_name(
default_channel: CollectingOutputChannel, default_processor: MessageProcessor
):
sender_id = "][]][xy,,=+2f'[:/;>] <0d]A[e_,02"
reminder = ReminderScheduled(
intent="greet", trigger_date_time=datetime.datetime.now()
)
job_name = reminder.scheduled_job_name(sender_id)
reminder_cancelled = ReminderCancelled()
assert reminder_cancelled.cancels_job_with_name(job_name, sender_id)
assert not reminder_cancelled.cancels_job_with_name(job_name.upper(), sender_id)
async def test_reminder_cancelled_cancels_job_with_name_special_name(
default_channel: CollectingOutputChannel, default_processor: MessageProcessor
):
sender_id = "][]][xy,,=+2f'[:/; >]<0d]A[e_,02"
name = "wkjbgr,34(,*&%^^&*(OP#LKMN V#NF# # #R"
reminder = ReminderScheduled(
intent="greet", trigger_date_time=datetime.datetime.now(), name=name
)
job_name = reminder.scheduled_job_name(sender_id)
reminder_cancelled = ReminderCancelled(name)
assert reminder_cancelled.cancels_job_with_name(job_name, sender_id)
assert not reminder_cancelled.cancels_job_with_name(job_name.upper(), sender_id)
async def cancel_reminder_and_check(
tracker: DialogueStateTracker,
default_processor: MessageProcessor,
reminder_canceled_event: ReminderCancelled,
num_jobs_before: int,
num_jobs_after: int,
) -> None:
# cancel the sixth reminder
tracker.update(reminder_canceled_event)
# check that the jobs were added
assert len((await jobs.scheduler()).get_jobs()) == num_jobs_before
await default_processor._cancel_reminders(tracker.events, tracker)
# check that only one job was removed
assert len((await jobs.scheduler()).get_jobs()) == num_jobs_after
async def test_reminder_cancelled_by_name(
default_channel: CollectingOutputChannel,
default_processor: MessageProcessor,
tracker_with_six_scheduled_reminders: DialogueStateTracker,
):
tracker = tracker_with_six_scheduled_reminders
await default_processor._schedule_reminders(
tracker.events, tracker, default_channel
)
# cancel the sixth reminder
await cancel_reminder_and_check(
tracker, default_processor, ReminderCancelled("special"), 6, 5
)
async def test_reminder_cancelled_by_entities(
default_channel: CollectingOutputChannel,
default_processor: MessageProcessor,
tracker_with_six_scheduled_reminders: DialogueStateTracker,
):
tracker = tracker_with_six_scheduled_reminders
await default_processor._schedule_reminders(
tracker.events, tracker, default_channel
)
# cancel the fourth reminder
await cancel_reminder_and_check(
tracker,
default_processor,
ReminderCancelled(entities=[{"entity": "name", "value": "Bruce Wayne"}]),
6,
5,
)
async def test_reminder_cancelled_by_intent(
default_channel: CollectingOutputChannel,
default_processor: MessageProcessor,
tracker_with_six_scheduled_reminders: DialogueStateTracker,
):
tracker = tracker_with_six_scheduled_reminders
await default_processor._schedule_reminders(
tracker.events, tracker, default_channel
)
# cancel the third, fifth, and sixth reminder
await cancel_reminder_and_check(
tracker, default_processor, ReminderCancelled(intent="default"), 6, 3
)
async def test_reminder_cancelled_all(
default_channel: CollectingOutputChannel,
default_processor: MessageProcessor,
tracker_with_six_scheduled_reminders: DialogueStateTracker,
):
tracker = tracker_with_six_scheduled_reminders
await default_processor._schedule_reminders(
tracker.events, tracker, default_channel
)
# cancel all reminders
await cancel_reminder_and_check(
tracker, default_processor, ReminderCancelled(), 6, 0
)
async def test_reminder_restart(
default_channel: CollectingOutputChannel, default_processor: MessageProcessor
):
sender_id = uuid.uuid4().hex
reminder = ReminderScheduled(
"utter_greet", datetime.datetime.now(), kill_on_user_message=False
)
tracker = await default_processor.tracker_store.get_or_create_tracker(sender_id)
tracker.update(reminder)
tracker.update(Restarted()) # cancels the reminder
tracker.update(UserUttered("test"))
await default_processor.tracker_store.save(tracker)
await default_processor.handle_reminder(reminder, sender_id, default_channel)
# retrieve the updated tracker
t = await default_processor.tracker_store.retrieve(sender_id)
assert len(t.events) == 4 # nothing should have been executed
@pytest.mark.parametrize(
"event_to_apply,session_expiration_time_in_minutes,has_expired",
[
# last user event is way in the past
(UserUttered(timestamp=1), 60, True),
# user event are very recent
(UserUttered("hello", timestamp=time.time()), 120, False),
# there is user event
(ActionExecuted(ACTION_LISTEN_NAME, timestamp=time.time()), 60, False),
# Old event, but sessions are disabled
(UserUttered("hello", timestamp=1), 0, False),
# there is no event
(None, 1, False),
],
)
async def test_has_session_expired(
event_to_apply: Optional[Event],
session_expiration_time_in_minutes: float,
has_expired: bool,
default_processor: MessageProcessor,
):
sender_id = uuid.uuid4().hex
default_processor.domain.session_config = SessionConfig(
session_expiration_time_in_minutes, True
)
# create new tracker without events
tracker = await default_processor.tracker_store.get_or_create_tracker(sender_id)
tracker.events.clear()
# apply desired event
if event_to_apply:
tracker.update(event_to_apply)
# noinspection PyProtectedMember
assert default_processor._has_session_expired(tracker) == has_expired
# noinspection PyProtectedMember
async def test_update_tracker_session(
default_channel: CollectingOutputChannel,
default_processor: MessageProcessor,
monkeypatch: MonkeyPatch,
):
sender_id = uuid.uuid4().hex
tracker = await default_processor.tracker_store.get_or_create_tracker(sender_id)
# patch `_has_session_expired()` so the `_update_tracker_session()` call actually
# does something
monkeypatch.setattr(default_processor, "_has_session_expired", lambda _: True)
await default_processor._update_tracker_session(tracker, default_channel)
# the save is not called in _update_tracker_session()
await default_processor.save_tracker(tracker)
# inspect tracker and make sure all events are present
tracker = await default_processor.tracker_store.retrieve_full_tracker(sender_id)
assert list(tracker.events) == [
ActionExecuted(ACTION_LISTEN_NAME),
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
ActionExecuted(ACTION_LISTEN_NAME),
]
async def test_update_tracker_session_with_metadata(
default_processor: MessageProcessor, monkeypatch: MonkeyPatch
):
model_id = default_processor.model_metadata.model_id
assistant_id = default_processor.model_metadata.assistant_id
sender_id = uuid.uuid4().hex
message_metadata = {"metadataTestKey": "metadataTestValue"}
message = UserMessage(
text="hi",
output_channel=CollectingOutputChannel(),
sender_id=sender_id,
metadata=message_metadata,
)
await default_processor.handle_message(message)
tracker = await default_processor.tracker_store.retrieve_full_tracker(sender_id)
events = list(tracker.events)
with_model_ids_expected = with_model_ids(
[
SlotSet(SESSION_START_METADATA_SLOT, message_metadata),
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
SlotSet(SESSION_START_METADATA_SLOT, message_metadata),
ActionExecuted(ACTION_LISTEN_NAME),
],
model_id,
)
final_expected = with_assistant_ids(with_model_ids_expected, assistant_id)
assert events[0:5] == final_expected[0:5]
assert tracker.slots[SESSION_START_METADATA_SLOT].value == message_metadata
assert events[2].metadata == {
ASSISTANT_ID_KEY: assistant_id,
METADATA_MODEL_ID: model_id,
}
assert isinstance(events[5], UserUttered)
@freezegun.freeze_time("2020-02-01")
async def test_custom_action_session_start_with_metadata(
default_processor: MessageProcessor,
):
domain = Domain.from_dict({KEY_ACTIONS: [ACTION_SESSION_START_NAME]})
default_processor.domain = domain
model_id = default_processor.model_metadata.model_id
action_server_url = "http://some-url"
default_processor.action_endpoint = EndpointConfig(action_server_url)
sender_id = uuid.uuid4().hex
metadata = {"metadataTestKey": "metadataTestValue"}
message = UserMessage(
text="hi",
output_channel=CollectingOutputChannel(),
sender_id=sender_id,
metadata=metadata,
)
with aioresponses() as mocked:
mocked.post(action_server_url, payload={"events": []})
await default_processor.handle_message(message)
last_request = tests.utilities.latest_request(mocked, "post", action_server_url)
tracker_for_custom_action = tests.utilities.json_of_latest_request(last_request)[
"tracker"
]
assert tracker_for_custom_action["events"] == [
{
"event": "slot",
"timestamp": 1580515200.0,
"name": SESSION_START_METADATA_SLOT,
"value": metadata,
"metadata": {"assistant_id": "placeholder_default", "model_id": model_id},
}
]
# noinspection PyProtectedMember
async def test_update_tracker_session_with_slots(
default_channel: CollectingOutputChannel,
default_processor: MessageProcessor,
monkeypatch: MonkeyPatch,
):
sender_id = uuid.uuid4().hex
tracker = await default_processor.tracker_store.get_or_create_tracker(sender_id)
# apply a user uttered and five slots
user_event = UserUttered("some utterance")
tracker.update(user_event)
slot_set_events = [SlotSet(f"slot key {i}", f"test value {i}") for i in range(5)]
for event in slot_set_events:
tracker.update(event)
# patch `_has_session_expired()` so the `_update_tracker_session()` call actually
# does something
monkeypatch.setattr(default_processor, "_has_session_expired", lambda _: True)
await default_processor._update_tracker_session(tracker, default_channel)
# the save is not called in _update_tracker_session()
await default_processor.save_tracker(tracker)
# inspect tracker and make sure all events are present
tracker = await default_processor.tracker_store.retrieve_full_tracker(sender_id)
events = list(tracker.events)
# the first three events should be up to the user utterance
assert events[:2] == [ActionExecuted(ACTION_LISTEN_NAME), user_event]
# next come the five slots
assert events[2:7] == slot_set_events
# the next two events are the session start sequence
assert events[7:9] == [ActionExecuted(ACTION_SESSION_START_NAME), SessionStarted()]
assert events[9:14] == slot_set_events
# finally an action listen, this should also be the last event
assert events[14] == events[-1] == ActionExecuted(ACTION_LISTEN_NAME)
async def test_fetch_tracker_and_update_session(
default_channel: CollectingOutputChannel, default_processor: MessageProcessor
):
model_id = default_processor.model_metadata.model_id
assistant_id = default_processor.model_metadata.assistant_id
sender_id = uuid.uuid4().hex
tracker = await default_processor.fetch_tracker_and_update_session(
sender_id, default_channel
)
# ensure session start sequence is present
assert list(tracker.events) == with_assistant_ids(
with_model_ids(
[
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
ActionExecuted(ACTION_LISTEN_NAME),
],
model_id,
),
assistant_id,
)
@pytest.mark.parametrize(
"initial_events,expected_event_types",
[
# tracker is initially not empty - when it is fetched, it will just contain
# these four events
(
[
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered("/greet", {INTENT_NAME_KEY: "greet", "confidence": 1.0}),
],
[ActionExecuted, SessionStarted, ActionExecuted, UserUttered],
),
# tracker is initially empty, and contains the session start sequence when
# fetched
([], [ActionExecuted, SessionStarted, ActionExecuted]),
],
)
async def test_fetch_tracker_with_initial_session(
default_channel: CollectingOutputChannel,
default_processor: MessageProcessor,
initial_events: List[Event],
expected_event_types: List[Type[Event]],
):
conversation_id = uuid.uuid4().hex
tracker = DialogueStateTracker.from_events(conversation_id, initial_events)
await default_processor.tracker_store.save(tracker)
tracker = await default_processor.fetch_tracker_with_initial_session(
conversation_id, default_channel
)
# the events in the fetched tracker are as expected
assert len(tracker.events) == len(expected_event_types)
assert all(
isinstance(tracker_event, expected_event_type)
for tracker_event, expected_event_type in zip(
tracker.events, expected_event_types
)
)
async def test_fetch_tracker_with_initial_session_does_not_update_session(
default_channel: CollectingOutputChannel,
default_processor: MessageProcessor,
monkeypatch: MonkeyPatch,
):
conversation_id = uuid.uuid4().hex
# the domain has a session expiration time of one second
monkeypatch.setattr(
default_processor.tracker_store.domain,
"session_config",
SessionConfig(carry_over_slots=True, session_expiration_time=1 / 60),
)
now = time.time()
# the tracker initially contains events
initial_events = [
ActionExecuted(ACTION_SESSION_START_NAME, timestamp=now - 10),
SessionStarted(timestamp=now - 9),
ActionExecuted(ACTION_LISTEN_NAME, timestamp=now - 8),
UserUttered(
"/greet", {INTENT_NAME_KEY: "greet", "confidence": 1.0}, timestamp=now - 7
),
]
tracker = DialogueStateTracker.from_events(conversation_id, initial_events)
await default_processor.tracker_store.save(tracker)
tracker = await default_processor.fetch_tracker_with_initial_session(
conversation_id, default_channel
)
# the conversation session has expired, but calling
# `fetch_tracker_with_initial_session()` did not update it
assert default_processor._has_session_expired(tracker)
assert [event.as_dict() for event in tracker.events] == [
event.as_dict() for event in initial_events
]
async def test_handle_message_with_session_start(
default_channel: CollectingOutputChannel,
default_processor: MessageProcessor,
monkeypatch: MonkeyPatch,
):
sender_id = uuid.uuid4().hex
model_id = default_processor.model_metadata.model_id
assistant_id = default_processor.model_metadata.assistant_id
entity = "name"
slot_1 = {entity: "Core"}
await default_processor.handle_message(
UserMessage(f"/greet{json.dumps(slot_1)}", default_channel, sender_id)
)
assert default_channel.latest_output() == {
"recipient_id": sender_id,
"text": "hey there Core!",
}
# patch processor so a session start is triggered
monkeypatch.setattr(default_processor, "_has_session_expired", lambda _: True)
slot_2 = {entity: "post-session start hello"}
# handle a new message
await default_processor.handle_message(
UserMessage(f"/greet{json.dumps(slot_2)}", default_channel, sender_id)
)
tracker = await default_processor.tracker_store.get_or_create_full_tracker(
sender_id
)
# make sure the sequence of events is as expected
with_model_ids_expected = with_model_ids(
[
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(
f"/greet{json.dumps(slot_1)}",
{INTENT_NAME_KEY: "greet", "confidence": 1.0},
[
{
"entity": entity,
"start": 6,
"end": 22,
"value": "Core",
"extractor": "RegexMessageHandler",
}
],
),
SlotSet(entity, slot_1[entity]),
DefinePrevUserUtteredFeaturization(False),
ActionExecuted("utter_greet"),
BotUttered("hey there Core!", metadata={"utter_action": "utter_greet"}),
ActionExecuted(ACTION_LISTEN_NAME),
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
# the initial SlotSet is reapplied after the SessionStarted sequence
SlotSet(entity, slot_1[entity]),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(
f"/greet{json.dumps(slot_2)}",
{INTENT_NAME_KEY: "greet", "confidence": 1.0},
[
{
"entity": entity,
"start": 6,
"end": 42,
"value": "post-session start hello",
"extractor": "RegexMessageHandler",
}
],
),
SlotSet(entity, slot_2[entity]),
DefinePrevUserUtteredFeaturization(False),
ActionExecuted("utter_greet"),
BotUttered(
"hey there post-session start hello!",
metadata={"utter_action": "utter_greet"},
),
ActionExecuted(ACTION_LISTEN_NAME),
],
model_id,
)
expected = with_assistant_ids(with_model_ids_expected, assistant_id=assistant_id)
assert list(tracker.events) == expected
# noinspection PyProtectedMember
@pytest.mark.parametrize(
"action_name, should_predict_another_action",
[
(ACTION_LISTEN_NAME, False),
(ACTION_SESSION_START_NAME, False),
("utter_greet", True),
],
)
async def test_should_predict_another_action(
default_processor: MessageProcessor,
action_name: Text,
should_predict_another_action: bool,
):
assert (
default_processor.should_predict_another_action(action_name)
== should_predict_another_action
)
async def test_action_unlikely_intent_metadata(default_processor: MessageProcessor):
tracker = DialogueStateTracker.from_events(
"some-sender", evts=[ActionExecuted(ACTION_LISTEN_NAME)]
)
domain = Domain.empty()
metadata = {"key1": 1, "key2": "2"}
await default_processor._run_action(
ActionUnlikelyIntent(),
tracker,
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
PolicyPrediction([], "some policy", action_metadata=metadata),
)
applied_events = tracker.applied_events()
assert applied_events == [
ActionExecuted(ACTION_LISTEN_NAME),
ActionExecuted(ACTION_UNLIKELY_INTENT_NAME, metadata=metadata),
]
assert applied_events[1].metadata == metadata
async def test_restart_triggers_session_start(
default_channel: CollectingOutputChannel,
default_processor: MessageProcessor,
monkeypatch: MonkeyPatch,
default_model_storage: ModelStorage,
default_execution_context: ExecutionContext,
):
sender_id = uuid.uuid4().hex
model_id = default_processor.model_metadata.model_id
assistant_id = default_processor.model_metadata.assistant_id
entity = "name"
slot_1 = {entity: "name1"}
await default_processor.handle_message(
UserMessage(f"/greet{json.dumps(slot_1)}", default_channel, sender_id)
)
assert default_channel.latest_output() == {
"recipient_id": sender_id,
"text": "hey there name1!",
}
# This restarts the chat
await default_processor.handle_message(
UserMessage("/restart", default_channel, sender_id)
)
tracker = await default_processor.tracker_store.get_or_create_tracker(sender_id)
with_model_ids_expected = with_model_ids(
[
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(
f"/greet{json.dumps(slot_1)}",
{INTENT_NAME_KEY: "greet", "confidence": 1.0},
[
{
"entity": entity,
"start": 6,
"end": 23,
"value": "name1",
"extractor": "RegexMessageHandler",
}
],
),
SlotSet(entity, slot_1[entity]),
DefinePrevUserUtteredFeaturization(use_text_for_featurization=False),
ActionExecuted("utter_greet"),
BotUttered("hey there name1!", metadata={"utter_action": "utter_greet"}),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered("/restart", {INTENT_NAME_KEY: "restart", "confidence": 1.0}),
DefinePrevUserUtteredFeaturization(use_text_for_featurization=False),
ActionExecuted(ACTION_RESTART_NAME),
Restarted(),
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
# No previous slot is set due to restart.
ActionExecuted(ACTION_LISTEN_NAME),
],
model_id,
)
expected = with_assistant_ids(with_model_ids_expected, assistant_id)
for actual, expected in zip(tracker.events, expected):
assert actual == expected
async def test_handle_message_if_action_manually_rejects(
default_processor: MessageProcessor, monkeypatch: MonkeyPatch
):
conversation_id = "test"
message = UserMessage("/greet", sender_id=conversation_id)
rejection_events = [
SlotSet("my_slot", "test"),
ActionExecutionRejected("utter_greet"),
SlotSet("some slot", "some value"),
]
async def mocked_run(self, *args: Any, **kwargs: Any) -> List[Event]:
return rejection_events
monkeypatch.setattr(ActionBotResponse, ActionBotResponse.run.__name__, mocked_run)
await default_processor.handle_message(message)
tracker = await default_processor.tracker_store.retrieve(conversation_id)
logged_events = list(tracker.events)
assert ActionExecuted("utter_greet") not in logged_events
assert all(event in logged_events for event in rejection_events)
async def test_policy_events_are_applied_to_tracker(
default_processor: MessageProcessor, monkeypatch: MonkeyPatch
):
model_id = default_processor.model_metadata.model_id
assistant_id = default_processor.model_metadata.assistant_id
expected_action = ACTION_LISTEN_NAME
policy_events = [LoopInterrupted(True)]
conversation_id = "test_policy_events_are_applied_to_tracker"
user_message = "/greet"
with_model_ids_expected_events = with_model_ids(
[
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(user_message, intent={"name": "greet"}),
*policy_events,
],
model_id,
)
expected_events = with_assistant_ids(with_model_ids_expected_events, assistant_id)
def combine_predictions(
self,
predictions: List[PolicyPrediction],
tracker: DialogueStateTracker,
domain: Domain,
**kwargs: Any,
) -> PolicyPrediction:
prediction = PolicyPrediction.for_action_name(
default_processor.domain, expected_action, "some policy"
)
prediction.events = policy_events
return prediction
monkeypatch.setattr(
DefaultPolicyPredictionEnsemble, "combine_predictions", combine_predictions
)
action_received_events = False
async def mocked_run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
# The action already has access to the policy events
nonlocal action_received_events
action_received_events = list(tracker.events) == expected_events
return []
monkeypatch.setattr(ActionListen, ActionListen.run.__name__, mocked_run)
await default_processor.handle_message(
UserMessage(user_message, sender_id=conversation_id)
)
assert action_received_events
tracker = await default_processor.get_tracker(conversation_id)
# The action was logged on the tracker as well
expected_events.append(
with_assistant_id(
with_model_id(ActionExecuted(ACTION_LISTEN_NAME), model_id), assistant_id
)
)
for event, expected in zip(tracker.events, expected_events):
assert event == expected
# noinspection PyTypeChecker
@pytest.mark.parametrize(
"reject_fn",
[
lambda: [ActionExecutionRejected(ACTION_LISTEN_NAME)],
lambda: (_ for _ in ()).throw(ActionExecutionRejection(ACTION_LISTEN_NAME)),
],
)
async def test_policy_events_not_applied_if_rejected(
default_processor: MessageProcessor,
monkeypatch: MonkeyPatch,
reject_fn: Callable[[], List[Event]],
):
model_id = default_processor.model_metadata.model_id
assistant_id = default_processor.model_metadata.assistant_id
expected_action = ACTION_LISTEN_NAME
expected_events = [LoopInterrupted(True)]
conversation_id = "test_policy_events_are_applied_to_tracker"
user_message = "/greet"
def combine_predictions(
self,
predictions: List[PolicyPrediction],
tracker: DialogueStateTracker,
domain: Domain,
**kwargs: Any,
) -> PolicyPrediction:
prediction = PolicyPrediction.for_action_name(
default_processor.domain, expected_action, "some policy"
)
prediction.events = expected_events
return prediction
monkeypatch.setattr(
DefaultPolicyPredictionEnsemble, "combine_predictions", combine_predictions
)
async def mocked_run(*args: Any, **kwargs: Any) -> List[Event]:
return reject_fn()
monkeypatch.setattr(ActionListen, ActionListen.run.__name__, mocked_run)
await default_processor.handle_message(
UserMessage(user_message, sender_id=conversation_id)
)
tracker = await default_processor.get_tracker(conversation_id)
events = with_model_ids(
[
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(user_message, intent={"name": "greet"}),
ActionExecutionRejected(ACTION_LISTEN_NAME),
],
model_id,
)
expected_events = with_assistant_ids(events, assistant_id)
for event, expected in zip(tracker.events, expected_events):
assert event == expected
async def test_logging_of_end_to_end_action(
default_processor: MessageProcessor, monkeypatch: MonkeyPatch
):
model_id = default_processor.model_metadata.model_id
assistant_id = default_processor.model_metadata.assistant_id
end_to_end_action = "hi, how are you?"
new_domain = Domain(
intents=["greet"],
entities=[],
slots=[],
responses={},
action_names=[],
forms={},
action_texts=[end_to_end_action],
data={},
)
default_processor.domain = new_domain
conversation_id = "test_logging_of_end_to_end_action"
user_message = "/greet"
number_of_calls = 0
def combine_predictions(
self,
predictions: List[PolicyPrediction],
tracker: DialogueStateTracker,
domain: Domain,
**kwargs: Any,
) -> PolicyPrediction:
nonlocal number_of_calls
if number_of_calls == 0:
prediction = PolicyPrediction.for_action_name(
new_domain, end_to_end_action, "some policy"
)
prediction.is_end_to_end_prediction = True
number_of_calls += 1
return prediction
else:
return PolicyPrediction.for_action_name(new_domain, ACTION_LISTEN_NAME)
monkeypatch.setattr(
DefaultPolicyPredictionEnsemble, "combine_predictions", combine_predictions
)
await default_processor.handle_message(
UserMessage(user_message, sender_id=conversation_id)
)
tracker = await default_processor.tracker_store.retrieve(conversation_id)
events = with_model_ids(
[
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(user_message, intent={"name": "greet"}),
ActionExecuted(action_text=end_to_end_action),
BotUttered("hi, how are you?", {}, {}, 123),
ActionExecuted(ACTION_LISTEN_NAME),
],
model_id=model_id,
)
expected_events = with_assistant_ids(events, assistant_id)
for event, expected in zip(tracker.events, expected_events):
assert event == expected
async def test_predict_next_action_with_hidden_rules(
trained_async: Callable, tmp_path: Path
):
rule_intent = "rule_intent"
rule_action = "rule_action"
story_intent = "story_intent"
story_action = "story_action"
rule_slot = "rule_slot"
story_slot = "story_slot"
domain_content = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intents:
- {rule_intent}
- {story_intent}
actions:
- {rule_action}
- {story_action}
slots:
{rule_slot}:
type: text
mappings:
- type: from_text
{story_slot}:
type: text
mappings:
- type: from_text
"""
)
domain = Domain.from_yaml(domain_content)
domain_path = tmp_path / "domain.yml"
rasa.shared.utils.io.write_text_file(domain_content, domain_path)
training_data = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
rules:
- rule: rule
steps:
- intent: {rule_intent}
- action: {rule_action}
- slot_was_set:
- {rule_slot}: {rule_slot}
stories:
- story: story
steps:
- intent: {story_intent}
- action: {story_action}
- slot_was_set:
- {story_slot}: {story_slot}
"""
)
training_data_path = tmp_path / "data.yml"
rasa.shared.utils.io.write_text_file(training_data, training_data_path)
config = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
assistant_id: placeholder_default
policies:
- name: RulePolicy
- name: MemoizationPolicy
"""
)
config_path = tmp_path / "config.yml"
rasa.shared.utils.io.write_text_file(config, config_path)
model_path = await trained_async(
str(domain_path), str(config_path), [str(training_data_path)]
)
agent = await load_agent(model_path=model_path)
processor = agent.processor
tracker = DialogueStateTracker.from_events(
"casd",
evts=[
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(intent={"name": rule_intent}),
],
slots=domain.slots,
)
action, prediction = processor.predict_next_with_tracker_if_should(tracker)
assert action._name == rule_action
assert prediction.hide_rule_turn
processor._log_action_on_tracker(
tracker, action, [SlotSet(rule_slot, rule_slot)], prediction
)
action, prediction = processor.predict_next_with_tracker_if_should(tracker)
assert isinstance(action, ActionListen)
assert prediction.hide_rule_turn
processor._log_action_on_tracker(tracker, action, None, prediction)
tracker.events.append(UserUttered(intent={"name": story_intent}))
# rules are hidden correctly if memo policy predicts next actions correctly
action, prediction = processor.predict_next_with_tracker_if_should(tracker)
assert action._name == story_action
assert not prediction.hide_rule_turn
processor._log_action_on_tracker(
tracker, action, [SlotSet(story_slot, story_slot)], prediction
)
action, prediction = processor.predict_next_with_tracker_if_should(tracker)
assert isinstance(action, ActionListen)
assert not prediction.hide_rule_turn
def test_predict_next_action_raises_limit_reached_exception(
default_processor: MessageProcessor,
):
tracker = DialogueStateTracker.from_events(
"test",
evts=[
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered("Hi!"),
ActionExecuted("test_action"),
],
)
tracker.set_latest_action({"action_name": "test_action"})
default_processor.max_number_of_predictions = 1
with pytest.raises(ActionLimitReached):
default_processor.predict_next_with_tracker_if_should(tracker)
async def test_processor_logs_text_tokens_in_tracker(
default_agent: Agent, whitespace_tokenizer: WhitespaceTokenizer
):
text = "Hello there"
tokens = whitespace_tokenizer.tokenize(Message(data={"text": text}), "text")
indices = [(t.start, t.end) for t in tokens]
message = UserMessage(text)
processor = default_agent.processor
tracker = await processor.log_message(message)
event = tracker.get_last_event_for(event_type=UserUttered)
event_tokens = event.as_dict().get("parse_data").get("text_tokens")
assert event_tokens == indices
async def test_processor_valid_slot_setting(default_agent: Agent):
processor = default_agent.processor
message = UserMessage(
"Hiya Peter",
CollectingOutputChannel(),
"test",
parse_data={
"intent": {"name": "greet"},
"entities": [{"entity": "name", "value": "Peter"}],
},
)
await processor.handle_message(message)
tracker = await processor.get_tracker("test")
assert SlotSet("name", "Peter") in tracker.events
async def test_parse_message_nlu_only(trained_moodbot_nlu_path: Text):
processor = Agent.load(model_path=trained_moodbot_nlu_path).processor
message = UserMessage("/greet")
result = await processor.parse_message(message)
assert result == {
"text": "/greet",
"intent": {"name": "greet", "confidence": 1.0},
"intent_ranking": [{"name": "greet", "confidence": 1.0}],
"entities": [],
}
message = UserMessage("Hello")
result = await processor.parse_message(message)
assert result["intent"]["name"]
async def test_parse_message_core_only(trained_core_model: Text):
processor = Agent.load(model_path=trained_core_model).processor
message = UserMessage("/greet")
result = await processor.parse_message(message)
assert result == {
"text": "/greet",
"intent": {"name": "greet", "confidence": 1.0},
"intent_ranking": [{"name": "greet", "confidence": 1.0}],
"entities": [],
}
message = UserMessage("Hello")
result = await processor.parse_message(message)
assert not result["intent"]["name"]
async def test_parse_message_full_model(trained_moodbot_path: Text):
processor = Agent.load(model_path=trained_moodbot_path).processor
message = UserMessage("/greet")
result = await processor.parse_message(message)
assert result == {
"text": "/greet",
"intent": {"name": "greet", "confidence": 1.0},
"intent_ranking": [{"name": "greet", "confidence": 1.0}],
"entities": [],
}
message = UserMessage("Hello")
result = await processor.parse_message(message)
assert result["intent"]["name"]
def test_predict_next_with_tracker_nlu_only(trained_nlu_model: Text):
processor = Agent.load(model_path=trained_nlu_model).processor
tracker = DialogueStateTracker("some_id", [])
tracker.followup_action = None
result = processor.predict_next_with_tracker(tracker)
assert result is None
def test_predict_next_with_tracker_core_only(trained_core_model: Text):
processor = Agent.load(model_path=trained_core_model).processor
tracker = DialogueStateTracker("some_id", [])
tracker.followup_action = None
result = processor.predict_next_with_tracker(tracker)
assert result["policy"] == "MemoizationPolicy"
def test_predict_next_with_tracker_full_model(trained_rasa_model: Text):
processor = Agent.load(model_path=trained_rasa_model).processor
tracker = DialogueStateTracker("some_id", [])
tracker.followup_action = None
result = processor.predict_next_with_tracker(tracker)
assert result["policy"] == "MemoizationPolicy"
async def test_get_tracker_adds_model_id(default_processor: MessageProcessor):
model_id = default_processor.model_metadata.model_id
tracker = await default_processor.get_tracker("bloop")
assert tracker.model_id == model_id
# FIXME: these tests take too long to run in the CI, disabling them for now
@pytest.mark.skip_on_ci
async def _test_processor_e2e_slot_set(e2e_bot_agent: Agent, caplog: LogCaptureFixture):
processor = e2e_bot_agent.processor
message = UserMessage("I am feeling sad.", CollectingOutputChannel(), "test")
with caplog.at_level(logging.DEBUG):
await processor.handle_message(message)
tracker = await processor.get_tracker("test")
assert SlotSet("mood", "sad") in tracker.events
assert any(
"An end-to-end prediction was made which has triggered the 2nd execution of "
"the default action 'action_extract_slots'." in message
for message in caplog.messages
)
async def test_model_name_is_available(trained_rasa_model: Text):
processor = Agent.load(model_path=trained_rasa_model).processor
assert len(processor.model_filename) > 0
assert "/" not in processor.model_filename
async def test_loads_correct_model_from_path(
trained_core_model: Text, trained_nlu_model: Text, tmp_path: Path
):
# We move both models to the same directory to prove we can load models by name
# from a directory with multiple models.
model_dir = tmp_path / "models"
os.makedirs(model_dir)
trained_core_model_name = os.path.basename(trained_core_model)
shutil.copy2(trained_core_model, model_dir)
trained_nlu_model_name = os.path.basename(trained_nlu_model)
shutil.copy2(trained_nlu_model, model_dir)
core_processor = Agent.load(
model_path=model_dir / trained_core_model_name
).processor
nlu_processor = Agent.load(model_path=model_dir / trained_nlu_model_name).processor
assert core_processor.model_filename == trained_core_model_name
assert nlu_processor.model_filename == trained_nlu_model_name
@pytest.mark.flaky
@pytest.mark.timeout(180, func_only=True)
async def test_custom_action_triggers_action_extract_slots(
trained_async: Callable,
caplog: LogCaptureFixture,
):
parent_folder = "data/test_custom_action_triggers_action_extract_slots"
domain_path = f"{parent_folder}/domain.yml"
config_path = f"{parent_folder}/config.yml"
stories_path = f"{parent_folder}/stories.yml"
nlu_path = f"{parent_folder}/nlu.yml"
model_path = await trained_async(domain_path, config_path, [stories_path, nlu_path])
agent = Agent.load(model_path)
processor = agent.processor
action_server_url = "http://some-url"
endpoint = EndpointConfig(action_server_url)
processor.action_endpoint = endpoint
entity_name = "mood"
slot_name = "mood_slot"
slot_value = "happy"
custom_action = "action_force_next_utter"
sender_id = uuid.uuid4().hex
message = UserMessage(
text="Activate custom action.",
output_channel=CollectingOutputChannel(),
sender_id=sender_id,
parse_data={
"intent": {"name": "activate_flow", "confidence": 1},
"entities": [],
},
)
with aioresponses() as mocked:
mocked.post(
action_server_url,
payload={
"events": [
{"event": "action", "name": "action_listen"},
{
"event": "user",
"text": "Feeling so happy",
"parse_data": {
"intent": {"name": "mood_great", "confidence": 1.0},
"entities": [{"entity": entity_name, "value": slot_value}],
},
},
]
},
)
with caplog.at_level(logging.DEBUG):
await processor.handle_message(message)
caplog_records = [rec.message for rec in caplog.records]
assert (
f"A `UserUttered` event was returned by executing "
f"action '{custom_action}'. This will run the default action "
f"'{ACTION_EXTRACT_SLOTS}'." in caplog_records
)
tracker = await processor.get_tracker(sender_id)
assert any(
isinstance(e, UserUttered) and e.text == "Feeling so happy"
for e in tracker.events
)
assert SlotSet(slot_name, slot_value) in tracker.events
assert tracker.get_slot(slot_name) == slot_value
assert any(
isinstance(e, BotUttered) and e.text == "Great, carry on!"
for e in tracker.events
)
async def test_processor_executes_bot_uttered_returned_by_action_extract_slots(
default_agent: Agent,
):
slot_name = "location"
domain_yaml = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
intents:
- inform
entities:
- {slot_name}
slots:
{slot_name}:
type: text
influence_conversation: false
mappings:
- type: from_entity
entity: {slot_name}
actions:
- action_validate_slot_mappings
"""
)
domain = Domain.from_yaml(domain_yaml)
processor = default_agent.processor
processor.domain = domain
action_server_url = "http:/my-action-server:5055/webhook"
processor.action_endpoint = EndpointConfig(action_server_url)
sender_id = uuid.uuid4().hex
message = UserMessage(
text="This is a test.",
output_channel=CollectingOutputChannel(),
sender_id=sender_id,
parse_data={
"intent": {"name": "inform", "confidence": 1},
"entities": [{"entity": slot_name, "value": "Lisbon"}],
},
)
bot_uttered_text = "This city is not yet supported."
with aioresponses() as mocked:
mocked.post(
action_server_url,
payload={
"events": [
{"event": "bot", "text": bot_uttered_text},
{"event": "slot", "name": "location", "value": None},
]
},
)
responses = await processor.handle_message(message)
assert any(bot_uttered_text in r.get("text") for r in responses)
tracker = await processor.get_tracker(sender_id)
assert tracker.get_slot(slot_name) is None
@pytest.mark.flaky
@pytest.mark.timeout(180, func_only=True)
@pytest.mark.parametrize(
"sender_id, message_text, message_intent",
[
("happy_path", "Hi", "greet"),
("another_form_activation", "switch forms", "switch_another_form"),
],
)
async def test_from_trigger_intent_with_mapping_conditions_when_form_not_activated(
trained_async: Callable,
sender_id: Text,
message_text: Text,
message_intent: Text,
):
parent_folder = "data/test_from_trigger_intent_with_mapping_conditions"
domain_path = f"{parent_folder}/domain.yml"
config_path = f"{parent_folder}/config.yml"
stories_path = f"{parent_folder}/stories.yml"
nlu_path = f"{parent_folder}/nlu.yml"
model_path = await trained_async(domain_path, config_path, [stories_path, nlu_path])
agent = Agent.load(model_path)
processor = agent.processor
slot_name = "test_trigger"
slot_value = "testing123"
user_messages = [
UserMessage(
text=message_text,
output_channel=CollectingOutputChannel(),
sender_id=sender_id,
parse_data={
"intent": {"name": message_intent, "confidence": 1},
"entities": [],
},
),
UserMessage(
text="great",
output_channel=CollectingOutputChannel(),
sender_id=sender_id,
parse_data={
"intent": {"name": "mood_great", "confidence": 1},
"entities": [],
},
),
]
for msg in user_messages:
await processor.handle_message(msg)
tracker = await processor.get_tracker(sender_id)
assert SlotSet(slot_name, slot_value) not in tracker.events
assert tracker.get_slot(slot_name) is None
@pytest.mark.flaky
@pytest.mark.timeout(120, func_only=True)
async def test_from_trigger_intent_no_form_condition_when_form_not_activated(
trained_async: Callable,
):
parent_folder = "data/test_from_trigger_intent_with_no_mapping_conditions"
domain_path = f"{parent_folder}/domain.yml"
config_path = f"{parent_folder}/config.yml"
stories_path = f"{parent_folder}/stories.yml"
nlu_path = f"{parent_folder}/nlu.yml"
model_path = await trained_async(domain_path, config_path, [stories_path, nlu_path])
agent = Agent.load(model_path)
processor = agent.processor
slot_name = "test_trigger"
slot_value = "testing123"
sender_id = uuid.uuid4().hex
user_messages = [
UserMessage(
text="Hi",
output_channel=CollectingOutputChannel(),
sender_id=sender_id,
parse_data={
"intent": {"name": "greet", "confidence": 1},
"entities": [],
},
),
UserMessage(
text="great",
output_channel=CollectingOutputChannel(),
sender_id=sender_id,
parse_data={
"intent": {"name": "mood_great", "confidence": 1},
"entities": [],
},
),
]
for msg in user_messages:
await processor.handle_message(msg)
tracker = await processor.get_tracker(sender_id)
assert SlotSet(slot_name, slot_value) not in tracker.events
assert tracker.get_slot(slot_name) is None
# test that the form activation path works as expected
sender_id_form_activation = "test_form_activation"
await processor.handle_message(
UserMessage(
text="great",
output_channel=CollectingOutputChannel(),
sender_id=sender_id_form_activation,
parse_data={
"intent": {"name": "mood_great", "confidence": 1},
"entities": [],
},
)
)
tracker = await processor.get_tracker(sender_id_form_activation)
assert ActiveLoop("test_form") in tracker.events
assert SlotSet(slot_name, slot_value) in tracker.events
assert tracker.get_slot(slot_name) == slot_value
@pytest.mark.timeout(120, func_only=True)
async def test_message_processor_raises_warning_if_no_assistant_id(
trained_async: Callable,
):
parent_folder = "data/test_moodbot"
domain_path = f"{parent_folder}/domain.yml"
config_path = "data/test_config/test_moodbot_config_no_assistant_id.yml"
stories_path = f"{parent_folder}/data/stories.yml"
nlu_path = f"{parent_folder}/data/nlu.yml"
model_path = await trained_async(
domain=domain_path, config=config_path, training_files=[stories_path, nlu_path]
)
warning_message = (
f"The model metadata does not contain a value for the '{ASSISTANT_ID_KEY}' "
f"attribute. Check that 'config.yml' file contains a value for "
f"the '{ASSISTANT_ID_KEY}' key and re-train the model. "
f"Failure to do so will result in streaming events without a "
f"unique assistant identifier."
)
with pytest.warns(UserWarning, match=warning_message):
Agent.load(model_path)
async def test_processor_fetch_full_tracker_with_initial_session_inexistent_tracker(
default_processor: MessageProcessor,
) -> None:
"""Test that the tracker is created with the correct initial session data."""
sender_id = uuid.uuid4().hex
tracker = await default_processor.fetch_full_tracker_with_initial_session(sender_id)
assert tracker.sender_id == sender_id
assert tracker.latest_message == UserUttered.empty()
assert tracker.latest_action_name == ACTION_LISTEN_NAME
assert len(tracker.events) == 3
first_recorded_event = tracker.events[0]
assert isinstance(first_recorded_event, ActionExecuted)
assert first_recorded_event.action_name == ACTION_SESSION_START_NAME
assert isinstance(tracker.events[1], SessionStarted)
last_recorded_event = tracker.events[2]
assert isinstance(last_recorded_event, ActionExecuted)
assert last_recorded_event.action_name == ACTION_LISTEN_NAME
async def test_processor_fetch_full_tracker_with_initial_session_existing_tracker(
default_processor: MessageProcessor,
):
"""Test that an existing tracker is correctly retrieved."""
sender_id = uuid.uuid4().hex
expected_events = [
UserUttered("hello"),
Restarted(),
ActionExecuted(ACTION_LISTEN_NAME),
]
tracker = DialogueStateTracker.from_events(sender_id, evts=expected_events)
await default_processor.save_tracker(tracker)
tracker = await default_processor.fetch_full_tracker_with_initial_session(sender_id)
assert tracker.sender_id == sender_id
assert all([event in expected_events for event in tracker.events])
async def test_run_anonymization_pipeline_no_pipeline(
monkeypatch: MonkeyPatch,
default_agent: Agent,
) -> None:
processor = default_agent.processor
sender_id = uuid.uuid4().hex
tracker = await processor.tracker_store.get_or_create_tracker(sender_id)
manager = plugin_manager()
monkeypatch.setattr(
manager.hook, "get_anonymization_pipeline", MagicMock(return_value=None)
)
event_diff = MagicMock()
monkeypatch.setattr(
"rasa.shared.core.trackers.TrackerEventDiffEngine.event_difference", event_diff
)
await processor.run_anonymization_pipeline(tracker)
event_diff.assert_not_called()
async def test_run_anonymization_pipeline_mocked_pipeline(
monkeypatch: MonkeyPatch,
default_agent: Agent,
) -> None:
processor = default_agent.processor
sender_id = uuid.uuid4().hex
tracker = await processor.tracker_store.get_or_create_tracker(sender_id)
manager = plugin_manager()
monkeypatch.setattr(
manager.hook,
"get_anonymization_pipeline",
MagicMock(return_value="mock_pipeline"),
)
event_diff = MagicMock()
monkeypatch.setattr(
"rasa.shared.core.trackers.TrackerEventDiffEngine.event_difference", event_diff
)
await processor.run_anonymization_pipeline(tracker)
event_diff.assert_called_once()
async def test_update_full_retrieval_intent(
default_processor: MessageProcessor,
) -> None:
parse_data = {
"text": "I like sunny days in berlin",
"intent": {"name": "chitchat", "confidence": 0.9},
"entities": [],
"response_selector": {
"all_retrieval_intents": ["faq", "chitchat"],
"faq": {
"response": {
"responses": [{"text": "Our return policy lasts 30 days."}],
"confidence": 1.0,
"intent_response_key": "faq/what_is_return_policy",
"utter_action": "utter_faq/what_is_return_policy",
},
"ranking": [
{
"confidence": 1.0,
"intent_response_key": "faq/what_is_return_policy",
},
{
"confidence": 2.3378809862799945e-19,
"intent_response_key": "faq/how_can_i_track_my_order",
},
],
},
"chitchat": {
"response": {
"responses": [
{
"text": "The sun is out today! Isn't that great?",
},
],
"confidence": 1.0,
"intent_response_key": "chitchat/ask_weather",
"utter_action": "utter_chitchat/ask_weather",
},
"ranking": [
{
"confidence": 1.0,
"intent_response_key": "chitchat/ask_weather",
},
{"confidence": 0.0, "intent_response_key": "chitchat/ask_name"},
],
},
},
}
default_processor._update_full_retrieval_intent(parse_data)
assert parse_data[INTENT][INTENT_NAME_KEY] == "chitchat"
# assert that parse_data["intent"] has a key called response
assert FULL_RETRIEVAL_INTENT_NAME_KEY in parse_data[INTENT]
assert parse_data[INTENT][FULL_RETRIEVAL_INTENT_NAME_KEY] == "chitchat/ask_weather"