rasa-pro 3.12.0.dev12__py3-none-any.whl → 3.12.0.dev13__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 (71) hide show
  1. rasa/cli/inspect.py +20 -1
  2. rasa/cli/shell.py +3 -3
  3. rasa/core/actions/action.py +20 -7
  4. rasa/core/actions/action_handle_digressions.py +142 -0
  5. rasa/core/actions/forms.py +10 -5
  6. rasa/core/channels/__init__.py +2 -0
  7. rasa/core/channels/voice_ready/audiocodes.py +42 -23
  8. rasa/core/channels/voice_stream/browser_audio.py +1 -0
  9. rasa/core/channels/voice_stream/call_state.py +7 -1
  10. rasa/core/channels/voice_stream/genesys.py +331 -0
  11. rasa/core/channels/voice_stream/tts/azure.py +2 -1
  12. rasa/core/channels/voice_stream/tts/cartesia.py +16 -3
  13. rasa/core/channels/voice_stream/twilio_media_streams.py +2 -1
  14. rasa/core/channels/voice_stream/voice_channel.py +2 -1
  15. rasa/core/migrate.py +2 -2
  16. rasa/core/policies/flows/flow_executor.py +36 -42
  17. rasa/core/run.py +4 -3
  18. rasa/dialogue_understanding/commands/can_not_handle_command.py +2 -2
  19. rasa/dialogue_understanding/commands/cancel_flow_command.py +62 -4
  20. rasa/dialogue_understanding/commands/change_flow_command.py +2 -2
  21. rasa/dialogue_understanding/commands/chit_chat_answer_command.py +2 -2
  22. rasa/dialogue_understanding/commands/clarify_command.py +2 -2
  23. rasa/dialogue_understanding/commands/correct_slots_command.py +11 -2
  24. rasa/dialogue_understanding/commands/handle_digressions_command.py +150 -0
  25. rasa/dialogue_understanding/commands/human_handoff_command.py +2 -2
  26. rasa/dialogue_understanding/commands/knowledge_answer_command.py +2 -2
  27. rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +2 -2
  28. rasa/dialogue_understanding/commands/set_slot_command.py +7 -15
  29. rasa/dialogue_understanding/commands/skip_question_command.py +2 -2
  30. rasa/dialogue_understanding/commands/start_flow_command.py +43 -2
  31. rasa/dialogue_understanding/commands/utils.py +1 -1
  32. rasa/dialogue_understanding/constants.py +1 -0
  33. rasa/dialogue_understanding/generator/command_generator.py +110 -73
  34. rasa/dialogue_understanding/generator/command_parser.py +1 -1
  35. rasa/dialogue_understanding/generator/llm_based_command_generator.py +161 -3
  36. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +10 -2
  37. rasa/dialogue_understanding/generator/nlu_command_adapter.py +44 -3
  38. rasa/dialogue_understanding/generator/single_step/command_prompt_template.jinja2 +53 -79
  39. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +11 -19
  40. rasa/dialogue_understanding/generator/utils.py +32 -1
  41. rasa/dialogue_understanding/patterns/correction.py +13 -1
  42. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +62 -2
  43. rasa/dialogue_understanding/patterns/handle_digressions.py +81 -0
  44. rasa/dialogue_understanding/processor/command_processor.py +115 -28
  45. rasa/dialogue_understanding/utils.py +31 -0
  46. rasa/dialogue_understanding_test/README.md +50 -0
  47. rasa/dialogue_understanding_test/test_case_simulation/test_case_tracker_simulator.py +3 -3
  48. rasa/model_manager/warm_rasa_process.py +0 -1
  49. rasa/model_training.py +24 -27
  50. rasa/shared/core/constants.py +28 -3
  51. rasa/shared/core/domain.py +13 -20
  52. rasa/shared/core/events.py +13 -2
  53. rasa/shared/core/flows/flow.py +17 -0
  54. rasa/shared/core/flows/flows_yaml_schema.json +38 -0
  55. rasa/shared/core/flows/steps/collect.py +18 -1
  56. rasa/shared/core/flows/utils.py +16 -1
  57. rasa/shared/core/slot_mappings.py +144 -108
  58. rasa/shared/core/slots.py +23 -2
  59. rasa/shared/core/trackers.py +3 -1
  60. rasa/shared/nlu/constants.py +1 -0
  61. rasa/shared/utils/llm.py +1 -1
  62. rasa/shared/utils/schemas/domain.yml +0 -1
  63. rasa/telemetry.py +43 -13
  64. rasa/utils/common.py +0 -1
  65. rasa/validator.py +189 -82
  66. rasa/version.py +1 -1
  67. {rasa_pro-3.12.0.dev12.dist-info → rasa_pro-3.12.0.dev13.dist-info}/METADATA +1 -1
  68. {rasa_pro-3.12.0.dev12.dist-info → rasa_pro-3.12.0.dev13.dist-info}/RECORD +71 -67
  69. {rasa_pro-3.12.0.dev12.dist-info → rasa_pro-3.12.0.dev13.dist-info}/NOTICE +0 -0
  70. {rasa_pro-3.12.0.dev12.dist-info → rasa_pro-3.12.0.dev13.dist-info}/WHEEL +0 -0
  71. {rasa_pro-3.12.0.dev12.dist-info → rasa_pro-3.12.0.dev13.dist-info}/entry_points.txt +0 -0
