From c0a5259a3ec2ba508cc5095e82ec91946e0743c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:19:54 +0000 Subject: [PATCH 1/4] Initial plan From 011496904bb2bbac5cb32d123232f57c4713d663 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:45:23 +0000 Subject: [PATCH 2/4] Use Responses API for hosted web search Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> --- .../AGUIClientServer/AGUIServer/Program.cs | 50 +++++----- .../05-end-to-end/AGUIClientServer/README.md | 21 +++-- ...HostingServiceCollectionExtensionsTests.cs | 92 +++++++++++++++++++ 3 files changed, 130 insertions(+), 33 deletions(-) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs index e3de957c1e7..b160aeb4362 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs @@ -7,7 +7,6 @@ using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; using Microsoft.Extensions.AI; -using OpenAI.Chat; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient().AddLogging(); @@ -19,35 +18,15 @@ const string AgentName = "AGUIAssistant"; -// Create the AI agent with tools +// Create a Responses-backed chat client so that hosted tools are sent in the tools array. // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -var agent = new AzureOpenAIClient( +IChatClient chatClient = new AzureOpenAIClient( new Uri(endpoint), new DefaultAzureCredential()) - .GetChatClient(deploymentName) - .AsAIAgent( - name: AgentName, - tools: [ - AIFunctionFactory.Create( - () => DateTimeOffset.UtcNow, - name: "get_current_time", - description: "Get the current UTC time." - ), - AIFunctionFactory.Create( - ([Description("The weather forecast request")]ServerWeatherForecastRequest request) => { - return new ServerWeatherForecastResponse() - { - Summary = "Sunny", - TemperatureC = 25, - Date = request.Date - }; - }, - name: "get_server_weather_forecast", - description: "Gets the forecast for a specific location and date", - AGUIServerSerializerContext.Default.Options) - ]); + .GetResponsesClient() + .AsIChatClient(deploymentName); // WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, // make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user @@ -57,7 +36,26 @@ // Register the agent with the host and configure it to use an in-memory session store // so that conversation state is maintained across requests. In production, you may want to use a persistent session store. builder - .AddAIAgent(AgentName, (_, _) => agent) + .AddAIAgent(AgentName, "You are a helpful assistant.", chatClient) + .WithAITools( + new HostedWebSearchTool(), + AIFunctionFactory.Create( + () => DateTimeOffset.UtcNow, + name: "get_current_time", + description: "Get the current UTC time."), + AIFunctionFactory.Create( + ([Description("The weather forecast request")] ServerWeatherForecastRequest request) => + { + return new ServerWeatherForecastResponse() + { + Summary = "Sunny", + TemperatureC = 25, + Date = request.Date + }; + }, + name: "get_server_weather_forecast", + description: "Gets the forecast for a specific location and date", + AGUIServerSerializerContext.Default.Options)) .WithInMemorySessionStore(); WebApplication app = builder.Build(); diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/README.md b/dotnet/samples/05-end-to-end/AGUIClientServer/README.md index 0501ec54694..388148e7109 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/README.md +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/README.md @@ -24,6 +24,8 @@ $env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" > **Note:** This sample uses `DefaultAzureCredential` for authentication. Make sure you're authenticated with Azure (e.g., via `az login`, Visual Studio, or environment variables). +> **Note:** The server uses the Azure OpenAI Responses API because hosted web search is a Responses API tool. Web search uses Grounding with Bing and may incur additional charges; review the [web search documentation and data usage terms](https://learn.microsoft.com/azure/foundry/openai/how-to/web-search) before using it. + ## Running the Sample ### Step 1: Start the AG-UI Server @@ -117,13 +119,18 @@ User (:q or quit to exit): :q The `AGUIServer` uses the `MapAGUIServer` extension method to expose an agent through the AG-UI protocol: ```csharp -AIAgent agent = new OpenAIClient(apiKey) - .GetChatClient(model) - .AsAIAgent( - instructions: "You are a helpful assistant.", - name: "AGUIAssistant"); - -app.MapAGUIServer("/", agent); +IChatClient chatClient = new AzureOpenAIClient( + new Uri(endpoint), + new DefaultAzureCredential()) + .GetResponsesClient() + .AsIChatClient(deploymentName); + +builder + .AddAIAgent("AGUIAssistant", "You are a helpful assistant.", chatClient) + .WithAITool(new HostedWebSearchTool()) + .WithInMemorySessionStore(); + +app.MapAGUIServer("AGUIAssistant", "/"); ``` This automatically handles: diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000000..13d1772c963 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using OpenAI; +using OpenAI.Responses; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +public sealed class AgentHostingServiceCollectionExtensionsTests +{ + [Fact] + public async Task AddAIAgent_WithHostedWebSearchTool_UsesResponsesToolAsync() + { + // Arrange + using var handler = new RecordingHandler(); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + using IChatClient chatClient = new ResponsesClient( + new ApiKeyCredential("test-key"), + new OpenAIClientOptions + { + Endpoint = new Uri("https://example.test/v1"), + Transport = new HttpClientPipelineTransport(httpClient) + }) + .AsIChatClient("test-model"); + + var services = new ServiceCollection(); + services + .AddAIAgent("test-agent", "You are a helpful assistant.", chatClient) + .WithAITool(new HostedWebSearchTool()); + using ServiceProvider serviceProvider = services.BuildServiceProvider(); + AIAgent agent = serviceProvider.GetRequiredKeyedService("test-agent"); + + // Act + await agent.RunAsync("What happened in the news today?"); + + // Assert + Assert.Equal("/v1/responses", handler.RequestUri?.AbsolutePath); + using JsonDocument request = JsonDocument.Parse(Assert.IsType(handler.RequestBody)); + Assert.Contains( + request.RootElement.GetProperty("tools").EnumerateArray(), + tool => tool.GetProperty("type").GetString() == "web_search"); + Assert.False(request.RootElement.TryGetProperty("web_search_options", out _)); + } + + private sealed class RecordingHandler : HttpMessageHandler + { + public Uri? RequestUri { get; private set; } + + public string? RequestBody { get; private set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + this.RequestUri = request.RequestUri; + this.RequestBody = await request.Content!.ReadAsStringAsync(cancellationToken); + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + """ + { + "id": "resp_1", + "object": "response", + "created_at": 1700000000, + "status": "completed", + "model": "test-model", + "output": [], + "usage": { + "input_tokens": 1, + "output_tokens": 1, + "total_tokens": 2 + } + } + """, + Encoding.UTF8, + "application/json"), + RequestMessage = request + }; + } + } +} From 3f197c8c812aa14a3984263406df5496a953b92c Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:55:31 +0100 Subject: [PATCH 3/4] .NET: Use OpenAI SDK for AG-UI web search Copilot-Session: 2b346dcb-1702-4296-bf22-b434b73fd26c --- .../AGUIServer/AGUIServer.csproj | 2 +- .../AGUIClientServer/AGUIServer/Program.cs | 11 ++++++----- .../05-end-to-end/AGUIClientServer/README.md | 17 +++++++++++------ ...ntHostingServiceCollectionExtensionsTests.cs | 9 ++++----- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/AGUIServer.csproj b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/AGUIServer.csproj index 8aa91a3406b..788f6a3f3ff 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/AGUIServer.csproj +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/AGUIServer.csproj @@ -9,8 +9,8 @@ - + diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs index b160aeb4362..1553a8947d1 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs @@ -1,12 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel.Primitives; using System.ComponentModel; using AGUIServer; -using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; using Microsoft.Extensions.AI; +using OpenAI; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient().AddLogging(); @@ -18,13 +19,13 @@ const string AgentName = "AGUIAssistant"; -// Create a Responses-backed chat client so that hosted tools are sent in the tools array. +// Create a Responses-backed OpenAI client that sends a bearer token to the Azure endpoint. // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -IChatClient chatClient = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +IChatClient chatClient = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetResponsesClient() .AsIChatClient(deploymentName); diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/README.md b/dotnet/samples/05-end-to-end/AGUIClientServer/README.md index 388148e7109..021ce829c05 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/README.md +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/README.md @@ -18,13 +18,18 @@ The demonstration has two components: Configure the required Azure OpenAI environment variables: ```powershell -$env:AZURE_OPENAI_ENDPOINT="<>" +$env:AZURE_OPENAI_ENDPOINT="https://.openai.azure.com/openai/v1/" $env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" ``` -> **Note:** This sample uses `DefaultAzureCredential` for authentication. Make sure you're authenticated with Azure (e.g., via `az login`, Visual Studio, or environment variables). +> [!NOTE] +> Include `/openai/v1/` in the endpoint. The OpenAI SDK uses `DefaultAzureCredential` to obtain a bearer token. Make sure you're authenticated with Azure, for example through `az login`, Visual Studio, or environment variables. -> **Note:** The server uses the Azure OpenAI Responses API because hosted web search is a Responses API tool. Web search uses Grounding with Bing and may incur additional charges; review the [web search documentation and data usage terms](https://learn.microsoft.com/azure/foundry/openai/how-to/web-search) before using it. +> [!NOTE] +> This sample calls Azure OpenAI inference directly through the resource endpoint. It does not require a Microsoft Foundry project. A project-scoped application would instead use a Foundry project endpoint with `Azure.AI.Projects` and the Agent Framework Foundry provider. + +> [!NOTE] +> The server uses the Azure OpenAI Responses API because hosted web search is a Responses API tool. Web search uses Grounding with Bing and may incur additional charges; review the [web search documentation and data usage terms](https://learn.microsoft.com/azure/foundry/openai/how-to/web-search) before using it. ## Running the Sample @@ -119,9 +124,9 @@ User (:q or quit to exit): :q The `AGUIServer` uses the `MapAGUIServer` extension method to expose an agent through the AG-UI protocol: ```csharp -IChatClient chatClient = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +IChatClient chatClient = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetResponsesClient() .AsIChatClient(deploymentName); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs index 13d1772c963..bd60116a371 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs @@ -19,7 +19,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; public sealed class AgentHostingServiceCollectionExtensionsTests { [Fact] - public async Task AddAIAgent_WithHostedWebSearchTool_UsesResponsesToolAsync() + public async Task HostedWebSearchTool_WithResponsesClient_UsesResponsesWireFormatAsync() { // Arrange using var handler = new RecordingHandler(); @@ -46,12 +46,11 @@ public async Task AddAIAgent_WithHostedWebSearchTool_UsesResponsesToolAsync() await agent.RunAsync("What happened in the news today?"); // Assert - Assert.Equal("/v1/responses", handler.RequestUri?.AbsolutePath); using JsonDocument request = JsonDocument.Parse(Assert.IsType(handler.RequestBody)); - Assert.Contains( - request.RootElement.GetProperty("tools").EnumerateArray(), - tool => tool.GetProperty("type").GetString() == "web_search"); Assert.False(request.RootElement.TryGetProperty("web_search_options", out _)); + JsonElement webSearchTool = Assert.Single(request.RootElement.GetProperty("tools").EnumerateArray()); + Assert.Equal("web_search", webSearchTool.GetProperty("type").GetString()); + Assert.Equal("/v1/responses", handler.RequestUri?.AbsolutePath); } private sealed class RecordingHandler : HttpMessageHandler From 3fcb0a5e6bd406d0b5b2959768cd4a7c6d307a54 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:20:57 +0100 Subject: [PATCH 4/4] .NET: Keep AG-UI web search history local Copilot-Session: 2b346dcb-1702-4296-bf22-b434b73fd26c --- .../05-end-to-end/AGUIClientServer/AGUIServer/Program.cs | 3 ++- dotnet/samples/05-end-to-end/AGUIClientServer/README.md | 4 ++-- .../AgentHostingServiceCollectionExtensionsTests.cs | 6 +++++- .../Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs index 1553a8947d1..6f1ed5ba3ef 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs @@ -8,6 +8,7 @@ using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; using Microsoft.Extensions.AI; using OpenAI; +using OpenAI.Responses; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient().AddLogging(); @@ -27,7 +28,7 @@ new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetResponsesClient() - .AsIChatClient(deploymentName); + .AsIChatClientWithStoredOutputDisabled(model: deploymentName); // WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, // make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/README.md b/dotnet/samples/05-end-to-end/AGUIClientServer/README.md index 021ce829c05..30adeb1eef2 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/README.md +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/README.md @@ -29,7 +29,7 @@ $env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" > This sample calls Azure OpenAI inference directly through the resource endpoint. It does not require a Microsoft Foundry project. A project-scoped application would instead use a Foundry project endpoint with `Azure.AI.Projects` and the Agent Framework Foundry provider. > [!NOTE] -> The server uses the Azure OpenAI Responses API because hosted web search is a Responses API tool. Web search uses Grounding with Bing and may incur additional charges; review the [web search documentation and data usage terms](https://learn.microsoft.com/azure/foundry/openai/how-to/web-search) before using it. +> The server uses the Azure OpenAI Responses API because hosted web search is a Responses API tool. It sets `store` to `false` so Agent Framework persists chat history in the configured session store instead of depending on service-retained responses. Web search uses Grounding with Bing and may incur additional charges; review the [web search documentation and data usage terms](https://learn.microsoft.com/azure/foundry/openai/how-to/web-search) before using it. ## Running the Sample @@ -128,7 +128,7 @@ IChatClient chatClient = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetResponsesClient() - .AsIChatClient(deploymentName); + .AsIChatClientWithStoredOutputDisabled(model: deploymentName); builder .AddAIAgent("AGUIAssistant", "You are a helpful assistant.", chatClient) diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs index bd60116a371..587af5ff339 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs @@ -33,7 +33,7 @@ public async Task HostedWebSearchTool_WithResponsesClient_UsesResponsesWireForma Endpoint = new Uri("https://example.test/v1"), Transport = new HttpClientPipelineTransport(httpClient) }) - .AsIChatClient("test-model"); + .AsIChatClientWithStoredOutputDisabled(model: "test-model"); var services = new ServiceCollection(); services @@ -47,6 +47,10 @@ public async Task HostedWebSearchTool_WithResponsesClient_UsesResponsesWireForma // Assert using JsonDocument request = JsonDocument.Parse(Assert.IsType(handler.RequestBody)); + Assert.False(request.RootElement.GetProperty("store").GetBoolean()); + Assert.Contains( + request.RootElement.GetProperty("include").EnumerateArray(), + property => property.GetString() == "reasoning.encrypted_content"); Assert.False(request.RootElement.TryGetProperty("web_search_options", out _)); JsonElement webSearchTool = Assert.Single(request.RootElement.GetProperty("tools").EnumerateArray()); Assert.Equal("web_search", webSearchTool.GetProperty("type").GetString()); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj index 17d9742436c..355e271d767 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj @@ -19,6 +19,7 @@ +