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
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user