dv-pipecat-ai 0.0.82.dev857__py3-none-any.whl → 0.0.85.dev837__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 (195) hide show
  1. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/METADATA +98 -130
  2. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/RECORD +192 -140
  3. pipecat/adapters/base_llm_adapter.py +38 -1
  4. pipecat/adapters/services/anthropic_adapter.py +9 -14
  5. pipecat/adapters/services/aws_nova_sonic_adapter.py +120 -5
  6. pipecat/adapters/services/bedrock_adapter.py +236 -13
  7. pipecat/adapters/services/gemini_adapter.py +12 -8
  8. pipecat/adapters/services/open_ai_adapter.py +19 -7
  9. pipecat/adapters/services/open_ai_realtime_adapter.py +5 -0
  10. pipecat/audio/dtmf/dtmf-0.wav +0 -0
  11. pipecat/audio/dtmf/dtmf-1.wav +0 -0
  12. pipecat/audio/dtmf/dtmf-2.wav +0 -0
  13. pipecat/audio/dtmf/dtmf-3.wav +0 -0
  14. pipecat/audio/dtmf/dtmf-4.wav +0 -0
  15. pipecat/audio/dtmf/dtmf-5.wav +0 -0
  16. pipecat/audio/dtmf/dtmf-6.wav +0 -0
  17. pipecat/audio/dtmf/dtmf-7.wav +0 -0
  18. pipecat/audio/dtmf/dtmf-8.wav +0 -0
  19. pipecat/audio/dtmf/dtmf-9.wav +0 -0
  20. pipecat/audio/dtmf/dtmf-pound.wav +0 -0
  21. pipecat/audio/dtmf/dtmf-star.wav +0 -0
  22. pipecat/audio/filters/krisp_viva_filter.py +193 -0
  23. pipecat/audio/filters/noisereduce_filter.py +15 -0
  24. pipecat/audio/turn/base_turn_analyzer.py +9 -1
  25. pipecat/audio/turn/smart_turn/base_smart_turn.py +14 -8
  26. pipecat/audio/turn/smart_turn/data/__init__.py +0 -0
  27. pipecat/audio/turn/smart_turn/data/smart-turn-v3.0.onnx +0 -0
  28. pipecat/audio/turn/smart_turn/http_smart_turn.py +6 -2
  29. pipecat/audio/turn/smart_turn/local_smart_turn.py +1 -1
  30. pipecat/audio/turn/smart_turn/local_smart_turn_v2.py +1 -1
  31. pipecat/audio/turn/smart_turn/local_smart_turn_v3.py +124 -0
  32. pipecat/audio/vad/data/README.md +10 -0
  33. pipecat/audio/vad/data/silero_vad_v2.onnx +0 -0
  34. pipecat/audio/vad/silero.py +9 -3
  35. pipecat/audio/vad/vad_analyzer.py +13 -1
  36. pipecat/extensions/voicemail/voicemail_detector.py +5 -5
  37. pipecat/frames/frames.py +277 -86
  38. pipecat/observers/loggers/debug_log_observer.py +3 -3
  39. pipecat/observers/loggers/llm_log_observer.py +7 -3
  40. pipecat/observers/loggers/user_bot_latency_log_observer.py +22 -10
  41. pipecat/pipeline/runner.py +18 -6
  42. pipecat/pipeline/service_switcher.py +64 -36
  43. pipecat/pipeline/task.py +125 -79
  44. pipecat/pipeline/tts_switcher.py +30 -0
  45. pipecat/processors/aggregators/dtmf_aggregator.py +2 -3
  46. pipecat/processors/aggregators/{gated_openai_llm_context.py → gated_llm_context.py} +9 -9
  47. pipecat/processors/aggregators/gated_open_ai_llm_context.py +12 -0
  48. pipecat/processors/aggregators/llm_context.py +40 -2
  49. pipecat/processors/aggregators/llm_response.py +32 -15
  50. pipecat/processors/aggregators/llm_response_universal.py +19 -15
  51. pipecat/processors/aggregators/user_response.py +6 -6
  52. pipecat/processors/aggregators/vision_image_frame.py +24 -2
  53. pipecat/processors/audio/audio_buffer_processor.py +43 -8
  54. pipecat/processors/dtmf_aggregator.py +174 -77
  55. pipecat/processors/filters/stt_mute_filter.py +17 -0
  56. pipecat/processors/frame_processor.py +110 -24
  57. pipecat/processors/frameworks/langchain.py +8 -2
  58. pipecat/processors/frameworks/rtvi.py +210 -68
  59. pipecat/processors/frameworks/strands_agents.py +170 -0
  60. pipecat/processors/logger.py +2 -2
  61. pipecat/processors/transcript_processor.py +26 -5
  62. pipecat/processors/user_idle_processor.py +35 -11
  63. pipecat/runner/daily.py +59 -20
  64. pipecat/runner/run.py +395 -93
  65. pipecat/runner/types.py +6 -4
  66. pipecat/runner/utils.py +51 -10
  67. pipecat/serializers/__init__.py +5 -1
  68. pipecat/serializers/asterisk.py +16 -2
  69. pipecat/serializers/convox.py +41 -4
  70. pipecat/serializers/custom.py +257 -0
  71. pipecat/serializers/exotel.py +5 -5
  72. pipecat/serializers/livekit.py +20 -0
  73. pipecat/serializers/plivo.py +5 -5
  74. pipecat/serializers/protobuf.py +6 -5
  75. pipecat/serializers/telnyx.py +2 -2
  76. pipecat/serializers/twilio.py +43 -23
  77. pipecat/serializers/vi.py +324 -0
  78. pipecat/services/ai_service.py +2 -6
  79. pipecat/services/anthropic/llm.py +2 -25
  80. pipecat/services/assemblyai/models.py +6 -0
  81. pipecat/services/assemblyai/stt.py +13 -5
  82. pipecat/services/asyncai/tts.py +5 -3
  83. pipecat/services/aws/__init__.py +1 -0
  84. pipecat/services/aws/llm.py +147 -105
  85. pipecat/services/aws/nova_sonic/__init__.py +0 -0
  86. pipecat/services/aws/nova_sonic/context.py +436 -0
  87. pipecat/services/aws/nova_sonic/frames.py +25 -0
  88. pipecat/services/aws/nova_sonic/llm.py +1265 -0
  89. pipecat/services/aws/stt.py +3 -3
  90. pipecat/services/aws_nova_sonic/__init__.py +19 -1
  91. pipecat/services/aws_nova_sonic/aws.py +11 -1151
  92. pipecat/services/aws_nova_sonic/context.py +8 -354
  93. pipecat/services/aws_nova_sonic/frames.py +13 -17
  94. pipecat/services/azure/llm.py +51 -1
  95. pipecat/services/azure/realtime/__init__.py +0 -0
  96. pipecat/services/azure/realtime/llm.py +65 -0
  97. pipecat/services/azure/stt.py +15 -0
  98. pipecat/services/cartesia/stt.py +77 -70
  99. pipecat/services/cartesia/tts.py +80 -13
  100. pipecat/services/deepgram/__init__.py +1 -0
  101. pipecat/services/deepgram/flux/__init__.py +0 -0
  102. pipecat/services/deepgram/flux/stt.py +640 -0
  103. pipecat/services/elevenlabs/__init__.py +4 -1
  104. pipecat/services/elevenlabs/stt.py +339 -0
  105. pipecat/services/elevenlabs/tts.py +87 -46
  106. pipecat/services/fish/tts.py +5 -2
  107. pipecat/services/gemini_multimodal_live/events.py +38 -524
  108. pipecat/services/gemini_multimodal_live/file_api.py +23 -173
  109. pipecat/services/gemini_multimodal_live/gemini.py +41 -1403
  110. pipecat/services/gladia/stt.py +56 -72
  111. pipecat/services/google/__init__.py +1 -0
  112. pipecat/services/google/gemini_live/__init__.py +3 -0
  113. pipecat/services/google/gemini_live/file_api.py +189 -0
  114. pipecat/services/google/gemini_live/llm.py +1582 -0
  115. pipecat/services/google/gemini_live/llm_vertex.py +184 -0
  116. pipecat/services/google/llm.py +15 -11
  117. pipecat/services/google/llm_openai.py +3 -3
  118. pipecat/services/google/llm_vertex.py +86 -16
  119. pipecat/services/google/stt.py +4 -0
  120. pipecat/services/google/tts.py +7 -3
  121. pipecat/services/heygen/api.py +2 -0
  122. pipecat/services/heygen/client.py +8 -4
  123. pipecat/services/heygen/video.py +2 -0
  124. pipecat/services/hume/__init__.py +5 -0
  125. pipecat/services/hume/tts.py +220 -0
  126. pipecat/services/inworld/tts.py +6 -6
  127. pipecat/services/llm_service.py +15 -5
  128. pipecat/services/lmnt/tts.py +4 -2
  129. pipecat/services/mcp_service.py +4 -2
  130. pipecat/services/mem0/memory.py +6 -5
  131. pipecat/services/mistral/llm.py +29 -8
  132. pipecat/services/moondream/vision.py +42 -16
  133. pipecat/services/neuphonic/tts.py +5 -2
  134. pipecat/services/openai/__init__.py +1 -0
  135. pipecat/services/openai/base_llm.py +27 -20
  136. pipecat/services/openai/realtime/__init__.py +0 -0
  137. pipecat/services/openai/realtime/context.py +272 -0
  138. pipecat/services/openai/realtime/events.py +1106 -0
  139. pipecat/services/openai/realtime/frames.py +37 -0
  140. pipecat/services/openai/realtime/llm.py +829 -0
  141. pipecat/services/openai/tts.py +49 -10
  142. pipecat/services/openai_realtime/__init__.py +27 -0
  143. pipecat/services/openai_realtime/azure.py +21 -0
  144. pipecat/services/openai_realtime/context.py +21 -0
  145. pipecat/services/openai_realtime/events.py +21 -0
  146. pipecat/services/openai_realtime/frames.py +21 -0
  147. pipecat/services/openai_realtime_beta/azure.py +16 -0
  148. pipecat/services/openai_realtime_beta/openai.py +17 -5
  149. pipecat/services/piper/tts.py +7 -9
  150. pipecat/services/playht/tts.py +34 -4
  151. pipecat/services/rime/tts.py +12 -12
  152. pipecat/services/riva/stt.py +3 -1
  153. pipecat/services/salesforce/__init__.py +9 -0
  154. pipecat/services/salesforce/llm.py +700 -0
  155. pipecat/services/sarvam/__init__.py +7 -0
  156. pipecat/services/sarvam/stt.py +540 -0
  157. pipecat/services/sarvam/tts.py +97 -13
  158. pipecat/services/simli/video.py +2 -2
  159. pipecat/services/speechmatics/stt.py +22 -10
  160. pipecat/services/stt_service.py +47 -0
  161. pipecat/services/tavus/video.py +2 -2
  162. pipecat/services/tts_service.py +75 -22
  163. pipecat/services/vision_service.py +7 -6
  164. pipecat/services/vistaar/llm.py +51 -9
  165. pipecat/tests/utils.py +4 -4
  166. pipecat/transcriptions/language.py +41 -1
  167. pipecat/transports/base_input.py +13 -34
  168. pipecat/transports/base_output.py +140 -104
  169. pipecat/transports/daily/transport.py +199 -26
  170. pipecat/transports/heygen/__init__.py +0 -0
  171. pipecat/transports/heygen/transport.py +381 -0
  172. pipecat/transports/livekit/transport.py +228 -63
  173. pipecat/transports/local/audio.py +6 -1
  174. pipecat/transports/local/tk.py +11 -2
  175. pipecat/transports/network/fastapi_websocket.py +1 -1
  176. pipecat/transports/smallwebrtc/connection.py +103 -19
  177. pipecat/transports/smallwebrtc/request_handler.py +246 -0
  178. pipecat/transports/smallwebrtc/transport.py +65 -23
  179. pipecat/transports/tavus/transport.py +23 -12
  180. pipecat/transports/websocket/client.py +41 -5
  181. pipecat/transports/websocket/fastapi.py +21 -11
  182. pipecat/transports/websocket/server.py +14 -7
  183. pipecat/transports/whatsapp/api.py +8 -0
  184. pipecat/transports/whatsapp/client.py +47 -0
  185. pipecat/utils/base_object.py +54 -22
  186. pipecat/utils/redis.py +58 -0
  187. pipecat/utils/string.py +13 -1
  188. pipecat/utils/tracing/service_decorators.py +21 -21
  189. pipecat/serializers/genesys.py +0 -95
  190. pipecat/services/google/test-google-chirp.py +0 -45
  191. pipecat/services/openai.py +0 -698
  192. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/WHEEL +0 -0
  193. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/licenses/LICENSE +0 -0
  194. {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/top_level.txt +0 -0
  195. /pipecat/services/{aws_nova_sonic → aws/nova_sonic}/ready.wav +0 -0
pipecat/runner/run.py CHANGED
@@ -67,15 +67,22 @@ To run locally:
67
67
 
68
68
  import argparse
69
69
  import asyncio
70
+ import mimetypes
70
71
  import os
71
72
  import sys
73
+ import uuid
72
74
  from contextlib import asynccontextmanager
73
- from typing import Dict
75
+ from http import HTTPMethod
76
+ from pathlib import Path
77
+ from typing import Any, Dict, List, Optional, TypedDict
74
78
 
79
+ import aiohttp
80
+ from fastapi.responses import FileResponse, Response
75
81
  from loguru import logger
76
82
 
77
83
  from pipecat.runner.types import (
78
84
  DailyRunnerArguments,
85
+ RunnerArguments,
79
86
  SmallWebRTCRunnerArguments,
80
87
  WebSocketRunnerArguments,
81
88
  )
@@ -83,7 +90,7 @@ from pipecat.runner.types import (
83
90
  try:
84
91
  import uvicorn
85
92
  from dotenv import load_dotenv
86
- from fastapi import BackgroundTasks, FastAPI, Request, WebSocket
93
+ from fastapi import BackgroundTasks, FastAPI, Header, HTTPException, Request, WebSocket
87
94
  from fastapi.middleware.cors import CORSMiddleware
88
95
  from fastapi.responses import HTMLResponse, RedirectResponse
89
96
  except ImportError as e:
@@ -97,6 +104,12 @@ except ImportError as e:
97
104
  load_dotenv(override=True)
98
105
  os.environ["ENV"] = "local"
99
106
 
107
+ TELEPHONY_TRANSPORTS = ["twilio", "telnyx", "plivo", "exotel"]
108
+
109
+ RUNNER_DOWNLOADS_FOLDER: Optional[str] = None
110
+ RUNNER_HOST: str = "localhost"
111
+ RUNNER_PORT: int = 7860
112
+
100
113
 
101
114
  def _get_bot_module():
102
115
  """Get the bot module from the calling script."""
@@ -151,7 +164,13 @@ async def _run_telephony_bot(websocket: WebSocket):
151
164
 
152
165
 
153
166
  def _create_server_app(
154
- transport_type: str, host: str = "localhost", proxy: str = None, esp32_mode: bool = False
167
+ *,
168
+ transport_type: str,
169
+ host: str = "localhost",
170
+ proxy: str,
171
+ esp32_mode: bool = False,
172
+ whatsapp_enabled: bool = False,
173
+ folder: Optional[str] = None,
155
174
  ):
156
175
  """Create FastAPI app with transport-specific routes."""
157
176
  app = FastAPI()
@@ -166,29 +185,46 @@ def _create_server_app(
166
185
 
167
186
  # Set up transport-specific routes
168
187
  if transport_type == "webrtc":
169
- _setup_webrtc_routes(app, esp32_mode=esp32_mode, host=host)
188
+ _setup_webrtc_routes(app, esp32_mode=esp32_mode, host=host, folder=folder)
189
+ if whatsapp_enabled:
190
+ _setup_whatsapp_routes(app)
170
191
  elif transport_type == "daily":
171
192
  _setup_daily_routes(app)
172
- elif transport_type in ["twilio", "telnyx", "plivo", "exotel"]:
173
- _setup_telephony_routes(app, transport_type, proxy)
193
+ elif transport_type in TELEPHONY_TRANSPORTS:
194
+ _setup_telephony_routes(app, transport_type=transport_type, proxy=proxy)
174
195
  else:
175
196
  logger.warning(f"Unknown transport type: {transport_type}")
176
197
 
177
198
  return app
178
199
 
179
200
 
180
- def _setup_webrtc_routes(app: FastAPI, esp32_mode: bool = False, host: str = "localhost"):
201
+ def _setup_webrtc_routes(
202
+ app: FastAPI, *, esp32_mode: bool = False, host: str = "localhost", folder: Optional[str] = None
203
+ ):
181
204
  """Set up WebRTC-specific routes."""
182
205
  try:
183
206
  from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI
184
207
 
185
- from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection
208
+ from pipecat.transports.smallwebrtc.connection import IceServer, SmallWebRTCConnection
209
+ from pipecat.transports.smallwebrtc.request_handler import (
210
+ IceCandidate,
211
+ SmallWebRTCPatchRequest,
212
+ SmallWebRTCRequest,
213
+ SmallWebRTCRequestHandler,
214
+ )
186
215
  except ImportError as e:
187
216
  logger.error(f"WebRTC transport dependencies not installed: {e}")
188
217
  return
189
218
 
190
- # Store connections by pc_id
191
- pcs_map: Dict[str, SmallWebRTCConnection] = {}
219
+ class IceConfig(TypedDict):
220
+ iceServers: List[IceServer]
221
+
222
+ class StartBotResult(TypedDict, total=False):
223
+ sessionId: str
224
+ iceConfig: Optional[IceConfig]
225
+
226
+ # In-memory store of active sessions: session_id -> session info
227
+ active_sessions: Dict[str, Dict[str, Any]] = {}
192
228
 
193
229
  # Mount the frontend
194
230
  app.mount("/client", SmallWebRTCPrebuiltUI)
@@ -198,62 +234,305 @@ def _setup_webrtc_routes(app: FastAPI, esp32_mode: bool = False, host: str = "lo
198
234
  """Redirect root requests to client interface."""
199
235
  return RedirectResponse(url="/client/")
200
236
 
201
- @app.post("/api/offer")
202
- async def offer(request: dict, background_tasks: BackgroundTasks):
203
- """Handle WebRTC offer requests and manage peer connections."""
204
- pc_id = request.get("pc_id")
205
-
206
- if pc_id and pc_id in pcs_map:
207
- pipecat_connection = pcs_map[pc_id]
208
- logger.info(f"Reusing existing connection for pc_id: {pc_id}")
209
- await pipecat_connection.renegotiate(
210
- sdp=request["sdp"],
211
- type=request["type"],
212
- restart_pc=request.get("restart_pc", False),
213
- )
214
- else:
215
- pipecat_connection = SmallWebRTCConnection()
216
- await pipecat_connection.initialize(sdp=request["sdp"], type=request["type"])
237
+ @app.get("/files/{filename:path}")
238
+ async def download_file(filename: str):
239
+ """Handle file downloads."""
240
+ if not folder:
241
+ logger.warning(f"Attempting to dowload {filename}, but downloads folder not setup.")
242
+ return
243
+
244
+ file_path = Path(folder) / filename
245
+ if not os.path.exists(file_path):
246
+ raise HTTPException(404)
247
+
248
+ media_type, _ = mimetypes.guess_type(file_path)
217
249
 
218
- @pipecat_connection.event_handler("closed")
219
- async def handle_disconnected(webrtc_connection: SmallWebRTCConnection):
220
- """Handle WebRTC connection closure and cleanup."""
221
- logger.info(f"Discarding peer connection for pc_id: {webrtc_connection.pc_id}")
222
- pcs_map.pop(webrtc_connection.pc_id, None)
250
+ return FileResponse(path=file_path, media_type=media_type, filename=filename)
251
+
252
+ # Initialize the SmallWebRTC request handler
253
+ small_webrtc_handler: SmallWebRTCRequestHandler = SmallWebRTCRequestHandler(
254
+ esp32_mode=esp32_mode, host=host
255
+ )
256
+
257
+ @app.post("/api/offer")
258
+ async def offer(request: SmallWebRTCRequest, background_tasks: BackgroundTasks):
259
+ """Handle WebRTC offer requests via SmallWebRTCRequestHandler."""
223
260
 
261
+ # Prepare runner arguments with the callback to run your bot
262
+ async def webrtc_connection_callback(connection):
224
263
  bot_module = _get_bot_module()
225
- runner_args = SmallWebRTCRunnerArguments(webrtc_connection=pipecat_connection)
264
+ runner_args = SmallWebRTCRunnerArguments(webrtc_connection=connection)
226
265
  background_tasks.add_task(bot_module.bot, runner_args)
227
266
 
228
- answer = pipecat_connection.get_answer()
267
+ # Delegate handling to SmallWebRTCRequestHandler
268
+ answer = await small_webrtc_handler.handle_web_request(
269
+ request=request,
270
+ webrtc_connection_callback=webrtc_connection_callback,
271
+ )
272
+ return answer
229
273
 
230
- # Apply ESP32 SDP munging if enabled
231
- if esp32_mode and host != "localhost":
232
- from pipecat.runner.utils import smallwebrtc_sdp_munging
274
+ @app.patch("/api/offer")
275
+ async def ice_candidate(request: SmallWebRTCPatchRequest):
276
+ """Handle WebRTC new ice candidate requests."""
277
+ logger.debug(f"Received patch request: {request}")
278
+ await small_webrtc_handler.handle_patch_request(request)
279
+ return {"status": "success"}
233
280
 
234
- answer["sdp"] = smallwebrtc_sdp_munging(answer["sdp"], host)
281
+ @app.post("/start")
282
+ async def rtvi_start(request: Request):
283
+ """Mimic Pipecat Cloud's /start endpoint."""
284
+ # Parse the request body
285
+ try:
286
+ request_data = await request.json()
287
+ logger.debug(f"Received request: {request_data}")
288
+ except Exception as e:
289
+ logger.error(f"Failed to parse request body: {e}")
290
+ request_data = {}
235
291
 
236
- pcs_map[answer["pc_id"]] = pipecat_connection
237
- return answer
292
+ # Store session info immediately in memory, replicate the behavior expected on Pipecat Cloud
293
+ session_id = str(uuid.uuid4())
294
+ active_sessions[session_id] = request_data
295
+
296
+ result: StartBotResult = {"sessionId": session_id}
297
+ if request_data.get("enableDefaultIceServers"):
298
+ result["iceConfig"] = IceConfig(
299
+ iceServers=[IceServer(urls="stun:stun.l.google.com:19302")]
300
+ )
301
+
302
+ return result
303
+
304
+ @app.api_route(
305
+ "/sessions/{session_id}/{path:path}",
306
+ methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
307
+ )
308
+ async def proxy_request(
309
+ session_id: str, path: str, request: Request, background_tasks: BackgroundTasks
310
+ ):
311
+ """Mimic Pipecat Cloud's proxy."""
312
+ active_session = active_sessions.get(session_id)
313
+ if active_session is None:
314
+ return Response(content="Invalid or not-yet-ready session_id", status_code=404)
315
+
316
+ if path.endswith("api/offer"):
317
+ # Parse the request body and convert to SmallWebRTCRequest
318
+ try:
319
+ request_data = await request.json()
320
+ if request.method == HTTPMethod.POST.value:
321
+ webrtc_request = SmallWebRTCRequest(
322
+ sdp=request_data["sdp"],
323
+ type=request_data["type"],
324
+ pc_id=request_data.get("pc_id"),
325
+ restart_pc=request_data.get("restart_pc"),
326
+ request_data=request_data,
327
+ )
328
+ return await offer(webrtc_request, background_tasks)
329
+ elif request.method == HTTPMethod.PATCH.value:
330
+ patch_request = SmallWebRTCPatchRequest(
331
+ pc_id=request_data["pc_id"],
332
+ candidates=[IceCandidate(**c) for c in request_data.get("candidates", [])],
333
+ )
334
+ return await ice_candidate(patch_request)
335
+ except Exception as e:
336
+ logger.error(f"Failed to parse WebRTC request: {e}")
337
+ return Response(content="Invalid WebRTC request", status_code=400)
338
+
339
+ logger.info(f"Received request for path: {path}")
340
+ return Response(status_code=200)
238
341
 
239
342
  @asynccontextmanager
240
- async def lifespan(app: FastAPI):
343
+ async def smallwebrtc_lifespan(app: FastAPI):
241
344
  """Manage FastAPI application lifecycle and cleanup connections."""
242
345
  yield
243
- coros = [pc.disconnect() for pc in pcs_map.values()]
244
- await asyncio.gather(*coros)
245
- pcs_map.clear()
346
+ await small_webrtc_handler.close()
347
+
348
+ # Add the SmallWebRTC lifespan to the app
349
+ _add_lifespan_to_app(app, smallwebrtc_lifespan)
350
+
351
+
352
+ def _add_lifespan_to_app(app: FastAPI, new_lifespan):
353
+ """Add a new lifespan context manager to the app, combining with existing if present.
354
+
355
+ Args:
356
+ app: The FastAPI application instance
357
+ new_lifespan: The new lifespan context manager to add
358
+ """
359
+ if hasattr(app.router, "lifespan_context") and app.router.lifespan_context is not None:
360
+ # If there's already a lifespan context, combine them
361
+ existing_lifespan = app.router.lifespan_context
362
+
363
+ @asynccontextmanager
364
+ async def combined_lifespan(app: FastAPI):
365
+ async with existing_lifespan(app):
366
+ async with new_lifespan(app):
367
+ yield
368
+
369
+ app.router.lifespan_context = combined_lifespan
370
+ else:
371
+ # No existing lifespan, use the new one
372
+ app.router.lifespan_context = new_lifespan
373
+
374
+
375
+ def _setup_whatsapp_routes(app: FastAPI):
376
+ """Set up WebRTC-specific routes."""
377
+ WHATSAPP_APP_SECRET = os.getenv("WHATSAPP_APP_SECRET")
378
+ WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID")
379
+ WHATSAPP_TOKEN = os.getenv("WHATSAPP_TOKEN")
380
+ WHATSAPP_WEBHOOK_VERIFICATION_TOKEN = os.getenv("WHATSAPP_WEBHOOK_VERIFICATION_TOKEN")
381
+
382
+ if not all(
383
+ [
384
+ WHATSAPP_APP_SECRET,
385
+ WHATSAPP_PHONE_NUMBER_ID,
386
+ WHATSAPP_TOKEN,
387
+ WHATSAPP_WEBHOOK_VERIFICATION_TOKEN,
388
+ ]
389
+ ):
390
+ logger.error(
391
+ """Missing required environment variables for WhatsApp transport:
392
+ WHATSAPP_APP_SECRET
393
+ WHATSAPP_PHONE_NUMBER_ID
394
+ WHATSAPP_TOKEN
395
+ WHATSAPP_WEBHOOK_VERIFICATION_TOKEN
396
+ """
397
+ )
398
+ return
399
+
400
+ try:
401
+ from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI
402
+
403
+ from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection
404
+ from pipecat.transports.smallwebrtc.request_handler import (
405
+ SmallWebRTCRequest,
406
+ SmallWebRTCRequestHandler,
407
+ )
408
+ from pipecat.transports.whatsapp.api import WhatsAppWebhookRequest
409
+ from pipecat.transports.whatsapp.client import WhatsAppClient
410
+ except ImportError as e:
411
+ logger.error(f"WhatsApp transport dependencies not installed: {e}")
412
+ return
413
+
414
+ # Global WhatsApp client instance
415
+ whatsapp_client: Optional[WhatsAppClient] = None
416
+
417
+ @app.get(
418
+ "/whatsapp",
419
+ summary="Verify WhatsApp webhook",
420
+ description="Handles WhatsApp webhook verification requests from Meta",
421
+ )
422
+ async def verify_webhook(request: Request):
423
+ """Verify WhatsApp webhook endpoint.
424
+
425
+ This endpoint is called by Meta's WhatsApp Business API to verify
426
+ the webhook URL during setup. It validates the verification token
427
+ and returns the challenge parameter if successful.
428
+ """
429
+ if whatsapp_client is None:
430
+ logger.error("WhatsApp client is not initialized")
431
+ raise HTTPException(status_code=503, detail="Service unavailable")
246
432
 
247
- app.router.lifespan_context = lifespan
433
+ params = dict(request.query_params)
434
+ logger.debug(f"Webhook verification request received with params: {list(params.keys())}")
435
+
436
+ try:
437
+ result = await whatsapp_client.handle_verify_webhook_request(
438
+ params=params, expected_verification_token=WHATSAPP_WEBHOOK_VERIFICATION_TOKEN
439
+ )
440
+ logger.info("Webhook verification successful")
441
+ return result
442
+ except ValueError as e:
443
+ logger.warning(f"Webhook verification failed: {e}")
444
+ raise HTTPException(status_code=403, detail="Verification failed")
445
+
446
+ @app.post(
447
+ "/whatsapp",
448
+ summary="Handle WhatsApp webhook events",
449
+ description="Processes incoming WhatsApp messages and call events",
450
+ )
451
+ async def whatsapp_webhook(
452
+ body: WhatsAppWebhookRequest,
453
+ background_tasks: BackgroundTasks,
454
+ request: Request,
455
+ x_hub_signature_256: str = Header(None),
456
+ ):
457
+ """Handle incoming WhatsApp webhook events.
458
+
459
+ For call events, establishes WebRTC connections and spawns bot instances
460
+ in the background to handle real-time communication.
461
+ """
462
+ if whatsapp_client is None:
463
+ logger.error("WhatsApp client is not initialized")
464
+ raise HTTPException(status_code=503, detail="Service unavailable")
465
+
466
+ # Validate webhook object type
467
+ if body.object != "whatsapp_business_account":
468
+ logger.warning(f"Invalid webhook object type: {body.object}")
469
+ raise HTTPException(status_code=400, detail="Invalid object type")
470
+
471
+ logger.debug(f"Processing WhatsApp webhook: {body.model_dump()}")
472
+
473
+ async def connection_callback(connection: SmallWebRTCConnection):
474
+ """Handle new WebRTC connections from WhatsApp calls.
475
+
476
+ Called when a WebRTC connection is established for a WhatsApp call.
477
+ Spawns a bot instance to handle the conversation.
478
+
479
+ Args:
480
+ connection: The established WebRTC connection
481
+ """
482
+ bot_module = _get_bot_module()
483
+ runner_args = SmallWebRTCRunnerArguments(webrtc_connection=connection)
484
+ background_tasks.add_task(bot_module.bot, runner_args)
485
+
486
+ try:
487
+ # Process the webhook request
488
+ raw_body = await request.body()
489
+ result = await whatsapp_client.handle_webhook_request(
490
+ body, connection_callback, sha256_signature=x_hub_signature_256, raw_body=raw_body
491
+ )
492
+ logger.debug(f"Webhook processed successfully: {result}")
493
+ return {"status": "success", "message": "Webhook processed successfully"}
494
+ except ValueError as ve:
495
+ logger.warning(f"Invalid webhook request format: {ve}")
496
+ raise HTTPException(status_code=400, detail=f"Invalid request: {str(ve)}")
497
+ except Exception as e:
498
+ logger.error(f"Internal error processing webhook: {e}")
499
+ raise HTTPException(status_code=500, detail="Internal server error processing webhook")
500
+
501
+ @asynccontextmanager
502
+ async def whatsapp_lifespan(app: FastAPI):
503
+ """Manage WhatsApp client lifecycle and cleanup connections."""
504
+ nonlocal whatsapp_client
505
+
506
+ # Initialize WhatsApp client with persistent HTTP session
507
+ async with aiohttp.ClientSession() as session:
508
+ whatsapp_client = WhatsAppClient(
509
+ whatsapp_token=WHATSAPP_TOKEN,
510
+ whatsapp_secret=WHATSAPP_APP_SECRET,
511
+ phone_number_id=WHATSAPP_PHONE_NUMBER_ID,
512
+ session=session,
513
+ )
514
+ logger.info("WhatsApp client initialized successfully")
515
+
516
+ try:
517
+ yield # Run the application
518
+ finally:
519
+ # Cleanup all active calls on shutdown
520
+ logger.info("Cleaning up WhatsApp client resources...")
521
+ if whatsapp_client:
522
+ await whatsapp_client.terminate_all_calls()
523
+ logger.info("WhatsApp cleanup completed")
524
+
525
+ # Add the WhatsApp lifespan to the app
526
+ _add_lifespan_to_app(app, whatsapp_lifespan)
248
527
 
249
528
 
250
529
  def _setup_daily_routes(app: FastAPI):
251
530
  """Set up Daily-specific routes."""
252
531
 
253
532
  @app.get("/")
254
- async def start_agent():
533
+ async def create_room_and_start_agent():
255
534
  """Launch a Daily bot and redirect to room."""
256
- print("Starting bot with Daily transport")
535
+ print("Starting bot with Daily transport and redirecting to Daily room")
257
536
 
258
537
  import aiohttp
259
538
 
@@ -268,11 +547,11 @@ def _setup_daily_routes(app: FastAPI):
268
547
  asyncio.create_task(bot_module.bot(runner_args))
269
548
  return RedirectResponse(room_url)
270
549
 
271
- async def _handle_rtvi_request(request: Request):
272
- """Common handler for both /start and /connect endpoints.
550
+ @app.post("/start")
551
+ async def start_agent(request: Request):
552
+ """Handler for /start endpoints.
273
553
 
274
554
  Expects POST body like::
275
-
276
555
  {
277
556
  "createDailyRoom": true,
278
557
  "dailyRoomProperties": { "start_video_off": true },
@@ -289,50 +568,35 @@ def _setup_daily_routes(app: FastAPI):
289
568
  logger.error(f"Failed to parse request body: {e}")
290
569
  request_data = {}
291
570
 
292
- # Extract the body data that should be passed to the bot
293
- # This mimics Pipecat Cloud's behavior
294
- bot_body = request_data.get("body", {})
571
+ create_daily_room = request_data.get("createDailyRoom", False)
572
+ body = request_data.get("body", {})
295
573
 
296
- # Log the extracted body data for debugging
297
- if bot_body:
298
- logger.info(f"Extracted body data for bot: {bot_body}")
299
- else:
300
- logger.debug("No body data provided in request")
301
-
302
- import aiohttp
303
-
304
- from pipecat.runner.daily import configure
574
+ bot_module = _get_bot_module()
305
575
 
306
- async with aiohttp.ClientSession() as session:
307
- room_url, token = await configure(session)
576
+ result = None
577
+ if create_daily_room:
578
+ import aiohttp
308
579
 
309
- # Start the bot in the background with extracted body data
310
- bot_module = _get_bot_module()
311
- runner_args = DailyRunnerArguments(room_url=room_url, token=token, body=bot_body)
312
- asyncio.create_task(bot_module.bot(runner_args))
313
- # Match PCC /start endpoint response format:
314
- return {"dailyRoom": room_url, "dailyToken": token}
580
+ from pipecat.runner.daily import configure
315
581
 
316
- @app.post("/start")
317
- async def rtvi_start(request: Request):
318
- """Launch a Daily bot and return connection info for RTVI clients."""
319
- return await _handle_rtvi_request(request)
582
+ async with aiohttp.ClientSession() as session:
583
+ room_url, token = await configure(session)
584
+ runner_args = DailyRunnerArguments(room_url=room_url, token=token, body=body)
585
+ result = {
586
+ "dailyRoom": room_url,
587
+ "dailyToken": token,
588
+ "sessionId": str(uuid.uuid4()),
589
+ }
590
+ else:
591
+ runner_args = RunnerArguments(body=body)
320
592
 
321
- @app.post("/connect")
322
- async def rtvi_connect(request: Request):
323
- """Launch a Daily bot and return connection info for RTVI clients.
593
+ # Start the bot in the background
594
+ asyncio.create_task(bot_module.bot(runner_args))
324
595
 
325
- .. deprecated:: 0.0.78
326
- Use /start instead. This endpoint will be removed in a future version.
327
- """
328
- logger.warning(
329
- "DEPRECATED: /connect endpoint is deprecated. Please use /start instead. "
330
- "This endpoint will be removed in a future version."
331
- )
332
- return await _handle_rtvi_request(request)
596
+ return result
333
597
 
334
598
 
335
- def _setup_telephony_routes(app: FastAPI, transport_type: str, proxy: str):
599
+ def _setup_telephony_routes(app: FastAPI, *, transport_type: str, proxy: str):
336
600
  """Set up telephony-specific routes."""
337
601
  # XML response templates (Exotel doesn't use XML webhooks)
338
602
  XML_TEMPLATES = {
@@ -388,8 +652,6 @@ def _setup_telephony_routes(app: FastAPI, transport_type: str, proxy: str):
388
652
  async def _run_daily_direct():
389
653
  """Run Daily bot with direct connection (no FastAPI server)."""
390
654
  try:
391
- import aiohttp
392
-
393
655
  from pipecat.runner.daily import configure
394
656
  except ImportError as e:
395
657
  logger.error("Daily transport dependencies not installed.")
@@ -435,6 +697,21 @@ def _validate_and_clean_proxy(proxy: str) -> str:
435
697
  return proxy
436
698
 
437
699
 
700
+ def runner_downloads_folder() -> Optional[str]:
701
+ """Returns the folder where files are stored for later download."""
702
+ return RUNNER_DOWNLOADS_FOLDER
703
+
704
+
705
+ def runner_host() -> str:
706
+ """Returns the host name of this runner."""
707
+ return RUNNER_HOST
708
+
709
+
710
+ def runner_port() -> int:
711
+ """Returns the port of this runner."""
712
+ return RUNNER_PORT
713
+
714
+
438
715
  def main():
439
716
  """Start the Pipecat development runner.
440
717
 
@@ -455,14 +732,16 @@ def main():
455
732
 
456
733
  The bot file must contain a `bot(runner_args)` function as the entry point.
457
734
  """
735
+ global RUNNER_DOWNLOADS_FOLDER, RUNNER_HOST, RUNNER_PORT
736
+
458
737
  parser = argparse.ArgumentParser(description="Pipecat Development Runner")
459
- parser.add_argument("--host", type=str, default="localhost", help="Host address")
460
- parser.add_argument("--port", type=int, default=7860, help="Port number")
738
+ parser.add_argument("--host", type=str, default=RUNNER_HOST, help="Host address")
739
+ parser.add_argument("--port", type=int, default=RUNNER_PORT, help="Port number")
461
740
  parser.add_argument(
462
741
  "-t",
463
742
  "--transport",
464
743
  type=str,
465
- choices=["daily", "webrtc", "twilio", "telnyx", "plivo", "exotel"],
744
+ choices=["daily", "webrtc", *TELEPHONY_TRANSPORTS],
466
745
  default="webrtc",
467
746
  help="Transport type",
468
747
  )
@@ -480,9 +759,16 @@ def main():
480
759
  default=False,
481
760
  help="Connect directly to Daily room (automatically sets transport to daily)",
482
761
  )
762
+ parser.add_argument("-f", "--folder", type=str, help="Path to downloads folder")
483
763
  parser.add_argument(
484
764
  "--verbose", "-v", action="count", default=0, help="Increase logging verbosity"
485
765
  )
766
+ parser.add_argument(
767
+ "--whatsapp",
768
+ action="store_true",
769
+ default=False,
770
+ help="Ensure requried WhatsApp environment variables are present",
771
+ )
486
772
 
487
773
  args = parser.parse_args()
488
774
 
@@ -502,6 +788,10 @@ def main():
502
788
  logger.error("For ESP32, you need to specify `--host IP` so we can do SDP munging.")
503
789
  return
504
790
 
791
+ if args.transport in TELEPHONY_TRANSPORTS and not args.proxy:
792
+ logger.error(f"For telephony transports, you need to specify `--proxy PROXY`.")
793
+ return
794
+
505
795
  # Log level
506
796
  logger.remove()
507
797
  logger.add(sys.stderr, level="TRACE" if args.verbose else "DEBUG")
@@ -521,10 +811,11 @@ def main():
521
811
  print()
522
812
  if args.esp32:
523
813
  print(f"🚀 Bot ready! (ESP32 mode)")
524
- print(f" → Open http://{args.host}:{args.port}/client in your browser")
814
+ elif args.whatsapp:
815
+ print(f"🚀 Bot ready! (WhatsApp)")
525
816
  else:
526
817
  print(f"🚀 Bot ready!")
527
- print(f" → Open http://{args.host}:{args.port}/client in your browser")
818
+ print(f" → Open http://{args.host}:{args.port}/client in your browser")
528
819
  print()
529
820
  elif args.transport == "daily":
530
821
  print()
@@ -532,8 +823,19 @@ def main():
532
823
  print(f" → Open http://{args.host}:{args.port} in your browser to start a session")
533
824
  print()
534
825
 
826
+ RUNNER_DOWNLOADS_FOLDER = args.folder
827
+ RUNNER_HOST = args.host
828
+ RUNNER_PORT = args.port
829
+
535
830
  # Create the app with transport-specific setup
536
- app = _create_server_app(args.transport, args.host, args.proxy, args.esp32)
831
+ app = _create_server_app(
832
+ transport_type=args.transport,
833
+ host=args.host,
834
+ proxy=args.proxy,
835
+ esp32_mode=args.esp32,
836
+ whatsapp_enabled=args.whatsapp,
837
+ folder=args.folder,
838
+ )
537
839
 
538
840
  # Run the server
539
841
  uvicorn.run(app, host=args.host, port=args.port)
pipecat/runner/types.py CHANGED
@@ -20,9 +20,11 @@ from fastapi import WebSocket
20
20
  class RunnerArguments:
21
21
  """Base class for runner session arguments."""
22
22
 
23
- handle_sigint: bool = field(init=False)
24
- handle_sigterm: bool = field(init=False)
25
- pipeline_idle_timeout_secs: int = field(init=False)
23
+ # Use kw_only so subclasses don't need to worry about ordering.
24
+ handle_sigint: bool = field(init=False, kw_only=True)
25
+ handle_sigterm: bool = field(init=False, kw_only=True)
26
+ pipeline_idle_timeout_secs: int = field(init=False, kw_only=True)
27
+ body: Optional[Any] = field(default_factory=dict, kw_only=True)
26
28
 
27
29
  def __post_init__(self):
28
30
  self.handle_sigint = False
@@ -42,7 +44,6 @@ class DailyRunnerArguments(RunnerArguments):
42
44
 
43
45
  room_url: str
44
46
  token: Optional[str] = None
45
- body: Optional[Any] = field(default_factory=dict)
46
47
 
47
48
 
48
49
  @dataclass
@@ -51,6 +52,7 @@ class WebSocketRunnerArguments(RunnerArguments):
51
52
 
52
53
  Parameters:
53
54
  websocket: WebSocket connection for audio streaming
55
+ body: Additional request data
54
56
  """
55
57
 
56
58
  websocket: WebSocket