Skip to content

Commit

Permalink
.Net OpenAI V2 - Text to Image Service - Phase 02 (#6951)
Browse files Browse the repository at this point in the history
- Updated ImageToText service implementation using OpenAI SDK
- Updated ImageToText service API's parameters order (modelId first) and
added modelId as required (OpenAI supports both dall-e-2 and dall-e-3)
- Added support for OpenAIClient breaking glass for Image to Text
Service
- Added support for custom/Non-default endpoint for Image to Text
Service
- Added missing Extensions (Service Collection + Kernel Builder) for
Embeddings and Image to Text modalities
- Added missing UnitTest for Embeddings
- Added UT convering Image to Text.
- Added integration tests for ImageTotext

- Resolve Partially #6916
  • Loading branch information
RogerBarreto committed Jun 26, 2024
1 parent 6729af1 commit c967a24
Show file tree
Hide file tree
Showing 19 changed files with 914 additions and 21 deletions.
3 changes: 2 additions & 1 deletion dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5C246969-D
ProjectSection(SolutionItems) = preProject
src\InternalUtilities\test\AssertExtensions.cs = src\InternalUtilities\test\AssertExtensions.cs
src\InternalUtilities\test\HttpMessageHandlerStub.cs = src\InternalUtilities\test\HttpMessageHandlerStub.cs
src\Connectors\Connectors.OpenAIV2.UnitTests\Utils\MoqExtensions.cs = src\Connectors\Connectors.OpenAIV2.UnitTests\Utils\MoqExtensions.cs
src\InternalUtilities\test\MultipleHttpMessageHandlerStub.cs = src\InternalUtilities\test\MultipleHttpMessageHandlerStub.cs
src\InternalUtilities\test\TestInternalUtilities.props = src\InternalUtilities\test\TestInternalUtilities.props
EndProjectSection
Expand Down Expand Up @@ -324,7 +325,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestsV2", "src\I
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AzureOpenAI", "src\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj", "{6744272E-8326-48CE-9A3F-6BE227A5E777}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.AzureOpenAI.UnitTests", "src\Connectors\Connectors.AzureOpenAI.UnitTests\Connectors.AzureOpenAI.UnitTests.csproj", "{DB219924-208B-4CDD-8796-EE424689901E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AzureOpenAI.UnitTests", "src\Connectors\Connectors.AzureOpenAI.UnitTests\Connectors.AzureOpenAI.UnitTests.csproj", "{DB219924-208B-4CDD-8796-EE424689901E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@
</ItemGroup>

<ItemGroup>
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/test/AssertExtensions.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/test/HttpMessageHandlerStub.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/test/*.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\SemanticKernel.Core\SemanticKernel.Core.csproj" />
<ProjectReference Include="..\Connectors.OpenAIV2\Connectors.OpenAIV2.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="SemanticKernel.Connectors.OpenAI.UnitTests"/>
</ItemGroup>

<ItemGroup>
<None Update="TestData\text-embeddings-multiple-response.txt">
Expand All @@ -44,6 +47,9 @@
<None Update="TestData\text-embeddings-response.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestData\text-to-image-response.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Http;
using Microsoft.SemanticKernel.Services;
using Moq;
using OpenAI;
using Xunit;
Expand All @@ -23,7 +24,7 @@ public void ItCanBeInstantiatedAndPropertiesSetAsExpected()
{
// Act
var logger = new Mock<ILogger<ClientCoreTests>>().Object;
var openAIClient = new OpenAIClient(new ApiKeyCredential("key"));
var openAIClient = new OpenAIClient("key");

var clientCoreModelConstructor = new ClientCore("model1", "apiKey");
var clientCoreOpenAIClientConstructor = new ClientCore("model1", openAIClient, logger: logger);
Expand Down Expand Up @@ -67,6 +68,8 @@ public void ItUsesEndpointAsExpected(string? clientBaseAddress, string? provided

// Assert
Assert.Equal(endpoint ?? client?.BaseAddress ?? new Uri("https://api.openai.com/v1"), clientCore.Endpoint);
Assert.True(clientCore.Attributes.ContainsKey(AIServiceExtensions.EndpointKey));
Assert.Equal(endpoint?.ToString() ?? client?.BaseAddress?.ToString() ?? "https://api.openai.com/v1", clientCore.Attributes[AIServiceExtensions.EndpointKey]);

client?.Dispose();
}
Expand Down Expand Up @@ -142,7 +145,7 @@ public async Task ItDoNotAddSemanticKernelHeadersWhenOpenAIClientIsProvidedAsync
var clientCore = new ClientCore(
modelId: "model",
openAIClient: new OpenAIClient(
new ApiKeyCredential("test"),
"test",
new OpenAIClientOptions()
{
Transport = new HttpClientPipelineTransport(client),
Expand Down Expand Up @@ -185,4 +188,65 @@ public void ItAddAttributesButDoesNothingIfNullOrEmpty(string? value)
Assert.Equal(value, clientCore.Attributes["key"]);
}
}

[Fact]
public void ItAddModelIdAttributeAsExpected()
{
// Arrange
var expectedModelId = "modelId";

// Act
var clientCore = new ClientCore(expectedModelId, "apikey");
var clientCoreBreakingGlass = new ClientCore(expectedModelId, new OpenAIClient(" "));

// Assert
Assert.True(clientCore.Attributes.ContainsKey(AIServiceExtensions.ModelIdKey));
Assert.True(clientCoreBreakingGlass.Attributes.ContainsKey(AIServiceExtensions.ModelIdKey));
Assert.Equal(expectedModelId, clientCore.Attributes[AIServiceExtensions.ModelIdKey]);
Assert.Equal(expectedModelId, clientCoreBreakingGlass.Attributes[AIServiceExtensions.ModelIdKey]);
}

[Fact]
public void ItAddOrNotOrganizationIdAttributeWhenProvided()
{
// Arrange
var expectedOrganizationId = "organizationId";

// Act
var clientCore = new ClientCore("modelId", "apikey", expectedOrganizationId);
var clientCoreWithoutOrgId = new ClientCore("modelId", "apikey");

// Assert
Assert.True(clientCore.Attributes.ContainsKey(ClientCore.OrganizationKey));
Assert.Equal(expectedOrganizationId, clientCore.Attributes[ClientCore.OrganizationKey]);
Assert.False(clientCoreWithoutOrgId.Attributes.ContainsKey(ClientCore.OrganizationKey));
}

[Fact]
public void ItThrowsIfModelIdIsNotProvided()
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new ClientCore(" ", "apikey"));
Assert.Throws<ArgumentException>(() => new ClientCore("", "apikey"));
Assert.Throws<ArgumentNullException>(() => new ClientCore(null!));
}

[Fact]
public void ItThrowsWhenNotUsingCustomEndpointAndApiKeyIsNotProvided()
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new ClientCore("modelId", " "));
Assert.Throws<ArgumentException>(() => new ClientCore("modelId", ""));
Assert.Throws<ArgumentNullException>(() => new ClientCore("modelId", apiKey: null!));
}

[Fact]
public void ItDoesNotThrowWhenUsingCustomEndpointAndApiKeyIsNotProvided()
{
// Act & Assert
ClientCore? clientCore = null;
clientCore = new ClientCore("modelId", " ", endpoint: new Uri("http://localhost"));
clientCore = new ClientCore("modelId", "", endpoint: new Uri("http://localhost"));
clientCore = new ClientCore("modelId", apiKey: null!, endpoint: new Uri("http://localhost"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Embeddings;
using Microsoft.SemanticKernel.Services;
using Microsoft.SemanticKernel.TextToImage;
using OpenAI;
using Xunit;

namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions;

public class KernelBuilderExtensionsTests
{
[Fact]
public void ItCanAddTextEmbeddingGenerationService()
{
// Arrange
var sut = Kernel.CreateBuilder();

// Act
var service = sut.AddOpenAITextEmbeddingGeneration("model", "key")
.Build()
.GetRequiredService<ITextEmbeddingGenerationService>();

// Assert
Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
}

[Fact]
public void ItCanAddTextEmbeddingGenerationServiceWithOpenAIClient()
{
// Arrange
var sut = Kernel.CreateBuilder();

// Act
var service = sut.AddOpenAITextEmbeddingGeneration("model", new OpenAIClient("key"))
.Build()
.GetRequiredService<ITextEmbeddingGenerationService>();

// Assert
Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
}

[Fact]
public void ItCanAddTextToImageService()
{
// Arrange
var sut = Kernel.CreateBuilder();

// Act
var service = sut.AddOpenAITextToImage("model", "key")
.Build()
.GetRequiredService<ITextToImageService>();

// Assert
Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
}

[Fact]
public void ItCanAddTextToImageServiceWithOpenAIClient()
{
// Arrange
var sut = Kernel.CreateBuilder();

// Act
var service = sut.AddOpenAITextToImage("model", new OpenAIClient("key"))
.Build()
.GetRequiredService<ITextToImageService>();

// Assert
Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Embeddings;
using Microsoft.SemanticKernel.Services;
using Microsoft.SemanticKernel.TextToImage;
using OpenAI;
using Xunit;

namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions;

public class ServiceCollectionExtensionsTests
{
[Fact]
public void ItCanAddTextEmbeddingGenerationService()
{
// Arrange
var sut = new ServiceCollection();

// Act
var service = sut.AddOpenAITextEmbeddingGeneration("model", "key")
.BuildServiceProvider()
.GetRequiredService<ITextEmbeddingGenerationService>();

// Assert
Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
}

[Fact]
public void ItCanAddTextEmbeddingGenerationServiceWithOpenAIClient()
{
// Arrange
var sut = new ServiceCollection();

// Act
var service = sut.AddOpenAITextEmbeddingGeneration("model", new OpenAIClient("key"))
.BuildServiceProvider()
.GetRequiredService<ITextEmbeddingGenerationService>();

// Assert
Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
}

[Fact]
public void ItCanAddImageToTextService()
{
// Arrange
var sut = new ServiceCollection();

// Act
var service = sut.AddOpenAITextToImage("model", "key")
.BuildServiceProvider()
.GetRequiredService<ITextToImageService>();

// Assert
Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
}

[Fact]
public void ItCanAddImageToTextServiceWithOpenAIClient()
{
// Arrange
var sut = new ServiceCollection();

// Act
var service = sut.AddOpenAITextToImage("model", new OpenAIClient("key"))
.BuildServiceProvider()
.GetRequiredService<ITextToImageService>();

// Assert
Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Services;
using Moq;
using OpenAI;
using Xunit;

namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services;

/// <summary>
/// Unit tests for <see cref="OpenAITextEmbeddingGenerationService"/> class.
/// </summary>
public class OpenAITextEmbeddingGenerationServiceTests
{
[Fact]
Expand Down Expand Up @@ -43,8 +49,9 @@ public async Task ItGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsEmpty()
}

[Fact]
public async Task IGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsWhitespace()
public async Task GetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsWhitespace()
{
// Arrange
using HttpMessageHandlerStub handler = new()
{
ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
Expand All @@ -54,7 +61,6 @@ public async Task IGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsWhitespace()
};
using HttpClient client = new(handler);

// Arrange
var sut = new OpenAITextEmbeddingGenerationService("model", "apikey", httpClient: client);

// Act
Expand All @@ -68,6 +74,7 @@ public async Task IGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsWhitespace()
[Fact]
public async Task ItThrowsIfNumberOfResultsDiffersFromInputsAsync()
{
// Arrange
using HttpMessageHandlerStub handler = new()
{
ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
Expand All @@ -77,10 +84,38 @@ public async Task ItThrowsIfNumberOfResultsDiffersFromInputsAsync()
};
using HttpClient client = new(handler);

// Arrange
var sut = new OpenAITextEmbeddingGenerationService("model", "apikey", httpClient: client);

// Act & Assert
await Assert.ThrowsAsync<KernelException>(async () => await sut.GenerateEmbeddingsAsync(["test"], null, CancellationToken.None));
}

[Fact]
public async Task GetEmbeddingsDoesLogActionAsync()
{
// Arrange
using HttpMessageHandlerStub handler = new()
{
ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(File.ReadAllText("./TestData/text-embeddings-response.txt"))
}
};
using HttpClient client = new(handler);

var modelId = "dall-e-2";
var logger = new Mock<ILogger<OpenAITextEmbeddingGenerationService>>();
logger.Setup(l => l.IsEnabled(It.IsAny<LogLevel>())).Returns(true);

var mockLoggerFactory = new Mock<ILoggerFactory>();
mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>())).Returns(logger.Object);

var sut = new OpenAITextEmbeddingGenerationService(modelId, "apiKey", httpClient: client, loggerFactory: mockLoggerFactory.Object);

// Act
await sut.GenerateEmbeddingsAsync(["description"]);

// Assert
logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAITextEmbeddingGenerationService.GenerateEmbeddingsAsync)}. OpenAI Model ID: {modelId}.", Times.Once());
}
}
Loading

0 comments on commit c967a24

Please sign in to comment.