rasa-pro 3.12.0.dev13__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 (128) hide show
  1. rasa/anonymization/anonymization_rule_executor.py +16 -10
  2. rasa/cli/data.py +16 -0
  3. rasa/cli/project_templates/calm/config.yml +2 -2
  4. rasa/cli/project_templates/calm/endpoints.yml +2 -2
  5. rasa/cli/utils.py +12 -0
  6. rasa/core/actions/action.py +84 -191
  7. rasa/core/actions/action_run_slot_rejections.py +16 -4
  8. rasa/core/channels/__init__.py +2 -0
  9. rasa/core/channels/studio_chat.py +19 -0
  10. rasa/core/channels/telegram.py +42 -24
  11. rasa/core/channels/voice_ready/utils.py +1 -1
  12. rasa/core/channels/voice_stream/asr/asr_engine.py +10 -4
  13. rasa/core/channels/voice_stream/asr/azure.py +14 -1
  14. rasa/core/channels/voice_stream/asr/deepgram.py +20 -4
  15. rasa/core/channels/voice_stream/audiocodes.py +264 -0
  16. rasa/core/channels/voice_stream/browser_audio.py +4 -1
  17. rasa/core/channels/voice_stream/call_state.py +3 -0
  18. rasa/core/channels/voice_stream/genesys.py +6 -2
  19. rasa/core/channels/voice_stream/tts/azure.py +9 -1
  20. rasa/core/channels/voice_stream/tts/cartesia.py +14 -8
  21. rasa/core/channels/voice_stream/voice_channel.py +23 -2
  22. rasa/core/constants.py +2 -0
  23. rasa/core/nlg/contextual_response_rephraser.py +18 -1
  24. rasa/core/nlg/generator.py +83 -15
  25. rasa/core/nlg/response.py +6 -3
  26. rasa/core/nlg/translate.py +55 -0
  27. rasa/core/policies/enterprise_search_prompt_with_citation_template.jinja2 +1 -1
  28. rasa/core/policies/flows/flow_executor.py +12 -5
  29. rasa/core/processor.py +72 -9
  30. rasa/dialogue_understanding/commands/can_not_handle_command.py +20 -2
  31. rasa/dialogue_understanding/commands/cancel_flow_command.py +24 -6
  32. rasa/dialogue_understanding/commands/change_flow_command.py +20 -2
  33. rasa/dialogue_understanding/commands/chit_chat_answer_command.py +20 -2
  34. rasa/dialogue_understanding/commands/clarify_command.py +29 -3
  35. rasa/dialogue_understanding/commands/command.py +1 -16
  36. rasa/dialogue_understanding/commands/command_syntax_manager.py +55 -0
  37. rasa/dialogue_understanding/commands/human_handoff_command.py +20 -2
  38. rasa/dialogue_understanding/commands/knowledge_answer_command.py +20 -2
  39. rasa/dialogue_understanding/commands/prompt_command.py +94 -0
  40. rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +20 -2
  41. rasa/dialogue_understanding/commands/set_slot_command.py +24 -2
  42. rasa/dialogue_understanding/commands/skip_question_command.py +20 -2
  43. rasa/dialogue_understanding/commands/start_flow_command.py +20 -2
  44. rasa/dialogue_understanding/commands/utils.py +98 -4
  45. rasa/dialogue_understanding/generator/__init__.py +2 -0
  46. rasa/dialogue_understanding/generator/command_parser.py +15 -12
  47. rasa/dialogue_understanding/generator/constants.py +3 -0
  48. rasa/dialogue_understanding/generator/llm_based_command_generator.py +12 -5
  49. rasa/dialogue_understanding/generator/llm_command_generator.py +5 -3
  50. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +16 -2
  51. rasa/dialogue_understanding/generator/prompt_templates/__init__.py +0 -0
  52. rasa/dialogue_understanding/generator/{single_step → prompt_templates}/command_prompt_template.jinja2 +2 -0
  53. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_claude_3_5_sonnet_20240620_template.jinja2 +77 -0
  54. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_default.jinja2 +68 -0
  55. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_gpt_4o_2024_11_20_template.jinja2 +84 -0
  56. rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +460 -0
  57. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +12 -310
  58. rasa/dialogue_understanding/patterns/collect_information.py +1 -1
  59. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +16 -0
  60. rasa/dialogue_understanding/patterns/validate_slot.py +65 -0
  61. rasa/dialogue_understanding/processor/command_processor.py +39 -0
  62. rasa/dialogue_understanding_test/du_test_case.py +28 -8
  63. rasa/dialogue_understanding_test/du_test_result.py +13 -9
  64. rasa/dialogue_understanding_test/io.py +14 -0
  65. rasa/e2e_test/utils/io.py +0 -37
  66. rasa/engine/graph.py +1 -0
  67. rasa/engine/language.py +140 -0
  68. rasa/engine/recipes/config_files/default_config.yml +4 -0
  69. rasa/engine/recipes/default_recipe.py +2 -0
  70. rasa/engine/recipes/graph_recipe.py +2 -0
  71. rasa/engine/storage/local_model_storage.py +1 -0
  72. rasa/engine/storage/storage.py +4 -1
  73. rasa/model_manager/runner_service.py +7 -4
  74. rasa/model_manager/socket_bridge.py +7 -6
  75. rasa/shared/constants.py +15 -13
  76. rasa/shared/core/constants.py +2 -0
  77. rasa/shared/core/flows/constants.py +11 -0
  78. rasa/shared/core/flows/flow.py +83 -19
  79. rasa/shared/core/flows/flows_yaml_schema.json +31 -3
  80. rasa/shared/core/flows/steps/collect.py +1 -36
  81. rasa/shared/core/flows/utils.py +28 -4
  82. rasa/shared/core/flows/validation.py +1 -1
  83. rasa/shared/core/slot_mappings.py +208 -5
  84. rasa/shared/core/slots.py +131 -1
  85. rasa/shared/core/trackers.py +74 -1
  86. rasa/shared/importers/importer.py +50 -2
  87. rasa/shared/nlu/training_data/schemas/responses.yml +19 -12
  88. rasa/shared/providers/_configs/azure_entra_id_config.py +541 -0
  89. rasa/shared/providers/_configs/azure_openai_client_config.py +138 -3
  90. rasa/shared/providers/_configs/client_config.py +3 -1
  91. rasa/shared/providers/_configs/default_litellm_client_config.py +3 -1
  92. rasa/shared/providers/_configs/huggingface_local_embedding_client_config.py +3 -1
  93. rasa/shared/providers/_configs/litellm_router_client_config.py +3 -1
  94. rasa/shared/providers/_configs/model_group_config.py +4 -2
  95. rasa/shared/providers/_configs/oauth_config.py +33 -0
  96. rasa/shared/providers/_configs/openai_client_config.py +3 -1
  97. rasa/shared/providers/_configs/rasa_llm_client_config.py +3 -1
  98. rasa/shared/providers/_configs/self_hosted_llm_client_config.py +3 -1
  99. rasa/shared/providers/constants.py +6 -0
  100. rasa/shared/providers/embedding/azure_openai_embedding_client.py +28 -3
  101. rasa/shared/providers/embedding/litellm_router_embedding_client.py +3 -1
  102. rasa/shared/providers/llm/_base_litellm_client.py +42 -17
  103. rasa/shared/providers/llm/azure_openai_llm_client.py +81 -25
  104. rasa/shared/providers/llm/default_litellm_llm_client.py +3 -1
  105. rasa/shared/providers/llm/litellm_router_llm_client.py +29 -8
  106. rasa/shared/providers/llm/llm_client.py +23 -7
  107. rasa/shared/providers/llm/openai_llm_client.py +9 -3
  108. rasa/shared/providers/llm/rasa_llm_client.py +11 -2
  109. rasa/shared/providers/llm/self_hosted_llm_client.py +30 -11
  110. rasa/shared/providers/router/_base_litellm_router_client.py +3 -1
  111. rasa/shared/providers/router/router_client.py +3 -1
  112. rasa/shared/utils/constants.py +3 -0
  113. rasa/shared/utils/llm.py +30 -7
  114. rasa/shared/utils/pykwalify_extensions.py +24 -0
  115. rasa/shared/utils/schemas/domain.yml +26 -0
  116. rasa/telemetry.py +2 -1
  117. rasa/tracing/config.py +2 -0
  118. rasa/tracing/constants.py +12 -0
  119. rasa/tracing/instrumentation/instrumentation.py +36 -0
  120. rasa/tracing/instrumentation/metrics.py +41 -0
  121. rasa/tracing/metric_instrument_provider.py +40 -0
  122. rasa/validator.py +372 -7
  123. rasa/version.py +1 -1
  124. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc1.dist-info}/METADATA +2 -1
  125. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc1.dist-info}/RECORD +128 -113
  126. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc1.dist-info}/NOTICE +0 -0
  127. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc1.dist-info}/WHEEL +0 -0
  128. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc1.dist-info}/entry_points.txt +0 -0
