diff --git a/examples/deployment/modal-example/.gitignore b/examples/deployment/modal-example/.gitignore new file mode 100644 index 000000000..ecafe6e74 --- /dev/null +++ b/examples/deployment/modal-example/.gitignore @@ -0,0 +1,91 @@ +# 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 \ No newline at end of file diff --git a/examples/deployment/modal-example/README.md b/examples/deployment/modal-example/README.md new file mode 100644 index 000000000..b73a5b299 --- /dev/null +++ b/examples/deployment/modal-example/README.md @@ -0,0 +1,37 @@ +# Deploying Pipecat to Modal.com + +Barebones deployment example for [modal.com](https://www.modal.com) + +1. Install dependencies + +```bash +python -m venv venv +source venv/bin/active # or OS equivalent +pip install -r requirements.txt +``` + +2. Setup .env + +```bash +cp env.example .env +``` + +Alternatively, you can configure your Modal app to use [secrets](https://modal.com/docs/guide/secrets) + +3. Test the app locally + +```bash +modal serve app.py +``` + +4. Deploy to production + +```bash +modal deploy app.py +``` + +## Configuration options + +This app sets some sensible defaults for reducing cold starts, such as `minkeep_warm=1`, which will keep at least 1 warm instance ready for your bot function. + +It has been configured to only allow a concurrency of 1 (`max_inputs=1`) as each user will require their own running function. \ No newline at end of file diff --git a/examples/deployment/modal-example/__init__.py b/examples/deployment/modal-example/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/deployment/modal-example/app.py b/examples/deployment/modal-example/app.py new file mode 100644 index 000000000..97a9f32ab --- /dev/null +++ b/examples/deployment/modal-example/app.py @@ -0,0 +1,75 @@ +import os + +import aiohttp +import modal +from fastapi import HTTPException +from fastapi.responses import JSONResponse +from loguru import logger + +from bot import _voice_bot_process + +MAX_SESSION_TIME = 15 * 60 # 15 minutes + +app = modal.App("pipecat-modal") + + +image = modal.Image.debian_slim(python_version="3.12").pip_install_from_requirements( + "requirements.txt" +) + + +@app.function( + image=image, + cpu=1.0, + secrets=[modal.Secret.from_dotenv()], + keep_warm=1, + enable_memory_snapshot=True, + max_inputs=1, # Do not reuse instances across requests + retries=0, +) +def launch_bot_process(room_url: str, token: str): + _voice_bot_process(room_url, token) + + +@app.function( + image=image, + secrets=[modal.Secret.from_dotenv()], +) +@modal.web_endpoint(method="POST") +async def start(): + from pipecat.transports.services.helpers.daily_rest import ( + DailyRESTHelper, + DailyRoomParams, + ) + + logger.info("Request received") + + async with aiohttp.ClientSession() as session: + daily_rest_helper = DailyRESTHelper( + daily_api_key=os.getenv("DAILY_API_KEY", ""), + daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"), + aiohttp_session=session, + ) + + # Create new Daily room + room = await daily_rest_helper.create_room(DailyRoomParams()) + if not room.url: + raise HTTPException( + status_code=500, + detail="Unable to create room", + ) + logger.info(f"Created room: {room.url}") + + # Create bot token for room + token = await daily_rest_helper.get_token(room.url, MAX_SESSION_TIME) + if not token: + raise HTTPException(status_code=500, detail=f"Failed to get token for room: {room.url}") + + logger.info(f"Bot token created: {token}") + + # Spawn a new bot process + launch_bot_process.spawn(room_url=room.url, token=token) + + # Return room URL to the user to join + # Note: in production, you would want to return a token to the user + return JSONResponse(content={"room_url": room.url, token: token}) diff --git a/examples/deployment/modal-example/bot.py b/examples/deployment/modal-example/bot.py new file mode 100644 index 000000000..8ab67ff82 --- /dev/null +++ b/examples/deployment/modal-example/bot.py @@ -0,0 +1,90 @@ +import asyncio +import os +import sys + +from dotenv import load_dotenv +from loguru import logger + +load_dotenv(override=True) + +logger.remove(0) +logger.add(sys.stderr, level="DEBUG") + + +async def main(room_url: str, token: str): + from pipecat.audio.vad.silero import SileroVADAnalyzer + from pipecat.frames.frames import EndFrame, 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 + + 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, + 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): + await transport.capture_participant_transcription(participant["id"]) + messages.append({"role": "system", "content": "Please 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): + await task.queue_frame(EndFrame()) + + runner = PipelineRunner() + + await runner.run(task) + + +def _voice_bot_process(room_url: str, token: str): + asyncio.run(main(room_url, token)) diff --git a/examples/deployment/modal-example/env.example b/examples/deployment/modal-example/env.example new file mode 100644 index 000000000..ce2f54104 --- /dev/null +++ b/examples/deployment/modal-example/env.example @@ -0,0 +1,3 @@ +DAILY_API_KEY= +OPENAI_API_KEY= +CARTESIA_API_KEY= \ No newline at end of file diff --git a/examples/deployment/modal-example/requirements.txt b/examples/deployment/modal-example/requirements.txt new file mode 100644 index 000000000..805717626 --- /dev/null +++ b/examples/deployment/modal-example/requirements.txt @@ -0,0 +1,5 @@ +python-dotenv==1.0.1 +modal==0.65.48 +pipecat-ai[daily,silero,cartesia,openai]==0.0.48 +fastapi==0.115.4 +aiohttp==3.10.10 \ No newline at end of file