Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OllamaSharp;
using OpenTelemetry;

namespace Microsoft.Extensions.Hosting;

Expand All @@ -10,6 +11,8 @@ namespace Microsoft.Extensions.Hosting;
/// </summary>
public static class AspireOllamaChatClientExtensions
{
private const string MeaiTelemetrySourceName = "Experimental.Microsoft.Extensions.AI";

/// <summary>
/// Registers a singleton <see cref="IChatClient"/> in the services provided by the <paramref name="builder"/>.
/// </summary>
Expand All @@ -19,8 +22,25 @@ public static ChatClientBuilder AddChatClient(this AspireOllamaApiClientBuilder
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));

return builder.AddChatClient(configureOpenTelemetry: null);
}

/// <summary>
/// Registers a singleton <see cref="IChatClient"/> in the services provided by the <paramref name="builder"/>.
/// </summary>
/// <param name="builder">An <see cref="AspireOllamaApiClientBuilder" />.</param>
/// <param name="configureOpenTelemetry">An optional delegate that can be used for customizing the OpenTelemetry chat client.</param>
/// <returns>A <see cref="ChatClientBuilder"/> that can be used to build a pipeline around the inner <see cref="IChatClient"/>.</returns>
public static ChatClientBuilder AddChatClient(
this AspireOllamaApiClientBuilder builder,
Action<OpenTelemetryChatClient>? configureOpenTelemetry)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));

AddTelemetrySource(builder.HostBuilder);

return builder.HostBuilder.Services.AddChatClient(
services => CreateInnerChatClient(services, builder));
services => CreateInnerChatClient(services, builder, configureOpenTelemetry));
}

/// <summary>
Expand All @@ -33,7 +53,22 @@ public static ChatClientBuilder AddKeyedChatClient(
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));

return builder.AddKeyedChatClient(builder.ServiceKey);
return builder.AddKeyedChatClient(builder.ServiceKey, configureOpenTelemetry: null);
}

/// <summary>
/// Registers a keyed singleton <see cref="IChatClient"/> in the services provided by the <paramref name="builder"/>.
/// </summary>
/// <param name="builder">An <see cref="AspireOllamaApiClientBuilder" />.</param>
/// <param name="configureOpenTelemetry">An optional delegate that can be used for customizing the OpenTelemetry chat client.</param>
/// <returns>A <see cref="ChatClientBuilder"/> that can be used to build a pipeline around the inner <see cref="IChatClient"/>.</returns>
public static ChatClientBuilder AddKeyedChatClient(
this AspireOllamaApiClientBuilder builder,
Action<OpenTelemetryChatClient>? configureOpenTelemetry)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));

return builder.AddKeyedChatClient(builder.ServiceKey, configureOpenTelemetry);
}

/// <summary>
Expand All @@ -49,9 +84,29 @@ public static ChatClientBuilder AddKeyedChatClient(
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
ArgumentNullException.ThrowIfNull(serviceKey, nameof(serviceKey));

return builder.AddKeyedChatClient(serviceKey, configureOpenTelemetry: null);
}

/// <summary>
/// Registers a keyed singleton <see cref="IChatClient"/> in the services provided by the <paramref name="builder"/> using the specified service key.
/// </summary>
/// <param name="builder">An <see cref="AspireOllamaApiClientBuilder" />.</param>
/// <param name="serviceKey">The service key to use for registering the <see cref="IChatClient"/>.</param>
/// <param name="configureOpenTelemetry">An optional delegate that can be used for customizing the OpenTelemetry chat client.</param>
/// <returns>A <see cref="ChatClientBuilder"/> that can be used to build a pipeline around the inner <see cref="IChatClient"/>.</returns>
public static ChatClientBuilder AddKeyedChatClient(
this AspireOllamaApiClientBuilder builder,
object serviceKey,
Action<OpenTelemetryChatClient>? configureOpenTelemetry)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
ArgumentNullException.ThrowIfNull(serviceKey, nameof(serviceKey));

AddTelemetrySource(builder.HostBuilder);

return builder.HostBuilder.Services.AddKeyedChatClient(
serviceKey,
services => CreateInnerChatClient(services, builder));
services => CreateInnerChatClient(services, builder, configureOpenTelemetry));
}

/// <summary>
Expand All @@ -61,7 +116,8 @@ public static ChatClientBuilder AddKeyedChatClient(
/// </summary>
private static IChatClient CreateInnerChatClient(
IServiceProvider services,
AspireOllamaApiClientBuilder builder)
AspireOllamaApiClientBuilder builder,
Action<OpenTelemetryChatClient>? configureOpenTelemetry)
{
var ollamaApiClient = services.GetRequiredKeyedService<IOllamaApiClient>(builder.ServiceKey);

Expand All @@ -73,6 +129,22 @@ private static IChatClient CreateInnerChatClient(
}

var loggerFactory = services.GetService<ILoggerFactory>();
return new OpenTelemetryChatClient(result, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient)));
var otelChatClient = new OpenTelemetryChatClient(result, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient)), MeaiTelemetrySourceName);

