rasa-pro 3.12.0.dev13__py3-none-any.whl → 3.12.0rc2__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 (139) hide show
  1. README.md +10 -13
  2. rasa/anonymization/anonymization_rule_executor.py +16 -10
  3. rasa/cli/data.py +16 -0
  4. rasa/cli/project_templates/calm/config.yml +2 -2
  5. rasa/cli/project_templates/calm/domain/list_contacts.yml +1 -2
  6. rasa/cli/project_templates/calm/domain/remove_contact.yml +1 -2
  7. rasa/cli/project_templates/calm/domain/shared.yml +1 -4
  8. rasa/cli/project_templates/calm/endpoints.yml +2 -2
  9. rasa/cli/utils.py +12 -0
  10. rasa/core/actions/action.py +84 -191
  11. rasa/core/actions/action_handle_digressions.py +35 -13
  12. rasa/core/actions/action_run_slot_rejections.py +16 -4
  13. rasa/core/channels/__init__.py +2 -0
  14. rasa/core/channels/studio_chat.py +19 -0
  15. rasa/core/channels/telegram.py +42 -24
  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 +4 -1
  22. rasa/core/channels/voice_stream/call_state.py +3 -0
  23. rasa/core/channels/voice_stream/genesys.py +6 -2
  24. rasa/core/channels/voice_stream/tts/azure.py +9 -1
  25. rasa/core/channels/voice_stream/tts/cartesia.py +14 -8
  26. rasa/core/channels/voice_stream/voice_channel.py +23 -2
  27. rasa/core/constants.py +2 -0
  28. rasa/core/nlg/contextual_response_rephraser.py +18 -1
  29. rasa/core/nlg/generator.py +83 -15
  30. rasa/core/nlg/response.py +6 -3
  31. rasa/core/nlg/translate.py +55 -0
  32. rasa/core/policies/enterprise_search_prompt_with_citation_template.jinja2 +1 -1
  33. rasa/core/policies/flows/flow_executor.py +19 -7
  34. rasa/core/processor.py +71 -9
  35. rasa/dialogue_understanding/commands/can_not_handle_command.py +20 -2
  36. rasa/dialogue_understanding/commands/cancel_flow_command.py +24 -6
  37. rasa/dialogue_understanding/commands/change_flow_command.py +20 -2
  38. rasa/dialogue_understanding/commands/chit_chat_answer_command.py +20 -2
  39. rasa/dialogue_understanding/commands/clarify_command.py +29 -3
  40. rasa/dialogue_understanding/commands/command.py +1 -16
  41. rasa/dialogue_understanding/commands/command_syntax_manager.py +55 -0
  42. rasa/dialogue_understanding/commands/handle_digressions_command.py +1 -7
  43. rasa/dialogue_understanding/commands/human_handoff_command.py +20 -2
  44. rasa/dialogue_understanding/commands/knowledge_answer_command.py +20 -2
  45. rasa/dialogue_understanding/commands/prompt_command.py +94 -0
  46. rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +20 -2
  47. rasa/dialogue_understanding/commands/set_slot_command.py +24 -2
  48. rasa/dialogue_understanding/commands/skip_question_command.py +20 -2
  49. rasa/dialogue_understanding/commands/start_flow_command.py +22 -2
  50. rasa/dialogue_understanding/commands/utils.py +71 -4
  51. rasa/dialogue_understanding/generator/__init__.py +2 -0
  52. rasa/dialogue_understanding/generator/command_parser.py +15 -12
  53. rasa/dialogue_understanding/generator/constants.py +3 -0
  54. rasa/dialogue_understanding/generator/llm_based_command_generator.py +12 -5
  55. rasa/dialogue_understanding/generator/llm_command_generator.py +5 -3
  56. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +17 -3
  57. rasa/dialogue_understanding/generator/prompt_templates/__init__.py +0 -0
  58. rasa/dialogue_understanding/generator/{single_step → prompt_templates}/command_prompt_template.jinja2 +2 -0
  59. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_claude_3_5_sonnet_20240620_template.jinja2 +77 -0
  60. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_default.jinja2 +68 -0
  61. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_gpt_4o_2024_11_20_template.jinja2 +84 -0
  62. rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +522 -0
  63. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +12 -310
  64. rasa/dialogue_understanding/patterns/collect_information.py +1 -1
  65. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +16 -0
  66. rasa/dialogue_understanding/patterns/validate_slot.py +65 -0
  67. rasa/dialogue_understanding/processor/command_processor.py +39 -0
  68. rasa/dialogue_understanding/stack/utils.py +38 -0
  69. rasa/dialogue_understanding_test/du_test_case.py +58 -18
  70. rasa/dialogue_understanding_test/du_test_result.py +14 -10
  71. rasa/dialogue_understanding_test/io.py +14 -0
  72. rasa/e2e_test/assertions.py +6 -8
  73. rasa/e2e_test/llm_judge_prompts/answer_relevance_prompt_template.jinja2 +5 -1
  74. rasa/e2e_test/llm_judge_prompts/groundedness_prompt_template.jinja2 +4 -0
  75. rasa/e2e_test/utils/io.py +0 -37
  76. rasa/engine/graph.py +1 -0
  77. rasa/engine/language.py +140 -0
  78. rasa/engine/recipes/config_files/default_config.yml +4 -0
  79. rasa/engine/recipes/default_recipe.py +2 -0
  80. rasa/engine/recipes/graph_recipe.py +2 -0
  81. rasa/engine/storage/local_model_storage.py +1 -0
  82. rasa/engine/storage/storage.py +4 -1
  83. rasa/llm_fine_tuning/conversations.py +1 -1
  84. rasa/model_manager/runner_service.py +7 -4
  85. rasa/model_manager/socket_bridge.py +7 -6
  86. rasa/shared/constants.py +15 -13
  87. rasa/shared/core/constants.py +2 -0
  88. rasa/shared/core/flows/constants.py +11 -0
  89. rasa/shared/core/flows/flow.py +83 -19
  90. rasa/shared/core/flows/flows_yaml_schema.json +31 -3
  91. rasa/shared/core/flows/steps/collect.py +1 -36
  92. rasa/shared/core/flows/utils.py +28 -4
  93. rasa/shared/core/flows/validation.py +1 -1
  94. rasa/shared/core/slot_mappings.py +208 -5
  95. rasa/shared/core/slots.py +137 -1
  96. rasa/shared/core/trackers.py +74 -1
  97. rasa/shared/importers/importer.py +50 -2
  98. rasa/shared/nlu/training_data/schemas/responses.yml +19 -12
  99. rasa/shared/providers/_configs/azure_entra_id_config.py +541 -0
  100. rasa/shared/providers/_configs/azure_openai_client_config.py +138 -3
  101. rasa/shared/providers/_configs/client_config.py +3 -1
  102. rasa/shared/providers/_configs/default_litellm_client_config.py +3 -1
  103. rasa/shared/providers/_configs/huggingface_local_embedding_client_config.py +3 -1
  104. rasa/shared/providers/_configs/litellm_router_client_config.py +3 -1
  105. rasa/shared/providers/_configs/model_group_config.py +4 -2
  106. rasa/shared/providers/_configs/oauth_config.py +33 -0
  107. rasa/shared/providers/_configs/openai_client_config.py +3 -1
  108. rasa/shared/providers/_configs/rasa_llm_client_config.py +3 -1
  109. rasa/shared/providers/_configs/self_hosted_llm_client_config.py +3 -1
  110. rasa/shared/providers/constants.py +6 -0
  111. rasa/shared/providers/embedding/azure_openai_embedding_client.py +28 -3
  112. rasa/shared/providers/embedding/litellm_router_embedding_client.py +3 -1
  113. rasa/shared/providers/llm/_base_litellm_client.py +42 -17
  114. rasa/shared/providers/llm/azure_openai_llm_client.py +81 -25
  115. rasa/shared/providers/llm/default_litellm_llm_client.py +3 -1
  116. rasa/shared/providers/llm/litellm_router_llm_client.py +29 -8
  117. rasa/shared/providers/llm/llm_client.py +23 -7
  118. rasa/shared/providers/llm/openai_llm_client.py +9 -3
  119. rasa/shared/providers/llm/rasa_llm_client.py +11 -2
  120. rasa/shared/providers/llm/self_hosted_llm_client.py +30 -11
  121. rasa/shared/providers/router/_base_litellm_router_client.py +3 -1
  122. rasa/shared/providers/router/router_client.py +3 -1
  123. rasa/shared/utils/constants.py +3 -0
  124. rasa/shared/utils/llm.py +33 -7
  125. rasa/shared/utils/pykwalify_extensions.py +24 -0
  126. rasa/shared/utils/schemas/domain.yml +26 -0
  127. rasa/telemetry.py +2 -1
  128. rasa/tracing/config.py +2 -0
  129. rasa/tracing/constants.py +12 -0
  130. rasa/tracing/instrumentation/instrumentation.py +36 -0
  131. rasa/tracing/instrumentation/metrics.py +41 -0
  132. rasa/tracing/metric_instrument_provider.py +40 -0
  133. rasa/validator.py +372 -7
  134. rasa/version.py +1 -1
  135. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc2.dist-info}/METADATA +13 -14
  136. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc2.dist-info}/RECORD +139 -124
  137. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc2.dist-info}/NOTICE +0 -0
  138. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc2.dist-info}/WHEEL +0 -0
  139. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc2.dist-info}/entry_points.txt +0 -0
