├── .env.sample ├── requirements.txt ├── .vscode └── tasks.json ├── azure.yaml ├── infra ├── main.parameters.json └── main.bicep ├── .dockerignore ├── LICENSE ├── Dockerfile ├── .gitignore ├── .github └── copilot-instructions.md ├── README.md ├── templates └── chat.html └── main.py /.env.sample: -------------------------------------------------------------------------------- 1 | AZURE_OPENAI_ENDPOINT= 2 | POOL_MANAGEMENT_ENDPOINT= 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.111.0 2 | azure-identity==1.16.1 3 | python-dotenv==1.0.1 4 | semantic-kernel>=1.35.0 5 | httpx 6 | jinja2 7 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Run FastAPI Development Server", 6 | "type": "shell", 7 | "command": "fastapi dev main.py", 8 | "isBackground": true, 9 | "problemMatcher": [], 10 | "group": "build" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/refs/heads/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: semantic-kernel-sessions 4 | 5 | infra: 6 | provider: bicep 7 | 8 | services: 9 | chat-api: 10 | project: ./ 11 | host: containerapp 12 | language: python 13 | docker: 14 | path: Dockerfile 15 | context: ./ 16 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "environmentName": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION}" 10 | }, 11 | "resourceGroupName": { 12 | "value": "rg-${AZURE_ENV_NAME}" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | **/.venv 3 | **/.classpath 4 | **/.dockerignore 5 | **/.env 6 | **/.git 7 | **/.gitignore 8 | **/.project 9 | **/.settings 10 | **/.toolstarget 11 | **/.vs 12 | **/.vscode 13 | **/*.*proj.user 14 | **/*.dbmdl 15 | **/*.jfm 16 | **/bin 17 | **/charts 18 | **/docker-compose* 19 | **/compose* 20 | **/Dockerfile* 21 | **/node_modules 22 | **/npm-debug.log 23 | **/obj 24 | **/secrets.dev.yaml 25 | **/values.dev.yaml 26 | LICENSE 27 | README.md 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Azure Samples 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # For more information, please refer to https://aka.ms/vscode-docker-python 2 | FROM python:3.12-slim 3 | 4 | # Force rebuild timestamp 5 | RUN echo "Build timestamp: $(date)" > /build_info.txt 6 | 7 | # Keeps Python from generating .pyc files in the container 8 | ENV PYTHONDONTWRITEBYTECODE=1 9 | 10 | # Turns off buffering for easier container logging 11 | ENV PYTHONUNBUFFERED=1 12 | 13 | # Update Debian packages and install security updates 14 | RUN apt-get update && \ 15 | apt-get upgrade -y && \ 16 | apt-get install -y --no-install-recommends \ 17 | ca-certificates && \ 18 | apt-get clean && \ 19 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 20 | 21 | # Install pip requirements 22 | COPY requirements.txt . 23 | RUN python -m pip install --no-cache-dir --upgrade pip && \ 24 | python -m pip install --no-cache-dir -r requirements.txt 25 | 26 | WORKDIR /app 27 | COPY . /app 28 | 29 | EXPOSE 8000 30 | 31 | # Creates a non-root user with an explicit UID and adds permission to access the /app folder 32 | # For more info, please refer to https://aka.ms/vscode-docker-python-configure-containers 33 | RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app 34 | USER appuser 35 | 36 | # During debugging, this entry point will be overridden. For more information, please refer to https://aka.ms/vscode-docker-python-debug 37 | CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Environments 55 | .env 56 | .venv 57 | env/ 58 | venv/ 59 | ENV/ 60 | env.bak/ 61 | venv.bak/ 62 | 63 | # Azure deployment files (keep the templates but ignore environment-specific files) 64 | # Azure Developer CLI 65 | .azure/ 66 | .azd/ 67 | 68 | # IDE files 69 | .vscode/settings.json 70 | .idea/ 71 | *.swp 72 | *.swo 73 | *~ 74 | 75 | # OS files 76 | .DS_Store 77 | Thumbs.db 78 | 79 | # Temporary files 80 | *.tmp 81 | 82 | # Security - Never commit secrets or credentials 83 | *.key 84 | *.pem 85 | *.p12 86 | *.pfx 87 | secrets.yaml 88 | secrets.json 89 | config.json 90 | .credentials 91 | .auth 92 | *.secrets 93 | *.temp 94 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | 2 | - [x] Verify that the copilot-instructions.md file in the .github directory is created. 3 | 4 | - [x] Clarify Project Requirements 5 | 6 | 7 | - [x] Scaffold the Project 8 | 9 | - [ ] Scaffold the Project 10 | 18 | 19 | - [ ] Customize the Project 20 | 21 | 22 | - [x] Install Required Extensions 23 | 24 | 25 | - [x] Compile the Project 26 | 32 | 33 | - [x] Create and Run Task 34 | 39 | 40 | - [x] Launch the Project 41 | 42 | 43 | - [x] Ensure Documentation is Complete 44 | 45 | 46 | 94 | - Work through each checklist item systematically. 95 | - Keep communication concise and focused. 96 | - Follow development best practices. 97 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | @minLength(3) 4 | @maxLength(15) 5 | @description('The name of the environment. Used to generate resource names.') 6 | param environmentName string 7 | 8 | @description('Primary location for all resources') 9 | param location string = resourceGroup().location 10 | 11 | @minLength(0) 12 | @maxLength(12) 13 | @description('Optional suffix appended to key vault name to avoid conflicts') 14 | param keyVaultSuffix string = 'z' 15 | 16 | var resourceToken = uniqueString(subscription().id, resourceGroup().id, location, environmentName) 17 | var resourcePrefix = 'sks' // semantic-kernel-sessions 18 | 19 | // Common resources 20 | resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { 21 | name: 'az-${resourcePrefix}-la-${resourceToken}' 22 | location: location 23 | properties: { 24 | sku: { 25 | name: 'PerGB2018' 26 | } 27 | retentionInDays: 30 28 | } 29 | tags: { 30 | 'azd-env-name': environmentName 31 | } 32 | } 33 | 34 | resource appInsights 'Microsoft.Insights/components@2020-02-02' = { 35 | name: 'az-${resourcePrefix}-ai-${resourceToken}' 36 | location: location 37 | kind: 'web' 38 | properties: { 39 | Application_Type: 'web' 40 | WorkspaceResourceId: logAnalytics.id 41 | } 42 | tags: { 43 | 'azd-env-name': environmentName 44 | } 45 | } 46 | 47 | resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { 48 | name: 'az-${resourcePrefix}-kv-${resourceToken}${keyVaultSuffix}' 49 | location: location 50 | properties: { 51 | sku: { 52 | family: 'A' 53 | name: 'standard' 54 | } 55 | tenantId: subscription().tenantId 56 | enableRbacAuthorization: true 57 | } 58 | tags: { 59 | 'azd-env-name': environmentName 60 | } 61 | } 62 | 63 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 64 | name: 'az-${resourcePrefix}-mi-${resourceToken}' 65 | location: location 66 | tags: { 67 | 'azd-env-name': environmentName 68 | } 69 | } 70 | 71 | // Role assignment for Key Vault Secrets Officer 72 | resource keyVaultSecretsOfficerRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 73 | scope: keyVault 74 | name: guid(keyVault.id, managedIdentity.id, 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7') 75 | properties: { 76 | principalId: managedIdentity.properties.principalId 77 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7') // Key Vault Secrets Officer 78 | principalType: 'ServicePrincipal' 79 | } 80 | } 81 | 82 | // Container Registry 83 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { 84 | name: 'az${resourcePrefix}cr${resourceToken}' 85 | location: location 86 | sku: { 87 | name: 'Basic' 88 | } 89 | properties: { 90 | adminUserEnabled: false 91 | } 92 | tags: { 93 | 'azd-env-name': environmentName 94 | } 95 | } 96 | 97 | // Role assignment for ACR Pull 98 | resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 99 | scope: containerRegistry 100 | name: guid(containerRegistry.id, managedIdentity.id, '7f951dda-4ed3-4680-a7ca-43fe172d538d') 101 | properties: { 102 | principalId: managedIdentity.properties.principalId 103 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') // AcrPull 104 | principalType: 'ServicePrincipal' 105 | } 106 | } 107 | 108 | // Azure OpenAI 109 | resource openAI 'Microsoft.CognitiveServices/accounts@2024-10-01' = { 110 | name: 'az-${resourcePrefix}-oai-${resourceToken}' 111 | location: location 112 | kind: 'OpenAI' 113 | sku: { 114 | name: 'S0' 115 | } 116 | properties: { 117 | customSubDomainName: 'az-${resourcePrefix}-oai-${resourceToken}' 118 | publicNetworkAccess: 'Enabled' 119 | } 120 | tags: { 121 | 'azd-env-name': environmentName 122 | } 123 | } 124 | 125 | // OpenAI GPT-3.5 Turbo deployment 126 | resource gptDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = { 127 | parent: openAI 128 | name: 'gpt-35-turbo' 129 | sku: { 130 | name: 'Standard' 131 | capacity: 20 132 | } 133 | properties: { 134 | model: { 135 | format: 'OpenAI' 136 | name: 'gpt-35-turbo' 137 | // version removed - will use latest default version 138 | } 139 | } 140 | } 141 | 142 | // Role assignment for Cognitive Services OpenAI User 143 | resource openAIUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 144 | scope: openAI 145 | name: guid(openAI.id, managedIdentity.id, '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd') 146 | properties: { 147 | principalId: managedIdentity.properties.principalId 148 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd') // Cognitive Services OpenAI User 149 | principalType: 'ServicePrincipal' 150 | } 151 | } 152 | 153 | // Container Apps Environment 154 | resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = { 155 | name: 'az-${resourcePrefix}-cae-${resourceToken}' 156 | location: location 157 | properties: { 158 | appLogsConfiguration: { 159 | destination: 'log-analytics' 160 | logAnalyticsConfiguration: { 161 | customerId: logAnalytics.properties.customerId 162 | sharedKey: logAnalytics.listKeys().primarySharedKey 163 | } 164 | } 165 | } 166 | tags: { 167 | 'azd-env-name': environmentName 168 | } 169 | } 170 | 171 | // Session pool for code execution - using latest API version with correct schema 172 | resource sessionPool 'Microsoft.App/sessionPools@2024-10-02-preview' = { 173 | name: 'az-${resourcePrefix}-sp-${resourceToken}' 174 | location: location 175 | properties: { 176 | environmentId: containerAppsEnvironment.id 177 | poolManagementType: 'Dynamic' 178 | containerType: 'PythonLTS' 179 | scaleConfiguration: { 180 | maxConcurrentSessions: 10 181 | readySessionInstances: 2 182 | } 183 | dynamicPoolConfiguration: { 184 | executionType: 'Timed' 185 | cooldownPeriodInSeconds: 300 186 | } 187 | sessionNetworkConfiguration: { 188 | status: 'EgressEnabled' 189 | } 190 | } 191 | tags: { 192 | 'azd-env-name': environmentName 193 | } 194 | } 195 | 196 | // Session Pool User role assignment for managed identity 197 | resource sessionPoolUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 198 | name: guid(sessionPool.id, managedIdentity.id, 'Session Pool User') 199 | scope: sessionPool 200 | properties: { 201 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0fb8eba5-a2bb-4abe-b1c1-49dfad359bb0') // Session Pool User 202 | principalType: 'ServicePrincipal' 203 | principalId: managedIdentity.properties.principalId 204 | } 205 | } 206 | 207 | // Chat API Container App 208 | resource chatApi 'Microsoft.App/containerApps@2023-05-01' = { 209 | name: 'az-${resourcePrefix}-ca-${resourceToken}' 210 | location: location 211 | identity: { 212 | type: 'UserAssigned' 213 | userAssignedIdentities: { 214 | '${managedIdentity.id}': {} 215 | } 216 | } 217 | properties: { 218 | managedEnvironmentId: containerAppsEnvironment.id 219 | configuration: { 220 | ingress: { 221 | external: true 222 | targetPort: 8000 223 | allowInsecure: false 224 | corsPolicy: { 225 | allowedOrigins: ['*'] 226 | allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] 227 | allowedHeaders: ['*'] 228 | } 229 | } 230 | registries: [ 231 | { 232 | server: containerRegistry.properties.loginServer 233 | identity: managedIdentity.id 234 | } 235 | ] 236 | } 237 | template: { 238 | containers: [ 239 | { 240 | name: 'chat-api' 241 | image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' 242 | resources: { 243 | cpu: json('0.5') 244 | memory: '1.0Gi' 245 | } 246 | env: [ 247 | { 248 | name: 'AZURE_OPENAI_ENDPOINT' 249 | value: openAI.properties.endpoint 250 | } 251 | { 252 | name: 'POOL_MANAGEMENT_ENDPOINT' 253 | value: sessionPool.properties.poolManagementEndpoint 254 | } 255 | { 256 | name: 'AZURE_CLIENT_ID' 257 | value: managedIdentity.properties.clientId 258 | } 259 | ] 260 | } 261 | ] 262 | scale: { 263 | minReplicas: 1 264 | maxReplicas: 3 265 | } 266 | } 267 | } 268 | tags: { 269 | 'azd-service-name': 'chat-api' 270 | 'azd-env-name': environmentName 271 | } 272 | } 273 | 274 | // Outputs 275 | output RESOURCE_GROUP_ID string = resourceGroup().id 276 | output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.properties.loginServer 277 | output AZURE_OPENAI_ENDPOINT string = openAI.properties.endpoint 278 | output POOL_MANAGEMENT_ENDPOINT string = sessionPool.properties.poolManagementEndpoint 279 | output CHAT_API_URL string = 'https://${chatApi.properties.configuration.ingress.fqdn}' 280 | output AZURE_CLIENT_ID string = managedIdentity.properties.clientId 281 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Container Apps Python Code Interpreter Sessions 2 | 3 | This project demonstrates how to use **Azure Container Apps dynamic sessions** with Python code interpreter capabilities to create an AI-powered application that can execute Python code securely and return results in real-time. 4 | 5 | ## Overview 6 | 7 | The application is a FastAPI-based web API that leverages **Azure Container Apps Python code interpreter sessions** to execute Python code dynamically. When users ask mathematical questions or request calculations, the application automatically detects the need for code execution and runs Python code in isolated, secure session pools. For general conversation, it uses Azure OpenAI for natural language responses. 8 | 9 | ## Features 10 | 11 | - **Python Code Interpreter Sessions**: Primary feature using Azure Container Apps dynamic sessions for secure Python code execution 12 | - **Intelligent Request Routing**: Automatically detects when Python code execution is needed vs. general conversation 13 | - **Secure Isolated Execution**: Each code execution runs in a separate, secure container session 14 | - **Azure OpenAI Integration**: Handles conversational AI and generates Python code when needed 15 | - **FastAPI Web API**: Provides RESTful endpoints and interactive web interface 16 | - **Session Management**: Tracks and manages multiple concurrent Python execution sessions 17 | 18 | ## Prerequisites 19 | 20 | - Python 3.10 or later 21 | - Azure subscription with access to: 22 | - **Azure Container Apps** (for Python session pools) 23 | - **Azure OpenAI Service** (for AI conversations and code generation) 24 | - Azure CLI installed and configured 25 | - [Azure Developer CLI (azd)](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd) installed 26 | - Git 27 | 28 | ## Quick Start with Azure Developer CLI (azd) 29 | 30 | The easiest way to deploy this application is using the Azure Developer CLI (azd): 31 | 32 | ### 1. Initialize and Deploy 33 | 34 | ```bash 35 | # Clone the repository 36 | git clone 37 | cd aca-python-code-interpreter-session 38 | 39 | # Login to Azure 40 | azd auth login 41 | 42 | # Initialize the project (first time only) 43 | azd init 44 | 45 | # Deploy to Azure (provisions resources and deploys the app) 46 | azd up 47 | ``` 48 | 49 | The `azd up` command will: 50 | 51 | - Create a new resource group 52 | - **Deploy Azure Container Apps session pool** configured for Python code interpretation 53 | - Deploy Azure OpenAI service with GPT-3.5 Turbo model 54 | - Deploy the FastAPI application to Azure Container Apps 55 | - Configure managed identity and permissions for secure session access 56 | 57 | ### 2. Access Your Application 58 | 59 | After deployment, azd will provide you with: 60 | - **Application URL**: Access your chat interface at `/ui` 61 | - **API Documentation**: View API docs at `/docs` 62 | - **Environment Details**: See resource details with `azd show` 63 | 64 | ### 3. Manage Your Deployment 65 | 66 | ```bash 67 | # View deployment status and URLs 68 | azd show 69 | 70 | # Redeploy after code changes 71 | azd deploy 72 | 73 | # View logs 74 | azd logs 75 | 76 | # Clean up resources 77 | azd down 78 | ``` 79 | 80 | ## Local Development 81 | 82 | ### 1. Setup Environment 83 | 84 | ```bash 85 | # Create virtual environment 86 | python -m venv .venv 87 | 88 | # Activate virtual environment 89 | # Windows: 90 | .venv\Scripts\activate 91 | # Linux/macOS: 92 | source .venv/bin/activate 93 | 94 | # Install dependencies 95 | pip install -r requirements.txt 96 | ``` 97 | 98 | ### 2. Configure Environment Variables 99 | 100 | Copy `.env.sample` to `.env` and configure: 101 | 102 | ```env 103 | AZURE_OPENAI_ENDPOINT= 104 | POOL_MANAGEMENT_ENDPOINT= 105 | ``` 106 | 107 | ### 3. Run Locally 108 | 109 | ```bash 110 | fastapi dev main.py 111 | ``` 112 | 113 | Access the application at: 114 | 115 | - **Chat Interface**: 116 | - **API Documentation**: 117 | 118 | ## How Python Session Pools Work 119 | 120 | This application showcases **Azure Container Apps dynamic sessions** with Python code interpreter capabilities: 121 | 122 | 1. **Session Creation**: When code execution is needed, the app requests a new Python session from the session pool 123 | 2. **Code Execution**: Python code runs in an isolated, secure container environment 124 | 3. **State Persistence**: Variables and imports persist within the same session for follow-up questions 125 | 4. **Resource Management**: Sessions automatically scale up/down based on demand and have configurable timeouts 126 | 5. **Security**: Each user gets isolated execution environments with no cross-contamination 127 | 128 | ### Example Interaction Flow 129 | 130 | ```text 131 | User: "What is 25 * 847?" 132 | App: Detects math → Generates Python code → Executes in session pool 133 | Session Pool: Runs "print(25 * 847)" → Returns "21175" 134 | App: Returns formatted result to user 135 | 136 | User: "What's the square root of that?" 137 | App: Uses same session → Executes "import math; print(math.sqrt(21175))" 138 | Session Pool: Returns "145.51..." (remembers previous calculation) 139 | ``` 140 | 141 | ## Manual Azure Deployment (Alternative) 142 | 143 | If you prefer manual deployment without azd, you can use Azure CLI: 144 | 145 | ### Create Azure Resources 146 | 147 | ```bash 148 | # Set variables 149 | RESOURCE_GROUP_NAME=aca-sessions-tutorial 150 | AZURE_OPENAI_LOCATION=swedencentral 151 | AZURE_OPENAI_NAME= 152 | SESSION_POOL_LOCATION=eastasia 153 | SESSION_POOL_NAME=code-interpreter-pool 154 | 155 | # Create resource group 156 | az group create --name $RESOURCE_GROUP_NAME --location $SESSION_POOL_LOCATION 157 | 158 | # Create Azure OpenAI service 159 | az cognitiveservices account create \ 160 | --name $AZURE_OPENAI_NAME \ 161 | --resource-group $RESOURCE_GROUP_NAME \ 162 | --location $AZURE_OPENAI_LOCATION \ 163 | --kind OpenAI \ 164 | --sku s0 \ 165 | --custom-domain $AZURE_OPENAI_NAME 166 | 167 | # Deploy GPT-3.5 Turbo model 168 | az cognitiveservices account deployment create \ 169 | --resource-group $RESOURCE_GROUP_NAME \ 170 | --name $AZURE_OPENAI_NAME \ 171 | --deployment-name gpt-35-turbo \ 172 | --model-name gpt-35-turbo \ 173 | --model-version "1106" \ 174 | --model-format OpenAI \ 175 | --sku-capacity "100" \ 176 | --sku-name "Standard" 177 | 178 | # Create session pool 179 | az containerapp sessionpool create \ 180 | --name $SESSION_POOL_NAME \ 181 | --resource-group $RESOURCE_GROUP_NAME \ 182 | --location $SESSION_POOL_LOCATION \ 183 | --max-sessions 100 \ 184 | --container-type PythonLTS \ 185 | --cooldown-period 300 186 | 187 | # Deploy application 188 | ENVIRONMENT_NAME=aca-sessions-tutorial-env 189 | CONTAINER_APP_NAME=chat-api 190 | 191 | az containerapp up \ 192 | --name $CONTAINER_APP_NAME \ 193 | --resource-group $RESOURCE_GROUP_NAME \ 194 | --location $SESSION_POOL_LOCATION \ 195 | --environment $ENVIRONMENT_NAME \ 196 | --env-vars "AZURE_OPENAI_ENDPOINT=" "POOL_MANAGEMENT_ENDPOINT=" \ 197 | --source . 198 | ``` 199 | 200 | **Note**: Configure managed identity and permissions as described in the [Azure Container Apps Sessions Tutorial](https://learn.microsoft.com/en-us/azure/container-apps/sessions-tutorial-semantic-kernel). 201 | 202 | ## API Usage 203 | 204 | ### Chat Endpoint 205 | 206 | **GET** `/chat?message=` 207 | 208 | Example: 209 | 210 | ```bash 211 | GET /chat?message=What time is it right now? 212 | ``` 213 | 214 | Response: 215 | 216 | ```json 217 | { 218 | "output": "The current time is 2024-01-15 14:30:25 UTC" 219 | } 220 | ``` 221 | 222 | ### Interactive Web Interface 223 | 224 | Visit `/ui` for a web-based chat interface that demonstrates Python code interpreter sessions: 225 | 226 | - **Math and calculation questions** automatically trigger secure Python code execution in session pools 227 | - **Code-related requests** are processed in isolated container sessions with persistent state 228 | - **Regular conversation** gets AI responses without code execution 229 | - **Session tracking** shows which responses used Python interpretation vs. conversational AI 230 | 231 | ## Key Components 232 | 233 | ### Azure Container Apps Python Session Pools 234 | 235 | - **Primary feature**: Secure, isolated Python code execution environments 236 | - **Dynamic scaling**: Sessions are created and destroyed based on demand 237 | - **Persistent state**: Each session maintains Python variable state during the conversation 238 | - **Security**: Complete isolation between different user sessions 239 | 240 | ### Intelligent Request Router 241 | 242 | - Automatically detects when Python code execution is needed 243 | - Routes mathematical and computational requests to session pools 244 | - Handles conversational requests with Azure OpenAI directly 245 | - Seamlessly combines both capabilities in a single interface 246 | 247 | ### Azure OpenAI Integration 248 | 249 | - Generates Python code for mathematical and computational problems 250 | - Provides conversational AI responses for general queries 251 | - Uses Semantic Kernel framework for structured AI interactions 252 | 253 | ### Authentication & Security 254 | 255 | - Uses Azure DefaultAzureCredential for secure service-to-service authentication 256 | - Managed identity configuration for session pool access 257 | - No hardcoded credentials or connection strings 258 | 259 | ## Troubleshooting 260 | 261 | ### Common Issues 262 | 263 | 1. **Authentication Errors**: Ensure proper role assignments for Azure services 264 | 2. **Environment Variables**: Verify all required environment variables are set 265 | 3. **Python Version**: Use Python 3.10 or later for compatibility 266 | 267 | ### Required Azure Roles 268 | 269 | - **Cognitive Services OpenAI User**: For Azure OpenAI access 270 | - **Azure ContainerApps Session Executor**: For session pool access 271 | 272 | ## License 273 | 274 | This project is licensed under the MIT License - see the LICENSE file for details. 275 | 276 | ## References 277 | 278 | - [Azure Container Apps Dynamic Sessions Overview](https://learn.microsoft.com/en-us/azure/container-apps/sessions) 279 | - [Azure Container Apps Sessions with Python Tutorial](https://learn.microsoft.com/en-us/azure/container-apps/sessions-tutorial-semantic-kernel) 280 | - [Azure Container Apps Sessions Code Interpreter](https://learn.microsoft.com/en-us/azure/container-apps/sessions-code-interpreter) 281 | - [Azure Developer CLI Documentation](https://learn.microsoft.com/azure/developer/azure-developer-cli/) 282 | - [Azure Container Apps Documentation](https://learn.microsoft.com/en-us/azure/container-apps/) 283 | - [FastAPI Documentation](https://fastapi.tiangolo.com/) 284 | -------------------------------------------------------------------------------- /templates/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Semantic Kernel Session Pool Demo 7 | 118 | 119 | 120 |
121 |
122 |

Semantic Kernel Session Pool Demo

123 |

Ask math questions to see Python code execution in Azure Container Apps Session Pools!

124 |
125 | 126 |
127 |
128 | Session Pool Status: Connecting...
129 | Current Session ID: None 130 |
131 | 132 |
133 |
Welcome! I can help you with math questions using Python code execution. Try asking:
134 |
    135 |
  • "What is 5 squared?"
  • 136 |
  • "Calculate the square root of 144"
  • 137 |
  • "What's 10 * 25 + 15?"
  • 138 |
139 |
🤖 System Welcome Message
140 |
141 |
142 | 143 |
144 | 151 | 152 |
153 |
154 | 155 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import re 4 | 5 | import dotenv 6 | import httpx 7 | from azure.identity import DefaultAzureCredential 8 | from fastapi import FastAPI, Request 9 | from fastapi.responses import RedirectResponse, HTMLResponse 10 | from fastapi.templating import Jinja2Templates 11 | from semantic_kernel import Kernel 12 | from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion 13 | from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings 14 | from semantic_kernel.contents.chat_history import ChatHistory 15 | from semantic_kernel.functions.kernel_arguments import KernelArguments 16 | 17 | dotenv.load_dotenv() 18 | 19 | app = FastAPI() 20 | templates = Jinja2Templates(directory="templates") 21 | 22 | pool_management_endpoint = os.getenv("POOL_MANAGEMENT_ENDPOINT") 23 | azure_openai_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") 24 | 25 | def auth_callback_factory(scope): 26 | auth_token = None 27 | 28 | async def auth_callback() -> str: 29 | """Auth callback for authentication with Azure services. 30 | This uses Azure's DefaultAzureCredential to get an access token. 31 | """ 32 | nonlocal auth_token 33 | current_utc_timestamp = int(datetime.datetime.now(datetime.timezone.utc).timestamp()) 34 | 35 | if not auth_token or auth_token.expires_on < current_utc_timestamp: 36 | credential = DefaultAzureCredential() 37 | auth_token = credential.get_token(scope) 38 | 39 | return auth_token.token 40 | 41 | return auth_callback 42 | 43 | 44 | async def execute_python_code(code: str) -> dict: 45 | """Execute Python code using Azure Container Apps Session Pool via HTTP API.""" 46 | if not pool_management_endpoint: 47 | return { 48 | "success": False, 49 | "error": "Session pool endpoint not configured", 50 | "output": "" 51 | } 52 | 53 | try: 54 | # Get authentication token with correct scope 55 | credential = DefaultAzureCredential() 56 | token = credential.get_token("https://dynamicsessions.io/.default") 57 | 58 | # Generate a session identifier (use timestamp for uniqueness) 59 | import uuid 60 | session_id = f"session-{uuid.uuid4().hex[:8]}" 61 | 62 | # Create session payload for code execution 63 | session_payload = { 64 | "properties": { 65 | "codeInputType": "inline", 66 | "executionType": "synchronous", 67 | "code": code 68 | } 69 | } 70 | 71 | headers = { 72 | "Authorization": f"Bearer {token.token}", 73 | "Content-Type": "application/json" 74 | } 75 | 76 | # Execute code via session pool API 77 | async with httpx.AsyncClient() as client: 78 | # Build the full URL for code execution 79 | execute_url = f"{pool_management_endpoint}/code/execute?api-version=2024-02-02-preview&identifier={session_id}" 80 | 81 | response = await client.post( 82 | execute_url, 83 | json=session_payload, 84 | headers=headers, 85 | timeout=60.0 86 | ) 87 | 88 | if response.status_code == 200: 89 | result = response.json() 90 | return { 91 | "success": True, 92 | "output": result.get("properties", {}).get("stdout", ""), 93 | "error": result.get("properties", {}).get("stderr", ""), 94 | "result": result, 95 | "session_id": session_id 96 | } 97 | else: 98 | return { 99 | "success": False, 100 | "error": f"HTTP {response.status_code}: {response.text}", 101 | "output": "" 102 | } 103 | 104 | except Exception as e: 105 | return { 106 | "success": False, 107 | "error": str(e), 108 | "output": "" 109 | } 110 | 111 | 112 | def extract_python_code(text: str) -> str: 113 | """Extract Python code from AI response.""" 114 | # Look for code blocks marked with ```python 115 | python_code_pattern = r'```python\n(.*?)\n```' 116 | matches = re.findall(python_code_pattern, text, re.DOTALL) 117 | 118 | if matches: 119 | return matches[0].strip() 120 | 121 | # Look for code blocks marked with ``` 122 | code_pattern = r'```\n(.*?)\n```' 123 | matches = re.findall(code_pattern, text, re.DOTALL) 124 | 125 | if matches: 126 | return matches[0].strip() 127 | 128 | # AGGRESSIVE: Check if the entire response looks like Python code 129 | lines = text.split('\n') 130 | python_lines = [] 131 | 132 | # More aggressive detection for math/calculation responses 133 | for line in lines: 134 | stripped = line.strip() 135 | if stripped and not stripped.startswith('---'): # Ignore separators 136 | # Check if line contains Python-like patterns 137 | if (stripped.startswith(('print(', 'import ', 'from ', 'def ', 'class ', 'if ', 'for ', 'while ', '#', 'result =', 'answer =', 'calc =')) or 138 | 'print(' in stripped or 139 | ' = ' in stripped and not stripped.startswith('#') or 140 | '**' in stripped or # Power operator 141 | ' + ' in stripped or ' - ' in stripped or ' * ' in stripped or ' / ' in stripped or 142 | stripped.endswith(')') and ('(' in stripped)): # Function calls 143 | python_lines.append(line) 144 | 145 | # If we found a significant amount of Python-like content, return it 146 | if python_lines and len(python_lines) >= 1: 147 | return '\n'.join(python_lines).strip() 148 | 149 | # FALLBACK: For simple math expressions, create Python code 150 | # Look for simple math patterns and convert them 151 | simple_math_patterns = [ 152 | r'(\d+\s*[\+\-\*\/\*\*]\s*\d+)', # Basic math operations 153 | r'(\d+\s*\*\*\s*\d+)', # Power operations 154 | r'(\d+\s*%\s*\d+)', # Modulo operations 155 | ] 156 | 157 | for pattern in simple_math_patterns: 158 | matches = re.findall(pattern, text) 159 | if matches: 160 | # Convert to Python code 161 | return f"result = {matches[0]}\nprint(result)" 162 | 163 | return "" 164 | 165 | 166 | @app.get("/") 167 | async def root(): 168 | """Redirect to the chat UI""" 169 | return RedirectResponse("/ui") 170 | 171 | @app.get("/health") 172 | async def health(): 173 | """Health check endpoint""" 174 | return {"status": "healthy", "timestamp": datetime.datetime.now().isoformat()} 175 | 176 | @app.get("/ui", response_class=HTMLResponse) 177 | async def chat_ui(request: Request): 178 | """Serve the chat UI""" 179 | return templates.TemplateResponse("chat.html", {"request": request}) 180 | 181 | 182 | @app.get("/debug") 183 | async def debug_info(): 184 | """Debug endpoint to check environment variables.""" 185 | return { 186 | "pool_endpoint": pool_management_endpoint, 187 | "azure_openai_endpoint": azure_openai_endpoint, 188 | "has_credentials": bool(pool_management_endpoint and azure_openai_endpoint) 189 | } 190 | 191 | 192 | @app.get("/chat") 193 | async def chat(message: str): 194 | """ 195 | Chat endpoint that processes user messages using Semantic Kernel and Azure Session Pools. 196 | Can execute Python code for calculations and programming tasks. 197 | """ 198 | print(f"DEBUG: Received message: {message}") 199 | print(f"DEBUG: Pool endpoint configured: {pool_management_endpoint}") 200 | 201 | kernel = Kernel() 202 | 203 | # Add Azure OpenAI chat completion service 204 | chat_service = AzureChatCompletion( 205 | service_id="chat-gpt", 206 | ad_token_provider=auth_callback_factory("https://cognitiveservices.azure.com/.default"), 207 | endpoint=azure_openai_endpoint, 208 | deployment_name="gpt-35-turbo", 209 | ) 210 | kernel.add_service(chat_service) 211 | 212 | # Create a chat history with the user's message 213 | chat_history = ChatHistory() 214 | 215 | # Smart detection for math/calculation questions 216 | math_keywords = ['calculate', 'squared', 'square', 'root', 'solve', 'what is', 'what\'s', 'how much', 217 | 'percentage', 'percent', 'times', 'multiply', 'divide', 'addition', 'subtract', 'plus', 'minus'] 218 | math_operators = ['+', '-', '*', '/', '=', '^', '**', 'x', ' x ', '×'] 219 | number_pattern = r'\b\d+\b' 220 | 221 | message_lower = message.lower() 222 | has_math_keywords = any(keyword in message_lower for keyword in math_keywords) 223 | has_math_operators = any(op in message_lower for op in math_operators) 224 | has_numbers = bool(re.search(number_pattern, message)) 225 | 226 | # Enhanced detection: if it has numbers and math operators, or numbers and math keywords 227 | is_math_question = (has_numbers and has_math_operators) or (has_math_keywords and has_numbers) 228 | 229 | # Debug logging for math detection 230 | print(f"DEBUG: Math detection for '{message}':") 231 | print(f"DEBUG: has_math_keywords={has_math_keywords}, has_math_operators={has_math_operators}, has_numbers={has_numbers}") 232 | print(f"DEBUG: is_math_question={is_math_question}") 233 | 234 | if is_math_question: 235 | # Enhanced prompt for code generation 236 | enhanced_prompt = f"""You are an AI assistant that MUST use Python code execution for mathematical calculations. 237 | 238 | CRITICAL RULES: 239 | 1. For math questions, you MUST write Python code 240 | 2. ALWAYS format Python code using ```python code blocks 241 | 3. Use print() statements to display results 242 | 243 | User question: "{message}" 244 | 245 | Provide Python code in ```python blocks that calculates and prints the answer.""" 246 | chat_history.add_user_message(enhanced_prompt) 247 | else: 248 | # Regular conversation prompt 249 | regular_prompt = f"""You are a helpful AI assistant. Respond naturally to the user's question. 250 | 251 | User: {message}""" 252 | chat_history.add_user_message(regular_prompt) 253 | 254 | try: 255 | # Create proper execution settings 256 | settings = PromptExecutionSettings( 257 | service_id="chat-gpt", 258 | max_tokens=500, 259 | temperature=0.7 260 | ) 261 | 262 | # Get response from Azure OpenAI 263 | response = await chat_service.get_chat_message_contents( 264 | chat_history=chat_history, 265 | settings=settings, 266 | kernel=kernel, 267 | arguments=KernelArguments() 268 | ) 269 | 270 | ai_response = str(response[0].content) if response and len(response) > 0 else "No response generated" 271 | 272 | # Check if the AI response contains Python code 273 | python_code = extract_python_code(ai_response) 274 | 275 | # Debug logging 276 | print(f"DEBUG: AI response length: {len(ai_response)}") 277 | print(f"DEBUG: Extracted Python code: {python_code}") 278 | print(f"DEBUG: Pool endpoint: {pool_management_endpoint}") 279 | 280 | result = { 281 | "output": ai_response, 282 | "note": "Response from Azure OpenAI via Semantic Kernel", 283 | "debug_extracted_code": python_code if python_code else "No Python code detected", 284 | "debug_pool_endpoint": pool_management_endpoint if pool_management_endpoint else "No pool endpoint configured", 285 | "debug_ai_response_length": len(ai_response), 286 | "debug_contains_code_blocks": "```python" in ai_response or "```" in ai_response 287 | } 288 | 289 | # If there's Python code AND it's a math question, execute it using session pools 290 | if python_code and pool_management_endpoint and is_math_question: 291 | try: 292 | execution_result = await execute_python_code(python_code) 293 | 294 | if execution_result["success"]: 295 | result.update({ 296 | "code_executed": python_code, 297 | "execution_output": execution_result["output"], 298 | "execution_error": execution_result["error"] if execution_result["error"] else None, 299 | "session_id": execution_result.get("session_id", "unknown"), 300 | "note": "AI response with Python code executed in Azure Session Pool" 301 | }) 302 | else: 303 | result.update({ 304 | "code_extracted": python_code, 305 | "execution_failed": execution_result["error"], 306 | "note": "AI response with Python code (execution failed)", 307 | "debug_execution_result": execution_result 308 | }) 309 | except Exception as e: 310 | result.update({ 311 | "code_extracted": python_code, 312 | "execution_exception": str(e), 313 | "note": "AI response with Python code (execution error)" 314 | }) 315 | elif python_code: 316 | result.update({ 317 | "code_extracted": python_code, 318 | "note": f"AI response with Python code (session pool {'not configured' if not pool_management_endpoint else 'available'})" 319 | }) 320 | 321 | return result 322 | 323 | except Exception as e: 324 | return { 325 | "output": f"Error: {str(e)}", 326 | "note": "There was an issue with the Azure OpenAI integration. Please check the logs.", 327 | "debug_info": { 328 | "azure_openai_endpoint": azure_openai_endpoint, 329 | "has_endpoint": bool(azure_openai_endpoint), 330 | "pool_endpoint": pool_management_endpoint 331 | } 332 | } 333 | # Debug build 09/03/2025 20:04:55 334 | --------------------------------------------------------------------------------