-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Expand file tree
/
Copy pathInMemoryChatHistoryProvider.cs
More file actions
139 lines (121 loc) · 5.83 KB
/
InMemoryChatHistoryProvider.cs
File metadata and controls
139 lines (121 loc) · 5.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides an in-memory implementation of <see cref="ChatHistoryProvider"/> with support for message reduction.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="InMemoryChatHistoryProvider"/> stores chat messages in the <see cref="AgentSession.StateBag"/>,
/// providing fast access and manipulation capabilities integrated with session state management.
/// </para>
/// <para>
/// This <see cref="ChatHistoryProvider"/> maintains all messages in memory. For long-running conversations or high-volume scenarios, consider using
/// message reduction strategies or alternative storage implementations.
/// </para>
/// </remarks>
public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
{
private readonly ProviderSessionState<State> _sessionState;
private IReadOnlyList<string>? _stateKeys;
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryChatHistoryProvider"/> class.
/// </summary>
/// <param name="options">
/// Optional configuration options that control the provider's behavior, including state initialization,
/// message reduction, and serialization settings. If <see langword="null"/>, default settings will be used.
/// </param>
public InMemoryChatHistoryProvider(InMemoryChatHistoryProviderOptions? options = null)
: base(
options?.ProvideOutputMessageFilter,
options?.StorageInputRequestMessageFilter,
options?.StorageInputResponseMessageFilter)
{
this._sessionState = new ProviderSessionState<State>(
options?.StateInitializer ?? (_ => new State()),
options?.StateKey ?? this.GetType().Name,
options?.JsonSerializerOptions);
this.ChatReducer = options?.ChatReducer;
this.ReducerTriggerEvent = options?.ReducerTriggerEvent ?? InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval;
}
/// <inheritdoc />
public override IReadOnlyList<string> StateKeys => this._stateKeys ??= [this._sessionState.StateKey];
/// <summary>
/// Gets the chat reducer used to process or reduce chat messages. If null, no reduction logic will be applied.
/// </summary>
public IChatReducer? ChatReducer { get; }
/// <summary>
/// Gets the event that triggers the reducer invocation in this provider.
/// </summary>
public InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent ReducerTriggerEvent { get; }
/// <summary>
/// Gets the chat messages stored for the specified session.
/// </summary>
/// <param name="session">The agent session containing the state.</param>
/// <returns>A list of chat messages, or an empty list if no state is found.</returns>
public List<ChatMessage> GetMessages(AgentSession? session)
=> this._sessionState.GetOrInitializeState(session).Messages;
/// <summary>
/// Sets the chat messages for the specified session.
/// </summary>
/// <param name="session">The agent session containing the state.</param>
/// <param name="messages">The messages to store.</param>
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
public void SetMessages(AgentSession? session, List<ChatMessage> messages)
{
Throw.IfNull(messages);
State state = this._sessionState.GetOrInitializeState(session);
state.Messages = messages;
}
/// <inheritdoc />
protected override async ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
State state = this._sessionState.GetOrInitializeState(context.Session);
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval && this.ChatReducer is not null)
{
// Apply pre-invocation compaction strategy if configured
await this.CompactMessagesAsync(state, cancellationToken).ConfigureAwait(false);
}
return state.Messages;
}
/// <inheritdoc />
protected override async ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
State state = this._sessionState.GetOrInitializeState(context.Session);
// Add request and response messages to the provider
var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
state.Messages.AddRange(allNewMessages);
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded)
{
// Apply pre-write compaction strategy if configured
await this.CompactMessagesAsync(state, cancellationToken).ConfigureAwait(false);
}
}
private async Task CompactMessagesAsync(State state, CancellationToken cancellationToken = default)
{
if (this.ChatReducer is not null)
{
// ChatReducer takes precedence, if configured
state.Messages = [.. await this.ChatReducer.ReduceAsync(state.Messages, cancellationToken).ConfigureAwait(false)];
return;
}
}
/// <summary>
/// Represents the state of a <see cref="InMemoryChatHistoryProvider"/> stored in the <see cref="AgentSession.StateBag"/>.
/// </summary>
public sealed class State
{
/// <summary>
/// Gets or sets the list of chat messages.
/// </summary>
[JsonPropertyName("messages")]
public List<ChatMessage> Messages { get; set; } = [];
}
}