rasa-pro 3.12.0.dev8__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.

Files changed (56) hide show
  1. rasa/core/actions/action.py +17 -3
  2. rasa/core/actions/action_handle_digressions.py +142 -0
  3. rasa/core/actions/forms.py +4 -2
  4. rasa/core/channels/voice_ready/audiocodes.py +42 -23
  5. rasa/core/channels/voice_stream/tts/azure.py +2 -1
  6. rasa/core/migrate.py +2 -2
  7. rasa/core/policies/flows/flow_executor.py +33 -1
  8. rasa/dialogue_understanding/commands/can_not_handle_command.py +2 -2
  9. rasa/dialogue_understanding/commands/cancel_flow_command.py +62 -4
  10. rasa/dialogue_understanding/commands/change_flow_command.py +2 -2
  11. rasa/dialogue_understanding/commands/chit_chat_answer_command.py +2 -2
  12. rasa/dialogue_understanding/commands/clarify_command.py +2 -2
  13. rasa/dialogue_understanding/commands/correct_slots_command.py +11 -2
  14. rasa/dialogue_understanding/commands/handle_digressions_command.py +150 -0
  15. rasa/dialogue_understanding/commands/human_handoff_command.py +2 -2
  16. rasa/dialogue_understanding/commands/knowledge_answer_command.py +2 -2
  17. rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +2 -2
  18. rasa/dialogue_understanding/commands/set_slot_command.py +7 -15
  19. rasa/dialogue_understanding/commands/skip_question_command.py +2 -2
  20. rasa/dialogue_understanding/commands/start_flow_command.py +43 -2
  21. rasa/dialogue_understanding/commands/utils.py +1 -1
  22. rasa/dialogue_understanding/constants.py +1 -0
  23. rasa/dialogue_understanding/generator/command_generator.py +10 -76
  24. rasa/dialogue_understanding/generator/command_parser.py +1 -1
  25. rasa/dialogue_understanding/generator/llm_based_command_generator.py +126 -2
  26. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +10 -2
  27. rasa/dialogue_understanding/generator/nlu_command_adapter.py +4 -2
  28. rasa/dialogue_understanding/generator/single_step/command_prompt_template.jinja2 +40 -40
  29. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +11 -19
  30. rasa/dialogue_understanding/patterns/correction.py +13 -1
  31. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +62 -2
  32. rasa/dialogue_understanding/patterns/handle_digressions.py +81 -0
  33. rasa/dialogue_understanding/processor/command_processor.py +117 -28
  34. rasa/dialogue_understanding/utils.py +31 -0
  35. rasa/dialogue_understanding_test/test_case_simulation/test_case_tracker_simulator.py +2 -2
  36. rasa/shared/core/constants.py +22 -1
  37. rasa/shared/core/domain.py +6 -4
  38. rasa/shared/core/events.py +13 -2
  39. rasa/shared/core/flows/flow.py +17 -0
  40. rasa/shared/core/flows/flows_yaml_schema.json +38 -0
  41. rasa/shared/core/flows/steps/collect.py +18 -1
  42. rasa/shared/core/flows/utils.py +16 -1
  43. rasa/shared/core/slot_mappings.py +6 -6
  44. rasa/shared/core/slots.py +19 -0
  45. rasa/shared/core/trackers.py +3 -1
  46. rasa/shared/nlu/constants.py +1 -0
  47. rasa/shared/providers/llm/_base_litellm_client.py +0 -40
  48. rasa/shared/utils/llm.py +1 -80
  49. rasa/shared/utils/schemas/domain.yml +0 -1
  50. rasa/validator.py +172 -22
  51. rasa/version.py +1 -1
  52. {rasa_pro-3.12.0.dev8.dist-info → rasa_pro-3.12.0.dev10.dist-info}/METADATA +1 -1
  53. {rasa_pro-3.12.0.dev8.dist-info → rasa_pro-3.12.0.dev10.dist-info}/RECORD +56 -53
  54. {rasa_pro-3.12.0.dev8.dist-info → rasa_pro-3.12.0.dev10.dist-info}/NOTICE +0 -0
  55. {rasa_pro-3.12.0.dev8.dist-info → rasa_pro-3.12.0.dev10.dist-info}/WHEEL +0 -0
  56. {rasa_pro-3.12.0.dev8.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 "hand over"
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"^hand over$"
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 "search"
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"^search$"
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 "repeat message"
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"^repeat message$"
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 [SlotSet(self.name, slot.coerce_value(self.value))]
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"set {self.name} {self.value}"
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"""^set ['"]?([a-zA-Z_][a-zA-Z0-9_-]*)['"]? ['"]?(.+?)['"]?$"""
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 "skip"
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"^skip$"
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"start {self.flow}"
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"^start ['\"]?([a-zA-Z0-9_-]+)['\"]?$"
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
 
@@ -1 +1,2 @@
1
1
  RASA_RECORD_COMMANDS_AND_PROMPTS_ENV_VAR_NAME = "RASA_RECORD_COMMANDS_AND_PROMPTS"
2
+ KEY_MINIMIZE_NUM_CALLS = "minimize_num_calls"
@@ -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.commands.set_slot_command import SetSlotExtractor
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.get(COMMANDS):
96
- # do not overwrite commands if they are already present
97
- # i.e. another command generator already predicted commands
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 _check_commands_against_slot_mappings(
282
- commands: List[Command],
283
- tracker: DialogueStateTracker,
284
- domain: Optional[Domain] = None,
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.strip()):
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
- return commands
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(