rasa-pro 3.12.18.dev1__py3-none-any.whl → 3.12.20__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.
- rasa/__init__.py +0 -6
- rasa/core/actions/action.py +1 -4
- rasa/core/channels/voice_stream/asr/azure.py +9 -0
- rasa/core/channels/voice_stream/twilio_media_streams.py +7 -0
- rasa/core/channels/voice_stream/voice_channel.py +40 -9
- rasa/core/policies/intentless_policy.py +1 -3
- rasa/core/processor.py +50 -5
- rasa/core/utils.py +11 -2
- rasa/dialogue_understanding/coexistence/llm_based_router.py +1 -0
- rasa/dialogue_understanding/commands/__init__.py +4 -0
- rasa/dialogue_understanding/commands/cancel_flow_command.py +3 -1
- rasa/dialogue_understanding/commands/set_slot_command.py +6 -0
- rasa/dialogue_understanding/commands/utils.py +26 -2
- rasa/dialogue_understanding/generator/command_generator.py +11 -1
- rasa/dialogue_understanding/generator/llm_based_command_generator.py +4 -15
- rasa/dialogue_understanding/generator/llm_command_generator.py +1 -3
- rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +4 -44
- rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +1 -14
- rasa/dialogue_understanding/processor/command_processor.py +5 -5
- rasa/dialogue_understanding/stack/frames/flow_stack_frame.py +17 -4
- rasa/dialogue_understanding/stack/utils.py +3 -1
- rasa/dialogue_understanding/utils.py +68 -12
- rasa/dialogue_understanding_test/du_test_schema.yml +3 -3
- rasa/e2e_test/e2e_test_coverage_report.py +1 -1
- rasa/e2e_test/e2e_test_schema.yml +3 -3
- rasa/hooks.py +0 -55
- rasa/llm_fine_tuning/utils.py +2 -4
- rasa/shared/constants.py +0 -5
- rasa/shared/core/flows/constants.py +2 -0
- rasa/shared/core/flows/flow.py +129 -13
- rasa/shared/core/flows/flows_list.py +18 -1
- rasa/shared/core/flows/steps/link.py +7 -2
- rasa/shared/providers/constants.py +0 -9
- rasa/shared/providers/llm/_base_litellm_client.py +4 -14
- rasa/shared/providers/llm/litellm_router_llm_client.py +7 -17
- rasa/shared/providers/llm/llm_client.py +15 -24
- rasa/shared/providers/llm/self_hosted_llm_client.py +2 -10
- rasa/tracing/instrumentation/attribute_extractors.py +2 -2
- rasa/version.py +1 -1
- {rasa_pro-3.12.18.dev1.dist-info → rasa_pro-3.12.20.dist-info}/METADATA +1 -2
- {rasa_pro-3.12.18.dev1.dist-info → rasa_pro-3.12.20.dist-info}/RECORD +44 -45
- rasa/monkey_patches.py +0 -91
- {rasa_pro-3.12.18.dev1.dist-info → rasa_pro-3.12.20.dist-info}/NOTICE +0 -0
- {rasa_pro-3.12.18.dev1.dist-info → rasa_pro-3.12.20.dist-info}/WHEEL +0 -0
- {rasa_pro-3.12.18.dev1.dist-info → rasa_pro-3.12.20.dist-info}/entry_points.txt +0 -0
|
@@ -53,7 +53,8 @@ class FlowStackFrameType(str, Enum):
|
|
|
53
53
|
typ: The string to create the `FlowStackFrameType` from.
|
|
54
54
|
|
|
55
55
|
Returns:
|
|
56
|
-
|
|
56
|
+
The created `FlowStackFrameType`.
|
|
57
|
+
"""
|
|
57
58
|
if typ is None:
|
|
58
59
|
return FlowStackFrameType.REGULAR
|
|
59
60
|
elif typ == FlowStackFrameType.INTERRUPT.value:
|
|
@@ -107,7 +108,8 @@ class BaseFlowStackFrame(DialogueStackFrame):
|
|
|
107
108
|
all_flows: All flows in the assistant.
|
|
108
109
|
|
|
109
110
|
Returns:
|
|
110
|
-
|
|
111
|
+
The current flow.
|
|
112
|
+
"""
|
|
111
113
|
flow = all_flows.flow_by_id(self.flow_id)
|
|
112
114
|
if not flow:
|
|
113
115
|
# we shouldn't ever end up with a frame that belongs to a non
|
|
@@ -122,9 +124,20 @@ class BaseFlowStackFrame(DialogueStackFrame):
|
|
|
122
124
|
all_flows: All flows in the assistant.
|
|
123
125
|
|
|
124
126
|
Returns:
|
|
125
|
-
The current flow step.
|
|
127
|
+
The current flow step.
|
|
128
|
+
"""
|
|
126
129
|
flow = self.flow(all_flows)
|
|
127
|
-
|
|
130
|
+
|
|
131
|
+
step_id = self.step_id
|
|
132
|
+
# in 3.11.4 we added the flow_id as a prefix to the step_id
|
|
133
|
+
# this causes issues when loading old dialogues as the prefix is missing
|
|
134
|
+
# (see https://rasahq.atlassian.net/jira/software/c/projects/ENG/boards/43?selectedIssue=ENG-1939)
|
|
135
|
+
# so we try to find the step by adding the flow prefix to old step_ids as well
|
|
136
|
+
# TODO: remove this in 4.0.0
|
|
137
|
+
alternative_step_id = f"{self.flow_id}_{self.step_id}"
|
|
138
|
+
|
|
139
|
+
step = flow.step_by_id(step_id) or flow.step_by_id(alternative_step_id)
|
|
140
|
+
|
|
128
141
|
if not step:
|
|
129
142
|
# we shouldn't ever end up with a frame that belongs to a non
|
|
130
143
|
# existing step, but if we do, we should raise an error
|
|
@@ -209,7 +209,9 @@ def get_collect_steps_excluding_ask_before_filling_for_active_flow(
|
|
|
209
209
|
All collect steps that are part of the current active flow,
|
|
210
210
|
excluding the collect steps that have to be asked before filling.
|
|
211
211
|
"""
|
|
212
|
-
active_frame = top_user_flow_frame(
|
|
212
|
+
active_frame = top_user_flow_frame(
|
|
213
|
+
dialogue_stack, ignore_call_and_link_frames=False
|
|
214
|
+
)
|
|
213
215
|
if active_frame is None:
|
|
214
216
|
return set()
|
|
215
217
|
active_flow = active_frame.flow(all_flows)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from contextlib import contextmanager
|
|
2
2
|
from typing import Any, Dict, Generator, List, Optional, Text
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import structlog
|
|
5
|
+
|
|
6
|
+
from rasa.dialogue_understanding.commands import Command, NoopCommand, SetSlotCommand
|
|
5
7
|
from rasa.dialogue_understanding.constants import (
|
|
6
8
|
RASA_RECORD_COMMANDS_AND_PROMPTS_ENV_VAR_NAME,
|
|
7
9
|
)
|
|
@@ -16,7 +18,6 @@ from rasa.shared.nlu.constants import (
|
|
|
16
18
|
KEY_USER_PROMPT,
|
|
17
19
|
PREDICTED_COMMANDS,
|
|
18
20
|
PROMPTS,
|
|
19
|
-
SET_SLOT_COMMAND,
|
|
20
21
|
)
|
|
21
22
|
from rasa.shared.nlu.training_data.message import Message
|
|
22
23
|
from rasa.shared.providers.llm.llm_response import LLMResponse
|
|
@@ -26,6 +27,8 @@ record_commands_and_prompts = get_bool_env_variable(
|
|
|
26
27
|
RASA_RECORD_COMMANDS_AND_PROMPTS_ENV_VAR_NAME, False
|
|
27
28
|
)
|
|
28
29
|
|
|
30
|
+
structlogger = structlog.get_logger()
|
|
31
|
+
|
|
29
32
|
|
|
30
33
|
@contextmanager
|
|
31
34
|
def set_record_commands_and_prompts() -> Generator:
|
|
@@ -144,21 +147,74 @@ def _handle_via_nlu_in_coexistence(
|
|
|
144
147
|
if not tracker:
|
|
145
148
|
return False
|
|
146
149
|
|
|
150
|
+
commands = message.get(COMMANDS, [])
|
|
151
|
+
|
|
152
|
+
# If coexistence routing slot is not active, this setup doesn't
|
|
153
|
+
# support dual routing -> default to CALM
|
|
147
154
|
if not tracker.has_coexistence_routing_slot:
|
|
155
|
+
structlogger.debug(
|
|
156
|
+
"utils.handle_via_nlu_in_coexistence"
|
|
157
|
+
".tracker_missing_route_session_to_calm_slot",
|
|
158
|
+
event_info=(
|
|
159
|
+
f"Tracker doesn't have the '{ROUTE_TO_CALM_SLOT}' slot."
|
|
160
|
+
f"Routing to CALM."
|
|
161
|
+
),
|
|
162
|
+
route_session_to_calm=commands,
|
|
163
|
+
)
|
|
148
164
|
return False
|
|
149
165
|
|
|
166
|
+
# Check if the routing decision is stored in the tracker slot
|
|
167
|
+
# If slot is true -> route to CALM
|
|
168
|
+
# If slot is false -> route to DM1
|
|
150
169
|
value = tracker.get_slot(ROUTE_TO_CALM_SLOT)
|
|
151
170
|
if value is not None:
|
|
171
|
+
structlogger.debug(
|
|
172
|
+
"utils.handle_via_nlu_in_coexistence"
|
|
173
|
+
".tracker_route_session_to_calm_slot_value",
|
|
174
|
+
event_info=(
|
|
175
|
+
f"Tracker slot '{ROUTE_TO_CALM_SLOT}' set to '{value}'. "
|
|
176
|
+
f"Routing to "
|
|
177
|
+
f"{'CALM' if value else 'NLU system'}."
|
|
178
|
+
),
|
|
179
|
+
route_session_to_calm_value_in_tracker=value,
|
|
180
|
+
)
|
|
152
181
|
return not value
|
|
153
182
|
|
|
154
|
-
# routing
|
|
155
|
-
#
|
|
156
|
-
if
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
183
|
+
# Non-sticky routing to DM1 is only allowed if NoopCommand is the sole predicted
|
|
184
|
+
# command. In that case, route to DM1
|
|
185
|
+
if len(commands) == 1 and commands[0].get("command") == NoopCommand.command():
|
|
186
|
+
structlogger.debug(
|
|
187
|
+
"utils.handle_via_nlu_in_coexistence.noop_command_detected",
|
|
188
|
+
event_info="NoopCommand found. Routing to NLU system non-sticky.",
|
|
189
|
+
commands=commands,
|
|
190
|
+
)
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
# If the slot was reset (e.g. new session), try to infer routing from
|
|
194
|
+
# attached commands. Look for a SetSlotCommand targeting the ROUTE_TO_CALM_SLOT
|
|
195
|
+
for command in message.get(COMMANDS, []):
|
|
196
|
+
# If slot is true -> route to CALM
|
|
197
|
+
# If slot is false -> route to DM1
|
|
198
|
+
if (
|
|
199
|
+
command.get("command") == SetSlotCommand.command()
|
|
200
|
+
and command.get("name") == ROUTE_TO_CALM_SLOT
|
|
201
|
+
):
|
|
202
|
+
structlogger.debug(
|
|
203
|
+
"utils.handle_via_nlu_in_coexistence.set_slot_command_detected",
|
|
204
|
+
event_info=(
|
|
205
|
+
f"SetSlotCommand setting the '{ROUTE_TO_CALM_SLOT}' to "
|
|
206
|
+
f"'{command.get('value')}'. "
|
|
207
|
+
f"Routing to "
|
|
208
|
+
f"{'CALM' if command.get('value') else 'NLU system'}."
|
|
209
|
+
),
|
|
210
|
+
commands=commands,
|
|
211
|
+
)
|
|
212
|
+
return not command.get("value")
|
|
213
|
+
|
|
214
|
+
# If no routing info is available -> default to CALM
|
|
215
|
+
structlogger.debug(
|
|
216
|
+
"utils.handle_via_nlu_in_coexistence.no_routing_info_available",
|
|
217
|
+
event_info="No routing info available. Routing to CALM.",
|
|
218
|
+
commands=commands,
|
|
219
|
+
)
|
|
164
220
|
return False
|
|
@@ -5,12 +5,12 @@ mapping:
|
|
|
5
5
|
sequence:
|
|
6
6
|
- type: map
|
|
7
7
|
mapping:
|
|
8
|
-
regex;(^[a-zA-Z_]+[a-zA-Z0-9_]*$):
|
|
8
|
+
regex;(^[a-zA-Z_]+[a-zA-Z0-9_\-]*$):
|
|
9
9
|
type: "seq"
|
|
10
10
|
sequence:
|
|
11
11
|
- type: map
|
|
12
12
|
mapping:
|
|
13
|
-
regex;(^[a-zA-Z_]+[a-zA-Z0-9_]*$):
|
|
13
|
+
regex;(^[a-zA-Z_]+[a-zA-Z0-9_\-]*$):
|
|
14
14
|
type: any
|
|
15
15
|
|
|
16
16
|
metadata:
|
|
@@ -129,7 +129,7 @@ mapping:
|
|
|
129
129
|
type: "seq"
|
|
130
130
|
sequence:
|
|
131
131
|
- type: "str"
|
|
132
|
-
pattern: ^[a-zA-Z_]+[a-zA-Z0-9_]*$
|
|
132
|
+
pattern: ^[a-zA-Z_]+[a-zA-Z0-9_\-]*$
|
|
133
133
|
metadata:
|
|
134
134
|
type: "str"
|
|
135
135
|
pattern: ^[a-zA-Z_]+[a-zA-Z0-9_]*$
|
|
@@ -21,7 +21,7 @@ from rasa.shared.core.flows.flow_path import FlowPath, FlowPathsList, PathNode
|
|
|
21
21
|
FLOW_NAME_COL_NAME = "Flow Name"
|
|
22
22
|
NUM_STEPS_COL_NAME = "Num Steps"
|
|
23
23
|
MISSING_STEPS_COL_NAME = "Missing Steps"
|
|
24
|
-
LINE_NUMBERS_COL_NAME = "Line Numbers"
|
|
24
|
+
LINE_NUMBERS_COL_NAME = "Line Numbers for Missing Steps"
|
|
25
25
|
COVERAGE_COL_NAME = "Coverage"
|
|
26
26
|
|
|
27
27
|
FLOWS_KEY = "flows"
|
|
@@ -5,12 +5,12 @@ mapping:
|
|
|
5
5
|
sequence:
|
|
6
6
|
- type: map
|
|
7
7
|
mapping:
|
|
8
|
-
regex;(^[a-zA-Z_]+[a-zA-Z0-9_]*$):
|
|
8
|
+
regex;(^[a-zA-Z_]+[a-zA-Z0-9_\-]*$):
|
|
9
9
|
type: "seq"
|
|
10
10
|
sequence:
|
|
11
11
|
- type: map
|
|
12
12
|
mapping:
|
|
13
|
-
regex;(^[a-zA-Z_]+[a-zA-Z0-9_]*$):
|
|
13
|
+
regex;(^[a-zA-Z_]+[a-zA-Z0-9_\-]*$):
|
|
14
14
|
type: any
|
|
15
15
|
|
|
16
16
|
metadata:
|
|
@@ -129,7 +129,7 @@ mapping:
|
|
|
129
129
|
type: "seq"
|
|
130
130
|
sequence:
|
|
131
131
|
- type: "str"
|
|
132
|
-
pattern: ^[a-zA-Z_]+[a-zA-Z0-9_]*$
|
|
132
|
+
pattern: ^[a-zA-Z_]+[a-zA-Z0-9_\-]*$
|
|
133
133
|
metadata:
|
|
134
134
|
type: "str"
|
|
135
135
|
pattern: ^[a-zA-Z_]+[a-zA-Z0-9_]*$
|
rasa/hooks.py
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import logging
|
|
3
|
-
import os
|
|
4
3
|
from typing import TYPE_CHECKING, List, Optional, Text, Union
|
|
5
4
|
|
|
6
|
-
import litellm
|
|
7
5
|
import pluggy
|
|
8
|
-
import structlog
|
|
9
|
-
|
|
10
|
-
from rasa.shared.providers.constants import (
|
|
11
|
-
LANGFUSE_CALLBACK_NAME,
|
|
12
|
-
LANGFUSE_HOST_ENV_VAR,
|
|
13
|
-
LANGFUSE_PROJECT_ID_ENV_VAR,
|
|
14
|
-
LANGFUSE_PUBLIC_KEY_ENV_VAR,
|
|
15
|
-
LANGFUSE_SECRET_KEY_ENV_VAR,
|
|
16
|
-
RASA_LANGFUSE_INTEGRATION_ENABLED_ENV_VAR,
|
|
17
|
-
)
|
|
18
6
|
|
|
19
7
|
# IMPORTANT: do not import anything from rasa here - use scoped imports
|
|
20
8
|
# this avoids circular imports, as the hooks are used in different places
|
|
@@ -30,7 +18,6 @@ if TYPE_CHECKING:
|
|
|
30
18
|
|
|
31
19
|
hookimpl = pluggy.HookimplMarker("rasa")
|
|
32
20
|
logger = logging.getLogger(__name__)
|
|
33
|
-
structlogger = structlog.get_logger()
|
|
34
21
|
|
|
35
22
|
|
|
36
23
|
@hookimpl # type: ignore[misc]
|
|
@@ -70,8 +57,6 @@ def configure_commandline(cmdline_arguments: argparse.Namespace) -> Optional[Tex
|
|
|
70
57
|
config.configure_tracing(tracer_provider)
|
|
71
58
|
config.configure_metrics(endpoints_file)
|
|
72
59
|
|
|
73
|
-
_init_langfuse_integration()
|
|
74
|
-
|
|
75
60
|
return endpoints_file
|
|
76
61
|
|
|
77
62
|
|
|
@@ -130,43 +115,3 @@ def after_server_stop() -> None:
|
|
|
130
115
|
|
|
131
116
|
if anon_pipeline is not None:
|
|
132
117
|
anon_pipeline.stop()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _is_langfuse_integration_enabled() -> bool:
|
|
136
|
-
return (
|
|
137
|
-
os.environ.get(RASA_LANGFUSE_INTEGRATION_ENABLED_ENV_VAR, "false").lower()
|
|
138
|
-
== "true"
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def _init_langfuse_integration() -> None:
|
|
143
|
-
if not _is_langfuse_integration_enabled():
|
|
144
|
-
structlogger.info(
|
|
145
|
-
"hooks._init_langfuse_integration.disabled",
|
|
146
|
-
event_info="Langfuse integration is disabled.",
|
|
147
|
-
)
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
not os.environ.get(LANGFUSE_HOST_ENV_VAR)
|
|
152
|
-
or not os.environ.get(LANGFUSE_PROJECT_ID_ENV_VAR)
|
|
153
|
-
or not os.environ.get(LANGFUSE_PUBLIC_KEY_ENV_VAR)
|
|
154
|
-
or not os.environ.get(LANGFUSE_SECRET_KEY_ENV_VAR)
|
|
155
|
-
):
|
|
156
|
-
structlogger.warning(
|
|
157
|
-
"hooks._init_langfuse_integration.missing_langfuse_keys",
|
|
158
|
-
event_info=(
|
|
159
|
-
"Langfuse integration is enabled, but some environment variables "
|
|
160
|
-
"are missing. Please set LANGFUSE_HOST, LANGFUSE_PROJECT_ID, "
|
|
161
|
-
"LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY environment "
|
|
162
|
-
"variables to use Langfuse integration."
|
|
163
|
-
),
|
|
164
|
-
)
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
litellm.success_callback = [LANGFUSE_CALLBACK_NAME]
|
|
168
|
-
litellm.failure_callback = [LANGFUSE_CALLBACK_NAME]
|
|
169
|
-
structlogger.info(
|
|
170
|
-
"hooks.langfuse_callbacks_initialized",
|
|
171
|
-
event_info="Langfuse integration initialized.",
|
|
172
|
-
)
|
rasa/llm_fine_tuning/utils.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from contextlib import contextmanager
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Callable, Generator, List, Union
|
|
4
4
|
|
|
5
5
|
import structlog
|
|
6
6
|
|
|
@@ -24,9 +24,7 @@ def make_mock_invoke_llm(commands: str) -> Callable:
|
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
26
|
async def _mock_invoke_llm(
|
|
27
|
-
self: LLMBasedCommandGenerator,
|
|
28
|
-
prompt: Union[List[dict], List[str], str],
|
|
29
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
27
|
+
self: LLMBasedCommandGenerator, prompt: Union[List[dict], List[str], str]
|
|
30
28
|
) -> LLMResponse:
|
|
31
29
|
structlogger.debug(
|
|
32
30
|
f"LLM call intercepted, response mocked. "
|
rasa/shared/constants.py
CHANGED
|
@@ -342,8 +342,3 @@ ROLE_SYSTEM = "system"
|
|
|
342
342
|
# Used for key values in ValidateSlotPatternFlowStackFrame
|
|
343
343
|
REFILL_UTTER = "refill_utter"
|
|
344
344
|
REJECTIONS = "rejections"
|
|
345
|
-
|
|
346
|
-
LANGFUSE_METADATA_USER_ID = "trace_user_id"
|
|
347
|
-
LANGFUSE_METADATA_SESSION_ID = "session_id"
|
|
348
|
-
LANGFUSE_CUSTOM_METADATA_DICT = "trace_metadata"
|
|
349
|
-
LANGFUSE_TAGS = "tags"
|
rasa/shared/core/flows/flow.py
CHANGED
|
@@ -4,7 +4,7 @@ import copy
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from functools import cached_property
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any, Dict, List, Optional, Set, Text, Union
|
|
7
|
+
from typing import Any, Dict, List, Optional, Set, Text, Tuple, Union
|
|
8
8
|
|
|
9
9
|
import structlog
|
|
10
10
|
from pydantic import BaseModel
|
|
@@ -15,10 +15,12 @@ from rasa.engine.language import Language
|
|
|
15
15
|
from rasa.shared.constants import RASA_DEFAULT_FLOW_PATTERN_PREFIX
|
|
16
16
|
from rasa.shared.core.flows.constants import (
|
|
17
17
|
KEY_ALWAYS_INCLUDE_IN_PROMPT,
|
|
18
|
+
KEY_CALLED_FLOW,
|
|
18
19
|
KEY_DESCRIPTION,
|
|
19
20
|
KEY_FILE_PATH,
|
|
20
21
|
KEY_ID,
|
|
21
22
|
KEY_IF,
|
|
23
|
+
KEY_LINKED_FLOW,
|
|
22
24
|
KEY_NAME,
|
|
23
25
|
KEY_NLU_TRIGGER,
|
|
24
26
|
KEY_PERSISTED_SLOTS,
|
|
@@ -41,6 +43,7 @@ from rasa.shared.core.flows.steps import (
|
|
|
41
43
|
CallFlowStep,
|
|
42
44
|
CollectInformationFlowStep,
|
|
43
45
|
EndFlowStep,
|
|
46
|
+
LinkFlowStep,
|
|
44
47
|
StartFlowStep,
|
|
45
48
|
)
|
|
46
49
|
from rasa.shared.core.flows.steps.constants import (
|
|
@@ -61,6 +64,8 @@ class FlowLanguageTranslation(BaseModel):
|
|
|
61
64
|
"""The human-readable name of the flow."""
|
|
62
65
|
|
|
63
66
|
class Config:
|
|
67
|
+
"""Configuration for the FlowLanguageTranslation model."""
|
|
68
|
+
|
|
64
69
|
extra = "ignore"
|
|
65
70
|
|
|
66
71
|
|
|
@@ -232,9 +237,9 @@ class Flow:
|
|
|
232
237
|
return translation.name if translation else None
|
|
233
238
|
|
|
234
239
|
def readable_name(self, language: Optional[Language] = None) -> str:
|
|
235
|
-
"""
|
|
236
|
-
|
|
237
|
-
falls back to the flow's name, and finally the flow's ID.
|
|
240
|
+
"""Returns the flow's name in the specified language if available.
|
|
241
|
+
|
|
242
|
+
Otherwise, falls back to the flow's name, and finally the flow's ID.
|
|
238
243
|
|
|
239
244
|
Args:
|
|
240
245
|
language: Preferred language code.
|
|
@@ -488,6 +493,9 @@ class Flow:
|
|
|
488
493
|
current_path: FlowPath,
|
|
489
494
|
all_paths: FlowPathsList,
|
|
490
495
|
visited_step_ids: Set[str],
|
|
496
|
+
call_stack: Optional[
|
|
497
|
+
List[Tuple[Optional[FlowStep], Optional[Flow], str]]
|
|
498
|
+
] = None,
|
|
491
499
|
) -> None:
|
|
492
500
|
"""Processes the flow steps recursively.
|
|
493
501
|
|
|
@@ -496,19 +504,25 @@ class Flow:
|
|
|
496
504
|
current_path: The current path being constructed.
|
|
497
505
|
all_paths: The list where completed paths are added.
|
|
498
506
|
visited_step_ids: A set of steps that have been visited to avoid cycles.
|
|
507
|
+
call_stack: Tuple list of (flow, path, flow_type) to track path when \
|
|
508
|
+
calling flows through call and link steps.
|
|
499
509
|
|
|
500
510
|
Returns:
|
|
501
511
|
None: This function modifies all_paths in place by appending new paths
|
|
502
512
|
as they are found.
|
|
503
513
|
"""
|
|
514
|
+
if call_stack is None:
|
|
515
|
+
call_stack = []
|
|
516
|
+
|
|
504
517
|
# Check if the step is relevant for testable_paths extraction.
|
|
505
|
-
# We only create new path nodes for
|
|
506
|
-
#
|
|
507
|
-
#
|
|
518
|
+
# We only create new path nodes for CollectInformationFlowStep,
|
|
519
|
+
# ActionFlowStep, CallFlowStep and LinkFlowStep,
|
|
520
|
+
# because these are externally visible changes
|
|
521
|
+
# in the assistant's behaviour (trackable in the e2e tests).
|
|
508
522
|
# For other flow steps, we only follow their links.
|
|
509
|
-
# We decided to ignore calls to other flows in our coverage analysis.
|
|
510
523
|
should_add_node = isinstance(
|
|
511
|
-
current_step,
|
|
524
|
+
current_step,
|
|
525
|
+
(CollectInformationFlowStep, ActionFlowStep, CallFlowStep, LinkFlowStep),
|
|
512
526
|
)
|
|
513
527
|
if should_add_node:
|
|
514
528
|
# Add current step to the current path that is being constructed.
|
|
@@ -520,10 +534,45 @@ class Flow:
|
|
|
520
534
|
)
|
|
521
535
|
)
|
|
522
536
|
|
|
537
|
+
# Check if the current step has already been visited or
|
|
538
|
+
# if the end of the path has been reached.
|
|
539
|
+
# If so, and we’re not within a called flow, we terminate the current path.
|
|
540
|
+
# This also applies for when we're inside a linked flow and reach its end.
|
|
541
|
+
# If we're inside a called flow and reach its end,
|
|
542
|
+
# continue with the next steps in its parent flow.
|
|
523
543
|
if current_step.id in visited_step_ids or self.is_end_of_path(current_step):
|
|
524
|
-
#
|
|
525
|
-
|
|
526
|
-
#
|
|
544
|
+
# Shallow copy is sufficient, since we only pop from the list and
|
|
545
|
+
# don't mutate the objects inside the tuples.
|
|
546
|
+
# The state of FlowStep and Flow does not change during the traversal.
|
|
547
|
+
call_stack_copy = call_stack.copy()
|
|
548
|
+
# parent_flow_type could be any of: None, i.e. main flow,
|
|
549
|
+
# KEY_CALLED_FLOW(=called_flow) or KEY_LINKED_FLOW(=linked_flow)
|
|
550
|
+
parent_step, parent_flow, parent_flow_type = (
|
|
551
|
+
call_stack_copy.pop() if call_stack_copy else (None, None, None)
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Check if within a called flow.
|
|
555
|
+
# If within linked flow, stop the traversal as this takes precedence.
|
|
556
|
+
if parent_step and parent_flow_type == KEY_CALLED_FLOW:
|
|
557
|
+
# As we have reached the END step of a called flow, we need to
|
|
558
|
+
# continue with the next links of the parent step.
|
|
559
|
+
if parent_flow is not None:
|
|
560
|
+
for link in parent_step.next.links:
|
|
561
|
+
parent_flow._handle_link(
|
|
562
|
+
current_path,
|
|
563
|
+
all_paths,
|
|
564
|
+
visited_step_ids,
|
|
565
|
+
link,
|
|
566
|
+
call_stack_copy,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
else:
|
|
570
|
+
# Found a cycle, or reached an end step, do not proceed further.
|
|
571
|
+
all_paths.paths.append(copy.deepcopy(current_path))
|
|
572
|
+
|
|
573
|
+
# Backtrack: remove the last node after reaching a terminal step.
|
|
574
|
+
# Ensures the path is correctly backtracked, after a path ends or
|
|
575
|
+
# a cycle is detected.
|
|
527
576
|
if should_add_node:
|
|
528
577
|
current_path.nodes.pop()
|
|
529
578
|
return
|
|
@@ -531,6 +580,62 @@ class Flow:
|
|
|
531
580
|
# Mark current step as visited in this path.
|
|
532
581
|
visited_step_ids.add(current_step.id)
|
|
533
582
|
|
|
583
|
+
# If the current step is a call step, we need to resolve the call
|
|
584
|
+
# and continue with the steps of the called flow.
|
|
585
|
+
if isinstance(current_step, CallFlowStep):
|
|
586
|
+
# Get the steps of the called flow and continue with them.
|
|
587
|
+
called_flow = current_step.called_flow_reference
|
|
588
|
+
if called_flow and (
|
|
589
|
+
start_step_in_called_flow := called_flow.first_step_in_flow()
|
|
590
|
+
):
|
|
591
|
+
call_stack.append((current_step, self, KEY_CALLED_FLOW))
|
|
592
|
+
called_flow._go_over_steps(
|
|
593
|
+
start_step_in_called_flow,
|
|
594
|
+
current_path,
|
|
595
|
+
all_paths,
|
|
596
|
+
visited_step_ids,
|
|
597
|
+
call_stack,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
# After processing the steps of the called (child) flow,
|
|
601
|
+
# remove them from the visited steps
|
|
602
|
+
# to allow the calling (parent) flow to revisit them later.
|
|
603
|
+
visited_step_ids.remove(current_step.id)
|
|
604
|
+
call_stack.pop()
|
|
605
|
+
|
|
606
|
+
# Backtrack: remove the last node
|
|
607
|
+
# after returning from a called (child) flow.
|
|
608
|
+
# Ensures the parent flow can continue exploring other branches.
|
|
609
|
+
if should_add_node:
|
|
610
|
+
current_path.nodes.pop()
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
# If the current step is a LinkFlowStep, step into the linked flow,
|
|
614
|
+
# process its links, and do not return from that flow anymore.
|
|
615
|
+
if isinstance(current_step, LinkFlowStep):
|
|
616
|
+
# Get the steps of the linked flow and continue with them.
|
|
617
|
+
linked_flow = current_step.linked_flow_reference
|
|
618
|
+
if linked_flow and (
|
|
619
|
+
start_step_in_linked_flow := linked_flow.first_step_in_flow()
|
|
620
|
+
):
|
|
621
|
+
call_stack.append((current_step, self, KEY_LINKED_FLOW))
|
|
622
|
+
linked_flow._go_over_steps(
|
|
623
|
+
start_step_in_linked_flow,
|
|
624
|
+
current_path,
|
|
625
|
+
all_paths,
|
|
626
|
+
visited_step_ids,
|
|
627
|
+
call_stack,
|
|
628
|
+
)
|
|
629
|
+
visited_step_ids.remove(current_step.id)
|
|
630
|
+
call_stack.pop()
|
|
631
|
+
|
|
632
|
+
# Backtrack: remove the last node
|
|
633
|
+
# after returning from a linked (child) flow.
|
|
634
|
+
# Ensures the parent can continue after the linked flow is processed.
|
|
635
|
+
if should_add_node:
|
|
636
|
+
current_path.nodes.pop()
|
|
637
|
+
return
|
|
638
|
+
|
|
534
639
|
# Iterate over all links of the current step.
|
|
535
640
|
for link in current_step.next.links:
|
|
536
641
|
self._handle_link(
|
|
@@ -538,12 +643,15 @@ class Flow:
|
|
|
538
643
|
all_paths,
|
|
539
644
|
visited_step_ids,
|
|
540
645
|
link,
|
|
646
|
+
call_stack,
|
|
541
647
|
)
|
|
542
648
|
|
|
543
649
|
# Backtrack the current step and remove it from the path.
|
|
544
650
|
visited_step_ids.remove(current_step.id)
|
|
545
651
|
|
|
546
|
-
#
|
|
652
|
+
# Backtrack: remove the last node
|
|
653
|
+
# after processing all links of the current step.
|
|
654
|
+
# Ensures the next recursion can start once all links are explored.
|
|
547
655
|
if should_add_node:
|
|
548
656
|
current_path.nodes.pop()
|
|
549
657
|
|
|
@@ -553,6 +661,9 @@ class Flow:
|
|
|
553
661
|
all_paths: FlowPathsList,
|
|
554
662
|
visited_step_ids: Set[str],
|
|
555
663
|
link: FlowStepLink,
|
|
664
|
+
call_stack: Optional[
|
|
665
|
+
List[Tuple[Optional[FlowStep], Optional[Flow], str]]
|
|
666
|
+
] = None,
|
|
556
667
|
) -> None:
|
|
557
668
|
"""Handles the next step in a flow.
|
|
558
669
|
|
|
@@ -561,6 +672,8 @@ class Flow:
|
|
|
561
672
|
all_paths: The list where completed paths are added.
|
|
562
673
|
visited_step_ids: A set of steps that have been visited to avoid cycles.
|
|
563
674
|
link: The link to be followed.
|
|
675
|
+
call_stack: Tuple list of (flow, path, flow_type) to track path when \
|
|
676
|
+
calling flows through call and link steps..
|
|
564
677
|
|
|
565
678
|
Returns:
|
|
566
679
|
None: This function modifies all_paths in place by appending new paths
|
|
@@ -575,6 +688,7 @@ class Flow:
|
|
|
575
688
|
current_path,
|
|
576
689
|
all_paths,
|
|
577
690
|
visited_step_ids,
|
|
691
|
+
call_stack,
|
|
578
692
|
)
|
|
579
693
|
return
|
|
580
694
|
# IfFlowStepLink and ElseFlowStepLink are conditional links.
|
|
@@ -588,6 +702,7 @@ class Flow:
|
|
|
588
702
|
current_path,
|
|
589
703
|
all_paths,
|
|
590
704
|
visited_step_ids,
|
|
705
|
+
call_stack,
|
|
591
706
|
)
|
|
592
707
|
return
|
|
593
708
|
else:
|
|
@@ -598,6 +713,7 @@ class Flow:
|
|
|
598
713
|
current_path,
|
|
599
714
|
all_paths,
|
|
600
715
|
visited_step_ids,
|
|
716
|
+
call_stack,
|
|
601
717
|
)
|
|
602
718
|
return
|
|
603
719
|
|
|
@@ -36,6 +36,7 @@ class FlowsList:
|
|
|
36
36
|
def __post_init__(self) -> None:
|
|
37
37
|
"""Initializes the FlowsList object."""
|
|
38
38
|
self._resolve_called_flows()
|
|
39
|
+
self._resolve_linked_flows()
|
|
39
40
|
|
|
40
41
|
def __iter__(self) -> Generator[Flow, None, None]:
|
|
41
42
|
"""Iterates over the flows."""
|
|
@@ -103,7 +104,10 @@ class FlowsList:
|
|
|
103
104
|
)
|
|
104
105
|
|
|
105
106
|
def _resolve_called_flows(self) -> None:
|
|
106
|
-
"""Resolves the called flows.
|
|
107
|
+
"""Resolves the called flows.
|
|
108
|
+
|
|
109
|
+
`Resolving` here means connecting the step to the actual `Flow` object.
|
|
110
|
+
"""
|
|
107
111
|
from rasa.shared.core.flows.steps import CallFlowStep
|
|
108
112
|
|
|
109
113
|
for flow in self.underlying_flows:
|
|
@@ -112,6 +116,19 @@ class FlowsList:
|
|
|
112
116
|
# only resolve the reference, if it isn't already resolved
|
|
113
117
|
step.called_flow_reference = self.flow_by_id(step.call)
|
|
114
118
|
|
|
119
|
+
def _resolve_linked_flows(self) -> None:
|
|
120
|
+
"""Resolves the linked flows.
|
|
121
|
+
|
|
122
|
+
`Resolving` here means connecting the step to the actual `Flow` object.
|
|
123
|
+
"""
|
|
124
|
+
from rasa.shared.core.flows.steps import LinkFlowStep
|
|
125
|
+
|
|
126
|
+
for flow in self.underlying_flows:
|
|
127
|
+
for step in flow.steps:
|
|
128
|
+
if isinstance(step, LinkFlowStep) and not step.linked_flow_reference:
|
|
129
|
+
# only resolve the reference, if it isn't already resolved
|
|
130
|
+
step.linked_flow_reference = self.flow_by_id(step.link)
|
|
131
|
+
|
|
115
132
|
def as_json_list(self) -> List[Dict[Text, Any]]:
|
|
116
133
|
"""Serialize the FlowsList object to list format and not to the original dict.
|
|
117
134
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any, Dict, Text
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Dict, Text
|
|
5
5
|
|
|
6
|
-
from rasa.shared.core.flows.flow_step import FlowStep
|
|
6
|
+
from rasa.shared.core.flows.flow_step import FlowStep, Optional
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from rasa.shared.core.flows.flow import Flow
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
@dataclass
|
|
@@ -12,6 +15,8 @@ class LinkFlowStep(FlowStep):
|
|
|
12
15
|
|
|
13
16
|
link: Text
|
|
14
17
|
"""The id of the flow that should be started subsequently."""
|
|
18
|
+
linked_flow_reference: Optional["Flow"] = None
|
|
19
|
+
"""The flow that is linked to by this step."""
|
|
15
20
|
|
|
16
21
|
def does_allow_for_next_step(self) -> bool:
|
|
17
22
|
"""Returns whether this step allows for following steps.
|