diff --git a/.github/workflows/docker-container-sampleagent-python.yml b/.github/workflows/docker-container-sampleagent-python.yml new file mode 100644 index 00000000..541a08f5 --- /dev/null +++ b/.github/workflows/docker-container-sampleagent-python.yml @@ -0,0 +1,277 @@ +name: Deploy Python Agent Framework to Azure Container Apps + +on: + push: + branches: + - users/tirthdoshi/local-playground + paths: + - 'python/agent-framework/sample-agent/**' + - '.github/workflows/docker-container-sampleagent-python.yml' + pull_request: + branches: + - main + paths: + - 'python/agent-framework/sample-agent/**' + - '.github/workflows/docker-container-sampleagent-python.yml' + workflow_dispatch: + +permissions: + id-token: write # Required for OIDC authentication + contents: read + pull-requests: write # Required to comment on PRs + +env: + AZURE_RESOURCE_GROUP: agent365-samples-rg + ACR_NAME: agent365samplesacr + CONTAINER_APP_NAME: agent-framework-python + CONTAINER_APP_ENV: agent365-env + IMAGE_NAME: agent-framework-python + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Azure Login with Service Principal + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Create Resource Group if needed + run: | + if ! az group exists --name ${{ env.AZURE_RESOURCE_GROUP }} --output tsv | grep -q true; then + echo "Creating Resource Group..." + az group create --name ${{ env.AZURE_RESOURCE_GROUP }} --location eastus + fi + + - name: Create ACR if needed + run: | + if ! az acr show --name ${{ env.ACR_NAME }} &> /dev/null; then + echo "Creating ACR..." + az acr create \ + --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ + --name ${{ env.ACR_NAME }} \ + --sku Basic \ + --admin-enabled true + fi + + - name: Login to Azure Container Registry + run: | + az acr login --name ${{ env.ACR_NAME }} + + - name: Build Docker Image + run: | + docker build \ + -t ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }} \ + -t ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:latest \ + -f python/agent-framework/sample-agent/Dockerfile \ + python/agent-framework/sample-agent + + - name: Push Docker Image to ACR + run: | + docker push ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }} + docker push ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:latest + + - name: Create Container App Environment if needed + run: | + if ! az containerapp env show --name ${{ env.CONTAINER_APP_ENV }} --resource-group ${{ env.AZURE_RESOURCE_GROUP }} &> /dev/null; then + echo "Creating Container App Environment..." + az containerapp env create \ + --name ${{ env.CONTAINER_APP_ENV }} \ + --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ + --location eastus + fi + + - name: Deploy to Azure Container App + run: | + if az containerapp show --name ${{ env.CONTAINER_APP_NAME }} --resource-group ${{ env.AZURE_RESOURCE_GROUP }} &> /dev/null; then + echo "Updating existing Container App..." + az containerapp update \ + --name ${{ env.CONTAINER_APP_NAME }} \ + --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ + --image ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }} \ + --set-env-vars \ + PORT=3978 \ + AZURE_OPENAI_ENDPOINT=${{ secrets.AZURE_OPENAI_ENDPOINT }} \ + AZURE_OPENAI_DEPLOYMENT=${{ secrets.AZURE_OPENAI_DEPLOYMENT }} \ + AZURE_OPENAI_API_VERSION=${{ secrets.AZURE_OPENAI_API_VERSION }} \ + AZURE_OPENAI_API_KEY=${{ secrets.AZURE_OPENAI_API_KEY }} \ + USE_AGENTIC_AUTH=true \ + ENABLE_OBSERVABILITY=true \ + ENABLE_OTEL=true \ + ENABLE_SENSITIVE_DATA=true \ + PYTHON_ENVIRONMENT=production \ + ENABLE_APPLICATION_INSIGHTS=${{ secrets.ENABLE_APPLICATION_INSIGHTS || 'false' }} \ + APPLICATIONINSIGHTS_CONNECTION_STRING=${{ secrets.APPLICATIONINSIGHTS_CONNECTION_STRING || '' }} \ + CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=${{ secrets.SERVICE_CONNECTION_CLIENT_ID }} \ + CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=${{ secrets.SERVICE_CONNECTION_CLIENT_SECRET }} \ + CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=${{ secrets.SERVICE_CONNECTION_TENANT_ID }} \ + CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=${{ secrets.SERVICE_CONNECTION_SCOPES || 'https://graph.microsoft.com/.default' }} \ + AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization \ + AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=${{ secrets.AGENTIC_SCOPES || 'https://graph.microsoft.com/.default' }} \ + AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALTERNATEBLUEPRINTCONNECTIONNAME=${{ secrets.AGENTIC_CONNECTION_NAME || 'https://graph.microsoft.com/.default' }} \ + CONNECTIONSMAP_0_SERVICEURL='*' \ + CONNECTIONSMAP_0_CONNECTION=SERVICE_CONNECTION + else + echo "Creating new Container App..." + az containerapp create \ + --name ${{ env.CONTAINER_APP_NAME }} \ + --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ + --environment ${{ env.CONTAINER_APP_ENV }} \ + --image ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }} \ + --registry-server ${{ env.ACR_NAME }}.azurecr.io \ + --target-port 3978 \ + --ingress external \ + --min-replicas 1 \ + --max-replicas 3 \ + --cpu 0.5 \ + --memory 1.0Gi \ + --env-vars \ + PORT=3978 \ + AZURE_OPENAI_ENDPOINT=${{ secrets.AZURE_OPENAI_ENDPOINT }} \ + AZURE_OPENAI_DEPLOYMENT=${{ secrets.AZURE_OPENAI_DEPLOYMENT }} \ + AZURE_OPENAI_API_VERSION=${{ secrets.AZURE_OPENAI_API_VERSION }} \ + AZURE_OPENAI_API_KEY=${{ secrets.AZURE_OPENAI_API_KEY }} \ + USE_AGENTIC_AUTH=true \ + ENABLE_OBSERVABILITY=true \ + ENABLE_OTEL=true \ + ENABLE_SENSITIVE_DATA=true \ + PYTHON_ENVIRONMENT=production \ + ENABLE_APPLICATION_INSIGHTS=${{ secrets.ENABLE_APPLICATION_INSIGHTS || 'false' }} \ + APPLICATIONINSIGHTS_CONNECTION_STRING=${{ secrets.APPLICATIONINSIGHTS_CONNECTION_STRING || '' }} \ + CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=${{ secrets.SERVICE_CONNECTION_CLIENT_ID }} \ + CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=${{ secrets.SERVICE_CONNECTION_CLIENT_SECRET }} \ + CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=${{ secrets.SERVICE_CONNECTION_TENANT_ID }} \ + CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=${{ secrets.SERVICE_CONNECTION_SCOPES || 'https://graph.microsoft.com/.default' }} \ + AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization \ + AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=${{ secrets.AGENTIC_SCOPES || 'https://graph.microsoft.com/.default' }} \ + AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALTERNATEBLUEPRINTCONNECTIONNAME=${{ secrets.AGENTIC_CONNECTION_NAME || 'https://graph.microsoft.com/.default' }} \ + CONNECTIONSMAP_0_SERVICEURL='*' \ + CONNECTIONSMAP_0_CONNECTION=SERVICE_CONNECTION + fi + + - name: Get Container App URL + id: get-url + run: | + FQDN=$(az containerapp show \ + --name ${{ env.CONTAINER_APP_NAME }} \ + --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ + --query properties.configuration.ingress.fqdn \ + --output tsv) + echo "๐Ÿš€ Container App deployed successfully!" + echo "๐ŸŒ URL: https://$FQDN" + echo "๐Ÿ“ Health: https://$FQDN/api/health" + echo "๐Ÿ“จ Messages: https://$FQDN/api/messages" + echo "app_url=https://$FQDN" >> $GITHUB_OUTPUT + echo "health_url=https://$FQDN/api/health" >> $GITHUB_OUTPUT + echo "messages_url=https://$FQDN/api/messages" >> $GITHUB_OUTPUT + + - name: View Container App Logs + run: | + echo "๐Ÿ“‹ Fetching recent logs..." + az containerapp logs show \ + --name ${{ env.CONTAINER_APP_NAME }} \ + --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ + --tail 50 \ + --follow false + + - name: Comment on PR with Deployment URL + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const appUrl = '${{ steps.get-url.outputs.app_url }}'; + const healthUrl = '${{ steps.get-url.outputs.health_url }}'; + const messagesUrl = '${{ steps.get-url.outputs.messages_url }}'; + const sha = '${{ github.sha }}'; + + const body = `## ๐Ÿš€ Deployment Successful! + + Your Python Agent Framework has been deployed to Azure Container Apps. + + ### ๐Ÿ”— Deployment Links + - **App URL**: ${appUrl} + - **Health Endpoint**: ${healthUrl} + - **Messages Endpoint**: ${messagesUrl} + + ### ๐Ÿ“ฆ Deployment Details + - **Container App**: \`${{ env.CONTAINER_APP_NAME }}\` + - **Resource Group**: \`${{ env.AZURE_RESOURCE_GROUP }}\` + - **Image Tag**: \`${sha.substring(0, 7)}\` + - **Commit**: ${sha} + + ### ๐Ÿงช Testing with Agents Playground + + #### Option 1: Direct Testing (Simple) + \`\`\`bash + # Check health + curl ${healthUrl} + + # Send a test message (requires authentication) + curl -X POST ${messagesUrl} \\ + -H "Content-Type: application/json" \\ + -d '{"type":"message","text":"Hello Agent!"}' + \`\`\` + + #### Option 2: Test with Agents Playground (Interactive) + + 1. **Install and authenticate ngrok:** + \`\`\`bash + # Download ngrok from https://ngrok.com/download + # Authenticate with your token + ngrok authtoken YOUR_NGROK_TOKEN + \`\`\` + + 2. **Start ngrok tunnel:** + \`\`\`bash + ngrok http 2000 + \`\`\` + Copy the HTTPS forwarding URL (e.g., \`https://abc123.ngrok.io\`) + + 3. **Launch Agents Playground:** + \`\`\`bash + agentsplayground -p 2000 -e ${messagesUrl} --su 'YOUR_NGROK_URL/_connector' + \`\`\` + + Replace \`YOUR_NGROK_URL\` with the ngrok URL from step 2. + + 4. **Test your agent** in the playground UI at http://localhost:2000 + + --- + *Deployed from commit ${sha.substring(0, 7)} by @${{ github.actor }}*`; + + // Find existing comment from this bot + const comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + + const botComment = comments.data.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('๐Ÿš€ Deployment Successful!') + ); + + if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + console.log('Updated existing comment'); + } else { + // Create new comment + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); + console.log('Created new comment'); + } + diff --git a/python/agent-framework/sample-agent/.env.template b/python/agent-framework/sample-agent/.env.template index 07f3d5c2..1750f916 100644 --- a/python/agent-framework/sample-agent/.env.template +++ b/python/agent-framework/sample-agent/.env.template @@ -46,3 +46,9 @@ PYTHON_ENVIRONMENT=development # Enable otel logs on AgentFramework SDK. Required for auto instrumentation ENABLE_OTEL=true ENABLE_SENSITIVE_DATA=true + +# Application Insights Configuration (Optional) +# Set to true to enable Application Insights telemetry +ENABLE_APPLICATION_INSIGHTS=false +# Your Application Insights connection string (required if enabled) +APPLICATIONINSIGHTS_CONNECTION_STRING= diff --git a/python/agent-framework/sample-agent/Dockerfile b/python/agent-framework/sample-agent/Dockerfile new file mode 100644 index 00000000..eb71dc9b --- /dev/null +++ b/python/agent-framework/sample-agent/Dockerfile @@ -0,0 +1,26 @@ +# Use Python 3.11 full Debian image +FROM python:3.11 + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy project files +COPY pyproject.toml ./ +COPY *.py ./ +COPY .env* ./ + +# Install uv +RUN pip install --no-cache-dir uv + +# Install dependencies +RUN uv pip install --system --pre -e . + +EXPOSE 3978 +ENV PORT=3978 + +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..19de51dc 100644 --- a/python/agent-framework/sample-agent/README.md +++ b/python/agent-framework/sample-agent/README.md @@ -3,6 +3,7 @@ This sample demonstrates how to build an agent using Agent Framework in Python with the Microsoft Agent 365 SDK. It covers: - **Observability**: End-to-end tracing, caching, and monitoring for agent applications +- **Application Insights**: Optional integration with Azure Application Insights for production monitoring - **Notifications**: Services and models for managing user notifications - **Tools**: Model Context Protocol tools for building advanced agent solutions - **Hosting Patterns**: Hosting with Microsoft 365 Agents SDK @@ -11,6 +12,18 @@ This sample uses the [Microsoft Agent 365 SDK for Python](https://github.com/mic For comprehensive documentation and guidance on building agents with the Microsoft Agent 365 SDK, including how to add tooling, observability, and notifications, visit the [Microsoft Agent 365 Developer Documentation](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/). +## Optional Features + +### Application Insights Integration + +This sample includes optional Application Insights integration for production monitoring and telemetry. Application Insights provides: +- Distributed tracing and performance monitoring +- Exception tracking and diagnostics +- Live metrics and real-time monitoring +- Custom metrics and events + +**To enable Application Insights**, see the [Application Insights Integration Guide](APPLICATION_INSIGHTS.md) for detailed setup instructions. + ## Prerequisites - Python 3.x diff --git a/python/agent-framework/sample-agent/agent.py b/python/agent-framework/sample-agent/agent.py index 4cf64639..d616368b 100644 --- a/python/agent-framework/sample-agent/agent.py +++ b/python/agent-framework/sample-agent/agent.py @@ -62,6 +62,14 @@ ) from token_cache import get_cached_agentic_token +# Application Insights (optional) +try: + from azure.monitor.opentelemetry import configure_azure_monitor + APPLICATION_INSIGHTS_AVAILABLE = True +except ImportError: + APPLICATION_INSIGHTS_AVAILABLE = False + logger.warning("Application Insights SDK not available. Install 'azure-monitor-opentelemetry' to enable.") + # @@ -91,6 +99,9 @@ def __init__(self): """Initialize the AgentFramework agent.""" self.logger = logging.getLogger(self.__class__.__name__) + # Initialize Application Insights if enabled + self._setup_application_insights() + # Initialize auto instrumentation with Agent 365 Observability SDK self._enable_agentframework_instrumentation() @@ -178,6 +189,35 @@ def _enable_agentframework_instrumentation(self): except Exception as e: logger.warning(f"โš ๏ธ Instrumentation failed: {e}") + def _setup_application_insights(self): + """Setup Application Insights telemetry (optional)""" + enable_app_insights = os.getenv("ENABLE_APPLICATION_INSIGHTS", "false").lower() == "true" + + if not enable_app_insights: + logger.info("๐Ÿ“Š Application Insights is disabled (set ENABLE_APPLICATION_INSIGHTS=true to enable)") + return + + if not APPLICATION_INSIGHTS_AVAILABLE: + logger.warning("โš ๏ธ Application Insights is enabled but SDK not available. Run: uv pip install azure-monitor-opentelemetry") + return + + connection_string = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING") + if not connection_string: + logger.warning("โš ๏ธ Application Insights enabled but APPLICATIONINSIGHTS_CONNECTION_STRING not set") + return + + try: + # Configure Azure Monitor with OpenTelemetry + configure_azure_monitor( + connection_string=connection_string, + # Enable auto-instrumentation for various libraries + enable_live_metrics=True, + ) + logger.info("โœ… Application Insights configured successfully") + logger.info(f"๐Ÿ“Š Telemetry will be sent to: {connection_string[:50]}...") + except Exception as e: + logger.error(f"โŒ Failed to configure Application Insights: {e}") + # # ========================================================================= diff --git a/python/agent-framework/sample-agent/docker-compose.yml b/python/agent-framework/sample-agent/docker-compose.yml new file mode 100644 index 00000000..4bf7dcd3 --- /dev/null +++ b/python/agent-framework/sample-agent/docker-compose.yml @@ -0,0 +1,25 @@ +# Docker Compose for AgentFramework Sample Agent +version: '3.8' + +services: + agent: + build: + context: . + dockerfile: Dockerfile + ports: + - "3978:3978" + env_file: + - .env + environment: + # Override port to ensure container uses 3978 + - PORT=3978 + volumes: + # Mount .env for local development (optional) + - ./.env:/app/.env + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3978/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s diff --git a/python/agent-framework/sample-agent/host_agent_server.py b/python/agent-framework/sample-agent/host_agent_server.py index 562d019d..7b7917c1 100644 --- a/python/agent-framework/sample-agent/host_agent_server.py +++ b/python/agent-framework/sample-agent/host_agent_server.py @@ -151,27 +151,48 @@ async def help_handler(context: TurnContext, _: TurnState): self.agent_app.conversation_update("membersAdded", auth_handlers=handler)(help_handler) self.agent_app.message("/help", auth_handlers=handler)(help_handler) - @self.agent_app.activity("message", auth_handlers=handler) + @self.agent_app.activity("installationUpdate", auth_handlers=handler) + async def on_installation_update(context: TurnContext, _: TurnState): + """Handle agent installation/uninstallation events""" + try: + action = getattr(context.activity, "action", None) + logger.info(f"๐Ÿ“ฆ Installation event: {action} in conversation {context.activity.conversation.id}") + + # Optionally send a welcome message when installed + if action == "add": + await context.send_activity( + f"๐Ÿ‘‹ Thanks for installing **{self.agent_class.__name__}**! " + "I'm ready to help. Send me a message or type /help to get started." + ) + except Exception as e: + logger.error(f"Error handling installation update: {e}") + + @self.agent_app.activity("message") async def on_message(context: TurnContext, _: TurnState): try: + logger.info(f"๐Ÿ“จ Processing message: '{context.activity.text}'") + result = await self._validate_agent_and_setup_context(context) if result is None: + logger.warning("โš ๏ธ Agent validation failed") return tenant_id, agent_id = result with BaggageBuilder().tenant_id(tenant_id).agent_id(agent_id).build(): user_message = context.activity.text or "" + if not user_message.strip() or user_message.strip() == "/help": + logger.info("โญ๏ธ Skipping empty or help message") return - logger.info(f"๐Ÿ“จ {user_message}") response = await self.agent_instance.process_user_message( user_message, self.agent_app.auth, self.auth_handler_name, context ) + logger.info(f"๐Ÿ“ค Sending response: {response[:100] if len(response) > 100 else response}") await context.send_activity(response) except Exception as e: - logger.error(f"โŒ Error: {e}") + logger.error(f"โŒ Error: {e}", exc_info=True) await context.send_activity(f"Sorry, I encountered an error: {str(e)}") @self.agent_notification.on_agent_notification( @@ -245,6 +266,10 @@ def create_auth_configuration(self) -> AgentAuthConfiguration | None: # --- Server --- def start_server(self, auth_configuration: AgentAuthConfiguration | None = None): async def entry_point(req: Request) -> Response: + # Log incoming request + body = await req.text() + logger.info(f"๐ŸŒ Incoming request to /api/messages") + # Removed logging of request body to avoid exposing sensitive information return await start_agent_process( req, req.app["agent_app"], req.app["adapter"] ) @@ -306,7 +331,8 @@ async def anonymous_claims(request, handler): print(f"โค๏ธ Health: http://localhost:{port}/api/health\n") try: - run_app(app, host="localhost", port=port, handle_signals=True) + # Bind to 0.0.0.0 to accept connections from outside the container + run_app(app, host="0.0.0.0", port=port, handle_signals=True) except KeyboardInterrupt: print("\n๐Ÿ‘‹ Server stopped") diff --git a/python/agent-framework/sample-agent/pyproject.toml b/python/agent-framework/sample-agent/pyproject.toml index 5837d8d0..5c9425e3 100644 --- a/python/agent-framework/sample-agent/pyproject.toml +++ b/python/agent-framework/sample-agent/pyproject.toml @@ -41,7 +41,11 @@ dependencies = [ "microsoft_agents_a365_observability_core >= 0.1.0", "microsoft_agents_a365_observability_extensions_agent_framework >= 0.1.0", "microsoft_agents_a365_runtime >= 0.1.0", - "microsoft_agents_a365_notifications >= 0.1.0" + "microsoft_agents_a365_notifications >= 0.1.0", + + # Application Insights (optional) + "opencensus-ext-azure>=1.1.13", + "azure-monitor-opentelemetry>=1.2.0" ] requires-python = ">=3.11"