Add AsBuilder extensions for IChatClient and IEmbeddingGenerator (#5652)

* Add ToBuilder extensions for IChatClient and IEmbeddingGenerator

Enables a fluent style of construction of a pipeline from a client/generator, and not having to specify the generic type parameters for the embedding generator builder.

* Rename ToBuilder to AsBuilder
This commit is contained in:
Stephen Toub 2024-11-18 10:12:30 -05:00 коммит произвёл GitHub
Родитель c4689473f5
Коммит 930af05f2b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
22 изменённых файлов: 115 добавлений и 31 удалений

Просмотреть файл

@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
/// <summary>Provides extension methods for working with <see cref="IChatClient"/> in the context of <see cref="ChatClientBuilder"/>.</summary>
public static class ChatClientBuilderChatClientExtensions
{
/// <summary>Creates a new <see cref="ChatClientBuilder"/> using <paramref name="innerClient"/> as its inner client.</summary>
/// <param name="innerClient">The client to use as the inner client.</param>
/// <returns>The new <see cref="ChatClientBuilder"/> instance.</returns>
/// <remarks>
/// This method is equivalent to using the <see cref="ChatClientBuilder"/> constructor directly,
/// specifying <paramref name="innerClient"/> as the inner client.
/// </remarks>
public static ChatClientBuilder AsBuilder(this IChatClient innerClient)
{
_ = Throw.IfNull(innerClient);
return new ChatClientBuilder(innerClient);
}
}

Просмотреть файл

@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
/// <summary>Provides extension methods for working with <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/>
/// in the context of <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/>.</summary>
public static class EmbeddingGeneratorBuilderEmbeddingGeneratorExtensions
{
/// <summary>
/// Creates a new <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> using
/// <paramref name="innerGenerator"/> as its inner generator.
/// </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="innerGenerator">The generator to use as the inner generator.</param>
/// <returns>The new <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> instance.</returns>
/// <remarks>
/// This method is equivalent to using the <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/>
/// constructor directly, specifying <paramref name="innerGenerator"/> as the inner generator.
/// </remarks>
public static EmbeddingGeneratorBuilder<TInput, TEmbedding> AsBuilder<TInput, TEmbedding>(
this IEmbeddingGenerator<TInput, TEmbedding> innerGenerator)
where TEmbedding : Embedding
{
_ = Throw.IfNull(innerGenerator);
return new EmbeddingGeneratorBuilder<TInput, TEmbedding>(innerGenerator);
}
}

Просмотреть файл

@ -77,7 +77,8 @@ public class AzureAIInferenceChatClientTests
Assert.Same(client, chatClient.GetService<ChatCompletionsClient>());
using IChatClient pipeline = new ChatClientBuilder(chatClient)
using IChatClient pipeline = chatClient
.AsBuilder()
.UseFunctionInvocation()
.UseOpenTelemetry()
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))

Просмотреть файл

@ -63,7 +63,8 @@ public class AzureAIInferenceEmbeddingGeneratorTests
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>>(embeddingGenerator)
using IEmbeddingGenerator<string, Embedding<float>> pipeline = embeddingGenerator
.AsBuilder()
.UseOpenTelemetry()
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))
.Build();

Просмотреть файл

@ -377,7 +377,8 @@ public abstract class ChatClientIntegrationTests : IDisposable
}, "GetTemperature");
// First call executes the function and calls the LLM
using var chatClient = new ChatClientBuilder(CreateChatClient()!)
using var chatClient = CreateChatClient()!
.AsBuilder()
.ConfigureOptions(options => options.Tools = [getTemperature])
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))
.UseFunctionInvocation()
@ -415,7 +416,8 @@ public abstract class ChatClientIntegrationTests : IDisposable
}, "GetTemperature");
// First call executes the function and calls the LLM
using var chatClient = new ChatClientBuilder(CreateChatClient()!)
using var chatClient = CreateChatClient()!
.AsBuilder()
.ConfigureOptions(options => options.Tools = [getTemperature])
.UseFunctionInvocation()
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))
@ -454,7 +456,8 @@ public abstract class ChatClientIntegrationTests : IDisposable
}, "GetTemperature");
// First call executes the function and calls the LLM
using var chatClient = new ChatClientBuilder(CreateChatClient()!)
using var chatClient = CreateChatClient()!
.AsBuilder()
.ConfigureOptions(options => options.Tools = [getTemperature])
.UseFunctionInvocation()
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))
@ -573,7 +576,7 @@ public abstract class ChatClientIntegrationTests : IDisposable
.AddInMemoryExporter(activities)
.Build();
var chatClient = new ChatClientBuilder(CreateChatClient()!)
var chatClient = CreateChatClient()!.AsBuilder()
.UseOpenTelemetry(sourceName: sourceName)
.Build();

