rasa-pro 3.12.5__py3-none-any.whl → 3.12.6__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 (35) hide show
  1. rasa/core/actions/action.py +0 -6
  2. rasa/core/channels/voice_ready/audiocodes.py +52 -17
  3. rasa/core/channels/voice_stream/audiocodes.py +53 -9
  4. rasa/core/channels/voice_stream/genesys.py +146 -16
  5. rasa/core/policies/flows/flow_executor.py +3 -38
  6. rasa/core/processor.py +19 -5
  7. rasa/core/utils.py +53 -0
  8. rasa/dialogue_understanding/commands/cancel_flow_command.py +4 -59
  9. rasa/dialogue_understanding/commands/start_flow_command.py +0 -41
  10. rasa/dialogue_understanding/generator/command_generator.py +67 -0
  11. rasa/dialogue_understanding/generator/llm_based_command_generator.py +2 -12
  12. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +0 -61
  13. rasa/dialogue_understanding/processor/command_processor.py +7 -65
  14. rasa/dialogue_understanding/stack/utils.py +0 -38
  15. rasa/shared/core/constants.py +0 -8
  16. rasa/shared/core/domain.py +12 -3
  17. rasa/shared/core/flows/flow.py +0 -17
  18. rasa/shared/core/flows/flows_yaml_schema.json +3 -38
  19. rasa/shared/core/flows/steps/collect.py +5 -18
  20. rasa/shared/core/flows/utils.py +1 -16
  21. rasa/shared/core/slot_mappings.py +23 -5
  22. rasa/shared/nlu/constants.py +0 -1
  23. rasa/shared/utils/common.py +11 -1
  24. rasa/validator.py +1 -123
  25. rasa/version.py +1 -1
  26. {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dist-info}/METADATA +4 -5
  27. {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dist-info}/RECORD +30 -35
  28. {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dist-info}/WHEEL +1 -1
  29. README.md +0 -38
  30. rasa/core/actions/action_handle_digressions.py +0 -164
  31. rasa/dialogue_understanding/commands/handle_digressions_command.py +0 -144
  32. rasa/dialogue_understanding/patterns/handle_digressions.py +0 -81
  33. rasa/keys +0 -1
  34. {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dist-info}/NOTICE +0 -0
  35. {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dist-info}/entry_points.txt +0 -0
@@ -105,10 +105,6 @@ logger = logging.getLogger(__name__)
105
105
  def default_actions(action_endpoint: Optional[EndpointConfig] = None) -> List["Action"]:
106
106
  """List default actions."""
107
107
  from rasa.core.actions.action_clean_stack import ActionCleanStack
108
- from rasa.core.actions.action_handle_digressions import (
109
- ActionBlockDigressions,
110
- ActionContinueDigression,
111
- )
112
108
  from rasa.core.actions.action_hangup import ActionHangup
113
109
  from rasa.core.actions.action_repeat_bot_messages import ActionRepeatBotMessages
114
110
  from rasa.core.actions.action_run_slot_rejections import ActionRunSlotRejections
@@ -143,8 +139,6 @@ def default_actions(action_endpoint: Optional[EndpointConfig] = None) -> List["A
143
139
  ActionResetRouting(),
144
140
  ActionHangup(),
145
141
  ActionRepeatBotMessages(),
146
- ActionBlockDigressions(),
147
- ActionContinueDigression(),
148
142
  ]
149
143
 
150
144
 
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import copy
3
+ import hmac
3
4
  import json
4
5
  import uuid
5
6
  from collections import defaultdict
@@ -114,11 +115,21 @@ class Conversation:
114
115
  async def handle_activities(
115
116
  self,
116
117
  message: Dict[Text, Any],
118
+ input_channel_name: str,
117
119
  output_channel: OutputChannel,
118
120
  on_new_message: Callable[[UserMessage], Awaitable[Any]],
119
121
  ) -> None:
120
122
  """Handle activities sent by Audiocodes."""
121
123
  structlogger.debug("audiocodes.handle.activities")
124
+ if input_channel_name == "":
125
+ structlogger.warning(
126
+ "audiocodes.handle.activities.empty_input_channel_name",
127
+ event_info=(
128
+ "Audiocodes input channel name is empty "
129
+ f"for conversation {self.conversation_id}"
130
+ ),
131
+ )
132
+
122
133
  for activity in message["activities"]:
