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,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
|
rasa/e2e_test/constants.py
CHANGED
|
@@ -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
|
+
|
rasa/e2e_test/e2e_test_case.py
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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=
|
|
317
|
-
|
|
318
|
-
|
|
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
|
+
}
|