Просмотреть файл

@ -81,7 +81,8 @@ public abstract class EmbeddingGeneratorIntegrationTests : IDisposable
{
SkipIfNotEnabled();
using var generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>(CreateEmbeddingGenerator()!)
using var generator = CreateEmbeddingGenerator()!
.AsBuilder()
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))
.UseCallCounting()
.Build();
@ -110,7 +111,8 @@ public abstract class EmbeddingGeneratorIntegrationTests : IDisposable
.AddInMemoryExporter(activities)
.Build();
var embeddingGenerator = new EmbeddingGeneratorBuilder<string, Embedding<float>>(CreateEmbeddingGenerator()!)
var embeddingGenerator = CreateEmbeddingGenerator()!
.AsBuilder()
.UseOpenTelemetry(sourceName: sourceName)
.Build();

Просмотреть файл

@ -37,7 +37,8 @@ public class ReducingChatClientTests
}
};
using var client = new ChatClientBuilder(innerClient)
using var client = innerClient
.AsBuilder()
.UseChatReducer(new TokenCountingChatReducer(_gpt4oTokenizer, 40))
.Build();

Просмотреть файл

@ -37,7 +37,8 @@ public class OllamaChatClientIntegrationTests : ChatClientIntegrationTests
{
SkipIfNotEnabled();
using var chatClient = new ChatClientBuilder(CreateChatClient()!)
using var chatClient = CreateChatClient()!
.AsBuilder()
.UseFunctionInvocation()
.UsePromptBasedFunctionCalling()
.Use(innerClient => new AssertNoToolsDefinedChatClient(innerClient))
@ -61,7 +62,8 @@ public class OllamaChatClientIntegrationTests : ChatClientIntegrationTests
{
SkipIfNotEnabled();
using var chatClient = new ChatClientBuilder(CreateChatClient()!)
using var chatClient = CreateChatClient()!
.AsBuilder()
.UseFunctionInvocation()
.UsePromptBasedFunctionCalling()
.Use(innerClient => new AssertNoToolsDefinedChatClient(innerClient))

Просмотреть файл

@ -48,7 +48,8 @@ public class OllamaChatClientTests
Assert.Same(client, client.GetService<OllamaChatClient>());
Assert.Same(client, client.GetService<IChatClient>());
using IChatClient pipeline = new ChatClientBuilder(client)
using IChatClient pipeline = client
.AsBuilder()
.UseFunctionInvocation()
.UseOpenTelemetry()
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))

Просмотреть файл

@ -29,7 +29,8 @@ public class OllamaEmbeddingGeneratorTests
Assert.Same(generator, generator.GetService<OllamaEmbeddingGenerator>());
Assert.Same(generator, generator.GetService<IEmbeddingGenerator<string, Embedding<float>>>());
using IEmbeddingGenerator<string, Embedding<float>> pipeline = new EmbeddingGeneratorBuilder<string, Embedding<float>>(generator)
using IEmbeddingGenerator<string, Embedding<float>> pipeline = generator
.AsBuilder()
.UseOpenTelemetry()
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))
.Build();

Просмотреть файл