@@ -7,14 +7,29 @@ from pathlib import Path
7
7
  from typing import Any, Dict, List, Optional, Set, Text, Union
8
8
 
9
9
  import structlog
10
+ from pydantic import BaseModel
10
11
  from pypred import Predicate
11
12
 
12
13
  import rasa.shared.utils.io
14
+ from rasa.engine.language import Language
13
15
  from rasa.shared.constants import RASA_DEFAULT_FLOW_PATTERN_PREFIX
14
16
  from rasa.shared.core.constants import (
15
17
  KEY_ASK_CONFIRM_DIGRESSIONS,
16
18
  KEY_BLOCK_DIGRESSIONS,
17
19
  )
20
+ from rasa.shared.core.flows.constants import (
21
+ KEY_ALWAYS_INCLUDE_IN_PROMPT,
22
+ KEY_DESCRIPTION,
23
+ KEY_FILE_PATH,
24
+ KEY_ID,
25
+ KEY_IF,
26
+ KEY_NAME,
27
+ KEY_NLU_TRIGGER,
28
+ KEY_PERSISTED_SLOTS,
29
+ KEY_RUN_PATTERN_COMPLETED,
30
+ KEY_STEPS,
31
+ KEY_TRANSLATION,
32
+ )
18
33
  from rasa.shared.core.flows.flow_path import FlowPath, FlowPathsList, PathNode
