dv-pipecat-ai 0.0.85.dev823__py3-none-any.whl → 0.0.85.dev825__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 dv-pipecat-ai might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dv-pipecat-ai
3
- Version: 0.0.85.dev823
3
+ Version: 0.0.85.dev825
4
4
  Summary: An open source framework for voice (and multimodal) assistants
5
5
  License-Expression: BSD-2-Clause
6
6
  Project-URL: Source, https://github.com/pipecat-ai/pipecat
@@ -1,4 +1,4 @@
1
- dv_pipecat_ai-0.0.85.dev823.dist-info/licenses/LICENSE,sha256=DWY2QGf2eMCFhuu2ChairtT6CB7BEFffNVhXWc4Od08,1301
1
+ dv_pipecat_ai-0.0.85.dev825.dist-info/licenses/LICENSE,sha256=DWY2QGf2eMCFhuu2ChairtT6CB7BEFffNVhXWc4Od08,1301
2
2
  pipecat/__init__.py,sha256=j0Xm6adxHhd7D06dIyyPV_GlBYLlBnTAERVvD_jAARQ,861
3
3
  pipecat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  pipecat/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -318,7 +318,7 @@ pipecat/services/riva/__init__.py,sha256=rObSsj504O_TMXhPBg_ymqKslZBhovlR-A0aaRZ
318
318
  pipecat/services/riva/stt.py,sha256=bAss4dimx8eideaSPmPHM15_rSV3tfXNf13o5n1mfv4,25146
319
319
  pipecat/services/riva/tts.py,sha256=idbqx3I2NlWCXtrIFsjEaYapxA3BLIA14ai3aMBh-2w,8158
320
320
  pipecat/services/salesforce/__init__.py,sha256=OFvYbcvCadYhcKdBAVLj3ZUXVXQ1HyVyhgxIFf6_Thg,173
321
- pipecat/services/salesforce/llm.py,sha256=_DQ5JG5QYJG5Y8_LZLsPu5NeGzDBL8oy3_fFqNhz3-E,23009
321
+ pipecat/services/salesforce/llm.py,sha256=xBy5PSftIwD0v3eXxC5QCSlb8R8HiU1KQTX4R9qEmkA,28108
322
322
  pipecat/services/sambanova/__init__.py,sha256=oTXExLic-qTcsfsiWmssf3Elclf3IIWoN41_2IpoF18,128
323
323
  pipecat/services/sambanova/llm.py,sha256=5XVfPLEk__W8ykFqLdV95ZUhlGGkAaJwmbciLdZYtTc,8976
324
324
  pipecat/services/sambanova/stt.py,sha256=ZZgEZ7WQjLFHbCko-3LNTtVajjtfUvbtVLtFcaNadVQ,2536
@@ -352,7 +352,7 @@ pipecat/tests/utils.py,sha256=DEHDQV8uhCuKIqoHUPGVdUoCiKqTCG9zv5GqLXWWwvY,7870
352
352
  pipecat/transcriptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
353
353
  pipecat/transcriptions/language.py,sha256=-mWI1MiZbasuoqZTOBH69dAmoM7-UJzWq9rSCcrnmh4,8228
354
354
  pipecat/transports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
355
- pipecat/transports/base_input.py,sha256=BDex3CfCefwXyjK0M_FBXYEjqXYKUKR5ODtxcRp66uI,20086
355
+ pipecat/transports/base_input.py,sha256=WGtoXXlF3GIjYgjtYnAgi8nZozd5abNlGNjwRnz8FRs,20138
356
356
  pipecat/transports/base_output.py,sha256=mNlIOo7tETlbYPbDyOtA2H-TkBGFKmjuCMDzQUtiwmk,35423
357
357
  pipecat/transports/base_transport.py,sha256=JlNiH0DysTfr6azwHauJqY_Z9HJC702O29Q0qrsLrg4,7530
358
358
  pipecat/transports/daily/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -415,7 +415,7 @@ pipecat/utils/tracing/service_decorators.py,sha256=fwzxFpi8DJl6BJbK74G0UEB4ccMJg
415
415
  pipecat/utils/tracing/setup.py,sha256=7TEgPNpq6M8lww8OQvf0P9FzYc5A30xICGklVA-fua0,2892
416
416
  pipecat/utils/tracing/turn_context_provider.py,sha256=ikon3plFOx0XbMrH6DdeHttNpb-U0gzMZIm3bWLc9eI,2485
417
417
  pipecat/utils/tracing/turn_trace_observer.py,sha256=dma16SBJpYSOE58YDWy89QzHyQFc_9gQZszKeWixuwc,9725
418
- dv_pipecat_ai-0.0.85.dev823.dist-info/METADATA,sha256=QzFZChlDc4joKwMeDE3JSyM7EQzpJLPuIyfsNhrVgzE,32924
419
- dv_pipecat_ai-0.0.85.dev823.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
420
- dv_pipecat_ai-0.0.85.dev823.dist-info/top_level.txt,sha256=kQzG20CxGf-nSsHmtXHx3hY2-8zHA3jYg8jk0TajqXc,8
421
- dv_pipecat_ai-0.0.85.dev823.dist-info/RECORD,,
418
+ dv_pipecat_ai-0.0.85.dev825.dist-info/METADATA,sha256=9do57Wuqp0n9sMyV0YkeHfibkx3w57ANLCiGI7Pbzsk,32924
419
+ dv_pipecat_ai-0.0.85.dev825.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
420
+ dv_pipecat_ai-0.0.85.dev825.dist-info/top_level.txt,sha256=kQzG20CxGf-nSsHmtXHx3hY2-8zHA3jYg8jk0TajqXc,8
421
+ dv_pipecat_ai-0.0.85.dev825.dist-info/RECORD,,
@@ -236,23 +236,49 @@ class SalesforceAgentLLMService(LLMService):
236
236
  if not redis_client:
237
237
  return
238
238
 
239
- ttl_seconds = 3600
239
+ ttl_seconds = 3600 # Default fallback
240
+
241
+ # Try to get expiration from expires_in parameter first
240
242
  if expires_in is not None:
241
243
  try:
242
244
  ttl_seconds = max(int(expires_in) - self._token_cache_leeway_secs, 30)
245
+ logger.debug(f"Using expires_in parameter: {expires_in}s, TTL: {ttl_seconds}s")
243
246
  except (TypeError, ValueError):
244
- logger.debug("Unable to parse Salesforce token expiry; falling back to default TTL")
247
+ logger.debug("Unable to parse expires_in parameter")
248
+ expires_in = None
249
+
250
+ # If no expires_in available, use default TTL
251
+ if expires_in is None:
252
+ logger.debug("No expiration info found, using default TTL")
245
253
 
246
254
  try:
247
255
  await redis_client.set(self._token_cache_key, token, ex=ttl_seconds)
256
+ logger.debug(f"Cached Salesforce token with TTL: {ttl_seconds}s")
248
257
  except Exception as exc: # pragma: no cover - cache failures shouldn't break flow
249
258
  logger.warning(f"Failed to store Salesforce token in Redis: {exc}")
250
259
 
251
- async def _get_access_token(self) -> str:
252
- """Get OAuth access token using client credentials."""
253
- cached_token = await self._get_cached_access_token()
254
- if cached_token:
255
- return cached_token
260
+ async def _clear_cached_access_token(self):
261
+ """Clear cached access token from Redis."""
262
+ redis_client = self._get_redis_client()
263
+ if not redis_client:
264
+ return
265
+
266
+ try:
267
+ await redis_client.delete(self._token_cache_key)
268
+ logger.debug("Cleared cached Salesforce access token")
269
+ except Exception as exc: # pragma: no cover - cache failures shouldn't break flow
270
+ logger.warning(f"Failed to clear Salesforce token from Redis: {exc}")
271
+
272
+ async def _get_access_token(self, *, force_refresh: bool = False) -> str:
273
+ """Get OAuth access token using client credentials.
274
+
275
+ Args:
276
+ force_refresh: If True, skip cache and fetch fresh token from Salesforce.
277
+ """
278
+ if not force_refresh:
279
+ cached_token = await self._get_cached_access_token()
280
+ if cached_token:
281
+ return cached_token
256
282
 
257
283
  token_url = f"{self._org_domain}/services/oauth2/token"
