dv-pipecat-ai 0.0.85.dev818__py3-none-any.whl → 0.0.85.dev858__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.

Files changed (32) hide show
  1. {dv_pipecat_ai-0.0.85.dev818.dist-info → dv_pipecat_ai-0.0.85.dev858.dist-info}/METADATA +2 -1
  2. {dv_pipecat_ai-0.0.85.dev818.dist-info → dv_pipecat_ai-0.0.85.dev858.dist-info}/RECORD +32 -29
  3. pipecat/audio/turn/smart_turn/local_smart_turn_v3.py +5 -1
  4. pipecat/frames/frames.py +34 -0
  5. pipecat/metrics/connection_metrics.py +45 -0
  6. pipecat/processors/aggregators/llm_response.py +25 -4
  7. pipecat/processors/dtmf_aggregator.py +17 -21
  8. pipecat/processors/frame_processor.py +51 -8
  9. pipecat/processors/metrics/frame_processor_metrics.py +108 -0
  10. pipecat/processors/transcript_processor.py +22 -1
  11. pipecat/serializers/__init__.py +2 -0
  12. pipecat/serializers/asterisk.py +16 -2
  13. pipecat/serializers/convox.py +2 -2
  14. pipecat/serializers/custom.py +2 -2
  15. pipecat/serializers/vi.py +326 -0
  16. pipecat/services/cartesia/tts.py +75 -10
  17. pipecat/services/deepgram/stt.py +317 -17
  18. pipecat/services/elevenlabs/stt.py +487 -19
  19. pipecat/services/elevenlabs/tts.py +28 -4
  20. pipecat/services/google/llm.py +26 -11
  21. pipecat/services/openai/base_llm.py +79 -14
  22. pipecat/services/salesforce/llm.py +321 -86
  23. pipecat/services/sarvam/tts.py +0 -1
  24. pipecat/services/soniox/stt.py +45 -10
  25. pipecat/services/vistaar/llm.py +97 -6
  26. pipecat/transcriptions/language.py +50 -0
  27. pipecat/transports/base_input.py +15 -11
  28. pipecat/transports/base_output.py +29 -3
  29. pipecat/utils/redis.py +58 -0
  30. {dv_pipecat_ai-0.0.85.dev818.dist-info → dv_pipecat_ai-0.0.85.dev858.dist-info}/WHEEL +0 -0
  31. {dv_pipecat_ai-0.0.85.dev818.dist-info → dv_pipecat_ai-0.0.85.dev858.dist-info}/licenses/LICENSE +0 -0
  32. {dv_pipecat_ai-0.0.85.dev818.dist-info → dv_pipecat_ai-0.0.85.dev858.dist-info}/top_level.txt +0 -0
@@ -436,10 +436,53 @@ class FrameProcessor(BaseObject):
436
436
  if frame:
437
437
  await self.push_frame(frame)
438
438
 
439
+ async def start_connection_metrics(self):
440
+ """Start connection establishment metrics collection."""
441
+ if self.can_generate_metrics() and self.metrics_enabled:
442
+ await self._metrics.start_connection_metrics()
443
+
444
+ async def stop_connection_metrics(
445
+ self,
446
+ success: bool = True,
447
+ error: str = None,
448
+ connection_type: str = None
449
+ ):
450
+ """Stop connection metrics collection and emit metrics frame.
451
+
452
+ Args:
453
+ success: Whether the connection was successful.
454
+ error: Error message if connection failed.
455
+ connection_type: Type of connection (websocket, http, etc.).
456
+ """
457
+ if self.can_generate_metrics() and self.metrics_enabled:
458
+ frame = await self._metrics.stop_connection_metrics(success, error, connection_type)
459
+ if frame:
460
+ await self.push_frame(frame)
461
+
462
+
463
+ async def start_reconnection_metrics(self):
464
+ """Start reconnection metrics collection."""
465
+ if self.can_generate_metrics() and self.metrics_enabled:
466
+ await self._metrics.start_reconnection_metrics()
467
+
468
+ async def stop_reconnection_metrics(self, success: bool = True, reason: str = None):
469
+ """Stop reconnection metrics collection and emit metrics frame.
470
+
471
+ Args:
472
+ success: Whether the reconnection was successful.
473
+ reason: Reason for reconnection.
474
+ """
475
+ if self.can_generate_metrics() and self.metrics_enabled:
476
+ frame = await self._metrics.stop_reconnection_metrics(success, reason)
477
+ if frame:
478
+ await self.push_frame(frame)
479
+
480
+
439
481
  async def stop_all_metrics(self):
440
482
  """Stop all active metrics collection."""
441
483
  await self.stop_ttfb_metrics()
442
484
  await self.stop_processing_metrics()
485
+ await self.stop_connection_metrics()
443
486
 
444
487
  def create_task(self, coroutine: Coroutine, name: Optional[str] = None) -> asyncio.Task:
445
488
  """Create a new task managed by this processor.
@@ -591,7 +634,7 @@ class FrameProcessor(BaseObject):
591
634
 
592
635
  async def pause_processing_system_frames(self):
593
636
  """Pause processing of queued system frames."""
594
- logger.trace(f"{self}: pausing system frame processing")
637
+ self.logger.trace(f"{self}: pausing system frame processing")
595
638
  self.__should_block_system_frames = True
596
639
  if self.__input_event:
597
640
  self.__input_event.clear()
@@ -811,8 +854,8 @@ class FrameProcessor(BaseObject):
811
854
  Returns:
812
855
  True if the processor has been started.
813
856
  """
814
- if not self.__started:
815
- logger.error(f"{self} Trying to process {frame} but StartFrame not received yet")
857
+ if not self.__started and not isinstance(frame, SystemFrame):
858
+ self.logger.error(f"{self} Trying to process {frame} but StartFrame not received yet")
816
859
  return self.__started
817
860
 
818
861
  def __create_input_task(self):
@@ -876,7 +919,7 @@ class FrameProcessor(BaseObject):
876
919
 
877
920
  await self._call_event_handler("on_after_process_frame", frame)
878
921
  except Exception as e:
879
- logger.exception(f"{self}: error processing frame: {e}")
922
+ self.logger.exception(f"{self}: error processing frame: {e}")
880
923
  await self.push_error(ErrorFrame(str(e)))
881
924
 
882
925
  async def __input_frame_task_handler(self):
@@ -890,11 +933,11 @@ class FrameProcessor(BaseObject):
890
933
  (frame, direction, callback) = await self.__input_queue.get()
891
934
 
892
935
  if self.__should_block_system_frames and self.__input_event:
893
- logger.trace(f"{self}: system frame processing paused")
936
+ self.logger.trace(f"{self}: system frame processing paused")
894
937
  await self.__input_event.wait()
895
938
  self.__input_event.clear()
896
939
  self.__should_block_system_frames = False
897
- logger.trace(f"{self}: system frame processing resumed")
940
+ self.logger.trace(f"{self}: system frame processing resumed")
898
941
 
899
942
  if isinstance(frame, SystemFrame):
900
943
  await self.__process_frame(frame, direction, callback)
@@ -913,11 +956,11 @@ class FrameProcessor(BaseObject):
913
956
  (frame, direction, callback) = await self.__process_queue.get()
914
957
 
915
958
  if self.__should_block_frames and self.__process_event:
916
- logger.trace(f"{self}: frame processing paused")
959
+ self.logger.trace(f"{self}: frame processing paused")
917
960
  await self.__process_event.wait()
918
961
  self.__process_event.clear()
919
962
  self.__should_block_frames = False
920
- logger.trace(f"{self}: frame processing resumed")
963
+ self.logger.trace(f"{self}: frame processing resumed")
921
964
 
922
965
  await self.__process_frame(frame, direction, callback)
923
966
 
@@ -20,6 +20,9 @@ from pipecat.metrics.metrics import (
20
20
  TTFBMetricsData,
21
21
  TTSUsageMetricsData,
22
22
  )
23
+ from pipecat.metrics.connection_metrics import (
24
+ ConnectionMetricsData,
25
+ )
23
26
  from pipecat.utils.asyncio.task_manager import BaseTaskManager
24
27
  from pipecat.utils.base_object import BaseObject
25
28
 
@@ -46,6 +49,13 @@ class FrameProcessorMetrics(BaseObject):
46
49
  self._last_ttfb_time = 0
