rasa-pro 3.12.5__py3-none-any.whl → 3.12.6.dev2__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/__init__.py +6 -0
- rasa/core/channels/voice_ready/audiocodes.py +6 -0
- rasa/core/channels/voice_stream/audiocodes.py +53 -9
- rasa/core/channels/voice_stream/genesys.py +146 -16
- rasa/core/nlg/contextual_response_rephraser.py +21 -4
- rasa/core/nlg/summarize.py +15 -1
- rasa/core/policies/enterprise_search_policy.py +16 -3
- rasa/core/policies/intentless_policy.py +17 -4
- rasa/core/policies/policy.py +2 -0
- rasa/dialogue_understanding/coexistence/llm_based_router.py +18 -4
- rasa/dialogue_understanding/generator/llm_based_command_generator.py +8 -2
- rasa/dialogue_understanding/generator/llm_command_generator.py +3 -1
- rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +12 -1
- rasa/hooks.py +55 -0
- rasa/monkey_patches.py +91 -0
- rasa/shared/constants.py +5 -0
- rasa/shared/core/slot_mappings.py +12 -0
- rasa/shared/providers/constants.py +9 -0
- rasa/shared/providers/llm/_base_litellm_client.py +14 -4
- rasa/shared/providers/llm/litellm_router_llm_client.py +17 -7
- rasa/shared/providers/llm/llm_client.py +24 -15
- rasa/shared/providers/llm/self_hosted_llm_client.py +10 -2
- rasa/shared/utils/health_check/health_check.py +7 -1
- rasa/tracing/instrumentation/attribute_extractors.py +4 -4
- rasa/tracing/instrumentation/intentless_policy_instrumentation.py +2 -1
- rasa/utils/licensing.py +15 -0
- rasa/version.py +1 -1
- {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dev2.dist-info}/METADATA +5 -5
- {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dev2.dist-info}/RECORD +32 -33
- {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dev2.dist-info}/WHEEL +1 -1
- README.md +0 -38
- rasa/keys +0 -1
- {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dev2.dist-info}/NOTICE +0 -0
- {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dev2.dist-info}/entry_points.txt +0 -0
rasa/__init__.py
CHANGED
|
@@ -5,5 +5,11 @@ from rasa import version
|
|
|
5
5
|
# define the version before the other imports since these need it
|
|
6
6
|
__version__ = version.__version__
|
|
7
7
|
|
|
8
|
+
from litellm.integrations.langfuse.langfuse import LangFuseLogger
|
|
9
|
+
|
|
10
|
+
from rasa.monkey_patches import litellm_langfuse_logger_init_fixed
|
|
11
|
+
|
|
12
|
+
# Monkey-patch the init method as early as possible before the class is used
|
|
13
|
+
LangFuseLogger.__init__ = litellm_langfuse_logger_init_fixed # type: ignore
|
|
8
14
|
|
|
9
15
|
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
@@ -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
|
|
@@ -245,8 +246,13 @@ class AudiocodesInput(InputChannel):
|
|
|
245
246
|
|
|
246
247
|
def _check_token(self, token: Optional[Text]) -> None:
|
|
247
248
|
if not token:
|
|
249
|
+
structlogger.error("audiocodes.token_not_provided")
|
|
248
250
|
raise HttpUnauthorized("Authentication token required.")
|
|
249
251
|
|
|
252
|
+
if not hmac.compare_digest(str(token), str(self.token)):
|
|
253
|
+
structlogger.error("audiocodes.invalid_token", invalid_token=token)
|
|
254
|
+
raise HttpUnauthorized("Invalid authentication token.")
|
|
255
|
+
|
|
250
256
|
def _get_conversation(
|
|
251
257
|
self, token: Optional[Text], conversation_id: Text
|
|
252
258
|
) -> Conversation:
|
|
@@ -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
|
|
|
@@ -7,6 +7,10 @@ from rasa import telemetry
|
|
|
7
7
|
from rasa.core.nlg.response import TemplatedNaturalLanguageGenerator
|
|
8
8
|
from rasa.core.nlg.summarize import summarize_conversation
|
|
9
9
|
from rasa.shared.constants import (
|
|
10
|
+
LANGFUSE_CUSTOM_METADATA_DICT,
|
|
11
|
+
LANGFUSE_METADATA_SESSION_ID,
|
|
12
|
+
LANGFUSE_METADATA_USER_ID,
|
|
13
|
+
LANGFUSE_TAGS,
|
|
10
14
|
LLM_CONFIG_KEY,
|
|
11
15
|
MODEL_CONFIG_KEY,
|
|
12
16
|
MODEL_GROUP_ID_CONFIG_KEY,
|
|
@@ -39,6 +43,7 @@ from rasa.shared.utils.llm import (
|
|
|
39
43
|
tracker_as_readable_transcript,
|
|
40
44
|
)
|
|
41
45
|
from rasa.utils.endpoints import EndpointConfig
|
|
46
|
+
from rasa.utils.licensing import get_human_readable_licence_owner
|
|
42
47
|
from rasa.utils.log_utils import log_llm
|
|
43
48
|
|
|
44
49
|
structlogger = structlog.get_logger()
|
|
@@ -130,6 +135,7 @@ class ContextualResponseRephraser(
|
|
|
130
135
|
"contextual_response_rephraser.init",
|
|
131
136
|
ContextualResponseRephraser.__name__,
|
|
132
137
|
)
|
|
138
|
+
self.user_id = get_human_readable_licence_owner()
|
|
133
139
|
|
|
134
140
|
@classmethod
|
|
135
141
|
def _add_prompt_and_llm_metadata_to_response(
|
|
@@ -199,7 +205,9 @@ class ContextualResponseRephraser(
|
|
|
199
205
|
return None
|
|
200
206
|
|
|
201
207
|
@measure_llm_latency
|
|
202
|
-
async def _generate_llm_response(
|
|
208
|
+
async def _generate_llm_response(
|
|
209
|
+
self, prompt: str, sender_id: str
|
|
210
|
+
) -> Optional[LLMResponse]:
|
|
203
211
|
"""Use LLM to generate a response.
|
|
204
212
|
|
|
205
213
|
Returns an LLMResponse object containing both the generated text
|
|
@@ -207,14 +215,21 @@ class ContextualResponseRephraser(
|
|
|
207
215
|
|
|
208
216
|
Args:
|
|
209
217
|
prompt: The prompt to send to the LLM.
|
|
218
|
+
sender_id: sender_id from the tracker.
|
|
210
219
|
|
|
211
220
|
Returns:
|
|
212
221
|
An LLMResponse object if successful, otherwise None.
|
|
213
222
|
"""
|
|
214
223
|
llm = llm_factory(self.llm_config, DEFAULT_LLM_CONFIG)
|
|
224
|
+
metadata = {
|
|
225
|
+
LANGFUSE_METADATA_USER_ID: self.user_id,
|
|
226
|
+
LANGFUSE_METADATA_SESSION_ID: sender_id,
|
|
227
|
+
LANGFUSE_CUSTOM_METADATA_DICT: {"component": self.__class__.__name__},
|
|
228
|
+
LANGFUSE_TAGS: [self.__class__.__name__],
|
|
229
|
+
}
|
|
215
230
|
|
|
216
231
|
try:
|
|
217
|
-
return await llm.acompletion(prompt)
|
|
232
|
+
return await llm.acompletion(prompt, metadata)
|
|
218
233
|
except Exception as e:
|
|
219
234
|
# unfortunately, langchain does not wrap LLM exceptions which means
|
|
220
235
|
# we have to catch all exceptions here
|
|
@@ -258,7 +273,9 @@ class ContextualResponseRephraser(
|
|
|
258
273
|
The history for the prompt.
|
|
259
274
|
"""
|
|
260
275
|
llm = llm_factory(self.llm_config, DEFAULT_LLM_CONFIG)
|
|
261
|
-
return await summarize_conversation(
|
|
276
|
+
return await summarize_conversation(
|
|
277
|
+
tracker, llm, max_turns=5, user_id=self.user_id, sender_id=tracker.sender_id
|
|
278
|
+
)
|
|
262
279
|
|
|
263
280
|
async def rephrase(
|
|
264
281
|
self,
|
|
@@ -315,7 +332,7 @@ class ContextualResponseRephraser(
|
|
|
315
332
|
or self.llm_property(MODEL_NAME_CONFIG_KEY),
|
|
316
333
|
llm_model_group_id=self.llm_property(MODEL_GROUP_ID_CONFIG_KEY),
|
|
317
334
|
)
|
|
318
|
-
llm_response = await self._generate_llm_response(prompt)
|
|
335
|
+
llm_response = await self._generate_llm_response(prompt, tracker.sender_id)
|
|
319
336
|
llm_response = LLMResponse.ensure_llm_response(llm_response)
|
|
320
337
|
|
|
321
338
|
response = self._add_prompt_and_llm_metadata_to_response(
|
rasa/core/nlg/summarize.py
CHANGED
|
@@ -4,6 +4,12 @@ import structlog
|
|
|
4
4
|
from jinja2 import Template
|
|
5
5
|
|
|
6
6
|
from rasa.core.tracker_store import DialogueStateTracker
|
|
7
|
+
from rasa.shared.constants import (
|
|
8
|
+
LANGFUSE_CUSTOM_METADATA_DICT,
|
|
9
|
+
LANGFUSE_METADATA_SESSION_ID,
|
|
10
|
+
LANGFUSE_METADATA_USER_ID,
|
|
11
|
+
LANGFUSE_TAGS,
|
|
12
|
+
)
|
|
7
13
|
from rasa.shared.providers.llm.llm_client import LLMClient
|
|
8
14
|
from rasa.shared.utils.llm import (
|
|
9
15
|
tracker_as_readable_transcript,
|
|
@@ -46,6 +52,8 @@ async def summarize_conversation(
|
|
|
46
52
|
tracker: DialogueStateTracker,
|
|
47
53
|
llm: LLMClient,
|
|
48
54
|
max_turns: Optional[int] = MAX_TURNS_DEFAULT,
|
|
55
|
+
user_id: Optional[str] = None,
|
|
56
|
+
sender_id: Optional[str] = None,
|
|
49
57
|
) -> str:
|
|
50
58
|
"""Summarizes the dialogue using the LLM.
|
|
51
59
|
|
|
@@ -58,8 +66,14 @@ async def summarize_conversation(
|
|
|
58
66
|
The summary of the dialogue.
|
|
59
67
|
"""
|
|
60
68
|
prompt = _create_summarization_prompt(tracker, max_turns)
|
|
69
|
+
metadata = {
|
|
70
|
+
LANGFUSE_METADATA_USER_ID: user_id or "unknown",
|
|
71
|
+
LANGFUSE_METADATA_SESSION_ID: sender_id or "",
|
|
72
|
+
LANGFUSE_CUSTOM_METADATA_DICT: {"component": "summarize_conversation"},
|
|
73
|
+
LANGFUSE_TAGS: ["summarize_conversation"],
|
|
74
|
+
}
|
|
61
75
|
try:
|
|
62
|
-
llm_response = await llm.acompletion(prompt)
|
|
76
|
+
llm_response = await llm.acompletion(prompt, metadata)
|
|
63
77
|
summarization = llm_response.choices[0].strip()
|
|
64
78
|
structlogger.debug(
|
|
65
79
|
"summarization.success", summarization=summarization, prompt=prompt
|
|
@@ -46,6 +46,10 @@ from rasa.graph_components.providers.forms_provider import Forms
|
|
|
46
46
|
from rasa.graph_components.providers.responses_provider import Responses
|
|
47
47
|
from rasa.shared.constants import (
|
|
48
48
|
EMBEDDINGS_CONFIG_KEY,
|
|
49
|
+
LANGFUSE_CUSTOM_METADATA_DICT,
|
|
50
|
+
LANGFUSE_METADATA_SESSION_ID,
|
|
51
|
+
LANGFUSE_METADATA_USER_ID,
|
|
52
|
+
LANGFUSE_TAGS,
|
|
49
53
|
MODEL_CONFIG_KEY,
|
|
50
54
|
MODEL_GROUP_ID_CONFIG_KEY,
|
|
51
55
|
MODEL_NAME_CONFIG_KEY,
|
|
@@ -545,7 +549,9 @@ class EnterpriseSearchPolicy(LLMHealthCheckMixin, EmbeddingsHealthCheckMixin, Po
|
|
|
545
549
|
|
|
546
550
|
if self.use_llm:
|
|
547
551
|
prompt = self._render_prompt(tracker, documents.results)
|
|
548
|
-
llm_response = await self._generate_llm_answer(
|
|
552
|
+
llm_response = await self._generate_llm_answer(
|
|
553
|
+
llm, prompt, tracker.sender_id
|
|
554
|
+
)
|
|
549
555
|
llm_response = LLMResponse.ensure_llm_response(llm_response)
|
|
550
556
|
|
|
551
557
|
self._add_prompt_and_llm_response_to_latest_message(
|
|
@@ -641,19 +647,26 @@ class EnterpriseSearchPolicy(LLMHealthCheckMixin, EmbeddingsHealthCheckMixin, Po
|
|
|
641
647
|
|
|
642
648
|
@measure_llm_latency
|
|
643
649
|
async def _generate_llm_answer(
|
|
644
|
-
self, llm: LLMClient, prompt: Text
|
|
650
|
+
self, llm: LLMClient, prompt: Text, sender_id: str
|
|
645
651
|
) -> Optional[LLMResponse]:
|
|
646
652
|
"""Fetches an LLM completion for the provided prompt.
|
|
647
653
|
|
|
648
654
|
Args:
|
|
649
655
|
llm: The LLM client used to get the completion.
|
|
650
656
|
prompt: The prompt text to send to the model.
|
|
657
|
+
sender_id: sender_id from the tracker.
|
|
651
658
|
|
|
652
659
|
Returns:
|
|
653
660
|
An LLMResponse object, or None if the call fails.
|
|
654
661
|
"""
|
|
662
|
+
metadata = {
|
|
663
|
+
LANGFUSE_METADATA_USER_ID: self.user_id,
|
|
664
|
+
LANGFUSE_METADATA_SESSION_ID: sender_id,
|
|
665
|
+
LANGFUSE_CUSTOM_METADATA_DICT: {"component": self.__class__.__name__},
|
|
666
|
+
LANGFUSE_TAGS: [self.__class__.__name__],
|
|
667
|
+
}
|
|
655
668
|
try:
|
|
656
|
-
return await llm.acompletion(prompt)
|
|
669
|
+
return await llm.acompletion(prompt, metadata)
|
|
657
670
|
except Exception as e:
|
|
658
671
|
# unfortunately, langchain does not wrap LLM exceptions which means
|
|
659
672
|
# we have to catch all exceptions here
|
|
@@ -30,6 +30,10 @@ from rasa.graph_components.providers.forms_provider import Forms
|
|
|
30
30
|
from rasa.graph_components.providers.responses_provider import Responses
|
|
31
31
|
from rasa.shared.constants import (
|
|
32
32
|
EMBEDDINGS_CONFIG_KEY,
|
|
33
|
+
LANGFUSE_CUSTOM_METADATA_DICT,
|
|
34
|
+
LANGFUSE_METADATA_SESSION_ID,
|
|
35
|
+
LANGFUSE_METADATA_USER_ID,
|
|
36
|
+
LANGFUSE_TAGS,
|
|
33
37
|
LLM_CONFIG_KEY,
|
|
34
38
|
MODEL_CONFIG_KEY,
|
|
35
39
|
MODEL_GROUP_ID_CONFIG_KEY,
|
|
@@ -619,6 +623,7 @@ class IntentlessPolicy(LLMHealthCheckMixin, EmbeddingsHealthCheckMixin, Policy):
|
|
|
619
623
|
response_examples: List[str],
|
|
620
624
|
conversation_samples: List[str],
|
|
621
625
|
history: str,
|
|
626
|
+
sender_id: str,
|
|
622
627
|
) -> Optional[str]:
|
|
623
628
|
"""Make the llm call to generate an answer."""
|
|
624
629
|
llm = llm_factory(self.config.get(LLM_CONFIG_KEY), DEFAULT_LLM_CONFIG)
|
|
@@ -634,11 +639,19 @@ class IntentlessPolicy(LLMHealthCheckMixin, EmbeddingsHealthCheckMixin, Policy):
|
|
|
634
639
|
log_event="intentless_policy.generate_answer.prompt_rendered",
|
|
635
640
|
prompt=prompt,
|
|
636
641
|
)
|
|
637
|
-
return await self._generate_llm_answer(llm, prompt)
|
|
642
|
+
return await self._generate_llm_answer(llm, prompt, sender_id)
|
|
638
643
|
|
|
639
|
-
async def _generate_llm_answer(
|
|
644
|
+
async def _generate_llm_answer(
|
|
645
|
+
self, llm: LLMClient, prompt: str, sender_id: str
|
|
646
|
+
) -> Optional[str]:
|
|
647
|
+
metadata = {
|
|
648
|
+
LANGFUSE_METADATA_USER_ID: self.user_id,
|
|
649
|
+
LANGFUSE_METADATA_SESSION_ID: sender_id,
|
|
650
|
+
LANGFUSE_CUSTOM_METADATA_DICT: {"component": self.__class__.__name__},
|
|
651
|
+
LANGFUSE_TAGS: [self.__class__.__name__],
|
|
652
|
+
}
|
|
640
653
|
try:
|
|
641
|
-
llm_response = await llm.acompletion(prompt)
|
|
654
|
+
llm_response = await llm.acompletion(prompt, metadata)
|
|
642
655
|
return llm_response.choices[0]
|
|
643
656
|
except Exception as e:
|
|
644
657
|
# unfortunately, langchain does not wrap LLM exceptions which means
|
|
@@ -714,7 +727,7 @@ class IntentlessPolicy(LLMHealthCheckMixin, EmbeddingsHealthCheckMixin, Policy):
|
|
|
714
727
|
final_response_examples.append(resp)
|
|
715
728
|
|
|
716
729
|
llm_response = await self.generate_answer(
|
|
717
|
-
final_response_examples, conversation_samples, history
|
|
730
|
+
final_response_examples, conversation_samples, history, tracker.sender_id
|
|
718
731
|
)
|
|
719
732
|
if not llm_response:
|
|
720
733
|
structlogger.debug("intentless_policy.prediction.skip_llm_fail")
|
rasa/core/policies/policy.py
CHANGED
|
@@ -39,6 +39,7 @@ from rasa.shared.core.generator import TrackerWithCachedStates
|
|
|
39
39
|
from rasa.shared.core.trackers import DialogueStateTracker
|
|
40
40
|
from rasa.shared.exceptions import FileIOException, RasaException
|
|
41
41
|
from rasa.shared.nlu.constants import ACTION_NAME, ACTION_TEXT, ENTITIES, INTENT, TEXT
|
|
42
|
+
from rasa.utils.licensing import get_human_readable_licence_owner
|
|
42
43
|
|
|
43
44
|
if TYPE_CHECKING:
|
|
44
45
|
from rasa.core.featurizers.tracker_featurizers import (
|
|
@@ -172,6 +173,7 @@ class Policy(GraphComponent):
|
|
|
172
173
|
|
|
173
174
|
self._model_storage = model_storage
|
|
174
175
|
self._resource = resource
|
|
176
|
+
self.user_id = get_human_readable_licence_owner()
|
|
175
177
|
|
|
176
178
|
@classmethod
|
|
177
179
|
def create(
|
|
@@ -23,6 +23,10 @@ from rasa.engine.recipes.default_recipe import DefaultV1Recipe
|
|
|
23
23
|
from rasa.engine.storage.resource import Resource
|
|
24
24
|
from rasa.engine.storage.storage import ModelStorage
|
|
25
25
|
from rasa.shared.constants import (
|
|
26
|
+
LANGFUSE_CUSTOM_METADATA_DICT,
|
|
27
|
+
LANGFUSE_METADATA_SESSION_ID,
|
|
28
|
+
LANGFUSE_METADATA_USER_ID,
|
|
29
|
+
LANGFUSE_TAGS,
|
|
26
30
|
MODEL_CONFIG_KEY,
|
|
27
31
|
OPENAI_PROVIDER,
|
|
28
32
|
PROMPT_CONFIG_KEY,
|
|
@@ -43,6 +47,7 @@ from rasa.shared.utils.llm import (
|
|
|
43
47
|
llm_factory,
|
|
44
48
|
resolve_model_client_config,
|
|
45
49
|
)
|
|
50
|
+
from rasa.utils.licensing import get_human_readable_licence_owner
|
|
46
51
|
from rasa.utils.log_utils import log_llm
|
|
47
52
|
|
|
48
53
|
LLM_BASED_ROUTER_PROMPT_FILE_NAME = "llm_based_router_prompt.jinja2"
|
|
@@ -113,6 +118,7 @@ class LLMBasedRouter(LLMHealthCheckMixin, GraphComponent):
|
|
|
113
118
|
self._model_storage = model_storage
|
|
114
119
|
self._resource = resource
|
|
115
120
|
self.validate_config()
|
|
121
|
+
self.user_id = get_human_readable_licence_owner()
|
|
116
122
|
|
|
117
123
|
def validate_config(self) -> None:
|
|
118
124
|
"""Validate the config of the router."""
|
|
@@ -160,7 +166,6 @@ class LLMBasedRouter(LLMHealthCheckMixin, GraphComponent):
|
|
|
160
166
|
**kwargs: Any,
|
|
161
167
|
) -> "LLMBasedRouter":
|
|
162
168
|
"""Loads trained component (see parent class for full docstring)."""
|
|
163
|
-
|
|
164
169
|
# Perform health check on the resolved LLM client config
|
|
165
170
|
llm_config = resolve_model_client_config(config.get(LLM_CONFIG_KEY, {}))
|
|
166
171
|
cls.perform_llm_health_check(
|
|
@@ -232,7 +237,7 @@ class LLMBasedRouter(LLMHealthCheckMixin, GraphComponent):
|
|
|
232
237
|
prompt=prompt,
|
|
233
238
|
)
|
|
234
239
|
# generating answer
|
|
235
|
-
answer = await self._generate_answer_using_llm(prompt)
|
|
240
|
+
answer = await self._generate_answer_using_llm(prompt, tracker.sender_id)
|
|
236
241
|
log_llm(
|
|
237
242
|
logger=structlogger,
|
|
238
243
|
log_module="LLMBasedRouter",
|
|
@@ -292,7 +297,9 @@ class LLMBasedRouter(LLMHealthCheckMixin, GraphComponent):
|
|
|
292
297
|
|
|
293
298
|
return Template(self.prompt_template).render(**inputs)
|
|
294
299
|
|
|
295
|
-
async def _generate_answer_using_llm(
|
|
300
|
+
async def _generate_answer_using_llm(
|
|
301
|
+
self, prompt: str, sender_id: str
|
|
302
|
+
) -> Optional[str]:
|
|
296
303
|
"""Use LLM to generate a response.
|
|
297
304
|
|
|
298
305
|
Args:
|
|
@@ -303,8 +310,15 @@ class LLMBasedRouter(LLMHealthCheckMixin, GraphComponent):
|
|
|
303
310
|
"""
|
|
304
311
|
llm = llm_factory(self.config.get(LLM_CONFIG_KEY), DEFAULT_LLM_CONFIG)
|
|
305
312
|
|
|
313
|
+
metadata = {
|
|
314
|
+
LANGFUSE_METADATA_USER_ID: self.user_id,
|
|
315
|
+
LANGFUSE_METADATA_SESSION_ID: sender_id,
|
|
316
|
+
LANGFUSE_CUSTOM_METADATA_DICT: {"component": self.__class__.__name__},
|
|
317
|
+
LANGFUSE_TAGS: [self.__class__.__name__],
|
|
318
|
+
}
|
|
319
|
+
|
|
306
320
|
try:
|
|
307
|
-
llm_response = await llm.acompletion(prompt)
|
|
321
|
+
llm_response = await llm.acompletion(prompt, metadata)
|
|
308
322
|
return llm_response.choices[0]
|
|
309
323
|
except Exception as e:
|
|
310
324
|
# unfortunately, langchain does not wrap LLM exceptions which means
|