rasa-pro 3.12.0.dev9__py3-none-any.whl → 3.12.0.dev10__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/core/actions/action.py +17 -3
- rasa/core/actions/action_handle_digressions.py +142 -0
- rasa/core/actions/forms.py +4 -2
- rasa/core/channels/voice_ready/audiocodes.py +42 -23
- rasa/core/channels/voice_stream/tts/azure.py +2 -1
- rasa/core/migrate.py +2 -2
- rasa/core/policies/flows/flow_executor.py +33 -1
- rasa/dialogue_understanding/commands/can_not_handle_command.py +2 -2
- rasa/dialogue_understanding/commands/cancel_flow_command.py +62 -4
- rasa/dialogue_understanding/commands/change_flow_command.py +2 -2
- rasa/dialogue_understanding/commands/chit_chat_answer_command.py +2 -2
- rasa/dialogue_understanding/commands/clarify_command.py +2 -2
- rasa/dialogue_understanding/commands/correct_slots_command.py +11 -2
- rasa/dialogue_understanding/commands/handle_digressions_command.py +150 -0
- rasa/dialogue_understanding/commands/human_handoff_command.py +2 -2
- rasa/dialogue_understanding/commands/knowledge_answer_command.py +2 -2
- rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +2 -2
- rasa/dialogue_understanding/commands/set_slot_command.py +7 -15
- rasa/dialogue_understanding/commands/skip_question_command.py +2 -2
- rasa/dialogue_understanding/commands/start_flow_command.py +43 -2
- rasa/dialogue_understanding/commands/utils.py +1 -1
- rasa/dialogue_understanding/constants.py +1 -0
- rasa/dialogue_understanding/generator/command_generator.py +10 -76
- rasa/dialogue_understanding/generator/command_parser.py +1 -1
- rasa/dialogue_understanding/generator/llm_based_command_generator.py +126 -2
- rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +10 -2
- rasa/dialogue_understanding/generator/nlu_command_adapter.py +4 -2
- rasa/dialogue_understanding/generator/single_step/command_prompt_template.jinja2 +40 -40
- rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +11 -19
- rasa/dialogue_understanding/patterns/correction.py +13 -1
- rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +62 -2
- rasa/dialogue_understanding/patterns/handle_digressions.py +81 -0
- rasa/dialogue_understanding/processor/command_processor.py +117 -28
- rasa/dialogue_understanding/utils.py +31 -0
- rasa/dialogue_understanding_test/test_case_simulation/test_case_tracker_simulator.py +2 -2
- rasa/shared/core/constants.py +22 -1
- rasa/shared/core/domain.py +6 -4
- rasa/shared/core/events.py +13 -2
- rasa/shared/core/flows/flow.py +17 -0
- rasa/shared/core/flows/flows_yaml_schema.json +38 -0
- rasa/shared/core/flows/steps/collect.py +18 -1
- rasa/shared/core/flows/utils.py +16 -1
- rasa/shared/core/slot_mappings.py +6 -6
- rasa/shared/core/slots.py +19 -0
- rasa/shared/core/trackers.py +3 -1
- rasa/shared/nlu/constants.py +1 -0
- rasa/shared/providers/llm/_base_litellm_client.py +0 -40
- rasa/shared/utils/llm.py +1 -86
- rasa/shared/utils/schemas/domain.yml +0 -1
- rasa/validator.py +172 -22
- rasa/version.py +1 -1
- {rasa_pro-3.12.0.dev9.dist-info → rasa_pro-3.12.0.dev10.dist-info}/METADATA +1 -1
- {rasa_pro-3.12.0.dev9.dist-info → rasa_pro-3.12.0.dev10.dist-info}/RECORD +56 -53
- {rasa_pro-3.12.0.dev9.dist-info → rasa_pro-3.12.0.dev10.dist-info}/NOTICE +0 -0
- {rasa_pro-3.12.0.dev9.dist-info → rasa_pro-3.12.0.dev10.dist-info}/WHEEL +0 -0
- {rasa_pro-3.12.0.dev9.dist-info → rasa_pro-3.12.0.dev10.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
|
|
8
|
+
from rasa.dialogue_understanding.commands.command import Command
|
|
9
|
+
from rasa.dialogue_understanding.patterns.cannot_handle import (
|
|
10
|
+
CannotHandlePatternFlowStackFrame,
|
|
11
|
+
)
|
|
12
|
+
from rasa.dialogue_understanding.patterns.handle_digressions import (
|
|
13
|
+
HandleDigressionsPatternFlowStackFrame,
|
|
14
|
+
)
|
|
15
|
+
from rasa.dialogue_understanding.stack.utils import (
|
|
16
|
+
top_flow_frame,
|
|
17
|
+
user_flows_on_the_stack,
|
|
18
|
+
)
|
|
19
|
+
from rasa.shared.core.events import Event
|
|
20
|
+
from rasa.shared.core.flows import FlowsList
|
|
21
|
+
from rasa.shared.core.flows.steps import CollectInformationFlowStep
|
|
22
|
+
from rasa.shared.core.flows.utils import ALL_LABEL
|
|
23
|
+
from rasa.shared.core.trackers import DialogueStateTracker
|
|
24
|
+
from rasa.shared.nlu.constants import HANDLE_DIGRESSIONS_COMMAND
|
|
25
|
+
|
|
26
|
+
structlogger = structlog.get_logger()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class HandleDigressionsCommand(Command):
|
|
31
|
+
"""A command to handle digressions during an active flow."""
|
|
32
|
+
|
|
33
|
+
flow: str
|
|
34
|
+
"""The interrupting flow."""
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def command(cls) -> str:
|
|
38
|
+
"""Returns the command type."""
|
|
39
|
+
return HANDLE_DIGRESSIONS_COMMAND
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_dict(cls, data: Dict[str, Any]) -> HandleDigressionsCommand:
|
|
43
|
+
"""Converts the dictionary to a command.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The converted dictionary.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
return HandleDigressionsCommand(flow=data["flow"])
|
|
50
|
+
except KeyError as e:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Missing parameter '{e}' while parsing HandleDigressionsCommand."
|
|
53
|
+
) from e
|
|
54
|
+
|
|
55
|
+
def run_command_on_tracker(
|
|
56
|
+
self,
|
|
57
|
+
tracker: DialogueStateTracker,
|
|
58
|
+
all_flows: FlowsList,
|
|
59
|
+
original_tracker: DialogueStateTracker,
|
|
60
|
+
) -> List[Event]:
|
|
61
|
+
"""Runs the command on the tracker.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
tracker: The tracker to run the command on.
|
|
65
|
+
all_flows: All flows in the assistant.
|
|
66
|
+
original_tracker: The tracker before any command was executed.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
The events to apply to the tracker.
|
|
70
|
+
"""
|
|
71
|
+
stack = tracker.stack
|
|
72
|
+
original_stack = original_tracker.stack
|
|
73
|
+
|
|
74
|
+
if self.flow in user_flows_on_the_stack(stack):
|
|
75
|
+
structlogger.debug(
|
|
76
|
+
"command_executor.skip_command.already_started_flow", command=self
|
|
77
|
+
)
|
|
78
|
+
return []
|
|
79
|
+
elif self.flow not in all_flows.flow_ids:
|
|
80
|
+
structlogger.debug(
|
|
81
|
+
"command_executor.push_cannot_handle.start_invalid_flow_id",
|
|
82
|
+
command=self,
|
|
83
|
+
)
|
|
84
|
+
stack.push(CannotHandlePatternFlowStackFrame())
|
|
85
|
+
return tracker.create_stack_updated_events(stack)
|
|
86
|
+
|
|
87
|
+
# this allows to include called user flows in the stack search
|
|
88
|
+
latest_user_frame = top_flow_frame(original_stack, ignore_call_frames=False)
|
|
89
|
+
|
|
90
|
+
if latest_user_frame is None:
|
|
91
|
+
structlogger.debug(
|
|
92
|
+
"command_executor.skip_command.no_top_flow", command=self
|
|
93
|
+
)
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
original_top_flow = latest_user_frame.flow(all_flows)
|
|
97
|
+
current_step = original_top_flow.step_by_id(latest_user_frame.step_id)
|
|
98
|
+
if not isinstance(current_step, CollectInformationFlowStep):
|
|
99
|
+
structlogger.debug(
|
|
100
|
+
"command_executor.skip_command.not_at_a_collect_step", command=self
|
|
101
|
+
)
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
ask_confirm_digressions = set(
|
|
105
|
+
current_step.ask_confirm_digressions
|
|
106
|
+
+ original_top_flow.ask_confirm_digressions
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
block_digressions = set(
|
|
110
|
+
current_step.block_digressions + original_top_flow.block_digressions
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if block_digressions:
|
|
114
|
+
if ALL_LABEL in block_digressions:
|
|
115
|
+
block_digressions.remove(ALL_LABEL)
|
|
116
|
+
block_digressions.add(self.flow)
|
|
117
|
+
|
|
118
|
+
if ask_confirm_digressions:
|
|
119
|
+
if ALL_LABEL in ask_confirm_digressions:
|
|
120
|
+
ask_confirm_digressions.remove(ALL_LABEL)
|
|
121
|
+
ask_confirm_digressions.add(self.flow)
|
|
122
|
+
|
|
123
|
+
structlogger.debug(
|
|
124
|
+
"command_executor.push_handle_digressions",
|
|
125
|
+
interrupting_flow_id=self.flow,
|
|
126
|
+
interrupted_flow_id=original_top_flow.id,
|
|
127
|
+
interrupted_step_id=current_step.id,
|
|
128
|
+
ask_confirm_digressions=ask_confirm_digressions,
|
|
129
|
+
block_digressions=block_digressions,
|
|
130
|
+
)
|
|
131
|
+
stack.push(
|
|
132
|
+
HandleDigressionsPatternFlowStackFrame(
|
|
133
|
+
interrupting_flow_id=self.flow,
|
|
134
|
+
interrupted_flow_id=original_top_flow.id,
|
|
135
|
+
interrupted_step_id=current_step.id,
|
|
136
|
+
ask_confirm_digressions=ask_confirm_digressions,
|
|
137
|
+
block_digressions=block_digressions,
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return tracker.create_stack_updated_events(stack)
|
|
142
|
+
|
|
143
|
+
def __hash__(self) -> int:
|
|
144
|
+
return hash(self.flow)
|
|
145
|
+
|
|
146
|
+
def __eq__(self, other: object) -> bool:
|
|
147
|
+
if not isinstance(other, HandleDigressionsCommand):
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
return other.flow == self.flow
|
|
@@ -66,7 +66,7 @@ class HumanHandoffCommand(Command):
|
|
|
66
66
|
|
|
67
67
|
def to_dsl(self) -> str:
|
|
68
68
|
"""Converts the command to a DSL string."""
|
|
69
|
-
return "
|
|
69
|
+
return "HumanHandoff()"
|
|
70
70
|
|
|
71
71
|
@classmethod
|
|
72
72
|
def from_dsl(cls, match: re.Match, **kwargs: Any) -> HumanHandoffCommand:
|
|
@@ -75,4 +75,4 @@ class HumanHandoffCommand(Command):
|
|
|
75
75
|
|
|
76
76
|
@staticmethod
|
|
77
77
|
def regex_pattern() -> str:
|
|
78
|
-
return r"
|
|
78
|
+
return r"HumanHandoff\(\)"
|
|
@@ -59,7 +59,7 @@ class KnowledgeAnswerCommand(FreeFormAnswerCommand):
|
|
|
59
59
|
|
|
60
60
|
def to_dsl(self) -> str:
|
|
61
61
|
"""Converts the command to a DSL string."""
|
|
62
|
-
return "
|
|
62
|
+
return "SearchAndReply()"
|
|
63
63
|
|
|
64
64
|
@classmethod
|
|
65
65
|
def from_dsl(cls, match: re.Match, **kwargs: Any) -> KnowledgeAnswerCommand:
|
|
@@ -68,4 +68,4 @@ class KnowledgeAnswerCommand(FreeFormAnswerCommand):
|
|
|
68
68
|
|
|
69
69
|
@staticmethod
|
|
70
70
|
def regex_pattern() -> str:
|
|
71
|
-
return r"
|
|
71
|
+
return r"SearchAndReply\(\)"
|
|
@@ -60,7 +60,7 @@ class RepeatBotMessagesCommand(Command):
|
|
|
60
60
|
|
|
61
61
|
def to_dsl(self) -> str:
|
|
62
62
|
"""Converts the command to a DSL string."""
|
|
63
|
-
return "
|
|
63
|
+
return "RepeatLastBotMessages()"
|
|
64
64
|
|
|
65
65
|
@classmethod
|
|
66
66
|
def from_dsl(cls, match: re.Match, **kwargs: Any) -> RepeatBotMessagesCommand:
|
|
@@ -69,4 +69,4 @@ class RepeatBotMessagesCommand(Command):
|
|
|
69
69
|
|
|
70
70
|
@staticmethod
|
|
71
71
|
def regex_pattern() -> str:
|
|
72
|
-
return r"
|
|
72
|
+
return r"RepeatLastBotMessages\(\)"
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from enum import Enum
|
|
6
5
|
from typing import Any, Dict, List
|
|
7
6
|
|
|
8
7
|
import structlog
|
|
@@ -19,6 +18,7 @@ from rasa.dialogue_understanding.stack.utils import (
|
|
|
19
18
|
get_collect_steps_excluding_ask_before_filling_for_active_flow,
|
|
20
19
|
)
|
|
21
20
|
from rasa.shared.constants import ROUTE_TO_CALM_SLOT
|
|
21
|
+
from rasa.shared.core.constants import SetSlotExtractor
|
|
22
22
|
from rasa.shared.core.events import Event, SlotSet
|
|
23
23
|
from rasa.shared.core.flows import FlowsList
|
|
24
24
|
from rasa.shared.core.trackers import DialogueStateTracker
|
|
@@ -27,17 +27,6 @@ from rasa.shared.nlu.constants import SET_SLOT_COMMAND
|
|
|
27
27
|
structlogger = structlog.get_logger()
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
class SetSlotExtractor(Enum):
|
|
31
|
-
"""The extractors that can set a slot."""
|
|
32
|
-
|
|
33
|
-
LLM = "LLM"
|
|
34
|
-
COMMAND_PAYLOAD_READER = "CommandPayloadReader"
|
|
35
|
-
NLU = "NLU"
|
|
36
|
-
|
|
37
|
-
def __str__(self) -> str:
|
|
38
|
-
return self.value
|
|
39
|
-
|
|
40
|
-
|
|
41
30
|
def get_flows_predicted_to_start_from_tracker(
|
|
42
31
|
tracker: DialogueStateTracker,
|
|
43
32
|
) -> List[str]:
|
|
@@ -137,6 +126,7 @@ class SetSlotCommand(Command):
|
|
|
137
126
|
in {
|
|
138
127
|
SetSlotExtractor.LLM.value,
|
|
139
128
|
SetSlotExtractor.COMMAND_PAYLOAD_READER.value,
|
|
129
|
+
SetSlotExtractor.NLU.value,
|
|
140
130
|
}
|
|
141
131
|
):
|
|
142
132
|
# Get the other predicted flows from the most recent message on the tracker.
|
|
@@ -154,7 +144,9 @@ class SetSlotCommand(Command):
|
|
|
154
144
|
return []
|
|
155
145
|
|
|
156
146
|
structlogger.debug("command_executor.set_slot", command=self)
|
|
157
|
-
return [
|
|
147
|
+
return [
|
|
148
|
+
SlotSet(self.name, slot.coerce_value(self.value), filled_by=self.extractor)
|
|
149
|
+
]
|
|
158
150
|
|
|
159
151
|
def __hash__(self) -> int:
|
|
160
152
|
return hash(self.value) + hash(self.name)
|
|
@@ -170,7 +162,7 @@ class SetSlotCommand(Command):
|
|
|
170
162
|
|
|
171
163
|
def to_dsl(self) -> str:
|
|
172
164
|
"""Converts the command to a DSL string."""
|
|
173
|
-
return f"
|
|
165
|
+
return f"SetSlot({self.name}, {self.value})"
|
|
174
166
|
|
|
175
167
|
@classmethod
|
|
176
168
|
def from_dsl(cls, match: re.Match, **kwargs: Any) -> SetSlotCommand:
|
|
@@ -181,4 +173,4 @@ class SetSlotCommand(Command):
|
|
|
181
173
|
|
|
182
174
|
@staticmethod
|
|
183
175
|
def regex_pattern() -> str:
|
|
184
|
-
return r"""
|
|
176
|
+
return r"""SetSlot\(['"]?([a-zA-Z_][a-zA-Z0-9_-]*)['"]?, ?['"]?(.*)['"]?\)"""
|
|
@@ -75,7 +75,7 @@ class SkipQuestionCommand(Command):
|
|
|
75
75
|
|
|
76
76
|
def to_dsl(self) -> str:
|
|
77
77
|
"""Converts the command to a DSL string."""
|
|
78
|
-
return "
|
|
78
|
+
return "SkipQuestion()"
|
|
79
79
|
|
|
80
80
|
@classmethod
|
|
81
81
|
def from_dsl(cls, match: re.Match, **kwargs: Any) -> SkipQuestionCommand:
|
|
@@ -84,4 +84,4 @@ class SkipQuestionCommand(Command):
|
|
|
84
84
|
|
|
85
85
|
@staticmethod
|
|
86
86
|
def regex_pattern() -> str:
|
|
87
|
-
return r"
|
|
87
|
+
return r"SkipQuestion\(\)"
|
|
@@ -7,6 +7,11 @@ from typing import Any, Dict, List, Optional
|
|
|
7
7
|
import structlog
|
|
8
8
|
|
|
9
9
|
from rasa.dialogue_understanding.commands.command import Command
|
|
10
|
+
from rasa.dialogue_understanding.patterns.clarify import FLOW_PATTERN_CLARIFICATION
|
|
11
|
+
from rasa.dialogue_understanding.patterns.continue_interrupted import (
|
|
12
|
+
ContinueInterruptedPatternFlowStackFrame,
|
|
13
|
+
)
|
|
14
|
+
from rasa.dialogue_understanding.stack.dialogue_stack import DialogueStack
|
|
10
15
|
from rasa.dialogue_understanding.stack.frames.flow_stack_frame import (
|
|
11
16
|
FlowStackFrameType,
|
|
12
17
|
UserFlowStackFrame,
|
|
@@ -68,6 +73,10 @@ class StartFlowCommand(Command):
|
|
|
68
73
|
applied_events: List[Event] = []
|
|
69
74
|
|
|
70
75
|
if self.flow in user_flows_on_the_stack(stack):
|
|
76
|
+
top_frame = stack.top()
|
|
77
|
+
if top_frame is not None and top_frame.type() == FLOW_PATTERN_CLARIFICATION:
|
|
78
|
+
return self.change_flow_frame_position_in_the_stack(stack, tracker)
|
|
79
|
+
|
|
71
80
|
structlogger.debug(
|
|
72
81
|
"command_executor.skip_command.already_started_flow", command=self
|
|
73
82
|
)
|
|
@@ -110,7 +119,7 @@ class StartFlowCommand(Command):
|
|
|
110
119
|
|
|
111
120
|
def to_dsl(self) -> str:
|
|
112
121
|
"""Converts the command to a DSL string."""
|
|
113
|
-
return f"
|
|
122
|
+
return f"StartFlow({self.flow})"
|
|
114
123
|
|
|
115
124
|
@classmethod
|
|
116
125
|
def from_dsl(cls, match: re.Match, **kwargs: Any) -> Optional[StartFlowCommand]:
|
|
@@ -119,4 +128,36 @@ class StartFlowCommand(Command):
|
|
|
119
128
|
|
|
120
129
|
@staticmethod
|
|
121
130
|
def regex_pattern() -> str:
|
|
122
|
-
return r"
|
|
131
|
+
return r"StartFlow\(['\"]?([a-zA-Z0-9_-]+)['\"]?\)"
|
|
132
|
+
|
|
133
|
+
def change_flow_frame_position_in_the_stack(
|
|
134
|
+
self, stack: DialogueStack, tracker: DialogueStateTracker
|
|
135
|
+
) -> List[Event]:
|
|
136
|
+
"""Changes the position of the flow frame in the stack.
|
|
137
|
+
|
|
138
|
+
This is a special case when pattern clarification is the active flow and
|
|
139
|
+
the same flow is selected to start. In this case, the existing flow frame
|
|
140
|
+
should be moved up in the stack.
|
|
141
|
+
"""
|
|
142
|
+
frames = stack.frames[:]
|
|
143
|
+
|
|
144
|
+
for idx, frame in enumerate(frames):
|
|
145
|
+
if isinstance(frame, UserFlowStackFrame) and frame.flow_id == self.flow:
|
|
146
|
+
structlogger.debug(
|
|
147
|
+
"command_executor.change_flow_position_during_clarification",
|
|
148
|
+
command=self,
|
|
149
|
+
index=idx,
|
|
150
|
+
)
|
|
151
|
+
# pop the continue interrupted flow frame if it exists
|
|
152
|
+
next_frame = frames[idx + 1] if idx + 1 < len(frames) else None
|
|
153
|
+
if (
|
|
154
|
+
isinstance(next_frame, ContinueInterruptedPatternFlowStackFrame)
|
|
155
|
+
and next_frame.previous_flow_name == self.flow
|
|
156
|
+
):
|
|
157
|
+
stack.frames.pop(idx + 1)
|
|
158
|
+
# move up the existing flow from the stack
|
|
159
|
+
stack.frames.pop(idx)
|
|
160
|
+
stack.push(frame)
|
|
161
|
+
return tracker.create_stack_updated_events(stack)
|
|
162
|
+
|
|
163
|
+
return []
|
|
@@ -27,7 +27,7 @@ def extract_cleaned_options(options_str: str) -> List[str]:
|
|
|
27
27
|
"""Extract and clean options from a string."""
|
|
28
28
|
return sorted(
|
|
29
29
|
opt.strip().strip('"').strip("'")
|
|
30
|
-
for opt in options_str.split("
|
|
30
|
+
for opt in options_str.split(",")
|
|
31
31
|
if opt.strip()
|
|
32
32
|
)
|
|
33
33
|
|
|
@@ -6,18 +6,17 @@ import structlog
|
|
|
6
6
|
from rasa.dialogue_understanding.commands import (
|
|
7
7
|
Command,
|
|
8
8
|
ErrorCommand,
|
|
9
|
-
SetSlotCommand,
|
|
10
9
|
StartFlowCommand,
|
|
11
10
|
)
|
|
12
|
-
from rasa.dialogue_understanding.
|
|
11
|
+
from rasa.dialogue_understanding.utils import (
|
|
12
|
+
_handle_via_nlu_in_coexistence,
|
|
13
|
+
)
|
|
13
14
|
from rasa.shared.constants import (
|
|
14
15
|
RASA_PATTERN_INTERNAL_ERROR_USER_INPUT_EMPTY,
|
|
15
16
|
RASA_PATTERN_INTERNAL_ERROR_USER_INPUT_TOO_LONG,
|
|
16
17
|
)
|
|
17
|
-
from rasa.shared.core.constants import SlotMappingType
|
|
18
18
|
from rasa.shared.core.domain import Domain
|
|
19
19
|
from rasa.shared.core.flows import FlowsList
|
|
20
|
-
from rasa.shared.core.slot_mappings import SlotFillingManager
|
|
21
20
|
from rasa.shared.core.trackers import DialogueStateTracker
|
|
22
21
|
from rasa.shared.nlu.constants import (
|
|
23
22
|
COMMANDS,
|
|
@@ -92,9 +91,9 @@ class CommandGenerator:
|
|
|
92
91
|
)
|
|
93
92
|
|
|
94
93
|
for message in messages:
|
|
95
|
-
if message
|
|
96
|
-
#
|
|
97
|
-
#
|
|
94
|
+
if _handle_via_nlu_in_coexistence(tracker, message):
|
|
95
|
+
# Skip running the CALM pipeline if the message should
|
|
96
|
+
# be handled by the NLU-based system in a coexistence mode.
|
|
98
97
|
continue
|
|
99
98
|
|
|
100
99
|
commands = await self._evaluate_and_predict(
|
|
@@ -106,9 +105,6 @@ class CommandGenerator:
|
|
|
106
105
|
commands = self._check_commands_against_startable_flows(
|
|
107
106
|
commands, startable_flows
|
|
108
107
|
)
|
|
109
|
-
commands = self._check_commands_against_slot_mappings(
|
|
110
|
-
commands, tracker, domain
|
|
111
|
-
)
|
|
112
108
|
commands_dicts = [command.as_dict() for command in commands]
|
|
113
109
|
message.set(COMMANDS, commands_dicts, add_to_output=True)
|
|
114
110
|
|
|
@@ -278,70 +274,8 @@ class CommandGenerator:
|
|
|
278
274
|
return len(message.get(TEXT, "").strip()) == 0
|
|
279
275
|
|
|
280
276
|
@staticmethod
|
|
281
|
-
def
|
|
282
|
-
commands
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
) -> List[Command]:
|
|
286
|
-
"""Check if the LLM-issued slot commands are fillable.
|
|
287
|
-
|
|
288
|
-
The LLM-issued slot commands are fillable if the slot
|
|
289
|
-
mappings are satisfied.
|
|
290
|
-
"""
|
|
291
|
-
if not domain:
|
|
292
|
-
return commands
|
|
293
|
-
|
|
294
|
-
llm_fillable_slot_names = [
|
|
295
|
-
command.name
|
|
296
|
-
for command in commands
|
|
297
|
-
if isinstance(command, SetSlotCommand)
|
|
298
|
-
and command.extractor == SetSlotExtractor.LLM.value
|
|
299
|
-
]
|
|
300
|
-
|
|
301
|
-
if not llm_fillable_slot_names:
|
|
302
|
-
return commands
|
|
303
|
-
|
|
304
|
-
llm_fillable_slots = [
|
|
305
|
-
slot for slot in domain.slots if slot.name in llm_fillable_slot_names
|
|
306
|
-
]
|
|
307
|
-
|
|
308
|
-
slot_filling_manager = SlotFillingManager(domain, tracker)
|
|
309
|
-
slots_to_be_removed = []
|
|
310
|
-
|
|
311
|
-
structlogger.debug(
|
|
312
|
-
"command_processor.check_commands_against_slot_mappings.active_flow",
|
|
313
|
-
active_flow=tracker.active_flow,
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
for slot in llm_fillable_slots:
|
|
317
|
-
should_fill_slot = False
|
|
318
|
-
for mapping in slot.mappings:
|
|
319
|
-
mapping_type = SlotMappingType(mapping.get("type"))
|
|
320
|
-
|
|
321
|
-
should_fill_slot = slot_filling_manager.should_fill_slot(
|
|
322
|
-
slot.name, mapping_type, mapping
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
if should_fill_slot:
|
|
326
|
-
break
|
|
327
|
-
|
|
328
|
-
if not should_fill_slot:
|
|
329
|
-
structlogger.debug(
|
|
330
|
-
"command_processor.check_commands_against_slot_mappings.slot_not_fillable",
|
|
331
|
-
slot_name=slot.name,
|
|
332
|
-
)
|
|
333
|
-
slots_to_be_removed.append(slot.name)
|
|
334
|
-
|
|
335
|
-
if not slots_to_be_removed:
|
|
336
|
-
return commands
|
|
337
|
-
|
|
338
|
-
filtered_commands = [
|
|
339
|
-
command
|
|
340
|
-
for command in commands
|
|
341
|
-
if not (
|
|
342
|
-
isinstance(command, SetSlotCommand)
|
|
343
|
-
and command.name in slots_to_be_removed
|
|
344
|
-
)
|
|
277
|
+
def _get_prior_commands(message: Message) -> List[Command]:
|
|
278
|
+
"""Get the prior commands from the tracker."""
|
|
279
|
+
return [
|
|
280
|
+
Command.command_from_json(command) for command in message.get(COMMANDS, [])
|
|
345
281
|
]
|
|
346
|
-
|
|
347
|
-
return filtered_commands
|
|
@@ -125,7 +125,7 @@ def _parse_standard_commands(
|
|
|
125
125
|
commands: List[Command] = []
|
|
126
126
|
for command_clz in standard_commands:
|
|
127
127
|
pattern = _get_compiled_pattern(command_clz.regex_pattern())
|
|
128
|
-
if match := pattern.search(action
|
|
128
|
+
if match := pattern.search(action):
|
|
129
129
|
parsed_command = command_clz.from_dsl(match, **kwargs)
|
|
130
130
|
if _additional_parsing_fn := _get_additional_parsing_logic(command_clz):
|
|
131
131
|
parsed_command = _additional_parsing_fn(parsed_command, flows, **kwargs)
|
|
@@ -8,7 +8,10 @@ from jinja2 import Template
|
|
|
8
8
|
import rasa.shared.utils.io
|
|
9
9
|
from rasa.dialogue_understanding.commands import (
|
|
10
10
|
Command,
|
|
11
|
+
SetSlotCommand,
|
|
12
|
+
StartFlowCommand,
|
|
11
13
|
)
|
|
14
|
+
from rasa.dialogue_understanding.constants import KEY_MINIMIZE_NUM_CALLS
|
|
12
15
|
from rasa.dialogue_understanding.generator import CommandGenerator
|
|
13
16
|
from rasa.dialogue_understanding.generator.constants import (
|
|
14
17
|
DEFAULT_LLM_CONFIG,
|
|
@@ -18,13 +21,20 @@ from rasa.dialogue_understanding.generator.constants import (
|
|
|
18
21
|
LLM_CONFIG_KEY,
|
|
19
22
|
)
|
|
20
23
|
from rasa.dialogue_understanding.generator.flow_retrieval import FlowRetrieval
|
|
24
|
+
from rasa.dialogue_understanding.stack.utils import top_flow_frame
|
|
21
25
|
from rasa.engine.graph import ExecutionContext, GraphComponent
|
|
22
26
|
from rasa.engine.recipes.default_recipe import DefaultV1Recipe
|
|
23
27
|
from rasa.engine.storage.resource import Resource
|
|
24
28
|
from rasa.engine.storage.storage import ModelStorage
|
|
29
|
+
from rasa.shared.core.constants import (
|
|
30
|
+
KEY_MAPPING_TYPE,
|
|
31
|
+
SetSlotExtractor,
|
|
32
|
+
SlotMappingType,
|
|
33
|
+
)
|
|
25
34
|
from rasa.shared.core.domain import Domain
|
|
26
35
|
from rasa.shared.core.flows import Flow, FlowsList, FlowStep
|
|
27
36
|
from rasa.shared.core.flows.steps.collect import CollectInformationFlowStep
|
|
37
|
+
from rasa.shared.core.slot_mappings import SlotFillingManager
|
|
28
38
|
from rasa.shared.core.trackers import DialogueStateTracker
|
|
29
39
|
from rasa.shared.exceptions import FileIOException, ProviderClientAPIException
|
|
30
40
|
from rasa.shared.nlu.constants import FLOWS_IN_PROMPT
|
|
@@ -357,8 +367,7 @@ class LLMBasedCommandGenerator(
|
|
|
357
367
|
"slots": slots_with_info,
|
|
358
368
|
}
|
|
359
369
|
)
|
|
360
|
-
|
|
361
|
-
return sorted(result, key=lambda x: x["name"])
|
|
370
|
+
return result
|
|
362
371
|
|
|
363
372
|
@staticmethod
|
|
364
373
|
def is_extractable(
|
|
@@ -454,3 +463,118 @@ class LLMBasedCommandGenerator(
|
|
|
454
463
|
if isinstance(current_step, CollectInformationFlowStep)
|
|
455
464
|
else (None, None)
|
|
456
465
|
)
|
|
466
|
+
|
|
467
|
+
@staticmethod
|
|
468
|
+
def _prior_commands_contain_start_flow(prior_commands: List[Command]) -> bool:
|
|
469
|
+
return any(isinstance(command, StartFlowCommand) for command in prior_commands)
|
|
470
|
+
|
|
471
|
+
@staticmethod
|
|
472
|
+
def _prior_commands_contain_set_slot_for_active_collect_step(
|
|
473
|
+
prior_commands: List[Command],
|
|
474
|
+
flows: FlowsList,
|
|
475
|
+
tracker: DialogueStateTracker,
|
|
476
|
+
) -> bool:
|
|
477
|
+
latest_user_frame = top_flow_frame(tracker.stack, ignore_call_frames=False)
|
|
478
|
+
|
|
479
|
+
if latest_user_frame is None:
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
active_flow = latest_user_frame.flow(flows)
|
|
483
|
+
active_step = active_flow.step_by_id(latest_user_frame.step_id)
|
|
484
|
+
|
|
485
|
+
if not isinstance(active_step, CollectInformationFlowStep):
|
|
486
|
+
return False
|
|
487
|
+
|
|
488
|
+
return any(
|
|
489
|
+
command.name == active_step.collect
|
|
490
|
+
for command in prior_commands
|
|
491
|
+
if isinstance(command, SetSlotCommand)
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
def _should_skip_llm_call(
|
|
495
|
+
self,
|
|
496
|
+
prior_commands: List[Command],
|
|
497
|
+
flows: FlowsList,
|
|
498
|
+
tracker: DialogueStateTracker,
|
|
499
|
+
) -> bool:
|
|
500
|
+
"""Skip invoking the LLM.
|
|
501
|
+
|
|
502
|
+
This returns True if the bot builder sets the property
|
|
503
|
+
KEY_MINIMIZE_NUM_CALLS to True and the prior commands
|
|
504
|
+
either contain a StartFlowCommand or a SetSlot command
|
|
505
|
+
for the current collect step.
|
|
506
|
+
"""
|
|
507
|
+
return self.config.get(KEY_MINIMIZE_NUM_CALLS, False) and (
|
|
508
|
+
self._prior_commands_contain_start_flow(prior_commands)
|
|
509
|
+
or self._prior_commands_contain_set_slot_for_active_collect_step(
|
|
510
|
+
prior_commands, flows, tracker
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
@staticmethod
|
|
515
|
+
def _check_commands_against_slot_mappings(
|
|
516
|
+
commands: List[Command],
|
|
517
|
+
tracker: DialogueStateTracker,
|
|
518
|
+
domain: Optional[Domain] = None,
|
|
519
|
+
) -> List[Command]:
|
|
520
|
+
"""Check if the LLM-issued slot commands are fillable.
|
|
521
|
+
|
|
522
|
+
The LLM-issued slot commands are fillable if the slot
|
|
523
|
+
mappings are satisfied (in particular the mapping conditions).
|
|
524
|
+
"""
|
|
525
|
+
if not domain:
|
|
526
|
+
return commands
|
|
527
|
+
|
|
528
|
+
llm_fillable_slots = [
|
|
529
|
+
tracker.slots.get(command.name)
|
|
530
|
+
for command in commands
|
|
531
|
+
if isinstance(command, SetSlotCommand)
|
|
532
|
+
and command.extractor == SetSlotExtractor.LLM.value
|
|
533
|
+
and tracker.slots.get(command.name) is not None
|
|
534
|
+
]
|
|
535
|
+
|
|
536
|
+
if not llm_fillable_slots:
|
|
537
|
+
return commands
|
|
538
|
+
|
|
539
|
+
slot_filling_manager = SlotFillingManager(domain, tracker)
|
|
540
|
+
slots_to_be_removed = []
|
|
541
|
+
|
|
542
|
+
structlogger.debug(
|
|
543
|
+
"command_processor.check_commands_against_slot_mappings.active_flow",
|
|
544
|
+
active_flow=tracker.active_flow,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
for slot in llm_fillable_slots:
|
|
548
|
+
should_fill_slot = False
|
|
549
|
+
for mapping in slot.mappings: # type: ignore[union-attr]
|
|
550
|
+
mapping_type = SlotMappingType(mapping.get(KEY_MAPPING_TYPE))
|
|
551
|
+
|
|
552
|
+
should_fill_slot = slot_filling_manager.should_fill_slot(
|
|
553
|
+
slot.name, # type: ignore[union-attr]
|
|
554
|
+
mapping_type,
|
|
555
|
+
mapping,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
if should_fill_slot:
|
|
559
|
+
break
|
|
560
|
+
|
|
561
|
+
if not should_fill_slot:
|
|
562
|
+
structlogger.debug(
|
|
563
|
+
"command_processor.check_commands_against_slot_mappings.slot_not_fillable",
|
|
564
|
+
slot_name=slot.name, # type: ignore[union-attr]
|
|
565
|
+
)
|
|
566
|
+
slots_to_be_removed.append(slot.name) # type: ignore[union-attr]
|
|
567
|
+
|
|
568
|
+
if not slots_to_be_removed:
|
|
569
|
+
return commands
|
|
570
|
+
|
|
571
|
+
filtered_commands = [
|
|
572
|
+
command
|
|
573
|
+
for command in commands
|
|
574
|
+
if not (
|
|
575
|
+
isinstance(command, SetSlotCommand)
|
|
576
|
+
and command.name in slots_to_be_removed
|
|
577
|
+
)
|
|
578
|
+
]
|
|
579
|
+
|
|
580
|
+
return filtered_commands
|
|
@@ -190,9 +190,14 @@ class MultiStepLLMCommandGenerator(LLMBasedCommandGenerator):
|
|
|
190
190
|
Returns:
|
|
191
191
|
The commands generated by the llm.
|
|
192
192
|
"""
|
|
193
|
+
prior_commands = self._get_prior_commands(message)
|
|
194
|
+
|
|
193
195
|
if tracker is None or flows.is_empty():
|
|
194
196
|
# cannot do anything if there are no flows or no tracker
|
|
195
|
-
return
|
|
197
|
+
return prior_commands
|
|
198
|
+
|
|
199
|
+
if self._should_skip_llm_call(prior_commands, flows, tracker):
|
|
200
|
+
return prior_commands
|
|
196
201
|
|
|
197
202
|
try:
|
|
198
203
|
commands = await self._predict_commands_with_multi_step(
|
|
@@ -221,7 +226,10 @@ class MultiStepLLMCommandGenerator(LLMBasedCommandGenerator):
|
|
|
221
226
|
commands=commands,
|
|
222
227
|
)
|
|
223
228
|
|
|
224
|
-
|
|
229
|
+
domain = kwargs.get("domain")
|
|
230
|
+
commands = self._check_commands_against_slot_mappings(commands, tracker, domain)
|
|
231
|
+
|
|
232
|
+
return prior_commands + commands
|
|
225
233
|
|
|
226
234
|
@classmethod
|
|
227
235
|
def parse_commands(
|