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.

Files changed (183) hide show
  1. README.md +0 -374
  2. rasa/__init__.py +1 -2
  3. rasa/__main__.py +5 -0
  4. rasa/anonymization/anonymization_rule_executor.py +2 -2
  5. rasa/api.py +27 -23
  6. rasa/cli/arguments/data.py +27 -2
  7. rasa/cli/arguments/default_arguments.py +25 -3
  8. rasa/cli/arguments/run.py +9 -9
  9. rasa/cli/arguments/train.py +11 -3
  10. rasa/cli/data.py +70 -8
  11. rasa/cli/e2e_test.py +104 -431
  12. rasa/cli/evaluate.py +1 -1
  13. rasa/cli/interactive.py +1 -0
  14. rasa/cli/llm_fine_tuning.py +398 -0
  15. rasa/cli/project_templates/calm/endpoints.yml +1 -1
  16. rasa/cli/project_templates/tutorial/endpoints.yml +1 -1
  17. rasa/cli/run.py +15 -14
  18. rasa/cli/scaffold.py +10 -8
  19. rasa/cli/studio/studio.py +35 -5
  20. rasa/cli/train.py +56 -8
  21. rasa/cli/utils.py +22 -5
  22. rasa/cli/x.py +1 -1
  23. rasa/constants.py +7 -1
  24. rasa/core/actions/action.py +98 -49
  25. rasa/core/actions/action_run_slot_rejections.py +4 -1
  26. rasa/core/actions/custom_action_executor.py +9 -6
  27. rasa/core/actions/direct_custom_actions_executor.py +80 -0
  28. rasa/core/actions/e2e_stub_custom_action_executor.py +68 -0
  29. rasa/core/actions/grpc_custom_action_executor.py +2 -2
  30. rasa/core/actions/http_custom_action_executor.py +6 -5
  31. rasa/core/agent.py +21 -17
  32. rasa/core/channels/__init__.py +2 -0
  33. rasa/core/channels/audiocodes.py +1 -16
  34. rasa/core/channels/voice_aware/__init__.py +0 -0
  35. rasa/core/channels/voice_aware/jambonz.py +103 -0
  36. rasa/core/channels/voice_aware/jambonz_protocol.py +344 -0
  37. rasa/core/channels/voice_aware/utils.py +20 -0
  38. rasa/core/channels/voice_native/__init__.py +0 -0
  39. rasa/core/constants.py +6 -1
  40. rasa/core/information_retrieval/faiss.py +7 -4
  41. rasa/core/information_retrieval/information_retrieval.py +8 -0
  42. rasa/core/information_retrieval/milvus.py +9 -2
  43. rasa/core/information_retrieval/qdrant.py +1 -1
  44. rasa/core/nlg/contextual_response_rephraser.py +32 -10
  45. rasa/core/nlg/summarize.py +4 -3
  46. rasa/core/policies/enterprise_search_policy.py +113 -45
  47. rasa/core/policies/flows/flow_executor.py +122 -76
  48. rasa/core/policies/intentless_policy.py +83 -29
  49. rasa/core/processor.py +72 -54
  50. rasa/core/run.py +5 -4
  51. rasa/core/tracker_store.py +8 -4
  52. rasa/core/training/interactive.py +1 -1
  53. rasa/core/utils.py +56 -57
  54. rasa/dialogue_understanding/coexistence/llm_based_router.py +53 -13
  55. rasa/dialogue_understanding/commands/__init__.py +6 -0
  56. rasa/dialogue_understanding/commands/restart_command.py +58 -0
  57. rasa/dialogue_understanding/commands/session_start_command.py +59 -0
  58. rasa/dialogue_understanding/commands/utils.py +40 -0
  59. rasa/dialogue_understanding/generator/constants.py +10 -3
  60. rasa/dialogue_understanding/generator/flow_retrieval.py +21 -5
  61. rasa/dialogue_understanding/generator/llm_based_command_generator.py +13 -3
  62. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +134 -90
  63. rasa/dialogue_understanding/generator/nlu_command_adapter.py +47 -7
  64. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +127 -41
  65. rasa/dialogue_understanding/patterns/restart.py +37 -0
  66. rasa/dialogue_understanding/patterns/session_start.py +37 -0
  67. rasa/dialogue_understanding/processor/command_processor.py +16 -3
  68. rasa/dialogue_understanding/processor/command_processor_component.py +6 -2
  69. rasa/e2e_test/aggregate_test_stats_calculator.py +134 -0
  70. rasa/e2e_test/assertions.py +1223 -0
  71. rasa/e2e_test/assertions_schema.yml +106 -0
  72. rasa/e2e_test/constants.py +20 -0
  73. rasa/e2e_test/e2e_config.py +220 -0
  74. rasa/e2e_test/e2e_config_schema.yml +26 -0
  75. rasa/e2e_test/e2e_test_case.py +131 -8
  76. rasa/e2e_test/e2e_test_converter.py +363 -0
  77. rasa/e2e_test/e2e_test_converter_prompt.jinja2 +70 -0
  78. rasa/e2e_test/e2e_test_coverage_report.py +364 -0
  79. rasa/e2e_test/e2e_test_result.py +26 -6
  80. rasa/e2e_test/e2e_test_runner.py +493 -71
  81. rasa/e2e_test/e2e_test_schema.yml +96 -0
  82. rasa/e2e_test/pykwalify_extensions.py +39 -0
  83. rasa/e2e_test/stub_custom_action.py +70 -0
  84. rasa/e2e_test/utils/__init__.py +0 -0
  85. rasa/e2e_test/utils/e2e_yaml_utils.py +55 -0
  86. rasa/e2e_test/utils/io.py +598 -0
  87. rasa/e2e_test/utils/validation.py +80 -0
  88. rasa/engine/graph.py +9 -3
  89. rasa/engine/recipes/default_components.py +0 -2
  90. rasa/engine/recipes/default_recipe.py +10 -2
  91. rasa/engine/storage/local_model_storage.py +40 -12
  92. rasa/engine/validation.py +78 -1
  93. rasa/env.py +9 -0
  94. rasa/graph_components/providers/story_graph_provider.py +59 -6
  95. rasa/llm_fine_tuning/__init__.py +0 -0
  96. rasa/llm_fine_tuning/annotation_module.py +241 -0
  97. rasa/llm_fine_tuning/conversations.py +144 -0
  98. rasa/llm_fine_tuning/llm_data_preparation_module.py +178 -0
  99. rasa/llm_fine_tuning/notebooks/unsloth_finetuning.ipynb +407 -0
  100. rasa/llm_fine_tuning/paraphrasing/__init__.py +0 -0
  101. rasa/llm_fine_tuning/paraphrasing/conversation_rephraser.py +281 -0
  102. rasa/llm_fine_tuning/paraphrasing/default_rephrase_prompt_template.jina2 +44 -0
  103. rasa/llm_fine_tuning/paraphrasing/rephrase_validator.py +121 -0
  104. rasa/llm_fine_tuning/paraphrasing/rephrased_user_message.py +10 -0
  105. rasa/llm_fine_tuning/paraphrasing_module.py +128 -0
  106. rasa/llm_fine_tuning/storage.py +174 -0
  107. rasa/llm_fine_tuning/train_test_split_module.py +441 -0
  108. rasa/model_training.py +56 -16
  109. rasa/nlu/persistor.py +157 -36
  110. rasa/server.py +45 -10
  111. rasa/shared/constants.py +76 -16
  112. rasa/shared/core/domain.py +27 -19
  113. rasa/shared/core/events.py +28 -2
  114. rasa/shared/core/flows/flow.py +208 -13
  115. rasa/shared/core/flows/flow_path.py +84 -0
  116. rasa/shared/core/flows/flows_list.py +33 -11
  117. rasa/shared/core/flows/flows_yaml_schema.json +269 -193
  118. rasa/shared/core/flows/validation.py +112 -25
  119. rasa/shared/core/flows/yaml_flows_io.py +149 -10
  120. rasa/shared/core/trackers.py +6 -0
  121. rasa/shared/core/training_data/structures.py +20 -0
  122. rasa/shared/core/training_data/visualization.html +2 -2
  123. rasa/shared/exceptions.py +4 -0
  124. rasa/shared/importers/importer.py +64 -16
  125. rasa/shared/nlu/constants.py +2 -0
  126. rasa/shared/providers/_configs/__init__.py +0 -0
  127. rasa/shared/providers/_configs/azure_openai_client_config.py +183 -0
  128. rasa/shared/providers/_configs/client_config.py +57 -0
  129. rasa/shared/providers/_configs/default_litellm_client_config.py +130 -0
  130. rasa/shared/providers/_configs/huggingface_local_embedding_client_config.py +234 -0
  131. rasa/shared/providers/_configs/openai_client_config.py +175 -0
  132. rasa/shared/providers/_configs/self_hosted_llm_client_config.py +176 -0
  133. rasa/shared/providers/_configs/utils.py +101 -0
  134. rasa/shared/providers/_ssl_verification_utils.py +124 -0
  135. rasa/shared/providers/embedding/__init__.py +0 -0
  136. rasa/shared/providers/embedding/_base_litellm_embedding_client.py +259 -0
  137. rasa/shared/providers/embedding/_langchain_embedding_client_adapter.py +74 -0
  138. rasa/shared/providers/embedding/azure_openai_embedding_client.py +277 -0
  139. rasa/shared/providers/embedding/default_litellm_embedding_client.py +102 -0
  140. rasa/shared/providers/embedding/embedding_client.py +90 -0
  141. rasa/shared/providers/embedding/embedding_response.py +41 -0
  142. rasa/shared/providers/embedding/huggingface_local_embedding_client.py +191 -0
  143. rasa/shared/providers/embedding/openai_embedding_client.py +172 -0
  144. rasa/shared/providers/llm/__init__.py +0 -0
  145. rasa/shared/providers/llm/_base_litellm_client.py +251 -0
  146. rasa/shared/providers/llm/azure_openai_llm_client.py +338 -0
  147. rasa/shared/providers/llm/default_litellm_llm_client.py +84 -0
  148. rasa/shared/providers/llm/llm_client.py +76 -0
  149. rasa/shared/providers/llm/llm_response.py +50 -0
  150. rasa/shared/providers/llm/openai_llm_client.py +155 -0
  151. rasa/shared/providers/llm/self_hosted_llm_client.py +293 -0
  152. rasa/shared/providers/mappings.py +75 -0
  153. rasa/shared/utils/cli.py +30 -0
  154. rasa/shared/utils/io.py +65 -2
  155. rasa/shared/utils/llm.py +246 -200
  156. rasa/shared/utils/yaml.py +121 -15
  157. rasa/studio/auth.py +6 -4
  158. rasa/studio/config.py +13 -4
  159. rasa/studio/constants.py +1 -0
  160. rasa/studio/data_handler.py +10 -3
  161. rasa/studio/download.py +19 -13
  162. rasa/studio/train.py +2 -3
  163. rasa/studio/upload.py +19 -11
  164. rasa/telemetry.py +113 -58
  165. rasa/tracing/instrumentation/attribute_extractors.py +32 -17
  166. rasa/utils/common.py +18 -19
  167. rasa/utils/endpoints.py +7 -4
  168. rasa/utils/json_utils.py +60 -0
  169. rasa/utils/licensing.py +9 -1
  170. rasa/utils/ml_utils.py +4 -2
  171. rasa/validator.py +213 -3
  172. rasa/version.py +1 -1
  173. rasa_pro-3.10.16.dist-info/METADATA +196 -0
  174. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/RECORD +179 -113
  175. rasa/nlu/classifiers/llm_intent_classifier.py +0 -519
  176. rasa/shared/providers/openai/clients.py +0 -43
  177. rasa/shared/providers/openai/session_handler.py +0 -110
  178. rasa_pro-3.9.18.dist-info/METADATA +0 -563
  179. /rasa/{shared/providers/openai → cli/project_templates/tutorial/actions}/__init__.py +0 -0
  180. /rasa/cli/project_templates/tutorial/{actions.py → actions/actions.py} +0 -0
  181. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/NOTICE +0 -0
  182. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/WHEEL +0 -0
  183. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,106 @@
1
+ schema;assertions:
2
+ extensions:
3
+ - pykwalify_extensions.py
4
+ func: require_assertion_keys
5
+ type: "seq"
6
+ matching: "any"
7
+ sequence:
8
+ - type: map
9
+ mapping:
10
+ flow_started:
11
+ type: str
12
+ nullable: false
13
+ flow_completed:
14
+ type: map
15
+ mapping:
16
+ flow_id:
17
+ type: str
18
+ required: true
19
+ flow_step_id:
20
+ type: str
21
+ flow_cancelled:
22
+ type: map
23
+ mapping:
24
+ flow_id:
25
+ type: str
26
+ required: true
27
+ flow_step_id:
28
+ type: str
29
+ pattern_clarification_contains:
30
+ type: seq
31
+ matching: "all"
32
+ nullable: false
33
+ sequence:
34
+ - type: str
35
+ nullable: false
36
+ action_executed:
37
+ type: str
38
+ nullable: false
39
+ slot_was_set:
40
+ type: seq
41
+ matching: "all"
42
+ nullable: false
43
+ sequence:
44
+ - type: map
45
+ mapping:
46
+ name:
47
+ type: str
48
+ required: true
49
+ value:
50
+ type: any
51
+ slot_was_not_set:
52
+ type: seq
53
+ matching: "all"
54
+ nullable: false
55
+ sequence:
56
+ - type: map
57
+ mapping:
58
+ name:
59
+ type: str
60
+ required: true
61
+ value:
62
+ type: any
63
+ bot_uttered:
64
+ type: map
65
+ nullable: false
66
+ mapping:
67
+ utter_name:
68
+ type: str
69
+ nullable: false
70
+ buttons:
71
+ type: seq
72
+ nullable: false
73
+ matching: "all"
74
+ sequence:
75
+ - type: map
76
+ mapping:
77
+ title:
78
+ type: str
79
+ nullable: false
80
+ payload:
81
+ type: str
82
+ nullable: false
83
+ text_matches:
84
+ type: str
85
+ nullable: false
86
+ generative_response_is_relevant:
87
+ type: map
88
+ mapping:
89
+ threshold:
90
+ type: float
91
+ required: true
92
+ utter_name:
93
+ type: str
94
+ nullable: false
95
+ generative_response_is_grounded:
96
+ type: map
97
+ mapping:
98
+ threshold:
99
+ type: float
100
+ required: true
101
+ utter_name:
102
+ type: str
103
+ nullable: false
104
+ ground_truth:
105
+ type: str
106
+ nullable: false
@@ -1,4 +1,8 @@
1
1
  SCHEMA_FILE_PATH = "e2e_test/e2e_test_schema.yml"
