mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
892c177e93
When converting base AgentRunOptions to ChatClientAgentRunOptions, the middleware now preserves AllowBackgroundResponses, ContinuationToken, and AdditionalProperties in addition to ResponseFormat. Added unit test verifying all properties are preserved during the conversion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1047 lines
45 KiB
C#
1047 lines
45 KiB
C#
// 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;
|
|
|
|
/// <summary>
|
|
/// Unit tests for FunctionCallMiddlewareAgent functionality.
|
|
/// </summary>
|
|
public sealed class FunctionInvocationDelegatingAgentTests
|
|
{
|
|
#region Basic Functionality Tests
|
|
|
|
/// <summary>
|
|
/// Tests that FunctionCallMiddlewareAgent can be created with valid parameters.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Constructor_ValidParameters_CreatesInstance()
|
|
{
|
|
// Arrange
|
|
var mockChatClient = new Mock<IChatClient>();
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
static ValueTask<object?> CallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that constructor throws ArgumentNullException for null inner agent.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Constructor_NullInnerAgent_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange
|
|
static ValueTask<object?> CallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next, CancellationToken cancellationToken)
|
|
=> next(context, cancellationToken);
|
|
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() => new FunctionInvocationDelegatingAgent(null!, CallbackAsync));
|
|
}
|
|
#endregion
|
|
|
|
#region Function Invocation Tests
|
|
|
|
/// <summary>
|
|
/// Tests that middleware is invoked when functions are called during agent execution without options.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RunAsync_WithFunctionCall_NoOptions_InvokesMiddlewareAsync()
|
|
{
|
|
// Arrange
|
|
var executionOrder = new List<string>();
|
|
var testFunction = AIFunctionFactory.Create(() =>
|
|
{
|
|
executionOrder.Add("Function-Executed");
|
|
return "Function result";
|
|
}, "TestFunction", "A test function");
|
|
|
|
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary<string, object?>());
|
|
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object, tools: [testFunction]);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
|
|
async ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that middleware is invoked when functions are called during agent execution without options.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RunAsync_WithFunctionCall_AgentRunOptions_InvokesMiddlewareAsync()
|
|
{
|
|
// Arrange
|
|
var executionOrder = new List<string>();
|
|
var testFunction = AIFunctionFactory.Create(() =>
|
|
{
|
|
executionOrder.Add("Function-Executed");
|
|
return "Function result";
|
|
}, "TestFunction", "A test function");
|
|
|
|
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary<string, object?>());
|
|
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object, tools: [testFunction]);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
|
|
async ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that middleware is invoked when functions are called during agent execution without options.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RunAsync_WithFunctionCall_CustomAgentRunOptions_ThrowsNotSupportedAsync()
|
|
{
|
|
// Arrange
|
|
var executionOrder = new List<string>();
|
|
var testFunction = AIFunctionFactory.Create(() =>
|
|
{
|
|
executionOrder.Add("Function-Executed");
|
|
return "Function result";
|
|
}, "TestFunction", "A test function");
|
|
|
|
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary<string, object?>());
|
|
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object, tools: [testFunction]);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
|
|
async ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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<NotSupportedException>(() =>
|
|
middleware.RunAsync(messages, null, new CustomAgentRunOptions(), CancellationToken.None));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that middleware is invoked when functions are called during agent execution.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RunAsync_WithFunctionCall_InvokesMiddlewareAsync()
|
|
{
|
|
// Arrange
|
|
var executionOrder = new List<string>();
|
|
var testFunction = AIFunctionFactory.Create(() =>
|
|
{
|
|
executionOrder.Add("Function-Executed");
|
|
return "Function result";
|
|
}, "TestFunction", "A test function");
|
|
|
|
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary<string, object?>());
|
|
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
|
|
async ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that multiple function calls trigger middleware for each invocation.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RunAsync_WithMultipleFunctionCalls_InvokesMiddlewareForEachAsync()
|
|
{
|
|
// Arrange
|
|
var executionOrder = new List<string>();
|
|
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<string, object?>());
|
|
var functionCall2 = new FunctionCallContent("call_2", "Function2", new Dictionary<string, object?>());
|
|
|
|
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall1, functionCall2);
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
|
|
async ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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
|
|
|
|
/// <summary>
|
|
/// Tests that FunctionInvocationContext contains correct values during middleware execution.
|
|
/// </summary>
|
|
[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<string, object?> { ["param"] = "value" });
|
|
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
|
|
FunctionInvocationContext? capturedContext = null;
|
|
AIAgent? capturedAgent = null;
|
|
|
|
async ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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
|
|
|
|
/// <summary>
|
|
/// Verify that AIAgentBuilder.Use method works correctly with function invocation middleware.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task AIAgentBuilder_Use_FunctionInvocationMiddleware_WorksCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
var mockChatClient = new Mock<IChatClient>();
|
|
var testFunction = AIFunctionFactory.Create(() => "test result", name: "TestFunction");
|
|
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary<string, object?>());
|
|
var executionOrder = new List<string>();
|
|
|
|
// Mock the chat client to return a function call, then a response
|
|
mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, [functionCall])));
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that multiple function invocation middleware are executed.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task AIAgentBuilder_Use_MultipleFunctionMiddleware_BothExecuteAsync()
|
|
{
|
|
// Arrange
|
|
var mockChatClient = new Mock<IChatClient>();
|
|
var testFunction = AIFunctionFactory.Create(() => "test result", name: "TestFunction");
|
|
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary<string, object?>());
|
|
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<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, [functionCall])));
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { 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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that AIAgentBuilder.Use method throws InvalidOperationException when inner agent is doesn't use a FunctinInvocking.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AIAgentBuilder_Use_NonFICCEnabledAgent_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var mockAgent = new Mock<AIAgent>();
|
|
|
|
// Act & Assert
|
|
var builder = new AIAgentBuilder(mockAgent.Object);
|
|
var exception = Assert.Throws<InvalidOperationException>(() =>
|
|
{
|
|
builder.Use((agent, context, next, cancellationToken) => next(context, cancellationToken));
|
|
builder.Build();
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that AIAgentBuilder.Use method throws InvalidOperationException when inner agent is doesn't use a FunctinInvokingChatClient.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AIAgentBuilder_Use_NonFICCDecoratedChatClientInAgent_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var mockChatClient = new Mock<IChatClient>();
|
|
|
|
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions() { UseProvidedChatClientAsIs = true });
|
|
|
|
// Act & Assert
|
|
var builder = new AIAgentBuilder(agent);
|
|
var exception = Assert.Throws<InvalidOperationException>(() =>
|
|
{
|
|
builder.Use((agent, context, next, cancellationToken) => next(context, cancellationToken));
|
|
builder.Build();
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests function invocation middleware when FunctionInvokingChatClient.CurrentContext is null (direct function invocation).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RunAsync_DirectFunctionInvocation_MiddlewareHandlesNullCurrentContextAsync()
|
|
{
|
|
// Arrange
|
|
var executionOrder = new List<string>();
|
|
var capturedContext = new List<FunctionInvocationContext>();
|
|
|
|
var testFunction = AIFunctionFactory.Create(() =>
|
|
{
|
|
executionOrder.Add("Function-Executed");
|
|
return "Function result";
|
|
}, "TestFunction", "A test function");
|
|
|
|
var mockChatClient = new Mock<IChatClient>();
|
|
|
|
// Setup mock to directly invoke the function (bypassing FunctionInvokingChatClient)
|
|
mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
|
.Returns<IEnumerable<ChatMessage>, 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<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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<ChatMessage> { 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
|
|
|
|
/// <summary>
|
|
/// Tests that exceptions thrown by middleware during pre-invocation surface to the caller.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RunAsync_MiddlewareThrowsPreInvocation_ExceptionSurfacesAsync()
|
|
{
|
|
// Arrange
|
|
var testFunction = AIFunctionFactory.Create(() => "Function result", "TestFunction", "A test function");
|
|
var mockChatClient = new Mock<IChatClient>();
|
|
|
|
mockChatClient.Setup(c => c.GetResponseAsync(
|
|
It.IsAny<IEnumerable<ChatMessage>>(),
|
|
It.IsAny<ChatOptions>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(() => new ChatResponse([
|
|
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call_123", "TestFunction", new Dictionary<string, object?>())])
|
|
]));
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
var expectedException = new InvalidOperationException("Pre-invocation error");
|
|
|
|
ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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<InvalidOperationException>(
|
|
() => middleware.RunAsync(messages, null, options, CancellationToken.None));
|
|
|
|
Assert.Same(expectedException, actualException);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that exceptions thrown by the function are handled by middleware.
|
|
/// </summary>
|
|
[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<string, object?>());
|
|
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
var middlewareHandledException = false;
|
|
|
|
async ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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
|
|
|
|
/// <summary>
|
|
/// Tests that middleware can modify function results.
|
|
/// </summary>
|
|
[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<string, object?>());
|
|
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
const string ModifiedResult = "Modified by middleware";
|
|
|
|
static async ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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<FunctionResultContent>()
|
|
.FirstOrDefault();
|
|
|
|
Assert.NotNull(functionResultContent);
|
|
Assert.Equal(ModifiedResult, functionResultContent.Result);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Middleware Chaining Tests
|
|
|
|
/// <summary>
|
|
/// Tests execution order with multiple function middleware instances in a chain.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RunAsync_MultipleFunctionMiddleware_ExecutesInCorrectOrderAsync()
|
|
{
|
|
// Arrange
|
|
var executionOrder = new List<string>();
|
|
var testFunction = AIFunctionFactory.Create(() =>
|
|
{
|
|
executionOrder.Add("Function-Executed");
|
|
return "Function result";
|
|
}, "TestFunction", "A test function");
|
|
|
|
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary<string, object?>());
|
|
var mockChatClient = new Mock<IChatClient>();
|
|
|
|
// 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<IEnumerable<ChatMessage>>(),
|
|
It.IsAny<ChatOptions>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(responseWithFunctionCall)
|
|
.ReturnsAsync(finalResponse);
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
|
|
async ValueTask<object?> FirstMiddlewareAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next, CancellationToken cancellationToken)
|
|
{
|
|
executionOrder.Add("First-Pre");
|
|
var result = await next(context, cancellationToken);
|
|
executionOrder.Add("First-Post");
|
|
return result;
|
|
}
|
|
|
|
async ValueTask<object?> SecondMiddlewareAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that function middleware works correctly when combined with running middleware.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RunAsync_FunctionMiddlewareWithRunningMiddleware_BothExecuteAsync()
|
|
{
|
|
// Arrange
|
|
var executionOrder = new List<string>();
|
|
var testFunction = AIFunctionFactory.Create(() =>
|
|
{
|
|
executionOrder.Add("Function-Executed");
|
|
return "Function result";
|
|
}, "TestFunction", "A test function");
|
|
|
|
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary<string, object?>());
|
|
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
|
|
async Task<AgentResponse> RunningMiddlewareCallbackAsync(IEnumerable<ChatMessage> 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<object?> FunctionMiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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
|
|
|
|
/// <summary>
|
|
/// Tests that function middleware works correctly with streaming responses.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RunStreamingAsync_WithFunctionCall_InvokesMiddlewareAsync()
|
|
{
|
|
// Arrange
|
|
var executionOrder = new List<string>();
|
|
var testFunction = AIFunctionFactory.Create(() =>
|
|
{
|
|
executionOrder.Add("Function-Executed");
|
|
return "Function result";
|
|
}, "TestFunction", "A test function");
|
|
|
|
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary<string, object?>());
|
|
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<IEnumerable<ChatMessage>>(),
|
|
It.IsAny<ChatOptions>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(streamingResponse.ToAsyncEnumerable());
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
|
|
async ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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<AgentResponseUpdate>();
|
|
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
|
|
|
|
/// <summary>
|
|
/// Tests that middleware is not invoked when no function calls are made.
|
|
/// </summary>
|
|
[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<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
|
|
async ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that middleware handles cancellation tokens correctly.
|
|
/// </summary>
|
|
[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<string, object?>());
|
|
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
var cancellationTokenSource = new CancellationTokenSource();
|
|
var expectedToken = cancellationTokenSource.Token;
|
|
CancellationToken? capturedToken = null;
|
|
|
|
async ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that middleware can prevent function execution by not calling next().
|
|
/// </summary>
|
|
[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<string, object?>());
|
|
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
|
|
|
|
var innerAgent = new ChatClientAgent(mockChatClient.Object);
|
|
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
|
|
|
|
static ValueTask<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next, CancellationToken cancellationToken)
|
|
{
|
|
// Don't call next() - this should prevent function execution
|
|
// Return the blocked result directly
|
|
return new ValueTask<object?>("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<FunctionResultContent>()
|
|
.FirstOrDefault();
|
|
|
|
Assert.NotNull(functionResultContent);
|
|
Assert.Equal("Blocked by middleware", functionResultContent.Result);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Options Preservation Tests
|
|
|
|
/// <summary>
|
|
/// Tests that FunctionInvocationDelegatingAgent preserves all original AgentRunOptions properties
|
|
/// when converting base AgentRunOptions to ChatClientAgentRunOptions.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RunAsync_WithBaseAgentRunOptions_PreservesAllOriginalOptionsAsync()
|
|
{
|
|
// Arrange
|
|
AgentRunOptions? capturedOptions = null;
|
|
var responseFormat = ChatResponseFormat.Json;
|
|
var additionalProperties = new AdditionalPropertiesDictionary { ["key1"] = "value1" };
|
|
|
|
Mock<IChatClient> 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<object?> MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> 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<ChatClientAgentRunOptions>(capturedOptions);
|
|
Assert.Same(responseFormat, capturedOptions.ResponseFormat);
|
|
Assert.True(capturedOptions.AllowBackgroundResponses);
|
|
Assert.Same(originalOptions.ContinuationToken, capturedOptions.ContinuationToken);
|
|
Assert.Same(additionalProperties, capturedOptions.AdditionalProperties);
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Creates a mock IChatClient with predefined responses for testing.
|
|
/// </summary>
|
|
/// <param name="responses">The responses to return in sequence.</param>
|
|
/// <returns>A configured mock IChatClient.</returns>
|
|
private static Mock<IChatClient> CreateMockChatClient(params ChatResponse[] responses)
|
|
{
|
|
var mockChatClient = new Mock<IChatClient>();
|
|
var responseQueue = new Queue<ChatResponse>(responses);
|
|
|
|
mockChatClient.Setup(c => c.GetResponseAsync(
|
|
It.IsAny<IEnumerable<ChatMessage>>(),
|
|
It.IsAny<ChatOptions>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(() => responseQueue.Count > 0 ? responseQueue.Dequeue() : responses.LastOrDefault() ?? CreateDefaultResponse());
|
|
|
|
return mockChatClient;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a mock IChatClient that returns responses with function calls for testing function middleware.
|
|
/// </summary>
|
|
/// <param name="functionCalls">The function calls to include in responses.</param>
|
|
/// <returns>A configured mock IChatClient.</returns>
|
|
private static Mock<IChatClient> CreateMockChatClientWithFunctionCalls(params FunctionCallContent[] functionCalls)
|
|
{
|
|
var mockChatClient = new Mock<IChatClient>();
|
|
|
|
var responseWithFunctionCalls = new ChatResponse([
|
|
new ChatMessage(ChatRole.Assistant, functionCalls.Cast<AIContent>().ToList())
|
|
]);
|
|
|
|
mockChatClient.Setup(c => c.GetResponseAsync(
|
|
It.IsAny<IEnumerable<ChatMessage>>(),
|
|
It.IsAny<ChatOptions>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(responseWithFunctionCalls);
|
|
|
|
return mockChatClient;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a default ChatResponse for fallback scenarios.
|
|
/// </summary>
|
|
/// <returns>A default ChatResponse.</returns>
|
|
private static ChatResponse CreateDefaultResponse()
|
|
{
|
|
return new ChatResponse([new ChatMessage(ChatRole.Assistant, "Default response")]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Custom AgentRunOptions class for testing
|
|
/// </summary>
|
|
private sealed class CustomAgentRunOptions : AgentRunOptions;
|
|
}
|