Files
wehub-resource-sync dc6079821b
CI Github Actions / Run Tests (push) Waiting to run
Automatic PR Merger / mergepal (push) Waiting to run
Docs Tests / Check for file changes (push) Waiting to run
Docs Tests / Test Documentation (push) Blocked by required conditions
Docs Tests / Documentation Linting Checks (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-performance) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-policies) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-cli) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-core-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-full-model-training) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-performance) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-policies) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-cli) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-core-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-full-model-training) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-predictors) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-other-unit-tests) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-performance) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-policies) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-cli) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-core-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-full-model-training) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-predictors) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-other-unit-tests) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-performance) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-policies) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-cli) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-core-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-predictors) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-other-unit-tests) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-full-model-training) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-predictors) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-other-unit-tests) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-performance) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-policies) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-cli) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-core-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-full-model-training) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-predictors) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-other-unit-tests) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-performance) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-policies) (push) Blocked by required conditions
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.10) (push) Blocked by required conditions
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.8) (push) Blocked by required conditions
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.9) (push) Blocked by required conditions
Continuous Integration / Run Flaky Tests (windows-2022, 3.10) (push) Blocked by required conditions
Continuous Integration / Run Flaky Tests (windows-2022, 3.8) (push) Blocked by required conditions
Continuous Integration / Run Flaky Tests (windows-2022, 3.9) (push) Blocked by required conditions
Continuous Integration / Check for file changes (push) Waiting to run
Continuous Integration / Wait for docs tests (push) Blocked by required conditions
Continuous Integration / Code Quality (push) Blocked by required conditions
Continuous Integration / Check for changelog (push) Waiting to run
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-cli) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-core-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-full-model-training) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-predictors) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-other-unit-tests) (push) Blocked by required conditions
Continuous Integration / Upload coverage reports to codeclimate (push) Blocked by required conditions
Continuous Integration / Run Non-Sequential Integration Tests (push) Blocked by required conditions
Continuous Integration / Run Broker Integration Tests (push) Blocked by required conditions
Continuous Integration / Run Sequential Integration Tests (push) Blocked by required conditions
Continuous Integration / Build Docker base images and setup environment (push) Waiting to run
Continuous Integration / Build Docker (default) (push) Blocked by required conditions
Continuous Integration / Build Docker (full) (push) Blocked by required conditions
Continuous Integration / Build Docker (mitie-en) (push) Blocked by required conditions
Continuous Integration / Build Docker (spacy-de) (push) Blocked by required conditions
Continuous Integration / Build Docker (spacy-en) (push) Blocked by required conditions
Continuous Integration / Build Docker (spacy-it) (push) Blocked by required conditions
Continuous Integration / Deploy to PyPI (push) Blocked by required conditions
Continuous Integration / Notify Slack & Publish Release Notes (push) Blocked by required conditions
Publish Documentation / Evaluate release tag (push) Waiting to run
Publish Documentation / Prebuild Docs (push) Blocked by required conditions
Publish Documentation / Preview Docs (push) Blocked by required conditions
Publish Documentation / Check for file changes (push) Waiting to run
Publish Documentation / Publish Docs (push) Waiting to run
Semgrep / Semgrep Workflow Security Scan (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:24:47 +08:00

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"