// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text.Json; 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 and collection semantics. /// /// /// /// stores chat messages entirely in local memory, providing fast access and manipulation /// capabilities. It implements both for agent integration and /// for direct collection manipulation. /// /// /// This store maintains all messages in memory. For long-running conversations or high-volume scenarios, consider using /// message reduction strategies or alternative storage implementations. /// /// [DebuggerDisplay("Count = {Count}")] [DebuggerTypeProxy(typeof(DebugView))] public sealed class InMemoryChatMessageStore : ChatMessageStore, IList { private List _messages; /// /// Initializes a new instance of the class. /// /// /// This constructor creates a basic in-memory store without message reduction capabilities. /// Messages will be stored exactly as added without any automatic processing or reduction. /// public InMemoryChatMessageStore() { this._messages = []; } /// /// Initializes a new instance of the class from previously serialized state. /// /// A representing the serialized state of the message store. /// Optional settings for customizing the JSON deserialization process. /// The is not a valid JSON object or cannot be deserialized. /// /// This constructor enables restoration of message stores from previously saved state, allowing /// conversation history to be preserved across application restarts or migrated between instances. /// The store will be configured with default settings and message reduction before retrieval. /// public InMemoryChatMessageStore(JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null) : this(null, serializedStoreState, jsonSerializerOptions, ChatReducerTriggerEvent.BeforeMessagesRetrieval) { } /// /// Initializes a new instance of the class. /// /// /// A instance used to process, reduce, or optimize chat messages. /// This can be used to implement strategies like message summarization, truncation, or cleanup. /// /// /// Specifies when the message reducer should be invoked. The default is , /// which applies reduction logic when messages are retrieved for agent consumption. /// /// is . /// /// Message reducers enable automatic management of message storage by implementing strategies to /// keep memory usage under control while preserving important conversation context. /// public InMemoryChatMessageStore(IChatReducer chatReducer, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval) : this(chatReducer, default, null, reducerTriggerEvent) { Throw.IfNull(chatReducer); } /// /// Initializes a new instance of the class, with an existing state from a serialized JSON element. /// /// An optional instance used to process or reduce chat messages. If null, no reduction logic will be applied. /// A representing the serialized state of the store. /// Optional settings for customizing the JSON deserialization process. /// The event that should trigger the reducer invocation. public InMemoryChatMessageStore(IChatReducer? chatReducer, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval) { this.ChatReducer = chatReducer; this.ReducerTriggerEvent = reducerTriggerEvent; if (serializedStoreState.ValueKind is JsonValueKind.Object) { var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions; var state = serializedStoreState.Deserialize( jso.GetTypeInfo(typeof(StoreState))) as StoreState; if (state?.Messages is { } messages) { this._messages = messages; return; } } this._messages = []; } /// /// 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 store. /// public ChatReducerTriggerEvent ReducerTriggerEvent { get; } /// public int Count => this._messages.Count; /// public bool IsReadOnly => ((IList)this._messages).IsReadOnly; /// public ChatMessage this[int index] { get => this._messages[index]; set => this._messages[index] = value; } /// public override async ValueTask> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) { _ = Throw.IfNull(context); if (this.ReducerTriggerEvent is ChatReducerTriggerEvent.BeforeMessagesRetrieval && this.ChatReducer is not null) { this._messages = (await this.ChatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList(); } return this._messages; } /// public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) { _ = Throw.IfNull(context); if (context.InvokeException is not null) { return; } // Add request, AI context provider, and response messages to the store var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []); this._messages.AddRange(allNewMessages); if (this.ReducerTriggerEvent is ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null) { this._messages = (await this.ChatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList(); } } /// public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) { StoreState state = new() { Messages = this._messages, }; var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions; return JsonSerializer.SerializeToElement(state, jso.GetTypeInfo(typeof(StoreState))); } /// public int IndexOf(ChatMessage item) => this._messages.IndexOf(item); /// public void Insert(int index, ChatMessage item) => this._messages.Insert(index, item); /// public void RemoveAt(int index) => this._messages.RemoveAt(index); /// public void Add(ChatMessage item) => this._messages.Add(item); /// public void Clear() => this._messages.Clear(); /// public bool Contains(ChatMessage item) => this._messages.Contains(item); /// public void CopyTo(ChatMessage[] array, int arrayIndex) => this._messages.CopyTo(array, arrayIndex); /// public bool Remove(ChatMessage item) => this._messages.Remove(item); /// public IEnumerator GetEnumerator() => this._messages.GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); internal sealed class StoreState { public List Messages { get; set; } = []; } /// /// Defines the events that can trigger a reducer in the . /// public enum ChatReducerTriggerEvent { /// /// Trigger the reducer when a new message is added. /// will only complete when reducer processing is done. /// AfterMessageAdded, /// /// Trigger the reducer before messages are retrieved from the store. /// The reducer will process the messages before they are returned to the caller. /// BeforeMessagesRetrieval } private sealed class DebugView(InMemoryChatMessageStore store) { [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public ChatMessage[] Items => store._messages.ToArray(); } }