diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 08dc2b9b..5c969115 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Restore dependencies run: dotnet restore ./app/app.sln - name: Build diff --git a/app/Directory.Packages.props b/app/Directory.Packages.props index 051efaf3..73c3619b 100644 --- a/app/Directory.Packages.props +++ b/app/Directory.Packages.props @@ -5,11 +5,11 @@ - - - + + + - + @@ -20,32 +20,31 @@ - - - - - + + + + + - + - + - + - + - - + - + diff --git a/app/Dockerfile b/app/Dockerfile index c67d1825..41d79c79 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,11 +1,11 @@ #See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app EXPOSE 8080 EXPOSE 443 -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src COPY ["Directory.Build.props", "."] COPY ["Directory.Packages.props", "."] diff --git a/app/SharedWebComponents/SharedWebComponents.csproj b/app/SharedWebComponents/SharedWebComponents.csproj index e979cb4e..96c6a583 100644 --- a/app/SharedWebComponents/SharedWebComponents.csproj +++ b/app/SharedWebComponents/SharedWebComponents.csproj @@ -1,15 +1,12 @@  - - net8.0 + net9.0 enable enable - - @@ -21,13 +18,10 @@ - - - - + \ No newline at end of file diff --git a/app/SharedWebComponents/SharedWebComponents.csproj.SdkResolver.1981936763.proj b/app/SharedWebComponents/SharedWebComponents.csproj.SdkResolver.1981936763.proj new file mode 100644 index 00000000..e52700a2 --- /dev/null +++ b/app/SharedWebComponents/SharedWebComponents.csproj.SdkResolver.1981936763.proj @@ -0,0 +1,7 @@ + + + + C:\Program Files\dotnet\dotnet.exe + 9.0.2 + + \ No newline at end of file diff --git a/app/backend/Extensions/ServiceCollectionExtensions.cs b/app/backend/Extensions/ServiceCollectionExtensions.cs index 7b5f8ff9..f2196324 100644 --- a/app/backend/Extensions/ServiceCollectionExtensions.cs +++ b/app/backend/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using OpenAI; + namespace MinimalApi.Extensions; internal static class ServiceCollectionExtensions @@ -61,7 +63,7 @@ internal static IServiceCollection AddAzureServices(this IServiceCollection serv var azureOpenAiServiceEndpoint = config["AzureOpenAiServiceEndpoint"]; ArgumentNullException.ThrowIfNullOrEmpty(azureOpenAiServiceEndpoint); - var openAIClient = new OpenAIClient(new Uri(azureOpenAiServiceEndpoint), s_azureCredential); + var openAIClient = new AzureOpenAIClient(new Uri(azureOpenAiServiceEndpoint), s_azureCredential); return openAIClient; } diff --git a/app/backend/Extensions/WebApplicationExtensions.cs b/app/backend/Extensions/WebApplicationExtensions.cs index 64464f52..f7c39d84 100644 --- a/app/backend/Extensions/WebApplicationExtensions.cs +++ b/app/backend/Extensions/WebApplicationExtensions.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using OpenAI; + namespace MinimalApi.Extensions; internal static class WebApplicationExtensions @@ -43,29 +45,26 @@ private static async IAsyncEnumerable OnPostChatPromptAsync( [EnumeratorCancellation] CancellationToken cancellationToken) { var deploymentId = config["AZURE_OPENAI_CHATGPT_DEPLOYMENT"]; - var response = await client.GetChatCompletionsStreamingAsync( - new ChatCompletionsOptions - { - DeploymentName = deploymentId, - Messages = - { - new ChatRequestSystemMessage(""" + var chatClient = client.GetChatClient(deploymentId); + var messages = new List + { + new OpenAI.Chat.SystemChatMessage(""" You're an AI assistant for developers, helping them write code more efficiently. You're name is **Blazor 📎 Clippy** and you're an expert Blazor developer. You're also an expert in ASP.NET Core, C#, TypeScript, and even JavaScript. You will always reply with a Markdown formatted response. """), - new ChatRequestUserMessage("What's your name?"), - new ChatRequestAssistantMessage("Hi, my name is **Blazor 📎 Clippy**! Nice to meet you."), - new ChatRequestUserMessage(prompt.Prompt) - } - }, cancellationToken); + new OpenAI.Chat.SystemChatMessage(@"What's your name?"), + new OpenAI.Chat.AssistantChatMessage(@"Hi, my name is **Blazor 📎 Clippy**! Nice to meet you."), + new OpenAI.Chat.UserChatMessage(prompt.Prompt) + }; + var response = chatClient.CompleteChatStreamingAsync(messages: messages, cancellationToken: cancellationToken); await foreach (var choice in response.WithCancellation(cancellationToken)) { - if (choice.ContentUpdate is { Length: > 0 }) + if (choice.ContentUpdate is { Count: > 0 }) { - yield return new ChatChunkResponse(choice.ContentUpdate.Length, choice.ContentUpdate); + yield return new ChatChunkResponse(choice.ContentUpdate.Count, choice.ContentUpdate.ToString()!); } } } @@ -146,14 +145,21 @@ private static async Task OnPostImagePromptAsync( IConfiguration config, CancellationToken cancellationToken) { - var result = await client.GetImageGenerationsAsync(new ImageGenerationOptions - { - Prompt = prompt.Prompt, - }, - cancellationToken); + //ToDo: update image generation model name + var imageGenerationModelName = config["AZURE_OPENAI_IMAGE_DEPLOYMENT"] ?? "dall-e-3"; + Console.WriteLine(@$"=============================== +Image generation model name: {imageGenerationModelName} +============================================="); + var imageClient = client.GetImageClient(imageGenerationModelName); + + var result = await imageClient.GenerateImageAsync(prompt.Prompt); - var imageUrls = result.Value.Data.Select(i => i.Url).ToList(); - var response = new ImageResponse(result.Value.Created, imageUrls); + var imageUrls = new List + { + result.Value.ImageUri + }; + //var response = new ImageResponse(result.Value..Created, imageUrls); + var response = new ImageResponse(DateTime.Now, imageUrls); return TypedResults.Ok(response); } diff --git a/app/backend/MinimalApi.csproj b/app/backend/MinimalApi.csproj index 04783d07..b6a923df 100644 --- a/app/backend/MinimalApi.csproj +++ b/app/backend/MinimalApi.csproj @@ -1,7 +1,6 @@  - - net8.0 + net9.0 enable enable preview @@ -11,7 +10,6 @@ Linux true - @@ -24,14 +22,11 @@ - - - - + \ No newline at end of file diff --git a/app/backend/Services/ReadRetrieveReadChatService.cs b/app/backend/Services/ReadRetrieveReadChatService.cs index 7c72dcd9..e643ffdc 100644 --- a/app/backend/Services/ReadRetrieveReadChatService.cs +++ b/app/backend/Services/ReadRetrieveReadChatService.cs @@ -4,6 +4,7 @@ using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Embeddings; +using OpenAI; namespace MinimalApi.Services; #pragma warning disable SKEXP0011 // Mark members as static @@ -84,7 +85,7 @@ public async Task ReplyAsync( string? query = null; if (overrides?.RetrievalMode != RetrievalMode.Vector) { - var getQueryChat = new ChatHistory(@"You are a helpful AI assistant, generate search query for followup question. + var getQueryChat = new ChatHistory(@"You are a helpful AI assistant, generate search query for follow up question. Make your respond simple and precise. Return the query only, do not return any other text. e.g. Northwind Health Plus AND standard plan. diff --git a/app/frontend/ClientApp.csproj b/app/frontend/ClientApp.csproj index f819d86e..c8ac6b1b 100644 --- a/app/frontend/ClientApp.csproj +++ b/app/frontend/ClientApp.csproj @@ -1,7 +1,6 @@  - - net8.0 + net9.0 enable enable preview @@ -9,7 +8,6 @@ true 48daa172-8fe4-4b81-94b2-0d5a3a5ad30e - @@ -19,14 +17,11 @@ - - - - + \ No newline at end of file diff --git a/app/functions/EmbedFunctions/Program.cs b/app/functions/EmbedFunctions/Program.cs index 9cc8a4a2..8bb1e57f 100644 --- a/app/functions/EmbedFunctions/Program.cs +++ b/app/functions/EmbedFunctions/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Azure.AI.OpenAI; +using OpenAI; var host = new HostBuilder() .ConfigureServices(services => @@ -70,7 +71,7 @@ uri is not null { var openaiEndPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new ArgumentNullException("AZURE_OPENAI_ENDPOINT is null"); embeddingModelName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT") ?? throw new ArgumentNullException("AZURE_OPENAI_EMBEDDING_DEPLOYMENT is null"); - openAIClient = new OpenAIClient(new Uri(openaiEndPoint), new DefaultAzureCredential()); + openAIClient = new AzureOpenAIClient(new Uri(openaiEndPoint), new DefaultAzureCredential()); } else { diff --git a/app/prepdocs/PrepareDocs/Program.Clients.cs b/app/prepdocs/PrepareDocs/Program.Clients.cs index 06ec021c..c17ba41b 100644 --- a/app/prepdocs/PrepareDocs/Program.Clients.cs +++ b/app/prepdocs/PrepareDocs/Program.Clients.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using OpenAI; + internal static partial class Program { private static BlobContainerClient? s_corpusContainerClient; @@ -178,7 +180,7 @@ private static Task GetOpenAIClientAsync(AppOptions options) => { var endpoint = o.AzureOpenAIServiceEndpoint; ArgumentNullException.ThrowIfNullOrEmpty(endpoint); - s_openAIClient = new OpenAIClient( + s_openAIClient = new AzureOpenAIClient( new Uri(endpoint), DefaultCredential); } diff --git a/app/shared/Shared/Models/ResponseChoice.cs b/app/shared/Shared/Models/ResponseChoice.cs index d1ed0961..269021cb 100644 --- a/app/shared/Shared/Models/ResponseChoice.cs +++ b/app/shared/Shared/Models/ResponseChoice.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; -using Azure.AI.OpenAI; namespace Shared.Models; @@ -44,8 +43,8 @@ public record ResponseChoice( [property: JsonPropertyName("context")] ResponseContext Context, [property: JsonPropertyName("citationBaseUrl")] string CitationBaseUrl) { - [JsonPropertyName("content_filter_results")] - public ContentFilterResult? ContentFilterResult { get; set; } + //[JsonPropertyName("content_filter_results")] + //public ContentFilterResult? ContentFilterResult { get; set; } } diff --git a/app/shared/Shared/Services/AzureSearchEmbedService.cs b/app/shared/Shared/Services/AzureSearchEmbedService.cs index 880d9e04..8b6943c3 100644 --- a/app/shared/Shared/Services/AzureSearchEmbedService.cs +++ b/app/shared/Shared/Services/AzureSearchEmbedService.cs @@ -1,11 +1,10 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Net; using System.Text; using System.Text.RegularExpressions; using Azure; using Azure.AI.FormRecognizer.DocumentAnalysis; -using Azure.AI.OpenAI; using Azure.Search.Documents; using Azure.Search.Documents.Indexes; using Azure.Search.Documents.Indexes.Models; @@ -13,6 +12,7 @@ using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Microsoft.Extensions.Logging; +using OpenAI; using Shared.Models; public sealed partial class AzureSearchEmbedService( @@ -449,8 +449,9 @@ private async Task IndexSectionsAsync(IEnumerable
sections) var batch = new IndexDocumentsBatch(); foreach (var section in sections) { - var embeddings = await openAIClient.GetEmbeddingsAsync(new Azure.AI.OpenAI.EmbeddingsOptions(embeddingModelName, [section.Content.Replace('\r', ' ')])); - var embedding = embeddings.Value.Data.FirstOrDefault()?.Embedding.ToArray() ?? []; + var embeddingsClient = openAIClient.GetEmbeddingClient(embeddingModelName); + var embeddings = await embeddingsClient.GenerateEmbeddingAsync(input: section.Content.Replace('\r', ' ')); + var embedding = embeddings.Value.ToFloats(); batch.Actions.Add(new IndexDocumentsAction( IndexActionType.MergeOrUpload, new SearchDocument @@ -494,5 +495,4 @@ private async Task IndexSectionsAsync(IEnumerable
sections) } } } - } diff --git a/app/tests/ClientApp.Tests/ClientApp.Tests.csproj b/app/tests/ClientApp.Tests/ClientApp.Tests.csproj index c2ea7353..9ccbe1cb 100644 --- a/app/tests/ClientApp.Tests/ClientApp.Tests.csproj +++ b/app/tests/ClientApp.Tests/ClientApp.Tests.csproj @@ -1,13 +1,11 @@  - - net8.0 + net9.0 enable enable false preview - @@ -22,9 +20,7 @@ all - - - + \ No newline at end of file diff --git a/app/tests/MinimalApi.Tests/AzureDocumentSearchServiceTest.cs b/app/tests/MinimalApi.Tests/AzureDocumentSearchServiceTest.cs index 21f2000d..adb38efe 100644 --- a/app/tests/MinimalApi.Tests/AzureDocumentSearchServiceTest.cs +++ b/app/tests/MinimalApi.Tests/AzureDocumentSearchServiceTest.cs @@ -1,15 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Azure.AI.OpenAI; using Azure.Identity; using Azure.Search.Documents; using FluentAssertions; -using MinimalApi.Services; using Shared.Models; namespace MinimalApi.Tests; @@ -44,10 +38,14 @@ public async Task QueryDocumentsTestEmbeddingOnlyAsync() var searchServceEndpoint = Environment.GetEnvironmentVariable("AZURE_SEARCH_SERVICE_ENDPOINT") ?? throw new InvalidOperationException(); var openAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException(); var openAiEmbeddingDeployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT") ?? throw new InvalidOperationException(); - var openAIClient = new OpenAIClient(new Uri(openAiEndpoint), new DefaultAzureCredential()); + var openAIClient = new AzureOpenAIClient(new Uri(openAiEndpoint), new DefaultAzureCredential()); var query = "What is included in my Northwind Health Plus plan that is not in standard?"; - var embeddingResponse = await openAIClient.GetEmbeddingsAsync(new EmbeddingsOptions(openAiEmbeddingDeployment, [query])); - var embedding = embeddingResponse.Value.Data.First().Embedding; + + var embeddingsClient = openAIClient.GetEmbeddingClient(openAiEmbeddingDeployment); + var embeddings = await embeddingsClient.GenerateEmbeddingAsync(input: query); + + var embedding = embeddings.Value.ToFloats(); + var searchClient = new SearchClient(new Uri(searchServceEndpoint), index, new DefaultAzureCredential()); var service = new AzureSearchService(searchClient); diff --git a/app/tests/MinimalApi.Tests/AzureSearchEmbedServiceTest.cs b/app/tests/MinimalApi.Tests/AzureSearchEmbedServiceTest.cs index af356f76..e5aad55e 100644 --- a/app/tests/MinimalApi.Tests/AzureSearchEmbedServiceTest.cs +++ b/app/tests/MinimalApi.Tests/AzureSearchEmbedServiceTest.cs @@ -1,21 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Azure; using Azure.AI.FormRecognizer.DocumentAnalysis; using Azure.AI.OpenAI; using Azure.Identity; using Azure.Search.Documents; using Azure.Search.Documents.Indexes; -using Azure.Search.Documents.Models; using Azure.Storage.Blobs; using FluentAssertions; -using Microsoft.Extensions.Logging; using NSubstitute; namespace MinimalApi.Tests; @@ -36,7 +28,7 @@ public async Task EnsureSearchIndexWithoutImageEmbeddingsAsync() var blobContainer = "test"; var azureCredential = new DefaultAzureCredential(); - var openAIClient = new OpenAIClient(new Uri(openAIEndpoint), azureCredential); + var openAIClient = new AzureOpenAIClient(new Uri(openAIEndpoint), azureCredential); var searchClient = new SearchClient(new Uri(azureSearchEndpoint), indexName, azureCredential); var searchIndexClient = new SearchIndexClient(new Uri(azureSearchEndpoint), azureCredential); var documentAnalysisClient = new DocumentAnalysisClient(new Uri(azureSearchEndpoint), azureCredential); @@ -93,7 +85,7 @@ public async Task EnsureSearchIndexWithImageEmbeddingsAsync() var blobContainer = "test"; var azureCredential = new DefaultAzureCredential(); - var openAIClient = new OpenAIClient(new Uri(openAIEndpoint), azureCredential); + var openAIClient = new AzureOpenAIClient(new Uri(openAIEndpoint), azureCredential); var searchClient = new SearchClient(new Uri(azureSearchEndpoint), indexName, azureCredential); var searchIndexClient = new SearchIndexClient(new Uri(azureSearchEndpoint), azureCredential); var documentAnalysisClient = new DocumentAnalysisClient(new Uri(azureSearchEndpoint), azureCredential); @@ -154,7 +146,7 @@ public async Task GetDocumentTextTestAsync() var blobContainer = "test"; var azureCredential = new DefaultAzureCredential(); - var openAIClient = new OpenAIClient(new Uri(openAIEndpoint), azureCredential); + var openAIClient = new AzureOpenAIClient(new Uri(openAIEndpoint), azureCredential); var searchClient = new SearchClient(new Uri(azureSearchEndpoint), indexName, azureCredential); var searchIndexClient = new SearchIndexClient(new Uri(azureSearchEndpoint), azureCredential); var documentAnalysisClient = new DocumentAnalysisClient(new Uri(azureFormRecognizerEndpoint), azureCredential); @@ -204,7 +196,7 @@ public async Task EmbedBlobWithoutImageEmbeddingTestAsync() var blobContainer = nameof(EmbedBlobWithoutImageEmbeddingTestAsync).ToLower(); var azureCredential = new DefaultAzureCredential(); - var openAIClient = new OpenAIClient(new Uri(openAIEndpoint), azureCredential); + var openAIClient = new AzureOpenAIClient(new Uri(openAIEndpoint), azureCredential); var searchClient = new SearchClient(new Uri(azureSearchEndpoint), indexName, azureCredential); var searchIndexClient = new SearchIndexClient(new Uri(azureSearchEndpoint), azureCredential); var documentAnalysisClient = new DocumentAnalysisClient(new Uri(azureFormRecognizerEndpoint), azureCredential); @@ -264,7 +256,7 @@ public async Task EmbedImageBlobTestAsync() var blobContainer = nameof(EmbedImageBlobTestAsync).ToLower(); var azureCredential = new DefaultAzureCredential(); - var openAIClient = new OpenAIClient(new Uri(openAIEndpoint), azureCredential); + var openAIClient = new AzureOpenAIClient(new Uri(openAIEndpoint), azureCredential); var searchClient = new SearchClient(new Uri(azureSearchEndpoint), indexName, azureCredential); var searchIndexClient = new SearchIndexClient(new Uri(azureSearchEndpoint), azureCredential); var documentAnalysisClient = new DocumentAnalysisClient(new Uri(azureFormRecognizerEndpoint), azureCredential); diff --git a/app/tests/MinimalApi.Tests/MinimalApi.Tests.csproj b/app/tests/MinimalApi.Tests/MinimalApi.Tests.csproj index 271574f7..04a1e3b3 100644 --- a/app/tests/MinimalApi.Tests/MinimalApi.Tests.csproj +++ b/app/tests/MinimalApi.Tests/MinimalApi.Tests.csproj @@ -1,13 +1,11 @@  - - net8.0 + net9.0 enable enable false preview - @@ -22,16 +20,13 @@ - - PreserveNewest data\%(RecursiveDir)%(Filename)%(Extension) - - + \ No newline at end of file diff --git a/app/tests/MinimalApi.Tests/ReadRetrieveReadChatServiceTest.cs b/app/tests/MinimalApi.Tests/ReadRetrieveReadChatServiceTest.cs index 42812634..c8f484fb 100644 --- a/app/tests/MinimalApi.Tests/ReadRetrieveReadChatServiceTest.cs +++ b/app/tests/MinimalApi.Tests/ReadRetrieveReadChatServiceTest.cs @@ -1,19 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Azure.AI.OpenAI; using Azure.Identity; using Azure.Search.Documents; -using Azure.Search.Documents.Models; using FluentAssertions; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using MinimalApi.Services; using NSubstitute; +using OpenAI; using Shared.Models; namespace MinimalApi.Tests; @@ -34,7 +28,7 @@ public async Task NorthwindHealthQuestionTest_TextOnlyAsync() }); var openAIEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException(); - var openAIClient = new OpenAIClient(new Uri(openAIEndpoint), new DefaultAzureCredential()); + var openAIClient = new AzureOpenAIClient(new Uri(openAIEndpoint), new DefaultAzureCredential()); var openAiEmbeddingDeployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT") ?? throw new InvalidOperationException(); var openAIChatGptDeployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_CHATGPT_DEPLOYMENT") ?? throw new InvalidOperationException();