From a3a3e6f664dd7d90d2b0c33ddac56cef6b28c1d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Wed, 3 Sep 2025 15:59:12 -0700 Subject: [PATCH 1/4] Add support for using ConversationID for AzureOpenAI and OpenAI --- .../.template.config/template.json | 7 ++++ .../ChatWithCustomData-CSharp.Web.csproj.in | 3 ++ .../Components/Pages/Chat/Chat.razor | 13 +++++++ .../Program.Aspire.cs | 2 +- .../ChatWithCustomData-CSharp.Web/Program.cs | 4 +-- .../ResponseClientHelper.cs | 36 +++++++++++++++++++ .../Components/Pages/Chat/Chat.razor | 4 ++- .../aichatweb/aichatweb.Web/Program.cs | 3 +- .../aichatweb.Web/ResponseClientHelper.cs | 36 +++++++++++++++++++ .../aichatweb.Web/aichatweb.Web.csproj | 1 + .../aichatweb/aichatweb.Web/Program.cs | 2 +- .../Components/Pages/Chat/Chat.razor | 4 ++- .../aichatweb/Program.cs | 2 +- .../aichatweb/aichatweb.csproj | 1 + 14 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/ResponseClientHelper.cs diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json index caeceae1dde..5b85d11ae4a 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json @@ -60,10 +60,17 @@ "ChatWithCustomData-CSharp.AppHost/**", "ChatWithCustomData-CSharp.ServiceDefaults/**", "ChatWithCustomData-CSharp.Web/Program.Aspire.cs", + "ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs", "README.Aspire.md", "*.sln" ] }, + { + "condition": "(IsAspire && !IsOpenAI && !IsAzureOpenAI)", + "exclude": [ + "ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs" + ] + }, { "condition": "(IsAspire)", "exclude": [ diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in index 0e753e8c907..e4a5267b435 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in @@ -5,6 +5,9 @@ enable enable d5681fae-b21b-4114-b781-48180f08c0c4 + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor index a7b1502d894..6cb2806aace 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor @@ -66,12 +66,22 @@ var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); +@*#if (IsAzureOpenAI || IsOpenAI) + await foreach (var update in ChatClient.GetStreamingResponseAsync([userMessage], chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; //Update conversation ID from current response + ChatMessageItem.NotifyChanged(currentResponseMessage); + } +#else*@ await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) { messages.AddMessages(update, filter: c => c is not TextContent); responseText.Text += update.Text; ChatMessageItem.NotifyChanged(currentResponseMessage); } +@*#endif*@ // Store the final response in the conversation, and begin getting suggestions messages.Add(currentResponseMessage!); @@ -96,6 +106,9 @@ CancelAnyCurrentResponse(); messages.Clear(); messages.Add(new(ChatRole.System, SystemPrompt)); +@*#if (IsAzureOpenAI || IsOpenAI) + chatOptions.ConversationId = null; +#endif*@ chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs index 84475f45a54..2351b2d401e 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs @@ -27,7 +27,7 @@ #else var openai = builder.AddAzureOpenAIClient("openai"); #endif -openai.AddChatClient("gpt-4o-mini") +openai.AddResponsesChatClient("gpt-4o-mini") .UseFunctionInvocation() .UseOpenTelemetry(configure: c => c.EnableSensitiveData = builder.Environment.IsDevelopment()); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs index f3f5740066f..06dd5d8509d 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs @@ -47,7 +47,7 @@ // dotnet user-secrets set OpenAI:Key YOUR-API-KEY var openAIClient = new OpenAIClient( new ApiKeyCredential(builder.Configuration["OpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: OpenAI:Key. See the README for details."))); -var chatClient = openAIClient.GetChatClient("gpt-4o-mini").AsIChatClient(); +var chatClient = openAIClient.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient(); var embeddingGenerator = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); #elif (IsAzureAiFoundry) @@ -66,7 +66,7 @@ #else new ApiKeyCredential(builder.Configuration["AzureOpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: AzureOpenAi:Key. See the README for details."))); #endif -var chatClient = azureOpenAi.GetChatClient("gpt-4o-mini").AsIChatClient(); +var chatClient = azureOpenAi.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient(); var embeddingGenerator = azureOpenAi.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); #endif diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs new file mode 100644 index 00000000000..4ec0173b665 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.AI; +using Aspire.OpenAI; +using OpenAI; + +//namespace chat.aspire.Web; +namespace Microsoft.Extensions.Hosting; + +public static class ResponseClientHelper +{ + + public static ChatClientBuilder AddResponsesChatClient(this AspireOpenAIClientBuilder builder, string? deploymentName) + { + ArgumentNullException.ThrowIfNull(builder, "builder"); + + return builder.HostBuilder.Services.AddChatClient((IServiceProvider services) => CreateInnerChatClient(services, builder, deploymentName)); + } + private static IChatClient CreateInnerChatClient(IServiceProvider services, AspireOpenAIClientBuilder builder, string? deploymentName) + { + OpenAIClient openAIClient = builder.ServiceKey is null ? services.GetRequiredService() : services.GetRequiredKeyedService(builder.ServiceKey); + + if (deploymentName is null) + { + deploymentName = "gpt-4o-mini"; + } + + IChatClient chatClient = openAIClient.GetOpenAIResponseClient(deploymentName).AsIChatClient(); + + if (builder.DisableTracing) + { + return chatClient; + } + + var loggerFactory = services.GetService(); + return new OpenTelemetryChatClient(chatClient, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient))); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index a7b1502d894..e7659b8b0f2 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -66,10 +66,11 @@ var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync([userMessage], chatOptions, currentResponseCancellation.Token)) { messages.AddMessages(update, filter: c => c is not TextContent); responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; //Update conversation ID from current response ChatMessageItem.NotifyChanged(currentResponseMessage); } @@ -96,6 +97,7 @@ CancelAnyCurrentResponse(); messages.Clear(); messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs index 450914c4461..3fddf295cd2 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs @@ -4,11 +4,12 @@ using aichatweb.Web.Services.Ingestion; var builder = WebApplication.CreateBuilder(args); + builder.AddServiceDefaults(); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); var openai = builder.AddAzureOpenAIClient("openai"); -openai.AddChatClient("gpt-4o-mini") +openai.AddResponsesChatClient("gpt-4o-mini") .UseFunctionInvocation() .UseOpenTelemetry(configure: c => c.EnableSensitiveData = builder.Environment.IsDevelopment()); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/ResponseClientHelper.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/ResponseClientHelper.cs new file mode 100644 index 00000000000..781c37a39e5 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/ResponseClientHelper.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.AI; +using Aspire.OpenAI; +using OpenAI; + +//namespace chat.aspire.Web; +namespace Microsoft.Extensions.Hosting; + +public static class ResponseClientHelper +{ + + public static ChatClientBuilder AddResponsesChatClient(this AspireOpenAIClientBuilder builder, string? deploymentName) + { + ArgumentNullException.ThrowIfNull(builder, "builder"); + + return builder.HostBuilder.Services.AddChatClient((IServiceProvider services) => CreateInnerChatClient(services, builder, deploymentName)); + } + private static IChatClient CreateInnerChatClient(IServiceProvider services, AspireOpenAIClientBuilder builder, string? deploymentName) + { + OpenAIClient openAIClient = builder.ServiceKey is null ? services.GetRequiredService() : services.GetRequiredKeyedService(builder.ServiceKey); + + if (deploymentName is null) + { + deploymentName = "gpt-4o-mini"; + } + + IChatClient chatClient = openAIClient.GetOpenAIResponseClient(deploymentName).AsIChatClient(); + + if (builder.DisableTracing) + { + return chatClient; + } + + var loggerFactory = services.GetService(); + return new OpenTelemetryChatClient(chatClient, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient))); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index af528b3f97d..f04eed489fe 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -5,6 +5,7 @@ enable enable secret + OPENAI001 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs index a98905903b3..32c30760404 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs @@ -9,7 +9,7 @@ builder.Services.AddRazorComponents().AddInteractiveServerComponents(); var openai = builder.AddAzureOpenAIClient("openai"); -openai.AddChatClient("gpt-4o-mini") +openai.AddResponsesChatClient("gpt-4o-mini") .UseFunctionInvocation() .UseOpenTelemetry(configure: c => c.EnableSensitiveData = builder.Environment.IsDevelopment()); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor index a7b1502d894..e7659b8b0f2 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor @@ -66,10 +66,11 @@ var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync([userMessage], chatOptions, currentResponseCancellation.Token)) { messages.AddMessages(update, filter: c => c is not TextContent); responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; //Update conversation ID from current response ChatMessageItem.NotifyChanged(currentResponseMessage); } @@ -96,6 +97,7 @@ CancelAnyCurrentResponse(); messages.Clear(); messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs index 2b9f0790817..4ed5afee9b2 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs @@ -16,7 +16,7 @@ // dotnet user-secrets set OpenAI:Key YOUR-API-KEY var openAIClient = new OpenAIClient( new ApiKeyCredential(builder.Configuration["OpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: OpenAI:Key. See the README for details."))); -var chatClient = openAIClient.GetChatClient("gpt-4o-mini").AsIChatClient(); +var chatClient = openAIClient.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient(); var embeddingGenerator = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); // You will need to set the endpoint and key to your own values diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 6aaa0a005b3..8184c9902ba 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -5,6 +5,7 @@ enable enable secret + OPENAI001 From b3c4ccf08b68f7ffb571df7093576d93b68c6311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Fri, 5 Sep 2025 14:20:57 -0700 Subject: [PATCH 2/4] Address feedback from PR review --- .../.template.config/template.json | 4 +- ...entBuilderResponsesChatClientExtensions.cs | 74 +++++++++++++++++++ .../Components/Pages/Chat/Chat.razor | 2 +- .../Program.Aspire.cs | 7 ++ .../ResponseClientHelper.cs | 36 --------- ...entBuilderResponsesChatClientExtensions.cs | 74 +++++++++++++++++++ .../Components/Pages/Chat/Chat.razor | 2 +- .../aichatweb/aichatweb.Web/Program.cs | 1 - .../aichatweb.Web/ResponseClientHelper.cs | 36 --------- .../aichatweb/aichatweb.Web/Program.cs | 2 +- .../Components/Pages/Chat/Chat.razor | 2 +- 11 files changed, 161 insertions(+), 79 deletions(-) create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs delete mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs delete mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/ResponseClientHelper.cs diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json index 5b85d11ae4a..10f5be257e8 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json @@ -60,7 +60,7 @@ "ChatWithCustomData-CSharp.AppHost/**", "ChatWithCustomData-CSharp.ServiceDefaults/**", "ChatWithCustomData-CSharp.Web/Program.Aspire.cs", - "ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs", + "ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs", "README.Aspire.md", "*.sln" ] @@ -68,7 +68,7 @@ { "condition": "(IsAspire && !IsOpenAI && !IsAzureOpenAI)", "exclude": [ - "ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs" + "ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs" ] }, { diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs new file mode 100644 index 00000000000..d1343264ec7 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.AI; +using Aspire.OpenAI; +using OpenAI; +using System.Data.Common; + +namespace ChatWithCustomData_CSharp.Web.Services; + +public static class AspireOpenAIClientBuilderResponsesChatClientExtensions +{ + private const string DeploymentKey = "Deployment"; + private const string ModelKey = "Model"; + + public static ChatClientBuilder AddResponsesChatClient(this AspireOpenAIClientBuilder builder, string? deploymentName) + { + ArgumentNullException.ThrowIfNull(builder, "builder"); + + return builder.HostBuilder.Services.AddChatClient((IServiceProvider services) => CreateInnerChatClient(services, builder, deploymentName)); + } + private static IChatClient CreateInnerChatClient(IServiceProvider services, AspireOpenAIClientBuilder builder, string? deploymentName) + { + OpenAIClient openAIClient = builder.ServiceKey is null ? services.GetRequiredService() : services.GetRequiredKeyedService(builder.ServiceKey); + + deploymentName ??= GetRequiredDeploymentName(builder); + + IChatClient chatClient = openAIClient.GetOpenAIResponseClient(deploymentName).AsIChatClient(); + + if (builder.DisableTracing) + { + return chatClient; + } + + var loggerFactory = services.GetService(); + return new OpenTelemetryChatClient(chatClient, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient))); + } + + private static string GetRequiredDeploymentName(this AspireOpenAIClientBuilder builder) + { + string? deploymentName = null; + + var configuration = builder.HostBuilder.Configuration; + if (configuration.GetConnectionString(builder.ConnectionName) is string connectionString) + { + // The reason we accept either 'Deployment' or 'Model' as the key is because some hosting solutions + // require specific named deployments (Azure Foundry AI) while others may use a generic model name (OpenAI, GitHub Models). + var connectionBuilder = new DbConnectionStringBuilder { ConnectionString = connectionString }; + var deploymentValue = ConnectionStringValue(connectionBuilder, DeploymentKey); + var modelValue = ConnectionStringValue(connectionBuilder, ModelKey); + if (deploymentValue is not null && modelValue is not null) + { + throw new InvalidOperationException( + $"The connection string '{builder.ConnectionName}' contains both '{DeploymentKey}' and '{ModelKey}' keys. Either of these may be specified, but not both."); + } + + deploymentName = deploymentValue ?? modelValue; + } + + if (string.IsNullOrEmpty(deploymentName)) + { + var configSection = configuration.GetSection(builder.ConfigurationSectionName); + deploymentName = configSection[DeploymentKey]; + } + + if (string.IsNullOrEmpty(deploymentName)) + { + throw new InvalidOperationException($"The deployment could not be determined. Ensure a '{DeploymentKey}' or '{ModelKey}' value is provided in 'ConnectionStrings:{builder.ConnectionName}', or specify a '{DeploymentKey}' in the '{builder.ConfigurationSectionName}' configuration section, or specify a '{nameof(deploymentName)}' in the call."); + } + + return deploymentName; + } + + private static string? ConnectionStringValue(DbConnectionStringBuilder connectionString, string key) + => connectionString.TryGetValue(key, out var value) ? value as string : null; + +} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor index 6cb2806aace..ce3e580e7dd 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor @@ -67,7 +67,7 @@ currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); @*#if (IsAzureOpenAI || IsOpenAI) - await foreach (var update in ChatClient.GetStreamingResponseAsync([userMessage], chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync(userMessage, chatOptions, currentResponseCancellation.Token)) { messages.AddMessages(update, filter: c => c is not TextContent); responseText.Text += update.Text; diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs index 2351b2d401e..244aa9fd8f6 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs @@ -27,10 +27,17 @@ #else var openai = builder.AddAzureOpenAIClient("openai"); #endif +#if (IsGHModels) +openai.AddChatClient("gpt-4o-mini") + .UseFunctionInvocation() + .UseOpenTelemetry(configure: c => + c.EnableSensitiveData = builder.Environment.IsDevelopment()); +#else // (IsOpenAI || IsAzureOpenAI) openai.AddResponsesChatClient("gpt-4o-mini") .UseFunctionInvocation() .UseOpenTelemetry(configure: c => c.EnableSensitiveData = builder.Environment.IsDevelopment()); +#endif openai.AddEmbeddingGenerator("text-embedding-3-small"); #endif diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs deleted file mode 100644 index 4ec0173b665..00000000000 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.Extensions.AI; -using Aspire.OpenAI; -using OpenAI; - -//namespace chat.aspire.Web; -namespace Microsoft.Extensions.Hosting; - -public static class ResponseClientHelper -{ - - public static ChatClientBuilder AddResponsesChatClient(this AspireOpenAIClientBuilder builder, string? deploymentName) - { - ArgumentNullException.ThrowIfNull(builder, "builder"); - - return builder.HostBuilder.Services.AddChatClient((IServiceProvider services) => CreateInnerChatClient(services, builder, deploymentName)); - } - private static IChatClient CreateInnerChatClient(IServiceProvider services, AspireOpenAIClientBuilder builder, string? deploymentName) - { - OpenAIClient openAIClient = builder.ServiceKey is null ? services.GetRequiredService() : services.GetRequiredKeyedService(builder.ServiceKey); - - if (deploymentName is null) - { - deploymentName = "gpt-4o-mini"; - } - - IChatClient chatClient = openAIClient.GetOpenAIResponseClient(deploymentName).AsIChatClient(); - - if (builder.DisableTracing) - { - return chatClient; - } - - var loggerFactory = services.GetService(); - return new OpenTelemetryChatClient(chatClient, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient))); - } -} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs new file mode 100644 index 00000000000..9d60ca57655 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.AI; +using Aspire.OpenAI; +using OpenAI; +using System.Data.Common; + +namespace aichatweb.Web.Services; + +public static class AspireOpenAIClientBuilderResponsesChatClientExtensions +{ + private const string DeploymentKey = "Deployment"; + private const string ModelKey = "Model"; + + public static ChatClientBuilder AddResponsesChatClient(this AspireOpenAIClientBuilder builder, string? deploymentName) + { + ArgumentNullException.ThrowIfNull(builder, "builder"); + + return builder.HostBuilder.Services.AddChatClient((IServiceProvider services) => CreateInnerChatClient(services, builder, deploymentName)); + } + private static IChatClient CreateInnerChatClient(IServiceProvider services, AspireOpenAIClientBuilder builder, string? deploymentName) + { + OpenAIClient openAIClient = builder.ServiceKey is null ? services.GetRequiredService() : services.GetRequiredKeyedService(builder.ServiceKey); + + deploymentName ??= GetRequiredDeploymentName(builder); + + IChatClient chatClient = openAIClient.GetOpenAIResponseClient(deploymentName).AsIChatClient(); + + if (builder.DisableTracing) + { + return chatClient; + } + + var loggerFactory = services.GetService(); + return new OpenTelemetryChatClient(chatClient, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient))); + } + + private static string GetRequiredDeploymentName(this AspireOpenAIClientBuilder builder) + { + string? deploymentName = null; + + var configuration = builder.HostBuilder.Configuration; + if (configuration.GetConnectionString(builder.ConnectionName) is string connectionString) + { + // The reason we accept either 'Deployment' or 'Model' as the key is because some hosting solutions + // require specific named deployments (Azure Foundry AI) while others may use a generic model name (OpenAI, GitHub Models). + var connectionBuilder = new DbConnectionStringBuilder { ConnectionString = connectionString }; + var deploymentValue = ConnectionStringValue(connectionBuilder, DeploymentKey); + var modelValue = ConnectionStringValue(connectionBuilder, ModelKey); + if (deploymentValue is not null && modelValue is not null) + { + throw new InvalidOperationException( + $"The connection string '{builder.ConnectionName}' contains both '{DeploymentKey}' and '{ModelKey}' keys. Either of these may be specified, but not both."); + } + + deploymentName = deploymentValue ?? modelValue; + } + + if (string.IsNullOrEmpty(deploymentName)) + { + var configSection = configuration.GetSection(builder.ConfigurationSectionName); + deploymentName = configSection[DeploymentKey]; + } + + if (string.IsNullOrEmpty(deploymentName)) + { + throw new InvalidOperationException($"The deployment could not be determined. Ensure a '{DeploymentKey}' or '{ModelKey}' value is provided in 'ConnectionStrings:{builder.ConnectionName}', or specify a '{DeploymentKey}' in the '{builder.ConfigurationSectionName}' configuration section, or specify a '{nameof(deploymentName)}' in the call."); + } + + return deploymentName; + } + + private static string? ConnectionStringValue(DbConnectionStringBuilder connectionString, string key) + => connectionString.TryGetValue(key, out var value) ? value as string : null; + +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index e7659b8b0f2..f2c1526ba5a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -66,7 +66,7 @@ var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([userMessage], chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync(userMessage, chatOptions, currentResponseCancellation.Token)) { messages.AddMessages(update, filter: c => c is not TextContent); responseText.Text += update.Text; diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs index 3fddf295cd2..6c767d46763 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs @@ -4,7 +4,6 @@ using aichatweb.Web.Services.Ingestion; var builder = WebApplication.CreateBuilder(args); - builder.AddServiceDefaults(); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/ResponseClientHelper.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/ResponseClientHelper.cs deleted file mode 100644 index 781c37a39e5..00000000000 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/ResponseClientHelper.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.Extensions.AI; -using Aspire.OpenAI; -using OpenAI; - -//namespace chat.aspire.Web; -namespace Microsoft.Extensions.Hosting; - -public static class ResponseClientHelper -{ - - public static ChatClientBuilder AddResponsesChatClient(this AspireOpenAIClientBuilder builder, string? deploymentName) - { - ArgumentNullException.ThrowIfNull(builder, "builder"); - - return builder.HostBuilder.Services.AddChatClient((IServiceProvider services) => CreateInnerChatClient(services, builder, deploymentName)); - } - private static IChatClient CreateInnerChatClient(IServiceProvider services, AspireOpenAIClientBuilder builder, string? deploymentName) - { - OpenAIClient openAIClient = builder.ServiceKey is null ? services.GetRequiredService() : services.GetRequiredKeyedService(builder.ServiceKey); - - if (deploymentName is null) - { - deploymentName = "gpt-4o-mini"; - } - - IChatClient chatClient = openAIClient.GetOpenAIResponseClient(deploymentName).AsIChatClient(); - - if (builder.DisableTracing) - { - return chatClient; - } - - var loggerFactory = services.GetService(); - return new OpenTelemetryChatClient(chatClient, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient))); - } -} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs index 32c30760404..a98905903b3 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs @@ -9,7 +9,7 @@ builder.Services.AddRazorComponents().AddInteractiveServerComponents(); var openai = builder.AddAzureOpenAIClient("openai"); -openai.AddResponsesChatClient("gpt-4o-mini") +openai.AddChatClient("gpt-4o-mini") .UseFunctionInvocation() .UseOpenTelemetry(configure: c => c.EnableSensitiveData = builder.Environment.IsDevelopment()); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor index e7659b8b0f2..f2c1526ba5a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor @@ -66,7 +66,7 @@ var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([userMessage], chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync(userMessage, chatOptions, currentResponseCancellation.Token)) { messages.AddMessages(update, filter: c => c is not TextContent); responseText.Text += update.Text; From 67b83665ff2abae67936bf89b7d7ca9a83e7a79e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Thu, 11 Sep 2025 00:08:45 -0700 Subject: [PATCH 3/4] Address PR Feedback: Dynamically detect stateful or stateless chat client at runtime, remove Aspire-specific workarounds, simplify template logic, and default to stateful API where supported. --- .../.template.config/template.json | 7 - ...entBuilderResponsesChatClientExtensions.cs | 74 ---------- .../ChatWithCustomData-CSharp.Web.csproj.in | 3 - .../Components/Pages/Chat/Chat.razor | 135 +++++++++--------- .../Program.Aspire.cs | 7 - .../ChatWithCustomData-CSharp.Web/Program.cs | 4 + ...entBuilderResponsesChatClientExtensions.cs | 74 ---------- .../Components/Pages/Chat/Chat.razor | 124 ++++++++-------- .../aichatweb/aichatweb.Web/Program.cs | 2 +- .../aichatweb.Web/aichatweb.Web.csproj | 1 - .../Components/Pages/Chat/Chat.razor | 122 ++++++++-------- .../Components/Pages/Chat/Chat.razor | 122 ++++++++-------- .../Components/Pages/Chat/Chat.razor | 122 ++++++++-------- .../Components/Pages/Chat/Chat.razor | 124 ++++++++-------- .../aichatweb/Program.cs | 2 + .../aichatweb/aichatweb.csproj | 1 - 16 files changed, 391 insertions(+), 533 deletions(-) delete mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs delete mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json index 10f5be257e8..caeceae1dde 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json @@ -60,17 +60,10 @@ "ChatWithCustomData-CSharp.AppHost/**", "ChatWithCustomData-CSharp.ServiceDefaults/**", "ChatWithCustomData-CSharp.Web/Program.Aspire.cs", - "ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs", "README.Aspire.md", "*.sln" ] }, - { - "condition": "(IsAspire && !IsOpenAI && !IsAzureOpenAI)", - "exclude": [ - "ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs" - ] - }, { "condition": "(IsAspire)", "exclude": [ diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs deleted file mode 100644 index d1343264ec7..00000000000 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Microsoft.Extensions.AI; -using Aspire.OpenAI; -using OpenAI; -using System.Data.Common; - -namespace ChatWithCustomData_CSharp.Web.Services; - -public static class AspireOpenAIClientBuilderResponsesChatClientExtensions -{ - private const string DeploymentKey = "Deployment"; - private const string ModelKey = "Model"; - - public static ChatClientBuilder AddResponsesChatClient(this AspireOpenAIClientBuilder builder, string? deploymentName) - { - ArgumentNullException.ThrowIfNull(builder, "builder"); - - return builder.HostBuilder.Services.AddChatClient((IServiceProvider services) => CreateInnerChatClient(services, builder, deploymentName)); - } - private static IChatClient CreateInnerChatClient(IServiceProvider services, AspireOpenAIClientBuilder builder, string? deploymentName) - { - OpenAIClient openAIClient = builder.ServiceKey is null ? services.GetRequiredService() : services.GetRequiredKeyedService(builder.ServiceKey); - - deploymentName ??= GetRequiredDeploymentName(builder); - - IChatClient chatClient = openAIClient.GetOpenAIResponseClient(deploymentName).AsIChatClient(); - - if (builder.DisableTracing) - { - return chatClient; - } - - var loggerFactory = services.GetService(); - return new OpenTelemetryChatClient(chatClient, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient))); - } - - private static string GetRequiredDeploymentName(this AspireOpenAIClientBuilder builder) - { - string? deploymentName = null; - - var configuration = builder.HostBuilder.Configuration; - if (configuration.GetConnectionString(builder.ConnectionName) is string connectionString) - { - // The reason we accept either 'Deployment' or 'Model' as the key is because some hosting solutions - // require specific named deployments (Azure Foundry AI) while others may use a generic model name (OpenAI, GitHub Models). - var connectionBuilder = new DbConnectionStringBuilder { ConnectionString = connectionString }; - var deploymentValue = ConnectionStringValue(connectionBuilder, DeploymentKey); - var modelValue = ConnectionStringValue(connectionBuilder, ModelKey); - if (deploymentValue is not null && modelValue is not null) - { - throw new InvalidOperationException( - $"The connection string '{builder.ConnectionName}' contains both '{DeploymentKey}' and '{ModelKey}' keys. Either of these may be specified, but not both."); - } - - deploymentName = deploymentValue ?? modelValue; - } - - if (string.IsNullOrEmpty(deploymentName)) - { - var configSection = configuration.GetSection(builder.ConfigurationSectionName); - deploymentName = configSection[DeploymentKey]; - } - - if (string.IsNullOrEmpty(deploymentName)) - { - throw new InvalidOperationException($"The deployment could not be determined. Ensure a '{DeploymentKey}' or '{ModelKey}' value is provided in 'ConnectionStrings:{builder.ConnectionName}', or specify a '{DeploymentKey}' in the '{builder.ConfigurationSectionName}' configuration section, or specify a '{nameof(deploymentName)}' in the call."); - } - - return deploymentName; - } - - private static string? ConnectionStringValue(DbConnectionStringBuilder connectionString, string key) - => connectionString.TryGetValue(key, out var value) ? value as string : null; - -} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in index e4a5267b435..0e753e8c907 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in @@ -5,9 +5,6 @@ enable enable d5681fae-b21b-4114-b781-48180f08c0c4 - diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor index ce3e580e7dd..93947cce6c5 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor @@ -24,7 +24,7 @@ @code { - private const string SystemPrompt = @" + private const string SystemPrompt = @" You are an assistant who answers questions about information you retrieve. Do not answer questions about anything else. Use only simple markdown to format your responses. @@ -40,76 +40,69 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; - private readonly ChatOptions chatOptions = new(); - private readonly List messages = new(); - private CancellationTokenSource? currentResponseCancellation; - private ChatMessage? currentResponseMessage; - private ChatInput? chatInput; - private ChatSuggestions? chatSuggestions; - - protected override void OnInitialized() - { - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; - } - - private async Task AddUserMessageAsync(ChatMessage userMessage) - { - CancelAnyCurrentResponse(); - - // Add the user message to the conversation - messages.Add(userMessage); - chatSuggestions?.Clear(); - await chatInput!.FocusAsync(); - - // Stream and display a new response from the IChatClient - var responseText = new TextContent(""); - currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); - currentResponseCancellation = new(); -@*#if (IsAzureOpenAI || IsOpenAI) - await foreach (var update in ChatClient.GetStreamingResponseAsync(userMessage, chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - chatOptions.ConversationId = update.ConversationId; //Update conversation ID from current response - ChatMessageItem.NotifyChanged(currentResponseMessage); - } -#else*@ - await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - ChatMessageItem.NotifyChanged(currentResponseMessage); - } -@*#endif*@ - - // Store the final response in the conversation, and begin getting suggestions - messages.Add(currentResponseMessage!); - currentResponseMessage = null; - chatSuggestions?.Update(messages); - } - - private void CancelAnyCurrentResponse() - { - // If a response was cancelled while streaming, include it in the conversation so it's not lost - if (currentResponseMessage is not null) - { - messages.Add(currentResponseMessage); - } - - currentResponseCancellation?.Cancel(); - currentResponseMessage = null; - } - - private async Task ResetConversationAsync() - { - CancelAnyCurrentResponse(); - messages.Clear(); - messages.Add(new(ChatRole.System, SystemPrompt)); -@*#if (IsAzureOpenAI || IsOpenAI) - chatOptions.ConversationId = null; -#endif*@ - chatSuggestions?.Clear(); + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs index 244aa9fd8f6..84475f45a54 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs @@ -27,17 +27,10 @@ #else var openai = builder.AddAzureOpenAIClient("openai"); #endif -#if (IsGHModels) openai.AddChatClient("gpt-4o-mini") .UseFunctionInvocation() .UseOpenTelemetry(configure: c => c.EnableSensitiveData = builder.Environment.IsDevelopment()); -#else // (IsOpenAI || IsAzureOpenAI) -openai.AddResponsesChatClient("gpt-4o-mini") - .UseFunctionInvocation() - .UseOpenTelemetry(configure: c => - c.EnableSensitiveData = builder.Environment.IsDevelopment()); -#endif openai.AddEmbeddingGenerator("text-embedding-3-small"); #endif diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs index 06dd5d8509d..f8d2826ab9a 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs @@ -47,7 +47,9 @@ // dotnet user-secrets set OpenAI:Key YOUR-API-KEY var openAIClient = new OpenAIClient( new ApiKeyCredential(builder.Configuration["OpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: OpenAI:Key. See the README for details."))); +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var chatClient = openAIClient.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient(); +#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var embeddingGenerator = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); #elif (IsAzureAiFoundry) @@ -66,7 +68,9 @@ #else new ApiKeyCredential(builder.Configuration["AzureOpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: AzureOpenAi:Key. See the README for details."))); #endif +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var chatClient = azureOpenAi.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient(); +#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var embeddingGenerator = azureOpenAi.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); #endif diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs deleted file mode 100644 index 9d60ca57655..00000000000 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Microsoft.Extensions.AI; -using Aspire.OpenAI; -using OpenAI; -using System.Data.Common; - -namespace aichatweb.Web.Services; - -public static class AspireOpenAIClientBuilderResponsesChatClientExtensions -{ - private const string DeploymentKey = "Deployment"; - private const string ModelKey = "Model"; - - public static ChatClientBuilder AddResponsesChatClient(this AspireOpenAIClientBuilder builder, string? deploymentName) - { - ArgumentNullException.ThrowIfNull(builder, "builder"); - - return builder.HostBuilder.Services.AddChatClient((IServiceProvider services) => CreateInnerChatClient(services, builder, deploymentName)); - } - private static IChatClient CreateInnerChatClient(IServiceProvider services, AspireOpenAIClientBuilder builder, string? deploymentName) - { - OpenAIClient openAIClient = builder.ServiceKey is null ? services.GetRequiredService() : services.GetRequiredKeyedService(builder.ServiceKey); - - deploymentName ??= GetRequiredDeploymentName(builder); - - IChatClient chatClient = openAIClient.GetOpenAIResponseClient(deploymentName).AsIChatClient(); - - if (builder.DisableTracing) - { - return chatClient; - } - - var loggerFactory = services.GetService(); - return new OpenTelemetryChatClient(chatClient, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient))); - } - - private static string GetRequiredDeploymentName(this AspireOpenAIClientBuilder builder) - { - string? deploymentName = null; - - var configuration = builder.HostBuilder.Configuration; - if (configuration.GetConnectionString(builder.ConnectionName) is string connectionString) - { - // The reason we accept either 'Deployment' or 'Model' as the key is because some hosting solutions - // require specific named deployments (Azure Foundry AI) while others may use a generic model name (OpenAI, GitHub Models). - var connectionBuilder = new DbConnectionStringBuilder { ConnectionString = connectionString }; - var deploymentValue = ConnectionStringValue(connectionBuilder, DeploymentKey); - var modelValue = ConnectionStringValue(connectionBuilder, ModelKey); - if (deploymentValue is not null && modelValue is not null) - { - throw new InvalidOperationException( - $"The connection string '{builder.ConnectionName}' contains both '{DeploymentKey}' and '{ModelKey}' keys. Either of these may be specified, but not both."); - } - - deploymentName = deploymentValue ?? modelValue; - } - - if (string.IsNullOrEmpty(deploymentName)) - { - var configSection = configuration.GetSection(builder.ConfigurationSectionName); - deploymentName = configSection[DeploymentKey]; - } - - if (string.IsNullOrEmpty(deploymentName)) - { - throw new InvalidOperationException($"The deployment could not be determined. Ensure a '{DeploymentKey}' or '{ModelKey}' value is provided in 'ConnectionStrings:{builder.ConnectionName}', or specify a '{DeploymentKey}' in the '{builder.ConfigurationSectionName}' configuration section, or specify a '{nameof(deploymentName)}' in the call."); - } - - return deploymentName; - } - - private static string? ConnectionStringValue(DbConnectionStringBuilder connectionString, string key) - => connectionString.TryGetValue(key, out var value) ? value as string : null; - -} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index f2c1526ba5a..93947cce6c5 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -24,7 +24,7 @@ @code { - private const string SystemPrompt = @" + private const string SystemPrompt = @" You are an assistant who answers questions about information you retrieve. Do not answer questions about anything else. Use only simple markdown to format your responses. @@ -40,65 +40,69 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; - private readonly ChatOptions chatOptions = new(); - private readonly List messages = new(); - private CancellationTokenSource? currentResponseCancellation; - private ChatMessage? currentResponseMessage; - private ChatInput? chatInput; - private ChatSuggestions? chatSuggestions; - - protected override void OnInitialized() - { - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; - } - - private async Task AddUserMessageAsync(ChatMessage userMessage) - { - CancelAnyCurrentResponse(); - - // Add the user message to the conversation - messages.Add(userMessage); - chatSuggestions?.Clear(); - await chatInput!.FocusAsync(); - - // Stream and display a new response from the IChatClient - var responseText = new TextContent(""); - currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); - currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync(userMessage, chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - chatOptions.ConversationId = update.ConversationId; //Update conversation ID from current response - ChatMessageItem.NotifyChanged(currentResponseMessage); - } - - // Store the final response in the conversation, and begin getting suggestions - messages.Add(currentResponseMessage!); - currentResponseMessage = null; - chatSuggestions?.Update(messages); - } - - private void CancelAnyCurrentResponse() - { - // If a response was cancelled while streaming, include it in the conversation so it's not lost - if (currentResponseMessage is not null) - { - messages.Add(currentResponseMessage); - } - - currentResponseCancellation?.Cancel(); - currentResponseMessage = null; - } - - private async Task ResetConversationAsync() - { - CancelAnyCurrentResponse(); - messages.Clear(); - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.ConversationId = null; - chatSuggestions?.Clear(); + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs index 6c767d46763..450914c4461 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs @@ -8,7 +8,7 @@ builder.Services.AddRazorComponents().AddInteractiveServerComponents(); var openai = builder.AddAzureOpenAIClient("openai"); -openai.AddResponsesChatClient("gpt-4o-mini") +openai.AddChatClient("gpt-4o-mini") .UseFunctionInvocation() .UseOpenTelemetry(configure: c => c.EnableSensitiveData = builder.Environment.IsDevelopment()); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index f04eed489fe..af528b3f97d 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -5,7 +5,6 @@ enable enable secret - OPENAI001 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor index a7b1502d894..93947cce6c5 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor @@ -24,7 +24,7 @@ @code { - private const string SystemPrompt = @" + private const string SystemPrompt = @" You are an assistant who answers questions about information you retrieve. Do not answer questions about anything else. Use only simple markdown to format your responses. @@ -40,63 +40,69 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; - private readonly ChatOptions chatOptions = new(); - private readonly List messages = new(); - private CancellationTokenSource? currentResponseCancellation; - private ChatMessage? currentResponseMessage; - private ChatInput? chatInput; - private ChatSuggestions? chatSuggestions; - - protected override void OnInitialized() - { - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; - } - - private async Task AddUserMessageAsync(ChatMessage userMessage) - { - CancelAnyCurrentResponse(); - - // Add the user message to the conversation - messages.Add(userMessage); - chatSuggestions?.Clear(); - await chatInput!.FocusAsync(); - - // Stream and display a new response from the IChatClient - var responseText = new TextContent(""); - currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); - currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - ChatMessageItem.NotifyChanged(currentResponseMessage); - } - - // Store the final response in the conversation, and begin getting suggestions - messages.Add(currentResponseMessage!); - currentResponseMessage = null; - chatSuggestions?.Update(messages); - } - - private void CancelAnyCurrentResponse() - { - // If a response was cancelled while streaming, include it in the conversation so it's not lost - if (currentResponseMessage is not null) - { - messages.Add(currentResponseMessage); - } - - currentResponseCancellation?.Cancel(); - currentResponseMessage = null; - } - - private async Task ResetConversationAsync() - { - CancelAnyCurrentResponse(); - messages.Clear(); - messages.Add(new(ChatRole.System, SystemPrompt)); - chatSuggestions?.Clear(); + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index a7b1502d894..93947cce6c5 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -24,7 +24,7 @@ @code { - private const string SystemPrompt = @" + private const string SystemPrompt = @" You are an assistant who answers questions about information you retrieve. Do not answer questions about anything else. Use only simple markdown to format your responses. @@ -40,63 +40,69 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; - private readonly ChatOptions chatOptions = new(); - private readonly List messages = new(); - private CancellationTokenSource? currentResponseCancellation; - private ChatMessage? currentResponseMessage; - private ChatInput? chatInput; - private ChatSuggestions? chatSuggestions; - - protected override void OnInitialized() - { - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; - } - - private async Task AddUserMessageAsync(ChatMessage userMessage) - { - CancelAnyCurrentResponse(); - - // Add the user message to the conversation - messages.Add(userMessage); - chatSuggestions?.Clear(); - await chatInput!.FocusAsync(); - - // Stream and display a new response from the IChatClient - var responseText = new TextContent(""); - currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); - currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - ChatMessageItem.NotifyChanged(currentResponseMessage); - } - - // Store the final response in the conversation, and begin getting suggestions - messages.Add(currentResponseMessage!); - currentResponseMessage = null; - chatSuggestions?.Update(messages); - } - - private void CancelAnyCurrentResponse() - { - // If a response was cancelled while streaming, include it in the conversation so it's not lost - if (currentResponseMessage is not null) - { - messages.Add(currentResponseMessage); - } - - currentResponseCancellation?.Cancel(); - currentResponseMessage = null; - } - - private async Task ResetConversationAsync() - { - CancelAnyCurrentResponse(); - messages.Clear(); - messages.Add(new(ChatRole.System, SystemPrompt)); - chatSuggestions?.Clear(); + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index a7b1502d894..93947cce6c5 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -24,7 +24,7 @@ @code { - private const string SystemPrompt = @" + private const string SystemPrompt = @" You are an assistant who answers questions about information you retrieve. Do not answer questions about anything else. Use only simple markdown to format your responses. @@ -40,63 +40,69 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; - private readonly ChatOptions chatOptions = new(); - private readonly List messages = new(); - private CancellationTokenSource? currentResponseCancellation; - private ChatMessage? currentResponseMessage; - private ChatInput? chatInput; - private ChatSuggestions? chatSuggestions; - - protected override void OnInitialized() - { - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; - } - - private async Task AddUserMessageAsync(ChatMessage userMessage) - { - CancelAnyCurrentResponse(); - - // Add the user message to the conversation - messages.Add(userMessage); - chatSuggestions?.Clear(); - await chatInput!.FocusAsync(); - - // Stream and display a new response from the IChatClient - var responseText = new TextContent(""); - currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); - currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - ChatMessageItem.NotifyChanged(currentResponseMessage); - } - - // Store the final response in the conversation, and begin getting suggestions - messages.Add(currentResponseMessage!); - currentResponseMessage = null; - chatSuggestions?.Update(messages); - } - - private void CancelAnyCurrentResponse() - { - // If a response was cancelled while streaming, include it in the conversation so it's not lost - if (currentResponseMessage is not null) - { - messages.Add(currentResponseMessage); - } - - currentResponseCancellation?.Cancel(); - currentResponseMessage = null; - } - - private async Task ResetConversationAsync() - { - CancelAnyCurrentResponse(); - messages.Clear(); - messages.Add(new(ChatRole.System, SystemPrompt)); - chatSuggestions?.Clear(); + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor index f2c1526ba5a..93947cce6c5 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor @@ -24,7 +24,7 @@ @code { - private const string SystemPrompt = @" + private const string SystemPrompt = @" You are an assistant who answers questions about information you retrieve. Do not answer questions about anything else. Use only simple markdown to format your responses. @@ -40,65 +40,69 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; - private readonly ChatOptions chatOptions = new(); - private readonly List messages = new(); - private CancellationTokenSource? currentResponseCancellation; - private ChatMessage? currentResponseMessage; - private ChatInput? chatInput; - private ChatSuggestions? chatSuggestions; - - protected override void OnInitialized() - { - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; - } - - private async Task AddUserMessageAsync(ChatMessage userMessage) - { - CancelAnyCurrentResponse(); - - // Add the user message to the conversation - messages.Add(userMessage); - chatSuggestions?.Clear(); - await chatInput!.FocusAsync(); - - // Stream and display a new response from the IChatClient - var responseText = new TextContent(""); - currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); - currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync(userMessage, chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - chatOptions.ConversationId = update.ConversationId; //Update conversation ID from current response - ChatMessageItem.NotifyChanged(currentResponseMessage); - } - - // Store the final response in the conversation, and begin getting suggestions - messages.Add(currentResponseMessage!); - currentResponseMessage = null; - chatSuggestions?.Update(messages); - } - - private void CancelAnyCurrentResponse() - { - // If a response was cancelled while streaming, include it in the conversation so it's not lost - if (currentResponseMessage is not null) - { - messages.Add(currentResponseMessage); - } - - currentResponseCancellation?.Cancel(); - currentResponseMessage = null; - } - - private async Task ResetConversationAsync() - { - CancelAnyCurrentResponse(); - messages.Clear(); - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.ConversationId = null; - chatSuggestions?.Clear(); + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs index 4ed5afee9b2..d469d9c43db 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs @@ -16,7 +16,9 @@ // dotnet user-secrets set OpenAI:Key YOUR-API-KEY var openAIClient = new OpenAIClient( new ApiKeyCredential(builder.Configuration["OpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: OpenAI:Key. See the README for details."))); +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var chatClient = openAIClient.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient(); +#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var embeddingGenerator = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); // You will need to set the endpoint and key to your own values diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 8184c9902ba..6aaa0a005b3 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -5,7 +5,6 @@ enable enable secret - OPENAI001 From 4f940e5b5cd0d65adbafc6238d28a785ec98084b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Thu, 11 Sep 2025 10:26:54 -0700 Subject: [PATCH 4/4] Fix whitespace formatting --- .../Components/Pages/Chat/Chat.razor | 128 +++++++++--------- .../Components/Pages/Chat/Chat.razor | 128 +++++++++--------- .../Components/Pages/Chat/Chat.razor | 128 +++++++++--------- .../Components/Pages/Chat/Chat.razor | 128 +++++++++--------- .../Components/Pages/Chat/Chat.razor | 128 +++++++++--------- .../Components/Pages/Chat/Chat.razor | 128 +++++++++--------- 6 files changed, 384 insertions(+), 384 deletions(-) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor index 93947cce6c5..8aa0ec9fd28 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor @@ -24,7 +24,7 @@ @code { - private const string SystemPrompt = @" + private const string SystemPrompt = @" You are an assistant who answers questions about information you retrieve. Do not answer questions about anything else. Use only simple markdown to format your responses. @@ -40,69 +40,69 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; - private int statefulMessageCount; - private readonly ChatOptions chatOptions = new(); - private readonly List messages = new(); - private CancellationTokenSource? currentResponseCancellation; - private ChatMessage? currentResponseMessage; - private ChatInput? chatInput; - private ChatSuggestions? chatSuggestions; - - protected override void OnInitialized() - { - statefulMessageCount = 0; - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; - } - - private async Task AddUserMessageAsync(ChatMessage userMessage) - { - CancelAnyCurrentResponse(); - - // Add the user message to the conversation - messages.Add(userMessage); - chatSuggestions?.Clear(); - await chatInput!.FocusAsync(); - - // Stream and display a new response from the IChatClient - var responseText = new TextContent(""); - currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); - currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - chatOptions.ConversationId = update.ConversationId; - ChatMessageItem.NotifyChanged(currentResponseMessage); - } - - // Store the final response in the conversation, and begin getting suggestions - messages.Add(currentResponseMessage!); - statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; - currentResponseMessage = null; - chatSuggestions?.Update(messages); - } - - private void CancelAnyCurrentResponse() - { - // If a response was cancelled while streaming, include it in the conversation so it's not lost - if (currentResponseMessage is not null) - { - messages.Add(currentResponseMessage); - } - - currentResponseCancellation?.Cancel(); - currentResponseMessage = null; - } - - private async Task ResetConversationAsync() - { - CancelAnyCurrentResponse(); - messages.Clear(); - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.ConversationId = null; - statefulMessageCount = 0; - chatSuggestions?.Clear(); + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index 93947cce6c5..8aa0ec9fd28 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -24,7 +24,7 @@ @code { - private const string SystemPrompt = @" + private const string SystemPrompt = @" You are an assistant who answers questions about information you retrieve. Do not answer questions about anything else. Use only simple markdown to format your responses. @@ -40,69 +40,69 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; - private int statefulMessageCount; - private readonly ChatOptions chatOptions = new(); - private readonly List messages = new(); - private CancellationTokenSource? currentResponseCancellation; - private ChatMessage? currentResponseMessage; - private ChatInput? chatInput; - private ChatSuggestions? chatSuggestions; - - protected override void OnInitialized() - { - statefulMessageCount = 0; - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; - } - - private async Task AddUserMessageAsync(ChatMessage userMessage) - { - CancelAnyCurrentResponse(); - - // Add the user message to the conversation - messages.Add(userMessage); - chatSuggestions?.Clear(); - await chatInput!.FocusAsync(); - - // Stream and display a new response from the IChatClient - var responseText = new TextContent(""); - currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); - currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - chatOptions.ConversationId = update.ConversationId; - ChatMessageItem.NotifyChanged(currentResponseMessage); - } - - // Store the final response in the conversation, and begin getting suggestions - messages.Add(currentResponseMessage!); - statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; - currentResponseMessage = null; - chatSuggestions?.Update(messages); - } - - private void CancelAnyCurrentResponse() - { - // If a response was cancelled while streaming, include it in the conversation so it's not lost - if (currentResponseMessage is not null) - { - messages.Add(currentResponseMessage); - } - - currentResponseCancellation?.Cancel(); - currentResponseMessage = null; - } - - private async Task ResetConversationAsync() - { - CancelAnyCurrentResponse(); - messages.Clear(); - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.ConversationId = null; - statefulMessageCount = 0; - chatSuggestions?.Clear(); + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor index 93947cce6c5..8aa0ec9fd28 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor @@ -24,7 +24,7 @@ @code { - private const string SystemPrompt = @" + private const string SystemPrompt = @" You are an assistant who answers questions about information you retrieve. Do not answer questions about anything else. Use only simple markdown to format your responses. @@ -40,69 +40,69 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; - private int statefulMessageCount; - private readonly ChatOptions chatOptions = new(); - private readonly List messages = new(); - private CancellationTokenSource? currentResponseCancellation; - private ChatMessage? currentResponseMessage; - private ChatInput? chatInput; - private ChatSuggestions? chatSuggestions; - - protected override void OnInitialized() - { - statefulMessageCount = 0; - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; - } - - private async Task AddUserMessageAsync(ChatMessage userMessage) - { - CancelAnyCurrentResponse(); - - // Add the user message to the conversation - messages.Add(userMessage); - chatSuggestions?.Clear(); - await chatInput!.FocusAsync(); - - // Stream and display a new response from the IChatClient - var responseText = new TextContent(""); - currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); - currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - chatOptions.ConversationId = update.ConversationId; - ChatMessageItem.NotifyChanged(currentResponseMessage); - } - - // Store the final response in the conversation, and begin getting suggestions - messages.Add(currentResponseMessage!); - statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; - currentResponseMessage = null; - chatSuggestions?.Update(messages); - } - - private void CancelAnyCurrentResponse() - { - // If a response was cancelled while streaming, include it in the conversation so it's not lost - if (currentResponseMessage is not null) - { - messages.Add(currentResponseMessage); - } - - currentResponseCancellation?.Cancel(); - currentResponseMessage = null; - } - - private async Task ResetConversationAsync() - { - CancelAnyCurrentResponse(); - messages.Clear(); - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.ConversationId = null; - statefulMessageCount = 0; - chatSuggestions?.Clear(); + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index 93947cce6c5..8aa0ec9fd28 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -24,7 +24,7 @@ @code { - private const string SystemPrompt = @" + private const string SystemPrompt = @" You are an assistant who answers questions about information you retrieve. Do not answer questions about anything else. Use only simple markdown to format your responses. @@ -40,69 +40,69 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; - private int statefulMessageCount; - private readonly ChatOptions chatOptions = new(); - private readonly List messages = new(); - private CancellationTokenSource? currentResponseCancellation; - private ChatMessage? currentResponseMessage; - private ChatInput? chatInput; - private ChatSuggestions? chatSuggestions; - - protected override void OnInitialized() - { - statefulMessageCount = 0; - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; - } - - private async Task AddUserMessageAsync(ChatMessage userMessage) - { - CancelAnyCurrentResponse(); - - // Add the user message to the conversation - messages.Add(userMessage); - chatSuggestions?.Clear(); - await chatInput!.FocusAsync(); - - // Stream and display a new response from the IChatClient - var responseText = new TextContent(""); - currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); - currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - chatOptions.ConversationId = update.ConversationId; - ChatMessageItem.NotifyChanged(currentResponseMessage); - } - - // Store the final response in the conversation, and begin getting suggestions - messages.Add(currentResponseMessage!); - statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; - currentResponseMessage = null; - chatSuggestions?.Update(messages); - } - - private void CancelAnyCurrentResponse() - { - // If a response was cancelled while streaming, include it in the conversation so it's not lost - if (currentResponseMessage is not null) - { - messages.Add(currentResponseMessage); - } - - currentResponseCancellation?.Cancel(); - currentResponseMessage = null; - } - - private async Task ResetConversationAsync() - { - CancelAnyCurrentResponse(); - messages.Clear(); - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.ConversationId = null; - statefulMessageCount = 0; - chatSuggestions?.Clear(); + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index 93947cce6c5..8aa0ec9fd28 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -24,7 +24,7 @@ @code { - private const string SystemPrompt = @" + private const string SystemPrompt = @" You are an assistant who answers questions about information you retrieve. Do not answer questions about anything else. Use only simple markdown to format your responses. @@ -40,69 +40,69 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; - private int statefulMessageCount; - private readonly ChatOptions chatOptions = new(); - private readonly List messages = new(); - private CancellationTokenSource? currentResponseCancellation; - private ChatMessage? currentResponseMessage; - private ChatInput? chatInput; - private ChatSuggestions? chatSuggestions; - - protected override void OnInitialized() - { - statefulMessageCount = 0; - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; - } - - private async Task AddUserMessageAsync(ChatMessage userMessage) - { - CancelAnyCurrentResponse(); - - // Add the user message to the conversation - messages.Add(userMessage); - chatSuggestions?.Clear(); - await chatInput!.FocusAsync(); - - // Stream and display a new response from the IChatClient - var responseText = new TextContent(""); - currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); - currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - chatOptions.ConversationId = update.ConversationId; - ChatMessageItem.NotifyChanged(currentResponseMessage); - } - - // Store the final response in the conversation, and begin getting suggestions - messages.Add(currentResponseMessage!); - statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; - currentResponseMessage = null; - chatSuggestions?.Update(messages); - } - - private void CancelAnyCurrentResponse() - { - // If a response was cancelled while streaming, include it in the conversation so it's not lost - if (currentResponseMessage is not null) - { - messages.Add(currentResponseMessage); - } - - currentResponseCancellation?.Cancel(); - currentResponseMessage = null; - } - - private async Task ResetConversationAsync() - { - CancelAnyCurrentResponse(); - messages.Clear(); - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.ConversationId = null; - statefulMessageCount = 0; - chatSuggestions?.Clear(); + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor index 93947cce6c5..8aa0ec9fd28 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor @@ -24,7 +24,7 @@ @code { - private const string SystemPrompt = @" + private const string SystemPrompt = @" You are an assistant who answers questions about information you retrieve. Do not answer questions about anything else. Use only simple markdown to format your responses. @@ -40,69 +40,69 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; - private int statefulMessageCount; - private readonly ChatOptions chatOptions = new(); - private readonly List messages = new(); - private CancellationTokenSource? currentResponseCancellation; - private ChatMessage? currentResponseMessage; - private ChatInput? chatInput; - private ChatSuggestions? chatSuggestions; - - protected override void OnInitialized() - { - statefulMessageCount = 0; - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; - } - - private async Task AddUserMessageAsync(ChatMessage userMessage) - { - CancelAnyCurrentResponse(); - - // Add the user message to the conversation - messages.Add(userMessage); - chatSuggestions?.Clear(); - await chatInput!.FocusAsync(); - - // Stream and display a new response from the IChatClient - var responseText = new TextContent(""); - currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); - currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) - { - messages.AddMessages(update, filter: c => c is not TextContent); - responseText.Text += update.Text; - chatOptions.ConversationId = update.ConversationId; - ChatMessageItem.NotifyChanged(currentResponseMessage); - } - - // Store the final response in the conversation, and begin getting suggestions - messages.Add(currentResponseMessage!); - statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; - currentResponseMessage = null; - chatSuggestions?.Update(messages); - } - - private void CancelAnyCurrentResponse() - { - // If a response was cancelled while streaming, include it in the conversation so it's not lost - if (currentResponseMessage is not null) - { - messages.Add(currentResponseMessage); - } - - currentResponseCancellation?.Cancel(); - currentResponseMessage = null; - } - - private async Task ResetConversationAsync() - { - CancelAnyCurrentResponse(); - messages.Clear(); - messages.Add(new(ChatRole.System, SystemPrompt)); - chatOptions.ConversationId = null; - statefulMessageCount = 0; - chatSuggestions?.Clear(); + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); await chatInput!.FocusAsync(); }