rasa-pro 3.12.4__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.
- rasa/core/actions/action.py +0 -6
- rasa/core/channels/voice_ready/audiocodes.py +52 -17
- rasa/core/channels/voice_stream/audiocodes.py +53 -9
- rasa/core/channels/voice_stream/genesys.py +146 -16
- rasa/core/policies/flows/flow_executor.py +3 -38
- rasa/core/policies/intentless_policy.py +6 -59
- rasa/core/processor.py +19 -5
- rasa/core/utils.py +53 -0
- rasa/dialogue_understanding/commands/cancel_flow_command.py +4 -59
- rasa/dialogue_understanding/commands/start_flow_command.py +0 -41
- rasa/dialogue_understanding/generator/_jinja_filters.py +9 -0
- rasa/dialogue_understanding/generator/command_generator.py +67 -0
- rasa/dialogue_understanding/generator/constants.py +4 -0
- rasa/dialogue_understanding/generator/llm_based_command_generator.py +22 -16
- rasa/dialogue_understanding/generator/nlu_command_adapter.py +1 -1
- rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_claude_3_5_sonnet_20240620_template.jinja2 +2 -2
- rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_gpt_4o_2024_11_20_template.jinja2 +2 -2
- rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +0 -61
- rasa/dialogue_understanding/processor/command_processor.py +27 -70
- rasa/dialogue_understanding/processor/command_processor_component.py +5 -2
- rasa/dialogue_understanding/stack/utils.py +0 -38
- rasa/e2e_test/llm_judge_prompts/answer_relevance_prompt_template.jinja2 +1 -1
- rasa/engine/validation.py +36 -1
- rasa/model_training.py +2 -1
- rasa/shared/constants.py +2 -0
- rasa/shared/core/constants.py +0 -8
- rasa/shared/core/domain.py +12 -3
- rasa/shared/core/flows/flow.py +0 -17
- rasa/shared/core/flows/flows_yaml_schema.json +3 -38
- rasa/shared/core/flows/steps/collect.py +5 -18
- rasa/shared/core/flows/utils.py +1 -16
- rasa/shared/core/policies/__init__.py +0 -0
- rasa/shared/core/policies/utils.py +87 -0
- rasa/shared/core/slot_mappings.py +23 -5
- rasa/shared/nlu/constants.py +0 -1
- rasa/shared/utils/common.py +11 -1
- rasa/tracing/instrumentation/attribute_extractors.py +2 -0
- rasa/validator.py +1 -123
- rasa/version.py +1 -1
- {rasa_pro-3.12.4.dist-info → rasa_pro-3.12.6.dist-info}/METADATA +4 -5
- {rasa_pro-3.12.4.dist-info → rasa_pro-3.12.6.dist-info}/RECORD +44 -46
- {rasa_pro-3.12.4.dist-info → rasa_pro-3.12.6.dist-info}/WHEEL +1 -1
- README.md +0 -38
- rasa/core/actions/action_handle_digressions.py +0 -164
- rasa/dialogue_understanding/commands/handle_digressions_command.py +0 -144
- rasa/dialogue_understanding/patterns/handle_digressions.py +0 -81
- rasa/keys +0 -1
- {rasa_pro-3.12.4.dist-info → rasa_pro-3.12.6.dist-info}/NOTICE +0 -0
- {rasa_pro-3.12.4.dist-info → rasa_pro-3.12.6.dist-info}/entry_points.txt +0 -0
rasa/core/actions/action.py
CHANGED
|
@@ -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(
|
|
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.
|
|
405
|
-
|
|
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("
|
|
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(
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
"
|
|
319
|
+
"audiocodes_stream.receive",
|
|
276
320
|
message="Error during audio streaming",
|
|
277
321
|
error=e,
|
|
278
322
|
)
|
|
279
|
-
|
|
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__(
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
"
|
|
296
|
-
"
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
680
|
-
|
|
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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import importlib.resources
|
|
2
2
|
import math
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from typing import TYPE_CHECKING, Any, Dict, List, Optional,
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Text, Tuple
|
|
5
5
|
|
|
6
6
|
import structlog
|
|
7
7
|
import tiktoken
|
|
@@ -18,7 +18,6 @@ from rasa.core.constants import (
|
|
|
18
18
|
UTTER_SOURCE_METADATA_KEY,
|
|
19
19
|
)
|
|
20
20
|
from rasa.core.policies.policy import Policy, PolicyPrediction, SupportedData
|
|
21
|
-
from rasa.dialogue_understanding.patterns.chitchat import FLOW_PATTERN_CHITCHAT
|
|
22
21
|
from rasa.dialogue_understanding.stack.frames import (
|
|
23
22
|
ChitChatStackFrame,
|
|
24
23
|
DialogueStackFrame,
|
|
@@ -38,10 +37,9 @@ from rasa.shared.constants import (
|
|
|
38
37
|
OPENAI_PROVIDER,
|
|
39
38
|
PROMPT_CONFIG_KEY,
|
|
40
39
|
PROVIDER_CONFIG_KEY,
|
|
41
|
-
REQUIRED_SLOTS_KEY,
|
|
42
40
|
TIMEOUT_CONFIG_KEY,
|
|
43
41
|
)
|
|
44
|
-
from rasa.shared.core.constants import ACTION_LISTEN_NAME
|
|
42
|
+
from rasa.shared.core.constants import ACTION_LISTEN_NAME
|
|
45
43
|
from rasa.shared.core.domain import KEY_RESPONSES_TEXT, Domain
|
|
46
44
|
from rasa.shared.core.events import (
|
|
47
45
|
ActionExecuted,
|
|
@@ -51,6 +49,7 @@ from rasa.shared.core.events import (
|
|
|
51
49
|
)
|
|
52
50
|
from rasa.shared.core.flows import FlowsList
|
|
53
51
|
from rasa.shared.core.generator import TrackerWithCachedStates
|
|
52
|
+
from rasa.shared.core.policies.utils import filter_responses_for_intentless_policy
|
|
54
53
|
from rasa.shared.core.trackers import DialogueStateTracker
|
|
55
54
|
from rasa.shared.exceptions import FileIOException, RasaCoreException
|
|
56
55
|
from rasa.shared.nlu.constants import PREDICTED_CONFIDENCE_KEY
|
|
@@ -146,59 +145,6 @@ class Conversation:
|
|
|
146
145
|
interactions: List[Interaction] = field(default_factory=list)
|
|
147
146
|
|
|
148
147
|
|
|
149
|
-
def collect_form_responses(forms: Forms) -> Set[Text]:
|
|
150
|
-
"""Collect responses that belong the requested slots in forms.
|
|
151
|
-
|
|
152
|
-
Args:
|
|
153
|
-
forms: the forms from the domain
|
|
154
|
-
Returns:
|
|
155
|
-
all utterances used in forms
|
|
156
|
-
"""
|
|
157
|
-
form_responses = set()
|
|
158
|
-
for _, form_info in forms.data.items():
|
|
159
|
-
for required_slot in form_info.get(REQUIRED_SLOTS_KEY, []):
|
|
160
|
-
form_responses.add(f"utter_ask_{required_slot}")
|
|
161
|
-
return form_responses
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def filter_responses(responses: Responses, forms: Forms, flows: FlowsList) -> Responses:
|
|
165
|
-
"""Filters out responses that are unwanted for the intentless policy.
|
|
166
|
-
|
|
167
|
-
This includes utterances used in flows and forms.
|
|
168
|
-
|
|
169
|
-
Args:
|
|
170
|
-
responses: the responses from the domain
|
|
171
|
-
forms: the forms from the domain
|
|
172
|
-
flows: all flows
|
|
173
|
-
Returns:
|
|
174
|
-
The remaining, relevant responses for the intentless policy.
|
|
175
|
-
"""
|
|
176
|
-
form_responses = collect_form_responses(forms)
|
|
177
|
-
flow_responses = flows.utterances
|
|
178
|
-
combined_responses = form_responses | flow_responses
|
|
179
|
-
filtered_responses = {
|
|
180
|
-
name: variants
|
|
181
|
-
for name, variants in responses.data.items()
|
|
182
|
-
if name not in combined_responses
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
pattern_chitchat = flows.flow_by_id(FLOW_PATTERN_CHITCHAT)
|
|
186
|
-
|
|
187
|
-
# The following condition is highly unlikely, but mypy requires the case
|
|
188
|
-
# of pattern_chitchat == None to be addressed
|
|
189
|
-
if not pattern_chitchat:
|
|
190
|
-
return Responses(data=filtered_responses)
|
|
191
|
-
|
|
192
|
-
# if action_trigger_chitchat, filter out "utter_free_chitchat_response"
|
|
193
|
-
has_action_trigger_chitchat = pattern_chitchat.has_action_step(
|
|
194
|
-
ACTION_TRIGGER_CHITCHAT
|
|
195
|
-
)
|
|
196
|
-
if has_action_trigger_chitchat:
|
|
197
|
-
filtered_responses.pop("utter_free_chitchat_response", None)
|
|
198
|
-
|
|
199
|
-
return Responses(data=filtered_responses)
|
|
200
|
-
|
|
201
|
-
|
|
202
148
|
def action_from_response(
|
|
203
149
|
text: Optional[str], responses: Dict[Text, List[Dict[Text, Any]]]
|
|
204
150
|
) -> Optional[str]:
|
|
@@ -512,7 +458,9 @@ class IntentlessPolicy(LLMHealthCheckMixin, EmbeddingsHealthCheckMixin, Policy):
|
|
|
512
458
|
# Perform health checks of both LLM and embeddings client configs
|
|
513
459
|
self._perform_health_checks(self.config, "intentless_policy.train")
|
|
514
460
|
|
|
515
|
-
responses =
|
|
461
|
+
responses = filter_responses_for_intentless_policy(
|
|
462
|
+
responses, forms, flows or FlowsList([])
|
|
463
|
+
)
|
|
516
464
|
telemetry.track_intentless_policy_train()
|
|
517
465
|
response_texts = [r for r in extract_ai_response_examples(responses.data)]
|
|
518
466
|
|
|
@@ -947,7 +895,6 @@ class IntentlessPolicy(LLMHealthCheckMixin, EmbeddingsHealthCheckMixin, Policy):
|
|
|
947
895
|
**kwargs: Any,
|
|
948
896
|
) -> "IntentlessPolicy":
|
|
949
897
|
"""Loads a trained policy (see parent class for full docstring)."""
|
|
950
|
-
|
|
951
898
|
# Perform health checks of both LLM and embeddings client configs
|
|
952
899
|
cls._perform_health_checks(config, "intentless_policy.load")
|
|
953
900
|
|