@ -95,7 +95,8 @@ public class OpenAIChatClientTests
Assert.NotNull(chatClient.GetService<ChatClient>());
using IChatClient pipeline = new ChatClientBuilder(chatClient)
using IChatClient pipeline = chatClient
.AsBuilder()
.UseFunctionInvocation()
.UseOpenTelemetry()
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))
@ -119,7 +120,8 @@ public class OpenAIChatClientTests
Assert.Same(chatClient, chatClient.GetService<IChatClient>());
Assert.Same(openAIClient, chatClient.GetService<ChatClient>());
using IChatClient pipeline = new ChatClientBuilder(chatClient)
using IChatClient pipeline = chatClient
.AsBuilder()
.UseFunctionInvocation()
.UseOpenTelemetry()
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))

Просмотреть файл

@ -78,7 +78,8 @@ public class OpenAIEmbeddingGeneratorTests
Assert.NotNull(embeddingGenerator.GetService<EmbeddingClient>());
using IEmbeddingGenerator<string, Embedding<float>> pipeline = new EmbeddingGeneratorBuilder<string, Embedding<float>>(embeddingGenerator)
using IEmbeddingGenerator<string, Embedding<float>> pipeline = embeddingGenerator
.AsBuilder()
.UseOpenTelemetry()
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))
.Build();
@ -100,7 +101,8 @@ public class OpenAIEmbeddingGeneratorTests
Assert.Same(embeddingGenerator, embeddingGenerator.GetService<IEmbeddingGenerator<string, Embedding<float>>>());
Assert.Same(openAIClient, embeddingGenerator.GetService<EmbeddingClient>());
using IEmbeddingGenerator<string, Embedding<float>> pipeline = new EmbeddingGeneratorBuilder<string, Embedding<float>>(embeddingGenerator)
using IEmbeddingGenerator<string, Embedding<float>> pipeline = embeddingGenerator
.AsBuilder()
.UseOpenTelemetry()
.UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions())))
.Build();

Просмотреть файл

@ -59,6 +59,7 @@ public class ChatClientBuilderTest
public void DoesNotAcceptNullInnerService()
{
Assert.Throws<ArgumentNullException>("innerClient", () => new ChatClientBuilder((IChatClient)null!));
Assert.Throws<ArgumentNullException>("innerClient", () => ((IChatClient)null!).AsBuilder());
}
[Fact]

Просмотреть файл

@ -23,7 +23,7 @@ public class ConfigureOptionsChatClientTests
public void ConfigureOptions_InvalidArgs_Throws()
{
using var innerClient = new TestChatClient();
var builder = new ChatClientBuilder(innerClient);
var builder = innerClient.AsBuilder();
Assert.Throws<ArgumentNullException>("configure", () => builder.ConfigureOptions(null!));
}
@ -55,7 +55,8 @@ public class ConfigureOptionsChatClientTests
},
};
using var client = new ChatClientBuilder(innerClient)
using var client = innerClient
.AsBuilder()
.ConfigureOptions(options =>
{
Assert.NotSame(providedOptions, options);

Просмотреть файл

@ -681,7 +681,8 @@ public class DistributedCachingChatClientTest
new(ChatRole.Assistant, [new TextContent("Hey")])]));
}
};
using var outer = new ChatClientBuilder(testClient)
using var outer = testClient
.AsBuilder()
.UseDistributedCache(configure: options =>
{
options.JsonSerializerOptions = TestJsonSerializerContext.Default.Options;

Просмотреть файл

@ -295,7 +295,7 @@ public class FunctionInvokingChatClientTests
}
};
IChatClient service = new ChatClientBuilder(innerClient).UseFunctionInvocation().Build();
IChatClient service = innerClient.AsBuilder().UseFunctionInvocation().Build();
List<ChatMessage> chat = [new ChatMessage(ChatRole.User, "hello")];
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
@ -415,7 +415,7 @@ public class FunctionInvokingChatClientTests
}
};
IChatClient service = configurePipeline(new ChatClientBuilder(innerClient)).Build();
IChatClient service = configurePipeline(innerClient.AsBuilder()).Build();
var result = await service.CompleteAsync(chat, options, cts.Token);
chat.Add(result.Message);

Просмотреть файл