2
+ E2E_CONFIG_SCHEMA_FILE_PATH = "e2e_test/e2e_config_schema.yml"
3
+ TEST_FILE_NAME = "test_file_name"
4
+ TEST_CASE_NAME = "test_case_name"
5
+ STUB_CUSTOM_ACTION_NAME_SEPARATOR = "::"
2
6
 
3
7
  KEY_FIXTURES = "fixtures"
4
8
  KEY_USER_INPUT = "user"
@@ -8,4 +12,20 @@ KEY_SLOT_SET = "slot_was_set"
8
12
  KEY_SLOT_NOT_SET = "slot_was_not_set"
9
13
  KEY_STEPS = "steps"
10
14
  KEY_TEST_CASE = "test_case"
15
+ KEY_TEST_CASES = "test_cases"
11
16
  KEY_METADATA = "metadata"
17
+ KEY_ASSERTIONS = "assertions"
18
+ KEY_ASSERTION_ORDER_ENABLED = "assertion_order_enabled"
19
+ KEY_STUB_CUSTOM_ACTIONS = "stub_custom_actions"
20
+
21
+ KEY_MODEL = "model"
22
+ KEY_LLM_AS_JUDGE = "llm_as_judge"
23
+ KEY_LLM_E2E_TEST_CONVERSION = "llm_e2e_test_conversion"
24
+
25
+ DEFAULT_E2E_INPUT_TESTS_PATH = "tests/e2e_test_cases.yml"
26
+ DEFAULT_E2E_OUTPUT_TESTS_PATH = "tests/e2e_results.yml"
27
+ DEFAULT_COVERAGE_OUTPUT_PATH = "e2e_coverage_results"
28
+
29
+ # Test status
30
+ STATUS_PASSED = "passed"
31
+ STATUS_FAILED = "failed"
@@ -0,0 +1,220 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Generator, Optional, Dict, Any
7
+
8
+ import structlog
9
+ from pydantic import BaseModel
10
+
11
+ from rasa.e2e_test.constants import (
12
+ E2E_CONFIG_SCHEMA_FILE_PATH,
13
+ KEY_LLM_AS_JUDGE,
14
+ KEY_LLM_E2E_TEST_CONVERSION,
15
+ )
16
+ from rasa.shared.constants import (
17
+ API_BASE_CONFIG_KEY,
18
+ DEPLOYMENT_CONFIG_KEY,
19
+ MODEL_CONFIG_KEY,
20
+ OPENAI_PROVIDER,
21
+ PROVIDER_CONFIG_KEY,
22
+ )
23
+ from rasa.shared.exceptions import RasaException
24
+ from rasa.shared.utils.yaml import (
25
+ parse_raw_yaml,
26
+ read_schema_file,
27
+ validate_yaml_content_using_schema,
28
+ )
29
+
30
+ structlogger = structlog.get_logger()
31
+
32
+ CONFTEST_PATTERNS = ["conftest.yml", "conftest.yaml"]
33
+
34
+
35
+ class InvalidLLMConfiguration(RasaException):
36
+ """Exception raised when the LLM configuration is invalid."""
37
+
38
+ def __init__(self, error_message: str) -> None:
39
+ """Creates a `InvalidLLMConfiguration`."""
40
+ super().__init__(error_message)
41
+
42
+
43
+ @dataclass
44
+ class LLMJudgeConfig:
45
+ """Class for storing the configuration of the LLM-As-Judge.
46
+
47
+ The LLM-As-Judge is used to measure the factual accuracy
48
+ (i.e., how grounded in the source documents the response is),
49
+ or relevance of the generated response during E2E testing.
50
+ """
51
+
52
+ api_type: str = "openai"
53
+ model: str = "gpt-4o-mini"
54
+
55
+ @staticmethod
56
+ def from_dict(config_data: Dict[str, Any]) -> LLMJudgeConfig:
57
+ """Loads the configuration from a dictionary."""
58
+ llm_type = config_data.pop("api_type", "openai")
59
+ if llm_type != "openai":
60
+ raise InvalidLLMConfiguration(
61
+ f"Invalid LLM type '{llm_type}'. Only 'openai' is supported."
62
+ )
63
+
64
+ return LLMJudgeConfig(**config_data)
65
+
66
+ def as_dict(self) -> Dict[str, Any]:
67
+ return dataclasses.asdict(self)
68
+
69
+ def get_model_uri(self) -> str:
70
+ return f"{self.api_type}:/{self.model}"
71
+
72
+
73
+ class LLME2ETestConverterConfig(BaseModel):
74
+ """Class for storing the LLM configuration of the E2ETestConverter.
75
+
76
+ This configuration is used to initialize the LiteLLM client.
77
+ """
78
+
79
+ provider: Optional[str]
80
+ model: Optional[str]
81
+ deployment: Optional[str]
82
+ api_base: Optional[str]
83
+ extra_parameters: Optional[Dict[str, Any]]
84
+
85
+ @classmethod
86
+ def from_dict(cls, config_data: Dict[str, Any]) -> LLME2ETestConverterConfig:
87
+ """Loads the configuration from a dictionary."""
88
+ expected_fields = [
89
+ PROVIDER_CONFIG_KEY,
90
+ API_BASE_CONFIG_KEY,
91
+ DEPLOYMENT_CONFIG_KEY,
92
+ MODEL_CONFIG_KEY,
93
+ ]
94
+ kwargs = {
95
+ expected_field: config_data.pop(expected_field, None)
96
+ for expected_field in expected_fields
97
+ }
98
+ return cls(extra_parameters=config_data, **kwargs)
99
+
100
+ @classmethod
101
+ def get_default_config(cls) -> Dict[str, Any]:
102
+ return {PROVIDER_CONFIG_KEY: OPENAI_PROVIDER, MODEL_CONFIG_KEY: "gpt-4o-mini"}
103
+
104
+ @staticmethod
105
+ def _clean_up_config(config_data: Dict[str, Any]) -> Dict[str, Any]:
106
+ """Remove None values from the configuration."""
107
+ return {key: value for key, value in config_data.items() if value}
108
+
109
+ def as_dict(self) -> Dict[str, Any]:
110
+ return self._clean_up_config(dict(self))
111
+
112
+
113
+ def get_conftest_path(test_case_path: Optional[Path]) -> Optional[Path]:
114
+ """Get the path to the conftest.yml file.
115
+
116
+ This assumes that the conftest.yml file is in the assistant project.
117
+ """
118
+ if test_case_path is None:
119
+ return None
120
+
121
+ while True:
122
+ if test_case_path.is_file():
123
+ test_case_path = test_case_path.parent
124
+
125
+ matches = find_conftest_path(test_case_path)
126
+ try:
127
+ match = next(matches)
128
+ structlogger.debug("e2e_config.get_conftest_path.found", match=match)
129
+ return match
130
+ except StopIteration:
131
+ pass
132
+
133
+ plausible_config_paths = [
134
+ test_case_path / "config.yml",
135
+ test_case_path / "config",
136
+ ]
137
+ for plausible_config_path in plausible_config_paths:
138
+ if plausible_config_path.exists():
139
+ # we reached the root of the assistant project
140
+ return None
141
+ # In case of an invalid path outside the assistant project,
142
+ # break the loop if we reach the root
143
+ if test_case_path == Path("."):
144
+ return None
145
+
146
+ test_case_path = test_case_path.parent
147
+
148
+
149
+ def find_conftest_path(path: Path) -> Generator[Path, None, None]:
150
+ """Find the path to the conftest.yml file."""
151
+ for pattern in CONFTEST_PATTERNS:
152
+ for file_path in path.rglob(pattern):
153
+ yield file_path
154
+
155
+
156
+ def create_llm_judge_config(test_case_path: Optional[Path]) -> LLMJudgeConfig:
157
+ """Create the LLM-Judge configuration from the dictionary."""
158
+ config_data = read_conftest_file(test_case_path)
159
+ if not config_data:
160
+ structlogger.debug("e2e_config.create_llm_judge_config.no_conftest_detected")
161
+ return LLMJudgeConfig.from_dict(config_data)
162
+
163
+ llm_judge_config_data = config_data.get(KEY_LLM_AS_JUDGE, {})
164
+ if not llm_judge_config_data:
165
+ structlogger.debug("e2e_config.create_llm_judge_config.no_llm_as_judge_key")
166
+
167
+ structlogger.info(
168
+ "e2e_config.create_llm_judge_config.success",
169
+ llm_judge_config_data=llm_judge_config_data,
170
+ )
171
+
172
+ try:
173
+ return LLMJudgeConfig.from_dict(llm_judge_config_data)
174
+ except InvalidLLMConfiguration as e:
175
+ structlogger.error(
176
+ "e2e_config.create_llm_judge_config.invalid_llm_configuration",
177
+ error_message=str(e),
178
+ event_info="Falling back to default configuration.",
179
+ )
180
+ return LLMJudgeConfig()
181
+
182
+
183
+ def create_llm_e2e_test_converter_config(
184
+ config_path: Path,
185
+ ) -> LLME2ETestConverterConfig:
186
+ """Create the LLME2ETestConverterConfig configuration from the dictionary."""
187
+ config_data = read_conftest_file(config_path)
188
+ if not config_data:
189
+ structlogger.debug(
190
+ "e2e_config.create_llm_e2e_test_converter_config.no_conftest_detected"
191
+ )
192
+ return LLME2ETestConverterConfig.from_dict(config_data)
193
+
194
+ llm_e2e_test_converter_config_data = config_data.get(
195
+ KEY_LLM_E2E_TEST_CONVERSION, {}
196
+ )
197
+ if not llm_e2e_test_converter_config_data:
198
+ structlogger.debug(
199
+ "e2e_config.create_llm_e2e_test_converter_config.no_llm_e2e_test_converter_config_key"
200
+ )
201
+
202
+ structlogger.info(
203
+ "e2e_config.create_llm_e2e_test_converter_config.success",
204
+ llm_e2e_test_converter_config_data=llm_e2e_test_converter_config_data,
205
+ )
206
+
207
+ return LLME2ETestConverterConfig.from_dict(llm_e2e_test_converter_config_data)
208
+
209
+
210
+ def read_conftest_file(test_case_path: Optional[Path]) -> Dict[str, Any]:
211
+ """Read the conftest.yml file."""
212
+ conftest_path = get_conftest_path(test_case_path)
213
+ if conftest_path is None:
214
+ return {}
215
+
216
+ e2e_config_schema = read_schema_file(E2E_CONFIG_SCHEMA_FILE_PATH)
217
+ config_data = parse_raw_yaml(conftest_path.read_text())
218
+ validate_yaml_content_using_schema(config_data, e2e_config_schema)
219
+
220
+ return config_data
@@ -0,0 +1,26 @@
1
+ mapping:
2
+ llm_as_judge:
3
+ type: map
4
+ mapping:
5
+ api_type:
6
+ type: str
7
+ required: true
8
+ nullable: false
9
+ model:
10
+ type: str
11
+ required: true
12
+ nullable: false
13
+ llm_e2e_test_conversion:
14
+ type: map
15
+ mapping:
16
+ api_type:
17
+ type: str
18
+ model:
19
+ type: str
20
+ deployment:
21
+ type: str
22
+ api_base:
23
+ type: str
24
+ "=":
25
+ type: any
26
+
@@ -1,20 +1,27 @@
1
1
  import logging