rasa/cli/inspect.py CHANGED
@@ -9,6 +9,10 @@ from rasa import telemetry
9
9
  from rasa.cli import SubParsersAction
10
10
  from rasa.cli.arguments import shell as arguments
11
11
  from rasa.core import constants
12
+ from rasa.engine.storage.local_model_storage import LocalModelStorage
13
+ from rasa.exceptions import ModelNotFound
14
+ from rasa.model import get_local_model
15
+ from rasa.shared.utils.cli import print_error
12
16
  from rasa.utils.cli import remove_argument_from_parser
13
17
 
14
18
 
@@ -55,6 +59,8 @@ async def open_inspector_in_browser(server_url: Text, voice: bool = False) -> No
55
59
  def inspect(args: argparse.Namespace) -> None:
56
60
  """Inspect the bot using the most recent model."""
57
61
  import rasa.cli.run
62
+ from rasa.cli.utils import get_validated_path
63
+ from rasa.shared.constants import DEFAULT_MODELS_PATH
58
64
 
59
65
  async def after_start_hook_open_inspector(_: Sanic, __: AbstractEventLoop) -> None:
60
66
  """Hook to open the browser on server start."""
@@ -71,5 +77,18 @@ def inspect(args: argparse.Namespace) -> None:
71
77
  args.credentials = None
72
78
  args.server_listeners = [(after_start_hook_open_inspector, "after_server_start")]
73
79
 
74
- telemetry.track_inspect_started(args.connector)
80
+ model = get_validated_path(args.model, "model", DEFAULT_MODELS_PATH)
81
+
82
+ try:
83
+ model = get_local_model(model)
84
+ except ModelNotFound:
85
+ print_error(
86
+ "No model found. Train a model before running the "
87
+ "server using `rasa train`."
88
+ )
89
+ return
90
+
91
+ metadata = LocalModelStorage.metadata_from_archive(model)
92
+
93
+ telemetry.track_inspect_started(args.connector, metadata.assistant_id)
75
94
  rasa.cli.run.run(args)
rasa/cli/shell.py CHANGED
@@ -95,7 +95,7 @@ def shell_nlu(args: argparse.Namespace) -> None:
95
95
  )
96
96
  return
97
97
 
98
- telemetry.track_shell_started("nlu")
98
+ telemetry.track_shell_started("nlu", metadata.assistant_id)
99
99
  rasa.nlu.run.run_cmdline(model)
100
100
 
101
101
 
@@ -129,12 +129,12 @@ def shell(args: argparse.Namespace) -> None:
129
129
  if metadata.training_type == TrainingType.NLU:
130
130
  import rasa.nlu.run
131
131
 
132
- telemetry.track_shell_started("nlu")
132
+ telemetry.track_shell_started("nlu", metadata.assistant_id)
133
133
 
134
134
  rasa.nlu.run.run_cmdline(model)
135
135
  else:
136
136
  import rasa.cli.run
137
137
 
138
- telemetry.track_shell_started("rasa")
138
+ telemetry.track_shell_started("rasa", metadata.assistant_id)
139
139
 
140
140
  rasa.cli.run.run(args)
@@ -73,9 +73,9 @@ from rasa.shared.core.constants import (
73
73
  ACTION_VALIDATE_SLOT_MAPPINGS,
74
74
  DEFAULT_SLOT_NAMES,
75
75
  KNOWLEDGE_BASE_SLOT_NAMES,
76
- MAPPING_TYPE,
77
76
  REQUESTED_SLOT,
78
77
  USER_INTENT_OUT_OF_SCOPE,
78
+ SetSlotExtractor,
79
79
  SlotMappingType,
80
80
  )
81
81
  from rasa.shared.core.domain import Domain
@@ -111,6 +111,7 @@ if TYPE_CHECKING:
111
111
  from rasa.core.channels.channel import OutputChannel
112
112
  from rasa.core.nlg import NaturalLanguageGenerator
113
113
  from rasa.shared.core.events import IntentPrediction
114
+ from rasa.shared.core.slot_mappings import SlotMapping
114
115
 
115
116
  logger = logging.getLogger(__name__)
116
117
 
@@ -118,6 +119,10 @@ logger = logging.getLogger(__name__)
118
119
  def default_actions(action_endpoint: Optional[EndpointConfig] = None) -> List["Action"]:
