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
2386 lines
74 KiB
Python
2386 lines
74 KiB
Python
import asyncio
|
|
import json
|
|
import os
|
|
import textwrap
|
|
import time
|
|
import urllib.parse
|
|
import uuid
|
|
import sys
|
|
from argparse import Namespace
|
|
from http import HTTPStatus
|
|
from multiprocessing import Manager
|
|
from multiprocessing.managers import DictProxy
|
|
from pathlib import Path
|
|
from typing import Any, List, Text, Tuple, Type, Generator, NoReturn, Dict, Optional
|
|
from unittest.mock import Mock, ANY
|
|
|
|
from _pytest.tmpdir import TempPathFactory
|
|
import pytest
|
|
from _pytest.monkeypatch import MonkeyPatch
|
|
from aioresponses import aioresponses
|
|
from freezegun import freeze_time
|
|
from unittest.mock import MagicMock
|
|
from ruamel.yaml import StringIO
|
|
from sanic import Sanic
|
|
from sanic_testing.testing import SanicASGITestClient
|
|
|
|
import rasa
|
|
import rasa.constants
|
|
import rasa.core.jobs
|
|
from rasa.engine.storage.local_model_storage import LocalModelStorage
|
|
import rasa.nlu
|
|
import rasa.server
|
|
import rasa.shared.constants
|
|
import rasa.shared.utils.io
|
|
import rasa.utils.io
|
|
from rasa.core import utils
|
|
from rasa.core.agent import Agent, load_agent
|
|
from rasa.core.channels import (
|
|
channel,
|
|
CollectingOutputChannel,
|
|
RestInput,
|
|
SlackInput,
|
|
CallbackInput,
|
|
)
|
|
from rasa.core.channels.slack import SlackBot
|
|
from rasa.core.tracker_store import InMemoryTrackerStore
|
|
import rasa.nlu.test
|
|
from rasa.nlu.test import CVEvaluationResult
|
|
from rasa.shared.core import events
|
|
from rasa.shared.core.constants import (
|
|
ACTION_RESTART_NAME,
|
|
ACTION_SESSION_START_NAME,
|
|
ACTION_LISTEN_NAME,
|
|
REQUESTED_SLOT,
|
|
SESSION_START_METADATA_SLOT,
|
|
)
|
|
from rasa.shared.core.domain import Domain, SessionConfig
|
|
from rasa.shared.core.events import (
|
|
Event,
|
|
Restarted,
|
|
UserUttered,
|
|
SlotSet,
|
|
BotUttered,
|
|
ActionExecuted,
|
|
SessionStarted,
|
|
)
|
|
from rasa.shared.core.trackers import DialogueStateTracker
|
|
from rasa.shared.nlu.constants import (
|
|
INTENT_NAME_KEY,
|
|
ENTITY_ATTRIBUTE_TYPE,
|
|
ENTITY_ATTRIBUTE_VALUE,
|
|
PREDICTED_CONFIDENCE_KEY,
|
|
)
|
|
from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION
|
|
from rasa.model_training import TrainingResult
|
|
from rasa.utils.endpoints import EndpointConfig
|
|
from tests.conftest import (
|
|
AsyncMock,
|
|
with_assistant_id,
|
|
with_assistant_ids,
|
|
with_model_id,
|
|
with_model_ids,
|
|
)
|
|
from tests.nlu.utilities import ResponseTest
|
|
from tests.utilities import json_of_latest_request, latest_request
|
|
|
|
# a couple of event instances that we can use for testing
|
|
test_events = [
|
|
Event.from_parameters(
|
|
{
|
|
"event": UserUttered.type_name,
|
|
"text": "/goodbye",
|
|
"parse_data": {
|
|
"intent": {"confidence": 1.0, INTENT_NAME_KEY: "greet"},
|
|
"entities": [],
|
|
},
|
|
}
|
|
),
|
|
BotUttered("Welcome!", {"test": True}),
|
|
SlotSet("cuisine", 34),
|
|
SlotSet("cuisine", "34"),
|
|
SlotSet("location", None),
|
|
SlotSet("location", [34, "34", None]),
|
|
]
|
|
|
|
# sequence of events expected at the beginning of trackers
|
|
session_start_sequence: List[Event] = [
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
ActionExecuted(ACTION_LISTEN_NAME),
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def rasa_app_without_api(rasa_server_without_api: Sanic) -> SanicASGITestClient:
|
|
return rasa_server_without_api.asgi_client
|
|
|
|
|
|
@pytest.fixture
|
|
def rasa_app(rasa_server: Sanic) -> SanicASGITestClient:
|
|
return rasa_server.asgi_client
|
|
|
|
|
|
@pytest.fixture
|
|
def rasa_non_trained_app(rasa_non_trained_server: Sanic) -> SanicASGITestClient:
|
|
return rasa_non_trained_server.asgi_client
|
|
|
|
|
|
@pytest.fixture
|
|
def rasa_app_nlu(rasa_nlu_server: Sanic) -> SanicASGITestClient:
|
|
return rasa_nlu_server.asgi_client
|
|
|
|
|
|
@pytest.fixture
|
|
def rasa_app_core(rasa_core_server: Sanic) -> SanicASGITestClient:
|
|
return rasa_core_server.asgi_client
|
|
|
|
|
|
@pytest.fixture
|
|
def rasa_secured_app(rasa_server_secured: Sanic) -> SanicASGITestClient:
|
|
return rasa_server_secured.asgi_client
|
|
|
|
|
|
@pytest.fixture
|
|
def rasa_secured_app_asymmetric(
|
|
rasa_server_secured_asymmetric: Sanic,
|
|
) -> SanicASGITestClient:
|
|
return rasa_server_secured_asymmetric.asgi_client
|
|
|
|
|
|
@pytest.fixture
|
|
def rasa_non_trained_secured_app(
|
|
rasa_non_trained_server_secured: Sanic,
|
|
) -> SanicASGITestClient:
|
|
return rasa_non_trained_server_secured.asgi_client
|
|
|
|
|
|
@pytest.fixture()
|
|
async def tear_down_scheduler() -> Generator[None, None, None]:
|
|
yield None
|
|
rasa.core.jobs.__scheduler = None
|
|
|
|
|
|
async def test_root(rasa_non_trained_app: SanicASGITestClient):
|
|
_, response = await rasa_non_trained_app.get("/")
|
|
assert response.status == HTTPStatus.OK
|
|
assert response.text.startswith("Hello from Rasa:")
|
|
|
|
|
|
async def test_root_without_enable_api(rasa_app_without_api: SanicASGITestClient):
|
|
_, response = await rasa_app_without_api.get("/")
|
|
assert response.status == HTTPStatus.OK
|
|
assert response.text.startswith("Hello from Rasa:")
|
|
|
|
|
|
async def test_root_secured(rasa_non_trained_secured_app: SanicASGITestClient):
|
|
_, response = await rasa_non_trained_secured_app.get("/")
|
|
assert response.status == HTTPStatus.OK
|
|
assert response.text.startswith("Hello from Rasa:")
|
|
|
|
|
|
async def test_version(rasa_non_trained_app: SanicASGITestClient):
|
|
_, response = await rasa_non_trained_app.get("/version")
|
|
content = response.json
|
|
assert response.status == HTTPStatus.OK
|
|
assert content.get("version") == rasa.__version__
|
|
assert (
|
|
content.get("minimum_compatible_version")
|
|
== rasa.constants.MINIMUM_COMPATIBLE_VERSION
|
|
)
|
|
|
|
|
|
async def test_status(rasa_app: SanicASGITestClient, trained_rasa_model: Text):
|
|
_, response = await rasa_app.get("/status")
|
|
model_file = response.json["model_file"]
|
|
assert response.status == HTTPStatus.OK
|
|
assert "model_id" in response.json
|
|
assert model_file == Path(trained_rasa_model).name
|
|
|
|
|
|
async def test_status_nlu_only(
|
|
rasa_app_nlu: SanicASGITestClient, trained_nlu_model: Text
|
|
):
|
|
_, response = await rasa_app_nlu.get("/status")
|
|
model_file = response.json["model_file"]
|
|
assert response.status == HTTPStatus.OK
|
|
assert "model_id" in response.json
|
|
assert "model_file" in response.json
|
|
assert model_file == Path(trained_nlu_model).name
|
|
|
|
|
|
async def test_status_secured(rasa_secured_app: SanicASGITestClient):
|
|
_, response = await rasa_secured_app.get("/status")
|
|
assert response.status == HTTPStatus.UNAUTHORIZED
|
|
|
|
|
|
async def test_status_not_ready_agent(rasa_app: SanicASGITestClient):
|
|
rasa_app.sanic_app.ctx.agent = None
|
|
_, response = await rasa_app.get("/status")
|
|
assert response.status == HTTPStatus.CONFLICT
|
|
|
|
|
|
@pytest.fixture
|
|
def shared_statuses() -> DictProxy:
|
|
return Manager().dict()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"response_test",
|
|
[
|
|
ResponseTest(
|
|
"/model/parse",
|
|
{
|
|
"entities": [],
|
|
"intent": {"confidence": 1.0, INTENT_NAME_KEY: "greet"},
|
|
"text": "hello",
|
|
},
|
|
payload={"text": "hello"},
|
|
),
|
|
ResponseTest(
|
|
"/model/parse",
|
|
{
|
|
"entities": [],
|
|
"intent": {"confidence": 1.0, INTENT_NAME_KEY: "greet"},
|
|
"text": "hello",
|
|
},
|
|
payload={"text": "hello"},
|
|
),
|
|
ResponseTest(
|
|
"/model/parse",
|
|
{
|
|
"entities": [],
|
|
"intent": {"confidence": 1.0, INTENT_NAME_KEY: "greet"},
|
|
"text": "hello ńöñàśçií",
|
|
},
|
|
payload={"text": "hello ńöñàśçií"},
|
|
),
|
|
],
|
|
)
|
|
async def test_parse(rasa_app: SanicASGITestClient, response_test: ResponseTest):
|
|
_, response = await rasa_app.post(
|
|
response_test.endpoint, json=response_test.payload
|
|
)
|
|
rjs = response.json
|
|
assert response.status == HTTPStatus.OK
|
|
assert all(prop in rjs for prop in ["entities", "intent", "text"])
|
|
assert rjs["entities"] == response_test.expected_response["entities"]
|
|
assert rjs["text"] == response_test.expected_response["text"]
|
|
assert rjs["intent"] == response_test.expected_response["intent"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"response_test",
|
|
[
|
|
ResponseTest(
|
|
"/model/parse?emulation_mode=wit",
|
|
{
|
|
"entities": [],
|
|
"intent": {"confidence": 1.0, INTENT_NAME_KEY: "greet"},
|
|
"text": "hello",
|
|
},
|
|
payload={"text": "hello"},
|
|
),
|
|
ResponseTest(
|
|
"/model/parse?emulation_mode=dialogflow",
|
|
{
|
|
"entities": [],
|
|
"intent": {"confidence": 1.0, INTENT_NAME_KEY: "greet"},
|
|
"text": "hello",
|
|
},
|
|
payload={"text": "hello"},
|
|
),
|
|
ResponseTest(
|
|
"/model/parse?emulation_mode=luis",
|
|
{
|
|
"entities": [],
|
|
"intent": {"confidence": 1.0, INTENT_NAME_KEY: "greet"},
|
|
"text": "hello ńöñàśçií",
|
|
},
|
|
payload={"text": "hello ńöñàśçií"},
|
|
),
|
|
],
|
|
)
|
|
async def test_parse_with_different_emulation_mode(
|
|
rasa_app: SanicASGITestClient, response_test: ResponseTest
|
|
):
|
|
_, response = await rasa_app.post(
|
|
response_test.endpoint, json=response_test.payload
|
|
)
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
|
|
async def test_parse_without_nlu_model(rasa_app_core: SanicASGITestClient):
|
|
_, response = await rasa_app_core.post("/model/parse", json={"text": "hello"})
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
rjs = response.json
|
|
assert all(prop in rjs for prop in ["entities", "intent", "text"])
|
|
|
|
|
|
async def test_parse_on_invalid_emulation_mode(rasa_app: SanicASGITestClient):
|
|
_, response = await rasa_app.post(
|
|
"/model/parse?emulation_mode=ANYTHING", json={"text": "hello"}
|
|
)
|
|
assert response.status == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
async def test_train_nlu_success(
|
|
rasa_app: SanicASGITestClient,
|
|
stack_config_path: Text,
|
|
nlu_data_path: Text,
|
|
domain_path: Text,
|
|
tmp_path_factory: TempPathFactory,
|
|
):
|
|
domain_data = rasa.shared.utils.io.read_yaml_file(domain_path)
|
|
config_data = rasa.shared.utils.io.read_yaml_file(stack_config_path)
|
|
nlu_data = rasa.shared.utils.io.read_yaml_file(nlu_data_path)
|
|
|
|
# combine all data into our payload
|
|
payload = {
|
|
key: val for d in [domain_data, config_data, nlu_data] for key, val in d.items()
|
|
}
|
|
|
|
data = StringIO()
|
|
rasa.shared.utils.io.write_yaml(payload, data)
|
|
|
|
_, response = await rasa_app.post(
|
|
"/model/train",
|
|
data=data.getvalue(),
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
)
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
# save model to temporary file
|
|
model_path = str(Path(tmp_path_factory.mktemp("model_dir")) / "model.tar.gz")
|
|
with open(model_path, "wb") as f:
|
|
f.write(response.body)
|
|
|
|
storage_path = tmp_path_factory.mktemp("storage_path")
|
|
model_storage, model_metadata = LocalModelStorage.from_model_archive(
|
|
storage_path, model_path
|
|
)
|
|
assert model_metadata.model_id
|
|
|
|
|
|
async def test_train_core_success_with(
|
|
rasa_app: SanicASGITestClient,
|
|
stack_config_path: Text,
|
|
stories_path: Text,
|
|
domain_path: Text,
|
|
tmp_path_factory: TempPathFactory,
|
|
):
|
|
payload = f"""
|
|
{Path(domain_path).read_text()}
|
|
{Path(stack_config_path).read_text()}
|
|
{Path(stories_path).read_text()}
|
|
"""
|
|
|
|
_, response = await rasa_app.post(
|
|
"/model/train",
|
|
data=payload,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
)
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
# save model to temporary file
|
|
model_path = str(Path(tmp_path_factory.mktemp("model_dir")) / "model.tar.gz")
|
|
with open(model_path, "wb") as f:
|
|
f.write(response.body)
|
|
|
|
storage_path = tmp_path_factory.mktemp("storage_path")
|
|
model_storage, model_metadata = LocalModelStorage.from_model_archive(
|
|
storage_path, model_path
|
|
)
|
|
assert model_metadata.model_id
|
|
|
|
|
|
async def test_train_with_retrieval_events_success(
|
|
rasa_app: SanicASGITestClient,
|
|
stack_config_path: Text,
|
|
tmp_path_factory: TempPathFactory,
|
|
):
|
|
payload = {}
|
|
|
|
tmp_path = tmp_path_factory.mktemp("tmp")
|
|
|
|
for file in [
|
|
"data/test_domains/default_retrieval_intents.yml",
|
|
stack_config_path,
|
|
"data/test_yaml_stories/stories_retrieval_intents.yml",
|
|
"data/test_responses/default.yml",
|
|
"data/test/stories_default_retrieval_intents.yml",
|
|
]:
|
|
# Read in as dictionaries to avoid that keys, which are specified in
|
|
# multiple files (such as 'version'), clash.
|
|
content = rasa.shared.utils.io.read_yaml_file(file)
|
|
payload.update(content)
|
|
|
|
concatenated_payload_file = tmp_path / "concatenated.yml"
|
|
rasa.shared.utils.io.write_yaml(payload, concatenated_payload_file)
|
|
|
|
payload_as_yaml = concatenated_payload_file.read_text()
|
|
|
|
# it usually takes a bit longer on windows so we're going to double the timeout
|
|
timeout = 60 * 10 if sys.platform == "win32" else 60 * 5
|
|
|
|
_, response = await rasa_app.post(
|
|
"/model/train",
|
|
data=payload_as_yaml,
|
|
timeout=timeout,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
)
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
assert_trained_model(response.body, tmp_path_factory)
|
|
|
|
|
|
def assert_trained_model(
|
|
response_body: bytes, tmp_path_factory: TempPathFactory
|
|
) -> None:
|
|
# save model to temporary file
|
|
|
|
model_path = str(Path(tmp_path_factory.mktemp("model_dir")) / "model.tar.gz")
|
|
with open(model_path, "wb") as f:
|
|
f.write(response_body)
|
|
|
|
storage_path = tmp_path_factory.mktemp("storage_path")
|
|
model_storage, model_metadata = LocalModelStorage.from_model_archive(
|
|
storage_path, model_path
|
|
)
|
|
assert model_metadata.model_id
|
|
|
|
|
|
async def test_train_with_yaml(
|
|
rasa_app: SanicASGITestClient, tmp_path_factory: TempPathFactory
|
|
):
|
|
training_data = f"""
|
|
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
|
|
|
|
stories:
|
|
- story: My story
|
|
steps:
|
|
- intent: greet
|
|
- action: utter_greet
|
|
|
|
rules:
|
|
- rule: My rule
|
|
steps:
|
|
- intent: greet
|
|
- action: utter_greet
|
|
|
|
intents:
|
|
- greet
|
|
|
|
nlu:
|
|
- intent: greet
|
|
examples: |
|
|
- hi
|
|
- hello
|
|
|
|
responses:
|
|
utter_greet:
|
|
- text: Hi
|
|
|
|
recipe: default.v1
|
|
language: en
|
|
assistant_id: placeholder_default
|
|
|
|
policies:
|
|
- name: RulePolicy
|
|
|
|
pipeline:
|
|
- name: KeywordIntentClassifier
|
|
"""
|
|
_, response = await rasa_app.post(
|
|
"/model/train",
|
|
data=training_data,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
assert_trained_model(response.body, tmp_path_factory)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"params", [{}, {"augmentation": 20, "num_threads": 2, "force_training": True}]
|
|
)
|
|
async def test_train_with_yaml_with_params(
|
|
monkeypatch: MonkeyPatch,
|
|
rasa_non_trained_app: SanicASGITestClient,
|
|
tmp_path: Path,
|
|
params: Dict,
|
|
):
|
|
fake_model = Path(tmp_path) / "fake_model.tar.gz"
|
|
fake_model.touch()
|
|
fake_model_path = str(fake_model)
|
|
mock_train = Mock(return_value=TrainingResult(model=fake_model_path))
|
|
monkeypatch.setattr(rasa.model_training, "train", mock_train)
|
|
|
|
training_data = """
|
|
stories: []
|
|
rules: []
|
|
intents: []
|
|
nlu: []
|
|
responses: {}
|
|
recipe: default.v1
|
|
language: en
|
|
policies: []
|
|
pipeline: []
|
|
"""
|
|
_, response = await rasa_non_trained_app.post(
|
|
"/model/train",
|
|
data=training_data,
|
|
params=params,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
assert mock_train.call_count == 1
|
|
args, kwargs = mock_train.call_args_list[0]
|
|
assert kwargs["core_additional_arguments"]["augmentation_factor"] == params.get(
|
|
"augmentation", 50
|
|
)
|
|
assert kwargs["nlu_additional_arguments"]["num_threads"] == params.get(
|
|
"num_threads", 1
|
|
)
|
|
assert kwargs["force_training"] == params.get("force_training", False)
|
|
|
|
|
|
async def test_train_with_invalid_yaml(rasa_non_trained_app: SanicASGITestClient):
|
|
invalid_yaml = """
|
|
rules:
|
|
rule my rule
|
|
"""
|
|
|
|
_, response = await rasa_non_trained_app.post(
|
|
"/model/train",
|
|
data=invalid_yaml,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
)
|
|
assert response.status == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"headers, expected",
|
|
[({}, False), ({"force_training": False}, False), ({"force_training": True}, True)],
|
|
)
|
|
def test_training_payload_from_yaml_force_training(
|
|
headers: Dict, expected: bool, tmp_path: Path
|
|
):
|
|
request = Mock()
|
|
request.body = b""
|
|
request.args = headers
|
|
|
|
payload = rasa.server._training_payload_from_yaml(request, tmp_path)
|
|
assert payload.get("force_training") == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"headers, expected",
|
|
[
|
|
({}, rasa.shared.constants.DEFAULT_MODELS_PATH),
|
|
({"save_to_default_model_directory": False}, ANY),
|
|
(
|
|
{"save_to_default_model_directory": True},
|
|
rasa.shared.constants.DEFAULT_MODELS_PATH,
|
|
),
|
|
],
|
|
)
|
|
def test_training_payload_from_yaml_save_to_default_model_directory(
|
|
headers: Dict, expected: Text, tmp_path: Path
|
|
):
|
|
request = Mock()
|
|
request.body = b""
|
|
request.args = headers
|
|
|
|
payload = rasa.server._training_payload_from_yaml(request, tmp_path)
|
|
assert payload.get("output")
|
|
assert payload.get("output") == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"headers, expected",
|
|
[
|
|
({}, rasa.shared.constants.DEFAULT_MODELS_PATH),
|
|
({"save_to_default_model_directory": False}, ANY),
|
|
(
|
|
{"save_to_default_model_directory": True},
|
|
rasa.shared.constants.DEFAULT_MODELS_PATH,
|
|
),
|
|
],
|
|
)
|
|
def test_nlu_training_payload_from_json(headers: Dict, expected: Text, tmp_path: Path):
|
|
request = Mock()
|
|
request.json = {"rasa_nlu_data": {"common_examples": []}}
|
|
request.args = headers
|
|
|
|
payload = rasa.server._nlu_training_payload_from_json(request, tmp_path)
|
|
assert payload.get("output")
|
|
assert payload.get("output") == expected
|
|
|
|
|
|
async def test_evaluate_stories(rasa_app: SanicASGITestClient, stories_path: Text):
|
|
stories = rasa.shared.utils.io.read_file(stories_path)
|
|
|
|
_, response = await rasa_app.post(
|
|
"/model/test/stories",
|
|
data=stories,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
js = response.json
|
|
assert set(js.keys()) == {
|
|
"report",
|
|
"precision",
|
|
"f1",
|
|
"accuracy",
|
|
"actions",
|
|
"in_training_data_fraction",
|
|
"is_end_to_end_evaluation",
|
|
}
|
|
assert not js["is_end_to_end_evaluation"]
|
|
assert set(js["actions"][0].keys()) == {
|
|
"action",
|
|
"predicted",
|
|
"confidence",
|
|
"policy",
|
|
}
|
|
|
|
|
|
async def test_evaluate_stories_not_ready_agent(
|
|
rasa_non_trained_app: SanicASGITestClient, stories_path: Text
|
|
):
|
|
stories = rasa.shared.utils.io.read_file(stories_path)
|
|
|
|
_, response = await rasa_non_trained_app.post("/model/test/stories", data=stories)
|
|
|
|
assert response.status == HTTPStatus.CONFLICT
|
|
|
|
|
|
async def test_evaluate_stories_end_to_end(
|
|
rasa_app: SanicASGITestClient, end_to_end_story_path: Text
|
|
):
|
|
stories = rasa.shared.utils.io.read_file(end_to_end_story_path)
|
|
|
|
_, response = await rasa_app.post(
|
|
"/model/test/stories?e2e=true",
|
|
data=stories,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
js = response.json
|
|
assert set(js.keys()) == {
|
|
"report",
|
|
"precision",
|
|
"f1",
|
|
"accuracy",
|
|
"actions",
|
|
"in_training_data_fraction",
|
|
"is_end_to_end_evaluation",
|
|
}
|
|
assert js["is_end_to_end_evaluation"]
|
|
assert js["actions"] != []
|
|
assert set(js["actions"][0].keys()) == {
|
|
"action",
|
|
"predicted",
|
|
"confidence",
|
|
"policy",
|
|
}
|
|
|
|
|
|
async def test_add_message(rasa_app: SanicASGITestClient):
|
|
|
|
conversation_id = "test_add_message_test_id"
|
|
|
|
_, response = await rasa_app.get(f"/conversations/{conversation_id}/tracker")
|
|
previous_num_events = len(response.json["events"])
|
|
|
|
unique_text = f"test_add_message_text_{time.time()}"
|
|
unique_slot_value = f"test_add_message_entity_{time.time()}"
|
|
data = {
|
|
"text": unique_text,
|
|
"sender": "user", # must be "user"
|
|
"parse_data": {
|
|
"text": unique_text, # this is what is used for "latest_message"
|
|
"intent": {PREDICTED_CONFIDENCE_KEY: 0.57, INTENT_NAME_KEY: "greet"},
|
|
"entities": [
|
|
{
|
|
ENTITY_ATTRIBUTE_TYPE: "name",
|
|
ENTITY_ATTRIBUTE_VALUE: unique_slot_value,
|
|
}
|
|
],
|
|
},
|
|
}
|
|
_, response = await rasa_app.post(
|
|
f"/conversations/{conversation_id}/messages",
|
|
headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE},
|
|
json=data,
|
|
)
|
|
assert response.json["latest_message"]["text"] == unique_text
|
|
|
|
_, response = await rasa_app.get(f"/conversations/{conversation_id}/tracker")
|
|
updated_events = response.json["events"]
|
|
assert len(updated_events) == previous_num_events + 2
|
|
assert updated_events[-2]["text"] == unique_text
|
|
assert updated_events[-1]["event"] == "slot"
|
|
assert updated_events[-1]["value"] == unique_slot_value
|
|
|
|
|
|
async def test_evaluate_intent(rasa_app: SanicASGITestClient, nlu_data_path: Text):
|
|
nlu_data = rasa.shared.utils.io.read_file(nlu_data_path)
|
|
|
|
_, response = await rasa_app.post(
|
|
"/model/test/intents",
|
|
data=nlu_data,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
assert set(response.json.keys()) == {
|
|
"intent_evaluation",
|
|
"entity_evaluation",
|
|
"response_selection_evaluation",
|
|
}
|
|
|
|
|
|
async def test_evaluate_invalid_intent_model_file(rasa_app: SanicASGITestClient):
|
|
_, response = await rasa_app.post(
|
|
"/model/test/intents?model=invalid.tar.gz",
|
|
json={},
|
|
headers={"Content-type": rasa.server.JSON_CONTENT_TYPE},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR
|
|
|
|
|
|
async def test_evaluate_intent_without_body(rasa_app: SanicASGITestClient):
|
|
_, response = await rasa_app.post(
|
|
"/model/test/intents", headers={"Content-type": rasa.server.YAML_CONTENT_TYPE}
|
|
)
|
|
|
|
assert response.status == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
async def test_evaluate_intent_on_just_nlu_model(
|
|
rasa_app_nlu: SanicASGITestClient, nlu_data_path: Text
|
|
):
|
|
nlu_data = rasa.shared.utils.io.read_file(nlu_data_path)
|
|
|
|
_, response = await rasa_app_nlu.post(
|
|
"/model/test/intents",
|
|
data=nlu_data,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
assert set(response.json.keys()) == {
|
|
"intent_evaluation",
|
|
"entity_evaluation",
|
|
"response_selection_evaluation",
|
|
}
|
|
|
|
|
|
async def test_evaluate_intent_with_model_param(
|
|
rasa_app: SanicASGITestClient, trained_nlu_model: Text, nlu_data_path: Text
|
|
):
|
|
_, response = await rasa_app.get("/status")
|
|
previous_model_file = response.json["model_file"]
|
|
|
|
nlu_data = rasa.shared.utils.io.read_file(nlu_data_path)
|
|
|
|
_, response = await rasa_app.post(
|
|
f"/model/test/intents?model={trained_nlu_model}",
|
|
data=nlu_data,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
assert set(response.json.keys()) == {
|
|
"intent_evaluation",
|
|
"entity_evaluation",
|
|
"response_selection_evaluation",
|
|
}
|
|
|
|
_, response = await rasa_app.get("/status")
|
|
assert previous_model_file == response.json["model_file"]
|
|
|
|
|
|
async def test_evaluate_intent_with_model_server(
|
|
rasa_app: SanicASGITestClient,
|
|
trained_rasa_model: Text,
|
|
nlu_data_path: Text,
|
|
tear_down_scheduler: None,
|
|
):
|
|
production_model_server_url = (
|
|
"https://example.com/webhooks/actions?model=production"
|
|
)
|
|
test_model_server_url = "https://example.com/webhooks/actions?model=test"
|
|
|
|
nlu_data = rasa.shared.utils.io.read_file(nlu_data_path)
|
|
|
|
with aioresponses() as mocked:
|
|
# Mock retrieving the production model from the model server
|
|
mocked.get(
|
|
production_model_server_url,
|
|
body=Path(trained_rasa_model).read_bytes(),
|
|
headers={"ETag": "production", "filename": "prod_model.tar.gz"},
|
|
)
|
|
# Mock retrieving the test model from the model server
|
|
mocked.get(
|
|
test_model_server_url,
|
|
body=Path(trained_rasa_model).read_bytes(),
|
|
headers={"ETag": "test", "filename": "test_model.tar.gz"},
|
|
)
|
|
|
|
agent_with_model_server = await load_agent(
|
|
model_server=EndpointConfig(production_model_server_url)
|
|
)
|
|
rasa_app.sanic_app.ctx.agent = agent_with_model_server
|
|
|
|
_, response = await rasa_app.post(
|
|
f"/model/test/intents?model={test_model_server_url}",
|
|
data=nlu_data,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
assert set(response.json.keys()) == {
|
|
"intent_evaluation",
|
|
"entity_evaluation",
|
|
"response_selection_evaluation",
|
|
}
|
|
|
|
production_model_server = rasa_app.sanic_app.ctx.agent.model_server
|
|
# Assert that the model server URL for the test didn't override the production
|
|
# model server URL
|
|
assert production_model_server.url == production_model_server_url
|
|
# Assert the tests didn't break pulling the models
|
|
assert production_model_server.kwargs.get("wait_time_between_pulls") != 0
|
|
|
|
|
|
async def test_cross_validation(
|
|
rasa_non_trained_app: SanicASGITestClient,
|
|
nlu_data_path: Text,
|
|
stack_config_path: Text,
|
|
):
|
|
nlu_data = Path(nlu_data_path).read_text()
|
|
config = Path(stack_config_path).read_text()
|
|
payload = f"{nlu_data}\n{config}"
|
|
|
|
_, response = await rasa_non_trained_app.post(
|
|
"/model/test/intents",
|
|
data=payload,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
params={"cross_validation_folds": 3},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
response_body = response.json
|
|
for required_key in {
|
|
"intent_evaluation",
|
|
"entity_evaluation",
|
|
"response_selection_evaluation",
|
|
}:
|
|
assert required_key in response_body
|
|
|
|
details = response_body[required_key]
|
|
assert all(
|
|
key in details for key in ["precision", "f1_score", "report", "errors"]
|
|
)
|
|
|
|
|
|
async def test_cross_validation_with_callback_success(
|
|
rasa_non_trained_app: SanicASGITestClient,
|
|
nlu_data_path: Text,
|
|
monkeypatch: MonkeyPatch,
|
|
stack_config_path: Text,
|
|
):
|
|
nlu_data = Path(nlu_data_path).read_text()
|
|
config = Path(stack_config_path).read_text()
|
|
payload = f"{nlu_data}\n{config}"
|
|
|
|
callback_url = "https://example.com/webhooks/actions"
|
|
with aioresponses() as mocked:
|
|
mocked.post(callback_url, payload={})
|
|
|
|
mocked_cross_validation = AsyncMock(
|
|
return_value=(
|
|
CVEvaluationResult({}, {}, {}),
|
|
CVEvaluationResult({}, {}, {}),
|
|
CVEvaluationResult({}, {}, {}),
|
|
)
|
|
)
|
|
monkeypatch.setattr(
|
|
rasa.nlu.test,
|
|
rasa.nlu.test.cross_validate.__name__,
|
|
mocked_cross_validation,
|
|
)
|
|
|
|
_, response = await rasa_non_trained_app.post(
|
|
"/model/test/intents",
|
|
data=payload,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
params={"cross_validation_folds": 3, "callback_url": callback_url},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.NO_CONTENT
|
|
|
|
# Sleep to give event loop time to process things in the background
|
|
await asyncio.sleep(1)
|
|
|
|
mocked_cross_validation.assert_called_once()
|
|
|
|
last_request = latest_request(mocked, "POST", callback_url)
|
|
assert last_request
|
|
|
|
content = last_request[0].kwargs["data"]
|
|
response_body = json.loads(content)
|
|
for required_key in {
|
|
"intent_evaluation",
|
|
"entity_evaluation",
|
|
"response_selection_evaluation",
|
|
}:
|
|
assert required_key in response_body
|
|
|
|
details = response_body[required_key]
|
|
assert all(
|
|
key in details for key in ["precision", "f1_score", "report", "errors"]
|
|
)
|
|
|
|
|
|
@pytest.mark.flaky
|
|
async def test_cross_validation_with_callback_error(
|
|
rasa_non_trained_app: SanicASGITestClient,
|
|
nlu_data_path: Text,
|
|
monkeypatch: MonkeyPatch,
|
|
stack_config_path: Text,
|
|
):
|
|
nlu_data = Path(nlu_data_path).read_text()
|
|
config = Path(stack_config_path).read_text()
|
|
payload = f"{nlu_data}\n{config}"
|
|
|
|
monkeypatch.setattr(
|
|
rasa.nlu.test,
|
|
rasa.nlu.test.cross_validate.__name__,
|
|
Mock(side_effect=ValueError()),
|
|
)
|
|
|
|
callback_url = "https://example.com/webhooks/actions"
|
|
with aioresponses() as mocked:
|
|
mocked.post(callback_url, payload={})
|
|
|
|
_, response = await rasa_non_trained_app.post(
|
|
"/model/test/intents",
|
|
data=payload,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
params={"cross_validation_folds": 3, "callback_url": callback_url},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.NO_CONTENT
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
last_request = latest_request(mocked, "POST", callback_url)
|
|
assert last_request
|
|
|
|
content = last_request[0].kwargs["json"]
|
|
assert content["code"] == HTTPStatus.INTERNAL_SERVER_ERROR
|
|
|
|
|
|
async def test_callback_unexpected_error(
|
|
rasa_non_trained_app: SanicASGITestClient,
|
|
nlu_data_path: Text,
|
|
monkeypatch: MonkeyPatch,
|
|
stack_config_path: Text,
|
|
):
|
|
nlu_data = Path(nlu_data_path).read_text()
|
|
config = Path(stack_config_path).read_text()
|
|
payload = f"{nlu_data}\n{config}"
|
|
|
|
async def raiseUnexpectedError() -> NoReturn:
|
|
raise ValueError()
|
|
|
|
monkeypatch.setattr(
|
|
rasa.server,
|
|
rasa.server._training_payload_from_yaml.__name__,
|
|
Mock(side_effect=ValueError()),
|
|
)
|
|
|
|
callback_url = "https://example.com/webhooks/actions"
|
|
with aioresponses() as mocked:
|
|
mocked.post(callback_url, payload={})
|
|
|
|
_, response = await rasa_non_trained_app.post(
|
|
"/model/test/intents",
|
|
data=payload,
|
|
headers={"Content-type": rasa.server.YAML_CONTENT_TYPE},
|
|
params={"cross_validation_folds": 3, "callback_url": callback_url},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.NO_CONTENT
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
last_request = latest_request(mocked, "POST", callback_url)
|
|
assert last_request
|
|
|
|
content = last_request[0].kwargs["json"]
|
|
assert content["code"] == HTTPStatus.INTERNAL_SERVER_ERROR
|
|
|
|
|
|
async def test_predict(rasa_app: SanicASGITestClient):
|
|
data = [
|
|
{"event": "action", "name": "action_listen"},
|
|
{
|
|
"event": "user",
|
|
"text": "hello",
|
|
"parse_data": {
|
|
"entities": [],
|
|
"intent": {"confidence": 0.57, INTENT_NAME_KEY: "greet"},
|
|
"text": "hello",
|
|
},
|
|
},
|
|
]
|
|
|
|
_, response = await rasa_app.post(
|
|
"/model/predict",
|
|
json=data,
|
|
headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE},
|
|
)
|
|
content = response.json
|
|
assert response.status == HTTPStatus.OK
|
|
assert "scores" in content
|
|
assert "tracker" in content
|
|
assert "policy" in content
|
|
|
|
|
|
async def test_predict_invalid_entities_format(rasa_app: SanicASGITestClient):
|
|
data = [
|
|
{"event": "action", "name": "action_listen"},
|
|
{
|
|
"event": "user",
|
|
"text": "hello",
|
|
"parse_data": {
|
|
"entities": {},
|
|
"intent": {"confidence": 0.57, INTENT_NAME_KEY: "greet"},
|
|
"text": "hello",
|
|
},
|
|
},
|
|
]
|
|
|
|
_, response = await rasa_app.post(
|
|
"/model/predict",
|
|
json=data,
|
|
headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE},
|
|
)
|
|
assert response.status == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
async def test_predict_empty_request_body(rasa_app: SanicASGITestClient):
|
|
_, response = await rasa_app.post(
|
|
"/model/predict", headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE}
|
|
)
|
|
assert response.status == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
async def test_append_events_empty_request_body(rasa_app: SanicASGITestClient):
|
|
_, response = await rasa_app.post(
|
|
"/conversations/testid/tracker/events",
|
|
headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE},
|
|
)
|
|
assert response.status == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
async def test_replace_events_empty_request_body(rasa_app: SanicASGITestClient):
|
|
_, response = await rasa_app.put(
|
|
"/conversations/testid/tracker/events",
|
|
headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE},
|
|
)
|
|
assert response.status == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
@freeze_time("2018-01-01")
|
|
async def test_requesting_non_existent_tracker(rasa_app: SanicASGITestClient):
|
|
model_id = rasa_app.sanic_app.ctx.agent.model_id
|
|
assistant_id = rasa_app.sanic_app.ctx.agent.processor.model_metadata.assistant_id
|
|
_, response = await rasa_app.get("/conversations/madeupid/tracker")
|
|
content = response.json
|
|
assert response.status == HTTPStatus.OK
|
|
assert content["paused"] is False
|
|
assert content["slots"] == {
|
|
"name": None,
|
|
REQUESTED_SLOT: None,
|
|
SESSION_START_METADATA_SLOT: None,
|
|
}
|
|
assert content["sender_id"] == "madeupid"
|
|
assert content["events"] == [
|
|
{
|
|
"event": "action",
|
|
"name": "action_session_start",
|
|
"policy": None,
|
|
"confidence": 1,
|
|
"timestamp": 1514764800,
|
|
"action_text": None,
|
|
"hide_rule_turn": False,
|
|
"metadata": {"assistant_id": assistant_id, "model_id": model_id},
|
|
},
|
|
{
|
|
"event": "session_started",
|
|
"timestamp": 1514764800,
|
|
"metadata": {"assistant_id": assistant_id, "model_id": model_id},
|
|
},
|
|
{
|
|
"event": "action",
|
|
INTENT_NAME_KEY: "action_listen",
|
|
"policy": None,
|
|
"confidence": None,
|
|
"timestamp": 1514764800,
|
|
"action_text": None,
|
|
"hide_rule_turn": False,
|
|
"metadata": {"assistant_id": assistant_id, "model_id": model_id},
|
|
},
|
|
]
|
|
assert content["latest_message"] == {
|
|
"text": None,
|
|
"intent": {},
|
|
"entities": [],
|
|
"message_id": None,
|
|
"metadata": {},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("event", test_events)
|
|
async def test_pushing_event(rasa_app: SanicASGITestClient, event: Event):
|
|
model_id = rasa_app.sanic_app.ctx.agent.model_id
|
|
assistant_id = rasa_app.sanic_app.ctx.agent.processor.model_metadata.assistant_id
|
|
sender_id = str(uuid.uuid1())
|
|
conversation = f"/conversations/{sender_id}"
|
|
|
|
serialized_event = event.as_dict()
|
|
# Remove timestamp so that a new one is assigned on the server
|
|
serialized_event.pop("timestamp")
|
|
|
|
time_before_adding_events = time.time()
|
|
# Wait a bit so that the server-generated timestamp is strictly greater
|
|
# than time_before_adding_events
|
|
time.sleep(0.01)
|
|
_, response = await rasa_app.post(
|
|
f"{conversation}/tracker/events",
|
|
json=serialized_event,
|
|
headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE},
|
|
)
|
|
assert response.json is not None
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
_, tracker_response = await rasa_app.get(f"/conversations/{sender_id}/tracker")
|
|
tracker = tracker_response.json
|
|
assert tracker is not None
|
|
|
|
assert len(tracker.get("events")) == 4
|
|
|
|
deserialized_events = [Event.from_parameters(event) for event in tracker["events"]]
|
|
|
|
# there is an initial session start sequence at the beginning of the tracker
|
|
|
|
assert deserialized_events[:3] == with_assistant_ids(
|
|
with_model_ids(session_start_sequence, model_id), assistant_id
|
|
)
|
|
|
|
assert deserialized_events[3] == with_assistant_id(
|
|
with_model_id(event, model_id), assistant_id
|
|
)
|
|
assert deserialized_events[3].timestamp > time_before_adding_events
|
|
|
|
|
|
async def test_pushing_event_with_existing_model_id(rasa_app: SanicASGITestClient):
|
|
model_id = rasa_app.sanic_app.ctx.agent.model_id
|
|
assistant_id = rasa_app.sanic_app.ctx.agent.processor.model_metadata.assistant_id
|
|
sender_id = str(uuid.uuid1())
|
|
conversation = f"/conversations/{sender_id}"
|
|
|
|
existing_model_id = "some_old_id"
|
|
assert existing_model_id != model_id
|
|
event = with_assistant_id(
|
|
with_model_id(BotUttered("hello!"), existing_model_id), assistant_id
|
|
)
|
|
serialized_event = event.as_dict()
|
|
|
|
# Wait a bit so that the server-generated timestamp is strictly greater
|
|
# than time_before_adding_events
|
|
_, response = await rasa_app.post(
|
|
f"{conversation}/tracker/events",
|
|
json=serialized_event,
|
|
headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE},
|
|
)
|
|
_, tracker_response = await rasa_app.get(f"/conversations/{sender_id}/tracker")
|
|
tracker = tracker_response.json
|
|
|
|
deserialized_events = [Event.from_parameters(event) for event in tracker["events"]]
|
|
|
|
# there is an initial session start sequence at the beginning of the tracker
|
|
received_event = deserialized_events[3]
|
|
assert received_event == with_assistant_id(
|
|
with_model_id(event, existing_model_id), assistant_id
|
|
)
|
|
|
|
|
|
async def test_push_multiple_events(rasa_app: SanicASGITestClient):
|
|
model_id = rasa_app.sanic_app.ctx.agent.model_id
|
|
assistant_id = rasa_app.sanic_app.ctx.agent.processor.model_metadata.assistant_id
|
|
conversation_id = str(uuid.uuid1())
|
|
conversation = f"/conversations/{conversation_id}"
|
|
|
|
events = [e.as_dict() for e in test_events]
|
|
_, response = await rasa_app.post(
|
|
f"{conversation}/tracker/events",
|
|
json=events,
|
|
headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE},
|
|
)
|
|
assert response.json is not None
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
_, tracker_response = await rasa_app.get(
|
|
f"/conversations/{conversation_id}/tracker"
|
|
)
|
|
tracker = tracker_response.json
|
|
assert tracker is not None
|
|
|
|
# there is an initial session start sequence at the beginning
|
|
assert [
|
|
Event.from_parameters(event) for event in tracker.get("events")
|
|
] == with_assistant_ids(
|
|
with_model_ids(session_start_sequence + test_events, model_id), assistant_id
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"params", ["?execute_side_effects=true&output_channel=callback", ""]
|
|
)
|
|
async def test_pushing_event_while_executing_side_effects(
|
|
rasa_server: Sanic, params: Text
|
|
):
|
|
input_channel = CallbackInput(EndpointConfig("https://example.com/callback"))
|
|
channel.register([input_channel], rasa_server, "/webhooks/")
|
|
rasa_app = rasa_server.asgi_client
|
|
sender_id = str(uuid.uuid1())
|
|
conversation = f"/conversations/{sender_id}"
|
|
|
|
serialized_event = test_events[1].as_dict()
|
|
|
|
with aioresponses() as mocked:
|
|
mocked.post(
|
|
"https://example.com/callback",
|
|
repeat=True,
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
await rasa_app.post(
|
|
f"{conversation}/tracker/events{params}",
|
|
json=serialized_event,
|
|
headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE},
|
|
)
|
|
|
|
r = latest_request(mocked, "post", "https://example.com/callback")
|
|
|
|
if not params:
|
|
assert r is None
|
|
else:
|
|
message_received = json_of_latest_request(r)
|
|
assert message_received.get("recipient_id") == sender_id
|
|
assert message_received.get("text") == serialized_event.get("text")
|
|
|
|
|
|
async def test_post_conversation_id_with_slash(rasa_app: SanicASGITestClient):
|
|
model_id = rasa_app.sanic_app.ctx.agent.model_id
|
|
assistant_id = rasa_app.sanic_app.ctx.agent.processor.model_metadata.assistant_id
|
|
conversation_id = str(uuid.uuid1())
|
|
id_len = len(conversation_id) // 2
|
|
conversation_id = conversation_id[:id_len] + "/+-_\\=" + conversation_id[id_len:]
|
|
conversation = f"/conversations/{conversation_id}"
|
|
|
|
events = [e.as_dict() for e in test_events]
|
|
_, response = await rasa_app.post(
|
|
f"{conversation}/tracker/events",
|
|
json=events,
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
assert response.json is not None
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
_, tracker_response = await rasa_app.get(
|
|
f"/conversations/{conversation_id}/tracker"
|
|
)
|
|
tracker = tracker_response.json
|
|
assert tracker is not None
|
|
|
|
# there is a session start sequence at the start
|
|
assert [
|
|
Event.from_parameters(event) for event in tracker.get("events")
|
|
] == with_assistant_ids(
|
|
with_model_ids(session_start_sequence + test_events, model_id), assistant_id
|
|
)
|
|
|
|
|
|
async def test_put_tracker(rasa_app: SanicASGITestClient):
|
|
data = [event.as_dict() for event in test_events]
|
|
_, response = await rasa_app.put(
|
|
"/conversations/pushtracker/tracker/events",
|
|
json=data,
|
|
headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE},
|
|
)
|
|
content = response.json
|
|
assert response.status == HTTPStatus.OK
|
|
assert len(content["events"]) == len(test_events)
|
|
assert content["sender_id"] == "pushtracker"
|
|
|
|
_, tracker_response = await rasa_app.get("/conversations/pushtracker/tracker")
|
|
tracker = tracker_response.json
|
|
assert tracker is not None
|
|
evts = tracker.get("events")
|
|
assert events.deserialise_events(evts) == test_events
|
|
|
|
|
|
async def test_predict_without_conversation_id(rasa_app: SanicASGITestClient):
|
|
_, response = await rasa_app.post("/conversations/non_existent_id/predict")
|
|
|
|
assert response.status == HTTPStatus.NOT_FOUND
|
|
assert response.json["message"] == "Conversation ID not found."
|
|
|
|
|
|
async def test_sorted_predict(rasa_app: SanicASGITestClient):
|
|
await _create_tracker_for_sender(rasa_app, "sortedpredict")
|
|
|
|
_, response = await rasa_app.post("/conversations/sortedpredict/predict")
|
|
scores = response.json["scores"]
|
|
sorted_scores = sorted(scores, key=lambda k: (-k["score"], k["action"]))
|
|
assert scores == sorted_scores
|
|
|
|
|
|
async def _create_tracker_for_sender(app: SanicASGITestClient, sender_id: Text) -> None:
|
|
data = [event.as_dict() for event in test_events[:3]]
|
|
_, response = await app.put(
|
|
f"/conversations/{sender_id}/tracker/events",
|
|
json=data,
|
|
headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE},
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
|
|
async def test_get_tracker_with_jwt(rasa_secured_app: SanicASGITestClient):
|
|
# token generated with secret "core" and algorithm HS256
|
|
# on https://jwt.io/
|
|
|
|
# {"user": {"username": "testadmin", "role": "admin"}}
|
|
jwt_header = {
|
|
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
|
|
"eyJ1c2VyIjp7InVzZXJuYW1lIjoidGVzdGFkbWluIiwic"
|
|
"m9sZSI6ImFkbWluIn19.NAQr0kbtSrY7d28XTqRzawq2u"
|
|
"QRre7IWTuIDrCn5AIw"
|
|
}
|
|
_, response = await rasa_secured_app.get(
|
|
"/conversations/testadmin/tracker", headers=jwt_header
|
|
)
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
_, response = await rasa_secured_app.get(
|
|
"/conversations/testuser/tracker", headers=jwt_header
|
|
)
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
# {"user": {"username": "testuser", "role": "user"}}
|
|
jwt_header = {
|
|
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
|
|
"eyJ1c2VyIjp7InVzZXJuYW1lIjoidGVzdHVzZXIiLCJyb"
|
|
"2xlIjoidXNlciJ9fQ.JnMTLYd56qut2w9h7hRQlDm1n3l"
|
|
"HJHOxxC_w7TtwCrs"
|
|
}
|
|
_, response = await rasa_secured_app.get(
|
|
"/conversations/testadmin/tracker", headers=jwt_header
|
|
)
|
|
assert response.status == HTTPStatus.FORBIDDEN
|
|
|
|
_, response = await rasa_secured_app.get(
|
|
"/conversations/testuser/tracker", headers=jwt_header
|
|
)
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
|
|
async def test_get_tracker_with_asymmetric_jwt(
|
|
rasa_secured_app_asymmetric: SanicASGITestClient,
|
|
encoded_jwt: Text,
|
|
) -> None:
|
|
jwt_header = {"Authorization": f"Bearer {encoded_jwt}"}
|
|
_, response = await rasa_secured_app_asymmetric.get(
|
|
"/conversations/myuser/tracker", headers=jwt_header
|
|
)
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
_, response = await rasa_secured_app_asymmetric.get(
|
|
"/conversations/testuser/tracker", headers=jwt_header
|
|
)
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
|
|
def test_list_routes(empty_agent: Agent):
|
|
app = rasa.server.create_app(empty_agent, auth_token=None)
|
|
|
|
routes = utils.list_routes(app)
|
|
assert set(routes.keys()) == {
|
|
"hello",
|
|
"version",
|
|
"status",
|
|
"retrieve_tracker",
|
|
"append_events",
|
|
"replace_events",
|
|
"retrieve_story",
|
|
"execute_action",
|
|
"trigger_intent",
|
|
"predict",
|
|
"add_message",
|
|
"train",
|
|
"evaluate_stories",
|
|
"evaluate_intents",
|
|
"tracker_predict",
|
|
"parse",
|
|
"load_model",
|
|
"unload_model",
|
|
"get_domain",
|
|
}
|
|
|
|
|
|
async def test_unload_model_error(rasa_app: SanicASGITestClient):
|
|
_, response = await rasa_app.get("/status")
|
|
assert response.status == HTTPStatus.OK
|
|
assert "model_file" in response.json and response.json["model_file"] is not None
|
|
|
|
_, response = await rasa_app.delete("/model")
|
|
assert response.status == HTTPStatus.NO_CONTENT
|
|
|
|
|
|
async def test_get_domain(rasa_app: SanicASGITestClient, domain_path: Text):
|
|
_, response = await rasa_app.get(
|
|
"/domain", headers={"accept": rasa.server.JSON_CONTENT_TYPE}
|
|
)
|
|
|
|
content = response.json
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
# assert only keys in `domain_path` fixture
|
|
original_domain_dict = Domain.load(domain_path).as_dict()
|
|
for key in original_domain_dict.keys():
|
|
assert key in content
|
|
|
|
|
|
async def test_get_domain_invalid_accept_header(rasa_app: SanicASGITestClient):
|
|
_, response = await rasa_app.get("/domain")
|
|
|
|
assert response.status == HTTPStatus.NOT_ACCEPTABLE
|
|
|
|
|
|
async def test_load_model(rasa_app: SanicASGITestClient, trained_core_model: Text):
|
|
_, response = await rasa_app.get("/status")
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
assert "model_id" in response.json
|
|
|
|
old_model_id = response.json["model_id"]
|
|
|
|
data = {"model_file": trained_core_model}
|
|
_, response = await rasa_app.put("/model", json=data)
|
|
|
|
assert response.status == HTTPStatus.NO_CONTENT
|
|
|
|
_, response = await rasa_app.get("/status")
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
assert "model_id" in response.json
|
|
|
|
assert old_model_id != response.json["model_id"]
|
|
|
|
|
|
async def test_load_model_from_model_server(
|
|
rasa_app: SanicASGITestClient, trained_core_model: Text, tear_down_scheduler: None
|
|
):
|
|
_, response = await rasa_app.get("/status")
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
assert "model_id" in response.json
|
|
|
|
old_model_id = response.json["model_id"]
|
|
|
|
endpoint = EndpointConfig("https://example.com/model/trained_core_model")
|
|
with open(trained_core_model, "rb") as f:
|
|
with aioresponses(passthrough=["http://127.0.0.1"]) as mocked:
|
|
headers = {}
|
|
fs = os.fstat(f.fileno())
|
|
headers["Content-Length"] = str(fs[6])
|
|
mocked.get(
|
|
"https://example.com/model/trained_core_model",
|
|
content_type="application/x-tar",
|
|
headers={
|
|
"filename": "some_model_name.tar.gz",
|
|
"ETag": "new_fingerprint",
|
|
},
|
|
body=f.read(),
|
|
)
|
|
data = {"model_server": {"url": endpoint.url}}
|
|
_, response = await rasa_app.put("/model", json=data)
|
|
|
|
assert response.status == HTTPStatus.NO_CONTENT
|
|
|
|
_, response = await rasa_app.get("/status")
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
assert "model_id" in response.json
|
|
|
|
assert old_model_id != response.json["model_id"]
|
|
|
|
|
|
async def test_load_model_invalid_request_body(
|
|
rasa_non_trained_app: SanicASGITestClient,
|
|
):
|
|
_, response = await rasa_non_trained_app.put("/model")
|
|
|
|
assert response.status == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
async def test_load_model_invalid_configuration(
|
|
rasa_non_trained_app: SanicASGITestClient,
|
|
):
|
|
data = {"model_file": "some-random-path"}
|
|
_, response = await rasa_non_trained_app.put("/model", json=data)
|
|
|
|
assert response.status == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
async def test_execute(rasa_app: SanicASGITestClient):
|
|
await _create_tracker_for_sender(rasa_app, "test_execute")
|
|
|
|
data = {INTENT_NAME_KEY: "utter_greet"}
|
|
_, response = await rasa_app.post("/conversations/test_execute/execute", json=data)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
parsed_content = response.json
|
|
assert parsed_content["tracker"]
|
|
assert parsed_content["messages"]
|
|
|
|
|
|
async def test_execute_without_conversation_id(rasa_app: SanicASGITestClient):
|
|
data = {INTENT_NAME_KEY: "utter_greet"}
|
|
_, response = await rasa_app.post(
|
|
"/conversations/non_existent_id/execute", json=data
|
|
)
|
|
|
|
assert response.status == HTTPStatus.NOT_FOUND
|
|
assert response.json["message"] == "Conversation ID not found."
|
|
|
|
|
|
async def test_execute_with_missing_action_name(rasa_app: SanicASGITestClient):
|
|
test_sender = "test_execute_with_missing_action_name"
|
|
await _create_tracker_for_sender(rasa_app, test_sender)
|
|
|
|
data = {"wrong-key": "utter_greet"}
|
|
_, response = await rasa_app.post(
|
|
f"/conversations/{test_sender}/execute", json=data
|
|
)
|
|
|
|
assert response.status == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
async def test_execute_with_not_existing_action(rasa_app: SanicASGITestClient):
|
|
test_sender = "test_execute_with_not_existing_action"
|
|
await _create_tracker_for_sender(rasa_app, test_sender)
|
|
|
|
data = {"name": "ka[pa[opi[opj[oj[oija"}
|
|
_, response = await rasa_app.post(
|
|
f"/conversations/{test_sender}/execute", json=data
|
|
)
|
|
|
|
assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR
|
|
|
|
|
|
async def test_trigger_intent(rasa_app: SanicASGITestClient):
|
|
data = {INTENT_NAME_KEY: "greet"}
|
|
_, response = await rasa_app.post(
|
|
"/conversations/test_trigger/trigger_intent", json=data
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
parsed_content = response.json
|
|
assert parsed_content["tracker"]
|
|
assert parsed_content["messages"]
|
|
|
|
|
|
async def test_trigger_intent_with_entity(rasa_app: SanicASGITestClient):
|
|
entity_name = "name"
|
|
entity_value = "Sara"
|
|
data = {INTENT_NAME_KEY: "greet", "entities": {entity_name: entity_value}}
|
|
_, response = await rasa_app.post(
|
|
"/conversations/test_trigger/trigger_intent", json=data
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
parsed_content = response.json
|
|
last_slot_set_event = [
|
|
event
|
|
for event in parsed_content["tracker"]["events"]
|
|
if event["event"] == "slot"
|
|
][-1]
|
|
|
|
assert parsed_content["tracker"]
|
|
assert parsed_content["messages"]
|
|
assert last_slot_set_event["name"] == entity_name
|
|
assert last_slot_set_event["value"] == entity_value
|
|
|
|
|
|
async def test_trigger_intent_with_missing_intent_name(rasa_app: SanicASGITestClient):
|
|
test_sender = "test_trigger_intent_with_missing_action_name"
|
|
|
|
data = {"wrong-key": "greet"}
|
|
_, response = await rasa_app.post(
|
|
f"/conversations/{test_sender}/trigger_intent", json=data
|
|
)
|
|
|
|
assert response.status == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
async def test_trigger_intent_with_not_existing_intent(rasa_app: SanicASGITestClient):
|
|
test_sender = "test_trigger_intent_with_not_existing_intent"
|
|
await _create_tracker_for_sender(rasa_app, test_sender)
|
|
|
|
data = {INTENT_NAME_KEY: "ka[pa[opi[opj[oj[oija"}
|
|
_, response = await rasa_app.post(
|
|
f"/conversations/{test_sender}/trigger_intent", json=data
|
|
)
|
|
|
|
assert response.status == HTTPStatus.NOT_FOUND
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"input_channels, output_channel_to_use, expected_channel",
|
|
[
|
|
(None, "slack", CollectingOutputChannel),
|
|
([], None, CollectingOutputChannel),
|
|
([RestInput()], "slack", CollectingOutputChannel),
|
|
([RestInput()], "rest", CollectingOutputChannel),
|
|
(
|
|
[RestInput(), SlackInput("test", slack_signing_secret="foobar")],
|
|
"slack",
|
|
SlackBot,
|
|
),
|
|
],
|
|
)
|
|
def test_get_output_channel(
|
|
input_channels: List[Text], output_channel_to_use: Text, expected_channel: Type
|
|
):
|
|
request = MagicMock()
|
|
app = MagicMock(ctx=Namespace())
|
|
app.ctx.input_channels = input_channels
|
|
request.app = app
|
|
request.args = {"output_channel": output_channel_to_use}
|
|
|
|
actual = rasa.server._get_output_channel(request, None)
|
|
|
|
assert isinstance(actual, expected_channel)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"input_channels, expected_channel",
|
|
[
|
|
([], CollectingOutputChannel),
|
|
([RestInput()], CollectingOutputChannel),
|
|
([RestInput(), SlackInput("test", slack_signing_secret="foobar")], SlackBot),
|
|
],
|
|
)
|
|
def test_get_latest_output_channel(input_channels: List[Text], expected_channel: Type):
|
|
request = MagicMock()
|
|
app = MagicMock(ctx=Namespace())
|
|
app.ctx.input_channels = input_channels
|
|
request.app = app
|
|
request.args = {"output_channel": "latest"}
|
|
|
|
tracker = DialogueStateTracker.from_events(
|
|
"default", [UserUttered("text", input_channel="slack")]
|
|
)
|
|
|
|
actual = rasa.server._get_output_channel(request, tracker)
|
|
|
|
assert isinstance(actual, expected_channel)
|
|
|
|
|
|
def test_app_when_app_has_no_input_channels():
|
|
request = MagicMock()
|
|
|
|
class NoInputChannels:
|
|
ctx = Namespace()
|
|
pass
|
|
|
|
request.app = NoInputChannels()
|
|
|
|
actual = rasa.server._get_output_channel(
|
|
request, DialogueStateTracker.from_events("default", [])
|
|
)
|
|
assert isinstance(actual, CollectingOutputChannel)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"conversation_events,until_time,fetch_all_sessions,expected",
|
|
# conversation with one session
|
|
[
|
|
(
|
|
[
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
UserUttered("hi", {"name": "greet"}),
|
|
ActionExecuted("utter_greet"),
|
|
],
|
|
None,
|
|
True,
|
|
f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
|
|
stories:
|
|
- story: some-conversation-ID
|
|
steps:
|
|
- intent: greet
|
|
user: |-
|
|
hi
|
|
- action: utter_greet""",
|
|
),
|
|
# conversation with multiple sessions
|
|
(
|
|
[
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
UserUttered("hi", {"name": "greet"}),
|
|
ActionExecuted("utter_greet"),
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
UserUttered("bye bye", {"name": "goodbye"}),
|
|
ActionExecuted("utter_goodbye"),
|
|
],
|
|
None,
|
|
True,
|
|
f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
|
|
stories:
|
|
- story: some-conversation-ID, story 1
|
|
steps:
|
|
- intent: greet
|
|
user: |-
|
|
hi
|
|
- action: utter_greet
|
|
- story: some-conversation-ID, story 2
|
|
steps:
|
|
- intent: goodbye
|
|
user: |-
|
|
bye bye
|
|
- action: utter_goodbye""",
|
|
),
|
|
# conversation with multiple sessions, but setting `all_sessions=false`
|
|
# means only the last one is returned
|
|
(
|
|
[
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
UserUttered("hi", {"name": "greet"}),
|
|
ActionExecuted("utter_greet"),
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
UserUttered("bye bye", {"name": "goodbye"}),
|
|
ActionExecuted("utter_goodbye"),
|
|
],
|
|
None,
|
|
False,
|
|
f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
|
|
stories:
|
|
- story: some-conversation-ID
|
|
steps:
|
|
- intent: goodbye
|
|
user: |-
|
|
bye bye
|
|
- action: utter_goodbye""",
|
|
),
|
|
# the default for `all_sessions` is `false` - this test checks that
|
|
# only the latest session is returned in that case
|
|
(
|
|
[
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
UserUttered("hi", {"name": "greet"}),
|
|
ActionExecuted("utter_greet"),
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
UserUttered("bye bye", {"name": "goodbye"}),
|
|
ActionExecuted("utter_goodbye"),
|
|
],
|
|
None,
|
|
None,
|
|
f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
|
|
stories:
|
|
- story: some-conversation-ID
|
|
steps:
|
|
- intent: goodbye
|
|
user: |-
|
|
bye bye
|
|
- action: utter_goodbye""",
|
|
),
|
|
# `until` parameter means only the first session is returned
|
|
(
|
|
[
|
|
ActionExecuted(ACTION_SESSION_START_NAME, timestamp=1),
|
|
SessionStarted(timestamp=2),
|
|
UserUttered("hi", {"name": "greet"}, timestamp=3),
|
|
ActionExecuted("utter_greet", timestamp=4),
|
|
ActionExecuted(ACTION_SESSION_START_NAME, timestamp=5),
|
|
SessionStarted(timestamp=6),
|
|
UserUttered("bye bye", {"name": "goodbye"}, timestamp=7),
|
|
ActionExecuted("utter_goodbye", timestamp=8),
|
|
],
|
|
4,
|
|
True,
|
|
f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
|
|
stories:
|
|
- story: some-conversation-ID
|
|
steps:
|
|
- intent: greet
|
|
user: |-
|
|
hi
|
|
- action: utter_greet""",
|
|
),
|
|
# empty conversation
|
|
([], None, True, f'version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"'),
|
|
# Conversation with slot
|
|
(
|
|
[
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
UserUttered("hi", {"name": "greet"}),
|
|
ActionExecuted("utter_greet"),
|
|
SlotSet(REQUESTED_SLOT, "some value"),
|
|
],
|
|
None,
|
|
True,
|
|
f"""version: "{rasa.shared.constants.LATEST_TRAINING_DATA_FORMAT_VERSION}"
|
|
stories:
|
|
- story: some-conversation-ID
|
|
steps:
|
|
- intent: greet
|
|
user: |-
|
|
hi
|
|
- action: utter_greet
|
|
- slot_was_set:
|
|
- requested_slot: some value""",
|
|
),
|
|
],
|
|
)
|
|
async def test_get_story(
|
|
rasa_app: SanicASGITestClient,
|
|
monkeypatch: MonkeyPatch,
|
|
conversation_events: List[Event],
|
|
until_time: Optional[float],
|
|
fetch_all_sessions: Optional[bool],
|
|
expected: Text,
|
|
):
|
|
conversation_id = "some-conversation-ID"
|
|
|
|
tracker_store = InMemoryTrackerStore(Domain.empty())
|
|
tracker = DialogueStateTracker.from_events(conversation_id, conversation_events)
|
|
|
|
await tracker_store.save(tracker)
|
|
|
|
monkeypatch.setattr(rasa_app.sanic_app.ctx.agent, "tracker_store", tracker_store)
|
|
monkeypatch.setattr(
|
|
rasa_app.sanic_app.ctx.agent.processor, "tracker_store", tracker_store
|
|
)
|
|
|
|
url = f"/conversations/{conversation_id}/story?"
|
|
|
|
query = {}
|
|
|
|
if fetch_all_sessions is not None:
|
|
query["all_sessions"] = fetch_all_sessions
|
|
|
|
if until_time is not None:
|
|
query["until"] = until_time
|
|
|
|
_, response = await rasa_app.get(url + urllib.parse.urlencode(query))
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
assert response.content.decode().strip() == expected
|
|
|
|
|
|
async def test_get_story_without_conversation_id(
|
|
rasa_app: SanicASGITestClient, monkeypatch: MonkeyPatch
|
|
):
|
|
conversation_id = "some-conversation-ID"
|
|
url = f"/conversations/{conversation_id}/story"
|
|
|
|
_, response = await rasa_app.get(url)
|
|
|
|
assert response.status == HTTPStatus.NOT_FOUND
|
|
assert response.json["message"] == "Conversation ID not found."
|
|
|
|
|
|
async def test_get_story_does_not_update_conversation_session(
|
|
rasa_app: SanicASGITestClient, monkeypatch: MonkeyPatch
|
|
):
|
|
conversation_id = "some-conversation-ID"
|
|
|
|
# domain with short session expiration time of one second
|
|
domain = Domain.empty()
|
|
domain.session_config = SessionConfig(
|
|
session_expiration_time=1 / 60, carry_over_slots=True
|
|
)
|
|
|
|
monkeypatch.setattr(rasa_app.sanic_app.ctx.agent.processor, "domain", domain)
|
|
|
|
# conversation contains one session that has expired
|
|
now = time.time()
|
|
conversation_events = [
|
|
ActionExecuted(ACTION_SESSION_START_NAME, timestamp=now - 10),
|
|
SessionStarted(timestamp=now - 9),
|
|
UserUttered("hi", {"name": "greet"}, timestamp=now - 8),
|
|
ActionExecuted("utter_greet", timestamp=now - 7),
|
|
]
|
|
|
|
tracker = DialogueStateTracker.from_events(conversation_id, conversation_events)
|
|
|
|
# the conversation session has expired
|
|
assert rasa_app.sanic_app.ctx.agent.processor._has_session_expired(tracker)
|
|
|
|
tracker_store = InMemoryTrackerStore(domain)
|
|
|
|
await tracker_store.save(tracker)
|
|
|
|
monkeypatch.setattr(rasa_app.sanic_app.ctx.agent, "tracker_store", tracker_store)
|
|
monkeypatch.setattr(
|
|
rasa_app.sanic_app.ctx.agent.processor, "tracker_store", tracker_store
|
|
)
|
|
|
|
_, response = await rasa_app.get(f"/conversations/{conversation_id}/story")
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
# expected story is returned
|
|
assert (
|
|
response.content.decode().strip()
|
|
== f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
|
|
stories:
|
|
- story: some-conversation-ID
|
|
steps:
|
|
- intent: greet
|
|
user: |-
|
|
hi
|
|
- action: utter_greet"""
|
|
)
|
|
|
|
# the tracker has the same number of events as were initially added
|
|
assert len(tracker.events) == len(conversation_events)
|
|
|
|
# the last event is still the same as before
|
|
assert tracker.events[-1].timestamp == conversation_events[-1].timestamp
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"initial_tracker_events,events_to_append,expected_events",
|
|
[
|
|
(
|
|
# the tracker is initially empty, and no events are appended
|
|
# so we'll just expect the session start sequence with an `action_listen`
|
|
[],
|
|
[],
|
|
[
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
ActionExecuted(ACTION_LISTEN_NAME),
|
|
],
|
|
),
|
|
(
|
|
# the tracker is initially empty, and a user utterance is appended
|
|
# we expect a tracker with a session start sequence and a user utterance
|
|
[],
|
|
[UserUttered("/greet", {"name": "greet", "confidence": 1.0})],
|
|
[
|
|
ActionExecuted(ACTION_SESSION_START_NAME),
|
|
SessionStarted(),
|
|
ActionExecuted(ACTION_LISTEN_NAME),
|
|
UserUttered("/greet", {"name": "greet", "confidence": 1.0}),
|
|
],
|
|
),
|
|
(
|
|
# the tracker is initially empty, and a session start sequence is appended
|
|
# we'll just expect the session start sequence
|
|
[],
|
|
[ActionExecuted(ACTION_SESSION_START_NAME), SessionStarted()],
|
|
[ActionExecuted(ACTION_SESSION_START_NAME), SessionStarted()],
|
|
),
|
|
(
|
|
# the tracker already contains some events - we can simply append events
|
|
[
|
|
ActionExecuted(ACTION_LISTEN_NAME),
|
|
UserUttered("/greet", {"name": "greet", "confidence": 1.0}),
|
|
],
|
|
[ActionExecuted("utter_greet")],
|
|
[
|
|
ActionExecuted(ACTION_LISTEN_NAME),
|
|
UserUttered("/greet", {"name": "greet", "confidence": 1.0}),
|
|
ActionExecuted("utter_greet"),
|
|
],
|
|
),
|
|
],
|
|
)
|
|
async def test_update_conversation_with_events(
|
|
rasa_app: SanicASGITestClient,
|
|
monkeypatch: MonkeyPatch,
|
|
initial_tracker_events: List[Event],
|
|
events_to_append: List[Event],
|
|
expected_events: List[Event],
|
|
):
|
|
conversation_id = "some-conversation-ID"
|
|
agent = rasa_app.sanic_app.ctx.agent
|
|
tracker_store = agent.tracker_store
|
|
domain = agent.domain
|
|
model_id = agent.model_id
|
|
assistant_id = agent.processor.model_metadata.assistant_id
|
|
|
|
if initial_tracker_events:
|
|
tracker = await agent.processor.get_tracker(conversation_id)
|
|
tracker.update_with_events(initial_tracker_events, domain)
|
|
await tracker_store.save(tracker)
|
|
|
|
fetched_tracker = await rasa.server.update_conversation_with_events(
|
|
conversation_id, agent.processor, domain, events_to_append
|
|
)
|
|
assert list(fetched_tracker.events) == with_assistant_ids(
|
|
with_model_ids(expected_events, model_id), assistant_id
|
|
)
|
|
|
|
|
|
async def test_append_events_does_not_repeat_session_start(
|
|
rasa_app: SanicASGITestClient,
|
|
):
|
|
session_start_events = [
|
|
{
|
|
"event": "action",
|
|
"timestamp": 1644577572.9639301,
|
|
"metadata": {
|
|
"assistant_id": "unique_stack_assistant_test_name",
|
|
"model_id": "f90a69066e4a438aa6edfbed5b529919",
|
|
},
|
|
"name": "action_session_start",
|
|
"policy": None,
|
|
"confidence": 1.0,
|
|
"action_text": None,
|
|
"hide_rule_turn": False,
|
|
},
|
|
{
|
|
"event": "session_started",
|
|
"timestamp": 1644577572.963996,
|
|
"metadata": {
|
|
"assistant_id": "unique_stack_assistant_test_name",
|
|
"model_id": "f90a69066e4a438aa6edfbed5b529919",
|
|
},
|
|
},
|
|
{
|
|
"event": "action",
|
|
"timestamp": 1644577572.964009,
|
|
"metadata": {
|
|
"assistant_id": "unique_stack_assistant_test_name",
|
|
"model_id": "f90a69066e4a438aa6edfbed5b529919",
|
|
},
|
|
"name": "action_listen",
|
|
"policy": None,
|
|
"confidence": None,
|
|
"action_text": None,
|
|
"hide_rule_turn": False,
|
|
},
|
|
]
|
|
_, response = await rasa_app.post(
|
|
"/conversations/testid/tracker/events", json=session_start_events
|
|
)
|
|
|
|
assert response.json["events"] == session_start_events
|
|
|
|
|
|
async def _create_tracker_for_query_params(
|
|
rasa_app: SanicASGITestClient,
|
|
model_id: Text,
|
|
) -> Tuple[Text, List[Event]]:
|
|
sender_id = uuid.uuid4().hex
|
|
|
|
events_to_store: List[Event] = (
|
|
session_start_sequence
|
|
+ [
|
|
UserUttered(
|
|
"hi",
|
|
parse_data={
|
|
"intent": {"name": "greet"},
|
|
"metadata": {"model_id": model_id},
|
|
},
|
|
),
|
|
ActionExecuted("utter_greet"),
|
|
BotUttered("hey there"),
|
|
ActionExecuted(ACTION_LISTEN_NAME),
|
|
UserUttered(
|
|
"/restart",
|
|
parse_data={
|
|
"intent": {"name": "restart"},
|
|
"metadata": {"model_id": model_id},
|
|
},
|
|
),
|
|
ActionExecuted(ACTION_RESTART_NAME),
|
|
Restarted(),
|
|
]
|
|
+ session_start_sequence
|
|
+ [
|
|
UserUttered(
|
|
"hi again",
|
|
parse_data={
|
|
"intent": {"name": "greet"},
|
|
"metadata": {"model_id": model_id},
|
|
},
|
|
),
|
|
]
|
|
)
|
|
serialized_events_to_store = [event.as_dict() for event in events_to_store]
|
|
|
|
_, response = await rasa_app.post(
|
|
f"/conversations/{sender_id}/tracker/events", json=serialized_events_to_store
|
|
)
|
|
assert response.status == 200
|
|
|
|
return sender_id, events_to_store
|
|
|
|
|
|
async def test_get_tracker_with_query_param_include_events_all(
|
|
rasa_app: SanicASGITestClient,
|
|
) -> None:
|
|
model_id = rasa_app.sanic_app.ctx.agent.model_id
|
|
assistant_id = rasa_app.sanic_app.ctx.agent.processor.model_metadata.assistant_id
|
|
sender_id, events_to_store = await _create_tracker_for_query_params(
|
|
rasa_app, model_id
|
|
)
|
|
|
|
_, response = await rasa_app.get(
|
|
f"/conversations/{sender_id}/tracker?include_events=ALL"
|
|
)
|
|
assert response.status == 200
|
|
|
|
tracker = response.json
|
|
assert tracker["sender_id"] == sender_id
|
|
|
|
serialized_actual_events = tracker["events"]
|
|
|
|
expected_events = with_assistant_ids(
|
|
with_model_ids(events_to_store, model_id), assistant_id
|
|
)
|
|
serialized_expected_events = [event.as_dict() for event in expected_events]
|
|
|
|
assert serialized_actual_events == serialized_expected_events
|
|
|
|
|
|
async def test_get_tracker_with_query_param_include_events_after_restart(
|
|
rasa_app: SanicASGITestClient,
|
|
) -> None:
|
|
model_id = rasa_app.sanic_app.ctx.agent.model_id
|
|
assistant_id = rasa_app.sanic_app.ctx.agent.processor.model_metadata.assistant_id
|
|
sender_id, events_to_store = await _create_tracker_for_query_params(
|
|
rasa_app, model_id
|
|
)
|
|
|
|
_, response = await rasa_app.get(
|
|
f"/conversations/{sender_id}/tracker?include_events=AFTER_RESTART"
|
|
)
|
|
assert response.status == 200
|
|
|
|
tracker = response.json
|
|
assert tracker["sender_id"] == sender_id
|
|
|
|
serialized_actual_events = tracker["events"]
|
|
|
|
restarted_event = [
|
|
event for event in events_to_store if isinstance(event, Restarted)
|
|
][0]
|
|
truncated_events = events_to_store[events_to_store.index(restarted_event) + 1 :]
|
|
expected_events = with_assistant_ids(
|
|
with_model_ids(truncated_events, model_id), assistant_id
|
|
)
|
|
serialized_expected_events = [e.as_dict() for e in expected_events]
|
|
|
|
assert serialized_actual_events == serialized_expected_events
|
|
|
|
|
|
async def test_get_tracker_with_query_param_include_events_applied(
|
|
rasa_app: SanicASGITestClient,
|
|
) -> None:
|
|
model_id = rasa_app.sanic_app.ctx.agent.model_id
|
|
assistant_id = rasa_app.sanic_app.ctx.agent.processor.model_metadata.assistant_id
|
|
sender_id, events_to_store = await _create_tracker_for_query_params(
|
|
rasa_app, model_id
|
|
)
|
|
|
|
_, response = await rasa_app.get(
|
|
f"/conversations/{sender_id}/tracker?include_events=APPLIED"
|
|
)
|
|
assert response.status == 200
|
|
|
|
tracker = response.json
|
|
assert tracker["sender_id"] == sender_id
|
|
|
|
serialized_actual_events = tracker["events"]
|
|
|
|
restarted_event = [
|
|
event for event in events_to_store if isinstance(event, Restarted)
|
|
][0]
|
|
truncated_events = events_to_store[events_to_store.index(restarted_event) + 1 :]
|
|
session_started = [
|
|
event for event in truncated_events if isinstance(event, SessionStarted)
|
|
][0]
|
|
truncated_events = truncated_events[truncated_events.index(session_started) + 1 :]
|
|
|
|
expected_events = with_assistant_ids(
|
|
with_model_ids(truncated_events, model_id), assistant_id
|
|
)
|
|
serialized_expected_events = [e.as_dict() for e in expected_events]
|
|
|
|
assert serialized_actual_events == serialized_expected_events
|
|
|
|
|
|
async def test_get_tracker_with_query_param_include_events_none(
|
|
rasa_app: SanicASGITestClient,
|
|
) -> None:
|
|
model_id = rasa_app.sanic_app.ctx.agent.model_id
|
|
sender_id, events_to_store = await _create_tracker_for_query_params(
|
|
rasa_app, model_id
|
|
)
|
|
|
|
_, response = await rasa_app.get(
|
|
f"/conversations/{sender_id}/tracker?include_events=NONE"
|
|
)
|
|
assert response.status == 200
|
|
|
|
tracker = response.json
|
|
assert tracker["sender_id"] == sender_id
|
|
|
|
serialized_actual_events = tracker["events"]
|
|
assert serialized_actual_events is None
|
|
|
|
|
|
async def test_retrieve_story_with_query_param_all_sessions_true(
|
|
rasa_app: SanicASGITestClient,
|
|
) -> None:
|
|
model_id = rasa_app.sanic_app.ctx.agent.model_id
|
|
sender_id, events_to_store = await _create_tracker_for_query_params(
|
|
rasa_app, model_id
|
|
)
|
|
|
|
_, response = await rasa_app.get(
|
|
f"/conversations/{sender_id}/story?all_sessions=true"
|
|
)
|
|
assert response.status == 200
|
|
|
|
story_content = response.body.decode("utf-8")
|
|
|
|
expected_first_story = textwrap.dedent(
|
|
f"""
|
|
- story: {sender_id}, story 1
|
|
steps:
|
|
- intent: greet
|
|
user: |-
|
|
hi
|
|
- action: utter_greet
|
|
- intent: restart
|
|
user: |-
|
|
/restart
|
|
- action: action_restart"""
|
|
)
|
|
assert expected_first_story in story_content
|
|
|
|
expected_second_story = textwrap.dedent(
|
|
f"""
|
|
- story: {sender_id}, story 2
|
|
steps:
|
|
- intent: greet
|
|
user: |-
|
|
hi again"""
|
|
)
|
|
assert expected_second_story in story_content
|
|
|
|
|
|
async def test_retrieve_story_with_query_param_all_sessions_false(
|
|
rasa_app: SanicASGITestClient,
|
|
) -> None:
|
|
model_id = rasa_app.sanic_app.ctx.agent.model_id
|
|
sender_id, events_to_store = await _create_tracker_for_query_params(
|
|
rasa_app, model_id
|
|
)
|
|
|
|
_, response = await rasa_app.get(
|
|
f"/conversations/{sender_id}/story?all_sessions=false"
|
|
)
|
|
assert response.status == 200
|
|
|
|
story_content = response.body.decode("utf-8")
|
|
expected_story = textwrap.dedent(
|
|
f"""
|
|
- story: {sender_id}
|
|
steps:
|
|
- intent: greet
|
|
user: |-
|
|
hi again"""
|
|
)
|
|
assert expected_story in story_content
|
|
|
|
|
|
async def test_retrieve_tracker_with_customized_action_session_start(
|
|
rasa_app: SanicASGITestClient,
|
|
monkeypatch: MonkeyPatch,
|
|
) -> None:
|
|
sender_id = str(uuid.uuid1())
|
|
|
|
async def mock_run_action_session_start(
|
|
self,
|
|
*args: Any,
|
|
**kwargs: Any,
|
|
) -> List[Event]:
|
|
_events: List[Event] = [
|
|
SessionStarted(),
|
|
BotUttered("Hey there!", {"name": "utter_greet"}),
|
|
ActionExecuted(ACTION_LISTEN_NAME),
|
|
]
|
|
|
|
return _events
|
|
|
|
monkeypatch.setattr(
|
|
"rasa.core.actions.action.ActionSessionStart.run", mock_run_action_session_start
|
|
)
|
|
|
|
_, response = await rasa_app.get(
|
|
f"/conversations/{sender_id}/tracker",
|
|
headers={"Content-Type": rasa.server.JSON_CONTENT_TYPE},
|
|
)
|
|
|
|
assert response.status == 200
|
|
|
|
tracker = response.json
|
|
assert tracker is not None
|
|
|
|
tracker_events = tracker.get("events")
|
|
assert len(tracker_events) == 4
|
|
|
|
assert tracker_events[0].get("event") == "action"
|
|
assert tracker_events[0].get("name") == "action_session_start"
|
|
|
|
assert tracker_events[1].get("event") == "session_started"
|
|
|
|
assert tracker_events[2].get("event") == "bot"
|
|
assert tracker_events[2].get("text") == "Hey there!"
|
|
|
|
assert tracker_events[3].get("event") == "action"
|
|
assert tracker_events[3].get("name") == "action_listen"
|