2
+ from collections import OrderedDict
2
3
  from dataclasses import dataclass
3
4
  from typing import Any, Dict, List, Optional, Text, Union
4
5
 
5
- from rasa.shared.core.events import BotUttered, SlotSet, UserUttered
6
-
6
+ from rasa.e2e_test.assertions import Assertion
7
7
  from rasa.e2e_test.constants import (
8
+ KEY_ASSERTIONS,
9
+ KEY_ASSERTION_ORDER_ENABLED,
8
10
  KEY_BOT_INPUT,
9
11
  KEY_BOT_UTTERED,
10
12
  KEY_FIXTURES,
11
13
  KEY_METADATA,
14
+ KEY_STUB_CUSTOM_ACTIONS,
12
15
  KEY_SLOT_NOT_SET,
13
16
  KEY_SLOT_SET,
14
17
  KEY_STEPS,
15
18
  KEY_TEST_CASE,
19
+ KEY_TEST_CASES,
16
20
  KEY_USER_INPUT,
17
21
  )
22
+ from rasa.e2e_test.stub_custom_action import StubCustomAction
23
+ from rasa.shared.core.events import BotUttered, SlotSet, UserUttered
24
+ from rasa.shared.exceptions import RasaException
18
25
 
19
26
  logger = logging.getLogger(__name__)
