Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/test/*.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

<ItemGroup>
<Compile Remove="Services\AzureOpenAITextToImageServiceTests.cs" />
</ItemGroup>

<ItemGroup>
<None Include="Services\AzureOpenAITextToImageServiceTests.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Unit tests for <see cref="AzureOpenAITextToImageServiceTests"/> class.
/// </summary>
public sealed class AzureOpenAITextToImageServiceTests : IDisposable
{
private readonly HttpMessageHandlerStub _messageHandlerStub;
private readonly HttpClient _httpClient;
private readonly Mock<ILoggerFactory> _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<ILoggerFactory>();
}

[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<ILogger<AzureOpenAITextToImageServiceTests>>();
logger.Setup(l => l.IsEnabled(It.IsAny<LogLevel>())).Returns(true);

this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>())).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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,20 @@
<Description>Semantic Kernel connectors for Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image.</Description>
</PropertyGroup>

<ItemGroup>
<Compile Remove="Core\ClientCore.TextToImage.cs" />
<Compile Remove="Services\AzureOpenAITextToImageService.cs" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="SemanticKernel.Connectors.AzureOpenAI.UnitTests" />
</ItemGroup>

<ItemGroup>
<None Include="Core\ClientCore.TextToImage.cs" />
<None Include="Services\AzureOpenAITextToImageService.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" VersionOverride="2.0.0-beta.2" />
</ItemGroup>
Expand Down
Comment thread
SergeyMenshykh marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Base class for AI clients that provides common functionality for interacting with OpenAI services.
/// </summary>
internal partial class ClientCore
{
/// <summary>
/// Generates an image with the provided configuration.
/// </summary>
/// <param name="prompt">Prompt to generate the image</param>
/// <param name="width">Width of the image</param>
/// <param name="height">Height of the image</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>Url of the generated image</returns>
internal async Task<string> 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<GeneratedImage> 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");
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// OpenAI text to image service.
/// </summary>
[Experimental("SKEXP0010")]
public class AzureOpenAITextToImageService : ITextToImageService
{
private readonly ClientCore _client;

/// <inheritdoc/>
public IReadOnlyDictionary<string, object?> Attributes => this._client.Attributes;

/// <summary>
/// Initializes a new instance of the <see cref="AzureOpenAITextToImageService"/> class.
/// </summary>
/// <param name="modelId">The model to use for image generation.</param>
/// <param name="apiKey">OpenAI API key, see https://platform.openai.com/account/api-keys</param>
/// <param name="organizationId">OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.</param>
/// <param name="endpoint">Non-default endpoint for the OpenAI API.</param>
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
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()));
}

/// <summary>
/// Initializes a new instance of the <see cref="AzureOpenAITextToImageService"/> class.
/// </summary>
/// <param name="modelId">Model name</param>
/// <param name="openAIClient">Custom <see cref="OpenAIClient"/> for HTTP requests.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
public AzureOpenAITextToImageService(
string modelId,
OpenAIClient openAIClient,
ILoggerFactory? loggerFactory = null)
{
this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService)));
}

/// <inheritdoc/>
public Task<string> GenerateImageAsync(string description, int width, int height, Kernel? kernel = null, CancellationToken cancellationToken = default)
{
this._client.LogActionDetails();
return this._client.GenerateImageAsync(description, width, height, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public sealed class OpenAIAudioToTextService : IAudioToTextService
public IReadOnlyDictionary<string, object?> Attributes => this._client.Attributes;

/// <summary>
/// Creates an instance of the <see cref="OpenAITextToAudioService"/> with API key auth.
/// Creates an instance of the <see cref="OpenAIAudioToTextService"/> with API key auth.
/// </summary>
/// <param name="modelId">Model name</param>
/// <param name="apiKey">OpenAI API Key</param>
Expand All @@ -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)));
}

/// <summary>
/// Creates an instance of the <see cref="OpenAITextToAudioService"/> with API key auth.
/// Creates an instance of the <see cref="OpenAIAudioToTextService"/> with API key auth.
/// </summary>
/// <param name="modelId">Model name</param>
/// <param name="openAIClient">Custom <see cref="OpenAIClient"/> for HTTP requests.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}

/// <inheritdoc/>
Expand Down