chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,7 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.env
@@ -0,0 +1,2 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
@@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
COPY . user_agent/
WORKDIR /app/user_agent
RUN if [ -f requirements.txt ]; then \
pip install -r requirements.txt; \
else \
echo "No requirements.txt found"; \
fi
EXPOSE 8088
CMD ["python", "main.py"]
@@ -0,0 +1,43 @@
# What this sample demonstrates
An [Agent Framework](https://github.com/microsoft/agent-framework) agent hosted using the **Responses protocol**.
## How It Works
### Model Integration
The agent uses `FoundryChatClient` from the Agent Framework to create a Responses client from the project endpoint and model deployment. The agent supports both streaming (SSE events) and non-streaming (JSON) response modes.
See [main.py](main.py) for the full implementation.
### Agent Hosting
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
## Running the Agent Host
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
## Interacting with the agent
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
```bash
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Hi"}'
```
The server will respond with a JSON object containing the response text and a response ID. You can use this response ID to continue the conversation in subsequent requests.
### Multi-turn conversation
To have a multi-turn conversation with the agent, include the previous response id in the request body. For example:
```bash
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "How are you?", "previous_response_id": "REPLACE_WITH_PREVIOUS_RESPONSE_ID"}'
```
## Deploying the Agent to Foundry
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
@@ -0,0 +1,23 @@
name: agent-framework-agent-basic-responses
description: >
A basic Agent Framework agent hosted by Foundry.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
template:
name: agent-framework-agent-basic-responses
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
resources:
- kind: model
id: gpt-4.1-mini
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
@@ -0,0 +1,12 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: agent-framework-agent-basic-responses
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: "0.5Gi"
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
@@ -0,0 +1,36 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
def main():
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=DefaultAzureCredential(),
)
agent = Agent(
client=client,
instructions="You are a friendly assistant. Keep your answers brief.",
# History will be managed by the hosting infrastructure, thus there
# is no need to store history by the service. Learn more at:
# https://developers.openai.com/api/reference/resources/responses/methods/create
default_options={"store": False},
)
server = ResponsesHostServer(agent)
server.run()
if __name__ == "__main__":
main()
@@ -0,0 +1,2 @@
agent-framework-foundry
agent-framework-foundry-hosting>=1.0.0a260630
@@ -0,0 +1,7 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.env
@@ -0,0 +1,2 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
@@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
COPY . user_agent/
WORKDIR /app/user_agent
RUN if [ -f requirements.txt ]; then \
pip install -r requirements.txt; \
else \
echo "No requirements.txt found"; \
fi
EXPOSE 8088
CMD ["python", "main.py"]
@@ -0,0 +1,61 @@
# What this sample demonstrates
An [Agent Framework](https://github.com/microsoft/agent-framework) agent with **locally-defined Python tools** hosted using the **Responses protocol**. It shows how to define custom tools with the `@tool` decorator and register them with the agent so the model can call them during a conversation.
## How It Works
### Model Integration
The agent uses `FoundryChatClient` from the Agent Framework to create a Responses client from the project endpoint and model deployment. The agent supports both streaming (SSE events) and non-streaming (JSON) response modes.
See [main.py](main.py) for the full implementation.
### Tools
Local tools are Python functions decorated with the Agent Framework's `@tool` decorator and registered with the agent. When the model chooses to call a tool during a conversation, the agent executes the corresponding function and returns the result to the model.
Each tool can be configured with one of two approval modes: **always_require** or **never_require**. With **always_require**, the agent requests explicit user approval before every invocation; with **never_require**, the agent invokes the tool automatically. To illustrate both behaviors, this sample defines two tools—one using `always_require` and the other using `never_require`.
When a tool is set to `always_require`, the agent host emits an `mcp_approval_request` output containing the approval request ID and details of the pending tool call. The client must reply with an `mcp_approval_response` indicating the same request ID and whether the user approved or denied the call before the agent will proceed.
> IMPORTANT: We are temporarily reusing the **mcp_approval_request** and **mcp_approval_response** message types defined in the [AzureAI AgentServer SDK](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/agentserver/azure-ai-agentserver-responses/docs/handler-implementation-guide.md#other-tool-call-types) because they map closely to this approval flow. They will likely be superseded by a more formal tool-approval content type in the Responses protocol in the future.
### Agent Hosting
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
## Running the Agent Host
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
## Interacting with the agent
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
```bash
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "What is the weather in Seattle?"}'
```
Send a POST request that triggers a tool call configured with `always_require` to see the approval flow in action:
```bash
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "List all the files in the current directory."}'
```
Sample output:
```bash
{"id":"caresp_3b6cba8c972b1d2f00bXmjpUGzfgSFsmgjtlgqUwqvROwl5lyG","object":"response","output":[{"type":"function_call","id":"fc_3b6cba8c972b1d2f00JIAQktGC1upcB6Dgxp1AVVLp0MoyRTX4","call_id":"call_hWwwZ8lqVQCAuo8ZyY4LXIya","name":"run_bash","arguments":"{\"command\":\"ls -la\"}","status":"completed","response_id":"caresp_3b6cba8c972b1d2f00bXmjpUGzfgSFsmgjtlgqUwqvROwl5lyG","agent_reference":null},{"type":"mcp_approval_request","id":"mcpr_3b6cba8c972b1d2f00IdqsjB6iidFmtsuYp6oI1AoAtUKQZxje","server_label":"agent_framework","name":"run_bash","arguments":"{\"command\":\"ls -la\"}","response_id":"caresp_3b6cba8c972b1d2f00bXmjpUGzfgSFsmgjtlgqUwqvROwl5lyG","agent_reference":null}],"created_at":1778021855,"model":"","status":"completed","completed_at":1778021865,"response_id":"caresp_3b6cba8c972b1d2f00bXmjpUGzfgSFsmgjtlgqUwqvROwl5lyG","agent_reference":{"type":"agent_reference"},"agent_session_id":"8caaaa19598306a1f2fb6d8939ef06874c52c63a83b57681ea4e4b75cf6a179","background":false}
```
To approve:
```bash
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": [{"type": "mcp_approval_response", "approval_request_id": "mcpr_3b6cba8c972b1d2f00IdqsjB6iidFmtsuYp6oI1AoAtUKQZxje", "approve": true}], "previous_response_id": "caresp_3b6cba8c972b1d2f00bXmjpUGzfgSFsmgjtlgqUwqvROwl5lyG"}'
```
## Deploying the Agent to Foundry
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
@@ -0,0 +1,23 @@
name: agent-framework-agent-with-local-tools-responses
description: >
An Agent Framework agent with local tools hosted by Foundry.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
template:
name: agent-framework-agent-with-local-tools-responses
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
resources:
- kind: model
id: gpt-4.1-mini
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
@@ -0,0 +1,11 @@
kind: hosted
name: agent-framework-agent-with-local-tools-responses
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
@@ -0,0 +1,74 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import subprocess
from random import randint
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
from pydantic import Field
# Load environment variables from .env file
load_dotenv()
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
@tool(approval_mode="always_require")
def run_bash(command: str) -> str:
"""Execute a shell command locally and return stdout, stderr, and exit code."""
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=30,
)
parts: list[str] = []
if result.stdout:
parts.append(result.stdout)
if result.stderr:
parts.append(f"stderr: {result.stderr}")
parts.append(f"exit_code: {result.returncode}")
return "\n".join(parts)
except subprocess.TimeoutExpired:
return "Command timed out after 30 seconds"
except Exception as e:
return f"Error executing command: {e}"
def main():
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=DefaultAzureCredential(),
)
agent = Agent(
client=client,
instructions="You are a friendly assistant. Keep your answers brief.",
tools=[get_weather, run_bash],
# History will be managed by the hosting infrastructure, thus there
# is no need to store history by the service. Learn more at:
# https://developers.openai.com/api/reference/resources/responses/methods/create
default_options={"store": False},
)
server = ResponsesHostServer(agent)
server.run()
if __name__ == "__main__":
main()
@@ -0,0 +1,2 @@
agent-framework-foundry
agent-framework-foundry-hosting>=1.0.0a260630
@@ -0,0 +1,7 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.env
@@ -0,0 +1,3 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
GITHUB_PAT="..."
@@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
COPY . user_agent/
WORKDIR /app/user_agent
RUN if [ -f requirements.txt ]; then \
pip install -r requirements.txt; \
else \
echo "No requirements.txt found"; \
fi
EXPOSE 8088
CMD ["python", "main.py"]
@@ -0,0 +1,33 @@
# What this sample demonstrates
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that connects to a **remote MCP server** (GitHub) for tool discovery and hosted using the **Responses protocol**. Instead of defining tools locally, the agent discovers and invokes tools at runtime from an MCP-compatible endpoint — in this case, the GitHub Copilot MCP server. This enables dynamic tool integration without redeployment.
## How It Works
### Model Integration
The agent uses `FoundryChatClient` from the Agent Framework to create an OpenAI-compatible Responses client. It registers a remote MCP tool pointing at `https://api.githubcopilot.com/mcp/`, authenticating with a GitHub Personal Access Token (PAT). When the model decides to call a tool, the framework forwards the call to the MCP server and returns the result to the model for the final reply.
See [main.py](main.py) for the full implementation.
### Agent Hosting
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
## Running the Agent Host
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
## Interacting with the agent
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
```bash
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "List all the repositories I own on GitHub."}'
```
## Deploying the Agent to Foundry
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
@@ -0,0 +1,25 @@
name: agent-framework-agent-with-remote-mcp-tools-responses
description: >
An Agent Framework agent with remote MCP tools hosted by Foundry.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
template:
name: agent-framework-agent-with-remote-mcp-tools-responses
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
- name: GITHUB_PAT
value: ${GITHUB_PAT}
resources:
- kind: model
id: gpt-4.1-mini
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
@@ -0,0 +1,13 @@
kind: hosted
name: agent-framework-agent-with-remote-mcp-tools-responses
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
- name: GITHUB_PAT
value: ${GITHUB_PAT}
@@ -0,0 +1,56 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import os
from agent_framework import Agent, ToolTypes
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
logger = logging.getLogger(__name__)
def main():
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=DefaultAzureCredential(),
)
github_pat = os.environ["GITHUB_PAT"]
tools: list[ToolTypes] = []
if not github_pat:
logger.warning("GITHUB_PAT environment variable is not set. The GitHub MCP tool will not get registered.")
else:
tools.append(
client.get_mcp_tool(
name="GitHub",
url="https://api.githubcopilot.com/mcp/",
headers={
"Authorization": f"Bearer {github_pat}",
},
approval_mode="never_require",
)
)
agent = Agent(
client=client,
instructions="You are a friendly assistant. Keep your answers brief.",
tools=tools,
# History will be managed by the hosting infrastructure, thus there
# is no need to store history by the service. Learn more at:
# https://developers.openai.com/api/reference/resources/responses/methods/create
default_options={"store": False},
)
server = ResponsesHostServer(agent)
server.run()
if __name__ == "__main__":
main()
@@ -0,0 +1,2 @@
agent-framework-foundry
agent-framework-foundry-hosting>=1.0.0a260630
@@ -0,0 +1,6 @@
agent.manifest.yaml
agent.yaml
.env.example
.env
toolbox.yaml
./scripts
@@ -0,0 +1,7 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.env
@@ -0,0 +1,3 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
TOOLBOX_ENDPOINT="..."
@@ -0,0 +1,18 @@
FROM python:3.12-slim
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . user_agent/
WORKDIR /app/user_agent
RUN if [ -f requirements.txt ]; then \
pip install -r requirements.txt; \
else \
echo "No requirements.txt found"; \
fi
EXPOSE 8088
CMD ["python", "main.py"]
@@ -0,0 +1,275 @@
# Agent with Foundry Toolbox (Responses Protocol)
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that uses **Foundry Toolbox** for tool discovery, hosted on Microsoft Foundry using the **Responses protocol**. Foundry Toolbox is a managed tool registry in Microsoft Foundry that lets you define tools centrally and share them across agents.
## Creating a Foundry Toolbox
You can create a Foundry Toolbox by code. Refer to this sample for an example: [Foundry Toolbox CRUD Sample](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_toolboxes_crud.py).
You can also create a Foundry Toolbox in the Foundry portal. Read more about it [in the Foundry toolbox documentation](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/toolbox).
This sample consumes a toolbox over its MCP endpoint. It bundles a [`toolbox.yaml`](toolbox.yaml) that defines 6 tools behind one endpoint:
- **Web search**, which grounds responses in real-time public web results.
- **Code interpreter**, which executes Python code in a secure sandbox and returns the output.
- **Azure Specs MCP**, which demonstrates connecting to an MCP server that doesn't require authentication.
- **GitHub MCP**, which demonstrates connecting to the GitHub MCP server using either a Personal Access Token (PAT) or OAuth2 (switch by changing the `project_connection_id` in `toolbox.yaml`).
- **Azure Language MCP with agent identity**, which demonstrates connecting to the Azure Language MCP server using agent identity for authentication.
- **Microsoft Foundry MCP with Entra pass-through**, which demonstrates connecting to the Microsoft Foundry MCP server using Entra pass-through for authentication.
### Authentication Methods
You can connect to MCP servers in Foundry Toolbox that use different authentication methods. This sample demonstrates the following authentication methods:
- [**No authentication**](https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/SUPPORTED_TOOLBOX_SCENARIOS.md#5-mcp-no-auth): The tool does not require any authentication. The agent can invoke the tool without providing any credentials. Sample MCP server: `https://gitmcp.io/Azure/azure-rest-api-specs`
- [**Key-based authentication**](https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/SUPPORTED_TOOLBOX_SCENARIOS.md#4-mcp-key-auth-github): The tool requires a key to authenticate. Sample MCP server: `https://api.githubcopilot.com/mcp` (GitHub MCP server) with a Personal Access Token (PAT) for authentication.
- [**OAuth2 authentication (managed)**](https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/SUPPORTED_TOOLBOX_SCENARIOS.md#6-mcp-oauth-managed-connector): The tool requires OAuth2 to authenticate. Sample MCP server: `https://api.githubcopilot.com/mcp` (GitHub MCP server) with OAuth2 for authentication.
- [**Agent identity authentication**](https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/SUPPORTED_TOOLBOX_SCENARIOS.md#8-mcp-agent-identity): The tool requires an agent identity token to authenticate. Sample MCP server: `https://{foundry-resource-name}.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview` ([Azure Language MCP server](https://learn.microsoft.com/en-us/azure/ai-services/language-service/concepts/foundry-tools-agents#azure-language-mcp-server-preview)) with agent identity for authentication.
- [**Entra Pass-through authentication**](https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/SUPPORTED_TOOLBOX_SCENARIOS.md#13-mcp-oauth-entra-passthrough): The tool requires an Entra pass-through token to authenticate; Foundry forwards the calling user's Entra token to the MCP server. Sample MCP server: the [Microsoft Foundry MCP server](https://learn.microsoft.com/en-us/azure/foundry/mcp/get-started?view=foundry&tabs=user), which exposes Foundry model-catalog, evaluation, agent, and session tools and requires only that the caller have access to the Foundry project (no extra license).
There are also Non-MCP tools in the toolbox that support different authentication methods. Learn more at the [Foundry sample repository](https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/SUPPORTED_TOOLBOX_SCENARIOS.md).
### Finding the Entra audience for an MCP server
An Entra pass-through connection requires an **audience** — the Entra resource that the MCP server validates tokens against. For the Microsoft Foundry MCP server (`https://mcp.ai.azure.com`), read it from the server's OAuth protected-resource metadata:
```bash
curl https://mcp.ai.azure.com/.well-known/oauth-protected-resource
```
```jsonc
{
"resource": "https://mcp.ai.azure.com",
"authorization_servers": ["https://login.microsoftonline.com/common/v2.0"],
"scopes_supported": ["https://mcp.ai.azure.com/Foundry.Mcp.Tools"]
}
```
Use the `resource` value (`https://mcp.ai.azure.com`) as the audience.
> For connector-backed MCP servers (for example Microsoft 365 / WorkIQ servers such as Outlook Mail), the audience is instead published in the Foundry Tools Catalog. Look it up with the helper scripts in [`scripts/`](scripts/): run `./scripts/list-foundry-connectors.ps1 -ConnectorName <name>` (or `./scripts/list-foundry-connectors.sh -n <name>`) and read `AzureActiveDirectoryResourceId` (equivalently `resourceUri`) under `properties.x-ms-connection-parameters`. Run the script with no connector name to list every connector with its name, title, and auth type.
### Creating Connections
Before creating the toolbox, create project connections for any tools that require authentication. The connection defines the authentication details and credentials for the tool, and the toolbox references the connection to authenticate tool invocations at runtime. The following connections are needed for this sample (used in `toolbox.yaml`):
For `ghmcppat`, run the following command to create a PAT-based connection to the GitHub MCP server:
```powershell
azd ai connection create ghmcppat --kind remote-tool --target https://api.githubcopilot.com/mcp --auth-type custom-keys --custom-key "Authorization=Bearer <github_pat>" -p https://<account>.services.ai.azure.com/api/projects/<project>
```
For `ghmcpoauth`, create an OAuth2-based connection to the GitHub MCP server:
```powershell
azd ai connection create ghmcpoauth --kind remote-tool --target https://api.githubcopilot.com/mcp --auth-type oauth2 --connector-name foundrygithubmcp -p https://<account>.services.ai.azure.com/api/projects/<project>
```
> This sample uses `ghmcppat` by default, but you can switch to `ghmcpoauth` in the `toolbox.yaml` file.
For `langmcpconn`, create an agent-identity-based connection to the Azure Language MCP server:
```powershell
azd ai connection create langmcpconn --kind remote-tool --target https://<language-service>.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview --auth-type project-managed-identity --audience https://cognitiveservices.azure.com/ -p https://<account>.services.ai.azure.com/api/projects/<project>
```
For `foundrymcpconn`, create an Entra pass-through connection to the Microsoft Foundry MCP server:
```powershell
azd ai connection create foundrymcpconn --kind remote-tool --target https://mcp.ai.azure.com --auth-type user-entra-token --audience https://mcp.ai.azure.com -p https://<account>.services.ai.azure.com/api/projects/<project>
```
### Creating the toolbox
You create the toolbox once from `toolbox.yaml`, then copy the versioned MCP endpoint it prints into the `TOOLBOX_ENDPOINT` environment variable. The agent connects to that endpoint at runtime.
```powershell
azd ai toolbox create agent-tools --from-file ./toolbox.yaml --project-endpoint https://<account>.services.ai.azure.com/api/projects/<project>
```
## How it works
### Model Integration
The agent uses `FoundryChatClient` from the Agent Framework to create an OpenAI-compatible Responses client. It connects to the toolbox's MCP endpoint via `MCPStreamableHTTPTool`, which discovers and invokes the toolbox's tools over MCP at runtime. The agent resolves the endpoint from the `TOOLBOX_ENDPOINT` environment variable. If that variable isn't set, it builds the unversioned (default-version) endpoint from `FOUNDRY_PROJECT_ENDPOINT` and `TOOLBOX_NAME`.
See [main.py](main.py) for the full implementation.
## Running the agent
### Option 1: Azure Developer CLI (`azd`)
#### Prerequisites
1. **Azure Developer CLI (`azd`)** — [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later)
2. Install the unified Foundry CLI extension bundle (provides `azd ai agent`, `connection`, `inspector`, `project`, `routine`, `skill`, and `toolbox`):
```bash
# If you previously installed individual extensions, uninstall them first:
# azd ext uninstall azure.ai.agents
# azd ext uninstall azure.ai.toolboxes
azd ext install microsoft.foundry
```
3. Authenticate:
```bash
azd auth login
```
#### Initialize the agent project
No cloning required. Create a new folder and initialize from the manifest:
```bash
mkdir my-toolbox-agent && cd my-toolbox-agent
azd ai agent init -m https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/agent-framework/responses/04-foundry-toolbox/agent.manifest.yaml
```
Follow the prompts to configure your Foundry project and model deployment. If you don't have an existing Foundry project, `azd ai agent init` will guide you through creating one. Initializing also sets the selected project as the active project for the `azd ai` commands that follow.
#### Create the toolbox with `azd ai`
> [!TIP]
> If you use GitHub Copilot for Azure to scaffold a hosted agent that consumes this toolbox, the following skill references describe the same endpoint contract (env var, headers, MCP protocol, citation patterns, and troubleshooting) that the agent must implement:
>
> - [Toolbox reference](https://github.com/microsoft/GitHub-Copilot-for-Azure/blob/main/plugin/skills/microsoft-foundry/foundry-agent/create/references/toolbox-reference.md) — endpoint format, MCP protocol, OAuth consent handling, citation patterns, and troubleshooting.
> - [Use toolbox in a hosted agent](https://github.com/microsoft/GitHub-Copilot-for-Azure/blob/main/plugin/skills/microsoft-foundry/foundry-agent/create/references/use-toolbox-in-hosted-agent.md) — endpoint resolution, env-var contract, payload shape, code integration patterns, and tracing.
The agent reads the toolbox's MCP endpoint from `TOOLBOX_ENDPOINT`. Create the toolbox once from the bundled [`toolbox.yaml`](toolbox.yaml):
```bash
azd ai toolbox create agent-tools --from-file ./toolbox.yaml --project-endpoint https://<account>.services.ai.azure.com/api/projects/<project>
```
The first version becomes the default automatically. Use `azd ai toolbox list`, `azd ai toolbox show agent-tools`, and `azd ai toolbox version list agent-tools` to inspect, and `azd ai toolbox delete agent-tools --force` to remove it.
To stage incremental changes safely, use `azd ai toolbox connection add/remove` and `azd ai toolbox skill add/list/remove`; each creates a new toolbox version that carries forward existing connections and skills but **doesn't** change the default. Promote a version with `azd ai toolbox publish agent-tools <version>` when you're ready to make it active.
`azd ai toolbox create` prints the toolbox's versioned MCP endpoint. Copy that endpoint and store it in your `azd` environment so the agent connects to it:
```bash
azd env set TOOLBOX_ENDPOINT "https://<account>.services.ai.azure.com/api/projects/<project>/toolboxes/agent-tools/versions/1/mcp?api-version=v1"
```
#### Provision Azure resources (if needed)
If you don't already have a Foundry project and model deployment:
```bash
azd provision
```
#### Run the agent locally
```bash
azd ai agent run
```
The agent host will start on `http://localhost:8088`.
#### Invoke the local agent
In a separate terminal, from the project directory:
```bash
azd ai agent invoke --local "What tools do you have?"
```
#### Deploy to Foundry
Once tested locally, deploy to Microsoft Foundry:
```bash
azd deploy
```
For the full deployment guide, see [Deploy a hosted agent](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
#### Invoke the deployed agent
```bash
azd ai agent invoke "What tools do you have?"
```
### Option 2: VS Code (Foundry Toolkit)
#### Prerequisites
1. **VS Code** with the **[Foundry Toolkit](https://learn.microsoft.com/en-us/azure/foundry/how-to/develop/get-started-projects-vs-code)** extension installed.
2. Sign in to Azure in VS Code.
3. The `agent-tools` toolbox must exist in your Foundry project. Create it from the bundled [`toolbox.yaml`](toolbox.yaml) (`azd ai toolbox create agent-tools --from-file ./toolbox.yaml`) or in the Foundry portal before you run the agent.
#### Create the project
1. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Create Hosted Agent**.
2. Select this sample from the gallery. The extension scaffolds the project into a new workspace and generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically.
3. Complete the **Foundry Project Setup** to pick the subscription and Foundry project (or create a new one).
#### Run and debug the agent
Press **F5** to start the agent in debug mode. The agent host will start on `http://localhost:8088`.
#### Test with Agent Inspector
1. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**.
2. The Inspector connects to the running agent. Send messages to chat and view streamed responses.
#### Deploy to Foundry
1. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Deploy Hosted Agent**. The extension opens a **Deploy Hosted Agent** wizard and reads `agent.yaml` to auto-populate settings.
2. If prompted, complete **Foundry Project Setup** to select subscription and project.
3. On the **Basics** tab, choose deployment method (**Code** or **Container**) and confirm the agent name.
4. On **Review + Deploy**, confirm runtime details, pick **CPU and Memory** size, and click **Deploy**.
5. After deployment, invoke the agent in the Agent Playground and stream live logs from the **Logs** tab.
### Creating a Foundry Toolbox
You can create a Foundry Toolbox by code. Refer to this sample for an example: [Foundry Toolbox CRUD Sample](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_toolboxes_crud.py).
You can also create a Foundry Toolbox in the Foundry portal. Read more about it [in the Foundry toolbox documentation](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/toolbox).
## Troubleshooting
### A single failing MCP source can fail the whole agent
A toolbox aggregates every tool source behind one MCP endpoint. If **any** referenced MCP server fails while the toolbox enumerates tools (`tools/list`), the toolbox fails the entire enumeration, so the agent can't load its tools and every request returns an error (HTTP 500) until that source recovers.
For example, a flaky third-party MCP source can intermittently return `HTTP 502 (Bad Gateway)` during enumeration, which surfaces as:
```
tools/list failed for 1 tool source(s), succeeded for 5 tool source(s)
{"errors":[{"name":"<server_label>","type":"mcp","error":{"code":"HTTP_502", ...}}]}
```
This is an upstream/service hiccup, not a problem with the agent code. Mitigations:
- Retry the request — these failures are usually transient.
- If a source is persistently unavailable, temporarily remove its tool entry (and connection) from `toolbox.yaml`, recreate the toolbox, and update `TOOLBOX_ENDPOINT`.
- Inspect deployed agent logs with `azd ai agent monitor` to identify which source failed.
### Entra pass-through forwards the caller's identity
The Foundry MCP tool authenticates with **Entra pass-through** (`foundrymcpconn`): Foundry forwards the
calling user's Entra token to `https://mcp.ai.azure.com`. The token is forwarded both from the Foundry
portal **Agent Playground** (signed-in user) and by `azd ai agent invoke` (the developer's Entra token),
so the tools operate as that user and only act on resources the user can already access. The Foundry MCP
server requires no extra license — just access to the Foundry project.
Because the tool acts as a specific user, running the agent **locally** (`python main.py`) or calling the
endpoint with a raw token uses whatever identity that token represents (`az login` user locally, the
agent's managed identity when hosted). If that identity has no access to the target resources, the tool
returns an authorization error even though it is discovered and called correctly.
> Some other Entra pass-through MCP servers add their **own** entitlement checks on top of the token. For
> example, the Microsoft 365 / WorkIQ servers (Outlook Mail, Teams) require the caller to hold a
> **Microsoft 365 Copilot (Business Chat)** license; without it they fail with
> `WorkIQ license check failed. Required service plan(s): [M365_COPILOT_BUSINESS_CHAT]`. That is a
> property of those servers, not of Entra pass-through itself.
## Next steps
- [Quickstart: Create a hosted agent](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent) — end-to-end walkthrough using `azd`
- [Tool catalog](https://learn.microsoft.com/en-us/azure/foundry/agents/concepts/tool-catalog) — browse available tools to extend your agent (Bing Search, Azure AI Search, file search, code interpreter, and more)
- [Manage hosted agents](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/manage-hosted-agent) — monitor and manage deployed agents
- [Basic agent](../01_basic/) — minimal agent with no tools
- [Add local tools](../02_tools/) — sample with locally-defined Python tool functions
- [Build multi-agent workflows](../05_workflows/) — sample with chained agent pipelines
@@ -0,0 +1,30 @@
name: agent-framework-agent-with-foundry-toolbox-responses
description: >
An Agent Framework agent with Foundry Toolbox integration.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
template:
name: agent-framework-agent-with-foundry-toolbox-responses
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
- name: TOOLBOX_ENDPOINT
# Full MCP endpoint of the toolbox the agent consumes. Create the toolbox
# from the bundled toolbox.yaml, then copy the versioned endpoint it prints
# and store it in your azd environment before you run or deploy:
# azd ai toolbox create my-toolbox --from-file ./toolbox.yaml
# azd env set TOOLBOX_ENDPOINT "<endpoint-from-output>"
value: "{{TOOLBOX_ENDPOINT}}"
resources:
- kind: model
id: gpt-4.1
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
@@ -0,0 +1,13 @@
kind: hosted
name: agent-framework-agent-with-foundry-toolbox-responses
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
- name: TOOLBOX_ENDPOINT
value: ${TOOLBOX_ENDPOINT}
@@ -0,0 +1,48 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import FoundryToolbox, ResponsesHostServer
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
async def main():
credential = DefaultAzureCredential()
# FoundryToolbox resolves the toolbox endpoint from the environment
# (TOOLBOX_ENDPOINT, or FOUNDRY_PROJECT_ENDPOINT + TOOLBOX_NAME), authenticates
# every request with the credential, and transparently forwards the platform
# per-request call-id to the toolbox. The hosting server enters the agent, which
# connects the toolbox on first use and closes it at shutdown.
toolbox = FoundryToolbox(credential)
# Create the chat client
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=credential,
)
agent = Agent(
client=client,
instructions="You are a friendly assistant. Keep your answers brief.",
tools=toolbox,
# History will be managed by the hosting infrastructure, thus there
# is no need to store history by the service. Learn more at:
# https://developers.openai.com/api/reference/resources/responses/methods/create
default_options={"store": False},
)
server = ResponsesHostServer(agent)
await server.run_async()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,2 @@
agent-framework-foundry
agent-framework-foundry-hosting>=1.0.0a260630
@@ -0,0 +1,105 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
List Foundry Tools Catalog connectors, or fetch full details for one connector.
.DESCRIPTION
Queries the Azure AI Foundry Tools Catalog (asset-gallery) connectors registry.
- With no -ConnectorName: lists all connectors (name, title, detected auth type).
- With -ConnectorName: prints the full JSON details for that connector.
A bearer token for https://ai.azure.com is required. It is read from the
-Token parameter, then the CATALOG_TOKEN environment variable, and finally
acquired automatically via 'az account get-access-token' (requires 'az login').
.EXAMPLE
./list-foundry-connectors.ps1
Lists all connectors.
.EXAMPLE
./list-foundry-connectors.ps1 -ConnectorName a365outlookmailmcp
Prints full details for the Work IQ Mail MCP connector.
.EXAMPLE
./list-foundry-connectors.ps1 -PageSize 2000
Lists more connectors in a single page.
#>
[CmdletBinding()]
param(
# annotations/name of a connector to fetch full details for. Omit to list all.
[string]$ConnectorName,
# Azure ML region host prefix.
[string]$Region = "eastus",
# Number of results to request in one page.
[int]$PageSize = 100,
# Catalog bearer token (audience https://ai.azure.com). Defaults to $env:CATALOG_TOKEN, else acquired via az.
[string]$Token = $env:CATALOG_TOKEN
)
$ErrorActionPreference = "Stop"
if (-not $Token) {
Write-Verbose "No token supplied; acquiring via 'az account get-access-token'..."
$Token = az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv
}
if (-not $Token) {
throw "Failed to acquire a catalog token. Run 'az login', or pass -Token / set `$env:CATALOG_TOKEN."
}
$uri = "https://$Region.api.azureml.ms/asset-gallery/v1.0/tools"
$headers = @{
"Authorization" = "Bearer $Token"
"Content-Type" = "application/json"
"x-ms-user-agent" = "AzureMachineLearningWorkspacePortal/12.0"
}
$filters = [System.Collections.ArrayList]@(
@{ field = "entityContainerId"; operator = "eq"; values = @("connectors-registry-prod-bl") }
@{ field = "type"; operator = "eq"; values = @("tools") }
@{ field = "kind"; operator = "eq"; values = @("Versioned") }
@{ field = "labels"; operator = "eq"; values = @("latest") }
)
if ($ConnectorName) {
[void]$filters.Add(@{ field = "annotations/name"; operator = "eq"; values = @($ConnectorName) })
}
$body = @{
freeTextSearch = "*"
filters = $filters
includeTotalResultCount = $true
pageSize = $PageSize
skip = 0
} | ConvertTo-Json -Depth 10
# The response can be several MB and may contain a property with an empty-string
# name, so read the raw content and parse with -AsHashtable.
$content = (Invoke-WebRequest -Method Post -Uri $uri -Headers $headers -Body $body).Content
$resp = $content | ConvertFrom-Json -AsHashtable -Depth 100
if ($ConnectorName) {
if ($resp.totalCount -eq 0) {
Write-Warning "No connector found with annotations/name '$ConnectorName'."
return
}
$resp.value | ConvertTo-Json -Depth 100
}
else {
Write-Host "Total connectors: $($resp.totalCount)"
$resp.value | ForEach-Object {
$params = $_.properties.'x-ms-connection-parameters'
$authType = if ($null -eq $params) {
"None"
}
else {
$types = $params.Values | ForEach-Object { $_.type }
if ($types -contains "oauthSetting") { "OAuth2" }
elseif ($types -contains "securestring") { "CustomKeys" }
else { "None" }
}
[pscustomobject]@{
Name = $_.annotations.name
Title = $_.properties.title
Auth = $authType
}
} | Format-Table -AutoSize
}
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
#
# List Foundry Tools Catalog connectors, or fetch full details for one connector.
#
# - With no -n: lists all connectors (name, title, detected auth type).
# - With -n NAME: prints the full JSON details for that connector.
#
# A bearer token for https://ai.azure.com is required. It is read from the
# -t option, then the CATALOG_TOKEN environment variable, and finally acquired
# automatically via 'az account get-access-token' (requires 'az login').
#
# Requires: curl, jq (and optionally az).
#
# Examples:
# ./list-foundry-connectors.sh
# ./list-foundry-connectors.sh -n a365outlookmailmcp
# ./list-foundry-connectors.sh -p 2000
#
set -euo pipefail
CONNECTOR_NAME=""
REGION="eastus"
PAGE_SIZE=100
TOKEN="${CATALOG_TOKEN:-}"
usage() {
cat <<EOF
Usage: $0 [-n connector_name] [-r region] [-p page_size] [-t token]
-n Connector annotations/name to fetch full details for (e.g. a365outlookmailmcp).
If omitted, lists all connectors (name, title, auth type).
-r Azure ML region host prefix (default: ${REGION}).
-p Page size (default: ${PAGE_SIZE}).
-t Catalog bearer token (audience https://ai.azure.com).
Defaults to \$CATALOG_TOKEN, else acquired via 'az'.
-h Show this help.
EOF
}
while getopts ":n:r:p:t:h" opt; do
case "$opt" in
n) CONNECTOR_NAME="$OPTARG" ;;
r) REGION="$OPTARG" ;;
p) PAGE_SIZE="$OPTARG" ;;
t) TOKEN="$OPTARG" ;;
h) usage; exit 0 ;;
\?) echo "Invalid option: -$OPTARG" >&2; usage; exit 1 ;;
:) echo "Option -$OPTARG requires an argument." >&2; usage; exit 1 ;;
esac
done
if [[ -z "$TOKEN" ]]; then
TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
fi
if [[ -z "$TOKEN" ]]; then
echo "Failed to acquire a catalog token. Run 'az login', or pass -t / set CATALOG_TOKEN." >&2
exit 1
fi
URI="https://${REGION}.api.azureml.ms/asset-gallery/v1.0/tools"
# Base filters; optionally narrow to a single connector by annotations/name.
FILTERS=$(jq -nc --arg connector "$CONNECTOR_NAME" '
[
{"field":"entityContainerId","operator":"eq","values":["connectors-registry-prod-bl"]},
{"field":"type", "operator":"eq","values":["tools"]},
{"field":"kind", "operator":"eq","values":["Versioned"]},
{"field":"labels", "operator":"eq","values":["latest"]}
] + ( ($connector | length) > 0
? [{"field":"annotations/name","operator":"eq","values":[$connector]}]
: [] )
')
BODY=$(cat <<EOF
{
"freeTextSearch": "*",
"filters": $FILTERS,
"includeTotalResultCount": true,
"pageSize": $PAGE_SIZE,
"skip": 0
}
EOF
)
RESPONSE=$(curl -sS -X POST "$URI" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "x-ms-user-agent: AzureMachineLearningWorkspacePortal/12.0" \
-d "$BODY")
if [[ -n "$CONNECTOR_NAME" ]]; then
# Full JSON details for the requested connector.
echo "$RESPONSE" | jq '.value'
else
# name, title, and detected auth type for each connector.
echo "$RESPONSE" | jq -r '
.totalCount as $total |
"Total connectors: \($total)",
(.value[] | "\(.annotations.name)\t\(.properties.title)\t\(
.properties["x-ms-connection-parameters"] |
if . == null then "None"
elif ([.[].type] | any(. == "oauthSetting")) then "OAuth2"
elif ([.[].type] | any(. == "securestring")) then "CustomKeys"
else "None" end
)")'
fi
@@ -0,0 +1,34 @@
# toolbox.yaml
# Defines the toolbox this agent consumes. Create it once with:
# azd ai toolbox create agent-tools --from-file ./toolbox.yaml
# After creating it, copy the versioned MCP endpoint the command prints and
# set it as the TOOLBOX_ENDPOINT environment variable. The agent connects to
# that endpoint at runtime.
description: Multiple MCP servers behind one endpoint
tools:
- type: web_search
name: web_search
require_approval: "never"
- type: code_interpreter
name: code_interpreter
require_approval: "never"
- type: mcp
# This MCP tool doesn't require authentication
server_label: noauth_mcp
server_url: "https://gitmcp.io/Azure/azure-rest-api-specs"
require_approval: "never"
- type: mcp
# This MCP tool uses the GitHub MCP server with a PAT for authentication or OAuth2
server_label: github
project_connection_id: ghmcppat # use `ghmcpoauth` for OAuth2 authentication
require_approval: "never"
- type: mcp
# This MCP tool uses the Azure Language MCP server with agent identity for authentication
server_label: language-mcp
project_connection_id: langmcpconn
require_approval: "never"
- type: mcp
# This MCP tool uses the Microsoft Foundry MCP server with Entra pass-through (user token) for authentication
server_label: foundry-mcp
project_connection_id: foundrymcpconn
require_approval: "never"
@@ -0,0 +1,7 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.env
@@ -0,0 +1,2 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
@@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
COPY . user_agent/
WORKDIR /app/user_agent
RUN if [ -f requirements.txt ]; then \
pip install -r requirements.txt; \
else \
echo "No requirements.txt found"; \
fi
EXPOSE 8088
CMD ["python", "main.py"]
@@ -0,0 +1,43 @@
# What this sample demonstrates
An [Agent Framework](https://github.com/microsoft/agent-framework) workflow demonstrating **multi-agent chaining** and hosted using the **Responses protocol**. It shows how to use the Agent Framework's `WorkflowBuilder` to compose a pipeline of specialized agents — a slogan writer, a legal reviewer, and a formatter — that process a request sequentially. Each agent receives only the output of the previous agent, and only the final formatted result is returned to the caller.
> The workflow will be used as an agent. Read more about Agent Framework workflows in the [Agent Framework documentation](https://learn.microsoft.com/en-us/agent-framework/workflows/) and workflow as an agent in the [Workflow as an Agent documentation](https://learn.microsoft.com/en-us/agent-framework/workflows/as-agents?pivots=programming-language-python).
> This sample requires a more advanced model because the model needs to continue the conversation from an assistant message. Not all models perform well in this scenario. Tested with OpenAI's model `gpt-5.4`.
## How It Works
### Model Integration
The agent creates three specialized `Agent` instances sharing the same `FoundryChatClient`: a **writer** that generates slogans, a **legal reviewer** that ensures compliance, and a **formatter** that styles the output. Each agent is wrapped in an `AgentExecutor` with `context_mode="last_agent"` so it only sees the previous agent's output. The `WorkflowBuilder` wires them into a linear pipeline and limits the output to the formatter's result.
See [main.py](main.py) for the full implementation.
### Agent Hosting
The workflow is exposed as a single agent via `.as_agent()` and hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
## Running the Agent Host
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
## Interacting with the agent
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
```bash
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Create a slogan for a new electric SUV that is affordable and fun to drive."}'
```
Invoke with `azd`:
```bash
azd ai agent invoke --local "Create a slogan for a new electric SUV that is affordable and fun to drive."
```
## Deploying the Agent to Foundry
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
@@ -0,0 +1,23 @@
name: agent-framework-workflows-responses
description: >
An Agent Framework workflow hosted by Foundry.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
template:
name: agent-framework-workflows-responses
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
resources:
- kind: model
id: gpt-5.4
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
@@ -0,0 +1,11 @@
kind: hosted
name: agent-framework-workflows-responses
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
@@ -0,0 +1,70 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from agent_framework import Agent, AgentExecutor, WorkflowBuilder
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
def main():
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=DefaultAzureCredential(),
)
writer_agent = Agent(
client=client,
instructions=("You are an excellent slogan writer. You create new slogans based on the given topic."),
name="writer",
)
legal_agent = Agent(
client=client,
instructions=(
"You are an excellent legal reviewer. "
"Make necessary corrections to the slogan so that it is legally compliant."
),
name="legal_reviewer",
)
format_agent = Agent(
client=client,
instructions=(
"You are an excellent content formatter. "
"You take the slogan and format it in a cool retro style when printing to a terminal."
),
name="formatter",
)
# Set the context mode to `last_agent` so that each agent only sees the output of the
# previous agent instead of the full conversation history
writer_executor = AgentExecutor(writer_agent, context_mode="last_agent")
legal_executor = AgentExecutor(legal_agent, context_mode="last_agent")
format_executor = AgentExecutor(format_agent, context_mode="last_agent")
workflow_agent = (
WorkflowBuilder(
start_executor=writer_executor,
# Select only the formatted result as Workflow Output.
# Unselected executor payloads are hidden unless selected as Intermediate Output.
output_from=[format_executor],
)
.add_edge(writer_executor, legal_executor)
.add_edge(legal_executor, format_executor)
.build()
.as_agent()
)
server = ResponsesHostServer(workflow_agent)
server.run()
if __name__ == "__main__":
main()
@@ -0,0 +1,2 @@
agent-framework-foundry
agent-framework-foundry-hosting>=1.0.0a260630
@@ -0,0 +1,10 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.env
# Local-only client tooling and sample data; not needed inside the agent image.
resources/
@@ -0,0 +1,3 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
TOOLBOX_ENDPOINT="..."
@@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
COPY . user_agent/
WORKDIR /app/user_agent
RUN if [ -f requirements.txt ]; then \
pip install -r requirements.txt; \
else \
echo "No requirements.txt found"; \
fi
EXPOSE 8088
CMD ["python", "main.py"]
@@ -0,0 +1,127 @@
# What this sample demonstrates
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that uses a local shell tool and a code interpreter tool for working with files, and hosted using the **Responses protocol**.
## How It Works
### Model Integration
The agent uses `FoundryChatClient` from the Agent Framework to create a Responses client from the project endpoint and model deployment. The agent supports both streaming (SSE events) and non-streaming (JSON) response modes.
See [main.py](main.py) for the full implementation.
### Agent Hosting
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
### Tools
This agent uses four tools:
1. **Get Current Working Directory Tool (`get_cwd`)** Returns the current working directory of the agent host process.
2. **List Files Tool (`list_files`)** Lists the files in a specified directory.
3. **Read File Tool (`read_file`)** Reads the contents of a specified file.
4. **Code Interpreter Tool (`code_interpreter`)** Allows the agent to execute Python code in a safe sandboxed environment.
5. **Web Search Tool (`web_search`)** Allows the agent to perform web searches using the Bing Search API.
> In this sample, the filesystem tools are function tools defined in Python using the `@tool` decorator from the Agent Framework. The code interpreter tool and web search tool are managed tools provided by [Foundry Toolbox](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/toolbox). Learn more about foundry toolbox integration with hosted agents with this [sample](../04_foundry_toolbox/).
## Running the Agent Host
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
An extra environment variable must be set to point to the toolbox MCP endpoint. You can provide it in one of two ways:
**Option A Set `FOUNDRY_TOOLBOX_ENDPOINT` directly** (recommended for local development):
```bash
export FOUNDRY_TOOLBOX_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>/toolboxes/<name>/mcp?api-version=v1"
```
Or in PowerShell:
```powershell
$env:FOUNDRY_TOOLBOX_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>/toolboxes/<name>/mcp?api-version=v1"
```
**Option B Set `TOOLBOX_NAME`** (used automatically by the Foundry hosting scaffolding after `azd provision`):
The agent derives the endpoint at runtime as:
```
{FOUNDRY_PROJECT_ENDPOINT}/toolboxes/{TOOLBOX_NAME}/mcp?api-version=v1
```
When deployed via `azd provision`, the scaffolding injects `TOOLBOX_NAME=agent-tools` and `FOUNDRY_PROJECT_ENDPOINT` automatically from the provisioned resources declared in [`agent.manifest.yaml`](agent.manifest.yaml).
## Interacting with the agent
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
```bash
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Find the quarterly report under `{cwd}/resources` and tell me the difference of revenue between q1 2026 and q1 2025?"}'
```
> When ruuning locally, it runs within the project directory, which contains the entire sample, so the `{cwd}/resources` path in the query above will allow the agent to locate the `resources` folder included with this sample and read the `contoso_q1_2026_report.txt` file from that folder.
The server will respond with a JSON object containing the response text and a response ID. You can use this response ID to continue the conversation in subsequent requests.
## Deploying the Agent to Foundry
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
## Uploading a file to a session
Deploying the agent won't automatically upload the files included with this sample to Foundry. To make these files available to the agent at runtime, you must upload them to a [hosted agent session](https://learn.microsoft.com/azure/foundry/agents/how-to/manage-hosted-sessions). Files are tied to a specific hosted agent session, so each time you start a new session you will need to upload the files again if the agent needs access to them during that session.
After you deploy the agent to Foundry, you have two ways to interact with the agent:
1. Using `azd ai agent invoke`.
2. Through the Foundry portal.
### Using `azd ai agent invoke`
After successfully deploying the agent to Foundry, run the following command:
> You must remain in the directory where your `azd` project is initialized so that the CLI can locate the deployed agent configuration.
```bash
azd ai agent invoke "Hi!"
```
The command will invoke the agent and the server will create a new session if one does not already exist for this interaction, returning the agent's response from the hosted agent session. Run the following if you want to force a new session:
```bash
azd ai agent invoke --new-session "Hi!"
```
Run the following command to upload a file to the hosted agent session:
```bash
azd ai agent files upload -f <path-to-contoso_q1_2026_report.txt>
```
> The above command will automatically detect the last active session and upload the file to that session without requiring you to explicitly provide a session ID. It is also possible to specify a particular session ID to upload the file to a specific hosted agent session by using the `--session-id` flag. Run `azd ai agent files upload -h` to see the full list of options and flags available for the `upload` command.
Once the file is uploaded to the hosted agent session, the agent will be able to access it during that session and use it to respond to queries that reference the uploaded file.
Invoke the agent again with a query that references the uploaded file to see how it can now use the file in its responses. For example:
```bash
azd ai agent invoke "Find the quarterly report under the home directory and tell me the difference of revenue between q1 2026 and q1 2025?"
```
### Using the Foundry Portal
Similar to using the `azd` CLI, you must invoke the agent first to create a session:
![alt text](./resources/start-a-session.png)
Once the session is created, you can grab the session ID and use `azd ai agent files upload --session-id <session-id>` to upload files to that specific hosted agent session.
![alt text](./resources/session-started.png)
Or you can upload files directly through the Foundry portal by navigating to Files tab in the agent playground:
![alt text](./resources/file-upload-portal.png)
@@ -0,0 +1,32 @@
name: agent-framework-agent-files-responses
description: >
An Agent Framework agent that can work with files hosted by Foundry.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
template:
name: agent-framework-agent-files-responses
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
- name: TOOLBOX_NAME
value: "agent-tools"
resources:
- kind: model
id: gpt-4.1-mini
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
- kind: toolbox
name: agent-tools
tools:
- type: web_search
name: web_search
- type: code_interpreter
name: code_interpreter
@@ -0,0 +1,14 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: agent-framework-agent-files-responses
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: "0.5Gi"
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
- name: TOOLBOX_NAME
value: "agent-tools"
@@ -0,0 +1,145 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from collections.abc import Callable
from urllib.parse import urlsplit
import httpx
from agent_framework import Agent, MCPStreamableHTTPTool, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
def resolve_toolbox_endpoint() -> str:
"""Resolve the toolbox MCP endpoint URL.
Prefers the explicit ``TOOLBOX_ENDPOINT`` env var (set in ``agent.yaml`` or
``agent.manifest.yaml`` and via ``azd env set TOOLBOX_ENDPOINT`` after the toolbox
is created); falls back to constructing the URL from ``FOUNDRY_PROJECT_ENDPOINT``
and ``TOOLBOX_NAME``.
"""
if (endpoint := os.environ.get("TOOLBOX_ENDPOINT")) is not None:
if not endpoint:
raise ValueError("TOOLBOX_ENDPOINT is set but empty")
return endpoint
try:
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/")
toolbox_name = os.environ["TOOLBOX_NAME"]
except KeyError as e:
raise ValueError(
"Either set TOOLBOX_ENDPOINT, or set both FOUNDRY_PROJECT_ENDPOINT "
"and TOOLBOX_NAME to build the toolbox MCP endpoint."
) from e
return f"{project_endpoint}/toolboxes/{toolbox_name}/mcp?api-version=v1"
def _toolbox_name_from_endpoint(endpoint: str) -> str:
"""Extract the toolbox name from a toolbox MCP endpoint URL.
Handles both the versioned (``.../toolboxes/<name>/versions/<n>/mcp``) and
unversioned (``.../toolboxes/<name>/mcp``) endpoint shapes that Foundry
produces. Falls back to ``"toolbox"`` when the path has no ``toolboxes``
segment.
"""
segments = urlsplit(endpoint).path.split("/")
if "toolboxes" in segments:
idx = segments.index("toolboxes")
if idx + 1 < len(segments) and segments[idx + 1]:
return segments[idx + 1]
return "toolbox"
class ToolboxAuth(httpx.Auth):
"""Injects a fresh bearer token on every request."""
def __init__(self, token_provider: Callable[[], str]):
self._get_token = token_provider
def auth_flow(self, request: httpx.Request):
request.headers["Authorization"] = f"Bearer {self._get_token()}"
yield request
@tool(description="Get the current working directory.", approval_mode="never_require")
def get_cwd() -> str:
"""Get the current working directory."""
try:
return os.getcwd()
except Exception as e:
return f"Error getting current working directory: {e}"
@tool(description="List files in a directory.", approval_mode="never_require")
def list_files(directory: str) -> list[str]:
"""List files in a directory."""
try:
return os.listdir(directory)
except Exception as e:
return [f"Error listing files in {directory}: {e}"]
@tool(description="Read the contents of a file.", approval_mode="never_require")
def read_file(file_path: str) -> str:
"""Read the contents of a file."""
try:
with open(file_path) as f:
return f.read()
except Exception as e:
return f"Error reading file {file_path}: {e}"
async def main():
credential = DefaultAzureCredential()
# Create the toolbox
token_provider = get_bearer_token_provider(credential, "https://ai.azure.com/.default")
# Resolve the endpoint once and derive a friendly tool name from it. When
# ``TOOLBOX_NAME`` isn't set, extract the toolbox name from the URL path so
# the tool's local name matches the upstream toolbox.
toolbox_endpoint = resolve_toolbox_endpoint()
toolbox_name = os.environ.get("TOOLBOX_NAME") or _toolbox_name_from_endpoint(toolbox_endpoint)
async with httpx.AsyncClient(
auth=ToolboxAuth(token_provider),
timeout=120.0,
) as http_client:
toolbox = MCPStreamableHTTPTool(
name=toolbox_name,
url=toolbox_endpoint,
http_client=http_client,
load_prompts=False,
)
# Create the chat client
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=credential,
)
agent = Agent(
client=client,
instructions=(
"You are a friendly assistant. Keep your answers brief. "
"Make sure all mathematical calculations are performed using the code interpreter "
"instead of mental arithmetic."
),
tools=[get_cwd, list_files, read_file, toolbox],
# History will be managed by the hosting infrastructure, thus there
# is no need to store history by the service. Learn more at:
# https://developers.openai.com/api/reference/resources/responses/methods/create
default_options={"store": False},
)
server = ResponsesHostServer(agent)
await server.run_async()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,2 @@
agent-framework-foundry
agent-framework-foundry-hosting>=1.0.0a260630
@@ -0,0 +1,121 @@
Contoso Corporation
Quarterly Report — Q1 2026 (Three months ended March 31, 2026)
DISCLAIMER
This document contains fictional data for sample/demo purposes only.
Contoso is a fictional company; all figures below are fabricated.
------------------------------------------------------------
1. EXECUTIVE SUMMARY
------------------------------------------------------------
Contoso delivered a solid first quarter, with total revenue of
$1,482.6M, up 11.4% year-over-year. Growth was led by the Cloud
Services segment (+22.7% YoY) and continued double-digit expansion
in International markets. Operating margin expanded 140 basis points
to 23.8% on disciplined cost management and improved gross margin.
Key highlights:
- Revenue: $1,482.6M (YoY +11.4%)
- Gross profit: $912.0M (gross margin 61.5%)
- Operating income: $352.9M (operating margin 23.8%)
- Net income: $268.4M (net margin 18.1%)
- Diluted EPS: $1.27 (vs. $1.04 prior year)
- Free cash flow: $311.5M
- Cash & equivalents: $2,140.8M
------------------------------------------------------------
2. INCOME STATEMENT (USD millions, unaudited)
------------------------------------------------------------
Q1 2026 Q1 2025 YoY %
Revenue 1,482.6 1,330.7 +11.4%
Cost of revenue 570.6 538.9 +5.9%
Gross profit 912.0 791.8 +15.2%
Gross margin 61.5% 59.5% +200 bps
Operating expenses
Research & development 241.4 220.5 +9.5%
Sales & marketing 218.7 205.1 +6.6%
General & administrative 99.0 88.6 +11.7%
Total operating expenses 559.1 514.2 +8.7%
Operating income 352.9 277.6 +27.1%
Operating margin 23.8% 20.9% +290 bps
Other income / (expense), net 8.4 5.1
Income before taxes 361.3 282.7
Provision for income taxes 92.9 72.6
Net income 268.4 210.1 +27.7%
Diluted EPS (USD) 1.27 1.04 +22.1%
------------------------------------------------------------
3. REVENUE BY SEGMENT (USD millions)
------------------------------------------------------------
Segment Q1 2026 Q1 2025 YoY %
Cloud Services 612.4 499.1 +22.7%
Productivity Software 448.9 422.6 +6.2%
Devices & Hardware 267.0 260.4 +2.5%
Professional Services 154.3 148.6 +3.8%
Total revenue 1,482.6 1,330.7 +11.4%
------------------------------------------------------------
4. REVENUE BY GEOGRAPHY (USD millions)
------------------------------------------------------------
Region Q1 2026 Q1 2025 YoY %
North America 812.1 756.0 +7.4%
EMEA 388.5 340.2 +14.2%
Asia-Pacific 221.7 183.4 +20.9%
Latin America 60.3 51.1 +18.0%
Total revenue 1,482.6 1,330.7 +11.4%
------------------------------------------------------------
5. SELECTED BALANCE SHEET ITEMS (USD millions)
------------------------------------------------------------
Mar 31, Dec 31,
2026 2025
Cash & equivalents 2,140.8 1,902.3
Short-term investments 845.6 820.4
Accounts receivable, net 1,012.7 988.5
Total current assets 4,510.2 4,190.6
Goodwill & intangibles 2,330.1 2,338.9
Total assets 9,884.5 9,512.0
Total current liabilities 2,118.4 2,054.7
Long-term debt 1,750.0 1,750.0
Total liabilities 4,402.6 4,310.5
Total stockholders' equity 5,481.9 5,201.5
------------------------------------------------------------
6. CASH FLOW HIGHLIGHTS (USD millions)
------------------------------------------------------------
Q1 2026 Q1 2025
Net cash from operating activities 382.0 298.7
Capital expenditures (70.5) (62.1)
Free cash flow 311.5 236.6
Share repurchases (120.0) (90.0)
Dividends paid (54.2) (48.6)
------------------------------------------------------------
7. KEY OPERATING METRICS
------------------------------------------------------------
Cloud paid seats (millions) 48.6 39.7 +22.4%
Cloud net revenue retention 118% 114%
Active enterprise customers 18,420 16,905 +9.0%
Headcount (end of period) 22,140 20,610 +7.4%
------------------------------------------------------------
8. OUTLOOK — Q2 2026 GUIDANCE
------------------------------------------------------------
Revenue: $1,520M $1,560M (YoY +10% to +13%)
Operating margin: 23.5% 24.5%
Diluted EPS: $1.30 $1.36
Capital expenditures: ~$80M
Management remains confident in the full-year plan and reiterates
fiscal-year 2026 revenue growth of 1012% and operating-margin
expansion of 100150 basis points versus FY 2025.
------------------------------------------------------------
9. NOTES
------------------------------------------------------------
- All figures are unaudited and rounded to one decimal place.
- Year-over-year comparisons are versus the same period in 2025.
- "Free cash flow" is defined as net cash from operating activities
less capital expenditures, and is a non-GAAP measure.
- This sample report is intended solely for demonstration of an
agent-driven document analysis pipeline.
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

@@ -0,0 +1,7 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.env
@@ -0,0 +1,3 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
ENABLE_SENSITIVE_DATA=true
@@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
COPY . user_agent/
WORKDIR /app/user_agent
RUN if [ -f requirements.txt ]; then \
pip install -r requirements.txt; \
else \
echo "No requirements.txt found"; \
fi
EXPOSE 8088
CMD ["python", "main.py"]
@@ -0,0 +1,53 @@
# What this sample demonstrates
An instrumented [Agent Framework](https://github.com/microsoft/agent-framework) agent hosted using the **Responses protocol**.
## How It Works
### Model Integration
The agent uses `FoundryChatClient` from the Agent Framework to create a Responses client from the project endpoint and model deployment. The agent supports both streaming (SSE events) and non-streaming (JSON) response modes.
See [main.py](main.py) for the full implementation.
### Agent Hosting
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
### Instrumentation
Agent Framework is [**natively instrumented**](https://learn.microsoft.com/en-us/agent-framework/agents/observability?pivots=programming-language-python) to capture diagnostics and telemetry for agent execution. Instrumentation is enabled by default. To also capture sensitive event payloads (prompts, tool arguments, etc.) set `ENABLE_SENSITIVE_DATA=true`. This sample demonstrates how to manage these settings via environment variables in `agent.manifest.yaml` and `agent.yaml`.
Foundry Hosted Agent has built-in observability thus you don't need to set up exporters manually to capture telemetry from your code. The traces, metrics, and logs generated by the agent are automatically collected and made available through Foundry's observability stack via Azure Monitor/Application Insights. The `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable is injected when the agent is deployed to Foundry, however it is still required to be set in your environment if you want to run the agent host locally and have telemetry sent to Application Insights from your local environment.
## Running the Agent Host
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
## Interacting with the agent
> Because the observability exporters are managed by Foundry, this sample must be run using `azd ai agent run`. Run this sample using `python main.py` will not send telemetry to Application Insights.
After the agent host is running locally, you can interact with the agent using the `azd ai agent invoke --local` command. For example:
```bash
azd ai agent invoke --local "What is the current weather?"
```
A couple of spans will be created for this request from Agent Framework's instrumentation, representing the generation of the response by the agent:
- `invoke_agent`: This span represents the invocation of the agent itself, capturing the start and end of the agent's processing for this request.
- `chat`: This span represents the call to the underlying model.
- `execute_tool`: This span represents the execution of any tools invoked by the agent as part of generating the response.
> For more information on the spans, refer to the [OpenTelemetry GenAI Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/)
## Deploying the Agent to Foundry
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
### Viewing Telemetry in Foundry
Once the agent is deployed to Foundry, the telemetry generated by the agent (traces, metrics, and logs) will be automatically collected and sent to Azure Monitor/Application Insights. You can view this telemetry by navigating to the Application Insights resource associated with your Foundry project or directly from the Foundry UI.
In the Foundry UI, next to the **Playground** tab is the **Traces** tab, where you can find the conversations and their corresponding trace IDs. Clicking on a trace ID will allow you to drill into the detailed trace information for that particular conversation.
@@ -0,0 +1,25 @@
name: agent-framework-agent-observability-responses
description: >
A basic Agent Framework agent hosted by Foundry.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
template:
name: agent-framework-agent-observability-responses
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
- name: ENABLE_SENSITIVE_DATA
value: true
resources:
- kind: model
id: gpt-4.1-mini
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
@@ -0,0 +1,14 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: agent-framework-agent-observability-responses
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: "0.5Gi"
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
- name: ENABLE_SENSITIVE_DATA
value: true
@@ -0,0 +1,57 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from random import randint
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
from pydantic import Field
# Load environment variables from .env file
load_dotenv()
@tool(approval_mode="never_require", description="Get the current location of the user.")
def get_current_location() -> str:
"""Get the current location of the agent."""
locations = ["New York", "London", "Paris", "Tokyo"]
return locations[randint(0, len(locations) - 1)]
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
async def main():
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=DefaultAzureCredential(),
)
agent = Agent(
client=client,
instructions="You are a friendly assistant. Keep your answers brief.",
tools=[get_weather, get_current_location],
# History will be managed by the hosting infrastructure, thus there
# is no need to store history by the service. Learn more at:
# https://developers.openai.com/api/reference/resources/responses/methods/create
default_options={"store": False},
)
server = ResponsesHostServer(agent)
await server.run_async()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,2 @@
agent-framework-foundry
agent-framework-foundry-hosting>=1.0.0a260630
@@ -0,0 +1,8 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.env
provision_index.py
@@ -0,0 +1,4 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
AZURE_SEARCH_ENDPOINT="https://<your-search>.search.windows.net"
AZURE_SEARCH_INDEX_NAME="contoso-outdoors"
@@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
COPY . user_agent/
WORKDIR /app/user_agent
RUN if [ -f requirements.txt ]; then \
pip install -r requirements.txt; \
else \
echo "No requirements.txt found"; \
fi
EXPOSE 8088
CMD ["python", "main.py"]
@@ -0,0 +1,186 @@
# What this sample demonstrates
An [Agent Framework](https://github.com/microsoft/agent-framework) agent with **Retrieval Augmented Generation (RAG)** capabilities backed by **Azure AI Search**, hosted using the **Responses protocol**. The agent grounds its answers in product documentation by running a search against an Azure AI Search index before each model invocation, then citing the source in its response.
## How It Works
### Model Integration
The agent uses `FoundryChatClient` from the Agent Framework to create a Responses client from the project endpoint and model deployment.
### RAG via Azure AI Search
`AzureAISearchContextProvider` runs a search against the configured Azure AI Search index **before each model invocation** and injects the top results into the model context. The agent then composes a grounded answer and cites the source document.
See [main.py](main.py) for the full implementation.
### Agent Hosting
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
## Prerequisites
- An Azure AI Foundry project with a deployed model (e.g., `gpt-4.1-mini`)
- An Azure AI Search service ([create one](https://learn.microsoft.com/azure/search/search-create-service-portal))
- **A pre-provisioned search index** with the schema and content described below
- Azure CLI logged in (`az login`)
### Required RBAC
Your identity (or the Managed Identity running the container in production) needs:
- **Azure AI User** on the Foundry project scope
- **Search Index Data Reader** on the Azure AI Search service (the sample only reads from the index)
## Provisioning the search index (one time)
The sample assumes the search index already exists and contains documents the agent can retrieve from. Provision it once via the Azure Portal, the [REST API](https://learn.microsoft.com/azure/search/search-how-to-create-search-index), or one of the snippets below.
### Option A: Python script (recommended)
[`provision_index.py`](provision_index.py) creates the index (if it doesn't already exist) and seeds it with the three Contoso Outdoors documents using `DefaultAzureCredential`. Your identity needs the following roles on the **Azure AI Search service** scope:
- **Search Service Contributor** — to create the index
- **Search Index Data Contributor** — to upload documents
> Note: `Search Service Contributor` only covers control-plane operations (create/list/delete indexes). It does **not** grant document write access — `Search Index Data Contributor` is required for that even if you already have `Search Service Contributor`.
Grant the roles to your signed-in user (replace `<search-name>` and `<rg>`):
```powershell
$searchId = az search service show -n <search-name> -g <rg> --query id -o tsv
$me = az ad signed-in-user show --query id -o tsv
az role assignment create --assignee $me --role "Search Service Contributor" --scope $searchId
az role assignment create --assignee $me --role "Search Index Data Contributor" --scope $searchId
```
Role propagation typically takes 15 minutes. Also confirm the search service has RBAC enabled (Portal → search service → **Keys****API Access control** → "Both" or "Role-based access control"); if it is set to "API Key" only, every AAD request returns `403 Forbidden`.
Then, from this directory:
```bash
export AZURE_SEARCH_ENDPOINT="https://<your-search>.search.windows.net"
export AZURE_SEARCH_INDEX_NAME="contoso-outdoors"
python provision_index.py
```
Or in PowerShell:
```powershell
$env:AZURE_SEARCH_ENDPOINT="https://<your-search>.search.windows.net"
$env:AZURE_SEARCH_INDEX_NAME="contoso-outdoors"
python provision_index.py
```
The script is safe to re-run: if the index already exists, it leaves the schema untouched and merges-or-uploads the documents. To change the schema, delete the index first (Azure AI Search does not allow modifying existing field attributes) and re-run the script.
### Index schema
| Field | Type | Attributes |
|---|---|---|
| `id` | `Edm.String` | key, filterable |
| `content` | `Edm.String` | searchable (full-text) |
| `sourceName` | `Edm.String` | retrievable, filterable |
| `sourceLink` | `Edm.String` | retrievable |
### Option B: Azure CLI + REST
```bash
SEARCH_ENDPOINT="https://<your-search>.search.windows.net"
INDEX_NAME="contoso-outdoors"
TOKEN=$(az account get-access-token --resource https://search.azure.com --query accessToken -o tsv)
# 1. Create the index.
curl -X PUT "$SEARCH_ENDPOINT/indexes/$INDEX_NAME?api-version=2024-07-01" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "contoso-outdoors",
"fields": [
{ "name": "id", "type": "Edm.String", "key": true, "filterable": true, "searchable": false, "retrievable": true },
{ "name": "content", "type": "Edm.String", "key": false, "filterable": false, "searchable": true, "retrievable": true, "analyzer": "standard.lucene" },
{ "name": "sourceName", "type": "Edm.String", "key": false, "filterable": true, "searchable": false, "retrievable": true },
{ "name": "sourceLink", "type": "Edm.String", "key": false, "filterable": false, "searchable": false, "retrievable": true }
]
}'
# 2. Upload three Contoso Outdoors documents matching the queries below.
curl -X POST "$SEARCH_ENDPOINT/indexes/$INDEX_NAME/docs/index?api-version=2024-07-01" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"value": [
{ "@search.action": "mergeOrUpload", "id": "return-policy", "sourceName": "Contoso Outdoors Return Policy", "sourceLink": "https://contoso.com/policies/returns", "content": "Customers may return any item within 30 days of delivery. Items should be unused and include original packaging. Refunds are issued to the original payment method within 5 business days of inspection. As a thank you, every accepted return ships back with a complimentary Contoso TrailRunner sticker pack, item code TR-CANARY-7821." },
{ "@search.action": "mergeOrUpload", "id": "shipping-guide", "sourceName": "Contoso Outdoors Shipping Guide", "sourceLink": "https://contoso.com/help/shipping", "content": "Standard shipping is free on orders over $50 and typically arrives in 3-5 business days within the continental United States. Expedited options are available at checkout. Use promo code SHIP-CANARY-4493 at checkout for a one-time free overnight upgrade on your first order." },
{ "@search.action": "mergeOrUpload", "id": "tent-care", "sourceName": "TrailRunner Tent Care Instructions", "sourceLink": "https://contoso.com/manuals/trailrunner-tent", "content": "Clean the tent fabric with lukewarm water and a non-detergent soap. Allow it to air dry completely before storage and avoid prolonged UV exposure to extend the lifespan of the waterproof coating. Replacement waterproofing kits are stocked under SKU TENT-CANARY-9067." }
]
}'
```
You can also point the sample at any existing index that exposes a retrievable text field such as `content`.
## Running the Agent Host
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
In addition to the standard environment variables, this sample requires:
```bash
export AZURE_SEARCH_ENDPOINT="https://<your-search>.search.windows.net"
export AZURE_SEARCH_INDEX_NAME="contoso-outdoors"
```
Or in PowerShell:
```powershell
$env:AZURE_SEARCH_ENDPOINT="https://<your-search>.search.windows.net"
$env:AZURE_SEARCH_INDEX_NAME="contoso-outdoors"
```
You can also place these in a `.env` file next to `main.py` — see [`.env.example`](.env.example).
## Interacting with the agent
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
```bash
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "What is your return policy?"}'
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "How long does shipping take?"}'
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "How do I clean my tent?"}'
```
Or with `azd`:
```bash
azd ai agent invoke --local "What is your return policy?"
```
## How RAG works in this sample
`AzureAISearchContextProvider` runs a search against the configured Azure AI Search index **before each model invocation**. When the index is seeded with the three Contoso Outdoors documents from the provisioning section above:
| User query mentions | Search result injected |
|---|---|
| "return", "refund" | Contoso Outdoors Return Policy (canary token: `TR-CANARY-7821`) |
| "shipping", "promo" | Contoso Outdoors Shipping Guide (canary token: `SHIP-CANARY-4493`) |
| "tent", "fabric" | TrailRunner Tent Care Instructions (canary token: `TENT-CANARY-9067`) |
The model receives the top three search results as additional context and cites the source in its response. Each seeded document includes a unique `*-CANARY-*` token that does not exist in any model training data, so you can prove an answer was grounded in retrieved content (not fabricated from training) by asking for the canary and checking it appears in the response.
Replace the seed documents (or point the sample at an existing index with your own content) to ground the agent in your own knowledge base.
## Deploying the Agent to Foundry
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
When deploying, make sure `AZURE_SEARCH_ENDPOINT` and `AZURE_SEARCH_INDEX_NAME` are set in your `azd` environment so they get injected into the hosted container per [`agent.manifest.yaml`](agent.manifest.yaml):
```bash
azd env set AZURE_SEARCH_ENDPOINT "https://<your-search>.search.windows.net"
azd env set AZURE_SEARCH_INDEX_NAME "contoso-outdoors"
```
If these are not set, running `azd ai agent init -m <agent-manifest.yaml>` will prompt you to enter them interactively.
The deployed agent's Managed Identity needs **Search Index Data Reader** on the Azure AI Search service.
@@ -0,0 +1,38 @@
name: agent-framework-agent-azure-search-rag-responses
description: >
An Agent Framework agent with Retrieval Augmented Generation (RAG) capabilities
backed by Azure AI Search. Uses AzureAISearchContextProvider to ground answers
in product documentation indexed in Azure AI Search before each model invocation.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- RAG
- Azure AI Search
template:
name: agent-framework-agent-azure-search-rag-responses
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
- name: AZURE_SEARCH_ENDPOINT
value: "{{AZURE_SEARCH_ENDPOINT}}"
- name: AZURE_SEARCH_INDEX_NAME
value: "{{AZURE_SEARCH_INDEX_NAME}}"
parameters:
properties:
- name: AZURE_SEARCH_ENDPOINT
secret: false
description: The endpoint of the Azure AI Search service to use for RAG (e.g., https://my-search-service.search.windows.net)
- name: AZURE_SEARCH_INDEX_NAME
secret: false
description: The name of the Azure AI Search index to use for RAG (e.g., contoso-outdoors)
resources:
- kind: model
id: gpt-4.1-mini
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
@@ -0,0 +1,16 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: agent-framework-agent-azure-search-rag-responses
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: "0.5Gi"
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
- name: AZURE_SEARCH_ENDPOINT
value: ${AZURE_SEARCH_ENDPOINT}
- name: AZURE_SEARCH_INDEX_NAME
value: ${AZURE_SEARCH_INDEX_NAME}
@@ -0,0 +1,59 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent
from agent_framework.azure import AzureAISearchContextProvider
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
async def main():
credential = DefaultAzureCredential()
# Connect to a pre-provisioned Azure AI Search index. The index is expected to
# exist and contain documents with the schema described in README.md
# (id / content / sourceName / sourceLink). The context provider runs a search
# against this index before each model invocation and injects the matching
# documents into the model context.
search_provider = AzureAISearchContextProvider(
source_id="azure_search_rag",
endpoint=os.environ["AZURE_SEARCH_ENDPOINT"],
index_name=os.environ["AZURE_SEARCH_INDEX_NAME"],
credential=credential,
mode="semantic",
top_k=3,
)
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=credential,
)
async with search_provider:
agent = Agent(
client=client,
instructions=(
"You are a helpful support specialist for Contoso Outdoors. "
"Answer questions using the provided context and cite the source "
"document when available."
),
context_providers=[search_provider],
# History will be managed by the hosting infrastructure, thus there
# is no need to store history by the service. Learn more at:
# https://developers.openai.com/api/reference/resources/responses/methods/create
default_options={"store": False},
)
server = ResponsesHostServer(agent)
await server.run_async()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,119 @@
# Copyright (c) Microsoft. All rights reserved.
"""Provision the Azure AI Search index used by this sample.
Creates (or updates) the ``contoso-outdoors`` index with the schema documented
in README.md and seeds it with three Contoso Outdoors documents containing
unique canary tokens. Safe to re-run; existing documents are merged-or-uploaded.
Usage (from this directory, with the venv activated and ``az login`` done):
python provision_index.py
Required env vars (also read from a local ``.env`` file if present):
AZURE_SEARCH_ENDPOINT e.g. https://<your-search>.search.windows.net
AZURE_SEARCH_INDEX_NAME e.g. contoso-outdoors
Your identity needs ``Search Service Contributor`` (to create the index) and
``Search Index Data Contributor`` (to upload documents) on the search service.
"""
import asyncio
import os
from azure.core.exceptions import ResourceNotFoundError
from azure.identity.aio import DefaultAzureCredential
from azure.search.documents.aio import SearchClient
from azure.search.documents.indexes.aio import SearchIndexClient
from azure.search.documents.indexes.models import (
SearchableField, # pyright: ignore[reportUnknownVariableType]
SearchFieldDataType,
SearchIndex,
SimpleField, # pyright: ignore[reportUnknownVariableType]
)
from dotenv import load_dotenv
DOCUMENTS: list[dict[str, str]] = [
{
"id": "return-policy",
"sourceName": "Contoso Outdoors Return Policy",
"sourceLink": "https://contoso.com/policies/returns",
"content": (
"Customers may return any item within 30 days of delivery. Items should be "
"unused and include original packaging. Refunds are issued to the original "
"payment method within 5 business days of inspection. As a thank you, every "
"accepted return ships back with a complimentary Contoso TrailRunner sticker "
"pack, item code TR-CANARY-7821."
),
},
{
"id": "shipping-guide",
"sourceName": "Contoso Outdoors Shipping Guide",
"sourceLink": "https://contoso.com/help/shipping",
"content": (
"Standard shipping is free on orders over $50 and typically arrives in 3-5 "
"business days within the continental United States. Expedited options are "
"available at checkout. Use promo code SHIP-CANARY-4493 at checkout for a "
"one-time free overnight upgrade on your first order."
),
},
{
"id": "tent-care",
"sourceName": "TrailRunner Tent Care Instructions",
"sourceLink": "https://contoso.com/manuals/trailrunner-tent",
"content": (
"Clean the tent fabric with lukewarm water and a non-detergent soap. Allow "
"it to air dry completely before storage and avoid prolonged UV exposure to "
"extend the lifespan of the waterproof coating. Replacement waterproofing "
"kits are stocked under SKU TENT-CANARY-9067."
),
},
]
def build_index(name: str) -> SearchIndex:
return SearchIndex(
name=name,
fields=[
SimpleField(name="id", type=SearchFieldDataType.STRING, key=True, filterable=True),
SearchableField(name="content", type=SearchFieldDataType.STRING, analyzer_name="standard.lucene"),
SimpleField(name="sourceName", type=SearchFieldDataType.STRING, filterable=True, retrievable=True),
SimpleField(name="sourceLink", type=SearchFieldDataType.STRING, retrievable=True),
],
)
async def main() -> None:
load_dotenv()
endpoint = os.environ["AZURE_SEARCH_ENDPOINT"]
index_name = os.environ["AZURE_SEARCH_INDEX_NAME"]
async with (
DefaultAzureCredential() as credential,
SearchIndexClient(endpoint=endpoint, credential=credential) as index_client,
SearchClient(endpoint=endpoint, index_name=index_name, credential=credential) as search_client,
):
index = build_index(index_name)
try:
await index_client.get_index(index_name)
print(
f"Index '{index_name}' already exists; leaving schema as-is "
"(delete the index manually to change the schema)."
)
except ResourceNotFoundError:
print(f"Creating index '{index_name}'...")
await index_client.create_index(index)
print(f"Uploading {len(DOCUMENTS)} document(s)...")
results = await search_client.merge_or_upload_documents(documents=DOCUMENTS) # type: ignore[arg-type]
failed = [(r.key, r.error_message) for r in results if not r.succeeded]
if failed:
raise RuntimeError(f"Failed to upload documents: {failed}")
print("Done.")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,3 @@
agent-framework-azure-ai-search
agent-framework-foundry
agent-framework-foundry-hosting>=1.0.0a260630
@@ -0,0 +1,10 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.env
provision_skills.py
skills
downloaded_skills
@@ -0,0 +1,6 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
# Comma-separated list of Foundry skill names to download at startup.
SKILL_NAMES="support-style,escalation-policy"
# Optional writable directory for downloaded skills. Defaults to the system temp directory.
# DOWNLOADED_SKILLS_DIR="/tmp/maf_downloaded_skills"
@@ -0,0 +1 @@
downloaded_skills/
@@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
COPY . user_agent/
WORKDIR /app/user_agent
RUN if [ -f requirements.txt ]; then \
pip install -r requirements.txt; \
else \
echo "No requirements.txt found"; \
fi
EXPOSE 8088
CMD ["python", "main.py"]
@@ -0,0 +1,139 @@
# What this sample demonstrates
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that loads its behavioral guidelines from [**Foundry Skills**](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/skills?view=foundry&pivots=python) at startup, hosted using the **Responses protocol**. Skills are authored once as `SKILL.md` files, uploaded to your Foundry project through `AIProjectClient.beta.skills`, and downloaded by the agent on boot so updates ship without code changes.
## How It Works
### Authoring skills
Each skill is a Markdown file with a YAML front matter block. This sample ships two source skills under [`skills/`](skills/):
| Skill | Purpose |
|---|---|
| [`support-style`](skills/support-style/SKILL.md) | Voice, formatting, and signature rules for Contoso Outdoors support replies. |
| [`escalation-policy`](skills/escalation-policy/SKILL.md) | When and how to escalate a customer ticket. |
Each `SKILL.md` includes a unique `*-CANARY-*` token that the model is asked to echo, so you can prove the skill was loaded from Foundry (not hallucinated) by checking the response.
> The `name` and `description` values in the YAML front matter must be **unquoted** — quoting them causes the Skills REST API to return HTTP 500 on import.
### Uploading skills with `AIProjectClient`
[`provision_skills.py`](provision_skills.py) walks `skills/*/SKILL.md`, packages each file as an in-memory ZIP (with `SKILL.md` at the archive root), and imports it through [`AIProjectClient.beta.skills.create_from_package`](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/skills?view=foundry&pivots=python#option-2-import-from-a-skillmd-zip). The client is constructed with `allow_preview=True` (Skills is a preview feature) and authenticates with `DefaultAzureCredential`. Existing skills are deleted first via `beta.skills.delete` so the script is safe to re-run after editing a `SKILL.md`, and `beta.skills.list` is called at the end to verify each skill round-trips.
### Downloading skills at agent startup
[`main.py`](main.py) reads the comma-separated `SKILL_NAMES` env var, opens an `AIProjectClient` (also with `allow_preview=True`), and for each skill name streams the ZIP archive from `beta.skills.download(name)` and unpacks it into a **separate writable runtime directory**. By default this directory is created under the system temp folder as `maf_downloaded_skills/<name>/`, which works in hosted containers where the application directory may be read-only. Set `DOWNLOADED_SKILLS_DIR` to override the location.
A [`SkillsProvider`](../../../../../packages/core/agent_framework/_skills.py) is then built over the downloaded skills directory and attached to the `Agent` as a context provider. The provider follows the [Agent Skills](https://agentskills.io/) progressive-disclosure pattern:
1. **Advertise** — skill names and descriptions are injected into the system prompt at session start (~100 tokens per skill).
2. **Load** — the model calls the `load_skill` tool when it decides a skill is relevant to the user's turn, and the full `SKILL.md` body is returned.
This means the model only pays the token cost for a skill's full body when it actually needs it, and updating a skill in Foundry + restarting the agent is enough to pick up the change — no code redeploy required.
### Agent Hosting
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
## Prerequisites
- An Azure AI Foundry project with a deployed model (e.g., `gpt-4.1-mini`)
- Azure CLI logged in (`az login`)
### Required RBAC
Your identity (or the Managed Identity running the container in production) needs **Azure AI User** on the Foundry project scope. This single role covers both authoring skills with `provision_skills.py` and downloading them from `main.py`.
## Provisioning the skills (one time)
From this directory, with the venv activated and `az login` done:
```bash
export FOUNDRY_PROJECT_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>"
python provision_skills.py
```
Or in PowerShell:
```powershell
$env:FOUNDRY_PROJECT_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>"
python provision_skills.py
```
Expected output:
```text
Provisioning skill 'escalation-policy' from skills/escalation-policy/SKILL.md...
Imported skill 'escalation-policy' (id=skill_..., has_blob=True).
Provisioning skill 'support-style' from skills/support-style/SKILL.md...
Imported skill 'support-style' (id=skill_..., has_blob=True).
Done.
```
Re-running the script after editing a `SKILL.md` re-imports the skill, replacing the previous version.
> To remove a skill manually, call `project.beta.skills.delete("<name>")` on an `AIProjectClient` constructed with `allow_preview=True`.
## Running the Agent Host
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
In addition to the standard environment variables, this sample requires:
```bash
export SKILL_NAMES="support-style,escalation-policy"
```
Or in PowerShell:
```powershell
$env:SKILL_NAMES="support-style,escalation-policy"
```
You can also place these in a `.env` file next to `main.py` — see [`.env.example`](.env.example).
On startup you should see:
```text
Downloading skill 'support-style' from Foundry...
Downloading skill 'escalation-policy' from Foundry...
```
The downloaded `SKILL.md` files land under `DOWNLOADED_SKILLS_DIR/<name>/SKILL.md`. The directory is recreated from scratch on every run, so deleting it manually is never necessary.
By default, the sample uses the system temp directory, for example `/tmp/maf_downloaded_skills` on Linux. To choose a different writable location, set `DOWNLOADED_SKILLS_DIR` before startup.
## Interacting with the agent
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
```bash
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Hi, I am Alex. I just want to confirm I can return my tent within 30 days."}'
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "I want a $750 refund on Order #A-1042 right now or I am calling my lawyer."}'
```
| Prompt mentions | Skill that should drive the response |
|---|---|
| Routine return / shipping / care question | Model loads `support-style` (canary `STYLE-CANARY-3318`) — no escalation. |
| Injury, legal threat, press, or refund > $500 | Model loads `escalation-policy` (canary `ESC-CANARY-7742`) **and** `support-style`. |
Because skills are loaded on demand, the canary token in a response also proves the model actually invoked `load_skill` for the matching skill (not just saw its name in the advertised list).
## Deploying the Agent to Foundry
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
When deploying, make sure `SKILL_NAMES` is set in your `azd` environment so it gets injected into the hosted container per [`agent.manifest.yaml`](agent.manifest.yaml):
```bash
azd env set SKILL_NAMES "support-style,escalation-policy"
```
If it is not set, running `azd ai agent init -m <agent.manifest.yaml>` will prompt you to enter it interactively.
The deployed agent's Managed Identity needs **Azure AI User** on the Foundry project to download skills at startup. Make sure you have run `provision_skills.py` against the same Foundry project before deploying — otherwise the agent will fail to start with HTTP 404 on the skill download.
> The `skills/` source folder is **not** deployed to Foundry — only the downloaded skills are used at runtime. The `provision_skills.py` step is required to upload the skills to Foundry before the agent can download them.
@@ -0,0 +1,32 @@
name: agent-framework-agent-foundry-skills-responses
description: >
An Agent Framework agent that downloads its instructions from the Foundry
Skills REST API at startup, demonstrating how to decouple behavioral
guidelines (tone, escalation policy, etc.) from agent code.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Foundry Skills
template:
name: agent-framework-agent-foundry-skills-responses
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
- name: SKILL_NAMES
value: "{{SKILL_NAMES}}"
parameters:
properties:
- name: SKILL_NAMES
secret: false
description: Comma-separated list of Foundry skill names to download at startup (e.g., support-style,escalation-policy)
resources:
- kind: model
id: gpt-4.1-mini
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
@@ -0,0 +1,14 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: agent-framework-agent-foundry-skills-responses
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: "0.5Gi"
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
- name: SKILL_NAMES
value: ${SKILL_NAMES}
@@ -0,0 +1,116 @@
# Copyright (c) Microsoft. All rights reserved.
"""Foundry Skills hosted agent sample.
At startup, this agent downloads each Foundry Skill named in
``SKILL_NAMES`` from the project's ``beta.skills`` API, unpacks each
one into a separate writable runtime directory and wires
that directory into a :class:`SkillsProvider` so the agent advertises the
skills to the model and loads them on demand (progressive disclosure).
Upload the skills to Foundry once with ``provision_skills.py`` before running
this sample.
"""
import asyncio
import io
import logging
import os
import shutil
import tempfile
import zipfile
from pathlib import Path
from typing import Final
from agent_framework import Agent, SkillsProvider
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.ai.projects.aio import AIProjectClient
from azure.identity.aio import DefaultAzureCredential
from dotenv import load_dotenv
load_dotenv()
# Runtime directory where skills downloaded from Foundry are unpacked.
# Kept separate from the static ``skills/`` source folder so the two never
# get confused: the source folder is the input to ``provision_skills.py``
# and the runtime folder is the output of this script's bootstrap step.
# Defaults to a system temp location because hosted containers may mount the
# application directory read-only. Set DOWNLOADED_SKILLS_DIR to override it.
_DEFAULT_DOWNLOADED_SKILLS_DIR: Final = Path(tempfile.gettempdir()) / "maf_downloaded_skills"
_DOWNLOADED_SKILLS_DIR_ENV = os.environ.get("DOWNLOADED_SKILLS_DIR")
DOWNLOADED_SKILLS_DIR: Final = Path((_DOWNLOADED_SKILLS_DIR_ENV or "").strip() or _DEFAULT_DOWNLOADED_SKILLS_DIR)
logger = logging.getLogger(__name__)
def _safe_extract_zip(zf: zipfile.ZipFile, dest_dir: Path) -> None:
"""Extract ``zf`` into ``dest_dir``, rejecting entries that escape it (zip-slip guard)."""
dest_root = dest_dir.resolve()
for member in zf.infolist():
member_path = (dest_root / member.filename).resolve()
if dest_root != member_path and dest_root not in member_path.parents:
raise RuntimeError(f"Refusing to extract unsafe path '{member.filename}' outside of '{dest_root}'.")
zf.extractall(dest_dir)
async def _bootstrap_skills(endpoint: str, skill_names: list[str], target_dir: Path) -> None:
"""Download each named skill via ``project.beta.skills`` and unpack it as ``<target_dir>/<name>/SKILL.md``."""
if target_dir.exists(): # noqa: ASYNC240
shutil.rmtree(target_dir)
target_dir.mkdir(parents=True) # noqa: ASYNC240
async with (
DefaultAzureCredential() as credential,
AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project,
):
for name in skill_names:
logger.info(f"Downloading skill '{name}' from Foundry...")
stream = await project.beta.skills.download(name)
zip_bytes = b"".join([chunk async for chunk in stream])
skill_dir = target_dir / name
skill_dir.mkdir()
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
_safe_extract_zip(zf, skill_dir)
if not (skill_dir / "SKILL.md").is_file():
raise RuntimeError(f"Downloaded archive for '{name}' did not contain a SKILL.md at the root.")
async def main() -> None:
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
skill_names = [name.strip() for name in os.environ["SKILL_NAMES"].split(",") if name.strip()]
if not skill_names:
raise RuntimeError("SKILL_NAMES must list at least one skill name.")
# Pull the latest copy of each skill from Foundry into a runtime-only folder.
await _bootstrap_skills(project_endpoint, skill_names, DOWNLOADED_SKILLS_DIR)
# Build a SkillsProvider over the unpacked folder. The provider advertises
# each skill's name + description to the model and exposes the ``load_skill``
# tool the model uses to retrieve the full SKILL.md body on demand. No
# script_runner is configured because the skills in this sample are
# instruction-only.
skills_provider = SkillsProvider.from_paths(skill_paths=str(DOWNLOADED_SKILLS_DIR))
async with DefaultAzureCredential() as credential:
client = FoundryChatClient(
project_endpoint=project_endpoint,
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=credential,
)
agent = Agent(
client=client,
instructions="You are a customer-support assistant for Contoso Outdoors.",
context_providers=[skills_provider],
# History will be managed by the hosting infrastructure, thus there
# is no need to store history by the service. Learn more at:
# https://developers.openai.com/api/reference/resources/responses/methods/create
default_options={"store": False},
)
server = ResponsesHostServer(agent)
await server.run_async()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,96 @@
# Copyright (c) Microsoft. All rights reserved.
"""Provision Foundry Skills used by this sample.
For each ``skills/<name>/SKILL.md`` file in this directory, this script packages
the file as an in-memory ZIP and imports it through the Foundry project's
:class:`~azure.ai.projects.aio.AIProjectClient` so the skill becomes downloadable
by any hosted agent in the project.
If a skill with the same name already exists in Foundry, it is deleted first
so the script is safe to re-run after editing a ``SKILL.md`` file.
Usage (from this directory, with the venv activated and ``az login`` done):
python provision_skills.py
Required env vars (also read from a local ``.env`` file if present):
FOUNDRY_PROJECT_ENDPOINT e.g. https://<account>.services.ai.azure.com/api/projects/<project>
Your identity needs the ``Azure AI User`` role on the Foundry project.
"""
import asyncio
import io
import os
import zipfile
from pathlib import Path
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import CreateSkillVersionFromFilesBody
from azure.core.exceptions import ResourceNotFoundError
from azure.identity.aio import DefaultAzureCredential
from dotenv import load_dotenv
SKILLS_DIR = Path(__file__).parent / "skills"
def _zip_skill_md(skill_md: Path) -> bytes:
"""Return the bytes of a ZIP archive containing ``SKILL.md`` at the root."""
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr("SKILL.md", skill_md.read_text(encoding="utf-8"))
return buffer.getvalue()
async def _delete_skill_if_exists(project: AIProjectClient, name: str) -> None:
try:
await project.beta.skills.delete(name)
except ResourceNotFoundError:
return
print(f" Deleted existing skill '{name}'.")
async def main() -> None:
load_dotenv()
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
skill_files = sorted(SKILLS_DIR.glob("*/SKILL.md"))
if not skill_files:
raise RuntimeError(f"No SKILL.md files found under {SKILLS_DIR}.")
async with (
DefaultAzureCredential() as credential,
AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project,
):
for skill_md in skill_files:
name = skill_md.parent.name
print(f"Provisioning skill '{name}' from {skill_md.relative_to(SKILLS_DIR.parent)}...")
await _delete_skill_if_exists(project, name)
imported = await project.beta.skills.create_from_files(
name,
content=CreateSkillVersionFromFilesBody(
files=[(f"{name}.zip", _zip_skill_md(skill_md), "application/zip")]
),
)
print(f" Imported skill '{imported.name}' (id={imported.skill_id}, version={imported.version}).")
print("Verifying skills via project.beta.skills.list()...")
listed = {skill.name: skill async for skill in project.beta.skills.list()}
for skill_md in skill_files:
name = skill_md.parent.name
skill = listed.get(name)
if skill is None:
raise RuntimeError(f"Skill '{name}' was imported but is not present in the project listing.")
print(
f" OK '{skill.name}': id={skill.id}, "
f"description={skill.description!r}, default_version={skill.default_version}"
)
print("Done.")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,3 @@
agent-framework-foundry
agent-framework-foundry-hosting>=1.0.0a260630
azure-ai-projects
@@ -0,0 +1,30 @@
---
name: escalation-policy
description: When and how to escalate Contoso Outdoors customer-support tickets.
---
# Contoso Outdoors Escalation Policy
You must follow this escalation policy on every conversation.
## Escalate immediately when the customer
- Reports an injury, allergic reaction, or other safety incident.
- Mentions legal action, regulators, or the press.
- Has waited more than 14 days for a refund that was already approved.
- Requests a refund larger than $500.
## How to escalate
1. Acknowledge the issue in one sentence.
2. Tell the customer you are escalating to a senior specialist.
3. Provide the escalation reference `ESC-CANARY-7742` and the SLA: a senior
specialist will reply within 1 business day.
4. Do not promise a specific outcome (refund, replacement, compensation) on
escalated tickets — only the senior specialist can commit to one.
## Do not escalate
- Routine returns within the standard 30-day window.
- Shipping status questions.
- Product care and usage questions.
@@ -0,0 +1,25 @@
---
name: support-style
description: Contoso Outdoors customer-support tone and formatting guidelines.
---
# Contoso Outdoors Support Style
You are speaking on behalf of Contoso Outdoors customer support.
## Voice
- Warm, concise, and confident — never apologetic in a hand-wringing way.
- Use the customer's name when it is known.
- Sign every response with `— Contoso Outdoors Support`.
## Formatting
- Keep replies to 13 short paragraphs unless the customer asks for detail.
- Use bullet lists only when enumerating concrete steps or options.
- Always reference order numbers as `Order #<id>` (e.g. `Order #A-1042`).
## Canary
To prove this skill was loaded, include the token `STYLE-CANARY-3318` on a
separate line at the bottom of every response, prefixed with `# `.
@@ -0,0 +1,8 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.env
provision_memory_store.py
@@ -0,0 +1,6 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
# Embedding model deployment (only needed by provision_memory_store.py).
AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME="text-embedding-3-small"
# Name of the Foundry Memory Store the agent should read/write to.
MEMORY_STORE_NAME="agent_framework_memory"
@@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
COPY . user_agent/
WORKDIR /app/user_agent
RUN if [ -f requirements.txt ]; then \
pip install -r requirements.txt; \
else \
echo "No requirements.txt found"; \
fi
EXPOSE 8088
CMD ["python", "main.py"]
@@ -0,0 +1,122 @@
# What this sample demonstrates
An [Agent Framework](https://github.com/microsoft/agent-framework) agent with persistent semantic memory backed by an **Azure AI Foundry Memory Store**, hosted using the **Responses protocol**. The agent remembers facts the user has shared (e.g., dietary preferences, name) across sessions by retrieving and updating memories around every model invocation via `FoundryMemoryProvider`.
## How It Works
### Model Integration
The agent uses `FoundryChatClient` from the Agent Framework to create a Responses client from the project endpoint and model deployment. `allow_preview=True` is passed so the same `AIProjectClient` can also call the preview `beta.memory_stores` API.
### Memory via Foundry Memory Store
`FoundryMemoryProvider` is wired into the agent as a context provider. Around each model invocation it:
1. **Retrieves user-profile memories** for the configured `scope` (e.g., user id) on the first turn of a session.
2. **Searches for contextual memories** matching the current user message and injects them into the model context.
3. **Updates the store** with new facts inferred from the conversation.
Crucially, the provider is constructed with `project_client=client.project_client` — i.e. it reuses the `AIProjectClient` that `FoundryChatClient` already created, instead of allocating a second one. This keeps a single authentication context and connection pool for both chat and memory operations.
See [main.py](main.py) for the full implementation.
### Agent Hosting
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
## Prerequisites
- An Azure AI Foundry project with:
- A deployed chat model (e.g., `gpt-4.1-mini`)
- A deployed embedding model (e.g., `text-embedding-3-small`) — used by the memory store itself, not by the agent at runtime
- Azure CLI logged in (`az login`)
### Required RBAC
Your identity (or the Managed Identity running the container in production) needs **Azure AI User** on the Foundry project scope. This single role covers both provisioning the memory store with `provision_memory_store.py` and reading/writing memories from `main.py`.
## Provisioning the memory store (one time)
[`provision_memory_store.py`](provision_memory_store.py) creates a Foundry Memory Store with the user-profile capability enabled (and chat-summary disabled) using `AIProjectClient.beta.memory_stores.create`. It is safe to re-run: if a store with the same name already exists, the script leaves it alone.
From this directory, with the venv activated and `az login` done:
```bash
export FOUNDRY_PROJECT_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>"
export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4.1-mini"
export AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME="text-embedding-3-small"
export MEMORY_STORE_NAME="agent_framework_memory"
python provision_memory_store.py
```
Or in PowerShell:
```powershell
$env:FOUNDRY_PROJECT_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4.1-mini"
$env:AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME="text-embedding-3-small"
$env:MEMORY_STORE_NAME="agent_framework_memory"
python provision_memory_store.py
```
Expected output (first run):
```text
Creating memory store 'agent_framework_memory'...
Created memory store 'agent_framework_memory' (id=memstore_...).
```
> To delete the store manually, call `project.beta.memory_stores.delete("<name>")` on an `AIProjectClient` constructed with `allow_preview=True`.
## Running the Agent Host
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
In addition to the standard environment variables, this sample requires:
```bash
export MEMORY_STORE_NAME="agent_framework_memory"
```
Or in PowerShell:
```powershell
$env:MEMORY_STORE_NAME="agent_framework_memory"
```
You can also place these in a `.env` file next to `main.py` — see [`.env.example`](.env.example).
## Interacting with the agent
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details.
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. The first request seeds a memory; subsequent requests (especially in new sessions) should be able to recall it because memories are persisted across Foundry Hosted Agents sessions.
> In this sample, the memory is scoped to the user by specifying `scope="{{$userId}}"`, thus memories are isolated across different users but shared across different sessions from the same user.
```bash
# 1. Tell the agent something to remember.
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" \
-d '{"input": "I prefer dark roast coffee and I am allergic to nuts."}'
# Wait a few seconds for the memory to be stored, then start a fresh conversation:
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" \
-d '{"input": "Can you recommend a coffee and a snack for me?"}'
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" \
-d '{"input": "What do you remember about my preferences?"}'
```
## Deploying the Agent to Foundry
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
When deploying, make sure `MEMORY_STORE_NAME` is set in your `azd` environment so it gets injected into the hosted container per [`agent.manifest.yaml`](agent.manifest.yaml):
```bash
azd env set MEMORY_STORE_NAME "agent_framework_memory"
```
If these are not set, running `azd ai agent init -m <agent.manifest.yaml>` will prompt you to enter them interactively.
The deployed agent's Managed Identity needs **Azure AI User** on the Foundry project to read and write memories at runtime. Make sure you have run `provision_memory_store.py` against the same Foundry project before deploying — otherwise the agent will fail on the first turn when it tries to read from a non-existent store.
@@ -0,0 +1,33 @@
name: agent-framework-agent-foundry-memory-responses
description: >
An Agent Framework agent with persistent semantic memory backed by an
Azure AI Foundry Memory Store. Uses FoundryMemoryProvider to retrieve and
store memories around each model invocation, allowing the agent to remember
facts about a user across sessions.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Foundry Memory
template:
name: agent-framework-agent-foundry-memory-responses
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
- name: MEMORY_STORE_NAME
value: "{{MEMORY_STORE_NAME}}"
parameters:
properties:
- name: MEMORY_STORE_NAME
secret: false
description: The name of the pre-provisioned Foundry Memory Store the agent will use (e.g., agent_framework_memory)
resources:
- kind: model
id: gpt-4.1-mini
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
@@ -0,0 +1,14 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: agent-framework-agent-foundry-memory-responses
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: "0.5Gi"
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
- name: MEMORY_STORE_NAME
value: ${MEMORY_STORE_NAME}
@@ -0,0 +1,71 @@
# Copyright (c) Microsoft. All rights reserved.
"""Foundry Memory hosted agent sample.
This agent uses :class:`FoundryMemoryProvider` to give an otherwise stateless
hosted agent persistent, semantic memory backed by an Azure AI Foundry
Memory Store. The store itself is provisioned once via
``provision_memory_store.py`` and its name is passed in through the
``MEMORY_STORE_NAME`` environment variable.
Unlike the standalone ``azure_ai_foundry_memory.py`` sample, here we construct
the :class:`FoundryChatClient` first and then reuse its underlying
``AIProjectClient`` for the memory provider, so both share a single client
instance and authentication context.
"""
import asyncio
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient, FoundryMemoryProvider
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity.aio import DefaultAzureCredential
from dotenv import load_dotenv
load_dotenv()
async def main() -> None:
# The chat client owns the AIProjectClient. ``allow_preview=True`` is required
# so the same client can call the preview ``beta.memory_stores`` API used by
# FoundryMemoryProvider.
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=DefaultAzureCredential(),
allow_preview=True,
)
# Reuse the project_client that FoundryChatClient just created, instead of
# constructing a second one for the memory provider.
memory_provider = FoundryMemoryProvider(
project_client=client.project_client,
memory_store_name=os.environ["MEMORY_STORE_NAME"],
# Scope memories by user id, so each user that interacts with the agent
# has their own isolated memories in the store (assuming those users are
# granted access). `{{userId}}` is a special placeholder that the hosting
# infrastructure will replace with the actual user id at runtime.
scope="{{$userId}}",
)
agent = Agent(
client=client,
instructions=(
"You are a helpful assistant that remembers facts the user has shared "
"across conversations. Relevant memories from previous interactions are "
"automatically provided to you in the system context. Use them when "
"answering, and acknowledge when you are relying on remembered facts."
),
context_providers=[memory_provider],
# History will be managed by the hosting infrastructure, thus there
# is no need to store history by the service. Learn more at:
# https://developers.openai.com/api/reference/resources/responses/methods/create
default_options={"store": False},
)
server = ResponsesHostServer(agent)
await server.run_async()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,90 @@
# Copyright (c) Microsoft. All rights reserved.
"""Provision the Azure AI Foundry Memory Store used by this sample.
Creates the memory store named by ``MEMORY_STORE_NAME`` if it does not
already exist. The store is configured with the user-profile capability so the
agent can remember stable facts about a user across sessions; chat-summary is
disabled to keep the demo focused on durable preferences. Safe to re-run: if a
store with the same name already exists, the script leaves it alone.
Usage (from this directory, with the venv activated and ``az login`` done):
python provision_memory_store.py
Required env vars (also read from a local ``.env`` file if present):
FOUNDRY_PROJECT_ENDPOINT e.g. https://<account>.services.ai.azure.com/api/projects/<project>
AZURE_AI_MODEL_DEPLOYMENT_NAME Chat model deployment used by the memory store
AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME Embedding model deployment used by the memory store
MEMORY_STORE_NAME Name of the memory store to create
Your identity needs ``Azure AI User`` on the Foundry project scope.
"""
import asyncio
import os
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import (
MemoryStoreDefaultDefinition,
MemoryStoreDefaultOptions,
)
from azure.core.exceptions import ResourceNotFoundError
from azure.identity.aio import DefaultAzureCredential
from dotenv import load_dotenv
load_dotenv()
async def main() -> None:
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
memory_store_name = os.environ["MEMORY_STORE_NAME"]
chat_model = os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"]
embedding_model = os.environ["AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME"]
async with (
DefaultAzureCredential() as credential,
AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project,
):
try:
existing = await project.beta.memory_stores.get(name=memory_store_name)
print(f"Memory store '{existing.name}' already exists (id={existing.id}); leaving as-is.")
return
except ResourceNotFoundError:
pass
print(f"Creating memory store '{memory_store_name}'...")
definition = MemoryStoreDefaultDefinition(
chat_model=chat_model,
embedding_model=embedding_model,
options=MemoryStoreDefaultOptions(
chat_summary_enabled=False,
user_profile_enabled=True,
user_profile_details=(
"Avoid irrelevant or sensitive data, such as age, financials, precise location, and credentials"
),
),
)
created = await project.beta.memory_stores.create(
name=memory_store_name,
description="Memory store for the Agent Framework foundry-hosted memory sample",
definition=definition,
)
print(f"Created memory store '{created.name}' (id={created.id}).")
# Verify the store actually exists on the service by reading it back.
# ``create`` returns the requested definition, but a follow-up ``get``
# confirms the store is persisted and reachable for the agent at runtime.
try:
verified = await project.beta.memory_stores.get(name=memory_store_name)
except ResourceNotFoundError as exc:
raise RuntimeError(
f"Memory store '{memory_store_name}' was not found after creation; "
"the service may not have persisted it."
) from exc
print(f"Verified memory store '{verified.name}' is available on the service (id={verified.id}).")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,3 @@
agent-framework-foundry
agent-framework-foundry-hosting>=1.0.0a260630
azure-ai-projects
@@ -0,0 +1,25 @@
FROM python:3.12-slim
# Bring in the `uv` binary from a pinned Astral image. Update this tag intentionally;
# `latest` would make rebuilds non-deterministic.
COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /uvx /usr/local/bin/
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
UV_PROJECT_ENVIRONMENT=/app/.venv \
PATH="/app/.venv/bin:${PATH}"
WORKDIR /app
# Sync dependencies first to maximize Docker layer caching.
COPY pyproject.toml ./
RUN uv sync --no-install-project --no-cache
# Now copy the rest of the agent and finalize the environment.
COPY . ./
RUN uv sync --no-cache
EXPOSE 8088
CMD ["uv", "run", "--no-sync", "python", "main.py"]
@@ -0,0 +1,116 @@
# What this sample demonstrates
An [Agent Framework](https://github.com/microsoft/agent-framework) agent with a
**Monty-backed CodeAct context provider** hosted using the **Responses protocol**.
The model receives one tool (`execute_code`) and runs Python inside a
[Monty](https://github.com/pydantic/monty) interpreter; the registered host
tools (`compute`, `fetch_data`) are only reachable from inside the sandbox via
typed `await compute(...)` calls or the generic `call_tool(...)` fallback.
> [!NOTE]
> `agent-framework-monty` is an **alpha** package, so the `pyproject.toml`
> sets `[tool.uv] prerelease = "allow"` to let `uv sync` pick up the
> `1.0.0a*` release from PyPI.
## How It Works
### Model Integration
The agent uses `FoundryChatClient` to create a Responses client from the project
endpoint and the model deployment. The agent supports both streaming (SSE
events) and non-streaming (JSON) response modes.
See [main.py](main.py) for the full implementation.
### CodeAct context provider
`MontyCodeActProvider` is added to the agent via `context_providers=[...]`. On
every run it injects:
- An `execute_code` tool that runs Python in the Monty interpreter.
- Dynamic CodeAct instructions describing the available host tools and DSL.
The host tools (`compute`, `fetch_data`) are **not** exposed as direct agent
tools — the model can only call them from inside `execute_code`, either as
typed async functions (`await compute(operation="multiply", a=6, b=7)`) or via
the generic `call_tool("compute", operation="multiply", a=6, b=7)` fallback.
Code is type-checked against the host tool signatures using
[ty](https://docs.astral.sh/ty/) before any tool runs.
OS-level access (filesystem, network, subprocess) is blocked inside the
sandbox; the registered host tools retain full Python access.
### Observability
Agent Framework's [native OpenTelemetry instrumentation](https://learn.microsoft.com/en-us/agent-framework/agents/observability?pivots=programming-language-python) is enabled by setting these env vars in `agent.yaml` / `agent.manifest.yaml`:
- `ENABLE_INSTRUMENTATION=true` — turns on the framework's span/metric/log emitters.
- `ENABLE_SENSITIVE_DATA=true` — includes prompts, tool inputs, tool outputs, and completions in telemetry. **Dev/test only.**
`main.py` wires Azure Monitor at startup:
1. Reads `APPLICATIONINSIGHTS_CONNECTION_STRING` (Foundry hosting injects this automatically for the project's attached Application Insights resource; set it yourself when running locally).
2. Calls `azure.monitor.opentelemetry.configure_azure_monitor(connection_string=...)` to register Azure Monitor exporters with the global OTel tracer/meter/logger providers.
3. Calls `agent_framework.observability.enable_instrumentation()` so Agent Framework emits its `invoke_agent`, `chat`, `execute_tool`, and `execute_code` spans on those providers.
Trace linking happens automatically: the Foundry hosting layer's incoming `Responses` request becomes the **parent span**, and every framework / tool span (including the `execute_code` invocation that runs Monty) becomes a child via OpenTelemetry context propagation since both layers share the same global tracer provider. In Application Insights you can click any operation and see the full tree from inbound HTTP all the way down to individual `compute(...)` / `fetch_data(...)` calls inside the Monty sandbox.
## Running the Agent Host
This sample uses `pyproject.toml` + `uv sync` rather than the parent
README's `requirements.txt` flow. To run locally:
1. Install dependencies into a local virtual environment:
```bash
uv sync
```
2. Set the environment variables described in the
[parent README](../../README.md#running-the-agent-host-locally) (Foundry
project endpoint, model deployment, optional Application Insights), then
start the host:
```bash
uv run python main.py
```
Refer to the parent README for the shared `azd` / Docker / invocation /
deployment guidance.
## Interacting with the agent
> Depending on how you run the agent host, you can invoke the agent using
> `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the
> [parent README](../../README.md) for more details. Use this README for
> sample queries you can send to the agent.
Send a POST request to the server with a JSON body containing an `"input"`
field. Try queries that benefit from combining Python with multiple tool calls:
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "Fetch all users, find the admins, then multiply the count by 7. Use a single execute_code call."}'
```
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "Compute the total price for one of every product in the products table. Use execute_code."}'
```
The model should respond with one `execute_code` call whose code looks like:
```python
users = await fetch_data(table="users")
admins = [u for u in users if u["role"] == "admin"]
result = await compute(operation="multiply", a=len(admins), b=7)
print(result)
```
## Deploying the Agent to Foundry
To host the agent on Foundry, follow the instructions in the
[Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry)
section of the README in the parent directory.
@@ -0,0 +1,28 @@
name: agent-framework-agent-monty-codeact-responses
description: >
An Agent Framework agent with a Monty-backed CodeAct context provider hosted by Foundry.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- CodeAct
- Monty
template:
name: agent-framework-agent-monty-codeact-responses
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
- name: ENABLE_INSTRUMENTATION
value: "true"
- name: ENABLE_SENSITIVE_DATA
value: "true"
resources:
- kind: model
id: gpt-4.1-mini
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
@@ -0,0 +1,15 @@
kind: hosted
name: agent-framework-agent-monty-codeact-responses
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
- name: ENABLE_INSTRUMENTATION
value: "true"
- name: ENABLE_SENSITIVE_DATA
value: "true"
@@ -0,0 +1,92 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from typing import Annotated, Any, Literal
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from agent_framework_monty import MontyCodeActProvider
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
from pydantic import Field
# Load environment variables from .env file (no-op when injected by Foundry).
load_dotenv()
@tool(approval_mode="never_require")
def compute(
operation: Annotated[
Literal["add", "subtract", "multiply", "divide"],
Field(description="Math operation: add, subtract, multiply, or divide."),
],
a: Annotated[float, Field(description="First numeric operand.")],
b: Annotated[float, Field(description="Second numeric operand.")],
) -> float:
"""Perform a math operation used by sandboxed code."""
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b else float("inf"),
}
return operations[operation]
@tool(approval_mode="never_require")
def fetch_data(
table: Annotated[str, Field(description="Name of the simulated table to query.")],
) -> list[dict[str, Any]]:
"""Fetch simulated records from a named table."""
data: dict[str, list[dict[str, Any]]] = {
"users": [
{"id": 1, "name": "Alice", "role": "admin"},
{"id": 2, "name": "Bob", "role": "user"},
{"id": 3, "name": "Charlie", "role": "admin"},
],
"products": [
{"id": 101, "name": "Widget", "price": 9.99},
{"id": 102, "name": "Gadget", "price": 19.99},
],
}
return data.get(table, [])
def main() -> None:
"""Host a Monty CodeAct agent over the Responses protocol."""
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=DefaultAzureCredential(),
)
# MontyCodeActProvider injects a sandboxed `execute_code` tool into every
# agent run, plus dynamic instructions describing the registered host tools.
# The host tools are hidden from the model - they can only be invoked from
# inside the sandbox (`await compute(...)` or `call_tool(...)`).
codeact = MontyCodeActProvider(
tools=[compute, fetch_data],
approval_mode="never_require",
)
agent = Agent(
client=client,
instructions=(
"You are a friendly assistant. Use `execute_code` to combine "
"Python control flow with the provided host tools whenever the "
"task requires lookups, transformations, or computation."
),
context_providers=[codeact],
# History will be managed by the hosting infrastructure, thus there
# is no need to store history by the service. Learn more at:
# https://developers.openai.com/api/reference/resources/responses/methods/create
default_options={"store": False},
)
server = ResponsesHostServer(agent)
server.run()
if __name__ == "__main__":
main()
@@ -0,0 +1,17 @@
[project]
name = "agent-framework-agent-monty-codeact-responses"
version = "0.1.0"
description = "Foundry-hosted Agent Framework agent with a Monty-backed CodeAct context provider."
requires-python = ">=3.12,<3.14"
dependencies = [
"agent-framework-foundry",
"agent-framework-foundry-hosting>=1.0.0a260630",
# agent-framework-monty is an alpha (1.0.0a*) release on PyPI.
"agent-framework-monty",
]
[tool.uv]
# `agent-framework-monty` is an alpha package; allow the prerelease resolver
# to pick up 1.0.0a* releases from PyPI.
prerelease = "allow"

Some files were not shown because too many files have changed in this diff Show More