Skip to content

Commit 4f775a0

Browse files
committed
Rework UseChatOptions as ConfigureOptions (dotnet#5606)
* Update README to include a section on UseChatOptions * Rework UseChat/EmbeddingGenerationOptions to always clone The callbacks now configure the supplied instance.
1 parent f9edff2 commit 4f775a0

File tree

8 files changed

+131
-155
lines changed

8 files changed

+131
-155
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,22 @@ IChatClient client = new ChatClientBuilder()
212212
Console.WriteLine((await client.CompleteAsync("What is AI?")).Message);
213213
```
214214

215+
#### Options
216+
217+
Every call to `CompleteAsync` or `CompleteStreamingAsync` may optionally supply a `ChatOptions` instance containing additional parameters for the operation. The most common parameters that are common amongst AI models and services show up as strongly-typed properties on the type, such as `ChatOptions.Temperature`. Other parameters may be supplied by name in a weakly-typed manner via the `ChatOptions.AdditionalProperties` dictionary.
218+
219+
Options may also be baked into an `IChatClient` via the `ConfigureOptions` extension method on `ChatClientBuilder`. This delegating client wraps another client and invokes the supplied delegate to populate a `ChatOptions` instance for every call. For example, to ensure that the `ChatOptions.ModelId` property defaults to a particular model name, code like the following may be used:
220+
```csharp
221+
using Microsoft.Extensions.AI;
222+
223+
IChatClient client = new ChatClientBuilder()
224+
.ConfigureOptions(options => options.ModelId ??= "phi3")
225+
.Use(new OllamaChatClient(new Uri("http://localhost:11434")));
226+
227+
Console.WriteLine(await client.CompleteAsync("What is AI?")); // will request "phi3"
228+
Console.WriteLine(await client.CompleteAsync("What is AI?", new() { ModelId = "llama3.1" })); // will request "llama3.1"
229+
```
230+
215231
#### Pipelines of Functionality
216232

217233
All of these `IChatClient`s may be layered, creating a pipeline of any number of components that all add additional functionality. Such components may come from `Microsoft.Extensions.AI`, may come from other NuGet packages, or may be your own custom implementations that augment the behavior in whatever ways you need.

src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClient.cs

Lines changed: 25 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,67 +8,54 @@
88
using System.Threading.Tasks;
99
using Microsoft.Shared.Diagnostics;
1010

11-
#pragma warning disable SA1629 // Documentation text should end with a period
12-
1311
namespace Microsoft.Extensions.AI;
1412

15-
/// <summary>A delegating chat client that updates or replaces the <see cref="ChatOptions"/> used by the remainder of the pipeline.</summary>
16-
/// <remarks>
17-
/// <para>
18-
/// The configuration callback is invoked with the caller-supplied <see cref="ChatOptions"/> instance. To override the caller-supplied options
19-
/// with a new instance, the callback may simply return that new instance, for example <c>_ => new ChatOptions() { MaxTokens = 1000 }</c>. To provide
20-
/// a new instance only if the caller-supplied instance is <see langword="null"/>, the callback may conditionally return a new instance, for example
21-
/// <c>options => options ?? new ChatOptions() { MaxTokens = 1000 }</c>. Any changes to the caller-provided options instance will persist on the
22-
/// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance
23-
/// and mutating the clone, for example:
24-
/// <c>
25-
/// options =>
26-
/// {
27-
/// var newOptions = options?.Clone() ?? new();
28-
/// newOptions.MaxTokens = 1000;
29-
/// return newOptions;
30-
/// }
31-
/// </c>
32-
/// </para>
33-
/// <para>
34-
/// The callback may return <see langword="null"/>, in which case a <see langword="null"/> options will be passed to the next client in the pipeline.
35-
/// </para>
36-
/// <para>
37-
/// The provided implementation of <see cref="IChatClient"/> is thread-safe for concurrent use so long as the employed configuration
38-
/// callback is also thread-safe for concurrent requests. If callers employ a shared options instance, care should be taken in the
39-
/// configuration callback, as multiple calls to it may end up running in parallel with the same options instance.
40-
/// </para>
41-
/// </remarks>
13+
/// <summary>A delegating chat client that configures a <see cref="ChatOptions"/> instance used by the remainder of the pipeline.</summary>
4214
public sealed class ConfigureOptionsChatClient : DelegatingChatClient
4315
{
4416
/// <summary>The callback delegate used to configure options.</summary>
45-
private readonly Func<ChatOptions?, ChatOptions?> _configureOptions;
17+
private readonly Action<ChatOptions> _configureOptions;
4618

47-
/// <summary>Initializes a new instance of the <see cref="ConfigureOptionsChatClient"/> class with the specified <paramref name="configureOptions"/> callback.</summary>
19+
/// <summary>Initializes a new instance of the <see cref="ConfigureOptionsChatClient"/> class with the specified <paramref name="configure"/> callback.</summary>
4820
/// <param name="innerClient">The inner client.</param>
49-
/// <param name="configureOptions">
50-
/// The delegate to invoke to configure the <see cref="ChatOptions"/> instance. It is passed the caller-supplied <see cref="ChatOptions"/>
51-
/// instance and should return the configured <see cref="ChatOptions"/> instance to use.
21+
/// <param name="configure">
22+
/// The delegate to invoke to configure the <see cref="ChatOptions"/> instance. It is passed a clone of the caller-supplied <see cref="ChatOptions"/> instance
23+
/// (or a newly-constructed instance if the caller-supplied instance is <see langword="null"/>).
5224
/// </param>
53-
public ConfigureOptionsChatClient(IChatClient innerClient, Func<ChatOptions?, ChatOptions?> configureOptions)
25+
/// <remarks>
26+
/// The <paramref name="configure"/> delegate is passed either a new instance of <see cref="ChatOptions"/> if
27+
/// the caller didn't supply a <see cref="ChatOptions"/> instance, or a clone (via <see cref="ChatOptions.Clone"/> of the caller-supplied
28+
/// instance if one was supplied.
29+
/// </remarks>
30+
public ConfigureOptionsChatClient(IChatClient innerClient, Action<ChatOptions> configure)
5431
: base(innerClient)
5532
{
56-
_configureOptions = Throw.IfNull(configureOptions);
33+
_configureOptions = Throw.IfNull(configure);
5734
}
5835

5936
/// <inheritdoc/>
6037
public override async Task<ChatCompletion> CompleteAsync(IList<ChatMessage> chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default)
6138
{
62-
return await base.CompleteAsync(chatMessages, _configureOptions(options), cancellationToken).ConfigureAwait(false);
39+
return await base.CompleteAsync(chatMessages, Configure(options), cancellationToken).ConfigureAwait(false);
6340
}
6441

6542
/// <inheritdoc/>
6643
public override async IAsyncEnumerable<StreamingChatCompletionUpdate> CompleteStreamingAsync(
6744
IList<ChatMessage> chatMessages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
6845
{
69-
await foreach (var update in base.CompleteStreamingAsync(chatMessages, _configureOptions(options), cancellationToken).ConfigureAwait(false))
46+
await foreach (var update in base.CompleteStreamingAsync(chatMessages, Configure(options), cancellationToken).ConfigureAwait(false))
7047
{
7148
yield return update;
7249
}
7350
}
51+
52+
/// <summary>Creates and configures the <see cref="ChatOptions"/> to pass along to the inner client.</summary>
53+
private ChatOptions Configure(ChatOptions? options)
54+
{
55+
options = options?.Clone() ?? new();
56+
57+
_configureOptions(options);
58+
59+
return options;
60+
}
7461
}

src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ConfigureOptionsChatClientBuilderExtensions.cs

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,25 @@ namespace Microsoft.Extensions.AI;
1212
public static class ConfigureOptionsChatClientBuilderExtensions
1313
{
1414
/// <summary>
15-
/// Adds a callback that updates or replaces <see cref="ChatOptions"/>. This can be used to set default options.
15+
/// Adds a callback that configures a <see cref="ChatOptions"/> to be passed to the next client in the pipeline.
1616
/// </summary>
1717
/// <param name="builder">The <see cref="ChatClientBuilder"/>.</param>
18-
/// <param name="configureOptions">
19-
/// The delegate to invoke to configure the <see cref="ChatOptions"/> instance. It is passed the caller-supplied <see cref="ChatOptions"/>
20-
/// instance and should return the configured <see cref="ChatOptions"/> instance to use.
18+
/// <param name="configure">
19+
/// The delegate to invoke to configure the <see cref="ChatOptions"/> instance.
20+
/// It is passed a clone of the caller-supplied <see cref="ChatOptions"/> instance (or a newly-constructed instance if the caller-supplied instance is <see langword="null"/>).
2121
/// </param>
22-
/// <returns>The <paramref name="builder"/>.</returns>
2322
/// <remarks>
24-
/// <para>
25-
/// The configuration callback is invoked with the caller-supplied <see cref="ChatOptions"/> instance. To override the caller-supplied options
26-
/// with a new instance, the callback may simply return that new instance, for example <c>_ => new ChatOptions() { MaxTokens = 1000 }</c>. To provide
27-
/// a new instance only if the caller-supplied instance is <see langword="null"/>, the callback may conditionally return a new instance, for example
28-
/// <c>options => options ?? new ChatOptions() { MaxTokens = 1000 }</c>. Any changes to the caller-provided options instance will persist on the
29-
/// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance
30-
/// and mutating the clone, for example:
31-
/// <c>
32-
/// options =>
33-
/// {
34-
/// var newOptions = options?.Clone() ?? new();
35-
/// newOptions.MaxTokens = 1000;
36-
/// return newOptions;
37-
/// }
38-
/// </c>
39-
/// </para>
40-
/// <para>
41-
/// The callback may return <see langword="null"/>, in which case a <see langword="null"/> options will be passed to the next client in the pipeline.
42-
/// </para>
23+
/// This can be used to set default options. The <paramref name="configure"/> delegate is passed either a new instance of
24+
/// <see cref="ChatOptions"/> if the caller didn't supply a <see cref="ChatOptions"/> instance, or a clone (via <see cref="ChatOptions.Clone"/>
25+
/// of the caller-supplied instance if one was supplied.
4326
/// </remarks>
44-
public static ChatClientBuilder UseChatOptions(
45-
this ChatClientBuilder builder, Func<ChatOptions?, ChatOptions?> configureOptions)
27+
/// <returns>The <paramref name="builder"/>.</returns>
28+
public static ChatClientBuilder ConfigureOptions(
29+
this ChatClientBuilder builder, Action<ChatOptions> configure)
4630
{
4731
_ = Throw.IfNull(builder);
48-
_ = Throw.IfNull(configureOptions);
32+
_ = Throw.IfNull(configure);
4933

50-
return builder.Use(innerClient => new ConfigureOptionsChatClient(innerClient, configureOptions));
34+
return builder.Use(innerClient => new ConfigureOptionsChatClient(innerClient, configure));
5135
}
5236
}

src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGenerator.cs

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,65 +3,41 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Runtime.CompilerServices;
76
using System.Threading;
87
using System.Threading.Tasks;
98
using Microsoft.Shared.Diagnostics;
109

11-
#pragma warning disable SA1629 // Documentation text should end with a period
12-
1310
namespace Microsoft.Extensions.AI;
1411

15-
/// <summary>A delegating embedding generator that updates or replaces the <see cref="EmbeddingGenerationOptions"/> used by the remainder of the pipeline.</summary>
12+
/// <summary>A delegating embedding generator that configures a <see cref="EmbeddingGenerationOptions"/> instance used by the remainder of the pipeline.</summary>
1613
/// <typeparam name="TInput">Specifies the type of the input passed to the generator.</typeparam>
1714
/// <typeparam name="TEmbedding">Specifies the type of the embedding instance produced by the generator.</typeparam>
18-
/// <remarks>
19-
/// <para>
20-
/// The configuration callback is invoked with the caller-supplied <see cref="EmbeddingGenerationOptions"/> instance. To override the caller-supplied options
21-
/// with a new instance, the callback may simply return that new instance, for example <c>_ => new EmbeddingGenerationOptions() { Dimensions = 100 }</c>. To provide
22-
/// a new instance only if the caller-supplied instance is <see langword="null"/>, the callback may conditionally return a new instance, for example
23-
/// <c>options => options ?? new EmbeddingGenerationOptions() { Dimensions = 100 }</c>. Any changes to the caller-provided options instance will persist on the
24-
/// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance
25-
/// and mutating the clone, for example:
26-
/// <c>
27-
/// options =>
28-
/// {
29-
/// var newOptions = options?.Clone() ?? new();
30-
/// newOptions.Dimensions = 100;
31-
/// return newOptions;
32-
/// }
33-
/// </c>
34-
/// </para>
35-
/// <para>
36-
/// The callback may return <see langword="null"/>, in which case a <see langword="null"/> options will be passed to the next generator in the pipeline.
37-
/// </para>
38-
/// <para>
39-
/// The provided implementation of <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> is thread-safe for concurrent use so long as the employed configuration
40-
/// callback is also thread-safe for concurrent requests. If callers employ a shared options instance, care should be taken in the
41-
/// configuration callback, as multiple calls to it may end up running in parallel with the same options instance.
42-
/// </para>
43-
/// </remarks>
4415
public sealed class ConfigureOptionsEmbeddingGenerator<TInput, TEmbedding> : DelegatingEmbeddingGenerator<TInput, TEmbedding>
4516
where TEmbedding : Embedding
4617
{
4718
/// <summary>The callback delegate used to configure options.</summary>
48-
private readonly Func<EmbeddingGenerationOptions?, EmbeddingGenerationOptions?> _configureOptions;
19+
private readonly Action<EmbeddingGenerationOptions> _configureOptions;
4920

5021
/// <summary>
5122
/// Initializes a new instance of the <see cref="ConfigureOptionsEmbeddingGenerator{TInput, TEmbedding}"/> class with the
52-
/// specified <paramref name="configureOptions"/> callback.
23+
/// specified <paramref name="configure"/> callback.
5324
/// </summary>
5425
/// <param name="innerGenerator">The inner generator.</param>
55-
/// <param name="configureOptions">
56-
/// The delegate to invoke to configure the <see cref="EmbeddingGenerationOptions"/> instance. It is passed the caller-supplied
57-
/// <see cref="EmbeddingGenerationOptions"/> instance and should return the configured <see cref="EmbeddingGenerationOptions"/> instance to use.
26+
/// <param name="configure">
27+
/// The delegate to invoke to configure the <see cref="EmbeddingGenerationOptions"/> instance. It is passed a clone of the caller-supplied
28+
/// <see cref="EmbeddingGenerationOptions"/> instance (or a newly-constructed instance if the caller-supplied instance is <see langword="null"/>).
5829
/// </param>
30+
/// <remarks>
31+
/// The <paramref name="configure"/> delegate is passed either a new instance of <see cref="EmbeddingGenerationOptions"/> if
32+
/// the caller didn't supply a <see cref="EmbeddingGenerationOptions"/> instance, or a clone (via <see cref="EmbeddingGenerationOptions.Clone"/> of the caller-supplied
33+
/// instance if one was supplied.
34+
/// </remarks>
5935
public ConfigureOptionsEmbeddingGenerator(
6036
IEmbeddingGenerator<TInput, TEmbedding> innerGenerator,
61-
Func<EmbeddingGenerationOptions?, EmbeddingGenerationOptions?> configureOptions)
37+
Action<EmbeddingGenerationOptions> configure)
6238
: base(innerGenerator)
6339
{
64-
_configureOptions = Throw.IfNull(configureOptions);
40+
_configureOptions = Throw.IfNull(configure);
6541
}
6642

6743
/// <inheritdoc/>
@@ -70,6 +46,16 @@ public override async Task<GeneratedEmbeddings<TEmbedding>> GenerateAsync(
7046
EmbeddingGenerationOptions? options = null,
7147
CancellationToken cancellationToken = default)
7248
{
73-
return await base.GenerateAsync(values, _configureOptions(options), cancellationToken).ConfigureAwait(false);
49+
return await base.GenerateAsync(values, Configure(options), cancellationToken).ConfigureAwait(false);
50+
}
51+
52+
/// <summary>Creates and configures the <see cref="EmbeddingGenerationOptions"/> to pass along to the inner client.</summary>
53+
private EmbeddingGenerationOptions Configure(EmbeddingGenerationOptions? options)
54+
{
55+
options = options?.Clone() ?? new();
56+
57+
_configureOptions(options);
58+
59+
return options;
7460
}
7561
}

src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,45 +12,30 @@ namespace Microsoft.Extensions.AI;
1212
public static class ConfigureOptionsEmbeddingGeneratorBuilderExtensions
1313
{
1414
/// <summary>
15-
/// Adds a callback that updates or replaces <see cref="EmbeddingGenerationOptions"/>. This can be used to set default options.
15+
/// Adds a callback that configures a <see cref="EmbeddingGenerationOptions"/> to be passed to the next client in the pipeline.
1616
/// </summary>
1717
/// <typeparam name="TInput">Specifies the type of the input passed to the generator.</typeparam>
1818
/// <typeparam name="TEmbedding">Specifies the type of the embedding instance produced by the generator.</typeparam>
1919
/// <param name="builder">The <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/>.</param>
20-
/// <param name="configureOptions">
21-
/// The delegate to invoke to configure the <see cref="EmbeddingGenerationOptions"/> instance. It is passed the caller-supplied
22-
/// <see cref="EmbeddingGenerationOptions"/> instance and should return the configured <see cref="EmbeddingGenerationOptions"/> instance to use.
20+
/// <param name="configure">
21+
/// The delegate to invoke to configure the <see cref="EmbeddingGenerationOptions"/> instance. It is passed a clone of the caller-supplied
22+
/// <see cref="EmbeddingGenerationOptions"/> instance (or a newly-constructed instance if the caller-supplied instance is <see langword="null"/>).
2323
/// </param>
24-
/// <returns>The <paramref name="builder"/>.</returns>
2524
/// <remarks>
26-
/// <para>
27-
/// The configuration callback is invoked with the caller-supplied <see cref="EmbeddingGenerationOptions"/> instance. To override the caller-supplied options
28-
/// with a new instance, the callback may simply return that new instance, for example <c>_ => new EmbeddingGenerationOptions() { Dimensions = 100 }</c>. To provide
29-
/// a new instance only if the caller-supplied instance is <see langword="null"/>, the callback may conditionally return a new instance, for example
30-
/// <c>options => options ?? new EmbeddingGenerationOptions() { Dimensions = 100 }</c>. Any changes to the caller-provided options instance will persist on the
31-
/// original instance, so the callback must take care to only do so when such mutations are acceptable, such as by cloning the original instance
32-
/// and mutating the clone, for example:
33-
/// <c>
34-
/// options =>
35-
/// {
36-
/// var newOptions = options?.Clone() ?? new();
37-
/// newOptions.Dimensions = 100;
38-
/// return newOptions;
39-
/// }
40-
/// </c>
41-
/// </para>
42-
/// <para>
43-
/// The callback may return <see langword="null"/>, in which case a <see langword="null"/> options will be passed to the next generator in the pipeline.
44-
/// </para>
25+
/// This can be used to set default options. The <paramref name="configure"/> delegate is passed either a new instance of
26+
/// <see cref="EmbeddingGenerationOptions"/> if the caller didn't supply a <see cref="EmbeddingGenerationOptions"/> instance, or
27+
/// a clone (via <see cref="EmbeddingGenerationOptions.Clone"/>
28+
/// of the caller-supplied instance if one was supplied.
4529
/// </remarks>
46-
public static EmbeddingGeneratorBuilder<TInput, TEmbedding> UseEmbeddingGenerationOptions<TInput, TEmbedding>(
30+
/// <returns>The <paramref name="builder"/>.</returns>
31+
public static EmbeddingGeneratorBuilder<TInput, TEmbedding> ConfigureOptions<TInput, TEmbedding>(
4732
this EmbeddingGeneratorBuilder<TInput, TEmbedding> builder,
48-
Func<EmbeddingGenerationOptions?, EmbeddingGenerationOptions?> configureOptions)
33+
Action<EmbeddingGenerationOptions> configure)
4934
where TEmbedding : Embedding
5035
{
5136
_ = Throw.IfNull(builder);
52-
_ = Throw.IfNull(configureOptions);
37+
_ = Throw.IfNull(configure);
5338

54-
return builder.Use(innerGenerator => new ConfigureOptionsEmbeddingGenerator<TInput, TEmbedding>(innerGenerator, configureOptions));
39+
return builder.Use(innerGenerator => new ConfigureOptionsEmbeddingGenerator<TInput, TEmbedding>(innerGenerator, configure));
5540
}
5641
}

0 commit comments

Comments
 (0)