119
120
  """List default actions."""
120
121
  from rasa.core.actions.action_clean_stack import ActionCleanStack
122
+ from rasa.core.actions.action_handle_digressions import (
123
+ ActionBlockDigressions,
124
+ ActionContinueDigression,
125
+ )
121
126
  from rasa.core.actions.action_hangup import ActionHangup
122
127
  from rasa.core.actions.action_repeat_bot_messages import ActionRepeatBotMessages
123
128
  from rasa.core.actions.action_run_slot_rejections import ActionRunSlotRejections
@@ -152,6 +157,8 @@ def default_actions(action_endpoint: Optional[EndpointConfig] = None) -> List["A
152
157
  ActionResetRouting(),
153
158
  ActionHangup(),
154
159
  ActionRepeatBotMessages(),
160
+ ActionBlockDigressions(),
161
+ ActionContinueDigression(),
155
162
  ]
156
163
 
157
164
 
@@ -940,7 +947,14 @@ class RemoteAction(Action):
940
947
  )
941
948
 
942
949
  events = rasa.shared.core.events.deserialise_events(events_json)
943
- return cast(List[Event], bot_messages) + events
950
+
951
+ processed_events = []
952
+ for event in events:
953
+ if isinstance(event, SlotSet) and event.filled_by is None:
954
+ event.filled_by = SetSlotExtractor.CUSTOM.value
955
+ processed_events.append(event)
956
+
957
+ return cast(List[Event], bot_messages) + processed_events
944
958
 
945
959
  def name(self) -> Text:
946
960
  return self._name
@@ -1208,7 +1222,7 @@ class ActionExtractSlots(Action):
1208
1222
 
1209
1223
  async def _execute_custom_action(
1210
1224
  self,
1211
- mapping: Dict[Text, Any],
1225
+ mapping: "SlotMapping",
1212
1226
  executed_custom_actions: Set[Text],
1213
1227
  output_channel: "OutputChannel",
1214
1228
  nlg: "NaturalLanguageGenerator",
@@ -1216,7 +1230,7 @@ class ActionExtractSlots(Action):
1216
1230
  domain: "Domain",
1217
1231
  calm_custom_action_names: Optional[Set[str]] = None,
1218
1232
  ) -> Tuple[List[Event], Set[Text]]:
1219
- custom_action = mapping.get("action")
1233
+ custom_action = mapping.run_action_every_turn
1220
1234
 
1221
1235
  if not custom_action or custom_action in executed_custom_actions:
1222
1236
  return [], executed_custom_actions
@@ -1317,10 +1331,9 @@ class ActionExtractSlots(Action):
1317
1331
  slot_events.append(SlotSet(slot.name, slot_value))
1318
1332
 
1319
1333
  for mapping in slot.mappings:
1320
- mapping_type = SlotMappingType(mapping.get(MAPPING_TYPE))
1321
- should_fill_custom_slot = mapping_type == SlotMappingType.CUSTOM
1334
+ should_fill_controlled_slot = mapping.type == SlotMappingType.CONTROLLED
1322
1335
 
1323
- if should_fill_custom_slot:
1336
+ if should_fill_controlled_slot:
1324
1337
  (
1325
1338
  custom_evts,
1326
1339
  executed_custom_actions,
@@ -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
@@ -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,6 +32,7 @@ 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
36
37
 
37
38
  input_channel_classes: List[Type[InputChannel]] = [
@@ -55,6 +56,7 @@ input_channel_classes: List[Type[InputChannel]] = [
55
56
  JambonzVoiceReadyInput,
56
57
  TwilioMediaStreamsInputChannel,
57
58
  BrowserAudioInputChannel,
59
+ GenesysInputChannel,
58
60
  StudioChatInput,
59
61
  ]
60
62
 
@@ -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"]
@@ -65,6 +65,7 @@ class BrowserAudioInputChannel(VoiceInputChannel):
65
65
  def map_input_message(
66
66
  self,
67
67
  message: Any,
68
+ ws: Websocket,
68
69
  ) -> VoiceChannelAction:
69
70
  data = json.loads(message)
70
71
  if "audio" in data:
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  from contextvars import ContextVar
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass, field
4
4
  from typing import Optional
5
5
 
6
6
  from werkzeug.local import LocalProxy
@@ -19,6 +19,12 @@ class CallState:
19
19
  should_hangup: bool = False
20
20
  connection_failed: bool = False
21
21
 
22
+ # Genesys requires the server and client each maintain a
23
+ # monotonically increasing message sequence number.
24
+ client_sequence_number: int = 0
25
+ server_sequence_number: int = 0
26
+ audio_buffer: bytearray = field(default_factory=bytearray)
27
+
22
28
 
23
29
  _call_state: ContextVar[CallState] = ContextVar("call_state")
24
30
  call_state = LocalProxy(_call_state)