// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Moq; namespace Microsoft.Agents.AI.UnitTests; /// /// Unit tests for FunctionCallMiddlewareAgent functionality. /// public sealed class FunctionInvocationDelegatingAgentTests { #region Basic Functionality Tests /// /// Tests that FunctionCallMiddlewareAgent can be created with valid parameters. /// [Fact] public void Constructor_ValidParameters_CreatesInstance() { // Arrange var mockChatClient = new Mock(); var innerAgent = new ChatClientAgent(mockChatClient.Object); static ValueTask CallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) => next(context, cancellationToken); // Act var middleware = new FunctionInvocationDelegatingAgent(innerAgent, CallbackAsync); // Assert Assert.NotNull(middleware); Assert.Equal(innerAgent.Id, middleware.Id); Assert.Equal(innerAgent.Name, middleware.Name); Assert.Equal(innerAgent.Description, middleware.Description); } /// /// Tests that constructor throws ArgumentNullException for null inner agent. /// [Fact] public void Constructor_NullInnerAgent_ThrowsArgumentNullException() { // Arrange static ValueTask CallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) => next(context, cancellationToken); // Act & Assert Assert.Throws(() => new FunctionInvocationDelegatingAgent(null!, CallbackAsync)); } #endregion #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. /// [Fact] public async Task RunAsync_WithFunctionCall_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); 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 var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); await middleware.RunAsync(messages, null, options, 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 multiple function calls trigger middleware for each invocation. /// [Fact] public async Task RunAsync_WithMultipleFunctionCalls_InvokesMiddlewareForEachAsync() { // Arrange var executionOrder = new List(); var function1 = AIFunctionFactory.Create(() => { executionOrder.Add("Function1-Executed"); return "Function1 result"; }, "Function1", "First test function"); var function2 = AIFunctionFactory.Create(() => { executionOrder.Add("Function2-Executed"); return "Function2 result"; }, "Function2", "Second test function"); var functionCall1 = new FunctionCallContent("call_1", "Function1", new Dictionary()); var functionCall2 = new FunctionCallContent("call_2", "Function2", new Dictionary()); var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall1, functionCall2); var innerAgent = new ChatClientAgent(mockChatClient.Object); var messages = new List { new(ChatRole.User, "Test message") }; async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { executionOrder.Add($"Middleware-Pre-{context.Function.Name}"); var result = await next(context, cancellationToken); executionOrder.Add($"Middleware-Post-{context.Function.Name}"); return result; } var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync); // Act var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [function1, function2] }); await middleware.RunAsync(messages, null, options, CancellationToken.None); // Assert Assert.Contains("Middleware-Pre-Function1", executionOrder); Assert.Contains("Function1-Executed", executionOrder); Assert.Contains("Middleware-Post-Function1", executionOrder); Assert.Contains("Middleware-Pre-Function2", executionOrder); Assert.Contains("Function2-Executed", executionOrder); Assert.Contains("Middleware-Post-Function2", executionOrder); } #endregion #region Context Validation Tests /// /// Tests that FunctionInvocationContext contains correct values during middleware execution. /// [Fact] public async Task RunAsync_MiddlewareContext_ContainsCorrectValuesAsync() { // Arrange var testFunction = AIFunctionFactory.Create(() => "Function result", "TestFunction", "A test function"); var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary { ["param"] = "value" }); var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall); var innerAgent = new ChatClientAgent(mockChatClient.Object); var messages = new List { new(ChatRole.User, "Test message") }; FunctionInvocationContext? capturedContext = null; AIAgent? capturedAgent = null; async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { capturedContext = context; capturedAgent = agent; return await next(context, cancellationToken); } var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync); // Act var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); await middleware.RunAsync(messages, null, options, CancellationToken.None); // Assert Assert.NotNull(capturedContext); Assert.Equal("TestFunction", capturedContext.Function.Name); Assert.Same(innerAgent, capturedAgent); // The agent passed should be the inner agent Assert.NotNull(capturedContext.Arguments); // Note: Additional context properties would need to be verified based on actual FunctionInvocationContext structure } #endregion #region AIAgentBuilder Use Method Tests /// /// Verify that AIAgentBuilder.Use method works correctly with function invocation middleware. /// [Fact] public async Task AIAgentBuilder_Use_FunctionInvocationMiddleware_WorksCorrectlyAsync() { // Arrange var mockChatClient = new Mock(); var testFunction = AIFunctionFactory.Create(() => "test result", name: "TestFunction"); var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary()); var executionOrder = new List(); // Mock the chat client to return a function call, then a response mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, [functionCall]))); var innerAgent = new ChatClientAgent(mockChatClient.Object); var messages = new List { new(ChatRole.User, "Test message") }; // Act var agent = new AIAgentBuilder(innerAgent) .Use((agent, context, next, cancellationToken) => { executionOrder.Add("Middleware-Pre"); var result = next(context, cancellationToken); executionOrder.Add("Middleware-Post"); return result; }) .Build(); var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); await agent.RunAsync(messages, null, options, CancellationToken.None); // Assert Assert.Contains("Middleware-Pre", executionOrder); Assert.Contains("Middleware-Post", executionOrder); } /// /// Verify that multiple function invocation middleware are executed. /// [Fact] public async Task AIAgentBuilder_Use_MultipleFunctionMiddleware_BothExecuteAsync() { // Arrange var mockChatClient = new Mock(); var testFunction = AIFunctionFactory.Create(() => "test result", name: "TestFunction"); var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary()); var firstMiddlewareExecuted = false; var secondMiddlewareExecuted = false; // Mock the chat client to return a function call, then a response mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, [functionCall]))); var innerAgent = new ChatClientAgent(mockChatClient.Object); var messages = new List { new(ChatRole.User, "Test message") }; // Act var agent = new AIAgentBuilder(innerAgent) .Use((agent, context, next, cancellationToken) => { firstMiddlewareExecuted = true; return next(context, cancellationToken); }) .Use((agent, context, next, cancellationToken) => { secondMiddlewareExecuted = true; return next(context, cancellationToken); }) .Build(); var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); await agent.RunAsync(messages, null, options, CancellationToken.None); // Assert Assert.True(firstMiddlewareExecuted, "First middleware should have executed"); Assert.True(secondMiddlewareExecuted, "Second middleware should have executed"); } /// /// Verify that AIAgentBuilder.Use method throws InvalidOperationException when inner agent is doesn't use a FunctinInvocking. /// [Fact] public void AIAgentBuilder_Use_NonFICCEnabledAgent_ThrowsInvalidOperationException() { // Arrange var mockAgent = new Mock(); // Act & Assert var builder = new AIAgentBuilder(mockAgent.Object); var exception = Assert.Throws(() => { builder.Use((agent, context, next, cancellationToken) => next(context, cancellationToken)); builder.Build(); }); } /// /// Verify that AIAgentBuilder.Use method throws InvalidOperationException when inner agent is doesn't use a FunctinInvokingChatClient. /// [Fact] public void AIAgentBuilder_Use_NonFICCDecoratedChatClientInAgent_ThrowsInvalidOperationException() { // Arrange var mockChatClient = new Mock(); var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions() { UseProvidedChatClientAsIs = true }); // Act & Assert var builder = new AIAgentBuilder(agent); var exception = Assert.Throws(() => { builder.Use((agent, context, next, cancellationToken) => next(context, cancellationToken)); builder.Build(); }); } /// /// Tests function invocation middleware when FunctionInvokingChatClient.CurrentContext is null (direct function invocation). /// [Fact] public async Task RunAsync_DirectFunctionInvocation_MiddlewareHandlesNullCurrentContextAsync() { // Arrange var executionOrder = new List(); var capturedContext = new List(); var testFunction = AIFunctionFactory.Create(() => { executionOrder.Add("Function-Executed"); return "Function result"; }, "TestFunction", "A test function"); var mockChatClient = new Mock(); // Setup mock to directly invoke the function (bypassing FunctionInvokingChatClient) mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) .Returns, ChatOptions, CancellationToken>(async (messages, options, ct) => { // Directly invoke the function to simulate null CurrentContext scenario if (options?.Tools?.FirstOrDefault() is AIFunction function) { executionOrder.Add("Direct-Function-Invocation"); await function.InvokeAsync([], ct); } return new ChatResponse([new ChatMessage(ChatRole.Assistant, "Response after direct invocation")]); }); var innerAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions { UseProvidedChatClientAsIs = true }); async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { executionOrder.Add("Middleware-Pre"); capturedContext.Add(context); var result = await next(context, cancellationToken); executionOrder.Add("Middleware-Post"); return result; } var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync); var messages = new List { new(ChatRole.User, "Test message") }; // Act var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); await middleware.RunAsync(messages, null, options, CancellationToken.None); // Assert Assert.Contains("Direct-Function-Invocation", executionOrder); Assert.Contains("Middleware-Pre", executionOrder); Assert.Contains("Function-Executed", executionOrder); Assert.Contains("Middleware-Post", executionOrder); // Verify that the context was created with Iteration = -1 (indicating no ambient context) Assert.Single(capturedContext); Assert.Equal(0, capturedContext[0].Iteration); Assert.Equal("TestFunction", capturedContext[0].Function.Name); Assert.NotNull(capturedContext[0].Arguments); } #endregion #region Error Handling Tests /// /// Tests that exceptions thrown by middleware during pre-invocation surface to the caller. /// [Fact] public async Task RunAsync_MiddlewareThrowsPreInvocation_ExceptionSurfacesAsync() { // Arrange var testFunction = AIFunctionFactory.Create(() => "Function result", "TestFunction", "A test function"); var mockChatClient = new Mock(); mockChatClient.Setup(c => c.GetResponseAsync( It.IsAny>(), It.IsAny(), It.IsAny())) .ReturnsAsync(() => new ChatResponse([ new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call_123", "TestFunction", new Dictionary())]) ])); var innerAgent = new ChatClientAgent(mockChatClient.Object); var messages = new List { new(ChatRole.User, "Test message") }; var expectedException = new InvalidOperationException("Pre-invocation error"); ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { throw expectedException; } var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync); // Act & Assert var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); var actualException = await Assert.ThrowsAsync( () => middleware.RunAsync(messages, null, options, CancellationToken.None)); Assert.Same(expectedException, actualException); } /// /// Tests that exceptions thrown by the function are handled by middleware. /// [Fact] public async Task RunAsync_FunctionThrowsException_MiddlewareCanHandleAsync() { // Arrange var functionException = new InvalidOperationException("Function error"); string ThrowingFunction() => throw functionException; var testFunction = AIFunctionFactory.Create(ThrowingFunction, "TestFunction", "A test function"); var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary()); var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall); var innerAgent = new ChatClientAgent(mockChatClient.Object); var messages = new List { new(ChatRole.User, "Test message") }; var middlewareHandledException = false; async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { try { return await next(context, cancellationToken); } catch (InvalidOperationException) { middlewareHandledException = true; return "Error handled by middleware"; } } var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync); // Act var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); await middleware.RunAsync(messages, null, options, CancellationToken.None); // Assert Assert.True(middlewareHandledException); } #endregion #region Result Modification Tests /// /// Tests that middleware can modify function results. /// [Fact] public async Task RunAsync_MiddlewareModifiesResult_ModifiedResultUsedAsync() { // Arrange var testFunction = AIFunctionFactory.Create(() => "Original result", "TestFunction", "A test function"); var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary()); var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall); var innerAgent = new ChatClientAgent(mockChatClient.Object); var messages = new List { new(ChatRole.User, "Test message") }; const string ModifiedResult = "Modified by middleware"; static async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { await next(context, cancellationToken); return ModifiedResult; // Return the modified result instead of setting context property } var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync); // Act var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); var response = await middleware.RunAsync(messages, null, options, CancellationToken.None); // Assert Assert.NotNull(response); // The modified result should be reflected in the response messages var functionResultContent = response.Messages .SelectMany(m => m.Contents) .OfType() .FirstOrDefault(); Assert.NotNull(functionResultContent); Assert.Equal(ModifiedResult, functionResultContent.Result); } #endregion #region Middleware Chaining Tests /// /// Tests execution order with multiple function middleware instances in a chain. /// [Fact] public async Task RunAsync_MultipleFunctionMiddleware_ExecutesInCorrectOrderAsync() { // 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 = new Mock(); // Setup sequence: first call returns function call, subsequent calls return final response var responseWithFunctionCall = new ChatResponse([ new ChatMessage(ChatRole.Assistant, [functionCall]) ]); var finalResponse = new ChatResponse([ new ChatMessage(ChatRole.Assistant, "Final response") ]); mockChatClient.SetupSequence(c => c.GetResponseAsync( It.IsAny>(), It.IsAny(), It.IsAny())) .ReturnsAsync(responseWithFunctionCall) .ReturnsAsync(finalResponse); var innerAgent = new ChatClientAgent(mockChatClient.Object); var messages = new List { new(ChatRole.User, "Test message") }; async ValueTask FirstMiddlewareAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { executionOrder.Add("First-Pre"); var result = await next(context, cancellationToken); executionOrder.Add("First-Post"); return result; } async ValueTask SecondMiddlewareAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { executionOrder.Add("Second-Pre"); var result = await next(context, cancellationToken); executionOrder.Add("Second-Post"); return result; } // Create nested middleware chain var firstMiddleware = new FunctionInvocationDelegatingAgent(innerAgent, FirstMiddlewareAsync); var secondMiddleware = new FunctionInvocationDelegatingAgent(firstMiddleware, SecondMiddlewareAsync); // Act var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); await secondMiddleware.RunAsync(messages, null, options, CancellationToken.None); // Assert var expectedOrder = new[] { "First-Pre", "Second-Pre", "Function-Executed", "Second-Post", "First-Post" }; Assert.Equal(expectedOrder, executionOrder); } /// /// Tests that function middleware works correctly when combined with running middleware. /// [Fact] public async Task RunAsync_FunctionMiddlewareWithRunningMiddleware_BothExecuteAsync() { // 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); var messages = new List { new(ChatRole.User, "Test message") }; async Task RunningMiddlewareCallbackAsync(IEnumerable messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken) { executionOrder.Add("Running-Pre"); var result = await innerAgent.RunAsync(messages, session, options, cancellationToken); executionOrder.Add("Running-Post"); return result; } async ValueTask FunctionMiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { executionOrder.Add("Function-Pre"); var result = await next(context, cancellationToken); executionOrder.Add("Function-Post"); return result; } // Create middleware chain: Function -> Running -> Inner using AIAgentBuilder var runningMiddleware = new AIAgentBuilder(innerAgent) .Use(RunningMiddlewareCallbackAsync, null) .Build(); var functionMiddleware = new FunctionInvocationDelegatingAgent(runningMiddleware, FunctionMiddlewareCallbackAsync); // Act var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); await functionMiddleware.RunAsync(messages, null, options, CancellationToken.None); // Assert Assert.Contains("Running-Pre", executionOrder); Assert.Contains("Running-Post", executionOrder); Assert.Contains("Function-Pre", executionOrder); Assert.Contains("Function-Post", executionOrder); Assert.Contains("Function-Executed", executionOrder); } #endregion #region Streaming Tests /// /// Tests that function middleware works correctly with streaming responses. /// [Fact] public async Task RunStreamingAsync_WithFunctionCall_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); // Setup streaming response with function calls var streamingResponse = new ChatResponseUpdate[] { new() { Contents = [functionCall] }, // Include function call in streaming response new() { Contents = [new TextContent("Streaming response")] } }; mockChatClient.Setup(c => c.GetStreamingResponseAsync( It.IsAny>(), It.IsAny(), It.IsAny())) .Returns(streamingResponse.ToAsyncEnumerable()); var innerAgent = new ChatClientAgent(mockChatClient.Object); 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 var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); var responseUpdates = new List(); await foreach (var update in middleware.RunStreamingAsync(messages, null, options, CancellationToken.None)) { responseUpdates.Add(update); } // Assert Assert.NotEmpty(responseUpdates); Assert.Contains("Middleware-Pre", executionOrder); Assert.Contains("Function-Executed", executionOrder); Assert.Contains("Middleware-Post", executionOrder); } #endregion #region Edge Cases /// /// Tests that middleware is not invoked when no function calls are made. /// [Fact] public async Task RunAsync_NoFunctionCalls_MiddlewareNotInvokedAsync() { // Arrange var middlewareInvoked = false; var mockChatClient = CreateMockChatClient( new ChatResponse([new ChatMessage(ChatRole.Assistant, "Regular response")])); var innerAgent = new ChatClientAgent(mockChatClient.Object); var messages = new List { new(ChatRole.User, "Test message") }; async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { middlewareInvoked = true; return await next(context, cancellationToken); } var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync); // Act await middleware.RunAsync(messages, null, null, CancellationToken.None); // Assert Assert.False(middlewareInvoked); } /// /// Tests that middleware handles cancellation tokens correctly. /// [Fact] public async Task RunAsync_CancellationToken_PropagatedToMiddlewareAsync() { // Arrange var testFunction = AIFunctionFactory.Create(() => "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); var messages = new List { new(ChatRole.User, "Test message") }; var cancellationTokenSource = new CancellationTokenSource(); var expectedToken = cancellationTokenSource.Token; CancellationToken? capturedToken = null; async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { capturedToken = cancellationToken; return await next(context, cancellationToken); } var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync); // Act var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); await middleware.RunAsync(messages, null, options, expectedToken); // Assert Assert.Equal(expectedToken, capturedToken); } /// /// Tests that middleware can prevent function execution by not calling next(). /// [Fact] public async Task RunAsync_MiddlewareDoesNotCallNext_FunctionNotExecutedAsync() { // Arrange var functionExecuted = false; var testFunction = AIFunctionFactory.Create(() => { functionExecuted = true; 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); var messages = new List { new(ChatRole.User, "Test message") }; static ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { // Don't call next() - this should prevent function execution // Return the blocked result directly return new ValueTask("Blocked by middleware"); } var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync); // Act var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] }); var response = await middleware.RunAsync(messages, null, options, CancellationToken.None); // Assert Assert.False(functionExecuted); Assert.NotNull(response); // Verify the middleware result is used var functionResultContent = response.Messages .SelectMany(m => m.Contents) .OfType() .FirstOrDefault(); Assert.NotNull(functionResultContent); Assert.Equal("Blocked by middleware", functionResultContent.Result); } #endregion #region Options Preservation Tests /// /// Tests that FunctionInvocationDelegatingAgent preserves all original AgentRunOptions properties /// when converting base AgentRunOptions to ChatClientAgentRunOptions. /// [Fact] public async Task RunAsync_WithBaseAgentRunOptions_PreservesAllOriginalOptionsAsync() { // Arrange AgentRunOptions? capturedOptions = null; var responseFormat = ChatResponseFormat.Json; var additionalProperties = new AdditionalPropertiesDictionary { ["key1"] = "value1" }; Mock mockChatClient = new(); var chatClientAgent = new ChatClientAgent(mockChatClient.Object); // Wrap the inner agent in a spy that captures the converted options and returns a dummy response var spyAgent = new AnonymousDelegatingAIAgent( chatClientAgent, runFunc: (messages, session, options, innerAgent, ct) => { capturedOptions = options; return Task.FromResult(new AgentResponse(new ChatResponse(new ChatMessage(ChatRole.Assistant, "test")) { ResponseId = "test" })); }, runStreamingFunc: null); static ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) => next(context, cancellationToken); var middleware = new FunctionInvocationDelegatingAgent(spyAgent, MiddlewareCallbackAsync); var originalOptions = new AgentRunOptions { ResponseFormat = responseFormat, AllowBackgroundResponses = true, ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), AdditionalProperties = additionalProperties, }; // Act await middleware.RunAsync([new(ChatRole.User, "Test")], null, originalOptions, CancellationToken.None); // Assert - All original properties were preserved on the converted options Assert.NotNull(capturedOptions); Assert.IsType(capturedOptions); Assert.Same(responseFormat, capturedOptions.ResponseFormat); Assert.True(capturedOptions.AllowBackgroundResponses); Assert.Same(originalOptions.ContinuationToken, capturedOptions.ContinuationToken); Assert.Same(additionalProperties, capturedOptions.AdditionalProperties); } #endregion /// /// Creates a mock IChatClient with predefined responses for testing. /// /// The responses to return in sequence. /// A configured mock IChatClient. private static Mock CreateMockChatClient(params ChatResponse[] responses) { var mockChatClient = new Mock(); var responseQueue = new Queue(responses); mockChatClient.Setup(c => c.GetResponseAsync( It.IsAny>(), It.IsAny(), It.IsAny())) .ReturnsAsync(() => responseQueue.Count > 0 ? responseQueue.Dequeue() : responses.LastOrDefault() ?? CreateDefaultResponse()); return mockChatClient; } /// /// Creates a mock IChatClient that returns responses with function calls for testing function middleware. /// /// The function calls to include in responses. /// A configured mock IChatClient. private static Mock CreateMockChatClientWithFunctionCalls(params FunctionCallContent[] functionCalls) { var mockChatClient = new Mock(); var responseWithFunctionCalls = new ChatResponse([ new ChatMessage(ChatRole.Assistant, functionCalls.Cast().ToList()) ]); mockChatClient.Setup(c => c.GetResponseAsync( It.IsAny>(), It.IsAny(), It.IsAny())) .ReturnsAsync(responseWithFunctionCalls); return mockChatClient; } /// /// Creates a default ChatResponse for fallback scenarios. /// /// A default ChatResponse. private static ChatResponse CreateDefaultResponse() { return new ChatResponse([new ChatMessage(ChatRole.Assistant, "Default response")]); } /// /// Custom AgentRunOptions class for testing /// private sealed class CustomAgentRunOptions : AgentRunOptions; }