20
27
 
@@ -47,6 +54,15 @@ class Fixture:
47
54
  },
48
55
  )
49
56
 
57
+ def as_dict(self) -> Dict[Text, Any]:
58
+ """Returns the fixture as a dictionary."""
59
+ return {
60
+ self.name: [
61
+ {slot_name: slot_value}
62
+ for slot_name, slot_value in self.slots_set.items()
63
+ ]
64
+ }
65
+
50
66
 
51
67
  @dataclass(frozen=True)
52
68
  class TestStep:
@@ -65,6 +81,8 @@ class TestStep:
65
81
  _slot_instance: Optional[Union[Text, Dict[Text, Any]]] = None
66
82
  _underlying: Optional[Dict[Text, Any]] = None
67
83
  metadata_name: Optional[Text] = None
84
+ assertions: Optional[List[Assertion]] = None
85
+ assertion_order_enabled: bool = False
68
86
 
69
87
  @staticmethod
70
88
  def from_dict(test_step_dict: Dict[Text, Any]) -> "TestStep":
@@ -73,7 +91,7 @@ class TestStep:
73
91
  Example:
74
92
  >>> TestStep.from_dict({"user": "hello"})
75
93
  TestStep(text="hello", actor="user")
76
- >>> TestStep.from_dict({"user": "hello", metadata: "some_metadata"})
94
+ >>> TestStep.from_dict({"user": "hello", "metadata": "some_metadata"})
77
95
  TestStep(text="hello", actor="user", metadata_name="some_metadata")
