From 340ec82623ffbe047e063c768aab933f19d17586 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:29:36 +0100 Subject: [PATCH] Foundry Agent Hosting --- .../Hosted-ChatClientAgent/.env.local | 1 + .../Hosted-ChatClientAgent/Program.cs | 2 +- .../Hosted-FoundryAgent/.env.local | 4 + .../Dockerfile.contributor | 19 +++ .../HostedFoundryAgent.csproj | 9 ++ .../Hosted-FoundryAgent/Program.cs | 49 ++++++- .../Hosted-FoundryAgent/README.md | 121 ++++++++++++++++++ 7 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local index 6d7831229d..cbf693b3a9 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local @@ -2,3 +2,4 @@ AZURE_AI_PROJECT_ENDPOINT= ASPNETCORE_URLS=http://+:8088 ASPNETCORE_ENVIRONMENT=Development AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +AZURE_BEARER_TOKEN= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs index b4d76bbc52..21cf34852a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -19,7 +19,7 @@ var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; // Use a chained credential: try a temporary dev token first (for local Docker debugging), -// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production). +// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity running in foundry). TokenCredential credential = new ChainedTokenCredential( new DevTemporaryTokenCredential(), new DefaultAzureCredential()); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local new file mode 100644 index 0000000000..1fefe43ebd --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local @@ -0,0 +1,4 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_BEARER_TOKEN= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor new file mode 100644 index 0000000000..2b6a2dbbc4 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor @@ -0,0 +1,19 @@ +# Dockerfile for contributors building from the agent-framework repository source. +# +# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source, +# which means a standard multi-stage Docker build cannot resolve dependencies outside +# this folder. Instead, pre-publish the app targeting the container runtime and copy +# the output into the container: +# +# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out +# docker build -f Dockerfile.contributor -t hosted-foundry-agent . +# docker run --rm -p 8088:8088 -e AGENT_NAME= -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-foundry-agent +# +# For end-users consuming the NuGet package (not ProjectReference), use the standard +# Dockerfile which performs a full dotnet restore + publish inside the container. +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final +WORKDIR /app +COPY out/ . +EXPOSE 8088 +ENV ASPNETCORE_URLS=http://+:8088 +ENTRYPOINT ["dotnet", "HostedFoundryAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj index 2fc0ec43e4..e49a15769f 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj @@ -14,8 +14,17 @@ + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs index c946d60058..a8f2f249c8 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs @@ -2,6 +2,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; +using Azure.Core; using Azure.Identity; using DotNetEnv; using Microsoft.Agents.AI.Foundry; @@ -15,7 +16,13 @@ var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJE var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? throw new InvalidOperationException("AGENT_NAME is not set."); -var aiProjectClient = new AIProjectClient(projectEndpoint, new DefaultAzureCredential()); +// Use a chained credential: try a temporary dev token first (for local Docker debugging), +// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity running in foundry). +TokenCredential credential = new ChainedTokenCredential( + new DevTemporaryTokenCredential(), + new DefaultAzureCredential()); + +var aiProjectClient = new AIProjectClient(projectEndpoint, credential); // Retrieve the Foundry-managed agent by name (latest version). ProjectsAgentRecord agentRecord = await aiProjectClient @@ -37,3 +44,43 @@ if (app.Environment.IsDevelopment()) } app.Run(); + +/// +/// A for local Docker debugging only. +/// +/// When debugging and testing a hosted agent in a local Docker container, Azure CLI +/// and other interactive credentials are not available. This credential reads a +/// pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable. +/// +/// This should NOT be used in production — tokens expire (~1 hour) and cannot be refreshed. +/// In production, the Foundry platform injects a managed identity automatically. +/// +/// Generate a token on your host and pass it to the container: +/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ... +/// +internal sealed class DevTemporaryTokenCredential : TokenCredential +{ + private const string EnvironmentVariable = "AZURE_BEARER_TOKEN"; + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return GetAccessToken(); + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(GetAccessToken()); + } + + private static AccessToken GetAccessToken() + { + var token = Environment.GetEnvironmentVariable(EnvironmentVariable); + if (string.IsNullOrEmpty(token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md new file mode 100644 index 0000000000..b95e7ff808 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md @@ -0,0 +1,121 @@ +# Hosted-FoundryAgent + +A hosted agent that delegates to a **Foundry-managed agent definition**. Instead of defining the model, instructions, and tools inline in code, this sample retrieves an existing agent registered in the Foundry platform via `AIProjectClient.AsAIAgent(agentRecord)` and hosts it using the Responses protocol. + +This is the **Foundry hosting** pattern — the agent's behavior is configured in the platform (via Foundry UI, CLI, or API), and this server simply wraps and serves it. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a **registered agent** (created via Foundry UI, CLI, or API) +- Azure CLI logged in (`az login`) + +## Configuration + +Copy the template and fill in your project endpoint: + +```bash +cp .env.local .env +``` + +Edit `.env` and set your Azure AI Foundry project endpoint: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +``` + +> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. + +You also need to set `AGENT_NAME` — the name of the Foundry-managed agent to host. This is injected automatically by the Foundry platform when deployed. For local development, pass it as an environment variable. + +## Running directly (contributors) + +This project uses `ProjectReference` to build against the local Agent Framework source. + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent +AGENT_NAME= dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "Hello!" +``` + +Or with curl (specifying the agent name explicitly): + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "Hello!", "model": ""}' +``` + +## Running with Docker + +Since this project uses `ProjectReference`, the standard `Dockerfile` cannot resolve dependencies outside this folder. Use `Dockerfile.contributor` which takes a pre-published output. + +### 1. Publish for the container runtime (Linux Alpine) + +```bash +dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out +``` + +### 2. Build the Docker image + +```bash +docker build -f Dockerfile.contributor -t hosted-foundry-agent . +``` + +### 3. Run the container + +Generate a bearer token on your host and pass it to the container: + +```bash +# Generate token (expires in ~1 hour) +export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) + +# Run with token +docker run --rm -p 8088:8088 \ + -e AGENT_NAME= \ + -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ + --env-file .env \ + hosted-foundry-agent +``` + +> **Note:** `AGENT_NAME` is passed via `-e` to simulate the platform injection. `AZURE_BEARER_TOKEN` provides Azure credentials to the container (tokens expire after ~1 hour). The `.env` file provides the remaining configuration. + +### 4. Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "Hello!" +``` + +Or with curl (specifying the agent name explicitly): + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "Hello!", "model": ""}' +``` + +## NuGet package users + +If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor` — it performs a full `dotnet restore` and `dotnet publish` inside the container. See the commented section in `HostedFoundryAgent.csproj` for the `PackageReference` alternative. + +## How it differs from Hosted-ChatClientAgent + +| | Hosted-ChatClientAgent | Hosted-FoundryAgent | +|---|---|---| +| **Agent definition** | Inline in code (`AsAIAgent(model, instructions)`) | Managed in Foundry platform (`AsAIAgent(agentRecord)`) | +| **Model/instructions** | Set in `Program.cs` | Set in Foundry UI/CLI/API | +| **Tools** | Defined in code | Configured in the platform | +| **Use case** | Full control over agent behavior | Platform-managed agent with centralized config |