47
50
  self._should_report_ttfb = True
48
51
  self._logger = logger
52
+
53
+ # Connection metrics state
54
+ self._start_connection_time = 0
55
+ self._connection_attempts = 0
56
+ self._last_connection_error = None
57
+ self._reconnection_start_time = 0
58
+ self._reconnect_count = 0
49
59
 
50
60
  async def setup(self, task_manager: BaseTaskManager):
51
61
  """Set up the metrics collector with a task manager.
@@ -195,3 +205,101 @@ class FrameProcessorMetrics(BaseObject):
195
205
  )
196
206
  self._logger.debug(f"{self._processor_name()} usage characters: {characters.value}")
197
207
  return MetricsFrame(data=[characters])
208
+
209
+ async def start_connection_metrics(self):
210
+ """Start measuring connection establishment time."""
211
+ self._start_connection_time = time.time()
212
+ self._connection_attempts += 1
213
+ self._last_connection_error = None
214
+
215
+ async def stop_connection_metrics(
216
+ self,
217
+ success: bool = True,
218
+ error: str = None,
219
+ connection_type: str = None
220
+ ):
221
+ """Stop connection measurement and generate metrics frame.
222
+
223
+ Args:
224
+ success: Whether the connection was successful.
225
+ error: Error message if connection failed.
226
+ connection_type: Type of connection (websocket, http, etc.).
227
+
228
+ Returns:
229
+ MetricsFrame containing connection data, or None if not measuring.
230
+ """
231
+ if self._start_connection_time == 0:
232
+ return None
233
+
234
+ connect_time = time.time() - self._start_connection_time
235
+
236
+ if not success:
237
+ self._last_connection_error = error
238
+
239
+ logstr = f"{self._processor_name()} connection "
240
+ logstr += "successful" if success else f"failed: {error}"
241
+ logstr += f" (attempt #{self._connection_attempts}, {connect_time:.3f}s)"
242
+
243
+ if success:
244
+ self._logger.debug(logstr)
245
+ else:
246
+ self._logger.warning(logstr)
247
+
248
+ connection_data = ConnectionMetricsData(
249
+ processor=self._processor_name(),
250
+ model=self._model_name(),
251
+ connect_time=round(connect_time, 3),
252
+ success=success,
253
+ connection_attempts=self._connection_attempts,
254
+ error_message=error,
255
+ connection_type=connection_type
256
+ )
257
+
258
+ self._start_connection_time = 0
259
+ return MetricsFrame(data=[connection_data])
260
+
261
+
262
+ async def start_reconnection_metrics(self):
263
+ """Start measuring reconnection downtime."""
264
+ self._reconnection_start_time = time.time()
265
+ self._reconnect_count += 1
266
+
267
+ async def stop_reconnection_metrics(
268
+ self,
269
+ success: bool = True,
270
+ reason: str = None
271
+ ):
272
+ """Stop reconnection measurement and generate metrics frame.
273
+
274
+ Args:
275
+ success: Whether the reconnection was successful.
276
+ reason: Reason for reconnection.
277
+
278
+ Returns:
279
+ MetricsFrame containing reconnection data, or None if not measuring.
280
+ """
281
+ if self._reconnection_start_time == 0:
282
+ return None
283
+
284
+ downtime = time.time() - self._reconnection_start_time
285
+
286
+ logstr = f"{self._processor_name()} reconnection #{self._reconnect_count} "
287
+ logstr += "successful" if success else "failed"
288
+ logstr += f" (downtime: {downtime:.3f}s)"
289
+ if reason:
290
+ logstr += f" - {reason}"
291
+
292
+ self._logger.debug(logstr)
293
+
294
+ reconnection_data = ConnectionMetricsData(
295
+ processor=self._processor_name(),
296
+ model=self._model_name(),
297
+ reconnect_count=self._reconnect_count,
298
+ downtime=round(downtime, 3),
299
+ reconnect_success=success,
300
+ reason=reason
301
+ )
302
+
303
+ self._reconnection_start_time = 0
304
+ return MetricsFrame(data=[reconnection_data])
305
+
@@ -20,6 +20,7 @@ from pipecat.frames.frames import (
20
20
  EndFrame,
21
21
  Frame,
22
22
  InterruptionFrame,
23
+ TranscriptDropFrame,
23
24
  TranscriptionFrame,
24
25
  TranscriptionMessage,
25
26
  TranscriptionUpdateFrame,
@@ -44,6 +45,7 @@ class BaseTranscriptProcessor(FrameProcessor):
44
45
  super().__init__(**kwargs)
45
46
  self._processed_messages: List[TranscriptionMessage] = []
46
47
  self._register_event_handler("on_transcript_update")
48
+ self._register_event_handler("on_transcript_drop")
47
49
 
48
50
  async def _emit_update(self, messages: List[TranscriptionMessage]):
49
51
  """Emit transcript updates for new messages.
