diff --git a/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs b/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs index 6960327286..7eefcebc55 100644 --- a/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs @@ -30,27 +30,34 @@ internal sealed class FunctionInvocationDelegatingAgent : DelegatingAIAgent // Decorate options to add the middleware function private AgentRunOptions? AgentRunOptionsWithFunctionMiddleware(AgentRunOptions? options) { - if (options is ChatClientAgentRunOptions aco) + if (options is null || options.GetType() == typeof(AgentRunOptions)) { - var originalFactory = aco.ChatClientFactory; - aco.ChatClientFactory = chatClient => - { - var builder = chatClient.AsBuilder(); - - if (originalFactory is not null) - { - builder.Use(originalFactory); - } - - return builder.ConfigureOptions(co - => co.Tools = co.Tools?.Select(tool => tool is AIFunction aiFunction - ? new MiddlewareEnabledFunction(this.InnerAgent, aiFunction, this._delegateFunc) - : tool) - .ToList()) - .Build(); - }; + options = new ChatClientAgentRunOptions(); } + if (options is not ChatClientAgentRunOptions aco) + { + throw new NotSupportedException($"Function Invocation Middleware is only supported without options or with {nameof(ChatClientAgentRunOptions)}."); + } + + var originalFactory = aco.ChatClientFactory; + aco.ChatClientFactory = chatClient => + { + var builder = chatClient.AsBuilder(); + + if (originalFactory is not null) + { + builder.Use(originalFactory); + } + + return builder.ConfigureOptions(co + => co.Tools = co.Tools?.Select(tool => tool is AIFunction aiFunction + ? new MiddlewareEnabledFunction(this.InnerAgent, aiFunction, this._delegateFunc) + : tool) + .ToList()) + .Build(); + }; + return options; } diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/FunctionInvocationDelegatingAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/FunctionInvocationDelegatingAgentTests.cs index c68de37177..5866e610b6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/FunctionInvocationDelegatingAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/FunctionInvocationDelegatingAgentTests.cs @@ -56,6 +56,135 @@ public sealed class FunctionInvocationDelegatingAgentTests #region Function Invocation Tests + /// + /// Tests that middleware is invoked when functions are called during agent execution without options. + /// + [Fact] + public async Task RunAsync_WithFunctionCall_NoOptions_InvokesMiddlewareAsync() + { + // Arrange + var executionOrder = new List(); + var testFunction = AIFunctionFactory.Create(() => + { + executionOrder.Add("Function-Executed"); + return "Function result"; + }, "TestFunction", "A test function"); + + var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary()); + var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall); + + var innerAgent = new ChatClientAgent(mockChatClient.Object, tools: [testFunction]); + var messages = new List { new(ChatRole.User, "Test message") }; + + async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) + { + executionOrder.Add("Middleware-Pre"); + var result = await next(context, cancellationToken); + executionOrder.Add("Middleware-Post"); + return result; + } + + var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync); + + // Act + await middleware.RunAsync(messages, null, null, CancellationToken.None); + + // Assert + Assert.Contains("Middleware-Pre", executionOrder); + Assert.Contains("Function-Executed", executionOrder); + Assert.Contains("Middleware-Post", executionOrder); + + // Verify execution order + var middlewarePreIndex = executionOrder.IndexOf("Middleware-Pre"); + var functionIndex = executionOrder.IndexOf("Function-Executed"); + var middlewarePostIndex = executionOrder.IndexOf("Middleware-Post"); + + Assert.True(middlewarePreIndex < functionIndex); + Assert.True(functionIndex < middlewarePostIndex); + } + + /// + /// Tests that middleware is invoked when functions are called during agent execution without options. + /// + [Fact] + public async Task RunAsync_WithFunctionCall_AgentRunOptions_InvokesMiddlewareAsync() + { + // Arrange + var executionOrder = new List(); + var testFunction = AIFunctionFactory.Create(() => + { + executionOrder.Add("Function-Executed"); + return "Function result"; + }, "TestFunction", "A test function"); + + var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary()); + var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall); + + var innerAgent = new ChatClientAgent(mockChatClient.Object, tools: [testFunction]); + var messages = new List { new(ChatRole.User, "Test message") }; + + async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) + { + executionOrder.Add("Middleware-Pre"); + var result = await next(context, cancellationToken); + executionOrder.Add("Middleware-Post"); + return result; + } + + var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync); + + // Act + await middleware.RunAsync(messages, null, new AgentRunOptions(), CancellationToken.None); + + // Assert + Assert.Contains("Middleware-Pre", executionOrder); + Assert.Contains("Function-Executed", executionOrder); + Assert.Contains("Middleware-Post", executionOrder); + + // Verify execution order + var middlewarePreIndex = executionOrder.IndexOf("Middleware-Pre"); + var functionIndex = executionOrder.IndexOf("Function-Executed"); + var middlewarePostIndex = executionOrder.IndexOf("Middleware-Post"); + + Assert.True(middlewarePreIndex < functionIndex); + Assert.True(functionIndex < middlewarePostIndex); + } + + /// + /// Tests that middleware is invoked when functions are called during agent execution without options. + /// + [Fact] + public async Task RunAsync_WithFunctionCall_CustomAgentRunOptions_ThrowsNotSupportedAsync() + { + // Arrange + var executionOrder = new List(); + var testFunction = AIFunctionFactory.Create(() => + { + executionOrder.Add("Function-Executed"); + return "Function result"; + }, "TestFunction", "A test function"); + + var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary()); + var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall); + + var innerAgent = new ChatClientAgent(mockChatClient.Object, tools: [testFunction]); + var messages = new List { new(ChatRole.User, "Test message") }; + + async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) + { + executionOrder.Add("Middleware-Pre"); + var result = await next(context, cancellationToken); + executionOrder.Add("Middleware-Post"); + return result; + } + + var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync); + + // Act + await Assert.ThrowsAsync(() => + middleware.RunAsync(messages, null, new CustomAgentRunOptions(), CancellationToken.None)); + } + /// /// Tests that middleware is invoked when functions are called during agent execution. /// @@ -848,4 +977,9 @@ public sealed class FunctionInvocationDelegatingAgentTests { return new ChatResponse([new ChatMessage(ChatRole.Assistant, "Default response")]); } + + /// + /// Custom AgentRunOptions class for testing + /// + private sealed class CustomAgentRunOptions : AgentRunOptions; }