mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Add helpers to more easily access in-memory ChatHistory and make ChatHistoryProvider management more configurable. (#4224)
* Add helpers to more easily access in-memory ChatHistory and make ChatHistoryProvider management more configurable. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
+231
@@ -0,0 +1,231 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="AgentSessionExtensions"/>.
|
||||
/// </summary>
|
||||
public class AgentSessionExtensionsTests
|
||||
{
|
||||
#region TryGetInMemoryChatHistory Tests
|
||||
|
||||
[Fact]
|
||||
public void TryGetInMemoryChatHistory_WithNullSession_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
AgentSession session = null!;
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => session.TryGetInMemoryChatHistory(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetInMemoryChatHistory_WhenStateExists_ReturnsTrueAndMessages()
|
||||
{
|
||||
// Arrange
|
||||
var session = new Mock<AgentSession>().Object;
|
||||
var expectedMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!")
|
||||
};
|
||||
|
||||
session.StateBag.SetValue(
|
||||
nameof(InMemoryChatHistoryProvider),
|
||||
new InMemoryChatHistoryProvider.State { Messages = expectedMessages });
|
||||
|
||||
// Act
|
||||
var result = session.TryGetInMemoryChatHistory(out var messages);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.NotNull(messages);
|
||||
Assert.Same(expectedMessages, messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetInMemoryChatHistory_WhenStateDoesNotExist_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var session = new Mock<AgentSession>().Object;
|
||||
|
||||
// Act
|
||||
var result = session.TryGetInMemoryChatHistory(out var messages);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Null(messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetInMemoryChatHistory_WithCustomStateKey_UsesCustomKey()
|
||||
{
|
||||
// Arrange
|
||||
var session = new Mock<AgentSession>().Object;
|
||||
const string CustomKey = "custom-history-key";
|
||||
var expectedMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Test message")
|
||||
};
|
||||
|
||||
session.StateBag.SetValue(
|
||||
CustomKey,
|
||||
new InMemoryChatHistoryProvider.State { Messages = expectedMessages });
|
||||
|
||||
// Act
|
||||
var result = session.TryGetInMemoryChatHistory(out var messages, stateKey: CustomKey);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.NotNull(messages);
|
||||
Assert.Same(expectedMessages, messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetInMemoryChatHistory_WithCustomStateKey_DoesNotFindDefaultKey()
|
||||
{
|
||||
// Arrange
|
||||
var session = new Mock<AgentSession>().Object;
|
||||
var expectedMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Test message")
|
||||
};
|
||||
|
||||
session.StateBag.SetValue(
|
||||
nameof(InMemoryChatHistoryProvider),
|
||||
new InMemoryChatHistoryProvider.State { Messages = expectedMessages });
|
||||
|
||||
// Act
|
||||
var result = session.TryGetInMemoryChatHistory(out var messages, stateKey: "other-key");
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Null(messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetInMemoryChatHistory_WhenStateExistsWithNullMessages_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var session = new Mock<AgentSession>().Object;
|
||||
session.StateBag.SetValue(
|
||||
nameof(InMemoryChatHistoryProvider),
|
||||
new InMemoryChatHistoryProvider.State { Messages = null! });
|
||||
|
||||
// Act
|
||||
var result = session.TryGetInMemoryChatHistory(out var messages);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Null(messages);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SetInMemoryChatHistory Tests
|
||||
|
||||
[Fact]
|
||||
public void SetInMemoryChatHistory_WithNullSession_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
AgentSession session = null!;
|
||||
var messages = new List<ChatMessage>();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => session.SetInMemoryChatHistory(messages));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetInMemoryChatHistory_WhenNoExistingState_CreatesNewState()
|
||||
{
|
||||
// Arrange
|
||||
var session = new Mock<AgentSession>().Object;
|
||||
var messages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi!")
|
||||
};
|
||||
|
||||
// Act
|
||||
session.SetInMemoryChatHistory(messages);
|
||||
|
||||
// Assert
|
||||
var result = session.TryGetInMemoryChatHistory(out var retrievedMessages);
|
||||
Assert.True(result);
|
||||
Assert.Same(messages, retrievedMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetInMemoryChatHistory_WhenExistingState_ReplacesMessages()
|
||||
{
|
||||
// Arrange
|
||||
var session = new Mock<AgentSession>().Object;
|
||||
var originalMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Original")
|
||||
};
|
||||
var newMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "New message"),
|
||||
new(ChatRole.Assistant, "New response")
|
||||
};
|
||||
|
||||
session.SetInMemoryChatHistory(originalMessages);
|
||||
|
||||
// Act
|
||||
session.SetInMemoryChatHistory(newMessages);
|
||||
|
||||
// Assert
|
||||
var result = session.TryGetInMemoryChatHistory(out var retrievedMessages);
|
||||
Assert.True(result);
|
||||
Assert.Same(newMessages, retrievedMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetInMemoryChatHistory_WithCustomStateKey_UsesCustomKey()
|
||||
{
|
||||
// Arrange
|
||||
var session = new Mock<AgentSession>().Object;
|
||||
const string CustomKey = "custom-history-key";
|
||||
var messages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Test")
|
||||
};
|
||||
|
||||
// Act
|
||||
session.SetInMemoryChatHistory(messages, stateKey: CustomKey);
|
||||
|
||||
// Assert
|
||||
var result = session.TryGetInMemoryChatHistory(out var retrievedMessages, stateKey: CustomKey);
|
||||
Assert.True(result);
|
||||
Assert.Same(messages, retrievedMessages);
|
||||
|
||||
// Verify default key is not set
|
||||
var defaultResult = session.TryGetInMemoryChatHistory(out _);
|
||||
Assert.False(defaultResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetInMemoryChatHistory_WithEmptyList_SetsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var session = new Mock<AgentSession>().Object;
|
||||
var messages = new List<ChatMessage>();
|
||||
|
||||
// Act
|
||||
session.SetInMemoryChatHistory(messages);
|
||||
|
||||
// Assert
|
||||
var result = session.TryGetInMemoryChatHistory(out var retrievedMessages);
|
||||
Assert.True(result);
|
||||
Assert.NotNull(retrievedMessages);
|
||||
Assert.Empty(retrievedMessages);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+13
-1
@@ -23,6 +23,10 @@ public class ChatClientAgentOptionsTests
|
||||
Assert.Null(options.ChatOptions);
|
||||
Assert.Null(options.ChatHistoryProvider);
|
||||
Assert.Null(options.AIContextProviders);
|
||||
Assert.False(options.UseProvidedChatClientAsIs);
|
||||
Assert.True(options.ClearOnChatHistoryProviderConflict);
|
||||
Assert.True(options.WarnOnChatHistoryProviderConflict);
|
||||
Assert.True(options.ThrowOnChatHistoryProviderConflict);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -125,7 +129,11 @@ public class ChatClientAgentOptionsTests
|
||||
ChatOptions = new() { Tools = tools },
|
||||
Id = "test-id",
|
||||
ChatHistoryProvider = mockChatHistoryProvider,
|
||||
AIContextProviders = [mockAIContextProvider]
|
||||
AIContextProviders = [mockAIContextProvider],
|
||||
UseProvidedChatClientAsIs = true,
|
||||
ClearOnChatHistoryProviderConflict = false,
|
||||
WarnOnChatHistoryProviderConflict = false,
|
||||
ThrowOnChatHistoryProviderConflict = false,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -138,6 +146,10 @@ public class ChatClientAgentOptionsTests
|
||||
Assert.Equal(original.Description, clone.Description);
|
||||
Assert.Same(original.ChatHistoryProvider, clone.ChatHistoryProvider);
|
||||
Assert.Equal(original.AIContextProviders, clone.AIContextProviders);
|
||||
Assert.Equal(original.UseProvidedChatClientAsIs, clone.UseProvidedChatClientAsIs);
|
||||
Assert.Equal(original.ClearOnChatHistoryProviderConflict, clone.ClearOnChatHistoryProviderConflict);
|
||||
Assert.Equal(original.WarnOnChatHistoryProviderConflict, clone.WarnOnChatHistoryProviderConflict);
|
||||
Assert.Equal(original.ThrowOnChatHistoryProviderConflict, clone.ThrowOnChatHistoryProviderConflict);
|
||||
|
||||
// ChatOptions should be cloned, not the same reference
|
||||
Assert.NotSame(original.ChatOptions, clone.ChatOptions);
|
||||
|
||||
+118
@@ -291,6 +291,124 @@ public class ChatClientAgent_ChatHistoryManagementTests
|
||||
Assert.Equal("Only ConversationId or ChatHistoryProvider may be used, but not both. The service returned a conversation id indicating server-side chat history management, but the agent has a ChatHistoryProvider configured.", exception.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that RunAsync clears the ChatHistoryProvider when ThrowOnChatHistoryProviderConflict is false
|
||||
/// and ClearOnChatHistoryProviderConflict is true.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_ClearsChatHistoryProvider_WhenThrowDisabledAndClearEnabledAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockService = new();
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatOptions = new() { Instructions = "test instructions" },
|
||||
ChatHistoryProvider = new InMemoryChatHistoryProvider(),
|
||||
ThrowOnChatHistoryProviderConflict = false,
|
||||
ClearOnChatHistoryProviderConflict = true,
|
||||
});
|
||||
|
||||
// Act
|
||||
ChatClientAgentSession? session = await agent.CreateSessionAsync() as ChatClientAgentSession;
|
||||
await agent.RunAsync([new(ChatRole.User, "test")], session);
|
||||
|
||||
// Assert
|
||||
Assert.Null(agent.ChatHistoryProvider);
|
||||
Assert.Equal("ConvId", session!.ConversationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that RunAsync does not throw and does not clear the ChatHistoryProvider when both
|
||||
/// ThrowOnChatHistoryProviderConflict and ClearOnChatHistoryProviderConflict are false.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_KeepsChatHistoryProvider_WhenThrowAndClearDisabledAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockService = new();
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
|
||||
var chatHistoryProvider = new InMemoryChatHistoryProvider();
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatOptions = new() { Instructions = "test instructions" },
|
||||
ChatHistoryProvider = chatHistoryProvider,
|
||||
ThrowOnChatHistoryProviderConflict = false,
|
||||
ClearOnChatHistoryProviderConflict = false,
|
||||
WarnOnChatHistoryProviderConflict = false,
|
||||
});
|
||||
|
||||
// Act
|
||||
ChatClientAgentSession? session = await agent.CreateSessionAsync() as ChatClientAgentSession;
|
||||
await agent.RunAsync([new(ChatRole.User, "test")], session);
|
||||
|
||||
// Assert
|
||||
Assert.Same(chatHistoryProvider, agent.ChatHistoryProvider);
|
||||
Assert.Equal("ConvId", session!.ConversationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that RunAsync still throws when ThrowOnChatHistoryProviderConflict is true
|
||||
/// even if ClearOnChatHistoryProviderConflict is also true (throw takes precedence).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_Throws_WhenThrowEnabledRegardlessOfClearSettingAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockService = new();
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatOptions = new() { Instructions = "test instructions" },
|
||||
ChatHistoryProvider = new InMemoryChatHistoryProvider(),
|
||||
ThrowOnChatHistoryProviderConflict = true,
|
||||
ClearOnChatHistoryProviderConflict = true,
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
ChatClientAgentSession? session = await agent.CreateSessionAsync() as ChatClientAgentSession;
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync([new(ChatRole.User, "test")], session));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that RunAsync does not throw when no ChatHistoryProvider is configured on options,
|
||||
/// even if the service returns a conversation id (default InMemoryChatHistoryProvider is used but not from options).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_DoesNotThrow_WhenNoChatHistoryProviderInOptionsAndConversationIdReturnedAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockService = new();
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatOptions = new() { Instructions = "test instructions" },
|
||||
});
|
||||
|
||||
// Act
|
||||
ChatClientAgentSession? session = await agent.CreateSessionAsync() as ChatClientAgentSession;
|
||||
await agent.RunAsync([new(ChatRole.User, "test")], session);
|
||||
|
||||
// Assert - no exception, session gets the conversation id
|
||||
Assert.Equal("ConvId", session!.ConversationId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ChatHistoryProvider Override Tests
|
||||
|
||||
Reference in New Issue
Block a user