diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d56a323f..e47f1010a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,6 +114,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed an issue in `RimeTTSService` where the last line of text sent didn't result in an audio output being generated. +### Other + +- Added a Pipecat Cloud deployment example to the `examples` directory. + ## [0.0.58] - 2025-02-26 ### Added diff --git a/examples/deployment/pipecat-cloud-example/.gitignore b/examples/deployment/pipecat-cloud-example/.gitignore new file mode 100644 index 000000000..8a5b4de3d --- /dev/null +++ b/examples/deployment/pipecat-cloud-example/.gitignore @@ -0,0 +1,94 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +dist/ +*.egg-info/ +*.egg +.installed.cfg +.eggs/ +downloads/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +MANIFEST + +# Virtual Environments +venv/ +env/ +.env +.venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.idea/ +.vscode/ +.spyderproject +.spyproject +.ropeproject + +# Testing and Coverage +.coverage +.coverage.* +htmlcov/ +.pytest_cache/ +.tox/ +.nox/ +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +cover/ + +# Logs and Databases +*.log +*.db +db.sqlite3 +db.sqlite3-journal +pip-log.txt + +# System Files +.DS_Store +Thumbs.db +desktop.ini +*.swp +*.swo +*.bak +*.tmp +*~ + +# Build and Documentation +docs/_build/ +.pybuilder/ +target/ +instance/ +.webassets-cache +.pdm.toml +.pdm-python +.pdm-build/ +__pypackages__/ + +# Other +*.mo +*.pot +*.sage.py +.mypy_cache/ +.dmypy.json +dmypy.json +.pyre/ +.pytype/ +cython_debug/ +.ipynb_checkpoints + +# Pipecat cloud +.pcc-deploy.toml \ No newline at end of file diff --git a/examples/deployment/pipecat-cloud-example/Dockerfile b/examples/deployment/pipecat-cloud-example/Dockerfile new file mode 100644 index 000000000..290899e5c --- /dev/null +++ b/examples/deployment/pipecat-cloud-example/Dockerfile @@ -0,0 +1,7 @@ +FROM dailyco/pipecat-base:latest + +COPY ./requirements.txt requirements.txt + +RUN pip install --no-cache-dir --upgrade -r requirements.txt + +COPY ./bot.py bot.py \ No newline at end of file diff --git a/examples/deployment/pipecat-cloud-example/README.md b/examples/deployment/pipecat-cloud-example/README.md new file mode 100644 index 000000000..7d5e4bd6f --- /dev/null +++ b/examples/deployment/pipecat-cloud-example/README.md @@ -0,0 +1,196 @@ +# Pipecat Cloud Starter Project + +[![Docs](https://img.shields.io/badge/Documentation-blue)](https://docs.pipecat.daily.co) [![Discord](https://img.shields.io/discord/1217145424381743145)](https://discord.gg/dailyco) + +A template voice agent for [Pipecat Cloud](https://www.daily.co/products/pipecat-cloud/) that demonstrates building and deploying a conversational AI agent. + +> **For a detailed step-by-step guide, see our [Quickstart Documentation](https://docs.pipecat.daily.co/quickstart).** + +## Prerequisites + +- Python 3.10+ +- Linux, MacOS, or Windows Subsystem for Linux (WSL) +- [Docker](https://www.docker.com) and a Docker repository (e.g., [Docker Hub](https://hub.docker.com)) +- A Docker Hub account (or other container registry account) +- [Pipecat Cloud](https://pipecat.daily.co) account + +> **Note**: If you haven't installed Docker yet, follow the official installation guides for your platform ([Linux](https://docs.docker.com/engine/install/), [Mac](https://docs.docker.com/desktop/setup/install/mac-install/), [Windows](https://docs.docker.com/desktop/setup/install/windows-install/)). For Docker Hub, [create a free account](https://hub.docker.com/signup) and log in via terminal with `docker login`. + +## Get Started + +### 1. Get the starter project + +Clone the starter project from GitHub: + +```bash +git clone https://github.com/daily-co/pipecat-cloud-starter +cd pipecat-cloud-starter +``` + +### 2. Set up your Python environment + +We recommend using a virtual environment to manage your Python dependencies. + +```bash +# Create a virtual environment +python -m venv .venv + +# Activate it +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# Install the Pipecat Cloud CLI +pip install pipecatcloud +``` + +### 3. Authenticate with Pipecat Cloud + +```bash +pcc auth login +``` + +### 4. Acquire required API keys + +This starter requires the following API keys: + +- **OpenAI API Key**: Get from [platform.openai.com/api-keys](https://platform.openai.com/api-keys) +- **Cartesia API Key**: Get from [play.cartesia.ai/keys](https://play.cartesia.ai/keys) +- **Daily API Key**: Automatically provided through your Pipecat Cloud account + +### 5. Configure to run locally (optional) + +You can test your agent locally before deploying to Pipecat Cloud: + +```bash +# Set environment variables with your API keys +export CARTESIA_API_KEY="your_cartesia_key" +export DAILY_API_KEY="your_daily_key" +export OPENAI_API_KEY="your_openai_key" +``` + +> Your `DAILY_API_KEY` can be found at [https://pipecat.daily.co](https://pipecat.daily.co) under the `Settings` in the `Daily (WebRTC)` tab. + +First install requirements: + +```bash +pip install -r requirements.txt +``` + +Then, launch the bot.py script locally: + +```bash +LOCAL_RUN=1 python bot.py +``` + +## Deploy & Run + +### 1. Build and push your Docker image + +```bash +# Build the image (targeting ARM architecture for cloud deployment) +docker build --platform=linux/arm64 -t my-first-agent:latest . + +# Tag with your Docker username and version +docker tag my-first-agent:latest your-username/my-first-agent:0.1 + +# Push to Docker Hub +docker push your-username/my-first-agent:0.1 +``` + +### 2. Create a secret set for your API keys + +The starter project requires API keys for OpenAI and Cartesia: + +```bash +# Copy the example env file +cp env.example .env + +# Edit .env to add your API keys: +# CARTESIA_API_KEY=your_cartesia_key +# OPENAI_API_KEY=your_openai_key + +# Create a secret set from your .env file +pcc secrets set my-first-agent-secrets --file .env +``` + +Alternatively, you can create secrets directly via CLI: + +```bash +pcc secrets set my-first-agent-secrets \ + CARTESIA_API_KEY=your_cartesia_key \ + OPENAI_API_KEY=your_openai_key +``` + +### 3. Deploy to Pipecat Cloud + +```bash +pcc deploy my-first-agent your-username/my-first-agent:0.1 --secrets my-first-agent-secrets +``` + +> **Note (Optional)**: For a more maintainable approach, you can use the included `pcc-deploy.toml` file: +> +> ```toml +> agent_name = "my-first-agent" +> image = "your-username/my-first-agent:0.1" +> secret_set = "my-first-agent-secrets" +> +> [scaling] +> min_instances = 0 +> ``` +> +> Then simply run `pcc deploy` without additional arguments. + +> **Note**: If your repository is private, you'll need to add credentials: +> +> ```bash +> # Create pull secret (you’ll be prompted for credentials) +> pcc secrets image-pull-secret pull-secret https://index.docker.io/v1/ +> +> # Deploy with credentials +> pcc deploy my-first-agent your-username/my-first-agent:0.1 --credentials pull-secret +> ``` + +### 4. Check deployment and scaling (optional) + +By default, your agent will use "scale-to-zero" configuration, which means it may have a cold start of around 10 seconds when first used. By default, idle instances are maintained for 5 minutes before being terminated when using scale-to-zero. + +For more responsive testing, you can scale your deployment to keep a minimum of one instance warm: + +```bash +# Ensure at least one warm instance is always available +pcc deploy my-first-agent your-username/my-first-agent:0.1 --min-instances 1 + +# Check the status of your deployment +pcc agent status my-first-agent +``` + +By default, idle instances are maintained for 5 minutes before being terminated when using scale-to-zero. + +### 5. Create an API key + +```bash +# Create a public API key for accessing your agent +pcc organizations keys create + +# Set it as the default key to use with your agent +pcc organizations keys use +``` + +### 6. Start your agent + +```bash +# Start a session with your agent in a Daily room +pcc agent start my-first-agent --use-daily +``` + +This will return a URL, which you can use to connect to your running agent. + +## Documentation + +For more details on Pipecat Cloud and its capabilities: + +- [Pipecat Cloud Documentation](https://docs.pipecat.daily.co) +- [Pipecat Project Documentation](https://docs.pipecat.ai) + +## Support + +Join our [Discord community](https://discord.gg/dailyco) for help and discussions. diff --git a/examples/deployment/pipecat-cloud-example/bot.py b/examples/deployment/pipecat-cloud-example/bot.py new file mode 100644 index 000000000..89d4973b7 --- /dev/null +++ b/examples/deployment/pipecat-cloud-example/bot.py @@ -0,0 +1,161 @@ +# +# Copyright (c) 2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import os + +import aiohttp +from dotenv import load_dotenv +from loguru import logger +from pipecatcloud.agent import DailySessionArguments + +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMMessagesFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext +from pipecat.services.cartesia import CartesiaTTSService +from pipecat.services.openai import OpenAILLMService +from pipecat.transports.services.daily import DailyParams, DailyTransport + +# Check if we're in local development mode +LOCAL_RUN = os.getenv("LOCAL_RUN") +if LOCAL_RUN: + import asyncio + import webbrowser + + try: + from local_runner import configure + except ImportError: + logger.error("Could not import local_runner module. Local development mode may not work.") + +# Load environment variables +load_dotenv(override=True) + + +async def main(room_url: str, token: str): + """Main pipeline setup and execution function. + + Args: + room_url: The Daily room URL + token: The Daily room token + """ + logger.debug("Starting bot in room: {}", room_url) + + transport = DailyTransport( + room_url, + token, + "bot", + DailyParams( + audio_out_enabled=True, + transcription_enabled=True, + vad_enabled=True, + vad_analyzer=SileroVADAnalyzer(), + ), + ) + + tts = CartesiaTTSService( + api_key=os.getenv("CARTESIA_API_KEY"), voice_id="79a125e8-cd45-4c13-8a67-188112f4dd22" + ) + + llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o") + + messages = [ + { + "role": "system", + "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way.", + }, + ] + + context = OpenAILLMContext(messages) + context_aggregator = llm.create_context_aggregator(context) + + pipeline = Pipeline( + [ + transport.input(), + context_aggregator.user(), + llm, + tts, + transport.output(), + context_aggregator.assistant(), + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + allow_interruptions=True, + enable_metrics=True, + enable_usage_metrics=True, + report_only_initial_ttfb=True, + ), + ) + + @transport.event_handler("on_first_participant_joined") + async def on_first_participant_joined(transport, participant): + logger.info("First participant joined: {}", participant["id"]) + await transport.capture_participant_transcription(participant["id"]) + # Kick off the conversation. + messages.append( + { + "role": "system", + "content": "Please start with 'Hello World' and introduce yourself to the user.", + } + ) + await task.queue_frames([LLMMessagesFrame(messages)]) + + @transport.event_handler("on_participant_left") + async def on_participant_left(transport, participant, reason): + logger.info("Participant left: {}", participant) + await task.cancel() + + runner = PipelineRunner() + + await runner.run(task) + + +async def bot(args: DailySessionArguments): + """Main bot entry point compatible with the FastAPI route handler. + + Args: + room_url: The Daily room URL + token: The Daily room token + body: The configuration object from the request body + session_id: The session ID for logging + """ + logger.info(f"Bot process initialized {args.room_url} {args.token}") + + try: + await main(args.room_url, args.token) + logger.info("Bot process completed") + except Exception as e: + logger.exception(f"Error in bot process: {str(e)}") + raise + + +# Local development functions +async def local_main(): + """Function for local development testing.""" + try: + async with aiohttp.ClientSession() as session: + (room_url, token) = await configure(session) + logger.warning("_") + logger.warning("_") + logger.warning(f"Talk to your voice agent here: {room_url}") + logger.warning("_") + logger.warning("_") + webbrowser.open(room_url) + await main(room_url, token) + except Exception as e: + logger.exception(f"Error in local development mode: {e}") + + +# Local development entry point +if LOCAL_RUN and __name__ == "__main__": + try: + asyncio.run(local_main()) + except Exception as e: + logger.exception(f"Failed to run in local mode: {e}") diff --git a/examples/deployment/pipecat-cloud-example/env.example b/examples/deployment/pipecat-cloud-example/env.example new file mode 100644 index 000000000..1cbb5c4f6 --- /dev/null +++ b/examples/deployment/pipecat-cloud-example/env.example @@ -0,0 +1,2 @@ +CARTESIA_API_KEY= +OPENAI_API_KEY= \ No newline at end of file diff --git a/examples/deployment/pipecat-cloud-example/local_runner.py b/examples/deployment/pipecat-cloud-example/local_runner.py new file mode 100644 index 000000000..432592534 --- /dev/null +++ b/examples/deployment/pipecat-cloud-example/local_runner.py @@ -0,0 +1,46 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import os + +import aiohttp + +from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper, DailyRoomParams + + +async def configure(aiohttp_session: aiohttp.ClientSession): + (url, token) = await configure_with_args(aiohttp_session) + return (url, token) + + +async def configure_with_args(aiohttp_session: aiohttp.ClientSession = None): + key = os.getenv("DAILY_API_KEY") + if not key: + raise Exception( + "No Daily API key specified. set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers." + ) + + daily_rest_helper = DailyRESTHelper( + daily_api_key=key, + daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"), + aiohttp_session=aiohttp_session, + ) + + room = await daily_rest_helper.create_room( + DailyRoomParams(properties={"enable_prejoin_ui": False}) + ) + if not room.url: + raise HTTPException(status_code=500, detail="Failed to create room") + + url = room.url + + # Create a meeting token for the given room with an expiration 1 hour in + # the future. + expiry_time: float = 60 * 60 + + token = await daily_rest_helper.get_token(url, expiry_time) + + return (url, token) diff --git a/examples/deployment/pipecat-cloud-example/pcc-deploy.toml b/examples/deployment/pipecat-cloud-example/pcc-deploy.toml new file mode 100644 index 000000000..063ed5929 --- /dev/null +++ b/examples/deployment/pipecat-cloud-example/pcc-deploy.toml @@ -0,0 +1,6 @@ +agent_name = "my-first-agent" +image = "your-username/my-first-agent:0.1" +secret_set = "my-first-agent-secrets" + +[scaling] + min_instances = 0 diff --git a/examples/deployment/pipecat-cloud-example/requirements.txt b/examples/deployment/pipecat-cloud-example/requirements.txt new file mode 100644 index 000000000..2e9fd4e9d --- /dev/null +++ b/examples/deployment/pipecat-cloud-example/requirements.txt @@ -0,0 +1,3 @@ +pipecatcloud +pipecat-ai[cartesia,daily,openai,silero]>=0.0.58 +python-dotenv~=1.0.1