Clean up logging, refactor cloud.py's _create_server_app

This commit is contained in:
Mark Backman
2025-07-28 08:18:36 -04:00
parent f5e23c36a4
commit 83a88d7c85
3 changed files with 170 additions and 167 deletions

View File

@@ -7,10 +7,11 @@
"""Pipecat Cloud-compatible development server for running Pipecat bots. """Pipecat Cloud-compatible development server for running Pipecat bots.
This module provides a FastAPI-based development server that can run bots This module provides a FastAPI-based development server that can run bots
structured for Pipecat Cloud deployment. It supports multiple transport types structured for Pipecat Cloud deployment. The runner enables you to run Pipecat
and handles room/token management automatically. bots locally or deployed without requiring any code changes. It supports
multiple transport types and handles room/token management automatically.
It requires the pipecatcloud package for proper session argument types. It requires the `pipecatcloud` package for proper session argument types.
Install with:: Install with::
@@ -72,7 +73,13 @@ Example::
from pipecat.runner.cloud import main from pipecat.runner.cloud import main
main() main()
Then run: `python bot.py -t daily` or `python bot.py -t webrtc` To run locally:
- Daily: `python bot.py -t daily`
- WebRTC: `python bot.py -t webrtc`
- Telephony: `python bot.py -t twilio -x your_username.ngork.io`
- Telephony: `python bot.py -t telnyx -x your_username.ngork.io`
- Telephony: `python bot.py -t plivo -x your_username.ngork
""" """
import argparse import argparse
@@ -90,7 +97,6 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from loguru import logger from loguru import logger
# Require pipecatcloud for cloud-compatible bots
try: try:
from pipecatcloud.agent import DailySessionArguments, WebSocketSessionArguments from pipecatcloud.agent import DailySessionArguments, WebSocketSessionArguments
except ImportError: except ImportError:
@@ -180,21 +186,33 @@ def _create_server_app(transport_type: str, host: str = "localhost", proxy: str
allow_headers=["*"], allow_headers=["*"],
) )
# Add transport-specific routes # Set up transport-specific routes
if transport_type == "webrtc": if transport_type == "webrtc":
# WebRTC setup _setup_webrtc_routes(app)
elif transport_type == "daily":
_setup_daily_routes(app)
elif transport_type in ["twilio", "telnyx", "plivo"]:
_setup_telephony_routes(app, transport_type, proxy)
else:
logger.warning(f"Unknown transport type: {transport_type}")
return app
def _setup_webrtc_routes(app: FastAPI):
"""Set up WebRTC-specific routes."""
try: try:
from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI
from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection
except ImportError as e: except ImportError as e:
logger.error(f"WebRTC transport dependencies not installed.") logger.error(f"WebRTC transport dependencies not installed.")
return app return
# Store connections by pc_id # Store connections by pc_id
pcs_map: Dict[str, SmallWebRTCConnection] = {} pcs_map: Dict[str, SmallWebRTCConnection] = {}
# Mount the frontend at / # Mount the frontend
app.mount("/client", SmallWebRTCPrebuiltUI) app.mount("/client", SmallWebRTCPrebuiltUI)
@app.get("/", include_in_schema=False) @app.get("/", include_in_schema=False)
@@ -226,7 +244,6 @@ def _create_server_app(transport_type: str, host: str = "localhost", proxy: str
pcs_map.pop(webrtc_connection.pc_id, None) pcs_map.pop(webrtc_connection.pc_id, None)
bot_module = _get_bot_module() bot_module = _get_bot_module()
session_args = SmallWebRTCSessionArguments( session_args = SmallWebRTCSessionArguments(
webrtc_connection=pipecat_connection, webrtc_connection=pipecat_connection,
session_id=None, session_id=None,
@@ -240,62 +257,22 @@ def _create_server_app(transport_type: str, host: str = "localhost", proxy: str
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""Manage FastAPI application lifecycle and cleanup connections.""" """Manage FastAPI application lifecycle and cleanup connections."""
yield # Run app yield
coros = [pc.disconnect() for pc in pcs_map.values()] coros = [pc.disconnect() for pc in pcs_map.values()]
await asyncio.gather(*coros) await asyncio.gather(*coros)
pcs_map.clear() pcs_map.clear()
app.router.lifespan_context = lifespan app.router.lifespan_context = lifespan
elif transport_type in ["twilio", "telnyx", "plivo"]:
# Direct telephony WebSocket handling (no utils dependency)
@app.post("/") def _setup_daily_routes(app: FastAPI):
async def start_call(): """Set up Daily-specific routes."""
"""Handle telephony webhook and return XML response."""
logger.debug(f"POST {transport_type.upper()} XML")
if transport_type == "twilio":
xml_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://{proxy}/ws"></Stream>
</Connect>
<Pause length="40"/>
</Response>"""
elif transport_type == "telnyx":
xml_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://{proxy}/ws" bidirectionalMode="rtp"></Stream>
</Connect>
<Pause length="40"/>
</Response>"""
elif transport_type == "plivo":
xml_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Stream bidirectional="true" keepCallAlive="true" contentType="audio/x-mulaw;rate=8000">wss://{proxy}/ws</Stream>
</Response>"""
else:
xml_content = "<Response></Response>"
return HTMLResponse(content=xml_content, media_type="application/xml")
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""Handle WebSocket connections for telephony."""
await websocket.accept()
logger.debug("WebSocket connection accepted")
await _run_telephony_bot(websocket)
# Add general routes
@app.get("/") @app.get("/")
async def start_agent(): async def start_agent():
"""Launch a bot and redirect appropriately.""" """Launch a Daily bot and redirect to room."""
print(f"Starting bot with {transport_type} transport") print("Starting bot with Daily transport")
if transport_type == "daily":
# Create Daily room and start bot
import aiohttp import aiohttp
from pipecat.runner.daily import configure from pipecat.runner.daily import configure
@@ -303,7 +280,7 @@ def _create_server_app(transport_type: str, host: str = "localhost", proxy: str
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
room_url, token = await configure(session) room_url, token = await configure(session)
# Start the bot in the background to join the room # Start the bot in the background
bot_module = _get_bot_module() bot_module = _get_bot_module()
session_args = DailySessionArguments( session_args = DailySessionArguments(
room_url=room_url, token=token, body={}, session_id=None room_url=room_url, token=token, body={}, session_id=None
@@ -311,17 +288,11 @@ def _create_server_app(transport_type: str, host: str = "localhost", proxy: str
asyncio.create_task(bot_module.bot(session_args)) asyncio.create_task(bot_module.bot(session_args))
return RedirectResponse(room_url) return RedirectResponse(room_url)
elif transport_type == "webrtc":
return RedirectResponse("/client/")
else:
return {"status": f"Bot started with {transport_type}"}
@app.post("/connect") @app.post("/connect")
async def rtvi_connect(): async def rtvi_connect():
"""Launch a bot and return connection info for RTVI clients.""" """Launch a Daily bot and return connection info for RTVI clients."""
print(f"Starting bot with {transport_type} transport") print("Starting bot with Daily transport")
if transport_type == "daily":
import aiohttp import aiohttp
from pipecat.runner.daily import configure from pipecat.runner.daily import configure
@@ -337,12 +308,49 @@ def _create_server_app(transport_type: str, host: str = "localhost", proxy: str
asyncio.create_task(bot_module.bot(session_args)) asyncio.create_task(bot_module.bot(session_args))
return {"room_url": room_url, "token": token} return {"room_url": room_url, "token": token}
else:
return { def _setup_telephony_routes(app: FastAPI, transport_type: str, proxy: str):
"error": f"RTVI connect not supported for {transport_type} transport. Use Daily." """Set up telephony-specific routes."""
# XML response templates
XML_TEMPLATES = {
"twilio": f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://{proxy}/ws"></Stream>
</Connect>
<Pause length="40"/>
</Response>""",
"telnyx": f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://{proxy}/ws" bidirectionalMode="rtp"></Stream>
</Connect>
<Pause length="40"/>
</Response>""",
"plivo": f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Stream bidirectional="true" keepCallAlive="true" contentType="audio/x-mulaw;rate=8000">wss://{proxy}/ws</Stream>
</Response>""",
} }
return app @app.post("/")
async def start_call():
"""Handle telephony webhook and return XML response."""
logger.debug(f"POST {transport_type.upper()} XML")
xml_content = XML_TEMPLATES.get(transport_type, "<Response></Response>")
return HTMLResponse(content=xml_content, media_type="application/xml")
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""Handle WebSocket connections for telephony."""
await websocket.accept()
logger.debug("WebSocket connection accepted")
await _run_telephony_bot(websocket)
@app.get("/")
async def start_agent():
"""Simple status endpoint for telephony transports."""
return {"status": f"Bot started with {transport_type}"}
def main(): def main():

View File

@@ -57,13 +57,10 @@ from loguru import logger
load_dotenv(override=True) load_dotenv(override=True)
def setup_websocket_routes( def _setup_websocket_routes(
app: FastAPI, transport_runner: Callable, transport_type: str, proxy: str = None app: FastAPI, transport_runner: Callable, transport_type: str, proxy: str = None
): ):
"""Set up WebSocket routes for telephony providers. """Set up WebSocket routes for telephony providers."""
This is used by the local runner (pipecat.runner.local) only.
"""
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
@@ -279,7 +276,7 @@ def _run_twilio(
await run(transport, MockArgs(), False) await run(transport, MockArgs(), False)
setup_websocket_routes(app, twilio_runner, "twilio", args.proxy) _setup_websocket_routes(app, twilio_runner, "twilio", args.proxy)
uvicorn.run(app, host=args.host, port=args.port) uvicorn.run(app, host=args.host, port=args.port)
@@ -321,7 +318,7 @@ def _run_telnyx(
await run(transport, MockArgs(), False) await run(transport, MockArgs(), False)
setup_websocket_routes(app, telnyx_runner, "telnyx", args.proxy) _setup_websocket_routes(app, telnyx_runner, "telnyx", args.proxy)
uvicorn.run(app, host=args.host, port=args.port) uvicorn.run(app, host=args.host, port=args.port)
@@ -361,7 +358,7 @@ def _run_plivo(
await run(transport, MockArgs(), False) await run(transport, MockArgs(), False)
setup_websocket_routes(app, plivo_runner, "plivo", args.proxy) _setup_websocket_routes(app, plivo_runner, "plivo", args.proxy)
uvicorn.run(app, host=args.host, port=args.port) uvicorn.run(app, host=args.host, port=args.port)

View File

@@ -42,7 +42,7 @@ from pipecat.transports.base_transport import BaseTransport
def detect_transport_type_from_message(message_data: dict) -> str: def detect_transport_type_from_message(message_data: dict) -> str:
"""Attempt to auto-detect transport type from WebSocket message structure.""" """Attempt to auto-detect transport type from WebSocket message structure."""
logger.debug("=== Auto-Detection Analysis ===") logger.trace("=== Auto-Detection Analysis ===")
# Twilio detection # Twilio detection
if ( if (
@@ -51,7 +51,7 @@ def detect_transport_type_from_message(message_data: dict) -> str:
and "streamSid" in message_data.get("start", {}) and "streamSid" in message_data.get("start", {})
and "callSid" in message_data.get("start", {}) and "callSid" in message_data.get("start", {})
): ):
logger.debug("Auto-detected: TWILIO") logger.trace("Auto-detected: TWILIO")
return "twilio" return "twilio"
# Telnyx detection # Telnyx detection
@@ -60,7 +60,7 @@ def detect_transport_type_from_message(message_data: dict) -> str:
and "start" in message_data and "start" in message_data
and "call_control_id" in message_data.get("start", {}) and "call_control_id" in message_data.get("start", {})
): ):
logger.debug("Auto-detected: TELNYX") logger.trace("Auto-detected: TELNYX")
return "telnyx" return "telnyx"
# Plivo detection # Plivo detection
@@ -69,10 +69,10 @@ def detect_transport_type_from_message(message_data: dict) -> str:
and "streamId" in message_data.get("start", {}) and "streamId" in message_data.get("start", {})
and "callId" in message_data.get("start", {}) and "callId" in message_data.get("start", {})
): ):
logger.debug("Auto-detected: PLIVO") logger.trace("Auto-detected: PLIVO")
return "plivo" return "plivo"
logger.debug("Auto-detection failed - unknown format") logger.trace("Auto-detection failed - unknown format")
return "unknown" return "unknown"
@@ -82,15 +82,13 @@ async def parse_telephony_websocket(websocket: WebSocket):
Returns: Returns:
tuple: (transport_type: str, stream_id: str, call_id: str) tuple: (transport_type: str, stream_id: str, call_id: str)
""" """
logger.info("=== Parsing Telephony WebSocket ===")
# Read first two messages # Read first two messages
start_data = websocket.iter_text() start_data = websocket.iter_text()
try: try:
# First message # First message
first_message_raw = await start_data.__anext__() first_message_raw = await start_data.__anext__()
logger.debug(f"First message: {first_message_raw}") logger.trace(f"First message: {first_message_raw}")
try: try:
first_message = json.loads(first_message_raw) first_message = json.loads(first_message_raw)
except json.JSONDecodeError: except json.JSONDecodeError:
@@ -98,7 +96,7 @@ async def parse_telephony_websocket(websocket: WebSocket):
# Second message # Second message
second_message_raw = await start_data.__anext__() second_message_raw = await start_data.__anext__()
logger.debug(f"Second message: {second_message_raw}") logger.trace(f"Second message: {second_message_raw}")
try: try:
second_message = json.loads(second_message_raw) second_message = json.loads(second_message_raw)
except json.JSONDecodeError: except json.JSONDecodeError:
@@ -112,11 +110,11 @@ async def parse_telephony_websocket(websocket: WebSocket):
if detected_type_first != "unknown": if detected_type_first != "unknown":
transport_type = detected_type_first transport_type = detected_type_first
call_data = first_message call_data = first_message
logger.info(f"Detected transport: {transport_type} (from first message)") logger.debug(f"Detected transport: {transport_type} (from first message)")
elif detected_type_second != "unknown": elif detected_type_second != "unknown":
transport_type = detected_type_second transport_type = detected_type_second
call_data = second_message call_data = second_message
logger.info(f"Detected transport: {transport_type} (from second message)") logger.debug(f"Detected transport: {transport_type} (from second message)")
else: else:
transport_type = "unknown" transport_type = "unknown"
call_data = second_message call_data = second_message
@@ -141,7 +139,7 @@ async def parse_telephony_websocket(websocket: WebSocket):
stream_id = None stream_id = None
call_id = None call_id = None
logger.info(f"Parsed - Type: {transport_type}, StreamId: {stream_id}, CallId: {call_id}") logger.debug(f"Parsed - Type: {transport_type}, StreamId: {stream_id}, CallId: {call_id}")
return transport_type, stream_id, call_id return transport_type, stream_id, call_id
except Exception as e: except Exception as e: