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

1619 lines
54 KiB
Python

import datetime
import json
import logging
import os
import textwrap
import time
from pathlib import Path
import tempfile
from typing import List, Text, Dict, Any, Type
import fakeredis
import freezegun
import pytest
from rasa.core.actions.action import ActionExtractSlots
from rasa.core.channels import CollectingOutputChannel
from rasa.core.nlg import TemplatedNaturalLanguageGenerator
from rasa.core.training import load_data
import rasa.shared.utils.io
import rasa.utils.io
from rasa.core import training
from rasa.shared.core.constants import (
ACTION_LISTEN_NAME,
ACTION_SESSION_START_NAME,
LOOP_NAME,
REQUESTED_SLOT,
LOOP_INTERRUPTED,
)
from rasa.shared.constants import (
ASSISTANT_ID_KEY,
DEFAULT_SENDER_ID,
LATEST_TRAINING_DATA_FORMAT_VERSION,
)
from rasa.core.agent import Agent
from rasa.shared.core.domain import Domain
from rasa.shared.core.events import (
AgentUttered,
AllSlotsReset,
ConversationPaused,
ConversationResumed,
FollowupAction,
ReminderScheduled,
SlotSet,
StoryExported,
UserUttered,
ActionExecuted,
Restarted,
ActionReverted,
UserUtteranceReverted,
SessionStarted,
Event,
ActiveLoop,
ActionExecutionRejected,
BotUttered,
LegacyForm,
LegacyFormValidation,
LoopInterrupted,
DefinePrevUserUtteredFeaturization,
EntitiesAdded,
)
from rasa.shared.core.slots import (
FloatSlot,
BooleanSlot,
ListSlot,
TextSlot,
Slot,
AnySlot,
)
from rasa.core.tracker_store import (
InMemoryTrackerStore,
RedisTrackerStore,
SQLTrackerStore,
)
from rasa.core.tracker_store import TrackerStore
from rasa.shared.core.trackers import DialogueStateTracker, EventVerbosity
from rasa.shared.core.training_data.story_reader.yaml_story_reader import (
YAMLStoryReader,
)
from tests.core.conftest import MockedMongoTrackerStore
from tests.dialogues import (
TEST_DIALOGUES,
TEST_MOODBOT_DIALOGUE,
TEST_DOMAINS_FOR_DIALOGUES,
)
from tests.core.utilities import tracker_from_dialogue, user_uttered, get_tracker
from rasa.shared.nlu.constants import (
ACTION_NAME,
METADATA_MODEL_ID,
PREDICTED_CONFIDENCE_KEY,
)
test_domain = Domain.load("data/test_moodbot/domain.yml")
class MockRedisTrackerStore(RedisTrackerStore):
def __init__(self, _domain: Domain) -> None:
super().__init__(_domain)
# Patch the Redis connection in RedisTrackerStore using fakeredis
self.red = fakeredis.FakeStrictRedis()
# added in redis==3.3.0, but not yet in fakeredis
self.red.connection_pool.connection_class.health_check_interval = 0
def stores_to_be_tested():
temp = tempfile.mkdtemp()
return [
MockRedisTrackerStore(test_domain),
InMemoryTrackerStore(test_domain),
SQLTrackerStore(test_domain, db=os.path.join(temp, "rasa.db")),
MockedMongoTrackerStore(test_domain),
]
def stores_to_be_tested_ids():
return ["redis-tracker", "in-memory-tracker", "SQL-tracker", "mongo-tracker"]
def test_tracker_duplicate(moodbot_domain: Domain):
tracker = tracker_from_dialogue(TEST_MOODBOT_DIALOGUE, moodbot_domain)
num_actions = len(
[
event
for event in TEST_MOODBOT_DIALOGUE.events
if isinstance(event, ActionExecuted)
]
)
# There is always one duplicated tracker more than we have actions,
# as the tracker also gets duplicated for the
# action that would be next (but isn't part of the operations)
assert len(list(tracker.generate_all_prior_trackers())) == num_actions + 1
@pytest.mark.parametrize("store", stores_to_be_tested(), ids=stores_to_be_tested_ids())
async def test_tracker_store_storage_and_retrieval(store: TrackerStore):
tracker = await store.get_or_create_tracker("some-id")
# the retrieved tracker should be empty
assert tracker.sender_id == "some-id"
# Action listen should be in there
assert list(tracker.events) == [ActionExecuted(ACTION_LISTEN_NAME)]
# lets log a test message
intent = {"name": "greet", "confidence": 1.0}
tracker.update(UserUttered("/greet", intent, []))
assert tracker.latest_message.intent.get("name") == "greet"
await store.save(tracker)
# retrieving the same tracker should result in the same tracker
retrieved_tracker = await store.get_or_create_tracker("some-id")
assert retrieved_tracker.sender_id == "some-id"
assert len(retrieved_tracker.events) == 2
assert retrieved_tracker.latest_message.intent.get("name") == "greet"
# getting another tracker should result in an empty tracker again
other_tracker = await store.get_or_create_tracker("some-other-id")
assert other_tracker.sender_id == "some-other-id"
assert len(other_tracker.events) == 1
@pytest.mark.parametrize("store", stores_to_be_tested(), ids=stores_to_be_tested_ids())
@pytest.mark.parametrize("pair", zip(TEST_DIALOGUES, TEST_DOMAINS_FOR_DIALOGUES))
async def test_tracker_store(store, pair):
dialogue, domainpath = pair
domain = Domain.load(domainpath)
tracker = tracker_from_dialogue(dialogue, domain)
await store.save(tracker)
restored = await store.retrieve(tracker.sender_id)
assert restored == tracker
def test_tracker_write_to_story(tmp_path: Path, moodbot_domain: Domain):
tracker = tracker_from_dialogue(TEST_MOODBOT_DIALOGUE, moodbot_domain)
p = tmp_path / "export.yml"
tracker.export_stories_to_file(str(p))
trackers = training.load_data(
str(p),
moodbot_domain,
use_story_concatenation=False,
tracker_limit=1000,
remove_duplicates=False,
)
assert len(trackers) == 1
recovered = trackers[0]
assert len(recovered.events) == 11
assert recovered.events[4].type_name == "user"
assert recovered.events[4].intent == {
"confidence": 1.0,
"name": "mood_unhappy",
"full_retrieval_intent_name": None,
}
async def test_tracker_state_regression_without_bot_utterance(default_agent: Agent):
sender_id = "test_tracker_state_regression_without_bot_utterance"
for i in range(0, 2):
await default_agent.handle_text("/greet", sender_id=sender_id)
tracker = await default_agent.tracker_store.get_or_create_tracker(sender_id)
# Ensures that the tracker has changed between the utterances
# (and wasn't reset in between them)
expected = (
"action_session_start;action_listen;greet;utter_greet;action_listen;"
"greet;utter_greet;action_listen"
)
assert (
";".join([e.as_story_string() for e in tracker.events if e.as_story_string()])
== expected
)
async def test_tracker_state_regression_with_bot_utterance(default_agent: Agent):
sender_id = "test_tracker_state_regression_with_bot_utterance"
for i in range(0, 2):
await default_agent.handle_text("/greet", sender_id=sender_id)
tracker = await default_agent.tracker_store.get_or_create_tracker(sender_id)
expected = [
"action_session_start",
None,
"action_listen",
"greet",
None, # DefinePrevUserUtteredFeaturization
"utter_greet",
None,
"action_listen",
"greet",
None, # DefinePrevUserUtteredFeaturization
"utter_greet",
None,
"action_listen",
]
assert [e.as_story_string() for e in tracker.events] == expected
async def test_bot_utterance_comes_after_action_event(default_agent: Agent):
sender_id = "test_bot_utterance_comes_after_action_event"
await default_agent.handle_text("/greet", sender_id=sender_id)
tracker = await default_agent.tracker_store.get_or_create_tracker(sender_id)
# important is, that the 'bot' comes after the second 'action' and not
# before
expected = [
"action",
"session_started",
"action",
"user",
"user_featurization",
"action",
"bot",
"action",
]
assert [e.type_name for e in tracker.events] == expected
@pytest.mark.parametrize(
"entities, expected_values",
[
([{"value": "greet", "entity": "entity_name"}], ["greet"]),
(
[
{"value": "greet", "entity": "entity_name"},
{"value": "bye", "entity": "other"},
],
["greet"],
),
(
[
{"value": "greet", "entity": "entity_name"},
{"value": "bye", "entity": "entity_name"},
],
["greet", "bye"],
),
(
[
{"value": "greet", "entity": "entity_name", "role": "role"},
{"value": "bye", "entity": "entity_name"},
],
["greet"],
),
(
[
{"value": "greet", "entity": "entity_name", "group": "group"},
{"value": "bye", "entity": "entity_name"},
],
["greet"],
),
(
[
{"value": "greet", "entity": "entity_name"},
{"value": "bye", "entity": "entity_name", "group": "group"},
],
# bye has group set, so it should not be extracted
["greet"],
),
],
)
def test_get_latest_entity_values(
entities: List[Dict[Text, Any]], expected_values: List[Text], domain: Domain
):
entity_type = entities[0].get("entity")
entity_role = entities[0].get("role")
entity_group = entities[0].get("group")
tracker = DialogueStateTracker("default", domain.slots)
# the retrieved tracker should be empty
assert len(tracker.events) == 0
assert list(tracker.get_latest_entity_values(entity_type)) == []
intent = {"name": "greet", PREDICTED_CONFIDENCE_KEY: 1.0}
tracker.update(UserUttered("/greet", intent, entities))
assert (
list(
tracker.get_latest_entity_values(
entity_type, entity_role=entity_role, entity_group=entity_group
)
)
== expected_values
)
assert list(tracker.get_latest_entity_values("unknown")) == []
async def test_tracker_update_slots_with_entity(domain: Domain):
tracker = DialogueStateTracker("default", domain.slots)
test_entity = domain.entities[0]
expected_slot_value = "test user"
intent = {"name": "greet", PREDICTED_CONFIDENCE_KEY: 1.0}
tracker.update(
UserUttered(
"/greet",
intent,
[
{
"start": 1,
"end": 5,
"value": expected_slot_value,
"entity": test_entity,
"extractor": "manual",
}
],
),
domain,
)
action_extract_slots = ActionExtractSlots(action_endpoint=None)
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
tracker.update_with_events(events, domain)
assert tracker.get_slot(test_entity) == expected_slot_value
def test_restart_event(domain: Domain):
tracker = DialogueStateTracker("default", domain.slots)
# the retrieved tracker should be empty
assert len(tracker.events) == 0
intent = {"name": "greet", PREDICTED_CONFIDENCE_KEY: 1.0}
tracker.update(ActionExecuted(ACTION_LISTEN_NAME))
tracker.update(UserUttered("/greet", intent, []))
tracker.update(ActionExecuted("my_action"))
tracker.update(ActionExecuted(ACTION_LISTEN_NAME))
assert len(tracker.events) == 4
assert tracker.latest_message.text == "/greet"
assert len(list(tracker.generate_all_prior_trackers())) == 4
tracker.update(Restarted())
assert len(tracker.events) == 5
assert tracker.followup_action == ACTION_SESSION_START_NAME
tracker.update(SessionStarted())
assert tracker.followup_action == ACTION_LISTEN_NAME
assert tracker.latest_message.text is None
assert len(list(tracker.generate_all_prior_trackers())) == 1
dialogue = tracker.as_dialogue()
recovered = DialogueStateTracker("default", domain.slots)
recovered.recreate_from_dialogue(dialogue)
assert recovered.current_state() == tracker.current_state()
assert len(recovered.events) == 6
assert recovered.latest_message.text is None
assert len(list(recovered.generate_all_prior_trackers())) == 1
def test_session_start(domain: Domain):
tracker = DialogueStateTracker("default", domain.slots)
# the retrieved tracker should be empty
assert len(tracker.events) == 0
# add a SessionStarted event
tracker.update(SessionStarted())
# tracker has one event
assert len(tracker.events) == 1
def test_revert_action_event(domain: Domain):
tracker = DialogueStateTracker("default", domain.slots)
# the retrieved tracker should be empty
assert len(tracker.events) == 0
intent = {"name": "greet", PREDICTED_CONFIDENCE_KEY: 1.0}
tracker.update(ActionExecuted(ACTION_LISTEN_NAME))
tracker.update(UserUttered("/greet", intent, []))
tracker.update(ActionExecuted("my_action"))
tracker.update(ActionExecuted(ACTION_LISTEN_NAME))
# Expecting count of 4:
# +3 executed actions
# +1 final state
assert tracker.latest_action.get(ACTION_NAME) == ACTION_LISTEN_NAME
assert len(list(tracker.generate_all_prior_trackers())) == 4
tracker.update(ActionReverted())
# Expecting count of 3:
# +3 executed actions
# +1 final state
# -1 reverted action
assert tracker.latest_action.get(ACTION_NAME) == "my_action"
assert len(list(tracker.generate_all_prior_trackers())) == 3
dialogue = tracker.as_dialogue()
recovered = DialogueStateTracker("default", domain.slots)
recovered.recreate_from_dialogue(dialogue)
assert recovered.current_state() == tracker.current_state()
assert tracker.latest_action.get(ACTION_NAME) == "my_action"
assert len(list(tracker.generate_all_prior_trackers())) == 3
def test_revert_user_utterance_event(domain: Domain):
tracker = DialogueStateTracker("default", domain.slots)
# the retrieved tracker should be empty
assert len(tracker.events) == 0
intent1 = {"name": "greet", PREDICTED_CONFIDENCE_KEY: 1.0}
tracker.update(ActionExecuted(ACTION_LISTEN_NAME))
tracker.update(UserUttered("/greet", intent1, []))
tracker.update(ActionExecuted("my_action_1"))
tracker.update(ActionExecuted(ACTION_LISTEN_NAME))
intent2 = {"name": "goodbye", PREDICTED_CONFIDENCE_KEY: 1.0}
tracker.update(UserUttered("/goodbye", intent2, []))
tracker.update(ActionExecuted("my_action_2"))
tracker.update(ActionExecuted(ACTION_LISTEN_NAME))
# Expecting count of 6:
# +5 executed actions
# +1 final state
assert tracker.latest_action.get(ACTION_NAME) == ACTION_LISTEN_NAME
assert len(list(tracker.generate_all_prior_trackers())) == 6
tracker.update(UserUtteranceReverted())
# Expecting count of 3:
# +5 executed actions
# +1 final state
# -2 rewound actions associated with the /goodbye
# -1 rewound action from the listen right before /goodbye
assert tracker.latest_action.get(ACTION_NAME) == "my_action_1"
assert len(list(tracker.generate_all_prior_trackers())) == 3
dialogue = tracker.as_dialogue()
recovered = DialogueStateTracker("default", domain.slots)
recovered.recreate_from_dialogue(dialogue)
assert recovered.current_state() == tracker.current_state()
assert tracker.latest_action.get(ACTION_NAME) == "my_action_1"
assert len(list(tracker.generate_all_prior_trackers())) == 3
def test_traveling_back_in_time(domain: Domain):
tracker = DialogueStateTracker("default", domain.slots)
# the retrieved tracker should be empty
assert len(tracker.events) == 0
intent = {"name": "greet", PREDICTED_CONFIDENCE_KEY: 1.0}
tracker.update(ActionExecuted(ACTION_LISTEN_NAME))
tracker.update(UserUttered("/greet", intent, []))
time.sleep(1)
time_for_timemachine = time.time()
time.sleep(1)
tracker.update(ActionExecuted("my_action"))
tracker.update(ActionExecuted(ACTION_LISTEN_NAME))
# Expecting count of 4:
# +3 executed actions
# +1 final state
assert tracker.latest_action.get(ACTION_NAME) == ACTION_LISTEN_NAME
assert len(tracker.events) == 4
assert len(list(tracker.generate_all_prior_trackers())) == 4
tracker = tracker.travel_back_in_time(time_for_timemachine)
# Expecting count of 2:
# +1 executed actions
# +1 final state
assert tracker.latest_action.get(ACTION_NAME) == ACTION_LISTEN_NAME
assert len(tracker.events) == 2
assert len(list(tracker.generate_all_prior_trackers())) == 2
def test_tracker_init_copy(domain: Domain):
sender_id = "some-id"
tracker = DialogueStateTracker(sender_id, domain.slots)
tracker_copy = tracker.init_copy()
assert tracker.sender_id == tracker_copy.sender_id
def _load_tracker_from_json(tracker_dump: Text, domain: Domain) -> DialogueStateTracker:
"""Read the json dump from the file and instantiate a tracker it."""
tracker_json = json.loads(rasa.shared.utils.io.read_file(tracker_dump))
sender_id = tracker_json.get("sender_id", DEFAULT_SENDER_ID)
return DialogueStateTracker.from_dict(
sender_id, tracker_json.get("events", []), domain.slots
)
def test_dump_and_restore_as_json(
default_agent: Agent, tmp_path: Path, stories_path: Text
):
trackers = load_data(stories_path, default_agent.domain)
for tracker in trackers:
out_path = tmp_path / "dumped_tracker.json"
dumped = tracker.current_state(EventVerbosity.AFTER_RESTART)
rasa.shared.utils.io.dump_obj_as_json_to_file(str(out_path), dumped)
restored_tracker = _load_tracker_from_json(str(out_path), default_agent.domain)
assert restored_tracker == tracker
def test_read_json_dump(default_agent: Agent):
tracker_dump = "data/test_trackers/tracker_moodbot.json"
tracker_json = json.loads(rasa.shared.utils.io.read_file(tracker_dump))
restored_tracker = _load_tracker_from_json(tracker_dump, default_agent.domain)
assert len(restored_tracker.events) == 7
assert restored_tracker.latest_action.get(ACTION_NAME) == "action_listen"
assert not restored_tracker.is_paused()
assert restored_tracker.sender_id == "mysender"
assert restored_tracker.events[-1].timestamp == 1517821726.211042
restored_state = restored_tracker.current_state(EventVerbosity.AFTER_RESTART)
assert restored_state == tracker_json
def test_current_state_after_restart(default_agent):
tracker_dump = "data/test_trackers/tracker_moodbot.json"
tracker_json = json.loads(rasa.shared.utils.io.read_file(tracker_dump))
tracker_json["events"].insert(3, {"event": "restart"})
tracker = DialogueStateTracker.from_dict(
tracker_json.get("sender_id"),
tracker_json.get("events", []),
default_agent.domain.slots,
)
events_after_restart = [e.as_dict() for e in list(tracker.events)[4:]]
state = tracker.current_state(EventVerbosity.AFTER_RESTART)
assert state.get("events") == events_after_restart
def test_current_state_all_events(empty_agent: Agent):
tracker_dump = "data/test_trackers/tracker_moodbot.json"
tracker_json = json.loads(rasa.shared.utils.io.read_file(tracker_dump))
tracker_json["events"].insert(3, {"event": "restart"})
tracker = DialogueStateTracker.from_dict(
tracker_json.get("sender_id"),
tracker_json.get("events", []),
empty_agent.domain.slots,
)
evts = [e.as_dict() for e in tracker.events]
state = tracker.current_state(EventVerbosity.ALL)
assert state.get("events") == evts
def test_current_state_no_events(empty_agent: Agent):
tracker_dump = "data/test_trackers/tracker_moodbot.json"
tracker_json = json.loads(rasa.shared.utils.io.read_file(tracker_dump))
tracker = DialogueStateTracker.from_dict(
tracker_json.get("sender_id"),
tracker_json.get("events", []),
empty_agent.domain.slots,
)
state = tracker.current_state(EventVerbosity.NONE)
assert state.get("events") is None
def test_current_state_applied_events(empty_agent: Agent):
tracker_dump = "data/test_trackers/tracker_moodbot.json"
tracker_json = json.loads(rasa.shared.utils.io.read_file(tracker_dump))
# add some events that result in other events not being applied anymore
tracker_json["events"].insert(1, {"event": "restart"})
tracker_json["events"].insert(7, {"event": "rewind"})
tracker_json["events"].insert(8, {"event": "undo"})
tracker = DialogueStateTracker.from_dict(
tracker_json.get("sender_id"),
tracker_json.get("events", []),
empty_agent.domain.slots,
)
evts = [e.as_dict() for e in tracker.events]
applied_events = [evts[2], evts[9]]
state = tracker.current_state(EventVerbosity.APPLIED)
assert state.get("events") == applied_events
def test_session_started_not_part_of_applied_events(empty_agent: Agent):
# take tracker dump and insert a SessionStarted event sequence
tracker_dump = "data/test_trackers/tracker_moodbot.json"
tracker_json = json.loads(rasa.shared.utils.io.read_file(tracker_dump))
tracker_json["events"].insert(
4, {"event": ActionExecuted.type_name, "name": ACTION_SESSION_START_NAME}
)
tracker_json["events"].insert(5, {"event": SessionStarted.type_name})
# initialise a tracker from this list of events
tracker = DialogueStateTracker.from_dict(
tracker_json.get("sender_id"),
tracker_json.get("events", []),
empty_agent.domain.slots,
)
# the SessionStart event was at index 5, the tracker's `applied_events()` should
# be the same as the list of events from index 6 onwards
assert tracker.applied_events() == list(tracker.events)[6:]
def test_get_last_event_for():
events = [ActionExecuted("one"), user_uttered("two", 1)]
tracker = get_tracker(events)
assert tracker.get_last_event_for(ActionExecuted).action_name == "one"
def test_get_last_event_with_reverted():
events = [ActionExecuted("one"), ActionReverted(), user_uttered("two", 1)]
tracker = get_tracker(events)
assert tracker.get_last_event_for(ActionExecuted) is None
def test_get_last_event_for_with_skip():
events = [ActionExecuted("one"), user_uttered("two", 1), ActionExecuted("three")]
tracker = get_tracker(events)
assert tracker.get_last_event_for(ActionExecuted, skip=1).action_name == "one"
def test_get_last_event_for_with_exclude():
events = [ActionExecuted("one"), user_uttered("two", 1), ActionExecuted("three")]
tracker = get_tracker(events)
assert (
tracker.get_last_event_for(
ActionExecuted, action_names_to_exclude=["three"]
).action_name
== "one"
)
def test_last_executed_has():
events = [
ActionExecuted("one"),
user_uttered("two", 1),
ActionExecuted(ACTION_LISTEN_NAME),
]
tracker = get_tracker(events)
assert tracker.last_executed_action_has("one") is True
def test_last_executed_has_not_name():
events = [
ActionExecuted("one"),
user_uttered("two", 1),
ActionExecuted(ACTION_LISTEN_NAME),
]
tracker = get_tracker(events)
assert tracker.last_executed_action_has("another") is False
def test_events_metadata():
# It should be possible to attach arbitrary metadata to any event and then
# retrieve it after getting the tracker dict representation.
events = [
ActionExecuted("one", metadata={"one": 1}),
user_uttered("two", 1, metadata={"two": 2}),
ActionExecuted(ACTION_LISTEN_NAME, metadata={"three": 3}),
]
events = get_tracker(events).current_state(EventVerbosity.ALL)["events"]
assert events[0]["metadata"] == {"one": 1}
assert events[1]["metadata"] == {"two": 2}
assert events[2]["metadata"] == {"three": 3}
@pytest.mark.parametrize("key, value", [("asfa", 1), ("htb", None)])
def test_tracker_without_slots(key, value, caplog):
event = SlotSet(key, value)
tracker = DialogueStateTracker.from_dict("any", [])
assert key in tracker.slots
with caplog.at_level(logging.INFO):
event.apply_to(tracker)
v = tracker.get_slot(key)
assert v == value
assert len(caplog.records) == 0
@pytest.mark.parametrize(
"slot_type, initial_value, value_to_set",
[
(FloatSlot, 4.234, 2.5),
(BooleanSlot, True, False),
(ListSlot, [1, 2, 3], [4, 5, 6]),
(TextSlot, "some string", "another string"),
(AnySlot, {"a": "nice dict"}, {"b": "better dict"}),
],
)
def test_tracker_does_not_modify_slots(
slot_type: Type[Slot], initial_value: Any, value_to_set: Any
):
slot_name = "some-slot"
slot = slot_type(slot_name, mappings=[{}], initial_value=initial_value)
tracker = DialogueStateTracker("some-conversation-id", [slot])
# change the slot value in the tracker
tracker._set_slot(slot_name, value_to_set)
# assert that the tracker contains the slot with the modified value
assert tracker.get_slot(slot_name) == value_to_set
# assert that the initial slot has not been affected
assert slot.value == initial_value
@pytest.mark.parametrize(
"events, expected_applied_events",
[
(
[
# Form gets triggered.
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("fill_whole_form"),
# Form executes and fills slots.
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet("slot1", "value"),
SlotSet("slot2", "value2"),
],
[
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("fill_whole_form"),
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet("slot1", "value"),
SlotSet("slot2", "value2"),
],
),
(
[
# Form gets triggered.
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("fill_whole_form"),
# Form executes and fills all slots right away. Form finishes.
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet("slot1", "value"),
SlotSet("slot2", "value2"),
ActiveLoop(None),
# Form is done. Regular conversation continues.
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("intent outside form"),
],
[
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("fill_whole_form"),
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet("slot1", "value"),
SlotSet("slot2", "value2"),
ActiveLoop(None),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("intent outside form"),
],
),
(
[
# Form gets triggered.
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
# Form executes and requests slot.
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
# User fills slot.
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("bye"),
# Form deactivates after all slots are finished.
ActionExecuted("loop"),
SlotSet("slot", "value"),
ActiveLoop(None),
SlotSet(REQUESTED_SLOT, None),
],
[
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
SlotSet("slot", "value"),
ActiveLoop(None),
SlotSet(REQUESTED_SLOT, None),
],
),
(
[
# Form was executed before and finished.
ActionExecuted("loop"),
ActiveLoop(None),
# Form gets triggered again (for whatever reason)..
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
# Form executes and requests slot.
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
# User fills slot.
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("bye"),
# Form deactivates after all slots are finished.
ActionExecuted("loop"),
SlotSet("slot", "value"),
ActiveLoop(None),
SlotSet(REQUESTED_SLOT, None),
],
[
ActionExecuted("loop"),
ActiveLoop(None),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
SlotSet("slot", "value"),
ActiveLoop(None),
SlotSet(REQUESTED_SLOT, None),
],
),
(
[
user_uttered("trigger form"),
ActionExecuted("form"),
ActiveLoop("form"),
SlotSet(REQUESTED_SLOT, "some slot"),
BotUttered("ask slot"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("fill requested slots"),
SlotSet("some slot", "value"),
ActionExecuted("form"),
SlotSet("some slot", "value"),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
[
user_uttered("trigger form"),
ActionExecuted("form"),
ActiveLoop("form"),
SlotSet(REQUESTED_SLOT, "some slot"),
BotUttered("ask slot"),
SlotSet("some slot", "value"),
SlotSet("some slot", "value"),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
),
(
[
user_uttered("trigger form"),
DefinePrevUserUtteredFeaturization(False),
ActionExecuted("form"),
ActiveLoop("form"),
SlotSet(REQUESTED_SLOT, "some slot"),
BotUttered("ask slot"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("fill requested slots"),
SlotSet("some slot", "value"),
DefinePrevUserUtteredFeaturization(False),
ActionExecuted("form"),
SlotSet("some slot", "value"),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
[
user_uttered("trigger form"),
DefinePrevUserUtteredFeaturization(False),
ActionExecuted("form"),
ActiveLoop("form"),
SlotSet(REQUESTED_SLOT, "some slot"),
BotUttered("ask slot"),
SlotSet("some slot", "value"),
SlotSet("some slot", "value"),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
),
],
)
def test_applied_events_with_loop_happy_path(
events: List[Event], expected_applied_events: List[Event]
):
tracker = DialogueStateTracker.from_events("👋", events)
applied = tracker.applied_events()
assert applied == expected_applied_events
@pytest.mark.parametrize(
"events, expected_applied_events",
[
(
[
# Form is triggered and requests slot.
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
# User sends chitchat instead of answering form.
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("chitchat"),
# Form rejected execution.
ActionExecutionRejected("loop"),
# Action which deals with unhappy path.
ActionExecuted("handling chitchat"),
# We immediately return to form after executing an action to handle it.
ActionExecuted("loop"),
# Form happy path continues until all slots are filled.
SlotSet(REQUESTED_SLOT, "bla"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("fill slots"),
ActionExecuted("loop"),
SlotSet("slot", "value"),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
[
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("chitchat"),
ActionExecutionRejected("loop"),
ActionExecuted("handling chitchat"),
ActionExecuted("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
SlotSet("slot", "value"),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
),
(
[
# Form gets triggered and requests slots.
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
# User sends chitchat instead of answering form.
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("chitchat"),
# Form rejected execution.
ActionExecutionRejected("loop"),
# Unhappy path kicks in.
ActionExecuted("ask if continue"),
ActionExecuted(ACTION_LISTEN_NAME),
# User decides to fill form eventually.
user_uttered("I want to continue with form"),
ActionExecuted("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("fill slots"),
ActionExecuted("loop"),
SlotSet("slot", "value"),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
[
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("chitchat"),
ActionExecutionRejected("loop"),
ActionExecuted("ask if continue"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("I want to continue with form"),
ActionExecuted("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
SlotSet("slot", "value"),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
],
),
(
[
# Form gets triggered and requests slots.
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
# User sends chitchat instead of answering form.
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("chitchat"),
# Form rejected execution.
ActionExecutionRejected("loop"),
# Unhappy path kicks in.
ActionExecuted("ask if continue"),
ActionExecuted(ACTION_LISTEN_NAME),
# User wants to quit form.
user_uttered("Stop the form"),
ActionExecuted("some action"),
ActiveLoop(None),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("outside the form"),
],
[
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("chitchat"),
ActionExecutionRejected("loop"),
ActionExecuted("ask if continue"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("Stop the form"),
ActionExecuted("some action"),
ActiveLoop(None),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("outside the form"),
],
),
(
[
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("chitchat"),
# Different action than form action after chitchat.
# This indicates we are in an unhappy path.
ActionExecuted("handle_chitchat"),
ActionExecuted("loop"),
ActiveLoop("loop"),
],
[
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
ActionExecuted("loop"),
ActiveLoop("loop"),
SlotSet(REQUESTED_SLOT, "bla"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("chitchat"),
# Different action than form action after chitchat.
# This indicates we are in an unhappy path.
ActionExecuted("handle_chitchat"),
ActionExecuted("loop"),
ActiveLoop("loop"),
],
),
(
[
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
ActionExecuted("loop"),
ActiveLoop("loop"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("chitchat"),
ActionExecuted("handle_chitchat"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("affirm"),
ActionExecuted("loop"),
],
[
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
ActionExecuted("loop"),
ActiveLoop("loop"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("chitchat"),
# Different action than form action indicates unhappy path
ActionExecuted("handle_chitchat"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("affirm"),
ActionExecuted("loop"),
],
),
(
[
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
DefinePrevUserUtteredFeaturization(False),
ActionExecuted("loop"),
ActiveLoop("loop"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("chitchat"),
DefinePrevUserUtteredFeaturization(False),
ActionExecuted("handle_chitchat"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("affirm"),
DefinePrevUserUtteredFeaturization(False),
ActionExecuted("loop"),
],
[
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("greet"),
DefinePrevUserUtteredFeaturization(False),
ActionExecuted("loop"),
ActiveLoop("loop"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("chitchat"),
DefinePrevUserUtteredFeaturization(False),
# Different action than form action indicates unhappy path
ActionExecuted("handle_chitchat"),
ActionExecuted(ACTION_LISTEN_NAME),
user_uttered("affirm"),
DefinePrevUserUtteredFeaturization(False),
ActionExecuted("loop"),
],
),
],
)
def test_applied_events_with_loop_unhappy_path(
events: List[Event], expected_applied_events: List[Event]
):
tracker = DialogueStateTracker.from_events("👋", events)
applied = tracker.applied_events()
assert applied == expected_applied_events
def test_reading_of_trackers_with_legacy_form_events():
loop_name1 = "my loop"
loop_name2 = "my form"
tracker = DialogueStateTracker.from_dict(
"sender",
events_as_dict=[
{"event": ActiveLoop.type_name, LOOP_NAME: loop_name1},
{"event": LegacyForm.type_name, LOOP_NAME: None},
{"event": LegacyForm.type_name, LOOP_NAME: loop_name2},
],
)
expected_events = [ActiveLoop(loop_name1), LegacyForm(None), LegacyForm(loop_name2)]
assert list(tracker.events) == expected_events
assert tracker.active_loop.name == loop_name2
def test_writing_trackers_with_legacy_form_events():
loop_name = "my loop"
tracker = DialogueStateTracker.from_events(
"sender", evts=[ActiveLoop(loop_name), LegacyForm(None), LegacyForm("some")]
)
events_as_dict = [event.as_dict() for event in tracker.events]
for event in events_as_dict:
assert event["event"] == ActiveLoop.type_name
def test_reading_of_trackers_with_legacy_form_validation_events():
loop_name = "form"
tracker = DialogueStateTracker.from_dict(
"sender",
events_as_dict=[
{"event": ActiveLoop.type_name, LOOP_NAME: loop_name},
{"event": LegacyFormValidation.type_name, "name": None, "validate": True},
{"event": LegacyFormValidation.type_name, "name": None, "validate": False},
],
)
expected_events = [
ActiveLoop(loop_name),
LegacyFormValidation(True),
LegacyFormValidation(False),
]
actual_events = list(tracker.events)
assert list(tracker.events) == expected_events
assert not actual_events[1].is_interrupted
assert actual_events[2].is_interrupted
assert tracker.active_loop.is_interrupted
def test_writing_trackers_with_legacy_for_validation_events():
tracker = DialogueStateTracker.from_events(
"sender", evts=[LegacyFormValidation(True), LegacyFormValidation(False)]
)
events_as_dict = [event.as_dict() for event in tracker.events]
for event in events_as_dict:
assert event["event"] == LoopInterrupted.type_name
assert not events_as_dict[0][LOOP_INTERRUPTED]
assert events_as_dict[1][LOOP_INTERRUPTED]
@pytest.mark.parametrize(
"conversation_events,n_subtrackers",
[
(
# conversation contains multiple sessions
[
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
UserUttered("hi", {"name": "greet"}),
ActionExecuted("utter_greet"),
# second session begins here
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
UserUttered("goodbye", {"name": "goodbye"}),
ActionExecuted("utter_goodbye"),
],
2,
),
(
# conversation contains only one session
[
ActionExecuted(ACTION_SESSION_START_NAME),
SessionStarted(),
UserUttered("hi", {"name": "greet"}),
ActionExecuted("utter_greet"),
],
1,
),
(
# this conversation does not contain a session
[UserUttered("hi", {"name": "greet"}), ActionExecuted("utter_greet")],
1,
),
],
)
def test_trackers_for_conversation_sessions(
conversation_events: List[Event], n_subtrackers: int
):
import rasa.shared.core.trackers as trackers_module
tracker = DialogueStateTracker.from_events(
"some-conversation-ID", conversation_events
)
subtrackers = trackers_module.get_trackers_for_conversation_sessions(tracker)
assert len(subtrackers) == n_subtrackers
def test_policy_predictions_dont_change_persistence():
original_user_message = UserUttered("hi", intent={"name": "greet"})
tracker = DialogueStateTracker.from_events(
"Vova",
evts=[
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered("hi", intent={"name": "greet"}),
DefinePrevUserUtteredFeaturization(True),
EntitiesAdded(entities=[{"entity": "entity1", "value": "value1"}]),
],
)
user_message: UserUttered = list(tracker.events)[1]
# The entities from the policy predictions are accessible
assert user_message.entities
actual_serialized = user_message.as_dict()
# Assert entities predicted by policies are not persisted
assert not actual_serialized["parse_data"]["entities"]
expected_serialized = original_user_message.as_dict()
# don't compare timestamps
expected_serialized.pop("timestamp")
actual_serialized.pop("timestamp")
assert actual_serialized == expected_serialized
@freezegun.freeze_time("2018-01-01")
def test_policy_prediction_reflected_in_tracker_state():
entities_predicted_by_policy = [{"entity": "entity1", "value": "value1"}]
nlu_entities = [{"entity": "entityNLU", "value": "value100"}]
tracker = DialogueStateTracker.from_events(
"Tester",
evts=[
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(
"hi",
intent={"name": "greet"},
entities=nlu_entities.copy(),
message_id="unique",
metadata={"some": "data"},
),
DefinePrevUserUtteredFeaturization(True),
EntitiesAdded(entities=entities_predicted_by_policy),
],
)
tracker_state = tracker.current_state()
expected_state = {
"sender_id": "Tester",
"slots": {},
"latest_message": {
"intent": {"name": "greet"},
"entities": nlu_entities + entities_predicted_by_policy,
"text": "hi",
"message_id": "unique",
"metadata": {"some": "data"},
},
"latest_event_time": 1514764800.0,
"followup_action": None,
"paused": False,
"events": None,
"latest_input_channel": None,
"active_loop": {},
"latest_action": {"action_name": "action_listen"},
"latest_action_name": "action_listen",
}
assert tracker_state == expected_state
# Make sure we didn't change the actual event
assert tracker.latest_message.parse_data["entities"] == nlu_entities
async def test_fill_slots_for_policy_entities():
policy_entity, policy_entity_value = "policy_entity", "end-to-end"
nlu_entity, nlu_entity_value = "nlu_entity", "nlu rocks"
domain = Domain.from_yaml(
textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
entities:
- {nlu_entity}
- {policy_entity}
slots:
{nlu_entity}:
type: text
mappings:
- type: from_entity
entity: {nlu_entity}
{policy_entity}:
type: text
mappings:
- type: from_entity
entity: {policy_entity}
"""
)
)
tracker = DialogueStateTracker.from_events(
"some sender",
evts=[
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(
"hi",
intent={"name": "greet"},
entities=[{"entity": nlu_entity, "value": nlu_entity_value}],
),
DefinePrevUserUtteredFeaturization(True),
EntitiesAdded(
entities=[
{"entity": policy_entity, "value": policy_entity_value},
{"entity": nlu_entity, "value": nlu_entity_value},
]
),
],
domain=domain,
slots=domain.slots,
)
expected_events = [
ActionExecuted(ACTION_LISTEN_NAME),
UserUttered(
"hi",
intent={"name": "greet"},
entities=[
{"entity": nlu_entity, "value": nlu_entity_value},
# Added by `DefinePrevUserUtteredEntities`
{"entity": policy_entity, "value": policy_entity_value},
],
),
DefinePrevUserUtteredFeaturization(True),
EntitiesAdded(
entities=[
{"entity": policy_entity, "value": policy_entity_value},
{"entity": nlu_entity, "value": nlu_entity_value},
]
),
SlotSet(nlu_entity, nlu_entity_value),
SlotSet(policy_entity, policy_entity_value),
]
action_extract_slots = ActionExtractSlots(action_endpoint=None)
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
tracker.update_with_events(events, domain)
# Slots are correctly set
assert tracker.slots[nlu_entity].value == nlu_entity_value
assert tracker.slots[policy_entity].value == policy_entity_value
for actual, expected in zip(tracker.events, expected_events):
assert actual == expected
def test_tracker_fingerprinting_consistency():
slot = TextSlot(name="name", mappings=[{}], influence_conversation=True)
slot.value = "example"
tr1 = DialogueStateTracker("test_sender_id", slots=[slot])
tr2 = DialogueStateTracker("test_sender_id", slots=[slot])
f1 = tr1.fingerprint()
f2 = tr2.fingerprint()
assert f1 == f2
def test_tracker_unique_fingerprint(domain: Domain):
slot = TextSlot(name="name", mappings=[{}], influence_conversation=True)
slot.value = "example"
tr = DialogueStateTracker("test_sender_id", slots=[slot])
f1 = tr.fingerprint()
event1 = UserUttered(
text="hello",
parse_data={
"intent": {"name": "greet", "confidence": 0.9604260921478271},
"entities": [
{"entity": "city", "value": "London"},
{"entity": "count", "value": 1},
],
"text": "hi",
"message_id": "3f4c04602a4947098c574b107d3ccc59",
"metadata": {},
"intent_ranking": [
{"name": "greet", "confidence": 0.9604260921478271},
{"name": "goodbye", "confidence": 0.01835782080888748},
{"name": "deny", "confidence": 0.011255578137934208},
],
},
)
tr.update(event1)
f2 = tr.fingerprint()
assert f1 != f2
event2 = ActionExecuted(action_name="action_listen")
tr.update(event2)
f3 = tr.fingerprint()
assert f2 != f3
def test_tracker_fingerprint_story_reading(domain: Domain):
def build_tracker(domain: Domain) -> DialogueStateTracker:
story_yaml = """
stories:
- story: test story
steps:
- intent: greet
- action: utter_greet
"""
reader = YAMLStoryReader(domain)
story_steps = reader.read_from_string(story_yaml)
events = []
for step in story_steps:
evts = step.events
if isinstance(evts, list):
events += evts
else:
events.append(evts)
slot = TextSlot(name="name", mappings=[{}], influence_conversation=True)
slot.value = "example"
tracker = DialogueStateTracker.from_events("sender_id", events, [slot])
return tracker
tracker1 = build_tracker(domain)
f1 = tracker1.fingerprint()
time.sleep(0.1)
tracker2 = build_tracker(domain)
f2 = tracker2.fingerprint()
assert f1 == f2
def test_model_id_is_added_to_events():
tracker = DialogueStateTracker("bloop", [])
tracker.model_id = "some_id"
tracker.update(ActionExecuted(action_name="test"))
tracker.update_with_events([UserUttered(), SessionStarted()], None)
assert all(e.metadata[METADATA_MODEL_ID] == "some_id" for e in tracker.events)
def test_model_id_is_not_added_to_events_with_id():
tracker = DialogueStateTracker("bloop", [])
tracker.model_id = "some_id"
tracker.update(
ActionExecuted(action_name="test", metadata={METADATA_MODEL_ID: "old_id"})
)
assert tracker.events[-1].metadata[METADATA_MODEL_ID] == "old_id"
@pytest.mark.parametrize(
"event",
[
ActionExecuted(action_name="action_listen"),
UserUttered(),
SessionStarted(),
SlotSet("my_slot", 1),
Restarted(),
AllSlotsReset(),
ConversationPaused(),
ConversationResumed(),
StoryExported(),
ActionReverted(),
UserUtteranceReverted(),
FollowupAction("my_action"),
BotUttered("my_text", {"my_data": 1}),
AgentUttered("my_text", "my_data"),
ReminderScheduled("my_intent", datetime.datetime.now()),
DefinePrevUserUtteredFeaturization(False),
EntitiesAdded([{"entity": "entity1", "value": "value1"}]),
ActionExecutionRejected("test_action"),
ActiveLoop("my_form"),
LoopInterrupted(True),
],
)
def test_assistant_id_is_added_to_events(event):
assistant_id = "some_unique_assistant_name"
tracker = DialogueStateTracker("123abcd", [])
tracker.assistant_id = assistant_id
tracker.update(event)
assert all(
event.metadata[ASSISTANT_ID_KEY] == assistant_id for event in tracker.events
)
def test_assistant_id_is_not_added_to_events_with_assistant_id():
assistant_id = "some_unique_assistant_name"
tracker = DialogueStateTracker("123abcd", [])
tracker.assistant_id = assistant_id
tracker.update(
ActionExecuted(action_name="test", metadata={ASSISTANT_ID_KEY: "old_name"})
)
assert tracker.events[-1].metadata[ASSISTANT_ID_KEY] == "old_name"