78
96
  >>> TestStep.from_dict({"bot": "hello world"})
79
97
  TestStep(text="hello world", actor="bot")
@@ -86,6 +104,15 @@ class TestStep:
86
104
  if test_step_dict.get(KEY_SLOT_NOT_SET):
87
105
  slot_instance = test_step_dict.get(KEY_SLOT_NOT_SET)
88
106
 
107
+ assertions = (
108
+ [
109
+ Assertion.create_typed_assertion(data)
110
+ for data in test_step_dict.get(KEY_ASSERTIONS, [])
111
+ ]
112
+ if KEY_ASSERTIONS in test_step_dict
113
+ else None
114
+ )
115
+
89
116
  return TestStep(
90
117
  text=test_step_dict.get(
91
118
  KEY_USER_INPUT, test_step_dict.get(KEY_BOT_INPUT, "")
@@ -99,6 +126,10 @@ class TestStep:
99
126
  _slot_instance=slot_instance,
100
127
  _underlying=test_step_dict,
101
128
  metadata_name=test_step_dict.get(KEY_METADATA, ""),
129
+ assertions=assertions,
130
+ assertion_order_enabled=test_step_dict.get(
131
+ KEY_ASSERTION_ORDER_ENABLED, False
132
+ ),
102
133
  )
103
134
 
104
135
  @staticmethod
@@ -110,7 +141,7 @@ class TestStep:
110
141
  and KEY_SLOT_SET not in test_step_dict
111
142
  and KEY_SLOT_NOT_SET not in test_step_dict
112
143
  ):
