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 @@ -432,10 +432,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.
IEmbeddingGenerator<string, Embedding<float>> generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>()
var generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>(
new SampleEmbeddingGenerator(new Uri("http://coolsite.ai"), "my-custom-model"))
.UseDistributedCache(new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())))
.UseOpenTelemetry(sourceName)
.Use(new SampleEmbeddingGenerator(new Uri("http://coolsite.ai"), "my-custom-model"));
.Build();

var embeddings = await generator.GenerateAsync(
[
Expand Down
7 changes: 3 additions & 4 deletions src/Libraries/Microsoft.Extensions.AI.Ollama/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ IDistributedCache cache = new MemoryDistributedCache(Options.Create(new MemoryDi
IEmbeddingGenerator<string, Embedding<float>> ollamaGenerator =
new OllamaEmbeddingGenerator(new Uri("http://localhost:11434/"), "all-minilm");

IEmbeddingGenerator<string, Embedding<float>> generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>()
IEmbeddingGenerator<string, Embedding<float>> generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>(ollamaGenerator)
.UseDistributedCache(cache)
.Use(ollamaGenerator);
.Build();

foreach (var prompt in new[] { "What is AI?", "What is .NET?", "What is AI?" })
{
Expand Down Expand Up @@ -256,8 +256,7 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddChatClient(
new OllamaChatClient(new Uri("http://localhost:11434/"), "llama3.1"));

builder.Services.AddEmbeddingGenerator<string,Embedding<float>>(g =>
g.Use(new OllamaEmbeddingGenerator(endpoint, "all-minilm")));
builder.Services.AddEmbeddingGenerator(new OllamaEmbeddingGenerator(endpoint, "all-minilm"));

var app = builder.Build();

Expand Down
8 changes: 4 additions & 4 deletions src/Libraries/Microsoft.Extensions.AI.OpenAI/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,9 @@ IEmbeddingGenerator<string, Embedding<float>> openAIGenerator =
new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY"))
.AsEmbeddingGenerator("text-embedding-3-small");

IEmbeddingGenerator<string, Embedding<float>> generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>()
IEmbeddingGenerator<string, Embedding<float>> generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>(openAIGenerator)
.UseDistributedCache(cache)
.Use(openAIGenerator);
.Build();

foreach (var prompt in new[] { "What is AI?", "What is .NET?", "What is AI?" })
{
Expand Down Expand Up @@ -284,8 +284,8 @@ builder.Services.AddSingleton(new OpenAIClient(builder.Configuration["OPENAI_API
builder.Services.AddChatClient(services =>
services.GetRequiredService<OpenAIClient>().AsChatClient("gpt-4o-mini"));

builder.Services.AddEmbeddingGenerator<string, Embedding<float>>(g =>
g.Use(g.Services.GetRequiredService<OpenAIClient>().AsEmbeddingGenerator("text-embedding-3-small")));
builder.Services.AddEmbeddingGenerator(services =>
services.GetRequiredService<OpenAIClient>().AsEmbeddingGenerator("text-embedding-3-small"));

var app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI;
/// <summary>A builder for creating pipelines of <see cref="IChatClient"/>.</summary>
public sealed class ChatClientBuilder
{
private Func<IServiceProvider, IChatClient> _innerClientFactory;
private readonly Func<IServiceProvider, IChatClient> _innerClientFactory;

/// <summary>The registered client factory instances.</summary>
private List<Func<IServiceProvider, IChatClient, IChatClient>>? _clientFactories;
Expand All @@ -30,7 +30,7 @@ public ChatClientBuilder(Func<IServiceProvider, IChatClient> innerClientFactory)
_innerClientFactory = Throw.IfNull(innerClientFactory);
}

/// <summary>Returns an <see cref="IChatClient"/> that represents the entire pipeline. Calls to this instance will pass through each of the pipeline stages in turn.</summary>
/// <summary>Builds an <see cref="IChatClient"/> that represents the entire pipeline. Calls to this instance will pass through each of the pipeline stages in turn.</summary>
/// <param name="services">
/// The <see cref="IServiceProvider"/> that should provide services to the <see cref="IChatClient"/> instances.
/// If null, an empty <see cref="IServiceProvider"/> will be used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static ChatClientBuilder AddChatClient(
return builder;
}

/// <summary>Registers a singleton <see cref="IChatClient"/> in the <see cref="IServiceCollection"/>.</summary>
/// <summary>Registers a keyed singleton <see cref="IChatClient"/> in the <see cref="IServiceCollection"/>.</summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the client should be added.</param>
/// <param name="serviceKey">The key with which to associate the client.</param>
/// <param name="innerClient">The inner <see cref="IChatClient"/> that represents the underlying backend.</param>
Expand All @@ -49,7 +49,7 @@ public static ChatClientBuilder AddKeyedChatClient(
IChatClient innerClient)
=> AddKeyedChatClient(serviceCollection, serviceKey, _ => innerClient);

/// <summary>Registers a singleton <see cref="IChatClient"/> in the <see cref="IServiceCollection"/>.</summary>
/// <summary>Registers a keyed singleton <see cref="IChatClient"/> in the <see cref="IServiceCollection"/>.</summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the client should be added.</param>
/// <param name="serviceKey">The key with which to associate the client.</param>
/// <param name="innerClientFactory">A callback that produces the inner <see cref="IChatClient"/> that represents the underlying backend.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,45 @@ namespace Microsoft.Extensions.AI;
public sealed class EmbeddingGeneratorBuilder<TInput, TEmbedding>
where TEmbedding : Embedding
{
private readonly Func<IServiceProvider, IEmbeddingGenerator<TInput, TEmbedding>> _innerGeneratorFactory;

/// <summary>The registered client factory instances.</summary>
private List<Func<IServiceProvider, IEmbeddingGenerator<TInput, TEmbedding>, IEmbeddingGenerator<TInput, TEmbedding>>>? _generatorFactories;

/// <summary>Initializes a new instance of the <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> class.</summary>
/// <param name="services">The service provider to use for dependency injection.</param>
public EmbeddingGeneratorBuilder(IServiceProvider? services = null)
/// <param name="innerGenerator">The inner <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> that represents the underlying backend.</param>
public EmbeddingGeneratorBuilder(IEmbeddingGenerator<TInput, TEmbedding> innerGenerator)
{
Services = services ?? EmptyServiceProvider.Instance;
_ = Throw.IfNull(innerGenerator);
_innerGeneratorFactory = _ => innerGenerator;
}

/// <summary>Gets the <see cref="IServiceProvider"/> associated with the builder instance.</summary>
public IServiceProvider Services { get; }
/// <summary>Initializes a new instance of the <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> class.</summary>
/// <param name="innerGeneratorFactory">A callback that produces the inner <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> that represents the underlying backend.</param>
public EmbeddingGeneratorBuilder(Func<IServiceProvider, IEmbeddingGenerator<TInput, TEmbedding>> innerGeneratorFactory)
{
_innerGeneratorFactory = Throw.IfNull(innerGeneratorFactory);
}

/// <summary>
/// Builds an instance of <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> using the specified inner generator.
/// Builds an <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> that represents the entire pipeline. Calls to this instance will pass through each of the pipeline stages in turn.
/// </summary>
/// <param name="innerGenerator">The inner generator to use.</param>
/// <returns>An instance of <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/>.</returns>
/// <remarks>
/// If there are any factories registered with this builder, <paramref name="innerGenerator"/> is used as a seed to
/// the last factory, and the result of each factory delegate is passed to the previously registered factory.
/// The final result is then returned from this call.
/// </remarks>
public IEmbeddingGenerator<TInput, TEmbedding> Use(IEmbeddingGenerator<TInput, TEmbedding> innerGenerator)
/// <param name="services">
/// The <see cref="IServiceProvider"/> that should provide services to the <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> instances.
/// If null, an empty <see cref="IServiceProvider"/> will be used.
/// </param>
/// <returns>An instance of <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> that represents the entire pipeline.</returns>
public IEmbeddingGenerator<TInput, TEmbedding> Build(IServiceProvider? services = null)
{
var embeddingGenerator = Throw.IfNull(innerGenerator);
services ??= EmptyServiceProvider.Instance;
var embeddingGenerator = _innerGeneratorFactory(services);

// To match intuitive expectations, apply the factories in reverse order, so that the first factory added is the outermost.
if (_generatorFactories is not null)
{
for (var i = _generatorFactories.Count - 1; i >= 0; i--)
{
embeddingGenerator = _generatorFactories[i](Services, embeddingGenerator) ??
embeddingGenerator = _generatorFactories[i](services, embeddingGenerator) ??
throw new InvalidOperationException(
$"The {nameof(IEmbeddingGenerator<TInput, TEmbedding>)} entry at index {i} returned null. " +
$"Ensure that the callbacks passed to {nameof(Use)} return non-null {nameof(IEmbeddingGenerator<TInput, TEmbedding>)} instances.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,74 @@ namespace Microsoft.Extensions.DependencyInjection;
/// <summary>Provides extension methods for registering <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> with a <see cref="IServiceCollection"/>.</summary>
public static class EmbeddingGeneratorBuilderServiceCollectionExtensions
{
/// <summary>Adds a embedding generator to the <see cref="IServiceCollection"/>.</summary>
/// <summary>Registers a singleton embedding generator in the <see cref="IServiceCollection"/>.</summary>
/// <typeparam name="TInput">The type from which embeddings will be generated.</typeparam>
/// <typeparam name="TEmbedding">The type of embeddings to generate.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to which the generator should be added.</param>
/// <param name="generatorFactory">The factory to use to construct the <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> instance.</param>
/// <returns>The <paramref name="services"/> collection.</returns>
/// <remarks>The generator is registered as a scoped service.</remarks>
public static IServiceCollection AddEmbeddingGenerator<TInput, TEmbedding>(
this IServiceCollection services,
Func<EmbeddingGeneratorBuilder<TInput, TEmbedding>, IEmbeddingGenerator<TInput, TEmbedding>> generatorFactory)
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the generator should be added.</param>
/// <param name="innerGenerator">The inner <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> that represents the underlying backend.</param>
/// <returns>An <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> that can be used to build a pipeline around the inner generator.</returns>
/// <remarks>The generator is registered as a singleton service.</remarks>
public static EmbeddingGeneratorBuilder<TInput, TEmbedding> AddEmbeddingGenerator<TInput, TEmbedding>(
this IServiceCollection serviceCollection,
IEmbeddingGenerator<TInput, TEmbedding> innerGenerator)
where TEmbedding : Embedding
=> AddEmbeddingGenerator(serviceCollection, _ => innerGenerator);

/// <summary>Registers a singleton embedding generator in the <see cref="IServiceCollection"/>.</summary>
/// <typeparam name="TInput">The type from which embeddings will be generated.</typeparam>
/// <typeparam name="TEmbedding">The type of embeddings to generate.</typeparam>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the generator should be added.</param>
/// <param name="innerGeneratorFactory">A callback that produces the inner <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> that represents the underlying backend.</param>
/// <returns>An <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> that can be used to build a pipeline around the inner generator.</returns>
/// <remarks>The generator is registered as a singleton service.</remarks>
public static EmbeddingGeneratorBuilder<TInput, TEmbedding> AddEmbeddingGenerator<TInput, TEmbedding>(
this IServiceCollection serviceCollection,
Func<IServiceProvider, IEmbeddingGenerator<TInput, TEmbedding>> innerGeneratorFactory)
where TEmbedding : Embedding
{
_ = Throw.IfNull(services);
_ = Throw.IfNull(generatorFactory);
_ = Throw.IfNull(serviceCollection);
_ = Throw.IfNull(innerGeneratorFactory);

return services.AddScoped(services =>
generatorFactory(new EmbeddingGeneratorBuilder<TInput, TEmbedding>(services)));
var builder = new EmbeddingGeneratorBuilder<TInput, TEmbedding>(innerGeneratorFactory);
_ = serviceCollection.AddSingleton(builder.Build);
return builder;
}

/// <summary>Adds an embedding generator to the <see cref="IServiceCollection"/>.</summary>
/// <summary>Registers a keyed singleton embedding generator in the <see cref="IServiceCollection"/>.</summary>
/// <typeparam name="TInput">The type from which embeddings will be generated.</typeparam>
/// <typeparam name="TEmbedding">The type of embeddings to generate.</typeparam>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the generator should be added.</param>
/// <param name="serviceKey">The key with which to associated the generator.</param>
/// <param name="innerGenerator">The inner <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> that represents the underlying backend.</param>
/// <returns>An <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> that can be used to build a pipeline around the inner generator.</returns>
/// <remarks>The generator is registered as a singleton service.</remarks>
public static EmbeddingGeneratorBuilder<TInput, TEmbedding> AddKeyedEmbeddingGenerator<TInput, TEmbedding>(
this IServiceCollection serviceCollection,
object serviceKey,
IEmbeddingGenerator<TInput, TEmbedding> innerGenerator)
where TEmbedding : Embedding
=> AddKeyedEmbeddingGenerator(serviceCollection, serviceKey, _ => innerGenerator);

/// <summary>Registers a keyed singleton embedding generator in the <see cref="IServiceCollection"/>.</summary>
/// <typeparam name="TInput">The type from which embeddings will be generated.</typeparam>
/// <typeparam name="TEmbedding">The type of embeddings to generate.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to which the service should be added.</param>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the generator should be added.</param>
/// <param name="serviceKey">The key with which to associated the generator.</param>
/// <param name="generatorFactory">The factory to use to construct the <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> instance.</param>
/// <returns>The <paramref name="services"/> collection.</returns>
/// <remarks>The generator is registered as a scoped service.</remarks>
public static IServiceCollection AddKeyedEmbeddingGenerator<TInput, TEmbedding>(
this IServiceCollection services,
/// <param name="innerGeneratorFactory">A callback that produces the inner <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> that represents the underlying backend.</param>
/// <returns>An <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> that can be used to build a pipeline around the inner generator.</returns>
/// <remarks>The generator is registered as a singleton service.</remarks>
public static EmbeddingGeneratorBuilder<TInput, TEmbedding> AddKeyedEmbeddingGenerator<TInput, TEmbedding>(
this IServiceCollection serviceCollection,
object serviceKey,
Func<EmbeddingGeneratorBuilder<TInput, TEmbedding>, IEmbeddingGenerator<TInput, TEmbedding>> generatorFactory)
Func<IServiceProvider, IEmbeddingGenerator<TInput, TEmbedding>> innerGeneratorFactory)
where TEmbedding : Embedding
{
_ = Throw.IfNull(services);
_ = Throw.IfNull(serviceCollection);
_ = Throw.IfNull(serviceKey);
_ = Throw.IfNull(generatorFactory);
_ = Throw.IfNull(innerGeneratorFactory);

return services.AddKeyedScoped(serviceKey, (services, _) =>
generatorFactory(new EmbeddingGeneratorBuilder<TInput, TEmbedding>(services)));
var builder = new EmbeddingGeneratorBuilder<TInput, TEmbedding>(innerGeneratorFactory);
_ = serviceCollection.AddKeyedSingleton(serviceKey, (services, _) => builder.Build(services));
return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ public void GetService_SuccessfullyReturnsUnderlyingClient()
Assert.Same(embeddingGenerator, embeddingGenerator.GetService<IEmbeddingGenerator<string, Embedding<float>>>());
Assert.Same(client, embeddingGenerator.GetService<EmbeddingsClient>());

using IEmbeddingGenerator<string, Embedding<float>> pipeline = new EmbeddingGeneratorBuilder<string, Embedding<float>>()
using IEmbeddingGenerator<string, Embedding<float>> pipeline = new EmbeddingGeneratorBuilder<string, Embedding<float>>(embeddingGenerator)
.UseOpenTelemetry()
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))
.Use(embeddingGenerator);
.Build();

Assert.NotNull(pipeline.GetService<DistributedCachingEmbeddingGenerator<string, Embedding<float>>>());
Assert.NotNull(pipeline.GetService<CachingEmbeddingGenerator<string, Embedding<float>>>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ public virtual async Task Caching_SameOutputsForSameInput()
{
SkipIfNotEnabled();

using var generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>()
using var generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>(CreateEmbeddingGenerator()!)
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))
.UseCallCounting()
.Use(CreateEmbeddingGenerator()!);
.Build();

string input = "Red, White, and Blue";
var embedding1 = await generator.GenerateEmbeddingAsync(input);
Expand All @@ -110,9 +110,9 @@ public virtual async Task OpenTelemetry_CanEmitTracesAndMetrics()
.AddInMemoryExporter(activities)
.Build();

var embeddingGenerator = new EmbeddingGeneratorBuilder<string, Embedding<float>>()
var embeddingGenerator = new EmbeddingGeneratorBuilder<string, Embedding<float>>(CreateEmbeddingGenerator()!)
.UseOpenTelemetry(sourceName: sourceName)
.Use(CreateEmbeddingGenerator()!);
.Build();

_ = await embeddingGenerator.GenerateEmbeddingAsync("Hello, world!");

Expand Down
Loading
Loading