dv-pipecat-ai 0.0.82.dev815__py3-none-any.whl → 0.0.82.dev857__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 (106) hide show
  1. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/METADATA +8 -3
  2. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/RECORD +106 -79
  3. pipecat/adapters/base_llm_adapter.py +44 -6
  4. pipecat/adapters/services/anthropic_adapter.py +302 -2
  5. pipecat/adapters/services/aws_nova_sonic_adapter.py +40 -2
  6. pipecat/adapters/services/bedrock_adapter.py +40 -2
  7. pipecat/adapters/services/gemini_adapter.py +276 -6
  8. pipecat/adapters/services/open_ai_adapter.py +88 -7
  9. pipecat/adapters/services/open_ai_realtime_adapter.py +39 -1
  10. pipecat/audio/dtmf/__init__.py +0 -0
  11. pipecat/audio/dtmf/types.py +47 -0
  12. pipecat/audio/dtmf/utils.py +70 -0
  13. pipecat/audio/filters/aic_filter.py +199 -0
  14. pipecat/audio/utils.py +9 -7
  15. pipecat/extensions/ivr/__init__.py +0 -0
  16. pipecat/extensions/ivr/ivr_navigator.py +452 -0
  17. pipecat/frames/frames.py +156 -43
  18. pipecat/pipeline/llm_switcher.py +76 -0
  19. pipecat/pipeline/parallel_pipeline.py +3 -3
  20. pipecat/pipeline/service_switcher.py +144 -0
  21. pipecat/pipeline/task.py +68 -28
  22. pipecat/pipeline/task_observer.py +10 -0
  23. pipecat/processors/aggregators/dtmf_aggregator.py +2 -2
  24. pipecat/processors/aggregators/llm_context.py +277 -0
  25. pipecat/processors/aggregators/llm_response.py +48 -15
  26. pipecat/processors/aggregators/llm_response_universal.py +840 -0
  27. pipecat/processors/aggregators/openai_llm_context.py +3 -3
  28. pipecat/processors/dtmf_aggregator.py +0 -2
  29. pipecat/processors/filters/stt_mute_filter.py +0 -2
  30. pipecat/processors/frame_processor.py +18 -11
  31. pipecat/processors/frameworks/rtvi.py +17 -10
  32. pipecat/processors/metrics/sentry.py +2 -0
  33. pipecat/runner/daily.py +137 -36
  34. pipecat/runner/run.py +1 -1
  35. pipecat/runner/utils.py +7 -7
  36. pipecat/serializers/asterisk.py +20 -4
  37. pipecat/serializers/exotel.py +1 -1
  38. pipecat/serializers/plivo.py +1 -1
  39. pipecat/serializers/telnyx.py +1 -1
  40. pipecat/serializers/twilio.py +1 -1
  41. pipecat/services/__init__.py +2 -2
  42. pipecat/services/anthropic/llm.py +113 -28
  43. pipecat/services/asyncai/tts.py +4 -0
  44. pipecat/services/aws/llm.py +82 -8
  45. pipecat/services/aws/tts.py +0 -10
  46. pipecat/services/aws_nova_sonic/aws.py +5 -0
  47. pipecat/services/cartesia/tts.py +28 -16
  48. pipecat/services/cerebras/llm.py +15 -10
  49. pipecat/services/deepgram/stt.py +8 -0
  50. pipecat/services/deepseek/llm.py +13 -8
  51. pipecat/services/fireworks/llm.py +13 -8
  52. pipecat/services/fish/tts.py +8 -6
  53. pipecat/services/gemini_multimodal_live/gemini.py +5 -0
  54. pipecat/services/gladia/config.py +7 -1
  55. pipecat/services/gladia/stt.py +23 -15
  56. pipecat/services/google/llm.py +159 -59
  57. pipecat/services/google/llm_openai.py +18 -3
  58. pipecat/services/grok/llm.py +2 -1
  59. pipecat/services/llm_service.py +38 -3
  60. pipecat/services/mem0/memory.py +2 -1
  61. pipecat/services/mistral/llm.py +5 -6
  62. pipecat/services/nim/llm.py +2 -1
  63. pipecat/services/openai/base_llm.py +88 -26
  64. pipecat/services/openai/image.py +6 -1
  65. pipecat/services/openai_realtime_beta/openai.py +5 -2
  66. pipecat/services/openpipe/llm.py +6 -8
  67. pipecat/services/perplexity/llm.py +13 -8
  68. pipecat/services/playht/tts.py +9 -6
  69. pipecat/services/rime/tts.py +1 -1
  70. pipecat/services/sambanova/llm.py +18 -13
  71. pipecat/services/sarvam/tts.py +415 -10
  72. pipecat/services/speechmatics/stt.py +2 -2
  73. pipecat/services/tavus/video.py +1 -1
  74. pipecat/services/tts_service.py +15 -5
  75. pipecat/services/vistaar/llm.py +2 -5
  76. pipecat/transports/base_input.py +32 -19
  77. pipecat/transports/base_output.py +39 -5
  78. pipecat/transports/daily/__init__.py +0 -0
  79. pipecat/transports/daily/transport.py +2371 -0
  80. pipecat/transports/daily/utils.py +410 -0
  81. pipecat/transports/livekit/__init__.py +0 -0
  82. pipecat/transports/livekit/transport.py +1042 -0
  83. pipecat/transports/network/fastapi_websocket.py +12 -546
  84. pipecat/transports/network/small_webrtc.py +12 -922
  85. pipecat/transports/network/webrtc_connection.py +9 -595
  86. pipecat/transports/network/websocket_client.py +12 -481
  87. pipecat/transports/network/websocket_server.py +12 -487
  88. pipecat/transports/services/daily.py +9 -2334
  89. pipecat/transports/services/helpers/daily_rest.py +12 -396
  90. pipecat/transports/services/livekit.py +12 -975
  91. pipecat/transports/services/tavus.py +12 -757
  92. pipecat/transports/smallwebrtc/__init__.py +0 -0
  93. pipecat/transports/smallwebrtc/connection.py +612 -0
  94. pipecat/transports/smallwebrtc/transport.py +936 -0
  95. pipecat/transports/tavus/__init__.py +0 -0
  96. pipecat/transports/tavus/transport.py +770 -0
  97. pipecat/transports/websocket/__init__.py +0 -0
  98. pipecat/transports/websocket/client.py +494 -0
  99. pipecat/transports/websocket/fastapi.py +559 -0
  100. pipecat/transports/websocket/server.py +500 -0
  101. pipecat/transports/whatsapp/__init__.py +0 -0
  102. pipecat/transports/whatsapp/api.py +345 -0
  103. pipecat/transports/whatsapp/client.py +364 -0
  104. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/WHEEL +0 -0
  105. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/licenses/LICENSE +0 -0
  106. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/top_level.txt +0 -0