123
134
  text = None
124
135
  if activity[ACTIVITY_ID_KEY] in self.activity_ids:
@@ -142,6 +153,7 @@ class Conversation:
142
153
  metadata = self.get_metadata(activity)
143
154
  user_msg = UserMessage(
144
155
  text=text,
156
+ input_channel=input_channel_name,
145
157
  output_channel=output_channel,
146
158
  sender_id=self.conversation_id,
147
159
  metadata=metadata,
@@ -245,8 +257,13 @@ class AudiocodesInput(InputChannel):
245
257
 
246
258
  def _check_token(self, token: Optional[Text]) -> None:
247
259
  if not token:
260
+ structlogger.error("audiocodes.token_not_provided")
248
261
  raise HttpUnauthorized("Authentication token required.")
249
262
 
263
+ if not hmac.compare_digest(str(token), str(self.token)):
264
+ structlogger.error("audiocodes.invalid_token", invalid_token=token)
265
+ raise HttpUnauthorized("Invalid authentication token.")
266
+
250
267
  def _get_conversation(
251
268
  self, token: Optional[Text], conversation_id: Text
252
269
  ) -> Conversation:
@@ -388,7 +405,12 @@ class AudiocodesInput(InputChannel):
388
405
  # start a background task to handle activities
389
406
  self._create_task(
390
407
  conversation_id,
391
- conversation.handle_activities(request.json, ac_output, on_new_message),
408
+ conversation.handle_activities(
409
+ request.json,
410
+ input_channel_name=self.name(),
411
+ output_channel=ac_output,
412
+ on_new_message=on_new_message,
413
+ ),
392
414
  )
393
415
  return response.json(response_json)
394
416
 
@@ -401,23 +423,9 @@ class AudiocodesInput(InputChannel):
401
423
  Example of payload:
402
424
  {"conversation": <conversation_id>, "reason": Optional[Text]}.
403
425
  """
404
- self._get_conversation(request.token, conversation_id)
405
- reason = {"reason": request.json.get("reason")}
406
- await on_new_message(
407
- UserMessage(
408
- text=f"{INTENT_MESSAGE_PREFIX}session_end",
409
- output_channel=None,
410
- sender_id=conversation_id,
411
- metadata=reason,
412
- )
413
- )
414
- del self.conversations[conversation_id]
415
- structlogger.debug(
416
- "audiocodes.disconnect",
417
- conversation=conversation_id,
418
- request=request.json,
426
+ return await self._handle_disconnect(
427
+ request, conversation_id, on_new_message
419
428
  )
420
- return response.json({})
421
429
 
422
430
  @ac_webhook.route("/conversation/<conversation_id>/keepalive", methods=["POST"])
423
431
  async def keepalive(request: Request, conversation_id: Text) -> HTTPResponse:
@@ -432,6 +440,32 @@ class AudiocodesInput(InputChannel):
432
440
 
433
441
  return ac_webhook
434
442
 
443
+ async def _handle_disconnect(
444
+ self,
445
+ request: Request,
446
+ conversation_id: Text,
447
+ on_new_message: Callable[[UserMessage], Awaitable[Any]],
448
+ ) -> HTTPResponse:
449
+ """Triggered when the call is disconnected."""
450
+ self._get_conversation(request.token, conversation_id)
451
+ reason = {"reason": request.json.get("reason")}
452
+ await on_new_message(
453
+ UserMessage(
454
+ text=f"{INTENT_MESSAGE_PREFIX}session_end",
455
+ input_channel=self.name(),
456
+ output_channel=None,
457
+ sender_id=conversation_id,
458
+ metadata=reason,
459
+ )
460
+ )
461
+ del self.conversations[conversation_id]
462
+ structlogger.debug(
463
+ "audiocodes.disconnect",
464
+ conversation=conversation_id,
465
+ request=request.json,
466
+ )
467
+ return response.json({})
468
+
435
469
 
436
470
  class AudiocodesOutput(OutputChannel):
437
471
  @classmethod
@@ -439,6 +473,7 @@ class AudiocodesOutput(OutputChannel):
439
473
  return CHANNEL_NAME
440
474
 
441
475
  def __init__(self) -> None:
476
+ super().__init__()
442
477
  self.messages: List[Dict] = []
443
478
 
444
479
  async def add_message(self, message: Dict) -> None:
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import base64
3
+ import hmac
3
4
  import json
4
5
  from typing import Any, Awaitable, Callable, Dict, Optional, Text
5
6
 
@@ -103,6 +104,7 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
103
104
 
104
105
  def __init__(
105
106
  self,
107
+ token: Optional[Text],
106
108
  server_url: str,
107
109
  asr_config: Dict,
108
110
  tts_config: Dict,
@@ -110,6 +112,22 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
110
112
  ):
111
113
  mark_as_beta_feature("Audiocodes (audiocodes_stream) Channel")
112
114
  super().__init__(server_url, asr_config, tts_config, monitor_silence)
115
+ self.token = token
116
+
117
+ @classmethod
118
+ def from_credentials(
119
+ cls, credentials: Optional[Dict[str, Any]]
120
+ ) -> VoiceInputChannel:
121
+ if not credentials:
122
+ raise ValueError("No credentials given for Audiocodes voice channel.")
123
+
124
+ return cls(
125
+ token=credentials.get("token"),
126
+ server_url=credentials["server_url"],
127
+ asr_config=credentials["asr"],
128
+ tts_config=credentials["tts"],
129
+ monitor_silence=credentials.get("monitor_silence", False),
130
+ )
113
131
 
114
132
  def channel_bytes_to_rasa_audio_bytes(self, input_bytes: bytes) -> RasaAudioBytes:
115
133
  return RasaAudioBytes(base64.b64decode(input_bytes))
@@ -135,6 +153,13 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
135
153
  )
136
154
  if activity["name"] == "start":
137
155
  return map_call_params(activity["parameters"])
156
+ elif data["type"] == "connection.validate":
157
+ # not part of call flow; only sent when integration is created
158
+ logger.info(
159
+ "audiocodes_stream.collect_call_parameters.connection.validate",
160
+ event_info="received request to validate integration",
161
+ )
162
+ self._send_validated(channel_websocket, data)
138
163
  else:
139
164
  logger.warning("audiocodes_stream.unknown_message", data=data)
140
165
  return None
@@ -158,7 +183,7 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
158
183
  elif activity["name"] == "playFinished":
159
184
  logger.debug("audiocodes_stream.playFinished", data=activity)
160
185
  if call_state.should_hangup:
161
- logger.info("audiocodes.hangup")
186
+ logger.info("audiocodes_stream.hangup")
162
187
  self._send_hangup(ws, data)
163
188
  # the conversation should continue until
164
189
  # we receive a end message from audiocodes
@@ -180,11 +205,10 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
180
205
  elif data["type"] == "session.end":
181
206
  logger.debug("audiocodes_stream.end", data=data)
182
207
  return EndConversationAction()
183
- elif data["type"] == "connection.validate":
184
- # not part of call flow; only sent when integration is created
185
- self._send_validated(ws, data)
186
208
  else:
187
- logger.warning("audiocodes_stream.unknown_message", data=data)
209
+ logger.warning(
210
+ "audiocodes_stream.map_input_message.unknown_message", data=data
211
+ )
188
212
 
189
213
  return ContinueConversationAction()
190
214
 
@@ -254,6 +278,17 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
254
278
  self.tts_cache,
255
279
  )
256
280
 
281
+ def _is_token_valid(self, token: Optional[Text]) -> bool:
282
+ # If no token is set, always return True
283
+ if not self.token:
284
+ return True
285
+
286
+ # Token is required, but not provided
287
+ if not token:
288
+ return False
289
+
290
+ return hmac.compare_digest(str(self.token), str(token))
291
+
257
292
  def blueprint(
258
293
  self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
259
294
  ) -> Blueprint:
@@ -266,17 +301,26 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
266
301
 
267
302
  @blueprint.websocket("/websocket") # type: ignore
268
303
  async def receive(request: Request, ws: Websocket) -> None:
269
- # TODO: validate API key header
270
- logger.info("audiocodes.receive", message="Starting audio streaming")
304
+ if not self._is_token_valid(request.token):
305
+ logger.error(
306
+ "audiocodes_stream.invalid_token",
307
+ invalid_token=request.token,
308
+ )
309
+ await ws.close(code=1008, reason="Invalid token")
310
+ return
311
+
312
+ logger.info(
313
+ "audiocodes_stream.receive", event_info="Started websocket connection"
314
+ )
271
315
  try:
272
316
  await self.run_audio_streaming(on_new_message, ws)
273
317
  except Exception as e:
274
318
  logger.exception(
275
- "audiocodes.receive",
319
+ "audiocodes_stream.receive",
276
320
  message="Error during audio streaming",
277
321
  error=e,
278
322
  )
279
- # return 500 error
323
+ await ws.close(code=1011, reason="Error during audio streaming")
280
324
  raise
281
325
 
282
326
  return blueprint
@@ -1,4 +1,7 @@
1
1
  import asyncio
2
+ import base64
3
+ import hashlib
4
+ import hmac
2
5
  import json
3
6
  from typing import Any, Awaitable, Callable, Dict, Optional, Text
4
7
 
@@ -45,6 +48,7 @@ in the documentation but observed in their example app
45
48
  https://github.com/GenesysCloudBlueprints/audioconnector-server-reference-implementation
46
49
  """
47
50
  MAXIMUM_BINARY_MESSAGE_SIZE = 64000 # 64KB
51
+ HEADER_API_KEY = "X-Api-Key"
48
52
  logger = structlog.get_logger(__name__)
49
53
 
50
54
 
@@ -86,8 +90,31 @@ class GenesysInputChannel(VoiceInputChannel):
86
90
  def name(cls) -> str:
87
91
  return "genesys"
88
92
 
89
- def __init__(self, *args: Any, **kwargs: Any) -> None:
93
+ def __init__(
94
+ self, api_key: Text, client_secret: Optional[Text], *args: Any, **kwargs: Any
95
+ ) -> None:
90
96
  super().__init__(*args, **kwargs)
97
+ self.api_key = api_key
98
+ self.client_secret = client_secret
99
+
100
+ @classmethod
101
+ def from_credentials(
102
+ cls, credentials: Optional[Dict[str, Any]]
103
+ ) -> VoiceInputChannel:
104
+ if not credentials:
105
+ raise ValueError("No credentials given for Genesys voice channel.")
106
+
107
+ if not credentials.get("api_key"):
108
+ raise ValueError("No API key given for Genesys voice channel (api_key).")
109
+
110
+ return cls(
111
+ api_key=credentials["api_key"],
112
+ client_secret=credentials.get("client_secret"),
113
+ server_url=credentials["server_url"],
114
+ asr_config=credentials["asr"],
115
+ tts_config=credentials["tts"],
116
+ monitor_silence=credentials.get("monitor_silence", False),
117
+ )
91
118
 
92
119
  def _ensure_channel_data_initialized(self) -> None:
93
120
  """Initialize Genesys-specific channel data if not already present.
@@ -273,6 +300,93 @@ class GenesysInputChannel(VoiceInputChannel):
273
300
  logger.debug("genesys.disconnect", message=message)
274
301
  _schedule_ws_task(ws.send(json.dumps(message)))
275
302
 
303
+ def _calculate_signature(self, request: Request) -> str:
304
+ """Calculate the signature using request data."""
305
+ org_id = request.headers.get("Audiohook-Organization-Id")
306
+ session_id = request.headers.get("Audiohook-Session-Id")
307
+ correlation_id = request.headers.get("Audiohook-Correlation-Id")
308
+ api_key = request.headers.get(HEADER_API_KEY)
309
+
310
+ # order of components is important!
311
+ components = [
312
+ ("@request-target", "/webhooks/genesys/websocket"),
313
+ ("audiohook-session-id", session_id),
314
+ ("audiohook-organization-id", org_id),
315
+ ("audiohook-correlation-id", correlation_id),
316
+ (HEADER_API_KEY.lower(), api_key),
317
+ ("@authority", self.server_url),
318
+ ]
319
+
320
+ # Create signature base string
321
+ signing_string = ""
322
+ for name, value in components:
323
+ signing_string += f'"{name}": {value}\n'
324
+
325
+ # Add @signature-params
326
+ signature_input = request.headers["Signature-Input"]
327
+ _, params_str = signature_input.split("=", 1)
328
+ signing_string += f'"@signature-params": {params_str}'
329
+
330
+ # Calculate the HMAC signature
331
+ key_bytes = base64.b64decode(self.client_secret)
332
+ signature = hmac.new(
333
+ key_bytes, signing_string.encode("utf-8"), hashlib.sha256
334
+ ).digest()
335
+ return base64.b64encode(signature).decode("utf-8")
336
+
337
+ async def _verify_signature(self, request: Request) -> bool:
338
+ """Verify the HTTP message signature from Genesys."""
339
+ if not self.client_secret:
340
+ logger.info(
341
+ "genesys.verify_signature.no_client_secret",
342
+ event_info="Signature verification skipped",
343
+ )
344
+ return True # Skip verification if no client secret
345
+
346
+ signature = request.headers.get("Signature")
347
+ signature_input = request.headers.get("Signature-Input")
348
+ if not signature or not signature_input:
349
+ logger.error("genesys.signature.missing_signature_header")
350
+ return False
351
+
352
+ try:
353
+ actual_signature = signature.split("=", 1)[1].strip(':"')
354
+ expected_signature = self._calculate_signature(request)
355
+ return hmac.compare_digest(
356
+ expected_signature.encode("utf-8"), actual_signature.encode("utf-8")
357
+ )
358
+ except Exception as e:
359
+ logger.exception("genesys.signature.verification_error", error=e)
360
+ return False
361
+
362
+ def _ensure_required_headers(self, request: Request) -> bool:
363
+ """Ensure required headers are present in the request."""
364
+ required_headers = [
365
+ "Audiohook-Organization-Id",
366
+ "Audiohook-Correlation-Id",
367
+ "Audiohook-Session-Id",
368
+ HEADER_API_KEY,
369
+ ]
370
+
371
+ missing_headers = [
372
+ header for header in required_headers if header not in request.headers
373
+ ]
374
+
375
+ if missing_headers:
376
+ logger.error(
377
+ "genesys.missing_required_headers",
378
+ missing_headers=missing_headers,
379
+ )
380
+ return False
381
+ return True
382
+
383
+ def _ensure_api_key(self, request: Request) -> bool:
384
+ """Ensure the API key is present in the request."""
385
+ api_key = request.headers.get(HEADER_API_KEY)
386
+ if not hmac.compare_digest(str(self.api_key), str(api_key)):
387
+ return False
388
+ return True
389
+
276
390
  def blueprint(
277
391
  self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
278
392
  ) -> Blueprint:
@@ -289,23 +403,39 @@ class GenesysInputChannel(VoiceInputChannel):
289
403
  "genesys.receive",
290
404
  audiohook_session_id=request.headers.get("audiohook-session-id"),
291
405
  )
292
- # validate required headers
293
- required_headers = [
294
- "audiohook-organization-id",
295
- "audiohook-correlation-id",
296
- "audiohook-session-id",
297
- "x-api-key",
298
- ]
299
-
300
- for header in required_headers:
301
- if header not in request.headers:
302
- await ws.close(1008, f"Missing required header: {header}")
303
- return
304
-
305
- # TODO: validate API key header
406
+
407
+ # verify signature
408
+ if not await self._verify_signature(request):
409
+ logger.error("genesys.receive.invalid_signature")
410
+ await ws.close(code=1008, reason="Invalid signature")
411
+ return
412
+
413
+ # ensure required headers are present
414
+ if not self._ensure_required_headers(request):
415
+ await ws.close(code=1002, reason="Missing required headers")
416
+ return
417
+
418
+ # ensure API key is correct
419
+ if not self._ensure_api_key(request):
420
+ logger.error(
421
+ "genesys.receive.invalid_api_key",
422
+ invalid_api_key=request.headers.get(HEADER_API_KEY),
423
+ )
424
+ await ws.close(code=1008, reason="Invalid API key")
425
+ return
426
+
306
427
  # process audio streaming
307
428
  logger.info("genesys.receive", message="Starting audio streaming")
308
- await self.run_audio_streaming(on_new_message, ws)
429
+ try:
430
+ await self.run_audio_streaming(on_new_message, ws)
431
+ except Exception as e:
432
+ logger.exception(
433
+ "genesys.receive",
434
+ message="Error during audio streaming",
435
+ error=e,
436
+ )
437
+ await ws.close(code=1011, reason="Error during audio streaming")
438
+ raise
309
439
 
310
440
  return blueprint
311
441
 
@@ -23,7 +23,6 @@ from rasa.core.policies.flows.flow_step_result import (
23
23
  )
24
24
  from rasa.dialogue_understanding.commands import CancelFlowCommand
25
25
  from rasa.dialogue_understanding.patterns.cancel import CancelPatternFlowStackFrame
26
- from rasa.dialogue_understanding.patterns.clarify import ClarifyPatternFlowStackFrame
27
26
  from rasa.dialogue_understanding.patterns.collect_information import (
28
27
  CollectInformationPatternFlowStackFrame,
29
28
  )
@@ -51,7 +50,6 @@ from rasa.dialogue_understanding.stack.frames.flow_stack_frame import (
51
50
  )
52
51
  from rasa.dialogue_understanding.stack.utils import (
53
52
  top_user_flow_frame,
54
- user_flows_on_the_stack,
55
53
  )
56
54
  from rasa.shared.constants import RASA_PATTERN_HUMAN_HANDOFF
57
55
  from rasa.shared.core.constants import (
@@ -280,33 +278,6 @@ def trigger_pattern_continue_interrupted(
280
278
  return events
281
279
 
282
280
 
283
- def trigger_pattern_clarification(
284
- current_frame: DialogueStackFrame, stack: DialogueStack, flows: FlowsList
285
- ) -> None:
286
- """Trigger the pattern to clarify which topic to continue if needed."""
287
- if not isinstance(current_frame, UserFlowStackFrame):
288
- return None
289
-
290
- if current_frame.frame_type in [
291
- FlowStackFrameType.CALL,
292
- FlowStackFrameType.INTERRUPT,
293
- ]:
294
- # we want to return to the flow that called
295
- # the current flow or the flow that was interrupted
296
- # by the current flow
297
- return None
298
-
299
- pending_flows = [
300
- flows.flow_by_id(frame.flow_id)
301
- for frame in stack.frames
302
- if isinstance(frame, UserFlowStackFrame)
303
- and frame.flow_id != current_frame.flow_id
304
- ]
305
-
306
- flow_names = [flow.readable_name() for flow in pending_flows if flow is not None]
307
- stack.push(ClarifyPatternFlowStackFrame(names=flow_names))
308
-
309
-
310
281
  def trigger_pattern_completed(
311
282
  current_frame: DialogueStackFrame, stack: DialogueStack, flows: FlowsList
312
283
  ) -> None:
@@ -675,15 +646,9 @@ def _run_end_step(
675
646
  structlogger.debug("flow.step.run.flow_end")
676
647
  current_frame = stack.pop()
677
648
  trigger_pattern_completed(current_frame, stack, flows)
678
- resumed_events = []
679
- if len(user_flows_on_the_stack(stack)) > 1:
680
- # if there are more user flows on the stack,
681
- # we need to trigger the pattern clarify
682
- trigger_pattern_clarification(current_frame, stack, flows)
683
- else:
684
- resumed_events = trigger_pattern_continue_interrupted(
685
- current_frame, stack, flows, tracker
686
- )
649
+ resumed_events = trigger_pattern_continue_interrupted(
650
+ current_frame, stack, flows, tracker
651
+ )
687
652
  reset_events: List[Event] = reset_scoped_slots(current_frame, flow, tracker)
688
653
  return ContinueFlowWithNextStep(
689
654
  events=initial_events + reset_events + resumed_events, has_flow_ended=True
rasa/core/processor.py CHANGED
@@ -76,6 +76,7 @@ from rasa.shared.core.constants import (
76
76
  SLOT_SILENCE_TIMEOUT,
77
77
  USER_INTENT_RESTART,
78
78
  USER_INTENT_SILENCE_TIMEOUT,
79
+ SetSlotExtractor,
79
80
  )
80
81
  from rasa.shared.core.events import (
81
82
  ActionExecuted,
@@ -766,13 +767,26 @@ class MessageProcessor:
766
767
  if self.http_interpreter:
767
768
  parse_data = await self.http_interpreter.parse(message)
768
769
  else:
769
- regex_reader = create_regex_pattern_reader(message, self.domain)
770
-
771
770
  processed_message = Message({TEXT: message.text})
772
- if regex_reader:
773
- processed_message = regex_reader.unpack_regex_message(
774
- message=processed_message, domain=self.domain
771
+
772
+ all_flows = await self.get_flows()
773
+ should_force_slot_command, slot_name = (
774
+ rasa.core.utils.should_force_slot_filling(tracker, all_flows)
775
+ )
776
+
777
+ if should_force_slot_command:
778
+ command = SetSlotCommand(
779
+ name=slot_name,
780
+ value=message.text,
781
+ extractor=SetSlotExtractor.COMMAND_PAYLOAD_READER.value,
775
782
  )
783
+ processed_message.set(COMMANDS, [command.as_dict()], add_to_output=True)
784
+ else:
785
+ regex_reader = create_regex_pattern_reader(message, self.domain)
786
+ if regex_reader:
787
+ processed_message = regex_reader.unpack_regex_message(
788
+ message=processed_message, domain=self.domain
789
+ )
776
790
 
777
791
  # Invalid use of slash syntax, sanitize the message before passing
778
792
  # it to the graph
rasa/core/utils.py CHANGED
@@ -19,6 +19,7 @@ from rasa.core.constants import (
19
19
  )
20
20
  from rasa.core.lock_store import InMemoryLockStore, LockStore, RedisLockStore
21
21
  from rasa.shared.constants import DEFAULT_ENDPOINTS_PATH, TCP_PROTOCOL
22
+ from rasa.shared.core.constants import SlotMappingType
22
23
  from rasa.shared.core.trackers import DialogueStateTracker
23
24
  from rasa.utils.endpoints import (
24
25
  EndpointConfig,
@@ -30,6 +31,7 @@ from rasa.utils.io import write_yaml
30
31
  if TYPE_CHECKING:
31
32
  from rasa.core.nlg import NaturalLanguageGenerator
32
33
  from rasa.shared.core.domain import Domain
34
+ from rasa.shared.core.flows.flows_list import FlowsList
33
35
 
34
36
  structlogger = structlog.get_logger()
35
37
 
@@ -364,3 +366,54 @@ def add_bot_utterance_metadata(
364
366
  ]
365
367
 
366
368
  return message
369
+
370
+
371
+ def should_force_slot_filling(
372
+ tracker: Optional[DialogueStateTracker], flows: "FlowsList"
373
+ ) -> Tuple[bool, Optional[str]]:
374
+ """Check if the flow should force slot filling.
375
+
376
+ This is only valid when the flow is at a collect information step which
377
+ has set `force_slot_filling` to true and the slot has a valid `from_text` mapping.
378
+
379
+ Args:
380
+ tracker: The dialogue state tracker.
381
+ flows: The list of flows.
382
+
383
+ Returns:
384
+ A tuple of a boolean indicating if the flow should force slot filling
385
+ and the name of the slot if applicable.
386
+ """
387
+ from rasa.dialogue_understanding.processor.command_processor import (
388
+ get_current_collect_step,
389
+ )
390
+
391
+ if tracker is None:
392
+ structlogger.error(
393
+ "slot.force_slot_filling.error",
394
+ event_info="Tracker is None. Cannot force slot filling.",
395
+ )
396
+ return False, None
397
+
398
+ stack = tracker.stack
399
+ step = get_current_collect_step(stack, flows)
400
+ if step is None or not step.force_slot_filling:
401
+ return False, None
402
+
403
+ slot_name = step.collect
404
+ slot = tracker.slots.get(slot_name)
405
+
406
+ if not slot:
407
+ structlogger.debug(
408
+ "slot.force_slot_filling.error",
409
+ event_info=f"Slot '{slot_name}' not found in tracker. "
410
+ f"Cannot force slot filling. "
411
+ f"Please check if the slot is defined in the domain.",
412
+ )
413
+ return False, None
414
+
415
+ for slot_mapping in slot.mappings:
416
+ if slot_mapping.type == SlotMappingType.FROM_TEXT:
417
+ return True, slot_name
418
+
419
+ return False, None