configureOpenTelemetry?.Invoke(otelChatClient);

return otelChatClient;
}

/// <summary>
/// Add the MEAI telemetry source to OpenTelemetry tracing.
/// </summary>
private static void AddTelemetrySource(IHostApplicationBuilder hostBuilder)
{
hostBuilder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing.AddSource(MeaiTelemetrySourceName);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="OllamaSharp" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ public sealed class OllamaSharpSettings
/// Gets or sets a boolean value that indicates whether tracing is disabled or not.
/// </summary>
/// <remarks>Currently, the OllamaSharp SDK does not support tracing, but this is here for future use.</remarks>
internal bool DisableTracing { get; set; }
public bool DisableTracing { get; set; }

}
11 changes: 11 additions & 0 deletions src/CommunityToolkit.Aspire.OllamaSharp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ public class MyService(IOllamaApiClient ollamaApiClient)

To use the integration with Microsoft.Extensions.AI, call the `AddOllamaSharpChatClient` or `AddOllamaSharpEmbeddingGenerator` extension method in the _Program.cs_ file of your project. These methods take the connection name as a parameter, just as `AddOllamaApiClient` does, and will register the `IOllamaApiClient`, as well as the `IChatClient` or `IEmbeddingGenerator` in the DI container. The `IEmbeddingsGenerator` is registered with the generic arguments of `<string, Embedding<float>>`.

#### Configuring OpenTelemetry

When using the chat client integration, you can optionally configure the OpenTelemetry chat client to control telemetry behavior such as enabling sensitive data:

```csharp
builder.AddOllamaApiClient("ollama")
.AddChatClient(otel => otel.EnableSensitiveData = true);
```

The integration automatically registers the Microsoft.Extensions.AI telemetry source (`Experimental.Microsoft.Extensions.AI`) with OpenTelemetry for distributed tracing.

## Additional documentation

- https://github.com/awaescher/OllamaSharp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,83 @@ public void CanMixChatClientsAndEmbeddingGeneratorsWithCustomServiceKeys()
Assert.Equal(chatClient1 as IOllamaApiClient, embeddingGenerator as IOllamaApiClient);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void CanConfigureOpenTelemetrySensitiveData(bool useKeyed)
{
var builder = Host.CreateEmptyApplicationBuilder(null);
builder.Configuration.AddInMemoryCollection([
new KeyValuePair<string, string?>("ConnectionStrings:Ollama", $"Endpoint={Endpoint}")
]);

if (useKeyed)
{
builder.AddKeyedOllamaApiClient("Ollama").AddKeyedChatClient(otel => otel.EnableSensitiveData = true);
}
else
{
builder.AddOllamaApiClient("Ollama").AddChatClient(otel => otel.EnableSensitiveData = true);
}

using var host = builder.Build();
var client = useKeyed ?
host.Services.GetRequiredKeyedService<IChatClient>("Ollama") :
host.Services.GetRequiredService<IChatClient>();

// Navigate through the client chain to find the OpenTelemetryChatClient
var otelClient = Assert.IsType<OpenTelemetryChatClient>(client);
Assert.True(otelClient.EnableSensitiveData);
}

[Fact]
public void CanConfigureOpenTelemetrySensitiveDataWithCustomServiceKey()
{
var builder = Host.CreateEmptyApplicationBuilder(null);
builder.Configuration.AddInMemoryCollection([
new KeyValuePair<string, string?>("ConnectionStrings:Ollama", $"Endpoint={Endpoint}")
]);

builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama")
.AddKeyedChatClient("ChatKey", otel => otel.EnableSensitiveData = true);

using var host = builder.Build();
var client = host.Services.GetRequiredKeyedService<IChatClient>("ChatKey");

var otelClient = Assert.IsType<OpenTelemetryChatClient>(client);
Assert.True(otelClient.EnableSensitiveData);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void OpenTelemetryConfigurationIsOptional(bool useKeyed)
{
var builder = Host.CreateEmptyApplicationBuilder(null);
builder.Configuration.AddInMemoryCollection([
new KeyValuePair<string, string?>("ConnectionStrings:Ollama", $"Endpoint={Endpoint}")
]);

// Test that we can still call the methods without configuration
if (useKeyed)
{
builder.AddKeyedOllamaApiClient("Ollama").AddKeyedChatClient(configureOpenTelemetry: null);
}
else
{
builder.AddOllamaApiClient("Ollama").AddChatClient(configureOpenTelemetry: null);
}

using var host = builder.Build();
var client = useKeyed ?
host.Services.GetRequiredKeyedService<IChatClient>("Ollama") :
host.Services.GetRequiredService<IChatClient>();

var otelClient = Assert.IsType<OpenTelemetryChatClient>(client);
// EnableSensitiveData should be false by default
Assert.False(otelClient.EnableSensitiveData);
}

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_InnerClient")]
private static extern IChatClient GetInnerClient(DelegatingChatClient client);
}
Loading