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,142 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ import structlog
6
+
7
+ from rasa.core.actions.action import Action, create_bot_utterance
8
+ from rasa.core.channels import OutputChannel
9
+ from rasa.core.nlg import NaturalLanguageGenerator
10
+ from rasa.core.utils import add_bot_utterance_metadata
11
+ from rasa.dialogue_understanding.patterns.continue_interrupted import (
12
+ ContinueInterruptedPatternFlowStackFrame,
13
+ )
14
+ from rasa.dialogue_understanding.patterns.handle_digressions import (
15
+ HandleDigressionsPatternFlowStackFrame,
16
+ )
17
+ from rasa.dialogue_understanding.stack.frames.flow_stack_frame import (
18
+ FlowStackFrameType,
19
+ UserFlowStackFrame,
20
+ )
21
+ from rasa.shared.core.constants import (
22
+ ACTION_BLOCK_DIGRESSION,
23
+ ACTION_CONTINUE_DIGRESSION,
24
+ )
25
+ from rasa.shared.core.domain import Domain
26
+ from rasa.shared.core.events import Event, FlowInterrupted
27
+ from rasa.shared.core.trackers import DialogueStateTracker
28
+
29
+ structlogger = structlog.get_logger()
30
+
31
+
32
+ class ActionBlockDigressions(Action):
33
+ """Action which blocks an interruption and continues the current flow."""
34
+
35
+ def name(self) -> str:
36
+ """Return the action name."""
37
+ return ACTION_BLOCK_DIGRESSION
38
+
39
+ async def run(
40
+ self,
41
+ output_channel: OutputChannel,
42
+ nlg: NaturalLanguageGenerator,
43
+ tracker: DialogueStateTracker,
44
+ domain: Domain,
45
+ metadata: Optional[Dict[str, Any]] = None,
46
+ ) -> List[Event]:
47
+ """Update the stack."""
48
+ structlogger.debug("action_block_digressions.run")
49
+ top_frame = tracker.stack.top()
50
+
51
+ if not isinstance(top_frame, HandleDigressionsPatternFlowStackFrame):
52
+ return []
53
+
54
+ blocked_flow_id = top_frame.interrupting_flow_id
55
+ frame_type = FlowStackFrameType.REGULAR
56
+
57
+ stack = tracker.stack
58
+ stack.push(
59
+ UserFlowStackFrame(flow_id=blocked_flow_id, frame_type=frame_type), 0
60
+ )
61
+ stack.push(
62
+ ContinueInterruptedPatternFlowStackFrame(
63
+ previous_flow_name=blocked_flow_id
64
+ ),
65
+ 1,
66
+ )
67
+ events = tracker.create_stack_updated_events(stack)
68
+
69
+ utterance = "utter_block_digressions"
70
+ message = await nlg.generate(
71
+ utterance,
72
+ tracker,
73
+ output_channel.name(),
74
+ )
75
+
76
+ if message is None:
77
+ structlogger.error(
78
+ "action_block_digressions.run.failed.finding.utter",
79
+ utterance=utterance,
80
+ )
81
+ else:
82
+ message = add_bot_utterance_metadata(
83
+ message, utterance, nlg, domain, tracker
84
+ )
85
+ events.append(create_bot_utterance(message))
86
+
87
+ return events
88
+
89
+
90
+ class ActionContinueDigression(Action):
91
+ """Action which continues with an interruption."""
92
+
93
+ def name(self) -> str:
94
+ """Return the action name."""
95
+ return ACTION_CONTINUE_DIGRESSION
96
+
97
+ async def run(
98
+ self,
99
+ output_channel: OutputChannel,
100
+ nlg: NaturalLanguageGenerator,
101
+ tracker: DialogueStateTracker,
102
+ domain: Domain,
103
+ metadata: Optional[Dict[str, Any]] = None,
104
+ ) -> List[Event]:
105
+ """Update the stack."""
106
+ structlogger.debug("action_continue_digression.run")
107
+ top_frame = tracker.stack.top()
108
+
109
+ if not isinstance(top_frame, HandleDigressionsPatternFlowStackFrame):
110
+ return []
111
+
112
+ blocked_flow_id = top_frame.interrupting_flow_id
113
+ frame_type = FlowStackFrameType.INTERRUPT
114
+ stack = tracker.stack
115
+ stack.push(UserFlowStackFrame(flow_id=blocked_flow_id, frame_type=frame_type))
116
+
117
+ events = [
118
+ FlowInterrupted(
119
+ flow_id=top_frame.interrupted_flow_id,
120
+ step_id=top_frame.interrupted_step_id,
121
+ )
122
+ ] + tracker.create_stack_updated_events(stack)
123
+
124
+ utterance = "utter_continue_interruption"
125
+ message = await nlg.generate(
126
+ utterance,
127
+ tracker,
128
+ output_channel.name(),
129
+ )
130
+
131
+ if message is None:
132
+ structlogger.error(
133
+ "action_continue_digression.run.failed.finding.utter",
134
+ utterance=utterance,
135
+ )
136
+ else:
137
+ message = add_bot_utterance_metadata(
138
+ message, utterance, nlg, domain, tracker
139
+ )
140
+ events.append(create_bot_utterance(message))
141
+
142
+ return events
@@ -9,14 +9,17 @@ from rasa.core.utils import add_bot_utterance_metadata
9
9
  from rasa.dialogue_understanding.patterns.collect_information import (
10
10
  CollectInformationPatternFlowStackFrame,
11
11
  )
