// 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; } = [];
}
}