.NET: Allow agents to opt into concurrent tool invocation (#7650)
* .NET: allow agents to opt into concurrent tool invocation * .NET: address concurrent invocation review feedback
This commit is contained in:
@@ -61,6 +61,19 @@ public sealed class ChatClientAgentOptions
|
||||
/// </remarks>
|
||||
public bool UseProvidedChatClientAsIs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether functions may be invoked concurrently when a model response
|
||||
/// contains multiple function calls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This setting is independent of <see cref="ChatOptions.AllowMultipleToolCalls"/>, which controls whether
|
||||
/// a model may return multiple tool calls in a single response. The default is <see langword="false"/>.
|
||||
/// This option has no effect when <see cref="UseProvidedChatClientAsIs"/> is <see langword="true"/>.
|
||||
/// When using a custom chat client stack, configure <see cref="FunctionInvokingChatClient.AllowConcurrentInvocation"/>
|
||||
/// directly on its <see cref="FunctionInvokingChatClient"/> instance.
|
||||
/// </remarks>
|
||||
public bool AllowConcurrentInvocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to set the <see cref="ChatClientAgent.ChatHistoryProvider"/> to <see langword="null"/>
|
||||
/// if the underlying AI service indicates that it manages chat history (for example, by returning a conversation id in the response), but a <see cref="ChatHistoryProvider"/> is configured for the agent.
|
||||
@@ -288,6 +301,7 @@ public sealed class ChatClientAgentOptions
|
||||
ChatHistoryProvider = this.ChatHistoryProvider,
|
||||
AIContextProviders = this.AIContextProviders is null ? null : new List<AIContextProvider>(this.AIContextProviders),
|
||||
UseProvidedChatClientAsIs = this.UseProvidedChatClientAsIs,
|
||||
AllowConcurrentInvocation = this.AllowConcurrentInvocation,
|
||||
ClearOnChatHistoryProviderConflict = this.ClearOnChatHistoryProviderConflict,
|
||||
WarnOnChatHistoryProviderConflict = this.WarnOnChatHistoryProviderConflict,
|
||||
ThrowOnChatHistoryProviderConflict = this.ThrowOnChatHistoryProviderConflict,
|
||||
|
||||
@@ -90,15 +90,23 @@ public static class ChatClientExtensions
|
||||
new InvocableFunctionBypassingChatClient(innerClient, services.GetService<ILoggerFactory>()));
|
||||
}
|
||||
|
||||
if (chatClient.GetService<FunctionInvokingChatClient>() is null)
|
||||
var functionInvokingChatClient = chatClient.GetService<FunctionInvokingChatClient>();
|
||||
if (functionInvokingChatClient is null)
|
||||
{
|
||||
chatBuilder.Use((innerClient, services) =>
|
||||
{
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
|
||||
return new FunctionInvokingChatClient(innerClient, loggerFactory, services);
|
||||
return new FunctionInvokingChatClient(innerClient, loggerFactory, services)
|
||||
{
|
||||
AllowConcurrentInvocation = options?.AllowConcurrentInvocation is true,
|
||||
};
|
||||
});
|
||||
}
|
||||
else if (options?.AllowConcurrentInvocation is true)
|
||||
{
|
||||
functionInvokingChatClient.AllowConcurrentInvocation = true;
|
||||
}
|
||||
|
||||
// MessageInjectingChatClient is injected when EnableMessageInjection is enabled.
|
||||
// It is registered after FunctionInvokingChatClient so that it sits between FIC and the inner client.
|
||||
|
||||
@@ -24,6 +24,7 @@ public class ChatClientAgentOptionsTests
|
||||
Assert.Null(options.ChatHistoryProvider);
|
||||
Assert.Null(options.AIContextProviders);
|
||||
Assert.False(options.UseProvidedChatClientAsIs);
|
||||
Assert.False(options.AllowConcurrentInvocation);
|
||||
Assert.True(options.ClearOnChatHistoryProviderConflict);
|
||||
Assert.True(options.WarnOnChatHistoryProviderConflict);
|
||||
Assert.True(options.ThrowOnChatHistoryProviderConflict);
|
||||
@@ -131,6 +132,7 @@ public class ChatClientAgentOptionsTests
|
||||
ChatHistoryProvider = mockChatHistoryProvider,
|
||||
AIContextProviders = [mockAIContextProvider],
|
||||
UseProvidedChatClientAsIs = true,
|
||||
AllowConcurrentInvocation = true,
|
||||
ClearOnChatHistoryProviderConflict = false,
|
||||
WarnOnChatHistoryProviderConflict = false,
|
||||
ThrowOnChatHistoryProviderConflict = false,
|
||||
@@ -149,6 +151,7 @@ public class ChatClientAgentOptionsTests
|
||||
Assert.Same(original.ChatHistoryProvider, clone.ChatHistoryProvider);
|
||||
Assert.Equal(original.AIContextProviders, clone.AIContextProviders);
|
||||
Assert.Equal(original.UseProvidedChatClientAsIs, clone.UseProvidedChatClientAsIs);
|
||||
Assert.Equal(original.AllowConcurrentInvocation, clone.AllowConcurrentInvocation);
|
||||
Assert.Equal(original.ClearOnChatHistoryProviderConflict, clone.ClearOnChatHistoryProviderConflict);
|
||||
Assert.Equal(original.WarnOnChatHistoryProviderConflict, clone.WarnOnChatHistoryProviderConflict);
|
||||
Assert.Equal(original.ThrowOnChatHistoryProviderConflict, clone.ThrowOnChatHistoryProviderConflict);
|
||||
|
||||
@@ -72,6 +72,43 @@ public sealed class ChatClientExtensionsTests
|
||||
Assert.Same(chatClientMock.Object, agent.ChatClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithConcurrentInvocation_EnablesConcurrentFunctionInvocation()
|
||||
{
|
||||
// Arrange
|
||||
var chatClientMock = new Mock<IChatClient>();
|
||||
var options = new ChatClientAgentOptions { AllowConcurrentInvocation = true };
|
||||
|
||||
// Act
|
||||
var agent = chatClientMock.Object.AsAIAgent(options);
|
||||
|
||||
// Assert
|
||||
var functionInvokingClient = agent.ChatClient.GetService<FunctionInvokingChatClient>();
|
||||
Assert.NotNull(functionInvokingClient);
|
||||
Assert.True(functionInvokingClient.AllowConcurrentInvocation);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
public void CreateAIAgent_WithExistingFunctionInvokingChatClient_ConfiguresConcurrentInvocation(bool initiallyEnabled, bool allowConcurrentInvocation)
|
||||
{
|
||||
// Arrange
|
||||
var chatClientMock = new Mock<IChatClient>();
|
||||
var chatClient = chatClientMock.Object.AsBuilder()
|
||||
.UseFunctionInvocation(configure: client => client.AllowConcurrentInvocation = initiallyEnabled)
|
||||
.Build();
|
||||
var options = new ChatClientAgentOptions { AllowConcurrentInvocation = allowConcurrentInvocation };
|
||||
|
||||
// Act
|
||||
var agent = chatClient.AsAIAgent(options);
|
||||
|
||||
// Assert
|
||||
var functionInvokingClient = agent.ChatClient.GetService<FunctionInvokingChatClient>();
|
||||
Assert.NotNull(functionInvokingClient);
|
||||
Assert.True(functionInvokingClient.AllowConcurrentInvocation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithNullClient_Throws()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user