12
+ from rasa.dialogue_understanding.patterns.validate_slot import (
13
+ ValidateSlotPatternFlowStackFrame,
14
+ )
12
15
  from rasa.shared.core.constants import ACTION_RUN_SLOT_REJECTIONS_NAME
13
16
  from rasa.shared.core.events import Event, SlotSet
14
- from rasa.shared.core.flows.steps.collect import SlotRejection
15
17
  from rasa.shared.core.slots import (
16
18
  BooleanSlot,
17
19
  CategoricalSlot,
18
20
  FloatSlot,
19
21
  Slot,
22
+ SlotRejection,
20
23
  )
21
24
 
22
25
  if TYPE_CHECKING:
@@ -138,10 +141,19 @@ class ActionRunSlotRejections(Action):
138
141
  """Run the predicate checks."""
139
142
  utterance = None
140
143
  top_frame = tracker.stack.top()
141
- if not isinstance(top_frame, CollectInformationPatternFlowStackFrame):
144
+ if not isinstance(
145
+ top_frame,
146
+ (
147
+ CollectInformationPatternFlowStackFrame,
148
+ ValidateSlotPatternFlowStackFrame,
149
+ ),
150
+ ):
142
151
  return []
152
+ elif isinstance(top_frame, CollectInformationPatternFlowStackFrame):
153
+ slot_name = top_frame.collect
154
+ elif isinstance(top_frame, ValidateSlotPatternFlowStackFrame):
155
+ slot_name = top_frame.validate
143
156
 
144
- slot_name = top_frame.collect
145
157
  slot_instance = tracker.slots.get(slot_name)
146
158
  if slot_instance and not slot_instance.has_been_set:
147
159
  # this is the first time the assistant asks for the slot value,
@@ -205,6 +217,6 @@ class ActionRunSlotRejections(Action):
205
217
  message = add_bot_utterance_metadata(
206
218
  message, utterance, nlg, domain, tracker
207
219
  )
208
- events.append(create_bot_utterance(message))
220
+ events.append(create_bot_utterance(message, tracker.current_language))
209
221
 
210
222
  return events
@@ -2,7 +2,7 @@ import copy
2
2
  import itertools
3
3
  import json
4
4
  import logging
5
- from typing import Any, Dict, List, Optional, Set, Text, Union
5
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Text, Union
6
6
 
7
7
  import structlog
8
8
 
