rasa-pro 3.12.0.dev12__py3-none-any.whl → 3.12.0rc1__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 (153) hide show
  1. rasa/anonymization/anonymization_rule_executor.py +16 -10
  2. rasa/cli/data.py +16 -0
  3. rasa/cli/inspect.py +20 -1
  4. rasa/cli/project_templates/calm/config.yml +2 -2
  5. rasa/cli/project_templates/calm/endpoints.yml +2 -2
  6. rasa/cli/shell.py +3 -3
  7. rasa/cli/utils.py +12 -0
  8. rasa/core/actions/action.py +99 -193
  9. rasa/core/actions/action_handle_digressions.py +142 -0
  10. rasa/core/actions/action_run_slot_rejections.py +16 -4
  11. rasa/core/actions/forms.py +10 -5
  12. rasa/core/channels/__init__.py +4 -0
  13. rasa/core/channels/studio_chat.py +19 -0
  14. rasa/core/channels/telegram.py +42 -24
  15. rasa/core/channels/voice_ready/audiocodes.py +42 -23
  16. rasa/core/channels/voice_ready/utils.py +1 -1
  17. rasa/core/channels/voice_stream/asr/asr_engine.py +10 -4
  18. rasa/core/channels/voice_stream/asr/azure.py +14 -1
  19. rasa/core/channels/voice_stream/asr/deepgram.py +20 -4
  20. rasa/core/channels/voice_stream/audiocodes.py +264 -0
  21. rasa/core/channels/voice_stream/browser_audio.py +5 -1
  22. rasa/core/channels/voice_stream/call_state.py +10 -1
  23. rasa/core/channels/voice_stream/genesys.py +335 -0
  24. rasa/core/channels/voice_stream/tts/azure.py +11 -2
  25. rasa/core/channels/voice_stream/tts/cartesia.py +29 -10
  26. rasa/core/channels/voice_stream/twilio_media_streams.py +2 -1
  27. rasa/core/channels/voice_stream/voice_channel.py +25 -3
  28. rasa/core/constants.py +2 -0
  29. rasa/core/migrate.py +2 -2
  30. rasa/core/nlg/contextual_response_rephraser.py +18 -1
  31. rasa/core/nlg/generator.py +83 -15
  32. rasa/core/nlg/response.py +6 -3
  33. rasa/core/nlg/translate.py +55 -0
  34. rasa/core/policies/enterprise_search_prompt_with_citation_template.jinja2 +1 -1
  35. rasa/core/policies/flows/flow_executor.py +47 -46
  36. rasa/core/processor.py +72 -9
  37. rasa/core/run.py +4 -3
  38. rasa/dialogue_understanding/commands/can_not_handle_command.py +20 -2
  39. rasa/dialogue_understanding/commands/cancel_flow_command.py +80 -4
  40. rasa/dialogue_understanding/commands/change_flow_command.py +20 -2
  41. rasa/dialogue_understanding/commands/chit_chat_answer_command.py +20 -2
  42. rasa/dialogue_understanding/commands/clarify_command.py +29 -3
  43. rasa/dialogue_understanding/commands/command.py +1 -16
  44. rasa/dialogue_understanding/commands/command_syntax_manager.py +55 -0
  45. rasa/dialogue_understanding/commands/correct_slots_command.py +11 -2
  46. rasa/dialogue_understanding/commands/handle_digressions_command.py +150 -0
  47. rasa/dialogue_understanding/commands/human_handoff_command.py +20 -2
  48. rasa/dialogue_understanding/commands/knowledge_answer_command.py +20 -2
  49. rasa/dialogue_understanding/commands/prompt_command.py +94 -0
  50. rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +20 -2
  51. rasa/dialogue_understanding/commands/set_slot_command.py +29 -15
  52. rasa/dialogue_understanding/commands/skip_question_command.py +20 -2
  53. rasa/dialogue_understanding/commands/start_flow_command.py +61 -2
  54. rasa/dialogue_understanding/commands/utils.py +98 -4
  55. rasa/dialogue_understanding/constants.py +1 -0
  56. rasa/dialogue_understanding/generator/__init__.py +2 -0
  57. rasa/dialogue_understanding/generator/command_generator.py +110 -73
  58. rasa/dialogue_understanding/generator/command_parser.py +16 -13
  59. rasa/dialogue_understanding/generator/constants.py +3 -0
  60. rasa/dialogue_understanding/generator/llm_based_command_generator.py +170 -5
  61. rasa/dialogue_understanding/generator/llm_command_generator.py +5 -3
  62. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +26 -4
  63. rasa/dialogue_understanding/generator/nlu_command_adapter.py +44 -3
  64. rasa/dialogue_understanding/generator/prompt_templates/__init__.py +0 -0
  65. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_template.jinja2 +60 -0
  66. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_claude_3_5_sonnet_20240620_template.jinja2 +77 -0
  67. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_default.jinja2 +68 -0
  68. rasa/dialogue_understanding/generator/{single_step/command_prompt_template.jinja2 → prompt_templates/command_prompt_v2_gpt_4o_2024_11_20_template.jinja2} +1 -1
  69. rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +460 -0
  70. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +12 -318
  71. rasa/dialogue_understanding/generator/utils.py +32 -1
  72. rasa/dialogue_understanding/patterns/collect_information.py +1 -1
  73. rasa/dialogue_understanding/patterns/correction.py +13 -1
  74. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +78 -2
  75. rasa/dialogue_understanding/patterns/handle_digressions.py +81 -0
  76. rasa/dialogue_understanding/patterns/validate_slot.py +65 -0
  77. rasa/dialogue_understanding/processor/command_processor.py +154 -28
  78. rasa/dialogue_understanding/utils.py +31 -0
  79. rasa/dialogue_understanding_test/README.md +50 -0
  80. rasa/dialogue_understanding_test/du_test_case.py +28 -8
  81. rasa/dialogue_understanding_test/du_test_result.py +13 -9
  82. rasa/dialogue_understanding_test/io.py +14 -0
  83. rasa/dialogue_understanding_test/test_case_simulation/test_case_tracker_simulator.py +3 -3
  84. rasa/e2e_test/utils/io.py +0 -37
  85. rasa/engine/graph.py +1 -0
  86. rasa/engine/language.py +140 -0
  87. rasa/engine/recipes/config_files/default_config.yml +4 -0
  88. rasa/engine/recipes/default_recipe.py +2 -0
  89. rasa/engine/recipes/graph_recipe.py +2 -0
  90. rasa/engine/storage/local_model_storage.py +1 -0
  91. rasa/engine/storage/storage.py +4 -1
  92. rasa/model_manager/runner_service.py +7 -4
  93. rasa/model_manager/socket_bridge.py +7 -6
  94. rasa/model_manager/warm_rasa_process.py +0 -1
  95. rasa/model_training.py +24 -27
  96. rasa/shared/constants.py +15 -13
  97. rasa/shared/core/constants.py +30 -3
  98. rasa/shared/core/domain.py +13 -20
  99. rasa/shared/core/events.py +13 -2
  100. rasa/shared/core/flows/constants.py +11 -0
  101. rasa/shared/core/flows/flow.py +100 -19
  102. rasa/shared/core/flows/flows_yaml_schema.json +69 -3
  103. rasa/shared/core/flows/steps/collect.py +19 -37
  104. rasa/shared/core/flows/utils.py +43 -4
  105. rasa/shared/core/flows/validation.py +1 -1
  106. rasa/shared/core/slot_mappings.py +350 -111
  107. rasa/shared/core/slots.py +154 -3
  108. rasa/shared/core/trackers.py +77 -2
  109. rasa/shared/importers/importer.py +50 -2
  110. rasa/shared/nlu/constants.py +1 -0
  111. rasa/shared/nlu/training_data/schemas/responses.yml +19 -12
  112. rasa/shared/providers/_configs/azure_entra_id_config.py +541 -0
  113. rasa/shared/providers/_configs/azure_openai_client_config.py +138 -3
  114. rasa/shared/providers/_configs/client_config.py +3 -1
  115. rasa/shared/providers/_configs/default_litellm_client_config.py +3 -1
  116. rasa/shared/providers/_configs/huggingface_local_embedding_client_config.py +3 -1
  117. rasa/shared/providers/_configs/litellm_router_client_config.py +3 -1
  118. rasa/shared/providers/_configs/model_group_config.py +4 -2
  119. rasa/shared/providers/_configs/oauth_config.py +33 -0
  120. rasa/shared/providers/_configs/openai_client_config.py +3 -1
  121. rasa/shared/providers/_configs/rasa_llm_client_config.py +3 -1
  122. rasa/shared/providers/_configs/self_hosted_llm_client_config.py +3 -1
  123. rasa/shared/providers/constants.py +6 -0
  124. rasa/shared/providers/embedding/azure_openai_embedding_client.py +28 -3
  125. rasa/shared/providers/embedding/litellm_router_embedding_client.py +3 -1
  126. rasa/shared/providers/llm/_base_litellm_client.py +42 -17
  127. rasa/shared/providers/llm/azure_openai_llm_client.py +81 -25
  128. rasa/shared/providers/llm/default_litellm_llm_client.py +3 -1
  129. rasa/shared/providers/llm/litellm_router_llm_client.py +29 -8
  130. rasa/shared/providers/llm/llm_client.py +23 -7
  131. rasa/shared/providers/llm/openai_llm_client.py +9 -3
  132. rasa/shared/providers/llm/rasa_llm_client.py +11 -2
  133. rasa/shared/providers/llm/self_hosted_llm_client.py +30 -11
  134. rasa/shared/providers/router/_base_litellm_router_client.py +3 -1
  135. rasa/shared/providers/router/router_client.py +3 -1
  136. rasa/shared/utils/constants.py +3 -0
  137. rasa/shared/utils/llm.py +31 -8
  138. rasa/shared/utils/pykwalify_extensions.py +24 -0
  139. rasa/shared/utils/schemas/domain.yml +26 -1
  140. rasa/telemetry.py +45 -14
  141. rasa/tracing/config.py +2 -0
  142. rasa/tracing/constants.py +12 -0
  143. rasa/tracing/instrumentation/instrumentation.py +36 -0
  144. rasa/tracing/instrumentation/metrics.py +41 -0
  145. rasa/tracing/metric_instrument_provider.py +40 -0
  146. rasa/utils/common.py +0 -1
  147. rasa/validator.py +561 -89
  148. rasa/version.py +1 -1
  149. {rasa_pro-3.12.0.dev12.dist-info → rasa_pro-3.12.0rc1.dist-info}/METADATA +2 -1
  150. {rasa_pro-3.12.0.dev12.dist-info → rasa_pro-3.12.0rc1.dist-info}/RECORD +153 -134
  151. {rasa_pro-3.12.0.dev12.dist-info → rasa_pro-3.12.0rc1.dist-info}/NOTICE +0 -0
  152. {rasa_pro-3.12.0.dev12.dist-info → rasa_pro-3.12.0rc1.dist-info}/WHEEL +0 -0
  153. {rasa_pro-3.12.0.dev12.dist-info → rasa_pro-3.12.0rc1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Dict, List
5
+
6
+ from rasa.dialogue_understanding.stack.frames import (
7
+ PatternFlowStackFrame,
8
+ )
9
+ from rasa.shared.constants import (
10
+ RASA_DEFAULT_FLOW_PATTERN_PREFIX,
11
+ REFILL_UTTER,
12
+ REJECTIONS,
13
+ )
14
+ from rasa.shared.core.slots import SlotRejection
15
+
16
+ FLOW_PATTERN_VALIDATE_SLOT = RASA_DEFAULT_FLOW_PATTERN_PREFIX + "validate_slot"
17
+
18
+
19
+ @dataclass
20
+ class ValidateSlotPatternFlowStackFrame(PatternFlowStackFrame):
21
+ """A pattern flow stack frame to validate slots."""
22
+
23
+ flow_id: str = FLOW_PATTERN_VALIDATE_SLOT
24
+ """The ID of the flow."""
25
+ validate: str = ""
26
+ """The slot that should be validated."""
27
+ refill_utter: str = ""
28
+ """The utter action that should be executed to ask the user to refill the
29
+ information."""
30
+ refill_action: str = ""
31
+ """The action that should be executed to ask the user to refill the
32
+ information."""
33
+ rejections: List[SlotRejection] = field(default_factory=list)
34
+ """The predicate check that should be applied to the filled slot.
35
+ If a predicate check fails, its `utter` action indicated under rejections
36
+ will be executed.
37
+ """
38
+
39
+ @classmethod
40
+ def type(cls) -> str:
41
+ """Returns the type of the frame."""
42
+ return FLOW_PATTERN_VALIDATE_SLOT
43
+
44
+ @staticmethod
45
+ def from_dict(data: Dict[str, Any]) -> ValidateSlotPatternFlowStackFrame:
46
+ """Creates a `DialogueStackFrame` from a dictionary.
47
+
48
+ Args:
49
+ data: The dictionary to create the `DialogueStackFrame` from.
50
+
51
+ Returns:
52
+ The created `DialogueStackFrame`.
53
+ """
54
+ rejections = [
55
+ SlotRejection.from_dict(rejection) for rejection in data.get(REJECTIONS, [])
56
+ ]
57
+
58
+ return ValidateSlotPatternFlowStackFrame(
59
+ frame_id=data["frame_id"],
60
+ step_id=data["step_id"],
61
+ validate=data["validate"],
62
+ refill_action=data["refill_action"],
63
+ refill_utter=data[REFILL_UTTER],
64
+ rejections=rejections,
65
+ )
@@ -18,7 +18,13 @@ from rasa.dialogue_understanding.commands import (
18
18
  from rasa.dialogue_understanding.commands.handle_code_change_command import (
19
19
  HandleCodeChangeCommand,
20
20
  )
21
+ from rasa.dialogue_understanding.commands.handle_digressions_command import (
22
+ HandleDigressionsCommand,
23
+ )
21
24
  from rasa.dialogue_understanding.commands.set_slot_command import SetSlotExtractor
25
+ from rasa.dialogue_understanding.commands.utils import (
26
+ create_validate_frames_from_slot_set_events,
27
+ )
22
28
  from rasa.dialogue_understanding.patterns.chitchat import FLOW_PATTERN_CHITCHAT
23
29
  from rasa.dialogue_understanding.patterns.collect_information import (
24
30
  CollectInformationPatternFlowStackFrame,
@@ -26,6 +32,9 @@ from rasa.dialogue_understanding.patterns.collect_information import (
26
32
  from rasa.dialogue_understanding.patterns.correction import (
27
33
  CorrectionPatternFlowStackFrame,
28
34
  )
35
+ from rasa.dialogue_understanding.patterns.validate_slot import (
36
+ ValidateSlotPatternFlowStackFrame,
37
+ )
29
38
  from rasa.dialogue_understanding.stack.dialogue_stack import DialogueStack
30
39
  from rasa.dialogue_understanding.stack.frames import (
31
40
  BaseFlowStackFrame,
@@ -47,6 +56,7 @@ from rasa.shared.core.constants import (
47
56
  from rasa.shared.core.events import Event, SlotSet
48
57
  from rasa.shared.core.flows import FlowsList
49
58
  from rasa.shared.core.flows.steps.collect import CollectInformationFlowStep
59
+ from rasa.shared.core.slot_mappings import SlotMapping
50
60
  from rasa.shared.core.slots import Slot
51
61
  from rasa.shared.core.trackers import DialogueStateTracker
52
62
  from rasa.shared.core.training_data.structures import StoryGraph
@@ -230,18 +240,51 @@ def execute_commands(
230
240
  # and then pushing the commands onto the stack in the reversed order.
231
241
  reversed_commands = list(reversed(commands))
232
242
 
243
+ # we need to keep track of the ValidateSlotPatternFlowStackFrame that
244
+ # should be pushed onto the stack before executing the StartFlowCommands.
245
+ # This is necessary to make sure that slots filled before the start of a
246
+ # flow can be immediately validated without waiting till the flow is started
247
+ # and completed.
248
+ stack_frames_to_follow_commands: List[ValidateSlotPatternFlowStackFrame] = []
249
+
233
250
  validate_state_of_commands(commands)
234
251
 
235
252
  for command in reversed_commands:
236
253
  new_events = command.run_command_on_tracker(
237
254
  tracker, all_flows, original_tracker
238
255
  )
256
+
257
+ _, stack_frames_to_follow_commands = (
258
+ create_validate_frames_from_slot_set_events(
259
+ tracker, new_events, stack_frames_to_follow_commands
260
+ )
261
+ )
262
+
239
263
  events.extend(new_events)
240
264
  tracker.update_with_events(new_events)
241
265
 
266
+ new_events = push_stack_frames_to_follow_commands(
267
+ tracker, stack_frames_to_follow_commands
268
+ )
269
+ events.extend(new_events)
270
+
242
271
  return remove_duplicated_set_slots(events)
243
272
 
244
273
 
274
+ def push_stack_frames_to_follow_commands(
275
+ tracker: DialogueStateTracker, stack_frames: List
276
+ ) -> List[Event]:
277
+ """Push stack frames to follow commands."""
278
+ new_events = []
279
+
280
+ for frame in stack_frames:
281
+ stack = tracker.stack
282
+ stack.push(frame)
283
+ new_events.extend(tracker.create_stack_updated_events(stack))
284
+ tracker.update_with_events(new_events)
285
+ return new_events
286
+
287
+
245
288
  def remove_duplicated_set_slots(events: List[Event]) -> List[Event]:
246
289
  """Removes duplicated set slot events.
247
290
 
@@ -395,6 +438,28 @@ def clean_up_commands(
395
438
  command=command,
396
439
  )
397
440
 
441
+ elif isinstance(command, StartFlowCommand) and active_flow is not None:
442
+ # push handle digressions command if we are at a collect step of
443
+ # a flow and a new flow is started
444
+ collect_info = get_current_collect_step(tracker.stack, all_flows)
445
+ current_flow = all_flows.flow_by_id(active_flow)
446
+ current_flow_condition = current_flow and (
447
+ current_flow.ask_confirm_digressions or current_flow.block_digressions
448
+ )
449
+
450
+ if collect_info and (
451
+ collect_info.ask_confirm_digressions
452
+ or collect_info.block_digressions
453
+ or current_flow_condition
454
+ ):
455
+ clean_commands.append(HandleDigressionsCommand(flow=command.flow))
456
+ structlogger.debug(
457
+ "command_processor.clean_up_commands.push_handle_digressions",
458
+ command=command,
459
+ )
460
+ else:
461
+ clean_commands.append(command)
462
+
398
463
  # handle chitchat command differently from other free-form answer commands
399
464
  elif isinstance(command, ChitChatAnswerCommand):
400
465
  clean_commands = clean_up_chitchat_command(
@@ -527,13 +592,48 @@ def clean_up_slot_command(
527
592
  )
528
593
  return resulting_commands
529
594
 
530
- if not should_slot_be_set(slot, command):
595
+ if not should_slot_be_set(slot, command, resulting_commands):
596
+ structlogger.debug(
597
+ "command_processor.clean_up_slot_command.skip_command.extractor_"
598
+ "does_not_match_slot_mapping",
599
+ extractor=command.extractor,
600
+ slot_name=slot.name,
601
+ )
602
+
603
+ # prevent adding a cannot handle command in case commands_so_far already
604
+ # contains a valid prior set slot command for the same slot whose current
605
+ # slot command was rejected by should_slot_be_set
606
+ slot_command_exists_already = any(
607
+ isinstance(command, SetSlotCommand) and command.name == slot.name
608
+ for command in resulting_commands
609
+ )
610
+
531
611
  cannot_handle = CannotHandleCommand(reason=CANNOT_HANDLE_REASON)
532
- if cannot_handle not in resulting_commands:
612
+ if not slot_command_exists_already and cannot_handle not in resulting_commands:
533
613
  resulting_commands.append(cannot_handle)
534
614
 
535
615
  return resulting_commands
536
616
 
617
+ if (
618
+ slot.filled_by == SetSlotExtractor.NLU.value
619
+ and command.extractor == SetSlotExtractor.LLM.value
620
+ ):
621
+ allow_nlu_correction = any(
622
+ [
623
+ mapping.allow_nlu_correction is True
624
+ for mapping in slot.mappings
625
+ if mapping.type == SlotMappingType.FROM_LLM
626
+ ]
627
+ )
628
+
629
+ if not allow_nlu_correction:
630
+ structlogger.debug(
631
+ "command_processor.clean_up_slot_command"
632
+ ".skip_command.disallow_llm_correction_of_nlu_set_value",
633
+ command=command,
634
+ )
635
+ return resulting_commands
636
+
537
637
  if command.name in slots_so_far and command.name != ROUTE_TO_CALM_SLOT:
538
638
  current_collect_info = get_current_collect_step(stack, all_flows)
539
639
 
@@ -574,7 +674,7 @@ def clean_up_slot_command(
574
674
  )
575
675
 
576
676
  # Group all corrections into one command
577
- corrected_slot = CorrectedSlot(command.name, command.value)
677
+ corrected_slot = CorrectedSlot(command.name, command.value, command.extractor)
578
678
  for c in resulting_commands:
579
679
  if isinstance(c, CorrectSlotsCommand):
580
680
  c.corrected_slots.append(corrected_slot)
@@ -658,7 +758,9 @@ def clean_up_chitchat_command(
658
758
  return resulting_commands
659
759
 
660
760
 
661
- def should_slot_be_set(slot: Slot, command: SetSlotCommand) -> bool:
761
+ def should_slot_be_set(
762
+ slot: Slot, command: SetSlotCommand, commands_so_far: Optional[List[Command]] = None
763
+ ) -> bool:
662
764
  """Check if a slot should be set by a command."""
663
765
  if command.extractor == SetSlotExtractor.COMMAND_PAYLOAD_READER.value:
664
766
  # if the command is issued by the command payload reader, it means the slot
@@ -666,37 +768,61 @@ def should_slot_be_set(slot: Slot, command: SetSlotCommand) -> bool:
666
768
  # we can always set it
667
769
  return True
668
770
 
771
+ if commands_so_far is None:
772
+ commands_so_far = []
773
+
774
+ set_slot_commands_so_far = [
775
+ command
776
+ for command in commands_so_far
777
+ if isinstance(command, SetSlotCommand) and command.name == slot.name
778
+ ]
779
+
669
780
  slot_mappings = slot.mappings
670
781
 
671
- if not slot_mappings:
672
- slot_mappings = [{"type": SlotMappingType.FROM_LLM.value}]
782
+ if not slot.mappings:
783
+ slot_mappings = [SlotMapping(type=SlotMappingType.FROM_LLM)]
673
784
 
674
- for mapping in slot_mappings:
675
- mapping_type = SlotMappingType(
676
- mapping.get("type", SlotMappingType.FROM_LLM.value)
677
- )
785
+ mapping_types = [mapping.type for mapping in slot_mappings]
678
786
 
679
- should_be_set_by_llm = (
680
- command.extractor == SetSlotExtractor.LLM.value
681
- and mapping_type == SlotMappingType.FROM_LLM
682
- )
683
- should_be_set_by_nlu = (
684
- command.extractor == SetSlotExtractor.NLU.value
685
- and mapping_type.is_predefined_type()
787
+ slot_has_nlu_mapping = any(
788
+ [mapping_type.is_predefined_type() for mapping_type in mapping_types]
789
+ )
790
+ slot_has_llm_mapping = any(
791
+ [mapping_type == SlotMappingType.FROM_LLM for mapping_type in mapping_types]
792
+ )
793
+ slot_has_controlled_mapping = any(
794
+ [mapping_type == SlotMappingType.CONTROLLED for mapping_type in mapping_types]
795
+ )
796
+
797
+ if set_slot_commands_so_far and command.extractor == SetSlotExtractor.LLM.value:
798
+ # covers the following scenarios:
799
+ # scenario 1: NLU mapping extracts a value for slot_a → If LLM extracts a value for slot_a, it is discarded. # noqa: E501
800
+ # scenario 2: NLU mapping is unable to extract a value for slot_a → If LLM extracts a value for slot_a, it is accepted. # noqa: E501
801
+ command_has_nlu_extractor = any(
802
+ [
803
+ command.extractor == SetSlotExtractor.NLU.value
804
+ for command in set_slot_commands_so_far
805
+ ]
686
806
  )
807
+ return not command_has_nlu_extractor and slot_has_llm_mapping
687
808
 
688
- if should_be_set_by_llm or should_be_set_by_nlu:
689
- # if the extractor matches the mapping type, we can continue
690
- # setting the slot
691
- break
809
+ if (
810
+ slot_has_nlu_mapping
811
+ and command.extractor == SetSlotExtractor.LLM.value
812
+ and not slot_has_llm_mapping
813
+ ):
814
+ return False
692
815
 
693
- structlogger.debug(
694
- "command_processor.clean_up_slot_command.skip_command.extractor_"
695
- "does_not_match_slot_mapping",
696
- extractor=command.extractor,
697
- slot_name=slot.name,
698
- mapping_type=mapping_type.value,
699
- )
816
+ if (
817
+ slot_has_llm_mapping
818
+ and command.extractor == SetSlotExtractor.NLU.value
819
+ and not slot_has_nlu_mapping
820
+ ):
821
+ return False
822
+
823
+ if slot_has_controlled_mapping and not (
824
+ slot_has_nlu_mapping or slot_has_llm_mapping
825
+ ):
700
826
  return False
701
827
 
702
828
  return True
@@ -5,7 +5,10 @@ from rasa.dialogue_understanding.commands import Command
5
5
  from rasa.dialogue_understanding.constants import (
6
6
  RASA_RECORD_COMMANDS_AND_PROMPTS_ENV_VAR_NAME,
7
7
  )
8
+ from rasa.shared.constants import ROUTE_TO_CALM_SLOT
9
+ from rasa.shared.core.trackers import DialogueStateTracker
8
10
  from rasa.shared.nlu.constants import (
11
+ COMMANDS,
9
12
  KEY_COMPONENT_NAME,
10
13
  KEY_LLM_RESPONSE_METADATA,
11
14
  KEY_PROMPT_NAME,
@@ -13,6 +16,7 @@ from rasa.shared.nlu.constants import (
13
16
  KEY_USER_PROMPT,
14
17
  PREDICTED_COMMANDS,
15
18
  PROMPTS,
19
+ SET_SLOT_COMMAND,
16
20
  )
17
21
  from rasa.shared.nlu.training_data.message import Message
18
22
  from rasa.shared.providers.llm.llm_response import LLMResponse
@@ -131,3 +135,30 @@ def add_prompt_to_message_parse_data(
131
135
 
132
136
  # Update the message with the new prompts list.
133
137
  message.set(PROMPTS, prompts, add_to_output=True)
138
+
139
+
140
+ def _handle_via_nlu_in_coexistence(
141
+ tracker: Optional[DialogueStateTracker], message: Message
142
+ ) -> bool:
143
+ """Check if the message should be handled by the NLU subsystem in coexistence mode.""" # noqa: E501
144
+ if not tracker:
145
+ return False
146
+
147
+ if not tracker.has_coexistence_routing_slot:
148
+ return False
149
+
150
+ value = tracker.get_slot(ROUTE_TO_CALM_SLOT)
151
+ if value is not None:
152
+ return not value
153
+
154
+ # routing slot has been reset so we need to check
155
+ # the command issued by the Router component
156
+ if message.get(COMMANDS):
157
+ for command in message.get(COMMANDS):
158
+ if (
159
+ command.get("command") == SET_SLOT_COMMAND
160
+ and command.get("name") == ROUTE_TO_CALM_SLOT
161
+ ):
162
+ return not command.get("value")
163
+
164
+ return False
@@ -377,3 +377,53 @@ Test cases in **to_review** may require manual intervention because the E2E test
377
377
  Review these cases to ensure that the converted test cases are correct and the list of commands and
378
378
  bot responses is complete.
379
379
 
380
+
381
+ ## Converting DUT test from one DSL to a another DSL
382
+
383
+ If you need to transform your commands from one DSL format to another
384
+ (for instance, updating `StartFlow(flow_name)` to `start flow_name` or `SetSlot(slot_name, slot_value)` to `set slot_name slot_value`),
385
+ you can use a standalone Python script:
386
+
387
+ ```bash
388
+ python convert_dut_dsl.py --dut-tests-dir <path> --output-dir <path> --dsl-mappings <path>
389
+ ```
390
+
391
+ The script has the following required parameters:
392
+
393
+ - `--dut-tests-dir <path>`: The directory (relative or absolute) containing your
394
+ existing Dialogue Understanding Tests (DUT). The script will look for `.yaml` or
395
+ `.yml` files within this folder (and subfolders).
396
+ - `--output-dir <path>`: The directory where transformed files will be saved. The folder
397
+ structure from your `dut-tests-dir` is preserved.
398
+ - `--dsl-mappings <path>`: The YAML file defining your DSL mapping rules.
399
+
400
+ The YAML file containing the mappings must adhere to the following format:
401
+ ```yaml
402
+ mappings:
403
+
404
+ - from_dsl_regex: "^StartFlow\\(([^)]*)\\)$"
405
+ to_dsl_pattern: "start {1}"
406
+
407
+ - from_dsl_regex: "^SetSlot\\(([^,]+),\\s*(.*)\\)$"
408
+ to_dsl_pattern: "set {1} {2}"
409
+
410
+ - from_dsl_regex: "Clarify\(([\"\'a-zA-Z0-9_, ]*)\)"
411
+ to_dsl_pattern: "clarify {1}"
412
+ input_separators:
413
+ - ","
414
+ - " "
415
+ output_separator: " "
416
+
417
+ # ... add more mappings here
418
+
419
+ ```
420
+
421
+ - `from_dsl_regex`: A regular expression (string) used to match the old DSL command.
422
+ Must include any necessary anchors (like ^ and $) and capturing groups ( ... ) for
423
+ dynamic parts.
424
+ - `to_dsl_pattern`: A string that contains placeholders like `{1}`, `{2}`, etc. Each
425
+ placeholder corresponds to a capturing group in from_dsl_regex, in order of
426
+ appearance.
427
+ - `input_separators`: Optional list of separators of the captured groups that can be replaced
428
+ with the `output_separator`
429
+ - `output_separator`: Output separator to replace separators from the list of `input_separators` in the captured group.
@@ -2,7 +2,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple
2
2
 
3
3
  from pydantic import BaseModel, Field
4
4
 
5
- from rasa.dialogue_understanding.commands import Command
5
+ from rasa.dialogue_understanding.commands.prompt_command import PromptCommand
6
6
  from rasa.dialogue_understanding.generator.command_parser import parse_commands
7
7
  from rasa.dialogue_understanding_test.command_comparison import are_command_lists_equal
8
8
  from rasa.dialogue_understanding_test.constants import (
@@ -30,6 +30,7 @@ from rasa.shared.nlu.constants import (
30
30
  KEY_USAGE = "usage"
31
31
  KEY_PROMPT_TOKENS = "prompt_tokens"
32
32
  KEY_COMPLETION_TOKENS = "completion_tokens"
33
+ KEY_CHOICES = "choices"
33
34
 
34
35
 
35
36
  class DialogueUnderstandingOutput(BaseModel):
@@ -65,11 +66,18 @@ class DialogueUnderstandingOutput(BaseModel):
65
66
  """
66
67
 
67
68
  # Dict with component name as key and list of commands as value
68
- commands: Dict[str, List[Command]]
69
+ commands: Dict[str, List[PromptCommand]]
69
70
  # List of prompts
70
71
  prompts: Optional[List[Dict[str, Any]]] = None
71
72
 
72
- def get_predicted_commands(self) -> List[Command]:
73
+ class Config:
74
+ """Skip validation for PromptCommand protocol as pydantic does not know how to
75
+ serialize or handle instances of a protocol.
76
+ """
77
+
78
+ arbitrary_types_allowed = True
79
+
80
+ def get_predicted_commands(self) -> List[PromptCommand]:
73
81
  """Get all commands from the output."""
74
82
  return [
75
83
  command
@@ -144,6 +152,11 @@ class DialogueUnderstandingOutput(BaseModel):
144
152
  KEY_COMPLETION_TOKENS
145
153
  )
146
154
 
155
+ choices = prompt_data.get(KEY_LLM_RESPONSE_METADATA, {}).get(KEY_CHOICES)
156
+ if choices and len(choices) > 0:
157
+ # Add the action list returned by the LLM to the prompt_info
158
+ prompt_info[KEY_CHOICES] = choices[0]
159
+
147
160
  data[component_name].append(prompt_info)
148
161
 
149
162
  return data
@@ -155,9 +168,16 @@ class DialogueUnderstandingTestStep(BaseModel):
155
168
  template: Optional[str] = None
156
169
  line: Optional[int] = None
157
170
  metadata_name: Optional[str] = None
158
- commands: Optional[List[Command]] = None
171
+ commands: Optional[List[PromptCommand]] = None
159
172
  dialogue_understanding_output: Optional[DialogueUnderstandingOutput] = None
160
173
 
174
+ class Config:
175
+ """Skip validation for PromptCommand protocol as pydantic does not know how to
176
+ serialize or handle instances of a protocol.
177
+ """
178
+
179
+ arbitrary_types_allowed = True
180
+
161
181
  def as_dict(self) -> Dict[str, Any]:
162
182
  if self.actor == ACTOR_USER:
163
183
  if self.commands:
@@ -178,7 +198,7 @@ class DialogueUnderstandingTestStep(BaseModel):
178
198
  def from_dict(
179
199
  step: Dict[str, Any],
180
200
  flows: FlowsList,
181
- custom_command_classes: List[Command] = [],
201
+ custom_command_classes: List[PromptCommand] = [],
182
202
  remove_default_commands: List[str] = [],
183
203
  ) -> "DialogueUnderstandingTestStep":
184
204
  """Creates a DialogueUnderstandingTestStep from a dictionary.
@@ -224,7 +244,7 @@ class DialogueUnderstandingTestStep(BaseModel):
224
244
  commands=commands,
225
245
  )
226
246
 
227
- def get_predicted_commands(self) -> List[Command]:
247
+ def get_predicted_commands(self) -> List[PromptCommand]:
228
248
  """Get all predicted commands from the test case."""
229
249
  if self.dialogue_understanding_output is None:
230
250
  return []
@@ -314,7 +334,7 @@ class DialogueUnderstandingTestCase(BaseModel):
314
334
  input_test_case: Dict[str, Any],
315
335
  flows: FlowsList,
316
336
  file: Optional[str] = None,
317
- custom_command_classes: List[Command] = [],
337
+ custom_command_classes: List[PromptCommand] = [],
318
338
  remove_default_commands: List[str] = [],
319
339
  ) -> "DialogueUnderstandingTestCase":
320
340
  """Creates a DialogueUnderstandingTestCase from a dictionary.
@@ -361,7 +381,7 @@ class DialogueUnderstandingTestCase(BaseModel):
361
381
 
362
382
  return [step.to_str() for step in steps]
363
383
 
364
- def get_expected_commands(self) -> List[Command]:
384
+ def get_expected_commands(self) -> List[PromptCommand]:
365
385
  """Get all commands from the test steps."""
366
386
  return [
367
387
  command
@@ -5,16 +5,13 @@ from typing import Any, Dict, List, Optional, Text
5
5
  import numpy as np
6
6
  from pydantic import BaseModel
7
7
 
8
- from rasa.dialogue_understanding.commands import Command
8
+ from rasa.dialogue_understanding.commands.prompt_command import PromptCommand
9
9
  from rasa.dialogue_understanding_test.du_test_case import (
10
10
  DialogueUnderstandingTestCase,
11
11
  DialogueUnderstandingTestStep,
12
12
  )
13
13
  from rasa.dialogue_understanding_test.utils import get_command_comparison
14
- from rasa.shared.nlu.constants import (
15
- KEY_SYSTEM_PROMPT,
16
- KEY_USER_PROMPT,
17
- )
14
+ from rasa.shared.nlu.constants import KEY_SYSTEM_PROMPT, KEY_USER_PROMPT
18
15
 
19
16
  if typing.TYPE_CHECKING:
20
17
  from rasa.dialogue_understanding_test.command_metric_calculation import (
@@ -46,7 +43,7 @@ class DialogueUnderstandingTestResult(BaseModel):
46
43
  passed: bool
47
44
  error_line: Optional[int] = None
48
45
 
49
- def get_expected_commands(self) -> List[Command]:
46
+ def get_expected_commands(self) -> List[PromptCommand]:
50
47
  return self.test_case.get_expected_commands()
51
48
 
52
49
 
@@ -60,10 +57,17 @@ class FailedTestStep(BaseModel):
60
57
  pass_status: bool
61
58
  command_generators: List[str]
62
59
  prompts: Optional[Dict[str, List[Dict[str, Any]]]] = None
63
- expected_commands: List[Command]
64
- predicted_commands: Dict[str, List[Command]]
60
+ expected_commands: List[PromptCommand]
61
+ predicted_commands: Dict[str, List[PromptCommand]]
65
62
  conversation_with_diff: List[str]
66
63
 
64
+ class Config:
65
+ """Skip validation for PromptCommand protocol as pydantic does not know how to
66
+ serialize or handle instances of a protocol.
67
+ """
68
+
69
+ arbitrary_types_allowed = True
70
+
67
71
  @classmethod
68
72
  def from_dialogue_understanding_test_step(
69
73
  cls,
@@ -74,7 +78,7 @@ class FailedTestStep(BaseModel):
74
78
  user_utterance = step.text or ""
75
79
  line_number = step.line or -1
76
80
 
77
- predicted_commands: Dict[str, List[Command]] = {}
81
+ predicted_commands: Dict[str, List[PromptCommand]] = {}
78
82
  prompts: Optional[Dict[str, List[Dict[str, Any]]]] = None
79
83
  command_generators: List[str] = []
80
84
 
@@ -8,6 +8,7 @@ import rasa.shared.data
8
8
  from rasa.dialogue_understanding_test.command_metric_calculation import CommandMetrics
9
9
  from rasa.dialogue_understanding_test.constants import SCHEMA_FILE_PATH
10
10
  from rasa.dialogue_understanding_test.du_test_case import (
11
+ KEY_CHOICES,
11
12
  KEY_COMPLETION_TOKENS,
12
13
  KEY_PROMPT_TOKENS,
13
14
  )
@@ -309,6 +310,7 @@ def print_failed_cases(
309
310
  print_prompt(step)
310
311
  rich.print("\n[red3]-- CONVERSATION --[/red3]")
311
312
  rich.print("\n".join(step.conversation_with_diff))
313
+ print_llm_output(step)
312
314
 
313
315
 
314
316
  def print_prompt(step: FailedTestStep) -> None:
@@ -341,6 +343,18 @@ def print_prompt(step: FailedTestStep) -> None:
341
343
  )
342
344
 
343
345
 
346
+ def print_llm_output(step: FailedTestStep) -> None:
347
+ if not step.prompts:
348
+ return
349
+
350
+ for component, component_prompts in step.prompts.items():
351
+ for prompt_data in component_prompts:
352
+ if KEY_CHOICES in prompt_data:
353
+ rich.print("\n[red3]-- CHOICES --[/red3]")
354
+ rich.print(prompt_data.get(KEY_CHOICES))
355
+ rich.print("[red3]-------------[/red3]")
356
+
357
+
344
358
  def print_command_summary(metrics: Dict[str, CommandMetrics]) -> None:
345
359
  """Print the command summary.
346
360
 
@@ -20,7 +20,7 @@ from rasa.dialogue_understanding_test.test_case_simulation.exception import (
20
20
  )
21
21
  from rasa.dialogue_understanding_test.utils import filter_metadata
22
22
  from rasa.e2e_test.e2e_test_case import Fixture, Metadata
23
- from rasa.shared.core.constants import MAPPING_TYPE, SlotMappingType
23
+ from rasa.shared.core.constants import SlotMappingType
24
24
  from rasa.shared.core.events import BotUttered, SlotSet, UserUttered
25
25
  from rasa.shared.core.trackers import DialogueStateTracker
26
26
  from rasa.shared.nlu.constants import COMMANDS, ENTITIES, INTENT
@@ -328,8 +328,8 @@ class TestCaseTrackerSimulator:
328
328
  command.extractor = SetSlotExtractor.COMMAND_PAYLOAD_READER.value
329
329
  # Use the SetSlotExtractor.NLU extractor if the slot mapping type is
330
330
  # not FROM_LLM.
331
- elif SlotMappingType.FROM_LLM.value not in [
332
- mapping[MAPPING_TYPE] for mapping in slot_definition.mappings
331
+ elif SlotMappingType.FROM_LLM not in [
332
+ mapping.type for mapping in slot_definition.mappings
333
333
  ]:
334
334
  command.extractor = SetSlotExtractor.NLU.value
335
335