Duration 2.5 hours
Day 5 of 5

Learning Objectives

By the end of this module, students will be able to:

  • Configure agents for production environments
  • Deploy with Docker containers
  • Set up proper logging and monitoring
  • Implement health checks

Topics

1. Production Configuration (30 min)

Environment Variables

# .env.production
SIGNALWIRE_SPACE_NAME=your-space
SIGNALWIRE_PROJECT_ID=your-project-id
SIGNALWIRE_TOKEN=your-token

# Server
HOST=0.0.0.0
PORT=3000
WORKERS=4

# Security
AUTH_USER=signalwire
AUTH_PASSWORD=strong-random-password

# Logging
LOG_LEVEL=info
LOG_FORMAT=json

Loading Configuration

#!/usr/bin/env python3
import os
from dotenv import load_dotenv
from signalwire_agents import AgentBase

# Load environment
load_dotenv()

class ProductionAgent(AgentBase):
    def __init__(self):
        super().__init__(
            name=os.getenv("AGENT_NAME", "production-agent"),
            route=os.getenv("AGENT_ROUTE", "/")
        )

        self._configure_auth()
        self._configure_prompts()
        self._configure_voice()

    def _configure_auth(self):
        user = os.getenv("AUTH_USER")
        password = os.getenv("AUTH_PASSWORD")

        if user and password:
            self.set_params({
                "swml_basic_auth_user": user,
                "swml_basic_auth_password": password
            })

Production Checklist

Item Status
Environment variables set [ ]
Secrets not in code [ ]
Authentication enabled [ ]
HTTPS configured [ ]
Logging configured [ ]
Error handling in place [ ]
Health checks implemented [ ]

2. Docker Deployment (35 min)

Dockerfile

FROM python:3.11-slim

# Set working directory
WORKDIR /app

# 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", "app:app", "--host", "0.0.0.0", "--port", "3000", "--workers", "4"]

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"
    environment:
      - SIGNALWIRE_SPACE_NAME=${SIGNALWIRE_SPACE_NAME}
      - SIGNALWIRE_PROJECT_ID=${SIGNALWIRE_PROJECT_ID}
      - SIGNALWIRE_TOKEN=${SIGNALWIRE_TOKEN}
      - AUTH_USER=${AUTH_USER}
      - AUTH_PASSWORD=${AUTH_PASSWORD}
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Building and Running

# Build image
docker build -t my-agent:latest .

# Run container
docker run -d \
  --name my-agent \
  -p 3000:3000 \
  --env-file .env.production \
  my-agent:latest

# Check logs
docker logs my-agent

# Check health
docker inspect --format='' my-agent

3. Kubernetes Deployment (30 min)

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: voice-agent
spec:
  replicas: 3
  selector:
    matchLabels:
      app: voice-agent
  template:
    metadata:
      labels:
        app: voice-agent
    spec:
      containers:
      - name: agent
        image: my-agent:latest
        ports:
        - containerPort: 3000
        env:
        - name: SIGNALWIRE_SPACE_NAME
          valueFrom:
            secretKeyRef:
              name: signalwire-secrets
              key: space-name
        - name: SIGNALWIRE_PROJECT_ID
          valueFrom:
            secretKeyRef:
              name: signalwire-secrets
              key: project-id
        - name: SIGNALWIRE_TOKEN
          valueFrom:
            secretKeyRef:
              name: signalwire-secrets
              key: token
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 10

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: voice-agent-service
spec:
  selector:
    app: voice-agent
  ports:
  - port: 80
    targetPort: 3000
  type: LoadBalancer

4. Logging Configuration (25 min)

Structured Logging

import logging
import json
from datetime import datetime

class JSONFormatter(logging.Formatter):
    def format(self, record):
        log_record = {
            "timestamp": datetime.utcnow().isoformat(),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }

        if record.exc_info:
            log_record["exception"] = self.formatException(record.exc_info)

        return json.dumps(log_record)

# Configure logging
def setup_logging():
    handler = logging.StreamHandler()
    handler.setFormatter(JSONFormatter())

    logging.basicConfig(
        level=os.getenv("LOG_LEVEL", "INFO").upper(),
        handlers=[handler]
    )

Agent Logging

import logging

logger = logging.getLogger(__name__)

class LoggedAgent(AgentBase):
    @AgentBase.tool(description="Process order")
    def process_order(self, args: dict, raw_data: dict = None) -> SwaigFunctionResult:
        order_id = args.get("order_id", "")
        logger.info(f"Processing order", extra={"order_id": order_id})

        try:
            result = do_processing(order_id)
            logger.info(f"Order processed", extra={
                "order_id": order_id,
                "status": "success"
            })
            return SwaigFunctionResult("Order processed.")

        except Exception as e:
            logger.error(f"Order processing failed", extra={
                "order_id": order_id,
                "error": str(e)
            })
            return SwaigFunctionResult("Error processing order.")

5. Health Checks (20 min)

Basic Health Endpoint

from signalwire_agents import AgentServer

server = AgentServer()
server.register(MyAgent())

@server.app.get("/health")
async def health():
    return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}

Comprehensive Health Check

@server.app.get("/health")
async def health():
    checks = {
        "agent": check_agent_health(),
        "database": check_database(),
        "external_api": check_external_api()
    }

    all_healthy = all(c["status"] == "healthy" for c in checks.values())

    return {
        "status": "healthy" if all_healthy else "degraded",
        "checks": checks,
        "timestamp": datetime.utcnow().isoformat()
    }

def check_agent_health():
    try:
        # Verify agent can generate SWML
        agent.get_swml()
        return {"status": "healthy"}
    except Exception as e:
        return {"status": "unhealthy", "error": str(e)}

def check_database():
    try:
        # Ping database
        db.ping()
        return {"status": "healthy"}
    except Exception as e:
        return {"status": "unhealthy", "error": str(e)}

Complete Production Example

#!/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
logging.basicConfig(
    level=os.getenv("LOG_LEVEL", "INFO").upper(),
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)


class ProductionAgent(AgentBase):
    def __init__(self):
        super().__init__(
            name="production-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
            })

        # Configuration
        self.prompt_add_section("Role", "You are a production support agent.")
        self.add_language("English", "en-US", "rime.spore")

        logger.info("ProductionAgent initialized")

    @AgentBase.tool(description="Get system status")
    def get_status(self, args: dict, raw_data: dict = None) -> SwaigFunctionResult:
        logger.info("Status check requested")
        return SwaigFunctionResult("All systems operational.")


def create_app():
    """Create the application."""
    host = os.getenv("HOST", "0.0.0.0")
    port = int(os.getenv("PORT", "3000"))

    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")
        }

    # Ready 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()

Key Takeaways

  1. Environment-based config - No secrets in code
  2. Docker for consistency - Same everywhere
  3. Health checks are essential - Know when things break
  4. Structured logging - Machine-parseable logs
  5. Multiple replicas - Scale horizontally

Preparation for Lab 2.10

  • Install Docker
  • Have cloud account ready (if deploying)
  • Review your agent for production readiness

Lab Preview

In Lab 2.10, you will:

  1. Containerize your agent
  2. Set up Docker Compose
  3. Implement health checks
  4. Deploy to a server

Back to top

SignalWire AI Agents Certification Program