@@ -1,23 +1,40 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import copy
4
- import logging
5
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Text, Tuple, Union, cast
4
+ from enum import Enum
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Dict,
9
+ List,
10
+ Optional,
11
+ Set,
12
+ Text,
13
+ Tuple,
14
+ Union,
15
+ cast,
16
+ )
6
17
 
18
+ import structlog
7
19
  from pydantic import BaseModel, Field
8
20
 
9
21
  import rasa.shared.utils.io
10
22
  from rasa.shared.constants import DOCS_URL_NLU_BASED_SLOTS, IGNORED_INTENTS
11
23
  from rasa.shared.core.constants import (
24
+ ACTION_EXTRACT_SLOTS,
25
+ ACTION_VALIDATE_SLOT_MAPPINGS,
12
26
  ACTIVE_LOOP,
13
27
  KEY_ACTION,
28
+ KEY_COEXISTENCE_SYSTEM,
14
29
  KEY_MAPPING_TYPE,
15
30
  KEY_RUN_ACTION_EVERY_TURN,
16
31
  MAPPING_CONDITIONS,
17
32
  REQUESTED_SLOT,
18
33
  SlotMappingType,
19
34
  )
35
+ from rasa.shared.core.events import BotUttered, Event, SlotSet
20
36
  from rasa.shared.core.slots import ListSlot, Slot
