From 8656ecbc30906f7be54de09cf836412868b8746e Mon Sep 17 00:00:00 2001 From: Lev Skuditsky Date: Tue, 11 Feb 2025 20:29:22 +0200 Subject: [PATCH 1/4] Support for configuring dimensions in Google AI embeddings generation --- ...GoogleAIClientEmbeddingsGenerationTests.cs | 51 ++++++++++++- .../GoogleAI/GoogleAIEmbeddingRequestTests.cs | 71 +++++++++++++++---- ...leAITextEmbeddingGenerationServiceTests.cs | 55 +++++++++++++- .../Core/GoogleAI/GoogleAIEmbeddingClient.cs | 8 ++- .../Core/GoogleAI/GoogleAIEmbeddingRequest.cs | 9 ++- .../GoogleAIKernelBuilderExtensions.cs | 7 +- .../GoogleAIMemoryBuilderExtensions.cs | 7 +- .../GoogleAIServiceCollectionExtensions.cs | 7 +- .../GoogleAITextEmbeddingGenerationService.cs | 12 +++- .../EmbeddingGenerationServiceExtensions.cs | 3 +- .../Services/AIServiceExtensions.cs | 16 +++++ 11 files changed, 215 insertions(+), 31 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIClientEmbeddingsGenerationTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIClientEmbeddingsGenerationTests.cs index 36b91707641a..855740b7421f 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIClientEmbeddingsGenerationTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIClientEmbeddingsGenerationTests.cs @@ -142,14 +142,61 @@ public async Task ItCreatesPostRequestWithSemanticKernelVersionHeaderAsync() Assert.Equal(expectedVersion, header); } + [Fact] + public async Task ShouldIncludeDimensionsInAllRequestsAsync() + { + // Arrange + const int Dimensions = 512; + var client = this.CreateEmbeddingsClient(dimensions: Dimensions); + var dataToEmbed = new List() + { + "First text to embed", + "Second text to embed", + "Third text to embed" + }; + + // Act + await client.GenerateEmbeddingsAsync(dataToEmbed); + + // Assert + var request = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent); + Assert.NotNull(request); + Assert.Equal(dataToEmbed.Count, request.Requests.Count); + Assert.All(request.Requests, item => Assert.Equal(Dimensions, item.Dimensions)); + } + + [Fact] + public async Task ShouldNotIncludeDimensionsInAllRequestsWhenNotProvidedAsync() + { + // Arrange + var client = this.CreateEmbeddingsClient(); + var dataToEmbed = new List() + { + "First text to embed", + "Second text to embed", + "Third text to embed" + }; + + // Act + await client.GenerateEmbeddingsAsync(dataToEmbed); + + // Assert + var request = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent); + Assert.NotNull(request); + Assert.Equal(dataToEmbed.Count, request.Requests.Count); + Assert.All(request.Requests, item => Assert.Null(item.Dimensions)); + } + private GoogleAIEmbeddingClient CreateEmbeddingsClient( - string modelId = "fake-model") + string modelId = "fake-model", + int? dimensions = null) { var client = new GoogleAIEmbeddingClient( httpClient: this._httpClient, modelId: modelId, apiVersion: GoogleAIVersion.V1, - apiKey: "fake-key"); + apiKey: "fake-key", + dimensions: dimensions); return client; } diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIEmbeddingRequestTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIEmbeddingRequestTests.cs index e15701009de2..f96fd6da5ca2 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIEmbeddingRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIEmbeddingRequestTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; using Microsoft.SemanticKernel.Connectors.Google.Core; using Xunit; @@ -7,35 +8,79 @@ namespace SemanticKernel.Connectors.Google.UnitTests.Core.GoogleAI; public sealed class GoogleAIEmbeddingRequestTests { + // Arrange + private static readonly string[] s_data = ["text1", "text2"]; + private const string ModelId = "modelId"; + private const string DimensionalityJsonPropertyName = "\"output_dimensionality\""; + private const int Dimensions = 512; + [Fact] public void FromDataReturnsValidRequestWithData() { - // Arrange - string[] data = ["text1", "text2"]; - var modelId = "modelId"; - // Act - var request = GoogleAIEmbeddingRequest.FromData(data, modelId); + var request = GoogleAIEmbeddingRequest.FromData(s_data, ModelId); // Assert Assert.Equal(2, request.Requests.Count); - Assert.Equal(data[0], request.Requests[0].Content.Parts![0].Text); - Assert.Equal(data[1], request.Requests[1].Content.Parts![0].Text); + Assert.Equal(s_data[0], request.Requests[0].Content.Parts![0].Text); + Assert.Equal(s_data[1], request.Requests[1].Content.Parts![0].Text); } [Fact] public void FromDataReturnsValidRequestWithModelId() { - // Arrange - string[] data = ["text1", "text2"]; - var modelId = "modelId"; + // Act + var request = GoogleAIEmbeddingRequest.FromData(s_data, ModelId); + + // Assert + Assert.Equal(2, request.Requests.Count); + Assert.Equal($"models/{ModelId}", request.Requests[0].Model); + Assert.Equal($"models/{ModelId}", request.Requests[1].Model); + } + [Fact] + public void FromDataSetsDimensionsToNullWhenNotProvided() + { // Act - var request = GoogleAIEmbeddingRequest.FromData(data, modelId); + var request = GoogleAIEmbeddingRequest.FromData(s_data, ModelId); // Assert Assert.Equal(2, request.Requests.Count); - Assert.Equal($"models/{modelId}", request.Requests[0].Model); - Assert.Equal($"models/{modelId}", request.Requests[1].Model); + Assert.Null(request.Requests[0].Dimensions); + Assert.Null(request.Requests[1].Dimensions); + } + + [Fact] + public void FromDataJsonDoesNotIncludeDimensionsWhenNull() + { + // Act + var request = GoogleAIEmbeddingRequest.FromData(s_data, ModelId); + string json = JsonSerializer.Serialize(request); + + // Assert + Assert.DoesNotContain(DimensionalityJsonPropertyName, json); + } + + [Fact] + public void FromDataSetsDimensionsWhenProvided() + { + // Act + var request = GoogleAIEmbeddingRequest.FromData(s_data, ModelId, Dimensions); + + // Assert + Assert.Equal(2, request.Requests.Count); + Assert.Equal(Dimensions, request.Requests[0].Dimensions); + Assert.Equal(Dimensions, request.Requests[1].Dimensions); + } + + [Fact] + public void FromDataJsonIncludesDimensionsWhenProvided() + { + // Act + var request = GoogleAIEmbeddingRequest.FromData(s_data, ModelId, Dimensions); + string json = JsonSerializer.Serialize(request); + + // Assert + Assert.Contains($"{DimensionalityJsonPropertyName}:{Dimensions}", json); } } diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAITextEmbeddingGenerationServiceTests.cs index 54b5bc2654de..8c7b3a74e6cc 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAITextEmbeddingGenerationServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAITextEmbeddingGenerationServiceTests.cs @@ -8,14 +8,63 @@ namespace SemanticKernel.Connectors.Google.UnitTests.Services; public sealed class GoogleAITextEmbeddingGenerationServiceTests { + private const string Model = "fake-model"; + private const string ApiKey = "fake-key"; + private const int Dimensions = 512; + [Fact] public void AttributesShouldContainModelId() { // Arrange & Act - string model = "fake-model"; - var service = new GoogleAITextEmbeddingGenerationService(model, "key"); + var service = new GoogleAITextEmbeddingGenerationService(Model, ApiKey); + + // Assert + Assert.Equal(Model, service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void AttributesShouldNotContainDimensionsWhenNotProvided() + { + // Arrange & Act + var service = new GoogleAITextEmbeddingGenerationService(Model, ApiKey); + + // Assert + Assert.False(service.Attributes.ContainsKey(AIServiceExtensions.DimensionsKey)); + } + + [Fact] + public void AttributesShouldContainDimensionsWhenProvided() + { + // Arrange & Act + var service = new GoogleAITextEmbeddingGenerationService(Model, ApiKey, dimensions: Dimensions); + + // Assert + Assert.Equal(Dimensions, service.Attributes[AIServiceExtensions.DimensionsKey]); + } + + [Fact] + public void GetDimensionsReturnsCorrectValue() + { + // Arrange + var service = new GoogleAITextEmbeddingGenerationService(Model, ApiKey, dimensions: Dimensions); + + // Act + var result = service.GetDimensions(); + + // Assert + Assert.Equal(Dimensions, result); + } + + [Fact] + public void GetDimensionsReturnsNullWhenNotProvided() + { + // Arrange + var service = new GoogleAITextEmbeddingGenerationService(Model, ApiKey); + + // Act + var result = service.GetDimensions(); // Assert - Assert.Equal(model, service.Attributes[AIServiceExtensions.ModelIdKey]); + Assert.Null(result); } } diff --git a/dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingClient.cs b/dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingClient.cs index 3851f609e023..ff2542549c78 100644 --- a/dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingClient.cs +++ b/dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingClient.cs @@ -17,6 +17,7 @@ internal sealed class GoogleAIEmbeddingClient : ClientBase { private readonly string _embeddingModelId; private readonly Uri _embeddingEndpoint; + private readonly int? _dimensions; /// /// Represents a client for interacting with the embeddings models by Google AI. @@ -26,12 +27,14 @@ internal sealed class GoogleAIEmbeddingClient : ClientBase /// Api key for GoogleAI endpoint /// Version of the Google API /// Logger instance used for logging (optional) + /// The number of dimensions that the model should use. If not specified, the default number of dimensions will be used. public GoogleAIEmbeddingClient( HttpClient httpClient, string modelId, string apiKey, GoogleAIVersion apiVersion, - ILogger? logger = null) + ILogger? logger = null, + int? dimensions = null) : base( httpClient: httpClient, logger: logger) @@ -43,6 +46,7 @@ public GoogleAIEmbeddingClient( this._embeddingModelId = modelId; this._embeddingEndpoint = new Uri($"https://generativelanguage.googleapis.com/{versionSubLink}/models/{this._embeddingModelId}:batchEmbedContents?key={apiKey}"); + this._dimensions = dimensions; } /// @@ -67,7 +71,7 @@ public async Task>> GenerateEmbeddingsAsync( } private GoogleAIEmbeddingRequest GetEmbeddingRequest(IEnumerable data) - => GoogleAIEmbeddingRequest.FromData(data, this._embeddingModelId); + => GoogleAIEmbeddingRequest.FromData(data, this._embeddingModelId, this._dimensions); private static List> DeserializeAndProcessEmbeddingsResponse(string body) => ProcessEmbeddingsResponse(DeserializeResponse(body)); diff --git a/dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingRequest.cs b/dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingRequest.cs index a9f5316c9934..b4d9cea41f43 100644 --- a/dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingRequest.cs +++ b/dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingRequest.cs @@ -11,7 +11,7 @@ internal sealed class GoogleAIEmbeddingRequest [JsonPropertyName("requests")] public IList Requests { get; set; } = null!; - public static GoogleAIEmbeddingRequest FromData(IEnumerable data, string modelId) => new() + public static GoogleAIEmbeddingRequest FromData(IEnumerable data, string modelId, int? dimensions = null) => new() { Requests = data.Select(text => new RequestEmbeddingContent { @@ -25,7 +25,8 @@ internal sealed class GoogleAIEmbeddingRequest Text = text } ] - } + }, + Dimensions = dimensions }).ToList() }; @@ -45,5 +46,9 @@ internal sealed class RequestEmbeddingContent [JsonPropertyName("taskType")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? TaskType { get; set; } // todo: enum + + [JsonPropertyName("output_dimensionality")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Dimensions { get; set; } } } diff --git a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs index a03fe357ad31..62618bd03fd5 100644 --- a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIKernelBuilderExtensions.cs @@ -56,6 +56,7 @@ public static IKernelBuilder AddGoogleAIGeminiChatCompletion( /// The version of the Google API. /// The optional service ID. /// The optional custom HttpClient. + /// The optional number of dimensions that the model should use. If not specified, the default number of dimensions will be used. /// The updated kernel builder. public static IKernelBuilder AddGoogleAIEmbeddingGeneration( this IKernelBuilder builder, @@ -63,7 +64,8 @@ public static IKernelBuilder AddGoogleAIEmbeddingGeneration( string apiKey, GoogleAIVersion apiVersion = GoogleAIVersion.V1_Beta, // todo: change beta to stable when stable version will be available string? serviceId = null, - HttpClient? httpClient = null) + HttpClient? httpClient = null, + int? dimensions = null) { Verify.NotNull(builder); Verify.NotNull(modelId); @@ -75,7 +77,8 @@ public static IKernelBuilder AddGoogleAIEmbeddingGeneration( apiKey: apiKey, apiVersion: apiVersion, httpClient: HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - loggerFactory: serviceProvider.GetService())); + loggerFactory: serviceProvider.GetService(), + dimensions: dimensions)); return builder; } } diff --git a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIMemoryBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIMemoryBuilderExtensions.cs index b178a224dbf3..5d81620f4bce 100644 --- a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIMemoryBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIMemoryBuilderExtensions.cs @@ -20,13 +20,15 @@ public static class GoogleAIMemoryBuilderExtensions /// The API key for authentication Gemini API. /// The version of the Google API. /// The optional custom HttpClient. + /// The optional number of dimensions that the model should use. If not specified, the default number of dimensions will be used. /// The updated memory builder. public static MemoryBuilder WithGoogleAITextEmbeddingGeneration( this MemoryBuilder builder, string modelId, string apiKey, GoogleAIVersion apiVersion = GoogleAIVersion.V1_Beta, - HttpClient? httpClient = null) + HttpClient? httpClient = null, + int? dimensions = null) { Verify.NotNull(builder); Verify.NotNull(modelId); @@ -38,6 +40,7 @@ public static MemoryBuilder WithGoogleAITextEmbeddingGeneration( apiKey: apiKey, apiVersion: apiVersion, httpClient: HttpClientProvider.GetHttpClient(httpClient ?? builderHttpClient), - loggerFactory: loggerFactory)); + loggerFactory: loggerFactory, + dimensions: dimensions)); } } diff --git a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.cs index a3742b36e7d9..2c6d11fc8b08 100644 --- a/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Google/Extensions/GoogleAIServiceCollectionExtensions.cs @@ -52,13 +52,15 @@ public static IServiceCollection AddGoogleAIGeminiChatCompletion( /// The API key for authentication Gemini API. /// The version of the Google API. /// Optional service ID. + /// The optional number of dimensions that the model should use. If not specified, the default number of dimensions will be used. /// The updated service collection. public static IServiceCollection AddGoogleAIEmbeddingGeneration( this IServiceCollection services, string modelId, string apiKey, GoogleAIVersion apiVersion = GoogleAIVersion.V1_Beta, // todo: change beta to stable when stable version will be available - string? serviceId = null) + string? serviceId = null, + int? dimensions = null) { Verify.NotNull(services); Verify.NotNull(modelId); @@ -70,6 +72,7 @@ public static IServiceCollection AddGoogleAIEmbeddingGeneration( apiKey: apiKey, apiVersion: apiVersion, httpClient: HttpClientProvider.GetHttpClient(serviceProvider), - loggerFactory: serviceProvider.GetService())); + loggerFactory: serviceProvider.GetService(), + dimensions: dimensions)); } } diff --git a/dotnet/src/Connectors/Connectors.Google/Services/GoogleAITextEmbeddingGenerationService.cs b/dotnet/src/Connectors/Connectors.Google/Services/GoogleAITextEmbeddingGenerationService.cs index 8707de39cf99..6ba97b199f40 100644 --- a/dotnet/src/Connectors/Connectors.Google/Services/GoogleAITextEmbeddingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.Google/Services/GoogleAITextEmbeddingGenerationService.cs @@ -29,12 +29,14 @@ public sealed class GoogleAITextEmbeddingGenerationService : ITextEmbeddingGener /// Version of the Google API /// The optional HTTP client. /// Optional logger factory to be used for logging. + /// The number of dimensions that the model should use. If not specified, the default number of dimensions will be used. public GoogleAITextEmbeddingGenerationService( string modelId, string apiKey, GoogleAIVersion apiVersion = GoogleAIVersion.V1_Beta, // todo: change beta to stable when stable version will be available HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) + ILoggerFactory? loggerFactory = null, + int? dimensions = null) { Verify.NotNullOrWhiteSpace(modelId); Verify.NotNullOrWhiteSpace(apiKey); @@ -46,8 +48,14 @@ public GoogleAITextEmbeddingGenerationService( modelId: modelId, apiKey: apiKey, apiVersion: apiVersion, - logger: loggerFactory?.CreateLogger(typeof(GoogleAITextEmbeddingGenerationService))); + logger: loggerFactory?.CreateLogger(typeof(GoogleAITextEmbeddingGenerationService)), + dimensions: dimensions); this._attributesInternal.Add(AIServiceExtensions.ModelIdKey, modelId); + + if (dimensions.HasValue) + { + this._attributesInternal.Add(AIServiceExtensions.DimensionsKey, dimensions); + } } /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs index c060c3f0d523..977f2d650858 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs @@ -104,7 +104,8 @@ public EmbeddingGenerationServiceEmbeddingGenerator(IEmbeddingGenerationService< this.Metadata = new EmbeddingGeneratorMetadata( service.GetType().Name, service.GetEndpoint() is string endpoint ? new Uri(endpoint) : null, - service.GetModelId()); + service.GetModelId(), + service.GetDimensions()); } /// diff --git a/dotnet/src/SemanticKernel.Abstractions/Services/AIServiceExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/Services/AIServiceExtensions.cs index 30a3ee7794e5..d744400d5383 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Services/AIServiceExtensions.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Services/AIServiceExtensions.cs @@ -28,6 +28,11 @@ public static class AIServiceExtensions /// public static string ApiVersionKey => "ApiVersion"; + /// + /// Gets the key used to store the dimensions value in the dictionary. + /// + public static string DimensionsKey => "Dimensions"; + /// /// Gets the model identifier from 's . /// @@ -49,6 +54,17 @@ public static class AIServiceExtensions /// The API version if it was specified in the service's attributes; otherwise, null. public static string? GetApiVersion(this IAIService service) => service.GetAttribute(ApiVersionKey); + /// + /// Gets the dimensions from 's . + /// + /// The service from which to get the dimensions. + /// The dimensions if it was specified in the service's attributes; otherwise, null. + public static int? GetDimensions(this IAIService service) + { + Verify.NotNull(service); + return service.Attributes?.TryGetValue(DimensionsKey, out object? value) == true ? value as int? : null; + } + /// /// Gets the specified attribute. /// From d583bfed589cb8e467078350b85bef76fd0ae8be Mon Sep 17 00:00:00 2001 From: Lev Skuditsky Date: Sat, 8 Mar 2025 14:51:07 +0200 Subject: [PATCH 2/4] .Net: Support for configuring dimensions in Google AI embeddings generation (PR comments) --- .../Memory/Google_EmbeddingGeneration.cs | 61 +++++++++++++ ...leAITextEmbeddingGenerationServiceTests.cs | 87 ++++++++++++++++++- .../GoogleAITextEmbeddingGenerationService.cs | 2 +- .../Google/EmbeddingGenerationTests.cs | 30 ++++++- .../Connectors/Google/TestsBase.cs | 10 +++ .../EmbeddingGenerationServiceExtensions.cs | 16 ++++ .../Services/AIServiceExtensions.cs | 16 ---- 7 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 dotnet/samples/Concepts/Memory/Google_EmbeddingGeneration.cs diff --git a/dotnet/samples/Concepts/Memory/Google_EmbeddingGeneration.cs b/dotnet/samples/Concepts/Memory/Google_EmbeddingGeneration.cs new file mode 100644 index 000000000000..aa13cb08889a --- /dev/null +++ b/dotnet/samples/Concepts/Memory/Google_EmbeddingGeneration.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Embeddings; +using xRetry; + +namespace Memory; + +// The following example shows how to use Semantic Kernel with Google AI for embedding generation, +// including the ability to specify custom dimensions. +public class Google_EmbeddingGeneration(ITestOutputHelper output) : BaseTest(output) +{ + [RetryFact(typeof(HttpOperationException))] + public async Task RunEmbeddingWithDefaultDimensionsAsync() + { + Assert.NotNull(TestConfiguration.GoogleAI.EmbeddingModelId); + Assert.NotNull(TestConfiguration.GoogleAI.ApiKey); + + IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); + kernelBuilder.AddGoogleAIEmbeddingGeneration( + modelId: TestConfiguration.GoogleAI.EmbeddingModelId!, + apiKey: TestConfiguration.GoogleAI.ApiKey); + Kernel kernel = kernelBuilder.Build(); + + var embeddingGenerator = kernel.GetRequiredService(); + + // Generate embeddings with the default dimensions for the model + var embeddings = await embeddingGenerator.GenerateEmbeddingsAsync( + ["Semantic Kernel is a lightweight, open-source development kit that lets you easily build AI agents and integrate the latest AI models into your codebase."]); + + Console.WriteLine($"Generated '{embeddings.Count}' embedding(s) with '{embeddings[0].Length}' dimensions (default) for the provided text"); + } + + [RetryFact(typeof(HttpOperationException))] + public async Task RunEmbeddingWithCustomDimensionsAsync() + { + Assert.NotNull(TestConfiguration.GoogleAI.EmbeddingModelId); + Assert.NotNull(TestConfiguration.GoogleAI.ApiKey); + + // Specify custom dimensions for the embeddings + const int CustomDimensions = 512; + + IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); + kernelBuilder.AddGoogleAIEmbeddingGeneration( + modelId: TestConfiguration.GoogleAI.EmbeddingModelId!, + apiKey: TestConfiguration.GoogleAI.ApiKey, + dimensions: CustomDimensions); + Kernel kernel = kernelBuilder.Build(); + + var embeddingGenerator = kernel.GetRequiredService(); + + // Generate embeddings with the specified custom dimensions + var embeddings = await embeddingGenerator.GenerateEmbeddingsAsync( + ["Semantic Kernel is a lightweight, open-source development kit that lets you easily build AI agents and integrate the latest AI models into your codebase."]); + + Console.WriteLine($"Generated '{embeddings.Count}' embedding(s) with '{embeddings[0].Length}' dimensions (custom: '{CustomDimensions}') for the provided text"); + + // Verify that we received embeddings with our requested dimensions + Assert.Equal(CustomDimensions, embeddings[0].Length); + } +} diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAITextEmbeddingGenerationServiceTests.cs index 8c7b3a74e6cc..6b9e30373a70 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAITextEmbeddingGenerationServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAITextEmbeddingGenerationServiceTests.cs @@ -1,16 +1,49 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; using Microsoft.SemanticKernel.Connectors.Google; +using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Services; using Xunit; namespace SemanticKernel.Connectors.Google.UnitTests.Services; -public sealed class GoogleAITextEmbeddingGenerationServiceTests +public sealed class GoogleAITextEmbeddingGenerationServiceTests : IDisposable { private const string Model = "fake-model"; private const string ApiKey = "fake-key"; private const int Dimensions = 512; + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + + public GoogleAITextEmbeddingGenerationServiceTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub + { + ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent( + """ + { + "embeddings": [ + { + "values": [0.1, 0.2, 0.3, 0.4, 0.5] + } + ] + } + """, + Encoding.UTF8, + "application/json" + ) + } + }; + + this._httpClient = new HttpClient(this._messageHandlerStub, disposeHandler: false); + } [Fact] public void AttributesShouldContainModelId() @@ -29,7 +62,7 @@ public void AttributesShouldNotContainDimensionsWhenNotProvided() var service = new GoogleAITextEmbeddingGenerationService(Model, ApiKey); // Assert - Assert.False(service.Attributes.ContainsKey(AIServiceExtensions.DimensionsKey)); + Assert.False(service.Attributes.ContainsKey(EmbeddingGenerationExtensions.DimensionsKey)); } [Fact] @@ -39,7 +72,7 @@ public void AttributesShouldContainDimensionsWhenProvided() var service = new GoogleAITextEmbeddingGenerationService(Model, ApiKey, dimensions: Dimensions); // Assert - Assert.Equal(Dimensions, service.Attributes[AIServiceExtensions.DimensionsKey]); + Assert.Equal(Dimensions, service.Attributes[EmbeddingGenerationExtensions.DimensionsKey]); } [Fact] @@ -67,4 +100,52 @@ public void GetDimensionsReturnsNullWhenNotProvided() // Assert Assert.Null(result); } + + [Fact] + public async Task ShouldNotIncludeDimensionsInRequestWhenNotProvidedAsync() + { + // Arrange + var service = new GoogleAITextEmbeddingGenerationService( + modelId: Model, + apiKey: ApiKey, + dimensions: null, + httpClient: this._httpClient); + var dataToEmbed = new List { "Text to embed" }; + + // Act + await service.GenerateEmbeddingsAsync(dataToEmbed); + + // Assert + Assert.NotNull(this._messageHandlerStub.RequestContent); + var requestBody = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent); + Assert.DoesNotContain("output_dimensionality", requestBody); + } + + [Theory] + [InlineData(Dimensions)] + [InlineData(Dimensions * 2)] + public async Task ShouldIncludeDimensionsInRequestWhenProvidedAsync(int? dimensions) + { + // Arrange + var service = new GoogleAITextEmbeddingGenerationService( + modelId: Model, + apiKey: ApiKey, + dimensions: dimensions, + httpClient: this._httpClient); + var dataToEmbed = new List { "Text to embed" }; + + // Act + await service.GenerateEmbeddingsAsync(dataToEmbed); + + // Assert + Assert.NotNull(this._messageHandlerStub.RequestContent); + var requestBody = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent); + Assert.Contains($"\"output_dimensionality\":{dimensions}", requestBody); + } + + public void Dispose() + { + this._messageHandlerStub.Dispose(); + this._httpClient.Dispose(); + } } diff --git a/dotnet/src/Connectors/Connectors.Google/Services/GoogleAITextEmbeddingGenerationService.cs b/dotnet/src/Connectors/Connectors.Google/Services/GoogleAITextEmbeddingGenerationService.cs index 6ba97b199f40..51971aa3618f 100644 --- a/dotnet/src/Connectors/Connectors.Google/Services/GoogleAITextEmbeddingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.Google/Services/GoogleAITextEmbeddingGenerationService.cs @@ -54,7 +54,7 @@ public GoogleAITextEmbeddingGenerationService( if (dimensions.HasValue) { - this._attributesInternal.Add(AIServiceExtensions.DimensionsKey, dimensions); + this._attributesInternal.Add(EmbeddingGenerationExtensions.DimensionsKey, dimensions); } } diff --git a/dotnet/src/IntegrationTests/Connectors/Google/EmbeddingGenerationTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/EmbeddingGenerationTests.cs index 79fc5db80aff..5b4f2d2f39a2 100644 --- a/dotnet/src/IntegrationTests/Connectors/Google/EmbeddingGenerationTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Google/EmbeddingGenerationTests.cs @@ -10,13 +10,14 @@ namespace SemanticKernel.IntegrationTests.Connectors.Google; public sealed class EmbeddingGenerationTests(ITestOutputHelper output) : TestsBase(output) { + private const string Input = "LLM is Large Language Model."; + [RetryTheory] [InlineData(ServiceType.GoogleAI, Skip = "This test is for manual verification.")] [InlineData(ServiceType.VertexAI, Skip = "This test is for manual verification.")] public async Task EmbeddingGenerationAsync(ServiceType serviceType) { // Arrange - const string Input = "LLM is Large Language Model."; var sut = this.GetEmbeddingService(serviceType); // Act @@ -26,4 +27,31 @@ public async Task EmbeddingGenerationAsync(ServiceType serviceType) this.Output.WriteLine($"Count of returned embeddings: {response.Length}"); Assert.Equal(768, response.Length); } + + [RetryTheory] + [InlineData(ServiceType.GoogleAI, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.VertexAI, Skip = "Not implemented yet in VertexAI. This test is for manual verification.")] + public async Task EmbeddingGenerationWithCustomDimensionsAsync(ServiceType serviceType) + { + // Arrange + var defaultService = this.GetEmbeddingService(serviceType); + var defaultResponse = await defaultService.GenerateEmbeddingAsync(Input); + int defaultDimensions = defaultResponse.Length; + + // Insure custom dimensions are different from default + int customDimensions = defaultDimensions == 512 ? 256 : 512; + + var sut = this.GetEmbeddingServiceWithDimensions(serviceType, customDimensions); + + // Act + var response = await sut.GenerateEmbeddingAsync(Input); + + // Assert + this.Output.WriteLine($"Default dimensions: {defaultDimensions}"); + this.Output.WriteLine($"Custom dimensions: {customDimensions}"); + this.Output.WriteLine($"Returned dimensions: {response.Length}"); + + Assert.Equal(customDimensions, response.Length); + Assert.NotEqual(defaultDimensions, response.Length); + } } diff --git a/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs b/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs index b6b2e2a6c02a..afcf0f402052 100644 --- a/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs +++ b/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs @@ -61,6 +61,16 @@ public abstract class TestsBase(ITestOutputHelper output) _ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, null) }; + protected ITextEmbeddingGenerationService GetEmbeddingServiceWithDimensions(ServiceType serviceType, int dimensions) => serviceType switch + { + ServiceType.GoogleAI => new GoogleAITextEmbeddingGenerationService( + this.GoogleAIGetEmbeddingModel(), + this.GoogleAIGetApiKey(), + dimensions: dimensions), + ServiceType.VertexAI => throw new NotImplementedException("Semantic Kernel does not support configuring dimensions for Vertex AI embeddings"), + _ => throw new ArgumentException($"Invalid service type: {serviceType}", nameof(serviceType)) + }; + public enum ServiceType { GoogleAI, diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs index c836e420b706..fec0dd8a0c27 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/Embeddings/EmbeddingGenerationServiceExtensions.cs @@ -18,6 +18,11 @@ namespace Microsoft.SemanticKernel.Embeddings; [Experimental("SKEXP0001")] public static class EmbeddingGenerationExtensions { + /// + /// Gets the key used to store the dimensions value in the dictionary. + /// + public static string DimensionsKey => "Dimensions"; + /// /// Generates an embedding from the given . /// @@ -90,6 +95,17 @@ public static ITextEmbeddingGenerationService AsTextEmbeddingGenerationService(t new EmbeddingGeneratorTextEmbeddingGenerationService(generator, serviceProvider); } + /// + /// Gets the dimensions from 's . + /// + /// The service from which to get the dimensions. + /// The dimensions if it was specified in the service's attributes; otherwise, null. + public static int? GetDimensions(this IEmbeddingGenerationService service) where TEmbedding : unmanaged + { + Verify.NotNull(service); + return service.Attributes.TryGetValue(DimensionsKey, out object? value) ? value as int? : null; + } + /// Provides an implementation of around an . private sealed class EmbeddingGenerationServiceEmbeddingGenerator : IEmbeddingGenerator> where TEmbedding : unmanaged diff --git a/dotnet/src/SemanticKernel.Abstractions/Services/AIServiceExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/Services/AIServiceExtensions.cs index 48e6f051dfc4..24bc16a0f8e7 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Services/AIServiceExtensions.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Services/AIServiceExtensions.cs @@ -30,11 +30,6 @@ public static class AIServiceExtensions /// public static string ApiVersionKey => "ApiVersion"; - /// - /// Gets the key used to store the dimensions value in the dictionary. - /// - public static string DimensionsKey => "Dimensions"; - /// /// Gets the model identifier from 's . /// @@ -56,17 +51,6 @@ public static class AIServiceExtensions /// The API version if it was specified in the service's attributes; otherwise, null. public static string? GetApiVersion(this IAIService service) => service.GetAttribute(ApiVersionKey); - /// - /// Gets the dimensions from 's . - /// - /// The service from which to get the dimensions. - /// The dimensions if it was specified in the service's attributes; otherwise, null. - public static int? GetDimensions(this IAIService service) - { - Verify.NotNull(service); - return service.Attributes?.TryGetValue(DimensionsKey, out object? value) == true ? value as int? : null; - } - /// /// Gets the specified attribute. /// From 62a9b3b80734fcbf2ec6b36a392219ef365fb2d3 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sat, 15 Mar 2025 11:04:21 +0000 Subject: [PATCH 3/4] Adjustments and readiness --- dotnet/Directory.Packages.props | 7 +- dotnet/samples/Concepts/Concepts.csproj | 1 + .../Memory/Google_EmbeddingGeneration.cs | 65 ++++++++++- .../Google/EmbeddingGenerationTests.cs | 11 +- .../Gemini/GeminiChatCompletionTests.cs | 10 +- .../Connectors/Google/TestsBase.cs | 104 +++++++++++------- .../InternalUtilities/TestConfiguration.cs | 2 + 7 files changed, 145 insertions(+), 55 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 6d7aa20e179b..60f9563f38df 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -24,6 +24,7 @@ + @@ -142,9 +143,9 @@ - - - + + + diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 728dce6b41fb..976143fe87c5 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -15,6 +15,7 @@ + diff --git a/dotnet/samples/Concepts/Memory/Google_EmbeddingGeneration.cs b/dotnet/samples/Concepts/Memory/Google_EmbeddingGeneration.cs index aa13cb08889a..58b45be5b834 100644 --- a/dotnet/samples/Concepts/Memory/Google_EmbeddingGeneration.cs +++ b/dotnet/samples/Concepts/Memory/Google_EmbeddingGeneration.cs @@ -1,17 +1,76 @@ // Copyright (c) Microsoft. All rights reserved. +using Google.Apis.Auth.OAuth2; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Embeddings; using xRetry; namespace Memory; -// The following example shows how to use Semantic Kernel with Google AI for embedding generation, +// The following example shows how to use Semantic Kernel with Google AI and Google's Vertex AI for embedding generation, // including the ability to specify custom dimensions. public class Google_EmbeddingGeneration(ITestOutputHelper output) : BaseTest(output) { + /// + /// This test demonstrates how to use the Google Vertex AI embedding generation service with default dimensions. + /// + /// + /// Currently custom dimensions are not supported for Vertex AI. + /// [RetryFact(typeof(HttpOperationException))] - public async Task RunEmbeddingWithDefaultDimensionsAsync() + public async Task GenerateEmbeddingWithDefaultDimensionsUsingVertexAI() + { + string? bearerToken = null; + + Assert.NotNull(TestConfiguration.VertexAI.EmbeddingModelId); + Assert.NotNull(TestConfiguration.VertexAI.ClientId); + Assert.NotNull(TestConfiguration.VertexAI.ClientSecret); + Assert.NotNull(TestConfiguration.VertexAI.Location); + Assert.NotNull(TestConfiguration.VertexAI.ProjectId); + + IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); + kernelBuilder.AddVertexAIEmbeddingGeneration( + modelId: TestConfiguration.VertexAI.EmbeddingModelId!, + bearerTokenProvider: GetBearerToken, + location: TestConfiguration.VertexAI.Location, + projectId: TestConfiguration.VertexAI.ProjectId); + Kernel kernel = kernelBuilder.Build(); + + var embeddingGenerator = kernel.GetRequiredService(); + + // Generate embeddings with the default dimensions for the model + var embeddings = await embeddingGenerator.GenerateEmbeddingsAsync( + ["Semantic Kernel is a lightweight, open-source development kit that lets you easily build AI agents and integrate the latest AI models into your codebase."]); + + Console.WriteLine($"Generated '{embeddings.Count}' embedding(s) with '{embeddings[0].Length}' dimensions (default) for the provided text"); + + // Uses Google.Apis.Auth.OAuth2 to get the bearer token + async ValueTask GetBearerToken() + { + if (!string.IsNullOrEmpty(bearerToken)) + { + return bearerToken; + } + + var credential = GoogleWebAuthorizationBroker.AuthorizeAsync( + new ClientSecrets + { + ClientId = TestConfiguration.VertexAI.ClientId, + ClientSecret = TestConfiguration.VertexAI.ClientSecret + }, + ["https://www.googleapis.com/auth/cloud-platform"], + "user", + CancellationToken.None); + + var userCredential = await credential.WaitAsync(CancellationToken.None); + bearerToken = userCredential.Token.AccessToken; + + return bearerToken; + } + } + + [RetryFact(typeof(HttpOperationException))] + public async Task GenerateEmbeddingWithDefaultDimensionsUsingGoogleAI() { Assert.NotNull(TestConfiguration.GoogleAI.EmbeddingModelId); Assert.NotNull(TestConfiguration.GoogleAI.ApiKey); @@ -32,7 +91,7 @@ public async Task RunEmbeddingWithDefaultDimensionsAsync() } [RetryFact(typeof(HttpOperationException))] - public async Task RunEmbeddingWithCustomDimensionsAsync() + public async Task GenerateEmbeddingWithCustomDimensionsUsingGoogleAI() { Assert.NotNull(TestConfiguration.GoogleAI.EmbeddingModelId); Assert.NotNull(TestConfiguration.GoogleAI.ApiKey); diff --git a/dotnet/src/IntegrationTests/Connectors/Google/EmbeddingGenerationTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/EmbeddingGenerationTests.cs index 5b4f2d2f39a2..2dc0b79bf92c 100644 --- a/dotnet/src/IntegrationTests/Connectors/Google/EmbeddingGenerationTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Google/EmbeddingGenerationTests.cs @@ -12,9 +12,9 @@ public sealed class EmbeddingGenerationTests(ITestOutputHelper output) : TestsBa { private const string Input = "LLM is Large Language Model."; - [RetryTheory] - [InlineData(ServiceType.GoogleAI, Skip = "This test is for manual verification.")] - [InlineData(ServiceType.VertexAI, Skip = "This test is for manual verification.")] + [RetryTheory(Skip = "This test is for manual verification.")] + [InlineData(ServiceType.GoogleAI)] + [InlineData(ServiceType.VertexAI)] public async Task EmbeddingGenerationAsync(ServiceType serviceType) { // Arrange @@ -28,9 +28,8 @@ public async Task EmbeddingGenerationAsync(ServiceType serviceType) Assert.Equal(768, response.Length); } - [RetryTheory] - [InlineData(ServiceType.GoogleAI, Skip = "This test is for manual verification.")] - [InlineData(ServiceType.VertexAI, Skip = "Not implemented yet in VertexAI. This test is for manual verification.")] + [RetryTheory(Skip = "This test is for manual verification.")] + [InlineData(ServiceType.GoogleAI)] public async Task EmbeddingGenerationWithCustomDimensionsAsync(ServiceType serviceType) { // Arrange diff --git a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatCompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatCompletionTests.cs index 37a3439bb75b..6249b85c7a9c 100644 --- a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatCompletionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatCompletionTests.cs @@ -148,21 +148,21 @@ public async Task ChatGenerationWithCachedContentAsync(ServiceType serviceType) // Setup initial cached content var cachedContentJson = File.ReadAllText(Path.Combine("Resources", "gemini_cached_content.json")) - .Replace("{{project}}", this.VertexAIGetProjectId()) - .Replace("{{location}}", this.VertexAIGetLocation()) - .Replace("{{model}}", this.VertexAIGetGeminiModel()); + .Replace("{{project}}", this.VertexAI.ProjectId!) + .Replace("{{location}}", this.VertexAI.Location!) + .Replace("{{model}}", this.VertexAI.Gemini.ModelId!); var cachedContentName = string.Empty; using (var httpClient = new HttpClient() { - DefaultRequestHeaders = { Authorization = new("Bearer", this.VertexAIGetBearerKey()) } + DefaultRequestHeaders = { Authorization = new("Bearer", this.VertexAI.BearerKey) } }) { using (var content = new StringContent(cachedContentJson, Encoding.UTF8, "application/json")) { using (var httpResponse = await httpClient.PostAsync( - new Uri($"https://{this.VertexAIGetLocation()}-aiplatform.googleapis.com/v1beta1/projects/{this.VertexAIGetProjectId()}/locations/{this.VertexAIGetLocation()}/cachedContents"), + new Uri($"https://{this.VertexAI.Location}-aiplatform.googleapis.com/v1beta1/projects/{this.VertexAI.ProjectId!}/locations/{this.VertexAI.Location}/cachedContents"), content)) { httpResponse.EnsureSuccessStatusCode(); diff --git a/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs b/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs index afcf0f402052..da0d98838f77 100644 --- a/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs +++ b/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs @@ -9,28 +9,44 @@ namespace SemanticKernel.IntegrationTests.Connectors.Google; -public abstract class TestsBase(ITestOutputHelper output) +public abstract class TestsBase { - private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddUserSecrets() - .AddEnvironmentVariables() - .Build(); + private readonly IConfigurationRoot _configuration; + protected ITestOutputHelper Output { get; } + private readonly GoogleAIConfig _googleAI; + private readonly VertexAIConfig _vertexAI; - protected ITestOutputHelper Output { get; } = output; + protected GoogleAIConfig GoogleAI => this._googleAI; + protected VertexAIConfig VertexAI => this._vertexAI; + + protected TestsBase(ITestOutputHelper output) + { + this.Output = output; + this._configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddUserSecrets() + .AddEnvironmentVariables() + .Build(); + + this._googleAI = new GoogleAIConfig(); + this._vertexAI = new VertexAIConfig(); + + this._configuration.GetSection("GoogleAI").Bind(this._googleAI); + this._configuration.GetSection("VertexAI").Bind(this._vertexAI); + } protected IChatCompletionService GetChatService(ServiceType serviceType, bool isBeta = false) => serviceType switch { ServiceType.GoogleAI => new GoogleAIGeminiChatCompletionService( - this.GoogleAIGetGeminiModel(), - this.GoogleAIGetApiKey(), + this.GoogleAI.Gemini.ModelId, + this.GoogleAI.ApiKey, isBeta ? GoogleAIVersion.V1_Beta : GoogleAIVersion.V1), ServiceType.VertexAI => new VertexAIGeminiChatCompletionService( - modelId: this.VertexAIGetGeminiModel(), - bearerKey: this.VertexAIGetBearerKey(), - location: this.VertexAIGetLocation(), - projectId: this.VertexAIGetProjectId(), + modelId: this.VertexAI.Gemini.ModelId, + bearerKey: this.VertexAI.BearerKey, + location: this.VertexAI.Location, + projectId: this.VertexAI.ProjectId, isBeta ? VertexAIVersion.V1_Beta : VertexAIVersion.V1), _ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, null) }; @@ -38,34 +54,34 @@ public abstract class TestsBase(ITestOutputHelper output) protected IChatCompletionService GetChatServiceWithVision(ServiceType serviceType) => serviceType switch { ServiceType.GoogleAI => new GoogleAIGeminiChatCompletionService( - this.GoogleAIGetGeminiVisionModel(), - this.GoogleAIGetApiKey()), + this.GoogleAI.Gemini.VisionModelId, + this.GoogleAI.ApiKey), ServiceType.VertexAI => new VertexAIGeminiChatCompletionService( - modelId: this.VertexAIGetGeminiVisionModel(), - bearerKey: this.VertexAIGetBearerKey(), - location: this.VertexAIGetLocation(), - projectId: this.VertexAIGetProjectId()), + modelId: this.VertexAI.Gemini.VisionModelId, + bearerKey: this.VertexAI.BearerKey, + location: this.VertexAI.Location, + projectId: this.VertexAI.ProjectId), _ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, null) }; protected ITextEmbeddingGenerationService GetEmbeddingService(ServiceType serviceType) => serviceType switch { ServiceType.GoogleAI => new GoogleAITextEmbeddingGenerationService( - this.GoogleAIGetEmbeddingModel(), - this.GoogleAIGetApiKey()), + this.GoogleAI.EmbeddingModelId, + this.GoogleAI.ApiKey), ServiceType.VertexAI => new VertexAITextEmbeddingGenerationService( - modelId: this.VertexAIGetEmbeddingModel(), - bearerKey: this.VertexAIGetBearerKey(), - location: this.VertexAIGetLocation(), - projectId: this.VertexAIGetProjectId()), + modelId: this.VertexAI.EmbeddingModelId, + bearerKey: this.VertexAI.BearerKey, + location: this.VertexAI.Location, + projectId: this.VertexAI.ProjectId), _ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, null) }; protected ITextEmbeddingGenerationService GetEmbeddingServiceWithDimensions(ServiceType serviceType, int dimensions) => serviceType switch { ServiceType.GoogleAI => new GoogleAITextEmbeddingGenerationService( - this.GoogleAIGetEmbeddingModel(), - this.GoogleAIGetApiKey(), + this.GoogleAI.EmbeddingModelId, + this.GoogleAI.ApiKey, dimensions: dimensions), ServiceType.VertexAI => throw new NotImplementedException("Semantic Kernel does not support configuring dimensions for Vertex AI embeddings"), _ => throw new ArgumentException($"Invalid service type: {serviceType}", nameof(serviceType)) @@ -77,14 +93,26 @@ public enum ServiceType VertexAI } - private string GoogleAIGetGeminiModel() => this._configuration.GetSection("GoogleAI:Gemini:ModelId").Get()!; - private string GoogleAIGetGeminiVisionModel() => this._configuration.GetSection("GoogleAI:Gemini:VisionModelId").Get()!; - private string GoogleAIGetEmbeddingModel() => this._configuration.GetSection("GoogleAI:EmbeddingModelId").Get()!; - private string GoogleAIGetApiKey() => this._configuration.GetSection("GoogleAI:ApiKey").Get()!; - internal string VertexAIGetGeminiModel() => this._configuration.GetSection("VertexAI:Gemini:ModelId").Get()!; - private string VertexAIGetGeminiVisionModel() => this._configuration.GetSection("VertexAI:Gemini:VisionModelId").Get()!; - private string VertexAIGetEmbeddingModel() => this._configuration.GetSection("VertexAI:EmbeddingModelId").Get()!; - internal string VertexAIGetBearerKey() => this._configuration.GetSection("VertexAI:BearerKey").Get()!; - internal string VertexAIGetLocation() => this._configuration.GetSection("VertexAI:Location").Get()!; - internal string VertexAIGetProjectId() => this._configuration.GetSection("VertexAI:ProjectId").Get()!; + protected sealed class VertexAIConfig + { + public string ModelId { get; set; } = null!; + public string BearerKey { get; set; } = null!; + public string Location { get; set; } = null!; + public string ProjectId { get; set; } = null!; + public string EmbeddingModelId { get; set; } = null!; + public GeminiConfig Gemini { get; set; } = new(); + } + + protected sealed class GoogleAIConfig + { + public string ApiKey { get; set; } = null!; + public string EmbeddingModelId { get; set; } = null!; + public GeminiConfig Gemini { get; set; } = new(); + } + + protected class GeminiConfig + { + public string ModelId { get; set; } = null!; + public string VisionModelId { get; set; } = null!; + } } diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs index e45f52216a14..fe6066a639b5 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs @@ -244,6 +244,8 @@ public class VertexAIConfig public string EmbeddingModelId { get; set; } public string Location { get; set; } public string ProjectId { get; set; } + public string? ClientId { get; set; } + public string? ClientSecret { get; set; } public GeminiConfig Gemini { get; set; } public class GeminiConfig From 84e91aca480281c3e677fe6182e006f6d9214b8f Mon Sep 17 00:00:00 2001 From: Lev Skuditsky Date: Tue, 18 Mar 2025 14:11:22 +0200 Subject: [PATCH 4/4] .Net: Fix JSON property name for output dimensionality in Google AI embedding requests --- .../Core/GoogleAI/GoogleAIEmbeddingRequestTests.cs | 2 +- .../Services/GoogleAITextEmbeddingGenerationServiceTests.cs | 4 ++-- .../Core/GoogleAI/GoogleAIEmbeddingRequest.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIEmbeddingRequestTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIEmbeddingRequestTests.cs index f96fd6da5ca2..731e20dda585 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIEmbeddingRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIEmbeddingRequestTests.cs @@ -11,7 +11,7 @@ public sealed class GoogleAIEmbeddingRequestTests // Arrange private static readonly string[] s_data = ["text1", "text2"]; private const string ModelId = "modelId"; - private const string DimensionalityJsonPropertyName = "\"output_dimensionality\""; + private const string DimensionalityJsonPropertyName = "\"outputDimensionality\""; private const int Dimensions = 512; [Fact] diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAITextEmbeddingGenerationServiceTests.cs index 6b9e30373a70..8611036f5571 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAITextEmbeddingGenerationServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAITextEmbeddingGenerationServiceTests.cs @@ -118,7 +118,7 @@ public async Task ShouldNotIncludeDimensionsInRequestWhenNotProvidedAsync() // Assert Assert.NotNull(this._messageHandlerStub.RequestContent); var requestBody = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent); - Assert.DoesNotContain("output_dimensionality", requestBody); + Assert.DoesNotContain("outputDimensionality", requestBody); } [Theory] @@ -140,7 +140,7 @@ public async Task ShouldIncludeDimensionsInRequestWhenProvidedAsync(int? dimensi // Assert Assert.NotNull(this._messageHandlerStub.RequestContent); var requestBody = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent); - Assert.Contains($"\"output_dimensionality\":{dimensions}", requestBody); + Assert.Contains($"\"outputDimensionality\":{dimensions}", requestBody); } public void Dispose() diff --git a/dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingRequest.cs b/dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingRequest.cs index b4d9cea41f43..d69953dc5423 100644 --- a/dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingRequest.cs +++ b/dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingRequest.cs @@ -47,7 +47,7 @@ internal sealed class RequestEmbeddingContent [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? TaskType { get; set; } // todo: enum - [JsonPropertyName("output_dimensionality")] + [JsonPropertyName("outputDimensionality")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? Dimensions { get; set; } }