Final cleanup

This commit is contained in:
Mark Backman
2025-07-25 21:25:01 -04:00
parent df211fb5a3
commit 7253077bb7
6 changed files with 235 additions and 58 deletions

View File

@@ -0,0 +1 @@
"""Pipecat runner package for local and cloud bot execution."""

View File

@@ -4,7 +4,63 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
"""Cloud-compatible development server - simplified without subprocesses.""" """Pipecat Cloud-compatible development server for running Pipecat bots.
This module provides a FastAPI-based development server that can run bots
structured for Pipecat Cloud deployment. It supports multiple transport types
and handles room/token management automatically.
All bots must implement a `bot(session_args)` async function as the entry point.
The server automatically discovers and executes this function when connections
are established.
Bot function signature::
async def bot(session_args):
# session_args contains transport-specific connection information
# For Daily: session_args.room_url, session_args.token, session_args.body
# For WebRTC: session_args.webrtc_connection
# For Telephony: session_args.websocket
# Create transport based on session_args attributes
if hasattr(session_args, 'room_url'):
# Daily transport setup
transport = DailyTransport(session_args.room_url, session_args.token, session_args.body)
elif hasattr(session_args, 'webrtc_connection'):
# WebRTC transport setup
transport = SmallWebRTCTransport(..., webrtc_connection=session_args.webrtc_connection)
# Run your bot logic
await run_bot_logic(transport)
Supported transports:
- Daily - Creates rooms and tokens, runs bot as participant
- LiveKit - Creates rooms and tokens, runs bot as agent
- WebRTC - Provides local WebRTC interface with prebuilt UI
- Telephony - Handles webhook and WebSocket connections for Twilio, Telnyx, Plivo
Example::
async def bot(session_args):
# Detect transport type from session_args
if hasattr(session_args, "room_url"):
# Daily or LiveKit
transport = create_daily_transport(session_args)
elif hasattr(session_args, "webrtc_connection"):
# WebRTC
transport = create_webrtc_transport(session_args)
# Your bot implementation
await run_pipeline(transport)
if __name__ == "__main__":
from pipecat.runner.cloud import main
main()
Then run: `python bot.py -t daily` or `python bot.py -t webrtc`
"""
import argparse import argparse
import asyncio import asyncio
@@ -20,14 +76,13 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from loguru import logger from loguru import logger
# Import the common transport utility functions from pipecat.runner.utils import setup_websocket_routes
from .transport_utilities import setup_websocket_routes
load_dotenv(override=True) load_dotenv(override=True)
os.environ["LOCAL_RUN"] = "1" os.environ["LOCAL_RUN"] = "1"
def get_bot_module(): def _get_bot_module():
"""Get the bot module from the calling script.""" """Get the bot module from the calling script."""
import importlib.util import importlib.util
@@ -68,12 +123,12 @@ def get_bot_module():
) )
async def run_bot_directly(transport_type: str, **kwargs): async def _run_bot_directly(transport_type: str, **kwargs):
"""Run a bot directly in the same process - no subprocess needed.""" """Run a bot directly in the same process - no subprocess needed."""
if transport_type == "webrtc": if transport_type == "webrtc":
if "webrtc_connection" in kwargs: if "webrtc_connection" in kwargs:
# Direct WebRTC connection # Direct WebRTC connection
bot_module = get_bot_module() bot_module = _get_bot_module()
class WebRTCSessionArgs: class WebRTCSessionArgs:
def __init__(self, webrtc_connection): def __init__(self, webrtc_connection):
@@ -88,7 +143,7 @@ async def run_bot_directly(transport_type: str, **kwargs):
elif transport_type in ["twilio", "telnyx", "plivo"]: elif transport_type in ["twilio", "telnyx", "plivo"]:
if "websocket" in kwargs: if "websocket" in kwargs:
# Direct WebSocket connection # Direct WebSocket connection
bot_module = get_bot_module() bot_module = _get_bot_module()
class WebSocketSessionArgs: class WebSocketSessionArgs:
def __init__(self, transport_type, websocket, call_info): def __init__(self, transport_type, websocket, call_info):
@@ -104,7 +159,7 @@ async def run_bot_directly(transport_type: str, **kwargs):
await bot_module.bot(session_args) await bot_module.bot(session_args)
def create_server_app(transport_type: str, host: str = "0.0.0.0", proxy: str = None): def _create_server_app(transport_type: str, host: str = "0.0.0.0", proxy: str = None):
"""Create FastAPI app with transport-specific routes.""" """Create FastAPI app with transport-specific routes."""
app = FastAPI() app = FastAPI()
@@ -163,13 +218,13 @@ def create_server_app(transport_type: str, host: str = "0.0.0.0", proxy: str = N
# Run bot directly # Run bot directly
background_tasks.add_task( background_tasks.add_task(
run_bot_directly, "webrtc", webrtc_connection=pipecat_connection _run_bot_directly, "webrtc", webrtc_connection=pipecat_connection
) )
answer = pipecat_connection.get_answer() answer = pipecat_connection.get_answer()
if host and host != "0.0.0.0": if host and host != "0.0.0.0":
from .transport_utilities import smallwebrtc_sdp_munging from pipecat.runner.utils import smallwebrtc_sdp_munging
answer["sdp"] = smallwebrtc_sdp_munging(answer["sdp"], host) answer["sdp"] = smallwebrtc_sdp_munging(answer["sdp"], host)
@@ -189,7 +244,7 @@ def create_server_app(transport_type: str, host: str = "0.0.0.0", proxy: str = N
app.router.lifespan_context = lifespan app.router.lifespan_context = lifespan
elif transport_type in ["twilio", "telnyx", "plivo"]: elif transport_type in ["twilio", "telnyx", "plivo"]:
setup_websocket_routes(app, run_bot_directly, transport_type, proxy) setup_websocket_routes(app, _run_bot_directly, transport_type, proxy)
# Add general routes # Add general routes
@app.get("/") @app.get("/")
@@ -201,13 +256,13 @@ def create_server_app(transport_type: str, host: str = "0.0.0.0", proxy: str = N
# Create Daily room and start bot # Create Daily room and start bot
import aiohttp import aiohttp
from .daily_runner import configure from pipecat.runner.daily import configure
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 to join the room
bot_module = get_bot_module() bot_module = _get_bot_module()
class DailySessionArgs: class DailySessionArgs:
def __init__(self, room_url, token): def __init__(self, room_url, token):
@@ -226,12 +281,12 @@ def create_server_app(transport_type: str, host: str = "0.0.0.0", proxy: str = N
elif transport_type == "livekit": elif transport_type == "livekit":
# Create LiveKit room and start bot # Create LiveKit room and start bot
from .livekit_runner import configure from pipecat.runner.livekit import configure
url, token, room_name = await configure() url, token, room_name = await configure()
# Start the bot in the background to join the room # Start the bot in the background to join the room
bot_module = get_bot_module() bot_module = _get_bot_module()
class LiveKitSessionArgs: class LiveKitSessionArgs:
def __init__(self, url, token, room_name): def __init__(self, url, token, room_name):
@@ -262,13 +317,13 @@ def create_server_app(transport_type: str, host: str = "0.0.0.0", proxy: str = N
if transport_type == "daily": if transport_type == "daily":
import aiohttp import aiohttp
from .daily_runner import configure from pipecat.runner.daily import configure
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 # Start the bot in the background
bot_module = get_bot_module() bot_module = _get_bot_module()
class DailySessionArgs: class DailySessionArgs:
def __init__(self, room_url, token): def __init__(self, room_url, token):
@@ -294,7 +349,20 @@ def create_server_app(transport_type: str, host: str = "0.0.0.0", proxy: str = N
def main(): def main():
"""Main entry point for cloud-compatible server.""" """Start the cloud-compatible development server.
Parses command-line arguments and starts a FastAPI server configured
for the specified transport type. The server will discover and run
any bot() function found in the current directory.
Command-line arguments:
--host: Server host address (default: 0.0.0.0)
--port: Server port (default: 7860)
-t/--transport: Transport type (daily, livekit, webrtc, twilio, telnyx, plivo)
-x/--proxy: Public proxy hostname for telephony webhooks
The bot file must contain a `bot(session_args)` function as the entry point.
"""
parser = argparse.ArgumentParser(description="Pipecat Cloud-Compatible Development Server") parser = argparse.ArgumentParser(description="Pipecat Cloud-Compatible Development Server")
parser.add_argument("--host", type=str, default="0.0.0.0", help="Host address") parser.add_argument("--host", type=str, default="0.0.0.0", help="Host address")
parser.add_argument("--port", type=int, default=7860, help="Port number") parser.add_argument("--port", type=int, default=7860, help="Port number")
@@ -311,7 +379,7 @@ def main():
args = parser.parse_args() args = parser.parse_args()
# Create the app with transport-specific setup # Create the app with transport-specific setup
app = create_server_app(args.transport, args.host, args.proxy) app = _create_server_app(args.transport, args.host, args.proxy)
# Run the server # Run the server
uvicorn.run(app, host=args.host, port=args.port) uvicorn.run(app, host=args.host, port=args.port)

View File

@@ -4,7 +4,30 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
"""Daily.co room configuration utilities for Pipecat examples.""" """Daily room and token configuration utilities.
This module provides helper functions for creating and configuring Daily rooms
and authentication tokens. It handles both command-line argument parsing and
environment variable configuration.
The module supports creating temporary rooms for development or using existing
rooms specified via arguments or environment variables.
Required environment variables:
- DAILY_API_KEY - Daily API key for room/token creation
- DAILY_SAMPLE_ROOM_URL (optional) - Existing room URL to use
- DAILY_SAMPLE_ROOM_TOKEN (optional) - Existing token to use
Example::
import aiohttp
from pipecat.runner.daily import configure
async with aiohttp.ClientSession() as session:
room_url, token = await configure(session)
# Use room_url and token with DailyTransport
"""
import argparse import argparse
import os import os
@@ -16,7 +39,7 @@ from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper
async def configure(aiohttp_session: aiohttp.ClientSession): async def configure(aiohttp_session: aiohttp.ClientSession):
"""Configure Daily.co room URL and token from arguments or environment. """Configure Daily room URL and token from arguments or environment.
Args: Args:
aiohttp_session: HTTP session for making API requests. aiohttp_session: HTTP session for making API requests.
@@ -34,7 +57,7 @@ async def configure(aiohttp_session: aiohttp.ClientSession):
async def configure_with_args( async def configure_with_args(
aiohttp_session: aiohttp.ClientSession, parser: Optional[argparse.ArgumentParser] = None aiohttp_session: aiohttp.ClientSession, parser: Optional[argparse.ArgumentParser] = None
): ):
"""Configure Daily.co room with command-line argument parsing. """Configure Daily room with command-line argument parsing.
Args: Args:
aiohttp_session: HTTP session for making API requests. aiohttp_session: HTTP session for making API requests.

View File

@@ -4,7 +4,29 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
"""LiveKit room configuration utilities for Pipecat applications.""" """LiveKit room and token configuration utilities.
This module provides helper functions for creating and configuring LiveKit
rooms and authentication tokens. It handles JWT token generation with
appropriate grants for both regular participants and AI agents.
The module supports creating tokens for development and testing, with
automatic agent detection for proper room permissions.
Required environment variables:
- LIVEKIT_API_KEY - LiveKit API key
- LIVEKIT_API_SECRET - LiveKit API secret
- LIVEKIT_URL - LiveKit server URL
- LIVEKIT_ROOM_NAME - Room name to join
Example::
from pipecat.runner.livekit import configure
url, token, room_name = await configure()
# Use with LiveKitTransport
"""
import argparse import argparse
import os import os

View File

@@ -4,29 +4,60 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
"""Direct execution runner for local-only examples.""" """Local-only development runner for simple Pipecat examples.
This module provides a simplified runner for local development and testing.
It supports direct function execution without requiring the structured `bot()`
function pattern needed for cloud deployment.
Supported transports:
- Daily - Uses environment variables or arguments for room/token
- LiveKit - Uses environment variables or arguments for connection
- WebRTC - Provides local WebRTC interface with prebuilt UI
- Telephony - Handles webhook and WebSocket connections for Twilio, Telnyx, Plivo
This runner is ideal for quick prototypes, examples, and bots that will only
run locally. For cloud-deployable bots, use `pipecat.runner.cloud` instead.
Example::
async def run_bot(transport, args, handle_sigint):
# Your bot implementation
pass
if __name__ == "__main__":
from pipecat.runner.local import main
transport_params = {
"webrtc": lambda: TransportParams(...)
}
main(run_bot, transport_params=transport_params)
Then run: `python bot.py -t webrtc`
"""
import argparse import argparse
import asyncio import asyncio
import os import os
import sys import sys
from contextlib import asynccontextmanager
from typing import Callable, Dict, Mapping, Optional from typing import Callable, Dict, Mapping, Optional
import aiohttp import aiohttp
import uvicorn import uvicorn
from dotenv import load_dotenv from dotenv import load_dotenv
from fastapi import BackgroundTasks, FastAPI from fastapi import BackgroundTasks, FastAPI
from fastapi.concurrency import asynccontextmanager
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from loguru import logger from loguru import logger
# Import the common transport utility functions from pipecat.runner.utils import setup_websocket_routes
from .transport_utilities import setup_websocket_routes
load_dotenv(override=True) load_dotenv(override=True)
def run_webrtc( def _run_webrtc(
run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {} run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {}
): ):
"""Run using WebRTC transport with FastAPI server.""" """Run using WebRTC transport with FastAPI server."""
@@ -91,7 +122,7 @@ def run_webrtc(
answer = pipecat_connection.get_answer() answer = pipecat_connection.get_answer()
if args.esp32 and args.host: if args.esp32 and args.host:
from .transport_utilities import smallwebrtc_sdp_munging from pipecat.runner.utils import smallwebrtc_sdp_munging
answer["sdp"] = smallwebrtc_sdp_munging(answer["sdp"], args.host) answer["sdp"] = smallwebrtc_sdp_munging(answer["sdp"], args.host)
@@ -112,16 +143,13 @@ def run_webrtc(
uvicorn.run(app, host=args.host, port=args.port) uvicorn.run(app, host=args.host, port=args.port)
def run_twilio( def _run_twilio(
run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {} run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {}
): ):
"""Run using Twilio transport with FastAPI WebSocket server.""" """Run using Twilio transport with FastAPI WebSocket server."""
try: try:
from pipecat.serializers.twilio import TwilioFrameSerializer from pipecat.serializers.twilio import TwilioFrameSerializer
from pipecat.transports.network.fastapi_websocket import ( from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketTransport
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
except ImportError as e: except ImportError as e:
logger.error(f"Twilio transport dependencies not installed.") logger.error(f"Twilio transport dependencies not installed.")
logger.debug(f"Import error: {e}") logger.debug(f"Import error: {e}")
@@ -157,16 +185,13 @@ def run_twilio(
uvicorn.run(app, host=args.host, port=args.port) uvicorn.run(app, host=args.host, port=args.port)
def run_telnyx( def _run_telnyx(
run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {} run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {}
): ):
"""Run using Telnyx transport with FastAPI WebSocket server.""" """Run using Telnyx transport with FastAPI WebSocket server."""
try: try:
from pipecat.serializers.telnyx import TelnyxFrameSerializer from pipecat.serializers.telnyx import TelnyxFrameSerializer
from pipecat.transports.network.fastapi_websocket import ( from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketTransport
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
except ImportError as e: except ImportError as e:
logger.error(f"Telnyx transport dependencies not installed.") logger.error(f"Telnyx transport dependencies not installed.")
logger.debug(f"Import error: {e}") logger.debug(f"Import error: {e}")
@@ -202,16 +227,13 @@ def run_telnyx(
uvicorn.run(app, host=args.host, port=args.port) uvicorn.run(app, host=args.host, port=args.port)
def run_plivo( def _run_plivo(
run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {} run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {}
): ):
"""Run using Plivo transport with FastAPI WebSocket server.""" """Run using Plivo transport with FastAPI WebSocket server."""
try: try:
from pipecat.serializers.plivo import PlivoFrameSerializer from pipecat.serializers.plivo import PlivoFrameSerializer
from pipecat.transports.network.fastapi_websocket import ( from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketTransport
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
except ImportError as e: except ImportError as e:
logger.error(f"Plivo transport dependencies not installed.") logger.error(f"Plivo transport dependencies not installed.")
logger.debug(f"Import error: {e}") logger.debug(f"Import error: {e}")
@@ -245,10 +267,10 @@ def run_plivo(
uvicorn.run(app, host=args.host, port=args.port) uvicorn.run(app, host=args.host, port=args.port)
def run_daily( def _run_daily(
run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {} run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {}
): ):
"""Run using Daily.co transport.""" """Run using Daily transport."""
try: try:
from pipecat.runner.daily import configure from pipecat.runner.daily import configure
from pipecat.transports.services.daily import DailyParams, DailyTransport from pipecat.transports.services.daily import DailyParams, DailyTransport
@@ -269,7 +291,7 @@ def run_daily(
asyncio.run(run_daily_impl()) asyncio.run(run_daily_impl())
def run_livekit( def _run_livekit(
run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {} run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {}
): ):
"""Run using LiveKit transport.""" """Run using LiveKit transport."""
@@ -292,7 +314,7 @@ def run_livekit(
asyncio.run(run_livekit_impl()) asyncio.run(run_livekit_impl())
def run_main( def _run_main(
run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {} run: Callable, args: argparse.Namespace, transport_params: Mapping[str, Callable] = {}
): ):
"""Run the application with the specified transport type.""" """Run the application with the specified transport type."""
@@ -302,17 +324,17 @@ def run_main(
match args.transport: match args.transport:
case "daily": case "daily":
run_daily(run, args, transport_params) _run_daily(run, args, transport_params)
case "livekit": case "livekit":
run_livekit(run, args, transport_params) _run_livekit(run, args, transport_params)
case "plivo": case "plivo":
run_plivo(run, args, transport_params) _run_plivo(run, args, transport_params)
case "telnyx": case "telnyx":
run_telnyx(run, args, transport_params) _run_telnyx(run, args, transport_params)
case "twilio": case "twilio":
run_twilio(run, args, transport_params) _run_twilio(run, args, transport_params)
case "webrtc": case "webrtc":
run_webrtc(run, args, transport_params) _run_webrtc(run, args, transport_params)
def main( def main(
@@ -321,7 +343,26 @@ def main(
parser: Optional[argparse.ArgumentParser] = None, parser: Optional[argparse.ArgumentParser] = None,
transport_params: Mapping[str, Callable] = {}, transport_params: Mapping[str, Callable] = {},
): ):
"""Main entry point for running Pipecat applications with transport selection.""" """Run a Pipecat bot with transport selection.
Args:
run: The bot function to execute. Must accept (transport, args, handle_sigint).
parser: Optional argument parser. If None, creates a default one.
transport_params: Mapping of transport names to parameter factory functions.
Each factory should return transport-specific parameters when called.
Command-line arguments:
--host: Server host address (default: localhost)
--port: Server port (default: 7860)
-t/--transport: Transport type (daily, livekit, webrtc, twilio, telnyx, plivo)
-x/--proxy: Public proxy hostname for telephony webhooks
--esp32: Enable SDP munging for ESP32 compatibility
-v/--verbose: Increase logging verbosity
The function handles argument parsing, transport setup, and bot execution.
Different transports may use FastAPI servers (WebRTC, telephony) or direct
execution (Daily, LiveKit).
"""
if not parser: if not parser:
parser = argparse.ArgumentParser(description="Pipecat Bot Runner") parser = argparse.ArgumentParser(description="Pipecat Bot Runner")
@@ -359,4 +400,4 @@ def main(
logger.remove(0) logger.remove(0)
logger.add(sys.stderr, level="TRACE" if args.verbose else "DEBUG") logger.add(sys.stderr, level="TRACE" if args.verbose else "DEBUG")
run_main(run, args, transport_params) _run_main(run, args, transport_params)

View File

@@ -4,10 +4,32 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
"""Common transport utility functions shared between server.py and run.py.""" """Transport utility functions and FastAPI route setup helpers.
This module provides common functionality for setting up transport-specific
FastAPI routes and handling WebRTC/WebSocket connections. It includes SDP
manipulation utilities for WebRTC compatibility and transport detection helpers.
Key features:
- WebRTC route setup with connection management
- WebSocket route setup for telephony providers
- SDP munging for ESP32 and other WebRTC compatibility
- Transport client ID detection across different transport types
- Video capture utilities for Daily transports
The utilities are designed to be transport-agnostic where possible, with
specific handlers for each transport type's unique requirements.
Example::
from pipecat.runner.utils import setup_webrtc_routes
app = FastAPI()
setup_webrtc_routes(app, bot_runner_function, host="localhost")
"""
import json import json
import os
import re import re
from typing import Any, Callable, Dict from typing import Any, Callable, Dict