258
284
  data = {
@@ -267,21 +293,58 @@ class SalesforceAgentLLMService(LLMService):
267
293
  token_data = response.json()
268
294
  access_token = token_data["access_token"]
269
295
  await self._set_cached_access_token(access_token, token_data.get("expires_in"))
296
+ logger.debug("Retrieved fresh Salesforce access token")
270
297
  return access_token
271
298
  except Exception as e:
272
299
  logger.error(f"Failed to get access token: {e}")
273
300
  raise
274
301
 
302
+ async def _make_authenticated_request(self, method: str, url: str, **kwargs):
303
+ """Make an authenticated HTTP request with automatic token refresh on auth errors.
304
+
305
+ Args:
306
+ method: HTTP method (GET, POST, DELETE, etc.)
307
+ url: Request URL
308
+ **kwargs: Additional arguments passed to httpx request
309
+
310
+ Returns:
311
+ httpx.Response: The HTTP response
312
+
313
+ Raises:
314
+ Exception: If request fails after token refresh attempt
315
+ """
316
+ # First attempt with current token
317
+ access_token = await self._get_access_token()
318
+ headers = kwargs.get("headers", {})
319
+ headers["Authorization"] = f"Bearer {access_token}"
320
+ kwargs["headers"] = headers
321
+
322
+ try:
323
+ response = await self._http_client.request(method, url, **kwargs)
324
+ response.raise_for_status()
325
+ return response
326
+ except httpx.HTTPStatusError as e:
327
+ # If authentication error, clear cache and retry with fresh token
328
+ if e.response.status_code in (401, 403):
329
+ logger.warning(f"Salesforce authentication error ({e.response.status_code}), refreshing token")
330
+ await self._clear_cached_access_token()
331
+
332
+ # Retry with fresh token
333
+ fresh_token = await self._get_access_token(force_refresh=True)
334
+ headers["Authorization"] = f"Bearer {fresh_token}"
335
+ kwargs["headers"] = headers
336
+
337
+ response = await self._http_client.request(method, url, **kwargs)
338
+ response.raise_for_status()
339
+ return response
340
+ else:
341
+ # Re-raise non-auth errors
342
+ raise
343
+
275
344
  async def _create_session(self) -> str:
276
345
  """Create a new Salesforce Agent session."""
277
- access_token = await self._get_access_token()
278
346
  session_url = f"{self._api_host}/einstein/ai-agent/v1/agents/{self._agent_id}/sessions"
279
347
 
280
- headers = {
281
- "Authorization": f"Bearer {access_token}",
282
- "Content-Type": "application/json",
283
- }
284
-
285
348
  external_session_key = f"pipecat-{int(time.time())}-{id(self)}"
286
349
 
287
350
  payload = {
@@ -295,8 +358,11 @@ class SalesforceAgentLLMService(LLMService):
295
358
  }
296
359
 
297
360
  try:
298
- response = await self._http_client.post(session_url, headers=headers, json=payload)
299
- response.raise_for_status()
361
+ response = await self._make_authenticated_request(
362
+ "POST", session_url,
363
+ headers={"Content-Type": "application/json"},
364
+ json=payload
365
+ )
300
366
  session_data = response.json()
301
367
  session_id = session_data["sessionId"]
302
368
 
@@ -351,13 +417,11 @@ class SalesforceAgentLLMService(LLMService):
351
417
  for session_id in expired_sessions:
352
418
  try:
353
419
  # End the session via API
354
- access_token = await self._get_access_token()
355
420
  url = f"{self._api_host}/einstein/ai-agent/v1/sessions/{session_id}"
356
- headers = {
357
- "Authorization": f"Bearer {access_token}",
358
- "x-session-end-reason": "UserRequest",
359
- }
360
- await self._http_client.delete(url, headers=headers)
421
+ await self._make_authenticated_request(
422
+ "DELETE", url,
423
+ headers={"x-session-end-reason": "UserRequest"}
424
+ )
361
425
  except Exception as e:
362
426
  logger.warning(f"Failed to end session {session_id}: {e}")
363
427
  finally:
@@ -402,14 +466,7 @@ class SalesforceAgentLLMService(LLMService):
402
466
 
403
467
  async def _stream_salesforce_response(self, session_id: str, user_message: str) -> AsyncGenerator[str, None]:
404
468
  """Stream response from Salesforce Agent API."""
405
- access_token = await self._get_access_token()
406
469
  url = f"{self._api_host}/einstein/ai-agent/v1/sessions/{session_id}/messages/stream"
407
-
408
- headers = {
409
- "Authorization": f"Bearer {access_token}",
410
- "Content-Type": "application/json",
411
- "Accept": "text/event-stream",
412
- }
413
470
 
414
471
  message_data = {
415
472
  "message": {
@@ -426,6 +483,14 @@ class SalesforceAgentLLMService(LLMService):
426
483
  ]
427
484
  }
428
485
 
486
+ # First attempt with current token
487
+ access_token = await self._get_access_token()
488
+ headers = {
489
+ "Authorization": f"Bearer {access_token}",
490
+ "Content-Type": "application/json",
491
+ "Accept": "text/event-stream",
492
+ }
493
+
429
494
  try:
430
495
  logger.info(f"🌐 Salesforce API request: {user_message[:50]}...")
431
496
  async with self._http_client.stream("POST", url, headers=headers, json=message_data) as response:
@@ -457,6 +522,49 @@ class SalesforceAgentLLMService(LLMService):
457
522
  logger.warning(f"JSON decode error: {e}, line: {line}")
458
523
  continue
459
524
 
525
+ except httpx.HTTPStatusError as e:
526
+ # If authentication error, retry with fresh token
527
+ if e.response.status_code in (401, 403):
528
+ logger.warning(f"Salesforce streaming authentication error ({e.response.status_code}), refreshing token")
529
+ await self._clear_cached_access_token()
530
+
531
+ # Retry with fresh token
532
+ fresh_token = await self._get_access_token(force_refresh=True)
533
+ headers["Authorization"] = f"Bearer {fresh_token}"
534
+
535
+ logger.info(f"🔄 Retrying Salesforce stream with fresh token: {user_message[:50]}...")
536
+ async with self._http_client.stream("POST", url, headers=headers, json=message_data) as response:
537
+ response.raise_for_status()
538
+
539
+ async for line in response.aiter_lines():
540
+ if not line:
541
+ continue
542
+
543
+ # Parse SSE format
544
+ if line.startswith("data: "):
545
+ try:
546
+ data = json.loads(line[6:])
547
+ message = data.get("message", {})
548
+ message_type = message.get("type")
549
+
550
+ if message_type == "TextChunk":
551
+ content = message.get("text", "") or message.get("message", "")
552
+ if content:
553
+ yield content
554
+ elif message_type == "EndOfTurn":
555
+ logger.info("🏁 Salesforce response complete")
556
+ break
557
+ elif message_type == "Inform":
558
+ # Skip INFORM events to avoid duplication
559
+ continue
560
+
561
+ except json.JSONDecodeError as e:
562
+ logger.warning(f"JSON decode error: {e}, line: {line}")
563
+ continue
564
+ else:
565
+ # Re-raise non-auth errors
566
+ logger.error(f"Failed to stream from Salesforce Agent API: {e}")
567
+ raise
460
568
  except Exception as e:
461
569
  logger.error(f"Failed to stream from Salesforce Agent API: {e}")
462
570
  raise
@@ -297,6 +297,17 @@ class BaseInputTransport(FrameProcessor):
297
297
  elif isinstance(frame, EmulateUserStoppedSpeakingFrame):
298
298
  self.logger.debug("Emulating user stopped speaking")
299
299
  await self._handle_user_interruption(VADState.QUIET, emulated=True)
300
+ elif isinstance(frame, VADParamsUpdateFrame):
301
+ if self.vad_analyzer:
302
+ self.vad_analyzer.set_params(frame.params)
303
+ speech_frame = SpeechControlParamsFrame(
304
+ vad_params=frame.params,
305
+ turn_params=self._params.turn_analyzer.params
306
+ if self._params.turn_analyzer
307
+ else None,
308
+ )
309
+ await self.push_frame(speech_frame)
310
+ await self.push_frame(frame, direction)
300
311
  # All other system frames
301
312
  elif isinstance(frame, SystemFrame):
302
313
  await self.push_frame(frame, direction)
@@ -309,16 +320,6 @@ class BaseInputTransport(FrameProcessor):
309
320
  elif isinstance(frame, StopFrame):
310
321
  await self.push_frame(frame, direction)
311
322
  await self.pause(frame)
312
- elif isinstance(frame, VADParamsUpdateFrame):
313
- if self.vad_analyzer:
314
- self.vad_analyzer.set_params(frame.params)
315
- speech_frame = SpeechControlParamsFrame(
316
- vad_params=frame.params,
317
- turn_params=self._params.turn_analyzer.params
318
- if self._params.turn_analyzer
319
- else None,
320
- )
321
- await self.push_frame(speech_frame)
322
323
  elif isinstance(frame, FilterUpdateSettingsFrame) and self._params.audio_in_filter:
323
324
  await self._params.audio_in_filter.process_frame(frame)
324
325
  # Other frames