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.

Files changed (34) hide show
  1. rasa/__init__.py +6 -0
  2. rasa/core/channels/voice_ready/audiocodes.py +6 -0
  3. rasa/core/channels/voice_stream/audiocodes.py +53 -9
  4. rasa/core/channels/voice_stream/genesys.py +146 -16
  5. rasa/core/nlg/contextual_response_rephraser.py +21 -4
  6. rasa/core/nlg/summarize.py +15 -1
  7. rasa/core/policies/enterprise_search_policy.py +16 -3
  8. rasa/core/policies/intentless_policy.py +17 -4
  9. rasa/core/policies/policy.py +2 -0
  10. rasa/dialogue_understanding/coexistence/llm_based_router.py +18 -4
  11. rasa/dialogue_understanding/generator/llm_based_command_generator.py +8 -2
  12. rasa/dialogue_understanding/generator/llm_command_generator.py +3 -1
  13. rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +12 -1
  14. rasa/hooks.py +55 -0
  15. rasa/monkey_patches.py +91 -0
  16. rasa/shared/constants.py +5 -0
  17. rasa/shared/core/slot_mappings.py +12 -0
  18. rasa/shared/providers/constants.py +9 -0
  19. rasa/shared/providers/llm/_base_litellm_client.py +14 -4
  20. rasa/shared/providers/llm/litellm_router_llm_client.py +17 -7
  21. rasa/shared/providers/llm/llm_client.py +24 -15
  22. rasa/shared/providers/llm/self_hosted_llm_client.py +10 -2
  23. rasa/shared/utils/health_check/health_check.py +7 -1
  24. rasa/tracing/instrumentation/attribute_extractors.py +4 -4
  25. rasa/tracing/instrumentation/intentless_policy_instrumentation.py +2 -1
  26. rasa/utils/licensing.py +15 -0
  27. rasa/version.py +1 -1
  28. {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dev2.dist-info}/METADATA +5 -5
  29. {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dev2.dist-info}/RECORD +32 -33
  30. {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dev2.dist-info}/WHEEL +1 -1
  31. README.md +0 -38
  32. rasa/keys +0 -1
  33. {rasa_pro-3.12.5.dist-info → rasa_pro-3.12.6.dev2.dist-info}/NOTICE +0 -0
  34. {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("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
 
@@ -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(self, prompt: str) -> Optional[LLMResponse]:
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(tracker, llm, max_turns=5)
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(
@@ -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(llm, prompt)
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(self, llm: LLMClient, prompt: str) -> Optional[str]:
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")
@@ -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(self, prompt: str) -> Optional[str]:
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