@@ -57,6 +59,19 @@ class BaseTranscriptProcessor(FrameProcessor):
57
59
  await self._call_event_handler("on_transcript_update", update_frame)
58
60
  await self.push_frame(update_frame)
59
61
 
62
+ async def _handle_transcript_drop(self, frame: TranscriptDropFrame):
63
+ """Handle transcript drop notifications by removing stored messages."""
64
+ if not frame.transcript_ids:
65
+ return
66
+
67
+ await self._call_event_handler("on_transcript_drop", frame)
68
+
69
+ drop_ids = set(frame.transcript_ids)
70
+ if drop_ids:
71
+ self._processed_messages = [
72
+ msg for msg in self._processed_messages if msg.message_id not in drop_ids
73
+ ]
74
+
60
75
 
61
76
  class UserTranscriptProcessor(BaseTranscriptProcessor):
62
77
  """Processes user transcription frames into timestamped conversation messages."""
@@ -72,9 +87,15 @@ class UserTranscriptProcessor(BaseTranscriptProcessor):
72
87
 
73
88
  if isinstance(frame, TranscriptionFrame):
74
89
  message = TranscriptionMessage(
75
- role="user", user_id=frame.user_id, content=frame.text, timestamp=frame.timestamp
90
+ role="user",
91
+ user_id=frame.user_id,
92
+ content=frame.text,
93
+ timestamp=frame.timestamp,
94
+ message_id=frame.id,
76
95
  )
77
96
  await self._emit_update([message])
97
+ elif isinstance(frame, TranscriptDropFrame):
98
+ await self._handle_transcript_drop(frame)
78
99
 
79
100
  await self.push_frame(frame, direction)
80
101
 
@@ -5,6 +5,7 @@ from .exotel import ExotelFrameSerializer
5
5
  from .plivo import PlivoFrameSerializer
6
6
  from .telnyx import TelnyxFrameSerializer
7
7
  from .twilio import TwilioFrameSerializer
8
+ from .vi import VIFrameSerializer
8
9
 
