Lab L2.10: Production Deployment
🎯 Assignment: Accept this lab on GitHub Classroom
You’ll get your own repository with starter code, instructions, and automatic grading.
| Â | Â |
|---|---|
| Duration | 60 minutes |
| Prerequisites | Previous module completed |
Objectives
- Configure production settings
- Implement health checks
- Set up environment management
How to Complete This Lab
- Accept the Assignment — Click the GitHub Classroom link above
- Clone Your Repo —
git clone <your-repo-url> - Read the README — Your repo has detailed requirements and grading criteria
- Write Your Code — Implement the solution in
solution/agent.py - Test Locally — Use
swaig-testto verify your agent works - Push to Submit —
git pushtriggers auto-grading
Key Concepts
The following exercises walk through the concepts you’ll need. Your GitHub Classroom repo README has the specific requirements for grading.
Part 1: Production Agent Code (20 min)
agent.py
#!/usr/bin/env python3
"""Production-ready agent."""
import os
import logging
from datetime import datetime
from dotenv import load_dotenv
from signalwire_agents import AgentServer, AgentBase, SwaigFunctionResult
# Load environment
load_dotenv()
# Configure logging
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig(
level=log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class ProductionAgent(AgentBase):
def __init__(self):
super().__init__(
name=os.getenv("AGENT_NAME", "production-agent"),
route=os.getenv("AGENT_ROUTE", "/agent")
)
# Authentication
auth_user = os.getenv("AUTH_USER")
auth_pass = os.getenv("AUTH_PASSWORD")
if auth_user and auth_pass:
self.set_params({
"swml_basic_auth_user": auth_user,
"swml_basic_auth_password": auth_pass
})
logger.info("Basic auth configured")
# Prompt configuration
self.prompt_add_section(
"Role",
os.getenv("AGENT_ROLE", "You are a helpful assistant.")
)
self.add_language("English", "en-US", "rime.spore")
self._setup_functions()
logger.info(f"Agent initialized: {self._name}")
def _setup_functions(self):
@self.tool(description="Get system status")
def get_status(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
logger.info("Status check requested")
return SwaigFunctionResult("All systems operational.")
@self.tool(description="Get current time")
def get_time(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
now = datetime.now().strftime("%I:%M %p")
return SwaigFunctionResult(f"The current time is {now}.")
def create_app():
"""Create the application."""
host = os.getenv("HOST", "0.0.0.0")
port = int(os.getenv("PORT", "3000"))
logger.info(f"Creating server on {host}:{port}")
server = AgentServer(host=host, port=port)
server.register(ProductionAgent())
# Health endpoint
@server.app.get("/health")
async def health():
return {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"version": os.getenv("APP_VERSION", "1.0.0")
}
# Readiness endpoint
@server.app.get("/ready")
async def ready():
return {"ready": True}
return server
# For uvicorn
server = create_app()
app = server.app
if __name__ == "__main__":
server.run()
Part 2: Docker Configuration (25 min)
Dockerfile
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY . .
# Create non-root user
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Run with uvicorn
CMD ["uvicorn", "agent:app", "--host", "0.0.0.0", "--port", "3000"]
requirements.txt
signalwire-agents>=1.0.7
python-dotenv>=1.0.0
uvicorn[standard]>=0.20.0
docker-compose.yml
version: '3.8'
services:
agent:
build: .
ports:
- "3000:3000"
env_file:
- .env.production
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
.env.production
# SignalWire credentials
SIGNALWIRE_SPACE_NAME=your-space
SIGNALWIRE_PROJECT_ID=your-project-id
SIGNALWIRE_TOKEN=your-token
# Server configuration
HOST=0.0.0.0
PORT=3000
# Agent configuration
AGENT_NAME=production-agent
AGENT_ROUTE=/agent
AGENT_ROLE=You are a helpful production assistant.
# Authentication
AUTH_USER=signalwire
AUTH_PASSWORD=change-this-password
# Logging
LOG_LEVEL=INFO
# Version
APP_VERSION=1.0.0
Part 3: Build and Run (20 min)
Task
Build and test the containerized agent.
Commands
# Build the image
docker build -t my-agent:latest .
# Run with docker-compose
docker-compose up -d
# Check logs
docker-compose logs -f
# Check health
curl http://localhost:3000/health
# Test the agent
curl http://localhost:3000/agent
# Stop
docker-compose down
Part 4: Production Testing (25 min)
Task
Verify all production components.
Test Checklist
# 1. Health check
curl -s http://localhost:3000/health | jq
# Expected: {"status": "healthy", "timestamp": "...", "version": "1.0.0"}
# 2. Readiness check
curl -s http://localhost:3000/ready | jq
# Expected: {"ready": true}
# 3. Agent SWML
curl -s http://localhost:3000/agent | jq
# Expected: SWML document with agent configuration
# 4. Authentication (if configured)
curl -s -u signalwire:change-this-password http://localhost:3000/agent | jq
# 5. Container health
docker inspect --format='' $(docker-compose ps -q agent)
# Expected: healthy
# 6. Logs
docker-compose logs --tail=50 agent
Validation Checklist
- Dockerfile builds without errors
- Container starts and stays running
- Health check passes
- Readiness endpoint responds
- Agent serves SWML at configured route
- Authentication works (if configured)
- Logs show expected output
- Container runs as non-root user
Production Considerations
Security
- Changed default AUTH_PASSWORD
- HTTPS configured (via reverse proxy)
- Secrets not committed to git
Monitoring
- Health checks configured
- Log aggregation set up
- Alerts configured for failures
Scaling
- Resource limits set
- Multiple replicas if needed
- Load balancer configured
Challenge Extension
Add a Kubernetes deployment:
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: voice-agent
spec:
replicas: 2
selector:
matchLabels:
app: voice-agent
template:
metadata:
labels:
app: voice-agent
spec:
containers:
- name: agent
image: my-agent:latest
ports:
- containerPort: 3000
envFrom:
- secretRef:
name: agent-secrets
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Submission
Upload your project directory containing:
agent.pyDockerfiledocker-compose.ymlrequirements.txt.env.production.example(without real credentials)
Complete Agent Code
Click to reveal complete solution
#!/usr/bin/env python3
"""Production-ready agent with health checks.
Lab 2.10 Deliverable: Demonstrates production deployment patterns
including environment configuration, logging, and health endpoints.
Environment variables:
SWML_BASIC_AUTH_USER: Basic auth username (auto-detected by SDK)
SWML_BASIC_AUTH_PASSWORD: Basic auth password (auto-detected by SDK)
AGENT_NAME: Agent name (default: production-agent)
AGENT_ROLE: Agent role description
HOST: Bind host (default: 0.0.0.0)
PORT: Bind port (default: 3000)
LOG_LEVEL: Logging level (default: INFO)
SUPPORT_EMAIL: Support email address
APP_VERSION: Application version
"""
import os
import logging
from datetime import datetime
from signalwire_agents import AgentServer, AgentBase, SwaigFunctionResult
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass # dotenv not required
# Configure logging
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig(
level=log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class ProductionAgent(AgentBase):
"""Production-ready agent with configuration from environment."""
def __init__(self):
super().__init__(name=os.getenv("AGENT_NAME", "production-agent"))
# Prompt configuration
self.prompt_add_section(
"Role",
os.getenv("AGENT_ROLE", "You are a helpful assistant.")
)
self.prompt_add_section(
"Guidelines",
bullets=[
"Keep responses concise",
"Be helpful and professional",
"Escalate if needed"
]
)
self.add_language("English", "en-US", "rime.spore")
self._setup_functions()
logger.info(f"Agent initialized: {self.get_name()}")
def _setup_functions(self):
@self.tool(description="Get system status")
def get_status() -> SwaigFunctionResult:
logger.info("Status check requested")
return SwaigFunctionResult("All systems operational.")
@self.tool(description="Get current time")
def get_time() -> SwaigFunctionResult:
now = datetime.now().strftime("%I:%M %p")
return SwaigFunctionResult(f"The current time is {now}.")
@self.tool(
description="Get help or support information"
)
def get_help() -> SwaigFunctionResult:
support_email = os.getenv("SUPPORT_EMAIL", "support@company.com")
return SwaigFunctionResult(
f"For additional help, contact {support_email}."
)
def create_app():
"""Create the application with health endpoints."""
host = os.getenv("HOST", "0.0.0.0")
port = int(os.getenv("PORT", "3000"))
logger.info(f"Creating server on {host}:{port}")
server = AgentServer(host=host, port=port)
server.register(ProductionAgent())
# Health endpoint
@server.app.get("/health")
async def health():
return {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"version": os.getenv("APP_VERSION", "1.0.0")
}
# Readiness endpoint
@server.app.get("/ready")
async def ready():
return {"ready": True}
# Info endpoint
@server.app.get("/info")
async def info():
return {
"name": os.getenv("AGENT_NAME", "production-agent"),
"version": os.getenv("APP_VERSION", "1.0.0"),
"environment": os.getenv("ENVIRONMENT", "production")
}
return server
if __name__ == "__main__":
server = create_app()
server.run()