From 9e842e18214c4d5dfa03bfdfbac2e12316f9bb8f Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:11:26 +0100 Subject: [PATCH] Update chat client agent for contributor and devs --- .gitignore | 4 + dotnet/.gitignore | 9 +- dotnet/agent-framework-dotnet.slnx | 4 +- .../Hosted-ChatClientAgent/.env.local | 4 + .../Dockerfile.contributor | 19 +++ .../HostedChatClientAgent.csproj | 9 ++ .../Hosted-ChatClientAgent/Program.cs | 49 +++++++- .../Hosted-ChatClientAgent/README.md | 109 ++++++++++++++++++ .../agent.manifest.yaml | 28 +++++ .../Hosted-ChatClientAgent/agent.yaml | 29 ++--- .../Hosted-FoundryAgent/agent.manifest.yaml | 28 +++++ .../Hosted-FoundryAgent/agent.yaml | 29 ++--- .../SimpleAgent}/Program.cs | 0 .../SimpleAgent}/SimpleAgent.csproj | 2 +- .../Hosting/AgentSessionStore.cs | 2 +- .../Hosting/ServiceCollectionExtensions.cs | 10 +- 16 files changed, 284 insertions(+), 51 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{UsingHostedAgent => Using-Samples/SimpleAgent}/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{UsingHostedAgent => Using-Samples/SimpleAgent}/SimpleAgent.csproj (85%) diff --git a/.gitignore b/.gitignore index 089abb5395..28d753c87b 100644 --- a/.gitignore +++ b/.gitignore @@ -134,6 +134,10 @@ celerybeat.pid .venv env/ venv/ + +# Foundry agent CLI (contains secrets, auto-generated) +.foundry-agent.json +.foundry-agent-build.log ENV/ env.bak/ venv.bak/ diff --git a/dotnet/.gitignore b/dotnet/.gitignore index ce1409abe9..572680831e 100644 --- a/dotnet/.gitignore +++ b/dotnet/.gitignore @@ -402,4 +402,11 @@ FodyWeavers.xsd *.msp # JetBrains Rider -*.sln.iml \ No newline at end of file +*.sln.iml + +# Foundry agent CLI config (contains secrets, auto-generated) +.foundry-agent.json +.foundry-agent-build.log + +# Pre-published output for Docker builds +out/ \ No newline at end of file diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 969a5c16d8..5ffb5692fd 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -280,8 +280,8 @@ - - + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local new file mode 100644 index 0000000000..6d7831229d --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local @@ -0,0 +1,4 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor new file mode 100644 index 0000000000..200f674bdd --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/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-chat-client-agent . +# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-chat-client-agent --env-file .env hosted-chat-client-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", "HostedChatClientAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj index 096681d505..b1fe8d3e3c 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj @@ -14,8 +14,17 @@ + + + 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 448f0f16af..b4d76bbc52 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Azure.AI.Projects; +using Azure.Core; using Azure.Identity; using DotNetEnv; using Microsoft.Agents.AI; @@ -17,8 +18,14 @@ 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). +TokenCredential credential = new ChainedTokenCredential( + new DevTemporaryTokenCredential(), + new DefaultAzureCredential()); + // Create the agent via the AI project client using the Responses API. -AIAgent agent = new AIProjectClient(projectEndpoint, new DefaultAzureCredential()) +AIAgent agent = new AIProjectClient(projectEndpoint, credential) .AsAIAgent( model: deployment, instructions: """ @@ -44,3 +51,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-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md new file mode 100644 index 0000000000..0c5ce36cfe --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md @@ -0,0 +1,109 @@ +# Hosted-ChatClientAgent + +A simple general-purpose AI assistant hosted as a Foundry Hosted Agent using the Agent Framework instance hosting pattern. The agent is created inline via `AIProjectClient.AsAIAgent(model, instructions)` and served using the Responses protocol. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`) +- 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 +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +``` + +> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. + +## Running directly (contributors) + +This project uses `ProjectReference` to build against the local Agent Framework source. + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent +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": "hosted-chat-client-agent"}' +``` + +## 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-chat-client-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=hosted-chat-client-agent \ + -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ + --env-file .env \ + hosted-chat-client-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": "hosted-chat-client-agent"}' +``` + +## 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 `HostedChatClientAgent.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml new file mode 100644 index 0000000000..58a07d8bb3 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml +name: hosted-chat-client-agent +displayName: "Hosted Chat Client Agent" + +description: > + A simple general-purpose AI assistant hosted as a Foundry Hosted Agent + using the Agent Framework instance hosting pattern. + +metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - Streaming + - Agent Framework + +template: + name: hosted-chat-client-agent + kind: hosted + protocols: + - protocol: responses + version: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml index dfab24e712..0a97abc35a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml @@ -1,20 +1,9 @@ -name: simple-agent -description: > - A simple general-purpose AI assistant hosted as a Foundry Hosted Agent. -metadata: - tags: - - AI Agent Hosting - - Simple Agent -template: - name: simple-agent - kind: hosted - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_AI_PROJECT_ENDPOINT - value: ${AZURE_AI_PROJECT_ENDPOINT} - - name: AZURE_AI_MODEL_DEPLOYMENT_NAME - value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} - - name: AGENT_NAME - value: ${AGENT_NAME} +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml +kind: hosted +name: hosted-chat-client-agent +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml new file mode 100644 index 0000000000..9b33646c8a --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml +name: hosted-foundry-agent +displayName: "Hosted Foundry Agent" + +description: > + A simple general-purpose AI assistant hosted as a Foundry Hosted Agent, + backed by a Foundry-managed agent definition. + +metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - Streaming + - Agent Framework + +template: + name: hosted-foundry-agent + kind: hosted + protocols: + - protocol: responses + version: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml index a0fe89f966..74223e72fe 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml @@ -1,20 +1,9 @@ -name: simple-agent -description: > - A simple general-purpose AI assistant hosted as a Foundry Hosted Agent, - backed by a Foundry-managed agent definition. -metadata: - tags: - - AI Agent Hosting - - Simple Agent - - Foundry Agent -template: - name: simple-agent - kind: hosted - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_AI_PROJECT_ENDPOINT - value: ${AZURE_AI_PROJECT_ENDPOINT} - - name: AGENT_NAME - value: ${AGENT_NAME} +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml +kind: hosted +name: hosted-foundry-agent +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/SimpleAgent.csproj similarity index 85% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/SimpleAgent.csproj index 814b5dba2d..05e150880e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/SimpleAgent.csproj @@ -18,7 +18,7 @@ - + diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs index 6aef3269b4..c61584e9e0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Threading; using System.Threading.Tasks; diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index bf3b3421f6..fe3b07c023 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -87,11 +87,11 @@ public static class FoundryHostingExtensions services.TryAddKeyedSingleton(agent.Name, agent); services.TryAddKeyedSingleton(agent.Name, agentSessionStore); } - else - { - services.TryAddSingleton(agent); - services.TryAddSingleton(agentSessionStore); - } + + // Also register as the default (non-keyed) agent so requests + // without an agent name can resolve it (e.g., local dev tooling). + services.TryAddSingleton(agent); + services.TryAddSingleton(agentSessionStore); services.TryAddSingleton(); return services;