9
10
  __all__ = [
10
11
  "FrameSerializer",
@@ -15,6 +16,7 @@ __all__ = [
15
16
  "PlivoFrameSerializer",
16
17
  "TelnyxFrameSerializer",
17
18
  "TwilioFrameSerializer",
19
+ "VIFrameSerializer",
18
20
  ]
19
21
 
20
22
  # Optional imports
@@ -1,4 +1,6 @@
1
1
  # asterisk_ws_serializer.py
2
+ """Frame serializer for Asterisk WebSocket communication."""
3
+
2
4
  import base64
3
5
  import json
4
6
  from typing import Literal, Optional
@@ -12,8 +14,8 @@ from pipecat.frames.frames import (
12
14
  EndFrame,
13
15
  Frame,
14
16
  InputAudioRawFrame,
17
+ InterruptionFrame,
15
18
  StartFrame,
16
- StartInterruptionFrame,
17
19
  TransportMessageFrame,
18
20
  TransportMessageUrgentFrame,
19
21
  )
@@ -21,6 +23,8 @@ from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializer
21
23
 
22
24
 
23
25
  class AsteriskFrameSerializer(FrameSerializer):
26
+ """Serializes Pipecat frames to/from Asterisk WebSocket JSON messages."""
27
+
24
28
  class InputParams(BaseModel):
25
29
  """Configuration parameters for AsteriskFrameSerializer.
26
30
 
@@ -39,6 +43,12 @@ class AsteriskFrameSerializer(FrameSerializer):
39
43
  auto_hang_up: bool = False # no-op here; adapter handles hangup
40
44
 
41
45
  def __init__(self, stream_id: str, params: Optional[InputParams] = None):
46
+ """Initialize the Asterisk frame serializer.
47
+
48
+ Args:
49
+ stream_id: Unique identifier for the media stream.
50
+ params: Configuration parameters for the serializer.
51
+ """
42
52
  self._stream_id = stream_id
43
53
  self._params = params or AsteriskFrameSerializer.InputParams()
44
54
  self._tel_rate = self._params.telephony_sample_rate
@@ -49,13 +59,16 @@ class AsteriskFrameSerializer(FrameSerializer):
49
59
 
50
60
  @property
51
61
  def type(self) -> FrameSerializerType:
62
+ """Return the serializer type (TEXT for JSON messages)."""
52
63
  return FrameSerializerType.TEXT # we send/recv JSON strings
53
64
 
54
65
  async def setup(self, frame: StartFrame):
66
+ """Setup the serializer with audio parameters from the StartFrame."""
55
67
  self._sample_rate = self._params.sample_rate or frame.audio_in_sample_rate
56
68
 
57
69
  # Pipecat -> Adapter (play to caller)
58
70
  async def serialize(self, frame: Frame) -> str | bytes | None:
71
+ """Serialize Pipecat frames to Asterisk WebSocket JSON messages."""
59
72
  # On pipeline end, ask bridge to hang up
60
73
  if (
61
74
  self._params.auto_hang_up
@@ -64,7 +77,7 @@ class AsteriskFrameSerializer(FrameSerializer):
64
77
  ):
65
78
  self._hangup_sent = True
66
79
  return json.dumps({"event": "hangup"})
67
- if isinstance(frame, StartInterruptionFrame):
80
+ if isinstance(frame, InterruptionFrame):
68
81
  return json.dumps({"event": "clear", "streamId": self._stream_id})
69
82
  if isinstance(frame, AudioRawFrame):
70
83
  pcm = frame.audio
@@ -114,6 +127,7 @@ class AsteriskFrameSerializer(FrameSerializer):
114
127
 
115
128
  # Adapter -> Pipecat (audio from caller)
116
129
  async def deserialize(self, data: str | bytes) -> Frame | None:
130
+ """Deserialize Asterisk WebSocket JSON messages to Pipecat frames."""
117
131
  try:
118
132
  msg = json.loads(data)
119
133
  except Exception:
@@ -22,9 +22,9 @@ from pipecat.frames.frames import (
22
22
  Frame,
23
23
  InputAudioRawFrame,
24
24
  InputDTMFFrame,
25
+ InterruptionFrame,
25
26
  KeypadEntry,
26
27
  StartFrame,
27
- StartInterruptionFrame,
28
28
  TransportMessageFrame,
29
29
  TransportMessageUrgentFrame,
30
30
  )
@@ -117,7 +117,7 @@ class ConVoxFrameSerializer(FrameSerializer):
117
117
  self._call_ended = True
118
118
  # Return the callEnd event to be sent via the WebSocket
119
119
  return await self._send_call_end_event()
120
- elif isinstance(frame, StartInterruptionFrame):
120
+ elif isinstance(frame, InterruptionFrame):
121
121
  # Clear/interrupt command for ConVox
122
122
  message = {
123
123
  "event": "clear",
@@ -28,8 +28,8 @@ from pipecat.frames.frames import (
28
28
  EndFrame,
29
29
  Frame,
30
30
  InputAudioRawFrame,
31
+ InterruptionFrame,
31
32
  StartFrame,
32
- StartInterruptionFrame,
33
33
  TransportMessageFrame,
34
34
  TransportMessageUrgentFrame,
35
35
  )
@@ -121,7 +121,7 @@ class CustomFrameSerializer(FrameSerializer):
121
121
  Returns:
122
122
  Serialized data as JSON string, or None if the frame isn't handled.
123
123
  """
124
- if isinstance(frame, StartInterruptionFrame):
124
+ if isinstance(frame, InterruptionFrame):
125
125
  # Send clear event to instruct client to discard buffered audio
126
126
  answer = {"event": "clear", "stream_sid": self._stream_sid}
127
127
  return json.dumps(answer)