mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
ff3e13c2aa
* Add thread storage and serialization POC * Switch to using JsonElement and add unit tests * Add additional unit tests. * Exclude private debugger properties from CodeCoverage. * Rename IChatMessagesStorable to IChatMessageStore * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Improve xml doc. * Update the message storing thread to always use external store for both local and remote storage. * Remove threadid from the IChatMessageStore interface, since the store should own the thread id itself, if it requires one. * Switch GetMessages to IEnumerable * Address pr comments. * Make jsonserializer options default consistent on DeserializeThreadAsync * Move message storing thread functionality into AgentThread and simplify AgentThread behavior. * Remove embedding generation from VectorStore chat history sample. * Remove unecessary code and fix formatting. * Make GetNewThread and DeserializeThread virtual with default implementations. Remove unsued json utilities. * Fix formatting * Remove problem test. * Add more unit tests * Remove unused using clause. * Address pr feedback. * Address PR comments. * Make InMemory store internal * Switch InMemoryChatMessageStore to implement IList instead of inheriting from List. * Rename store deserialize param. * Update serialization based on PR comments. * Remove confusing comment. * Address Deserialization PR comments in the same way as Serialization * Add State to IChatMessageStore Serialize and Deserialize names. Make Thread Deserialize internal. Make AgentThread type switching fobidden. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
262 lines
10 KiB
C#
262 lines
10 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Moq;
|
|
using Moq.Protected;
|
|
|
|
namespace Microsoft.Extensions.AI.Agents.Abstractions.UnitTests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for the <see cref="AIAgent"/> class.
|
|
/// </summary>
|
|
public class AgentTests
|
|
{
|
|
private readonly Mock<AIAgent> _agentMock;
|
|
private readonly Mock<AgentThread> _agentThreadMock;
|
|
private readonly AgentRunResponse _invokeResponse = new();
|
|
private readonly List<AgentRunResponseUpdate> _invokeStreamingResponses = [];
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="AgentTests"/> class.
|
|
/// </summary>
|
|
public AgentTests()
|
|
{
|
|
this._agentThreadMock = new Mock<AgentThread>(MockBehavior.Strict);
|
|
|
|
this._invokeResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Hi"));
|
|
this._invokeStreamingResponses.Add(new AgentRunResponseUpdate(ChatRole.Assistant, "Hi"));
|
|
|
|
this._agentMock = new Mock<AIAgent>() { CallBase = true };
|
|
this._agentMock
|
|
.Setup(x => x.RunAsync(
|
|
It.IsAny<IReadOnlyCollection<ChatMessage>>(),
|
|
this._agentThreadMock.Object,
|
|
It.IsAny<AgentRunOptions?>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(this._invokeResponse);
|
|
this._agentMock
|
|
.Setup(x => x.RunStreamingAsync(
|
|
It.IsAny<IReadOnlyCollection<ChatMessage>>(),
|
|
this._agentThreadMock.Object,
|
|
It.IsAny<AgentRunOptions?>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(this._invokeStreamingResponses.ToAsyncEnumerable());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that invoking without a message calls the mocked invoke method with an empty array.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task InvokeWithoutMessageCallsMockedInvokeWithEmptyArrayAsync()
|
|
{
|
|
// Arrange
|
|
var options = new AgentRunOptions();
|
|
var cancellationToken = new CancellationToken();
|
|
|
|
// Act
|
|
var response = await this._agentMock.Object.RunAsync(this._agentThreadMock.Object, options, cancellationToken);
|
|
Assert.Equal(this._invokeResponse, response);
|
|
|
|
// Verify that the mocked method was called with the expected parameters
|
|
this._agentMock.Verify(
|
|
x => x.RunAsync(
|
|
It.Is<IReadOnlyCollection<ChatMessage>>(messages => messages.Count == 0),
|
|
this._agentThreadMock.Object,
|
|
options,
|
|
cancellationToken),
|
|
Times.Once);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that invoking with a string message calls the mocked invoke method with the message in the ICollection of messages.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task InvokeWithStringMessageCallsMockedInvokeWithMessageInCollectionAsync()
|
|
{
|
|
// Arrange
|
|
var message = "Hello, Agent!";
|
|
var options = new AgentRunOptions();
|
|
var cancellationToken = new CancellationToken();
|
|
|
|
// Act
|
|
var response = await this._agentMock.Object.RunAsync(message, this._agentThreadMock.Object, options, cancellationToken);
|
|
Assert.Equal(this._invokeResponse, response);
|
|
|
|
// Verify that the mocked method was called with the expected parameters
|
|
this._agentMock.Verify(
|
|
x => x.RunAsync(
|
|
It.Is<IReadOnlyCollection<ChatMessage>>(messages => messages.Count == 1 && messages.First().Text == message),
|
|
this._agentThreadMock.Object,
|
|
options,
|
|
cancellationToken),
|
|
Times.Once);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that invoking with a single message calls the mocked invoke method with the message in the ICollection of messages.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task InvokeWithSingleMessageCallsMockedInvokeWithMessageInCollectionAsync()
|
|
{
|
|
// Arrange
|
|
var message = new ChatMessage(ChatRole.User, "Hello, Agent!");
|
|
var options = new AgentRunOptions();
|
|
var cancellationToken = new CancellationToken();
|
|
|
|
// Act
|
|
var response = await this._agentMock.Object.RunAsync(message, this._agentThreadMock.Object, options, cancellationToken);
|
|
Assert.Equal(this._invokeResponse, response);
|
|
|
|
// Verify that the mocked method was called with the expected parameters
|
|
this._agentMock.Verify(
|
|
x => x.RunAsync(
|
|
It.Is<IReadOnlyCollection<ChatMessage>>(messages => messages.Count == 1 && messages.First() == message),
|
|
this._agentThreadMock.Object,
|
|
options,
|
|
cancellationToken),
|
|
Times.Once);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that invoking streaming without a message calls the mocked invoke method with an empty array.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task InvokeStreamingWithoutMessageCallsMockedInvokeWithEmptyArrayAsync()
|
|
{
|
|
// Arrange
|
|
var options = new AgentRunOptions();
|
|
var cancellationToken = new CancellationToken();
|
|
|
|
// Act
|
|
await foreach (var response in this._agentMock.Object.RunStreamingAsync(this._agentThreadMock.Object, options, cancellationToken))
|
|
{
|
|
// Assert
|
|
Assert.Contains(response, this._invokeStreamingResponses);
|
|
}
|
|
|
|
// Verify that the mocked method was called with the expected parameters
|
|
this._agentMock.Verify(
|
|
x => x.RunStreamingAsync(
|
|
It.Is<IReadOnlyCollection<ChatMessage>>(messages => messages.Count == 0),
|
|
this._agentThreadMock.Object,
|
|
options,
|
|
cancellationToken),
|
|
Times.Once);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that invoking streaming with a string message calls the mocked invoke method with the message in the ICollection of messages.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task InvokeStreamingWithStringMessageCallsMockedInvokeWithMessageInCollectionAsync()
|
|
{
|
|
// Arrange
|
|
var message = "Hello, Agent!";
|
|
var options = new AgentRunOptions();
|
|
var cancellationToken = new CancellationToken();
|
|
|
|
// Act
|
|
await foreach (var response in this._agentMock.Object.RunStreamingAsync(message, this._agentThreadMock.Object, options, cancellationToken))
|
|
{
|
|
// Assert
|
|
Assert.Contains(response, this._invokeStreamingResponses);
|
|
}
|
|
|
|
// Verify that the mocked method was called with the expected parameters
|
|
this._agentMock.Verify(
|
|
x => x.RunStreamingAsync(
|
|
It.Is<IReadOnlyCollection<ChatMessage>>(messages => messages.Count == 1 && messages.First().Text == message),
|
|
this._agentThreadMock.Object,
|
|
options,
|
|
cancellationToken),
|
|
Times.Once);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that invoking streaming with a single message calls the mocked invoke method with the message in the ICollection of messages.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task InvokeStreamingWithSingleMessageCallsMockedInvokeWithMessageInCollectionAsync()
|
|
{
|
|
// Arrange
|
|
var message = new ChatMessage(ChatRole.User, "Hello, Agent!");
|
|
var options = new AgentRunOptions();
|
|
var cancellationToken = new CancellationToken();
|
|
|
|
// Act
|
|
await foreach (var response in this._agentMock.Object.RunStreamingAsync(message, this._agentThreadMock.Object, options, cancellationToken))
|
|
{
|
|
// Assert
|
|
Assert.Contains(response, this._invokeStreamingResponses);
|
|
}
|
|
|
|
// Verify that the mocked method was called with the expected parameters
|
|
this._agentMock.Verify(
|
|
x => x.RunStreamingAsync(
|
|
It.Is<IReadOnlyCollection<ChatMessage>>(messages => messages.Count == 1 && messages.First() == message),
|
|
this._agentThreadMock.Object,
|
|
options,
|
|
cancellationToken),
|
|
Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateAgentIDIsIdempotent()
|
|
{
|
|
var agent = new MockAgent();
|
|
|
|
string id = agent.Id;
|
|
Assert.NotNull(id);
|
|
Assert.Equal(id, agent.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NotifyThreadOfNewMessagesNotifiesThreadAsync()
|
|
{
|
|
var cancellationToken = new CancellationToken();
|
|
|
|
var messages = new[] { new ChatMessage(ChatRole.User, "msg1"), new ChatMessage(ChatRole.User, "msg2") };
|
|
|
|
var threadMock = new Mock<TestAgentThread>() { CallBase = true };
|
|
threadMock.SetupAllProperties();
|
|
threadMock.Object.ConversationId = "test-thread-id";
|
|
var agent = new MockAgent();
|
|
|
|
await agent.NotifyThreadOfNewMessagesAsync(threadMock.Object, messages, cancellationToken);
|
|
|
|
threadMock.Protected().Verify("OnNewMessagesAsync", Times.Once(), messages, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Typed mock thread.
|
|
/// </summary>
|
|
public abstract class TestAgentThread : AgentThread;
|
|
|
|
private sealed class MockAgent : AIAgent
|
|
{
|
|
public new Task NotifyThreadOfNewMessagesAsync(AgentThread thread, IReadOnlyCollection<ChatMessage> messages, CancellationToken cancellationToken)
|
|
{
|
|
return base.NotifyThreadOfNewMessagesAsync(thread, messages, cancellationToken);
|
|
}
|
|
|
|
public override Task<AgentRunResponse> RunAsync(IReadOnlyCollection<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
|
{
|
|
throw new System.NotImplementedException();
|
|
}
|
|
|
|
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IReadOnlyCollection<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
|
{
|
|
throw new System.NotImplementedException();
|
|
}
|
|
}
|
|
}
|