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,169 +186,171 @@ 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)
try: elif transport_type == "daily":
from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI _setup_daily_routes(app)
from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection
except ImportError as e:
logger.error(f"WebRTC transport dependencies not installed.")
return app
# Store connections by pc_id
pcs_map: Dict[str, SmallWebRTCConnection] = {}
# Mount the frontend at /
app.mount("/client", SmallWebRTCPrebuiltUI)
@app.get("/", include_in_schema=False)
async def root_redirect():
"""Redirect root requests to client interface."""
return RedirectResponse(url="/client/")
@app.post("/api/offer")
async def offer(request: dict, background_tasks: BackgroundTasks):
"""Handle WebRTC offer requests and manage peer connections."""
pc_id = request.get("pc_id")
if pc_id and pc_id in pcs_map:
pipecat_connection = pcs_map[pc_id]
logger.info(f"Reusing existing connection for pc_id: {pc_id}")
await pipecat_connection.renegotiate(
sdp=request["sdp"],
type=request["type"],
restart_pc=request.get("restart_pc", False),
)
else:
pipecat_connection = SmallWebRTCConnection()
await pipecat_connection.initialize(sdp=request["sdp"], type=request["type"])
@pipecat_connection.event_handler("closed")
async def handle_disconnected(webrtc_connection: SmallWebRTCConnection):
"""Handle WebRTC connection closure and cleanup."""
logger.info(f"Discarding peer connection for pc_id: {webrtc_connection.pc_id}")
pcs_map.pop(webrtc_connection.pc_id, None)
bot_module = _get_bot_module()
session_args = SmallWebRTCSessionArguments(
webrtc_connection=pipecat_connection,
session_id=None,
)
background_tasks.add_task(bot_module.bot, session_args)
answer = pipecat_connection.get_answer()
pcs_map[answer["pc_id"]] = pipecat_connection
return answer
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Manage FastAPI application lifecycle and cleanup connections."""
yield # Run app
coros = [pc.disconnect() for pc in pcs_map.values()]
await asyncio.gather(*coros)
pcs_map.clear()
app.router.lifespan_context = lifespan
elif transport_type in ["twilio", "telnyx", "plivo"]: elif transport_type in ["twilio", "telnyx", "plivo"]:
# Direct telephony WebSocket handling (no utils dependency) _setup_telephony_routes(app, transport_type, proxy)
else:
logger.warning(f"Unknown transport type: {transport_type}")
@app.post("/") return app
async def start_call():
"""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"?> def _setup_webrtc_routes(app: FastAPI):
"""Set up WebRTC-specific routes."""
try:
from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI
from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection
except ImportError as e:
logger.error(f"WebRTC transport dependencies not installed.")
return
# Store connections by pc_id
pcs_map: Dict[str, SmallWebRTCConnection] = {}
# Mount the frontend
app.mount("/client", SmallWebRTCPrebuiltUI)
@app.get("/", include_in_schema=False)
async def root_redirect():
"""Redirect root requests to client interface."""
return RedirectResponse(url="/client/")
@app.post("/api/offer")
async def offer(request: dict, background_tasks: BackgroundTasks):
"""Handle WebRTC offer requests and manage peer connections."""
pc_id = request.get("pc_id")
if pc_id and pc_id in pcs_map:
pipecat_connection = pcs_map[pc_id]
logger.info(f"Reusing existing connection for pc_id: {pc_id}")
await pipecat_connection.renegotiate(
sdp=request["sdp"],
type=request["type"],
restart_pc=request.get("restart_pc", False),
)
else:
pipecat_connection = SmallWebRTCConnection()
await pipecat_connection.initialize(sdp=request["sdp"], type=request["type"])
@pipecat_connection.event_handler("closed")
async def handle_disconnected(webrtc_connection: SmallWebRTCConnection):
"""Handle WebRTC connection closure and cleanup."""
logger.info(f"Discarding peer connection for pc_id: {webrtc_connection.pc_id}")
pcs_map.pop(webrtc_connection.pc_id, None)
bot_module = _get_bot_module()
session_args = SmallWebRTCSessionArguments(
webrtc_connection=pipecat_connection,
session_id=None,
)
background_tasks.add_task(bot_module.bot, session_args)
answer = pipecat_connection.get_answer()
pcs_map[answer["pc_id"]] = pipecat_connection
return answer
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Manage FastAPI application lifecycle and cleanup connections."""
yield
coros = [pc.disconnect() for pc in pcs_map.values()]
await asyncio.gather(*coros)
pcs_map.clear()
app.router.lifespan_context = lifespan
def _setup_daily_routes(app: FastAPI):
"""Set up Daily-specific routes."""
@app.get("/")
async def start_agent():
"""Launch a Daily bot and redirect to room."""
print("Starting bot with Daily transport")
import aiohttp
from pipecat.runner.daily import configure
async with aiohttp.ClientSession() as session:
room_url, token = await configure(session)
# Start the bot in the background
bot_module = _get_bot_module()
session_args = DailySessionArguments(
room_url=room_url, token=token, body={}, session_id=None
)
asyncio.create_task(bot_module.bot(session_args))
return RedirectResponse(room_url)
@app.post("/connect")
async def rtvi_connect():
"""Launch a Daily bot and return connection info for RTVI clients."""
print("Starting bot with Daily transport")
import aiohttp
from pipecat.runner.daily import configure
async with aiohttp.ClientSession() as session:
room_url, token = await configure(session)
# Start the bot in the background
bot_module = _get_bot_module()
session_args = DailySessionArguments(
room_url=room_url, token=token, body={}, session_id=None
)
asyncio.create_task(bot_module.bot(session_args))
return {"room_url": room_url, "token": token}
def _setup_telephony_routes(app: FastAPI, transport_type: str, proxy: str):
"""Set up telephony-specific routes."""
# XML response templates
XML_TEMPLATES = {
"twilio": f"""<?xml version="1.0" encoding="UTF-8"?>
<Response> <Response>
<Connect> <Connect>
<Stream url="wss://{proxy}/ws"></Stream> <Stream url="wss://{proxy}/ws"></Stream>
</Connect> </Connect>
<Pause length="40"/> <Pause length="40"/>
</Response>""" </Response>""",
elif transport_type == "telnyx": "telnyx": f"""<?xml version="1.0" encoding="UTF-8"?>
xml_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<Response> <Response>
<Connect> <Connect>
<Stream url="wss://{proxy}/ws" bidirectionalMode="rtp"></Stream> <Stream url="wss://{proxy}/ws" bidirectionalMode="rtp"></Stream>
</Connect> </Connect>
<Pause length="40"/> <Pause length="40"/>
</Response>""" </Response>""",
elif transport_type == "plivo": "plivo": f"""<?xml version="1.0" encoding="UTF-8"?>
xml_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<Response> <Response>
<Stream bidirectional="true" keepCallAlive="true" contentType="audio/x-mulaw;rate=8000">wss://{proxy}/ws</Stream> <Stream bidirectional="true" keepCallAlive="true" contentType="audio/x-mulaw;rate=8000">wss://{proxy}/ws</Stream>
</Response>""" </Response>""",
else: }
xml_content = "<Response></Response>"
return HTMLResponse(content=xml_content, media_type="application/xml") @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") @app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket): async def websocket_endpoint(websocket: WebSocket):
"""Handle WebSocket connections for telephony.""" """Handle WebSocket connections for telephony."""
await websocket.accept() await websocket.accept()
logger.debug("WebSocket connection accepted") logger.debug("WebSocket connection accepted")
await _run_telephony_bot(websocket) 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.""" """Simple status endpoint for telephony transports."""
print(f"Starting bot with {transport_type} transport") return {"status": f"Bot started with {transport_type}"}
if transport_type == "daily":
# Create Daily room and start bot
import aiohttp
from pipecat.runner.daily import configure
async with aiohttp.ClientSession() as session:
room_url, token = await configure(session)
# Start the bot in the background to join the room
bot_module = _get_bot_module()
session_args = DailySessionArguments(
room_url=room_url, token=token, body={}, session_id=None
)
asyncio.create_task(bot_module.bot(session_args))
return RedirectResponse(room_url)
elif transport_type == "webrtc":
return RedirectResponse("/client/")
else:
return {"status": f"Bot started with {transport_type}"}
@app.post("/connect")
async def rtvi_connect():
"""Launch a bot and return connection info for RTVI clients."""
print(f"Starting bot with {transport_type} transport")
if transport_type == "daily":
import aiohttp
from pipecat.runner.daily import configure
async with aiohttp.ClientSession() as session:
room_url, token = await configure(session)
# Start the bot in the background
bot_module = _get_bot_module()
session_args = DailySessionArguments(
room_url=room_url, token=token, body={}, session_id=None
)
asyncio.create_task(bot_module.bot(session_args))
return {"room_url": room_url, "token": token}
else:
return {
"error": f"RTVI connect not supported for {transport_type} transport. Use Daily."
}
return app
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: