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:
committed by
GitHub
Unverified
parent
c9cd067be6
commit
822a3eb5ea
@@ -10,7 +10,6 @@ using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
@@ -39,9 +38,10 @@ Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session
|
||||
// We can use the ChatHistoryProvider, that is also used by the agent, to read the
|
||||
// chat history from the session state, and see how the reducer is affecting the stored messages.
|
||||
// Here we expect to see 2 messages, the original user message and the agent response message.
|
||||
var provider = agent.GetService<InMemoryChatHistoryProvider>();
|
||||
List<ChatMessage>? chatHistory = provider?.GetMessages(session);
|
||||
Console.WriteLine($"\nChat history has {chatHistory?.Count} messages.\n");
|
||||
if (session.TryGetInMemoryChatHistory(out var chatHistory))
|
||||
{
|
||||
Console.WriteLine($"\nChat history has {chatHistory.Count} messages.\n");
|
||||
}
|
||||
|
||||
// Invoke the agent a few more times.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a robot.", session));
|
||||
@@ -51,16 +51,22 @@ Console.WriteLine(await agent.RunAsync("Tell me a joke about a robot.", session)
|
||||
// to trigger the reducer is just before messages are contributed to a new agent run.
|
||||
// So at this time, we have not yet triggered the reducer for the most recently added messages,
|
||||
// and they are still in the chat history.
|
||||
chatHistory = provider?.GetMessages(session);
|
||||
Console.WriteLine($"\nChat history has {chatHistory?.Count} messages.\n");
|
||||
if (session.TryGetInMemoryChatHistory(out chatHistory))
|
||||
{
|
||||
Console.WriteLine($"\nChat history has {chatHistory.Count} messages.\n");
|
||||
}
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a lemur.", session));
|
||||
chatHistory = provider?.GetMessages(session);
|
||||
Console.WriteLine($"\nChat history has {chatHistory?.Count} messages.\n");
|
||||
if (session.TryGetInMemoryChatHistory(out chatHistory))
|
||||
{
|
||||
Console.WriteLine($"\nChat history has {chatHistory.Count} messages.\n");
|
||||
}
|
||||
|
||||
// At this point, the chat history has exceeded the limit and the original message will not exist anymore,
|
||||
// so asking a follow up question about it may not work as expected.
|
||||
Console.WriteLine(await agent.RunAsync("What was the first joke I asked you to tell again?", session));
|
||||
|
||||
chatHistory = provider?.GetMessages(session);
|
||||
Console.WriteLine($"\nChat history has {chatHistory?.Count} messages.\n");
|
||||
if (session.TryGetInMemoryChatHistory(out chatHistory))
|
||||
{
|
||||
Console.WriteLine($"\nChat history has {chatHistory.Count} messages.\n");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="AgentSession"/>.
|
||||
/// </summary>
|
||||
public static class AgentSessionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the in-memory chat history messages associated with the specified agent session, if the agent is storing memories in the session using the <see cref="InMemoryChatHistoryProvider"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is only applicable when using <see cref="InMemoryChatHistoryProvider"/> and if the service does not require in-service chat history storage.
|
||||
/// </remarks>
|
||||
/// <param name="session">The agent session from which to retrieve in-memory chat history.</param>
|
||||
/// <param name="messages">When this method returns, contains the list of chat history messages if available; otherwise, null.</param>
|
||||
/// <param name="stateKey">An optional key used to identify the chat history state in the session's state bag. If null, the default key for
|
||||
/// in-memory chat history is used.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional JSON serializer options to use when accessing the session state. If null, default options are used.</param>
|
||||
/// <returns><see langword="true"/> if the in-memory chat history messages were found and retrieved; <see langword="false"/> otherwise.</returns>
|
||||
public static bool TryGetInMemoryChatHistory(this AgentSession session, [MaybeNullWhen(false)] out List<ChatMessage> messages, string? stateKey = null, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
_ = Throw.IfNull(session);
|
||||
|
||||
if (session.StateBag.TryGetValue(stateKey ?? nameof(InMemoryChatHistoryProvider), out InMemoryChatHistoryProvider.State? state, jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions) && state?.Messages is not null)
|
||||
{
|
||||
messages = state.Messages;
|
||||
return true;
|
||||
}
|
||||
|
||||
messages = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the in-memory chat message history for the specified agent session, replacing any existing messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is only applicable when using <see cref="InMemoryChatHistoryProvider"/> and if the service does not require in-service chat history storage.
|
||||
/// If messages are set, but a different <see cref="ChatHistoryProvider"/> is used, or if chat history is stored in the underlying AI service, the messages will be ignored.
|
||||
/// </remarks>
|
||||
/// <param name="session">The agent session whose in-memory chat history will be updated.</param>
|
||||
/// <param name="messages">The list of chat messages to store in memory for the session. Replaces any existing messages for the specified
|
||||
/// state key.</param>
|
||||
/// <param name="stateKey">The key used to identify the in-memory chat history within the session's state bag. If null, a default key is
|
||||
/// used.</param>
|
||||
/// <param name="jsonSerializerOptions">The serializer options used when accessing or storing the state. If null, default options are applied.</param>
|
||||
public static void SetInMemoryChatHistory(this AgentSession session, List<ChatMessage> messages, string? stateKey = null, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
_ = Throw.IfNull(session);
|
||||
|
||||
if (session.StateBag.TryGetValue(stateKey ?? nameof(InMemoryChatHistoryProvider), out InMemoryChatHistoryProvider.State? state, jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions) && state is not null)
|
||||
{
|
||||
state.Messages = messages;
|
||||
return;
|
||||
}
|
||||
|
||||
session.StateBag.SetValue(stateKey ?? nameof(InMemoryChatHistoryProvider), new InMemoryChatHistoryProvider.State() { Messages = messages }, jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
// Use the ChatHistoryProvider from options if provided.
|
||||
// If one was not provided, and we later find out that the underlying service does not manage chat history server-side,
|
||||
// we will use the default InMemoryChatHistoryProvider at that time.
|
||||
this.ChatHistoryProvider = options?.ChatHistoryProvider;
|
||||
this.ChatHistoryProvider = options?.ChatHistoryProvider ?? new InMemoryChatHistoryProvider();
|
||||
this.AIContextProviders = this._agentOptions?.AIContextProviders as IReadOnlyList<AIContextProvider> ?? this._agentOptions?.AIContextProviders?.ToList();
|
||||
|
||||
// Validate that no two providers share the same StateKey, since they would overwrite each other's state in the session.
|
||||
@@ -743,25 +743,31 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(responseConversationId))
|
||||
{
|
||||
if (this.ChatHistoryProvider is not null)
|
||||
if (this._agentOptions?.ChatHistoryProvider is not null)
|
||||
{
|
||||
// The agent has a ChatHistoryProvider configured, but the service returned a conversation id,
|
||||
// meaning the service manages chat history server-side. Both cannot be used simultaneously.
|
||||
throw new InvalidOperationException(
|
||||
$"Only {nameof(ChatClientAgentSession.ConversationId)} or {nameof(this.ChatHistoryProvider)} may be used, but not both. The service returned a conversation id indicating server-side chat history management, but the agent has a {nameof(this.ChatHistoryProvider)} configured.");
|
||||
if (this._agentOptions?.WarnOnChatHistoryProviderConflict is true)
|
||||
{
|
||||
this._logger.LogAgentChatClientHistoryProviderConflict(nameof(ChatClientAgentSession.ConversationId), nameof(this.ChatHistoryProvider), this.Id, this.GetLoggingAgentName());
|
||||
}
|
||||
|
||||
if (this._agentOptions?.ThrowOnChatHistoryProviderConflict is true)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Only {nameof(ChatClientAgentSession.ConversationId)} or {nameof(this.ChatHistoryProvider)} may be used, but not both. The service returned a conversation id indicating server-side chat history management, but the agent has a {nameof(this.ChatHistoryProvider)} configured.");
|
||||
}
|
||||
|
||||
if (this._agentOptions?.ClearOnChatHistoryProviderConflict is true)
|
||||
{
|
||||
this.ChatHistoryProvider = null;
|
||||
}
|
||||
}
|
||||
|
||||
// If we got a conversation id back from the chat client, it means that the service supports server side session storage
|
||||
// so we should update the session with the new id.
|
||||
session.ConversationId = responseConversationId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the service doesn't use service side chat history storage (i.e. we got no id back from invocation), and
|
||||
// the agent has no ChatHistoryProvider yet, we should use the default InMemoryChatHistoryProvider so that
|
||||
// we have somewhere to store the chat history.
|
||||
this.ChatHistoryProvider ??= new InMemoryChatHistoryProvider();
|
||||
}
|
||||
}
|
||||
|
||||
private Task NotifyChatHistoryProviderOfFailureAsync(
|
||||
@@ -807,13 +813,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
|
||||
private ChatHistoryProvider? ResolveChatHistoryProvider(ChatOptions? chatOptions, ChatClientAgentSession session)
|
||||
{
|
||||
ChatHistoryProvider? provider = this.ChatHistoryProvider;
|
||||
|
||||
if (session.ConversationId is not null && provider is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Only {nameof(ChatClientAgentSession.ConversationId)} or {nameof(this.ChatHistoryProvider)} may be used, but not both. The current {nameof(ChatClientAgentSession)} has a {nameof(ChatClientAgentSession.ConversationId)} indicating server-side chat history management, but the agent has a {nameof(this.ChatHistoryProvider)} configured.");
|
||||
}
|
||||
ChatHistoryProvider? provider = session.ConversationId is null ? this.ChatHistoryProvider : null;
|
||||
|
||||
// If someone provided an override ChatHistoryProvider via AdditionalProperties, we should use that instead.
|
||||
if (chatOptions?.AdditionalProperties?.TryGetValue(out ChatHistoryProvider? overrideProvider) is true)
|
||||
|
||||
@@ -56,4 +56,17 @@ internal static partial class ChatClientAgentLogMessages
|
||||
string agentId,
|
||||
string agentName,
|
||||
Type clientType);
|
||||
|
||||
/// <summary>
|
||||
/// Logs <see cref="ChatClientAgent"/> warning about <see cref="ChatHistoryProvider"/> conflict.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Warning,
|
||||
Message = "Agent {AgentId}/{AgentName}: Only {ConversationIdName} or {ChatHistoryProviderName} may be used, but not both. The service returned a conversation id indicating server-side chat history management, but the agent has a {ChatHistoryProviderName} configured.")]
|
||||
public static partial void LogAgentChatClientHistoryProviderConflict(
|
||||
this ILogger logger,
|
||||
string conversationIdName,
|
||||
string chatHistoryProviderName,
|
||||
string agentId,
|
||||
string agentName);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,36 @@ public sealed class ChatClientAgentOptions
|
||||
/// </remarks>
|
||||
public bool UseProvidedChatClientAsIs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to set the <see cref="ChatClientAgent.ChatHistoryProvider"/> to <see langword="null"/>
|
||||
/// if the underlying AI service indicates that it manages chat history (for example, by returning a conversation id in the response), but a <see cref="ChatHistoryProvider"/> is configured for the agent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that even if this setting is set to <see langword="false"/>, the <see cref="ChatHistoryProvider"/> will still not be used if the underlying AI service indicates that it manages chat history.
|
||||
/// </remarks>
|
||||
/// <value>
|
||||
/// Default is <see langword="true"/>.
|
||||
/// </value>
|
||||
public bool ClearOnChatHistoryProviderConflict { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to log a warning if the underlying AI service indicates that it manages chat history
|
||||
/// (for example, by returning a conversation id in the response), but a <see cref="ChatHistoryProvider"/> is configured for the agent.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Default is <see langword="true"/>.
|
||||
/// </value>
|
||||
public bool WarnOnChatHistoryProviderConflict { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether an exception is thrown if the underlying AI service indicates that it manages chat history
|
||||
/// (for example, by returning a conversation id in the response), but a <see cref="ChatHistoryProvider"/> is configured for the agent.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Default is <see langword="true"/>.
|
||||
/// </value>
|
||||
public bool ThrowOnChatHistoryProviderConflict { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ChatClientAgentOptions"/> with the same values as this instance.
|
||||
/// </summary>
|
||||
@@ -71,5 +101,9 @@ public sealed class ChatClientAgentOptions
|
||||
ChatOptions = this.ChatOptions?.Clone(),
|
||||
ChatHistoryProvider = this.ChatHistoryProvider,
|
||||
AIContextProviders = this.AIContextProviders is null ? null : new List<AIContextProvider>(this.AIContextProviders),
|
||||
UseProvidedChatClientAsIs = this.UseProvidedChatClientAsIs,
|
||||
ClearOnChatHistoryProviderConflict = this.ClearOnChatHistoryProviderConflict,
|
||||
WarnOnChatHistoryProviderConflict = this.WarnOnChatHistoryProviderConflict,
|
||||
ThrowOnChatHistoryProviderConflict = this.ThrowOnChatHistoryProviderConflict,
|
||||
};
|
||||
}
|
||||
|
||||
+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