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
rasa/shared/core/domain.py
CHANGED
|
@@ -11,7 +11,6 @@ from typing import (
|
|
|
11
11
|
Dict,
|
|
12
12
|
Iterable,
|
|
13
13
|
List,
|
|
14
|
-
MutableMapping,
|
|
15
14
|
NamedTuple,
|
|
16
15
|
NoReturn,
|
|
17
16
|
Optional,
|
|
@@ -20,6 +19,7 @@ from typing import (
|
|
|
20
19
|
Tuple,
|
|
21
20
|
Union,
|
|
22
21
|
cast,
|
|
22
|
+
MutableMapping,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
import structlog
|
|
@@ -51,11 +51,11 @@ from rasa.shared.core.constants import (
|
|
|
51
51
|
)
|
|
52
52
|
from rasa.shared.core.events import SlotSet, UserUttered
|
|
53
53
|
from rasa.shared.core.slots import (
|
|
54
|
-
AnySlot,
|
|
55
|
-
CategoricalSlot,
|
|
56
|
-
ListSlot,
|
|
57
54
|
Slot,
|
|
55
|
+
CategoricalSlot,
|
|
58
56
|
TextSlot,
|
|
57
|
+
AnySlot,
|
|
58
|
+
ListSlot,
|
|
59
59
|
)
|
|
60
60
|
from rasa.shared.exceptions import (
|
|
61
61
|
RasaException,
|
|
@@ -63,21 +63,21 @@ from rasa.shared.exceptions import (
|
|
|
63
63
|
YamlSyntaxException,
|
|
64
64
|
)
|
|
65
65
|
from rasa.shared.nlu.constants import (
|
|
66
|
-
ENTITIES,
|
|
67
|
-
ENTITY_ATTRIBUTE_GROUP,
|
|
68
|
-
ENTITY_ATTRIBUTE_ROLE,
|
|
69
66
|
ENTITY_ATTRIBUTE_TYPE,
|
|
70
|
-
|
|
67
|
+
ENTITY_ATTRIBUTE_ROLE,
|
|
68
|
+
ENTITY_ATTRIBUTE_GROUP,
|
|
71
69
|
RESPONSE_IDENTIFIER_DELIMITER,
|
|
70
|
+
INTENT_NAME_KEY,
|
|
71
|
+
ENTITIES,
|
|
72
72
|
)
|
|
73
73
|
from rasa.shared.utils.cli import print_error_and_exit
|
|
74
74
|
from rasa.shared.utils.yaml import (
|
|
75
75
|
KEY_TRAINING_DATA_FORMAT_VERSION,
|
|
76
|
-
dump_obj_as_yaml_to_string,
|
|
77
76
|
read_yaml,
|
|
77
|
+
validate_training_data_format_version,
|
|
78
78
|
read_yaml_file,
|
|
79
|
+
dump_obj_as_yaml_to_string,
|
|
79
80
|
validate_raw_yaml_using_schema_file_with_responses,
|
|
80
|
-
validate_training_data_format_version,
|
|
81
81
|
)
|
|
82
82
|
|
|
83
83
|
if TYPE_CHECKING:
|
|
@@ -271,8 +271,8 @@ class Domain:
|
|
|
271
271
|
|
|
272
272
|
additional_arguments = {
|
|
273
273
|
**data.get("config", {}),
|
|
274
|
-
"actions_which_explicitly_need_domain":
|
|
275
|
-
domain_actions
|
|
274
|
+
"actions_which_explicitly_need_domain": (
|
|
275
|
+
cls._collect_actions_which_explicitly_need_domain(domain_actions)
|
|
276
276
|
),
|
|
277
277
|
}
|
|
278
278
|
session_config = cls._get_session_config(data.get(SESSION_CONFIG_KEY, {}))
|
|
@@ -790,13 +790,18 @@ class Domain:
|
|
|
790
790
|
}
|
|
791
791
|
else:
|
|
792
792
|
intent_name = next(iter(intent.keys()))
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
793
|
+
try:
|
|
794
|
+
return (
|
|
795
|
+
intent_name,
|
|
796
|
+
cls._transform_intent_properties_for_internal_use(
|
|
797
|
+
intent, entity_properties
|
|
798
|
+
),
|
|
799
|
+
)
|
|
800
|
+
except AttributeError:
|
|
801
|
+
raise InvalidDomain(
|
|
802
|
+
f"Detected invalid intent definition: {intent}. "
|
|
803
|
+
f"Please make sure all intent definitions are valid."
|
|
804
|
+
)
|
|
800
805
|
|
|
801
806
|
@classmethod
|
|
802
807
|
def _add_default_intents(
|
|
@@ -2047,6 +2052,9 @@ class Domain:
|
|
|
2047
2052
|
|
|
2048
2053
|
return action_names
|
|
2049
2054
|
|
|
2055
|
+
def is_custom_action(self, action_name: str) -> bool:
|
|
2056
|
+
return action_name in self._custom_actions
|
|
2057
|
+
|
|
2050
2058
|
|
|
2051
2059
|
def warn_about_duplicates_found_during_domain_merging(
|
|
2052
2060
|
duplicates: Dict[Text, List[Text]],
|
rasa/shared/core/events.py
CHANGED
|
@@ -930,16 +930,42 @@ class BotUttered(SkipEventInMDStoryMixin):
|
|
|
930
930
|
|
|
931
931
|
return self.__members() == other.__members()
|
|
932
932
|
|
|
933
|
+
def _clean_up_metadata(self) -> Dict[str, Any]:
|
|
934
|
+
"""Removes search_results metadata key from the metadata.
|
|
935
|
+
|
|
936
|
+
This is intended to prevent increasing the string representation
|
|
937
|
+
character length of the bot event.
|
|
938
|
+
"""
|
|
939
|
+
from rasa.core.policies.enterprise_search_policy import (
|
|
940
|
+
SEARCH_RESULTS_METADATA_KEY,
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
metadata = copy.deepcopy(self.metadata)
|
|
944
|
+
|
|
945
|
+
if SEARCH_RESULTS_METADATA_KEY in self.metadata:
|
|
946
|
+
metadata.pop(SEARCH_RESULTS_METADATA_KEY)
|
|
947
|
+
structlogger.debug(
|
|
948
|
+
"search_results.metadata.removed",
|
|
949
|
+
event_info="Removed search_results metadata key only "
|
|
950
|
+
"from the string representation of the bot event.",
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
return metadata
|
|
954
|
+
|
|
933
955
|
def __str__(self) -> Text:
|
|
934
956
|
"""Returns text representation of event."""
|
|
957
|
+
metadata = self._clean_up_metadata()
|
|
958
|
+
|
|
935
959
|
return "BotUttered(text: {}, data: {}, metadata: {})".format(
|
|
936
|
-
self.text, json.dumps(self.data), json.dumps(
|
|
960
|
+
self.text, json.dumps(self.data), json.dumps(metadata)
|
|
937
961
|
)
|
|
938
962
|
|
|
939
963
|
def __repr__(self) -> Text:
|
|
940
964
|
"""Returns text representation of event for debugging."""
|
|
965
|
+
metadata = self._clean_up_metadata()
|
|
966
|
+
|
|
941
967
|
return "BotUttered('{}', {}, {}, {})".format(
|
|
942
|
-
self.text, json.dumps(self.data), json.dumps(
|
|
968
|
+
self.text, json.dumps(self.data), json.dumps(metadata), self.timestamp
|
|
943
969
|
)
|
|
944
970
|
|
|
945
971
|
def apply_to(self, tracker: "DialogueStateTracker") -> None:
|
rasa/shared/core/flows/flow.py
CHANGED
|
@@ -1,33 +1,40 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import copy
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
5
|
from functools import cached_property
|
|
5
|
-
from
|
|
6
|
-
from
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Text, Optional, Dict, Any, List, Set, Union
|
|
8
|
+
|
|
7
9
|
import structlog
|
|
10
|
+
from pypred import Predicate
|
|
8
11
|
|
|
9
12
|
import rasa.shared.utils.io
|
|
10
13
|
from rasa.shared.constants import RASA_DEFAULT_FLOW_PATTERN_PREFIX
|
|
14
|
+
from rasa.shared.core.flows.flow_path import PathNode, FlowPath, FlowPathsList
|
|
11
15
|
from rasa.shared.core.flows.flow_step import FlowStep
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
CONTINUE_STEP_PREFIX,
|
|
18
|
-
START_STEP,
|
|
19
|
-
END_STEP,
|
|
16
|
+
from rasa.shared.core.flows.flow_step_links import (
|
|
17
|
+
FlowStepLink,
|
|
18
|
+
StaticFlowStepLink,
|
|
19
|
+
IfFlowStepLink,
|
|
20
|
+
ElseFlowStepLink,
|
|
20
21
|
)
|
|
22
|
+
from rasa.shared.core.flows.flow_step_sequence import FlowStepSequence
|
|
23
|
+
from rasa.shared.core.flows.nlu_trigger import NLUTriggers
|
|
21
24
|
from rasa.shared.core.flows.steps import (
|
|
22
25
|
CollectInformationFlowStep,
|
|
23
26
|
EndFlowStep,
|
|
24
27
|
StartFlowStep,
|
|
25
28
|
ActionFlowStep,
|
|
26
29
|
)
|
|
27
|
-
from rasa.shared.core.flows.
|
|
30
|
+
from rasa.shared.core.flows.steps.constants import (
|
|
31
|
+
CONTINUE_STEP_PREFIX,
|
|
32
|
+
START_STEP,
|
|
33
|
+
END_STEP,
|
|
34
|
+
)
|
|
35
|
+
from rasa.shared.core.flows.steps.continuation import ContinueFlowStep
|
|
28
36
|
from rasa.shared.core.slots import Slot
|
|
29
37
|
|
|
30
|
-
|
|
31
38
|
structlogger = structlog.get_logger()
|
|
32
39
|
|
|
33
40
|
|
|
@@ -51,14 +58,21 @@ class Flow:
|
|
|
51
58
|
"""
|
|
52
59
|
A flag that checks whether the flow should always be included in the prompt or not.
|
|
53
60
|
"""
|
|
61
|
+
file_path: Optional[str] = None
|
|
62
|
+
"""The path to the file where the flow is stored."""
|
|
54
63
|
|
|
55
64
|
@staticmethod
|
|
56
|
-
def from_json(
|
|
65
|
+
def from_json(
|
|
66
|
+
flow_id: Text,
|
|
67
|
+
data: Dict[Text, Any],
|
|
68
|
+
file_path: Optional[Union[str, Path]] = None,
|
|
69
|
+
) -> Flow:
|
|
57
70
|
"""Create a Flow object from serialized data.
|
|
58
71
|
|
|
59
72
|
Args:
|
|
60
73
|
flow_id: id of the flow
|
|
61
74
|
data: data for a Flow object in a serialized format.
|
|
75
|
+
file_path: the file path of the flow
|
|
62
76
|
|
|
63
77
|
Returns:
|
|
64
78
|
A Flow object.
|
|
@@ -66,6 +80,9 @@ class Flow:
|
|
|
66
80
|
step_sequence = FlowStepSequence.from_json(data.get("steps"))
|
|
67
81
|
nlu_triggers = NLUTriggers.from_json(data.get("nlu_trigger"))
|
|
68
82
|
|
|
83
|
+
if file_path and isinstance(file_path, Path):
|
|
84
|
+
file_path = str(file_path)
|
|
85
|
+
|
|
69
86
|
return Flow(
|
|
70
87
|
id=flow_id,
|
|
71
88
|
custom_name=data.get("name"),
|
|
@@ -75,8 +92,16 @@ class Flow:
|
|
|
75
92
|
guard_condition=str(data["if"]) if "if" in data else None,
|
|
76
93
|
step_sequence=Flow.resolve_default_ids(step_sequence),
|
|
77
94
|
nlu_triggers=nlu_triggers,
|
|
95
|
+
# If we are reading the flows in after training the file_path is part of
|
|
96
|
+
# data. When the model is trained, take the provided file_path.
|
|
97
|
+
file_path=data.get("file_path") if "file_path" in data else file_path,
|
|
78
98
|
)
|
|
79
99
|
|
|
100
|
+
def get_full_name(self) -> str:
|
|
101
|
+
if self.file_path:
|
|
102
|
+
return f"{self.file_path}::{self.name}"
|
|
103
|
+
return self.name
|
|
104
|
+
|
|
80
105
|
@staticmethod
|
|
81
106
|
def create_default_name(flow_id: str) -> str:
|
|
82
107
|
"""Create a default flow name for when it is missing."""
|
|
@@ -140,6 +165,8 @@ class Flow:
|
|
|
140
165
|
data["always_include_in_prompt"] = self.always_include_in_prompt
|
|
141
166
|
if self.nlu_triggers:
|
|
142
167
|
data["nlu_trigger"] = self.nlu_triggers.as_json()
|
|
168
|
+
if self.file_path:
|
|
169
|
+
data["file_path"] = self.file_path
|
|
143
170
|
|
|
144
171
|
return data
|
|
145
172
|
|
|
@@ -360,3 +387,171 @@ class Flow:
|
|
|
360
387
|
return True
|
|
361
388
|
|
|
362
389
|
return False
|
|
390
|
+
|
|
391
|
+
def extract_all_paths(self) -> FlowPathsList:
|
|
392
|
+
"""Extracts all possible flow paths.
|
|
393
|
+
|
|
394
|
+
Extracts all possible flow paths from a given flow structure by
|
|
395
|
+
recursively exploring each step.
|
|
396
|
+
This function initializes an empty list to collect paths, an empty path list,
|
|
397
|
+
and a set of visited step IDs to prevent revisiting steps.
|
|
398
|
+
It calls `go_over_steps` to recursively explore and fill the paths list.
|
|
399
|
+
"""
|
|
400
|
+
flow_paths_list = FlowPathsList(self.id, paths=[])
|
|
401
|
+
steps: List[FlowStep] = self.steps
|
|
402
|
+
current_path: FlowPath = FlowPath(flow=self.id, nodes=[])
|
|
403
|
+
step_ids_visited: Set[str] = set()
|
|
404
|
+
|
|
405
|
+
self._go_over_steps(steps, current_path, flow_paths_list, step_ids_visited)
|
|
406
|
+
|
|
407
|
+
if not flow_paths_list.is_path_part_of_list(current_path):
|
|
408
|
+
flow_paths_list.paths.append(copy.deepcopy(current_path))
|
|
409
|
+
|
|
410
|
+
structlogger.debug(
|
|
411
|
+
"shared.core.flows.flow.extract_all_paths",
|
|
412
|
+
comment="Extraction complete",
|
|
413
|
+
number_of_paths=len(flow_paths_list.paths),
|
|
414
|
+
flow_name=self.name,
|
|
415
|
+
)
|
|
416
|
+
return flow_paths_list
|
|
417
|
+
|
|
418
|
+
def _go_over_steps(
|
|
419
|
+
self,
|
|
420
|
+
steps_to_go: Union[str, List[FlowStep]],
|
|
421
|
+
current_path: FlowPath,
|
|
422
|
+
completed_paths: FlowPathsList,
|
|
423
|
+
step_ids_visited: Set[str],
|
|
424
|
+
) -> None:
|
|
425
|
+
"""Processes the flow steps recursively.
|
|
426
|
+
|
|
427
|
+
Either following direct step IDs or handling conditions, and adds complete
|
|
428
|
+
paths to the collected_paths.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
steps_to_go: Either a direct step ID or a list of steps to process.
|
|
432
|
+
current_path: The current path being constructed.
|
|
433
|
+
completed_paths: The list where completed paths are added.
|
|
434
|
+
step_ids_visited: A set of step IDs that have been visited to avoid cycles.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
None: This function modifies collected_paths in place by appending new paths
|
|
438
|
+
as they are found.
|
|
439
|
+
"""
|
|
440
|
+
# Case 1: If the steps_to_go is a custom_id string
|
|
441
|
+
# This happens when a "next" of, for example, a IfFlowStepLink is targeting
|
|
442
|
+
# a specific step by id
|
|
443
|
+
if isinstance(steps_to_go, str):
|
|
444
|
+
for i, step in enumerate(self.steps):
|
|
445
|
+
# We don't need to check for 'id' as a link can only happen to a
|
|
446
|
+
# custom id.
|
|
447
|
+
if step.custom_id == steps_to_go:
|
|
448
|
+
self._go_over_steps(
|
|
449
|
+
self.steps[i:], current_path, completed_paths, step_ids_visited
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Case 2: If steps_to_go is a list of steps
|
|
453
|
+
else:
|
|
454
|
+
for i, step in enumerate(steps_to_go):
|
|
455
|
+
# 1. Check if the step is relevant for testable_paths extraction.
|
|
456
|
+
# We only create new path nodes for ActionFlowStep and
|
|
457
|
+
# CollectInformationFlowStep because these are externally visible
|
|
458
|
+
# changes in the assistant's behaviour (trackable in the e2e tests).
|
|
459
|
+
# For other flow steps, we only follow their links.
|
|
460
|
+
# We decided to ignore calls to other flows in our coverage analysis.
|
|
461
|
+
if not isinstance(step, (CollectInformationFlowStep, ActionFlowStep)):
|
|
462
|
+
self._handle_links(
|
|
463
|
+
step.next.links,
|
|
464
|
+
current_path,
|
|
465
|
+
completed_paths,
|
|
466
|
+
step_ids_visited,
|
|
467
|
+
)
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
# 2. Check if already visited this custom step id
|
|
471
|
+
# in order to keep track of loops
|
|
472
|
+
if step.custom_id is not None and step.custom_id in step_ids_visited:
|
|
473
|
+
if not completed_paths.is_path_part_of_list(current_path):
|
|
474
|
+
completed_paths.paths.append(copy.deepcopy(current_path))
|
|
475
|
+
return # Stop traversing this path if we've revisited a step
|
|
476
|
+
elif step.custom_id is not None:
|
|
477
|
+
step_ids_visited.add(step.custom_id)
|
|
478
|
+
|
|
479
|
+
# 3. Append step info to the path
|
|
480
|
+
current_path.nodes.append(
|
|
481
|
+
PathNode(
|
|
482
|
+
flow=current_path.flow,
|
|
483
|
+
step_id=step.id,
|
|
484
|
+
lines=step.metadata["line_numbers"],
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# 4. Check if 'END' branch
|
|
489
|
+
if (
|
|
490
|
+
len(step.next.links) == 1
|
|
491
|
+
and isinstance(step.next.links[0], StaticFlowStepLink)
|
|
492
|
+
and step.next.links[0].target == END_STEP
|
|
493
|
+
):
|
|
494
|
+
if not completed_paths.is_path_part_of_list(current_path):
|
|
495
|
+
completed_paths.paths.append(copy.deepcopy(current_path))
|
|
496
|
+
return
|
|
497
|
+
else:
|
|
498
|
+
self._handle_links(
|
|
499
|
+
step.next.links,
|
|
500
|
+
current_path,
|
|
501
|
+
completed_paths,
|
|
502
|
+
step_ids_visited,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
def _handle_links(
|
|
506
|
+
self,
|
|
507
|
+
links: List[FlowStepLink],
|
|
508
|
+
path: FlowPath,
|
|
509
|
+
collected_paths: FlowPathsList,
|
|
510
|
+
step_ids_visited: set,
|
|
511
|
+
) -> None:
|
|
512
|
+
"""Processes the next step in a flow.
|
|
513
|
+
|
|
514
|
+
Potentially recursively calling itself to handle conditional paths and
|
|
515
|
+
branching.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
links: Links listed in the "next" attribute.
|
|
519
|
+
path: The current path taken in the flow.
|
|
520
|
+
collected_paths: A list of paths collected so far.
|
|
521
|
+
step_ids_visited: A set of step IDs that have already been visited
|
|
522
|
+
to avoid loops.
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
None: Modifies collected_paths in place by appending new paths
|
|
526
|
+
as they are completed.
|
|
527
|
+
"""
|
|
528
|
+
steps = self.steps
|
|
529
|
+
|
|
530
|
+
for link in links:
|
|
531
|
+
# Direct step id reference
|
|
532
|
+
if isinstance(link, StaticFlowStepLink):
|
|
533
|
+
# Find this id in the flow steps and restart from there
|
|
534
|
+
for i, step in enumerate(steps):
|
|
535
|
+
if step.id == link.target_step_id:
|
|
536
|
+
self._go_over_steps(
|
|
537
|
+
steps[i:],
|
|
538
|
+
copy.deepcopy(path),
|
|
539
|
+
collected_paths,
|
|
540
|
+
copy.deepcopy(step_ids_visited),
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# If conditions
|
|
544
|
+
elif isinstance(link, (IfFlowStepLink, ElseFlowStepLink)):
|
|
545
|
+
# Handling conditional paths
|
|
546
|
+
target_steps: Union[str, List[FlowStep]]
|
|
547
|
+
if isinstance(link.target_reference, FlowStepSequence):
|
|
548
|
+
target_steps = link.target_reference.child_steps
|
|
549
|
+
else:
|
|
550
|
+
target_steps = link.target_reference
|
|
551
|
+
|
|
552
|
+
self._go_over_steps(
|
|
553
|
+
target_steps,
|
|
554
|
+
copy.deepcopy(path),
|
|
555
|
+
collected_paths,
|
|
556
|
+
copy.deepcopy(step_ids_visited),
|
|
557
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import List, Optional, Set
|
|
3
|
+
|
|
4
|
+
import structlog
|
|
5
|
+
|
|
6
|
+
NODE_KEY_SEPARATOR = " | "
|
|
7
|
+
|
|
8
|
+
structlogger = structlog.get_logger()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class PathNode:
|
|
13
|
+
"""Representation of a path step."""
|
|
14
|
+
|
|
15
|
+
step_id: str
|
|
16
|
+
"""Step ID"""
|
|
17
|
+
|
|
18
|
+
flow: str
|
|
19
|
+
"""Flow name"""
|
|
20
|
+
|
|
21
|
+
lines: Optional[str] = None
|
|
22
|
+
"""Line numbers range from the original flow .yaml file"""
|
|
23
|
+
|
|
24
|
+
def __eq__(self, other: object) -> bool:
|
|
25
|
+
if not isinstance(other, PathNode):
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
return self.flow == other.flow and self.step_id == other.step_id
|
|
29
|
+
|
|
30
|
+
def __hash__(self) -> int:
|
|
31
|
+
return hash((self.flow, self.step_id))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class FlowPath:
|
|
36
|
+
"""Representation of a path through a flow.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
flow (str): The name of the flow.
|
|
40
|
+
nodes (List[PathNode]): A list of nodes that constitute the path.
|
|
41
|
+
test_name (str): Name of the test from which it was extracted.
|
|
42
|
+
test_passing (bool): Test status: True if 'passed'.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
flow: str
|
|
46
|
+
nodes: List[PathNode] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
def are_paths_matching(self, other_path: "FlowPath") -> bool:
|
|
49
|
+
"""Compares this FlowPath to another to determine if they are identical."""
|
|
50
|
+
if len(self.nodes) != len(other_path.nodes):
|
|
51
|
+
return False
|
|
52
|
+
return all(
|
|
53
|
+
node == other_node for node, other_node in zip(self.nodes, other_path.nodes)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class FlowPathsList:
|
|
59
|
+
"""Representing a list of all available paths through a flow.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
flow (str): The name of the flow.
|
|
63
|
+
paths (List[FlowPath]): All paths of that flow.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
flow: str
|
|
67
|
+
paths: List[FlowPath] = field(default=list)
|
|
68
|
+
|
|
69
|
+
def get_unique_nodes(self) -> Set[PathNode]:
|
|
70
|
+
"""Returns the unique nodes of all flow paths."""
|
|
71
|
+
nodes = set()
|
|
72
|
+
|
|
73
|
+
for path in self.paths:
|
|
74
|
+
for node in path.nodes:
|
|
75
|
+
nodes.add(node)
|
|
76
|
+
|
|
77
|
+
return nodes
|
|
78
|
+
|
|
79
|
+
def get_number_of_unique_nodes(self) -> int:
|
|
80
|
+
return len(self.get_unique_nodes())
|
|
81
|
+
|
|
82
|
+
def is_path_part_of_list(self, flow_path: FlowPath) -> bool:
|
|
83
|
+
"""Checks if the FlowPath exists in a list of FlowPaths."""
|
|
84
|
+
return any(flow_path.are_paths_matching(path) for path in self.paths)
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from dataclasses import dataclass
|
|
3
|
-
from
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Generator, Any, Optional, Dict, Text, Set, Union
|
|
4
6
|
|
|
5
7
|
import rasa.shared.utils.io
|
|
6
8
|
from rasa.shared.core.flows import Flow
|
|
9
|
+
from rasa.shared.core.flows.flow_path import FlowPathsList
|
|
7
10
|
from rasa.shared.core.flows.validation import (
|
|
8
11
|
validate_flow,
|
|
9
12
|
validate_link_in_call_restriction,
|
|
@@ -64,11 +67,16 @@ class FlowsList:
|
|
|
64
67
|
return FlowsList(list(merged_flows.values()))
|
|
65
68
|
|
|
66
69
|
@classmethod
|
|
67
|
-
def from_json(
|
|
68
|
-
|
|
70
|
+
def from_json(
|
|
71
|
+
cls,
|
|
72
|
+
data: Optional[Dict[Text, Dict[Text, Any]]],
|
|
73
|
+
file_path: Optional[Union[str, Path]] = None,
|
|
74
|
+
) -> FlowsList:
|
|
75
|
+
"""Create a FlowsList object from serialized data.
|
|
69
76
|
|
|
70
77
|
Args:
|
|
71
78
|
data: data for a FlowsList in a serialized format
|
|
79
|
+
file_path: the file path of the flows
|
|
72
80
|
|
|
73
81
|
Returns:
|
|
74
82
|
A FlowsList object.
|
|
@@ -78,7 +86,7 @@ class FlowsList:
|
|
|
78
86
|
|
|
79
87
|
return cls(
|
|
80
88
|
underlying_flows=[
|
|
81
|
-
Flow.from_json(flow_id, flow_config)
|
|
89
|
+
Flow.from_json(flow_id, flow_config, file_path)
|
|
82
90
|
for flow_id, flow_config in data.items()
|
|
83
91
|
]
|
|
84
92
|
)
|
|
@@ -139,7 +147,8 @@ class FlowsList:
|
|
|
139
147
|
"""Get all ids of flows that can be started by a user.
|
|
140
148
|
|
|
141
149
|
Returns:
|
|
142
|
-
|
|
150
|
+
The ids of all flows that can be started by a user.
|
|
151
|
+
"""
|
|
143
152
|
return {f.id for f in self.user_flows}
|
|
144
153
|
|
|
145
154
|
@property
|
|
@@ -147,7 +156,8 @@ class FlowsList:
|
|
|
147
156
|
"""Get all ids of flows.
|
|
148
157
|
|
|
149
158
|
Returns:
|
|
150
|
-
|
|
159
|
+
The ids of all flows.
|
|
160
|
+
"""
|
|
151
161
|
return {f.id for f in self.underlying_flows}
|
|
152
162
|
|
|
153
163
|
@property
|
|
@@ -155,7 +165,8 @@ class FlowsList:
|
|
|
155
165
|
"""Get all flows that can be started by a user.
|
|
156
166
|
|
|
157
167
|
Returns:
|
|
158
|
-
|
|
168
|
+
All flows that can be started by a user.
|
|
169
|
+
"""
|
|
159
170
|
return FlowsList(
|
|
160
171
|
[f for f in self.underlying_flows if not f.is_rasa_default_flow]
|
|
161
172
|
)
|
|
@@ -179,14 +190,14 @@ class FlowsList:
|
|
|
179
190
|
slots: The slots to evaluate the starting conditions against.
|
|
180
191
|
|
|
181
192
|
Returns:
|
|
182
|
-
|
|
193
|
+
All flows for which the starting conditions are met.
|
|
194
|
+
"""
|
|
183
195
|
return FlowsList(
|
|
184
196
|
[f for f in self.underlying_flows if f.is_startable(context, slots)]
|
|
185
197
|
)
|
|
186
198
|
|
|
187
199
|
def get_flows_always_included_in_prompt(self) -> FlowsList:
|
|
188
|
-
"""
|
|
189
|
-
Gets all flows based on their inclusion status in prompts.
|
|
200
|
+
"""Gets all flows based on their inclusion status in prompts.
|
|
190
201
|
|
|
191
202
|
Args:
|
|
192
203
|
always_included: Inclusion status.
|
|
@@ -210,14 +221,25 @@ class FlowsList:
|
|
|
210
221
|
[f for f in self.underlying_flows if not f.is_startable_only_via_link()]
|
|
211
222
|
)
|
|
212
223
|
|
|
213
|
-
def available_slot_names(
|
|
224
|
+
def available_slot_names(
|
|
225
|
+
self, ask_before_filling: Optional[bool] = None
|
|
226
|
+
) -> Set[str]:
|
|
214
227
|
"""Get all slot names collected by flows."""
|
|
215
228
|
return {
|
|
216
229
|
step.collect
|
|
217
230
|
for flow in self.underlying_flows
|
|
218
231
|
for step in flow.get_collect_steps()
|
|
232
|
+
if ask_before_filling is None
|
|
233
|
+
or step.ask_before_filling == ask_before_filling
|
|
219
234
|
}
|
|
220
235
|
|
|
221
236
|
def available_custom_actions(self) -> Set[str]:
|
|
222
237
|
"""Get all custom actions collected by flows."""
|
|
223
238
|
return set().union(*[flow.custom_actions for flow in self.underlying_flows])
|
|
239
|
+
|
|
240
|
+
def extract_flow_paths(self) -> Dict[str, FlowPathsList]:
|
|
241
|
+
paths = {}
|
|
242
|
+
for flow in self.user_flows.underlying_flows:
|
|
243
|
+
paths[flow.id] = flow.extract_all_paths()
|
|
244
|
+
|
|
245
|
+
return paths
|