From a5f168d042bc8bfd7425468dbfaba439706a31e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Jun 2025 05:43:55 +0000 Subject: [PATCH 1/7] Initial plan From 5f1985c06727f15013b4a00e599adfc566b9104d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Jun 2025 05:50:48 +0000 Subject: [PATCH 2/7] Add AddKeyedOllamaApiClient overloads with custom service key support Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- .../AspireOllamaApiClientBuilder.cs | 4 +- .../AspireOllamaChatClientExtensions.cs | 2 +- ...spireOllamaEmbeddingGeneratorExtensions.cs | 2 +- .../AspireOllamaSharpExtensions.cs | 43 ++++++++++++++-- .../OllamaApiClientTests.cs | 49 +++++++++++++++++++ .../OllamaSharpIChatClientTests.cs | 23 +++++++++ 6 files changed, 115 insertions(+), 8 deletions(-) diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaApiClientBuilder.cs b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaApiClientBuilder.cs index fa6609e7..2580ab33 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaApiClientBuilder.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaApiClientBuilder.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.Hosting; /// The with which services are being registered. /// The service key used to register the service, if any. /// A flag to indicate whether tracing should be disabled. -public class AspireOllamaApiClientBuilder(IHostApplicationBuilder hostBuilder, string serviceKey, bool disableTracing) +public class AspireOllamaApiClientBuilder(IHostApplicationBuilder hostBuilder, object? serviceKey, bool disableTracing) { /// /// The host application builder used to configure the application. @@ -18,7 +18,7 @@ public class AspireOllamaApiClientBuilder(IHostApplicationBuilder hostBuilder, s /// /// Gets the service key used to register the service, if any. /// - public string ServiceKey { get; } = serviceKey; + public object? ServiceKey { get; } = serviceKey; /// /// Gets a flag indicating whether tracing should be disabled. diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs index ea7a8b53..559af318 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs @@ -32,7 +32,7 @@ public static ChatClientBuilder AddKeyedChatClient( this AspireOllamaApiClientBuilder builder) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentException.ThrowIfNullOrEmpty(builder.ServiceKey, nameof(builder.ServiceKey)); + ArgumentNullException.ThrowIfNull(builder.ServiceKey, nameof(builder.ServiceKey)); return builder.HostBuilder.Services.AddKeyedChatClient( builder.ServiceKey, diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaEmbeddingGeneratorExtensions.cs b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaEmbeddingGeneratorExtensions.cs index d7b11ee7..5e308371 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaEmbeddingGeneratorExtensions.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaEmbeddingGeneratorExtensions.cs @@ -31,7 +31,7 @@ public static EmbeddingGeneratorBuilder> AddKeyedEmbedd this AspireOllamaApiClientBuilder builder) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentException.ThrowIfNullOrEmpty(builder.ServiceKey, nameof(builder.ServiceKey)); + ArgumentNullException.ThrowIfNull(builder.ServiceKey, nameof(builder.ServiceKey)); return builder.HostBuilder.Services.AddKeyedEmbeddingGenerator( builder.ServiceKey, diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaSharpExtensions.cs b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaSharpExtensions.cs index 88da797c..7b7960c8 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaSharpExtensions.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaSharpExtensions.cs @@ -43,6 +43,37 @@ public static AspireOllamaApiClientBuilder AddKeyedOllamaApiClient(this IHostApp return AddOllamaClientInternal(builder, $"{DefaultConfigSectionName}:{connectionName}", connectionName, serviceKey: connectionName, configureSettings: configureSettings); } + /// + /// Adds services to the container using the specified . + /// + /// The to read config from and add services to. + /// A unique key that identifies this instance of the Ollama client service. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration. + /// Thrown when no Ollama endpoint is provided. + public static AspireOllamaApiClientBuilder AddKeyedOllamaApiClient(this IHostApplicationBuilder builder, object serviceKey, string connectionName, Action? configureSettings = null) + { + ArgumentNullException.ThrowIfNull(serviceKey, nameof(serviceKey)); + ArgumentException.ThrowIfNullOrWhiteSpace(connectionName, nameof(connectionName)); + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + return AddOllamaClientInternal(builder, $"{DefaultConfigSectionName}:{connectionName}", connectionName, serviceKey: serviceKey, configureSettings: configureSettings); + } + + /// + /// Adds services to the container using the specified . + /// + /// The to read config from and add services to. + /// A unique key that identifies this instance of the Ollama client service. + /// The settings required to configure the . + /// Thrown when no Ollama endpoint is provided. + public static AspireOllamaApiClientBuilder AddKeyedOllamaApiClient(this IHostApplicationBuilder builder, object serviceKey, OllamaSharpSettings settings) + { + ArgumentNullException.ThrowIfNull(serviceKey, nameof(serviceKey)); + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(settings, nameof(settings)); + return AddOllamaClientInternal(builder, DefaultConfigSectionName, serviceKey.ToString() ?? "default", serviceKey: serviceKey, configureSettings: null, settings: settings); + } + /// /// Adds and services to the container. /// @@ -105,11 +136,15 @@ private static AspireOllamaApiClientBuilder AddOllamaClientInternal( IHostApplicationBuilder builder, string configurationSectionName, string connectionName, - string? serviceKey = null, - Action? configureSettings = null) + object? serviceKey = null, + Action? configureSettings = null, + OllamaSharpSettings? settings = null) { - OllamaSharpSettings settings = new(); - builder.Configuration.GetSection(configurationSectionName).Bind(settings); + settings ??= new(); + if (string.IsNullOrEmpty(settings.Endpoint?.ToString())) + { + builder.Configuration.GetSection(configurationSectionName).Bind(settings); + } if (builder.Configuration.GetConnectionString(connectionName) is string connectionString) { diff --git a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaApiClientTests.cs b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaApiClientTests.cs index bcb143e9..0c10cab6 100644 --- a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaApiClientTests.cs +++ b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaApiClientTests.cs @@ -124,6 +124,55 @@ public void CanSetMultipleKeyedClients() Assert.NotEqual(client, client3); } + [Fact] + public void CanSetMultipleKeyedClientsWithCustomServiceKeys() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:Ollama", $"Endpoint={Endpoint}"), + new KeyValuePair("ConnectionStrings:Ollama2", "Endpoint=https://localhost:5002/"), + new KeyValuePair("ConnectionStrings:Ollama3", "Endpoint=https://localhost:5003/") + ]); + + // Use custom service keys instead of connection names + builder.AddKeyedOllamaApiClient("ChatModel", "Ollama"); + builder.AddKeyedOllamaApiClient("VisionModel", "Ollama2"); + builder.AddKeyedOllamaApiClient("EmbeddingModel", "Ollama3"); + + using var host = builder.Build(); + var chatClient = host.Services.GetRequiredKeyedService("ChatModel"); + var visionClient = host.Services.GetRequiredKeyedService("VisionModel"); + var embeddingClient = host.Services.GetRequiredKeyedService("EmbeddingModel"); + + Assert.Equal(Endpoint, chatClient.Uri); + Assert.Equal("https://localhost:5002/", visionClient.Uri?.ToString()); + Assert.Equal("https://localhost:5003/", embeddingClient.Uri?.ToString()); + + Assert.NotEqual(chatClient, visionClient); + Assert.NotEqual(chatClient, embeddingClient); + Assert.NotEqual(visionClient, embeddingClient); + } + + [Fact] + public void CanSetKeyedClientWithSettingsOverload() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + var settings = new OllamaSharpSettings + { + Endpoint = Endpoint, + SelectedModel = "testmodel" + }; + + builder.AddKeyedOllamaApiClient("TestService", settings); + + using var host = builder.Build(); + var client = host.Services.GetRequiredKeyedService("TestService"); + + Assert.Equal(Endpoint, client.Uri); + Assert.Equal("testmodel", client.SelectedModel); + } + [Fact] public void RegisteringChatClientAndEmbeddingGeneratorReturnsCorrectModelForServices() { diff --git a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs index c2900960..2d092014 100644 --- a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs +++ b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs @@ -150,6 +150,29 @@ public void CanChainUseMethodsCorrectly() Assert.IsType(GetInnerClient(otelClient), exactMatch: false); } + [Fact] + public void CanSetMultipleKeyedChatClientsWithCustomServiceKeys() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:Ollama", $"Endpoint={Endpoint}"), + new KeyValuePair("ConnectionStrings:Ollama2", "Endpoint=https://localhost:5002/") + ]); + + // Use custom service keys for different chat clients + builder.AddKeyedOllamaApiClient("ChatModel", "Ollama").AddKeyedChatClient(); + builder.AddKeyedOllamaApiClient("VisionModel", "Ollama2").AddKeyedChatClient(); + + using var host = builder.Build(); + var chatClient = host.Services.GetRequiredKeyedService("ChatModel"); + var visionClient = host.Services.GetRequiredKeyedService("VisionModel"); + + Assert.Equal(Endpoint, chatClient.GetService()?.ProviderUri); + Assert.Equal("https://localhost:5002/", visionClient.GetService()?.ProviderUri?.ToString()); + + Assert.NotEqual(chatClient, visionClient); + } + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_InnerClient")] private static extern IChatClient GetInnerClient(DelegatingChatClient client); } From c084865ffc69230700abe5a5002f63e28e5d3a27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Jun 2025 05:51:51 +0000 Subject: [PATCH 3/7] Update API surface and add usage example for custom service keys Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- .../api/CommunityToolkit.Aspire.OllamaSharp.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/api/CommunityToolkit.Aspire.OllamaSharp.cs b/src/CommunityToolkit.Aspire.OllamaSharp/api/CommunityToolkit.Aspire.OllamaSharp.cs index 78af13a3..ec7311d3 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/api/CommunityToolkit.Aspire.OllamaSharp.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/api/CommunityToolkit.Aspire.OllamaSharp.cs @@ -26,13 +26,13 @@ namespace Microsoft.Extensions.Hosting { public partial class AspireOllamaApiClientBuilder { - public AspireOllamaApiClientBuilder(IHostApplicationBuilder hostBuilder, string serviceKey, bool disableTracing) { } + public AspireOllamaApiClientBuilder(IHostApplicationBuilder hostBuilder, object? serviceKey, bool disableTracing) { } public bool DisableTracing { get { throw null; } } public IHostApplicationBuilder HostBuilder { get { throw null; } } - public string ServiceKey { get { throw null; } } + public object? ServiceKey { get { throw null; } } } public static partial class AspireOllamaChatClientExtensions @@ -53,6 +53,10 @@ public static partial class AspireOllamaSharpExtensions { public static AspireOllamaApiClientBuilder AddKeyedOllamaApiClient(this IHostApplicationBuilder builder, string connectionName, System.Action? configureSettings = null) { throw null; } + public static AspireOllamaApiClientBuilder AddKeyedOllamaApiClient(this IHostApplicationBuilder builder, object serviceKey, string connectionName, System.Action? configureSettings = null) { throw null; } + + public static AspireOllamaApiClientBuilder AddKeyedOllamaApiClient(this IHostApplicationBuilder builder, object serviceKey, CommunityToolkit.Aspire.OllamaSharp.OllamaSharpSettings settings) { throw null; } + [System.Obsolete("This approach to registering IChatClient is deprecated, use AddKeyedOllamaApiClient().AddChatClient() instead.")] public static void AddKeyedOllamaSharpChatClient(this IHostApplicationBuilder builder, string connectionName, System.Action? configureSettings = null) { } From b28d706b9faedab7822bacf17712b3da3452ca7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Jun 2025 05:53:33 +0000 Subject: [PATCH 4/7] Add comprehensive test demonstrating the exact issue resolution Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- .../OllamaApiClientTests.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaApiClientTests.cs b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaApiClientTests.cs index 0c10cab6..291de588 100644 --- a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaApiClientTests.cs +++ b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaApiClientTests.cs @@ -173,6 +173,43 @@ public void CanSetKeyedClientWithSettingsOverload() Assert.Equal("testmodel", client.SelectedModel); } + [Fact] + public void CanUseSameConnectionWithDifferentServiceKeys() + { + // This test demonstrates the main use case from the issue: + // Using the same connection but different service keys for different models + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:LocalAI", $"Endpoint={Endpoint}") + ]); + + // Same connection, different service keys and models + builder.AddKeyedOllamaApiClient("ChatModel", "LocalAI", settings => + { + settings.SelectedModel = "llama3.2"; + }); + + builder.AddKeyedOllamaApiClient("VisionModel", "LocalAI", settings => + { + settings.SelectedModel = "llava"; + }); + + using var host = builder.Build(); + var chatClient = host.Services.GetRequiredKeyedService("ChatModel"); + var visionClient = host.Services.GetRequiredKeyedService("VisionModel"); + + // Both use the same endpoint + Assert.Equal(Endpoint, chatClient.Uri); + Assert.Equal(Endpoint, visionClient.Uri); + + // But have different models + Assert.Equal("llama3.2", chatClient.SelectedModel); + Assert.Equal("llava", visionClient.SelectedModel); + + // And are different instances + Assert.NotEqual(chatClient, visionClient); + } + [Fact] public void RegisteringChatClientAndEmbeddingGeneratorReturnsCorrectModelForServices() { From 2c95517c1e4479b65289d2b9af2a54360da459e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Jun 2025 02:28:13 +0000 Subject: [PATCH 5/7] Add custom service key support to AddKeyedChatClient and AddKeyedEmbeddingGenerator Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- .../AspireOllamaChatClientExtensions.cs | 18 ++++++++ ...spireOllamaEmbeddingGeneratorExtensions.cs | 18 ++++++++ .../CommunityToolkit.Aspire.OllamaSharp.cs | 4 ++ .../OllamaSharpIChatClientTests.cs | 46 +++++++++++++++++++ .../OllamaSharpIEmbeddingGeneratorTests.cs | 46 +++++++++++++++++++ 5 files changed, 132 insertions(+) diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs index 559af318..a5abb7d2 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs @@ -39,6 +39,24 @@ public static ChatClientBuilder AddKeyedChatClient( services => CreateInnerChatClient(services, builder)); } + /// + /// Registers a keyed singleton in the services provided by the using the specified service key. + /// + /// An . + /// The service key to use for registering the . + /// A that can be used to build a pipeline around the inner . + public static ChatClientBuilder AddKeyedChatClient( + this AspireOllamaApiClientBuilder builder, + object serviceKey) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(serviceKey, nameof(serviceKey)); + + return builder.HostBuilder.Services.AddKeyedChatClient( + serviceKey, + services => CreateInnerChatClient(services, builder)); + } + /// /// Wrap the in a telemetry client if tracing is enabled. /// Note that this doesn't use ".UseOpenTelemetry()" because the order of the clients would be incorrect. diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaEmbeddingGeneratorExtensions.cs b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaEmbeddingGeneratorExtensions.cs index 5e308371..9d2f9d43 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaEmbeddingGeneratorExtensions.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaEmbeddingGeneratorExtensions.cs @@ -38,6 +38,24 @@ public static EmbeddingGeneratorBuilder> AddKeyedEmbedd services => CreateInnerEmbeddingGenerator(services, builder)); } + /// + /// Registers a keyed singleton in the services provided by the using the specified service key. + /// + /// An . + /// The service key to use for registering the . + /// A that can be used to build a pipeline around the inner . + public static EmbeddingGeneratorBuilder> AddKeyedEmbeddingGenerator( + this AspireOllamaApiClientBuilder builder, + object serviceKey) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(serviceKey, nameof(serviceKey)); + + return builder.HostBuilder.Services.AddKeyedEmbeddingGenerator( + serviceKey, + services => CreateInnerEmbeddingGenerator(services, builder)); + } + /// /// Wrap the in a telemetry client if tracing is enabled. /// Note that this doesn't use ".UseOpenTelemetry()" because the order of the clients would be incorrect. diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/api/CommunityToolkit.Aspire.OllamaSharp.cs b/src/CommunityToolkit.Aspire.OllamaSharp/api/CommunityToolkit.Aspire.OllamaSharp.cs index ec7311d3..2f851fbc 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/api/CommunityToolkit.Aspire.OllamaSharp.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/api/CommunityToolkit.Aspire.OllamaSharp.cs @@ -40,6 +40,8 @@ public static partial class AspireOllamaChatClientExtensions public static AI.ChatClientBuilder AddChatClient(this AspireOllamaApiClientBuilder builder) { throw null; } public static AI.ChatClientBuilder AddKeyedChatClient(this AspireOllamaApiClientBuilder builder) { throw null; } + + public static AI.ChatClientBuilder AddKeyedChatClient(this AspireOllamaApiClientBuilder builder, object serviceKey) { throw null; } } public static partial class AspireOllamaEmbeddingGeneratorExtensions @@ -47,6 +49,8 @@ public static partial class AspireOllamaEmbeddingGeneratorExtensions public static AI.EmbeddingGeneratorBuilder> AddEmbeddingGenerator(this AspireOllamaApiClientBuilder builder) { throw null; } public static AI.EmbeddingGeneratorBuilder> AddKeyedEmbeddingGenerator(this AspireOllamaApiClientBuilder builder) { throw null; } + + public static AI.EmbeddingGeneratorBuilder> AddKeyedEmbeddingGenerator(this AspireOllamaApiClientBuilder builder, object serviceKey) { throw null; } } public static partial class AspireOllamaSharpExtensions diff --git a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs index 2d092014..dc6c0d06 100644 --- a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs +++ b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs @@ -173,6 +173,52 @@ public void CanSetMultipleKeyedChatClientsWithCustomServiceKeys() Assert.NotEqual(chatClient, visionClient); } + [Fact] + public void CanSetMultipleChatClientsWithDifferentServiceKeys() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:Ollama", $"Endpoint={Endpoint}") + ]); + + // Use one Ollama API client with multiple chat clients using different service keys + builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama") + .AddKeyedChatClient("ChatKey1") + .AddKeyedChatClient("ChatKey2"); + + using var host = builder.Build(); + var chatClient1 = host.Services.GetRequiredKeyedService("ChatKey1"); + var chatClient2 = host.Services.GetRequiredKeyedService("ChatKey2"); + + Assert.Equal(Endpoint, chatClient1.GetService()?.ProviderUri); + Assert.Equal(Endpoint, chatClient2.GetService()?.ProviderUri); + + Assert.NotEqual(chatClient1, chatClient2); + } + + [Fact] + public void CanMixChatClientsAndEmbeddingGeneratorsWithCustomServiceKeys() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:Ollama", $"Endpoint={Endpoint}") + ]); + + // Use one Ollama API client with both chat clients and embedding generators using different service keys + builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama") + .AddKeyedChatClient("ChatKey1") + .AddKeyedChatClient("ChatKey2"); + + using var host = builder.Build(); + var chatClient1 = host.Services.GetRequiredKeyedService("ChatKey1"); + var chatClient2 = host.Services.GetRequiredKeyedService("ChatKey2"); + + Assert.Equal(Endpoint, chatClient1.GetService()?.ProviderUri); + Assert.Equal(Endpoint, chatClient2.GetService()?.ProviderUri); + + Assert.NotEqual(chatClient1, chatClient2); + } + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_InnerClient")] private static extern IChatClient GetInnerClient(DelegatingChatClient client); } diff --git a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIEmbeddingGeneratorTests.cs b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIEmbeddingGeneratorTests.cs index 97c931ed..e3c6ff12 100644 --- a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIEmbeddingGeneratorTests.cs +++ b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIEmbeddingGeneratorTests.cs @@ -149,6 +149,52 @@ public void CanChainUseMethodsCorrectly() Assert.IsType(GetInnerGenerator(otelClient), exactMatch: false); } + [Fact] + public void CanSetMultipleEmbeddingGeneratorsWithDifferentServiceKeys() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:Ollama", $"Endpoint={Endpoint}") + ]); + + // Use one Ollama API client with multiple embedding generators using different service keys + builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama") + .AddKeyedEmbeddingGenerator("EmbedKey1") + .AddKeyedEmbeddingGenerator("EmbedKey2"); + + using var host = builder.Build(); + var embedGenerator1 = host.Services.GetRequiredKeyedService>>("EmbedKey1"); + var embedGenerator2 = host.Services.GetRequiredKeyedService>>("EmbedKey2"); + + Assert.Equal(Endpoint, embedGenerator1.GetService()?.ProviderUri); + Assert.Equal(Endpoint, embedGenerator2.GetService()?.ProviderUri); + + Assert.NotEqual(embedGenerator1, embedGenerator2); + } + + [Fact] + public void CanMixChatClientsAndEmbeddingGeneratorsWithCustomServiceKeys() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:Ollama", $"Endpoint={Endpoint}") + ]); + + // Use one Ollama API client with both chat clients and embedding generators using different service keys + var ollamaBuilder = builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama"); + ollamaBuilder.AddKeyedChatClient("ChatKey"); + ollamaBuilder.AddKeyedEmbeddingGenerator("EmbedKey"); + + using var host = builder.Build(); + var chatClient = host.Services.GetRequiredKeyedService("ChatKey"); + var embedGenerator = host.Services.GetRequiredKeyedService>>("EmbedKey"); + + Assert.Equal(Endpoint, chatClient.GetService()?.ProviderUri); + Assert.Equal(Endpoint, embedGenerator.GetService()?.ProviderUri); + + Assert.NotEqual(chatClient, embedGenerator); + } + private static IEmbeddingGenerator GetInnerGenerator(DelegatingEmbeddingGenerator generator) where TEmbedding : Embedding => (IEmbeddingGenerator)(generator.GetType() From e5f1e83d44069b5772f3279721bd9b9d72f38937 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 27 Jun 2025 03:59:18 +0000 Subject: [PATCH 6/7] Bit of cleanup to how I'd prefer this done --- .../AspireOllamaApiClientBuilder.cs | 4 +-- .../AspireOllamaChatClientExtensions.cs | 5 +-- ...spireOllamaEmbeddingGeneratorExtensions.cs | 6 +--- .../AspireOllamaSharpExtensions.cs | 1 - .../OllamaSharpIChatClientTests.cs | 22 ++++++------- .../OllamaSharpIEmbeddingGeneratorTests.cs | 31 +++---------------- 6 files changed, 19 insertions(+), 50 deletions(-) diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaApiClientBuilder.cs b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaApiClientBuilder.cs index 2580ab33..8d0f6e82 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaApiClientBuilder.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaApiClientBuilder.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.Hosting; /// The with which services are being registered. /// The service key used to register the service, if any. /// A flag to indicate whether tracing should be disabled. -public class AspireOllamaApiClientBuilder(IHostApplicationBuilder hostBuilder, object? serviceKey, bool disableTracing) +public class AspireOllamaApiClientBuilder(IHostApplicationBuilder hostBuilder, object serviceKey, bool disableTracing) { /// /// The host application builder used to configure the application. @@ -18,7 +18,7 @@ public class AspireOllamaApiClientBuilder(IHostApplicationBuilder hostBuilder, o /// /// Gets the service key used to register the service, if any. /// - public object? ServiceKey { get; } = serviceKey; + public object ServiceKey { get; } = serviceKey; /// /// Gets a flag indicating whether tracing should be disabled. diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs index a5abb7d2..adccd168 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs @@ -32,11 +32,8 @@ public static ChatClientBuilder AddKeyedChatClient( this AspireOllamaApiClientBuilder builder) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentNullException.ThrowIfNull(builder.ServiceKey, nameof(builder.ServiceKey)); - return builder.HostBuilder.Services.AddKeyedChatClient( - builder.ServiceKey, - services => CreateInnerChatClient(services, builder)); + return builder.AddKeyedChatClient(builder.ServiceKey); } /// diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaEmbeddingGeneratorExtensions.cs b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaEmbeddingGeneratorExtensions.cs index 9d2f9d43..b68bf33e 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaEmbeddingGeneratorExtensions.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaEmbeddingGeneratorExtensions.cs @@ -31,11 +31,7 @@ public static EmbeddingGeneratorBuilder> AddKeyedEmbedd this AspireOllamaApiClientBuilder builder) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentNullException.ThrowIfNull(builder.ServiceKey, nameof(builder.ServiceKey)); - - return builder.HostBuilder.Services.AddKeyedEmbeddingGenerator( - builder.ServiceKey, - services => CreateInnerEmbeddingGenerator(services, builder)); + return builder.AddKeyedEmbeddingGenerator(builder.ServiceKey); } /// diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaSharpExtensions.cs b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaSharpExtensions.cs index 7b7960c8..6287f94c 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaSharpExtensions.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaSharpExtensions.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using OllamaSharp; using System.Data.Common; diff --git a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs index dc6c0d06..b409e907 100644 --- a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs +++ b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs @@ -142,11 +142,11 @@ public void CanChainUseMethodsCorrectly() using var host = builder.Build(); var client = host.Services.GetRequiredService(); - + var distributedCacheClient = Assert.IsType(client); var functionInvocationClient = Assert.IsType(GetInnerClient(distributedCacheClient)); var otelClient = Assert.IsType(GetInnerClient(functionInvocationClient)); - + Assert.IsType(GetInnerClient(otelClient), exactMatch: false); } @@ -182,9 +182,9 @@ public void CanSetMultipleChatClientsWithDifferentServiceKeys() ]); // Use one Ollama API client with multiple chat clients using different service keys - builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama") - .AddKeyedChatClient("ChatKey1") - .AddKeyedChatClient("ChatKey2"); + var cb = builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama"); + cb.AddKeyedChatClient("ChatKey1"); + cb.AddKeyedChatClient("ChatKey2"); using var host = builder.Build(); var chatClient1 = host.Services.GetRequiredKeyedService("ChatKey1"); @@ -205,18 +205,18 @@ public void CanMixChatClientsAndEmbeddingGeneratorsWithCustomServiceKeys() ]); // Use one Ollama API client with both chat clients and embedding generators using different service keys - builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama") - .AddKeyedChatClient("ChatKey1") - .AddKeyedChatClient("ChatKey2"); + var cb = builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama"); + cb.AddKeyedChatClient("ChatKey1"); + cb.AddKeyedEmbeddingGenerator("EmbeddingKey1"); using var host = builder.Build(); var chatClient1 = host.Services.GetRequiredKeyedService("ChatKey1"); - var chatClient2 = host.Services.GetRequiredKeyedService("ChatKey2"); + var embeddingGenerator = host.Services.GetRequiredKeyedService>>("EmbeddingKey1"); Assert.Equal(Endpoint, chatClient1.GetService()?.ProviderUri); - Assert.Equal(Endpoint, chatClient2.GetService()?.ProviderUri); + Assert.Equal(Endpoint, embeddingGenerator.GetService()?.ProviderUri); - Assert.NotEqual(chatClient1, chatClient2); + Assert.Equal(chatClient1 as IOllamaApiClient, embeddingGenerator as IOllamaApiClient); } [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_InnerClient")] diff --git a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIEmbeddingGeneratorTests.cs b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIEmbeddingGeneratorTests.cs index e3c6ff12..43341d0d 100644 --- a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIEmbeddingGeneratorTests.cs +++ b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIEmbeddingGeneratorTests.cs @@ -158,9 +158,9 @@ public void CanSetMultipleEmbeddingGeneratorsWithDifferentServiceKeys() ]); // Use one Ollama API client with multiple embedding generators using different service keys - builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama") - .AddKeyedEmbeddingGenerator("EmbedKey1") - .AddKeyedEmbeddingGenerator("EmbedKey2"); + var cb = builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama"); + cb.AddKeyedEmbeddingGenerator("EmbedKey1"); + cb.AddKeyedEmbeddingGenerator("EmbedKey2"); using var host = builder.Build(); var embedGenerator1 = host.Services.GetRequiredKeyedService>>("EmbedKey1"); @@ -172,32 +172,9 @@ public void CanSetMultipleEmbeddingGeneratorsWithDifferentServiceKeys() Assert.NotEqual(embedGenerator1, embedGenerator2); } - [Fact] - public void CanMixChatClientsAndEmbeddingGeneratorsWithCustomServiceKeys() - { - var builder = Host.CreateEmptyApplicationBuilder(null); - builder.Configuration.AddInMemoryCollection([ - new KeyValuePair("ConnectionStrings:Ollama", $"Endpoint={Endpoint}") - ]); - - // Use one Ollama API client with both chat clients and embedding generators using different service keys - var ollamaBuilder = builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama"); - ollamaBuilder.AddKeyedChatClient("ChatKey"); - ollamaBuilder.AddKeyedEmbeddingGenerator("EmbedKey"); - - using var host = builder.Build(); - var chatClient = host.Services.GetRequiredKeyedService("ChatKey"); - var embedGenerator = host.Services.GetRequiredKeyedService>>("EmbedKey"); - - Assert.Equal(Endpoint, chatClient.GetService()?.ProviderUri); - Assert.Equal(Endpoint, embedGenerator.GetService()?.ProviderUri); - - Assert.NotEqual(chatClient, embedGenerator); - } - private static IEmbeddingGenerator GetInnerGenerator(DelegatingEmbeddingGenerator generator) where TEmbedding : Embedding => - (IEmbeddingGenerator)(generator.GetType() + (IEmbeddingGenerator)(generator.GetType() .GetProperty("InnerGenerator", BindingFlags.Instance | BindingFlags.NonPublic)? .GetValue(generator, null) ?? throw new InvalidOperationException()); } From 63729e3ab30ede360ca316f788ed42c6eb6122fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Jun 2025 01:47:21 +0000 Subject: [PATCH 7/7] Revert API surface file changes - should be auto-generated Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- .../api/CommunityToolkit.Aspire.OllamaSharp.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/api/CommunityToolkit.Aspire.OllamaSharp.cs b/src/CommunityToolkit.Aspire.OllamaSharp/api/CommunityToolkit.Aspire.OllamaSharp.cs index 2f851fbc..78af13a3 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/api/CommunityToolkit.Aspire.OllamaSharp.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/api/CommunityToolkit.Aspire.OllamaSharp.cs @@ -26,13 +26,13 @@ namespace Microsoft.Extensions.Hosting { public partial class AspireOllamaApiClientBuilder { - public AspireOllamaApiClientBuilder(IHostApplicationBuilder hostBuilder, object? serviceKey, bool disableTracing) { } + public AspireOllamaApiClientBuilder(IHostApplicationBuilder hostBuilder, string serviceKey, bool disableTracing) { } public bool DisableTracing { get { throw null; } } public IHostApplicationBuilder HostBuilder { get { throw null; } } - public object? ServiceKey { get { throw null; } } + public string ServiceKey { get { throw null; } } } public static partial class AspireOllamaChatClientExtensions @@ -40,8 +40,6 @@ public static partial class AspireOllamaChatClientExtensions public static AI.ChatClientBuilder AddChatClient(this AspireOllamaApiClientBuilder builder) { throw null; } public static AI.ChatClientBuilder AddKeyedChatClient(this AspireOllamaApiClientBuilder builder) { throw null; } - - public static AI.ChatClientBuilder AddKeyedChatClient(this AspireOllamaApiClientBuilder builder, object serviceKey) { throw null; } } public static partial class AspireOllamaEmbeddingGeneratorExtensions @@ -49,18 +47,12 @@ public static partial class AspireOllamaEmbeddingGeneratorExtensions public static AI.EmbeddingGeneratorBuilder> AddEmbeddingGenerator(this AspireOllamaApiClientBuilder builder) { throw null; } public static AI.EmbeddingGeneratorBuilder> AddKeyedEmbeddingGenerator(this AspireOllamaApiClientBuilder builder) { throw null; } - - public static AI.EmbeddingGeneratorBuilder> AddKeyedEmbeddingGenerator(this AspireOllamaApiClientBuilder builder, object serviceKey) { throw null; } } public static partial class AspireOllamaSharpExtensions { public static AspireOllamaApiClientBuilder AddKeyedOllamaApiClient(this IHostApplicationBuilder builder, string connectionName, System.Action? configureSettings = null) { throw null; } - public static AspireOllamaApiClientBuilder AddKeyedOllamaApiClient(this IHostApplicationBuilder builder, object serviceKey, string connectionName, System.Action? configureSettings = null) { throw null; } - - public static AspireOllamaApiClientBuilder AddKeyedOllamaApiClient(this IHostApplicationBuilder builder, object serviceKey, CommunityToolkit.Aspire.OllamaSharp.OllamaSharpSettings settings) { throw null; } - [System.Obsolete("This approach to registering IChatClient is deprecated, use AddKeyedOllamaApiClient().AddChatClient() instead.")] public static void AddKeyedOllamaSharpChatClient(this IHostApplicationBuilder builder, string connectionName, System.Action? configureSettings = null) { }