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
@@ -0,0 +1,364 @@
1
+ #
2
+ # Copyright (c) 2024–2025, Daily
3
+ #
4
+ # SPDX-License-Identifier: BSD 2-Clause License
5
+ #
6
+
7
+ """WhatsApp API Client.
8
+
9
+ This module provides a client for communicating with the WhatsApp Cloud API,
10
+ handling webhook requests, managing WebRTC connections, and processing
11
+ WhatsApp call events.
12
+ """
13
+
14
+ import asyncio
15
+ from typing import Awaitable, Callable, Dict, List, Optional
16
+
17
+ import aiohttp
18
+ from loguru import logger
19
+
20
+ from pipecat.transports.smallwebrtc.connection import IceServer, SmallWebRTCConnection
21
+ from pipecat.transports.whatsapp.api import (
22
+ WhatsAppApi,
23
+ WhatsAppConnectCall,
24
+ WhatsAppConnectCallValue,
25
+ WhatsAppTerminateCall,
26
+ WhatsAppTerminateCallValue,
27
+ WhatsAppWebhookRequest,
28
+ )
29
+
30
+
31
+ class WhatsAppClient:
32
+ """WhatsApp Cloud API client for handling calls and webhook requests.
33
+
34
+ This client manages WhatsApp call connections using WebRTC, processes webhook
35
+ events from WhatsApp, and maintains ongoing call state. It supports both
36
+ incoming call handling and call termination through the WhatsApp Cloud API.
37
+
38
+ Attributes:
39
+ _whatsapp_api: WhatsApp API instance for making API calls
40
+ _ongoing_calls_map: Dictionary mapping call IDs to WebRTC connections
41
+ _ice_servers: List of ICE servers for WebRTC connections
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ whatsapp_token: str,
47
+ phone_number_id: str,
48
+ session: aiohttp.ClientSession,
49
+ ice_servers: Optional[List[IceServer]] = None,
50
+ ) -> None:
51
+ """Initialize the WhatsApp client.
52
+
53
+ Args:
54
+ whatsapp_token: WhatsApp API access token
55
+ phone_number_id: WhatsApp phone number ID for the business account
56
+ session: aiohttp session for making HTTP requests
57
+ ice_servers: List of ICE servers for WebRTC connections. If None,
58
+ defaults to Google's public STUN server
59
+ """
60
+ self._whatsapp_api = WhatsAppApi(
61
+ whatsapp_token=whatsapp_token, phone_number_id=phone_number_id, session=session
62
+ )
63
+ self._ongoing_calls_map: Dict[str, SmallWebRTCConnection] = {}
64
+
65
+ # Set default ICE servers if none provided
66
+ if ice_servers is None:
67
+ self._ice_servers = [IceServer(urls="stun:stun.l.google.com:19302")]
68
+ else:
69
+ self._ice_servers = ice_servers
70
+
71
+ async def terminate_all_calls(self) -> None:
72
+ """Terminate all ongoing WhatsApp calls.
73
+
74
+ This method will:
75
+ 1. Send termination requests to WhatsApp API for each ongoing call
76
+ 2. Disconnect all WebRTC connections
77
+ 3. Clear the ongoing calls map
78
+
79
+ All terminations are executed concurrently for efficiency.
80
+ """
81
+ logger.debug("Will terminate all ongoing WhatsApp calls")
82
+
83
+ if not self._ongoing_calls_map:
84
+ logger.debug("No ongoing calls to terminate")
85
+ return
86
+
87
+ logger.debug(f"Terminating {len(self._ongoing_calls_map)} ongoing calls")
88
+
89
+ # Terminate each call via WhatsApp API
90
+ termination_tasks = []
91
+ for call_id, pipecat_connection in self._ongoing_calls_map.items():
92
+ logger.debug(f"Terminating call {call_id}")
93
+ # Call WhatsApp API to terminate the call
94
+ if self._whatsapp_api:
95
+ termination_tasks.append(self._whatsapp_api.terminate_call_to_whatsapp(call_id))
96
+ # Disconnect the pipecat connection
97
+ termination_tasks.append(pipecat_connection.disconnect())
98
+
99
+ # Execute all terminations concurrently
100
+ await asyncio.gather(*termination_tasks, return_exceptions=True)
101
+
102
+ # Clear the ongoing calls map
103
+ self._ongoing_calls_map.clear()
104
+ logger.debug("All calls terminated successfully")
105
+
106
+ async def handle_verify_webhook_request(
107
+ self, params: Dict[str, str], expected_verification_token: str
108
+ ) -> int:
109
+ """Handle a verify webhook request from WhatsApp.
110
+
111
+ Args:
112
+ params: Dictionary containing webhook parameters from query string
113
+ expected_verification_token: The expected verification token to validate against
114
+
115
+ Returns:
116
+ int: The challenge value if verification succeeds
117
+
118
+ Raises:
119
+ ValueError: If verification fails due to missing parameters or invalid token
120
+ """
121
+ mode = params.get("hub.mode")
122
+ challenge = params.get("hub.challenge")
123
+ verify_token = params.get("hub.verify_token")
124
+
125
+ if not mode or not challenge or not verify_token:
126
+ raise ValueError("Missing required webhook verification parameters")
127
+
128
+ if mode != "subscribe":
129
+ raise ValueError(f"Invalid hub mode: expected 'subscribe', got '{mode}'")
130
+
131
+ if verify_token != expected_verification_token:
132
+ raise ValueError("Webhook verification token mismatch")
133
+
134
+ return int(challenge)
135
+
136
+ async def handle_webhook_request(
137
+ self,
138
+ request: WhatsAppWebhookRequest,
139
+ connection_callback: Optional[Callable[[SmallWebRTCConnection], Awaitable[None]]] = None,
140
+ ) -> bool:
141
+ """Handle a webhook request from WhatsApp.
142
+
143
+ This method processes incoming webhook requests and handles both
144
+ connect and terminate events. For connect events, it establishes
145
+ a WebRTC connection and optionally invokes a callback with the
146
+ new connection.
147
+
148
+ Args:
149
+ request: The webhook request from WhatsApp containing call events
150
+ connection_callback: Optional callback function to invoke when a new
151
+ WebRTC connection is established. The callback
152
+ receives the SmallWebRTCConnection instance.
153
+
154
+ Returns:
155
+ bool: True if the webhook request was handled successfully, False otherwise
156
+
157
+ Raises:
158
+ ValueError: If the webhook request contains no supported events
159
+ Exception: If connection establishment or API calls fail
160
+ """
161
+ try:
162
+ for entry in request.entry:
163
+ for change in entry.changes:
164
+ # Handle connect events
165
+ if isinstance(change.value, WhatsAppConnectCallValue):
166
+ for call in change.value.calls:
167
+ if call.event == "connect":
168
+ logger.debug(f"Processing connect event for call {call.id}")
169
+ try:
170
+ connection = await self._handle_connect_event(call)
171
+
172
+ # Invoke callback if provided
173
+ if connection_callback and connection:
174
+ try:
175
+ await connection_callback(connection)
176
+ logger.debug(
177
+ f"Connection callback executed successfully for call {call.id}"
178
+ )
179
+ except Exception as callback_error:
180
+ logger.error(
181
+ f"Connection callback failed for call {call.id}: {callback_error}"
182
+ )
183
+ # Continue execution despite callback failure
184
+
185
+ return True
186
+ except Exception as connect_error:
187
+ logger.error(
188
+ f"Failed to handle connect event for call {call.id}: {connect_error}"
189
+ )
190
+ raise
191
+
192
+ # Handle terminate events
193
+ elif isinstance(change.value, WhatsAppTerminateCallValue):
194
+ for call in change.value.calls:
195
+ if call.event == "terminate":
196
+ logger.debug(f"Processing terminate event for call {call.id}")
197
+ try:
198
+ return await self._handle_terminate_event(call)
199
+ except Exception as terminate_error:
200
+ logger.error(
201
+ f"Failed to handle terminate event for call {call.id}: {terminate_error}"
202
+ )
203
+ raise
204
+
205
+ # No supported events found
206
+ error_msg = "No supported event found in webhook request"
207
+ logger.warning(f"{error_msg}: {request}")
208
+ raise ValueError(error_msg)
209
+
210
+ except Exception as e:
211
+ logger.error(f"Error processing webhook request: {e}")
212
+ logger.debug(f"Webhook request details: {request}")
213
+ raise
214
+
215
+ def _filter_sdp_for_whatsapp(self, sdp: str) -> str:
216
+ """Filter SDP to be compatible with WhatsApp requirements.
217
+
218
+ WhatsApp only supports SHA-256 fingerprints, so this method removes
219
+ other fingerprint types from the SDP.
220
+
221
+ Args:
222
+ sdp: The original SDP string
223
+
224
+ Returns:
225
+ Filtered SDP string compatible with WhatsApp
226
+ """
227
+ lines = sdp.splitlines()
228
+ filtered = []
229
+ for line in lines:
230
+ if line.startswith("a=fingerprint:") and not line.startswith("a=fingerprint:sha-256"):
231
+ continue # drop sha-384 / sha-512
232
+ filtered.append(line)
233
+ return "\r\n".join(filtered) + "\r\n"
234
+
235
+ async def _handle_connect_event(self, call: WhatsAppConnectCall) -> SmallWebRTCConnection:
236
+ """Handle a CONNECT event by establishing WebRTC connection and accepting the call.
237
+
238
+ This method:
239
+ 1. Creates a new WebRTC connection using configured ICE servers
240
+ 2. Initializes the connection with the provided SDP
241
+ 3. Generates an SDP answer and filters it for WhatsApp compatibility
242
+ 4. Pre-accepts the call with WhatsApp API
243
+ 5. Accepts the call with WhatsApp API
244
+ 6. Stores the connection for later management
245
+
246
+ Args:
247
+ call: WhatsApp connect call event
248
+
249
+ Returns:
250
+ The established SmallWebRTCConnection instance
251
+
252
+ Raises:
253
+ Exception: If pre-accept or accept API calls fail
254
+ """
255
+ logger.debug(f"Incoming call from {call.from_}, call_id: {call.id}")
256
+
257
+ pipecat_connection = None
258
+ try:
259
+ # Create and initialize WebRTC connection
260
+ pipecat_connection = SmallWebRTCConnection(self._ice_servers)
261
+ await pipecat_connection.initialize(sdp=call.session.sdp, type=call.session.sdp_type)
262
+ sdp_answer = pipecat_connection.get_answer().get("sdp")
263
+ sdp_answer = self._filter_sdp_for_whatsapp(sdp_answer)
264
+
265
+ logger.debug(f"SDP answer generated for call {call.id}")
266
+
267
+ # Pre-accept the call
268
+ try:
269
+ pre_accept_resp = await self._whatsapp_api.answer_call_to_whatsapp(
270
+ call.id, "pre_accept", sdp_answer, call.from_
271
+ )
272
+ if not pre_accept_resp.get("success", False):
273
+ logger.error(f"Failed to pre-accept call {call.id}: {pre_accept_resp}")
274
+ raise Exception(f"Failed to pre-accept call: {pre_accept_resp}")
275
+
276
+ logger.debug(f"Pre-accept successful for call {call.id}")
277
+ except Exception as e:
278
+ logger.error(f"Pre-accept API call failed for call {call.id}: {e}")
279
+ raise Exception(f"Failed to pre-accept call: {e}")
280
+
281
+ # Accept the call
282
+ try:
283
+ accept_resp = await self._whatsapp_api.answer_call_to_whatsapp(
284
+ call.id, "accept", sdp_answer, call.from_
285
+ )
286
+ if not accept_resp.get("success", False):
287
+ logger.error(f"Failed to accept call {call.id}: {accept_resp}")
288
+ raise Exception(f"Failed to accept call: {accept_resp}")
289
+
290
+ logger.debug(f"Accept successful for call {call.id}")
291
+ except Exception as e:
292
+ logger.error(f"Accept API call failed for call {call.id}: {e}")
293
+ raise Exception(f"Failed to accept call: {e}")
294
+
295
+ # Store the connection for management
296
+ self._ongoing_calls_map[call.id] = pipecat_connection
297
+
298
+ # Set up disconnect handler
299
+ @pipecat_connection.event_handler("closed")
300
+ async def handle_disconnected(webrtc_connection: SmallWebRTCConnection):
301
+ logger.debug(
302
+ f"Peer connection closed: {webrtc_connection.pc_id} for call {call.id}"
303
+ )
304
+ # Clean up from ongoing calls map
305
+ self._ongoing_calls_map.pop(call.id, None)
306
+
307
+ logger.debug(f"WebRTC connection established successfully for call {call.id}")
308
+ return pipecat_connection
309
+
310
+ except Exception as e:
311
+ # Clean up connection on failure
312
+ if pipecat_connection:
313
+ try:
314
+ await pipecat_connection.disconnect()
315
+ except Exception as cleanup_error:
316
+ logger.error(
317
+ f"Failed to cleanup connection for call {call.id}: {cleanup_error}"
318
+ )
319
+
320
+ logger.error(f"Failed to handle connect event for call {call.id}: {e}")
321
+ raise
322
+
323
+ async def _handle_terminate_event(self, call: WhatsAppTerminateCall) -> bool:
324
+ """Handle a TERMINATE event by cleaning up resources and logging call completion.
325
+
326
+ This method:
327
+ 1. Logs call termination details including duration if available
328
+ 2. Disconnects the associated WebRTC connection
329
+ 3. Removes the call from the ongoing calls map
330
+
331
+ Args:
332
+ call: WhatsApp terminate call event
333
+
334
+ Returns:
335
+ bool: True if the call was terminated successfully, False otherwise
336
+ """
337
+ logger.debug(f"Call terminated from {call.from_}, call_id: {call.id}")
338
+ logger.debug(f"Call status: {call.status}")
339
+ if call.duration:
340
+ logger.debug(f"Call duration: {call.duration} seconds")
341
+
342
+ try:
343
+ if call.id in self._ongoing_calls_map:
344
+ pipecat_connection = self._ongoing_calls_map[call.id]
345
+ logger.debug(f"Disconnecting WebRTC connection for call {call.id}")
346
+
347
+ try:
348
+ await pipecat_connection.disconnect()
349
+ logger.debug(f"WebRTC connection disconnected successfully for call {call.id}")
350
+ except Exception as disconnect_error:
351
+ logger.error(
352
+ f"Failed to disconnect WebRTC connection for call {call.id}: {disconnect_error}"
353
+ )
354
+
355
+ # Remove from ongoing calls map
356
+ self._ongoing_calls_map.pop(call.id, None)
357
+ else:
358
+ logger.warning(f"Call {call.id} not found in ongoing calls map")
359
+
360
+ return True
361
+
362
+ except Exception as e:
363
+ logger.error(f"Error handling terminate event for call {call.id}: {e}")
364
+ return False