37
+ from rasa.shared.exceptions import RasaException
21
38
  from rasa.shared.nlu.constants import (
22
39
  ENTITIES,
23
40
  ENTITY_ATTRIBUTE_GROUP,
@@ -30,13 +47,14 @@ from rasa.shared.nlu.constants import (
30
47
  )
31
48
 
32
49
  if TYPE_CHECKING:
50
+ from rasa.core.channels.channel import OutputChannel
51
+ from rasa.core.nlg import NaturalLanguageGenerator
33
52
  from rasa.shared.core.domain import Domain
34
53
  from rasa.shared.core.trackers import DialogueStateTracker
35
54
  from rasa.shared.nlu.training_data.message import Message
36
55
  from rasa.utils.endpoints import EndpointConfig
37
56
 
38
-
39
- logger = logging.getLogger(__name__)
57
+ structlogger = structlog.get_logger()
40
58
 
41
59
 
42
60
  class SlotMappingCondition(BaseModel):
@@ -58,6 +76,12 @@ class SlotMappingCondition(BaseModel):
58
76
  return self.model_dump(exclude_none=True)
59
77
 
60
78
 
79
+ class CoexistenceSystemType(Enum):
80
+ NLU = "NLU"
81
+ CALM = "CALM"
82
+ SHARED = "SHARED"
83
+
84
+
61
85
  class SlotMapping(BaseModel):
62
86
  """Defines functionality for the available slot mappings."""
63
87
 
@@ -71,6 +95,7 @@ class SlotMapping(BaseModel):
71
95
  value: Optional[Any] = None
72
96
  allow_nlu_correction: Optional[bool] = None
73
97
  run_action_every_turn: Optional[str] = None
98
+ coexistence_system: Optional[CoexistenceSystemType] = None
74
99
 
75
100
  @staticmethod
76
101
  def from_dict(data: Dict[str, Any], slot_name: str) -> SlotMapping:
@@ -92,10 +117,16 @@ class SlotMapping(BaseModel):
92
117
 
93
118
  run_action_every_turn = data_copy.pop(KEY_RUN_ACTION_EVERY_TURN, None)
94
119
 
120
+ coexistence_system = data_copy.pop(KEY_COEXISTENCE_SYSTEM, None)
121
+ coexistence_system_type = (
122
+ CoexistenceSystemType(coexistence_system) if coexistence_system else None
123
+ )
124
+
95
125
  return SlotMapping(
96
126
  type=mapping_type,
97
127
  conditions=conditions,
98
128
  run_action_every_turn=run_action_every_turn,
129
+ coexistence_system=coexistence_system_type,
99
130
  **data_copy,
100
131
  )
101
132
 
@@ -324,6 +355,7 @@ class SlotFillingManager:
324
355
  self.tracker = tracker
325
356
  self.message = message
326
357
  self._action_endpoint = action_endpoint
358
+ self.executed_custom_actions: Set[str] = set()
327
359
 
328
360
  def is_slot_mapping_valid(
329
361
  self,
@@ -524,6 +556,173 @@ class SlotFillingManager:
524
556
 
525
557
  return True
526
558
 
559
+ @staticmethod
560
+ def should_fill_slot_in_coexistence(
561
+ is_calm_system: bool,
562
+ slot: Slot,
563
+ calm_slot_names: Set[str],
564
+ ) -> bool:
565
+ """Check if a slot should be filled in a coexistence assistant."""
566
+ if slot.shared_for_coexistence:
567
+ return True
568
+
569
+ coexistence_systems = [
570
+ mapping.coexistence_system
571
+ for mapping in slot.mappings
572
+ if mapping.type == SlotMappingType.CONTROLLED
573
+ ]
574
+
575
+ if not is_calm_system and (
576
+ slot.name in calm_slot_names
577
+ or CoexistenceSystemType.CALM in coexistence_systems
578
+ ):
579
+ return False
580
+
581
+ if is_calm_system and (
582
+ slot.name not in calm_slot_names
583
+ or CoexistenceSystemType.NLU in coexistence_systems
584
+ ):
585
+ return False
586
+
587
+ return True
588
+
589
+ async def run_action_at_every_turn(
590
+ self,
591
+ slot: Slot,
592
+ output_channel: "OutputChannel",
593
+ nlg: "NaturalLanguageGenerator",
594
+ ) -> List[Event]:
595
+ """Runs a custom action at every turn to fill a slot.
596
+
597
+ This executes only if the slot has a controlled mapping type
598
+ with the `run_action_every_turn` key set.
599
+ """
600
+ slot_events: List[Event] = []
601
+ for mapping in slot.mappings:
602
+ should_fill_controlled_slot = mapping.type == SlotMappingType.CONTROLLED
603
+
604
+ if not should_fill_controlled_slot:
605
+ continue
606
+
607
+ custom_events = await self._execute_custom_action(
608
+ mapping,
609
+ output_channel,
610
+ nlg,
611
+ )
612
+ slot_events.extend(custom_events)
613
+
614
+ return slot_events
615
+
616
+ async def _execute_custom_action(
617
+ self,
618
+ mapping: "SlotMapping",
619
+ output_channel: "OutputChannel",
620
+ nlg: "NaturalLanguageGenerator",
621
+ ) -> List[Event]:
622
+ custom_action = mapping.run_action_every_turn
623
+
624
+ if not custom_action or custom_action in self.executed_custom_actions:
625
+ return []
626
+
627
+ slot_events = await self._run_custom_action(custom_action, output_channel, nlg)
628
+
629
+ self.executed_custom_actions.add(custom_action)
630
+
631
+ return slot_events
632
+
633
+ async def _run_custom_action(
634
+ self,
635
+ custom_action: str,
636
+ output_channel: "OutputChannel",
637
+ nlg: "NaturalLanguageGenerator",
638
+ recreate_tracker: bool = False,
639
+ ) -> List[Event]:
640
+ from rasa.core.actions.action import RemoteAction
641
+ from rasa.shared.core.trackers import DialogueStateTracker
642
+ from rasa.utils.endpoints import ClientResponseError
643
+
644
+ slot_events: List[Event] = []
645
+ remote_action = RemoteAction(custom_action, self._action_endpoint)
646
+ disallowed_types = set()
647
+
648
+ tracker = (
649
+ DialogueStateTracker.from_events(
650
+ self.tracker.sender_id,
651
+ self.tracker.events_after_latest_restart() + slot_events,
652
+ slots=self.domain.slots,
653
+ )
654
+ if recreate_tracker
655
+ else self.tracker
656
+ )
657
+
658
+ try:
659
+ custom_events = await remote_action.run(
660
+ output_channel, nlg, tracker, self.domain
661
+ )
662
+ for event in custom_events:
663
+ if isinstance(event, SlotSet):
664
+ slot_events.append(event)
665
+ elif isinstance(event, BotUttered):
666
+ slot_events.append(event)
667
+ else:
668
+ disallowed_types.add(event.type_name)
669
+ except (RasaException, ClientResponseError) as e:
670
+ structlogger.warning(
671
+ "slot_filling_manager.run_custom_action_failed",
672
+ failed_custom_action=custom_action,
673
+ event_info=f"Failed to execute custom action '{custom_action}' "
674
+ f"as a result of error '{e!s}'. The default action "
675
+ f"'{ACTION_EXTRACT_SLOTS}' failed to fill slots with custom "
676
+ f"mappings.",
677
+ )
678
+
679
+ for type_name in disallowed_types:
680
+ structlogger.info(
681
+ "slot_filling_manager.run_custom_action_disallowed_event",
682
+ custom_action_name=custom_action,
683
+ disallowed_evet_type=type_name,
684
+ event_info=f"Running custom action '{custom_action}' has resulted "
685
+ f"in an event of type '{type_name}'. This is "
686
+ f"disallowed and the tracker will not be "
687
+ f"updated with this event.",
688
+ )
689
+
690
+ return slot_events
691
+
692
+ async def execute_validation_action(
693
+ self,
694
+ extraction_events: List[Event],
695
+ output_channel: "OutputChannel",
696
+ nlg: "NaturalLanguageGenerator",
697
+ ) -> List[Event]:
698
+ slot_events: List[SlotSet] = [
699
+ event for event in extraction_events if isinstance(event, SlotSet)
700
+ ]
701
+
702
+ slot_candidates = "\n".join([e.key for e in slot_events])
703
+ structlogger.debug(
704
+ "slot_filling_manager.execute_validation_action",
705
+ slot_candidates=slot_candidates,
706
+ event_info=f"Validating extracted slots: {slot_candidates}",
707
+ )
708
+
709
+ if ACTION_VALIDATE_SLOT_MAPPINGS not in self.domain.user_actions:
710
+ return cast(List[Event], slot_events)
711
+
712
+ validate_events = await self._run_custom_action(
713
+ ACTION_VALIDATE_SLOT_MAPPINGS, output_channel, nlg, recreate_tracker=True
714
+ )
715
+ validated_slot_names = [
716
+ event.key for event in validate_events if isinstance(event, SlotSet)
717
+ ]
718
+
719
+ # If the custom action doesn't return a SlotSet event for an extracted slot
720
+ # candidate we assume that it was valid. The custom action has to return a
721
+ # SlotSet(slot_name, None) event to mark a Slot as invalid.
722
+ return validate_events + [
723
+ event for event in slot_events if event.key not in validated_slot_names
724
+ ]
725
+
527
726
 
528
727
  def extract_slot_value(
529
728
  slot: Slot, slot_filling_manager: SlotFillingManager
@@ -554,7 +753,11 @@ def extract_slot_value(
554
753
  value is not None
555
754
  or slot_filling_manager.tracker.get_slot(slot.name) is not None
556
755
  ):
557
- logger.debug(f"Extracted value '{value}' for slot '{slot.name}'.")
756
+ structlogger.debug(
757
+ "slot_filling_manager.extract_slot_value",
758
+ slot_name=slot.name,
759
+ event_info=f"Extracted value '{value}' for slot '{slot.name}'.",
760
+ )
558
761
 
559
762
  is_extracted = True
560
763
  return value, is_extracted
rasa/shared/core/slots.py CHANGED
@@ -1,7 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  from abc import ABC, abstractmethod
5
+ from dataclasses import dataclass
3
6
  from typing import Any, Dict, List, Optional, Text, Type
4
7
 
8
+ from pydantic import BaseModel, Field
9
+
5
10
  import rasa.shared.core.constants
6
11
  import rasa.shared.utils.common
7
12
  import rasa.shared.utils.io
@@ -9,6 +14,9 @@ from rasa.shared.constants import (
9
14
  DOCS_URL_CATEGORICAL_SLOTS,
10
15
  DOCS_URL_NLU_BASED_SLOTS,
11
16
  DOCS_URL_SLOTS,
17
+ REFILL_UTTER,
18
+ REJECTIONS,
19
+ UTTER_ASK_PREFIX,
12
20
  )
13
21
  from rasa.shared.exceptions import RasaException
14
22
 
@@ -23,6 +31,80 @@ class InvalidSlotConfigError(RasaException, ValueError):
23
31
  """Raised if a slot's config is invalid."""
24
32
 
25
33
 
34
+ @dataclass
35
+ class SlotRejection:
36
+ """A pair of validation condition and an utterance for the case of failure."""
37
+
38
+ if_: str
39
+ """The condition that should be checked."""
40
+ utter: str
41
+ """The utterance that should be executed if the condition is met."""
42
+
43
+ @staticmethod
44
+ def from_dict(data: Dict[str, Any]) -> SlotRejection:
45
+ """Create a SlotRejection object from serialized data.
46
+
47
+ Args:
48
+ data: data for a SlotRejection object in a serialized format
49
+
50
+ Returns:
51
+ A SlotRejection object
52
+ """
53
+ return SlotRejection(
54
+ if_=data["if"],
55
+ utter=data["utter"],
56
+ )
57
+
58
+ def as_dict(self) -> Dict[str, Any]:
59
+ """Serialize the SlotRejection object.
60
+
61
+ Returns:
62
+ the SlotRejection object as serialized data
63
+ """
64
+ return {
65
+ "if": self.if_,
66
+ "utter": self.utter,
67
+ }
68
+
69
+
70
+ class SlotValidation(BaseModel):
71
+ rejections: List[SlotRejection] = Field(alias=REJECTIONS)
72
+ """how the slot value is validated using predicate evaluation."""
73
+ refill_utter: str = Field(alias=REFILL_UTTER)
74
+ """The utterance that the assistant uses to ask for the slot."""
75
+
76
+ @staticmethod
77
+ def from_dict(data: Dict[str, Any]) -> SlotValidation:
78
+ """Creates a SlotValidation object from serialised data.
79
+
80
+ Args:
81
+ data: data for a SlotValidation object in a serialized format
82
+
83
+ Returns:
84
+ A SlotValidation object
85
+ """
86
+ rejections = data.get(REJECTIONS)
87
+ if rejections is not None:
88
+ rejections = [
89
+ SlotRejection.from_dict(rejection) for rejection in rejections
90
+ ]
91
+
92
+ return SlotValidation(
93
+ rejections=rejections, refill_utter=data.get(REFILL_UTTER)
94
+ )
95
+
96
+ def as_dict(self) -> Dict[str, Any]:
97
+ """Serialize the SlotValidation object.
98
+
99
+ Returns:
100
+ the SlotValidation object as serialized data
101
+ """
102
+ return {
103
+ REJECTIONS: [rejection.as_dict() for rejection in self.rejections],
104
+ REFILL_UTTER: self.refill_utter,
105
+ }
106
+
107
+
26
108
  class Slot(ABC):
27
109
  """Key-value store for storing information during a conversation."""
28
110
 
@@ -42,6 +124,7 @@ class Slot(ABC):
42
124
  is_builtin: bool = False,
43
125
  shared_for_coexistence: bool = False,
44
126
  filled_by: Optional[str] = None,
127
+ validation: Optional[Dict[str, Any]] = None,
45
128
  ) -> None:
46
129
  """Create a Slot.
47
130
 
@@ -59,6 +142,8 @@ class Slot(ABC):
59
142
  shared_for_coexistence: If `True` the slot is not forgotten after either
60
143
  dm1 or CALM finishes.
61
144
  filled_by: The name of the extractor that fills the slot.
145
+ validation: The validation rules that should be used to validate
146
+ slot values.
62
147
  """
63
148
  from rasa.shared.core.slot_mappings import SlotMapping
64
149
 
@@ -73,6 +158,12 @@ class Slot(ABC):
73
158
  self.shared_for_coexistence = shared_for_coexistence
74
159
  self._filled_by = filled_by
75
160
 
161
+ if validation:
162
+ validation.setdefault(REFILL_UTTER, f"{UTTER_ASK_PREFIX}{self.name}")
163
+ self.validation = (
164
+ SlotValidation.from_dict(validation) if validation else validation
165
+ )
166
+
76
167
  def feature_dimensionality(self) -> int:
77
168
  """How many features this single slot creates.
78
169
 
@@ -191,12 +282,15 @@ class Slot(ABC):
191
282
 
192
283
  def persistence_info(self) -> Dict[str, Any]:
193
284
  """Returns relevant information to persist this slot."""
194
- return {
285
+ persistence_info_dict = {
195
286
  "type": rasa.shared.utils.common.module_path_from_instance(self),
196
287
  "initial_value": self.initial_value,
197
288
  "influence_conversation": self.influence_conversation,
198
289
  "mappings": [mapping.as_dict() for mapping in self.mappings],
199
290
  }
291
+ if self.validation:
292
+ persistence_info_dict["validation"] = self.validation.as_dict() # type: ignore
293
+ return persistence_info_dict
200
294
 
201
295
  def fingerprint(self) -> Text:
202
296
  """Returns a unique hash for the slot which is stable across python runs.
@@ -213,6 +307,10 @@ class Slot(ABC):
213
307
  return False
214
308
  return self.name == other.name and self.value == other.value
215
309
 
310
+ def requires_validation(self) -> bool:
311
+ """Indicates if the slot requires validation."""
312
+ return True if self.validation else False
313
+
216
314
 
217
315
  class FloatSlot(Slot):
218
316
  """A slot storing a float value."""
@@ -231,6 +329,7 @@ class FloatSlot(Slot):
231
329
  is_builtin: bool = False,
232
330
  shared_for_coexistence: bool = False,
233
331
  filled_by: Optional[str] = None,
332
+ validation: Optional[Dict[str, Any]] = None,
234
333
  ) -> None:
235
334
  """Creates a FloatSlot.
236
335
 
@@ -247,6 +346,7 @@ class FloatSlot(Slot):
247
346
  is_builtin,
248
347
  shared_for_coexistence,
249
348
  filled_by=filled_by,
349
+ validation=validation,
250
350
  )
251
351
  self.max_value = max_value
252
352
  self.min_value = min_value
@@ -405,6 +505,7 @@ class CategoricalSlot(Slot):
405
505
  is_builtin: bool = False,
406
506
  shared_for_coexistence: bool = False,
407
507
  filled_by: Optional[str] = None,
508
+ validation: Optional[Dict[str, Any]] = None,
408
509
  ) -> None:
409
510
  """Creates a `Categorical Slot` (see parent class for detailed docstring)."""
410
511
  super().__init__(
@@ -416,6 +517,7 @@ class CategoricalSlot(Slot):
416
517
  is_builtin,
417
518
  shared_for_coexistence,
418
519
  filled_by=filled_by,
520
+ validation=validation,
419
521
  )
420
522
  if values and None in values:
421
523
  rasa.shared.utils.io.raise_warning(
@@ -627,6 +729,7 @@ class AnySlot(Slot):
627
729
  is_builtin: bool = False,
628
730
  shared_for_coexistence: bool = False,
629
731
  filled_by: Optional[str] = None,
732
+ validation: Optional[Dict[str, Any]] = None,
630
733
  ) -> None:
631
734
  """Creates an `Any Slot` (see parent class for detailed docstring).
632
735
 
@@ -651,6 +754,7 @@ class AnySlot(Slot):
651
754
  is_builtin,
652
755
  shared_for_coexistence,
653
756
  filled_by=filled_by,
757
+ validation=validation,
654
758
  )