113
- raise ValueError(
144
+ raise RasaException(
114
145
  f"Test step is missing either the {KEY_USER_INPUT}, {KEY_BOT_INPUT}, "
115
146
  f"{KEY_SLOT_NOT_SET}, {KEY_SLOT_SET} "
116
147
  f"or {KEY_BOT_UTTERED} key: {test_step_dict}"
@@ -120,15 +151,70 @@ class TestStep:
120
151
  test_step_dict.get(KEY_SLOT_SET) is not None
121
152
  and test_step_dict.get(KEY_SLOT_NOT_SET) is not None
122
153
  ):
123
- raise ValueError(
154
+ raise RasaException(
124
155
  f"Test step has both {KEY_SLOT_SET} and {KEY_SLOT_NOT_SET} keys: "
125
156
  f"{test_step_dict}. You must only use one of the keys in a test step."
126
157
  )
127
158
 
159
+ if KEY_USER_INPUT not in test_step_dict and KEY_ASSERTIONS in test_step_dict:
160
+ raise RasaException(
161
+ f"Test step with assertions must only be used with the "
162
+ f"'{KEY_USER_INPUT}' key: {test_step_dict}"
163
+ )
164
+
165
+ if (
166
+ KEY_USER_INPUT not in test_step_dict
167
+ and KEY_ASSERTION_ORDER_ENABLED in test_step_dict
168
+ ):
169
+ raise RasaException(
170
+ f"Test step with '{KEY_ASSERTION_ORDER_ENABLED}' key must "
171
+ f"only be used with the '{KEY_USER_INPUT}' key: "
172
+ f"{test_step_dict}"
173
+ )
174
+
175
+ if (
176
+ KEY_ASSERTION_ORDER_ENABLED in test_step_dict
177
+ and KEY_ASSERTIONS not in test_step_dict
178
+ ):
179
+ raise RasaException(
180
+ f"You must specify the '{KEY_ASSERTIONS}' key in the user test step "
181
+ f"where you are using '{KEY_ASSERTION_ORDER_ENABLED}' key: "
182
+ f"{test_step_dict}"
183
+ )
184
+
128
185
  def as_dict(self) -> Dict[Text, Any]:
129
186
  """Returns the underlying dictionary of the test step."""
130
187
  return self._underlying or {}
131
188
 
189
+ def as_dict_yaml_format(self) -> Dict[Text, Any]:
190
+ """Returns the test step as a dictionary in YAML format."""
191
+ if not self._underlying:
192
+ return {}
193
+
194
+ result = self._underlying.copy()
195
+
196
+ def _handle_slots(key: str) -> None:
197
+ """Slots should be a list of strings or dicts."""
198
+ if (
199
+ self._underlying
200
+ and key in self._underlying
201
+ and isinstance(self._underlying[key], OrderedDict)
202
+ ):
203
+ result[key] = [
204
+ {key: value} for key, value in self._underlying[key].items()
205
+ ]
206
+ elif (
207
+ self._underlying
208
+ and key in self._underlying
209
+ and isinstance(self._underlying[key], str)
210
+ ):
211
+ result[key] = [self._underlying[key]]
212
+
213
+ _handle_slots(KEY_SLOT_SET)
214
+ _handle_slots(KEY_SLOT_NOT_SET)
215
+
216
+ return result
217
+
132
218
  def matches_event(self, other: Union[BotUttered, SlotSet, None]) -> bool:
133
219
  """Compares the test step with BotUttered or SlotSet event.
134
220
 
@@ -313,13 +399,25 @@ class TestCase:
313
399
  name=input_test_case.get(KEY_TEST_CASE, "default"),
314
400
  steps=steps,
315
401
  file=file,
316
- line=input_test_case.lc.line + 1
317
- if hasattr(input_test_case, "lc")
318
- else None,
402
+ line=(
403
+ input_test_case.lc.line + 1 if hasattr(input_test_case, "lc") else None
404
+ ),
319
405
  fixture_names=input_test_case.get(KEY_FIXTURES),
320
406
  metadata_name=input_test_case.get(KEY_METADATA),
321
407
  )
322
408
 
409
+ def as_dict(self) -> Dict[Text, Any]:
410
+ """Returns the test case as a dictionary."""
411
+ result = {
412
+ KEY_TEST_CASE: self.name,
413
+ KEY_STEPS: [step.as_dict_yaml_format() for step in self.steps],
414
+ }
415
+ if self.fixture_names:
416
+ result[KEY_FIXTURES] = self.fixture_names
417
+ if self.metadata_name:
418
+ result[KEY_METADATA] = self.metadata_name
419
+ return result
420
+
323
421
  def file_with_line(self) -> Text:
324
422
  """Returns the file name and line number of the test case."""
325
423
  if not self.file:
@@ -328,6 +426,15 @@ class TestCase:
328
426
  line = str(self.line) if self.line is not None else ""
329
427
  return f"{self.file}:{line}"
330
428
 
429
+ def uses_assertions(self) -> bool:
430
+ """Checks if the test case uses assertions."""
431
+ try:
432
+ next(step for step in self.steps if step.assertions is not None)
433
+ except StopIteration:
434
+ return False
435
+
436
+ return True
437
+
331
438
 
332
439
  @dataclass(frozen=True)
333
440
  class Metadata:
@@ -356,6 +463,10 @@ class Metadata:
356
463
  },
357
464
  )
358
465
 
466
+ def as_dict(self) -> Dict[Text, Any]:
467
+ """Returns the metadata as a dictionary."""
468
+ return {self.name: self.metadata}
469
+
359
470
 
360
471
  @dataclass(frozen=True)
361
472
  class TestSuite:
@@ -364,3 +475,15 @@ class TestSuite:
364
475
  test_cases: List[TestCase]
365
476
  fixtures: List[Fixture]
366
477
  metadata: List[Metadata]
478
+ stub_custom_actions: Dict[Text, StubCustomAction]
479
+
480
+ def as_dict(self) -> Dict[Text, Any]:
481
+ """Returns the test suite as a dictionary."""
482
+ return {
483
+ KEY_FIXTURES: [fixture.as_dict() for fixture in self.fixtures],
484
+ KEY_METADATA: [metadata.as_dict() for metadata in self.metadata],
485
+ KEY_STUB_CUSTOM_ACTIONS: {
486
+ key: value.as_dict() for key, value in self.stub_custom_actions.items()
487
+ },
488
+ KEY_TEST_CASES: [test_case.as_dict() for test_case in self.test_cases],
489
+ }