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

1091 lines
34 KiB
Python

import inspect
import logging
import secrets
import shutil
import tempfile
import os
import textwrap
from pathlib import Path
from typing import Text
from unittest.mock import Mock
import pytest
from _pytest.capture import CaptureFixture
from _pytest.logging import LogCaptureFixture
from _pytest.monkeypatch import MonkeyPatch
from _pytest.tmpdir import TempPathFactory
import rasa
from rasa.core.policies.rule_policy import RulePolicy
from rasa.core.policies.ted_policy import TEDPolicy
import rasa.model
import rasa.model_training
import rasa.core
import rasa.core.train
import rasa.nlu
from rasa.engine.storage.local_model_storage import LocalModelStorage
from rasa.engine.recipes.default_recipe import DefaultV1Recipe
from rasa.engine.graph import GraphModelConfiguration
from rasa.engine.training.graph_trainer import GraphTrainer
from rasa.shared.data import TrainingType
from rasa.shared.core.events import ActionExecuted, SlotSet
from rasa.shared.core.training_data.structures import RuleStep, StoryGraph, StoryStep
from rasa.nlu.classifiers.diet_classifier import DIETClassifier
from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION
import rasa.shared.utils.io
from rasa.shared.core.domain import Domain
from rasa.shared.exceptions import InvalidConfigException
from rasa.utils.tensorflow.constants import EPOCHS
def count_temp_rasa_files(directory: Text) -> int:
return len(
[
entry
for entry in os.listdir(directory)
if not any(
[
# Ignore the following files/directories:
entry == "__pycache__", # Python bytecode
entry.endswith(".py") # Temp .py files created by TF
# Anything else is considered to be created by Rasa
]
)
]
)
def test_train_temp_files(
tmp_path: Path,
monkeypatch: MonkeyPatch,
domain_path: Text,
stories_path: Text,
stack_config_path: Text,
nlu_data_path: Text,
):
(tmp_path / "training").mkdir()
(tmp_path / "models").mkdir()
monkeypatch.setattr(tempfile, "tempdir", tmp_path / "training")
output = str(tmp_path / "models")
rasa.train(
domain_path,
stack_config_path,
[stories_path, nlu_data_path],
output=output,
force_training=True,
)
assert count_temp_rasa_files(tempfile.tempdir) == 0
# After training the model, try to do it again. This shouldn't try to train
# a new model because nothing has been changed. It also shouldn't create
# any temp files.
rasa.train(
domain_path, stack_config_path, [stories_path, nlu_data_path], output=output
)
assert count_temp_rasa_files(tempfile.tempdir) == 0
def test_train_core_temp_files(
tmp_path: Path,
monkeypatch: MonkeyPatch,
domain_path: Text,
stories_path: Text,
stack_config_path: Text,
):
(tmp_path / "training").mkdir()
(tmp_path / "models").mkdir()
monkeypatch.setattr(tempfile, "tempdir", tmp_path / "training")
rasa.model_training.train_core(
domain_path, stack_config_path, stories_path, output=str(tmp_path / "models")
)
assert count_temp_rasa_files(tempfile.tempdir) == 0
def test_train_nlu_temp_files(
tmp_path: Path,
monkeypatch: MonkeyPatch,
stack_config_path: Text,
nlu_data_path: Text,
):
(tmp_path / "training").mkdir()
(tmp_path / "models").mkdir()
monkeypatch.setattr(tempfile, "tempdir", tmp_path / "training")
rasa.model_training.train_nlu(
stack_config_path, nlu_data_path, output=str(tmp_path / "models")
)
assert count_temp_rasa_files(tempfile.tempdir) == 0
def test_train_nlu_wrong_format_error_message(
capsys: CaptureFixture,
tmp_path: Path,
monkeypatch: MonkeyPatch,
stack_config_path: Text,
incorrect_nlu_data_path: Text,
):
(tmp_path / "training").mkdir()
(tmp_path / "models").mkdir()
monkeypatch.setattr(tempfile, "tempdir", tmp_path / "training")
rasa.model_training.train_nlu(
stack_config_path, incorrect_nlu_data_path, output=str(tmp_path / "models")
)
captured = capsys.readouterr()
assert "Please verify the data format" in captured.out
def test_train_nlu_with_responses_no_domain_warns(tmp_path: Path):
data_path = "data/test_nlu_no_responses/nlu_no_responses.yml"
with pytest.warns(UserWarning) as records:
rasa.model_training.train_nlu(
"data/test_config/config_response_selector_minimal.yml",
data_path,
output=str(tmp_path / "models"),
)
assert any(
"You either need to add a response phrase or correct the intent"
in record.message.args[0]
for record in records
)
def test_train_nlu_with_responses_and_domain_no_warns(tmp_path: Path):
data_path = "data/test_nlu_no_responses/nlu_no_responses.yml"
domain_path = "data/test_nlu_no_responses/domain_with_only_responses.yml"
with pytest.warns(None) as records:
rasa.model_training.train_nlu(
"data/test_config/config_response_selector_minimal.yml",
data_path,
output=str(tmp_path / "models"),
domain=domain_path,
)
assert not any(
"You either need to add a response phrase or correct the intent"
in record.message.args[0]
for record in records
)
def test_train_nlu_no_nlu_file_error_message(
capsys: CaptureFixture,
tmp_path: Path,
monkeypatch: MonkeyPatch,
stack_config_path: Text,
):
(tmp_path / "training").mkdir()
(tmp_path / "models").mkdir()
monkeypatch.setattr(tempfile, "tempdir", tmp_path / "training")
rasa.model_training.train_nlu(
stack_config_path, "", output=str(tmp_path / "models")
)
captured = capsys.readouterr()
assert "No NLU data given" in captured.out
def test_train_core_autoconfig(
tmp_path: Path,
monkeypatch: MonkeyPatch,
domain_path: Text,
stories_path: Text,
stack_config_path: Text,
):
monkeypatch.setattr(tempfile, "tempdir", tmp_path)
# mock function that returns configuration
mocked_auto_configure = Mock(wraps=DefaultV1Recipe.auto_configure)
monkeypatch.setattr(DefaultV1Recipe, "auto_configure", mocked_auto_configure)
# skip actual core training
monkeypatch.setattr(GraphTrainer, GraphTrainer.train.__name__, Mock())
# do training
rasa.model_training.train_core(
domain_path,
stack_config_path,
stories_path,
output="test_train_core_temp_files_models",
)
mocked_auto_configure.assert_called_once()
_, args, _ = mocked_auto_configure.mock_calls[0]
assert args[2] == TrainingType.CORE
def test_train_nlu_autoconfig(
tmp_path: Path,
monkeypatch: MonkeyPatch,
stack_config_path: Text,
nlu_data_path: Text,
):
monkeypatch.setattr(tempfile, "tempdir", tmp_path)
# mock function that returns configuration
mocked_auto_configuration = Mock(wraps=DefaultV1Recipe.auto_configure)
monkeypatch.setattr(DefaultV1Recipe, "auto_configure", mocked_auto_configuration)
monkeypatch.setattr(GraphTrainer, GraphTrainer.train.__name__, Mock())
# do training
rasa.model_training.train_nlu(
stack_config_path, nlu_data_path, output="test_train_nlu_temp_files_models"
)
mocked_auto_configuration.assert_called_once()
_, args, _ = mocked_auto_configuration.mock_calls[0]
assert args[2] == TrainingType.NLU
def new_model_path_in_same_dir(old_model_path: Text) -> Text:
return str(Path(old_model_path).parent / (secrets.token_hex(8) + ".tar.gz"))
class TestE2e:
def test_e2e_gives_experimental_warning(
self,
moodbot_domain_path: Path,
e2e_bot_config_file: Path,
e2e_stories_path: Text,
nlu_data_path: Text,
caplog: LogCaptureFixture,
tmp_path: Path,
):
with caplog.at_level(logging.WARNING):
rasa.train(
str(moodbot_domain_path),
str(e2e_bot_config_file),
[e2e_stories_path, nlu_data_path],
output=str(tmp_path),
dry_run=True,
)
assert any(
[
"The end-to-end training is currently experimental" in record.message
for record in caplog.records
]
)
def test_models_not_retrained_if_no_new_data(
self,
trained_e2e_model: Text,
moodbot_domain_path: Path,
e2e_bot_config_file: Path,
e2e_stories_path: Text,
nlu_data_path: Text,
trained_e2e_model_cache: Path,
):
result = rasa.train(
str(moodbot_domain_path),
str(e2e_bot_config_file),
[e2e_stories_path, nlu_data_path],
output=new_model_path_in_same_dir(trained_e2e_model),
dry_run=True,
)
assert result.code == 0
def test_retrains_nlu_and_core_if_new_e2e_example(
self,
trained_e2e_model: Text,
moodbot_domain_path: Path,
e2e_bot_config_file: Path,
e2e_stories_path: Text,
nlu_data_path: Text,
tmp_path: Path,
trained_e2e_model_cache: Path,
):
stories_yaml = rasa.shared.utils.io.read_yaml_file(e2e_stories_path)
stories_yaml["stories"][1]["steps"].append({"user": "new message!"})
new_stories_file = tmp_path / "new_stories.yml"
rasa.shared.utils.io.write_yaml(stories_yaml, new_stories_file)
result = rasa.train(
str(moodbot_domain_path),
str(e2e_bot_config_file),
[new_stories_file, nlu_data_path],
output=new_model_path_in_same_dir(trained_e2e_model),
dry_run=True,
)
assert result.code == rasa.model_training.CODE_NEEDS_TO_BE_RETRAINED
fingerprints = result.dry_run_results
assert not fingerprints["train_CountVectorsFeaturizer3"].is_hit
assert not fingerprints["train_DIETClassifier5"].is_hit
assert not fingerprints["end_to_end_features_provider"].is_hit
assert not fingerprints["train_TEDPolicy0"].is_hit
assert not fingerprints["train_RulePolicy1"].is_hit
def test_retrains_only_core_if_new_e2e_example_seen_before(
self,
trained_e2e_model: Text,
moodbot_domain_path: Path,
e2e_bot_config_file: Path,
e2e_stories_path: Text,
nlu_data_path: Text,
tmp_path: Path,
trained_e2e_model_cache: Path,
):
stories_yaml = rasa.shared.utils.io.read_yaml_file(e2e_stories_path)
stories_yaml["stories"][1]["steps"].append({"user": "Yes"})
new_stories_file = tmp_path / "new_stories.yml"
rasa.shared.utils.io.write_yaml(stories_yaml, new_stories_file)
result = rasa.train(
str(moodbot_domain_path),
str(e2e_bot_config_file),
[new_stories_file, nlu_data_path],
output=new_model_path_in_same_dir(trained_e2e_model),
dry_run=True,
)
assert result.code == rasa.model_training.CODE_NEEDS_TO_BE_RETRAINED
fingerprints = result.dry_run_results
assert fingerprints["train_CountVectorsFeaturizer3"].is_hit
assert fingerprints["train_DIETClassifier5"].is_hit
assert fingerprints["end_to_end_features_provider"].is_hit
assert not fingerprints["train_TEDPolicy0"].is_hit
assert not fingerprints["train_RulePolicy1"].is_hit
def test_nlu_and_core_trained_if_no_nlu_data_but_e2e_stories(
self,
moodbot_domain_path: Path,
e2e_bot_config_file: Path,
e2e_stories_path: Text,
tmp_path: Path,
monkeypatch: MonkeyPatch,
):
train_mock = Mock()
monkeypatch.setattr(GraphTrainer, GraphTrainer.train.__name__, train_mock)
rasa.train(
str(moodbot_domain_path),
str(e2e_bot_config_file),
[e2e_stories_path],
output=str(tmp_path),
)
args, _ = train_mock.call_args
model_configuration: GraphModelConfiguration = args[0]
for schema in [
model_configuration.train_schema,
model_configuration.predict_schema,
]:
assert any(
issubclass(node.uses, DIETClassifier) for node in schema.nodes.values()
)
assert any(
issubclass(node.uses, TEDPolicy) for node in schema.nodes.values()
)
def test_new_nlu_data_retrains_core_if_there_are_e2e_stories(
self,
trained_e2e_model: Text,
moodbot_domain_path: Path,
e2e_bot_config_file: Path,
e2e_stories_path: Text,
nlu_data_path: Text,
tmp_path: Path,
trained_e2e_model_cache: Path,
):
nlu_yaml = rasa.shared.utils.io.read_yaml_file(nlu_data_path)
nlu_yaml["nlu"][0]["examples"] += "- surprise!\n"
new_nlu_file = tmp_path / "new_nlu.yml"
rasa.shared.utils.io.write_yaml(nlu_yaml, new_nlu_file)
result = rasa.train(
str(moodbot_domain_path),
str(e2e_bot_config_file),
[e2e_stories_path, new_nlu_file],
output=new_model_path_in_same_dir(trained_e2e_model),
dry_run=True,
)
assert result.code == rasa.model_training.CODE_NEEDS_TO_BE_RETRAINED
fingerprints = result.dry_run_results
assert not fingerprints["train_CountVectorsFeaturizer3"].is_hit
assert not fingerprints["train_DIETClassifier5"].is_hit
assert not fingerprints["end_to_end_features_provider"].is_hit
assert not fingerprints["train_TEDPolicy0"].is_hit
assert fingerprints["train_RulePolicy1"].is_hit
def test_new_nlu_data_does_not_retrain_core_if_there_are_no_e2e_stories(
self,
moodbot_domain_path: Path,
e2e_bot_config_file: Path,
simple_stories_path: Text,
nlu_data_path: Text,
tmp_path: Path,
):
rasa.train(
str(moodbot_domain_path),
str(e2e_bot_config_file),
[simple_stories_path, nlu_data_path],
output=str(tmp_path),
)
nlu_yaml = rasa.shared.utils.io.read_yaml_file(nlu_data_path)
nlu_yaml["nlu"][0]["examples"] += "- surprise!\n"
new_nlu_file = tmp_path / "new_nlu.yml"
rasa.shared.utils.io.write_yaml(nlu_yaml, new_nlu_file)
result = rasa.train(
str(moodbot_domain_path),
str(e2e_bot_config_file),
[simple_stories_path, new_nlu_file],
output=str(tmp_path),
dry_run=True,
)
assert result.code == rasa.model_training.CODE_NEEDS_TO_BE_RETRAINED
fingerprints = result.dry_run_results
assert not fingerprints["train_CountVectorsFeaturizer3"].is_hit
assert not fingerprints["train_DIETClassifier5"].is_hit
assert "end_to_end_features_provider" not in fingerprints
assert fingerprints["train_TEDPolicy0"].is_hit
assert fingerprints["train_RulePolicy1"].is_hit
def test_training_core_with_e2e_fails_gracefully(
self,
capsys: CaptureFixture,
tmp_path: Path,
domain_path: Text,
stack_config_path: Text,
e2e_stories_path: Text,
):
rasa.model_training.train_core(
domain_path, stack_config_path, e2e_stories_path, output=str(tmp_path)
)
assert not list(tmp_path.glob("*"))
captured = capsys.readouterr()
assert (
"Stories file contains e2e stories. "
"Please train using `rasa train` so that the NLU model is also trained."
) in captured.out
@pytest.mark.timeout(300, func_only=True)
@pytest.mark.parametrize("use_latest_model", [True, False])
def test_model_finetuning(
tmp_path: Path,
domain_path: Text,
stories_path: Text,
stack_config_path: Text,
nlu_data_path: Text,
trained_rasa_model: Text,
use_latest_model: bool,
):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
if use_latest_model:
trained_rasa_model = str(Path(trained_rasa_model).parent)
result = rasa.train(
domain_path,
stack_config_path,
[stories_path, nlu_data_path],
output=output,
force_training=True,
model_to_finetune=trained_rasa_model,
finetuning_epoch_fraction=0.1,
)
assert Path(result.model).is_file()
@pytest.mark.timeout(300, func_only=True)
@pytest.mark.parametrize("use_latest_model", [True, False])
def test_model_finetuning_core(
tmp_path: Path,
trained_moodbot_core_path: Text,
use_latest_model: bool,
tmp_path_factory: TempPathFactory,
):
(tmp_path / "models").mkdir()
output = tmp_path / "models"
if use_latest_model:
trained_moodbot_core_path = str(Path(trained_moodbot_core_path).parent)
# Typically models will be fine-tuned with a smaller number of epochs than training
# from scratch.
# Fine-tuning will use the number of epochs in the new config.
old_config = rasa.shared.utils.io.read_yaml_file("data/test_moodbot/config.yml")
old_config["policies"][0]["epochs"] = 10
new_config_path = tmp_path / "new_config.yml"
rasa.shared.utils.io.write_yaml(old_config, new_config_path)
old_stories = rasa.shared.utils.io.read_yaml_file(
"data/test_moodbot/data/stories.yml"
)
old_stories["stories"].append(
{"story": "new story", "steps": [{"intent": "greet"}]}
)
new_stories_path = tmp_path / "new_stories.yml"
rasa.shared.utils.io.write_yaml(old_stories, new_stories_path)
result = rasa.model_training.train_core(
"data/test_moodbot/domain.yml",
str(new_config_path),
str(new_stories_path),
output=str(output),
model_to_finetune=trained_moodbot_core_path,
finetuning_epoch_fraction=0.2,
)
storage_dir = tmp_path_factory.mktemp("finetuned model")
_, metadata = LocalModelStorage.from_model_archive(storage_dir, Path(result))
assert metadata.train_schema.nodes["train_TEDPolicy0"].config[EPOCHS] == 2
assert metadata.training_type == TrainingType.CORE
def test_model_finetuning_core_with_default_epochs(
tmp_path: Path,
monkeypatch: MonkeyPatch,
trained_moodbot_core_path: Text,
tmp_path_factory: TempPathFactory,
):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
# Providing a new config with no epochs will mean the default amount are used
# and then scaled by `finetuning_epoch_fraction`.
old_config = rasa.shared.utils.io.read_yaml_file("data/test_moodbot/config.yml")
del old_config["policies"][0]["epochs"]
new_config_path = tmp_path / "new_config.yml"
rasa.shared.utils.io.write_yaml(old_config, new_config_path)
model_name = rasa.model_training.train_core(
"data/test_moodbot/domain.yml",
str(new_config_path),
"data/test_moodbot/data/stories.yml",
output=output,
model_to_finetune=trained_moodbot_core_path,
finetuning_epoch_fraction=2,
)
storage_dir = tmp_path_factory.mktemp("finetuned model")
_, metadata = LocalModelStorage.from_model_archive(storage_dir, Path(model_name))
assert metadata.train_schema.nodes["train_TEDPolicy0"].config[EPOCHS] == 2
def test_model_finetuning_core_new_domain_label(
tmp_path: Path,
trained_default_agent_model: Text,
simple_config_path: Text,
):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
# Simulate addition to training data
old_domain = rasa.shared.utils.io.read_yaml_file(
"data/test_domains/default_with_slots.yml"
)
old_domain["intents"].append("a_new_one")
new_domain_path = tmp_path / "new_domain.yml"
rasa.shared.utils.io.write_yaml(old_domain, new_domain_path)
with pytest.raises(InvalidConfigException):
rasa.model_training.train_core(
domain=str(new_domain_path),
config=simple_config_path,
stories="data/test_yaml_stories/stories_defaultdomain.yml",
output=output,
model_to_finetune=trained_default_agent_model,
)
def test_model_finetuning_new_domain_label_stops_all_training(
tmp_path: Path, trained_moodbot_path: Text
):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
old_domain = rasa.shared.utils.io.read_yaml_file("data/test_moodbot/domain.yml")
old_domain["intents"].append("a_new_one")
new_domain_path = tmp_path / "new_domain.yml"
rasa.shared.utils.io.write_yaml(old_domain, new_domain_path)
with pytest.raises(InvalidConfigException):
rasa.train(
domain=str(new_domain_path),
config="data/test_moodbot/config.yml",
training_files=[
"data/test_moodbot/data/stories.yml",
"data/test_moodbot/data/nlu.yml",
],
output=output,
model_to_finetune=trained_moodbot_path,
)
@pytest.mark.timeout(300, func_only=True)
@pytest.mark.parametrize("use_latest_model", [True, False])
def test_model_finetuning_nlu(
tmp_path: Path,
trained_nlu_moodbot_path: Text,
use_latest_model: bool,
tmp_path_factory: TempPathFactory,
):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
if use_latest_model:
trained_nlu_moodbot_path = str(Path(trained_nlu_moodbot_path).parent)
# Typically models will be fine-tuned with a smaller number of epochs than training
# from scratch.
# Fine-tuning will use the number of epochs in the new config.
old_config = rasa.shared.utils.io.read_yaml_file("data/test_moodbot/config.yml")
old_config["pipeline"][-1][EPOCHS] = 10
new_config_path = tmp_path / "new_config.yml"
rasa.shared.utils.io.write_yaml(old_config, new_config_path)
old_nlu = rasa.shared.utils.io.read_yaml_file("data/test_moodbot/data/nlu.yml")
old_nlu["nlu"][-1]["examples"] += "- perfect\n"
new_nlu_path = tmp_path / "new_nlu.yml"
rasa.shared.utils.io.write_yaml(old_nlu, new_nlu_path)
model_name = rasa.model_training.train_nlu(
str(new_config_path),
str(new_nlu_path),
domain="data/test_moodbot/domain.yml",
output=output,
model_to_finetune=trained_nlu_moodbot_path,
finetuning_epoch_fraction=0.2,
)
storage_dir = tmp_path_factory.mktemp("finetuned model")
_, metadata = LocalModelStorage.from_model_archive(storage_dir, Path(model_name))
assert metadata.train_schema.nodes["train_DIETClassifier5"].config[EPOCHS] == 2
assert metadata.training_type == TrainingType.NLU
def test_model_finetuning_nlu_new_label(tmp_path: Path, trained_nlu_moodbot_path: Text):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
old_nlu = rasa.shared.utils.io.read_yaml_file("data/test_moodbot/data/nlu.yml")
old_nlu["nlu"].append({"intent": "a_new_one", "examples": "-blah"})
new_nlu_path = tmp_path / "new_nlu.yml"
rasa.shared.utils.io.write_yaml(old_nlu, new_nlu_path)
with pytest.raises(InvalidConfigException):
rasa.model_training.train_nlu(
"data/test_moodbot/config.yml",
str(new_nlu_path),
domain="data/test_moodbot/domain.yml",
output=output,
model_to_finetune=trained_nlu_moodbot_path,
)
def test_model_finetuning_nlu_new_entity(
tmp_path: Path, trained_nlu_moodbot_path: Text
):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
old_nlu = rasa.shared.utils.io.read_yaml_file("data/test_moodbot/data/nlu.yml")
old_nlu["nlu"][-1]["examples"] = "-[blah](something)"
new_nlu_path = tmp_path / "new_nlu.yml"
rasa.shared.utils.io.write_yaml(old_nlu, new_nlu_path)
with pytest.raises(InvalidConfigException):
rasa.model_training.train_nlu(
"data/test_moodbot/config.yml",
str(new_nlu_path),
domain="data/test_moodbot/domain.yml",
output=output,
model_to_finetune=trained_nlu_moodbot_path,
)
def test_model_finetuning_nlu_new_label_already_in_domain(
tmp_path: Path,
trained_rasa_model: Text,
nlu_data_path: Text,
config_path: Text,
domain_path: Text,
):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
old_nlu = rasa.shared.utils.io.read_yaml_file(nlu_data_path)
# This intent exists in `domain_path` but not yet in the nlu data
old_nlu["nlu"].append({"intent": "why", "examples": "whyy??"})
new_nlu_path = tmp_path / "new_nlu.yml"
rasa.shared.utils.io.write_yaml(old_nlu, new_nlu_path)
with pytest.raises(InvalidConfigException):
rasa.model_training.train_nlu(
config_path,
str(new_nlu_path),
domain=domain_path,
output=output,
model_to_finetune=trained_rasa_model,
)
def test_model_finetuning_nlu_new_label_to_domain_only(
tmp_path: Path, trained_nlu_moodbot_path: Text
):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
old_domain = rasa.shared.utils.io.read_yaml_file("data/test_moodbot/domain.yml")
old_domain["intents"].append("a_new_one")
new_domain_path = tmp_path / "new_domain.yml"
rasa.shared.utils.io.write_yaml(old_domain, new_domain_path)
result = rasa.model_training.train_nlu(
"data/test_moodbot/config.yml",
"data/test_moodbot/data/nlu.yml",
domain=str(new_domain_path),
output=output,
model_to_finetune=trained_nlu_moodbot_path,
)
assert Path(result).is_file()
@pytest.mark.timeout(200, func_only=True)
def test_model_finetuning_nlu_with_default_epochs(
tmp_path: Path,
monkeypatch: MonkeyPatch,
trained_nlu_moodbot_path: Text,
tmp_path_factory: TempPathFactory,
):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
# Providing a new config with no epochs will mean the default amount are used
# and then scaled by `finetuning_epoch_fraction`.
old_config = rasa.shared.utils.io.read_yaml_file("data/test_moodbot/config.yml")
del old_config["pipeline"][-1][EPOCHS]
new_config_path = tmp_path / "new_config.yml"
rasa.shared.utils.io.write_yaml(old_config, new_config_path)
model_name = rasa.model_training.train_nlu(
str(new_config_path),
"data/test_moodbot/data/nlu.yml",
output=output,
model_to_finetune=trained_nlu_moodbot_path,
finetuning_epoch_fraction=0.01,
)
storage_dir = tmp_path_factory.mktemp("finetuned model")
_, metadata = LocalModelStorage.from_model_archive(storage_dir, Path(model_name))
assert metadata.train_schema.nodes["train_DIETClassifier5"].config[EPOCHS] == 3
@pytest.mark.parametrize("model_to_fine_tune", ["invalid-path-to-model", "."])
def test_model_finetuning_with_invalid_model(
tmp_path: Path,
monkeypatch: MonkeyPatch,
domain_path: Text,
stories_path: Text,
stack_config_path: Text,
nlu_data_path: Text,
model_to_fine_tune: Text,
capsys: CaptureFixture,
):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
with pytest.raises(SystemExit):
rasa.train(
domain_path,
stack_config_path,
[stories_path, nlu_data_path],
output=output,
force_training=True,
model_to_finetune=model_to_fine_tune,
finetuning_epoch_fraction=1,
)
output = capsys.readouterr().out
assert "No model for finetuning found" in output
@pytest.mark.parametrize("model_to_fine_tune", ["invalid-path-to-model", "."])
def test_model_finetuning_with_invalid_model_core(
tmp_path: Path,
domain_path: Text,
stories_path: Text,
stack_config_path: Text,
model_to_fine_tune: Text,
capsys: CaptureFixture,
):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
with pytest.raises(SystemExit):
rasa.model_training.train_core(
domain_path,
stack_config_path,
stories_path,
output=output,
model_to_finetune=model_to_fine_tune,
finetuning_epoch_fraction=1,
)
assert "No model for finetuning found" in capsys.readouterr().out
@pytest.mark.parametrize("model_to_fine_tune", ["invalid-path-to-model", "."])
def test_model_finetuning_with_invalid_model_nlu(
tmp_path: Path,
monkeypatch: MonkeyPatch,
domain_path: Text,
stack_config_path: Text,
nlu_data_path: Text,
model_to_fine_tune: Text,
capsys: CaptureFixture,
):
(tmp_path / "models").mkdir()
output = str(tmp_path / "models")
with pytest.raises(SystemExit):
rasa.model_training.train_nlu(
stack_config_path,
nlu_data_path,
domain=domain_path,
output=output,
model_to_finetune=model_to_fine_tune,
finetuning_epoch_fraction=1,
)
assert "No model for finetuning found" in capsys.readouterr().out
def test_models_not_retrained_if_only_new_responses(
trained_e2e_model: Text,
moodbot_domain_path: Path,
e2e_bot_config_file: Path,
e2e_stories_path: Text,
nlu_data_path: Text,
trained_e2e_model_cache: Path,
tmp_path: Path,
):
domain = Domain.load(moodbot_domain_path)
domain_with_extra_response = """
version: '2.0'
responses:
utter_greet:
- text: "Hi from Rasa"
"""
domain_with_extra_response = Domain.from_yaml(domain_with_extra_response)
new_domain = domain.merge(domain_with_extra_response)
new_domain_path = tmp_path / "domain.yml"
rasa.shared.utils.io.write_yaml(new_domain.as_dict(), new_domain_path)
result = rasa.train(
str(new_domain_path),
str(e2e_bot_config_file),
[e2e_stories_path, nlu_data_path],
output=str(tmp_path),
dry_run=True,
)
assert result.code == 0
def test_models_not_retrained_if_only_new_action(
trained_e2e_model: Text,
moodbot_domain_path: Path,
e2e_bot_config_file: Path,
e2e_stories_path: Text,
nlu_data_path: Text,
trained_e2e_model_cache: Path,
tmp_path: Path,
):
domain = Domain.load(moodbot_domain_path)
domain_with_extra_response = """
version: '2.0'
responses:
utter_greet_new:
- text: "Hi from Rasa"
"""
new_domain = domain.merge(Domain.from_yaml(domain_with_extra_response))
new_domain_path = tmp_path / "domain.yml"
rasa.shared.utils.io.write_yaml(new_domain.as_dict(), new_domain_path)
result = rasa.train(
str(new_domain_path),
str(e2e_bot_config_file),
[e2e_stories_path, nlu_data_path],
output=str(tmp_path),
dry_run=True,
)
assert result.code == rasa.model_training.CODE_NEEDS_TO_BE_RETRAINED
def test_invalid_graph_schema(
tmp_path: Path, domain_path: Text, stories_path: Text, nlu_data_path: Text
):
config = textwrap.dedent(
"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
recipe: "default.v1"
assistant_id: placeholder_default
pipeline:
- name: WhitespaceTokenizer
- name: TEDPolicy
"""
)
new_config_path = tmp_path / "config.yml"
rasa.shared.utils.io.write_yaml(
rasa.shared.utils.io.read_yaml(config), new_config_path
)
with pytest.raises(InvalidConfigException) as captured_exception:
rasa.train(
domain_path,
str(new_config_path),
[stories_path, nlu_data_path],
output=str(tmp_path),
)
assert "Found policy 'TEDPolicy1' in NLU pipeline." in str(captured_exception)
def test_fingerprint_changes_if_module_changes(
tmp_path: Path, domain_path: Text, stories_path: Text, monkeypatch: MonkeyPatch
):
rule_policy_path = inspect.getfile(RulePolicy)
module_name = "custom_rule_policy"
new_class_name = "CustomRulePolicy"
custom_module_path = Path(tmp_path, f"{module_name}.py")
shutil.copy2(rule_policy_path, custom_module_path)
# Rename class as the class name has to be unique
source_code = custom_module_path.read_text()
source_code = source_code.replace("RulePolicy", new_class_name)
custom_module_path.write_text(source_code)
config = textwrap.dedent(
f"""
version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"
recipe: "default.v1"
assistant_id: placeholder_default
policies:
- name: RulePolicy
- name: {module_name}.{new_class_name}
"""
)
monkeypatch.syspath_prepend(tmp_path)
new_config_path = tmp_path / "config.yml"
rasa.shared.utils.io.write_yaml(
rasa.shared.utils.io.read_yaml(config), new_config_path
)
# Train to initialize cache
rasa.train(domain_path, str(new_config_path), [stories_path], output=str(tmp_path))
# Make sure that the caching works as expected the code didn't change
result = rasa.train(
domain_path,
str(new_config_path),
[stories_path],
output=str(tmp_path),
dry_run=True,
)
assert result.code == 0
# Make a change to the code so a new training is necessary
source_code = custom_module_path.read_text()
source_code = source_code.replace("Dict[Text, Any]", "Dict")
custom_module_path.write_text(source_code)
result = rasa.train(
domain_path,
str(new_config_path),
[stories_path],
output=str(tmp_path),
dry_run=True,
)
assert result.code == rasa.model_training.CODE_NEEDS_TO_BE_RETRAINED
assert not result.dry_run_results[f"train_{module_name}.{new_class_name}1"].is_hit
def test_check_unresolved_slots(capsys: CaptureFixture):
stories = StoryGraph(
[
StoryStep(
"greet",
events=[SlotSet("temp1"), ActionExecuted("temp3"), SlotSet("cuisine")],
),
RuleStep("bye", events=[SlotSet("temp4")]),
]
)
domain_path = "data/test_domains/default_with_mapping.yml"
domain = Domain.load(domain_path)
with pytest.raises(SystemExit):
rasa.model_training._check_unresolved_slots(domain, stories)
error_output = capsys.readouterr().out
assert (
"temp1" in error_output
and "temp4" in error_output
and "cuisine" not in error_output
)
stories = StoryGraph(
[
StoryStep(
"greet",
events=[
SlotSet("location"),
ActionExecuted("temp"),
SlotSet("cuisine"),
],
)
]
)
assert rasa.model_training._check_unresolved_slots(domain, stories) is None