@ -40,7 +40,8 @@ public class LoggingChatClientTests
},
};
using IChatClient client = new ChatClientBuilder(innerClient)
using IChatClient client = innerClient
.AsBuilder()
.UseLogging()
.Build(services);
@ -86,7 +87,8 @@ public class LoggingChatClientTests
yield return new StreamingChatCompletionUpdate { Role = ChatRole.Assistant, Text = "whale" };
}
using IChatClient client = new ChatClientBuilder(innerClient)
using IChatClient client = innerClient
.AsBuilder()
.UseLogging(logger)
.Build();

Просмотреть файл

@ -86,7 +86,8 @@ public class OpenTelemetryChatClientTests
};
}
var chatClient = new ChatClientBuilder(innerClient)
var chatClient = innerClient
.AsBuilder()
.UseOpenTelemetry(loggerFactory, sourceName, configure: instance =>
{
instance.EnableSensitiveData = enableSensitiveData;

Просмотреть файл

@ -21,7 +21,7 @@ public class ConfigureOptionsEmbeddingGeneratorTests
public void ConfigureOptions_InvalidArgs_Throws()
{
using var innerGenerator = new TestEmbeddingGenerator();
var builder = new EmbeddingGeneratorBuilder<string, Embedding<float>>(innerGenerator);
var builder = innerGenerator.AsBuilder();
Assert.Throws<ArgumentNullException>("configure", () => builder.ConfigureOptions(null!));
}
@ -45,7 +45,8 @@ public class ConfigureOptionsEmbeddingGeneratorTests
}
};
using var generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>(innerGenerator)
using var generator = innerGenerator
.AsBuilder()
.ConfigureOptions(options =>
{
Assert.NotSame(providedOptions, options);

Просмотреть файл

@ -321,7 +321,8 @@ public class DistributedCachingEmbeddingGeneratorTest
return Task.FromResult<GeneratedEmbeddings<Embedding<float>>>([_expectedEmbedding]);
},
};
using var outer = new EmbeddingGeneratorBuilder<string, Embedding<float>>(testGenerator)
using var outer = testGenerator
.AsBuilder()
.UseDistributedCache(configure: instance =>
{
instance.JsonSerializerOptions = TestJsonSerializerContext.Default.Options;

Просмотреть файл

@ -36,7 +36,7 @@ public class EmbeddingGeneratorBuilderTests
{
// Arrange
using var expectedInnerGenerator = new TestEmbeddingGenerator();
var builder = new EmbeddingGeneratorBuilder<string, Embedding<float>>(expectedInnerGenerator);
var builder = expectedInnerGenerator.AsBuilder();
builder.Use(next => new InnerServiceCapturingEmbeddingGenerator("First", next));
builder.Use(next => new InnerServiceCapturingEmbeddingGenerator("Second", next));
@ -58,6 +58,7 @@ public class EmbeddingGeneratorBuilderTests
public void DoesNotAcceptNullInnerService()
{
Assert.Throws<ArgumentNullException>("innerGenerator", () => new EmbeddingGeneratorBuilder<string, Embedding<float>>((IEmbeddingGenerator<string, Embedding<float>>)null!));
Assert.Throws<ArgumentNullException>("innerGenerator", () => ((IEmbeddingGenerator<string, Embedding<float>>)null!).AsBuilder());
}
[Fact]
@ -71,7 +72,7 @@ public class EmbeddingGeneratorBuilderTests
public void DoesNotAllowFactoriesToReturnNull()
{
using var innerGenerator = new TestEmbeddingGenerator();
var builder = new EmbeddingGeneratorBuilder<string, Embedding<float>>(innerGenerator);
var builder = innerGenerator.AsBuilder();
builder.Use(_ => null!);
var ex = Assert.Throws<InvalidOperationException>(() => builder.Build());
Assert.Contains("entry at index 0", ex.Message);

Просмотреть файл

@ -39,7 +39,8 @@ public class LoggingEmbeddingGeneratorTests
},
};
using IEmbeddingGenerator<string, Embedding<float>> generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>(innerGenerator)
using IEmbeddingGenerator<string, Embedding<float>> generator = innerGenerator
.AsBuilder()
.UseLogging()
.Build(services);