From 6063d8bf3d5979bf7a2de412ee7d94b25749b460 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 1 Nov 2024 09:38:10 -0400 Subject: [PATCH 1/2] Add UseEmbeddingGenerationOptions Counterpart to UseChatOptions --- .../ConfigureOptionsChatClient.cs | 4 +- ...igureOptionsChatClientBuilderExtensions.cs | 2 +- .../ConfigureOptionsEmbeddingGenerator.cs | 72 +++++++++++++++++++ ...ionsEmbeddingGeneratorBuilderExtensions.cs | 51 +++++++++++++ ...ConfigureOptionsEmbeddingGeneratorTests.cs | 56 +++++++++++++++ 5 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGenerator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/ConfigureOptionsEmbeddingGeneratorTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs index 895bf8873df..acc9867e993 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs @@ -39,7 +39,7 @@ namespace Microsoft.Extensions.AI; public sealed class ConfigureOptionsChatClient : DelegatingChatClient { /// The callback delegate used to configure options. - private readonly Func _configureOptions; + private readonly Func _configureOptions; /// Initializes a new instance of the class with the specified callback. /// The inner client. @@ -47,7 +47,7 @@ public sealed class ConfigureOptionsChatClient : DelegatingChatClient /// The delegate to invoke to configure the instance. It is passed the caller-supplied /// instance and should return the configured instance to use. /// - public ConfigureOptionsChatClient(IChatClient innerClient, Func configureOptions) + public ConfigureOptionsChatClient(IChatClient innerClient, Func configureOptions) : base(innerClient) { _configureOptions = Throw.IfNull(configureOptions); diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs index 12b903c0dac..827359f9d61 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs @@ -37,7 +37,7 @@ public static class ConfigureOptionsChatClientBuilderExtensions /// /// public static ChatClientBuilder UseChatOptions( - this ChatClientBuilder builder, Func configureOptions) + this ChatClientBuilder builder, Func configureOptions) { _ = Throw.IfNull(builder); _ = Throw.IfNull(configureOptions); diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGenerator.cs new file mode 100644 index 00000000000..5ed2df41868 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGenerator.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable SA1629 // Documentation text should end with a period + +namespace Microsoft.Extensions.AI; + +/// A delegating embedding generator that updates or replaces the used by the remainder of the pipeline. +/// Specifies the type of the input passed to the generator. +/// Specifies the type of the embedding instance produced by the generator. +/// +/// +/// The configuration callback is invoked with the caller-supplied instance. To override the caller-supplied options +/// with a new instance, the callback may simply return that new instance, for example _ => new EmbeddingGenerationOptions() { Dimensions = 100 }. To provide +/// a new instance only if the caller-supplied instance is `null`, the callback may conditionally return a new instance, for example +/// options => options ?? new EmbeddingGenerationOptions() { Dimensions = 100 }. Any changes to the caller-provided options instance will persist on the +/// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance +/// and mutating the clone, for example: +/// +/// options => +/// { +/// var newOptions = options?.Clone() ?? new(); +/// newOptions.Dimensions = 100; +/// return newOptions; +/// } +/// +/// +/// +/// The provided implementation of is thread-safe for concurrent use so long as the employed configuration +/// callback is also thread-safe for concurrent requests. If callers employ a shared options instance, care should be taken in the +/// configuration callback, as multiple calls to it may end up running in parallel with the same options instance. +/// +/// +public sealed class ConfigureOptionsEmbeddingGenerator : DelegatingEmbeddingGenerator + where TEmbedding : Embedding +{ + /// The callback delegate used to configure options. + private readonly Func _configureOptions; + + /// + /// Initializes a new instance of the class with the + /// specified callback. + /// + /// The inner generator. + /// + /// The delegate to invoke to configure the instance. It is passed the caller-supplied + /// instance and should return the configured instance to use. + /// + public ConfigureOptionsEmbeddingGenerator( + IEmbeddingGenerator innerGenerator, + Func configureOptions) + : base(innerGenerator) + { + _configureOptions = Throw.IfNull(configureOptions); + } + + /// + public override async Task> GenerateAsync( + IEnumerable values, + EmbeddingGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + return await base.GenerateAsync(values, _configureOptions(options), cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs new file mode 100644 index 00000000000..99867f3009c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable SA1629 // Documentation text should end with a period + +namespace Microsoft.Extensions.AI; + +/// Provides extensions for configuring instances. +public static class ConfigureOptionsEmbeddingGeneratorBuilderExtensions +{ + /// + /// Adds a callback that updates or replaces . This can be used to set default options. + /// + /// Specifies the type of the input passed to the generator. + /// Specifies the type of the embedding instance produced by the generator. + /// The . + /// + /// The delegate to invoke to configure the instance. It is passed the caller-supplied + /// instance and should return the configured instance to use. + /// + /// The . + /// + /// The configuration callback is invoked with the caller-supplied instance. To override the caller-supplied options + /// with a new instance, the callback may simply return that new instance, for example _ => new EmbeddingGenerationOptions() { Dimensions = 100 }. To provide + /// a new instance only if the caller-supplied instance is `null`, the callback may conditionally return a new instance, for example + /// options => options ?? new EmbeddingGenerationOptions() { Dimensions = 100 }. Any changes to the caller-provided options instance will persist on the + /// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance + /// and mutating the clone, for example: + /// + /// options => + /// { + /// var newOptions = options?.Clone() ?? new(); + /// newOptions.Dimensions = 100; + /// return newOptions; + /// } + /// + /// + public static EmbeddingGeneratorBuilder UseEmbeddingGenerationOptions( + this EmbeddingGeneratorBuilder builder, + Func configureOptions) + where TEmbedding : Embedding + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configureOptions); + + return builder.Use(innerGenerator => new ConfigureOptionsEmbeddingGenerator(innerGenerator, configureOptions)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/ConfigureOptionsEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/ConfigureOptionsEmbeddingGeneratorTests.cs new file mode 100644 index 00000000000..4353cf42559 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/ConfigureOptionsEmbeddingGeneratorTests.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ConfigureOptionsEmbeddingGeneratorTests +{ + [Fact] + public void ConfigureOptionsEmbeddingGenerator_InvalidArgs_Throws() + { + Assert.Throws("innerGenerator", () => new ConfigureOptionsEmbeddingGenerator>(null!, _ => new EmbeddingGenerationOptions())); + Assert.Throws("configureOptions", () => new ConfigureOptionsEmbeddingGenerator>(new TestEmbeddingGenerator(), null!)); + } + + [Fact] + public void UseEmbeddingGenerationOptions_InvalidArgs_Throws() + { + var builder = new EmbeddingGeneratorBuilder>(); + Assert.Throws("configureOptions", () => builder.UseEmbeddingGenerationOptions(null!)); + } + + [Fact] + public async Task ConfigureOptions_ReturnedInstancePassedToNextClient() + { + EmbeddingGenerationOptions providedOptions = new(); + EmbeddingGenerationOptions returnedOptions = new(); + GeneratedEmbeddings> expectedEmbeddings = []; + using CancellationTokenSource cts = new(); + + using IEmbeddingGenerator> innerGenerator = new TestEmbeddingGenerator + { + GenerateAsyncCallback = (inputs, options, cancellationToken) => + { + Assert.Same(returnedOptions, options); + Assert.Equal(cts.Token, cancellationToken); + return Task.FromResult(expectedEmbeddings); + } + }; + + using var generator = new EmbeddingGeneratorBuilder>() + .UseEmbeddingGenerationOptions(options => + { + Assert.Same(providedOptions, options); + return returnedOptions; + }) + .Use(innerGenerator); + + var embeddings = await generator.GenerateAsync([], providedOptions, cts.Token); + Assert.Same(expectedEmbeddings, embeddings); + } +} From db4beb82d23b6368616a2b50c6bbbeb1f065497e Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 1 Nov 2024 09:53:11 -0400 Subject: [PATCH 2/2] Document/test null options returned from callback --- .../ChatCompletion/ConfigureOptionsChatClient.cs | 5 ++++- .../ConfigureOptionsChatClientBuilderExtensions.cs | 7 ++++++- .../Embeddings/ConfigureOptionsEmbeddingGenerator.cs | 5 ++++- ...ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs | 7 ++++++- .../ChatCompletion/ConfigureOptionsChatClientTests.cs | 8 +++++--- .../Embeddings/ConfigureOptionsEmbeddingGeneratorTests.cs | 8 +++++--- 6 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs index acc9867e993..990c92d3ad9 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs @@ -17,7 +17,7 @@ namespace Microsoft.Extensions.AI; /// /// The configuration callback is invoked with the caller-supplied instance. To override the caller-supplied options /// with a new instance, the callback may simply return that new instance, for example _ => new ChatOptions() { MaxTokens = 1000 }. To provide -/// a new instance only if the caller-supplied instance is `null`, the callback may conditionally return a new instance, for example +/// a new instance only if the caller-supplied instance is , the callback may conditionally return a new instance, for example /// options => options ?? new ChatOptions() { MaxTokens = 1000 }. Any changes to the caller-provided options instance will persist on the /// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance /// and mutating the clone, for example: @@ -31,6 +31,9 @@ namespace Microsoft.Extensions.AI; /// /// /// +/// The callback may return , in which case a options will be passed to the next client in the pipeline. +/// +/// /// The provided implementation of is thread-safe for concurrent use so long as the employed configuration /// callback is also thread-safe for concurrent requests. If callers employ a shared options instance, care should be taken in the /// configuration callback, as multiple calls to it may end up running in parallel with the same options instance. diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs index 827359f9d61..2d98fbd9003 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs @@ -21,9 +21,10 @@ public static class ConfigureOptionsChatClientBuilderExtensions /// /// The . /// + /// /// The configuration callback is invoked with the caller-supplied instance. To override the caller-supplied options /// with a new instance, the callback may simply return that new instance, for example _ => new ChatOptions() { MaxTokens = 1000 }. To provide - /// a new instance only if the caller-supplied instance is `null`, the callback may conditionally return a new instance, for example + /// a new instance only if the caller-supplied instance is , the callback may conditionally return a new instance, for example /// options => options ?? new ChatOptions() { MaxTokens = 1000 }. Any changes to the caller-provided options instance will persist on the /// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance /// and mutating the clone, for example: @@ -35,6 +36,10 @@ public static class ConfigureOptionsChatClientBuilderExtensions /// return newOptions; /// } /// + /// + /// + /// The callback may return , in which case a options will be passed to the next client in the pipeline. + /// /// public static ChatClientBuilder UseChatOptions( this ChatClientBuilder builder, Func configureOptions) diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGenerator.cs index 5ed2df41868..9068ac41caa 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGenerator.cs @@ -19,7 +19,7 @@ namespace Microsoft.Extensions.AI; /// /// The configuration callback is invoked with the caller-supplied instance. To override the caller-supplied options /// with a new instance, the callback may simply return that new instance, for example _ => new EmbeddingGenerationOptions() { Dimensions = 100 }. To provide -/// a new instance only if the caller-supplied instance is `null`, the callback may conditionally return a new instance, for example +/// a new instance only if the caller-supplied instance is , the callback may conditionally return a new instance, for example /// options => options ?? new EmbeddingGenerationOptions() { Dimensions = 100 }. Any changes to the caller-provided options instance will persist on the /// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance /// and mutating the clone, for example: @@ -33,6 +33,9 @@ namespace Microsoft.Extensions.AI; /// /// /// +/// The callback may return , in which case a options will be passed to the next generator in the pipeline. +/// +/// /// The provided implementation of is thread-safe for concurrent use so long as the employed configuration /// callback is also thread-safe for concurrent requests. If callers employ a shared options instance, care should be taken in the /// configuration callback, as multiple calls to it may end up running in parallel with the same options instance. diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs index 99867f3009c..011f4c058e9 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs @@ -23,9 +23,10 @@ public static class ConfigureOptionsEmbeddingGeneratorBuilderExtensions /// /// The . /// + /// /// The configuration callback is invoked with the caller-supplied instance. To override the caller-supplied options /// with a new instance, the callback may simply return that new instance, for example _ => new EmbeddingGenerationOptions() { Dimensions = 100 }. To provide - /// a new instance only if the caller-supplied instance is `null`, the callback may conditionally return a new instance, for example + /// a new instance only if the caller-supplied instance is , the callback may conditionally return a new instance, for example /// options => options ?? new EmbeddingGenerationOptions() { Dimensions = 100 }. Any changes to the caller-provided options instance will persist on the /// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance /// and mutating the clone, for example: @@ -37,6 +38,10 @@ public static class ConfigureOptionsEmbeddingGeneratorBuilderExtensions /// return newOptions; /// } /// + /// + /// + /// The callback may return , in which case a options will be passed to the next generator in the pipeline. + /// /// public static EmbeddingGeneratorBuilder UseEmbeddingGenerationOptions( this EmbeddingGeneratorBuilder builder, diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ConfigureOptionsChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ConfigureOptionsChatClientTests.cs index a27761c99ec..a911340813f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ConfigureOptionsChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ConfigureOptionsChatClientTests.cs @@ -26,11 +26,13 @@ public void UseChatOptions_InvalidArgs_Throws() Assert.Throws("configureOptions", () => builder.UseChatOptions(null!)); } - [Fact] - public async Task ConfigureOptions_ReturnedInstancePassedToNextClient() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullReturned) { ChatOptions providedOptions = new(); - ChatOptions returnedOptions = new(); + ChatOptions? returnedOptions = nullReturned ? null : new(); ChatCompletion expectedCompletion = new(Array.Empty()); var expectedUpdates = Enumerable.Range(0, 3).Select(i => new StreamingChatCompletionUpdate()).ToArray(); using CancellationTokenSource cts = new(); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/ConfigureOptionsEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/ConfigureOptionsEmbeddingGeneratorTests.cs index 4353cf42559..b8a4b82cb59 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/ConfigureOptionsEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/ConfigureOptionsEmbeddingGeneratorTests.cs @@ -24,11 +24,13 @@ public void UseEmbeddingGenerationOptions_InvalidArgs_Throws() Assert.Throws("configureOptions", () => builder.UseEmbeddingGenerationOptions(null!)); } - [Fact] - public async Task ConfigureOptions_ReturnedInstancePassedToNextClient() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullReturned) { EmbeddingGenerationOptions providedOptions = new(); - EmbeddingGenerationOptions returnedOptions = new(); + EmbeddingGenerationOptions? returnedOptions = nullReturned ? null : new(); GeneratedEmbeddings> expectedEmbeddings = []; using CancellationTokenSource cts = new();