rasa-pro 3.9.18__py3-none-any.whl → 3.10.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of rasa-pro might be problematic. Click here for more details.
- README.md +0 -374
- rasa/__init__.py +1 -2
- rasa/__main__.py +5 -0
- rasa/anonymization/anonymization_rule_executor.py +2 -2
- rasa/api.py +27 -23
- rasa/cli/arguments/data.py +27 -2
- rasa/cli/arguments/default_arguments.py +25 -3
- rasa/cli/arguments/run.py +9 -9
- rasa/cli/arguments/train.py +11 -3
- rasa/cli/data.py +70 -8
- rasa/cli/e2e_test.py +104 -431
- rasa/cli/evaluate.py +1 -1
- rasa/cli/interactive.py +1 -0
- rasa/cli/llm_fine_tuning.py +398 -0
- rasa/cli/project_templates/calm/endpoints.yml +1 -1
- rasa/cli/project_templates/tutorial/endpoints.yml +1 -1
- rasa/cli/run.py +15 -14
- rasa/cli/scaffold.py +10 -8
- rasa/cli/studio/studio.py +35 -5
- rasa/cli/train.py +56 -8
- rasa/cli/utils.py +22 -5
- rasa/cli/x.py +1 -1
- rasa/constants.py +7 -1
- rasa/core/actions/action.py +98 -49
- rasa/core/actions/action_run_slot_rejections.py +4 -1
- rasa/core/actions/custom_action_executor.py +9 -6
- rasa/core/actions/direct_custom_actions_executor.py +80 -0
- rasa/core/actions/e2e_stub_custom_action_executor.py +68 -0
- rasa/core/actions/grpc_custom_action_executor.py +2 -2
- rasa/core/actions/http_custom_action_executor.py +6 -5
- rasa/core/agent.py +21 -17
- rasa/core/channels/__init__.py +2 -0
- rasa/core/channels/audiocodes.py +1 -16
- rasa/core/channels/voice_aware/__init__.py +0 -0
- rasa/core/channels/voice_aware/jambonz.py +103 -0
- rasa/core/channels/voice_aware/jambonz_protocol.py +344 -0
- rasa/core/channels/voice_aware/utils.py +20 -0
- rasa/core/channels/voice_native/__init__.py +0 -0
- rasa/core/constants.py +6 -1
- rasa/core/information_retrieval/faiss.py +7 -4
- rasa/core/information_retrieval/information_retrieval.py +8 -0
- rasa/core/information_retrieval/milvus.py +9 -2
- rasa/core/information_retrieval/qdrant.py +1 -1
- rasa/core/nlg/contextual_response_rephraser.py +32 -10
- rasa/core/nlg/summarize.py +4 -3
- rasa/core/policies/enterprise_search_policy.py +113 -45
- rasa/core/policies/flows/flow_executor.py +122 -76
- rasa/core/policies/intentless_policy.py +83 -29
- rasa/core/processor.py +72 -54
- rasa/core/run.py +5 -4
- rasa/core/tracker_store.py +8 -4
- rasa/core/training/interactive.py +1 -1
- rasa/core/utils.py +56 -57
- rasa/dialogue_understanding/coexistence/llm_based_router.py +53 -13
- rasa/dialogue_understanding/commands/__init__.py +6 -0
- rasa/dialogue_understanding/commands/restart_command.py +58 -0
- rasa/dialogue_understanding/commands/session_start_command.py +59 -0
- rasa/dialogue_understanding/commands/utils.py +40 -0
- rasa/dialogue_understanding/generator/constants.py +10 -3
- rasa/dialogue_understanding/generator/flow_retrieval.py +21 -5
- rasa/dialogue_understanding/generator/llm_based_command_generator.py +13 -3
- rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +134 -90
- rasa/dialogue_understanding/generator/nlu_command_adapter.py +47 -7
- rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +127 -41
- rasa/dialogue_understanding/patterns/restart.py +37 -0
- rasa/dialogue_understanding/patterns/session_start.py +37 -0
- rasa/dialogue_understanding/processor/command_processor.py +16 -3
- rasa/dialogue_understanding/processor/command_processor_component.py +6 -2
- rasa/e2e_test/aggregate_test_stats_calculator.py +134 -0
- rasa/e2e_test/assertions.py +1223 -0
- rasa/e2e_test/assertions_schema.yml +106 -0
- rasa/e2e_test/constants.py +20 -0
- rasa/e2e_test/e2e_config.py +220 -0
- rasa/e2e_test/e2e_config_schema.yml +26 -0
- rasa/e2e_test/e2e_test_case.py +131 -8
- rasa/e2e_test/e2e_test_converter.py +363 -0
- rasa/e2e_test/e2e_test_converter_prompt.jinja2 +70 -0
- rasa/e2e_test/e2e_test_coverage_report.py +364 -0
- rasa/e2e_test/e2e_test_result.py +26 -6
- rasa/e2e_test/e2e_test_runner.py +493 -71
- rasa/e2e_test/e2e_test_schema.yml +96 -0
- rasa/e2e_test/pykwalify_extensions.py +39 -0
- rasa/e2e_test/stub_custom_action.py +70 -0
- rasa/e2e_test/utils/__init__.py +0 -0
- rasa/e2e_test/utils/e2e_yaml_utils.py +55 -0
- rasa/e2e_test/utils/io.py +598 -0
- rasa/e2e_test/utils/validation.py +80 -0
- rasa/engine/graph.py +9 -3
- rasa/engine/recipes/default_components.py +0 -2
- rasa/engine/recipes/default_recipe.py +10 -2
- rasa/engine/storage/local_model_storage.py +40 -12
- rasa/engine/validation.py +78 -1
- rasa/env.py +9 -0
- rasa/graph_components/providers/story_graph_provider.py +59 -6
- rasa/llm_fine_tuning/__init__.py +0 -0
- rasa/llm_fine_tuning/annotation_module.py +241 -0
- rasa/llm_fine_tuning/conversations.py +144 -0
- rasa/llm_fine_tuning/llm_data_preparation_module.py +178 -0
- rasa/llm_fine_tuning/notebooks/unsloth_finetuning.ipynb +407 -0
- rasa/llm_fine_tuning/paraphrasing/__init__.py +0 -0
- rasa/llm_fine_tuning/paraphrasing/conversation_rephraser.py +281 -0
- rasa/llm_fine_tuning/paraphrasing/default_rephrase_prompt_template.jina2 +44 -0
- rasa/llm_fine_tuning/paraphrasing/rephrase_validator.py +121 -0
- rasa/llm_fine_tuning/paraphrasing/rephrased_user_message.py +10 -0
- rasa/llm_fine_tuning/paraphrasing_module.py +128 -0
- rasa/llm_fine_tuning/storage.py +174 -0
- rasa/llm_fine_tuning/train_test_split_module.py +441 -0
- rasa/model_training.py +56 -16
- rasa/nlu/persistor.py +157 -36
- rasa/server.py +45 -10
- rasa/shared/constants.py +76 -16
- rasa/shared/core/domain.py +27 -19
- rasa/shared/core/events.py +28 -2
- rasa/shared/core/flows/flow.py +208 -13
- rasa/shared/core/flows/flow_path.py +84 -0
- rasa/shared/core/flows/flows_list.py +33 -11
- rasa/shared/core/flows/flows_yaml_schema.json +269 -193
- rasa/shared/core/flows/validation.py +112 -25
- rasa/shared/core/flows/yaml_flows_io.py +149 -10
- rasa/shared/core/trackers.py +6 -0
- rasa/shared/core/training_data/structures.py +20 -0
- rasa/shared/core/training_data/visualization.html +2 -2
- rasa/shared/exceptions.py +4 -0
- rasa/shared/importers/importer.py +64 -16
- rasa/shared/nlu/constants.py +2 -0
- rasa/shared/providers/_configs/__init__.py +0 -0
- rasa/shared/providers/_configs/azure_openai_client_config.py +183 -0
- rasa/shared/providers/_configs/client_config.py +57 -0
- rasa/shared/providers/_configs/default_litellm_client_config.py +130 -0
- rasa/shared/providers/_configs/huggingface_local_embedding_client_config.py +234 -0
- rasa/shared/providers/_configs/openai_client_config.py +175 -0
- rasa/shared/providers/_configs/self_hosted_llm_client_config.py +176 -0
- rasa/shared/providers/_configs/utils.py +101 -0
- rasa/shared/providers/_ssl_verification_utils.py +124 -0
- rasa/shared/providers/embedding/__init__.py +0 -0
- rasa/shared/providers/embedding/_base_litellm_embedding_client.py +259 -0
- rasa/shared/providers/embedding/_langchain_embedding_client_adapter.py +74 -0
- rasa/shared/providers/embedding/azure_openai_embedding_client.py +277 -0
- rasa/shared/providers/embedding/default_litellm_embedding_client.py +102 -0
- rasa/shared/providers/embedding/embedding_client.py +90 -0
- rasa/shared/providers/embedding/embedding_response.py +41 -0
- rasa/shared/providers/embedding/huggingface_local_embedding_client.py +191 -0
- rasa/shared/providers/embedding/openai_embedding_client.py +172 -0
- rasa/shared/providers/llm/__init__.py +0 -0
- rasa/shared/providers/llm/_base_litellm_client.py +251 -0
- rasa/shared/providers/llm/azure_openai_llm_client.py +338 -0
- rasa/shared/providers/llm/default_litellm_llm_client.py +84 -0
- rasa/shared/providers/llm/llm_client.py +76 -0
- rasa/shared/providers/llm/llm_response.py +50 -0
- rasa/shared/providers/llm/openai_llm_client.py +155 -0
- rasa/shared/providers/llm/self_hosted_llm_client.py +293 -0
- rasa/shared/providers/mappings.py +75 -0
- rasa/shared/utils/cli.py +30 -0
- rasa/shared/utils/io.py +65 -2
- rasa/shared/utils/llm.py +246 -200
- rasa/shared/utils/yaml.py +121 -15
- rasa/studio/auth.py +6 -4
- rasa/studio/config.py +13 -4
- rasa/studio/constants.py +1 -0
- rasa/studio/data_handler.py +10 -3
- rasa/studio/download.py +19 -13
- rasa/studio/train.py +2 -3
- rasa/studio/upload.py +19 -11
- rasa/telemetry.py +113 -58
- rasa/tracing/instrumentation/attribute_extractors.py +32 -17
- rasa/utils/common.py +18 -19
- rasa/utils/endpoints.py +7 -4
- rasa/utils/json_utils.py +60 -0
- rasa/utils/licensing.py +9 -1
- rasa/utils/ml_utils.py +4 -2
- rasa/validator.py +213 -3
- rasa/version.py +1 -1
- rasa_pro-3.10.16.dist-info/METADATA +196 -0
- {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/RECORD +179 -113
- rasa/nlu/classifiers/llm_intent_classifier.py +0 -519
- rasa/shared/providers/openai/clients.py +0 -43
- rasa/shared/providers/openai/session_handler.py +0 -110
- rasa_pro-3.9.18.dist-info/METADATA +0 -563
- /rasa/{shared/providers/openai → cli/project_templates/tutorial/actions}/__init__.py +0 -0
- /rasa/cli/project_templates/tutorial/{actions.py → actions/actions.py} +0 -0
- {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/NOTICE +0 -0
- {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/WHEEL +0 -0
- {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from textwrap import dedent
|
|
9
|
+
from typing import Any, Dict, Generator, List, Optional, TYPE_CHECKING, Tuple, Union
|
|
10
|
+
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
import pandas as pd
|
|
13
|
+
import rich
|
|
14
|
+
import structlog
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
import rasa.shared.data
|
|
18
|
+
import rasa.shared.utils.cli
|
|
19
|
+
import rasa.utils.io
|
|
20
|
+
from rasa.e2e_test.constants import (
|
|
21
|
+
KEY_FIXTURES,
|
|
22
|
+
KEY_METADATA,
|
|
23
|
+
KEY_STUB_CUSTOM_ACTIONS,
|
|
24
|
+
KEY_TEST_CASE,
|
|
25
|
+
KEY_TEST_CASES,
|
|
26
|
+
STATUS_FAILED,
|
|
27
|
+
STATUS_PASSED,
|
|
28
|
+
STUB_CUSTOM_ACTION_NAME_SEPARATOR,
|
|
29
|
+
)
|
|
30
|
+
from rasa.e2e_test.e2e_test_case import Fixture, Metadata, TestSuite, TestCase
|
|
31
|
+
from rasa.e2e_test.utils.validation import (
|
|
32
|
+
read_e2e_test_schema,
|
|
33
|
+
validate_path_to_test_cases,
|
|
34
|
+
validate_test_case,
|
|
35
|
+
)
|
|
36
|
+
from rasa.shared.utils.yaml import (
|
|
37
|
+
is_key_in_yaml,
|
|
38
|
+
parse_raw_yaml,
|
|
39
|
+
validate_yaml_data_using_schema_with_assertions,
|
|
40
|
+
)
|
|
41
|
+
from rasa.utils.beta import BetaNotEnabledException, ensure_beta_feature_is_enabled
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from rasa.e2e_test.e2e_test_result import TestResult
|
|
45
|
+
from rasa.e2e_test.aggregate_test_stats_calculator import AccuracyCalculation
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
RASA_PRO_BETA_E2E_ASSERTIONS_ENV_VAR_NAME = "RASA_PRO_BETA_E2E_ASSERTIONS"
|
|
49
|
+
RASA_PRO_BETA_STUB_CUSTOM_ACTION_ENV_VAR_NAME = "RASA_PRO_BETA_STUB_CUSTOM_ACTION"
|
|
50
|
+
|
|
51
|
+
structlogger = structlog.get_logger()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def color_difference(diff: List[str]) -> Generator[str, None, None]:
|
|
55
|
+
"""Colorize the difference between two strings.
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> color_difference(["+ Hello", "- World"])
|
|
59
|
+
["<ansigreen>+ Hello</ansigreen>", "<ansired>- World</ansired>"]
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
diff: List of lines of the diff.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Generator of colored lines.
|
|
66
|
+
"""
|
|
67
|
+
for line in diff:
|
|
68
|
+
if line.startswith("+"):
|
|
69
|
+
yield "[green3]" + line + "[/green3]"
|
|
70
|
+
elif line.startswith("-"):
|
|
71
|
+
yield "[red3]" + line + "[/red3]"
|
|
72
|
+
elif line.startswith("^"):
|
|
73
|
+
yield "[blue3]" + line + "[/blue3]"
|
|
74
|
+
elif line.startswith("?"):
|
|
75
|
+
yield "[grey37]" + line + "[/grey37]"
|
|
76
|
+
else:
|
|
77
|
+
yield line
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def print_failed_case(fail: "TestResult") -> None:
|
|
81
|
+
"""Print the details of a failed test case.
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> print_failed_case(TestResult(TestCase([TestStep()]), True,
|
|
85
|
+
... ["- Hello", "+ World"]))
|
|
86
|
+
---------------------- test in test.md failed ----------------------
|
|
87
|
+
Mismatch starting at test.md:1:
|
|
88
|
+
<ansired>- Hello</ansired>
|
|
89
|
+
<ansigreen>+ World</ansigreen>
|
|
90
|
+
"""
|
|
91
|
+
fail_headline = (
|
|
92
|
+
f"'{fail.test_case.name}' in {fail.test_case.file_with_line()} failed"
|
|
93
|
+
)
|
|
94
|
+
rasa.shared.utils.cli.print_error(
|
|
95
|
+
f"{rasa.shared.utils.cli.pad(fail_headline, char='-')}\n"
|
|
96
|
+
)
|
|
97
|
+
rasa.shared.utils.cli.print_error(
|
|
98
|
+
f"Mismatch starting at {fail.test_case.file}:{fail.error_line}: \n"
|
|
99
|
+
)
|
|
100
|
+
if fail.difference:
|
|
101
|
+
rich.print(("\n".join(color_difference(fail.difference))))
|
|
102
|
+
|
|
103
|
+
if fail.assertion_failure:
|
|
104
|
+
rasa.shared.utils.cli.print_error(
|
|
105
|
+
f"Assertion type '{fail.assertion_failure.assertion.type()}' failed "
|
|
106
|
+
f"with this error message: {fail.assertion_failure.error_message}\n"
|
|
107
|
+
)
|
|
108
|
+
rasa.shared.utils.cli.print_error("Actual events transcript:\n")
|
|
109
|
+
rasa.shared.utils.cli.print_error(
|
|
110
|
+
"\n".join(fail.assertion_failure.actual_events_transcript)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def print_test_summary(failed: List["TestResult"]) -> None:
|
|
115
|
+
"""Print the summary of the test run.
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> print_test_summary([TestResult(TestCase([TestStep()]), True,
|
|
119
|
+
... ["- Hello", "+ World"])])
|
|
120
|
+
=================== short test summary info ===================
|
|
121
|
+
FAILED test.md::test
|
|
122
|
+
"""
|
|
123
|
+
rasa.shared.utils.cli.print_info(
|
|
124
|
+
rasa.shared.utils.cli.pad("short test summary info")
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
for f in failed:
|
|
128
|
+
rasa.shared.utils.cli.print_error(
|
|
129
|
+
f"FAILED {f.test_case.file}::{f.test_case.name}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def print_final_line(
|
|
134
|
+
passed: List["TestResult"], failed: List["TestResult"], has_failed: bool
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Print the final line of the test output.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
passed: List of passed test cases.
|
|
140
|
+
failed: List of failed test cases.
|
|
141
|
+
has_failed: Boolean, true if the test run has failed.
|
|
142
|
+
"""
|
|
143
|
+
final_line_color = "green3" if not has_failed else "red3"
|
|
144
|
+
|
|
145
|
+
width = shutil.get_terminal_size((80, 20)).columns
|
|
146
|
+
|
|
147
|
+
# calculate the length of the text - this is a bit hacky but works
|
|
148
|
+
text_lengt = (
|
|
149
|
+
math.ceil(len(passed) / 10) # length of the number of passed tests
|
|
150
|
+
+ math.ceil(len(failed) / 10) # length of the number of failed tests
|
|
151
|
+
+ 18 # length of the text " failed, passed "
|
|
152
|
+
)
|
|
153
|
+
# we can't use the padding function here as the text contains html tags
|
|
154
|
+
# which are not taken into account when calculating the length
|
|
155
|
+
padding = max(6, width - text_lengt)
|
|
156
|
+
pre_pad = "=" * max(3, padding // 2)
|
|
157
|
+
post_pad = "=" * max(3, math.ceil(padding / 2))
|
|
158
|
+
rich.print(
|
|
159
|
+
f"[{final_line_color}]{pre_pad} "
|
|
160
|
+
f"[bold red3]{len(failed)} failed[/bold red3]"
|
|
161
|
+
f"[bright_white], [/bright_white]"
|
|
162
|
+
f"[bold green3]{len(passed)} passed[/bold green3]"
|
|
163
|
+
f" {post_pad}[/{final_line_color}]"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def print_aggregate_stats(accuracy_calculations: List["AccuracyCalculation"]) -> None:
|
|
168
|
+
"""Print the aggregate statistics of the test run."""
|
|
169
|
+
rasa.shared.utils.cli.print_info(
|
|
170
|
+
rasa.shared.utils.cli.pad("Accuracy By Assertion Type")
|
|
171
|
+
)
|
|
172
|
+
table = Table()
|
|
173
|
+
table.add_column("Assertion Type", justify="center", style="cyan")
|
|
174
|
+
table.add_column("Accuracy", justify="center", style="cyan")
|
|
175
|
+
|
|
176
|
+
for accuracy_calculation in accuracy_calculations:
|
|
177
|
+
table.add_row(
|
|
178
|
+
accuracy_calculation.assertion_type, f"{accuracy_calculation.accuracy:.2%}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
rich.print(table)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def print_test_result(
|
|
185
|
+
passed: List["TestResult"],
|
|
186
|
+
failed: List["TestResult"],
|
|
187
|
+
fail_fast: bool = False,
|
|
188
|
+
**kwargs: Any,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Print the result of the test run.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
passed: List of passed test cases.
|
|
194
|
+
failed: List of failed test cases.
|
|
195
|
+
fail_fast: If true, stop after the first failure.
|
|
196
|
+
**kwargs: additional arguments
|
|
197
|
+
"""
|
|
198
|
+
if failed:
|
|
199
|
+
# print failure headline
|
|
200
|
+
print("\n")
|
|
201
|
+
rich.print(f"[bold]{rasa.shared.utils.cli.pad('FAILURES', char='=')}[/bold]")
|
|
202
|
+
|
|
203
|
+
# print failed test_Case
|
|
204
|
+
for fail in failed:
|
|
205
|
+
print_failed_case(fail)
|
|
206
|
+
|
|
207
|
+
accuracy_calculations = kwargs.get("accuracy_calculations", [])
|
|
208
|
+
if accuracy_calculations:
|
|
209
|
+
print_aggregate_stats(accuracy_calculations)
|
|
210
|
+
|
|
211
|
+
print_test_summary(failed)
|
|
212
|
+
|
|
213
|
+
if fail_fast:
|
|
214
|
+
rasa.shared.utils.cli.print_error(
|
|
215
|
+
rasa.shared.utils.cli.pad("stopping after 1 failure", char="!")
|
|
216
|
+
)
|
|
217
|
+
has_failed = True
|
|
218
|
+
elif len(failed) + len(passed) == 0:
|
|
219
|
+
# no tests were run, print error
|
|
220
|
+
rasa.shared.utils.cli.print_error(
|
|
221
|
+
rasa.shared.utils.cli.pad("no test cases found", char="!")
|
|
222
|
+
)
|
|
223
|
+
print_e2e_help()
|
|
224
|
+
has_failed = True
|
|
225
|
+
elif failed:
|
|
226
|
+
has_failed = True
|
|
227
|
+
else:
|
|
228
|
+
has_failed = False
|
|
229
|
+
|
|
230
|
+
print_final_line(passed, failed, has_failed=has_failed)
|
|
231
|
+
sys.exit(1 if has_failed else 0)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def print_e2e_help() -> None:
|
|
235
|
+
"""Print help guiding users how to write e2e tests."""
|
|
236
|
+
rasa.shared.utils.cli.print_info(
|
|
237
|
+
dedent(
|
|
238
|
+
"""\
|
|
239
|
+
To start using e2e tests create a yaml file in a test directory, e.g.
|
|
240
|
+
'tests/test_cases.yml'. You can find example test cases in the starter
|
|
241
|
+
pack at
|
|
242
|
+
|
|
243
|
+
https://github.com/RasaHQ/starter-pack-intentless-policy#testing-the-policy
|
|
244
|
+
|
|
245
|
+
Here is an example of a test case in yaml format:
|
|
246
|
+
|
|
247
|
+
test_cases:
|
|
248
|
+
- test_case: "test_greet"
|
|
249
|
+
steps:
|
|
250
|
+
- user: "hello there!"
|
|
251
|
+
- bot: "Hey! How are you?"
|
|
252
|
+
|
|
253
|
+
To run the e2e tests, execute:
|
|
254
|
+
>>> rasa test e2e <path-to-test-cases>
|
|
255
|
+
"""
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def split_into_passed_failed(
|
|
261
|
+
results: List["TestResult"],
|
|
262
|
+
) -> Tuple[List["TestResult"], List["TestResult"]]:
|
|
263
|
+
"""Get the summary of the test results.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
results: List of test results.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Tuple consisting of passed count, failed count and failed test cases.
|
|
270
|
+
"""
|
|
271
|
+
passed_cases = [r for r in results if r.pass_status]
|
|
272
|
+
failed_cases = [r for r in results if not r.pass_status]
|
|
273
|
+
|
|
274
|
+
return passed_cases, failed_cases
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def has_test_case_with_assertions(test_cases: List["TestCase"]) -> bool:
|
|
278
|
+
"""Check if the test cases contain assertions."""
|
|
279
|
+
try:
|
|
280
|
+
next(test_case for test_case in test_cases if test_case.uses_assertions())
|
|
281
|
+
except StopIteration:
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@lru_cache(maxsize=1)
|
|
288
|
+
def extract_test_case_from_path(path: str) -> Tuple[str, str]:
|
|
289
|
+
"""Extract test case from path if specified.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
path: Path to the file or folder containing test cases.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Tuple consisting of the path to test cases and the extracted test case name.
|
|
296
|
+
"""
|
|
297
|
+
test_case_name = ""
|
|
298
|
+
|
|
299
|
+
if "::" in str(path):
|
|
300
|
+
splitted_path = path.split("::")
|
|
301
|
+
test_case_name = splitted_path[-1]
|
|
302
|
+
path = splitted_path[0]
|
|
303
|
+
|
|
304
|
+
return path, test_case_name
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def is_test_case_file(file_path: Union[str, Path]) -> bool:
|
|
308
|
+
"""Check if file contains test cases.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
file_path: Path of the file to check.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
`True` if the file contains test cases, `False` otherwise.
|
|
315
|
+
"""
|
|
316
|
+
return rasa.shared.data.is_likely_yaml_file(file_path) and is_key_in_yaml(
|
|
317
|
+
file_path, KEY_TEST_CASES
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def read_test_cases(path: str) -> TestSuite:
|
|
322
|
+
"""Read test cases from the given path.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
path: Path to the file or folder containing test cases.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
TestSuite.
|
|
329
|
+
"""
|
|
330
|
+
from rasa.e2e_test.stub_custom_action import (
|
|
331
|
+
StubCustomAction,
|
|
332
|
+
get_stub_custom_action_key,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
path, test_case_name = extract_test_case_from_path(path)
|
|
336
|
+
validate_path_to_test_cases(path)
|
|
337
|
+
|
|
338
|
+
test_files = rasa.shared.data.get_data_files([path], is_test_case_file)
|
|
339
|
+
e2e_test_schema = read_e2e_test_schema()
|
|
340
|
+
|
|
341
|
+
input_test_cases = []
|
|
342
|
+
fixtures: Dict[str, Fixture] = {}
|
|
343
|
+
metadata: Dict[str, Metadata] = {}
|
|
344
|
+
stub_custom_actions: Dict[str, StubCustomAction] = {}
|
|
345
|
+
|
|
346
|
+
beta_flag_verified = False
|
|
347
|
+
|
|
348
|
+
for test_file in test_files:
|
|
349
|
+
test_file_content = parse_raw_yaml(Path(test_file).read_text(encoding="utf-8"))
|
|
350
|
+
|
|
351
|
+
validate_yaml_data_using_schema_with_assertions(
|
|
352
|
+
yaml_data=test_file_content, schema_content=e2e_test_schema
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
test_cases_content = test_file_content.get(KEY_TEST_CASES) or []
|
|
356
|
+
|
|
357
|
+
if test_case_name:
|
|
358
|
+
test_cases = [
|
|
359
|
+
TestCase.from_dict(test_case_dict, file=test_file)
|
|
360
|
+
for test_case_dict in test_cases_content
|
|
361
|
+
if test_case_name == test_case_dict.get(KEY_TEST_CASE)
|
|
362
|
+
]
|
|
363
|
+
else:
|
|
364
|
+
test_cases = [
|
|
365
|
+
TestCase.from_dict(test_case_dict, file=test_file)
|
|
366
|
+
for test_case_dict in test_cases_content
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
beta_flag_verified = verify_beta_feature_flag_for_assertions(
|
|
370
|
+
test_cases, beta_flag_verified
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
input_test_cases.extend(test_cases)
|
|
374
|
+
fixtures_content = test_file_content.get(KEY_FIXTURES) or []
|
|
375
|
+
for fixture in fixtures_content:
|
|
376
|
+
fixture_obj = Fixture.from_dict(fixture_dict=fixture)
|
|
377
|
+
|
|
378
|
+
# avoid adding duplicates from across multiple files
|
|
379
|
+
if fixtures.get(fixture_obj.name) is None:
|
|
380
|
+
fixtures[fixture_obj.name] = fixture_obj
|
|
381
|
+
|
|
382
|
+
metadata_contents = test_file_content.get(KEY_METADATA) or []
|
|
383
|
+
for metadata_content in metadata_contents:
|
|
384
|
+
metadata_obj = Metadata.from_dict(metadata_dict=metadata_content)
|
|
385
|
+
|
|
386
|
+
# avoid adding duplicates from across multiple files
|
|
387
|
+
if metadata.get(metadata_obj.name) is None:
|
|
388
|
+
metadata[metadata_obj.name] = metadata_obj
|
|
389
|
+
|
|
390
|
+
stub_custom_actions_contents = (
|
|
391
|
+
test_file_content.get(KEY_STUB_CUSTOM_ACTIONS) or {}
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
for action_name, stub_data in stub_custom_actions_contents.items():
|
|
395
|
+
if STUB_CUSTOM_ACTION_NAME_SEPARATOR in action_name:
|
|
396
|
+
stub_custom_action_key = action_name
|
|
397
|
+
else:
|
|
398
|
+
test_file_name = Path(test_file).name
|
|
399
|
+
stub_custom_action_key = get_stub_custom_action_key(
|
|
400
|
+
test_file_name, action_name
|
|
401
|
+
)
|
|
402
|
+
stub_custom_actions[stub_custom_action_key] = StubCustomAction.from_dict(
|
|
403
|
+
action_name=action_name,
|
|
404
|
+
stub_data=stub_data,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
validate_test_case(test_case_name, input_test_cases)
|
|
408
|
+
try:
|
|
409
|
+
if stub_custom_actions:
|
|
410
|
+
ensure_beta_feature_is_enabled(
|
|
411
|
+
"enabling stubs for custom actions",
|
|
412
|
+
RASA_PRO_BETA_STUB_CUSTOM_ACTION_ENV_VAR_NAME,
|
|
413
|
+
)
|
|
414
|
+
except BetaNotEnabledException as exc:
|
|
415
|
+
rasa.shared.utils.cli.print_error_and_exit(str(exc))
|
|
416
|
+
|
|
417
|
+
return TestSuite(
|
|
418
|
+
input_test_cases,
|
|
419
|
+
list(fixtures.values()),
|
|
420
|
+
list(metadata.values()),
|
|
421
|
+
stub_custom_actions,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def verify_beta_feature_flag_for_assertions(
|
|
426
|
+
test_cases: List["TestCase"], beta_flag_verified: bool
|
|
427
|
+
) -> bool:
|
|
428
|
+
"""Verify the beta feature flag for assertions."""
|
|
429
|
+
if beta_flag_verified:
|
|
430
|
+
return True
|
|
431
|
+
|
|
432
|
+
if not has_test_case_with_assertions(test_cases):
|
|
433
|
+
return beta_flag_verified
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
ensure_beta_feature_is_enabled(
|
|
437
|
+
"end-to-end testing with assertions",
|
|
438
|
+
RASA_PRO_BETA_E2E_ASSERTIONS_ENV_VAR_NAME,
|
|
439
|
+
)
|
|
440
|
+
except BetaNotEnabledException as exc:
|
|
441
|
+
rasa.shared.utils.cli.print_error_and_exit(str(exc))
|
|
442
|
+
|
|
443
|
+
return True
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _save_coverage_report(
|
|
447
|
+
report: Optional[pd.DataFrame], test_status: str, output_dir: str
|
|
448
|
+
) -> None:
|
|
449
|
+
if report is None:
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
if report is not None and not report.empty:
|
|
453
|
+
if test_status == STATUS_PASSED:
|
|
454
|
+
rasa.shared.utils.cli.print_success(report.to_string(index=False))
|
|
455
|
+
else:
|
|
456
|
+
rasa.shared.utils.cli.print_error(report.to_string(index=False))
|
|
457
|
+
|
|
458
|
+
output_filename = f"coverage_report_for_{test_status}_tests.csv"
|
|
459
|
+
output_file_path = os.path.join(output_dir, output_filename)
|
|
460
|
+
report.to_csv(output_file_path, index=False)
|
|
461
|
+
structlogger.info(
|
|
462
|
+
"rasa.e2e_test.save_coverage_report",
|
|
463
|
+
message=f"Coverage result for {test_status} e2e tests"
|
|
464
|
+
f" is written to '{output_file_path}'.",
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def write_test_results_to_file(results: List["TestResult"], output_file: str) -> None:
|
|
469
|
+
"""Write test results to a file.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
results: List of test results.
|
|
473
|
+
output_file: Path to the output file.
|
|
474
|
+
"""
|
|
475
|
+
Path(output_file).touch()
|
|
476
|
+
|
|
477
|
+
data = {"test_results": [test_result.as_dict() for test_result in results]}
|
|
478
|
+
|
|
479
|
+
rasa.utils.io.write_yaml(
|
|
480
|
+
data, target=output_file, transform=transform_results_output_to_yaml
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
if STATUS_PASSED in output_file:
|
|
484
|
+
rasa.shared.utils.cli.print_info(
|
|
485
|
+
f"Passing test results have been saved at path: {output_file}."
|
|
486
|
+
)
|
|
487
|
+
elif STATUS_FAILED in output_file:
|
|
488
|
+
rasa.shared.utils.cli.print_info(
|
|
489
|
+
f"Failing test results have been saved at path: {output_file}."
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def transform_results_output_to_yaml(yaml_string: str) -> str:
|
|
494
|
+
"""Transform the output of the YAML writer to make it more readable.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
yaml_string: The YAML string to transform.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
The transformed YAML string.
|
|
501
|
+
"""
|
|
502
|
+
result = []
|
|
503
|
+
for s in yaml_string.splitlines(True):
|
|
504
|
+
if s.startswith("- name"):
|
|
505
|
+
result.append("\n")
|
|
506
|
+
result.append(s)
|
|
507
|
+
elif s.startswith("\n"):
|
|
508
|
+
result.append(s.strip())
|
|
509
|
+
elif s.strip().startswith("#"):
|
|
510
|
+
continue
|
|
511
|
+
else:
|
|
512
|
+
result.append(s)
|
|
513
|
+
return "".join(result)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _save_tested_commands_histogram(
|
|
517
|
+
count_dict: Dict[str, int], test_status: str, output_dir: str
|
|
518
|
+
) -> None:
|
|
519
|
+
"""Creates a command histogram and saves it to the specified directory.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
count_dict (Dict[str, int]): A dictionary where keys are commands
|
|
523
|
+
and values are counts.
|
|
524
|
+
test_status (str): passing or failing
|
|
525
|
+
output_dir (str): The directory path where the histogram
|
|
526
|
+
image will be saved.
|
|
527
|
+
"""
|
|
528
|
+
if not count_dict:
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
# Sort the dictionary by keys
|
|
532
|
+
sorted_count_dict = dict(sorted(count_dict.items()))
|
|
533
|
+
|
|
534
|
+
plt.figure(figsize=(10, 6))
|
|
535
|
+
bars = plt.bar(sorted_count_dict.keys(), sorted_count_dict.values(), color="blue")
|
|
536
|
+
plt.xlabel("Commands")
|
|
537
|
+
plt.ylabel("Counts")
|
|
538
|
+
plt.title(f"Command histogram for {test_status} tests")
|
|
539
|
+
plt.xticks(rotation=45, ha="right")
|
|
540
|
+
plt.tight_layout()
|
|
541
|
+
|
|
542
|
+
# Add total number to each bar
|
|
543
|
+
for bar in bars:
|
|
544
|
+
yval = bar.get_height()
|
|
545
|
+
plt.text(
|
|
546
|
+
bar.get_x() + bar.get_width() / 2,
|
|
547
|
+
yval + 0.5,
|
|
548
|
+
int(yval),
|
|
549
|
+
ha="center",
|
|
550
|
+
va="bottom",
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
output_filename = f"commands_histogram_for_{test_status}_tests.png"
|
|
554
|
+
output_file_path = pathlib.Path().joinpath(output_dir, output_filename)
|
|
555
|
+
plt.savefig(output_file_path)
|
|
556
|
+
plt.close()
|
|
557
|
+
|
|
558
|
+
structlogger.info(
|
|
559
|
+
"rasa.e2e_test._save_tested_commands_histogram",
|
|
560
|
+
message=f"Commands histogram for {test_status} e2e tests "
|
|
561
|
+
f"are written to '{output_file_path}'.",
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def save_test_cases_to_yaml(
|
|
566
|
+
test_results: List["TestResult"],
|
|
567
|
+
output_dir: str,
|
|
568
|
+
status: str,
|
|
569
|
+
test_suite: TestSuite,
|
|
570
|
+
) -> None:
|
|
571
|
+
"""Extracts TestCases from a list of TestResults and saves them to a YAML file."""
|
|
572
|
+
if not test_results:
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
test_cases = [result.test_case for result in test_results]
|
|
576
|
+
new_test_suite = TestSuite(
|
|
577
|
+
test_cases=test_cases,
|
|
578
|
+
fixtures=test_suite.fixtures,
|
|
579
|
+
metadata=test_suite.metadata,
|
|
580
|
+
stub_custom_actions=test_suite.stub_custom_actions,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
output_filename = f"{status}.yml"
|
|
584
|
+
output_file_path = os.path.join(output_dir, output_filename)
|
|
585
|
+
rasa.utils.io.write_yaml(new_test_suite.as_dict(), target=output_file_path)
|
|
586
|
+
|
|
587
|
+
structlogger.info(
|
|
588
|
+
"rasa.e2e_test.save_e2e_test_cases",
|
|
589
|
+
message=f"E2e tests with '{status}' status are written to file: "
|
|
590
|
+
f"'{output_file_path}'.",
|
|
591
|
+
)
|
|
592
|
+
if status == STATUS_PASSED:
|
|
593
|
+
structlogger.info(
|
|
594
|
+
"rasa.e2e_test.save_e2e_test_cases",
|
|
595
|
+
message=f"You can use the file: '{output_file_path}' in case you want to "
|
|
596
|
+
f"create training data for fine-tuning an LLM via "
|
|
597
|
+
f"'rasa llm finetune prepare-data'.",
|
|
598
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union
|
|
4
|
+
|
|
5
|
+
import structlog
|
|
6
|
+
|
|
7
|
+
import rasa.shared.utils.io
|
|
8
|
+
from rasa.e2e_test.constants import SCHEMA_FILE_PATH
|
|
9
|
+
from rasa.shared.utils.yaml import read_schema_file
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from rasa.e2e_test.e2e_test_case import TestCase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
structlogger = structlog.get_logger()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_path_to_test_cases(path: str) -> None:
|
|
19
|
+
"""Validate that path to test cases exists."""
|
|
20
|
+
if not Path(path).exists():
|
|
21
|
+
rasa.shared.utils.io.raise_warning(
|
|
22
|
+
f"Path to test cases does not exist: {path}. "
|
|
23
|
+
f"Please provide a valid path to test cases. "
|
|
24
|
+
f"Exiting...",
|
|
25
|
+
UserWarning,
|
|
26
|
+
)
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_test_case(test_case_name: str, input_test_cases: List["TestCase"]) -> None:
|
|
31
|
+
"""Validate that test case exists."""
|
|
32
|
+
if test_case_name and not input_test_cases:
|
|
33
|
+
rasa.shared.utils.io.raise_warning(
|
|
34
|
+
f"Test case does not exist: {test_case_name}. "
|
|
35
|
+
f"Please check for typos and provide a valid test case name. "
|
|
36
|
+
f"Exiting...",
|
|
37
|
+
UserWarning,
|
|
38
|
+
)
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_model_path(model_path: Optional[str], parameter: str, default: str) -> str:
|
|
43
|
+
"""Validate the model path.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
model_path: Path to the model.
|
|
47
|
+
parameter: Name of the parameter.
|
|
48
|
+
default: Default path to the model.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Path to the model.
|
|
52
|
+
"""
|
|
53
|
+
if model_path and Path(model_path).exists():
|
|
54
|
+
return model_path
|
|
55
|
+
|
|
56
|
+
if model_path and not Path(model_path).exists():
|
|
57
|
+
rasa.shared.utils.io.raise_warning(
|
|
58
|
+
f"The provided model path '{model_path}' could not be found. "
|
|
59
|
+
f"Using default location '{default}' instead.",
|
|
60
|
+
UserWarning,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
elif model_path is None:
|
|
64
|
+
structlogger.info(
|
|
65
|
+
"rasa.e2e_test.validate_model_path",
|
|
66
|
+
message=f"Parameter '{parameter}' is not set. "
|
|
67
|
+
f"Using default location '{default}' instead.",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
Path(default).mkdir(exist_ok=True)
|
|
71
|
+
return default
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def read_e2e_test_schema() -> Union[List[Any], Dict[str, Any]]:
|
|
75
|
+
"""Read the schema for the e2e test files.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
The content of the schema.
|
|
79
|
+
"""
|
|
80
|
+
return read_schema_file(SCHEMA_FILE_PATH)
|
rasa/engine/graph.py
CHANGED
|
@@ -67,6 +67,14 @@ class SchemaNode:
|
|
|
67
67
|
is_input: bool = False
|
|
68
68
|
resource: Optional[Resource] = None
|
|
69
69
|
|
|
70
|
+
def matches_type(self, node_type: Type, include_subtypes: bool = True) -> bool:
|
|
71
|
+
"""Checks if schema node's 'uses' is of specified node type.
|
|
72
|
+
By default, it also checks for subtypes of the specified node type.
|
|
73
|
+
"""
|
|
74
|
+
return (self.uses is node_type) or (
|
|
75
|
+
include_subtypes and issubclass(self.uses, node_type)
|
|
76
|
+
)
|
|
77
|
+
|
|
70
78
|
|
|
71
79
|
@dataclass
|
|
72
80
|
class GraphSchema:
|
|
@@ -166,9 +174,7 @@ class GraphSchema:
|
|
|
166
174
|
By default, it also checks for subtypes of the specified node type.
|
|
167
175
|
"""
|
|
168
176
|
for node in self.nodes.values():
|
|
169
|
-
if
|
|
170
|
-
include_subtypes and issubclass(node.uses, node_type)
|
|
171
|
-
):
|
|
177
|
+
if node.matches_type(node_type, include_subtypes):
|
|
172
178
|
return True
|
|
173
179
|
return False
|
|
174
180
|
|