File without changes
@@ -0,0 +1,494 @@
1
+ #
2
+ # Copyright (c) 2024–2025, Daily
3
+ #
4
+ # SPDX-License-Identifier: BSD 2-Clause License
5
+ #
6
+
7
+ """WebSocket client transport implementation for Pipecat.
8
+
9
+ This module provides a WebSocket client transport that enables bidirectional
10
+ communication over WebSocket connections, with support for audio streaming,
11
+ frame serialization, and connection management.
12
+ """
13
+
14
+ import asyncio
15
+ import io
16
+ import time
17
+ import wave
18
+ from typing import Awaitable, Callable, Optional
19
+
20
+ import websockets
21
+ from loguru import logger
22
+ from pydantic.main import BaseModel
23
+ from websockets.asyncio.client import connect as websocket_connect
24
+
25
+ from pipecat.frames.frames import (
26
+ CancelFrame,
27
+ EndFrame,
28
+ Frame,
29
+ InputAudioRawFrame,
30
+ OutputAudioRawFrame,
31
+ StartFrame,
32
+ TransportMessageFrame,
33
+ TransportMessageUrgentFrame,
34
+ )
35
+ from pipecat.processors.frame_processor import FrameProcessorSetup
36
+ from pipecat.serializers.base_serializer import FrameSerializer
37
+ from pipecat.serializers.protobuf import ProtobufFrameSerializer
38
+ from pipecat.transports.base_input import BaseInputTransport
39
+ from pipecat.transports.base_output import BaseOutputTransport
40
+ from pipecat.transports.base_transport import BaseTransport, TransportParams
41
+ from pipecat.utils.asyncio.task_manager import BaseTaskManager
42
+
43
+
44
+ class WebsocketClientParams(TransportParams):
45
+ """Configuration parameters for WebSocket client transport.
46
+
47
+ Parameters:
48
+ add_wav_header: Whether to add WAV headers to audio frames.
49
+ serializer: Frame serializer for encoding/decoding messages.
50
+ """
51
+
52
+ add_wav_header: bool = True
53
+ serializer: Optional[FrameSerializer] = None
54
+
55
+
56
+ class WebsocketClientCallbacks(BaseModel):
57
+ """Callback functions for WebSocket client events.
58
+
59
+ Parameters:
60
+ on_connected: Called when WebSocket connection is established.
61
+ on_disconnected: Called when WebSocket connection is closed.
62
+ on_message: Called when a message is received from the WebSocket.
63
+ """
64
+
65
+ on_connected: Callable[[websockets.WebSocketClientProtocol], Awaitable[None]]
66
+ on_disconnected: Callable[[websockets.WebSocketClientProtocol], Awaitable[None]]
67
+ on_message: Callable[[websockets.WebSocketClientProtocol, websockets.Data], Awaitable[None]]
68
+
69
+
70
+ class WebsocketClientSession:
71
+ """Manages a WebSocket client connection session.
72
+
73
+ Handles connection lifecycle, message sending/receiving, and provides
74
+ callback mechanisms for connection events.
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ uri: str,
80
+ params: WebsocketClientParams,
81
+ callbacks: WebsocketClientCallbacks,
82
+ transport_name: str,
83
+ ):
84
+ """Initialize the WebSocket client session.
85
+
86
+ Args:
87
+ uri: The WebSocket URI to connect to.
88
+ params: Configuration parameters for the session.
89
+ callbacks: Callback functions for session events.
90
+ transport_name: Name of the parent transport for logging.
91
+ """
92
+ self._uri = uri
93
+ self._params = params
94
+ self._callbacks = callbacks
95
+ self._transport_name = transport_name
96
+
97
+ self._leave_counter = 0
98
+ self._task_manager: Optional[BaseTaskManager] = None
99
+ self._websocket: Optional[websockets.WebSocketClientProtocol] = None
100
+
101
+ @property
102
+ def task_manager(self) -> BaseTaskManager:
103
+ """Get the task manager for this session.
104
+
105
+ Returns:
106
+ The task manager instance.
107
+
108
+ Raises:
109
+ Exception: If task manager is not initialized.
110
+ """
111
+ if not self._task_manager:
112
+ raise Exception(
113
+ f"{self._transport_name}::WebsocketClientSession: TaskManager not initialized (pipeline not started?)"
114
+ )
115
+ return self._task_manager
116
+
117
+ async def setup(self, task_manager: BaseTaskManager):
118
+ """Set up the session with a task manager.
119
+
120
+ Args:
121
+ task_manager: The task manager to use for session tasks.
122
+ """
123
+ self._leave_counter += 1
124
+ if not self._task_manager:
125
+ self._task_manager = task_manager
126
+
127
+ async def connect(self):
128
+ """Connect to the WebSocket server."""
129
+ if self._websocket:
130
+ return
131
+
132
+ try:
133
+ self._websocket = await websocket_connect(uri=self._uri, open_timeout=10)
134
+ self._client_task = self.task_manager.create_task(
135
+ self._client_task_handler(),
136
+ f"{self._transport_name}::WebsocketClientSession::_client_task_handler",
137
+ )
138
+ await self._callbacks.on_connected(self._websocket)
139
+ except TimeoutError:
140
+ logger.error(f"Timeout connecting to {self._uri}")
141
+
142
+ async def disconnect(self):
143
+ """Disconnect from the WebSocket server."""
144
+ self._leave_counter -= 1
145
+ if not self._websocket or self._leave_counter > 0:
146
+ return
147
+
148
+ await self.task_manager.cancel_task(self._client_task)
149
+
150
+ await self._websocket.close()
151
+ self._websocket = None
152
+
153
+ async def send(self, message: websockets.Data):
154
+ """Send a message through the WebSocket connection.
155
+
156
+ Args:
157
+ message: The message data to send.
158
+ """
159
+ try:
160
+ if self._websocket:
161
+ await self._websocket.send(message)
162
+ except Exception as e:
163
+ logger.error(f"{self} exception sending data: {e.__class__.__name__} ({e})")
164
+
165
+ async def _client_task_handler(self):
166
+ """Handle incoming messages from the WebSocket connection."""
167
+ try:
168
+ # Handle incoming messages
169
+ async for message in self._websocket:
170
+ await self._callbacks.on_message(self._websocket, message)
171
+ except Exception as e:
172
+ logger.error(f"{self} exception receiving data: {e.__class__.__name__} ({e})")
173
+
174
+ await self._callbacks.on_disconnected(self._websocket)
175
+
176
+ def __str__(self):
177
+ """String representation of the WebSocket client session."""
178
+ return f"{self._transport_name}::WebsocketClientSession"
179
+
180
+
181
+ class WebsocketClientInputTransport(BaseInputTransport):
182
+ """WebSocket client input transport for receiving frames.
183
+
184
+ Handles incoming WebSocket messages, deserializes them to frames,
185
+ and pushes them downstream in the processing pipeline.
186
+ """
187
+
188
+ def __init__(
189
+ self,
190
+ transport: BaseTransport,
191
+ session: WebsocketClientSession,
192
+ params: WebsocketClientParams,
193
+ ):
194
+ """Initialize the WebSocket client input transport.
195
+
196
+ Args:
197
+ transport: The parent transport instance.
198
+ session: The WebSocket session to use for communication.
199
+ params: Configuration parameters for the transport.
200
+ """
201
+ super().__init__(params)
202
+
203
+ self._transport = transport
204
+ self._session = session
205
+ self._params = params
206
+
207
+ # Whether we have seen a StartFrame already.
208
+ self._initialized = False
209
+
210
+ async def setup(self, setup: FrameProcessorSetup):
211
+ """Set up the input transport with the frame processor setup.
212
+
213
+ Args:
214
+ setup: The frame processor setup configuration.
215
+ """
216
+ await super().setup(setup)
217
+ await self._session.setup(setup.task_manager)
218
+
219
+ async def start(self, frame: StartFrame):
220
+ """Start the input transport and initialize the WebSocket connection.
221
+
222
+ Args:
223
+ frame: The start frame containing initialization parameters.
224
+ """
225
+ await super().start(frame)
226
+
227
+ if self._initialized:
228
+ return
229
+
230
+ self._initialized = True
231
+
232
+ if self._params.serializer:
233
+ await self._params.serializer.setup(frame)
234
+ await self._session.connect()
235
+ await self.set_transport_ready(frame)
236
+
237
+ async def stop(self, frame: EndFrame):
238
+ """Stop the input transport and disconnect from WebSocket.
239
+
240
+ Args:
241
+ frame: The end frame signaling transport shutdown.
242
+ """
243
+ await super().stop(frame)
244
+ await self._session.disconnect()
245
+
246
+ async def cancel(self, frame: CancelFrame):
247
+ """Cancel the input transport and disconnect from WebSocket.
248
+
249
+ Args:
250
+ frame: The cancel frame signaling immediate cancellation.
251
+ """
252
+ await super().cancel(frame)
253
+ await self._session.disconnect()
254
+
255
+ async def cleanup(self):
256
+ """Clean up the input transport resources."""
257
+ await super().cleanup()
258
+ await self._transport.cleanup()
259
+
260
+ async def on_message(self, websocket, message):
261
+ """Handle incoming WebSocket messages.
262
+
263
+ Args:
264
+ websocket: The WebSocket connection that received the message.
265
+ message: The received message data.
266
+ """
267
+ if not self._params.serializer:
268
+ return
269
+ frame = await self._params.serializer.deserialize(message)
270
+ if not frame:
271
+ return
272
+ if isinstance(frame, InputAudioRawFrame) and self._params.audio_in_enabled:
273
+ await self.push_audio_frame(frame)
274
+ else:
275
+ await self.push_frame(frame)
276
+
277
+
278
+ class WebsocketClientOutputTransport(BaseOutputTransport):
279
+ """WebSocket client output transport for sending frames.
280
+
281
+ Handles outgoing frames, serializes them for WebSocket transmission,
282
+ and manages audio streaming with proper timing simulation.
283
+ """
284
+
285
+ def __init__(
286
+ self,
287
+ transport: BaseTransport,
288
+ session: WebsocketClientSession,
289
+ params: WebsocketClientParams,
290
+ ):
291
+ """Initialize the WebSocket client output transport.
292
+
293
+ Args:
294
+ transport: The parent transport instance.
295
+ session: The WebSocket session to use for communication.
296
+ params: Configuration parameters for the transport.
297
+ """
298
+ super().__init__(params)
299
+
300
+ self._transport = transport
301
+ self._session = session
302
+ self._params = params
303
+
304
+ # write_audio_frame() is called quickly, as soon as we get audio
305
+ # (e.g. from the TTS), and since this is just a network connection we
306
+ # would be sending it to quickly. Instead, we want to block to emulate
307
+ # an audio device, this is what the send interval is. It will be
308
+ # computed on StartFrame.
309
+ self._send_interval = 0
310
+ self._next_send_time = 0
311
+
312
+ # Whether we have seen a StartFrame already.
313
+ self._initialized = False
314
+
315
+ async def setup(self, setup: FrameProcessorSetup):
316
+ """Set up the output transport with the frame processor setup.
317
+
318
+ Args:
319
+ setup: The frame processor setup configuration.
320
+ """
321
+ await super().setup(setup)
322
+ await self._session.setup(setup.task_manager)
323
+
324
+ async def start(self, frame: StartFrame):
325
+ """Start the output transport and initialize the WebSocket connection.
326
+
327
+ Args:
328
+ frame: The start frame containing initialization parameters.
329
+ """
330
+ await super().start(frame)
331
+
332
+ if self._initialized:
333
+ return
334
+
335
+ self._initialized = True
336
+
337
+ self._send_interval = (self.audio_chunk_size / self.sample_rate) / 2
338
+ if self._params.serializer:
339
+ await self._params.serializer.setup(frame)
340
+ await self._session.connect()
341
+ await self.set_transport_ready(frame)
342
+
343
+ async def stop(self, frame: EndFrame):
344
+ """Stop the output transport and disconnect from WebSocket.
345
+
346
+ Args:
347
+ frame: The end frame signaling transport shutdown.
348
+ """
349
+ await super().stop(frame)
350
+ await self._session.disconnect()
351
+
352
+ async def cancel(self, frame: CancelFrame):
353
+ """Cancel the output transport and disconnect from WebSocket.
354
+
355
+ Args:
356
+ frame: The cancel frame signaling immediate cancellation.
357
+ """
358
+ await super().cancel(frame)
359
+ await self._session.disconnect()
360
+
361
+ async def cleanup(self):
362
+ """Clean up the output transport resources."""
363
+ await super().cleanup()
364
+ await self._transport.cleanup()
365
+
366
+ async def send_message(self, frame: TransportMessageFrame | TransportMessageUrgentFrame):
367
+ """Send a transport message through the WebSocket.
368
+
369
+ Args:
370
+ frame: The transport message frame to send.
371
+ """
372
+ await self._write_frame(frame)
373
+
374
+ async def write_audio_frame(self, frame: OutputAudioRawFrame):
375
+ """Write an audio frame to the WebSocket with optional WAV header.
376
+
377
+ Args:
378
+ frame: The output audio frame to write.
379
+ """
380
+ frame = OutputAudioRawFrame(
381
+ audio=frame.audio,
382
+ sample_rate=self.sample_rate,
383
+ num_channels=self._params.audio_out_channels,
384
+ )
385
+
386
+ if self._params.add_wav_header:
387
+ with io.BytesIO() as buffer:
388
+ with wave.open(buffer, "wb") as wf:
389
+ wf.setsampwidth(2)
390
+ wf.setnchannels(frame.num_channels)
391
+ wf.setframerate(frame.sample_rate)
392
+ wf.writeframes(frame.audio)
393
+ wav_frame = OutputAudioRawFrame(
394
+ buffer.getvalue(),
395
+ sample_rate=frame.sample_rate,
396
+ num_channels=frame.num_channels,
397
+ )
398
+ frame = wav_frame
399
+
400
+ await self._write_frame(frame)
401
+
402
+ # Simulate audio playback with a sleep.
403
+ await self._write_audio_sleep()
404
+
405
+ async def _write_frame(self, frame: Frame):
406
+ """Write a frame to the WebSocket after serialization."""
407
+ if not self._params.serializer:
408
+ return
409
+ payload = await self._params.serializer.serialize(frame)
410
+ if payload:
411
+ await self._session.send(payload)
412
+
413
+ async def _write_audio_sleep(self):
414
+ """Simulate audio playback timing with sleep delays."""
415
+ # Simulate a clock.
416
+ current_time = time.monotonic()
417
+ sleep_duration = max(0, self._next_send_time - current_time)
418
+ await asyncio.sleep(sleep_duration)
419
+ if sleep_duration == 0:
420
+ self._next_send_time = time.monotonic() + self._send_interval
421
+ else:
422
+ self._next_send_time += self._send_interval
423
+
424
+
425
+ class WebsocketClientTransport(BaseTransport):
426
+ """WebSocket client transport for bidirectional communication.
427
+
428
+ Provides a complete WebSocket client transport implementation with
429
+ input and output capabilities, connection management, and event handling.
430
+ """
431
+
432
+ def __init__(
433
+ self,
434
+ uri: str,
435
+ params: Optional[WebsocketClientParams] = None,
436
+ ):
437
+ """Initialize the WebSocket client transport.
438
+
439
+ Args:
440
+ uri: The WebSocket URI to connect to.
441
+ params: Optional configuration parameters for the transport.
442
+ """
443
+ super().__init__()
444
+
445
+ self._params = params or WebsocketClientParams()
446
+ self._params.serializer = self._params.serializer or ProtobufFrameSerializer()
447
+
448
+ callbacks = WebsocketClientCallbacks(
449
+ on_connected=self._on_connected,
450
+ on_disconnected=self._on_disconnected,
451
+ on_message=self._on_message,
452
+ )
453
+
454
+ self._session = WebsocketClientSession(uri, self._params, callbacks, self.name)
455
+ self._input: Optional[WebsocketClientInputTransport] = None
456
+ self._output: Optional[WebsocketClientOutputTransport] = None
457
+
458
+ # Register supported handlers. The user will only be able to register
459
+ # these handlers.
460
+ self._register_event_handler("on_connected")
461
+ self._register_event_handler("on_disconnected")
462
+
463
+ def input(self) -> WebsocketClientInputTransport:
464
+ """Get the input transport for receiving frames.
465
+
466
+ Returns:
467
+ The WebSocket client input transport instance.
468
+ """
469
+ if not self._input:
470
+ self._input = WebsocketClientInputTransport(self, self._session, self._params)
471
+ return self._input
472
+
473
+ def output(self) -> WebsocketClientOutputTransport:
474
+ """Get the output transport for sending frames.
475
+
476
+ Returns:
477
+ The WebSocket client output transport instance.
478
+ """
479
+ if not self._output:
480
+ self._output = WebsocketClientOutputTransport(self, self._session, self._params)
481
+ return self._output
482
+
483
+ async def _on_connected(self, websocket):
484
+ """Handle WebSocket connection established event."""
485
+ await self._call_event_handler("on_connected", websocket)
486
+
487
+ async def _on_disconnected(self, websocket):
488
+ """Handle WebSocket connection closed event."""
489
+ await self._call_event_handler("on_disconnected", websocket)
490
+
491
+ async def _on_message(self, websocket, message):
492
+ """Handle incoming WebSocket message."""
493
+ if self._input:
494
+ await self._input.on_message(websocket, message)