diff --git a/python/agent-framework/sample-agent/.dockerignore b/python/agent-framework/sample-agent/.dockerignore new file mode 100644 index 00000000..645c5929 --- /dev/null +++ b/python/agent-framework/sample-agent/.dockerignore @@ -0,0 +1,29 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +build/ +dist/ +*.egg-info/ + +# IDE +.vscode/ +.idea/ + +# Local files +.env +.env.local +*.log + +# Git +.git/ +.gitignore + +# Documentation +*.md +!README.md diff --git a/python/agent-framework/sample-agent/Dockerfile b/python/agent-framework/sample-agent/Dockerfile new file mode 100644 index 00000000..c279ef4b --- /dev/null +++ b/python/agent-framework/sample-agent/Dockerfile @@ -0,0 +1,49 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Python Agent Framework - Container Deployment +# Related to: https://github.com/microsoft/Agent365-Samples/pull/187 +FROM python:3.12-slim + +WORKDIR /app + +# Suppress apt-get warnings in Docker +ENV DEBIAN_FRONTEND=noninteractive + +# Install system dependencies (gcc needed for some Python packages) +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + curl \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# Upgrade pip +RUN pip install --no-cache-dir --upgrade pip + +# Copy dependency metadata first for better layer caching +COPY pyproject.toml . + +# Install Python dependencies from pyproject.toml +RUN pip install --no-cache-dir . + +# Copy application code +COPY . . + +# Create non-root user for security +RUN useradd --create-home --shell /bin/bash agentuser \ + && chown -R agentuser:agentuser /app +USER agentuser + +# Expose port +EXPOSE 3978 + +# Set environment variables +ENV PORT=3978 +ENV PYTHONUNBUFFERED=1 + +# Health check using PORT env var +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl --fail --silent http://127.0.0.1:${PORT}/api/health || exit 1 + +# Run the agent +CMD ["python", "start_with_generic_host.py"] diff --git a/python/agent-framework/sample-agent/README.md b/python/agent-framework/sample-agent/README.md index e315def9..7a02fa64 100644 --- a/python/agent-framework/sample-agent/README.md +++ b/python/agent-framework/sample-agent/README.md @@ -24,6 +24,88 @@ To set up and test this agent, refer to the [Configure Agent Testing](https://le For a detailed explanation of the agent code and implementation, see the [Agent Code Walkthrough](AGENT-CODE-WALKTHROUGH.md). +## Container Deployment + +Container deployment is the recommended approach for production Agent 365 workloads. Here's why: + +### Why Container Deployment? + +**Production Readiness** +- **Consistency**: Containers ensure identical behavior across development, staging, and production environments, eliminating "works on my machine" issues +- **Isolation**: Each agent runs in its own isolated environment with explicit dependencies, preventing conflicts with other services +- **Scalability**: Container orchestrators (Kubernetes, Azure Container Apps) can automatically scale agent instances based on demand + +**Azure Integration** +- **Azure Container Apps**: Purpose-built for microservices and agents with built-in autoscaling, managed certificates, and seamless Azure service integration +- **Azure Kubernetes Service (AKS)**: Enterprise-grade orchestration for complex multi-agent deployments +- **Azure Container Registry**: Private registry for secure image storage with geo-replication + +**Operational Benefits** +- **Health Checks**: Container runtimes monitor `/api/health` to automatically restart unhealthy agents +- **Rolling Updates**: Deploy new versions with zero downtime using blue-green or canary strategies +- **Resource Limits**: Define CPU/memory boundaries to prevent runaway processes + +### Network Binding Fix + +This sample binds to `0.0.0.0` (all network interfaces) instead of `localhost`. This is **required** for container deployments because: + +- `localhost` (127.0.0.1) only accepts connections from inside the container +- External traffic (Bot Framework messages, health checks) comes from outside the container network +- Binding to `0.0.0.0` allows the agent to receive requests on any network interface + +**Symptoms if using localhost in containers:** +- Container starts but health checks fail with "Connection refused" +- Agent works locally but fails when deployed to Docker/Kubernetes/Container Apps +- Bot Framework cannot reach the `/api/messages` endpoint + +### Build and run with Docker + +```bash +# Navigate to the sample directory first +cd python/agent-framework/sample-agent + +# Build the container image +docker build -t python-agent . + +# Run with required environment variables +docker run -p 3978:3978 \ + -e AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com/ \ + -e AZURE_OPENAI_API_KEY=your-key \ + -e AZURE_OPENAI_DEPLOYMENT=gpt-4o \ + python-agent +``` + +### Azure Container Apps + +```bash +# Navigate to the sample directory +cd python/agent-framework/sample-agent + +# Ensure you're logged in to Azure CLI +az login +az account set --subscription + +# Build and push to Azure Container Registry +az acr build --registry --image python-agent:latest . + +# Create Container App with required environment variables +az containerapp create \ + --name python-agent \ + --resource-group \ + --environment \ + --image .azurecr.io/python-agent:latest \ + --target-port 3978 \ + --ingress external \ + --env-vars \ + AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com/ \ + AZURE_OPENAI_API_KEY=secretref:openai-key \ + AZURE_OPENAI_DEPLOYMENT=gpt-4o + +# Note: For production, use Azure Container Apps secrets for API keys: +# az containerapp secret set --name python-agent --resource-group \ +# --secrets openai-key= +``` + ## Support For issues, questions, or feedback: diff --git a/python/agent-framework/sample-agent/host_agent_server.py b/python/agent-framework/sample-agent/host_agent_server.py index 00a80375..219f382f 100644 --- a/python/agent-framework/sample-agent/host_agent_server.py +++ b/python/agent-framework/sample-agent/host_agent_server.py @@ -1,4 +1,5 @@ -# Copyright (c) Microsoft. All rights reserved. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. """Generic Agent Host Server - Hosts agents implementing AgentInterface""" @@ -332,12 +333,13 @@ async def anonymous_claims(request, handler): print(f"šŸ¢ {self.agent_class.__name__}") print("=" * 80) print(f"šŸ”’ Auth: {'Enabled' if auth_configuration else 'Anonymous'}") - print(f"šŸš€ Server: localhost:{port}") - print(f"šŸ“š Endpoint: http://localhost:{port}/api/messages") - print(f"ā¤ļø Health: http://localhost:{port}/api/health\n") + print(f"šŸš€ Server: 0.0.0.0:{port}") + print(f"šŸ“š Endpoint: http://0.0.0.0:{port}/api/messages") + print(f"ā¤ļø Health: http://0.0.0.0:{port}/api/health") + print(" (Use localhost:PORT for local testing)\n") try: - run_app(app, host="localhost", port=port, handle_signals=True) + run_app(app, host="0.0.0.0", port=port, handle_signals=True) except KeyboardInterrupt: print("\nšŸ‘‹ Server stopped")