19
34
  from rasa.shared.core.flows.flow_step import FlowStep
20
35
  from rasa.shared.core.flows.flow_step_links import (
@@ -43,6 +58,16 @@ from rasa.shared.core.slots import Slot
43
58
  structlogger = structlog.get_logger()
44
59
 
45
60
 
61
+ class FlowLanguageTranslation(BaseModel):
62
+ """Represents the translation of the flow properties in a specific language."""
63
+
64
+ name: str
65
+ """The human-readable name of the flow."""
66
+
67
+ class Config:
68
+ extra = "ignore"
69
+
70
+
46
71
  @dataclass
47
72
  class Flow:
48
73
  """Represents the configuration of a flow."""
@@ -53,6 +78,8 @@ class Flow:
53
78
  """The human-readable name of the flow."""
54
79
  description: Optional[Text] = None
55
80
  """The description of the flow."""
81
+ translation: Dict[Text, FlowLanguageTranslation] = field(default_factory=dict)
82
+ """The translation of the flow properties in different languages."""
56
83
  guard_condition: Optional[Text] = None
57
84
  """The condition that needs to be fulfilled for the flow to be startable."""
58
85
  step_sequence: FlowStepSequence = field(default_factory=FlowStepSequence.empty)
@@ -71,6 +98,8 @@ class Flow:
71
98
  """The flow ids for which the assistant should ask for confirmation."""
72
99
  block_digressions: List[str] = field(default_factory=list)
73
100
  """The flow ids that the assistant should block from digressing to."""
101
+ run_pattern_completed: bool = True
102
+ """Whether the pattern_completed flow should be run after the flow ends."""
74
103
 
75
104
  @staticmethod
76
105
  def from_json(
@@ -88,6 +117,8 @@ class Flow:
88
117
  Returns:
89
118
  A Flow object.
90
119
  """
120
+ from rasa.shared.core.flows.utils import extract_translations
121
+
91
122
  step_sequence = FlowStepSequence.from_json(flow_id, data.get("steps"))
92
123
  nlu_triggers = NLUTriggers.from_json(data.get("nlu_trigger"))
93
124
 
@@ -96,21 +127,25 @@ class Flow:
96
127
 
97
128
  return Flow(
98
129
  id=flow_id,
99
- custom_name=data.get("name"),
100
- description=data.get("description"),
101
- always_include_in_prompt=data.get("always_include_in_prompt"),
102
- # str or bool are permitted in the flow schema but internally we want a str
103
- guard_condition=str(data["if"]) if "if" in data else None,
130
+ custom_name=data.get(KEY_NAME),
131
+ description=data.get(KEY_DESCRIPTION),
132
+ always_include_in_prompt=data.get(KEY_ALWAYS_INCLUDE_IN_PROMPT),
133
+ # str or bool are permitted in the flow schema, but internally we want a str
134
+ guard_condition=str(data[KEY_IF]) if KEY_IF in data else None,
104
135
  step_sequence=Flow.resolve_default_ids(step_sequence),
105
136
  nlu_triggers=nlu_triggers,
106
137
  # If we are reading the flows in after training the file_path is part of
107
138
  # data. When the model is trained, take the provided file_path.
108
- file_path=data.get("file_path") if "file_path" in data else file_path,
109
- persisted_slots=data.get("persisted_slots", []),
139
+ file_path=data.get(KEY_FILE_PATH) if KEY_FILE_PATH in data else file_path,
140
+ persisted_slots=data.get(KEY_PERSISTED_SLOTS, []),
110
141
  ask_confirm_digressions=extract_digression_prop(
111
142
  KEY_ASK_CONFIRM_DIGRESSIONS, data
112
143
  ),
113
144
  block_digressions=extract_digression_prop(KEY_BLOCK_DIGRESSIONS, data),
145
+ run_pattern_completed=data.get(KEY_RUN_PATTERN_COMPLETED, True),
146
+ translation=extract_translations(
147
+ translation_data=data.get(KEY_TRANSLATION, {})
148
+ ),
114
149
  )
115
150
 
116
151
  def get_full_name(self) -> str:
@@ -168,33 +203,62 @@ class Flow:
168
203
  The Flow object as serialized data.
169
204
  """
170
205
  data: Dict[Text, Any] = {
171
- "id": self.id,
172
- "steps": self.step_sequence.as_json(),
206
+ KEY_ID: self.id,
207
+ KEY_STEPS: self.step_sequence.as_json(),
173
208
  }
174
209
  if self.custom_name is not None:
175
- data["name"] = self.custom_name
210
+ data[KEY_NAME] = self.custom_name
176
211
  if self.description is not None:
177
- data["description"] = self.description
212
+ data[KEY_DESCRIPTION] = self.description
178
213
  if self.guard_condition is not None:
179
- data["if"] = self.guard_condition
214
+ data[KEY_IF] = self.guard_condition
180
215
  if self.always_include_in_prompt is not None:
181
- data["always_include_in_prompt"] = self.always_include_in_prompt
216
+ data[KEY_ALWAYS_INCLUDE_IN_PROMPT] = self.always_include_in_prompt
182
217
  if self.nlu_triggers:
183
- data["nlu_trigger"] = self.nlu_triggers.as_json()
218
+ data[KEY_NLU_TRIGGER] = self.nlu_triggers.as_json()
184
219
  if self.file_path:
185
- data["file_path"] = self.file_path
220
+ data[KEY_FILE_PATH] = self.file_path
186
221
  if self.persisted_slots:
187
- data["persisted_slots"] = self.persisted_slots
222
+ data[KEY_PERSISTED_SLOTS] = self.persisted_slots
188
223
  if self.ask_confirm_digressions:
189
224
  data[KEY_ASK_CONFIRM_DIGRESSIONS] = self.ask_confirm_digressions
190
225
  if self.block_digressions:
191
226
  data[KEY_BLOCK_DIGRESSIONS] = self.block_digressions
227
+ if self.run_pattern_completed is not None:
228
+ data["run_pattern_completed"] = self.run_pattern_completed
229
+ if self.translation:
230
+ data[KEY_TRANSLATION] = {
231
+ language_code: translation.dict()
232
+ for language_code, translation in self.translation.items()
233
+ }
192
234
 
193
235
  return data
194
236
 
195
- def readable_name(self) -> str:
196
- """Returns the name of the flow or its id if no name is set."""
197
- return self.name or self.id
237
+ def localized_name(self, language: Optional[Language] = None) -> Optional[Text]:
238
+ """Returns the language specific flow name or None.
239
+
240
+ Args:
241
+ language: Preferred language code.
242
+
243
+ Returns:
244
+ Flow name in the specified language or None.
245
+ """
246
+ language_code = language.code if language else None
247
+ translation = self.translation.get(language_code)
248
+ return translation.name if translation else None
249
+
250
+ def readable_name(self, language: Optional[Language] = None) -> str:
251
+ """
252
+ Returns the flow's name in the specified language if available; otherwise
253
+ falls back to the flow's name, and finally the flow's ID.
254
+
255
+ Args:
256
+ language: Preferred language code.
257
+
258
+ Returns:
259
+ string: the localized name, the default name, or the flow's ID.
260
+ """
261
+ return self.localized_name(language) or self.name or self.id
198
262
 
199
263
  def step_by_id(self, step_id: Optional[Text]) -> Optional[FlowStep]:
200
264
  """Returns the step with the given id."""
@@ -288,9 +288,37 @@
288
288
  "additionalProperties": false,
289
289
  "schema_name": "dictionary with flow properties",
290
290
  "properties": {
291
+ "name": {
292
+ "type": "string"
293
+ },
291
294
  "description": {
292
295
  "type": "string"
293
296
  },
297
+ "translation": {
298
+ "type": "object",
299
+ "schema_name": "flow translation mapping",
300
+ "properties": {
301
+ "metadata": {
302
+ "type": "object"
303
+ }
304
+ },
305
+ "patternProperties": {
306
+ "^(?!metadata$).+": {
307
+ "type": "object",
308
+ "additionalProperties": false,
309
+ "required": [
310
+ "name"
311
+ ],
312
+ "properties": {
313
+ "name": {
314
+ "type": "string"
315
+ },
316
+ "metadata": {}
317
+ }
318
+ }
319
+ },
320
+ "additionalProperties": false
321
+ },
294
322
  "if": {
295
323
  "type": [
296
324
  "string",
@@ -300,9 +328,6 @@
300
328
  "always_include_in_prompt": {
301
329
  "type": "boolean"
302
330
  },
303
- "name": {
304
- "type": "string"
305
- },
306
331
  "nlu_trigger": {
307
332
  "$ref": "#/$defs/nlu_trigger"
308
333
  },
@@ -320,6 +345,9 @@
320
345
  },
321
346
  "block_digressions": {
322
347
  "$ref": "#/$defs/block_digressions"
348
+ },
349
+ "run_pattern_completed": {
350
+ "type": "boolean"
323
351
  }
324
352
  }
325
353
  },
@@ -10,42 +10,7 @@ from rasa.shared.core.constants import (
10
10
  )
11
11
  from rasa.shared.core.flows.flow_step import FlowStep
12
12
  from rasa.shared.core.flows.utils import extract_digression_prop
13
-
14
-
15
- @dataclass
16
- class SlotRejection:
17
- """A pair of validation condition and an utterance for the case of failure."""
18
-
19
- if_: str
20
- """The condition that should be checked."""
21
- utter: str
22
- """The utterance that should be executed if the condition is met."""
23
-
24
- @staticmethod
25
- def from_dict(data: Dict[str, Any]) -> SlotRejection:
26
- """Create a SlotRejection object from serialized data.
27
-
28
- Args:
29
- data: data for a SlotRejection object in a serialized format
30
-
31
- Returns:
32
- A SlotRejection object
33
- """
34
- return SlotRejection(
35
- if_=data["if"],
36
- utter=data["utter"],
37
- )
38
-
39
- def as_dict(self) -> Dict[str, Any]:
40
- """Serialize the SlotRejection object.
41
-
42
- Returns:
43
- the SlotRejection object as serialized data
44
- """
45
- return {
46
- "if": self.if_,
47
- "utter": self.utter,
48
- }
13
+ from rasa.shared.core.slots import SlotRejection
49
14
 
50
15
 
51
16
  @dataclass
@@ -1,17 +1,21 @@
1
- from typing import Any, Dict, List, Set
1
+ from typing import TYPE_CHECKING, Any, Dict, List, Set, Text
2
2
 
3
3
  from rasa.shared.utils.io import raise_deprecation_warning
4
4
 
5
+ if TYPE_CHECKING:
6
+ from rasa.shared.core.flows.flow import FlowLanguageTranslation
7
+
8
+
5
9
  RESET_PROPERTY_NAME = "reset_after_flow_ends"
6
10
  PERSIST_PROPERTY_NAME = "persisted_slots"
7
11
  ALL_LABEL = "ALL"
8
12
 
9
13
 
10
- def warn_deprecated_collect_step_config(flow_id: str, collect_step: str) -> None:
14
+ def warn_deprecated_collect_step_config() -> None:
11
15
  """Warns about deprecated reset_after_flow_ends usage in collect steps."""
12
16
  raise_deprecation_warning(
13
- f"Configuring '{RESET_PROPERTY_NAME}' in collect step '{collect_step}' is "
14
- f"deprecated and will be removed in Rasa Pro 4.0.0. In flow id '{flow_id}', "
17
+ f"Configuring '{RESET_PROPERTY_NAME}' in collect steps is "
18
+ f"deprecated and will be removed in Rasa Pro 4.0.0. In the parent flow, "
15
19
  f"please use the '{PERSIST_PROPERTY_NAME}' "
16
20
  "property at the flow level instead."
17
21
  )
@@ -53,3 +57,23 @@ def extract_digression_prop(prop: str, data: Dict[str, Any]) -> List[str]:
53
57
  digression_property = [ALL_LABEL] if digression_property else []
54
58
 
55
59
  return digression_property
60
+
61
+
62
+ def extract_translations(
63
+ translation_data: Dict[Text, Any],
64
+ ) -> Dict[Text, "FlowLanguageTranslation"]:
65
+ """Extracts translations from a dictionary.
66
+
67
+ Args:
68
+ translation_data: The dictionary containing the translations.
69
+
70
+ Returns:
71
+ A dictionary containing the extracted translations.
72
+ """
73
+ from rasa.shared.core.flows.flow import FlowLanguageTranslation
74
+
75
+ return {
76
+ language_code: FlowLanguageTranslation.parse_obj({**data})
77
+ for language_code, data in translation_data.items()
78
+ if language_code != "metadata"
79
+ }
@@ -723,7 +723,7 @@ def validate_slot_persistence_configuration(flow: Flow) -> None:
723
723
  flow_slots.add(step.collect)
724
724
  if not step.reset_after_flow_ends:
725
725
  collect_step = step.collect
726
- warn_deprecated_collect_step_config(flow_id, collect_step)
726
+ warn_deprecated_collect_step_config()
727
727
  if has_flow_level_persistence:
728
728
  raise DuplicateSlotPersistConfigException(flow_id, collect_step)
729
729
 
@@ -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