From 926c907aedc9d76c0a50fd5bc006d4aa37e5b53f Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 3 Jul 2024 12:37:58 +0100 Subject: [PATCH] feat(AzureOpenAIV2): copy openai text-to-image service + unit tests to azure-openai project to simplify code review process of the follow-up PR. --- .../Connectors.AzureOpenAI.UnitTests.csproj | 8 ++ .../AzureOpenAITextToImageServiceTests.cs | 107 ++++++++++++++++++ .../Connectors.AzureOpenAI.csproj | 10 ++ .../Core/ClientCore.TextToImage.cs | 44 +++++++ .../Services/AzureOpenAITextToImageService.cs | 66 +++++++++++ .../Services/OpenAIAudioToTextService.cs | 6 +- .../Services/OpenAITextToImageService.cs | 2 +- 7 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj index a0a695a6719c..056ac691dfa4 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj @@ -30,6 +30,14 @@ + + + + + + + + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs new file mode 100644 index 000000000000..b0d44113febb --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Services; +using Moq; +using OpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Services; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAITextToImageServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public AzureOpenAITextToImageServiceTests() + { + this._messageHandlerStub = new() + { + ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("./TestData/text-to-image-response.txt")) + } + }; + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Fact] + public void ConstructorWorksCorrectly() + { + // Arrange & Act + var sut = new AzureOpenAITextToImageServiceTests("model", "api-key", "organization"); + + // Assert + Assert.NotNull(sut); + Assert.Equal("organization", sut.Attributes[ClientCore.OrganizationKey]); + Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void OpenAIClientConstructorWorksCorrectly() + { + // Arrange + var sut = new AzureOpenAITextToImageServiceTests("model", new OpenAIClient("apikey")); + + // Assert + Assert.NotNull(sut); + Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Theory] + [InlineData(256, 256, "dall-e-2")] + [InlineData(512, 512, "dall-e-2")] + [InlineData(1024, 1024, "dall-e-2")] + [InlineData(1024, 1024, "dall-e-3")] + [InlineData(1024, 1792, "dall-e-3")] + [InlineData(1792, 1024, "dall-e-3")] + [InlineData(123, 321, "custom-model-1")] + [InlineData(179, 124, "custom-model-2")] + public async Task GenerateImageWorksCorrectlyAsync(int width, int height, string modelId) + { + // Arrange + var sut = new AzureOpenAITextToImageServiceTests(modelId, "api-key", httpClient: this._httpClient); + Assert.Equal(modelId, sut.Attributes["ModelId"]); + + // Act + var result = await sut.GenerateImageAsync("description", width, height); + + // Assert + Assert.Equal("https://image-url/", result); + } + + [Fact] + public async Task GenerateImageDoesLogActionAsync() + { + // Assert + var modelId = "dall-e-2"; + var logger = new Mock>(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); + + // Arrange + var sut = new AzureOpenAITextToImageServiceTests(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); + + // Act + await sut.GenerateImageAsync("description", 256, 256); + + // Assert + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(AzureOpenAITextToImageServiceTests.GenerateImageAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 35c31788610d..720cd1cf71f5 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -21,10 +21,20 @@ Semantic Kernel connectors for Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. + + + + + + + + + + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs new file mode 100644 index 000000000000..b6490a058fb9 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel; +using System.Threading; +using System.Threading.Tasks; +using OpenAI.Images; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal partial class ClientCore +{ + /// + /// Generates an image with the provided configuration. + /// + /// Prompt to generate the image + /// Width of the image + /// Height of the image + /// The to monitor for cancellation requests. The default is . + /// Url of the generated image + internal async Task GenerateImageAsync( + string prompt, + int width, + int height, + CancellationToken cancellationToken) + { + Verify.NotNullOrWhiteSpace(prompt); + + var size = new GeneratedImageSize(width, height); + + var imageOptions = new ImageGenerationOptions() + { + Size = size, + ResponseFormat = GeneratedImageFormat.Uri + }; + + ClientResult response = await RunRequestAsync(() => this.Client.GetImageClient(this.ModelId).GenerateImageAsync(prompt, imageOptions, cancellationToken)).ConfigureAwait(false); + var generatedImage = response.Value; + + return generatedImage.ImageUri?.ToString() ?? throw new KernelException("The generated image is not in url format"); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs new file mode 100644 index 000000000000..a48b3177ebee --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.TextToImage; +using OpenAI; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// OpenAI text to image service. +/// +[Experimental("SKEXP0010")] +public class AzureOpenAITextToImageService : ITextToImageService +{ + private readonly ClientCore _client; + + /// + public IReadOnlyDictionary Attributes => this._client.Attributes; + + /// + /// Initializes a new instance of the class. + /// + /// The model to use for image generation. + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// Non-default endpoint for the OpenAI API. + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAITextToImageService( + string modelId, + string? apiKey = null, + string? organizationId = null, + Uri? endpoint = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + this._client = new(modelId, apiKey, organizationId, endpoint, httpClient, loggerFactory?.CreateLogger(this.GetType())); + } + + /// + /// Initializes a new instance of the class. + /// + /// Model name + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAITextToImageService( + string modelId, + OpenAIClient openAIClient, + ILoggerFactory? loggerFactory = null) + { + this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); + } + + /// + public Task GenerateImageAsync(string description, int width, int height, Kernel? kernel = null, CancellationToken cancellationToken = default) + { + this._client.LogActionDetails(); + return this._client.GenerateImageAsync(description, width, height, cancellationToken); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs index a226d6c59040..cb37384845df 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs @@ -33,7 +33,7 @@ public sealed class OpenAIAudioToTextService : IAudioToTextService public IReadOnlyDictionary Attributes => this._client.Attributes; /// - /// Creates an instance of the with API key auth. + /// Creates an instance of the with API key auth. /// /// Model name /// OpenAI API Key @@ -49,11 +49,11 @@ public OpenAIAudioToTextService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); + this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIAudioToTextService))); } /// - /// Creates an instance of the with API key auth. + /// Creates an instance of the with API key auth. /// /// Model name /// Custom for HTTP requests. diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs index 1a6038aa3f43..15ebcf049a93 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs @@ -64,7 +64,7 @@ public OpenAITextToImageService( OpenAIClient openAIClient, ILoggerFactory? loggerFactory = null) { - this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); + this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextToImageService))); } ///