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
@@ -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
- INTENT_NAME_KEY,
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": cls._collect_actions_which_explicitly_need_domain( # noqa: E501
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
- return (
795
- intent_name,
796
- cls._transform_intent_properties_for_internal_use(
797
- intent, entity_properties
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]],
@@ -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(self.metadata)
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(self.metadata), self.timestamp
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:
@@ -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 typing import Text, Optional, Dict, Any, List, Set
6
- from pypred import Predicate
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
- from rasa.shared.core.flows.flow_step_links import StaticFlowStepLink
14
- from rasa.shared.core.flows.nlu_trigger import NLUTriggers
15
- from rasa.shared.core.flows.steps.continuation import ContinueFlowStep
16
- from rasa.shared.core.flows.steps.constants import (
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.flow_step_sequence import FlowStepSequence
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(flow_id: Text, data: Dict[Text, Any]) -> Flow:
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 typing import List, Generator, Any, Optional, Dict, Text, Set
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(cls, data: Optional[Dict[Text, Dict[Text, Any]]]) -> FlowsList:
68
- """Create a FlowsList object from serialized data
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
- The ids of all flows that can be started by a user."""
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
- The ids of all flows."""
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
- All flows that can be started by a user."""
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
- All flows for which the starting conditions are met."""
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(self) -> Set[str]:
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