// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; /// /// Provides an in-memory implementation of with support for message reduction. /// /// /// /// stores chat messages in the , /// providing fast access and manipulation capabilities integrated with session state management. /// /// /// This maintains all messages in memory. For long-running conversations or high-volume scenarios, consider using /// message reduction strategies or alternative storage implementations. /// /// public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider { private static IEnumerable DefaultExcludeChatHistoryFilter(IEnumerable messages) => messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory); private readonly string _stateKey; private readonly Func _stateInitializer; private readonly JsonSerializerOptions _jsonSerializerOptions; private readonly Func, IEnumerable> _storageInputMessageFilter; private readonly Func, IEnumerable>? _retrievalOutputMessageFilter; /// /// Initializes a new instance of the class. /// /// /// Optional configuration options that control the provider's behavior, including state initialization, /// message reduction, and serialization settings. If , default settings will be used. /// public InMemoryChatHistoryProvider(InMemoryChatHistoryProviderOptions? options = null) { this._stateInitializer = options?.StateInitializer ?? (_ => new State()); this.ChatReducer = options?.ChatReducer; this.ReducerTriggerEvent = options?.ReducerTriggerEvent ?? InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval; this._stateKey = options?.StateKey ?? base.StateKey; this._jsonSerializerOptions = options?.JsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions; this._storageInputMessageFilter = options?.StorageInputMessageFilter ?? DefaultExcludeChatHistoryFilter; this._retrievalOutputMessageFilter = options?.RetrievalOutputMessageFilter; } /// public override string StateKey => this._stateKey; /// /// Gets the chat reducer used to process or reduce chat messages. If null, no reduction logic will be applied. /// public IChatReducer? ChatReducer { get; } /// /// Gets the event that triggers the reducer invocation in this provider. /// public InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent ReducerTriggerEvent { get; } /// /// Gets the chat messages stored for the specified session. /// /// The agent session containing the state. /// A list of chat messages, or an empty list if no state is found. public List GetMessages(AgentSession? session) => this.GetOrInitializeState(session).Messages; /// /// Sets the chat messages for the specified session. /// /// The agent session containing the state. /// The messages to store. /// is . public void SetMessages(AgentSession? session, List messages) { _ = Throw.IfNull(messages); var state = this.GetOrInitializeState(session); state.Messages = messages; } /// /// Gets the state from the session's StateBag, or initializes it using the state initializer if not present. /// /// The agent session containing the StateBag. /// The provider state, or null if no session is available. private State GetOrInitializeState(AgentSession? session) { if (session?.StateBag.TryGetValue(this._stateKey, out var state, this._jsonSerializerOptions) is true && state is not null) { return state; } state = this._stateInitializer(session); if (session is not null) { session.StateBag.SetValue(this._stateKey, state, this._jsonSerializerOptions); } return state; } /// protected override async ValueTask> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default) { _ = Throw.IfNull(context); var state = this.GetOrInitializeState(context.Session); if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval && this.ChatReducer is not null) { state.Messages = (await this.ChatReducer.ReduceAsync(state.Messages, cancellationToken).ConfigureAwait(false)).ToList(); } IEnumerable output = state.Messages; if (this._retrievalOutputMessageFilter is not null) { output = this._retrievalOutputMessageFilter(output); } return output .Select(message => message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, this.GetType().FullName!)) .Concat(context.RequestMessages); } /// protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default) { _ = Throw.IfNull(context); if (context.InvokeException is not null) { return; } var state = this.GetOrInitializeState(context.Session); // Add request and response messages to the provider var allNewMessages = this._storageInputMessageFilter(context.RequestMessages).Concat(context.ResponseMessages ?? []); state.Messages.AddRange(allNewMessages); if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null) { state.Messages = (await this.ChatReducer.ReduceAsync(state.Messages, cancellationToken).ConfigureAwait(false)).ToList(); } } /// /// Represents the state of a stored in the . /// public sealed class State { /// /// Gets or sets the list of chat messages. /// [JsonPropertyName("messages")] public List Messages { get; set; } = []; } }