diff --git a/LICENSE b/LICENSE index 09951d9..fa66142 100644 --- a/LICENSE +++ b/LICENSE @@ -1,17 +1,21 @@ -MIT No Attribution +MIT License -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +Copyright (c) 2025 Pipecat Voice AI Agent AWS Deployment -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/speech-to-speech/README.md b/speech-to-speech/README.md index 8155496..c5be5ea 100644 --- a/speech-to-speech/README.md +++ b/speech-to-speech/README.md @@ -43,6 +43,10 @@ The following projects were developed by AWS teams and showcase examples of how This serverless implementation provides a lightweight, easily deployable, and scalable Nova Sonic infrastructure using AWS Lambda and AppSync Events, offering a streamlined approach to real-time speech-to-speech communication. It features serverless real-time communication between server and client using AppSync Events, reference to past conversation history, tool use implementation, automatic resume for conversations exceeding 8 minutes, and an extensible web UI built with Next.js. +- [Pipecat Voice AI Agent - Production AWS Deployment](sample-codes/pipecat-voice-agent/) + + A comprehensive production-ready deployment of the Pipecat Voice AI Agent featuring dual-channel voice interactions through both Twilio phone calls and WebRTC browser chat. This sample demonstrates AWS Nova Sonic integration with complete infrastructure as code using AWS CDK, supporting both ECS and EKS deployment options. It includes SSL certificate management for Twilio webhooks, auto-scaling, monitoring, security best practices, and comprehensive documentation for production deployments. + - [Sonic Playground for Experimenting](https://github.com/aws-samples/sample-sonic-java-playground) This solution serves as an experimental playground for developers to test and optimize Nova Sonic capabilities by configuring various model parameters and finding the optimal settings for their specific use cases. The application supports creating new conversation sessions with voice IDs for language selection, TopP, Temperature, MaxTokens for response length control, and system prompts. Built with Java Spring Boot and React, it provides a reference implementation for speech-to-speech applications. diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/.env.example b/speech-to-speech/sample-codes/pipecat-voice-agent/.env.example new file mode 100644 index 0000000..1fd037f --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/.env.example @@ -0,0 +1,25 @@ +# Daily.co API Configuration +DAILY_API_KEY=your_daily_api_key_here +DAILY_API_URL=https://api.daily.co/v1 + +# AWS Configuration +AWS_ACCESS_KEY_ID=your_aws_access_key_here +AWS_SECRET_ACCESS_KEY=your_aws_secret_key_here +AWS_REGION=us-east-1 + +# Twilio Configuration (for phone service) +TWILIO_RECOVERY_CODE=your_twilio_recovery_code +TWILIO_ACCOUNT_SID=your_twilio_account_sid +TWILIO_AUTH_TOKEN=your_twilio_auth_token +TWILIO_PHONE_NUMBER=+1234567890 +TWILIO_SID=your_twilio_sid +TWILIO_SECRET=your_twilio_secret +TWILIO_AUTH_LIVE=your_twilio_auth_live + +# Optional Configuration +ENVIRONMENT=development +LOG_LEVEL=INFO +HOST=0.0.0.0 +FAST_API_PORT=7860 +MAX_BOTS_PER_ROOM=1 +MAX_CONCURRENT_ROOMS=10 \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/README.md b/speech-to-speech/sample-codes/pipecat-voice-agent/README.md new file mode 100644 index 0000000..a0fdf9f --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/README.md @@ -0,0 +1,164 @@ +# Pipecat Voice AI Agent - AWS Cloud Deployment + +A production-ready containerized deployment of the Pipecat Voice AI Agent on AWS, featuring both WebRTC and Twilio phone integration with AWS Nova Sonic for natural voice conversations. Supports both ECS and EKS deployment options. + +## Overview + +This sample demonstrates how to deploy a voice AI agent using: +- **AWS Nova Sonic** for speech-to-text and text-to-speech +- **Pipecat framework** for voice AI conversations +- **Twilio** for phone call integration +- **Daily.co** for WebRTC browser-based voice chat +- **AWS ECS/EKS** for scalable container deployment +- **AWS CDK** for infrastructure as code + +## Architecture + +The solution provides two deployment options: +- **ECS**: Managed container orchestration with Fargate +- **EKS**: Kubernetes-native deployment with Fargate + +Both support: +- Phone calls via Twilio WebSocket integration +- Browser voice chat via WebRTC +- AWS Nova Sonic for natural voice processing +- Production-ready monitoring and scaling + +## Prerequisites + +- AWS CLI configured with appropriate permissions +- Node.js 18+ and npm +- Docker +- Python 3.10+ +- AWS CDK CLI (`npm install -g aws-cdk`) + +## Quick Start + +1. **Clone and setup**: +```bash +git clone +cd speech-to-speech/sample-codes/pipecat-voice-agent +./setup-project.sh +``` + +2. **Configure secrets**: +```bash +cp .env.example .env +# Edit .env with your API keys +python3 scripts/setup-secrets.py +``` + +3. **Deploy infrastructure** (choose ECS or EKS): + +**ECS Deployment:** +```bash +cd infrastructure +./deploy.sh --environment test --region us-east-1 +``` + +**EKS Deployment:** +```bash +cd infrastructure/-eks +cdk deploy PipecatEksStack --parameters environment=test +``` + +4. **Build and deploy application**: +```bash +./scripts/build-and-push.sh -e test -t latest +./scripts/deploy-service.sh -e test -t latest +``` + +## Key Features + +### Voice AI Capabilities +- **Natural Conversations**: AWS Nova Sonic provides human-like speech synthesis +- **Real-time Processing**: Low-latency speech-to-text and text-to-speech +- **Multi-channel Support**: Both phone calls and web browser voice chat +- **Function Calling**: Example weather function with extensible architecture + +### Production Infrastructure +- **Auto-scaling**: ECS/EKS services scale based on demand +- **High Availability**: Multi-AZ deployment with load balancing +- **Security**: AWS Secrets Manager, IAM roles, VPC isolation +- **Monitoring**: CloudWatch logs, metrics, and health checks +- **SSL/TLS**: Automatic certificate management for Twilio webhooks + +### Twilio Integration +- **Phone Number Support**: Inbound calls to your Twilio number +- **WebSocket Streaming**: Real-time bidirectional audio +- **SSL Certificate Requirements**: Production-ready HTTPS endpoints +- **Call Management**: Active call monitoring and session handling + +## Environment Variables + +Required configuration (stored in AWS Secrets Manager): + +```bash +# Daily.co WebRTC +DAILY_API_KEY=your_daily_api_key + +# AWS Configuration +AWS_REGION=us-east-1 +AWS_ACCESS_KEY_ID=your_access_key +AWS_SECRET_ACCESS_KEY=your_secret_key + +# Twilio Phone Integration +TWILIO_ACCOUNT_SID=your_account_sid +TWILIO_AUTH_TOKEN=your_auth_token +TWILIO_PHONE_NUMBER=+1234567890 +``` + +## Testing Your Deployment + +### WebRTC Voice Chat +1. Visit your load balancer URL +2. Click "Connect" to join a voice room +3. Speak to interact with the AI agent + +### Phone Integration +1. Configure Twilio webhook to point to your deployment +2. Call your Twilio phone number +3. Have a voice conversation with the AI + +## Important: Twilio SSL Requirements + +For production Twilio integration: +- **Valid SSL Certificate**: Must be from a trusted CA (Let's Encrypt, etc.) +- **No Self-Signed Certificates**: Twilio rejects untrusted certificates +- **HTTPS Required**: Use standard port 443 +- **Load Balancer SSL**: AWS automatically handles certificate management + +## Documentation + +- [EKS Architecture Overview](docs/EKS_ARCHITECTURE.md) +- [Deployment Guide](infrastructure/DEPLOYMENT_GUIDE.md) +- [Cleanup Guide](docs/CLEANUP_GUIDE.md) +- [Troubleshooting Guide](docs/TROUBLESHOOTING_GUIDE.md) + +## Cost Considerations + +- **Fargate**: Pay only for running containers +- **Nova Sonic**: Usage-based pricing for speech processing +- **Load Balancers**: Fixed hourly cost plus data transfer +- **Twilio**: Per-minute charges for phone calls + +## Security Best Practices + +- Secrets stored in AWS Secrets Manager +- IAM roles with least-privilege access +- VPC isolation with security groups +- Container runs as non-root user +- TLS encryption for all external communication + +## Contributing + +This sample follows AWS best practices for: +- Infrastructure as Code (CDK) +- Container security +- Monitoring and observability +- Cost optimization +- Multi-AZ high availability + +## License + +This sample code is made available under the MIT-0 license. See the LICENSE file. \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/aws/README.md b/speech-to-speech/sample-codes/pipecat-voice-agent/aws/README.md new file mode 100644 index 0000000..c3ec969 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/aws/README.md @@ -0,0 +1,51 @@ +# AWS Configuration + +This directory contains AWS-specific configuration files for the Pipecat ECS deployment. + +## Structure + +### Policies (`policies/`) + +- `ecs-task-execution-role-trust-policy.json` - Trust policy for ECS task execution role +- `execution-role-secrets-policy.json` - Policy for accessing AWS Secrets Manager +- `pipecat-task-policy.json` - Task-specific permissions policy + +### Task Definitions (`task-definitions/`) + +- `phone-task-definition.json` - ECS task definition for the phone service + +## Usage + +These files are typically used by: + +- AWS CDK infrastructure deployment (in `infrastructure/` directory) +- Manual AWS CLI commands for policy and role creation +- ECS service deployment scripts + +## Policy Overview + +### ECS Task Execution Role + +Allows ECS to pull images from ECR and write logs to CloudWatch. + +### Secrets Access Policy + +Grants access to specific secrets in AWS Secrets Manager for: + +- Daily.co API keys +- Twilio credentials +- Other application secrets + +### Task Policy + +Application-level permissions for: + +- AWS Bedrock access +- CloudWatch logging +- Other AWS services used by the application + +## Notes + +- These policies follow the principle of least privilege +- Secrets are injected as environment variables by ECS +- All configurations are designed for production security standards diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/aws/policies/ecs-task-execution-role-trust-policy.json b/speech-to-speech/sample-codes/pipecat-voice-agent/aws/policies/ecs-task-execution-role-trust-policy.json new file mode 100644 index 0000000..57608a2 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/aws/policies/ecs-task-execution-role-trust-policy.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/aws/policies/execution-role-secrets-policy.json b/speech-to-speech/sample-codes/pipecat-voice-agent/aws/policies/execution-role-secrets-policy.json new file mode 100644 index 0000000..3190633 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/aws/policies/execution-role-secrets-policy.json @@ -0,0 +1,14 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "secretsmanager:GetSecretValue" + ], + "Resource": [ + "arn:aws:secretsmanager:eu-north-1:094271239310:secret:pipecat/*" + ] + } + ] +} \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/aws/policies/pipecat-task-policy.json b/speech-to-speech/sample-codes/pipecat-voice-agent/aws/policies/pipecat-task-policy.json new file mode 100644 index 0000000..58f2861 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/aws/policies/pipecat-task-policy.json @@ -0,0 +1,22 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "secretsmanager:GetSecretValue" + ], + "Resource": [ + "arn:aws:secretsmanager:eu-north-1:094271239310:secret:pipecat/*" + ] + } + ] +} \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/aws/task-definitions/phone-task-definition.json b/speech-to-speech/sample-codes/pipecat-voice-agent/aws/task-definitions/phone-task-definition.json new file mode 100644 index 0000000..845699f --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/aws/task-definitions/phone-task-definition.json @@ -0,0 +1,79 @@ +{ + "family": "pipecat-phone-task-test", + "networkMode": "awsvpc", + "requiresCompatibilities": ["FARGATE"], + "cpu": "2048", + "memory": "4096", + "executionRoleArn": "arn:aws:iam::094271239310:role/pipecat-ecs-execution-role", + "taskRoleArn": "arn:aws:iam::094271239310:role/pipecat-ecs-task-role", + "containerDefinitions": [ + { + "name": "pipecat-container", + "image": "094271239310.dkr.ecr.eu-north-1.amazonaws.com/pipecat-voice-agent-test:phone-amd64", + "portMappings": [ + { + "containerPort": 7860, + "protocol": "tcp" + } + ], + "environment": [ + { + "name": "AWS_REGION", + "value": "eu-north-1" + }, + { + "name": "HOST", + "value": "0.0.0.0" + }, + { + "name": "FAST_API_PORT", + "value": "7860" + } + ], + "secrets": [ + { + "name": "DAILY_API_KEY", + "valueFrom": "arn:aws:secretsmanager:eu-north-1:094271239310:secret:pipecat/daily-api-key-IERDHx" + }, + { + "name": "AWS_ACCESS_KEY_ID", + "valueFrom": "arn:aws:secretsmanager:eu-north-1:094271239310:secret:pipecat/aws-credentials-fZt1aK:AWS_ACCESS_KEY_ID::" + }, + { + "name": "AWS_SECRET_ACCESS_KEY", + "valueFrom": "arn:aws:secretsmanager:eu-north-1:094271239310:secret:pipecat/aws-credentials-fZt1aK:AWS_SECRET_ACCESS_KEY::" + }, + { + "name": "TWILIO_ACCOUNT_SID", + "valueFrom": "arn:aws:secretsmanager:eu-north-1:094271239310:secret:pipecat/twilio-credentials-XyAjIL:TWILIO_ACCOUNT_SID::" + }, + { + "name": "TWILIO_AUTH_TOKEN", + "valueFrom": "arn:aws:secretsmanager:eu-north-1:094271239310:secret:pipecat/twilio-credentials-XyAjIL:TWILIO_AUTH_TOKEN::" + }, + { + "name": "TWILIO_PHONE_NUMBER", + "valueFrom": "arn:aws:secretsmanager:eu-north-1:094271239310:secret:pipecat/twilio-credentials-XyAjIL:TWILIO_PHONE_NUMBER::" + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/pipecat-voice-agent-test", + "awslogs-region": "eu-north-1", + "awslogs-stream-prefix": "phone" + } + }, + "healthCheck": { + "command": [ + "CMD-SHELL", + "curl -f http://localhost:7860/health || exit 1" + ], + "interval": 30, + "timeout": 5, + "retries": 3, + "startPeriod": 60 + } + } + ] +} \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/bot.py b/speech-to-speech/sample-codes/pipecat-voice-agent/bot.py new file mode 100644 index 0000000..75facdc --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/bot.py @@ -0,0 +1,154 @@ +import asyncio +import os +import argparse +import aiohttp +from datetime import datetime +from dotenv import load_dotenv + +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.audio.vad.silero import SileroVADAnalyzer, VADParams +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.services.aws_nova_sonic.aws import AWSNovaSonicLLMService +from pipecat.services.aws.llm import AWSBedrockLLMContext +from pipecat.services.llm_service import FunctionCallParams +from pipecat.transports.services.daily import DailyParams, DailyTransport + +load_dotenv(override=True) + + +async def fetch_weather_from_api(params: FunctionCallParams): + temperature = 75 if params.arguments["format"] == "fahrenheit" else 24 + await params.result_callback( + { + "conditions": "nice", + "temperature": temperature, + "format": params.arguments["format"], + "timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"), + } + ) + + +weather_function = FunctionSchema( + name="get_current_weather", + description="Get the current weather", + properties={ + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the users location.", + }, + }, + required=["location", "format"], +) + +# Create tools schema +tools = ToolsSchema(standard_tools=[weather_function]) + + +async def main(room_url, token): + """Main bot execution function. + + Sets up and runs the bot pipeline including: + - Set up WebRTC transport + - Speech-to-text and text-to-speech services + - Language model integration + """ + async with aiohttp.ClientSession() as session: + print(f"Starting server with room: {room_url}") + + # Set up Daily transport with audio parameters + transport = DailyTransport( + room_url, + token, + "Amazon Voice AI Agent", + DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + camera_in_enabled=False, + camera_out_enabled=False, + vad_enabled=True, + vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.5)), + transcription_enabled=True, + ), + ) + + # Initialize LLM service + llm = AWSNovaSonicLLMService( + access_key_id=os.getenv("AWS_ACCESS_KEY_ID"), + secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"), + region=os.getenv("AWS_REGION"), + voice_id="tiffany", # matthew, tiffany, amy + ) + + # Register function for function calls + llm.register_function("get_current_weather", fetch_weather_from_api) + + # Set up context and context management. + system_instruction = ( + "You are a friendly assistant. The user and you will engage in a spoken dialog exchanging " + "the transcripts of a natural real-time conversation. Keep your responses short, generally " + "two or three sentences for chatty scenarios. " + "Start by greeting the user." + ) + context = AWSBedrockLLMContext( + messages=[{"role": "system", "content": f"{system_instruction}"}], + tools=tools, + ) + context_aggregator = llm.create_context_aggregator(context) + + await asyncio.sleep(0.1) # Give Nova Sonic time to initialise + + # Build the pipeline + pipeline = Pipeline( + [ + transport.input(), + context_aggregator.user(), + llm, + transport.output(), + context_aggregator.assistant(), + ] + ) + + # Configure the pipeline task + task = PipelineTask( + pipeline, + params=PipelineParams( + allow_interruptions=True, + enable_metrics=True, + enable_usage_metrics=True, + ), + ) + + @transport.event_handler("on_first_participant_joined") + async def on_first_participant_joined(transport, participant): + await transport.capture_participant_transcription(participant["id"]) + await task.queue_frames([context_aggregator.user().get_context_frame()]) + + @transport.event_handler("on_participant_left") + async def on_participant_left(transport, participant, reason): + print(f"Participant left: {participant}") + await task.cancel() + + runner = PipelineRunner(handle_sigint=False) + await runner.run(task) + + +if __name__ == "__main__": + # Parse command line arguments for server configuration + default_host = os.getenv("HOST", "0.0.0.0") + default_port = int(os.getenv("FAST_API_PORT", "7860")) + + parser = argparse.ArgumentParser(description="Daily FastAPI server") + parser.add_argument("-u", "--url", type=str, help="Daily room url") + parser.add_argument("-t", "--token", type=str, help="Daily room token") + + config = parser.parse_args() + + asyncio.run(main(config.url, config.token)) diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/config/README.md b/speech-to-speech/sample-codes/pipecat-voice-agent/config/README.md new file mode 100644 index 0000000..0114d90 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/config/README.md @@ -0,0 +1,30 @@ +# Configuration + +This directory contains application configuration files. + +## Files + +- `deployment_config.py` - Main deployment configuration including environment settings, resource limits, and AWS service configurations + +## Usage + +The deployment configuration is imported by the main application: + +```python +from config.deployment_config import config +``` + +## Configuration Structure + +The deployment config typically includes: + +- Environment-specific settings (development/production) +- Resource limits and constraints +- AWS service configurations +- Application-specific parameters + +## Notes + +- Configuration files should not contain sensitive information like API keys +- Use environment variables for secrets and sensitive data +- Different environments can have different configuration values diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/config/deployment_config.py b/speech-to-speech/sample-codes/pipecat-voice-agent/config/deployment_config.py new file mode 100644 index 0000000..ab3e928 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/config/deployment_config.py @@ -0,0 +1,85 @@ +""" +Deployment configuration for Pipecat Voice AI Agent. + +This module provides configuration management for different deployment environments. +""" + +import os +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class DeploymentConfig: + """Configuration class for deployment settings.""" + + # Environment settings + environment: str = "development" + log_level: str = "INFO" + + # Server settings + host: str = "0.0.0.0" + port: int = 7860 + + # AWS settings + aws_region: str = "eu-north-1" + + # Daily.co settings + daily_api_url: str = "https://api.daily.co/v1" + + # ECS specific settings + max_bots_per_room: int = 1 + health_check_timeout: int = 30 + graceful_shutdown_timeout: int = 30 + + # Resource limits and stability settings + max_concurrent_rooms: int = 10 + bot_cleanup_interval: int = 300 # 5 minutes + memory_cleanup_threshold: float = 0.8 # 80% memory usage + + # Health check settings + health_check_interval: int = 30 + health_check_retries: int = 3 + + # Performance tuning + enable_request_pooling: bool = True + max_request_pool_size: int = 100 + request_timeout: int = 30 + + @classmethod + def from_environment(cls) -> "DeploymentConfig": + """Create configuration from environment variables.""" + return cls( + environment=os.getenv("ENVIRONMENT", "development"), + log_level=os.getenv("LOG_LEVEL", "INFO"), + host=os.getenv("HOST", "0.0.0.0"), + port=int(os.getenv("FAST_API_PORT", "7860")), + aws_region=os.getenv("AWS_REGION", "eu-north-1"), + daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"), + max_bots_per_room=int(os.getenv("MAX_BOTS_PER_ROOM", "1")), + health_check_timeout=int(os.getenv("HEALTH_CHECK_TIMEOUT", "30")), + graceful_shutdown_timeout=int(os.getenv("GRACEFUL_SHUTDOWN_TIMEOUT", "30")), + max_concurrent_rooms=int(os.getenv("MAX_CONCURRENT_ROOMS", "10")), + bot_cleanup_interval=int(os.getenv("BOT_CLEANUP_INTERVAL", "300")), + memory_cleanup_threshold=float( + os.getenv("MEMORY_CLEANUP_THRESHOLD", "0.8") + ), + health_check_interval=int(os.getenv("HEALTH_CHECK_INTERVAL", "30")), + health_check_retries=int(os.getenv("HEALTH_CHECK_RETRIES", "3")), + enable_request_pooling=os.getenv("ENABLE_REQUEST_POOLING", "true").lower() + == "true", + max_request_pool_size=int(os.getenv("MAX_REQUEST_POOL_SIZE", "100")), + request_timeout=int(os.getenv("REQUEST_TIMEOUT", "30")), + ) + + def is_production(self) -> bool: + """Check if running in production environment.""" + return self.environment.lower() == "production" + + def is_development(self) -> bool: + """Check if running in development environment.""" + return self.environment.lower() == "development" + + +# Global configuration instance +config = DeploymentConfig.from_environment() diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docker/Dockerfile b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/Dockerfile new file mode 100644 index 0000000..2c79638 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/Dockerfile @@ -0,0 +1,77 @@ +FROM python:3.12-slim + +# Install system dependencies and clean up in same layer +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + tini \ + && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Create non-root user and group with specific UID/GID for security +RUN groupadd -r -g 1001 pipecat && \ + useradd -r -g pipecat -u 1001 -m -d /home/pipecat pipecat + +# Set working directory +WORKDIR /app + +# Copy requirements first for better Docker layer caching +COPY requirements.txt . + +# Install Python dependencies with security improvements +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir --no-compile -r requirements.txt && \ + pip cache purge + +# Copy application files +COPY *.py ./ + +# Create directories that might be referenced (even if empty) +RUN mkdir -p assets utils logs && \ + # Create a logs directory with proper permissions + mkdir -p /app/logs && \ + # Ensure proper permissions for application directories + chmod 755 /app/assets /app/utils /app/logs + +# Change ownership to non-root user +RUN chown -R pipecat:pipecat /app + +# Switch to non-root user +USER pipecat + +# Add labels for better container management and metadata +LABEL maintainer="pipecat-deployment" \ + version="1.0" \ + description="Pipecat Voice AI Agent for ECS deployment" \ + org.opencontainers.image.title="Pipecat Voice AI Agent" \ + org.opencontainers.image.description="Containerized Pipecat voice AI agent for AWS ECS deployment" \ + org.opencontainers.image.version="1.0" \ + org.opencontainers.image.vendor="Pipecat Deployment" + +# Health check configuration with improved reliability and multiple endpoints +HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \ + CMD curl -f http://localhost:7860/health && curl -f http://localhost:7860/ready || exit 1 + +# Expose port +EXPOSE 7860 + +# Set environment variables for better container behavior +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + # Add graceful shutdown timeout + GRACEFUL_TIMEOUT=30 \ + # Optimize Python for container environment + PYTHONHASHSEED=random \ + # Set default host and port for container + HOST=0.0.0.0 \ + FAST_API_PORT=7860 + +# Use tini as init system for proper signal handling and zombie reaping +ENTRYPOINT ["/usr/bin/tini", "--"] + +# Use exec form for proper signal handling +CMD ["python3", "server.py", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docker/Dockerfile.phone b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/Dockerfile.phone new file mode 100644 index 0000000..60cd8bd --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/Dockerfile.phone @@ -0,0 +1,83 @@ +FROM python:3.12-slim + +# Install system dependencies and clean up in same layer +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + tini \ + && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Create non-root user and group with specific UID/GID for security +RUN groupadd -r -g 1001 pipecat && \ + useradd -r -g pipecat -u 1001 -m -d /home/pipecat pipecat + +# Set working directory +WORKDIR /app + +# Copy requirements first for better Docker layer caching +COPY requirements.txt . + +# Install Python dependencies with security improvements +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir --no-compile -r requirements.txt && \ + pip cache purge + +# Create config directory +RUN mkdir -p config + +# Copy application files - specifically for phone service +COPY server_clean.py ./ +COPY bot.py ./ +COPY logger_config.py ./ +COPY config/deployment_config.py ./config/ + +# Create directories that might be referenced (even if empty) +RUN mkdir -p assets utils logs && \ + # Create a logs directory with proper permissions + mkdir -p /app/logs && \ + # Ensure proper permissions for application directories + chmod 755 /app/assets /app/utils /app/logs + +# Change ownership to non-root user +RUN chown -R pipecat:pipecat /app + +# Switch to non-root user +USER pipecat + +# Add labels for better container management and metadata +LABEL maintainer="pipecat-deployment" \ + version="1.0" \ + description="Pipecat Phone Service for Twilio Integration" \ + org.opencontainers.image.title="Pipecat Phone Service" \ + org.opencontainers.image.description="Containerized Pipecat phone service for Twilio integration" \ + org.opencontainers.image.version="1.0" \ + org.opencontainers.image.vendor="Pipecat Deployment" + +# Health check configuration with improved reliability +HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \ + CMD curl -f http://localhost:7860/health || exit 1 + +# Expose port +EXPOSE 7860 + +# Set environment variables for better container behavior +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + # Add graceful shutdown timeout + GRACEFUL_TIMEOUT=30 \ + # Optimize Python for container environment + PYTHONHASHSEED=random \ + # Set default host and port for container + HOST=0.0.0.0 \ + FAST_API_PORT=7860 + +# Use tini as init system for proper signal handling and zombie reaping +ENTRYPOINT ["/usr/bin/tini", "--"] + +# Use exec form for proper signal handling - specifically run server_clean.py +CMD ["python3", "server_clean.py", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docker/README.md b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/README.md new file mode 100644 index 0000000..f5787a9 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/README.md @@ -0,0 +1,62 @@ +# Docker Configuration + +This directory contains all Docker-related files for the Pipecat ECS deployment project. + +## Files + +### Dockerfiles + +- `Dockerfile` - Main application container for the Pipecat voice AI agent +- `Dockerfile.phone` - Phone service container for Twilio integration using server_clean.py + +### Docker Compose + +- `docker-compose.test.yml` - Test configuration for local development and testing + +### Scripts + +- `scripts/build-phone-service.sh` - Build the phone service container +- `scripts/build-phone-quick.sh` - Quick build script for phone service +- `scripts/docker-test-setup.sh` - Setup script for Docker testing environment +- `scripts/test-docker-locally.sh` - Local Docker testing script + +## Usage + +### Building Images + +From the project root directory: + +```bash +# Build main application +docker build -f docker/Dockerfile -t pipecat-app . + +# Build phone service +docker build -f docker/Dockerfile.phone -t pipecat-phone-service . +``` + +### Using Scripts + +```bash +# Quick phone service build +./docker/scripts/build-phone-quick.sh + +# Full phone service build and test +./docker/scripts/build-phone-service.sh + +# Local testing setup +./docker/scripts/docker-test-setup.sh +``` + +### Docker Compose Testing + +```bash +# Run test environment +docker-compose -f docker/docker-compose.test.yml up +``` + +## Notes + +- All Docker builds should be run from the project root directory +- The phone service container uses `server_clean.py` for Twilio integration +- Health checks are configured for ECS compatibility +- Images use non-root users for security diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docker/docker-compose.test.yml b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/docker-compose.test.yml new file mode 100644 index 0000000..cdda7d6 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/docker-compose.test.yml @@ -0,0 +1,31 @@ +version: "3.8" + +services: + pipecat-phone-service: + build: + context: .. + dockerfile: docker/Dockerfile.phone + container_name: pipecat-phone-test + ports: + - "7860:7860" + env_file: + - .env + environment: + - HOST=0.0.0.0 + - FAST_API_PORT=7860 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:7860/health"] + interval: 30s + timeout: 10s + start_period: 90s + retries: 3 + restart: unless-stopped + volumes: + # Optional: Mount logs directory for debugging + - ./logs:/app/logs + networks: + - pipecat-network + +networks: + pipecat-network: + driver: bridge diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docker/scripts/build-phone-quick.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/scripts/build-phone-quick.sh new file mode 100755 index 0000000..04e557d --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/scripts/build-phone-quick.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Quick build script using existing ECR repository +# Since server_clean.py handles both WebRTC and phone calls + +set -e + +ENVIRONMENT="test" +AWS_REGION="eu-north-1" +IMAGE_TAG="latest" +REPO_URI="094271239310.dkr.ecr.eu-north-1.amazonaws.com/pipecat-phone-service-test" + +echo "๐Ÿณ Building phone service using existing repository" +echo "Repository: $REPO_URI" +echo "Tag: $IMAGE_TAG" + +# Login to ECR +echo "๐Ÿ” Logging in to ECR..." +aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REPO_URI + +# Build the image for linux/amd64 platform (required for ECS Fargate) +echo "๐Ÿ”จ Building Docker image for linux/amd64..." +docker build --platform linux/amd64 -f ../Dockerfile.phone -t pipecat-phone-service:$IMAGE_TAG .. + +# Tag for ECR +echo "๐Ÿท๏ธ Tagging for ECR..." +docker tag pipecat-phone-service:$IMAGE_TAG $REPO_URI:$IMAGE_TAG + +# Push to ECR +echo "๐Ÿ“ค Pushing to ECR..." +docker push $REPO_URI:$IMAGE_TAG + +echo "โœ… Build complete!" +echo "Image: $REPO_URI:$IMAGE_TAG" +echo "" +echo "Next: Update your ECS service to use this image" \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docker/scripts/build-phone-service.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/scripts/build-phone-service.sh new file mode 100755 index 0000000..36c8343 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/scripts/build-phone-service.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# Build script for Pipecat Phone Service container + +set -e + +echo "๐Ÿ”จ Building Pipecat Phone Service container..." + +# Build the container image +docker build -f ../Dockerfile.phone -t pipecat-phone-service:latest .. + +echo "โœ… Container built successfully!" + +# Test the container locally (optional) +if [ "$1" = "--test" ]; then + echo "๐Ÿงช Testing container locally..." + + # Check if .env file exists + if [ -f ".env" ]; then + echo "๐Ÿ“‹ Using .env file for environment variables" + docker run --rm -p 7860:7860 --env-file .env pipecat-phone-service:latest & + else + echo "โš ๏ธ No .env file found. Running with minimal environment..." + docker run --rm -p 7860:7860 \ + -e HOST=0.0.0.0 \ + -e FAST_API_PORT=7860 \ + pipecat-phone-service:latest & + fi + + CONTAINER_PID=$! + + # Wait a bit for container to start + sleep 10 + + # Test health endpoint + echo "๐Ÿฅ Testing health endpoint..." + if curl -f http://localhost:7860/health; then + echo "โœ… Health check passed!" + else + echo "โŒ Health check failed!" + fi + + # Stop the container + kill $CONTAINER_PID 2>/dev/null || true + + echo "๐Ÿงช Container test completed!" +fi + +echo "๐Ÿš€ Phone service container is ready for deployment!" +echo "" +echo "To run locally:" +echo " docker run --rm -p 7860:7860 --env-file .env pipecat-phone-service:latest" +echo "" +echo "To test health endpoint:" +echo " curl http://localhost:7860/health" \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docker/scripts/docker-test-setup.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/scripts/docker-test-setup.sh new file mode 100755 index 0000000..2060375 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/scripts/docker-test-setup.sh @@ -0,0 +1,158 @@ +#!/bin/bash + +# Docker Test Setup Script for Pipecat Phone Service +# This script helps test the Docker container locally before cloud deployment + +set -e + +echo "๐Ÿณ Setting up Docker test environment for Pipecat Phone Service" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if .env file exists +if [ ! -f ".env" ]; then + print_error ".env file not found!" + echo "Please create a .env file with your credentials first." + exit 1 +fi + +# Create a test-specific environment file (sanitized for Docker) +print_status "Creating Docker test environment file..." + +# Create .env.docker from .env but with Docker-friendly settings +cp .env .env.docker + +# Override host settings for Docker +echo "" >> .env.docker +echo "# Docker-specific overrides" >> .env.docker +echo "HOST=0.0.0.0" >> .env.docker +echo "FAST_API_PORT=7860" >> .env.docker + +print_success "Created .env.docker file" + +# Build the Docker image +print_status "Building Docker image..." +docker build -f ../Dockerfile.phone -t pipecat-phone-service:test .. + +if [ $? -eq 0 ]; then + print_success "Docker image built successfully" +else + print_error "Failed to build Docker image" + exit 1 +fi + +# Check if container is already running +if [ "$(docker ps -q -f name=pipecat-phone-test)" ]; then + print_warning "Stopping existing container..." + docker stop pipecat-phone-test + docker rm pipecat-phone-test +fi + +print_status "Starting Docker container..." + +# Run the container with environment variables +docker run -d \ + --name pipecat-phone-test \ + --env-file .env.docker \ + -p 7860:7860 \ + --health-cmd="curl -f http://localhost:7860/health || exit 1" \ + --health-interval=30s \ + --health-timeout=10s \ + --health-start-period=90s \ + --health-retries=3 \ + pipecat-phone-service:test + +if [ $? -eq 0 ]; then + print_success "Container started successfully" + echo "Container name: pipecat-phone-test" + echo "Port mapping: 7860:7860" + echo "Health check: enabled" +else + print_error "Failed to start container" + exit 1 +fi + +# Wait for container to be ready +print_status "Waiting for container to be ready..." +sleep 10 + +# Check container status +print_status "Checking container status..." +docker ps -f name=pipecat-phone-test + +# Check container logs +print_status "Recent container logs:" +docker logs --tail 20 pipecat-phone-test + +# Test health endpoint +print_status "Testing health endpoint..." +sleep 5 + +if curl -f http://localhost:7860/health > /dev/null 2>&1; then + print_success "Health check passed!" + echo "" + echo "๐ŸŽ‰ Container is running successfully!" + echo "" + echo "Available endpoints:" + echo " - Health check: http://localhost:7860/health" + echo " - Incoming calls: http://localhost:7860/incoming-call" + echo " - Active calls: http://localhost:7860/active-calls" + echo " - WebRTC: http://localhost:7860/" + echo "" + echo "To test with Twilio using serveo:" + echo " 1. Run: ssh -R 80:localhost:7860 serveo.net" + echo " 2. Copy the serveo URL from the output" + echo " 3. Set Twilio webhook to: https://your-serveo-url/incoming-call" + echo "" + echo "Useful commands:" + echo " - View logs: docker logs -f pipecat-phone-test" + echo " - Stop container: docker stop pipecat-phone-test" + echo " - Remove container: docker rm pipecat-phone-test" + echo " - Shell into container: docker exec -it pipecat-phone-test /bin/bash" +else + print_error "Health check failed!" + echo "" + echo "Container logs:" + docker logs pipecat-phone-test + echo "" + echo "Container status:" + docker ps -a -f name=pipecat-phone-test +fi + +echo "" +print_status "Test setup complete!" + +# Optional: Start serveo tunnel +echo "" +read -p "Do you want to start a serveo tunnel now? (y/n): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + print_status "Starting serveo tunnel..." + echo "This will create a public tunnel to your local service." + echo "Copy the URL that appears and use it for your Twilio webhook." + echo "Press Ctrl+C to stop the tunnel when done testing." + echo "" + ssh -R 80:localhost:7860 serveo.net +fi \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docker/scripts/test-docker-locally.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/scripts/test-docker-locally.sh new file mode 100644 index 0000000..9c225a9 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docker/scripts/test-docker-locally.sh @@ -0,0 +1,150 @@ +#!/bin/bash + +# Comprehensive Docker Testing Script +# Tests the container functionality step by step + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +print_status() { echo -e "${BLUE}[INFO]${NC} $1"; } +print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } +print_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +echo "๐Ÿงช Testing Pipecat Phone Service Docker Container" +echo "================================================" + +# Test 1: Container Health +print_status "Test 1: Container Health Check" +if docker ps -f name=pipecat-phone-test | grep -q "pipecat-phone-test"; then + print_success "Container is running" + + # Check health status + health_status=$(docker inspect --format='{{.State.Health.Status}}' pipecat-phone-test 2>/dev/null || echo "no-health-check") + echo "Health status: $health_status" +else + print_error "Container is not running. Run ./docker-test-setup.sh first" + exit 1 +fi + +# Test 2: HTTP Health Endpoint +print_status "Test 2: HTTP Health Endpoint" +if response=$(curl -s http://localhost:7860/health); then + print_success "Health endpoint accessible" + echo "Response: $response" | jq . 2>/dev/null || echo "Response: $response" +else + print_error "Health endpoint not accessible" + echo "Container logs:" + docker logs --tail 10 pipecat-phone-test +fi + +# Test 3: Environment Variables +print_status "Test 3: Environment Variables Check" +docker exec pipecat-phone-test env | grep -E "(AWS_|TWILIO_|DAILY_)" | head -5 +if [ $? -eq 0 ]; then + print_success "Environment variables are loaded" +else + print_warning "Could not verify environment variables" +fi + +# Test 4: Python Dependencies +print_status "Test 4: Python Dependencies" +if docker exec pipecat-phone-test python3 -c "import pipecat; print('Pipecat version:', pipecat.__version__)" 2>/dev/null; then + print_success "Pipecat is installed correctly" +else + print_error "Pipecat import failed" +fi + +if docker exec pipecat-phone-test python3 -c "import twilio; print('Twilio SDK available')" 2>/dev/null; then + print_success "Twilio SDK is available" +else + print_error "Twilio SDK not available" +fi + +# Test 5: AWS Credentials +print_status "Test 5: AWS Credentials Test" +aws_test=$(docker exec pipecat-phone-test python3 -c " +import os +import boto3 +try: + client = boto3.client('bedrock-runtime', region_name=os.getenv('AWS_REGION', 'eu-north-1')) + print('AWS credentials configured') +except Exception as e: + print(f'AWS error: {e}') +" 2>/dev/null) +echo "AWS test result: $aws_test" + +# Test 6: Port Accessibility +print_status "Test 6: Port Accessibility" +if nc -z localhost 7860 2>/dev/null; then + print_success "Port 7860 is accessible" +else + print_error "Port 7860 is not accessible" +fi + +# Test 7: WebSocket Endpoint (basic connectivity) +print_status "Test 7: WebSocket Endpoint Test" +# Test if the WebSocket endpoint exists (will fail auth but should connect) +if curl -s -o /dev/null -w "%{http_code}" http://localhost:7860/media-stream/test-call-id | grep -q "426"; then + print_success "WebSocket endpoint is available (426 = Upgrade Required, expected)" +else + print_warning "WebSocket endpoint test inconclusive" +fi + +# Test 8: Container Resource Usage +print_status "Test 8: Container Resource Usage" +docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}" pipecat-phone-test + +# Test 9: Log Analysis +print_status "Test 9: Log Analysis" +echo "Recent logs (last 10 lines):" +docker logs --tail 10 pipecat-phone-test + +# Check for common error patterns +if docker logs pipecat-phone-test 2>&1 | grep -i error | head -3; then + print_warning "Found some errors in logs (check above)" +else + print_success "No obvious errors in logs" +fi + +# Test 10: File Permissions +print_status "Test 10: File Permissions Check" +docker exec pipecat-phone-test ls -la /app/ | head -5 +docker exec pipecat-phone-test whoami + +echo "" +echo "๐ŸŽฏ Test Summary" +echo "===============" + +# Final connectivity test +if curl -s http://localhost:7860/health | grep -q "healthy"; then + print_success "โœ… Container is healthy and ready for testing" + echo "" + echo "Next steps for Twilio testing:" + echo "1. Install ngrok: brew install ngrok (or download from ngrok.com)" + echo "2. Expose your container: ngrok http 7860" + echo "3. Copy the ngrok URL (e.g., https://abc123.ngrok.io)" + echo "4. Set Twilio webhook to: https://abc123.ngrok.io/incoming-call" + echo "5. Call your Twilio number to test" + echo "" + echo "Alternative with serveo (no signup required):" + echo "ssh -R 80:localhost:7860 serveo.net" + echo "Then use the provided serveo URL for Twilio webhook" +else + print_error "โŒ Container has issues - check logs above" + echo "" + echo "Troubleshooting commands:" + echo "- View full logs: docker logs pipecat-phone-test" + echo "- Shell into container: docker exec -it pipecat-phone-test /bin/bash" + echo "- Restart container: docker restart pipecat-phone-test" + echo "- Rebuild: docker build -f ../Dockerfile.phone -t pipecat-phone-service:test .." +fi + +echo "" +print_status "Testing complete!" \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docs/CLEANUP_GUIDE.md b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/CLEANUP_GUIDE.md new file mode 100644 index 0000000..6f5779d --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/CLEANUP_GUIDE.md @@ -0,0 +1,150 @@ +# Cleanup Guide + +This guide helps you clean up AWS resources and local development environment. + +## Local Development Cleanup + +### Clean Docker Resources +```bash +# Remove containers +docker container prune -f + +# Remove images +docker image prune -f + +# Remove volumes +docker volume prune -f + +# Remove networks +docker network prune -f +``` + +### Clean Python Environment +```bash +# Deactivate virtual environment +deactivate + +# Remove virtual environment +rm -rf .venv + +# Clean Python cache +find . -type d -name "__pycache__" -exec rm -rf {} + +find . -name "*.pyc" -delete +``` + +## AWS Environment Cleanup + +### ECS Environment Cleanup + +```bash +# Stop ECS service +aws ecs update-service \ + --cluster pipecat-cluster-test \ + --service pipecat-service-test \ + --desired-count 0 + +# Delete ECS service +aws ecs delete-service \ + --cluster pipecat-cluster-test \ + --service pipecat-service-test + +# Delete CDK stack +cd infrastructure +cdk destroy PipecatEcsStack-test +``` + +### EKS Environment Cleanup + +```bash +# Delete Kubernetes resources +kubectl delete namespace pipecat + +# Delete CDK stack +cd infrastructure/-eks +cdk destroy PipecatEksStack + +# Clean kubectl config (optional) +kubectl config delete-context arn:aws:eks:us-east-1:ACCOUNT:cluster/pipecat-eks-cluster-test +``` + +### Clean ECR Repositories + +```bash +# List repositories +aws ecr describe-repositories --query 'repositories[?contains(repositoryName, `pipecat`)].repositoryName' + +# Delete repository (replace with actual name) +aws ecr delete-repository --repository-name pipecat-voice-agent-test --force +``` + +### Clean Secrets Manager + +```bash +# List secrets +aws secretsmanager list-secrets --query 'SecretList[?contains(Name, `pipecat`)].Name' + +# Delete secret (replace with actual ARN) +aws secretsmanager delete-secret --secret-id arn:aws:secretsmanager:region:account:secret:name +``` + +### Clean CloudWatch Logs + +```bash +# List log groups +aws logs describe-log-groups --query 'logGroups[?contains(logGroupName, `pipecat`)].logGroupName' + +# Delete log group +aws logs delete-log-group --log-group-name /ecs/pipecat-voice-agent-test +``` + +## Complete Cleanup Script + +Create a cleanup script for convenience: + +```bash +#!/bin/bash +# cleanup.sh + +ENVIRONMENT=${1:-test} + +echo "Cleaning up environment: $ENVIRONMENT" + +# ECS cleanup +aws ecs update-service --cluster pipecat-cluster-$ENVIRONMENT --service pipecat-service-$ENVIRONMENT --desired-count 0 +aws ecs delete-service --cluster pipecat-cluster-$ENVIRONMENT --service pipecat-service-$ENVIRONMENT + +# CDK cleanup +cd infrastructure +cdk destroy PipecatEcsStack-$ENVIRONMENT --force + +# ECR cleanup +aws ecr delete-repository --repository-name pipecat-voice-agent-$ENVIRONMENT --force + +echo "Cleanup complete for environment: $ENVIRONMENT" +``` + +## Verification + +After cleanup, verify resources are removed: + +```bash +# Check ECS +aws ecs list-clusters +aws ecs list-services --cluster pipecat-cluster-test + +# Check ECR +aws ecr describe-repositories + +# Check Secrets +aws secretsmanager list-secrets + +# Check CloudWatch +aws logs describe-log-groups +``` + +## Cost Monitoring + +Monitor costs to ensure cleanup was successful: +- Check AWS Cost Explorer +- Review billing dashboard +- Set up cost alerts for future deployments \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docs/DEPLOYMENT_PROCESS.md b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/DEPLOYMENT_PROCESS.md new file mode 100644 index 0000000..521b9ec --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/DEPLOYMENT_PROCESS.md @@ -0,0 +1,452 @@ +# Pipecat ECS Deployment Process + +This document provides a comprehensive guide for deploying the Pipecat Voice AI Agent to AWS ECS, including both WebRTC and Twilio phone integration capabilities. + +## Overview + +The deployment process consists of several phases: + +1. **Prerequisites Setup** - AWS account, credentials, and service configuration +2. **Infrastructure Deployment** - AWS resources using CDK +3. **Application Deployment** - Container build and ECS service deployment +4. **Configuration** - Secrets management and environment setup +5. **Testing & Validation** - End-to-end functionality verification + +## Prerequisites + +### AWS Account Setup + +1. **AWS Account Requirements**: + + - Active AWS account with billing enabled + - Sufficient service limits for ECS Fargate tasks + - Access to required AWS regions (eu-north-1 recommended) + +2. **Required AWS Services**: + + - Amazon ECS (Elastic Container Service) + - Amazon ECR (Elastic Container Registry) + - Application Load Balancer (ALB) + - AWS Secrets Manager + - Amazon CloudWatch + - Amazon Bedrock (with Nova Sonic model access) + - AWS VPC and related networking services + +3. **AWS CLI Configuration**: + + ```bash + # Install AWS CLI v2 + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip awscliv2.zip + sudo ./aws/install + + # Configure credentials + aws configure + # Enter: Access Key ID, Secret Access Key, Region (eu-north-1), Output format (json) + + # Verify configuration + aws sts get-caller-identity + ``` + +### Development Environment Setup + +1. **Required Tools**: + + ```bash + # Node.js and npm (for CDK) + curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - + sudo apt-get install -y nodejs + + # AWS CDK + npm install -g aws-cdk + + # Docker + sudo apt-get update + sudo apt-get install docker.io + sudo usermod -aG docker $USER + + # Python 3.10+ (for application) + sudo apt-get install python3.10 python3-pip + ``` + +2. **Service Account Setup**: + - **Daily.co**: Create account and obtain API key + - **Twilio** (for phone integration): Create account, purchase phone number, obtain API credentials + +### AWS Service Configuration + +1. **Amazon Bedrock Setup**: + + ```bash + # Enable Bedrock service in your region + aws bedrock list-foundation-models --region eu-north-1 + + # Request access to Nova Sonic model through AWS Console + # Navigate to: Bedrock Console > Model Access > Request Access + ``` + +2. **IAM Permissions**: + Required permissions for deployment user/role: + - ECS: Full access for cluster and service management + - ECR: Full access for container registry + - ALB: Full access for load balancer management + - VPC: Full access for networking + - IAM: Role creation and policy attachment + - CloudFormation: Stack management + - Secrets Manager: Secret creation and access + - CloudWatch: Log group and metrics management + - Bedrock: Model invocation permissions + +## Infrastructure Deployment + +### Phase 1: CDK Infrastructure + +1. **Navigate to Infrastructure Directory**: + + ```bash + cd infrastructure + ``` + +2. **Install Dependencies**: + + ```bash + npm install + npm run build + npm test # Verify everything works + ``` + +3. **Bootstrap CDK** (first time only): + + ```bash + cdk bootstrap --region eu-north-1 + ``` + +4. **Deploy Infrastructure**: + + ```bash + # Basic deployment (test environment, default VPC) + ./deploy.sh + + # Production deployment with custom VPC + ./deploy.sh --environment prod --custom-vpc --region eu-north-1 + ``` + +5. **Verify Infrastructure**: + + ```bash + # Check stack status + aws cloudformation describe-stacks --stack-name PipecatEcsStack-test + + # Get important outputs + aws cloudformation describe-stacks \ + --stack-name PipecatEcsStack-test \ + --query 'Stacks[0].Outputs' + ``` + +### Phase 2: Secrets Configuration + +1. **Set Up Environment Variables**: + + ```bash + # Create .env file in project root + cat > .env << EOF + DAILY_API_KEY=your_daily_api_key_here + AWS_ACCESS_KEY_ID=your_aws_access_key + AWS_SECRET_ACCESS_KEY=your_aws_secret_key + AWS_REGION=eu-north-1 + + # Twilio credentials (for phone integration) + TWILIO_ACCOUNT_SID=your_twilio_account_sid + TWILIO_API_SID=your_twilio_api_sid + TWILIO_API_SECRET=your_twilio_api_secret + EOF + ``` + +2. **Deploy Secrets to AWS**: + + ```bash + # Navigate back to project root + cd .. + + # Run secrets setup script + python3 scripts/setup-secrets.py + + # Verify secrets were created + python3 tests/test-secrets-integration.py + ``` + +## Application Deployment + +### Phase 3: Container Build and Deployment + +1. **Build and Push Container**: + + ```bash + # Make scripts executable + chmod +x scripts/build-and-push.sh + chmod +x scripts/deploy-service.sh + + # Build and push to ECR + ./scripts/build-and-push.sh -e test -t latest + + # Deploy to ECS + ./scripts/deploy-service.sh -e test -t latest --update + ``` + +2. **Alternative: Full Automated Deployment**: + ```bash + # Complete deployment in one command + ./scripts/deployment/full-deploy.sh -e test + ``` + +### Phase 4: Service Configuration + +1. **ECS Service Settings**: + + - **Task Definition**: 1 vCPU, 2GB memory + - **Desired Count**: 2 tasks (for high availability) + - **Health Check**: `/health` endpoint + - **Auto Scaling**: CPU-based scaling (70% threshold) + +2. **Load Balancer Configuration**: + - **Scheme**: Internet-facing + - **Listeners**: HTTP (80), optional HTTPS (443) + - **Target Group**: ECS tasks on port 7860 + - **Health Check**: GET `/health` with 30s interval + +## Configuration Management + +### Environment Variables + +**Required Environment Variables**: + +- `DAILY_API_KEY`: Daily.co API key (from Secrets Manager) +- `AWS_REGION`: AWS region for Bedrock services +- `HOST`: Server host (0.0.0.0 for containers) +- `FAST_API_PORT`: Server port (7860) + +**Optional Environment Variables**: + +- `ENVIRONMENT`: deployment environment (test/prod) +- `LOG_LEVEL`: logging level (INFO/DEBUG/WARNING/ERROR) +- `MAX_BOTS_PER_ROOM`: maximum bots per room (default: 1) +- `MAX_CONCURRENT_ROOMS`: maximum concurrent rooms (default: 10) + +**Twilio Environment Variables** (for phone integration): + +- `TWILIO_ACCOUNT_SID`: Twilio account identifier +- `TWILIO_API_SID`: Twilio API key SID +- `TWILIO_API_SECRET`: Twilio API key secret + +### Secrets Management + +All sensitive configuration is stored in AWS Secrets Manager: + +1. **Daily API Credentials**: + + ```json + { + "name": "pipecat/daily-api-key", + "value": "your_daily_api_key" + } + ``` + +2. **AWS Credentials** (if not using IAM roles): + + ```json + { + "name": "pipecat/aws-credentials", + "value": { + "access_key_id": "your_access_key", + "secret_access_key": "your_secret_key" + } + } + ``` + +3. **Twilio Credentials**: + ```json + { + "name": "pipecat/twilio-credentials", + "value": { + "account_sid": "ACxxxxx", + "api_sid": "SKxxxxx", + "api_secret": "xxxxx" + } + } + ``` + +## Testing and Validation + +### Phase 5: Deployment Verification + +1. **Health Check Validation**: + + ```bash + # Get ALB DNS name + ALB_DNS=$(aws cloudformation describe-stacks \ + --stack-name PipecatEcsStack-test \ + --query 'Stacks[0].Outputs[?OutputKey==`LoadBalancerDnsName`].OutputValue' \ + --output text) + + # Test health endpoint + curl http://$ALB_DNS/health + + # Test readiness endpoint + curl http://$ALB_DNS/ready + ``` + +2. **End-to-End Testing**: + + ```bash + # Run comprehensive test suite + python3 tests/test-end-to-end.py + + # Test specific functionality + python3 tests/test-complete-user-journey.py + + # Test Bedrock integration + python3 tests/test-bedrock-access.py + ``` + +3. **WebRTC Functionality**: + + - Navigate to `http://$ALB_DNS` in browser + - Test voice conversation with Nova Sonic + - Verify function calling (weather queries) + - Test multiple concurrent sessions + +4. **Twilio Phone Integration** (if configured): + - Call the configured Twilio phone number + - Test voice conversation through phone + - Verify function calling works via voice commands + - Test call quality and responsiveness + +### Monitoring and Logging + +1. **CloudWatch Logs**: + + ```bash + # View application logs + aws logs tail /ecs/pipecat-voice-agent-test/application --follow + + # View ECS service logs + aws logs tail /ecs/pipecat-voice-agent-test --follow + ``` + +2. **ECS Service Monitoring**: + + ```bash + # Check service status + aws ecs describe-services \ + --cluster pipecat-cluster-test \ + --services pipecat-service-test + + # Check task health + aws ecs list-tasks --cluster pipecat-cluster-test + ``` + +3. **Application Metrics**: + - CPU and memory utilization + - Request count and response times + - Error rates and health check status + - Custom metrics for voice sessions and function calls + +## Troubleshooting + +### Common Deployment Issues + +1. **CDK Bootstrap Required**: + + ```bash + cdk bootstrap --region your-region + ``` + +2. **Insufficient IAM Permissions**: + + - Verify deployment user has all required permissions + - Check CloudFormation stack events for specific errors + +3. **Docker Build Failures**: + + ```bash + # Test container locally first + ./scripts/deployment/test-container.sh + ``` + +4. **ECS Task Failures**: + + - Check CloudWatch logs for application errors + - Verify secrets are properly configured + - Ensure Bedrock model access is granted + +5. **Load Balancer Health Check Failures**: + - Verify `/health` endpoint is responding + - Check security group configurations + - Ensure tasks are running and healthy + +### Performance Optimization + +1. **Resource Allocation**: + + - Monitor CPU and memory usage + - Adjust task definition resources as needed + - Configure auto-scaling based on metrics + +2. **Cost Optimization**: + - Use Spot instances for non-production environments + - Implement proper log retention policies + - Monitor and optimize data transfer costs + +## Security Considerations + +1. **Network Security**: + + - Use private subnets for ECS tasks + - Implement proper security group rules + - Enable VPC Flow Logs for monitoring + +2. **Application Security**: + + - Run containers as non-root user + - Use minimal base images + - Regularly update dependencies + +3. **Secrets Management**: + + - Never store secrets in container images + - Use AWS Secrets Manager for all sensitive data + - Implement proper IAM roles and policies + +4. **Monitoring and Auditing**: + - Enable CloudTrail for API auditing + - Monitor access to secrets and resources + - Set up alerts for security events + +## Next Steps + +After successful deployment: + +1. **Production Readiness**: + + - Implement HTTPS/TLS termination + - Set up custom domain names + - Configure backup and disaster recovery + +2. **Scaling and Performance**: + + - Implement advanced auto-scaling policies + - Set up performance monitoring and alerting + - Optimize for high availability + +3. **CI/CD Pipeline**: + + - Set up automated testing and deployment + - Implement blue-green deployment strategies + - Configure automated rollback procedures + +4. **Advanced Features**: + - Implement Twilio phone integration + - Add advanced monitoring and analytics + - Integrate with additional AI services + +This deployment process provides a solid foundation for running the Pipecat Voice AI Agent in a production AWS environment with proper security, monitoring, and scalability considerations. diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docs/EKS_ARCHITECTURE.md b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/EKS_ARCHITECTURE.md new file mode 100644 index 0000000..8274000 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/EKS_ARCHITECTURE.md @@ -0,0 +1,47 @@ +# EKS Architecture Overview + +## Network Flow +``` +Internet โ†’ NLB (TLS Listener) โ†’ Target Group โ†’ Kubernetes Pod (Fargate) +``` + +## Key Components + +### EKS Cluster Configuration +- **Region:** us-east-1 +- **Kubernetes Version:** v1.31 +- **Node Type:** t3.medium (Fargate serverless) +- **OS:** Amazon Linux 2023.7.20250512 +- **Container Runtime:** containerd://1.7.27 +- **Architecture:** amd64 + +### Networking +- **VPC:** Custom VPC with public/private subnets +- **Subnets:** Multi-AZ deployment (2 AZs) +- **Security Groups:** Restrictive ingress/egress rules +- **Load Balancer:** Network Load Balancer (Layer 4) + +### Security +- **IRSA:** IAM Roles for Service Accounts +- **Secrets:** AWS Secrets Manager integration +- **TLS:** Certificate Manager for HTTPS +- **Non-root:** Container runs as user 1001 + +### Data Flow + +#### Phone Call Flow: +``` +๐Ÿ“ž Phone Call โ†’ Twilio โ†’ Webhook โ†’ NLB โ†’ EKS Pod โ†’ Nova Sonic +``` + +#### WebRTC Flow: +``` +๐ŸŒ Browser โ†’ NLB โ†’ EKS Pod โ†’ Daily.co Room +``` + +## Key Features +- โœ… Dual Voice Channels: Phone calls (Twilio) + Web chat (Daily.co) +- โœ… Serverless: Fargate eliminates node management +- โœ… Scalable: Kubernetes horizontal pod autoscaling +- โœ… Secure: IRSA, Secrets Manager, TLS termination +- โœ… Resilient: Multi-AZ deployment with health checks \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docs/PHONE_SERVICE_DEPLOYMENT.md b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/PHONE_SERVICE_DEPLOYMENT.md new file mode 100644 index 0000000..fff7a7a --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/PHONE_SERVICE_DEPLOYMENT.md @@ -0,0 +1,311 @@ +# Phone Service Deployment Guide + +This guide covers deploying the Pipecat Phone Service with Twilio integration to AWS ECS. + +## Overview + +The phone service deployment creates a separate ECS service that handles Twilio phone calls using the Nova Sonic AI agent. Unlike local development where you might use serveo or ngrok for tunneling, the cloud deployment provides a public Application Load Balancer that Twilio can reach directly. + +## Architecture + +``` +Twilio Phone Call โ†’ Twilio Webhook โ†’ Phone ALB โ†’ Phone ECS Service โ†’ Nova Sonic +``` + +### Key Components + +1. **Separate ECS Service**: Dedicated service running `server_clean.py` +2. **Separate ALB**: Public load balancer for Twilio webhooks +3. **ECR Repository**: Dedicated repository for phone service images +4. **Security Groups**: Configured to allow Twilio webhook traffic + +## Prerequisites + +1. **Infrastructure Deployed**: Main infrastructure must be deployed first +2. **Twilio Account**: Active Twilio account with phone number +3. **Secrets Configured**: Twilio credentials in AWS Secrets Manager +4. **Docker**: For building container images + +## Deployment Steps + +### 1. Deploy Infrastructure (if not already done) + +```bash +cd infrastructure +./deploy.sh -e test -r eu-north-1 +cd .. +``` + +### 2. Build and Deploy Phone Service + +```bash +# Full deployment (build + deploy) +./scripts/deployment/deploy-phone-service.sh + +# Or step by step: +./scripts/build-phone-service.sh +./scripts/deployment/deploy-phone-service.sh --deploy-only +``` + +### 3. Configure Twilio Webhook + +After deployment, you'll get a webhook URL like: + +``` +http://pipecat-phone-alb-123456789.eu-north-1.elb.amazonaws.com/incoming-call +``` + +**Manual Configuration Required:** + +1. Log in to [Twilio Console](https://console.twilio.com/) +2. Go to **Phone Numbers > Manage > Active numbers** +3. Click on your Twilio phone number +4. In **Voice Configuration**: + - Set **"A call comes in"** webhook to the provided URL + - Set HTTP method to **POST** +5. Click **Save configuration** + +## Key Differences from Local Development + +### Local Development (with serveo/ngrok) + +```bash +# Local server +python server_clean.py --host 0.0.0.0 --port 7860 + +# Tunnel (example with serveo) +ssh -R 80:localhost:7860 serveo.net + +# Twilio webhook: https://yoursubdomain.serveo.net/incoming-call +``` + +### Cloud Deployment + +```bash +# Container in ECS +# Public ALB provides direct access +# No tunneling needed + +# Twilio webhook: http://phone-alb-dns-name/incoming-call +``` + +## Service Configuration + +### Container Specifications + +- **CPU**: 2 vCPU (2048 units) +- **Memory**: 4 GB (4096 MB) +- **Port**: 7860 +- **Health Check**: `/health` endpoint + +### Auto Scaling + +- **Min Capacity**: 1 task +- **Max Capacity**: 4 tasks +- **CPU Target**: 75% +- **Memory Target**: 85% + +### Security + +- **ALB Security Group**: Allows HTTP (80) and HTTPS (443) from anywhere +- **ECS Security Group**: Allows traffic from ALB only +- **IAM Roles**: Bedrock, Secrets Manager, and Daily.co permissions + +## Environment Variables + +The phone service uses the same environment variables as the main service: + +```bash +# AWS Configuration +AWS_REGION=eu-north-1 +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= + +# Twilio Configuration +TWILIO_ACCOUNT_SID= +TWILIO_AUTH_TOKEN= +TWILIO_PHONE_NUMBER= +TWILIO_API_SID= +TWILIO_API_SECRET= + +# Daily.co (for potential WebRTC fallback) +DAILY_API_KEY= +DAILY_API_URL= + +# Service Configuration +HOST=0.0.0.0 +FAST_API_PORT=7860 +SERVICE_TYPE=phone +``` + +## Monitoring and Troubleshooting + +### Health Check + +```bash +# Check service health +curl http://phone-alb-dns-name/health + +# Expected response: +{ + "status": "healthy", + "timestamp": "2024-01-01T12:00:00.000000", + "service": "fixed-nova-sonic-server", + "webrtc_enabled": true, + "twilio_enabled": true, + "nova_sonic_enabled": true, + "phone_number": "+1234567890" +} +``` + +### Logs + +```bash +# View phone service logs +aws logs tail /ecs/pipecat-phone-service-test --follow --region eu-north-1 + +# View specific log streams +aws logs describe-log-streams \ + --log-group-name /ecs/pipecat-phone-service-test \ + --region eu-north-1 +``` + +### Service Status + +```bash +# Check ECS service status +aws ecs describe-services \ + --cluster pipecat-cluster-test \ + --services pipecat-phone-service-test \ + --region eu-north-1 + +# Check running tasks +aws ecs list-tasks \ + --cluster pipecat-cluster-test \ + --service-name pipecat-phone-service-test \ + --region eu-north-1 +``` + +### Active Calls + +```bash +# Check active calls via API +curl http://phone-alb-dns-name/active-calls +``` + +## Testing + +### 1. Health Check Test + +```bash +curl http://phone-alb-dns-name/health +``` + +### 2. Phone Call Test + +1. Call your Twilio phone number +2. You should hear the Nova Sonic AI agent +3. Try asking for weather information +4. Check logs for call processing + +### 3. Load Testing + +```bash +# Multiple concurrent calls can be tested +# Monitor CPU/Memory usage during testing +``` + +## Common Issues + +### 1. Twilio Webhook Not Working + +- **Check**: ALB DNS name is correct in Twilio configuration +- **Check**: Security groups allow HTTP traffic from anywhere +- **Check**: ECS tasks are healthy and running + +### 2. Nova Sonic Connection Issues + +- **Check**: AWS credentials in Secrets Manager +- **Check**: Bedrock permissions in IAM role +- **Check**: Nova Sonic model availability in region + +### 3. Call Quality Issues + +- **Check**: ECS task resources (CPU/Memory) +- **Check**: Network connectivity to Daily.co and AWS services +- **Check**: Audio processing logs + +### 4. Deployment Failures + +- **Check**: ECR repository exists and is accessible +- **Check**: ECS service has proper IAM roles +- **Check**: Task definition is valid + +## Cost Considerations + +### ECS Fargate Costs + +- **2 vCPU, 4GB RAM**: ~$0.08/hour per task +- **2 tasks minimum**: ~$0.16/hour = ~$115/month +- **Auto-scaling**: Additional costs during peak usage + +### ALB Costs + +- **Load Balancer**: ~$16/month +- **LCU (Load Balancer Capacity Units)**: Based on usage + +### Data Transfer + +- **Twilio to ALB**: Minimal cost +- **ALB to ECS**: No cost (same AZ) +- **ECS to external APIs**: Standard data transfer rates + +## Security Best Practices + +1. **Use HTTPS**: Configure SSL certificate for production +2. **Webhook Validation**: Enable Twilio signature validation +3. **Network Security**: Use private subnets for ECS tasks +4. **Secrets Management**: Never hardcode credentials +5. **IAM Roles**: Use least privilege principle + +## Production Considerations + +1. **SSL/TLS**: Configure HTTPS listener on ALB +2. **Domain Name**: Use custom domain instead of ALB DNS +3. **Monitoring**: Set up CloudWatch alarms +4. **Backup**: Regular ECR image backups +5. **Scaling**: Adjust auto-scaling based on call volume + +## Cleanup + +To remove the phone service: + +```bash +# Delete ECS service +aws ecs update-service \ + --cluster pipecat-cluster-test \ + --service pipecat-phone-service-test \ + --desired-count 0 \ + --region eu-north-1 + +aws ecs delete-service \ + --cluster pipecat-cluster-test \ + --service pipecat-phone-service-test \ + --region eu-north-1 + +# Remove from infrastructure (requires CDK redeployment) +# Or destroy entire stack: +cd infrastructure +cdk destroy PipecatEcsStack-test +``` + +## Support + +For issues with: + +- **Twilio Integration**: Check Twilio Console logs +- **AWS Infrastructure**: Check CloudFormation events +- **Nova Sonic**: Check Bedrock service status +- **Container Issues**: Check ECS task logs + +Remember: The phone service runs independently from the main WebRTC service, so both can operate simultaneously. diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docs/SECRETS_SETUP.md b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/SECRETS_SETUP.md new file mode 100644 index 0000000..5f57ba4 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/SECRETS_SETUP.md @@ -0,0 +1,245 @@ +# AWS Secrets Manager Integration + +This document describes how AWS Secrets Manager is integrated with the Pipecat ECS deployment to securely manage sensitive configuration data. + +## Overview + +The Pipecat ECS deployment uses AWS Secrets Manager to store and inject sensitive configuration data into ECS tasks. This approach provides several benefits: + +- **Security**: Secrets are encrypted at rest and in transit +- **Rotation**: Secrets can be rotated without redeploying the application +- **Audit**: All secret access is logged in CloudTrail +- **Separation**: Secrets are managed separately from application code + +## Secrets Structure + +### 1. Daily API Credentials (`pipecat/daily-api-key`) + +Contains Daily.co API credentials required for WebRTC functionality: + +```json +{ + "DAILY_API_KEY": "your-daily-api-key", + "DAILY_API_URL": "https://api.daily.co/v1" +} +``` + +### 2. AWS Credentials (`pipecat/aws-credentials`) + +Contains AWS credentials for Bedrock access (alternative to IAM roles): + +```json +{ + "AWS_ACCESS_KEY_ID": "your-aws-access-key", + "AWS_SECRET_ACCESS_KEY": "your-aws-secret-key", + "AWS_REGION": "eu-north-1" +} +``` + +## Setup Process + +### 1. Create Secrets + +Run the setup script to create the secrets in AWS Secrets Manager: + +```bash +python3 setup-secrets.py +``` + +This script will: + +- Read credentials from your `.env` file +- Create the secrets in AWS Secrets Manager +- Verify the secrets can be retrieved +- Display the secret ARNs for CDK configuration + +### 2. CDK Infrastructure + +The CDK infrastructure automatically: + +- References the secrets by name +- Grants the ECS execution role permission to retrieve secrets +- Configures the ECS task definition to inject secrets as environment variables + +### 3. ECS Task Injection + +When ECS starts a task, it automatically: + +- Retrieves the secret values from Secrets Manager +- Injects them as environment variables into the container +- Makes them available to the application code + +## Environment Variables + +The following environment variables are injected into ECS tasks: + +| Variable | Source | Description | +| ----------------------- | ------------------------- | --------------------- | +| `DAILY_API_KEY` | `pipecat/daily-api-key` | Daily.co API key | +| `DAILY_API_URL` | `pipecat/daily-api-key` | Daily.co API URL | +| `AWS_ACCESS_KEY_ID` | `pipecat/aws-credentials` | AWS access key | +| `AWS_SECRET_ACCESS_KEY` | `pipecat/aws-credentials` | AWS secret key | +| `AWS_REGION` | Environment/CDK | AWS region | +| `HOST` | CDK | Server host (0.0.0.0) | +| `FAST_API_PORT` | CDK | Server port (7860) | + +## IAM Permissions + +### ECS Execution Role + +The ECS execution role needs permission to retrieve secrets: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["secretsmanager:GetSecretValue"], + "Resource": ["arn:aws:secretsmanager:REGION:ACCOUNT:secret:pipecat/*"] + } + ] +} +``` + +### ECS Task Role + +The ECS task role needs permissions for: + +- Bedrock model invocation +- Optional Secrets Manager access (for runtime secret retrieval) + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream" + ], + "Resource": ["arn:aws:bedrock:*::foundation-model/amazon.nova-sonic-v1:0"] + }, + { + "Effect": "Allow", + "Action": ["secretsmanager:GetSecretValue"], + "Resource": ["arn:aws:secretsmanager:REGION:ACCOUNT:secret:pipecat/*"] + } + ] +} +``` + +## Testing + +### 1. Test Secrets Access + +Verify that secrets can be retrieved: + +```bash +python3 test-secrets-integration.py +``` + +### 2. Test Bedrock Access + +Verify that Bedrock Nova Sonic can be accessed: + +```bash +python3 test-bedrock-access.py +``` + +### 3. Health Check + +The application includes a comprehensive health check that verifies: + +- Daily API helper initialization +- Required environment variables +- Active bot processes + +Access the health check at: `http://your-alb-dns/health` + +## Security Best Practices + +### 1. Least Privilege + +- ECS execution role only has permission to retrieve specific secrets +- ECS task role only has permission for required AWS services +- Secrets are scoped to the `pipecat/` namespace + +### 2. Encryption + +- Secrets are encrypted at rest using AWS KMS +- Secrets are encrypted in transit using TLS +- Environment variables in ECS are encrypted + +### 3. Rotation + +- Secrets can be rotated in AWS Secrets Manager +- ECS tasks will pick up new secret values on restart +- No application code changes required for rotation + +### 4. Monitoring + +- All secret access is logged in CloudTrail +- ECS task logs include environment variable validation +- Health checks verify secret availability + +## Troubleshooting + +### Common Issues + +1. **Secret Not Found** + + - Verify the secret exists in AWS Secrets Manager + - Check the secret name matches the CDK configuration + - Ensure you're in the correct AWS region + +2. **Permission Denied** + + - Verify the ECS execution role has `secretsmanager:GetSecretValue` permission + - Check the resource ARN in the IAM policy + - Ensure the secret ARN is correct + +3. **Environment Variables Not Set** + + - Check the ECS task definition includes the secret mappings + - Verify the secret keys match the expected names + - Check ECS task logs for secret retrieval errors + +4. **Bedrock Access Issues** + - Verify Nova Sonic model access has been requested + - Check the ECS task role has Bedrock permissions + - Ensure the model is available in your region + +### Debugging Commands + +```bash +# List secrets +aws secretsmanager list-secrets --region eu-north-1 + +# Get secret value +aws secretsmanager get-secret-value --secret-id pipecat/daily-api-key --region eu-north-1 + +# Check ECS task logs +aws logs get-log-events --log-group-name /ecs/pipecat-voice-agent-test --log-stream-name ecs/pipecat-container/TASK-ID + +# Test Bedrock access +aws bedrock list-foundation-models --region eu-north-1 +``` + +## Cost Considerations + +- AWS Secrets Manager charges per secret per month +- Additional charges for API calls (retrievals) +- ECS automatically caches secrets to minimize API calls +- Consider consolidating secrets to reduce costs + +## Next Steps + +After setting up secrets: + +1. Deploy the updated CDK infrastructure +2. Build and push the container image to ECR +3. Deploy the ECS service +4. Test the application end-to-end +5. Monitor logs and metrics diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docs/TROUBLESHOOTING_GUIDE.md b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/TROUBLESHOOTING_GUIDE.md new file mode 100644 index 0000000..50989ee --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/TROUBLESHOOTING_GUIDE.md @@ -0,0 +1,251 @@ +# Troubleshooting Guide + +Common issues and solutions for the Pipecat Voice AI Agent deployment. + +## General Issues + +### Container Fails to Start + +**Symptoms:** +- ECS tasks stop immediately +- Health checks fail +- Container exits with error code + +**Solutions:** +1. Check environment variables in AWS Secrets Manager +2. Verify IAM permissions for ECS task role +3. Check CloudWatch logs for detailed error messages +4. Ensure Docker image was built and pushed correctly + +```bash +# Check ECS service events +aws ecs describe-services --cluster pipecat-cluster-test --services pipecat-service-test + +# Check CloudWatch logs +aws logs tail /ecs/pipecat-voice-agent-test/application --follow +``` + +### Health Checks Failing + +**Symptoms:** +- Load balancer shows unhealthy targets +- Service restarts frequently + +**Solutions:** +1. Verify `/health` endpoint is responding +2. Check if Daily API key is valid +3. Ensure AWS credentials have proper permissions +4. Verify network connectivity + +```bash +# Test health endpoint directly +curl http://your-alb-dns-name/health + +# Check target group health +aws elbv2 describe-target-health --target-group-arn your-target-group-arn +``` + +## Phone Service Issues + +### Twilio Webhook Errors + +**Symptoms:** +- Phone calls don't connect to AI +- Twilio shows webhook errors +- SSL certificate errors + +**Solutions:** +1. **SSL Certificate Issues:** + - Ensure load balancer has valid SSL certificate + - Use AWS Certificate Manager for automatic SSL + - Avoid self-signed certificates + - Use standard HTTPS port (443) + +2. **Webhook Configuration:** + ```bash + # Test webhook endpoint + curl -X POST https://your-domain.com/incoming-call \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "CallSid=test&From=+1234567890&To=+0987654321" + ``` + +3. **Network Connectivity:** + - Ensure security groups allow inbound HTTPS (443) + - Verify load balancer is internet-facing + - Check DNS resolution + +### Nova Sonic Integration Issues + +**Symptoms:** +- Voice synthesis not working +- Speech recognition errors +- AWS Bedrock access denied + +**Solutions:** +1. **AWS Permissions:** + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream" + ], + "Resource": "*" + } + ] + } + ``` + +2. **Region Availability:** + - Ensure Nova Sonic is available in your AWS region + - Check AWS Bedrock service status + +3. **Credentials:** + - Verify AWS credentials in Secrets Manager + - Test Bedrock access manually + +## EKS-Specific Issues + +### Pods Stuck in Pending + +**Symptoms:** +- Pods don't start +- `kubectl get pods` shows Pending status + +**Solutions:** +1. Check Fargate profile selectors +2. Verify namespace matches Fargate profile +3. Check resource requests vs. Fargate capacity + +```bash +# Check pod events +kubectl describe pods -n pipecat + +# Check Fargate profiles +aws eks describe-fargate-profile --cluster-name pipecat-eks-cluster-test --fargate-profile-name default +``` + +### LoadBalancer Not Getting External IP + +**Symptoms:** +- Service shows `` for EXTERNAL-IP +- Can't access application from internet + +**Solutions:** +1. Install AWS Load Balancer Controller +2. Check service annotations +3. Verify IAM permissions for load balancer controller + +```bash +# Check service status +kubectl get services -n pipecat + +# Check load balancer controller logs +kubectl logs -n kube-system deployment/aws-load-balancer-controller +``` + +### IRSA Permission Issues + +**Symptoms:** +- Pods can't access AWS services +- Access denied errors in logs + +**Solutions:** +1. Verify service account annotations +2. Check IAM role trust policy +3. Ensure OIDC provider is configured + +```bash +# Check service account +kubectl describe serviceaccount pipecat-service-account -n pipecat + +# Test AWS access from pod +kubectl exec -it deployment/pipecat-phone-service -n pipecat -- aws sts get-caller-identity +``` + +## WebRTC Issues + +### Daily.co Connection Problems + +**Symptoms:** +- Can't join voice rooms +- WebRTC connection fails +- Browser shows connection errors + +**Solutions:** +1. **API Key Issues:** + - Verify Daily API key is valid + - Check API key permissions + - Ensure key is properly stored in Secrets Manager + +2. **Network Issues:** + - Check firewall settings + - Verify WebRTC ports are open + - Test from different networks + +3. **Browser Issues:** + - Enable microphone permissions + - Use HTTPS (required for WebRTC) + - Try different browsers + +## Monitoring and Debugging + +### Enable Debug Logging + +Add to environment variables: +```bash +LOG_LEVEL=DEBUG +``` + +### CloudWatch Queries + +Useful CloudWatch Insights queries: + +```sql +# Find errors in logs +fields @timestamp, @message +| filter @message like /ERROR/ +| sort @timestamp desc +| limit 100 + +# Monitor health check failures +fields @timestamp, @message +| filter @message like /health/ +| sort @timestamp desc +| limit 50 +``` + +### Performance Monitoring + +```bash +# Check ECS service metrics +aws cloudwatch get-metric-statistics \ + --namespace AWS/ECS \ + --metric-name CPUUtilization \ + --dimensions Name=ServiceName,Value=pipecat-service-test \ + --start-time 2024-01-01T00:00:00Z \ + --end-time 2024-01-02T00:00:00Z \ + --period 300 \ + --statistics Average +``` + +## Getting Help + +1. **Check CloudWatch Logs:** Always start with application logs +2. **Review AWS Service Health:** Check AWS status page +3. **Test Components Individually:** Isolate the problem +4. **Use AWS Support:** For infrastructure issues +5. **Check Pipecat Documentation:** For framework-specific issues + +## Common Error Messages + +| Error | Cause | Solution | +|-------|-------|----------| +| `Daily API key not found` | Missing or invalid API key | Check Secrets Manager | +| `SSL certificate verify failed` | Invalid SSL certificate | Use trusted CA certificate | +| `Access denied` | IAM permissions | Review and update IAM policies | +| `Connection timeout` | Network/firewall issue | Check security groups | +| `Pod has unbound immediate PersistentVolumeClaims` | Storage issue | Check PVC configuration | \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docs/TWILIO_SECRETS_SETUP.md b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/TWILIO_SECRETS_SETUP.md new file mode 100644 index 0000000..45594b7 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/TWILIO_SECRETS_SETUP.md @@ -0,0 +1,174 @@ +# Twilio Credentials Setup for AWS Secrets Manager + +This document explains how Twilio credentials are managed in AWS Secrets Manager for the Pipecat ECS deployment. + +## Overview + +The Twilio credentials are stored securely in AWS Secrets Manager and automatically injected into ECS tasks as environment variables. This allows the phone service to access Twilio APIs without hardcoding credentials in the container image. + +## Secret Structure + +The Twilio credentials are stored in AWS Secrets Manager under the name `pipecat/twilio-credentials` with the following structure: + +```json +{ + "TWILIO_ACCOUNT_SID": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "TWILIO_AUTH_TOKEN": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "TWILIO_PHONE_NUMBER": "+1234567890", + "TWILIO_API_SID": "SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "TWILIO_API_SECRET": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +} +``` + +## Environment Variables + +When the ECS task starts, these credentials are automatically injected as environment variables: + +- `TWILIO_ACCOUNT_SID`: Your Twilio Account SID (starts with "AC") +- `TWILIO_AUTH_TOKEN`: Your Twilio Auth Token (32 characters) +- `TWILIO_PHONE_NUMBER`: Your Twilio phone number (E.164 format, e.g., "+441182304072") +- `TWILIO_API_SID`: Your Twilio API Key SID (starts with "SK") +- `TWILIO_API_SECRET`: Your Twilio API Key Secret + +## Setup Process + +### 1. Create/Update Secrets + +Run the setup script to create or update the Twilio credentials secret: + +```bash +python3 scripts/setup-secrets.py +``` + +This script will: + +- Read Twilio credentials from your `.env` file +- Create or update the `pipecat/twilio-credentials` secret in AWS Secrets Manager +- Verify the secret can be accessed + +### 2. Test Secret Access + +Verify that the secret can be retrieved: + +```bash +python3 scripts/test-twilio-secret.py +``` + +### 3. Test ECS Integration + +Simulate how ECS tasks will access all secrets: + +```bash +python3 scripts/test-ecs-secret-access.py +``` + +## IAM Permissions + +The ECS task role and execution role have been configured with the necessary permissions to access the Twilio credentials secret: + +```typescript +// Task Role - for application runtime access +SecretsManagerAccess: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ["secretsmanager:GetSecretValue"], + resources: [ + "arn:aws:secretsmanager:REGION:ACCOUNT:secret:pipecat/twilio-credentials*" + ], + }), + ], +}), +``` + +## Application Usage + +In your Python application, the Twilio credentials will be available as environment variables: + +```python +import os +from twilio.rest import Client + +# Credentials are automatically available from Secrets Manager +account_sid = os.getenv('TWILIO_ACCOUNT_SID') +auth_token = os.getenv('TWILIO_AUTH_TOKEN') +phone_number = os.getenv('TWILIO_PHONE_NUMBER') + +# Initialize Twilio client +client = Client(account_sid, auth_token) + +# Use the phone number for outbound calls or webhook configuration +print(f"Twilio phone number: {phone_number}") +``` + +## Security Considerations + +1. **No Hardcoded Credentials**: Credentials are never stored in the container image or code +2. **Encrypted at Rest**: AWS Secrets Manager encrypts secrets using AWS KMS +3. **Encrypted in Transit**: Secrets are retrieved over HTTPS +4. **IAM Access Control**: Only authorized ECS tasks can access the secrets +5. **Audit Trail**: All secret access is logged in CloudTrail + +## Troubleshooting + +### Secret Not Found + +If you get a "secret not found" error: + +1. Verify the secret exists: `aws secretsmanager list-secrets --region eu-north-1` +2. Check the secret name matches exactly: `pipecat/twilio-credentials` +3. Ensure you're using the correct AWS region + +### Access Denied + +If you get an "access denied" error: + +1. Verify your AWS credentials have Secrets Manager permissions +2. Check that the ECS task role has the correct permissions +3. Ensure the secret ARN in the IAM policy is correct + +### Invalid Credentials + +If Twilio API calls fail: + +1. Verify credentials in the AWS console: Secrets Manager โ†’ `pipecat/twilio-credentials` +2. Test credentials locally with the Twilio CLI +3. Check that the Account SID starts with "AC" and is 34 characters +4. Verify the Auth Token is 32 characters + +## Manual Secret Management + +You can also manage the secret manually using the AWS CLI: + +### View Secret + +```bash +aws secretsmanager get-secret-value \ + --secret-id pipecat/twilio-credentials \ + --region eu-north-1 +``` + +### Update Secret + +```bash +aws secretsmanager update-secret \ + --secret-id pipecat/twilio-credentials \ + --secret-string '{"TWILIO_ACCOUNT_SID":"ACxxx","TWILIO_AUTH_TOKEN":"xxx","TWILIO_PHONE_NUMBER":"+1234567890","TWILIO_API_SID":"SKxxx","TWILIO_API_SECRET":"xxx"}' \ + --region eu-north-1 +``` + +## Next Steps + +After setting up the Twilio credentials secret: + +1. Deploy the updated CDK infrastructure: `npm run deploy` (in infrastructure/) +2. Build and push the phone service container image +3. Deploy the phone service ECS task +4. Configure Twilio webhook URL to point to your phone service ALB +5. Test inbound phone calls to verify the integration works + +## Related Documentation + +- [SECRETS_SETUP.md](SECRETS_SETUP.md) - General secrets setup +- [DEPLOYMENT_PROCESS.md](DEPLOYMENT_PROCESS.md) - Full deployment guide +- [Twilio Integration Tasks](../.kiro/specs/pipecat-ecs-deployment/tasks.md) - Implementation tasks diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/docs/cloudwatch-queries.md b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/cloudwatch-queries.md new file mode 100644 index 0000000..6ba7d25 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/docs/cloudwatch-queries.md @@ -0,0 +1,266 @@ +# CloudWatch Logs Insights Queries + +This document contains useful CloudWatch Logs Insights queries for monitoring and troubleshooting the Pipecat ECS deployment. + +## Basic Queries + +### All Logs from Last Hour + +``` +fields @timestamp, level, message, service.name +| filter @timestamp > @timestamp - 1h +| sort @timestamp desc +``` + +### Error Logs Only + +``` +fields @timestamp, level, message, error.type, error.details +| filter level = "ERROR" +| sort @timestamp desc +``` + +### Performance Metrics + +``` +fields @timestamp, message, duration_ms, function +| filter ispresent(duration_ms) +| stats avg(duration_ms), max(duration_ms), min(duration_ms) by function +| sort avg desc +``` + +## Health Check Monitoring + +### Health Check Failures + +``` +fields @timestamp, message, request_id, error +| filter message like /health check failed/ +| sort @timestamp desc +``` + +### Health Check Response Times + +``` +fields @timestamp, duration_ms, request_id +| filter message like /health_check/ +| stats avg(duration_ms), max(duration_ms), count() by bin(5m) +``` + +## Bot Lifecycle Monitoring + +### Bot Process Events + +``` +fields @timestamp, lifecycle_event, bot_pid, room_url +| filter ispresent(lifecycle_event) +| sort @timestamp desc +``` + +### Bot Startup Failures + +``` +fields @timestamp, message, error.type, error.details, room_url +| filter error.type like /BOT_STARTUP_FAILED/ +| sort @timestamp desc +``` + +### Active Bot Count Over Time + +``` +fields @timestamp, active_bots +| filter ispresent(active_bots) +| stats max(active_bots) by bin(5m) +| sort @timestamp desc +``` + +## Request Monitoring + +### HTTP Request Patterns + +``` +fields @timestamp, http_method, http_path, request_id, client_ip +| filter ispresent(http_method) +| stats count() by http_method, http_path +| sort count desc +``` + +### Slow Requests + +``` +fields @timestamp, message, duration_ms, request_id, http_path +| filter duration_ms > 1000 +| sort duration_ms desc +``` + +### Request Volume by Endpoint + +``` +fields @timestamp, http_path +| filter ispresent(http_path) +| stats count() by http_path, bin(5m) +| sort @timestamp desc +``` + +## Error Analysis + +### Error Types and Frequency + +``` +fields @timestamp, error.type, error.details +| filter ispresent(error.type) +| stats count() by error.type +| sort count desc +``` + +### Room Creation Failures + +``` +fields @timestamp, message, error.details, request_id +| filter error.type like /ROOM_CREATION_FAILED/ or error.type like /TOKEN_GENERATION_FAILED/ +| sort @timestamp desc +``` + +### AWS Service Errors + +``` +fields @timestamp, message, error.type, error.details +| filter error.details like /AWS/ or error.details like /Bedrock/ or error.details like /SecretManager/ +| sort @timestamp desc +``` + +## Performance Analysis + +### Function Performance Summary + +``` +fields @timestamp, function, duration_ms +| filter ispresent(duration_ms) +| stats avg(duration_ms) as avg_duration, max(duration_ms) as max_duration, count() as call_count by function +| sort avg_duration desc +``` + +### Response Time Percentiles + +``` +fields @timestamp, duration_ms +| filter ispresent(duration_ms) +| stats pct(duration_ms, 50) as p50, pct(duration_ms, 90) as p90, pct(duration_ms, 95) as p95, pct(duration_ms, 99) as p99 by bin(5m) +| sort @timestamp desc +``` + +## Service Health Monitoring + +### Service Startup Events + +``` +fields @timestamp, message +| filter message like /Starting Pipecat Voice AI Agent server/ +| sort @timestamp desc +``` + +### Cleanup Events + +``` +fields @timestamp, message, terminated_count, killed_count, error_count +| filter message like /Cleanup completed/ +| sort @timestamp desc +``` + +### Environment Configuration Issues + +``` +fields @timestamp, message, missing_vars +| filter ispresent(missing_vars) +| sort @timestamp desc +``` + +## Daily.co Integration Monitoring + +### Daily Room Operations + +``` +fields @timestamp, message, room_url, request_id +| filter message like /Daily room/ or message like /Room created/ +| sort @timestamp desc +``` + +### Daily API Errors + +``` +fields @timestamp, message, error.details +| filter message like /Daily/ and level = "ERROR" +| sort @timestamp desc +``` + +## Custom Metrics Extraction + +### Extract Bot Metrics for CloudWatch Custom Metrics + +``` +fields @timestamp, active_bots, total_bot_processes +| filter ispresent(active_bots) +| stats max(active_bots) as max_active_bots, max(total_bot_processes) as max_total_processes by bin(1m) +``` + +### Extract Response Time Metrics + +``` +fields @timestamp, duration_ms, function +| filter ispresent(duration_ms) +| stats avg(duration_ms) as avg_response_time by function, bin(1m) +``` + +## Troubleshooting Queries + +### Find Correlated Errors by Request ID + +``` +fields @timestamp, message, request_id, level +| filter request_id = "YOUR_REQUEST_ID_HERE" +| sort @timestamp asc +``` + +### Find All Events for a Specific Bot + +``` +fields @timestamp, message, bot_pid, lifecycle_event +| filter bot_pid = YOUR_BOT_PID_HERE +| sort @timestamp asc +``` + +### Memory and Resource Issues + +``` +fields @timestamp, message +| filter message like /memory/ or message like /resource/ or message like /limit/ +| sort @timestamp desc +``` + +## Usage Instructions + +1. Go to CloudWatch Logs Insights in the AWS Console +2. Select the log group: `/ecs/pipecat-voice-agent-{environment}/application` +3. Copy and paste any of the above queries +4. Adjust the time range as needed +5. Click "Run query" + +## Creating CloudWatch Alarms from Queries + +You can create CloudWatch alarms based on these queries by: + +1. Running the query in CloudWatch Logs Insights +2. Clicking "Add to dashboard" or "Create alarm" +3. Setting appropriate thresholds and notification targets + +## Automated Monitoring + +The `monitoring.py` script can be used to continuously monitor service health and automatically log issues to CloudWatch. + +```bash +# Run continuous monitoring +python monitoring.py --url http://your-alb-dns-name --interval 30 + +# Single health check +python monitoring.py --url http://your-alb-dns-name --single-check +``` diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/bin/pipecat-eks.ts b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/bin/pipecat-eks.ts new file mode 100644 index 0000000..7729875 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/bin/pipecat-eks.ts @@ -0,0 +1,18 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { PipecatEksStack } from '../lib/pipecat-eks-stack'; + +const app = new cdk.App(); + +const environment = app.node.tryGetContext('environment') || 'test'; +const useDefaultVpc = app.node.tryGetContext('useDefaultVpc') === 'true'; + +new PipecatEksStack(app, `PipecatEksStack-${environment}`, { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION || 'eu-north-1', + }, + environment, + useDefaultVpc, +}); \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/cdk.json b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/cdk.json new file mode 100644 index 0000000..57c538d --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/pipecat-eks.ts" +} \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/lib/pipecat-eks-stack.ts b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/lib/pipecat-eks-stack.ts new file mode 100644 index 0000000..dd5ce96 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/lib/pipecat-eks-stack.ts @@ -0,0 +1,214 @@ +import * as cdk from 'aws-cdk-lib'; +import * as eks from 'aws-cdk-lib/aws-eks'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as ecr from 'aws-cdk-lib/aws-ecr'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +//import { KubectlV30Layer } from 'aws-cdk-lib/lambda-layer-kubectl'; +import { Construct } from 'constructs'; + +export interface PipecatEksStackProps extends cdk.StackProps { + environment: string; + useDefaultVpc: boolean; +} + +export class PipecatEksStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: PipecatEksStackProps) { + super(scope, id, props); + + const { environment, useDefaultVpc } = props; + + // VPC Configuration + let vpc: ec2.IVpc; + if (useDefaultVpc) { + vpc = ec2.Vpc.fromLookup(this, 'DefaultVpc', { + isDefault: true, + }); + } else { + vpc = new ec2.Vpc(this, 'PipecatVpc', { + maxAzs: 2, + natGateways: 2, + subnetConfiguration: [ + { + cidrMask: 24, + name: 'Public', + subnetType: ec2.SubnetType.PUBLIC, + }, + { + cidrMask: 24, + name: 'Private', + subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, + }, + ], + }); + } + + // ECR Repositories (reuse existing ones if they exist) + const voiceAgentRepo = ecr.Repository.fromRepositoryName( + this, + 'VoiceAgentRepository', + `pipecat-voice-agent-${environment}` + ); + + const phoneServiceRepo = ecr.Repository.fromRepositoryName( + this, + 'PhoneServiceRepository', + `pipecat-phone-service-${environment}` + ); + + // CloudWatch Log Groups + const appLogGroup = new logs.LogGroup(this, 'ApplicationLogGroup', { + logGroupName: `/eks/pipecat-voice-agent-${environment}/application`, + retention: logs.RetentionDays.ONE_WEEK, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const phoneLogGroup = new logs.LogGroup(this, 'PhoneLogGroup', { + logGroupName: `/eks/pipecat-phone-service-${environment}/application`, + retention: logs.RetentionDays.ONE_WEEK, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // EKS Cluster + const cluster = new eks.Cluster(this, 'PipecatCluster', { + clusterName: `pipecat-eks-cluster-${environment}`, + version: eks.KubernetesVersion.V1_31, + vpc, + defaultCapacity: 0, // We'll use Fargate + endpointAccess: eks.EndpointAccess.PUBLIC_AND_PRIVATE, + kubectlLayer: lambda.LayerVersion.fromLayerVersionArn( + this, + 'KubectlLayer', + `arn:aws:lambda:${this.region}:017000801446:layer:kubectl:1` + ), + clusterLogging: [ + eks.ClusterLoggingTypes.API, + eks.ClusterLoggingTypes.AUTHENTICATOR, + eks.ClusterLoggingTypes.SCHEDULER, + eks.ClusterLoggingTypes.CONTROLLER_MANAGER, + ], + }); + + // Fargate Profile + cluster.addFargateProfile('PipecatFargateProfile', { + selectors: [ + { + namespace: 'pipecat', + }, + { + namespace: 'kube-system', + labels: { + 'app.kubernetes.io/name': 'aws-load-balancer-controller', + }, + }, + ], + vpc, + subnetSelection: { + subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, + }, + }); + + // Service Account for Pipecat applications + const pipecatServiceAccount = cluster.addServiceAccount('PipecatServiceAccount', { + name: 'pipecat-service-account', + namespace: 'pipecat', + }); + + // Add permissions for Secrets Manager and other AWS services + pipecatServiceAccount.addToPrincipalPolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + resources: [ + `arn:aws:secretsmanager:${this.region}:${this.account}:secret:pipecat/*`, + ], + })); + + pipecatServiceAccount.addToPrincipalPolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], + resources: ['*'], + })); + + pipecatServiceAccount.addToPrincipalPolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + 'logs:DescribeLogStreams', + ], + resources: [ + appLogGroup.logGroupArn, + phoneLogGroup.logGroupArn, + ], + })); + + // Install AWS Load Balancer Controller + const albController = new eks.HelmChart(this, 'AWSLoadBalancerController', { + cluster, + chart: 'aws-load-balancer-controller', + repository: 'https://aws.github.io/eks-charts', + namespace: 'kube-system', + values: { + clusterName: cluster.clusterName, + serviceAccount: { + create: true, + name: 'aws-load-balancer-controller', + annotations: { + 'eks.amazonaws.com/role-arn': pipecatServiceAccount.role.roleArn, + }, + }, + region: this.region, + vpcId: vpc.vpcId, + }, + }); + + // Outputs + new cdk.CfnOutput(this, 'ClusterName', { + value: cluster.clusterName, + description: 'EKS Cluster Name', + }); + + new cdk.CfnOutput(this, 'ClusterEndpoint', { + value: cluster.clusterEndpoint, + description: 'EKS Cluster Endpoint', + }); + + new cdk.CfnOutput(this, 'VoiceAgentRepositoryUri', { + value: voiceAgentRepo.repositoryUri, + description: 'Voice Agent ECR Repository URI', + }); + + new cdk.CfnOutput(this, 'PhoneServiceRepositoryUri', { + value: phoneServiceRepo.repositoryUri, + description: 'Phone Service ECR Repository URI', + }); + + new cdk.CfnOutput(this, 'ApplicationLogGroupName', { + value: appLogGroup.logGroupName, + description: 'Application Log Group Name', + }); + + new cdk.CfnOutput(this, 'PhoneLogGroupName', { + value: phoneLogGroup.logGroupName, + description: 'Phone Service Log Group Name', + }); + + new cdk.CfnOutput(this, 'KubectlCommand', { + value: `aws eks update-kubeconfig --region ${this.region} --name ${cluster.clusterName}`, + description: 'Command to configure kubectl', + }); + + new cdk.CfnOutput(this, 'ServiceAccountArn', { + value: pipecatServiceAccount.role.roleArn, + description: 'Pipecat Service Account Role ARN', + }); + } +} \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/package.json b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/package.json new file mode 100644 index 0000000..3307bef --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/package.json @@ -0,0 +1,15 @@ +{ + "name": "infrastructure-eks", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk", + "deploy": "cdk deploy", + "diff": "cdk diff", + "synth": "cdk synth" + },"diff": "cdk diff", + "synth": "cdk synth" + }, \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/tsconfig.json b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/tsconfig.json new file mode 100644 index 0000000..231b317 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/-eks/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["./node_modules/@types"] + }, + "exclude": ["node_modules", "cdk.out"] +} \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/.gitignore b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/.gitignore new file mode 100644 index 0000000..f60797b --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/.npmignore b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/.npmignore new file mode 100644 index 0000000..c1d6d45 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/DEPLOYMENT_GUIDE.md b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..924a52f --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/DEPLOYMENT_GUIDE.md @@ -0,0 +1,194 @@ +# Pipecat ECS Deployment Guide + +This guide walks you through deploying the Pipecat voice AI agent infrastructure to AWS ECS. + +## Prerequisites Checklist + +Before deploying, ensure you have: + +- [x] AWS CLI configured with appropriate credentials +- [x] Node.js and npm installed +- [x] AWS CDK installed globally: `npm install -g aws-cdk` +- [x] Docker installed (for building container images) +- [x] Daily.co API key available +- [x] AWS account with sufficient permissions for ECS, ECR, ALB, VPC, IAM, and Secrets Manager + +## Step-by-Step Deployment + +### 1. Prepare the Infrastructure + +```bash +# Navigate to infrastructure directory +cd pipecat-ecs-deployment/infrastructure + +# Install dependencies +npm install + +# Build the project +npm run build + +# Run tests to verify everything is working +npm test +``` + +### 2. Bootstrap CDK (if not done before) + +```bash +# Bootstrap CDK in your target region +cdk bootstrap --region eu-north-1 +``` + +### 3. Deploy the Infrastructure + +```bash +# Deploy with default settings (test environment, default VPC) +./deploy.sh + +# Or deploy with custom options +./deploy.sh --environment prod --custom-vpc --region us-west-2 +``` + +### 4. Set Up AWS Secrets Manager + +Before deploying the infrastructure, set up the required secrets: + +```bash +# Navigate back to the main directory +cd .. + +# Set up secrets using the automated script +python3 setup-secrets.py + +# Verify secrets were created successfully +python3 test-secrets-integration.py +``` + +This will create two secrets: + +- `pipecat/daily-api-key`: Contains Daily.co API credentials +- `pipecat/aws-credentials`: Contains AWS credentials for Bedrock access + +**Note**: Ensure your `.env` file contains the required credentials before running the setup script. + +### 5. Verify Deployment + +Check that all resources were created successfully: + +```bash +# Check stack status +aws cloudformation describe-stacks --stack-name PipecatEcsStack-test + +# Get stack outputs +aws cloudformation describe-stacks \ + --stack-name PipecatEcsStack-test \ + --query 'Stacks[0].Outputs' +``` + +## What Gets Created + +The infrastructure deployment creates: + +### Core Infrastructure + +- **ECS Cluster**: `pipecat-cluster-{environment}` +- **ECR Repository**: `pipecat-voice-agent-{environment}` +- **Application Load Balancer**: `pipecat-alb-{environment}` +- **Target Group**: For routing traffic to ECS tasks + +### Networking + +- **VPC**: Uses default VPC or creates new one +- **Security Groups**: + - ALB security group (allows HTTP/HTTPS from internet) + - ECS security group (allows traffic from ALB on port 7860) + +### IAM Roles + +- **Task Role**: For Pipecat application with Bedrock and Secrets Manager permissions +- **Execution Role**: For ECS task execution with ECR and CloudWatch permissions + +### Monitoring + +- **CloudWatch Log Group**: `/ecs/pipecat-voice-agent-{environment}` + +## Important Outputs + +After deployment, note these key outputs: + +- **LoadBalancerDnsName**: URL to access your application +- **RepositoryUri**: ECR repository for pushing container images +- **ClusterName**: ECS cluster name for service deployment +- **TaskRoleArn**: IAM role ARN for ECS tasks +- **ExecutionRoleArn**: IAM role ARN for ECS execution + +## Next Steps + +After infrastructure deployment: + +1. **Build and push container image** to the ECR repository +2. **Create ECS service and task definition** (next task in the implementation plan) +3. **Test the deployment** by accessing the Load Balancer DNS name +4. **Set up monitoring and alerting** as needed + +## Troubleshooting + +### Common Issues + +1. **CDK Bootstrap Required** + + ```bash + cdk bootstrap --region your-region + ``` + +2. **Insufficient Permissions** + Ensure your AWS credentials have permissions for all required services. + +3. **Default VPC Not Found** + Use `--custom-vpc` flag to create a new VPC. + +4. **Region Not Supported** + Ensure Amazon Bedrock Nova Sonic is available in your target region. + +### Useful Commands + +```bash +# View detailed stack information +aws cloudformation describe-stack-resources --stack-name PipecatEcsStack-test + +# Check ECR repository +aws ecr describe-repositories --repository-names pipecat-voice-agent-test + +# View CloudWatch logs +aws logs describe-log-groups --log-group-name-prefix "/ecs/pipecat" + +# Clean up (destroy stack) +cdk destroy PipecatEcsStack-test +``` + +## Cost Considerations + +### Test Environment + +- Uses default VPC (no NAT Gateway costs) +- Minimal resource allocation +- Short log retention (1 week) + +### Production Environment + +- Consider custom VPC with NAT Gateways +- Adjust resource limits based on usage +- Longer log retention periods +- Enable detailed monitoring + +## Security Notes + +- All secrets are stored in AWS Secrets Manager with encryption at rest +- IAM roles follow principle of least privilege +- Security groups restrict network access appropriately +- Container runs as non-root user (when Dockerfile is updated) +- Secrets are injected as environment variables by ECS (not stored in container images) +- All secret access is logged in CloudTrail for audit purposes + +For detailed information about secrets management, see [SECRETS_SETUP.md](../SECRETS_SETUP.md). + +This infrastructure provides a solid foundation for deploying the Pipecat voice AI agent to AWS ECS with proper security, monitoring, and scalability considerations. diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/README.md b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/README.md new file mode 100644 index 0000000..a0af1e9 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/README.md @@ -0,0 +1,165 @@ +# Pipecat ECS Infrastructure + +This CDK project creates the AWS infrastructure needed to deploy the Pipecat voice AI agent to Amazon ECS. + +## Architecture + +The infrastructure includes: + +- **VPC**: Uses default VPC or creates a new one with public/private subnets +- **ECS Cluster**: Fargate-enabled cluster for running containers +- **ECR Repository**: For storing container images +- **Application Load Balancer**: Internet-facing ALB for routing traffic +- **Security Groups**: Proper network security configuration +- **IAM Roles**: Task and execution roles with minimal required permissions +- **CloudWatch**: Log groups for monitoring + +## Prerequisites + +1. **AWS CLI configured** with appropriate credentials +2. **Node.js and npm** installed +3. **AWS CDK** installed globally: `npm install -g aws-cdk` +4. **Docker** installed (for building container images) + +## Quick Start + +1. **Install dependencies**: + + ```bash + npm install + ``` + +2. **Build the project**: + + ```bash + npm run build + ``` + +3. **Deploy the infrastructure**: + + ```bash + ./deploy.sh + ``` + + Or with custom options: + + ```bash + ./deploy.sh --environment prod --custom-vpc --region us-west-2 + ``` + +4. **Create the Daily API key secret**: + ```bash + aws secretsmanager create-secret \ + --name 'pipecat/daily-api-key' \ + --secret-string 'YOUR_DAILY_API_KEY' \ + --region eu-north-1 + ``` + +## Available Scripts + +- `npm run build` - Compile TypeScript to JavaScript +- `npm run deploy` - Deploy using default settings +- `npm run deploy:test` - Deploy test environment +- `npm run deploy:prod` - Deploy production environment with custom VPC +- `npm run synth` - Synthesize CloudFormation template +- `npm run diff` - Show differences between deployed and current state +- `npm run destroy` - Destroy the stack +- `npm run ecr:login` - Login to ECR +- `npm run ecr:get-uri` - Get ECR repository URI + +## Configuration + +### Environment Variables + +The deployment can be customized using these context variables: + +- `environment` - Environment name (default: "test") +- `useDefaultVpc` - Use default VPC (default: true) + +### Deployment Options + +```bash +# Test environment with default VPC +./deploy.sh --environment test + +# Production environment with custom VPC +./deploy.sh --environment prod --custom-vpc + +# Different AWS region +./deploy.sh --region eu-west-1 +``` + +## Outputs + +After deployment, the stack provides these outputs: + +- **VpcId** - VPC ID for the deployment +- **ClusterName** - ECS cluster name +- **RepositoryUri** - ECR repository URI for container images +- **LoadBalancerDnsName** - DNS name to access the application +- **TaskRoleArn** - ARN of the ECS task role +- **ExecutionRoleArn** - ARN of the ECS execution role + +## Security + +The infrastructure follows AWS security best practices: + +- **IAM Roles**: Minimal permissions for Bedrock and Secrets Manager access +- **Security Groups**: Restrictive ingress rules +- **Secrets Management**: API keys stored in AWS Secrets Manager +- **Network Security**: Private subnets for ECS tasks (when using custom VPC) + +## Cost Considerations + +For testing environments: + +- Uses default VPC to avoid NAT Gateway costs +- Minimal resource allocation +- Short log retention periods + +For production environments: + +- Consider using custom VPC with NAT Gateways +- Adjust resource limits and retention policies +- Enable detailed monitoring + +## Next Steps + +After deploying the infrastructure: + +1. Build and push your container image to ECR +2. Create the ECS service and task definition +3. Configure auto-scaling policies +4. Set up monitoring and alerting + +## Troubleshooting + +### Common Issues + +1. **CDK Bootstrap Required**: + + ```bash + cdk bootstrap --region your-region + ``` + +2. **Insufficient Permissions**: + Ensure your AWS credentials have permissions for ECS, ECR, ALB, VPC, and IAM. + +3. **Default VPC Not Found**: + Use `--custom-vpc` flag to create a new VPC. + +4. **Secret Not Found**: + Create the Daily API key secret before deploying the ECS service. + +### Useful Commands + +```bash +# Check stack status +aws cloudformation describe-stacks --stack-name PipecatEcsStack-test + +# View logs +aws logs describe-log-groups --log-group-name-prefix "/ecs/pipecat" + +# List ECR repositories +aws ecr describe-repositories --repository-names pipecat-voice-agent-test +``` diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/bin/infrastructure.ts b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/bin/infrastructure.ts new file mode 100644 index 0000000..a253a5b --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/bin/infrastructure.ts @@ -0,0 +1,27 @@ +#!/usr/bin/env node +import * as cdk from "aws-cdk-lib"; +import { InfrastructureStack } from "../lib/infrastructure-stack"; + +const app = new cdk.App(); + +// Get environment from context or default to 'test' +const environment = app.node.tryGetContext("environment") || "test"; +const useDefaultVpc = app.node.tryGetContext("useDefaultVpc") !== false; + +new InfrastructureStack(app, `PipecatEcsStack-${environment}`, { + environment: environment, + useDefaultVpc: useDefaultVpc, + + // Use current CLI configuration for account/region + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, + + // Add tags for resource management + tags: { + Project: "Pipecat-Voice-Agent", + Environment: environment, + ManagedBy: "CDK", + }, +}); diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/cdk.context.json b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/cdk.context.json new file mode 100644 index 0000000..9326319 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/cdk.context.json @@ -0,0 +1,72 @@ +{ + "environment": "test", + "useDefaultVpc": true, + "aws-region": "eu-north-1", + "vpc-provider:account=094271239310:filter.isDefault=true:region=eu-north-1:returnAsymmetricSubnets=true": { + "vpcId": "vpc-075df9749df7039ed", + "vpcCidrBlock": "172.31.0.0/16", + "ownerAccountId": "094271239310", + "availabilityZones": [], + "subnetGroups": [ + { + "name": "Public", + "type": "Public", + "subnets": [ + { + "subnetId": "subnet-0b55a75a8d32514b2", + "cidr": "172.31.16.0/20", + "availabilityZone": "eu-north-1a", + "routeTableId": "rtb-03a1438a192f4e504" + }, + { + "subnetId": "subnet-03027ea513322f090", + "cidr": "172.31.32.0/20", + "availabilityZone": "eu-north-1b", + "routeTableId": "rtb-03a1438a192f4e504" + }, + { + "subnetId": "subnet-0fc0f7963a3aefac6", + "cidr": "172.31.0.0/20", + "availabilityZone": "eu-north-1c", + "routeTableId": "rtb-03a1438a192f4e504" + } + ] + } + ] + }, + "acknowledged-issue-numbers": [ + 34892 + ], + "vpc-provider:account=039612864592:filter.isDefault=true:region=eu-north-1:returnAsymmetricSubnets=true": { + "vpcId": "vpc-00ab3400742a15e62", + "vpcCidrBlock": "172.31.0.0/16", + "ownerAccountId": "039612864592", + "availabilityZones": [], + "subnetGroups": [ + { + "name": "Public", + "type": "Public", + "subnets": [ + { + "subnetId": "subnet-03509200c30440bce", + "cidr": "172.31.16.0/20", + "availabilityZone": "eu-north-1a", + "routeTableId": "rtb-04d8b78791834c0a4" + }, + { + "subnetId": "subnet-0c7ad231518104602", + "cidr": "172.31.32.0/20", + "availabilityZone": "eu-north-1b", + "routeTableId": "rtb-04d8b78791834c0a4" + }, + { + "subnetId": "subnet-0e4572c8bcaf68309", + "cidr": "172.31.0.0/20", + "availabilityZone": "eu-north-1c", + "routeTableId": "rtb-04d8b78791834c0a4" + } + ] + } + ] + } +} diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/cdk.json b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/cdk.json new file mode 100644 index 0000000..baccacc --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/cdk.json @@ -0,0 +1,99 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/infrastructure.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-kms:applyImportedAliasPermissionsToPrincipal": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/core:explicitStackTags": true, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, + "@aws-cdk/aws-events:requireEventBusPolicySid": true, + "@aws-cdk/core:aspectPrioritiesMutating": true, + "@aws-cdk/aws-dynamodb:retainTableReplica": true, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, + "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true, + "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, + "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true, + "@aws-cdk/aws-lambda:useCdkManagedLogGroup": true + } +} diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/deploy.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/deploy.sh new file mode 100755 index 0000000..a802fe9 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/deploy.sh @@ -0,0 +1,153 @@ +#!/bin/bash + +# Pipecat ECS Infrastructure Deployment Script + +set -e + +# Default values +ENVIRONMENT="test" +USE_DEFAULT_VPC="true" +AWS_REGION="eu-north-1" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -e|--environment) + ENVIRONMENT="$2" + shift 2 + ;; + --custom-vpc) + USE_DEFAULT_VPC="false" + shift + ;; + -r|--region) + AWS_REGION="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "Options:" + echo " -e, --environment ENV Set environment (default: test)" + echo " --custom-vpc Use custom VPC instead of default" + echo " -r, --region REGION Set AWS region (default: eu-north-1)" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + echo "Unknown option $1" + exit 1 + ;; + esac +done + +echo "Deploying Pipecat ECS Infrastructure..." +echo "Environment: $ENVIRONMENT" +echo "Use Default VPC: $USE_DEFAULT_VPC" +echo "AWS Region: $AWS_REGION" + +# Check if AWS CLI is configured +if ! aws sts get-caller-identity > /dev/null 2>&1; then + echo "Error: AWS CLI is not configured or credentials are invalid" + echo "Please run 'aws configure' to set up your credentials" + exit 1 +fi + +# Check if CDK is bootstrapped +echo "Checking CDK bootstrap status..." +if ! aws cloudformation describe-stacks --stack-name CDKToolkit --region $AWS_REGION > /dev/null 2>&1; then + echo "CDK is not bootstrapped in region $AWS_REGION" + echo "Running CDK bootstrap..." + npx cdk bootstrap --region $AWS_REGION +fi + +# Install dependencies +echo "Installing dependencies..." +npm install + +# Build the project +echo "Building CDK project..." +npm run build + +# Deploy the stack +echo "Deploying CDK stack..." +npx cdk deploy \ + --context environment=$ENVIRONMENT \ + --context useDefaultVpc=$USE_DEFAULT_VPC \ + --region $AWS_REGION \ + --require-approval never + +echo "Infrastructure deployment completed successfully!" +echo "" + +# Get ECR repository URI from stack outputs +STACK_NAME="PipecatEcsStack-$ENVIRONMENT" +REPOSITORY_URI=$(aws cloudformation describe-stacks \ + --stack-name $STACK_NAME \ + --region $AWS_REGION \ + --query 'Stacks[0].Outputs[?OutputKey==`RepositoryUri`].OutputValue' \ + --output text 2>/dev/null || echo "") + +if [ -n "$REPOSITORY_URI" ]; then + echo "ECR Repository created: $REPOSITORY_URI" + + # Build and push initial Docker image + echo "" + echo "Building and pushing initial Docker image..." + + # Go back to parent directory to access Dockerfile and build scripts + cd .. + + # Check if Docker is running + if ! docker info > /dev/null 2>&1; then + echo "Warning: Docker is not running. Skipping image build." + echo "Please start Docker and run: ./scripts/build-and-push.sh -e $ENVIRONMENT -r $AWS_REGION" + else + # Make build script executable and run it + chmod +x scripts/build-and-push.sh + + # Build and push the image + if ./scripts/build-and-push.sh -e $ENVIRONMENT -r $AWS_REGION -t latest --force; then + echo "โœ“ Docker image built and pushed successfully!" + + # Update ECS service with the new image + echo "" + echo "Updating ECS service with new image..." + chmod +x scripts/deploy-service.sh + + if ./scripts/deploy-service.sh -e $ENVIRONMENT -r $AWS_REGION -t latest --update; then + echo "โœ“ ECS service updated successfully!" + + # Get application URL + ALB_DNS=$(aws cloudformation describe-stacks \ + --stack-name $STACK_NAME \ + --region $AWS_REGION \ + --query 'Stacks[0].Outputs[?OutputKey==`LoadBalancerDnsName`].OutputValue' \ + --output text 2>/dev/null || echo "Not available") + + echo "" + echo "๐ŸŽ‰ Complete deployment finished!" + if [ "$ALB_DNS" != "Not available" ]; then + echo "๐ŸŒ Application URL: http://$ALB_DNS" +fi + echo "๐Ÿ“‹ Monitor your application:" + echo " - Service status: aws ecs describe-services --cluster pipecat-cluster-$ENVIRONMENT --services pipecat-service-$ENVIRONMENT --region $AWS_REGION" + echo " - Application logs: aws logs tail /ecs/pipecat-voice-agent-$ENVIRONMENT/application --follow --region $AWS_REGION" + else + echo "โš ๏ธ ECS service update failed. You may need to run it manually:" + echo " ./scripts/deploy-service.sh -e $ENVIRONMENT -r $AWS_REGION -t latest --update" + fi + else + echo "โš ๏ธ Docker image build failed. You may need to run it manually:" + echo " ./scripts/build-and-push.sh -e $ENVIRONMENT -r $AWS_REGION -t latest" + fi + fi +else + echo "โš ๏ธ Could not retrieve ECR repository URI from stack outputs" +fi + +echo "" +echo "Next steps:" +echo "1. Set up secrets in AWS Secrets Manager (if not done already):" +echo " python3 setup-secrets.py" +echo "2. Test the application at the URL above" +echo "3. Monitor the deployment and logs" \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/ecr-helper.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/ecr-helper.sh new file mode 100755 index 0000000..6644318 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/ecr-helper.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# ECR Helper Script for Pipecat ECS Deployment + +set -e + +# Default values +ENVIRONMENT="test" +AWS_REGION="eu-north-1" +ACTION="" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -e|--environment) + ENVIRONMENT="$2" + shift 2 + ;; + -r|--region) + AWS_REGION="$2" + shift 2 + ;; + --login) + ACTION="login" + shift + ;; + --get-uri) + ACTION="get-uri" + shift + ;; + -h|--help) + echo "Usage: $0 [OPTIONS] ACTION" + echo "Actions:" + echo " --login Login to ECR" + echo " --get-uri Get ECR repository URI" + echo "Options:" + echo " -e, --environment ENV Set environment (default: test)" + echo " -r, --region REGION Set AWS region (default: eu-north-1)" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + echo "Unknown option $1" + exit 1 + ;; + esac +done + +if [ -z "$ACTION" ]; then + echo "Error: No action specified. Use --help for usage information." + exit 1 +fi + +REPOSITORY_NAME="pipecat-voice-agent-$ENVIRONMENT" + +case $ACTION in + "login") + echo "Logging into ECR..." + aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $(aws sts get-caller-identity --query Account --output text).dkr.ecr.$AWS_REGION.amazonaws.com + echo "ECR login successful!" + ;; + "get-uri") + ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + REPOSITORY_URI="$ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$REPOSITORY_NAME" + echo $REPOSITORY_URI + ;; +esac \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/jest.config.js b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/jest.config.js new file mode 100644 index 0000000..08263b8 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/lib/infrastructure-stack.ts b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/lib/infrastructure-stack.ts new file mode 100644 index 0000000..d43b74c --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/lib/infrastructure-stack.ts @@ -0,0 +1,1217 @@ +import * as cdk from "aws-cdk-lib"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as ecs from "aws-cdk-lib/aws-ecs"; +import * as ecr from "aws-cdk-lib/aws-ecr"; +import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2"; +import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; +import * as logs from "aws-cdk-lib/aws-logs"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch"; +import * as sns from "aws-cdk-lib/aws-sns"; +import * as acm from "aws-cdk-lib/aws-certificatemanager"; +import { Construct } from "constructs"; + +export interface PipecatEcsStackProps extends cdk.StackProps { + environment?: string; + useDefaultVpc?: boolean; + domainName?: string; // Optional domain name for HTTPS + certificateArn?: string; // Optional certificate ARN +} + +export class InfrastructureStack extends cdk.Stack { + public readonly vpc: ec2.IVpc; + public readonly cluster: ecs.Cluster; + public readonly repository: ecr.IRepository; + public readonly loadBalancer: elbv2.ApplicationLoadBalancer; + public readonly taskRole: iam.Role; + public readonly executionRole: iam.Role; + + constructor(scope: Construct, id: string, props?: PipecatEcsStackProps) { + super(scope, id, props); + + const environment = props?.environment || "test"; + const useDefaultVpc = props?.useDefaultVpc ?? true; + + // VPC Configuration + if (useDefaultVpc) { + this.vpc = ec2.Vpc.fromLookup(this, "DefaultVpc", { + isDefault: true, + }); + } else { + this.vpc = new ec2.Vpc(this, "PipecatVpc", { + maxAzs: 2, + natGateways: 1, + subnetConfiguration: [ + { + cidrMask: 24, + name: "Public", + subnetType: ec2.SubnetType.PUBLIC, + }, + { + cidrMask: 24, + name: "Private", + subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, + }, + ], + }); + } + + // ECR Repository for container images + // Use existing ECR repository + this.repository = ecr.Repository.fromRepositoryName( + this, + "PipecatRepository", + `pipecat-voice-agent-${environment}` + ); + + // ECS Cluster + this.cluster = new ecs.Cluster(this, "PipecatCluster", { + clusterName: `pipecat-cluster-${environment}`, + vpc: this.vpc, + enableFargateCapacityProviders: true, + }); + + // CloudWatch Log Groups for different log types + const applicationLogGroup = new logs.LogGroup( + this, + "PipecatApplicationLogGroup", + { + logGroupName: `/ecs/pipecat-voice-agent-${environment}/application`, + retention: logs.RetentionDays.TWO_WEEKS, // Longer retention for application logs + removalPolicy: cdk.RemovalPolicy.DESTROY, + } + ); + + const accessLogGroup = new logs.LogGroup(this, "PipecatAccessLogGroup", { + logGroupName: `/ecs/pipecat-voice-agent-${environment}/access`, + retention: logs.RetentionDays.ONE_WEEK, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const errorLogGroup = new logs.LogGroup(this, "PipecatErrorLogGroup", { + logGroupName: `/ecs/pipecat-voice-agent-${environment}/error`, + retention: logs.RetentionDays.ONE_MONTH, // Longer retention for error logs + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // Main log group for backward compatibility + const logGroup = applicationLogGroup; + + // Secrets for Daily API Key, AWS Credentials, and Twilio + const dailyApiKeySecret = secretsmanager.Secret.fromSecretNameV2( + this, + "DailyApiKeySecret", + "pipecat/daily-api-key" + ); + + const awsCredentialsSecret = secretsmanager.Secret.fromSecretNameV2( + this, + "AwsCredentialsSecret", + "pipecat/aws-credentials" + ); + + const twilioCredentialsSecret = secretsmanager.Secret.fromSecretNameV2( + this, + "TwilioCredentialsSecret", + "pipecat/twilio-credentials" + ); + + // IAM Roles - FIXED: Added Nova Sonic bidirectional streaming permission + this.taskRole = new iam.Role(this, "PipecatTaskRole", { + assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"), + description: "Role for Pipecat ECS tasks", + inlinePolicies: { + BedrockAccess: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + "bedrock:InvokeModelWithBidirectionalStream", // ADDED: Nova Sonic support + "bedrock:ListFoundationModels", // ADDED: For Nova Sonic testing + ], + resources: [ + `arn:aws:bedrock:${this.region}::foundation-model/amazon.nova-sonic-v1:0`, + `arn:aws:bedrock:${this.region}::foundation-model/*`, // ADDED: For broader access + ], + }), + ], + }), + SecretsManagerAccess: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ["secretsmanager:GetSecretValue"], + resources: [ + `arn:aws:secretsmanager:${this.region}:${this.account}:secret:pipecat/*`, + dailyApiKeySecret.secretArn, + awsCredentialsSecret.secretArn, + twilioCredentialsSecret.secretArn, + ], + }), + ], + }), + }, + }); + + this.executionRole = new iam.Role(this, "PipecatExecutionRole", { + assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"), + description: "Execution role for Pipecat ECS tasks", + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName( + "service-role/AmazonECSTaskExecutionRolePolicy" + ), + ], + inlinePolicies: { + SecretsManagerAccess: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ["secretsmanager:GetSecretValue"], + resources: [ + `arn:aws:secretsmanager:${this.region}:${this.account}:secret:pipecat/*`, + dailyApiKeySecret.secretArn, + awsCredentialsSecret.secretArn, + twilioCredentialsSecret.secretArn, + ], + }), + ], + }), + }, + }); + + // Security Groups + const albSecurityGroup = new ec2.SecurityGroup(this, "AlbSecurityGroup", { + vpc: this.vpc, + description: "Security group for Pipecat ALB", + allowAllOutbound: true, + }); + + albSecurityGroup.addIngressRule( + ec2.Peer.anyIpv4(), + ec2.Port.tcp(80), + "Allow HTTP traffic from anywhere" + ); + + albSecurityGroup.addIngressRule( + ec2.Peer.anyIpv4(), + ec2.Port.tcp(443), + "Allow HTTPS traffic from anywhere" + ); + + const ecsSecurityGroup = new ec2.SecurityGroup(this, "EcsSecurityGroup", { + vpc: this.vpc, + description: "Security group for Pipecat ECS tasks", + allowAllOutbound: true, + }); + + ecsSecurityGroup.addIngressRule( + albSecurityGroup, + ec2.Port.tcp(7860), + "Allow traffic from ALB to ECS tasks" + ); + + // Application Load Balancer + this.loadBalancer = new elbv2.ApplicationLoadBalancer( + this, + "PipecatLoadBalancer", + { + vpc: this.vpc, + internetFacing: true, + securityGroup: albSecurityGroup, + loadBalancerName: `pipecat-alb-${environment}`, + } + ); + + // Target Group (will be used by ECS service) + const targetGroup = new elbv2.ApplicationTargetGroup( + this, + "PipecatTargetGroup", + { + vpc: this.vpc, + port: 7860, + protocol: elbv2.ApplicationProtocol.HTTP, + targetType: elbv2.TargetType.IP, + healthCheck: { + enabled: true, + path: "/health", + protocol: elbv2.Protocol.HTTP, + port: "7860", + healthyHttpCodes: "200", + interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(10), // Increased timeout + healthyThresholdCount: 2, + unhealthyThresholdCount: 5, // More tolerant of failures + }, + // WebSocket support configuration + protocolVersion: elbv2.ApplicationProtocolVersion.HTTP1, + stickinessCookieDuration: cdk.Duration.seconds(86400), // 24 hours for WebSocket sessions + } + ); + + // Configure target group attributes for WebSocket support + const cfnTargetGroup = targetGroup.node + .defaultChild as elbv2.CfnTargetGroup; + cfnTargetGroup.addPropertyOverride("TargetGroupAttributes", [ + { + Key: "stickiness.enabled", + Value: "true", + }, + { + Key: "stickiness.type", + Value: "lb_cookie", + }, + { + Key: "stickiness.lb_cookie.duration_seconds", + Value: "86400", + }, + { + Key: "load_balancing.algorithm.type", + Value: "least_outstanding_requests", + }, + ]); + + // ALB Listeners - HTTP and HTTPS + this.loadBalancer.addListener("PipecatHttpListener", { + port: 80, + protocol: elbv2.ApplicationProtocol.HTTP, + defaultTargetGroups: [targetGroup], + }); + + // Add HTTPS listener if certificate is provided + if (props?.certificateArn) { + const certificate = acm.Certificate.fromCertificateArn( + this, + "PipecatCertificate", + props.certificateArn + ); + + this.loadBalancer.addListener("PipecatHttpsListener", { + port: 443, + protocol: elbv2.ApplicationProtocol.HTTPS, + certificates: [certificate], + defaultTargetGroups: [targetGroup], + }); + } + + // ECS Task Definition with optimized resources for stability + const taskDefinition = new ecs.FargateTaskDefinition( + this, + "PipecatTaskDefinition", + { + family: `pipecat-voice-agent-${environment}`, + cpu: 2048, // 2 vCPU - increased for better performance + memoryLimitMiB: 4096, // 4 GB - increased to prevent OOM kills + taskRole: this.taskRole, + executionRole: this.executionRole, + } + ); + + // Container Definition + const container = taskDefinition.addContainer("PipecatContainer", { + image: ecs.ContainerImage.fromEcrRepository(this.repository, "latest"), + containerName: "pipecat-container", + logging: ecs.LogDrivers.awsLogs({ + logGroup: applicationLogGroup, + streamPrefix: "ecs", + datetimeFormat: "%Y-%m-%d %H:%M:%S", + multilinePattern: "^\\d{4}-\\d{2}-\\d{2}", + }), + environment: { + AWS_REGION: this.region, + HOST: "0.0.0.0", + FAST_API_PORT: "7860", + ENVIRONMENT: environment, + LOG_LEVEL: "INFO", + GRACEFUL_SHUTDOWN_TIMEOUT: "30", + BOT_CLEANUP_INTERVAL: "300", + MEMORY_CLEANUP_THRESHOLD: "0.8", + HEALTH_CHECK_INTERVAL: "30", + HEALTH_CHECK_RETRIES: "5", + ENABLE_REQUEST_POOLING: "true", + MAX_REQUEST_POOL_SIZE: "100", + REQUEST_TIMEOUT: "30", + // External domain for WebSocket URLs - use ALB DNS if no custom domain + EXTERNAL_DOMAIN: props?.domainName || this.loadBalancer.loadBalancerDnsName, + FORCE_HTTPS: props?.certificateArn ? "true" : "false", + }, + secrets: { + // Daily.co API credentials + DAILY_API_KEY: ecs.Secret.fromSecretsManager( + dailyApiKeySecret, + "DAILY_API_KEY" + ), + DAILY_API_URL: ecs.Secret.fromSecretsManager( + dailyApiKeySecret, + "DAILY_API_URL" + ), + + // AWS credentials for Bedrock access (alternative to IAM roles) + AWS_ACCESS_KEY_ID: ecs.Secret.fromSecretsManager( + awsCredentialsSecret, + "AWS_ACCESS_KEY_ID" + ), + AWS_SECRET_ACCESS_KEY: ecs.Secret.fromSecretsManager( + awsCredentialsSecret, + "AWS_SECRET_ACCESS_KEY" + ), + + // Twilio API credentials + TWILIO_ACCOUNT_SID: ecs.Secret.fromSecretsManager( + twilioCredentialsSecret, + "TWILIO_ACCOUNT_SID" + ), + TWILIO_AUTH_TOKEN: ecs.Secret.fromSecretsManager( + twilioCredentialsSecret, + "TWILIO_AUTH_TOKEN" + ), + TWILIO_PHONE_NUMBER: ecs.Secret.fromSecretsManager( + twilioCredentialsSecret, + "TWILIO_PHONE_NUMBER" + ), + TWILIO_API_SID: ecs.Secret.fromSecretsManager( + twilioCredentialsSecret, + "TWILIO_API_SID" + ), + TWILIO_API_SECRET: ecs.Secret.fromSecretsManager( + twilioCredentialsSecret, + "TWILIO_API_SECRET" + ), + }, + healthCheck: { + command: [ + "CMD-SHELL", + "curl -f http://localhost:7860/health || exit 1", + ], + interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(10), // Increased timeout + startPeriod: cdk.Duration.seconds(120), // Longer startup period + retries: 5, // More retries before marking unhealthy + }, + }); + + // Port mapping + container.addPortMappings({ + containerPort: 7860, + protocol: ecs.Protocol.TCP, + name: "http", + }); + + // ECS Service with optimized deployment configuration + const service = new ecs.FargateService(this, "PipecatService", { + cluster: this.cluster, + taskDefinition: taskDefinition, + serviceName: `pipecat-service-${environment}`, + desiredCount: 3, // Increased to 3 tasks for better stability + minHealthyPercent: 66, // Keep 2/3 healthy during deployments + maxHealthyPercent: 200, + healthCheckGracePeriod: cdk.Duration.seconds(300), // 5 minutes grace period + securityGroups: [ecsSecurityGroup], + vpcSubnets: useDefaultVpc + ? { + // For default VPC, use public subnets since private subnets may not exist + subnetType: ec2.SubnetType.PUBLIC, + } + : { + // For custom VPC, use private subnets with NAT gateway + subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, + }, + assignPublicIp: useDefaultVpc, // Assign public IP when using public subnets + enableExecuteCommand: true, // For debugging + circuitBreaker: { rollback: true }, // Enable deployment circuit breaker + }); + + // Attach service to target group + service.attachToApplicationTargetGroup(targetGroup); + + // Auto Scaling Configuration with more conservative settings + const scaling = service.autoScaleTaskCount({ + minCapacity: 2, // Minimum 2 tasks for HA + maxCapacity: 6, // Increased max capacity + }); + + // Scale based on CPU utilization with higher threshold + scaling.scaleOnCpuUtilization("CpuScaling", { + targetUtilizationPercent: 75, // Higher threshold to reduce jittering + scaleInCooldown: cdk.Duration.minutes(10), // Longer cooldown to prevent flapping + scaleOutCooldown: cdk.Duration.minutes(3), + }); + + // Scale based on memory utilization with higher threshold + scaling.scaleOnMemoryUtilization("MemoryScaling", { + targetUtilizationPercent: 85, // Higher threshold + scaleInCooldown: cdk.Duration.minutes(10), // Longer cooldown + scaleOutCooldown: cdk.Duration.minutes(3), + }); + + // CloudWatch Monitoring and Alarms + this.setupMonitoring( + service, + this.loadBalancer, + targetGroup, + environment, + applicationLogGroup, + accessLogGroup, + errorLogGroup + ); + + // Create separate phone service infrastructure + this.createPhoneService( + environment, + useDefaultVpc, + applicationLogGroup, + accessLogGroup, + errorLogGroup, + dailyApiKeySecret, + awsCredentialsSecret, + twilioCredentialsSecret, + ecsSecurityGroup + ); + + // Store references as class properties for use by other constructs + (this as any).targetGroup = targetGroup; + (this as any).ecsSecurityGroup = ecsSecurityGroup; + (this as any).logGroup = logGroup; + (this as any).applicationLogGroup = applicationLogGroup; + (this as any).accessLogGroup = accessLogGroup; + (this as any).errorLogGroup = errorLogGroup; + (this as any).dailyApiKeySecret = dailyApiKeySecret; + (this as any).taskDefinition = taskDefinition; + (this as any).service = service; + + // Outputs + new cdk.CfnOutput(this, "VpcId", { + value: this.vpc.vpcId, + description: "VPC ID for the Pipecat deployment", + }); + + new cdk.CfnOutput(this, "ClusterName", { + value: this.cluster.clusterName, + description: "ECS Cluster name", + }); + + new cdk.CfnOutput(this, "RepositoryUri", { + value: this.repository.repositoryUri, + description: "ECR Repository URI for Pipecat container images", + }); + + new cdk.CfnOutput(this, "LoadBalancerDnsName", { + value: this.loadBalancer.loadBalancerDnsName, + description: "DNS name of the Application Load Balancer", + }); + + new cdk.CfnOutput(this, "TaskRoleArn", { + value: this.taskRole.roleArn, + description: "ARN of the ECS task role", + }); + + new cdk.CfnOutput(this, "ExecutionRoleArn", { + value: this.executionRole.roleArn, + description: "ARN of the ECS execution role", + }); + + new cdk.CfnOutput(this, "TaskDefinitionArn", { + value: taskDefinition.taskDefinitionArn, + description: "ARN of the ECS task definition", + }); + + new cdk.CfnOutput(this, "ServiceName", { + value: service.serviceName, + description: "Name of the ECS service", + }); + + new cdk.CfnOutput(this, "ServiceArn", { + value: service.serviceArn, + description: "ARN of the ECS service", + }); + + // MOVED: Log Group Outputs to main constructor where variables are in scope + new cdk.CfnOutput(this, "ApplicationLogGroupName", { + value: applicationLogGroup.logGroupName, + description: "CloudWatch Log Group for application logs", + }); + + new cdk.CfnOutput(this, "AccessLogGroupName", { + value: accessLogGroup.logGroupName, + description: "CloudWatch Log Group for access logs", + }); + + new cdk.CfnOutput(this, "ErrorLogGroupName", { + value: errorLogGroup.logGroupName, + description: "CloudWatch Log Group for error logs", + }); + } + + private setupMonitoring( + service: ecs.FargateService, + loadBalancer: elbv2.ApplicationLoadBalancer, + targetGroup: elbv2.ApplicationTargetGroup, + environment: string, + applicationLogGroup: logs.LogGroup, + accessLogGroup: logs.LogGroup, + errorLogGroup: logs.LogGroup + ) { + // SNS Topic for alerts (optional - can be used for notifications) + const alertTopic = new sns.Topic(this, "PipecatAlerts", { + topicName: `pipecat-alerts-${environment}`, + displayName: `Pipecat Voice Agent Alerts - ${environment}`, + }); + + // ECS Service Metrics and Alarms + + // CPU Utilization Alarm + const cpuAlarm = new cloudwatch.Alarm(this, "HighCpuUtilization", { + alarmName: `pipecat-high-cpu-${environment}`, + alarmDescription: "ECS service CPU utilization is high", + metric: service.metricCpuUtilization({ + period: cdk.Duration.minutes(5), + statistic: cloudwatch.Stats.AVERAGE, + }), + threshold: 80, + evaluationPeriods: 2, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING, + }); + + // Memory Utilization Alarm + const memoryAlarm = new cloudwatch.Alarm(this, "HighMemoryUtilization", { + alarmName: `pipecat-high-memory-${environment}`, + alarmDescription: "ECS service memory utilization is high", + metric: service.metricMemoryUtilization({ + period: cdk.Duration.minutes(5), + statistic: cloudwatch.Stats.AVERAGE, + }), + threshold: 85, + evaluationPeriods: 2, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING, + }); + + // Task Count Alarm (for service availability) + const taskCountAlarm = new cloudwatch.Alarm(this, "LowTaskCount", { + alarmName: `pipecat-low-task-count-${environment}`, + alarmDescription: "ECS service has too few running tasks", + metric: new cloudwatch.Metric({ + namespace: "AWS/ECS", + metricName: "RunningTaskCount", + dimensionsMap: { + ServiceName: service.serviceName, + ClusterName: service.cluster.clusterName, + }, + period: cdk.Duration.minutes(1), + statistic: cloudwatch.Stats.AVERAGE, + }), + threshold: 1, + evaluationPeriods: 2, + comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD, + treatMissingData: cloudwatch.TreatMissingData.BREACHING, + }); + + // Application Load Balancer Metrics and Alarms + + // Target Response Time Alarm + const responseTimeAlarm = new cloudwatch.Alarm(this, "HighResponseTime", { + alarmName: `pipecat-high-response-time-${environment}`, + alarmDescription: "Application response time is high", + metric: targetGroup.metricTargetResponseTime({ + period: cdk.Duration.minutes(5), + statistic: cloudwatch.Stats.AVERAGE, + }), + threshold: 5, // 5 seconds + evaluationPeriods: 2, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING, + }); + + // HTTP 5xx Error Rate Alarm + const http5xxAlarm = new cloudwatch.Alarm(this, "High5xxErrorRate", { + alarmName: `pipecat-high-5xx-errors-${environment}`, + alarmDescription: "High rate of HTTP 5xx errors", + metric: loadBalancer.metricHttpCodeTarget( + elbv2.HttpCodeTarget.TARGET_5XX_COUNT, + { + period: cdk.Duration.minutes(5), + statistic: cloudwatch.Stats.SUM, + } + ), + threshold: 10, // 10 errors in 5 minutes + evaluationPeriods: 1, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING, + }); + + // Unhealthy Host Count Alarm + const unhealthyHostAlarm = new cloudwatch.Alarm(this, "UnhealthyHosts", { + alarmName: `pipecat-unhealthy-hosts-${environment}`, + alarmDescription: "Unhealthy targets detected", + metric: targetGroup.metricUnhealthyHostCount({ + period: cdk.Duration.minutes(1), + statistic: cloudwatch.Stats.AVERAGE, + }), + threshold: 0, + evaluationPeriods: 2, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING, + }); + + // Custom CloudWatch Dashboard + const dashboard = new cloudwatch.Dashboard(this, "PipecatDashboard", { + dashboardName: `pipecat-voice-agent-${environment}`, + widgets: [ + [ + // ECS Service Metrics + new cloudwatch.GraphWidget({ + title: "ECS Service - CPU & Memory Utilization", + left: [ + service.metricCpuUtilization({ + period: cdk.Duration.minutes(5), + label: "CPU Utilization (%)", + }), + ], + right: [ + service.metricMemoryUtilization({ + period: cdk.Duration.minutes(5), + label: "Memory Utilization (%)", + }), + ], + width: 12, + height: 6, + }), + ], + [ + // Task Count and Health + new cloudwatch.GraphWidget({ + title: "ECS Service - Task Count", + left: [ + new cloudwatch.Metric({ + namespace: "AWS/ECS", + metricName: "RunningTaskCount", + dimensionsMap: { + ServiceName: service.serviceName, + ClusterName: service.cluster.clusterName, + }, + period: cdk.Duration.minutes(1), + label: "Running Tasks", + }), + ], + width: 6, + height: 6, + }), + // ALB Response Time + new cloudwatch.GraphWidget({ + title: "Load Balancer - Response Time", + left: [ + targetGroup.metricTargetResponseTime({ + period: cdk.Duration.minutes(5), + label: "Response Time (seconds)", + }), + ], + width: 6, + height: 6, + }), + ], + [ + // HTTP Status Codes + new cloudwatch.GraphWidget({ + title: "Load Balancer - HTTP Status Codes", + left: [ + loadBalancer.metricHttpCodeTarget( + elbv2.HttpCodeTarget.TARGET_2XX_COUNT, + { + period: cdk.Duration.minutes(5), + label: "2xx Success", + } + ), + loadBalancer.metricHttpCodeTarget( + elbv2.HttpCodeTarget.TARGET_4XX_COUNT, + { + period: cdk.Duration.minutes(5), + label: "4xx Client Error", + } + ), + loadBalancer.metricHttpCodeTarget( + elbv2.HttpCodeTarget.TARGET_5XX_COUNT, + { + period: cdk.Duration.minutes(5), + label: "5xx Server Error", + } + ), + ], + width: 6, + height: 6, + }), + // Target Health + new cloudwatch.GraphWidget({ + title: "Target Group - Health Status", + left: [ + targetGroup.metricHealthyHostCount({ + period: cdk.Duration.minutes(1), + label: "Healthy Targets", + }), + targetGroup.metricUnhealthyHostCount({ + period: cdk.Duration.minutes(1), + label: "Unhealthy Targets", + }), + ], + width: 6, + height: 6, + }), + ], + ], + }); + + // Store references for potential use by other constructs + (this as any).alertTopic = alertTopic; + (this as any).dashboard = dashboard; + (this as any).alarms = { + cpu: cpuAlarm, + memory: memoryAlarm, + taskCount: taskCountAlarm, + responseTime: responseTimeAlarm, + http5xx: http5xxAlarm, + unhealthyHosts: unhealthyHostAlarm, + }; + + // Output dashboard URL + new cdk.CfnOutput(this, "DashboardUrl", { + value: `https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#dashboards:name=${dashboard.dashboardName}`, + description: "CloudWatch Dashboard URL for monitoring", + }); + + new cdk.CfnOutput(this, "AlertTopicArn", { + value: alertTopic.topicArn, + description: + "SNS Topic ARN for alerts (subscribe to receive notifications)", + }); + + // REMOVED: Log Group Outputs moved to main constructor + } + + private createPhoneService( + environment: string, + useDefaultVpc: boolean, + applicationLogGroup: logs.LogGroup, + accessLogGroup: logs.LogGroup, + errorLogGroup: logs.LogGroup, + dailyApiKeySecret: secretsmanager.ISecret, + awsCredentialsSecret: secretsmanager.ISecret, + twilioCredentialsSecret: secretsmanager.ISecret, + ecsSecurityGroup: ec2.SecurityGroup + ) { + // ECR Repository for phone service container images + // Use existing phone service ECR repository + const phoneRepository = ecr.Repository.fromRepositoryName( + this, + "PipecatPhoneRepository", + `pipecat-phone-service-${environment}` + ); + + // CloudWatch Log Group for phone service + const phoneLogGroup = new logs.LogGroup(this, "PipecatPhoneLogGroup", { + logGroupName: `/ecs/pipecat-phone-service-${environment}/application`, + retention: logs.RetentionDays.TWO_WEEKS, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // IAM Roles for phone service (same permissions as main service) + const phoneTaskRole = new iam.Role(this, "PipecatPhoneTaskRole", { + assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"), + description: "Role for Pipecat Phone ECS tasks", + inlinePolicies: { + BedrockAccess: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + "bedrock:InvokeModelWithBidirectionalStream", // Nova Sonic support + "bedrock:ListFoundationModels", // For Nova Sonic testing + ], + resources: [ + `arn:aws:bedrock:${this.region}::foundation-model/amazon.nova-sonic-v1:0`, + `arn:aws:bedrock:${this.region}::foundation-model/*`, // For broader access + ], + }), + ], + }), + SecretsManagerAccess: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ["secretsmanager:GetSecretValue"], + resources: [ + `arn:aws:secretsmanager:${this.region}:${this.account}:secret:pipecat/*`, + dailyApiKeySecret.secretArn, + awsCredentialsSecret.secretArn, + twilioCredentialsSecret.secretArn, + ], + }), + ], + }), + }, + }); + + const phoneExecutionRole = new iam.Role(this, "PipecatPhoneExecutionRole", { + assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"), + description: "Execution role for Pipecat Phone ECS tasks", + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName( + "service-role/AmazonECSTaskExecutionRolePolicy" + ), + ], + inlinePolicies: { + SecretsManagerAccess: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ["secretsmanager:GetSecretValue"], + resources: [ + `arn:aws:secretsmanager:${this.region}:${this.account}:secret:pipecat/*`, + dailyApiKeySecret.secretArn, + awsCredentialsSecret.secretArn, + twilioCredentialsSecret.secretArn, + ], + }), + ], + }), + }, + }); + + // ECS Task Definition for phone service with optimized resources + const phoneTaskDefinition = new ecs.FargateTaskDefinition( + this, + "PipecatPhoneTaskDefinition", + { + family: `pipecat-phone-service-${environment}`, + cpu: 2048, // 2 vCPU - same as main service for Nova Sonic + memoryLimitMiB: 4096, // 4 GB - same as main service + taskRole: phoneTaskRole, + executionRole: phoneExecutionRole, + } + ); + + // Container Definition for phone service + const phoneContainer = phoneTaskDefinition.addContainer( + "PipecatPhoneContainer", + { + image: ecs.ContainerImage.fromEcrRepository(phoneRepository, "latest"), + containerName: "pipecat-phone-container", + logging: ecs.LogDrivers.awsLogs({ + logGroup: phoneLogGroup, + streamPrefix: "ecs", + datetimeFormat: "%Y-%m-%d %H:%M:%S", + multilinePattern: "^\\d{4}-\\d{2}-\\d{2}", + }), + environment: { + AWS_REGION: this.region, + HOST: "0.0.0.0", + FAST_API_PORT: "7860", + ENVIRONMENT: environment, + LOG_LEVEL: "INFO", + GRACEFUL_SHUTDOWN_TIMEOUT: "30", + BOT_CLEANUP_INTERVAL: "300", + MEMORY_CLEANUP_THRESHOLD: "0.8", + HEALTH_CHECK_INTERVAL: "30", + HEALTH_CHECK_RETRIES: "5", + ENABLE_REQUEST_POOLING: "true", + MAX_REQUEST_POOL_SIZE: "100", + REQUEST_TIMEOUT: "30", + SERVICE_TYPE: "phone", // Distinguish from main service + FORCE_HTTPS: "true", // Force wss:// for Twilio WebSocket connections + }, + secrets: { + // Daily.co API credentials (for potential WebRTC fallback) + DAILY_API_KEY: ecs.Secret.fromSecretsManager( + dailyApiKeySecret, + "DAILY_API_KEY" + ), + DAILY_API_URL: ecs.Secret.fromSecretsManager( + dailyApiKeySecret, + "DAILY_API_URL" + ), + + // AWS credentials for Bedrock access + AWS_ACCESS_KEY_ID: ecs.Secret.fromSecretsManager( + awsCredentialsSecret, + "AWS_ACCESS_KEY_ID" + ), + AWS_SECRET_ACCESS_KEY: ecs.Secret.fromSecretsManager( + awsCredentialsSecret, + "AWS_SECRET_ACCESS_KEY" + ), + + // Twilio API credentials (primary for phone service) + TWILIO_ACCOUNT_SID: ecs.Secret.fromSecretsManager( + twilioCredentialsSecret, + "TWILIO_ACCOUNT_SID" + ), + TWILIO_AUTH_TOKEN: ecs.Secret.fromSecretsManager( + twilioCredentialsSecret, + "TWILIO_AUTH_TOKEN" + ), + TWILIO_PHONE_NUMBER: ecs.Secret.fromSecretsManager( + twilioCredentialsSecret, + "TWILIO_PHONE_NUMBER" + ), + TWILIO_API_SID: ecs.Secret.fromSecretsManager( + twilioCredentialsSecret, + "TWILIO_API_SID" + ), + TWILIO_API_SECRET: ecs.Secret.fromSecretsManager( + twilioCredentialsSecret, + "TWILIO_API_SECRET" + ), + }, + healthCheck: { + command: [ + "CMD-SHELL", + "curl -f http://localhost:7860/health || exit 1", + ], + interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(10), + startPeriod: cdk.Duration.seconds(120), // Longer startup period for Nova Sonic + retries: 5, + }, + } + ); + + // Port mapping for phone service + phoneContainer.addPortMappings({ + containerPort: 7860, + protocol: ecs.Protocol.TCP, + name: "http", + }); + + // Create separate ALB for phone service (Twilio needs public access) + const phoneAlbSecurityGroup = new ec2.SecurityGroup( + this, + "PhoneAlbSecurityGroup", + { + vpc: this.vpc, + description: "Security group for Pipecat Phone ALB (Twilio webhooks)", + allowAllOutbound: true, + } + ); + + // Allow HTTP traffic from anywhere (Twilio webhooks) + phoneAlbSecurityGroup.addIngressRule( + ec2.Peer.anyIpv4(), + ec2.Port.tcp(80), + "Allow HTTP traffic from Twilio webhooks" + ); + + // Allow HTTPS traffic from anywhere (optional, for production) + phoneAlbSecurityGroup.addIngressRule( + ec2.Peer.anyIpv4(), + ec2.Port.tcp(443), + "Allow HTTPS traffic from Twilio webhooks" + ); + + // Create separate security group for phone service ECS tasks + const phoneEcsSecurityGroup = new ec2.SecurityGroup( + this, + "PhoneEcsSecurityGroup", + { + vpc: this.vpc, + description: "Security group for Pipecat Phone ECS tasks", + allowAllOutbound: true, + } + ); + + phoneEcsSecurityGroup.addIngressRule( + phoneAlbSecurityGroup, + ec2.Port.tcp(7860), + "Allow traffic from Phone ALB to Phone ECS tasks" + ); + + // Application Load Balancer for phone service + const phoneLoadBalancer = new elbv2.ApplicationLoadBalancer( + this, + "PipecatPhoneLoadBalancer", + { + vpc: this.vpc, + internetFacing: true, + securityGroup: phoneAlbSecurityGroup, + loadBalancerName: `pipecat-phone-alb-${environment}`, + } + ); + + // Target Group for phone service + const phoneTargetGroup = new elbv2.ApplicationTargetGroup( + this, + "PipecatPhoneTargetGroup", + { + vpc: this.vpc, + port: 7860, + protocol: elbv2.ApplicationProtocol.HTTP, + targetType: elbv2.TargetType.IP, + healthCheck: { + enabled: true, + path: "/health", + protocol: elbv2.Protocol.HTTP, + port: "7860", + healthyHttpCodes: "200", + interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(10), + healthyThresholdCount: 2, + unhealthyThresholdCount: 5, + }, + // WebSocket support configuration + protocolVersion: elbv2.ApplicationProtocolVersion.HTTP1, + stickinessCookieDuration: cdk.Duration.seconds(86400), // 24 hours for WebSocket sessions + } + ); + + // Configure phone target group attributes for WebSocket support + const cfnPhoneTargetGroup = phoneTargetGroup.node + .defaultChild as elbv2.CfnTargetGroup; + cfnPhoneTargetGroup.addPropertyOverride("TargetGroupAttributes", [ + { + Key: "stickiness.enabled", + Value: "true", + }, + { + Key: "stickiness.type", + Value: "lb_cookie", + }, + { + Key: "stickiness.lb_cookie.duration_seconds", + Value: "86400", + }, + { + Key: "load_balancing.algorithm.type", + Value: "least_outstanding_requests", + }, + ]); + + // ALB Listeners for phone service (HTTP and HTTPS) + phoneLoadBalancer.addListener("PipecatPhoneListener", { + port: 80, + protocol: elbv2.ApplicationProtocol.HTTP, + defaultTargetGroups: [phoneTargetGroup], + }); + + // Add HTTPS listener (requires certificate) + // Uncomment and configure when you have a domain and certificate + /* + const certificate = acm.Certificate.fromCertificateArn( + this, + "PhoneServiceCertificate", + "arn:aws:acm:region:account:certificate/certificate-id" + ); + + phoneLoadBalancer.addListener("PipecatPhoneHttpsListener", { + port: 443, + protocol: elbv2.ApplicationProtocol.HTTPS, + certificates: [certificate], + defaultTargetGroups: [phoneTargetGroup], + }); + */ + + // ECS Service for phone service with appropriate scaling + const phoneService = new ecs.FargateService(this, "PipecatPhoneService", { + cluster: this.cluster, + taskDefinition: phoneTaskDefinition, + serviceName: `pipecat-phone-service-${environment}`, + desiredCount: 2, // Start with 2 tasks for phone service + minHealthyPercent: 50, // Allow 1 task to be down during deployments + maxHealthyPercent: 200, + healthCheckGracePeriod: cdk.Duration.seconds(300), // 5 minutes grace period + securityGroups: [phoneEcsSecurityGroup], // Use phone-specific security group + vpcSubnets: useDefaultVpc + ? { + subnetType: ec2.SubnetType.PUBLIC, + } + : { + subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, + }, + assignPublicIp: useDefaultVpc, + enableExecuteCommand: true, // For debugging + circuitBreaker: { rollback: true }, + }); + + // Attach phone service to phone target group + phoneService.attachToApplicationTargetGroup(phoneTargetGroup); + + // Auto Scaling Configuration for phone service + const phoneScaling = phoneService.autoScaleTaskCount({ + minCapacity: 1, // Minimum 1 task for phone service + maxCapacity: 4, // Max 4 tasks for phone service + }); + + // Scale based on CPU utilization + phoneScaling.scaleOnCpuUtilization("PhoneCpuScaling", { + targetUtilizationPercent: 75, + scaleInCooldown: cdk.Duration.minutes(10), + scaleOutCooldown: cdk.Duration.minutes(3), + }); + + // Scale based on memory utilization + phoneScaling.scaleOnMemoryUtilization("PhoneMemoryScaling", { + targetUtilizationPercent: 85, + scaleInCooldown: cdk.Duration.minutes(10), + scaleOutCooldown: cdk.Duration.minutes(3), + }); + + // Store phone service references + (this as any).phoneRepository = phoneRepository; + (this as any).phoneTaskDefinition = phoneTaskDefinition; + (this as any).phoneService = phoneService; + (this as any).phoneTaskRole = phoneTaskRole; + (this as any).phoneExecutionRole = phoneExecutionRole; + (this as any).phoneLogGroup = phoneLogGroup; + (this as any).phoneLoadBalancer = phoneLoadBalancer; + (this as any).phoneTargetGroup = phoneTargetGroup; + + // Outputs for phone service + new cdk.CfnOutput(this, "PhoneRepositoryUri", { + value: phoneRepository.repositoryUri, + description: + "ECR Repository URI for Pipecat phone service container images", + }); + + new cdk.CfnOutput(this, "PhoneTaskDefinitionArn", { + value: phoneTaskDefinition.taskDefinitionArn, + description: "ARN of the phone service ECS task definition", + }); + + new cdk.CfnOutput(this, "PhoneServiceName", { + value: phoneService.serviceName, + description: "Name of the phone service ECS service", + }); + + new cdk.CfnOutput(this, "PhoneServiceArn", { + value: phoneService.serviceArn, + description: "ARN of the phone service ECS service", + }); + + new cdk.CfnOutput(this, "PhoneTaskRoleArn", { + value: phoneTaskRole.roleArn, + description: "ARN of the phone service ECS task role", + }); + + new cdk.CfnOutput(this, "PhoneExecutionRoleArn", { + value: phoneExecutionRole.roleArn, + description: "ARN of the phone service ECS execution role", + }); + + new cdk.CfnOutput(this, "PhoneLogGroupName", { + value: phoneLogGroup.logGroupName, + description: "CloudWatch Log Group for phone service logs", + }); + + new cdk.CfnOutput(this, "PhoneLoadBalancerDnsName", { + value: phoneLoadBalancer.loadBalancerDnsName, + description: + "DNS name of the Phone Service Application Load Balancer (for Twilio webhooks)", + }); + + new cdk.CfnOutput(this, "TwilioWebhookUrl", { + value: `http://${phoneLoadBalancer.loadBalancerDnsName}/incoming-call`, + description: + "Twilio webhook URL - configure this in your Twilio phone number settings", + }); + } +} diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/package.json b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/package.json new file mode 100644 index 0000000..1987d85 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/package.json @@ -0,0 +1,42 @@ +{ + "name": "infrastructure", + "version": "0.1.0", + "bin": { + "infrastructure": "bin/infrastructure.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk", + "deploy": "./deploy.sh", + "deploy:test": "./deploy.sh --environment test", + "deploy:prod": "./deploy.sh --environment prod --custom-vpc", + "ecr:login": "./ecr-helper.sh --login", + "ecr:get-uri": "./ecr-helper.sh --get-uri", + "synth": "cdk synth", + "diff": "cdk diff", + "destroy": "cdk destroy" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.7.9", + "aws-cdk": "2.1023.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "@aws-cdk/aws-ec2": "^1.203.0", + "@aws-cdk/aws-ecr": "^1.203.0", + "@aws-cdk/aws-ecs": "^1.203.0", + "@aws-cdk/aws-ecs-patterns": "^1.203.0", + "@aws-cdk/aws-elasticloadbalancingv2": "^1.203.0", + "@aws-cdk/aws-iam": "^1.203.0", + "@aws-cdk/aws-logs": "^1.203.0", + "@aws-cdk/aws-secretsmanager": "^1.203.0", + "aws-cdk-lib": "^2.206.0", + "constructs": "^10.4.2" + } +} diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/test/infrastructure.test.ts b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/test/infrastructure.test.ts new file mode 100644 index 0000000..c6fe4b4 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/test/infrastructure.test.ts @@ -0,0 +1,240 @@ +import * as cdk from "aws-cdk-lib"; +import { Template, Match } from "aws-cdk-lib/assertions"; +import { InfrastructureStack } from "../lib/infrastructure-stack"; + +describe("Pipecat ECS Infrastructure Stack", () => { + let app: cdk.App; + let stack: InfrastructureStack; + let template: Template; + + beforeEach(() => { + app = new cdk.App(); + stack = new InfrastructureStack(app, "TestStack", { + environment: "test", + useDefaultVpc: true, + env: { account: "123456789012", region: "eu-north-1" }, + }); + template = Template.fromStack(stack); + }); + + test("Creates ECR Repository", () => { + template.hasResourceProperties("AWS::ECR::Repository", { + RepositoryName: "pipecat-voice-agent-test", + }); + }); + + test("Creates ECS Cluster", () => { + template.hasResourceProperties("AWS::ECS::Cluster", { + ClusterName: "pipecat-cluster-test", + }); + }); + + test("Creates Application Load Balancer", () => { + template.hasResourceProperties( + "AWS::ElasticLoadBalancingV2::LoadBalancer", + { + Name: "pipecat-alb-test", + Scheme: "internet-facing", + Type: "application", + } + ); + }); + + test("Creates Target Group with Health Check", () => { + template.hasResourceProperties("AWS::ElasticLoadBalancingV2::TargetGroup", { + Port: 7860, + Protocol: "HTTP", + TargetType: "ip", + HealthCheckPath: "/health", + HealthCheckPort: "7860", + }); + }); + + test("Creates IAM Task Role with Bedrock Permissions", () => { + // Check task role exists + template.hasResourceProperties("AWS::IAM::Role", { + Description: "Role for Pipecat ECS tasks", + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "ecs-tasks.amazonaws.com", + }, + }, + ], + }, + }); + + // Check execution role exists + template.hasResourceProperties("AWS::IAM::Role", { + Description: "Execution role for Pipecat ECS tasks", + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "ecs-tasks.amazonaws.com", + }, + }, + ], + }, + }); + }); + + test("Creates Security Groups with Proper Rules", () => { + // ALB Security Group + template.hasResourceProperties("AWS::EC2::SecurityGroup", { + GroupDescription: "Security group for Pipecat ALB", + SecurityGroupIngress: [ + { + CidrIp: "0.0.0.0/0", + FromPort: 80, + IpProtocol: "tcp", + ToPort: 80, + }, + { + CidrIp: "0.0.0.0/0", + FromPort: 443, + IpProtocol: "tcp", + ToPort: 443, + }, + ], + }); + + // ECS Security Group + template.hasResourceProperties("AWS::EC2::SecurityGroup", { + GroupDescription: "Security group for Pipecat ECS tasks", + }); + }); + + test("Creates CloudWatch Log Group", () => { + template.hasResourceProperties("AWS::Logs::LogGroup", { + LogGroupName: "/ecs/pipecat-voice-agent-test", + RetentionInDays: 7, + }); + }); + + test("Creates ECS Task Definition", () => { + template.hasResourceProperties("AWS::ECS::TaskDefinition", { + Family: "pipecat-voice-agent-test", + Cpu: "1024", + Memory: "2048", + NetworkMode: "awsvpc", + RequiresCompatibilities: ["FARGATE"], + ContainerDefinitions: [ + { + Name: "pipecat-container", + PortMappings: [ + { + ContainerPort: 7860, + Protocol: "tcp", + }, + ], + Environment: [ + { + Name: "AWS_REGION", + Value: "eu-north-1", + }, + { + Name: "HOST", + Value: "0.0.0.0", + }, + { + Name: "FAST_API_PORT", + Value: "7860", + }, + ], + HealthCheck: { + Command: [ + "CMD-SHELL", + "curl -f http://localhost:7860/health || exit 1", + ], + Interval: 30, + Timeout: 5, + StartPeriod: 60, + Retries: 3, + }, + }, + ], + }); + }); + + test("Creates ECS Service", () => { + template.hasResourceProperties("AWS::ECS::Service", { + ServiceName: "pipecat-service-test", + DesiredCount: 2, + LaunchType: "FARGATE", + DeploymentConfiguration: { + MaximumPercent: 200, + MinimumHealthyPercent: 50, + }, + HealthCheckGracePeriodSeconds: 60, + EnableExecuteCommand: true, + }); + }); + + test("Creates Auto Scaling Target", () => { + template.hasResourceProperties( + "AWS::ApplicationAutoScaling::ScalableTarget", + { + MaxCapacity: 4, + MinCapacity: 1, + ResourceId: Match.anyValue(), // ResourceId is a CloudFormation function + RoleARN: Match.anyValue(), + ScalableDimension: "ecs:service:DesiredCount", + ServiceNamespace: "ecs", + } + ); + }); + + test("Creates CPU Scaling Policy", () => { + template.hasResourceProperties( + "AWS::ApplicationAutoScaling::ScalingPolicy", + { + PolicyName: Match.stringLikeRegexp(".*CpuScaling.*"), + PolicyType: "TargetTrackingScaling", + TargetTrackingScalingPolicyConfiguration: { + TargetValue: 70, + PredefinedMetricSpecification: { + PredefinedMetricType: "ECSServiceAverageCPUUtilization", + }, + ScaleInCooldown: 300, + ScaleOutCooldown: 120, + }, + } + ); + }); + + test("Creates Memory Scaling Policy", () => { + template.hasResourceProperties( + "AWS::ApplicationAutoScaling::ScalingPolicy", + { + PolicyName: Match.stringLikeRegexp(".*MemoryScaling.*"), + PolicyType: "TargetTrackingScaling", + TargetTrackingScalingPolicyConfiguration: { + TargetValue: 80, + PredefinedMetricSpecification: { + PredefinedMetricType: "ECSServiceAverageMemoryUtilization", + }, + ScaleInCooldown: 300, + ScaleOutCooldown: 120, + }, + } + ); + }); + + test("Has Required Outputs", () => { + template.hasOutput("VpcId", {}); + template.hasOutput("ClusterName", {}); + template.hasOutput("RepositoryUri", {}); + template.hasOutput("LoadBalancerDnsName", {}); + template.hasOutput("TaskRoleArn", {}); + template.hasOutput("ExecutionRoleArn", {}); + template.hasOutput("TaskDefinitionArn", {}); + template.hasOutput("ServiceName", {}); + template.hasOutput("ServiceArn", {}); + }); +}); diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/tsconfig.json b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/tsconfig.json new file mode 100644 index 0000000..28bb557 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/infrastructure/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "es2022" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/pipecat-phone-service.yaml b/speech-to-speech/sample-codes/pipecat-voice-agent/pipecat-phone-service.yaml new file mode 100644 index 0000000..b1c6cda --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/pipecat-phone-service.yaml @@ -0,0 +1,94 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pipecat-phone-service + namespace: nova-sonic +spec: + replicas: 1 + selector: + matchLabels: + app: pipecat-phone-service + template: + metadata: + labels: + app: pipecat-phone-service + spec: + serviceAccountName: nova-sonic-sa + containers: + - name: server + image: 805372491860.dkr.ecr.us-east-1.amazonaws.com/pipecat-phone-service:5 + imagePullPolicy: Always + ports: + - name: ws + containerPort: 7860 + protocol: TCP + env: + - name: AWS_ACCESS_KEY_ID + value: "" + - name: AWS_SECRET_ACCESS_KEY + value: "" + - name: AWS_REGION + value: "eu-north-1" + - name: HOST + value: "0.0.0.0" + - name: FAST_API_PORT + value: "7860" + - name: TWILIO_RECOVERY_CODE + value: "" + - name: TWILIO_ACCOUNT_SID + value: "" + - name: TWILIO_AUTH_TOKEN + value: "" + - name: TWILIO_SID + value: "" + - name: TWILIO_SECRET + value: "" + - name: TWILIO_AUTH_LIVE + value: "" + - name: TWILIO_PHONE_NUMBER + value: "+" + - name: DAILY_API_KEY + value: "" + resources: + requests: + cpu: 1 + memory: 128Mi + limits: + cpu: 1 + memory: 512Mi + readinessProbe: + tcpSocket: + port: 7860 + periodSeconds: 5 + livenessProbe: + tcpSocket: + port: 7860 + periodSeconds: 10 + terminationGracePeriodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: pipecat-phone-service + namespace: nova-sonic + annotations: + service.beta.kubernetes.io/aws-load-balancer-type: "nlb" + service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing" + service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip" + service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "600" + service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true" + # TLS listener + cert (ACM) + service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "arn:aws:acm:us-east-1:account number:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443" + # (optional) pick a stricter policy; defaults are fine if you omit + # service.beta.kubernetes.io/aws-load-balancer-ssl-negotiation-policy: "ELBSecurityPolicy-TLS13-1-2-2021-06" +spec: + type: LoadBalancer + selector: + app: pipecat-phone-service + ports: + - name: https + port: 443 # NLB listener (TLS) + targetPort: 7860 # Pod port (plaintext HTTP/WS) + protocol: TCP + externalTrafficPolicy: Cluster \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/requirements.txt b/speech-to-speech/sample-codes/pipecat-voice-agent/requirements.txt new file mode 100644 index 0000000..8c42a83 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/requirements.txt @@ -0,0 +1,12 @@ +python-dotenv +fastapi[all] +uvicorn +websockets==13.1 +pipecat-ai[daily,aws-nova-sonic,silero]==0.0.79 +loguru +boto3 +botocore +aioboto3 +psutil +twilio +certifi \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/README.md b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/README.md new file mode 100644 index 0000000..8995bb7 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/README.md @@ -0,0 +1,351 @@ +# Deployment Scripts + +This directory contains scripts for building, pushing, and deploying the Pipecat voice AI agent to AWS ECS. + +## Scripts Overview + +### 1. `build-and-push.sh` + +Builds the Docker image and pushes it to Amazon ECR. + +**Usage:** + +```bash +./build-and-push.sh [OPTIONS] +``` + +**Options:** + +- `-e, --environment ENV`: Set environment (default: test) +- `-r, --region REGION`: Set AWS region (default: eu-north-1) +- `-t, --tag TAG`: Set image tag (default: latest) +- `-f, --dockerfile PATH`: Set Dockerfile path (default: .) +- `--force`: Force rebuild even if image exists +- `-h, --help`: Show help message + +**Examples:** + +```bash +# Build with defaults +./build-and-push.sh + +# Build for production with specific tag +./build-and-push.sh -e prod -t v1.2.3 + +# Force rebuild +./build-and-push.sh --force +``` + +### 2. `deploy-service.sh` + +Creates or updates the ECS service with a new container image. + +**Usage:** + +```bash +./deploy-service.sh [OPTIONS] +``` + +**Options:** + +- `-e, --environment ENV`: Set environment (default: test) +- `-r, --region REGION`: Set AWS region (default: eu-north-1) +- `-t, --tag TAG`: Set image tag (default: latest) +- `--update`: Update existing service (default action) +- `--create`: Create new service +- `--scale COUNT`: Scale service to specified count +- `--no-wait`: Don't wait for deployment to complete +- `-h, --help`: Show help message + +**Examples:** + +```bash +# Update service with latest image +./deploy-service.sh + +# Update with specific tag +./deploy-service.sh -t v1.2.3 + +# Scale to 4 tasks +./deploy-service.sh --scale 4 +``` + +### 3. `../deploy.sh` (Main Deployment Script) + +Orchestrates the complete deployment process by running both build and deploy scripts. + +**Usage:** + +```bash +./deploy.sh [OPTIONS] +``` + +**Options:** + +- `-e, --environment ENV`: Set environment (default: test) +- `-r, --region REGION`: Set AWS region (default: eu-north-1) +- `-t, --tag TAG`: Set image tag (default: latest) +- `--skip-build`: Skip Docker build and push +- `--skip-deploy`: Skip ECS service deployment +- `--force-build`: Force rebuild even if image exists +- `--build-only`: Only build and push image +- `--deploy-only`: Only deploy to ECS (skip build) +- `-h, --help`: Show help message + +**Examples:** + +```bash +# Full deployment with defaults +./deploy.sh + +# Deploy to production with specific tag +./deploy.sh -e prod -t v1.2.3 + +# Only build and push image +./deploy.sh --build-only + +# Only deploy existing image +./deploy.sh --deploy-only -t v1.2.3 +``` + +## Prerequisites + +Before using these scripts, ensure you have: + +1. **AWS CLI configured** with appropriate credentials: + + ```bash + aws configure + ``` + +2. **Docker installed** and running: + + ```bash + docker --version + ``` + +3. **Infrastructure deployed** using CDK: + + ```bash + cd infrastructure + ./deploy.sh + ``` + +4. **Secrets configured** in AWS Secrets Manager: + ```bash + python3 setup-secrets.py + ``` + +## Deployment Workflow + +### Standard Deployment Process + +1. **Deploy Infrastructure** (one-time setup): + + ```bash + cd infrastructure + ./deploy.sh + ``` + +2. **Set up Secrets** (one-time setup): + + ```bash + python3 setup-secrets.py + ``` + +3. **Deploy Application**: + ```bash + ./deploy.sh + ``` + +### Development Workflow + +For development and testing: + +```bash +# Build and test locally first +docker build -t pipecat-test . +docker run -p 7860:7860 --env-file .env pipecat-test + +# Deploy to test environment +./deploy.sh -e test + +# Deploy to staging +./deploy.sh -e staging -t staging-$(date +%Y%m%d-%H%M%S) + +# Deploy to production +./deploy.sh -e prod -t v1.0.0 +``` + +### CI/CD Integration + +The scripts are designed to work with the GitHub Actions workflow in `.github/workflows/deploy.yml`. The workflow automatically: + +- Runs tests on pull requests +- Builds and pushes images on main/develop branches +- Deploys to appropriate environments based on branch +- Provides deployment status and URLs + +## Environment Configuration + +### Supported Environments + +- **test**: Development and testing environment +- **staging**: Pre-production environment +- **prod**: Production environment + +### Environment-Specific Resources + +Each environment creates separate AWS resources: + +- ECS Cluster: `pipecat-cluster-{environment}` +- ECR Repository: `pipecat-voice-agent-{environment}` +- Load Balancer: `pipecat-alb-{environment}` +- CloudWatch Logs: `/ecs/pipecat-voice-agent-{environment}` + +## Monitoring and Troubleshooting + +### Useful Commands + +**Check service status:** + +```bash +aws ecs describe-services --cluster pipecat-cluster-test --services pipecat-service-test --region eu-north-1 +``` + +**View service logs:** + +```bash +aws logs tail /ecs/pipecat-voice-agent-test --follow --region eu-north-1 +``` + +**List running tasks:** + +```bash +aws ecs list-tasks --cluster pipecat-cluster-test --service-name pipecat-service-test --region eu-north-1 +``` + +**Get load balancer URL:** + +```bash +aws cloudformation describe-stacks --stack-name PipecatEcsStack-test --region eu-north-1 --query 'Stacks[0].Outputs[?OutputKey==`LoadBalancerDnsName`].OutputValue' --output text +``` + +### Common Issues + +1. **ECR Repository Not Found** + + - Ensure infrastructure is deployed: `cd infrastructure && ./deploy.sh` + +2. **Docker Build Fails** + + - Check Dockerfile syntax and dependencies + - Ensure Docker daemon is running + +3. **ECS Service Update Fails** + + - Check task definition is valid + - Verify IAM permissions + - Check CloudWatch logs for container errors + +4. **Health Check Failures** + - Ensure `/health` endpoint is implemented in the application + - Check security group allows traffic on port 7860 + - Verify container starts successfully + +### Enhanced Monitoring and Logging + +The deployment includes comprehensive monitoring and logging capabilities: + +#### CloudWatch Log Groups + +- `/ecs/pipecat-voice-agent-{environment}/application` - Structured application logs +- `/ecs/pipecat-voice-agent-{environment}/access` - HTTP access logs +- `/ecs/pipecat-voice-agent-{environment}/error` - Error-specific logs + +#### Monitoring Tools + +**Log Analysis Script:** + +```bash +# View recent errors +./log-analysis.sh errors 2 + +# Monitor health checks +./log-analysis.sh health 1 + +# Real-time log monitoring +./log-analysis.sh monitor + +# Export logs for analysis +./log-analysis.sh export 24 +``` + +**Continuous Health Monitoring:** + +```bash +# Run continuous monitoring +python ../monitoring.py --url http://your-alb-dns-name --interval 30 + +# Single health check +python ../monitoring.py --url http://your-alb-dns-name --single-check +``` + +#### CloudWatch Insights Queries + +Pre-built queries are available in `../cloudwatch-queries.md`: + +```bash +# Run custom query +./log-analysis.sh query 'fields @timestamp, message | filter level = "ERROR"' 1 +``` + +#### CloudWatch Dashboard + +Access the monitoring dashboard: + +- Go to CloudWatch Console +- Navigate to Dashboards +- Select `pipecat-voice-agent-{environment}` + +#### Alarms and Metrics + +The deployment automatically creates alarms for: + +- High CPU/Memory utilization +- Low task count +- High response times +- HTTP 5xx errors +- Unhealthy targets + +### Log Analysis + +**Application logs:** + +```bash +aws logs filter-log-events --log-group-name /ecs/pipecat-voice-agent-test/application --region eu-north-1 +``` + +**ECS service events:** + +```bash +aws ecs describe-services --cluster pipecat-cluster-test --services pipecat-service-test --region eu-north-1 --query 'services[0].events[0:10]' +``` + +## Security Considerations + +- All secrets are stored in AWS Secrets Manager +- Container images are scanned for vulnerabilities in ECR +- IAM roles follow principle of least privilege +- Network access is restricted by security groups +- All API calls are logged in CloudTrail + +## Cost Optimization + +- Use appropriate instance sizes for your workload +- Consider using Spot instances for non-production environments +- Set up auto-scaling to handle variable load +- Monitor CloudWatch metrics to optimize resource allocation +- Clean up unused images in ECR regularly + +For more detailed information, see the main [DEPLOYMENT_GUIDE.md](../infrastructure/DEPLOYMENT_GUIDE.md). diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/build-and-push.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/build-and-push.sh new file mode 100755 index 0000000..136cde1 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/build-and-push.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# Build and push script for Pipecat Voice Agent container + +set -e + +# Default values +ENVIRONMENT="test" +AWS_REGION="eu-north-1" +IMAGE_TAG="latest" +FORCE_BUILD=false + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -e|--environment) + ENVIRONMENT="$2" + shift 2 + ;; + -r|--region) + AWS_REGION="$2" + shift 2 + ;; + -t|--tag) + IMAGE_TAG="$2" + shift 2 + ;; + --force) + FORCE_BUILD=true + shift + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "Options:" + echo " -e, --environment ENV Set environment (default: test)" + echo " -r, --region REGION Set AWS region (default: eu-north-1)" + echo " -t, --tag TAG Set image tag (default: latest)" + echo " --force Force rebuild" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + echo "Unknown option $1" + exit 1 + ;; + esac +done + +REPO_URI="094271239310.dkr.ecr.$AWS_REGION.amazonaws.com/pipecat-voice-agent-$ENVIRONMENT" + +echo "๐Ÿณ Building and pushing Pipecat Voice Agent container" +echo "Repository: $REPO_URI" +echo "Tag: $IMAGE_TAG" +echo "Environment: $ENVIRONMENT" +echo "Region: $AWS_REGION" + +# Check if Docker is running +if ! docker info > /dev/null 2>&1; then + echo "โŒ Docker is not running. Please start Docker and try again." + exit 1 +fi + +# Login to ECR +echo "๐Ÿ” Logging in to ECR..." +aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REPO_URI + +# Build the image for linux/amd64 platform (required for ECS Fargate) +echo "๐Ÿ”จ Building Docker image for linux/amd64..." +docker build --platform linux/amd64 -t pipecat-voice-agent:$IMAGE_TAG . + +# Tag for ECR +echo "๐Ÿท๏ธ Tagging for ECR..." +docker tag pipecat-voice-agent:$IMAGE_TAG $REPO_URI:$IMAGE_TAG + +# Push to ECR +echo "๐Ÿ“ค Pushing to ECR..." +docker push $REPO_URI:$IMAGE_TAG + +echo "โœ… Build and push complete!" +echo "Image: $REPO_URI:$IMAGE_TAG" +echo "" +echo "Next: Update your ECS service to use this image" \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/build-phone-service.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/build-phone-service.sh new file mode 100755 index 0000000..6fcee27 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/build-phone-service.sh @@ -0,0 +1,176 @@ +#!/bin/bash + +# Build and Push Script for Pipecat Phone Service +# This script builds the phone service container and pushes it to ECR + +set -e + +# Default values +ENVIRONMENT="test" +AWS_REGION="eu-north-1" +IMAGE_TAG="latest" +FORCE_BUILD="false" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_header() { + echo -e "${BLUE}[BUILD-PHONE]${NC} $1" +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -e|--environment) + ENVIRONMENT="$2" + shift 2 + ;; + -r|--region) + AWS_REGION="$2" + shift 2 + ;; + -t|--tag) + IMAGE_TAG="$2" + shift 2 + ;; + --force) + FORCE_BUILD="true" + shift + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "This script builds and pushes the Pipecat Phone Service Docker image:" + echo "1. Build phone service Docker image using Dockerfile.phone" + echo "2. Tag and push to ECR repository" + echo "" + echo "Options:" + echo " -e, --environment ENV Set environment (default: test)" + echo " -r, --region REGION Set AWS region (default: eu-north-1)" + echo " -t, --tag TAG Set image tag (default: latest)" + echo " --force Force rebuild even if image exists" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " $0 # Build with defaults" + echo " $0 -e prod -t v1.2.3 # Build for prod with specific tag" + echo " $0 --force # Force rebuild" + exit 0 + ;; + *) + echo "Unknown option $1" + exit 1 + ;; + esac +done + +print_header "Building Pipecat Phone Service" +print_status "Environment: $ENVIRONMENT" +print_status "AWS Region: $AWS_REGION" +print_status "Image Tag: $IMAGE_TAG" +print_status "Force Build: $FORCE_BUILD" + +# Validate prerequisites +print_status "Validating prerequisites..." + +# Check if we're in the right directory +if [ ! -f "docker/Dockerfile.phone" ] || [ ! -f "server_clean.py" ]; then + print_error "This script must be run from the pipecat-ecs-deployment directory" + print_error "Current directory: $(pwd)" + exit 1 +fi + +# Check if infrastructure is deployed +STACK_NAME="PipecatEcsStack-$ENVIRONMENT" +if ! aws cloudformation describe-stacks --stack-name $STACK_NAME --region $AWS_REGION > /dev/null 2>&1; then + print_error "Infrastructure stack '$STACK_NAME' not found" + print_error "Please deploy the infrastructure first" + exit 1 +fi + +# Get ECR repository URI +PHONE_REPO_URI=$(aws cloudformation describe-stacks \ + --stack-name $STACK_NAME \ + --region $AWS_REGION \ + --query 'Stacks[0].Outputs[?OutputKey==`PhoneRepositoryUri`].OutputValue' \ + --output text 2>/dev/null || echo "") + +if [ -z "$PHONE_REPO_URI" ]; then + print_error "Phone service repository not found in infrastructure" + print_error "Please redeploy infrastructure with phone service support" + exit 1 +fi + +print_status "Phone Repository URI: $PHONE_REPO_URI" +print_status "Prerequisites validated successfully!" + +# Login to ECR +print_status "Logging in to ECR..." +aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $PHONE_REPO_URI + +# Check if image already exists +if [ "$FORCE_BUILD" = "false" ]; then + if aws ecr describe-images --repository-name $(basename $PHONE_REPO_URI) --image-ids imageTag=$IMAGE_TAG --region $AWS_REGION > /dev/null 2>&1; then + print_warning "Image with tag '$IMAGE_TAG' already exists in ECR" + print_status "Use --force to rebuild anyway" + exit 0 + fi +fi + +# Build the phone service image +print_header "Building phone service Docker image" +print_status "Using Dockerfile.phone..." + +docker build -f docker/Dockerfile.phone -t pipecat-phone-service:$IMAGE_TAG . + +if [ $? -ne 0 ]; then + print_error "Docker build failed" + exit 1 +fi + +print_status "Docker build completed successfully!" + +# Tag for ECR +print_status "Tagging image for ECR..." +docker tag pipecat-phone-service:$IMAGE_TAG $PHONE_REPO_URI:$IMAGE_TAG + +# Push to ECR +print_header "Pushing image to ECR" +print_status "Pushing to: $PHONE_REPO_URI:$IMAGE_TAG" + +docker push $PHONE_REPO_URI:$IMAGE_TAG + +if [ $? -ne 0 ]; then + print_error "Docker push failed" + exit 1 +fi + +print_status "Docker push completed successfully!" + +# Final status +print_header "Build Complete!" +print_status "โœ… Phone service image built and pushed successfully" +print_status "๐Ÿ“ฆ Image: $PHONE_REPO_URI:$IMAGE_TAG" +print_status "" +print_status "๐Ÿ“‹ Next steps:" +print_status " 1. Deploy the service: ./scripts/deployment/deploy-phone-service.sh --deploy-only -t $IMAGE_TAG" +print_status " 2. Or run full deployment: ./scripts/deployment/deploy-phone-service.sh" + +print_header "Build script completed!" \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deploy-service.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deploy-service.sh new file mode 100755 index 0000000..0ba85ab --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deploy-service.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Deploy service script for updating ECS service + +set -e + +# Default values +ENVIRONMENT="test" +AWS_REGION="eu-north-1" +IMAGE_TAG="latest" +UPDATE_SERVICE=false + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -e|--environment) + ENVIRONMENT="$2" + shift 2 + ;; + -r|--region) + AWS_REGION="$2" + shift 2 + ;; + -t|--tag) + IMAGE_TAG="$2" + shift 2 + ;; + --update) + UPDATE_SERVICE=true + shift + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "Options:" + echo " -e, --environment ENV Set environment (default: test)" + echo " -r, --region REGION Set AWS region (default: eu-north-1)" + echo " -t, --tag TAG Set image tag (default: latest)" + echo " --update Update ECS service" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + echo "Unknown option $1" + exit 1 + ;; + esac +done + +CLUSTER_NAME="pipecat-cluster-$ENVIRONMENT" +SERVICE_NAME="pipecat-service-$ENVIRONMENT" +REPO_URI="094271239310.dkr.ecr.$AWS_REGION.amazonaws.com/pipecat-voice-agent-$ENVIRONMENT" + +echo "๐Ÿš€ Deploying ECS service update" +echo "Cluster: $CLUSTER_NAME" +echo "Service: $SERVICE_NAME" +echo "Image: $REPO_URI:$IMAGE_TAG" +echo "Environment: $ENVIRONMENT" +echo "Region: $AWS_REGION" + +if [ "$UPDATE_SERVICE" = true ]; then + echo "๐Ÿ”„ Forcing service update..." + aws ecs update-service \ + --cluster $CLUSTER_NAME \ + --service $SERVICE_NAME \ + --force-new-deployment \ + --region $AWS_REGION + + echo "โœ… Service update initiated!" + echo "" + echo "Monitor deployment status:" + echo " aws ecs describe-services --cluster $CLUSTER_NAME --services $SERVICE_NAME --region $AWS_REGION" + echo "" + echo "View service logs:" + echo " aws logs tail /ecs/pipecat-voice-agent-$ENVIRONMENT/application --follow --region $AWS_REGION" +else + echo "โ„น๏ธ Use --update flag to actually update the service" +fi \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deployment/deploy-phone-service.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deployment/deploy-phone-service.sh new file mode 100755 index 0000000..81345d3 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deployment/deploy-phone-service.sh @@ -0,0 +1,340 @@ +#!/bin/bash + +# Phone Service Deployment Script for Pipecat ECS +# This script deploys the phone service with Twilio integration + +set -e + +# Default values +ENVIRONMENT="test" +AWS_REGION="eu-north-1" +IMAGE_TAG="latest" +SKIP_BUILD="false" +SKIP_DEPLOY="false" +FORCE_BUILD="false" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_header() { + echo -e "${BLUE}[PHONE-DEPLOY]${NC} $1" +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -e|--environment) + ENVIRONMENT="$2" + shift 2 + ;; + -r|--region) + AWS_REGION="$2" + shift 2 + ;; + -t|--tag) + IMAGE_TAG="$2" + shift 2 + ;; + --skip-build) + SKIP_BUILD="true" + shift + ;; + --skip-deploy) + SKIP_DEPLOY="true" + shift + ;; + --force-build) + FORCE_BUILD="true" + shift + ;; + --build-only) + SKIP_DEPLOY="true" + shift + ;; + --deploy-only) + SKIP_BUILD="true" + shift + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "This script deploys the Pipecat Phone Service with Twilio integration:" + echo "1. Build and push phone service Docker image to ECR" + echo "2. Update phone service ECS service with new image" + echo "3. Display Twilio webhook URL for configuration" + echo "" + echo "Options:" + echo " -e, --environment ENV Set environment (default: test)" + echo " -r, --region REGION Set AWS region (default: eu-north-1)" + echo " -t, --tag TAG Set image tag (default: latest)" + echo " --skip-build Skip Docker build and push" + echo " --skip-deploy Skip ECS service deployment" + echo " --force-build Force rebuild even if image exists" + echo " --build-only Only build and push image" + echo " --deploy-only Only deploy to ECS (skip build)" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " $0 # Full phone service deployment" + echo " $0 -e prod -t v1.2.3 # Deploy to prod with specific tag" + echo " $0 --build-only # Only build and push phone image" + echo " $0 --deploy-only -t v1.2.3 # Only deploy existing phone image" + echo "" + echo "Prerequisites:" + echo " - AWS CLI configured with appropriate credentials" + echo " - Docker installed and running" + echo " - Infrastructure deployed with phone service support" + echo " - Twilio credentials configured in AWS Secrets Manager" + exit 0 + ;; + *) + echo "Unknown option $1" + exit 1 + ;; + esac +done + +print_header "Starting Pipecat Phone Service Deployment" +print_status "Environment: $ENVIRONMENT" +print_status "AWS Region: $AWS_REGION" +print_status "Image Tag: $IMAGE_TAG" +print_status "Skip Build: $SKIP_BUILD" +print_status "Skip Deploy: $SKIP_DEPLOY" + +# Validate prerequisites +print_status "Validating prerequisites..." + +# Check if we're in the right directory +if [ ! -f "docker/Dockerfile.phone" ] || [ ! -f "server_clean.py" ]; then + print_error "This script must be run from the pipecat-ecs-deployment directory" + print_error "Current directory: $(pwd)" + exit 1 +fi + +# Check if infrastructure is deployed +STACK_NAME="PipecatEcsStack-$ENVIRONMENT" +if ! aws cloudformation describe-stacks --stack-name $STACK_NAME --region $AWS_REGION > /dev/null 2>&1; then + print_error "Infrastructure stack '$STACK_NAME' not found" + print_error "Please deploy the infrastructure first with phone service support" + exit 1 +fi + +# Check if phone service exists in the stack +PHONE_REPO_URI=$(aws cloudformation describe-stacks \ + --stack-name $STACK_NAME \ + --region $AWS_REGION \ + --query 'Stacks[0].Outputs[?OutputKey==`PhoneRepositoryUri`].OutputValue' \ + --output text 2>/dev/null || echo "") + +if [ -z "$PHONE_REPO_URI" ]; then + print_error "Phone service repository not found in infrastructure" + print_error "Please redeploy infrastructure with phone service support" + exit 1 +fi + +print_status "Prerequisites validated successfully!" + +# Step 1: Build and push phone service Docker image +if [ "$SKIP_BUILD" = "false" ]; then + print_header "Step 1: Building and pushing phone service Docker image" + + # Get ECR repository URI + PHONE_REPO_URI=$(aws cloudformation describe-stacks \ + --stack-name $STACK_NAME \ + --region $AWS_REGION \ + --query 'Stacks[0].Outputs[?OutputKey==`PhoneRepositoryUri`].OutputValue' \ + --output text) + + if [ -z "$PHONE_REPO_URI" ]; then + print_error "Failed to get phone repository URI from CloudFormation" + exit 1 + fi + + print_status "Phone Repository URI: $PHONE_REPO_URI" + + # Login to ECR + print_status "Logging in to ECR..." + aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $PHONE_REPO_URI + + # Build the phone service image + print_status "Building phone service Docker image..." + docker build -f docker/Dockerfile.phone -t pipecat-phone-service:$IMAGE_TAG . + + # Tag for ECR + docker tag pipecat-phone-service:$IMAGE_TAG $PHONE_REPO_URI:$IMAGE_TAG + + # Check if image already exists (unless force build) + if [ "$FORCE_BUILD" = "false" ]; then + if aws ecr describe-images --repository-name $(basename $PHONE_REPO_URI) --image-ids imageTag=$IMAGE_TAG --region $AWS_REGION > /dev/null 2>&1; then + print_warning "Image with tag '$IMAGE_TAG' already exists in ECR" + print_status "Use --force-build to rebuild anyway" + fi + fi + + # Push to ECR + print_status "Pushing phone service image to ECR..." + docker push $PHONE_REPO_URI:$IMAGE_TAG + + print_status "Phone service build and push completed successfully!" +else + print_warning "Skipping build step as requested" +fi + +# Step 2: Deploy phone service to ECS +if [ "$SKIP_DEPLOY" = "false" ]; then + print_header "Step 2: Deploying phone service to ECS" + + # Get service details + PHONE_SERVICE_NAME=$(aws cloudformation describe-stacks \ + --stack-name $STACK_NAME \ + --region $AWS_REGION \ + --query 'Stacks[0].Outputs[?OutputKey==`PhoneServiceName`].OutputValue' \ + --output text) + + CLUSTER_NAME=$(aws cloudformation describe-stacks \ + --stack-name $STACK_NAME \ + --region $AWS_REGION \ + --query 'Stacks[0].Outputs[?OutputKey==`ClusterName`].OutputValue' \ + --output text) + + if [ -z "$PHONE_SERVICE_NAME" ] || [ -z "$CLUSTER_NAME" ]; then + print_error "Failed to get phone service or cluster name from CloudFormation" + exit 1 + fi + + print_status "Phone Service: $PHONE_SERVICE_NAME" + print_status "Cluster: $CLUSTER_NAME" + + # Update the service to use the new image + print_status "Updating phone service with new image..." + + # Get current task definition + CURRENT_TASK_DEF=$(aws ecs describe-services \ + --cluster $CLUSTER_NAME \ + --services $PHONE_SERVICE_NAME \ + --region $AWS_REGION \ + --query 'services[0].taskDefinition' \ + --output text) + + if [ -z "$CURRENT_TASK_DEF" ]; then + print_error "Failed to get current task definition" + exit 1 + fi + + # Get the task definition family name + TASK_DEF_FAMILY=$(echo $CURRENT_TASK_DEF | cut -d':' -f6 | cut -d'/' -f2) + + # Create new task definition revision with updated image + print_status "Creating new task definition revision..." + + # Get current task definition JSON + TASK_DEF_JSON=$(aws ecs describe-task-definition \ + --task-definition $CURRENT_TASK_DEF \ + --region $AWS_REGION \ + --query 'taskDefinition') + + # Update the image in the task definition + NEW_TASK_DEF=$(echo $TASK_DEF_JSON | jq --arg image "$PHONE_REPO_URI:$IMAGE_TAG" \ + '.containerDefinitions[0].image = $image | del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .placementConstraints, .compatibilities, .registeredAt, .registeredBy)') + + # Register new task definition + NEW_TASK_DEF_ARN=$(echo $NEW_TASK_DEF | aws ecs register-task-definition \ + --region $AWS_REGION \ + --cli-input-json file:///dev/stdin \ + --query 'taskDefinition.taskDefinitionArn' \ + --output text) + + if [ -z "$NEW_TASK_DEF_ARN" ]; then + print_error "Failed to register new task definition" + exit 1 + fi + + print_status "New task definition: $NEW_TASK_DEF_ARN" + + # Update the service + print_status "Updating phone service..." + aws ecs update-service \ + --cluster $CLUSTER_NAME \ + --service $PHONE_SERVICE_NAME \ + --task-definition $NEW_TASK_DEF_ARN \ + --region $AWS_REGION > /dev/null + + # Wait for deployment to complete + print_status "Waiting for phone service deployment to complete..." + aws ecs wait services-stable \ + --cluster $CLUSTER_NAME \ + --services $PHONE_SERVICE_NAME \ + --region $AWS_REGION + + print_status "Phone service deployment completed successfully!" +else + print_warning "Skipping deployment step as requested" +fi + +# Final status and Twilio configuration +print_header "Phone Service Deployment Complete!" + +if [ "$SKIP_BUILD" = "false" ] && [ "$SKIP_DEPLOY" = "false" ]; then + print_status "Full phone service deployment completed successfully!" +elif [ "$SKIP_BUILD" = "true" ]; then + print_status "Phone service deployment completed (build skipped)" +elif [ "$SKIP_DEPLOY" = "true" ]; then + print_status "Phone service build completed (deployment skipped)" +fi + +# Get phone service URL for Twilio configuration +PHONE_ALB_DNS=$(aws cloudformation describe-stacks \ + --stack-name $STACK_NAME \ + --region $AWS_REGION \ + --query 'Stacks[0].Outputs[?OutputKey==`PhoneLoadBalancerDnsName`].OutputValue' \ + --output text 2>/dev/null || echo "Not available") + +TWILIO_WEBHOOK_URL=$(aws cloudformation describe-stacks \ + --stack-name $STACK_NAME \ + --region $AWS_REGION \ + --query 'Stacks[0].Outputs[?OutputKey==`TwilioWebhookUrl`].OutputValue' \ + --output text 2>/dev/null || echo "Not available") + +if [ "$PHONE_ALB_DNS" != "Not available" ]; then + print_header "๐Ÿ”— Twilio Configuration Required" + print_status "๐Ÿ“ž Phone Service URL: http://$PHONE_ALB_DNS" + print_status "๐ŸŽฏ Twilio Webhook URL: $TWILIO_WEBHOOK_URL" + print_status "" + print_status "๐Ÿ“‹ Manual Twilio Configuration Steps:" + print_status " 1. Log in to your Twilio Console (https://console.twilio.com/)" + print_status " 2. Go to Phone Numbers > Manage > Active numbers" + print_status " 3. Click on your Twilio phone number" + print_status " 4. In the 'Voice Configuration' section:" + print_status " - Set 'A call comes in' webhook to: $TWILIO_WEBHOOK_URL" + print_status " - Set HTTP method to: POST" + print_status " 5. Click 'Save configuration'" + print_status "" + print_status "๐Ÿงช Testing:" + print_status " - Health check: curl http://$PHONE_ALB_DNS/health" + print_status " - Call your Twilio number to test the integration" +fi + +print_status "๐Ÿ“‹ Next steps:" +print_status " 1. Configure Twilio webhook URL as shown above" +print_status " 2. Test phone calls to your Twilio number" +print_status " 3. Monitor logs: aws logs tail /ecs/pipecat-phone-service-$ENVIRONMENT --follow --region $AWS_REGION" +print_status " 4. Check service status: aws ecs describe-services --cluster $CLUSTER_NAME --services $PHONE_SERVICE_NAME --region $AWS_REGION" + +print_header "Phone service deployment script completed!" \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deployment/deploy.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deployment/deploy.sh new file mode 100755 index 0000000..2e07829 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deployment/deploy.sh @@ -0,0 +1,229 @@ +#!/bin/bash + +# Main Deployment Script for Pipecat ECS +# This script orchestrates the complete deployment process + +set -e + +# Default values +ENVIRONMENT="test" +AWS_REGION="eu-north-1" +IMAGE_TAG="latest" +SKIP_BUILD="false" +SKIP_DEPLOY="false" +FORCE_BUILD="false" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_header() { + echo -e "${BLUE}[DEPLOY]${NC} $1" +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -e|--environment) + ENVIRONMENT="$2" + shift 2 + ;; + -r|--region) + AWS_REGION="$2" + shift 2 + ;; + -t|--tag) + IMAGE_TAG="$2" + shift 2 + ;; + --skip-build) + SKIP_BUILD="true" + shift + ;; + --skip-deploy) + SKIP_DEPLOY="true" + shift + ;; + --force-build) + FORCE_BUILD="true" + shift + ;; + --build-only) + SKIP_DEPLOY="true" + shift + ;; + --deploy-only) + SKIP_BUILD="true" + shift + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "This script orchestrates the complete Pipecat ECS deployment process:" + echo "1. Build and push Docker image to ECR" + echo "2. Update ECS service with new image" + echo "" + echo "Options:" + echo " -e, --environment ENV Set environment (default: test)" + echo " -r, --region REGION Set AWS region (default: eu-north-1)" + echo " -t, --tag TAG Set image tag (default: latest)" + echo " --skip-build Skip Docker build and push" + echo " --skip-deploy Skip ECS service deployment" + echo " --force-build Force rebuild even if image exists" + echo " --build-only Only build and push image" + echo " --deploy-only Only deploy to ECS (skip build)" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " $0 # Full deployment with defaults" + echo " $0 -e prod -t v1.2.3 # Deploy to prod with specific tag" + echo " $0 --build-only # Only build and push image" + echo " $0 --deploy-only -t v1.2.3 # Only deploy existing image" + echo " $0 --force-build # Force rebuild and deploy" + echo "" + echo "Prerequisites:" + echo " - AWS CLI configured with appropriate credentials" + echo " - Docker installed and running" + echo " - Infrastructure deployed (run: cd infrastructure && ./deploy.sh)" + echo " - Secrets configured in AWS Secrets Manager" + exit 0 + ;; + *) + echo "Unknown option $1" + exit 1 + ;; + esac +done + +print_header "Starting Pipecat ECS Deployment" +print_status "Environment: $ENVIRONMENT" +print_status "AWS Region: $AWS_REGION" +print_status "Image Tag: $IMAGE_TAG" +print_status "Skip Build: $SKIP_BUILD" +print_status "Skip Deploy: $SKIP_DEPLOY" + +# Validate prerequisites +print_status "Validating prerequisites..." + +# Check if we're in the right directory +if [ ! -f "docker/Dockerfile" ] || [ ! -f "server.py" ]; then + print_error "This script must be run from the pipecat-ecs-deployment directory" + print_error "Current directory: $(pwd)" + exit 1 +fi + +# Check if scripts exist +if [ ! -f "scripts/build-and-push.sh" ] || [ ! -f "scripts/deploy-service.sh" ]; then + print_error "Deployment scripts not found. Please ensure scripts directory exists." + exit 1 +fi + +# Make sure scripts are executable +chmod +x scripts/build-and-push.sh scripts/deploy-service.sh + +# Check if infrastructure is deployed +STACK_NAME="PipecatEcsStack-$ENVIRONMENT" +if ! aws cloudformation describe-stacks --stack-name $STACK_NAME --region $AWS_REGION > /dev/null 2>&1; then + print_warning "Infrastructure stack '$STACK_NAME' not found" + print_status "Deploying infrastructure first..." + + cd infrastructure + chmod +x deploy.sh + ./deploy.sh -e $ENVIRONMENT -r $AWS_REGION + + if [ $? -ne 0 ]; then + print_error "Infrastructure deployment failed" + exit 1 + fi + + cd .. + print_status "Infrastructure deployed successfully!" + + # Infrastructure deployment already built and pushed the image, so skip build step + SKIP_BUILD="true" +fi + +print_status "Prerequisites validated successfully!" + +# Step 1: Build and push Docker image +if [ "$SKIP_BUILD" = "false" ]; then + print_header "Step 1: Building and pushing Docker image" + + BUILD_ARGS="-e $ENVIRONMENT -r $AWS_REGION -t $IMAGE_TAG" + if [ "$FORCE_BUILD" = "true" ]; then + BUILD_ARGS="$BUILD_ARGS --force" + fi + + ./scripts/build-and-push.sh $BUILD_ARGS + + if [ $? -ne 0 ]; then + print_error "Build and push failed" + exit 1 + fi + + print_status "Build and push completed successfully!" +else + print_warning "Skipping build step as requested" +fi + +# Step 2: Deploy to ECS +if [ "$SKIP_DEPLOY" = "false" ]; then + print_header "Step 2: Deploying to ECS" + + ./scripts/deploy-service.sh -e $ENVIRONMENT -r $AWS_REGION -t $IMAGE_TAG --update + + if [ $? -ne 0 ]; then + print_error "ECS deployment failed" + exit 1 + fi + + print_status "ECS deployment completed successfully!" +else + print_warning "Skipping deployment step as requested" +fi + +# Final status and next steps +print_header "Deployment Complete!" + +if [ "$SKIP_BUILD" = "false" ] && [ "$SKIP_DEPLOY" = "false" ]; then + print_status "Full deployment completed successfully!" +elif [ "$SKIP_BUILD" = "true" ]; then + print_status "Deployment completed (build skipped)" +elif [ "$SKIP_DEPLOY" = "true" ]; then + print_status "Build completed (deployment skipped)" +fi + +# Get application URL +STACK_NAME="PipecatEcsStack-$ENVIRONMENT" +ALB_DNS=$(aws cloudformation describe-stacks \ + --stack-name $STACK_NAME \ + --region $AWS_REGION \ + --query 'Stacks[0].Outputs[?OutputKey==`LoadBalancerDnsName`].OutputValue' \ + --output text 2>/dev/null || echo "Not available") + +if [ "$ALB_DNS" != "Not available" ]; then + print_status "๐ŸŒ Application URL: http://$ALB_DNS" +fi + +print_status "๐Ÿ“‹ Next steps:" +print_status " 1. Test the application at the URL above" +print_status " 2. Monitor logs: aws logs tail /ecs/pipecat-voice-agent-$ENVIRONMENT --follow --region $AWS_REGION" +print_status " 3. Check service status: aws ecs describe-services --cluster pipecat-cluster-$ENVIRONMENT --services pipecat-service-$ENVIRONMENT --region $AWS_REGION" + +print_header "Deployment script completed!" \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deployment/fix-jittering-issues.py b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deployment/fix-jittering-issues.py new file mode 100644 index 0000000..070fd26 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deployment/fix-jittering-issues.py @@ -0,0 +1,603 @@ +#!/usr/bin/env python3 +""" +Fix Jittering Issues in Pipecat ECS Deployment + +This script addresses common causes of ECS task jittering: +1. Resource allocation optimization +2. Health check configuration tuning +3. Auto-scaling policy adjustments +4. Load balancer target group settings +""" + +import boto3 +import json +import time +import logging +from typing import Dict, Any, List + +# Configure logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +class JitteringFixer: + """Fix jittering issues in ECS deployment.""" + + def __init__(self): + self.ecs_client = boto3.client("ecs") + self.elbv2_client = boto3.client("elbv2") + self.cloudwatch_client = boto3.client("cloudwatch") + self.application_autoscaling_client = boto3.client("application-autoscaling") + + # Configuration + self.cluster_name = "pipecat-cluster-test" + self.service_name = "pipecat-service-test" + self.target_group_name = "pipecat-alb-test" + + def analyze_current_issues(self) -> Dict[str, Any]: + """Analyze current deployment issues.""" + logger.info("Analyzing current deployment issues...") + + issues = { + "service_issues": [], + "target_group_issues": [], + "scaling_issues": [], + "resource_issues": [], + } + + try: + # Check ECS service status + service_response = self.ecs_client.describe_services( + cluster=self.cluster_name, services=[self.service_name] + ) + + if service_response["services"]: + service = service_response["services"][0] + + # Check running vs desired count + running_count = service["runningCount"] + desired_count = service["desiredCount"] + pending_count = service["pendingCount"] + + if running_count != desired_count: + issues["service_issues"].append( + f"Running count ({running_count}) != Desired count ({desired_count})" + ) + + if pending_count > 0: + issues["service_issues"].append(f"Pending tasks: {pending_count}") + + # Check deployment status + deployments = service.get("deployments", []) + for deployment in deployments: + if deployment["status"] != "PRIMARY": + issues["service_issues"].append( + f"Non-primary deployment: {deployment['status']}" + ) + + logger.info( + f"Service status: Running={running_count}, Desired={desired_count}, Pending={pending_count}" + ) + + # Check target group health + target_groups = self.elbv2_client.describe_target_groups( + Names=[self.target_group_name] + ) + + if target_groups["TargetGroups"]: + tg = target_groups["TargetGroups"][0] + tg_arn = tg["TargetGroupArn"] + + # Get target health + health_response = self.elbv2_client.describe_target_health( + TargetGroupArn=tg_arn + ) + + healthy_targets = sum( + 1 + for target in health_response["TargetHealthDescriptions"] + if target["TargetHealth"]["State"] == "healthy" + ) + total_targets = len(health_response["TargetHealthDescriptions"]) + + if healthy_targets < total_targets: + issues["target_group_issues"].append( + f"Unhealthy targets: {total_targets - healthy_targets}/{total_targets}" + ) + + logger.info( + f"Target group health: {healthy_targets}/{total_targets} healthy" + ) + + # Check health check settings + health_check_interval = tg.get("HealthCheckIntervalSeconds", 30) + health_check_timeout = tg.get("HealthCheckTimeoutSeconds", 5) + healthy_threshold = tg.get("HealthyThresholdCount", 2) + unhealthy_threshold = tg.get("UnhealthyThresholdCount", 2) + + if health_check_interval < 30: + issues["target_group_issues"].append( + f"Health check interval too aggressive: {health_check_interval}s" + ) + + if unhealthy_threshold < 3: + issues["target_group_issues"].append( + f"Unhealthy threshold too low: {unhealthy_threshold}" + ) + + except Exception as e: + logger.error(f"Error analyzing issues: {str(e)}") + issues["analysis_errors"] = [str(e)] + + return issues + + def fix_target_group_health_checks(self) -> bool: + """Optimize target group health check settings.""" + logger.info("Fixing target group health check settings...") + + try: + # Get target group ARN + target_groups = self.elbv2_client.describe_target_groups( + Names=[self.target_group_name] + ) + + if not target_groups["TargetGroups"]: + logger.error("Target group not found") + return False + + tg_arn = target_groups["TargetGroups"][0]["TargetGroupArn"] + + # Update health check settings to be less aggressive + self.elbv2_client.modify_target_group( + TargetGroupArn=tg_arn, + HealthCheckIntervalSeconds=30, # Check every 30 seconds + HealthCheckTimeoutSeconds=10, # 10 second timeout + HealthyThresholdCount=2, # 2 consecutive successes to be healthy + UnhealthyThresholdCount=5, # 5 consecutive failures to be unhealthy + HealthCheckPath="/health", # Ensure correct health check path + HealthCheckProtocol="HTTP", + HealthCheckPort="traffic-port", + ) + + logger.info("โœ… Target group health check settings optimized") + return True + + except Exception as e: + logger.error(f"Failed to fix target group health checks: {str(e)}") + return False + + def optimize_ecs_service_configuration(self) -> bool: + """Optimize ECS service configuration to reduce jittering.""" + logger.info("Optimizing ECS service configuration...") + + try: + # Get current service configuration + service_response = self.ecs_client.describe_services( + cluster=self.cluster_name, services=[self.service_name] + ) + + if not service_response["services"]: + logger.error("Service not found") + return False + + service = service_response["services"][0] + + # Update service with optimized settings + self.ecs_client.update_service( + cluster=self.cluster_name, + service=self.service_name, + deploymentConfiguration={ + "maximumPercent": 200, # Allow up to 200% during deployment + "minimumHealthyPercent": 50, # Keep at least 50% healthy during deployment + "deploymentCircuitBreaker": {"enable": True, "rollback": True}, + }, + healthCheckGracePeriodSeconds=300, # 5 minutes grace period for health checks + enableExecuteCommand=True, + ) + + logger.info("โœ… ECS service configuration optimized") + return True + + except Exception as e: + logger.error(f"Failed to optimize ECS service: {str(e)}") + return False + + def update_task_definition_resources(self) -> bool: + """Update task definition with optimized resource allocation.""" + logger.info("Updating task definition resources...") + + try: + # Get current task definition + service_response = self.ecs_client.describe_services( + cluster=self.cluster_name, services=[self.service_name] + ) + + if not service_response["services"]: + logger.error("Service not found") + return False + + current_task_def_arn = service_response["services"][0]["taskDefinition"] + + # Get task definition details + task_def_response = self.ecs_client.describe_task_definition( + taskDefinition=current_task_def_arn + ) + + task_def = task_def_response["taskDefinition"] + + # Create new task definition with optimized resources + new_task_def = { + "family": task_def["family"], + "networkMode": task_def["networkMode"], + "requiresCompatibilities": task_def["requiresCompatibilities"], + "cpu": "1024", # Increase CPU to 1 vCPU + "memory": "3072", # Increase memory to 3GB + "executionRoleArn": task_def["executionRoleArn"], + "taskRoleArn": task_def.get("taskRoleArn"), + "containerDefinitions": [], + } + + # Update container definitions with optimized resources + for container in task_def["containerDefinitions"]: + updated_container = container.copy() + + # Increase container resources + updated_container["cpu"] = 1024 # 1 vCPU + updated_container["memory"] = 2048 # 2GB hard limit + updated_container["memoryReservation"] = 1536 # 1.5GB soft limit + + # Optimize health check + if "healthCheck" in updated_container: + updated_container["healthCheck"] = { + "command": [ + "CMD-SHELL", + "curl -f http://localhost:7860/health || exit 1", + ], + "interval": 30, + "timeout": 10, + "retries": 5, + "startPeriod": 60, + } + + # Add resource limits for stability + if "ulimits" not in updated_container: + updated_container["ulimits"] = [ + {"name": "nofile", "softLimit": 65536, "hardLimit": 65536} + ] + + new_task_def["containerDefinitions"].append(updated_container) + + # Register new task definition + new_task_response = self.ecs_client.register_task_definition(**new_task_def) + new_task_def_arn = new_task_response["taskDefinition"]["taskDefinitionArn"] + + # Update service to use new task definition + self.ecs_client.update_service( + cluster=self.cluster_name, + service=self.service_name, + taskDefinition=new_task_def_arn, + ) + + logger.info(f"โœ… Task definition updated: {new_task_def_arn}") + return True + + except Exception as e: + logger.error(f"Failed to update task definition: {str(e)}") + return False + + def configure_auto_scaling(self) -> bool: + """Configure auto-scaling policies to reduce jittering.""" + logger.info("Configuring auto-scaling policies...") + + try: + resource_id = f"service/{self.cluster_name}/{self.service_name}" + + # Register scalable target + try: + self.application_autoscaling_client.register_scalable_target( + ServiceNamespace="ecs", + ResourceId=resource_id, + ScalableDimension="ecs:service:DesiredCount", + MinCapacity=2, # Minimum 2 tasks + MaxCapacity=10, # Maximum 10 tasks + RoleArn=f"arn:aws:iam::{boto3.client('sts').get_caller_identity()['Account']}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService", + ) + logger.info("โœ… Scalable target registered") + except Exception as e: + if "already exists" not in str(e): + logger.warning(f"Failed to register scalable target: {str(e)}") + + # Create CPU-based scaling policy with less aggressive settings + try: + cpu_policy_response = self.application_autoscaling_client.put_scaling_policy( + PolicyName=f"{self.service_name}-cpu-scaling", + ServiceNamespace="ecs", + ResourceId=resource_id, + ScalableDimension="ecs:service:DesiredCount", + PolicyType="TargetTrackingScaling", + TargetTrackingScalingPolicyConfiguration={ + "TargetValue": 70.0, # Target 70% CPU utilization + "PredefinedMetricSpecification": { + "PredefinedMetricType": "ECSServiceAverageCPUUtilization" + }, + "ScaleOutCooldown": 300, # 5 minutes cooldown for scale out + "ScaleInCooldown": 600, # 10 minutes cooldown for scale in + "DisableScaleIn": False, + }, + ) + logger.info("โœ… CPU scaling policy configured") + except Exception as e: + logger.warning(f"Failed to create CPU scaling policy: {str(e)}") + + # Create memory-based scaling policy + try: + memory_policy_response = self.application_autoscaling_client.put_scaling_policy( + PolicyName=f"{self.service_name}-memory-scaling", + ServiceNamespace="ecs", + ResourceId=resource_id, + ScalableDimension="ecs:service:DesiredCount", + PolicyType="TargetTrackingScaling", + TargetTrackingScalingPolicyConfiguration={ + "TargetValue": 80.0, # Target 80% memory utilization + "PredefinedMetricSpecification": { + "PredefinedMetricType": "ECSServiceAverageMemoryUtilization" + }, + "ScaleOutCooldown": 300, # 5 minutes cooldown for scale out + "ScaleInCooldown": 600, # 10 minutes cooldown for scale in + "DisableScaleIn": False, + }, + ) + logger.info("โœ… Memory scaling policy configured") + except Exception as e: + logger.warning(f"Failed to create memory scaling policy: {str(e)}") + + return True + + except Exception as e: + logger.error(f"Failed to configure auto-scaling: {str(e)}") + return False + + def create_enhanced_cloudwatch_alarms(self) -> bool: + """Create enhanced CloudWatch alarms for better monitoring.""" + logger.info("Creating enhanced CloudWatch alarms...") + + try: + # Task count stability alarm + self.cloudwatch_client.put_metric_alarm( + AlarmName=f"{self.service_name}-task-count-stability", + ComparisonOperator="LessThanThreshold", + EvaluationPeriods=2, + MetricName="RunningTaskCount", + Namespace="AWS/ECS", + Period=60, + Statistic="Average", + Threshold=2.0, + ActionsEnabled=True, + AlarmDescription="Alert when running task count drops below 2", + Dimensions=[ + {"Name": "ServiceName", "Value": self.service_name}, + {"Name": "ClusterName", "Value": self.cluster_name}, + ], + Unit="Count", + TreatMissingData="breaching", + ) + + # CPU utilization alarm with higher threshold + self.cloudwatch_client.put_metric_alarm( + AlarmName=f"{self.service_name}-high-cpu-utilization", + ComparisonOperator="GreaterThanThreshold", + EvaluationPeriods=3, + MetricName="CPUUtilization", + Namespace="AWS/ECS", + Period=300, + Statistic="Average", + Threshold=85.0, + ActionsEnabled=True, + AlarmDescription="Alert when CPU utilization exceeds 85% for 15 minutes", + Dimensions=[ + {"Name": "ServiceName", "Value": self.service_name}, + {"Name": "ClusterName", "Value": self.cluster_name}, + ], + Unit="Percent", + TreatMissingData="notBreaching", + ) + + # Memory utilization alarm + self.cloudwatch_client.put_metric_alarm( + AlarmName=f"{self.service_name}-high-memory-utilization", + ComparisonOperator="GreaterThanThreshold", + EvaluationPeriods=3, + MetricName="MemoryUtilization", + Namespace="AWS/ECS", + Period=300, + Statistic="Average", + Threshold=90.0, + ActionsEnabled=True, + AlarmDescription="Alert when memory utilization exceeds 90% for 15 minutes", + Dimensions=[ + {"Name": "ServiceName", "Value": self.service_name}, + {"Name": "ClusterName", "Value": self.cluster_name}, + ], + Unit="Percent", + TreatMissingData="notBreaching", + ) + + logger.info("โœ… Enhanced CloudWatch alarms created") + return True + + except Exception as e: + logger.error(f"Failed to create CloudWatch alarms: {str(e)}") + return False + + def wait_for_service_stability(self, timeout_minutes: int = 10) -> bool: + """Wait for service to stabilize after changes.""" + logger.info( + f"Waiting for service stability (timeout: {timeout_minutes} minutes)..." + ) + + start_time = time.time() + timeout_seconds = timeout_minutes * 60 + + while time.time() - start_time < timeout_seconds: + try: + service_response = self.ecs_client.describe_services( + cluster=self.cluster_name, services=[self.service_name] + ) + + if service_response["services"]: + service = service_response["services"][0] + running_count = service["runningCount"] + desired_count = service["desiredCount"] + pending_count = service["pendingCount"] + + # Check if service is stable + deployments = service.get("deployments", []) + primary_deployment = next( + (d for d in deployments if d["status"] == "PRIMARY"), None + ) + + if ( + running_count == desired_count + and pending_count == 0 + and primary_deployment + and primary_deployment.get("rolloutState") == "COMPLETED" + ): + + logger.info( + f"โœ… Service stabilized: {running_count}/{desired_count} tasks running" + ) + return True + + logger.info( + f"Service status: Running={running_count}, Desired={desired_count}, Pending={pending_count}" + ) + + time.sleep(30) # Check every 30 seconds + + except Exception as e: + logger.warning(f"Error checking service stability: {str(e)}") + time.sleep(30) + + logger.warning("Service did not stabilize within timeout period") + return False + + def run_jittering_fixes(self) -> Dict[str, Any]: + """Run all jittering fixes.""" + logger.info("=" * 60) + logger.info("FIXING JITTERING ISSUES") + logger.info("=" * 60) + + start_time = time.time() + results = {} + + # Step 1: Analyze current issues + logger.info("\n1. Analyzing current issues...") + issues = self.analyze_current_issues() + results["analysis"] = issues + + # Step 2: Fix target group health checks + logger.info("\n2. Fixing target group health checks...") + tg_fixed = self.fix_target_group_health_checks() + results["target_group_fixed"] = tg_fixed + + # Step 3: Optimize ECS service configuration + logger.info("\n3. Optimizing ECS service configuration...") + service_optimized = self.optimize_ecs_service_configuration() + results["service_optimized"] = service_optimized + + # Step 4: Update task definition resources + logger.info("\n4. Updating task definition resources...") + task_def_updated = self.update_task_definition_resources() + results["task_definition_updated"] = task_def_updated + + # Step 5: Configure auto-scaling + logger.info("\n5. Configuring auto-scaling...") + autoscaling_configured = self.configure_auto_scaling() + results["autoscaling_configured"] = autoscaling_configured + + # Step 6: Create enhanced CloudWatch alarms + logger.info("\n6. Creating enhanced CloudWatch alarms...") + alarms_created = self.create_enhanced_cloudwatch_alarms() + results["alarms_created"] = alarms_created + + # Step 7: Wait for service stability + logger.info("\n7. Waiting for service stability...") + service_stable = self.wait_for_service_stability() + results["service_stable"] = service_stable + + # Calculate results + total_time = time.time() - start_time + fixes_applied = sum( + 1 for key, value in results.items() if key != "analysis" and value is True + ) + total_fixes = len(results) - 1 # Exclude analysis + + # Summary + logger.info("\n" + "=" * 60) + logger.info("JITTERING FIXES SUMMARY") + logger.info("=" * 60) + logger.info(f"Total Fixes Applied: {fixes_applied}/{total_fixes}") + logger.info(f"Success Rate: {(fixes_applied/total_fixes)*100:.1f}%") + logger.info(f"Total Time: {total_time:.2f} seconds") + + # Detailed results + logger.info("\nDETAILED RESULTS:") + fix_names = { + "target_group_fixed": "Target Group Health Checks", + "service_optimized": "ECS Service Configuration", + "task_definition_updated": "Task Definition Resources", + "autoscaling_configured": "Auto-scaling Policies", + "alarms_created": "CloudWatch Alarms", + "service_stable": "Service Stability", + } + + for key, name in fix_names.items(): + status_icon = "โœ…" if results.get(key) else "โŒ" + logger.info( + f"{status_icon} {name}: {'APPLIED' if results.get(key) else 'FAILED'}" + ) + + # Recommendations + logger.info("\nRECOMMENDATIONS:") + if not service_stable: + logger.info( + "- Monitor service for the next 15-20 minutes to ensure stability" + ) + if fixes_applied < total_fixes: + logger.info("- Review failed fixes and apply manually if needed") + logger.info("- Monitor CloudWatch metrics for improved stability") + logger.info("- Consider increasing task count if load is consistently high") + + overall_success = fixes_applied >= (total_fixes * 0.8) # 80% success rate + + logger.info("\n" + "=" * 60) + if overall_success: + logger.info("๐ŸŽ‰ JITTERING FIXES: MOSTLY SUCCESSFUL") + else: + logger.info("โš ๏ธ JITTERING FIXES: PARTIAL SUCCESS") + logger.info("=" * 60) + + return { + "overall_success": overall_success, + "fixes_applied": fixes_applied, + "total_fixes": total_fixes, + "success_rate": (fixes_applied / total_fixes) * 100, + "total_time": total_time, + "detailed_results": results, + "issues_found": issues, + } + + +def main(): + """Main function to run jittering fixes.""" + fixer = JitteringFixer() + results = fixer.run_jittering_fixes() + + # Exit with appropriate code + sys.exit(0 if results["overall_success"] else 1) + + +if __name__ == "__main__": + main() diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deployment/full-deploy.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deployment/full-deploy.sh new file mode 100755 index 0000000..c353caa --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/deployment/full-deploy.sh @@ -0,0 +1,183 @@ +#!/bin/bash + +# Complete Pipecat ECS Deployment Script +# This script handles the complete deployment flow: +# 1. Set up secrets in AWS Secrets Manager +# 2. Deploy infrastructure (ECR, ECS, ALB, etc.) +# 3. Build and push Docker image +# 4. Deploy/update ECS service + +set -e + +# Default values +ENVIRONMENT="test" +AWS_REGION="eu-north-1" +IMAGE_TAG="latest" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_header() { + echo -e "${BLUE}[DEPLOY]${NC} $1" +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -e|--environment) + ENVIRONMENT="$2" + shift 2 + ;; + -r|--region) + AWS_REGION="$2" + shift 2 + ;; + -t|--tag) + IMAGE_TAG="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Complete deployment script for Pipecat ECS deployment" + echo "" + echo "Options:" + echo " -e, --environment ENV Set environment (default: test)" + echo " -r, --region REGION Set AWS region (default: eu-north-1)" + echo " -t, --tag TAG Set image tag (default: latest)" + echo " -h, --help Show this help message" + echo "" + echo "This script will:" + echo " 1. Set up AWS Secrets Manager secrets" + echo " 2. Deploy CDK infrastructure" + echo " 3. Build and push Docker image to ECR" + echo " 4. Deploy ECS service" + echo "" + exit 0 + ;; + *) + echo "Unknown option $1" + exit 1 + ;; + esac +done + +print_header "๐Ÿš€ Starting Complete Pipecat ECS Deployment" +print_status "Environment: $ENVIRONMENT" +print_status "AWS Region: $AWS_REGION" +print_status "Image Tag: $IMAGE_TAG" +echo "" + +# Check prerequisites +print_status "Checking prerequisites..." + +# Check if we're in the right directory +if [ ! -f "docker/Dockerfile" ] || [ ! -f "server.py" ]; then + print_error "This script must be run from the pipecat-ecs-deployment directory" + print_error "Current directory: $(pwd)" + exit 1 +fi + +# Check if AWS CLI is configured +if ! aws sts get-caller-identity > /dev/null 2>&1; then + print_error "AWS CLI is not configured or credentials are invalid" + print_error "Please run 'aws configure' to set up your credentials" + exit 1 +fi + +# Check if Docker is running +if ! docker info > /dev/null 2>&1; then + print_error "Docker is not running. Please start Docker and try again." + exit 1 +fi + +# Check if .env file exists +if [ ! -f ".env" ]; then + print_error ".env file not found. Please create it with your credentials." + exit 1 +fi + +print_status "โœ“ Prerequisites validated successfully!" +echo "" + +# Step 1: Set up secrets +print_header "๐Ÿ“‹ Step 1: Setting up AWS Secrets Manager" +print_status "Creating/updating secrets for Daily API and AWS credentials..." + +if python3 setup-secrets.py; then + print_status "โœ“ Secrets setup completed successfully!" +else + print_error "Secrets setup failed" + exit 1 +fi +echo "" + +# Step 2: Deploy infrastructure +print_header "๐Ÿ—๏ธ Step 2: Deploying Infrastructure" +print_status "Deploying CDK infrastructure (ECR, ECS, ALB, etc.)..." + +cd infrastructure +chmod +x deploy.sh + +if ./deploy.sh -e $ENVIRONMENT -r $AWS_REGION; then + print_status "โœ“ Infrastructure deployment completed successfully!" +else + print_error "Infrastructure deployment failed" + exit 1 +fi + +cd .. +echo "" + +# The infrastructure deployment script already handles image build and ECS service deployment +# So we're done at this point! + +print_header "๐ŸŽ‰ Deployment Complete!" +print_status "Your Pipecat Voice AI Agent has been successfully deployed to AWS ECS!" + +# Get application URL +STACK_NAME="PipecatEcsStack-$ENVIRONMENT" +ALB_DNS=$(aws cloudformation describe-stacks \ + --stack-name $STACK_NAME \ + --region $AWS_REGION \ + --query 'Stacks[0].Outputs[?OutputKey==`LoadBalancerDnsName`].OutputValue' \ + --output text 2>/dev/null || echo "Not available") + +if [ "$ALB_DNS" != "Not available" ]; then + echo "" + print_status "๐ŸŒ Application URL: http://$ALB_DNS" + print_status "๐Ÿ” Health Check: http://$ALB_DNS/health" + print_status "โšก Ready Check: http://$ALB_DNS/ready" +fi + +echo "" +print_status "๐Ÿ“Š Monitoring Commands:" +print_status " Service status: aws ecs describe-services --cluster pipecat-cluster-$ENVIRONMENT --services pipecat-service-$ENVIRONMENT --region $AWS_REGION" +print_status " Application logs: aws logs tail /ecs/pipecat-voice-agent-$ENVIRONMENT/application --follow --region $AWS_REGION" +print_status " Service events: aws ecs describe-services --cluster pipecat-cluster-$ENVIRONMENT --services pipecat-service-$ENVIRONMENT --region $AWS_REGION --query 'services[0].events[0:5]'" + +echo "" +print_status "๐Ÿงช Testing your deployment:" +print_status " 1. Wait 2-3 minutes for the service to fully start" +print_status " 2. Test health endpoint: curl http://$ALB_DNS/health" +print_status " 3. Check ECS service status in AWS Console" +print_status " 4. Monitor CloudWatch logs for any issues" + +print_header "โœ… Deployment script completed successfully!" \ No newline at end of file diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/monitoring.py b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/monitoring.py new file mode 100644 index 0000000..5172219 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/monitoring.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python3 +""" +Monitoring utilities for Pipecat ECS deployment. + +This module provides utilities for monitoring the health and performance +of the Pipecat voice agent service running in ECS. +""" + +import asyncio +import aiohttp +import json +import time +from datetime import datetime, timedelta +from typing import Dict, List, Optional +from logger_config import logger, log_performance, log_error + + +class ServiceMonitor: + """Monitor for Pipecat service health and performance.""" + + def __init__(self, service_url: str, check_interval: int = 30): + """Initialize the service monitor. + + Args: + service_url: Base URL of the service to monitor + check_interval: Interval between health checks in seconds + """ + self.service_url = service_url.rstrip("/") + self.check_interval = check_interval + self.session: Optional[aiohttp.ClientSession] = None + self.health_history: List[Dict] = [] + self.performance_history: List[Dict] = [] + + async def __aenter__(self): + """Async context manager entry.""" + self.session = aiohttp.ClientSession() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + if self.session: + await self.session.close() + + async def check_health(self) -> Dict: + """Perform a health check on the service. + + Returns: + Dict containing health check results + """ + start_time = time.time() + + try: + async with self.session.get( + f"{self.service_url}/health", timeout=aiohttp.ClientTimeout(total=10) + ) as response: + duration_ms = (time.time() - start_time) * 1000 + + health_data = { + "timestamp": datetime.utcnow().isoformat(), + "status_code": response.status, + "response_time_ms": duration_ms, + "healthy": response.status == 200, + } + + if response.status == 200: + try: + body = await response.json() + health_data["details"] = body + health_data["service_status"] = body.get("status", "unknown") + health_data["active_bots"] = body.get("checks", {}).get( + "active_bots", 0 + ) + except Exception as e: + logger.warning(f"Failed to parse health response: {e}") + health_data["parse_error"] = str(e) + else: + health_data["error"] = f"HTTP {response.status}" + try: + error_body = await response.text() + health_data["error_details"] = error_body + except: + pass + + self.health_history.append(health_data) + + # Keep only last 100 health checks + if len(self.health_history) > 100: + self.health_history = self.health_history[-100:] + + if health_data["healthy"]: + logger.bind( + response_time_ms=duration_ms, + active_bots=health_data.get("active_bots", 0), + ).info("Health check passed") + else: + logger.bind( + status_code=response.status, + response_time_ms=duration_ms, + error=health_data.get("error"), + ).warning("Health check failed") + + return health_data + + except asyncio.TimeoutError: + duration_ms = (time.time() - start_time) * 1000 + health_data = { + "timestamp": datetime.utcnow().isoformat(), + "healthy": False, + "error": "timeout", + "response_time_ms": duration_ms, + } + self.health_history.append(health_data) + log_error( + Exception("Health check timeout"), + "HEALTH_CHECK_TIMEOUT", + response_time_ms=duration_ms, + ) + return health_data + + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + health_data = { + "timestamp": datetime.utcnow().isoformat(), + "healthy": False, + "error": str(e), + "response_time_ms": duration_ms, + } + self.health_history.append(health_data) + log_error(e, "HEALTH_CHECK_ERROR", response_time_ms=duration_ms) + return health_data + + async def check_readiness(self) -> Dict: + """Perform a readiness check on the service. + + Returns: + Dict containing readiness check results + """ + start_time = time.time() + + try: + async with self.session.get( + f"{self.service_url}/ready", timeout=aiohttp.ClientTimeout(total=5) + ) as response: + duration_ms = (time.time() - start_time) * 1000 + + readiness_data = { + "timestamp": datetime.utcnow().isoformat(), + "status_code": response.status, + "response_time_ms": duration_ms, + "ready": response.status == 200, + } + + if response.status == 200: + try: + body = await response.json() + readiness_data["details"] = body + except Exception as e: + logger.warning(f"Failed to parse readiness response: {e}") + + return readiness_data + + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + return { + "timestamp": datetime.utcnow().isoformat(), + "ready": False, + "error": str(e), + "response_time_ms": duration_ms, + } + + def get_health_summary(self, minutes: int = 10) -> Dict: + """Get a summary of health checks over the specified time period. + + Args: + minutes: Number of minutes to look back + + Returns: + Dict containing health summary + """ + cutoff_time = datetime.utcnow() - timedelta(minutes=minutes) + + recent_checks = [ + check + for check in self.health_history + if datetime.fromisoformat(check["timestamp"]) > cutoff_time + ] + + if not recent_checks: + return { + "period_minutes": minutes, + "total_checks": 0, + "healthy_checks": 0, + "unhealthy_checks": 0, + "success_rate": 0.0, + "avg_response_time_ms": 0.0, + } + + healthy_checks = sum( + 1 for check in recent_checks if check.get("healthy", False) + ) + total_checks = len(recent_checks) + + response_times = [ + check["response_time_ms"] + for check in recent_checks + if "response_time_ms" in check + ] + avg_response_time = ( + sum(response_times) / len(response_times) if response_times else 0 + ) + + return { + "period_minutes": minutes, + "total_checks": total_checks, + "healthy_checks": healthy_checks, + "unhealthy_checks": total_checks - healthy_checks, + "success_rate": ( + (healthy_checks / total_checks) * 100 if total_checks > 0 else 0 + ), + "avg_response_time_ms": avg_response_time, + "min_response_time_ms": min(response_times) if response_times else 0, + "max_response_time_ms": max(response_times) if response_times else 0, + } + + async def run_continuous_monitoring(self): + """Run continuous monitoring of the service.""" + logger.info(f"Starting continuous monitoring of {self.service_url}") + + while True: + try: + # Perform health check + health_result = await self.check_health() + + # Log summary every 10 checks + if len(self.health_history) % 10 == 0: + summary = self.get_health_summary(10) + logger.bind( + success_rate=summary["success_rate"], + avg_response_time_ms=summary["avg_response_time_ms"], + total_checks=summary["total_checks"], + ).info("Health monitoring summary (last 10 minutes)") + + # Wait for next check + await asyncio.sleep(self.check_interval) + + except KeyboardInterrupt: + logger.info("Monitoring stopped by user") + break + except Exception as e: + log_error(e, "MONITORING_ERROR") + await asyncio.sleep(self.check_interval) + + +async def main(): + """Main function for running the monitoring script.""" + import argparse + import os + + parser = argparse.ArgumentParser(description="Monitor Pipecat service health") + parser.add_argument( + "--url", + default=os.getenv("SERVICE_URL", "http://localhost:7860"), + help="Service URL to monitor", + ) + parser.add_argument( + "--interval", type=int, default=30, help="Check interval in seconds" + ) + parser.add_argument( + "--single-check", + action="store_true", + help="Perform a single health check and exit", + ) + + args = parser.parse_args() + + async with ServiceMonitor(args.url, args.interval) as monitor: + if args.single_check: + # Single health check + health = await monitor.check_health() + readiness = await monitor.check_readiness() + + print( + json.dumps( + { + "health": health, + "readiness": readiness, + }, + indent=2, + ) + ) + else: + # Continuous monitoring + await monitor.run_continuous_monitoring() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/setup-secrets.py b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/setup-secrets.py new file mode 100755 index 0000000..89d2e7e --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/setup-secrets.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +""" +Script to create AWS Secrets Manager secrets for the Pipecat ECS deployment. +This script creates the necessary secrets that will be injected into ECS tasks. +""" + +import os +import json +import boto3 +import logging +from datetime import datetime +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def create_secrets(): + """Create AWS Secrets Manager secrets for the Pipecat deployment.""" + + print("=" * 60) + print("Setting up AWS Secrets Manager for Pipecat ECS Deployment") + print("=" * 60) + + aws_region = os.getenv("AWS_REGION", "eu-north-1") + daily_api_key = os.getenv("DAILY_API_KEY") + aws_access_key = os.getenv("AWS_ACCESS_KEY_ID") + aws_secret_key = os.getenv("AWS_SECRET_ACCESS_KEY") + + # Twilio credentials + twilio_account_sid = os.getenv("TWILIO_ACCOUNT_SID") + twilio_auth_token = os.getenv("TWILIO_AUTH_TOKEN") + twilio_phone_number = os.getenv("TWILIO_PHONE_NUMBER") + twilio_api_sid = os.getenv("TWILIO_SID") + twilio_api_secret = os.getenv("TWILIO_SECRET") + + if not daily_api_key: + print("โœ— DAILY_API_KEY not found in environment variables") + return False + + if not aws_access_key or not aws_secret_key: + print("โœ— AWS credentials not found in environment variables") + return False + + if not twilio_account_sid or not twilio_auth_token: + print( + "โœ— Twilio credentials (TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) not found in environment variables" + ) + return False + + try: + # Create Secrets Manager client + secrets_client = boto3.client("secretsmanager", region_name=aws_region) + + print(f"Using AWS region: {aws_region}") + print() + + # Secret 1: Daily API Key + daily_secret_name = "pipecat/daily-api-key" + daily_secret_value = { + "DAILY_API_KEY": daily_api_key, + "DAILY_API_URL": os.getenv("DAILY_API_URL", "https://api.daily.co/v1"), + } + + try: + # Try to create the secret + response = secrets_client.create_secret( + Name=daily_secret_name, + Description="Daily.co API credentials for Pipecat voice agent", + SecretString=json.dumps(daily_secret_value), + Tags=[ + {"Key": "Application", "Value": "pipecat-voice-agent"}, + {"Key": "Environment", "Value": "test"}, + ], + ) + print(f"โœ“ Created secret: {daily_secret_name}") + print(f" ARN: {response['ARN']}") + + except secrets_client.exceptions.ResourceExistsException: + # Secret already exists, update it + secrets_client.update_secret( + SecretId=daily_secret_name, SecretString=json.dumps(daily_secret_value) + ) + print(f"โœ“ Updated existing secret: {daily_secret_name}") + + # Secret 2: AWS Credentials (for ECS tasks that need explicit credentials) + aws_secret_name = "pipecat/aws-credentials" + aws_secret_value = { + "AWS_ACCESS_KEY_ID": aws_access_key, + "AWS_SECRET_ACCESS_KEY": aws_secret_key, + "AWS_REGION": aws_region, + } + + try: + # Try to create the secret + response = secrets_client.create_secret( + Name=aws_secret_name, + Description="AWS credentials for Pipecat voice agent Bedrock access", + SecretString=json.dumps(aws_secret_value), + Tags=[ + {"Key": "Application", "Value": "pipecat-voice-agent"}, + {"Key": "Environment", "Value": "test"}, + ], + ) + print(f"โœ“ Created secret: {aws_secret_name}") + print(f" ARN: {response['ARN']}") + + except secrets_client.exceptions.ResourceExistsException: + # Secret already exists, update it + secrets_client.update_secret( + SecretId=aws_secret_name, SecretString=json.dumps(aws_secret_value) + ) + print(f"โœ“ Updated existing secret: {aws_secret_name}") + + # Secret 3: Twilio Credentials + twilio_secret_name = "pipecat/twilio-credentials" + twilio_secret_value = { + "TWILIO_ACCOUNT_SID": twilio_account_sid, + "TWILIO_AUTH_TOKEN": twilio_auth_token, + "TWILIO_PHONE_NUMBER": twilio_phone_number or "", + "TWILIO_API_SID": twilio_api_sid or "", + "TWILIO_API_SECRET": twilio_api_secret or "", + } + + try: + # Try to create the secret + response = secrets_client.create_secret( + Name=twilio_secret_name, + Description="Twilio API credentials for Pipecat phone integration", + SecretString=json.dumps(twilio_secret_value), + Tags=[ + {"Key": "Application", "Value": "pipecat-voice-agent"}, + {"Key": "Environment", "Value": "test"}, + {"Key": "Service", "Value": "twilio"}, + ], + ) + print(f"โœ“ Created secret: {twilio_secret_name}") + print(f" ARN: {response['ARN']}") + + except secrets_client.exceptions.ResourceExistsException: + # Secret already exists, update it + secrets_client.update_secret( + SecretId=twilio_secret_name, + SecretString=json.dumps(twilio_secret_value), + ) + print(f"โœ“ Updated existing secret: {twilio_secret_name}") + + print() + print("โœ“ All secrets created/updated successfully!") + + # Verify secrets can be retrieved + print("\nVerifying secret access...") + + try: + daily_response = secrets_client.get_secret_value(SecretId=daily_secret_name) + daily_data = json.loads(daily_response["SecretString"]) + print(f"โœ“ Daily API secret verified - contains {len(daily_data)} keys") + + aws_response = secrets_client.get_secret_value(SecretId=aws_secret_name) + aws_data = json.loads(aws_response["SecretString"]) + print(f"โœ“ AWS credentials secret verified - contains {len(aws_data)} keys") + + twilio_response = secrets_client.get_secret_value( + SecretId=twilio_secret_name + ) + twilio_data = json.loads(twilio_response["SecretString"]) + print( + f"โœ“ Twilio credentials secret verified - contains {len(twilio_data)} keys" + ) + + except Exception as e: + print(f"โœ— Error verifying secrets: {str(e)}") + return False + + print() + print("๐Ÿ“‹ Secret ARNs for CDK configuration:") + account_id = boto3.client("sts").get_caller_identity()["Account"] + print( + f"Daily API Key: arn:aws:secretsmanager:{aws_region}:{account_id}:secret:{daily_secret_name}" + ) + print( + f"AWS Credentials: arn:aws:secretsmanager:{aws_region}:{account_id}:secret:{aws_secret_name}" + ) + print( + f"Twilio Credentials: arn:aws:secretsmanager:{aws_region}:{account_id}:secret:{twilio_secret_name}" + ) + + return True + + except Exception as e: + print(f"โœ— Error creating secrets: {str(e)}") + return False + + +def list_secrets(): + """List existing Pipecat-related secrets.""" + + print("\n" + "=" * 60) + print("Existing Pipecat Secrets") + print("=" * 60) + + aws_region = os.getenv("AWS_REGION", "eu-north-1") + + try: + secrets_client = boto3.client("secretsmanager", region_name=aws_region) + + # List all secrets + response = secrets_client.list_secrets() + + pipecat_secrets = [ + secret + for secret in response.get("SecretList", []) + if secret["Name"].startswith("pipecat/") + ] + + if not pipecat_secrets: + print("No Pipecat secrets found.") + return + + for secret in pipecat_secrets: + print(f"Name: {secret['Name']}") + print(f" ARN: {secret['ARN']}") + print(f" Description: {secret.get('Description', 'N/A')}") + print(f" Created: {secret.get('CreatedDate', 'N/A')}") + print(f" Last Changed: {secret.get('LastChangedDate', 'N/A')}") + print() + + except Exception as e: + print(f"โœ— Error listing secrets: {str(e)}") + + +def main(): + """Main function.""" + + print(f"AWS Secrets Manager Setup - {datetime.now().isoformat()}") + + # Create secrets + success = create_secrets() + + # List existing secrets + list_secrets() + + if success: + print("\n๐ŸŽ‰ Secrets setup completed successfully!") + print("\nNext steps:") + print("1. Update your CDK infrastructure to reference these secrets") + print("2. Deploy the updated infrastructure") + print("3. Test the ECS deployment with secrets injection") + else: + print("\nโš ๏ธ Secrets setup failed. Please check the errors above.") + + return success + + +if __name__ == "__main__": + success = main() + exit(0 if success else 1) diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/test-ecs-secret-access.py b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/test-ecs-secret-access.py new file mode 100644 index 0000000..74ff718 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/test-ecs-secret-access.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +""" +Test script to simulate ECS task access to all secrets (Daily, AWS, Twilio). +This simulates what the containerized application would do when accessing secrets. +""" + +import os +import json +import boto3 +import logging +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def test_all_secrets_access(): + """Test that all secrets can be retrieved as an ECS task would.""" + + print("=" * 70) + print("Testing All Pipecat Secrets Access (ECS Task Simulation)") + print("=" * 70) + + aws_region = os.getenv("AWS_REGION", "eu-north-1") + + secrets_to_test = [ + { + "name": "pipecat/daily-api-key", + "description": "Daily.co API credentials", + "expected_keys": ["DAILY_API_KEY", "DAILY_API_URL"], + }, + { + "name": "pipecat/aws-credentials", + "description": "AWS credentials for Bedrock", + "expected_keys": [ + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_REGION", + ], + }, + { + "name": "pipecat/twilio-credentials", + "description": "Twilio API credentials", + "expected_keys": [ + "TWILIO_ACCOUNT_SID", + "TWILIO_AUTH_TOKEN", + "TWILIO_PHONE_NUMBER", + "TWILIO_API_SID", + "TWILIO_API_SECRET", + ], + }, + ] + + try: + # Create Secrets Manager client + secrets_client = boto3.client("secretsmanager", region_name=aws_region) + + print(f"Using AWS region: {aws_region}") + print(f"Testing {len(secrets_to_test)} secrets...") + print() + + all_secrets_data = {} + + for secret_info in secrets_to_test: + secret_name = secret_info["name"] + description = secret_info["description"] + expected_keys = secret_info["expected_keys"] + + print(f"Testing: {secret_name} ({description})") + + try: + # Retrieve the secret + response = secrets_client.get_secret_value(SecretId=secret_name) + secret_data = json.loads(response["SecretString"]) + + print(f" โœ“ Successfully retrieved secret") + print(f" โœ“ Contains {len(secret_data)} keys") + + # Verify expected keys + missing_keys = [] + for key in expected_keys: + if key in secret_data and secret_data[key]: + print(f" โœ“ {key}: present") + else: + missing_keys.append(key) + print(f" โœ— {key}: missing or empty") + + if missing_keys: + print(f" โš  Missing keys: {missing_keys}") + else: + print(f" โœ“ All expected keys present") + + all_secrets_data[secret_name] = secret_data + + except Exception as e: + print(f" โœ— Error retrieving secret: {str(e)}") + return False + + print() + + # Test that we can create environment variables as ECS would + print("Simulating ECS environment variable injection:") + env_vars = {} + + # Daily.co credentials + daily_data = all_secrets_data.get("pipecat/daily-api-key", {}) + env_vars.update(daily_data) + + # AWS credentials + aws_data = all_secrets_data.get("pipecat/aws-credentials", {}) + env_vars.update(aws_data) + + # Twilio credentials + twilio_data = all_secrets_data.get("pipecat/twilio-credentials", {}) + env_vars.update(twilio_data) + + print(f" โœ“ Would inject {len(env_vars)} environment variables") + + # Verify critical variables for phone service + critical_vars = [ + "DAILY_API_KEY", + "TWILIO_ACCOUNT_SID", + "TWILIO_AUTH_TOKEN", + "TWILIO_PHONE_NUMBER", + ] + + missing_critical = [] + for var in critical_vars: + if var in env_vars and env_vars[var]: + print(f" โœ“ {var}: available") + else: + missing_critical.append(var) + print(f" โœ— {var}: missing") + + if missing_critical: + print(f" โš  Missing critical variables: {missing_critical}") + return False + + print() + print("๐ŸŽ‰ All secrets access test completed successfully!") + print() + print("โœ… Ready for ECS deployment with Twilio integration") + print("โœ… Phone service will have access to all required credentials") + print("โœ… Both WebRTC and phone calling modes will be supported") + + return True + + except Exception as e: + print(f"โœ— Error testing secrets access: {str(e)}") + return False + + +if __name__ == "__main__": + success = test_all_secrets_access() + exit(0 if success else 1) diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/test-twilio-secret.py b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/test-twilio-secret.py new file mode 100644 index 0000000..c4ea8c5 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/scripts/test-twilio-secret.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Test script to verify Twilio credentials can be retrieved from AWS Secrets Manager. +""" + +import os +import json +import boto3 +import logging +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def test_twilio_secret_access(): + """Test that Twilio credentials can be retrieved from AWS Secrets Manager.""" + + print("=" * 60) + print("Testing Twilio Credentials Access from AWS Secrets Manager") + print("=" * 60) + + aws_region = os.getenv("AWS_REGION", "eu-north-1") + twilio_secret_name = "pipecat/twilio-credentials" + + try: + # Create Secrets Manager client + secrets_client = boto3.client("secretsmanager", region_name=aws_region) + + print(f"Using AWS region: {aws_region}") + print(f"Testing secret: {twilio_secret_name}") + print() + + # Retrieve the secret + response = secrets_client.get_secret_value(SecretId=twilio_secret_name) + secret_data = json.loads(response["SecretString"]) + + print("โœ“ Successfully retrieved Twilio credentials from Secrets Manager") + print(f"โœ“ Secret contains {len(secret_data)} keys") + print() + + # Verify expected keys are present + expected_keys = [ + "TWILIO_ACCOUNT_SID", + "TWILIO_AUTH_TOKEN", + "TWILIO_PHONE_NUMBER", + "TWILIO_API_SID", + "TWILIO_API_SECRET", + ] + + print("Verifying credential keys:") + for key in expected_keys: + if key in secret_data: + value = secret_data[key] + if value: + # Mask sensitive values for display + if "TOKEN" in key or "SECRET" in key: + masked_value = ( + value[:4] + "*" * (len(value) - 8) + value[-4:] + if len(value) > 8 + else "*" * len(value) + ) + print(f" โœ“ {key}: {masked_value}") + else: + print(f" โœ“ {key}: {value}") + else: + print(f" โš  {key}: (empty)") + else: + print(f" โœ— {key}: (missing)") + + print() + + # Test basic validation + account_sid = secret_data.get("TWILIO_ACCOUNT_SID", "") + auth_token = secret_data.get("TWILIO_AUTH_TOKEN", "") + phone_number = secret_data.get("TWILIO_PHONE_NUMBER", "") + + if account_sid.startswith("AC") and len(account_sid) == 34: + print("โœ“ TWILIO_ACCOUNT_SID format looks valid") + else: + print("โš  TWILIO_ACCOUNT_SID format may be invalid") + + if len(auth_token) == 32: + print("โœ“ TWILIO_AUTH_TOKEN length looks valid") + else: + print("โš  TWILIO_AUTH_TOKEN length may be invalid") + + if phone_number.startswith("+") and len(phone_number) > 10: + print("โœ“ TWILIO_PHONE_NUMBER format looks valid") + else: + print("โš  TWILIO_PHONE_NUMBER format may be invalid") + + print() + print("๐ŸŽ‰ Twilio credentials secret test completed successfully!") + print() + print("Next steps:") + print("1. Deploy updated CDK infrastructure with Twilio secret integration") + print("2. Test ECS task can access Twilio credentials") + print("3. Verify phone service can initialize with these credentials") + + return True + + except Exception as e: + print(f"โœ— Error testing Twilio secret access: {str(e)}") + return False + + +if __name__ == "__main__": + success = test_twilio_secret_access() + exit(0 if success else 1) diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/server.py b/speech-to-speech/sample-codes/pipecat-voice-agent/server.py new file mode 100644 index 0000000..3dd5a21 --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/server.py @@ -0,0 +1,857 @@ +""" +Main production RTVI Bot Server +- Full-featured FASTAPI server with Daily.co WebRTC integration +- Production-ready with comprehensive logging, health checks, and process management +- Handles both browser access and RTVI client connections + +- Main production server +""" + +import uvicorn +import argparse +import os +import logging +import subprocess +import signal +import sys +import time +import uuid +import gc +import psutil +import threading +from contextlib import asynccontextmanager +from typing import Any, Dict +from logger_config import ( + logger, + log_performance, + log_error, + log_request, + log_bot_lifecycle, +) +from config.deployment_config import config + +import asyncio +import aiohttp +from dotenv import load_dotenv + +# from bot import main + +from fastapi import FastAPI, HTTPException, Request, BackgroundTasks +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse, RedirectResponse, FileResponse +from datetime import datetime + +from pipecat.transports.services.helpers.daily_rest import ( + DailyRESTHelper, + DailyRoomParams, +) + +# from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection + +# Load environment variables from .env file +load_dotenv(override=True) + +logger = logging.getLogger("pc") + +# Maximum number of bot instances allowed per room +MAX_BOTS_PER_ROOM = config.max_bots_per_room + +# Dictionary to track bot processes: {pid: (process, room_url, start_time)} +bot_procs = {} + +# Store Daily API helpers +daily_helpers = {} + +# Resource monitoring +_resource_monitor_task = None +_cleanup_task = None + +# pcs_map: Dict[str, SmallWebRTCConnection] = {} + + +async def monitor_resources(): + """Monitor system resources and cleanup if needed.""" + while True: + try: + # Get current memory usage + memory_percent = psutil.virtual_memory().percent / 100.0 + cpu_percent = psutil.cpu_percent(interval=1) / 100.0 + + # Log resource usage + logger.debug( + f"Resource usage - Memory: {memory_percent:.1%}, CPU: {cpu_percent:.1%}" + ) + + # Trigger cleanup if memory usage is high + if memory_percent > config.memory_cleanup_threshold: + logger.warning(f"High memory usage detected: {memory_percent:.1%}") + await cleanup_stale_processes() + + # Force garbage collection + gc.collect() + + # Check for stale bot processes + await cleanup_stale_processes() + + # Sleep for the configured interval + await asyncio.sleep(config.bot_cleanup_interval) + + except Exception as e: + logger.error(f"Error in resource monitoring: {str(e)}") + await asyncio.sleep(60) # Wait 1 minute before retrying + + +async def cleanup_stale_processes(): + """Clean up stale bot processes that are no longer needed.""" + current_time = time.time() + stale_pids = [] + + for pid, entry in bot_procs.items(): + proc = entry[0] + room_url = entry[1] + start_time = entry[2] if len(entry) > 2 else current_time + + # Check if process is still running + if proc.poll() is not None: + # Process has finished + stale_pids.append(pid) + log_bot_lifecycle("finished", pid, room_url) + elif current_time - start_time > 3600: # 1 hour timeout + # Process has been running too long, terminate it + logger.warning( + f"Terminating long-running bot process - PID: {pid}, Room: {room_url}" + ) + try: + proc.terminate() + proc.wait(timeout=config.graceful_shutdown_timeout) + stale_pids.append(pid) + log_bot_lifecycle("timeout_terminated", pid, room_url) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + stale_pids.append(pid) + log_bot_lifecycle("timeout_killed", pid, room_url) + except Exception as e: + log_error(e, "STALE_CLEANUP_ERROR", bot_pid=pid, room_url=room_url) + + # Remove stale processes from tracking + for pid in stale_pids: + if pid in bot_procs: + del bot_procs[pid] + + if stale_pids: + logger.info(f"Cleaned up {len(stale_pids)} stale bot processes") + + +def cleanup(): + """Cleanup function to terminate all bot processes. + + Called during server shutdown. + """ + cleanup_start = time.time() + logger.info("Starting cleanup of bot processes...") + + terminated_count = 0 + killed_count = 0 + error_count = 0 + + for pid, entry in bot_procs.items(): + proc = entry[0] + room_url = entry[1] + + if proc.poll() is None: # Process is still running + log_bot_lifecycle("terminating", pid, room_url) + try: + proc.terminate() + # Give process time to terminate gracefully + proc.wait(timeout=config.graceful_shutdown_timeout) + terminated_count += 1 + log_bot_lifecycle("terminated", pid, room_url) + except subprocess.TimeoutExpired: + logger.warning( + f"Force killing bot process - PID: {pid}, Room: {room_url}" + ) + proc.kill() + proc.wait() + killed_count += 1 + log_bot_lifecycle("killed", pid, room_url) + except Exception as e: + error_count += 1 + log_error(e, "CLEANUP_ERROR", bot_pid=pid, room_url=room_url) + else: + log_bot_lifecycle("already_finished", pid, room_url) + + cleanup_duration = (time.time() - cleanup_start) * 1000 + logger.info( + f"Cleanup completed - Terminated: {terminated_count}, Killed: {killed_count}, " + f"Errors: {error_count}, Total: {len(bot_procs)}, Duration: {cleanup_duration:.2f}ms" + ) + + log_performance("cleanup", cleanup_duration) + + +def signal_handler(signum, frame): + """Handle shutdown signals gracefully. + + Args: + signum: Signal number + frame: Current stack frame + """ + logger.info(f"Received signal {signum}, initiating graceful shutdown...") + cleanup() + sys.exit(0) + + +def get_bot_file(): + return "bot" + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """FastAPI lifespan manager that handles startup and shutdown tasks. + + - Creates aiohttp session + - Initializes Daily API helper + - Starts resource monitoring + - Cleans up resources on shutdown + """ + global _resource_monitor_task, _cleanup_task + + # Initialize aiohttp session with connection pooling + connector = aiohttp.TCPConnector( + limit=config.max_request_pool_size if config.enable_request_pooling else 100, + limit_per_host=30, + ttl_dns_cache=300, + use_dns_cache=True, + ) + aiohttp_session = aiohttp.ClientSession( + connector=connector, timeout=aiohttp.ClientTimeout(total=config.request_timeout) + ) + + daily_helpers["rest"] = DailyRESTHelper( + daily_api_key=os.getenv("DAILY_API_KEY", ""), + daily_api_url=config.daily_api_url, + aiohttp_session=aiohttp_session, + ) + + # Start resource monitoring task + _resource_monitor_task = asyncio.create_task(monitor_resources()) + logger.info("Resource monitoring started") + + yield + + # Cleanup on shutdown + if _resource_monitor_task: + _resource_monitor_task.cancel() + try: + await _resource_monitor_task + except asyncio.CancelledError: + pass + + await aiohttp_session.close() + cleanup() + + +# Initialize FastAPI app with lifespan manager +app = FastAPI(lifespan=lifespan) + +# Configure CORS to allow requests from any origin +app.add_middleware( + CORSMiddleware, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/health") +async def health_check(): + """Health check endpoint for ECS monitoring. + + Performs comprehensive health checks including: + - Daily API helper initialization + - Environment variable validation + - Basic connectivity tests + + Returns: + JSONResponse: Health status with timestamp and details + + Raises: + HTTPException: If service is unhealthy + """ + start_time = time.time() + request_id = str(uuid.uuid4()) + + health_status = { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "service": "pipecat-voice-agent", + "version": "1.0", + "checks": {}, + "request_id": request_id, + } + + try: + log_request(request_id, "GET", "/health") + + # Check 1: Daily API helper initialization + daily_helper = daily_helpers.get("rest") + if not daily_helper: + health_status["checks"]["daily_helper"] = "failed" + logger.error(f"Daily API helper not initialized - Request ID: {request_id}") + raise HTTPException( + status_code=503, detail="Daily API helper not initialized" + ) + health_status["checks"]["daily_helper"] = "ok" + logger.debug(f"Daily API helper check passed - Request ID: {request_id}") + + # Check 2: Required environment variables (from secrets or direct env vars) + required_env_vars = ["DAILY_API_KEY", "AWS_REGION"] + missing_vars = [] + env_status = {} + + for var in required_env_vars: + value = os.getenv(var) + if not value: + missing_vars.append(var) + env_status[var] = "missing" + else: + env_status[var] = "present" + # Log source of environment variable (for debugging) + if var == "DAILY_API_KEY": + logger.info( + f"Daily API key loaded: {value[:10]}... - Request ID: {request_id}" + ) + elif var == "AWS_REGION": + logger.info(f"AWS region: {value} - Request ID: {request_id}") + + # Optional AWS credentials check (may come from IAM role or secrets) + aws_access_key = os.getenv("AWS_ACCESS_KEY_ID") + aws_secret_key = os.getenv("AWS_SECRET_ACCESS_KEY") + + if aws_access_key and aws_secret_key: + env_status["aws_credentials"] = "explicit" + logger.info( + f"Using explicit AWS credentials: {aws_access_key[:10]}... - Request ID: {request_id}" + ) + else: + env_status["aws_credentials"] = "iam_role" + logger.info( + f"Using IAM role for AWS credentials - Request ID: {request_id}" + ) + + if missing_vars: + health_status["checks"]["environment"] = { + "status": "failed", + "missing": missing_vars, + "details": env_status, + } + logger.error( + f"Missing required environment variables: {missing_vars} - Request ID: {request_id}" + ) + raise HTTPException( + status_code=503, + detail=f"Missing required environment variables: {', '.join(missing_vars)}", + ) + + health_status["checks"]["environment"] = {"status": "ok", "details": env_status} + + # Check 3: Bot process tracking + active_bots = sum(1 for proc in bot_procs.values() if proc[0].poll() is None) + health_status["checks"]["active_bots"] = active_bots + health_status["checks"]["total_bot_processes"] = len(bot_procs) + + # Check 4: Resource usage + try: + memory_percent = psutil.virtual_memory().percent + cpu_percent = psutil.cpu_percent(interval=0.1) # Quick check + + health_status["checks"]["memory_usage_percent"] = round(memory_percent, 1) + health_status["checks"]["cpu_usage_percent"] = round(cpu_percent, 1) + + # Add resource status + if memory_percent > 90: + health_status["checks"]["resource_status"] = "critical_memory" + elif cpu_percent > 90: + health_status["checks"]["resource_status"] = "critical_cpu" + elif memory_percent > 80 or cpu_percent > 80: + health_status["checks"]["resource_status"] = "warning" + else: + health_status["checks"]["resource_status"] = "ok" + + except Exception as e: + health_status["checks"]["resource_status"] = "unavailable" + logger.warning(f"Could not get resource usage: {str(e)}") + + logger.debug( + f"Bot process check completed - Active: {active_bots}, " + f"Total: {len(bot_procs)} - Request ID: {request_id}" + ) + + # Log successful health check + duration_ms = (time.time() - start_time) * 1000 + log_performance("health_check", duration_ms, request_id=request_id) + + return JSONResponse(health_status) + + except HTTPException: + # Re-raise HTTP exceptions + duration_ms = (time.time() - start_time) * 1000 + logger.warning( + f"Health check failed with HTTP exception - Request ID: {request_id}, Duration: {duration_ms:.2f}ms" + ) + raise + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + log_error( + e, "HEALTH_CHECK_ERROR", request_id=request_id, duration_ms=duration_ms + ) + health_status["status"] = "unhealthy" + health_status["error"] = str(e) + raise HTTPException(status_code=503, detail=f"Service unhealthy: {str(e)}") + + +@app.get("/ready") +async def readiness_check(): + """Readiness check endpoint for ECS deployment. + + This endpoint indicates whether the service is ready to accept traffic. + Unlike /health, this can be used for more sophisticated deployment strategies. + + Returns: + JSONResponse: Readiness status + + Raises: + HTTPException: If service is not ready + """ + start_time = time.time() + request_id = str(uuid.uuid4()) + + try: + log_request(request_id, "GET", "/ready") + + # Check if Daily helper is ready + daily_helper = daily_helpers.get("rest") + if not daily_helper: + logger.error( + f"Service not ready - Daily helper not initialized - Request ID: {request_id}" + ) + raise HTTPException( + status_code=503, + detail="Service not ready - Daily helper not initialized", + ) + + # Check if we can create rooms (basic functionality test) + # This is a lightweight check - we don't actually create a room + if not os.getenv("DAILY_API_KEY"): + logger.error( + f"Service not ready - Daily API key not configured - Request ID: {request_id}" + ) + raise HTTPException( + status_code=503, + detail="Service not ready - Daily API key not configured", + ) + + duration_ms = (time.time() - start_time) * 1000 + log_performance("readiness_check", duration_ms, request_id=request_id) + + return JSONResponse( + { + "status": "ready", + "timestamp": datetime.utcnow().isoformat(), + "service": "pipecat-voice-agent", + "request_id": request_id, + } + ) + + except HTTPException: + duration_ms = (time.time() - start_time) * 1000 + logger.warning( + f"Readiness check failed - Request ID: {request_id}, Duration: {duration_ms:.2f}ms" + ) + raise + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + log_error( + e, "READINESS_CHECK_ERROR", request_id=request_id, duration_ms=duration_ms + ) + raise HTTPException(status_code=503, detail=f"Service not ready: {str(e)}") + + +async def create_room_and_token() -> tuple[str, str]: + """Helper function to create a Daily room and generate an access token. + + Returns: + tuple[str, str]: A tuple containing (room_url, token) + + Raises: + HTTPException: If room creation or token generation fails + """ + start_time = time.time() + request_id = str(uuid.uuid4()) + + try: + logger.info(f"Creating Daily room - Request ID: {request_id}") + + room = await daily_helpers["rest"].create_room(DailyRoomParams()) + if not room.url: + log_error( + Exception("Room creation returned empty URL"), + "ROOM_CREATION_FAILED", + request_id=request_id, + ) + raise HTTPException(status_code=500, detail="Failed to create room") + + logger.info( + f"Daily room created successfully - Room: {room.url} - Request ID: {request_id}" + ) + + token = await daily_helpers["rest"].get_token(room.url) + if not token: + log_error( + Exception(f"Token generation failed for room: {room.url}"), + "TOKEN_GENERATION_FAILED", + request_id=request_id, + room_url=room.url, + ) + raise HTTPException( + status_code=500, detail=f"Failed to get token for room: {room.url}" + ) + + duration_ms = (time.time() - start_time) * 1000 + log_performance( + "create_room_and_token", + duration_ms, + request_id=request_id, + room_url=room.url, + ) + + logger.info( + f"Room and token created successfully - Room: {room.url}, " + f"Token length: {len(token)} - Request ID: {request_id}" + ) + + return room.url, token + + except HTTPException: + raise + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + log_error( + e, "CREATE_ROOM_TOKEN_ERROR", request_id=request_id, duration_ms=duration_ms + ) + raise HTTPException( + status_code=500, detail=f"Failed to create room and token: {str(e)}" + ) + + +@app.get("/") +async def start_agent(request: Request): + """Endpoint for direct browser access to the bot. + + Creates a room, starts a bot instance, and redirects to the Daily room URL. + + Returns: + RedirectResponse: Redirects to the Daily room URL + + Raises: + HTTPException: If room creation, token generation, or bot startup fails + """ + start_time = time.time() + request_id = str(uuid.uuid4()) + + try: + log_request( + request_id, + "GET", + "/", + client_ip=request.client.host if request.client else "unknown", + ) + + logger.info( + f"Starting agent for direct browser access - Request ID: {request_id}" + ) + room_url, token = await create_room_and_token() + + logger.info( + f"Room created, checking bot limits - Room: {room_url} - Request ID: {request_id}" + ) + + # Check if there is already an existing process running in this room + num_bots_in_room = sum( + 1 + for proc in bot_procs.values() + if proc[1] == room_url and proc[0].poll() is None + ) + if num_bots_in_room >= MAX_BOTS_PER_ROOM: + logger.error( + f"Max bot limit reached for room - Room: {room_url}, " + f"Current: {num_bots_in_room}, Max: {MAX_BOTS_PER_ROOM} - Request ID: {request_id}" + ) + raise HTTPException( + status_code=500, detail=f"Max bot limit reached for room: {room_url}" + ) + + # Spawn a new bot process + try: + bot_file = get_bot_file() + proc = subprocess.Popen( + ["python3", "-m", bot_file, "-u", room_url, "-t", token], + shell=False, + bufsize=1, + cwd=os.path.dirname(os.path.abspath(__file__)), + # Add resource limits to prevent runaway processes + preexec_fn=None, # Don't change process group for better cleanup + ) + bot_procs[proc.pid] = (proc, room_url, time.time()) + + log_bot_lifecycle("started", proc.pid, room_url, request_id=request_id) + + except Exception as e: + log_error(e, "BOT_STARTUP_FAILED", request_id=request_id, room_url=room_url) + raise HTTPException( + status_code=500, detail=f"Failed to start subprocess: {e}" + ) + + duration_ms = (time.time() - start_time) * 1000 + log_performance( + "start_agent", + duration_ms, + request_id=request_id, + room_url=room_url, + bot_pid=proc.pid, + ) + + logger.info( + f"Agent started successfully, redirecting to room - Room: {room_url}, " + f"PID: {proc.pid} - Request ID: {request_id}" + ) + + return RedirectResponse(room_url) + + except HTTPException: + duration_ms = (time.time() - start_time) * 1000 + logger.warning( + f"Start agent failed - Request ID: {request_id}, Duration: {duration_ms:.2f}ms" + ) + raise + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + log_error( + e, "START_AGENT_ERROR", request_id=request_id, duration_ms=duration_ms + ) + raise HTTPException(status_code=500, detail=f"Failed to start agent: {str(e)}") + + +@app.post("/connect") +async def rtvi_connect(request: Request) -> Dict[Any, Any]: + """RTVI connect endpoint that creates a room and returns connection credentials. + + This endpoint is called by RTVI clients to establish a connection. + + Returns: + Dict[Any, Any]: Authentication bundle containing room_url and token + + Raises: + HTTPException: If room creation, token generation, or bot startup fails + """ + start_time = time.time() + request_id = str(uuid.uuid4()) + + try: + log_request( + request_id, + "POST", + "/connect", + client_ip=request.client.host if request.client else "unknown", + ) + + logger.info(f"Creating room for RTVI connection - Request ID: {request_id}") + room_url, token = await create_room_and_token() + + logger.info( + f"Room created for RTVI, starting bot process - Room: {room_url} - Request ID: {request_id}" + ) + + # Start the bot process + try: + bot_file = get_bot_file() + proc = subprocess.Popen( + ["python3", "-m", bot_file, "-u", room_url, "-t", token], + shell=False, + bufsize=1, + cwd=os.path.dirname(os.path.abspath(__file__)), + # Add resource limits to prevent runaway processes + preexec_fn=None, # Don't change process group for better cleanup + ) + bot_procs[proc.pid] = (proc, room_url, time.time()) + + log_bot_lifecycle("started_rtvi", proc.pid, room_url, request_id=request_id) + + except Exception as e: + log_error( + e, "RTVI_BOT_STARTUP_FAILED", request_id=request_id, room_url=room_url + ) + raise HTTPException( + status_code=500, detail=f"Failed to start subprocess: {e}" + ) + + duration_ms = (time.time() - start_time) * 1000 + log_performance( + "rtvi_connect", + duration_ms, + request_id=request_id, + room_url=room_url, + bot_pid=proc.pid, + ) + + logger.info( + f"RTVI connection established successfully - Room: {room_url}, " + f"PID: {proc.pid} - Request ID: {request_id}" + ) + + # Return the authentication bundle in format expected by DailyTransport + return {"room_url": room_url, "token": token} + + except HTTPException: + duration_ms = (time.time() - start_time) * 1000 + logger.warning( + f"RTVI connect failed - Request ID: {request_id}, Duration: {duration_ms:.2f}ms" + ) + raise + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + log_error( + e, "RTVI_CONNECT_ERROR", request_id=request_id, duration_ms=duration_ms + ) + raise HTTPException( + status_code=500, detail=f"Failed to establish RTVI connection: {str(e)}" + ) + + +@app.get("/status/{pid}") +def get_status(pid: int, request: Request): + """Get the status of a specific bot process. + + Args: + pid (int): Process ID of the bot + request (Request): FastAPI request object + + Returns: + JSONResponse: Status information for the bot + + Raises: + HTTPException: If the specified bot process is not found + """ + start_time = time.time() + request_id = str(uuid.uuid4()) + + try: + log_request( + request_id, + "GET", + f"/status/{pid}", + client_ip=request.client.host if request.client else "unknown", + ) + + # Look up the subprocess + proc = bot_procs.get(pid) + + # If the subprocess doesn't exist, return an error + if not proc: + logger.warning( + f"Bot process not found - PID: {pid} - Request ID: {request_id}" + ) + raise HTTPException( + status_code=404, detail=f"Bot with process id: {pid} not found" + ) + + # Check the status of the subprocess + status = "running" if proc[0].poll() is None else "finished" + room_url = proc[1] + + duration_ms = (time.time() - start_time) * 1000 + log_performance( + "get_status", + duration_ms, + request_id=request_id, + bot_pid=pid, + bot_status=status, + ) + + logger.info( + f"Bot status retrieved - PID: {pid}, Status: {status}, " + f"Room: {room_url} - Request ID: {request_id}" + ) + + return JSONResponse( + { + "bot_id": pid, + "status": status, + "room_url": room_url, + "request_id": request_id, + "timestamp": datetime.utcnow().isoformat(), + } + ) + + except HTTPException: + duration_ms = (time.time() - start_time) * 1000 + logger.warning( + f"Get status failed - PID: {pid}, Duration: {duration_ms:.2f}ms - Request ID: {request_id}" + ) + raise + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + log_error( + e, + "GET_STATUS_ERROR", + request_id=request_id, + bot_pid=pid, + duration_ms=duration_ms, + ) + raise HTTPException( + status_code=500, detail=f"Failed to get bot status: {str(e)}" + ) + + +if __name__ == "__main__": + # Set up signal handlers for graceful shutdown + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + # Parse command line arguments for server configuration + default_host = os.getenv("HOST", "0.0.0.0") + default_port = int(os.getenv("FAST_API_PORT", "7860")) + + parser = argparse.ArgumentParser(description="Daily FastAPI server") + parser.add_argument("--host", type=str, default=default_host, help="Host address") + parser.add_argument("--port", type=int, default=default_port, help="Port number") + parser.add_argument("--reload", action="store_true", help="Reload code on change") + + config = parser.parse_args() + + logger.info( + f"Starting Pipecat Voice AI Agent server on {config.host}:{config.port}" + ) + + try: + # Start the FastAPI server with improved configuration for containers + uvicorn.run( + "server:app", + host=config.host, + port=config.port, + reload=config.reload, + # Add container-friendly settings + access_log=True, + log_level="info", + # Enable graceful shutdown + timeout_keep_alive=30, + timeout_graceful_shutdown=int(os.getenv("GRACEFUL_TIMEOUT", "30")), + ) + except KeyboardInterrupt: + logger.info("Received keyboard interrupt, shutting down...") + cleanup() + except Exception as e: + logger.error(f"Server error: {e}") + cleanup() + sys.exit(1) diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/server_clean.py b/speech-to-speech/sample-codes/pipecat-voice-agent/server_clean.py new file mode 100644 index 0000000..768238f --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/server_clean.py @@ -0,0 +1,642 @@ +#!/usr/bin/env python3 +""" +FIXED VERSION - Improved Nova Sonic + Twilio integration +Key fixes: +- Better session management +- Proper initialization timing +- Improved error handling +- Fixed context frame delivery +""" + +import os +import json +import asyncio +import logging +import subprocess +import signal +import sys +import time +from typing import Dict, Any +from datetime import datetime +from contextlib import asynccontextmanager + +from fastapi import FastAPI, Request, WebSocket, HTTPException +from fastapi.responses import Response, JSONResponse, RedirectResponse +from fastapi.middleware.cors import CORSMiddleware +from twilio.twiml.voice_response import VoiceResponse +from dotenv import load_dotenv + +# Pipecat imports +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.services.aws_nova_sonic.aws import AWSNovaSonicLLMService, Params +from pipecat.services.aws.llm import AWSBedrockLLMContext +from pipecat.services.llm_service import FunctionCallParams +from pipecat.serializers.twilio import TwilioFrameSerializer +from pipecat.transports.network.fastapi_websocket import ( + FastAPIWebsocketParams, + FastAPIWebsocketTransport, +) +from pipecat.transports.services.daily import DailyParams, DailyTransport +from pipecat.transports.services.helpers.daily_rest import ( + DailyRESTHelper, + DailyRoomParams, +) +from pipecat.frames.frames import StartFrame, EndFrame + +import aiohttp +import ssl +import certifi + +# Create SSL context +ssl_context = ssl.create_default_context(cafile=certifi.where()) +load_dotenv() + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Store active sessions +bot_procs = {} # WebRTC bot processes +call_sessions: Dict[str, Dict[str, Any]] = {} # Twilio call sessions +daily_helpers = {} + + +# Weather function for Nova Sonic +async def fetch_weather_from_api(params: FunctionCallParams): + """Weather API function for Nova Sonic.""" + temperature = 75 if params.arguments["format"] == "fahrenheit" else 24 + await params.result_callback( + { + "conditions": "nice", + "temperature": temperature, + "format": params.arguments["format"], + "timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"), + } + ) + + +weather_function = FunctionSchema( + name="get_current_weather", + description="Get the current weather", + properties={ + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the users location.", + }, + }, + required=["location", "format"], +) + +tools = ToolsSchema(standard_tools=[weather_function]) + + +async def run_twilio_bot(websocket: WebSocket, stream_sid: str, call_sid: str): + """Run the Pipecat bot for a specific Twilio call.""" + logger.info(f"๐Ÿค– Creating Nova Sonic pipeline for call {call_sid}") + + try: + # Create Twilio serializer + serializer = TwilioFrameSerializer( + stream_sid=stream_sid, + call_sid=call_sid, + account_sid=os.getenv("TWILIO_ACCOUNT_SID"), + auth_token=os.getenv("TWILIO_AUTH_TOKEN"), + ) + + # Create FastAPI WebSocket transport + transport = FastAPIWebsocketTransport( + websocket=websocket, + params=FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + audio_in_sample_rate=8000, + audio_out_sample_rate=8000, + audio_out_channels=1, + add_wav_header=False, + vad_analyzer=SileroVADAnalyzer(), # + serializer=serializer, + session_timeout=120, # Increased timeout + ), + ) + + # Improved system instruction + system_instruction = ( + "You are a helpful AI assistant answering phone calls. " + "Keep responses concise and natural. After answering questions, " + "ask if there's anything else you can help with to keep the conversation going. " + "You can provide weather information using the get_current_weather function. " + "Always be polite and helpful." + ) + + # Create Nova Sonic service with improved configuration + try: + llm = AWSNovaSonicLLMService( + access_key_id=os.getenv("AWS_ACCESS_KEY_ID"), + secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"), + region=os.getenv("AWS_REGION"), + voice_id="matthew", + system_instructions=system_instruction, + # Add these parameters for better stability + params=Params( + input_sample_rate=8000, + output_sample_rate=8000, + input_sample_size=16, + output_sample_size=16, + input_channel_count=1, + output_channel_count=1, + max_tokens=512, + temperature=0.7, + top_p=0.9, + ), + ) + + # Wait longer for initialization and register function + await asyncio.sleep(0.5) # Increased wait time + llm.register_function("get_current_weather", fetch_weather_from_api) + logger.info(f"โœ… Nova Sonic service initialized for call {call_sid}") + + except Exception as e: + logger.error(f"Failed to initialize Nova Sonic: {e}") + raise + + context = AWSBedrockLLMContext( + messages=[{"role": "system", "content": system_instruction}], + tools=tools, + ) + context_aggregator = llm.create_context_aggregator(context) + + # Build pipeline + pipeline = Pipeline( + [ + transport.input(), + context_aggregator.user(), + llm, + transport.output(), + context_aggregator.assistant(), + ] + ) + + # Create task with improved parameters + task = PipelineTask( + pipeline, + params=PipelineParams( + allow_interruptions=True, + enable_metrics=True, + enable_usage_metrics=True, + audio_in_sample_rate=8000, + audio_out_sample_rate=8000, + send_initial_empty_metrics=False, # Avoid initial frame issues + ), + ) + + # Store session info + call_sessions[call_sid].update( + { + "pipeline": pipeline, + "task": task, + "transport": transport, + "llm": llm, + "context_aggregator": context_aggregator, + "nova_sonic_ready": False, + } + ) + + # Flag to track Nova Sonic readiness + nova_sonic_connected = asyncio.Event() + initial_greeting_sent = False + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + """Handle client connection - improved timing.""" + logger.info(f"๐Ÿ”— Client connected for call {call_sid}") + + # Wait a bit for Nova Sonic to be fully ready + await asyncio.sleep(1.0) + + try: + # Mark Nova Sonic as ready + call_sessions[call_sid]["nova_sonic_ready"] = True + nova_sonic_connected.set() + + # Send initial context frame + context_frame = context_aggregator.user().get_context_frame() + await task.queue_frames([context_frame]) + + # Send a greeting to start the conversation + if not initial_greeting_sent: + logger.info(f"๐ŸŽค Sending initial greeting for call {call_sid}") + # Let Nova Sonic generate its own greeting based on system prompt + + logger.info(f"โœ… Pipeline fully initialized for call {call_sid}") + + except Exception as e: + logger.error(f"โŒ Error in client connected handler: {e}") + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + """Handle client disconnection.""" + logger.info(f"๐Ÿ”Œ Client disconnected for call {call_sid}") + await task.cancel() + + # Additional error handling for Nova Sonic + original_error_handler = getattr(llm, "_handle_error", None) + + def enhanced_error_handler(error): + logger.error(f"๐Ÿšจ Nova Sonic error for call {call_sid}: {error}") + if "No open prompt found" in str(error) or "No open content found" in str( + error + ): + logger.info(f"๐Ÿ”„ Nova Sonic session lost, will reconnect automatically") + if original_error_handler: + return original_error_handler(error) + + # Override error handler if possible + if hasattr(llm, "_handle_error"): + llm._handle_error = enhanced_error_handler + + # Start the pipeline + logger.info(f"๐Ÿš€ Starting pipeline for call {call_sid}") + runner = PipelineRunner(handle_sigint=False) + + # Wait for Nova Sonic to be ready before running + try: + await asyncio.wait_for(nova_sonic_connected.wait(), timeout=10.0) + except asyncio.TimeoutError: + logger.warning( + f"โš ๏ธ Nova Sonic not ready within timeout for call {call_sid}, proceeding anyway" + ) + + await runner.run(task) + + logger.info(f"โœ… Nova Sonic pipeline completed for call {call_sid}") + + except Exception as e: + logger.error(f"โŒ Error in Nova Sonic pipeline for call {call_sid}: {e}") + import traceback + + logger.error(f"โŒ Traceback: {traceback.format_exc()}") + finally: + await cleanup_call_session(call_sid) + + +def cleanup(): + """Cleanup function for graceful shutdown.""" + logger.info("Starting cleanup...") + for pid, entry in bot_procs.items(): + proc = entry[0] + if proc.poll() is None: + try: + proc.terminate() + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + logger.info("Cleanup completed") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """FastAPI lifespan manager.""" + # Initialize aiohttp session + ssl_context = ssl.create_default_context(cafile=certifi.where()) + connector = aiohttp.TCPConnector(ssl=ssl_context) + aiohttp_session = aiohttp.ClientSession() + + daily_helpers["rest"] = DailyRESTHelper( + daily_api_key=os.getenv("DAILY_API_KEY", ""), + daily_api_url="https://api.daily.co/v1", + aiohttp_session=aiohttp_session, + ) + + yield + + await aiohttp_session.close() + cleanup() + + +# Initialize FastAPI app +app = FastAPI(lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/health") +async def health_check(): + """Health check endpoint.""" + daily_helper = daily_helpers.get("rest") + if not daily_helper: + raise HTTPException(status_code=503, detail="Daily API helper not initialized") + + return { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "service": "fixed-nova-sonic-server", + "webrtc_enabled": bool(os.getenv("DAILY_API_KEY")), + "twilio_enabled": bool(os.getenv("TWILIO_AUTH_TOKEN")), + "nova_sonic_enabled": bool(os.getenv("AWS_ACCESS_KEY_ID")), + "phone_number": os.getenv("TWILIO_PHONE_NUMBER"), + } + + +async def create_room_and_token(): + """Create Daily room and token.""" + room = await daily_helpers["rest"].create_room(DailyRoomParams()) + if not room.url: + raise HTTPException(status_code=500, detail="Failed to create room") + + token = await daily_helpers["rest"].get_token(room.url) + if not token: + raise HTTPException(status_code=500, detail="Failed to get token") + + return room.url, token + + +@app.get("/") +async def start_webrtc_agent(request: Request): + """Start WebRTC agent (existing functionality).""" + logger.info("Starting WebRTC agent") + room_url, token = await create_room_and_token() + + try: + proc = subprocess.Popen( + ["python3", "-m", "bot", "-u", room_url, "-t", token], + shell=False, + bufsize=1, + cwd=os.path.dirname(os.path.abspath(__file__)), + ) + bot_procs[proc.pid] = (proc, room_url, time.time()) + logger.info(f"WebRTC bot started - PID: {proc.pid}, Room: {room_url}") + + return RedirectResponse(room_url) + except Exception as e: + logger.error(f"Failed to start WebRTC bot: {e}") + raise HTTPException(status_code=500, detail=f"Failed to start bot: {e}") + + +@app.post("/connect") +async def rtvi_connect(request: Request): + """RTVI connect endpoint.""" + logger.info("Creating RTVI connection") + room_url, token = await create_room_and_token() + + try: + proc = subprocess.Popen( + ["python3", "-m", "bot", "-u", room_url, "-t", token], + shell=False, + bufsize=1, + cwd=os.path.dirname(os.path.abspath(__file__)), + ) + bot_procs[proc.pid] = (proc, room_url, time.time()) + logger.info(f"RTVI bot started - PID: {proc.pid}, Room: {room_url}") + + return {"room_url": room_url, "token": token} + except Exception as e: + logger.error(f"Failed to start RTVI bot: {e}") + raise HTTPException(status_code=500, detail=f"Failed to start bot: {e}") + + +@app.post("/incoming-call") +@app.get("/incoming-call") +async def handle_incoming_call(request: Request): + """Handle incoming Twilio calls with proper TwiML.""" + logger.info("๐Ÿ“ž Incoming Twilio call received!") + + if request.method == "GET": + form_data = dict(request.query_params) + else: + form_data = await request.form() + form_data = dict(form_data) + + caller = form_data.get("From", "Unknown") + call_sid = form_data.get("CallSid", "Unknown") + + logger.info(f"Call from {caller}, SID: {call_sid}") + + try: + call_sessions[call_sid] = { + "caller": caller, + "start_time": asyncio.get_event_loop().time(), + "status": "connecting", + } + + response = VoiceResponse() + + # Use for bidirectional audio + connect = response.connect() + + # Get WebSocket URL - Try different approaches for Twilio compatibility + def get_websocket_url(request: Request, call_sid: str) -> str: + """Generate proper WebSocket URL for Twilio.""" + # Use environment variable for external domain if available + external_domain = os.getenv("EXTERNAL_DOMAIN") + force_https = os.getenv("FORCE_HTTPS", "false").lower() == "true" + + if external_domain: + # Use the configured external domain + host = external_domain + # Force HTTPS if configured or if we detect load balancer forwarding + forwarded_proto = request.headers.get("x-forwarded-proto", "http") + use_https = force_https or forwarded_proto == "https" + ws_protocol = "wss" if use_https else "ws" + else: + # Fallback to header-based detection + forwarded_proto = request.headers.get("x-forwarded-proto", "http") + host = request.headers.get("x-forwarded-host") or request.headers.get( + "host", "localhost" + ) + ws_protocol = "wss" if forwarded_proto == "https" else "ws" + + return f"{ws_protocol}://{host}/media-stream/{call_sid}" + + # Try both ws:// and wss:// - Twilio might accept ws:// for testing + # For production, this should always be wss:// + websocket_url = get_websocket_url(request, call_sid) + + logger.info(f"๐Ÿ”— Host header: {host}") + logger.info(f"๐Ÿ”— Base URL: {request.base_url}") + logger.info(f"๐Ÿ”— Generated WebSocket URL: {websocket_url}") + + # Use Connect+Stream for bidirectional audio + connect.stream(url=websocket_url) + + logger.info(f"โœ… TwiML response generated for call {call_sid}") + logger.info(f"๐Ÿ”— WebSocket URL: {websocket_url}") + + return Response(content=str(response), media_type="application/xml") + + except Exception as e: + logger.error(f"โŒ Error handling incoming call: {e}") + error_response = VoiceResponse() + error_response.say( + "Sorry, there was an error. Please try again later.", + voice="alice", + ) + return Response(content=str(error_response), media_type="application/xml") + + +@app.websocket("/media-stream/{call_sid}") +async def handle_media_stream(websocket: WebSocket, call_sid: str): + """Handle Twilio media stream with enhanced debugging.""" + await websocket.accept() + logger.info(f"๐Ÿ”„ Media stream connected for call {call_sid}") + + session = call_sessions.get(call_sid) + if not session: + logger.error(f"โŒ No session found for call {call_sid}") + await websocket.close() + return + + session["status"] = "streaming" + + # Simplified logging - remove the debug wrapper that might cause issues + try: + # Wait for connected event + connected_message = await websocket.receive_text() + connected_data = json.loads(connected_message) + + logger.info( + f"๐Ÿ“ก Received: {connected_data.get('event')} - Protocol: {connected_data.get('protocol', 'unknown')}" + ) + + if connected_data.get("event") == "connected": + logger.info(f"๐Ÿ“ก Twilio connected for call {call_sid}") + + # Wait for start event + start_message = await websocket.receive_text() + start_data = json.loads(start_message) + + logger.info(f"๐ŸŽค Received: {start_data.get('event')} event") + + if start_data.get("event") == "start": + stream_sid = start_data.get("start", {}).get("streamSid") + media_format = start_data.get("start", {}).get("mediaFormat", {}) + + logger.info(f"๐ŸŽค Audio streaming started:") + logger.info(f" - Call ID: {call_sid}") + logger.info(f" - Stream ID: {stream_sid}") + logger.info(f" - Media format: {media_format}") + + session["stream_sid"] = stream_sid + + # Start the Nova Sonic bot + await run_twilio_bot(websocket, stream_sid, call_sid) + else: + logger.error( + f"โŒ Expected 'start' event, got: {start_data.get('event')}" + ) + await websocket.close() + else: + logger.error( + f"โŒ Expected 'connected' event, got: {connected_data.get('event')}" + ) + await websocket.close() + + except Exception as e: + logger.error(f"โŒ WebSocket error for call {call_sid}: {e}") + import traceback + + logger.error(f"โŒ Traceback: {traceback.format_exc()}") + finally: + logger.info(f"๐Ÿ”Œ Media stream disconnected for call {call_sid}") + await cleanup_call_session(call_sid) + + +async def cleanup_call_session(call_sid: str): + """Clean up call session.""" + session = call_sessions.get(call_sid) + if not session: + return + + try: + if "task" in session: + await session["task"].cancel() + + session["status"] = "ended" + session["end_time"] = asyncio.get_event_loop().time() + duration = session.get("end_time", 0) - session.get("start_time", 0) + logger.info(f"๐Ÿ“Š Call {call_sid} ended - Duration: {duration:.1f}s") + except Exception as e: + logger.error(f"โŒ Error cleaning up call session {call_sid}: {e}") + + +@app.get("/active-calls") +async def get_active_calls(): + """Get active call sessions.""" + active_calls = [] + current_time = asyncio.get_event_loop().time() + + for call_sid, session in call_sessions.items(): + if session.get("status") in ["connecting", "streaming"]: + duration = current_time - session.get("start_time", current_time) + active_calls.append( + { + "call_sid": call_sid, + "status": session.get("status"), + "caller": session.get("caller"), + "duration": duration, + "has_pipeline": "pipeline" in session, + "nova_sonic_ready": session.get("nova_sonic_ready", False), + } + ) + + return { + "active_calls": active_calls, + "total_active": len(active_calls), + "webrtc_bots": len([p for p in bot_procs.values() if p[0].poll() is None]), + } + + +if __name__ == "__main__": + import uvicorn + import argparse + + # Set up signal handlers + signal.signal(signal.SIGTERM, lambda s, f: (cleanup(), sys.exit(0))) + signal.signal(signal.SIGINT, lambda s, f: (cleanup(), sys.exit(0))) + + default_host = os.getenv("HOST", "0.0.0.0") + default_port = int(os.getenv("FAST_API_PORT", "7860")) + + parser = argparse.ArgumentParser(description="Fixed Nova Sonic + Twilio server") + parser.add_argument("--host", type=str, default=default_host, help="Host address") + parser.add_argument("--port", type=int, default=default_port, help="Port number") + parser.add_argument("--reload", action="store_true", help="Reload on change") + + config = parser.parse_args() + + logger.info(f"๐Ÿš€ Starting Fixed Nova Sonic server on {config.host}:{config.port}") + logger.info(f"๐Ÿ“ž Twilio phone: {os.getenv('TWILIO_PHONE_NUMBER')}") + logger.info(f"๐ŸŒ WebRTC: {bool(os.getenv('DAILY_API_KEY'))}") + logger.info(f"๐Ÿค– Nova Sonic: {bool(os.getenv('AWS_ACCESS_KEY_ID'))}") + + try: + uvicorn.run( + "server_clean:app", + host=config.host, + port=config.port, + reload=config.reload, + access_log=True, + log_level="info", + ) + except KeyboardInterrupt: + logger.info("Server stopped") + cleanup() + except Exception as e: + logger.error(f"Server error: {e}") + cleanup() + sys.exit(1) diff --git a/speech-to-speech/sample-codes/pipecat-voice-agent/setup-project.sh b/speech-to-speech/sample-codes/pipecat-voice-agent/setup-project.sh new file mode 100755 index 0000000..1a5b32e --- /dev/null +++ b/speech-to-speech/sample-codes/pipecat-voice-agent/setup-project.sh @@ -0,0 +1,196 @@ +#!/bin/bash + +# Pipecat Voice AI Agent - Project Setup Script +# This script prepares the project for GitHub upload and local development + +set -e + +echo "๐Ÿš€ Setting up Pipecat Voice AI Agent project..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}โœ… $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}โš ๏ธ $1${NC}" +} + +print_error() { + echo -e "${RED}โŒ $1${NC}" +} + +print_info() { + echo -e "${BLUE}โ„น๏ธ $1${NC}" +} + +# Check if we're in a git repository +if [ ! -d ".git" ]; then + print_error "This is not a git repository. Please run 'git init' first." + exit 1 +fi + +# Remove any existing .env file (security) +if [ -f ".env" ]; then + print_warning "Removing .env file with sensitive credentials..." + rm -f .env + print_status "Removed .env file" +fi + +# Create .env.example if it doesn't exist +if [ ! -f ".env.example" ]; then + print_info "Creating .env.example template..." + cat > .env.example << 'EOF' +# Daily.co API Configuration +DAILY_API_KEY=your_daily_api_key_here +DAILY_API_URL=https://api.daily.co/v1 + +# AWS Configuration +AWS_ACCESS_KEY_ID=your_aws_access_key_here +AWS_SECRET_ACCESS_KEY=your_aws_secret_key_here +AWS_REGION=us-east-1 + +# Twilio Configuration (for phone service) +TWILIO_RECOVERY_CODE=your_twilio_recovery_code +TWILIO_ACCOUNT_SID=your_twilio_account_sid +TWILIO_AUTH_TOKEN=your_twilio_auth_token +TWILIO_PHONE_NUMBER=+1234567890 +TWILIO_SID=your_twilio_sid +TWILIO_SECRET=your_twilio_secret +TWILIO_AUTH_LIVE=your_twilio_auth_live + +# Optional Configuration +ENVIRONMENT=development +LOG_LEVEL=INFO +HOST=0.0.0.0 +FAST_API_PORT=7860 +MAX_BOTS_PER_ROOM=1 +MAX_CONCURRENT_ROOMS=10 +EOF + print_status "Created .env.example template" +fi + +# Make scripts executable +print_info "Making scripts executable..." +find scripts/ -name "*.sh" -type f -exec chmod +x {} \; +if [ -f "infrastructure/deploy.sh" ]; then + chmod +x infrastructure/deploy.sh +fi +if [ -f "setup-project.sh" ]; then + chmod +x setup-project.sh +fi +print_status "Scripts are now executable" + +# Check for required tools +print_info "Checking required tools..." + +check_tool() { + if command -v $1 &> /dev/null; then + print_status "$1 is installed" + return 0 + else + print_error "$1 is not installed" + return 1 + fi +} + +MISSING_TOOLS=0 + +# Check essential tools +if ! check_tool "aws"; then + print_warning "Install AWS CLI: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" + MISSING_TOOLS=1 +fi + +if ! check_tool "docker"; then + print_warning "Install Docker: https://docs.docker.com/get-docker/" + MISSING_TOOLS=1 +fi + +if ! check_tool "node"; then + print_warning "Install Node.js: https://nodejs.org/" + MISSING_TOOLS=1 +fi + +if ! check_tool "python3"; then + print_warning "Install Python 3.10+: https://www.python.org/downloads/" + MISSING_TOOLS=1 +fi + +# Check optional tools +if ! check_tool "kubectl"; then + print_warning "Install kubectl for EKS deployments: https://kubernetes.io/docs/tasks/tools/" +fi + +if ! check_tool "cdk"; then + print_warning "Install AWS CDK: npm install -g aws-cdk" +fi + +# Install Python dependencies if requirements.txt exists +if [ -f "requirements.txt" ]; then + print_info "Python dependencies available in requirements.txt" + if command -v python3 &> /dev/null; then + print_info "For local development, create a virtual environment:" + print_info "python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt" + else + print_warning "Python3 not found" + fi +fi + +# Install CDK dependencies for ECS infrastructure +if [ -f "infrastructure/package.json" ]; then + print_info "Installing ECS CDK dependencies..." + cd infrastructure + npm install + cd .. + print_status "ECS CDK dependencies installed" +fi + +# Install CDK dependencies for EKS infrastructure +if [ -f "infrastructure/-eks/package.json" ]; then + print_info "Installing EKS CDK dependencies..." + cd infrastructure/-eks + npm install + cd ../.. + print_status "EKS CDK dependencies installed" +fi + +# Check git status +print_info "Checking git status..." +if [ -n "$(git status --porcelain)" ]; then + print_warning "You have uncommitted changes. Consider committing them before uploading to GitHub." + git status --short +else + print_status "Working directory is clean" +fi + +# Summary +echo "" +echo "๐Ÿ“‹ Setup Summary:" +echo "==================" + +if [ $MISSING_TOOLS -eq 0 ]; then + print_status "All essential tools are installed" +else + print_warning "Some tools are missing - install them before deployment" +fi + +print_info "Next steps:" +echo "1. Copy .env.example to .env and fill in your credentials (for local development)" +echo "2. Configure AWS credentials: aws configure" +echo "3. Set up GitHub repository secrets (see .github/SETUP.md)" +echo "4. Choose deployment platform:" +echo " - ECS: cd infrastructure && ./deploy.sh --environment test" +echo " - EKS: cd infrastructure/-eks && cdk deploy" +echo "5. Push to GitHub to trigger automated deployments" + +echo "" +print_status "Project setup complete! ๐ŸŽ‰" +print_info "Read README.md for detailed deployment instructions" \ No newline at end of file