From 1f8ae147e50eb441a9af84b04623e7dac38b5dd6 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 14 Nov 2024 05:36:08 -0800 Subject: [PATCH] Change ChatClientBuilder to register singletons and support lambda-less chaining (#5642) * Change ChatClientBuilder to register singletons and support lambda-less chaining * Add generic keyed version * Improve XML doc * Update README files * Remove generic DI registration methods --- .../README.md | 25 +++-- .../README.md | 25 +++-- .../Microsoft.Extensions.AI.Ollama/README.md | 25 +++-- .../Microsoft.Extensions.AI.OpenAI/README.md | 25 +++-- .../ChatCompletion/ChatClientBuilder.cs | 33 ++++--- ...lientBuilderServiceCollectionExtensions.cs | 70 ++++++++----- .../AzureAIInferenceChatClientTests.cs | 4 +- .../ChatClientIntegrationTests.cs | 16 +-- .../ReducingChatClientTests.cs | 4 +- .../OllamaChatClientIntegrationTests.cs | 8 +- .../OllamaChatClientTests.cs | 4 +- .../OpenAIChatClientTests.cs | 8 +- .../ChatCompletion/ChatClientBuilderTest.cs | 31 +++--- .../ConfigureOptionsChatClientTests.cs | 7 +- .../DependencyInjectionPatterns.cs | 99 +++++++++++-------- .../DistributedCachingChatClientTest.cs | 4 +- .../FunctionInvokingChatClientTests.cs | 4 +- .../ChatCompletion/LoggingChatClientTests.cs | 8 +- .../OpenTelemetryChatClientTests.cs | 4 +- .../ScopedChatClientExtensions.cs | 11 --- .../SingletonChatClientExtensions.cs | 11 +++ 21 files changed, 239 insertions(+), 187 deletions(-) delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ScopedChatClientExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/SingletonChatClientExtensions.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md index 7e8b369d80b..f02a0eff4a6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md @@ -150,9 +150,9 @@ using Microsoft.Extensions.AI; [Description("Gets the current weather")] string GetCurrentWeather() => Random.Shared.NextDouble() > 0.5 ? "It's sunny" : "It's raining"; -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.1")) .UseFunctionInvocation() - .Use(new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.1")); + .Build(); var response = client.CompleteStreamingAsync( "Should I wear a rain coat?", @@ -174,9 +174,9 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(new SampleChatClient(new Uri("http://coolsite.ai"), "my-custom-model")) .UseDistributedCache(new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()))) - .Use(new SampleChatClient(new Uri("http://coolsite.ai"), "my-custom-model")); + .Build(); string[] prompts = ["What is AI?", "What is .NET?", "What is AI?"]; @@ -205,9 +205,9 @@ var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() .AddConsoleExporter() .Build(); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(new SampleChatClient(new Uri("http://coolsite.ai"), "my-custom-model")) .UseOpenTelemetry(sourceName, c => c.EnableSensitiveData = true) - .Use(new SampleChatClient(new Uri("http://coolsite.ai"), "my-custom-model")); + .Build(); Console.WriteLine((await client.CompleteAsync("What is AI?")).Message); ``` @@ -220,9 +220,9 @@ Options may also be baked into an `IChatClient` via the `ConfigureOptions` exten ```csharp using Microsoft.Extensions.AI; -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(new OllamaChatClient(new Uri("http://localhost:11434"))) .ConfigureOptions(options => options.ModelId ??= "phi3") - .Use(new OllamaChatClient(new Uri("http://localhost:11434"))); + .Build(); Console.WriteLine(await client.CompleteAsync("What is AI?")); // will request "phi3" Console.WriteLine(await client.CompleteAsync("What is AI?", new() { ModelId = "llama3.1" })); // will request "llama3.1" @@ -248,11 +248,11 @@ var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() // Explore changing the order of the intermediate "Use" calls to see that impact // that has on what gets cached, traced, etc. -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.1")) .UseDistributedCache(new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()))) .UseFunctionInvocation() .UseOpenTelemetry(sourceName, c => c.EnableSensitiveData = true) - .Use(new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.1")); + .Build(); ChatOptions options = new() { @@ -341,9 +341,8 @@ using Microsoft.Extensions.Hosting; // App Setup var builder = Host.CreateApplicationBuilder(); builder.Services.AddDistributedMemoryCache(); -builder.Services.AddChatClient(b => b - .UseDistributedCache() - .Use(new SampleChatClient(new Uri("http://coolsite.ai"), "my-custom-model"))); +builder.Services.AddChatClient(new SampleChatClient(new Uri("http://coolsite.ai"), "my-custom-model")) + .UseDistributedCache(); var host = builder.Build(); // Elsewhere in the app diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/README.md b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/README.md index f34e89a08fb..65396b80307 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/README.md @@ -85,9 +85,9 @@ IChatClient azureClient = new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!)) .AsChatClient("gpt-4o-mini"); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(azureClient) .UseFunctionInvocation() - .Use(azureClient); + .Build(); ChatOptions chatOptions = new() { @@ -120,9 +120,9 @@ IChatClient azureClient = new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!)) .AsChatClient("gpt-4o-mini"); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(azureClient) .UseDistributedCache(cache) - .Use(azureClient); + .Build(); for (int i = 0; i < 3; i++) { @@ -156,9 +156,9 @@ IChatClient azureClient = new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!)) .AsChatClient("gpt-4o-mini"); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(azureClient) .UseOpenTelemetry(sourceName, c => c.EnableSensitiveData = true) - .Use(azureClient); + .Build(); Console.WriteLine(await client.CompleteAsync("What is AI?")); ``` @@ -196,11 +196,11 @@ IChatClient azureClient = new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!)) .AsChatClient("gpt-4o-mini"); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(azureClient) .UseDistributedCache(cache) .UseFunctionInvocation() .UseOpenTelemetry(sourceName, c => c.EnableSensitiveData = true) - .Use(azureClient); + .Build(); for (int i = 0; i < 3; i++) { @@ -236,10 +236,9 @@ builder.Services.AddSingleton( builder.Services.AddDistributedMemoryCache(); builder.Services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Trace)); -builder.Services.AddChatClient(b => b +builder.Services.AddChatClient(services => services.GetRequiredService().AsChatClient("gpt-4o-mini")) .UseDistributedCache() - .UseLogging() - .Use(b.Services.GetRequiredService().AsChatClient("gpt-4o-mini"))); + .UseLogging(); var app = builder.Build(); @@ -261,8 +260,8 @@ builder.Services.AddSingleton(new ChatCompletionsClient( new("https://models.inference.ai.azure.com"), new AzureKeyCredential(builder.Configuration["GH_TOKEN"]!))); -builder.Services.AddChatClient(b => - b.Use(b.Services.GetRequiredService().AsChatClient("gpt-4o-mini"))); +builder.Services.AddChatClient(services => + services.GetRequiredService().AsChatClient("gpt-4o-mini")); var app = builder.Build(); diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/README.md b/src/Libraries/Microsoft.Extensions.AI.Ollama/README.md index 3d2eddcafc1..1eae652e7c8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Ollama/README.md @@ -70,9 +70,9 @@ using Microsoft.Extensions.AI; IChatClient ollamaClient = new OllamaChatClient(new Uri("http://localhost:11434/"), "llama3.1"); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(ollamaClient) .UseFunctionInvocation() - .Use(ollamaClient); + .Build(); ChatOptions chatOptions = new() { @@ -97,9 +97,9 @@ IDistributedCache cache = new MemoryDistributedCache(Options.Create(new MemoryDi IChatClient ollamaClient = new OllamaChatClient(new Uri("http://localhost:11434/"), "llama3.1"); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(ollamaClient) .UseDistributedCache(cache) - .Use(ollamaClient); + .Build(); for (int i = 0; i < 3; i++) { @@ -128,9 +128,9 @@ var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() IChatClient ollamaClient = new OllamaChatClient(new Uri("http://localhost:11434/"), "llama3.1"); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(ollamaClient) .UseOpenTelemetry(sourceName, c => c.EnableSensitiveData = true) - .Use(ollamaClient); + .Build(); Console.WriteLine(await client.CompleteAsync("What is AI?")); ``` @@ -163,11 +163,11 @@ var chatOptions = new ChatOptions IChatClient ollamaClient = new OllamaChatClient(new Uri("http://localhost:11434/"), "llama3.1"); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(ollamaClient) .UseDistributedCache(cache) .UseFunctionInvocation() .UseOpenTelemetry(sourceName, c => c.EnableSensitiveData = true) - .Use(ollamaClient); + .Build(); for (int i = 0; i < 3; i++) { @@ -235,10 +235,9 @@ var builder = Host.CreateApplicationBuilder(); builder.Services.AddDistributedMemoryCache(); builder.Services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Trace)); -builder.Services.AddChatClient(b => b +builder.Services.AddChatClient(new OllamaChatClient(new Uri("http://localhost:11434/"), "llama3.1")) .UseDistributedCache() - .UseLogging() - .Use(new OllamaChatClient(new Uri("http://localhost:11434/"), "llama3.1"))); + .UseLogging(); var app = builder.Build(); @@ -254,8 +253,8 @@ using Microsoft.Extensions.AI; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddChatClient(c => - c.Use(new OllamaChatClient(new Uri("http://localhost:11434/"), "llama3.1"))); +builder.Services.AddChatClient( + new OllamaChatClient(new Uri("http://localhost:11434/"), "llama3.1")); builder.Services.AddEmbeddingGenerator>(g => g.Use(new OllamaEmbeddingGenerator(endpoint, "all-minilm"))); diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/README.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/README.md index 696cc0c01bf..fa0e2956e86 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/README.md @@ -77,9 +77,9 @@ IChatClient openaiClient = new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")) .AsChatClient("gpt-4o-mini"); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(openaiClient) .UseFunctionInvocation() - .Use(openaiClient); + .Build(); ChatOptions chatOptions = new() { @@ -110,9 +110,9 @@ IChatClient openaiClient = new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")) .AsChatClient("gpt-4o-mini"); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(openaiClient) .UseDistributedCache(cache) - .Use(openaiClient); + .Build(); for (int i = 0; i < 3; i++) { @@ -144,9 +144,9 @@ IChatClient openaiClient = new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")) .AsChatClient("gpt-4o-mini"); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(openaiClient) .UseOpenTelemetry(sourceName, c => c.EnableSensitiveData = true) - .Use(openaiClient); + .Build(); Console.WriteLine(await client.CompleteAsync("What is AI?")); ``` @@ -182,11 +182,11 @@ IChatClient openaiClient = new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")) .AsChatClient("gpt-4o-mini"); -IChatClient client = new ChatClientBuilder() +IChatClient client = new ChatClientBuilder(openaiClient) .UseDistributedCache(cache) .UseFunctionInvocation() .UseOpenTelemetry(sourceName, c => c.EnableSensitiveData = true) - .Use(openaiClient); + .Build(); for (int i = 0; i < 3; i++) { @@ -260,10 +260,9 @@ builder.Services.AddSingleton(new OpenAIClient(Environment.GetEnvironmentVariabl builder.Services.AddDistributedMemoryCache(); builder.Services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Trace)); -builder.Services.AddChatClient(b => b +builder.Services.AddChatClient(services => services.GetRequiredService().AsChatClient("gpt-4o-mini")) .UseDistributedCache() - .UseLogging() - .Use(b.Services.GetRequiredService().AsChatClient("gpt-4o-mini"))); + .UseLogging(); var app = builder.Build(); @@ -282,8 +281,8 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton(new OpenAIClient(builder.Configuration["OPENAI_API_KEY"])); -builder.Services.AddChatClient(b => - b.Use(b.Services.GetRequiredService().AsChatClient("gpt-4o-mini"))); +builder.Services.AddChatClient(services => + services.GetRequiredService().AsChatClient("gpt-4o-mini")); builder.Services.AddEmbeddingGenerator>(g => g.Use(g.Services.GetRequiredService().AsEmbeddingGenerator("text-embedding-3-small"))); diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilder.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilder.cs index d7934ba7809..abbf4776d51 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilder.cs @@ -10,32 +10,43 @@ namespace Microsoft.Extensions.AI; /// A builder for creating pipelines of . public sealed class ChatClientBuilder { + private Func _innerClientFactory; + /// The registered client factory instances. private List>? _clientFactories; /// Initializes a new instance of the class. - /// The service provider to use for dependency injection. - public ChatClientBuilder(IServiceProvider? services = null) + /// The inner that represents the underlying backend. + public ChatClientBuilder(IChatClient innerClient) { - Services = services ?? EmptyServiceProvider.Instance; + _ = Throw.IfNull(innerClient); + _innerClientFactory = _ => innerClient; } - /// Gets the associated with the builder instance. - public IServiceProvider Services { get; } + /// Initializes a new instance of the class. + /// A callback that produces the inner that represents the underlying backend. + public ChatClientBuilder(Func innerClientFactory) + { + _innerClientFactory = Throw.IfNull(innerClientFactory); + } - /// Completes the pipeline by adding a final that represents the underlying backend. This is typically a client for an LLM service. - /// The inner client to use. - /// An instance of that represents the entire pipeline. Calls to this instance will pass through each of the pipeline stages in turn. - public IChatClient Use(IChatClient innerClient) + /// Returns an that represents the entire pipeline. Calls to this instance will pass through each of the pipeline stages in turn. + /// + /// The that should provide services to the instances. + /// If null, an empty will be used. + /// + /// An instance of that represents the entire pipeline. + public IChatClient Build(IServiceProvider? services = null) { - var chatClient = Throw.IfNull(innerClient); + services ??= EmptyServiceProvider.Instance; + var chatClient = _innerClientFactory(services); // To match intuitive expectations, apply the factories in reverse order, so that the first factory added is the outermost. if (_clientFactories is not null) { for (var i = _clientFactories.Count - 1; i >= 0; i--) { - chatClient = _clientFactories[i](Services, chatClient) ?? + chatClient = _clientFactories[i](services, chatClient) ?? throw new InvalidOperationException( $"The {nameof(ChatClientBuilder)} entry at index {i} returned null. " + $"Ensure that the callbacks passed to {nameof(Use)} return non-null {nameof(IChatClient)} instances."); diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs index 9d419f434af..a057a507f24 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs @@ -10,38 +10,62 @@ namespace Microsoft.Extensions.DependencyInjection; /// Provides extension methods for registering with a . public static class ChatClientBuilderServiceCollectionExtensions { - /// Adds a chat client to the . - /// The to which the client should be added. - /// The factory to use to construct the instance. - /// The collection. - /// The client is registered as a scoped service. - public static IServiceCollection AddChatClient( - this IServiceCollection services, - Func clientFactory) + /// Registers a singleton in the . + /// The to which the client should be added. + /// The inner that represents the underlying backend. + /// A that can be used to build a pipeline around the inner client. + /// The client is registered as a singleton service. + public static ChatClientBuilder AddChatClient( + this IServiceCollection serviceCollection, + IChatClient innerClient) + => AddChatClient(serviceCollection, _ => innerClient); + + /// Registers a singleton in the . + /// The to which the client should be added. + /// A callback that produces the inner that represents the underlying backend. + /// A that can be used to build a pipeline around the inner client. + /// The client is registered as a singleton service. + public static ChatClientBuilder AddChatClient( + this IServiceCollection serviceCollection, + Func innerClientFactory) { - _ = Throw.IfNull(services); - _ = Throw.IfNull(clientFactory); + _ = Throw.IfNull(serviceCollection); + _ = Throw.IfNull(innerClientFactory); - return services.AddScoped(services => - clientFactory(new ChatClientBuilder(services))); + var builder = new ChatClientBuilder(innerClientFactory); + _ = serviceCollection.AddSingleton(builder.Build); + return builder; } - /// Adds a chat client to the . - /// The to which the client should be added. + /// Registers a singleton in the . + /// The to which the client should be added. + /// The key with which to associate the client. + /// The inner that represents the underlying backend. + /// A that can be used to build a pipeline around the inner client. + /// The client is registered as a scoped service. + public static ChatClientBuilder AddKeyedChatClient( + this IServiceCollection serviceCollection, + object serviceKey, + IChatClient innerClient) + => AddKeyedChatClient(serviceCollection, serviceKey, _ => innerClient); + + /// Registers a singleton in the . + /// The to which the client should be added. /// The key with which to associate the client. - /// The factory to use to construct the instance. - /// The collection. + /// A callback that produces the inner that represents the underlying backend. + /// A that can be used to build a pipeline around the inner client. /// The client is registered as a scoped service. - public static IServiceCollection AddKeyedChatClient( - this IServiceCollection services, + public static ChatClientBuilder AddKeyedChatClient( + this IServiceCollection serviceCollection, object serviceKey, - Func clientFactory) + Func innerClientFactory) { - _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceCollection); _ = Throw.IfNull(serviceKey); - _ = Throw.IfNull(clientFactory); + _ = Throw.IfNull(innerClientFactory); - return services.AddKeyedScoped(serviceKey, (services, _) => - clientFactory(new ChatClientBuilder(services))); + var builder = new ChatClientBuilder(innerClientFactory); + _ = serviceCollection.AddKeyedSingleton(serviceKey, (services, _) => builder.Build(services)); + return builder; } } diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs index 476ad973ddc..c0f79efdd62 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs @@ -77,11 +77,11 @@ public void GetService_SuccessfullyReturnsUnderlyingClient() Assert.Same(client, chatClient.GetService()); - using IChatClient pipeline = new ChatClientBuilder() + using IChatClient pipeline = new ChatClientBuilder(chatClient) .UseFunctionInvocation() .UseOpenTelemetry() .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) - .Use(chatClient); + .Build(); Assert.NotNull(pipeline.GetService()); Assert.NotNull(pipeline.GetService()); diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index ce376e3927d..871769df33c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -377,12 +377,12 @@ public virtual async Task Caching_BeforeFunctionInvocation_AvoidsExtraCalls() }, "GetTemperature"); // First call executes the function and calls the LLM - using var chatClient = new ChatClientBuilder() + using var chatClient = new ChatClientBuilder(CreateChatClient()!) .ConfigureOptions(options => options.Tools = [getTemperature]) .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) .UseFunctionInvocation() .UseCallCounting() - .Use(CreateChatClient()!); + .Build(); var llmCallCount = chatClient.GetService(); var message = new ChatMessage(ChatRole.User, "What is the temperature?"); @@ -415,12 +415,12 @@ public virtual async Task Caching_AfterFunctionInvocation_FunctionOutputUnchange }, "GetTemperature"); // First call executes the function and calls the LLM - using var chatClient = new ChatClientBuilder() + using var chatClient = new ChatClientBuilder(CreateChatClient()!) .ConfigureOptions(options => options.Tools = [getTemperature]) .UseFunctionInvocation() .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) .UseCallCounting() - .Use(CreateChatClient()!); + .Build(); var llmCallCount = chatClient.GetService(); var message = new ChatMessage(ChatRole.User, "What is the temperature?"); @@ -454,12 +454,12 @@ public virtual async Task Caching_AfterFunctionInvocation_FunctionOutputChangedA }, "GetTemperature"); // First call executes the function and calls the LLM - using var chatClient = new ChatClientBuilder() + using var chatClient = new ChatClientBuilder(CreateChatClient()!) .ConfigureOptions(options => options.Tools = [getTemperature]) .UseFunctionInvocation() .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) .UseCallCounting() - .Use(CreateChatClient()!); + .Build(); var llmCallCount = chatClient.GetService(); var message = new ChatMessage(ChatRole.User, "What is the temperature?"); @@ -573,9 +573,9 @@ public virtual async Task OpenTelemetry_CanEmitTracesAndMetrics() .AddInMemoryExporter(activities) .Build(); - var chatClient = new ChatClientBuilder() + var chatClient = new ChatClientBuilder(CreateChatClient()!) .UseOpenTelemetry(sourceName: sourceName) - .Use(CreateChatClient()!); + .Build(); var response = await chatClient.CompleteAsync([new(ChatRole.User, "What's the biggest animal?")]); diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ReducingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ReducingChatClientTests.cs index 684211ab60b..7e3783976dc 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ReducingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ReducingChatClientTests.cs @@ -37,9 +37,9 @@ public async Task Reduction_LimitsMessagesBasedOnTokenLimit() } }; - using var client = new ChatClientBuilder() + using var client = new ChatClientBuilder(innerClient) .UseChatReducer(new TokenCountingChatReducer(_gpt4oTokenizer, 40)) - .Use(innerClient); + .Build(); List messages = [ diff --git a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientIntegrationTests.cs index 4c71690baaf..23d910f5e33 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientIntegrationTests.cs @@ -37,11 +37,11 @@ public async Task PromptBasedFunctionCalling_NoArgs() { SkipIfNotEnabled(); - using var chatClient = new ChatClientBuilder() + using var chatClient = new ChatClientBuilder(CreateChatClient()!) .UseFunctionInvocation() .UsePromptBasedFunctionCalling() .Use(innerClient => new AssertNoToolsDefinedChatClient(innerClient)) - .Use(CreateChatClient()!); + .Build(); var secretNumber = 42; var response = await chatClient.CompleteAsync("What is the current secret number? Answer with digits only.", new ChatOptions @@ -61,11 +61,11 @@ public async Task PromptBasedFunctionCalling_WithArgs() { SkipIfNotEnabled(); - using var chatClient = new ChatClientBuilder() + using var chatClient = new ChatClientBuilder(CreateChatClient()!) .UseFunctionInvocation() .UsePromptBasedFunctionCalling() .Use(innerClient => new AssertNoToolsDefinedChatClient(innerClient)) - .Use(CreateChatClient()!); + .Build(); var stockPriceTool = AIFunctionFactory.Create([Description("Returns the stock price for a given ticker symbol")] ( [Description("The ticker symbol")] string symbol, diff --git a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs index 3879e9e2ec3..4e01987a158 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs @@ -48,11 +48,11 @@ public void GetService_SuccessfullyReturnsUnderlyingClient() Assert.Same(client, client.GetService()); Assert.Same(client, client.GetService()); - using IChatClient pipeline = new ChatClientBuilder() + using IChatClient pipeline = new ChatClientBuilder(client) .UseFunctionInvocation() .UseOpenTelemetry() .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) - .Use(client); + .Build(); Assert.NotNull(pipeline.GetService()); Assert.NotNull(pipeline.GetService()); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index fb912235cfc..41c118dc3cb 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -95,11 +95,11 @@ public void GetService_OpenAIClient_SuccessfullyReturnsUnderlyingClient() Assert.NotNull(chatClient.GetService()); - using IChatClient pipeline = new ChatClientBuilder() + using IChatClient pipeline = new ChatClientBuilder(chatClient) .UseFunctionInvocation() .UseOpenTelemetry() .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) - .Use(chatClient); + .Build(); Assert.NotNull(pipeline.GetService()); Assert.NotNull(pipeline.GetService()); @@ -119,11 +119,11 @@ public void GetService_ChatClient_SuccessfullyReturnsUnderlyingClient() Assert.Same(chatClient, chatClient.GetService()); Assert.Same(openAIClient, chatClient.GetService()); - using IChatClient pipeline = new ChatClientBuilder() + using IChatClient pipeline = new ChatClientBuilder(chatClient) .UseFunctionInvocation() .UseOpenTelemetry() .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) - .Use(chatClient); + .Build(); Assert.NotNull(pipeline.GetService()); Assert.NotNull(pipeline.GetService()); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientBuilderTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientBuilderTest.cs index ba1c85d700a..8630cfe1702 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientBuilderTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientBuilderTest.cs @@ -13,17 +13,23 @@ public class ChatClientBuilderTest public void PassesServiceProviderToFactories() { var expectedServiceProvider = new ServiceCollection().BuildServiceProvider(); - using TestChatClient expectedResult = new(); - var builder = new ChatClientBuilder(expectedServiceProvider); + using TestChatClient expectedInnerClient = new(); + using TestChatClient expectedOuterClient = new(); + + var builder = new ChatClientBuilder(services => + { + Assert.Same(expectedServiceProvider, services); + return expectedInnerClient; + }); builder.Use((serviceProvider, innerClient) => { Assert.Same(expectedServiceProvider, serviceProvider); - return expectedResult; + Assert.Same(expectedInnerClient, innerClient); + return expectedOuterClient; }); - using TestChatClient innerClient = new(); - Assert.Equal(expectedResult, builder.Use(innerClient: innerClient)); + Assert.Same(expectedOuterClient, builder.Build(expectedServiceProvider)); } [Fact] @@ -31,14 +37,14 @@ public void BuildsPipelineInOrderAdded() { // Arrange using TestChatClient expectedInnerClient = new(); - var builder = new ChatClientBuilder(); + var builder = new ChatClientBuilder(expectedInnerClient); builder.Use(next => new InnerClientCapturingChatClient("First", next)); builder.Use(next => new InnerClientCapturingChatClient("Second", next)); builder.Use(next => new InnerClientCapturingChatClient("Third", next)); // Act - var first = (InnerClientCapturingChatClient)builder.Use(expectedInnerClient); + var first = (InnerClientCapturingChatClient)builder.Build(); // Assert Assert.Equal("First", first.Name); @@ -52,23 +58,22 @@ public void BuildsPipelineInOrderAdded() [Fact] public void DoesNotAcceptNullInnerService() { - Assert.Throws(() => new ChatClientBuilder().Use((IChatClient)null!)); + Assert.Throws(() => new ChatClientBuilder((IChatClient)null!)); } [Fact] public void DoesNotAcceptNullFactories() { - ChatClientBuilder builder = new(); - Assert.Throws(() => builder.Use((Func)null!)); - Assert.Throws(() => builder.Use((Func)null!)); + Assert.Throws(() => new ChatClientBuilder((Func)null!)); } [Fact] public void DoesNotAllowFactoriesToReturnNull() { - ChatClientBuilder builder = new(); + using var innerClient = new TestChatClient(); + ChatClientBuilder builder = new(innerClient); builder.Use(_ => null!); - var ex = Assert.Throws(() => builder.Use(new TestChatClient())); + var ex = Assert.Throws(() => builder.Build()); Assert.Contains("entry at index 0", ex.Message); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ConfigureOptionsChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ConfigureOptionsChatClientTests.cs index 6b1e6587f1f..68a898dc743 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ConfigureOptionsChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ConfigureOptionsChatClientTests.cs @@ -22,7 +22,8 @@ public void ConfigureOptionsChatClient_InvalidArgs_Throws() [Fact] public void ConfigureOptions_InvalidArgs_Throws() { - var builder = new ChatClientBuilder(); + using var innerClient = new TestChatClient(); + var builder = new ChatClientBuilder(innerClient); Assert.Throws("configure", () => builder.ConfigureOptions(null!)); } @@ -54,7 +55,7 @@ public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullP }, }; - using var client = new ChatClientBuilder() + using var client = new ChatClientBuilder(innerClient) .ConfigureOptions(options => { Assert.NotSame(providedOptions, options); @@ -69,7 +70,7 @@ public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullP returnedOptions = options; }) - .Use(innerClient); + .Build(); var completion = await client.CompleteAsync(Array.Empty(), providedOptions, cts.Token); Assert.Same(expectedCompletion, completion); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DependencyInjectionPatterns.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DependencyInjectionPatterns.cs index 9bbfbea98c3..54c5011b103 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DependencyInjectionPatterns.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DependencyInjectionPatterns.cs @@ -12,12 +12,11 @@ public class DependencyInjectionPatterns private IServiceCollection ServiceCollection { get; } = new ServiceCollection(); [Fact] - public void CanRegisterScopedUsingGenericType() + public void CanRegisterSingletonUsingFactory() { // Arrange/Act - ServiceCollection.AddChatClient(builder => builder - .UseScopedMiddleware() - .Use(new TestChatClient())); + ServiceCollection.AddChatClient(services => new TestChatClient { Services = services }) + .UseSingletonMiddleware(); // Assert var services = ServiceCollection.BuildServiceProvider(); @@ -28,27 +27,20 @@ public void CanRegisterScopedUsingGenericType() var instance1Copy = scope1.ServiceProvider.GetRequiredService(); var instance2 = scope2.ServiceProvider.GetRequiredService(); - // Each scope gets a distinct outer *AND* inner client - var outer1 = Assert.IsType(instance1); - var outer2 = Assert.IsType(instance2); - var inner1 = Assert.IsType(((ScopedChatClient)instance1).InnerClient); - var inner2 = Assert.IsType(((ScopedChatClient)instance2).InnerClient); - - Assert.NotSame(outer1.Services, outer2.Services); - Assert.NotSame(instance1, instance2); - Assert.NotSame(inner1, inner2); - Assert.Same(instance1, instance1Copy); // From the same scope + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerClient); } [Fact] - public void CanRegisterScopedUsingFactory() + public void CanRegisterSingletonUsingSharedInstance() { // Arrange/Act - ServiceCollection.AddChatClient(builder => - { - builder.UseScopedMiddleware(); - return builder.Use(new TestChatClient { Services = builder.Services }); - }); + using var singleton = new TestChatClient(); + ServiceCollection.AddChatClient(singleton) + .UseSingletonMiddleware(); // Assert var services = ServiceCollection.BuildServiceProvider(); @@ -56,45 +48,68 @@ public void CanRegisterScopedUsingFactory() using var scope2 = services.CreateScope(); var instance1 = scope1.ServiceProvider.GetRequiredService(); + var instance1Copy = scope1.ServiceProvider.GetRequiredService(); var instance2 = scope2.ServiceProvider.GetRequiredService(); - // Each scope gets a distinct outer *AND* inner client - var outer1 = Assert.IsType(instance1); - var outer2 = Assert.IsType(instance2); - var inner1 = Assert.IsType(((ScopedChatClient)instance1).InnerClient); - var inner2 = Assert.IsType(((ScopedChatClient)instance2).InnerClient); + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerClient); + } + + [Fact] + public void CanRegisterKeyedSingletonUsingFactory() + { + // Arrange/Act + ServiceCollection.AddKeyedChatClient("mykey", services => new TestChatClient { Services = services }) + .UseSingletonMiddleware(); - Assert.Same(outer1.Services, inner1.Services); - Assert.Same(outer2.Services, inner2.Services); - Assert.NotSame(outer1.Services, outer2.Services); + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + Assert.Null(services.GetService()); + + var instance1 = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance1Copy = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance2 = scope2.ServiceProvider.GetRequiredKeyedService("mykey"); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerClient); } [Fact] - public void CanRegisterScopedUsingSharedInstance() + public void CanRegisterKeyedSingletonUsingSharedInstance() { // Arrange/Act using var singleton = new TestChatClient(); - ServiceCollection.AddChatClient(builder => - { - builder.UseScopedMiddleware(); - return builder.Use(singleton); - }); + ServiceCollection.AddKeyedChatClient("mykey", singleton) + .UseSingletonMiddleware(); // Assert var services = ServiceCollection.BuildServiceProvider(); using var scope1 = services.CreateScope(); using var scope2 = services.CreateScope(); - var instance1 = scope1.ServiceProvider.GetRequiredService(); - var instance2 = scope2.ServiceProvider.GetRequiredService(); - // Each scope gets a distinct outer instance, but the same inner client - Assert.IsType(instance1); - Assert.IsType(instance2); - Assert.Same(singleton, ((ScopedChatClient)instance1).InnerClient); - Assert.Same(singleton, ((ScopedChatClient)instance2).InnerClient); + Assert.Null(services.GetService()); + + var instance1 = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance1Copy = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance2 = scope2.ServiceProvider.GetRequiredKeyedService("mykey"); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerClient); } - public class ScopedChatClient(IServiceProvider services, IChatClient inner) : DelegatingChatClient(inner) + public class SingletonMiddleware(IServiceProvider services, IChatClient inner) : DelegatingChatClient(inner) { public new IChatClient InnerClient => base.InnerClient; public IServiceProvider Services => services; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs index 772bb9cf7d6..dcc6068b3ce 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs @@ -681,12 +681,12 @@ public async Task CanResolveIDistributedCacheFromDI() new(ChatRole.Assistant, [new TextContent("Hey")])])); } }; - using var outer = new ChatClientBuilder(services) + using var outer = new ChatClientBuilder(testClient) .UseDistributedCache(configure: options => { options.JsonSerializerOptions = TestJsonSerializerContext.Default.Options; }) - .Use(testClient); + .Build(services); // Act: Make a request that should populate the cache Assert.Empty(_storage.Keys); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 542851baa69..1e4558901ca 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -295,7 +295,7 @@ public async Task RejectsMultipleChoicesAsync() } }; - IChatClient service = new ChatClientBuilder().UseFunctionInvocation().Use(innerClient); + IChatClient service = new ChatClientBuilder(innerClient).UseFunctionInvocation().Build(); List chat = [new ChatMessage(ChatRole.User, "hello")]; var ex = await Assert.ThrowsAsync( @@ -415,7 +415,7 @@ private static async Task> InvokeAndAssertAsync( } }; - IChatClient service = configurePipeline(new ChatClientBuilder()).Use(innerClient); + IChatClient service = configurePipeline(new ChatClientBuilder(innerClient)).Build(); var result = await service.CompleteAsync(chat, options, cts.Token); chat.Add(result.Message); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/LoggingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/LoggingChatClientTests.cs index feb91ac925e..38bc4e8f67d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/LoggingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/LoggingChatClientTests.cs @@ -40,9 +40,9 @@ public async Task CompleteAsync_LogsStartAndCompletion(LogLevel level) }, }; - using IChatClient client = new ChatClientBuilder(services) + using IChatClient client = new ChatClientBuilder(innerClient) .UseLogging() - .Use(innerClient); + .Build(services); await client.CompleteAsync( [new(ChatRole.User, "What's the biggest animal?")], @@ -86,9 +86,9 @@ static async IAsyncEnumerable GetUpdatesAsync() yield return new StreamingChatCompletionUpdate { Role = ChatRole.Assistant, Text = "whale" }; } - using IChatClient client = new ChatClientBuilder() + using IChatClient client = new ChatClientBuilder(innerClient) .UseLogging(logger) - .Use(innerClient); + .Build(); await foreach (var update in client.CompleteStreamingAsync( [new(ChatRole.User, "What's the biggest animal?")], diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs index 2ad428fad76..2080e2f02b2 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs @@ -86,13 +86,13 @@ async static IAsyncEnumerable CallbackAsync( }; } - var chatClient = new ChatClientBuilder() + var chatClient = new ChatClientBuilder(innerClient) .UseOpenTelemetry(loggerFactory, sourceName, configure: instance => { instance.EnableSensitiveData = enableSensitiveData; instance.JsonSerializerOptions = TestJsonSerializerContext.Default.Options; }) - .Use(innerClient); + .Build(); List chatMessages = [ diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ScopedChatClientExtensions.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ScopedChatClientExtensions.cs deleted file mode 100644 index d9ad92dc266..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ScopedChatClientExtensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.AI; - -public static class ScopedChatClientExtensions -{ - public static ChatClientBuilder UseScopedMiddleware(this ChatClientBuilder builder) - => builder.Use((services, inner) - => new DependencyInjectionPatterns.ScopedChatClient(services, inner)); -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/SingletonChatClientExtensions.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/SingletonChatClientExtensions.cs new file mode 100644 index 00000000000..e971a0ad322 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/SingletonChatClientExtensions.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AI; + +public static class SingletonChatClientExtensions +{ + public static ChatClientBuilder UseSingletonMiddleware(this ChatClientBuilder builder) + => builder.Use((services, inner) + => new DependencyInjectionPatterns.SingletonMiddleware(services, inner)); +}