Skip to content

Commit 066592e

Browse files
committed
Add example for custom code interpreter
1 parent d3c74ff commit 066592e

File tree

6 files changed

+1505
-0
lines changed

6 files changed

+1505
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
AZURE_AI_PROJECT_ENDPOINT=
2+
AZURE_AI_CONNECTION_ID=
3+
AZURE_AI_MODEL_DEPLOYMENT_NAME=
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Custom Code Interpreter with Session Pool MCP server
2+
3+
This provides example Bicep code for setting up a Container Apps dynamic session pool
4+
with a custom code interpreter image, as well as Python client code demonstrating
5+
how to use it with a Foundry Hosted Agent.
6+
7+
- The `az` CLI
8+
- The `uv` or `pip` Python package managers
9+
10+
## Running code sample
11+
12+
### Enable MCP server for dynamic sessions
13+
14+
This is required to enable the preview feature.
15+
16+
```console
17+
az feature register --namespace Microsoft.App --name SessionPoolsSupportMCP
18+
az provider register -n Microsoft.App
19+
```
20+
21+
### Create a dynamic session pool with a code interpreter image
22+
23+
Run the following, substituting the appropriate values. Set parameters if you wish.
24+
25+
```console
26+
az deployment group create \
27+
--name custom-code-interpreter \
28+
--subscription <your_subscription> \
29+
--resource-group <your_resource_group> \
30+
--template-file ./infra.bicep
31+
```
32+
33+
This can take a while!
34+
35+
### Use the custom code interpreter in an agent
36+
37+
Copy the [`.env.sample`](./.env.sample) file to `.env` and fill in the values with
38+
the output of the above deployment, which you can find in the Web Portal under the
39+
resource group.
40+
41+
Install the Python dependencies (`uv sync` or `pip install`). Finally, run `./main.py`.
42+
43+
## Limitations
44+
45+
File input/output and use of file stores are not directly supported in APIs, so you must use URLs (such as data URLs for small files and Azure Blob Service SAS URLs for large ones) to get data in and out.
46+
47+
## References
48+
49+
- [Azure Container Apps Dynamic Sessions](/azure/container-apps/sessions)
50+
- [Session Pools with Custom Containers](/azure/container-apps/session-pool#custom-container-pool)
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
@description('Suffix for resource names to ensure uniqueness')
2+
@minLength(3)
3+
param suffix string = uniqueString(resourceGroup().id)
4+
5+
@description('Container Apps environment name')
6+
@minLength(3)
7+
param environmentName string = 'aca-env-${suffix}'
8+
9+
@description('Session pool name')
10+
@minLength(3)
11+
param sessionPoolName string = 'sp-${suffix}'
12+
13+
@description('The amount of CPU to provide to each container instance, in vCPU counts')
14+
@minValue(1)
15+
@maxValue(16)
16+
param cpu int = 1
17+
18+
@description('The amount of RAM to provide to each container instance, in GiB')
19+
@minValue(1)
20+
@maxValue(16)
21+
param memory int = 2
22+
23+
@description('Location of all ACA resources.')
24+
@allowed([
25+
'eastus'
26+
'swedencentral'
27+
'northeurope'
28+
])
29+
param location string = 'swedencentral'
30+
31+
@description('Use managed identity for deployment script principal')
32+
param useManagedIdentity bool = true
33+
34+
@description('An image that implements the code interpreter HTTP API')
35+
param image string = 'mcr.microsoft.com/k8se/services/codeinterpreter:0.9.18-python3.12'
36+
37+
@description('Model deployment name')
38+
param modelDeploymentName string = 'my-gpt-4o-mini'
39+
40+
@description('Model to deploy')
41+
param modelName string = 'gpt-4o-mini'
42+
43+
resource environment 'Microsoft.App/managedEnvironments@2025-10-02-preview' = {
44+
name: environmentName
45+
location: location
46+
properties: {
47+
workloadProfiles: [
48+
{
49+
name: 'Consumption'
50+
workloadProfileType: 'Consumption'
51+
}
52+
]
53+
}
54+
}
55+
56+
resource sessionPool 'Microsoft.App/sessionPools@2025-10-02-preview' = {
57+
name: sessionPoolName
58+
location: location
59+
properties: {
60+
environmentId: environment.id
61+
poolManagementType: 'Dynamic'
62+
containerType: 'CustomContainer'
63+
scaleConfiguration: {
64+
maxConcurrentSessions: 10
65+
readySessionInstances: 5
66+
}
67+
dynamicPoolConfiguration: {
68+
lifecycleConfiguration: {
69+
cooldownPeriodInSeconds: 600
70+
lifecycleType: 'Timed'
71+
}
72+
}
73+
customContainerTemplate: {
74+
containers: [
75+
{
76+
name: 'jupyterpython'
77+
image: image
78+
env: [
79+
{
80+
name: 'SYS_RUNTIME_SANDBOX'
81+
value: 'AzureContainerApps-DynamicSessions'
82+
}
83+
{
84+
name: 'AZURE_CODE_EXEC_ENV'
85+
value: 'AzureContainerApps-DynamicSessions-Py3.12'
86+
}
87+
{
88+
name: 'AZURECONTAINERAPPS_SESSIONS_SANDBOX_VERSION'
89+
value: '7758'
90+
}
91+
{
92+
name: 'JUPYTER_TOKEN'
93+
value: 'AzureContainerApps-DynamicSessions'
94+
}
95+
]
96+
resources: {
97+
cpu: cpu
98+
memory: '${memory}Gi'
99+
}
100+
probes: [
101+
{
102+
type: 'Liveness'
103+
httpGet: {
104+
path: '/health'
105+
port: 6000
106+
}
107+
failureThreshold: 4
108+
}
109+
{
110+
type: 'Startup'
111+
httpGet: {
112+
path: '/health'
113+
port: 6000
114+
}
115+
failureThreshold: 30
116+
periodSeconds: 2
117+
}
118+
]
119+
}
120+
]
121+
ingress: {
122+
targetPort: 6000
123+
}
124+
}
125+
mcpServerSettings: {
126+
isMcpServerEnabled: true
127+
}
128+
sessionNetworkConfiguration: {
129+
status: 'egressEnabled'
130+
}
131+
}
132+
}
133+
134+
resource scriptPrincipal 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = if (useManagedIdentity){
135+
name: 'deployScriptIdentity-${suffix}'
136+
location: location
137+
}
138+
139+
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (useManagedIdentity) {
140+
name: guid(scriptPrincipal!.id, 'apps-sessionpool-contributor')
141+
scope: resourceGroup()
142+
properties: {
143+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f7669afb-68b2-44b4-9c5f-6d2a47fddda0') // Container Apps SessionPools Contributor
144+
principalId: scriptPrincipal!.properties.principalId
145+
principalType: 'ServicePrincipal'
146+
}
147+
}
148+
149+
resource deployScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
150+
name: 'getmcpkey-${suffix}'
151+
location: location
152+
kind: 'AzureCLI'
153+
identity: useManagedIdentity ? {
154+
type: 'UserAssigned'
155+
userAssignedIdentities: {
156+
'${scriptPrincipal!.id}': {}
157+
}
158+
} : null
159+
properties: {
160+
azCliVersion: '2.77.0'
161+
scriptContent: '''
162+
az rest --method post --url "$SESSION_POOL_ID/fetchMCPServerCredentials?api-version=2025-02-02-preview" | jq -c '{"key": .apiKey}' > $AZ_SCRIPTS_OUTPUT_PATH
163+
'''
164+
timeout: 'PT30M'
165+
retentionInterval: 'P1D'
166+
cleanupPreference: 'OnSuccess'
167+
environmentVariables: [
168+
{
169+
name: 'SESSION_POOL_ID'
170+
value: sessionPool.id
171+
}
172+
]
173+
}
174+
}
175+
176+
resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-10-01-preview' = {
177+
name: 'aia-${suffix}'
178+
location: location
179+
kind: 'AIServices'
180+
sku: {
181+
name: 'S0'
182+
}
183+
properties: {
184+
customSubDomainName: 'myaiaccount-${suffix}'
185+
allowProjectManagement: true
186+
}
187+
188+
resource project 'projects' = {
189+
name: 'aip-${suffix}s'
190+
properties: {
191+
description: 'This is my AI project.'
192+
}
193+
194+
resource mcpConn 'connections' = {
195+
name: 'aic-${suffix}'
196+
properties: {
197+
authType: 'CustomKeys'
198+
category: 'RemoteTool'
199+
credentials: {
200+
keys: {
201+
'x-ms-apikey': deployScript.properties.outputs.key
202+
}
203+
}
204+
target: sessionPool.properties.mcpServerSettings.mcpServerEndpoint
205+
}
206+
}
207+
}
208+
209+
resource model 'deployments' = {
210+
name: modelDeploymentName
211+
sku: {
212+
name: 'GlobalStandard'
213+
capacity: 1
214+
}
215+
properties: {
216+
model: {
217+
format: 'OpenAI'
218+
name: modelName
219+
}
220+
}
221+
}
222+
}
223+
224+
@description('Outputs the ID of the project connection for the Code Interpreter MCP Tool')
225+
output AZURE_AI_CONNECTION_ID string = aiAccount::project::mcpConn.id
226+
227+
@description('Model deployment name')
228+
output AZURE_AI_MODEL_DEPLOYMENT_NAME string = aiAccount::model.name
229+
230+
@description('AI Project Endpoint')
231+
output AZURE_AI_PROJECT_ENDPOINT string = aiAccount::project.properties.endpoints['AI Foundry API']
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
3+
import dotenv
4+
from azure.ai.projects import AIProjectClient
5+
from azure.ai.projects.models import PromptAgentDefinition, MCPTool
6+
from azure.identity import DefaultAzureCredential
7+
8+
dotenv.load_dotenv()
9+
10+
project_client = AIProjectClient(
11+
endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
12+
credential=DefaultAzureCredential(),
13+
)
14+
openai_client = project_client.get_openai_client()
15+
16+
tools = [
17+
MCPTool(
18+
# This is just a placeholder. Connection details are in
19+
# the project connection referenced by `project_connection_id`.
20+
server_url='https://localhost',
21+
server_label="python_tool",
22+
require_approval='never',
23+
allowed_tools=[
24+
'launchShell',
25+
'runPythonCodeInRemoteEnvironment',
26+
],
27+
project_connection_id=os.environ["AZURE_AI_CONNECTION_ID"]
28+
),
29+
]
30+
31+
EXAMPLE_DATA_FILE_URL="https://raw.githubusercontent.com/Azure/azure-sdk-for-python-pr/refs/heads/ericsuh/custom-code-interpreter/sdk/ai/azure-ai-projects/samples/agents/custom-code-interpreter/sample_data.csv"
32+
33+
with project_client:
34+
agent = project_client.agents.create_version(
35+
agent_name="MyAgent",
36+
definition=PromptAgentDefinition(
37+
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
38+
instructions="""\
39+
You are a helpful agent that can use a Python code interpreter to assist users. Use the `python_tool` MCP
40+
server to perform any calculations or numerical analyses. ALWAYS call the `launchShell` tool first before
41+
calling the `runPythonCodeInRemoteEnvironment` tool. If you need to display any non-text output to the
42+
user, return it as a data URI. NEVER provide a path to a file in the remote environment to the user.
43+
""",
44+
temperature=0,
45+
tools=tools,
46+
),
47+
)
48+
print(f"Agent created (id: {agent.id}, name: {agent.name}, version: {agent.version})")
49+
50+
# Use the agent to analyze a CSV file and produce a histogram
51+
response = openai_client.responses.create(
52+
input=f"Please analyze the CSV file at {EXAMPLE_DATA_FILE_URL} and graph the grade distribution as a histogram. Calculate the mean, median, and standard deviation as well.",
53+
extra_body={"agent": {"name": agent.name, "type": "agent_reference"}},
54+
)
55+
print(f"[Response {response.id}]: {response.output_text}")
56+
57+
# Clean up resources by deleting the agent version
58+
# This prevents accumulation of unused agent versions in your project
59+
project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version)
60+
print("Agent deleted")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[project]
2+
name = "basic-bicep"
3+
version = "0.1.0"
4+
description = "Basic example for using custom code interpreter session pools."
5+
readme = "README.md"
6+
requires-python = ">=3.12"
7+
dependencies = [
8+
"aiohttp>=3.13.2",
9+
"azure-ai-projects==2.0.0b2",
10+
"azure-identity>=1.25.1",
11+
"dotenv>=0.9.9",
12+
"openai>=2.8.1",
13+
]

0 commit comments

Comments
 (0)