655
759
 
656
760
  def __eq__(self, other: Any) -> bool:
@@ -673,3 +777,35 @@ class AnySlot(Slot):
673
777
  f"implement a custom slot type by subclassing '{Slot.__name__}'. "
674
778
  f"See the documentation for more information: {DOCS_URL_NLU_BASED_SLOTS}"
675
779
  )
780
+
781
+
782
+ class StrictCategoricalSlot(CategoricalSlot):
783
+ """A categorical slot that strictly enforces allowed values."""
784
+
785
+ type_name = "strict_categorical"
786
+
787
+ def coerce_value(self, value: Any) -> Any:
788
+ """Coerce the value to one of the allowed ones or raise an error if invalid."""
789
+ if value is None:
790
+ return value
791
+
792
+ for allowed_value in self.values:
793
+ # Allowed values are always stored as strings, so we can use casefold().
794
+ if value.casefold() == allowed_value.casefold():
795
+ return allowed_value
796
+
797
+ raise InvalidSlotConfigError(
798
+ f"Value '{value}' is not allowed for the slot '{self.name}'. "
799
+ f"Allowed values are: {self.values}"
800
+ )
801
+
802
+ @Slot.value.setter # type: ignore[attr-defined,misc]
803
+ def value(self, value: Any) -> None:
804
+ """Set the slot's value using strict coercion."""
805
+ coerced_value = self.coerce_value(value)
806
+ super(StrictCategoricalSlot, self.__class__).value.fset(self, coerced_value)
807
+
808
+ def add_default_value(self) -> None:
809
+ # StrictCategoricalSlot enforces validation against a specified set of values,
810
+ # so default values should not be automatically added.
811
+ pass
@@ -6,6 +6,7 @@ import os
6
6
  import time