@@ -16,7 +16,7 @@ from rasa.shared.constants import UTTER_PREFIX
16
16
  from rasa.shared.core.constants import (
17
17
  ACTION_EXTRACT_SLOTS,
18
18
  ACTION_LISTEN_NAME,
19
- MAPPING_TYPE,
19
+ KEY_MAPPING_TYPE,
20
20
  REQUESTED_SLOT,
21
21
  SLOT_MAPPINGS,
22
22
  SlotMappingType,
@@ -35,6 +35,9 @@ from rasa.shared.core.slots import ListSlot
35
35
  from rasa.shared.core.trackers import DialogueStateTracker
36
36
  from rasa.utils.endpoints import EndpointConfig
37
37
 
38
+ if TYPE_CHECKING:
39
+ from rasa.shared.core.slot_mappings import SlotMapping
40
+
38
41
  logger = logging.getLogger(__name__)
39
42
  structlogger = structlog.get_logger()
40
43
 
@@ -158,7 +161,9 @@ class FormAction(LoopAction):
158
161
  domain_slots = domain.as_dict().get(KEY_SLOTS, {})
159
162
  for slot in domain.required_slots_for_form(self.name()):
160
163
  for slot_mapping in domain_slots.get(slot, {}).get(SLOT_MAPPINGS, []):
161
- if slot_mapping.get(MAPPING_TYPE) == str(SlotMappingType.FROM_ENTITY):
164
+ if slot_mapping.get(KEY_MAPPING_TYPE) == str(
165
+ SlotMappingType.FROM_ENTITY
166
+ ):
162
167
  mapping_as_string = json.dumps(slot_mapping, sort_keys=True)
163
168
  if mapping_as_string in unique_entity_slot_mappings:
164
169
  unique_entity_slot_mappings.remove(mapping_as_string)
@@ -169,7 +174,7 @@ class FormAction(LoopAction):
169
174
  return unique_entity_slot_mappings
170
175
 
171
176
  def entity_mapping_is_unique(
172
- self, slot_mapping: Dict[Text, Any], domain: Domain
177
+ self, slot_mapping: "SlotMapping", domain: Domain
173
178
  ) -> bool:
174
179
  """Verifies if the from_entity mapping is unique."""
175
180
  if not self._have_unique_entity_mappings_been_initialized:
@@ -177,7 +182,7 @@ class FormAction(LoopAction):
177
182
  self._unique_entity_mappings = self._create_unique_entity_mappings(domain)
178
183
  self._have_unique_entity_mappings_been_initialized = True
179
184
 
180
- mapping_as_string = json.dumps(slot_mapping, sort_keys=True)
185
+ mapping_as_string = json.dumps(slot_mapping.as_dict(), sort_keys=True)
181
186
  return mapping_as_string in self._unique_entity_mappings
182
187
 
183
188
  @staticmethod
@@ -32,7 +32,9 @@ from rasa.core.channels.vier_cvg import CVGInput
32
32
  from rasa.core.channels.voice_stream.twilio_media_streams import (
33
33
  TwilioMediaStreamsInputChannel,
34
34
  )
35
+ from rasa.core.channels.voice_stream.genesys import GenesysInputChannel
35
36
  from rasa.core.channels.studio_chat import StudioChatInput
37
+ from rasa.core.channels.voice_stream.audiocodes import AudiocodesVoiceInputChannel
36
38
 
37
39
  input_channel_classes: List[Type[InputChannel]] = [
38
40
  CmdlineInput,
@@ -55,7 +57,9 @@ input_channel_classes: List[Type[InputChannel]] = [
55
57
  JambonzVoiceReadyInput,
56
58
  TwilioMediaStreamsInputChannel,
57
59
  BrowserAudioInputChannel,
60
+ GenesysInputChannel,
58
61
  StudioChatInput,
62
+ AudiocodesVoiceInputChannel,
59
63
  ]
60
64
 
61
65
  # Mapping from an input channel name to its class to allow name based lookup.
@@ -18,6 +18,8 @@ from sanic import Sanic
18
18
  from rasa.core.channels.socketio import SocketBlueprint, SocketIOInput
19
19
  from rasa.hooks import hookimpl
20
20
  from rasa.plugin import plugin_manager
21
+ from rasa.shared.core.constants import ACTION_LISTEN_NAME
22
+ from rasa.shared.core.events import ActionExecuted
21
23
  from rasa.shared.core.trackers import EventVerbosity
22
24
 
23
25
  if TYPE_CHECKING:
@@ -43,6 +45,15 @@ def tracker_as_dump(tracker: "DialogueStateTracker") -> str:
43
45
  return json.dumps(state)
44
46
 
45
47
 
48
+ def does_need_action_prediction(tracker: "DialogueStateTracker") -> bool:
49
+ """Check if the tracker needs an action prediction."""
50
+ return (
51
+ len(tracker.events) == 0
52
+ or not isinstance(tracker.events[-1], ActionExecuted)
53
+ or tracker.events[-1].action_name != ACTION_LISTEN_NAME
54
+ )
55
+
56
+
46
57
  class StudioTrackerUpdatePlugin:
47
58
  """Plugin for publishing tracker updates a socketio channel."""
48
59
 
@@ -169,6 +180,14 @@ class StudioChatInput(SocketIOInput):
169
180
  # will override an existing tracker with the same id!
170
181
  await self.agent.tracker_store.save(tracker)
171
182
 
183
+ processor = self.agent.processor
184
+ if processor and does_need_action_prediction(tracker):
185
+ output_channel = self.get_output_channel()
186
+
187
+ await processor._run_prediction_loop(output_channel, tracker)
188
+ await processor.run_anonymization_pipeline(tracker)
189
+ await self.agent.tracker_store.save(tracker)
190
+
172
191
  await self.on_tracker_updated(tracker)
173
192
 
174
193
  def blueprint(
@@ -1,21 +1,9 @@
1
1
  import json
2
2
  import logging
3
+ import typing
3
4
  from copy import deepcopy
4
5
  from typing import Any, Awaitable, Callable, Dict, List, Optional, Text
5
6
 
6
- from aiogram import Bot
7
- from aiogram.exceptions import TelegramAPIError
8
- from aiogram.types import (
9
- InlineKeyboardButton,
10
- KeyboardButton,
11
- Message,
12
- Update,
13
- )
14
- from aiogram.utils.keyboard import (
15
- InlineKeyboardBuilder,
16
- KeyboardBuilder,
17
- ReplyKeyboardBuilder,
18
- )
19
7
  from sanic import Blueprint, response
20
8
  from sanic.request import Request
21
9
  from sanic.response import HTTPResponse
@@ -27,8 +15,11 @@ from rasa.shared.exceptions import RasaException
27
15
 
28
16
  logger = logging.getLogger(__name__)
29
17
 
18
+ if typing.TYPE_CHECKING:
19
+ from aiogram.types import Message, Update
30
20
 
31
- class TelegramOutput(Bot, OutputChannel):
21
+
22
+ class TelegramOutput(OutputChannel):
32
23
  """Output channel for Telegram."""
33
24
 
34
25
  # skipcq: PYL-W0236
@@ -37,20 +28,28 @@ class TelegramOutput(Bot, OutputChannel):
37
28
  return "telegram"
38
29
 
39
30
  def __init__(self, access_token: Optional[Text]) -> None:
40
- Bot.__init__(self, access_token)
31
+ try:
32
+ from aiogram import Bot
33
+
34
+ self.bot = Bot(access_token)
35
+ except ImportError:
36
+ raise ImportError(
37
+ "To use the Telegram channel, please install the aiogram package "
38
+ "with 'pip install aiogram'"
39
+ )
41
40
 
42
41
  async def send_text_message(
43
42
  self, recipient_id: Text, text: Text, **kwargs: Any
44
43
  ) -> None:
45
44
  """Sends text message."""
46
45
  for message_part in text.strip().split("\n\n"):
47
- await self.send_message(recipient_id, message_part)
46
+ await self.bot.send_message(recipient_id, message_part)
48
47
 
49
48
  async def send_image_url(
50
49
  self, recipient_id: Text, image: Text, **kwargs: Any
51
50
  ) -> None:
52
51
  """Sends an image."""
53
- await self.send_photo(recipient_id, image)
52
+ await self.bot.send_photo(recipient_id, image)
54
53
 
55
54
  async def send_text_with_buttons(
56
55
  self,
@@ -70,8 +69,15 @@ class TelegramOutput(Bot, OutputChannel):
70
69
 
71
70
  :button_type reply: reply keyboard
72
71
  """
72
+ from aiogram.types import InlineKeyboardButton, KeyboardButton
73
+ from aiogram.utils.keyboard import (
74
+ InlineKeyboardBuilder,
75
+ KeyboardBuilder,
76
+ ReplyKeyboardBuilder,
77
+ )
78
+
73
79
  if button_type == "inline":
74
- reply_markup_builder: KeyboardBuilder = InlineKeyboardBuilder()
80
+ reply_markup_builder: "KeyboardBuilder" = InlineKeyboardBuilder()
75
81
  button_list = [
76
82
  InlineKeyboardButton(text=s["title"], callback_data=s["payload"])
77
83
  for s in buttons
@@ -110,7 +116,7 @@ class TelegramOutput(Bot, OutputChannel):
110
116
  )
111
117
  return
112
118
 
113
- await self.send_message(recipient_id, text, reply_markup=reply_markup)
119
+ await self.bot.send_message(recipient_id, text, reply_markup=reply_markup)
114
120
 
115
121
  async def send_custom_json(
116
122
  self, recipient_id: Text, json_message: Dict[Text, Any], **kwargs: Any
@@ -150,9 +156,17 @@ class TelegramOutput(Bot, OutputChannel):
150
156
  for params in send_functions.keys():
151
157
  if all(json_message.get(p) is not None for p in params):
152
158
  args = [json_message.pop(p) for p in params]
153
- api_call = getattr(self, send_functions[params])
159
+ api_call = getattr(self.bot, send_functions[params])
154
160
  await api_call(recipient_id, *args, **json_message)
155
161
 
162
+ async def get_me(self) -> Any:
163
+ """Get information about the bot itself."""
164
+ return await self.bot.get_me()
165
+
166
+ async def set_webhook(self, url: Text) -> None:
167
+ """Set the webhook URL for telegram."""
168
+ await self.bot.set_webhook(url=url)
169
+
156
170
 
157
171
  class TelegramInput(InputChannel):
158
172
  """Telegram input channel."""
@@ -185,19 +199,19 @@ class TelegramInput(InputChannel):
185
199
  self.debug_mode = debug_mode
186
200
 
187
201
  @staticmethod
188
- def _is_location(message: Message) -> bool:
202
+ def _is_location(message: "Message") -> bool:
189
203
  return message.location is not None
190
204
 
191
205
  @staticmethod
192
- def _is_user_message(message: Message) -> bool:
206
+ def _is_user_message(message: "Message") -> bool:
193
207
  return message.text is not None
194
208
 
195
209
  @staticmethod
196
- def _is_edited_message(message: Update) -> bool:
210
+ def _is_edited_message(message: "Update") -> bool:
197
211
  return message.edited_message is not None
198
212
 
199
213
  @staticmethod
200
- def _is_button(message: Update) -> bool:
214
+ def _is_button(message: "Update") -> bool:
201
215
  return message.callback_query is not None
202
216
 
203
217
  def blueprint(
@@ -223,6 +237,8 @@ class TelegramInput(InputChannel):
223
237
 
224
238
  @telegram_webhook.route("/webhook", methods=["GET", "POST"])
225
239
  async def message(request: Request) -> Any:
240
+ from aiogram.types import Update
241
+
226
242
  if request.method == "POST":
227
243
  request_dict = request.json
228
244
  if isinstance(request_dict, Text):
@@ -322,6 +338,8 @@ class TelegramInput(InputChannel):
322
338
  return TelegramOutput(self.access_token)
323
339
 
324
340
  async def set_webhook(self, channel: TelegramOutput) -> None:
341
+ from aiogram.exceptions import TelegramAPIError
342
+
325
343
  try:
326
344
  await channel.set_webhook(url=self.webhook_url)
327
345
  except TelegramAPIError as error:
@@ -1,9 +1,11 @@
1
+ import asyncio
1
2
  import copy
2
3
  import json
3
4
  import uuid
5
+ from collections import defaultdict
4
6
  from dataclasses import asdict
5
7
  from datetime import datetime, timedelta, timezone
6
- from typing import Any, Awaitable, Callable, Dict, List, Optional, Text, Union
8
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Text, Union
7
9
 
8
10
  import structlog
9
11
  from jsonschema import ValidationError, validate
@@ -223,6 +225,16 @@ class AudiocodesInput(InputChannel):
223
225
  self.scheduler_job = None
224
226
  self.keep_alive = keep_alive
225
227
  self.keep_alive_expiration_factor = keep_alive_expiration_factor
228
+ self.background_tasks: Dict[Text, Set[asyncio.Task]] = defaultdict(set)
229
+
230
+ def _create_task(self, conversation_id: Text, coro: Awaitable[Any]) -> asyncio.Task:
231
+ """Create and track an asyncio task for a conversation."""
232
+ task: asyncio.Task = asyncio.create_task(coro)
233
+ self.background_tasks[conversation_id].add(task)
234
+ task.add_done_callback(
235
+ lambda t: self.background_tasks[conversation_id].discard(t)
236
+ )
237
+ return task
226
238
 
227
239
  async def _set_scheduler_job(self) -> None:
228
240
  if self.scheduler_job:
@@ -251,11 +263,20 @@ class AudiocodesInput(InputChannel):
251
263
  )
252
264
  now = datetime.now(timezone.utc)
253
265
  delta = timedelta(seconds=self.keep_alive * self.keep_alive_expiration_factor)
254
- self.conversations = {
255
- k: v
256
- for k, v in self.conversations.items()
257
- if v.is_active_conversation(now, delta)
258
- }
266
+
267
+ # clean up conversations
268
+ inactive = [
269
+ conv_id
270
+ for conv_id, conv in self.conversations.items()
271
+ if not conv.is_active_conversation(now, delta)
272
+ ]
273
+
274
+ # cancel tasks and remove conversations
275
+ for conv_id in inactive:
276
+ for task in self.background_tasks[conv_id]:
277
+ task.cancel()
278
+ self.background_tasks.pop(conv_id, None)
279
+ self.conversations.pop(conv_id, None)
259
280
 
260
281
  def handle_start_conversation(self, body: Dict[Text, Any]) -> Dict[Text, Any]:
261
282
  conversation_id = body["conversation"]
@@ -347,31 +368,29 @@ class AudiocodesInput(InputChannel):
347
368
  structlogger.debug("audiocodes.on_activities", conversation=conversation_id)
348
369
  conversation = self._get_conversation(request.token, conversation_id)
349
370
  if conversation is None:
371
+ structlogger.warning(
372
+ "audiocodes.on_activities.no_conversation", request=request.json
373
+ )
350
374
  return response.json({})
351
375
  elif conversation.ws:
352
376
  ac_output: Union[WebsocketOutput, AudiocodesOutput] = WebsocketOutput(
353
377
  conversation.ws, conversation_id
354
378
  )
355
- await conversation.handle_activities(
356
- request.json,
357
- output_channel=ac_output,
358
- on_new_message=on_new_message,
359
- )
360
- return response.json({})
379
+ response_json = {}
361
380
  else:
362
381
  # handle non websocket case where messages get returned in json
363
382
  ac_output = AudiocodesOutput()
364
- await conversation.handle_activities(
365
- request.json,
366
- output_channel=ac_output,
367
- on_new_message=on_new_message,
368
- )
369
- return response.json(
370
- {
371
- "conversation": conversation_id,
372
- "activities": ac_output.messages,
373
- }
374
- )
383
+ response_json = {
384
+ "conversation": conversation_id,
385
+ "activities": ac_output.messages,
386
+ }
387
+
388
+ # start a background task to handle activities
389
+ self._create_task(
390
+ conversation_id,
391
+ conversation.handle_activities(request.json, ac_output, on_new_message),
392
+ )
393
+ return response.json(response_json)
375
394
 
376
395
  @ac_webhook.route(
377
396
  "/conversation/<conversation_id>/disconnect", methods=["POST"]
@@ -29,7 +29,7 @@ class CallParameters:
29
29
 
30
30
  call_id: str
31
31
  user_phone: str
32
- bot_phone: str
32
+ bot_phone: Optional[str] = None
33
33
  user_name: Optional[str] = None
34
34
  user_host: Optional[str] = None
35
35
  bot_host: Optional[str] = None
@@ -10,6 +10,7 @@ from typing import (
10
10
  TypeVar,
11
11
  )
12
12
 
13
+ import structlog
13
14
  from websockets.legacy.client import WebSocketClientProtocol
14
15
 
15
16
  from rasa.core.channels.voice_stream.asr.asr_event import ASREvent
@@ -20,6 +21,7 @@ from rasa.shared.utils.common import validate_environment
20
21
 
21
22
  T = TypeVar("T", bound="ASREngineConfig")
22
23
  E = TypeVar("E", bound="ASREngine")
24
+ logger = structlog.get_logger(__name__)
23
25
 
24
26
 
25
27
  @dataclass
@@ -74,10 +76,14 @@ class ASREngine(Generic[T]):
74
76
  """Stream the events returned by the ASR system as it is fed audio bytes."""
75
77
  if self.asr_socket is None:
76
78
  raise ConnectionException("Websocket not connected.")
77
- async for message in self.asr_socket:
78
- asr_event = self.engine_event_to_asr_event(message)
79
- if asr_event:
80
- yield asr_event
79
+
80
+ try:
81
+ async for message in self.asr_socket:
82
+ asr_event = self.engine_event_to_asr_event(message)
83
+ if asr_event:
84
+ yield asr_event
85
+ except Exception as e:
86
+ logger.warning(f"Error while streaming ASR events: {e}")
81
87
 
82
88
  def engine_event_to_asr_event(self, e: Any) -> Optional[ASREvent]:
83
89
  """Translate an engine event to a common ASREvent."""
@@ -18,6 +18,8 @@ from rasa.shared.exceptions import ConnectionException
18
18
  class AzureASRConfig(ASREngineConfig):
19
19
  language: Optional[str] = None
20
20
  speech_region: Optional[str] = None
21
+ speech_host: Optional[str] = None
22
+ speech_endpoint: Optional[str] = None
21
23
 
22
24
 
23
25
  class AzureASR(ASREngine[AzureASRConfig]):
@@ -52,9 +54,18 @@ class AzureASR(ASREngine[AzureASRConfig]):
52
54
  async def connect(self) -> None:
53
55
  import azure.cognitiveservices.speech as speechsdk
54
56
 
57
+ # connecting to eastus by default
58
+ if (
59
+ self.config.speech_region is None
60
+ and self.config.speech_host is None
61
+ and self.config.speech_endpoint is None
62
+ ):
63
+ self.config.speech_region = "eastus"
55
64
  speech_config = speechsdk.SpeechConfig(
56
65
  subscription=os.environ[AZURE_SPEECH_API_KEY_ENV_VAR],
57
66
  region=self.config.speech_region,
67
+ endpoint=self.config.speech_endpoint,
68
+ host=self.config.speech_host,
58
69
  )
59
70
  audio_format = speechsdk.audio.AudioStreamFormat(
60
71
  samples_per_second=HERTZ,
@@ -123,7 +134,9 @@ class AzureASR(ASREngine[AzureASRConfig]):
123
134
 
124
135
  @staticmethod
125
136
  def get_default_config() -> AzureASRConfig:
126
- return AzureASRConfig("en-US", "eastus")
137
+ return AzureASRConfig(
138
+ language=None, speech_region=None, speech_host=None, speech_endpoint=None
139
+ )
127
140
 
128
141
  @classmethod
129
142
  def from_config_dict(cls, config: Dict) -> "AzureASR":