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
1345 lines
43 KiB
Python
1345 lines
43 KiB
Python
import logging
|
|
import warnings
|
|
from collections import deque
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
|
|
import fakeredis
|
|
import pytest
|
|
import sqlalchemy
|
|
import uuid
|
|
|
|
from _pytest.capture import CaptureFixture
|
|
from _pytest.logging import LogCaptureFixture
|
|
from _pytest.monkeypatch import MonkeyPatch
|
|
from moto import mock_dynamodb
|
|
from pymongo.errors import OperationFailure
|
|
|
|
from rasa.core.agent import Agent
|
|
from rasa.nlu.tokenizers.whitespace_tokenizer import WhitespaceTokenizer
|
|
from rasa.shared.constants import DEFAULT_SENDER_ID
|
|
from sqlalchemy.dialects.postgresql.base import PGDialect
|
|
from sqlalchemy.dialects.sqlite.base import SQLiteDialect
|
|
from sqlalchemy.dialects.oracle.base import OracleDialect
|
|
from sqlalchemy.engine.url import URL
|
|
from typing import Any, Tuple, Text, Type, Dict, List, Union, Optional, ContextManager
|
|
from unittest.mock import MagicMock, Mock
|
|
|
|
import rasa.core.tracker_store
|
|
from rasa.shared.core.constants import (
|
|
ACTION_LISTEN_NAME,
|
|
ACTION_RESTART_NAME,
|
|
ACTION_SESSION_START_NAME,
|
|
)
|
|
from rasa.core.constants import POSTGRESQL_SCHEMA
|
|
from rasa.shared.core.domain import Domain
|
|
from rasa.shared.core.events import (
|
|
SlotSet,
|
|
ActionExecuted,
|
|
Restarted,
|
|
UserUttered,
|
|
SessionStarted,
|
|
BotUttered,
|
|
Event,
|
|
)
|
|
from rasa.shared.exceptions import ConnectionException, RasaException
|
|
from rasa.core.tracker_store import (
|
|
TrackerStore,
|
|
InMemoryTrackerStore,
|
|
RedisTrackerStore,
|
|
DEFAULT_REDIS_TRACKER_STORE_KEY_PREFIX,
|
|
SQLTrackerStore,
|
|
DynamoTrackerStore,
|
|
FailSafeTrackerStore,
|
|
AwaitableTrackerStore,
|
|
)
|
|
from rasa.shared.core.trackers import DialogueStateTracker, TrackerEventDiffEngine
|
|
from rasa.shared.nlu.training_data.message import Message
|
|
from rasa.utils.endpoints import EndpointConfig, read_endpoint_config
|
|
from tests.conftest import AsyncMock
|
|
from tests.core.conftest import MockedMongoTrackerStore
|
|
|
|
test_domain = Domain.load("data/test_domains/default.yml")
|
|
|
|
|
|
async def get_or_create_tracker_store(store: TrackerStore) -> None:
|
|
slot_key = "location"
|
|
slot_val = "Easter Island"
|
|
|
|
tracker = await store.get_or_create_tracker(DEFAULT_SENDER_ID)
|
|
ev = SlotSet(slot_key, slot_val)
|
|
tracker.update(ev)
|
|
assert tracker.get_slot(slot_key) == slot_val
|
|
|
|
await store.save(tracker)
|
|
|
|
again = await store.get_or_create_tracker(DEFAULT_SENDER_ID)
|
|
assert again.get_slot(slot_key) == slot_val
|
|
|
|
|
|
def test_get_or_create():
|
|
get_or_create_tracker_store(InMemoryTrackerStore(test_domain))
|
|
|
|
|
|
# noinspection PyPep8Naming
|
|
@mock_dynamodb
|
|
def test_dynamo_get_or_create():
|
|
get_or_create_tracker_store(DynamoTrackerStore(test_domain))
|
|
|
|
|
|
@mock_dynamodb
|
|
async def test_dynamo_tracker_floats():
|
|
conversation_id = uuid.uuid4().hex
|
|
|
|
tracker_store = DynamoTrackerStore(test_domain)
|
|
tracker = await tracker_store.get_or_create_tracker(
|
|
conversation_id, append_action_listen=False
|
|
)
|
|
|
|
# save `slot` event with known `float`-type timestamp
|
|
timestamp = 13423.23434623
|
|
tracker.update(SlotSet("key", "val", timestamp=timestamp))
|
|
await tracker_store.save(tracker)
|
|
|
|
# retrieve tracker and the event timestamp is retrieved as a `float`
|
|
tracker = await tracker_store.get_or_create_tracker(conversation_id)
|
|
retrieved_timestamp = tracker.events[0].timestamp
|
|
assert isinstance(retrieved_timestamp, float)
|
|
assert retrieved_timestamp == timestamp
|
|
|
|
|
|
async def test_restart_after_retrieval_from_tracker_store(domain: Domain):
|
|
store = InMemoryTrackerStore(domain)
|
|
tr = await store.get_or_create_tracker("myuser")
|
|
synth = [ActionExecuted("action_listen") for _ in range(4)]
|
|
|
|
for e in synth:
|
|
tr.update(e)
|
|
|
|
tr.update(Restarted())
|
|
latest_restart = tr.idx_after_latest_restart()
|
|
|
|
await store.save(tr)
|
|
tr2 = await store.retrieve("myuser")
|
|
latest_restart_after_loading = tr2.idx_after_latest_restart()
|
|
assert latest_restart == latest_restart_after_loading
|
|
|
|
|
|
async def test_tracker_store_remembers_max_history(domain: Domain):
|
|
store = InMemoryTrackerStore(domain)
|
|
tr = await store.get_or_create_tracker("myuser", max_event_history=42)
|
|
tr.update(Restarted())
|
|
|
|
await store.save(tr)
|
|
tr2 = await store.retrieve("myuser")
|
|
assert tr._max_event_history == tr2._max_event_history == 42
|
|
|
|
|
|
def test_tracker_store_endpoint_config_loading(endpoints_path: Text):
|
|
cfg = read_endpoint_config(endpoints_path, "tracker_store")
|
|
|
|
assert cfg == EndpointConfig.from_dict(
|
|
{
|
|
"type": "redis",
|
|
"url": "localhost",
|
|
"port": 6379,
|
|
"db": 0,
|
|
"username": "username",
|
|
"password": "password",
|
|
"timeout": 30000,
|
|
"use_ssl": True,
|
|
"ssl_keyfile": "keyfile.key",
|
|
"ssl_certfile": "certfile.crt",
|
|
"ssl_ca_certs": "my-bundle.ca-bundle",
|
|
}
|
|
)
|
|
|
|
|
|
def test_create_tracker_store_from_endpoint_config(
|
|
domain: Domain, endpoints_path: Text
|
|
):
|
|
store = read_endpoint_config(endpoints_path, "tracker_store")
|
|
tracker_store = RedisTrackerStore(
|
|
domain=domain,
|
|
host="localhost",
|
|
port=6379,
|
|
db=0,
|
|
username="username",
|
|
password="password",
|
|
record_exp=3000,
|
|
use_ssl=True,
|
|
ssl_keyfile="keyfile.key",
|
|
ssl_certfile="certfile.crt",
|
|
ssl_ca_certs="my-bundle.ca-bundle",
|
|
)
|
|
|
|
assert isinstance(tracker_store, type(TrackerStore.create(store, domain)))
|
|
|
|
|
|
def test_redis_tracker_store_invalid_key_prefix(domain: Domain):
|
|
|
|
test_invalid_key_prefix = "$$ &!"
|
|
|
|
tracker_store = RedisTrackerStore(
|
|
domain=domain,
|
|
host="localhost",
|
|
port=6379,
|
|
db=0,
|
|
password="password",
|
|
key_prefix=test_invalid_key_prefix,
|
|
record_exp=3000,
|
|
)
|
|
|
|
assert tracker_store._get_key_prefix() == DEFAULT_REDIS_TRACKER_STORE_KEY_PREFIX
|
|
|
|
|
|
def test_redis_tracker_store_valid_key_prefix(domain: Domain):
|
|
test_valid_key_prefix = "spanish"
|
|
|
|
tracker_store = RedisTrackerStore(
|
|
domain=domain,
|
|
host="localhost",
|
|
port=6379,
|
|
db=0,
|
|
password="password",
|
|
key_prefix=test_valid_key_prefix,
|
|
record_exp=3000,
|
|
)
|
|
|
|
assert (
|
|
tracker_store._get_key_prefix()
|
|
== f"{test_valid_key_prefix}:{DEFAULT_REDIS_TRACKER_STORE_KEY_PREFIX}"
|
|
)
|
|
|
|
|
|
def test_exception_tracker_store_from_endpoint_config(
|
|
domain: Domain, monkeypatch: MonkeyPatch, endpoints_path: Text
|
|
):
|
|
"""Check if tracker store properly handles exceptions.
|
|
|
|
If we can not create a tracker store by instantiating the
|
|
expected type (e.g. due to an exception) we should fallback to
|
|
the default `InMemoryTrackerStore`."""
|
|
|
|
store = read_endpoint_config(endpoints_path, "tracker_store")
|
|
mock = Mock(side_effect=Exception("test exception"))
|
|
monkeypatch.setattr(rasa.core.tracker_store, "RedisTrackerStore", mock)
|
|
|
|
with pytest.raises(Exception) as e:
|
|
TrackerStore.create(store, domain)
|
|
|
|
assert "test exception" in str(e.value)
|
|
|
|
|
|
def test_raise_connection_exception_redis_tracker_store_creation(
|
|
domain: Domain, monkeypatch: MonkeyPatch, endpoints_path: Text
|
|
):
|
|
store = read_endpoint_config(endpoints_path, "tracker_store")
|
|
monkeypatch.setattr(
|
|
rasa.core.tracker_store,
|
|
"RedisTrackerStore",
|
|
Mock(side_effect=ConnectionError()),
|
|
)
|
|
|
|
with pytest.raises(ConnectionException):
|
|
TrackerStore.create(store, domain)
|
|
|
|
|
|
def test_mongo_tracker_store_raise_exception(domain: Domain, monkeypatch: MonkeyPatch):
|
|
monkeypatch.setattr(
|
|
rasa.core.tracker_store,
|
|
"MongoTrackerStore",
|
|
Mock(
|
|
side_effect=OperationFailure("not authorized on logs to execute command.")
|
|
),
|
|
)
|
|
with pytest.raises(ConnectionException) as error:
|
|
TrackerStore.create(
|
|
EndpointConfig(username="username", password="password", type="mongod"),
|
|
domain,
|
|
)
|
|
|
|
assert "not authorized on logs to execute command." in str(error.value)
|
|
|
|
|
|
class HostExampleTrackerStore(RedisTrackerStore):
|
|
pass
|
|
|
|
|
|
class NonAsyncTrackerStore(TrackerStore):
|
|
def retrieve(self, sender_id: Text) -> Optional[DialogueStateTracker]:
|
|
pass
|
|
|
|
def save(self, tracker: DialogueStateTracker) -> None:
|
|
pass
|
|
|
|
|
|
def test_tracker_store_with_host_argument_from_string(domain: Domain):
|
|
endpoints_path = "data/test_endpoints/custom_tracker_endpoints.yml"
|
|
store_config = read_endpoint_config(endpoints_path, "tracker_store")
|
|
store_config.type = "tests.core.test_tracker_stores.HostExampleTrackerStore"
|
|
|
|
with warnings.catch_warnings(record=True) as record:
|
|
warnings.simplefilter("error")
|
|
tracker_store = TrackerStore.create(store_config, domain)
|
|
|
|
assert len(record) == 0
|
|
|
|
assert isinstance(tracker_store, HostExampleTrackerStore)
|
|
|
|
|
|
def test_tracker_store_from_invalid_module(domain: Domain):
|
|
endpoints_path = "data/test_endpoints/custom_tracker_endpoints.yml"
|
|
store_config = read_endpoint_config(endpoints_path, "tracker_store")
|
|
store_config.type = "a.module.which.cannot.be.found"
|
|
|
|
with pytest.warns(UserWarning):
|
|
tracker_store = TrackerStore.create(store_config, domain)
|
|
|
|
assert isinstance(tracker_store, InMemoryTrackerStore)
|
|
|
|
|
|
def test_tracker_store_from_invalid_string(domain: Domain):
|
|
endpoints_path = "data/test_endpoints/custom_tracker_endpoints.yml"
|
|
store_config = read_endpoint_config(endpoints_path, "tracker_store")
|
|
store_config.type = "any string"
|
|
|
|
with pytest.warns(UserWarning):
|
|
tracker_store = TrackerStore.create(store_config, domain)
|
|
|
|
assert isinstance(tracker_store, InMemoryTrackerStore)
|
|
|
|
|
|
async def _tracker_store_and_tracker_with_slot_set() -> Tuple[
|
|
InMemoryTrackerStore, DialogueStateTracker
|
|
]:
|
|
# returns an InMemoryTrackerStore containing a tracker with a slot set
|
|
|
|
slot_key = "cuisine"
|
|
slot_val = "French"
|
|
|
|
store = InMemoryTrackerStore(test_domain)
|
|
tracker = await store.get_or_create_tracker(DEFAULT_SENDER_ID)
|
|
ev = SlotSet(slot_key, slot_val)
|
|
tracker.update(ev)
|
|
|
|
return store, tracker
|
|
|
|
|
|
async def test_tracker_serialisation():
|
|
store, tracker = await _tracker_store_and_tracker_with_slot_set()
|
|
serialised = store.serialise_tracker(tracker)
|
|
|
|
assert tracker == store.deserialise_tracker(DEFAULT_SENDER_ID, serialised)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"full_url",
|
|
[
|
|
"postgresql://localhost",
|
|
"postgresql://localhost:5432",
|
|
"postgresql://user:secret@localhost",
|
|
"sqlite:///",
|
|
],
|
|
)
|
|
def test_get_db_url_with_fully_specified_url(full_url: Text):
|
|
assert SQLTrackerStore.get_db_url(host=full_url) == full_url
|
|
|
|
|
|
def test_get_db_url_with_port_in_host():
|
|
host = "localhost:1234"
|
|
dialect = "postgresql"
|
|
db = "mydb"
|
|
|
|
expected = f"{dialect}://{host}/{db}"
|
|
|
|
assert (
|
|
str(SQLTrackerStore.get_db_url(dialect=dialect, host=host, db=db)) == expected
|
|
)
|
|
|
|
|
|
def test_db_get_url_with_sqlite():
|
|
expected = "sqlite:///rasa.db"
|
|
assert str(SQLTrackerStore.get_db_url(dialect="sqlite", db="rasa.db")) == expected
|
|
|
|
|
|
def test_get_db_url_with_correct_host():
|
|
expected = "postgresql://localhost:5005/mydb"
|
|
|
|
assert (
|
|
str(
|
|
SQLTrackerStore.get_db_url(
|
|
dialect="postgresql", host="localhost", port=5005, db="mydb"
|
|
)
|
|
)
|
|
== expected
|
|
)
|
|
|
|
|
|
def test_get_db_url_with_query():
|
|
expected = "postgresql://localhost:5005/mydb?driver=my-driver"
|
|
|
|
assert (
|
|
str(
|
|
SQLTrackerStore.get_db_url(
|
|
dialect="postgresql",
|
|
host="localhost",
|
|
port=5005,
|
|
db="mydb",
|
|
query={"driver": "my-driver"},
|
|
)
|
|
)
|
|
== expected
|
|
)
|
|
|
|
|
|
def test_sql_tracker_store_logs_do_not_show_password(caplog: LogCaptureFixture):
|
|
dialect = "postgresql"
|
|
host = "localhost"
|
|
port = 9901
|
|
db = "some-database"
|
|
username = "db-user"
|
|
password = "some-password"
|
|
|
|
with caplog.at_level(logging.DEBUG):
|
|
_ = SQLTrackerStore(None, dialect, host, port, db, username, password)
|
|
|
|
# the URL in the logs does not contain the password
|
|
assert password not in caplog.text
|
|
|
|
# instead the password is displayed as '***'
|
|
assert f"postgresql://{username}:***@{host}:{port}/{db}" in caplog.text
|
|
|
|
|
|
def test_db_url_with_query_from_endpoint_config(tmp_path: Path):
|
|
endpoint_config = """
|
|
tracker_store:
|
|
dialect: postgresql
|
|
url: localhost
|
|
port: 5123
|
|
username: user
|
|
password: pw
|
|
login_db: login-db
|
|
query:
|
|
driver: my-driver
|
|
another: query
|
|
"""
|
|
f = tmp_path / "tmp_config_file.yml"
|
|
f.write_text(endpoint_config)
|
|
store_config = read_endpoint_config(str(f), "tracker_store")
|
|
|
|
url = SQLTrackerStore.get_db_url(**store_config.kwargs)
|
|
|
|
import itertools
|
|
|
|
# order of query dictionary in yaml is random, test against both permutations
|
|
connection_url = "postgresql://user:pw@:5123/login-db?"
|
|
assert any(
|
|
str(url) == connection_url + "&".join(permutation)
|
|
for permutation in (
|
|
itertools.permutations(("another=query", "driver=my-driver"))
|
|
)
|
|
)
|
|
|
|
|
|
async def test_fail_safe_tracker_store_if_no_errors():
|
|
mocked_tracker_store = Mock()
|
|
|
|
tracker_store = FailSafeTrackerStore(mocked_tracker_store, None)
|
|
|
|
# test save
|
|
mocked_tracker_store.save = AsyncMock()
|
|
await tracker_store.save(None)
|
|
mocked_tracker_store.save.assert_called_once()
|
|
|
|
# test retrieve
|
|
expected = [1]
|
|
mocked_tracker_store.retrieve = AsyncMock(return_value=expected)
|
|
sender_id = "10"
|
|
assert await tracker_store.retrieve(sender_id) == expected
|
|
mocked_tracker_store.retrieve.assert_called_once_with(sender_id)
|
|
|
|
# test keys
|
|
expected = ["sender 1", "sender 2"]
|
|
mocked_tracker_store.keys = AsyncMock(return_value=expected)
|
|
assert await tracker_store.keys() == expected
|
|
mocked_tracker_store.keys.assert_called_once()
|
|
|
|
|
|
async def test_fail_safe_tracker_store_with_save_error():
|
|
mocked_tracker_store = Mock()
|
|
mocked_tracker_store.save = Mock(side_effect=Exception())
|
|
|
|
fallback_tracker_store = Mock()
|
|
fallback_tracker_store.save = AsyncMock()
|
|
|
|
on_error_callback = Mock()
|
|
|
|
tracker_store = FailSafeTrackerStore(
|
|
mocked_tracker_store, on_error_callback, fallback_tracker_store
|
|
)
|
|
await tracker_store.save(None)
|
|
|
|
fallback_tracker_store.save.assert_called_once()
|
|
on_error_callback.assert_called_once()
|
|
|
|
|
|
async def test_fail_safe_tracker_store_with_keys_error():
|
|
mocked_tracker_store = Mock()
|
|
mocked_tracker_store.keys = Mock(side_effect=Exception())
|
|
|
|
on_error_callback = Mock()
|
|
|
|
tracker_store = FailSafeTrackerStore(mocked_tracker_store, on_error_callback)
|
|
assert await tracker_store.keys() == []
|
|
on_error_callback.assert_called_once()
|
|
|
|
|
|
async def test_fail_safe_tracker_store_with_retrieve_error():
|
|
mocked_tracker_store = Mock()
|
|
mocked_tracker_store.retrieve = Mock(side_effect=Exception())
|
|
|
|
fallback_tracker_store = Mock()
|
|
on_error_callback = Mock()
|
|
|
|
tracker_store = FailSafeTrackerStore(
|
|
mocked_tracker_store, on_error_callback, fallback_tracker_store
|
|
)
|
|
|
|
assert await tracker_store.retrieve("sender_id") is None
|
|
on_error_callback.assert_called_once()
|
|
|
|
|
|
def test_set_fail_safe_tracker_store_domain(domain: Domain):
|
|
tracker_store = InMemoryTrackerStore(domain)
|
|
fallback_tracker_store = InMemoryTrackerStore(None)
|
|
failsafe_store = FailSafeTrackerStore(tracker_store, None, fallback_tracker_store)
|
|
|
|
failsafe_store.domain = domain
|
|
assert failsafe_store.domain is domain
|
|
assert tracker_store.domain is failsafe_store.domain
|
|
assert fallback_tracker_store.domain is failsafe_store.domain
|
|
|
|
|
|
async def create_tracker_with_partially_saved_events(
|
|
tracker_store: TrackerStore,
|
|
) -> Tuple[List[Event], DialogueStateTracker]:
|
|
# creates a tracker with two events and saved it to the tracker store
|
|
# following that, it adds three more events that are not saved to the tracker store
|
|
sender_id = uuid.uuid4().hex
|
|
|
|
# create tracker with two events and save it
|
|
events = [UserUttered("hello"), BotUttered("what")]
|
|
tracker = DialogueStateTracker.from_events(sender_id, events)
|
|
await tracker_store.save(tracker)
|
|
|
|
# add more events to the tracker, do not yet save it
|
|
events = [ActionExecuted(ACTION_LISTEN_NAME), UserUttered("123"), BotUttered("yes")]
|
|
for event in events:
|
|
tracker.update(event)
|
|
|
|
return events, tracker
|
|
|
|
|
|
async def _saved_tracker_with_multiple_session_starts(
|
|
tracker_store: TrackerStore, sender_id: Text
|
|
) -> DialogueStateTracker:
|
|
tracker = DialogueStateTracker.from_events(
|
|
sender_id,
|
|
[
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
UserUttered("hi"),
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
],
|
|
)
|
|
|
|
await tracker_store.save(tracker)
|
|
return await tracker_store.retrieve(sender_id)
|
|
|
|
|
|
async def test_mongo_additional_events(domain: Domain):
|
|
tracker_store = MockedMongoTrackerStore(domain)
|
|
events, tracker = await create_tracker_with_partially_saved_events(tracker_store)
|
|
|
|
# make sure only new events are returned
|
|
# noinspection PyProtectedMember
|
|
assert list(tracker_store._additional_events(tracker)) == events
|
|
|
|
|
|
async def test_mongo_additional_events_with_session_start(domain: Domain):
|
|
sender = "test_mongo_additional_events_with_session_start"
|
|
tracker_store = MockedMongoTrackerStore(domain)
|
|
tracker = await _saved_tracker_with_multiple_session_starts(tracker_store, sender)
|
|
|
|
tracker.update(UserUttered("hi2"))
|
|
|
|
# noinspection PyProtectedMember
|
|
additional_events = list(tracker_store._additional_events(tracker))
|
|
|
|
assert len(additional_events) == 1
|
|
assert isinstance(additional_events[0], UserUttered)
|
|
|
|
|
|
# we cannot parametrise over this and the previous test due to the different ways of
|
|
# calling _additional_events()
|
|
async def test_sql_additional_events(domain: Domain):
|
|
tracker_store = SQLTrackerStore(domain)
|
|
additional_events, tracker = await create_tracker_with_partially_saved_events(
|
|
tracker_store
|
|
)
|
|
|
|
# make sure only new events are returned
|
|
with tracker_store.session_scope() as session:
|
|
# noinspection PyProtectedMember
|
|
assert (
|
|
list(tracker_store._additional_events(session, tracker))
|
|
== additional_events
|
|
)
|
|
|
|
|
|
async def test_sql_additional_events_with_session_start(domain: Domain):
|
|
sender = "test_sql_additional_events_with_session_start"
|
|
tracker_store = SQLTrackerStore(domain)
|
|
tracker = await _saved_tracker_with_multiple_session_starts(tracker_store, sender)
|
|
|
|
tracker.update(UserUttered("hi2"), domain)
|
|
|
|
# make sure only new events are returned
|
|
with tracker_store.session_scope() as session:
|
|
# noinspection PyProtectedMember
|
|
additional_events = list(tracker_store._additional_events(session, tracker))
|
|
assert len(additional_events) == 1
|
|
assert isinstance(additional_events[0], UserUttered)
|
|
|
|
|
|
async def test_tracker_store_retrieve_ordered_by_id(
|
|
domain: Domain,
|
|
):
|
|
tracker_store_kwargs = {"host": "sqlite:///"}
|
|
tracker_store = SQLTrackerStore(domain, **tracker_store_kwargs)
|
|
events = [
|
|
SessionStarted(timestamp=1),
|
|
UserUttered("Hola", {"name": "greet"}, timestamp=2),
|
|
BotUttered("Hi", timestamp=2),
|
|
UserUttered("How are you?", {"name": "greet"}, timestamp=2),
|
|
BotUttered("I am good, whats up", timestamp=2),
|
|
UserUttered("Ciao", {"name": "greet"}, timestamp=2),
|
|
BotUttered("Bye", timestamp=2),
|
|
]
|
|
sender_id = "test_sql_tracker_store_events_order"
|
|
tracker = DialogueStateTracker.from_events(sender_id, events)
|
|
await tracker_store.save(tracker)
|
|
|
|
# Save other tracker to ensure that we don't run into problems with other senders
|
|
other_tracker = DialogueStateTracker.from_events("other-sender", [SessionStarted()])
|
|
await tracker_store.save(other_tracker)
|
|
|
|
# Retrieve tracker with events since latest SessionStarted
|
|
tracker = await tracker_store.retrieve(sender_id)
|
|
|
|
assert len(tracker.events) == 7
|
|
# assert the order of events is same as the order in which they were added
|
|
assert all((event == tracker.events[i] for i, event in enumerate(events)))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"tracker_store_type,tracker_store_kwargs",
|
|
[(MockedMongoTrackerStore, {}), (SQLTrackerStore, {"host": "sqlite:///"})],
|
|
)
|
|
async def test_tracker_store_retrieve_with_session_started_events(
|
|
tracker_store_type: Type[TrackerStore],
|
|
tracker_store_kwargs: Dict,
|
|
domain: Domain,
|
|
):
|
|
tracker_store = tracker_store_type(domain, **tracker_store_kwargs)
|
|
events = [
|
|
UserUttered("Hola", {"name": "greet"}, timestamp=1),
|
|
BotUttered("Hi", timestamp=2),
|
|
SessionStarted(timestamp=3),
|
|
UserUttered("Ciao", {"name": "greet"}, timestamp=4),
|
|
]
|
|
sender_id = "test_sql_tracker_store_with_session_events"
|
|
tracker = DialogueStateTracker.from_events(sender_id, events)
|
|
await tracker_store.save(tracker)
|
|
|
|
# Save other tracker to ensure that we don't run into problems with other senders
|
|
other_tracker = DialogueStateTracker.from_events("other-sender", [SessionStarted()])
|
|
await tracker_store.save(other_tracker)
|
|
|
|
# Retrieve tracker with events since latest SessionStarted
|
|
tracker = await tracker_store.retrieve(sender_id)
|
|
|
|
assert len(tracker.events) == 2
|
|
assert all((event == tracker.events[i] for i, event in enumerate(events[2:])))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"tracker_store_type,tracker_store_kwargs",
|
|
[(MockedMongoTrackerStore, {}), (SQLTrackerStore, {"host": "sqlite:///"})],
|
|
)
|
|
async def test_tracker_store_retrieve_without_session_started_events(
|
|
tracker_store_type: Type[TrackerStore],
|
|
tracker_store_kwargs: Dict,
|
|
domain,
|
|
):
|
|
tracker_store = tracker_store_type(domain, **tracker_store_kwargs)
|
|
|
|
# Create tracker with a SessionStarted event
|
|
events = [
|
|
UserUttered("Hola", {"name": "greet"}),
|
|
BotUttered("Hi"),
|
|
UserUttered("Ciao", {"name": "greet"}),
|
|
BotUttered("Hi2"),
|
|
]
|
|
|
|
sender_id = "test_sql_tracker_store_retrieve_without_session_started_events"
|
|
tracker = DialogueStateTracker.from_events(sender_id, events)
|
|
await tracker_store.save(tracker)
|
|
|
|
# Save other tracker to ensure that we don't run into problems with other senders
|
|
other_tracker = DialogueStateTracker.from_events("other-sender", [SessionStarted()])
|
|
await tracker_store.save(other_tracker)
|
|
|
|
tracker = await tracker_store.retrieve(sender_id)
|
|
|
|
assert len(tracker.events) == 4
|
|
assert all(event == tracker.events[i] for i, event in enumerate(events))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"tracker_store_type,tracker_store_kwargs",
|
|
[
|
|
(MockedMongoTrackerStore, {}),
|
|
(SQLTrackerStore, {"host": "sqlite:///"}),
|
|
(InMemoryTrackerStore, {}),
|
|
],
|
|
)
|
|
async def test_tracker_store_retrieve_with_events_from_previous_sessions(
|
|
tracker_store_type: Type[TrackerStore], tracker_store_kwargs: Dict
|
|
):
|
|
tracker_store = tracker_store_type(Domain.empty(), **tracker_store_kwargs)
|
|
|
|
conversation_id = uuid.uuid4().hex
|
|
tracker = DialogueStateTracker.from_events(
|
|
conversation_id,
|
|
[
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
UserUttered("hi"),
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
],
|
|
)
|
|
await tracker_store.save(tracker)
|
|
|
|
actual = await tracker_store.retrieve_full_tracker(conversation_id)
|
|
|
|
assert len(actual.events) == len(tracker.events)
|
|
|
|
|
|
def test_session_scope_error(
|
|
monkeypatch: MonkeyPatch, capsys: CaptureFixture, domain: Domain
|
|
):
|
|
tracker_store = SQLTrackerStore(domain)
|
|
tracker_store.sessionmaker = Mock()
|
|
|
|
requested_schema = uuid.uuid4().hex
|
|
|
|
# `ensure_schema_exists()` raises `ValueError`
|
|
mocked_ensure_schema_exists = Mock(side_effect=ValueError(requested_schema))
|
|
monkeypatch.setattr(
|
|
rasa.core.tracker_store, "ensure_schema_exists", mocked_ensure_schema_exists
|
|
)
|
|
|
|
# `SystemExit` is triggered by failing `ensure_schema_exists()`
|
|
with pytest.raises(SystemExit):
|
|
with tracker_store.session_scope() as _:
|
|
pass
|
|
|
|
# error message is printed
|
|
assert (
|
|
f"Requested PostgreSQL schema '{requested_schema}' was not found in the "
|
|
f"database." in capsys.readouterr()[0]
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"url,is_postgres_url",
|
|
[
|
|
(f"{PGDialect.name}://admin:pw@localhost:5432/rasa", True),
|
|
(f"{SQLiteDialect.name}:///", False),
|
|
(URL(PGDialect.name), True),
|
|
(URL(SQLiteDialect.name), False),
|
|
],
|
|
)
|
|
def test_is_postgres_url(url: Union[Text, URL], is_postgres_url: bool):
|
|
assert rasa.core.tracker_store.is_postgresql_url(url) == is_postgres_url
|
|
|
|
|
|
def set_or_delete_postgresql_schema_env_var(
|
|
monkeypatch: MonkeyPatch, value: Optional[Text]
|
|
) -> None:
|
|
"""Set `POSTGRESQL_SCHEMA` environment variable using `MonkeyPatch`.
|
|
|
|
Args:
|
|
monkeypatch: Instance of `MonkeyPatch` to use for patching.
|
|
value: Value of the `POSTGRESQL_SCHEMA` environment variable to set.
|
|
"""
|
|
if value is None:
|
|
monkeypatch.delenv(POSTGRESQL_SCHEMA, raising=False)
|
|
else:
|
|
monkeypatch.setenv(POSTGRESQL_SCHEMA, value)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"url,schema_env,kwargs",
|
|
[
|
|
# postgres without schema
|
|
(
|
|
f"{PGDialect.name}://admin:pw@localhost:5432/rasa",
|
|
None,
|
|
{
|
|
"pool_size": rasa.core.tracker_store.POSTGRESQL_DEFAULT_POOL_SIZE,
|
|
"max_overflow": rasa.core.tracker_store.POSTGRESQL_DEFAULT_MAX_OVERFLOW,
|
|
},
|
|
),
|
|
# postgres with schema
|
|
(
|
|
f"{PGDialect.name}://admin:pw@localhost:5432/rasa",
|
|
"schema1",
|
|
{
|
|
"connect_args": {"options": "-csearch_path=schema1"},
|
|
"pool_size": rasa.core.tracker_store.POSTGRESQL_DEFAULT_POOL_SIZE,
|
|
"max_overflow": rasa.core.tracker_store.POSTGRESQL_DEFAULT_MAX_OVERFLOW,
|
|
},
|
|
),
|
|
# oracle without schema
|
|
(f"{OracleDialect.name}://admin:pw@localhost:5432/rasa", None, {}),
|
|
# oracle with schema
|
|
(f"{OracleDialect.name}://admin:pw@localhost:5432/rasa", "schema1", {}),
|
|
# sqlite
|
|
(f"{SQLiteDialect.name}:///", None, {}),
|
|
],
|
|
)
|
|
def test_create_engine_kwargs(
|
|
monkeypatch: MonkeyPatch,
|
|
url: Union[Text, URL],
|
|
schema_env: Optional[Text],
|
|
kwargs: Dict[Text, Dict[Text, Union[Text, int]]],
|
|
):
|
|
set_or_delete_postgresql_schema_env_var(monkeypatch, schema_env)
|
|
|
|
assert rasa.core.tracker_store.create_engine_kwargs(url) == kwargs
|
|
|
|
|
|
@contextmanager
|
|
def does_not_raise():
|
|
"""Contextmanager to be used when an expression is not expected to raise an
|
|
exception.
|
|
|
|
This contextmanager can be used in parametrized tests, where some input objects
|
|
are expected to raise and others are not.
|
|
|
|
Example:
|
|
|
|
@pytest.mark.parametrize(
|
|
"a,b,raises_context",
|
|
[
|
|
# 5/6 is a legal divison
|
|
(5, 6, does_not_raise()),
|
|
# 5/0 raises a `ZeroDivisionError`
|
|
(5, 0, pytest.raises(ZeroDivisionError)),
|
|
],
|
|
)
|
|
def test_divide(
|
|
a: int, b: int, raises_context: ContextManager,
|
|
):
|
|
with raises_context:
|
|
_ = a / b
|
|
|
|
"""
|
|
yield
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"is_postgres,schema_env,schema_exists,raises_context",
|
|
[
|
|
(True, "schema1", True, does_not_raise()),
|
|
(True, "schema1", False, pytest.raises(ValueError)),
|
|
(False, "schema1", False, does_not_raise()),
|
|
(True, None, False, does_not_raise()),
|
|
(False, None, False, does_not_raise()),
|
|
],
|
|
)
|
|
def test_ensure_schema_exists(
|
|
monkeypatch: MonkeyPatch,
|
|
is_postgres: bool,
|
|
schema_env: Optional[Text],
|
|
schema_exists: bool,
|
|
raises_context: ContextManager,
|
|
):
|
|
set_or_delete_postgresql_schema_env_var(monkeypatch, schema_env)
|
|
monkeypatch.setattr(
|
|
rasa.core.tracker_store, "is_postgresql_url", lambda _: is_postgres
|
|
)
|
|
monkeypatch.setattr(sqlalchemy, "exists", Mock())
|
|
|
|
# mock the `session.query().scalar()` query which returns whether the schema
|
|
# exists in the db
|
|
scalar = Mock(return_value=schema_exists)
|
|
query = Mock(scalar=scalar)
|
|
session = Mock()
|
|
session.query = Mock(return_value=query)
|
|
|
|
with raises_context:
|
|
rasa.core.tracker_store.ensure_schema_exists(session)
|
|
|
|
|
|
def test_current_state_without_events(domain: Domain):
|
|
tracker_store = MockedMongoTrackerStore(domain)
|
|
|
|
# insert some events
|
|
events = [
|
|
UserUttered("Hola", {"name": "greet"}),
|
|
BotUttered("Hi"),
|
|
UserUttered("Ciao", {"name": "greet"}),
|
|
BotUttered("Hi2"),
|
|
]
|
|
|
|
sender_id = "test_mongo_tracker_store_current_state_without_events"
|
|
tracker = DialogueStateTracker.from_events(sender_id, events)
|
|
|
|
# get current state without events
|
|
# noinspection PyProtectedMember
|
|
state = tracker_store._current_tracker_state_without_events(tracker)
|
|
|
|
# `events` key should not be in there
|
|
assert state and "events" not in state
|
|
|
|
|
|
def test_login_db_with_no_postgresql(tmp_path: Path):
|
|
with pytest.warns(UserWarning):
|
|
SQLTrackerStore(db=str(tmp_path / "rasa.db"), login_db=str(tmp_path / "other"))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"config",
|
|
[
|
|
{
|
|
"type": "mongod",
|
|
"url": "mongodb://0.0.0.0:42/?serverSelectionTimeoutMS=5000",
|
|
},
|
|
{"type": "dynamo"},
|
|
],
|
|
)
|
|
def test_tracker_store_connection_error(config: Dict, domain: Domain):
|
|
store = EndpointConfig.from_dict(config)
|
|
|
|
with pytest.raises(ConnectionException):
|
|
TrackerStore.create(store, domain)
|
|
|
|
|
|
async def prepare_token_serialisation(
|
|
tracker_store: TrackerStore, response_selector_agent: Agent, sender_id: Text
|
|
):
|
|
text = "Good morning"
|
|
tokenizer = WhitespaceTokenizer(WhitespaceTokenizer.get_default_config())
|
|
tokens = tokenizer.tokenize(Message(data={"text": text}), "text")
|
|
indices = [[t.start, t.end] for t in tokens]
|
|
|
|
tracker = await tracker_store.get_or_create_tracker(sender_id=sender_id)
|
|
parse_data = await response_selector_agent.parse_message(text)
|
|
event = UserUttered(
|
|
"Good morning",
|
|
parse_data.get("intent"),
|
|
parse_data.get("entities", []),
|
|
parse_data,
|
|
)
|
|
|
|
tracker.update(event)
|
|
await tracker_store.save(tracker)
|
|
|
|
retrieved_tracker = await tracker_store.retrieve(sender_id=sender_id)
|
|
event = retrieved_tracker.get_last_event_for(event_type=UserUttered)
|
|
event_tokens = event.as_dict().get("parse_data").get("text_tokens")
|
|
|
|
assert event_tokens == indices
|
|
|
|
|
|
def test_inmemory_tracker_store_with_token_serialisation(
|
|
domain: Domain, default_agent: Agent
|
|
):
|
|
tracker_store = InMemoryTrackerStore(domain)
|
|
prepare_token_serialisation(tracker_store, default_agent, "inmemory")
|
|
|
|
|
|
def test_mongo_tracker_store_with_token_serialisation(
|
|
domain: Domain, response_selector_agent: Agent
|
|
):
|
|
tracker_store = MockedMongoTrackerStore(domain)
|
|
prepare_token_serialisation(tracker_store, response_selector_agent, "mongo")
|
|
|
|
|
|
def test_sql_tracker_store_with_token_serialisation(
|
|
domain: Domain, response_selector_agent: Agent
|
|
):
|
|
tracker_store = SQLTrackerStore(domain, **{"host": "sqlite:///"})
|
|
prepare_token_serialisation(tracker_store, response_selector_agent, "sql")
|
|
|
|
|
|
def test_sql_tracker_store_creation_with_invalid_port(domain: Domain):
|
|
with pytest.raises(RasaException) as error:
|
|
TrackerStore.create(
|
|
EndpointConfig(port="$DB_PORT", type="sql"),
|
|
domain,
|
|
)
|
|
assert "port '$DB_PORT' cannot be cast to integer." in str(error.value)
|
|
|
|
|
|
def test_create_non_async_tracker_store(domain: Domain):
|
|
endpoint_config = EndpointConfig(
|
|
type="tests.core.test_tracker_stores.NonAsyncTrackerStore"
|
|
)
|
|
with pytest.warns(FutureWarning):
|
|
tracker_store = TrackerStore.create(endpoint_config)
|
|
assert isinstance(tracker_store, AwaitableTrackerStore)
|
|
assert isinstance(tracker_store._tracker_store, NonAsyncTrackerStore)
|
|
|
|
|
|
def test_create_awaitable_tracker_store_with_endpoint_config():
|
|
endpoint_config = EndpointConfig(
|
|
type="tests.core.test_tracker_stores.NonAsyncTrackerStore"
|
|
)
|
|
tracker_store = AwaitableTrackerStore.create(endpoint_config)
|
|
|
|
assert isinstance(tracker_store, AwaitableTrackerStore)
|
|
assert isinstance(tracker_store._tracker_store, NonAsyncTrackerStore)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"endpoints_file, expected_type",
|
|
[
|
|
(None, InMemoryTrackerStore),
|
|
("data/test_endpoints/endpoints_sql.yml", SQLTrackerStore),
|
|
("data/test_endpoints/endpoints_redis.yml", RedisTrackerStore),
|
|
],
|
|
)
|
|
def test_create_tracker_store_from_endpoints_file(
|
|
endpoints_file: Optional[Text], expected_type: Any, domain: Domain
|
|
) -> None:
|
|
endpoint_config = read_endpoint_config(endpoints_file, "tracker_store")
|
|
tracker_store = rasa.core.tracker_store.create_tracker_store(
|
|
endpoint_config, domain
|
|
)
|
|
|
|
assert rasa.core.tracker_store.check_if_tracker_store_async(tracker_store) is True
|
|
assert isinstance(tracker_store, expected_type)
|
|
|
|
|
|
async def test_fail_safe_tracker_store_retrieve_full_tracker(
|
|
domain: Domain, tracker_with_restarted_event: DialogueStateTracker
|
|
) -> None:
|
|
primary_tracker_store = SQLTrackerStore(domain)
|
|
sender_id = tracker_with_restarted_event.sender_id
|
|
|
|
tracker_store = FailSafeTrackerStore(primary_tracker_store)
|
|
await tracker_store.save(tracker_with_restarted_event)
|
|
|
|
tracker = await tracker_store.retrieve_full_tracker(sender_id)
|
|
assert tracker == tracker_with_restarted_event
|
|
|
|
|
|
async def test_fail_safe_tracker_store_retrieve_full_tracker_with_exception(
|
|
caplog: LogCaptureFixture,
|
|
) -> None:
|
|
primary_tracker_store = MagicMock()
|
|
primary_tracker_store.domain = Domain.empty()
|
|
primary_tracker_store.event_broker = None
|
|
|
|
exception = Exception("Something went wrong")
|
|
primary_tracker_store.retrieve_full_tracker = AsyncMock(side_effect=exception)
|
|
|
|
tracker_store = FailSafeTrackerStore(primary_tracker_store)
|
|
with caplog.at_level(logging.ERROR):
|
|
await tracker_store.retrieve_full_tracker("some_id")
|
|
|
|
assert "Error happened when trying to retrieve conversation tracker" in caplog.text
|
|
assert f"Please investigate the following error: {exception}." in caplog.text
|
|
|
|
|
|
async def test_sql_get_or_create_full_tracker_without_action_listen() -> None:
|
|
tracker_store = SQLTrackerStore(Domain.empty())
|
|
sender_id = uuid.uuid4().hex
|
|
tracker = await tracker_store.get_or_create_full_tracker(
|
|
sender_id=sender_id, append_action_listen=False
|
|
)
|
|
assert tracker.sender_id == sender_id
|
|
assert tracker.events == deque()
|
|
|
|
|
|
async def test_sql_get_or_create_full_tracker_with_action_listen() -> None:
|
|
tracker_store = SQLTrackerStore(Domain.empty())
|
|
sender_id = uuid.uuid4().hex
|
|
tracker = await tracker_store.get_or_create_full_tracker(
|
|
sender_id=sender_id, append_action_listen=True
|
|
)
|
|
assert tracker.sender_id == sender_id
|
|
assert tracker.events == deque([ActionExecuted(ACTION_LISTEN_NAME)])
|
|
|
|
|
|
async def test_sql_get_or_create_full_tracker_with_existing_tracker(
|
|
tracker_with_restarted_event: DialogueStateTracker,
|
|
) -> None:
|
|
sender_id = tracker_with_restarted_event.sender_id
|
|
|
|
tracker_store = SQLTrackerStore(Domain.empty())
|
|
await tracker_store.save(tracker_with_restarted_event)
|
|
|
|
tracker = await tracker_store.get_or_create_full_tracker(
|
|
sender_id=sender_id, append_action_listen=False
|
|
)
|
|
assert tracker.sender_id == sender_id
|
|
assert tracker.events == deque(tracker_with_restarted_event.events)
|
|
|
|
|
|
async def test_sql_tracker_store_retrieve_full_tracker(
|
|
domain: Domain, tracker_with_restarted_event: DialogueStateTracker
|
|
) -> None:
|
|
tracker_store = SQLTrackerStore(domain)
|
|
sender_id = tracker_with_restarted_event.sender_id
|
|
await tracker_store.save(tracker_with_restarted_event)
|
|
|
|
tracker = await tracker_store.retrieve_full_tracker(sender_id)
|
|
assert tracker == tracker_with_restarted_event
|
|
|
|
|
|
async def test_sql_tracker_store_retrieve(
|
|
domain: Domain,
|
|
tracker_with_restarted_event: DialogueStateTracker,
|
|
events_after_restart: List[Event],
|
|
) -> None:
|
|
tracker_store = SQLTrackerStore(domain)
|
|
sender_id = tracker_with_restarted_event.sender_id
|
|
await tracker_store.save(tracker_with_restarted_event)
|
|
|
|
tracker = await tracker_store.retrieve(sender_id)
|
|
|
|
# the retrieved tracker with the latest session would not contain
|
|
# `action_session_start` event because the SQLTrackerStore filters
|
|
# only the events after `session_started` event
|
|
assert list(tracker.events) == events_after_restart[1:]
|
|
|
|
|
|
async def test_in_memory_tracker_store_retrieve_full_tracker(
|
|
domain: Domain,
|
|
tracker_with_restarted_event: DialogueStateTracker,
|
|
) -> None:
|
|
tracker_store = InMemoryTrackerStore(domain)
|
|
sender_id = tracker_with_restarted_event.sender_id
|
|
|
|
await tracker_store.save(tracker_with_restarted_event)
|
|
|
|
tracker = await tracker_store.retrieve_full_tracker(sender_id)
|
|
assert tracker == tracker_with_restarted_event
|
|
|
|
|
|
async def test_in_memory_tracker_store_retrieve(
|
|
domain: Domain,
|
|
tracker_with_restarted_event: DialogueStateTracker,
|
|
events_after_restart: List[Event],
|
|
) -> None:
|
|
tracker_store = InMemoryTrackerStore(domain)
|
|
sender_id = tracker_with_restarted_event.sender_id
|
|
await tracker_store.save(tracker_with_restarted_event)
|
|
|
|
tracker = await tracker_store.retrieve(sender_id)
|
|
assert list(tracker.events) == events_after_restart
|
|
|
|
|
|
async def test_mongo_tracker_store_retrieve_full_tracker(
|
|
domain: Domain,
|
|
tracker_with_restarted_event: DialogueStateTracker,
|
|
) -> None:
|
|
tracker_store = MockedMongoTrackerStore(domain)
|
|
sender_id = tracker_with_restarted_event.sender_id
|
|
|
|
await tracker_store.save(tracker_with_restarted_event)
|
|
|
|
tracker = await tracker_store.retrieve_full_tracker(sender_id)
|
|
assert tracker == tracker_with_restarted_event
|
|
|
|
|
|
async def test_mongo_tracker_store_retrieve(
|
|
domain: Domain,
|
|
tracker_with_restarted_event: DialogueStateTracker,
|
|
events_after_restart: List[Event],
|
|
) -> None:
|
|
tracker_store = MockedMongoTrackerStore(domain)
|
|
sender_id = tracker_with_restarted_event.sender_id
|
|
|
|
await tracker_store.save(tracker_with_restarted_event)
|
|
|
|
tracker = await tracker_store.retrieve(sender_id)
|
|
|
|
# the retrieved tracker with the latest session would not contain
|
|
# `action_session_start` event because the MongoTrackerStore filters
|
|
# only the events after `session_started` event
|
|
assert list(tracker.events) == events_after_restart[1:]
|
|
|
|
|
|
class MockedRedisTrackerStore(RedisTrackerStore):
|
|
def __init__(
|
|
self,
|
|
domain: Domain,
|
|
) -> None:
|
|
self.red = fakeredis.FakeStrictRedis()
|
|
self.key_prefix = DEFAULT_REDIS_TRACKER_STORE_KEY_PREFIX
|
|
self.record_exp = None
|
|
super(RedisTrackerStore, self).__init__(domain, None)
|
|
|
|
|
|
async def test_redis_tracker_store_retrieve_full_tracker(
|
|
domain: Domain,
|
|
tracker_with_restarted_event: DialogueStateTracker,
|
|
) -> None:
|
|
tracker_store = MockedRedisTrackerStore(domain)
|
|
sender_id = tracker_with_restarted_event.sender_id
|
|
|
|
await tracker_store.save(tracker_with_restarted_event)
|
|
|
|
tracker = await tracker_store.retrieve_full_tracker(sender_id)
|
|
assert tracker == tracker_with_restarted_event
|
|
|
|
|
|
async def test_redis_tracker_store_retrieve(
|
|
domain: Domain,
|
|
tracker_with_restarted_event: DialogueStateTracker,
|
|
events_after_restart: List[Event],
|
|
) -> None:
|
|
tracker_store = MockedRedisTrackerStore(domain)
|
|
sender_id = tracker_with_restarted_event.sender_id
|
|
|
|
await tracker_store.save(tracker_with_restarted_event)
|
|
|
|
tracker = await tracker_store.retrieve(sender_id)
|
|
assert list(tracker.events) == events_after_restart
|
|
|
|
|
|
async def test_redis_tracker_store_merge_trackers_same_session() -> None:
|
|
start_session_sequence = [
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
ActionExecuted(ACTION_LISTEN_NAME),
|
|
]
|
|
events: List[Event] = start_session_sequence + [UserUttered("hello")]
|
|
prior_tracker = DialogueStateTracker.from_events(
|
|
"same-session",
|
|
evts=events,
|
|
)
|
|
|
|
events += [BotUttered("Hey! How can I help you?")]
|
|
|
|
new_tracker = DialogueStateTracker.from_events(
|
|
"same-session",
|
|
evts=events,
|
|
)
|
|
|
|
actual_tracker = RedisTrackerStore._merge_trackers(prior_tracker, new_tracker)
|
|
|
|
assert actual_tracker == new_tracker
|
|
|
|
|
|
def test_redis_tracker_store_merge_trackers_overlapping_session() -> None:
|
|
prior_tracker_events: List[Event] = [
|
|
ActionExecuted(ACTION_SESSION_START_NAME, timestamp=1),
|
|
SessionStarted(timestamp=2),
|
|
ActionExecuted(ACTION_LISTEN_NAME, timestamp=3),
|
|
UserUttered("hello", timestamp=4),
|
|
BotUttered("Hey! How can I help you?", timestamp=5),
|
|
UserUttered("/restart", timestamp=6),
|
|
ActionExecuted(ACTION_RESTART_NAME, timestamp=7),
|
|
]
|
|
|
|
new_start_session = [
|
|
ActionExecuted(ACTION_SESSION_START_NAME, timestamp=8),
|
|
SessionStarted(timestamp=9),
|
|
ActionExecuted(ACTION_LISTEN_NAME, timestamp=10),
|
|
]
|
|
|
|
prior_tracker_events += new_start_session
|
|
prior_tracker = DialogueStateTracker.from_events(
|
|
"overlapping-session",
|
|
evts=prior_tracker_events,
|
|
)
|
|
|
|
after_restart_event = [UserUttered("hi again", timestamp=11)]
|
|
new_tracker_events = new_start_session + after_restart_event
|
|
|
|
new_tracker = DialogueStateTracker.from_events(
|
|
"overlapping-session",
|
|
evts=new_tracker_events,
|
|
)
|
|
|
|
actual_tracker = RedisTrackerStore._merge_trackers(prior_tracker, new_tracker)
|
|
|
|
expected_events = prior_tracker_events + after_restart_event
|
|
|
|
assert list(actual_tracker.events) == expected_events
|
|
|
|
|
|
def test_redis_tracker_store_merge_trackers_different_session() -> None:
|
|
prior_tracker_events: List[Event] = [
|
|
ActionExecuted(ACTION_SESSION_START_NAME, timestamp=1),
|
|
SessionStarted(timestamp=2),
|
|
ActionExecuted(ACTION_LISTEN_NAME, timestamp=3),
|
|
UserUttered("hello", timestamp=4),
|
|
BotUttered("Hey! How can I help you?", timestamp=5),
|
|
]
|
|
prior_tracker = DialogueStateTracker.from_events(
|
|
"different-session",
|
|
evts=prior_tracker_events,
|
|
)
|
|
|
|
new_session = [
|
|
ActionExecuted(ACTION_SESSION_START_NAME, timestamp=8),
|
|
SessionStarted(timestamp=9),
|
|
ActionExecuted(ACTION_LISTEN_NAME, timestamp=10),
|
|
UserUttered("I need help.", timestamp=11),
|
|
]
|
|
|
|
new_tracker = DialogueStateTracker.from_events(
|
|
"different-session",
|
|
evts=new_session,
|
|
)
|
|
|
|
actual_tracker = RedisTrackerStore._merge_trackers(prior_tracker, new_tracker)
|
|
|
|
expected_events = prior_tracker_events + new_session
|
|
assert list(actual_tracker.events) == expected_events
|
|
|
|
|
|
async def test_tracker_event_diff_engine_event_difference() -> None:
|
|
start_session_sequence = [
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
ActionExecuted(ACTION_LISTEN_NAME),
|
|
]
|
|
events: List[Event] = start_session_sequence + [UserUttered("hello")]
|
|
prior_tracker = DialogueStateTracker.from_events(
|
|
"same-session",
|
|
evts=events,
|
|
)
|
|
new_events = [BotUttered("Hey! How can I help you?")]
|
|
events += new_events
|
|
|
|
new_tracker = DialogueStateTracker.from_events(
|
|
"same-session",
|
|
evts=events,
|
|
)
|
|
|
|
event_diff = TrackerEventDiffEngine.event_difference(prior_tracker, new_tracker)
|
|
|
|
assert new_events == event_diff
|