7
7
  from collections import deque
8
8
  from enum import Enum
9
+ from functools import cached_property
9
10
  from typing import (
10
11
  TYPE_CHECKING,
11
12
  Any,
@@ -26,6 +27,7 @@ from typing import (
26
27
  )
27
28
 
28
29
  import rasa.shared.utils.io
30
+ from rasa.engine.language import Language
29
31
  from rasa.shared.constants import (
30
32
  ASSISTANT_ID_KEY,
31
33
  DEFAULT_SENDER_ID,
@@ -37,6 +39,7 @@ from rasa.shared.core.constants import (
37
39
  ACTION_SESSION_START_NAME,
38
40
  ACTIVE_LOOP,
39
41
  FOLLOWUP_ACTION,
42
+ LANGUAGE_SLOT,
40
43
  LOOP_NAME,
41
44
  PREVIOUS_ACTION,
42
45
  SHOULD_NOT_BE_SET,
@@ -61,7 +64,8 @@ from rasa.shared.core.events import (
61
64
  UserUttered,
62
65
  )
63
66
  from rasa.shared.core.flows import FlowsList
64
- from rasa.shared.core.slots import AnySlot, Slot
67
+ from rasa.shared.core.slots import AnySlot, Slot, StrictCategoricalSlot
68
+ from rasa.shared.exceptions import RasaException
65
69
  from rasa.shared.nlu.constants import (
66
70
  ACTION_NAME,
67
71
  ACTION_TEXT,
@@ -1097,6 +1101,75 @@ class DialogueStateTracker:
1097
1101
 
1098
1102
  return FlowsList(active_flows)
1099
1103
 
1104
+ @cached_property
1105
+ def supported_languages(self) -> List[Language]:
1106
+ """Returns the supported languages for this model configuration
1107
+
1108
+ Returns:
1109
+ A list of supported languages.
1110
+ """
1111
+ if LANGUAGE_SLOT not in self.slots:
1112
+ raise RasaException(
1113
+ f"The required slot '{LANGUAGE_SLOT}' is missing from the tracker. "
1114
+ f"Please ensure that a slot named '{LANGUAGE_SLOT}' exists."
1115
+ )
1116
+
1117
+ language_slot = self.slots[LANGUAGE_SLOT]
1118
+
1119
+ if not isinstance(language_slot, StrictCategoricalSlot):
1120
+ raise RasaException(
1121
+ f"The slot '{LANGUAGE_SLOT}' must be of type "
1122
+ f"'{StrictCategoricalSlot.type_name}'. "
1123
+ f"Please update the slot configuration accordingly."
1124
+ )
1125
+
1126
+ return [
1127
+ Language.from_language_code(language_code)
1128
+ for language_code in language_slot.values
1129
+ ]
1130
+
1131
+ @property
1132
+ def current_language(self) -> Optional[Language]:
1133
+ """Get the language of the current conversation.
1134
+
1135
+ Returns:
1136
+ The language of the current conversation or `None` if not set.
1137
+ """
1138
+ language_code = self.get_slot("language")
1139
+ if not language_code:
1140
+ return None
1141
+
1142
+ supported_languages = self.supported_languages or []
1143
+ matching_language = (
1144
+ language
1145
+ for language in supported_languages
1146
+ if language.code == language_code
1147
+ )
1148
+ return next(matching_language, None)
1149
+
1150
+ @property
1151
+ def default_language(self) -> Language:
1152
+ """Get the assistant's default language.
1153
+
1154
+ Returns:
1155
+ The assistant's default language.
1156
+
1157
+ Raises:
1158
+ RasaException: If no default language is defined in the config.
1159
+ """
1160
+ supported_languages = self.supported_languages or []
1161
+ matching_language = (
1162
+ language for language in supported_languages if language.is_default is True
1163
+ )
1164
+ try:
1165
+ return next(matching_language)
1166
+ except StopIteration:
1167
+ raise RasaException(
1168
+ "No default language configured. "
1169
+ "Please configure the `language` parameter in config.yml file. "
1170
+ "Example: `language: en`."
1171
+ )
1172
+
1100
1173
 
1101
1174
  class TrackerEventDiffEngine:
1102
1175
  """Computes event difference of two trackers."""