.NET: Rename ChatMessageStore to ChatHistoryProvider (#3375)

* Rename ChatMessageStore to ChatHistoryProvider

* Fix merge issue

* Fixed PR comments

* Fix tests after property rename

* Add unit tests and fix merge issues

* Fix encoding
This commit is contained in:
westey
2026-01-23 15:49:01 +00:00
committed by GitHub
parent 9e57dc7dbe
commit 21e00c054b
45 changed files with 1037 additions and 904 deletions
@@ -82,7 +82,7 @@ public static partial class AgentAbstractionsJsonUtilities
[JsonSerializable(typeof(AgentResponseUpdate[]))]
[JsonSerializable(typeof(ServiceIdAgentThread.ServiceIdAgentThreadState))]
[JsonSerializable(typeof(InMemoryAgentThread.InMemoryAgentThreadState))]
[JsonSerializable(typeof(InMemoryChatMessageStore.StoreState))]
[JsonSerializable(typeof(InMemoryChatHistoryProvider.State))]
[ExcludeFromCodeCoverage]
private sealed partial class JsonContext : JsonSerializerContext;
@@ -68,7 +68,7 @@ public abstract class AgentThread
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="AgentThread"/>,
/// including itself or any services it might be wrapping. For example, to access a <see cref="ChatMessageStore"/> if available for the instance,
/// including itself or any services it might be wrapping. For example, to access a <see cref="ChatHistoryProvider"/> if available for the instance,
/// <see cref="GetService"/> may be used to request it.
/// </remarks>
public virtual object? GetService(Type serviceType, object? serviceKey = null)
@@ -11,11 +11,12 @@ using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides an abstract base class for storing and managing chat messages associated with agent conversations.
/// Provides an abstract base class for fetching chat messages from, and adding chat messages to, chat history for the purposes of agent execution.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="ChatMessageStore"/> defines the contract for persistent storage of chat messages in agent conversations.
/// <see cref="ChatHistoryProvider"/> defines the contract that an <see cref="AIAgent"/> can use to retrieve messsages from chat history
/// and provide notification of newly produced messages.
/// Implementations are responsible for managing message persistence, retrieval, and any necessary optimization
/// strategies such as truncation, summarization, or archival.
/// </para>
@@ -28,11 +29,15 @@ namespace Microsoft.Agents.AI;
/// <item><description>Supporting serialization for thread persistence and migration</description></item>
/// </list>
/// </para>
/// <para>
/// A <see cref="ChatHistoryProvider"/> is only relevant for scenarios where the underlying AI service that the agent is using
/// does not use in-service chat history storage.
/// </para>
/// </remarks>
public abstract class ChatMessageStore
public abstract class ChatHistoryProvider
{
/// <summary>
/// Called at the start of agent invocation to retrieve all messages from the store that should be provided as context for the next agent invocation.
/// Called at the start of agent invocation to provide messages from the chat history as context for the next agent invocation.
/// </summary>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
@@ -56,14 +61,14 @@ public abstract class ChatMessageStore
/// </list>
/// </para>
/// <para>
/// Each store instance should be associated with a single conversation thread to ensure proper message isolation
/// Each <see cref="ChatHistoryProvider"/> instance should be associated with a single <see cref="AgentThread"/> to ensure proper message isolation
/// and context management.
/// </para>
/// </remarks>
public abstract ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default);
/// <summary>
/// Called at the end of the agent invocation to add new messages to the store.
/// Called at the end of the agent invocation to add new messages to the chat history.
/// </summary>
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
@@ -71,7 +76,7 @@ public abstract class ChatMessageStore
/// <remarks>
/// <para>
/// Messages should be added in the order they were generated to maintain proper chronological sequence.
/// The store is responsible for preserving message ordering and ensuring that subsequent calls to
/// The <see cref="ChatHistoryProvider"/> is responsible for preserving message ordering and ensuring that subsequent calls to
/// <see cref="InvokingAsync"/> return messages in the correct chronological order.
/// </para>
/// <para>
@@ -80,7 +85,6 @@ public abstract class ChatMessageStore
/// <item><description>Validating message content and metadata</description></item>
/// <item><description>Applying storage optimizations or compression</description></item>
/// <item><description>Triggering background maintenance operations</description></item>
/// <item><description>Updating indices or search capabilities</description></item>
/// </list>
/// </para>
/// <para>
@@ -97,13 +101,13 @@ public abstract class ChatMessageStore
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
public abstract JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null);
/// <summary>Asks the <see cref="ChatMessageStore"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <summary>Asks the <see cref="ChatHistoryProvider"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="ChatMessageStore"/>,
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="ChatHistoryProvider"/>,
/// including itself or any services it might be wrapping.
/// </remarks>
public virtual object? GetService(Type serviceType, object? serviceKey = null)
@@ -115,12 +119,12 @@ public abstract class ChatMessageStore
: null;
}
/// <summary>Asks the <see cref="ChatMessageStore"/> for an object of type <typeparamref name="TService"/>.</summary>
/// <summary>Asks the <see cref="ChatHistoryProvider"/> for an object of type <typeparamref name="TService"/>.</summary>
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="ChatMessageStore"/>,
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="ChatHistoryProvider"/>,
/// including itself or any services it might be wrapping.
/// </remarks>
public TService? GetService<TService>(object? serviceKey = null)
@@ -130,9 +134,9 @@ public abstract class ChatMessageStore
/// Contains the context information provided to <see cref="InvokingAsync(InvokingContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// This class provides context about the invocation before the messages are retrieved from the store,
/// including the new messages that will be used. Stores can use this information to determine what
/// messages should be retrieved for the invocation.
/// This class provides context about the invocation including the new messages that will be used.
/// A <see cref="ChatHistoryProvider"/> can use this information to determine what messages should be provided
/// for the invocation.
/// </remarks>
public sealed class InvokingContext
{
@@ -169,12 +173,12 @@ public abstract class ChatMessageStore
/// Initializes a new instance of the <see cref="InvokedContext"/> class with the specified request messages.
/// </summary>
/// <param name="requestMessages">The caller provided messages that were used by the agent for this invocation.</param>
/// <param name="chatMessageStoreMessages">The messages retrieved from the <see cref="ChatMessageStore"/> for this invocation.</param>
/// <param name="chatHistoryProviderMessages">The messages retrieved from the <see cref="ChatHistoryProvider"/> for this invocation.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
public InvokedContext(IEnumerable<ChatMessage> requestMessages, IEnumerable<ChatMessage>? chatMessageStoreMessages)
public InvokedContext(IEnumerable<ChatMessage> requestMessages, IEnumerable<ChatMessage>? chatHistoryProviderMessages)
{
this.RequestMessages = Throw.IfNull(requestMessages);
this.ChatMessageStoreMessages = chatMessageStoreMessages;
this.ChatHistoryProviderMessages = chatHistoryProviderMessages;
}
/// <summary>
@@ -182,18 +186,18 @@ public abstract class ChatMessageStore
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances representing new messages that were provided by the caller.
/// This does not include any <see cref="ChatMessageStore"/> supplied messages.
/// This does not include any <see cref="ChatHistoryProvider"/> supplied messages.
/// </value>
public IEnumerable<ChatMessage> RequestMessages { get; set { field = Throw.IfNull(value); } }
/// <summary>
/// Gets the messages retrieved from the <see cref="ChatMessageStore"/> for this invocation, if any.
/// Gets the messages retrieved from the <see cref="ChatHistoryProvider"/> for this invocation, if any.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances that were retrieved from the <see cref="ChatMessageStore"/>,
/// and were used by the agent as part of the invocation. May be null on the first run.
/// A collection of <see cref="ChatMessage"/> instances that were retrieved from the <see cref="ChatHistoryProvider"/>,
/// and were used by the agent as part of the invocation.
/// </value>
public IEnumerable<ChatMessage>? ChatMessageStoreMessages { get; set; }
public IEnumerable<ChatMessage>? ChatHistoryProviderMessages { get; set; }
/// <summary>
/// Gets or sets the messages provided by the <see cref="AIContextProvider"/> for this invocation, if any.
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Contains extension methods for the <see cref="ChatHistoryProvider"/> class.
/// </summary>
public static class ChatHistoryProviderExtensions
{
/// <summary>
/// Adds message filtering to an existing <see cref="ChatHistoryProvider"/>, so that messages passed to the <see cref="ChatHistoryProvider"/> and messages
/// provided by the <see cref="ChatHistoryProvider"/> can be filtered, updated or replaced.
/// </summary>
/// <param name="provider">The <see cref="ChatHistoryProvider"/> to add the message filter to.</param>
/// <param name="invokingMessagesFilter">An optional filter function to apply to messages produced by the <see cref="ChatHistoryProvider"/>. If null, no filter is applied at this
/// stage.</param>
/// <param name="invokedMessagesFilter">An optional filter function to apply to the invoked context messages before they are passed to the <see cref="ChatHistoryProvider"/>. If null, no
/// filter is applied at this stage.</param>
/// <returns>The <see cref="ChatHistoryProvider"/> with filtering applied.</returns>
public static ChatHistoryProvider WithMessageFilters(
this ChatHistoryProvider provider,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? invokingMessagesFilter = null,
Func<ChatHistoryProvider.InvokedContext, ChatHistoryProvider.InvokedContext>? invokedMessagesFilter = null)
{
return new ChatHistoryProviderMessageFilter(
innerProvider: provider,
invokingMessagesFilter: invokingMessagesFilter,
invokedMessagesFilter: invokedMessagesFilter);
}
/// <summary>
/// Decorates the provided chat message <see cref="ChatHistoryProvider"/> so that it does not add
/// messages produced by any <see cref="AIContextProvider"/> to chat history.
/// </summary>
/// <param name="provider">The <see cref="ChatHistoryProvider"/> to add the message filter to.</param>
/// <returns>A new <see cref="ChatHistoryProvider"/> instance that filters out <see cref="AIContextProvider"/> messages so they do not get added.</returns>
public static ChatHistoryProvider WithAIContextProviderMessageRemoval(this ChatHistoryProvider provider)
{
return new ChatHistoryProviderMessageFilter(
innerProvider: provider,
invokedMessagesFilter: (ctx) =>
{
ctx.AIContextProviderMessages = null;
return ctx;
});
}
}
@@ -11,33 +11,33 @@ using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// A <see cref="ChatMessageStore"/> decorator that allows filtering the messages
/// passed into and out of an inner <see cref="ChatMessageStore"/>.
/// A <see cref="ChatHistoryProvider"/> decorator that allows filtering the messages
/// passed into and out of an inner <see cref="ChatHistoryProvider"/>.
/// </summary>
public sealed class ChatMessageStoreMessageFilter : ChatMessageStore
public sealed class ChatHistoryProviderMessageFilter : ChatHistoryProvider
{
private readonly ChatMessageStore _innerChatMessageStore;
private readonly ChatHistoryProvider _innerProvider;
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? _invokingMessagesFilter;
private readonly Func<InvokedContext, InvokedContext>? _invokedMessagesFilter;
/// <summary>
/// Initializes a new instance of the <see cref="ChatMessageStoreMessageFilter"/> class.
/// Initializes a new instance of the <see cref="ChatHistoryProviderMessageFilter"/> class.
/// </summary>
/// <remarks>Use this constructor to customize how messages are filtered before and after invocation by
/// providing appropriate filter functions. If no filters are provided, the message store operates without
/// providing appropriate filter functions. If no filters are provided, the <see cref="ChatHistoryProvider"/> operates without
/// additional filtering.</remarks>
/// <param name="innerChatMessageStore">The underlying chat message store to be wrapped. Cannot be null.</param>
/// <param name="invokingMessagesFilter">An optional filter function to apply to messages before they are invoked. If null, no filter is applied at this
/// stage.</param>
/// <param name="invokedMessagesFilter">An optional filter function to apply to the invocation context after messages have been invoked. If null, no
/// <param name="innerProvider">The underlying <see cref="ChatHistoryProvider"/> to be wrapped. Cannot be null.</param>
/// <param name="invokingMessagesFilter">An optional filter function to apply to messages provided by the <see cref="ChatHistoryProvider"/>
/// before they are used by the agent. If null, no filter is applied at this stage.</param>
/// <param name="invokedMessagesFilter">An optional filter function to apply to the invocation context after messages have been produced. If null, no
/// filter is applied at this stage.</param>
/// <exception cref="ArgumentNullException">Thrown if innerChatMessageStore is null.</exception>
public ChatMessageStoreMessageFilter(
ChatMessageStore innerChatMessageStore,
/// <exception cref="ArgumentNullException">Thrown if <paramref name="innerProvider"/> is null.</exception>
public ChatHistoryProviderMessageFilter(
ChatHistoryProvider innerProvider,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? invokingMessagesFilter = null,
Func<InvokedContext, InvokedContext>? invokedMessagesFilter = null)
{
this._innerChatMessageStore = Throw.IfNull(innerChatMessageStore);
this._innerProvider = Throw.IfNull(innerProvider);
if (invokingMessagesFilter == null && invokedMessagesFilter == null)
{
@@ -51,7 +51,7 @@ public sealed class ChatMessageStoreMessageFilter : ChatMessageStore
/// <inheritdoc />
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var messages = await this._innerChatMessageStore.InvokingAsync(context, cancellationToken).ConfigureAwait(false);
var messages = await this._innerProvider.InvokingAsync(context, cancellationToken).ConfigureAwait(false);
return this._invokingMessagesFilter != null ? this._invokingMessagesFilter(messages) : messages;
}
@@ -63,12 +63,12 @@ public sealed class ChatMessageStoreMessageFilter : ChatMessageStore
context = this._invokedMessagesFilter(context);
}
return this._innerChatMessageStore.InvokedAsync(context, cancellationToken);
return this._innerProvider.InvokedAsync(context, cancellationToken);
}
/// <inheritdoc />
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
return this._innerChatMessageStore.Serialize(jsonSerializerOptions);
return this._innerProvider.Serialize(jsonSerializerOptions);
}
}
@@ -1,50 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Contains extension methods for the <see cref="ChatMessageStore"/> class.
/// </summary>
public static class ChatMessageStoreExtensions
{
/// <summary>
/// Adds message filtering to an existing store, so that messages passed to the store and messages produced by the store
/// can be filtered, updated or replaced.
/// </summary>
/// <param name="store">The store to add the message filter to.</param>
/// <param name="invokingMessagesFilter">An optional filter function to apply to messages produced by the store. If null, no filter is applied at this
/// stage.</param>
/// <param name="invokedMessagesFilter">An optional filter function to apply to the invoked context messages before they are passed to the store. If null, no
/// filter is applied at this stage.</param>
/// <returns>The <see cref="ChatMessageStore"/> with filtering applied.</returns>
public static ChatMessageStore WithMessageFilters(
this ChatMessageStore store,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? invokingMessagesFilter = null,
Func<ChatMessageStore.InvokedContext, ChatMessageStore.InvokedContext>? invokedMessagesFilter = null)
{
return new ChatMessageStoreMessageFilter(
innerChatMessageStore: store,
invokingMessagesFilter: invokingMessagesFilter,
invokedMessagesFilter: invokedMessagesFilter);
}
/// <summary>
/// Decorates the provided chat message store so that it does not store messages produced by any <see cref="AIContextProvider"/>.
/// </summary>
/// <param name="store">The store to add the message filter to.</param>
/// <returns>A new <see cref="ChatMessageStore"/> instance that filters out <see cref="AIContextProvider"/> messages so they do not get stored.</returns>
public static ChatMessageStore WithAIContextProviderMessageRemoval(this ChatMessageStore store)
{
return new ChatMessageStoreMessageFilter(
innerChatMessageStore: store,
invokedMessagesFilter: (ctx) =>
{
ctx.AIContextProviderMessages = null;
return ctx;
});
}
}
@@ -9,11 +9,11 @@ using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides an abstract base class for agent threads that maintain all conversation state in local memory.
/// Provides an abstract base class for an <see cref="AgentThread"/> that maintain all chat history in local memory.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="InMemoryAgentThread"/> is designed for scenarios where conversation state should be stored locally
/// <see cref="InMemoryAgentThread"/> is designed for scenarios where chat history should be stored locally
/// rather than in external services or databases. This approach provides high performance and simplicity while
/// maintaining full control over the conversation data.
/// </para>
@@ -28,17 +28,17 @@ public abstract class InMemoryAgentThread : AgentThread
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryAgentThread"/> class.
/// </summary>
/// <param name="messageStore">
/// An optional <see cref="InMemoryChatMessageStore"/> instance to use for storing chat messages.
/// If <see langword="null"/>, a new empty message store will be created.
/// <param name="chatHistoryProvider">
/// An optional <see cref="InMemoryChatHistoryProvider"/> instance to use for storing chat messages.
/// If <see langword="null"/>, a new empty <see cref="InMemoryChatHistoryProvider"/> will be created.
/// </param>
/// <remarks>
/// This constructor allows sharing of message stores between threads or providing pre-configured
/// message stores with specific reduction or processing logic.
/// This constructor allows sharing of <see cref="ChatHistoryProvider"/> between threads or providing pre-configured
/// <see cref="ChatHistoryProvider"/> with specific reduction or processing logic.
/// </remarks>
protected InMemoryAgentThread(InMemoryChatMessageStore? messageStore = null)
protected InMemoryAgentThread(InMemoryChatHistoryProvider? chatHistoryProvider = null)
{
this.MessageStore = messageStore ?? [];
this.ChatHistoryProvider = chatHistoryProvider ?? [];
}
/// <summary>
@@ -52,7 +52,7 @@ public abstract class InMemoryAgentThread : AgentThread
/// </remarks>
protected InMemoryAgentThread(IEnumerable<ChatMessage> messages)
{
this.MessageStore = [.. messages];
this.ChatHistoryProvider = [.. messages];
}
/// <summary>
@@ -60,9 +60,9 @@ public abstract class InMemoryAgentThread : AgentThread
/// </summary>
/// <param name="serializedThreadState">A <see cref="JsonElement"/> representing the serialized state of the thread.</param>
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
/// <param name="messageStoreFactory">
/// Optional factory function to create the <see cref="InMemoryChatMessageStore"/> from its serialized state.
/// If not provided, a default factory will be used that creates a basic in-memory store.
/// <param name="chatHistoryProviderFactory">
/// Optional factory function to create the <see cref="InMemoryChatHistoryProvider"/> from its serialized state.
/// If not provided, a default factory will be used that creates a basic <see cref="InMemoryChatHistoryProvider"/>.
/// </param>
/// <exception cref="ArgumentException">The <paramref name="serializedThreadState"/> is not a JSON object.</exception>
/// <exception cref="JsonException">The <paramref name="serializedThreadState"/> is invalid or cannot be deserialized to the expected type.</exception>
@@ -73,7 +73,7 @@ public abstract class InMemoryAgentThread : AgentThread
protected InMemoryAgentThread(
JsonElement serializedThreadState,
JsonSerializerOptions? jsonSerializerOptions = null,
Func<JsonElement, JsonSerializerOptions?, InMemoryChatMessageStore>? messageStoreFactory = null)
Func<JsonElement, JsonSerializerOptions?, InMemoryChatHistoryProvider>? chatHistoryProviderFactory = null)
{
if (serializedThreadState.ValueKind != JsonValueKind.Object)
{
@@ -83,15 +83,15 @@ public abstract class InMemoryAgentThread : AgentThread
var state = serializedThreadState.Deserialize(
AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(InMemoryAgentThreadState))) as InMemoryAgentThreadState;
this.MessageStore =
messageStoreFactory?.Invoke(state?.StoreState ?? default, jsonSerializerOptions) ??
new InMemoryChatMessageStore(state?.StoreState ?? default, jsonSerializerOptions);
this.ChatHistoryProvider =
chatHistoryProviderFactory?.Invoke(state?.ChatHistoryProviderState ?? default, jsonSerializerOptions) ??
new InMemoryChatHistoryProvider(state?.ChatHistoryProviderState ?? default, jsonSerializerOptions);
}
/// <summary>
/// Gets or sets the <see cref="InMemoryChatMessageStore"/> used by this thread.
/// Gets or sets the <see cref="InMemoryChatHistoryProvider"/> used by this thread.
/// </summary>
public InMemoryChatMessageStore MessageStore { get; }
public InMemoryChatHistoryProvider ChatHistoryProvider { get; }
/// <summary>
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
@@ -100,11 +100,11 @@ public abstract class InMemoryAgentThread : AgentThread
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
var storeState = this.MessageStore.Serialize(jsonSerializerOptions);
var chatHistoryProviderState = this.ChatHistoryProvider.Serialize(jsonSerializerOptions);
var state = new InMemoryAgentThreadState
{
StoreState = storeState,
ChatHistoryProviderState = chatHistoryProviderState,
};
return JsonSerializer.SerializeToElement(state, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(InMemoryAgentThreadState)));
@@ -112,13 +112,13 @@ public abstract class InMemoryAgentThread : AgentThread
/// <inheritdoc/>
public override object? GetService(Type serviceType, object? serviceKey = null) =>
base.GetService(serviceType, serviceKey) ?? this.MessageStore?.GetService(serviceType, serviceKey);
base.GetService(serviceType, serviceKey) ?? this.ChatHistoryProvider?.GetService(serviceType, serviceKey);
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay => $"Count = {this.MessageStore.Count}";
private string DebuggerDisplay => $"Count = {this.ChatHistoryProvider.Count}";
internal sealed class InMemoryAgentThreadState
{
public JsonElement? StoreState { get; set; }
public JsonElement? ChatHistoryProviderState { get; set; }
}
}
@@ -14,55 +14,54 @@ using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides an in-memory implementation of <see cref="ChatMessageStore"/> with support for message reduction and collection semantics.
/// Provides an in-memory implementation of <see cref="ChatHistoryProvider"/> with support for message reduction and collection semantics.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="InMemoryChatMessageStore"/> stores chat messages entirely in local memory, providing fast access and manipulation
/// capabilities. It implements both <see cref="ChatMessageStore"/> for agent integration and <see cref="IList{ChatMessage}"/>
/// <see cref="InMemoryChatHistoryProvider"/> stores chat messages entirely in local memory, providing fast access and manipulation
/// capabilities. It implements both <see cref="ChatHistoryProvider"/> for agent integration and <see cref="IList{ChatMessage}"/>
/// for direct collection manipulation.
/// </para>
/// <para>
/// This store maintains all messages in memory. For long-running conversations or high-volume scenarios, consider using
/// 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>
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(DebugView))]
public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessage>, IReadOnlyList<ChatMessage>
public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider, IList<ChatMessage>, IReadOnlyList<ChatMessage>
{
private List<ChatMessage> _messages;
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryChatMessageStore"/> class.
/// Initializes a new instance of the <see cref="InMemoryChatHistoryProvider"/> class.
/// </summary>
/// <remarks>
/// This constructor creates a basic in-memory store without message reduction capabilities.
/// This constructor creates a basic in-memory <see cref="ChatHistoryProvider"/> without message reduction capabilities.
/// Messages will be stored exactly as added without any automatic processing or reduction.
/// </remarks>
public InMemoryChatMessageStore()
public InMemoryChatHistoryProvider()
{
this._messages = [];
}
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryChatMessageStore"/> class from previously serialized state.
/// Initializes a new instance of the <see cref="InMemoryChatHistoryProvider"/> class from previously serialized state.
/// </summary>
/// <param name="serializedStoreState">A <see cref="JsonElement"/> representing the serialized state of the message store.</param>
/// <param name="serializedState">A <see cref="JsonElement"/> representing the serialized state of the provider.</param>
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
/// <exception cref="ArgumentException">The <paramref name="serializedStoreState"/> is not a valid JSON object or cannot be deserialized.</exception>
/// <exception cref="ArgumentException">The <paramref name="serializedState"/> is not a valid JSON object or cannot be deserialized.</exception>
/// <remarks>
/// This constructor enables restoration of message stores from previously saved state, allowing
/// This constructor enables restoration of messages 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.
/// </remarks>
public InMemoryChatMessageStore(JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null)
: this(null, serializedStoreState, jsonSerializerOptions, ChatReducerTriggerEvent.BeforeMessagesRetrieval)
public InMemoryChatHistoryProvider(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
: this(null, serializedState, jsonSerializerOptions, ChatReducerTriggerEvent.BeforeMessagesRetrieval)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryChatMessageStore"/> class.
/// Initializes a new instance of the <see cref="InMemoryChatHistoryProvider"/> class.
/// </summary>
/// <param name="chatReducer">
/// A <see cref="IChatReducer"/> instance used to process, reduce, or optimize chat messages.
@@ -77,29 +76,29 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
/// Message reducers enable automatic management of message storage by implementing strategies to
/// keep memory usage under control while preserving important conversation context.
/// </remarks>
public InMemoryChatMessageStore(IChatReducer chatReducer, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval)
public InMemoryChatHistoryProvider(IChatReducer chatReducer, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval)
: this(chatReducer, default, null, reducerTriggerEvent)
{
Throw.IfNull(chatReducer);
}
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryChatMessageStore"/> class, with an existing state from a serialized JSON element.
/// Initializes a new instance of the <see cref="InMemoryChatHistoryProvider"/> class, with an existing state from a serialized JSON element.
/// </summary>
/// <param name="chatReducer">An optional <see cref="IChatReducer"/> instance used to process or reduce chat messages. If null, no reduction logic will be applied.</param>
/// <param name="serializedStoreState">A <see cref="JsonElement"/> representing the serialized state of the store.</param>
/// <param name="serializedState">A <see cref="JsonElement"/> representing the serialized state of the provider.</param>
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
/// <param name="reducerTriggerEvent">The event that should trigger the reducer invocation.</param>
public InMemoryChatMessageStore(IChatReducer? chatReducer, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval)
public InMemoryChatHistoryProvider(IChatReducer? chatReducer, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval)
{
this.ChatReducer = chatReducer;
this.ReducerTriggerEvent = reducerTriggerEvent;
if (serializedStoreState.ValueKind is JsonValueKind.Object)
if (serializedState.ValueKind is JsonValueKind.Object)
{
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
var state = serializedStoreState.Deserialize(
jso.GetTypeInfo(typeof(StoreState))) as StoreState;
var state = serializedState.Deserialize(
jso.GetTypeInfo(typeof(State))) as State;
if (state?.Messages is { } messages)
{
this._messages = messages;
@@ -116,7 +115,7 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
public IChatReducer? ChatReducer { get; }
/// <summary>
/// Gets the event that triggers the reducer invocation in this store.
/// Gets the event that triggers the reducer invocation in this provider.
/// </summary>
public ChatReducerTriggerEvent ReducerTriggerEvent { get; }
@@ -156,7 +155,7 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
return;
}
// Add request, AI context provider, and response messages to the store
// Add request, AI context provider, and response messages to the provider
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
this._messages.AddRange(allNewMessages);
@@ -169,13 +168,13 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
/// <inheritdoc />
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
StoreState state = new()
State state = new()
{
Messages = this._messages,
};
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
return JsonSerializer.SerializeToElement(state, jso.GetTypeInfo(typeof(StoreState)));
return JsonSerializer.SerializeToElement(state, jso.GetTypeInfo(typeof(State)));
}
/// <inheritdoc />
@@ -218,13 +217,13 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
IEnumerator IEnumerable.GetEnumerator()
=> this.GetEnumerator();
internal sealed class StoreState
internal sealed class State
{
public List<ChatMessage> Messages { get; set; } = [];
}
/// <summary>
/// Defines the events that can trigger a reducer in the <see cref="InMemoryChatMessageStore"/>.
/// Defines the events that can trigger a reducer in the <see cref="InMemoryChatHistoryProvider"/>.
/// </summary>
public enum ChatReducerTriggerEvent
{
@@ -235,15 +234,15 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
AfterMessageAdded,
/// <summary>
/// Trigger the reducer before messages are retrieved from the store.
/// Trigger the reducer before messages are retrieved from the provider.
/// The reducer will process the messages before they are returned to the caller.
/// </summary>
BeforeMessagesRetrieval
}
private sealed class DebugView(InMemoryChatMessageStore store)
private sealed class DebugView(InMemoryChatHistoryProvider provider)
{
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public ChatMessage[] Items => store._messages.ToArray();
public ChatMessage[] Items => provider._messages.ToArray();
}
}
@@ -192,7 +192,7 @@ public static class PersistentAgentsClientExtensions
Description = options.Description ?? persistentAgentMetadata.Description,
ChatOptions = options.ChatOptions,
AIContextProviderFactory = options.AIContextProviderFactory,
ChatMessageStoreFactory = options.ChatMessageStoreFactory,
ChatHistoryProviderFactory = options.ChatHistoryProviderFactory,
UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs
};
@@ -583,7 +583,7 @@ public static partial class AzureAIProjectChatClientExtensions
if (options is not null)
{
agentOptions.AIContextProviderFactory = options.AIContextProviderFactory;
agentOptions.ChatMessageStoreFactory = options.ChatMessageStoreFactory;
agentOptions.ChatHistoryProviderFactory = options.ChatHistoryProviderFactory;
agentOptions.UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs;
}
@@ -15,11 +15,11 @@ using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides a Cosmos DB implementation of the <see cref="ChatMessageStore"/> abstract class.
/// Provides a Cosmos DB implementation of the <see cref="ChatHistoryProvider"/> abstract class.
/// </summary>
[RequiresUnreferencedCode("The CosmosChatMessageStore uses JSON serialization which is incompatible with trimming.")]
[RequiresDynamicCode("The CosmosChatMessageStore uses JSON serialization which is incompatible with NativeAOT.")]
public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
[RequiresUnreferencedCode("The CosmosChatHistoryProvider uses JSON serialization which is incompatible with trimming.")]
[RequiresDynamicCode("The CosmosChatHistoryProvider uses JSON serialization which is incompatible with NativeAOT.")]
public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
{
private readonly CosmosClient _cosmosClient;
private readonly Container _container;
@@ -60,7 +60,7 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
public int MaxBatchSize { get; set; } = 100;
/// <summary>
/// Gets or sets the maximum number of messages to retrieve from the store.
/// Gets or sets the maximum number of messages to retrieve from the provider.
/// This helps prevent exceeding LLM context windows in long conversations.
/// Default is null (no limit). When set, only the most recent messages are returned.
/// </summary>
@@ -73,17 +73,17 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
public int? MessageTtlSeconds { get; set; } = 86400;
/// <summary>
/// Gets the conversation ID associated with this message store.
/// Gets the conversation ID associated with this provider.
/// </summary>
public string ConversationId { get; init; }
/// <summary>
/// Gets the database ID associated with this message store.
/// Gets the database ID associated with this provider.
/// </summary>
public string DatabaseId { get; init; }
/// <summary>
/// Gets the container ID associated with this message store.
/// Gets the container ID associated with this provider.
/// </summary>
public string ContainerId { get; init; }
@@ -97,7 +97,7 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
/// <param name="ownsClient">Whether this instance owns the CosmosClient and should dispose it.</param>
/// <param name="tenantId">Optional tenant identifier for hierarchical partitioning.</param>
/// <param name="userId">Optional user identifier for hierarchical partitioning.</param>
internal CosmosChatMessageStore(CosmosClient cosmosClient, string databaseId, string containerId, string conversationId, bool ownsClient, string? tenantId = null, string? userId = null)
internal CosmosChatHistoryProvider(CosmosClient cosmosClient, string databaseId, string containerId, string conversationId, bool ownsClient, string? tenantId = null, string? userId = null)
{
this._cosmosClient = Throw.IfNull(cosmosClient);
this._container = this._cosmosClient.GetContainer(Throw.IfNullOrWhitespace(databaseId), Throw.IfNullOrWhitespace(containerId));
@@ -121,20 +121,20 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosChatMessageStore"/> class using a connection string.
/// Initializes a new instance of the <see cref="CosmosChatHistoryProvider"/> class using a connection string.
/// </summary>
/// <param name="connectionString">The Cosmos DB connection string.</param>
/// <param name="databaseId">The identifier of the Cosmos DB database.</param>
/// <param name="containerId">The identifier of the Cosmos DB container.</param>
/// <exception cref="ArgumentNullException">Thrown when any required parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosChatMessageStore(string connectionString, string databaseId, string containerId)
public CosmosChatHistoryProvider(string connectionString, string databaseId, string containerId)
: this(connectionString, databaseId, containerId, Guid.NewGuid().ToString("N"))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosChatMessageStore"/> class using a connection string.
/// Initializes a new instance of the <see cref="CosmosChatHistoryProvider"/> class using a connection string.
/// </summary>
/// <param name="connectionString">The Cosmos DB connection string.</param>
/// <param name="databaseId">The identifier of the Cosmos DB database.</param>
@@ -142,13 +142,13 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
/// <param name="conversationId">The unique identifier for this conversation thread.</param>
/// <exception cref="ArgumentNullException">Thrown when any required parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosChatMessageStore(string connectionString, string databaseId, string containerId, string conversationId)
public CosmosChatHistoryProvider(string connectionString, string databaseId, string containerId, string conversationId)
: this(new CosmosClient(Throw.IfNullOrWhitespace(connectionString)), databaseId, containerId, conversationId, ownsClient: true)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosChatMessageStore"/> class using TokenCredential for authentication.
/// Initializes a new instance of the <see cref="CosmosChatHistoryProvider"/> class using TokenCredential for authentication.
/// </summary>
/// <param name="accountEndpoint">The Cosmos DB account endpoint URI.</param>
/// <param name="tokenCredential">The TokenCredential to use for authentication (e.g., DefaultAzureCredential, ManagedIdentityCredential).</param>
@@ -156,13 +156,13 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
/// <param name="containerId">The identifier of the Cosmos DB container.</param>
/// <exception cref="ArgumentNullException">Thrown when any required parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosChatMessageStore(string accountEndpoint, TokenCredential tokenCredential, string databaseId, string containerId)
public CosmosChatHistoryProvider(string accountEndpoint, TokenCredential tokenCredential, string databaseId, string containerId)
: this(accountEndpoint, tokenCredential, databaseId, containerId, Guid.NewGuid().ToString("N"))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosChatMessageStore"/> class using a TokenCredential for authentication.
/// Initializes a new instance of the <see cref="CosmosChatHistoryProvider"/> class using a TokenCredential for authentication.
/// </summary>
/// <param name="accountEndpoint">The Cosmos DB account endpoint URI.</param>
/// <param name="tokenCredential">The TokenCredential to use for authentication (e.g., DefaultAzureCredential, ManagedIdentityCredential).</param>
@@ -171,26 +171,26 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
/// <param name="conversationId">The unique identifier for this conversation thread.</param>
/// <exception cref="ArgumentNullException">Thrown when any required parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosChatMessageStore(string accountEndpoint, TokenCredential tokenCredential, string databaseId, string containerId, string conversationId)
public CosmosChatHistoryProvider(string accountEndpoint, TokenCredential tokenCredential, string databaseId, string containerId, string conversationId)
: this(new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential)), databaseId, containerId, conversationId, ownsClient: true)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosChatMessageStore"/> class using an existing <see cref="CosmosClient"/>.
/// Initializes a new instance of the <see cref="CosmosChatHistoryProvider"/> class using an existing <see cref="CosmosClient"/>.
/// </summary>
/// <param name="cosmosClient">The <see cref="CosmosClient"/> instance to use for Cosmos DB operations.</param>
/// <param name="databaseId">The identifier of the Cosmos DB database.</param>
/// <param name="containerId">The identifier of the Cosmos DB container.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="cosmosClient"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosChatMessageStore(CosmosClient cosmosClient, string databaseId, string containerId)
public CosmosChatHistoryProvider(CosmosClient cosmosClient, string databaseId, string containerId)
: this(cosmosClient, databaseId, containerId, Guid.NewGuid().ToString("N"))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosChatMessageStore"/> class using an existing <see cref="CosmosClient"/>.
/// Initializes a new instance of the <see cref="CosmosChatHistoryProvider"/> class using an existing <see cref="CosmosClient"/>.
/// </summary>
/// <param name="cosmosClient">The <see cref="CosmosClient"/> instance to use for Cosmos DB operations.</param>
/// <param name="databaseId">The identifier of the Cosmos DB database.</param>
@@ -198,13 +198,13 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
/// <param name="conversationId">The unique identifier for this conversation thread.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="cosmosClient"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosChatMessageStore(CosmosClient cosmosClient, string databaseId, string containerId, string conversationId)
public CosmosChatHistoryProvider(CosmosClient cosmosClient, string databaseId, string containerId, string conversationId)
: this(cosmosClient, databaseId, containerId, conversationId, ownsClient: false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosChatMessageStore"/> class using a connection string with hierarchical partition keys.
/// Initializes a new instance of the <see cref="CosmosChatHistoryProvider"/> class using a connection string with hierarchical partition keys.
/// </summary>
/// <param name="connectionString">The Cosmos DB connection string.</param>
/// <param name="databaseId">The identifier of the Cosmos DB database.</param>
@@ -214,13 +214,13 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
/// <param name="sessionId">The session identifier for hierarchical partitioning.</param>
/// <exception cref="ArgumentNullException">Thrown when any required parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosChatMessageStore(string connectionString, string databaseId, string containerId, string tenantId, string userId, string sessionId)
public CosmosChatHistoryProvider(string connectionString, string databaseId, string containerId, string tenantId, string userId, string sessionId)
: this(new CosmosClient(Throw.IfNullOrWhitespace(connectionString)), databaseId, containerId, Throw.IfNullOrWhitespace(sessionId), ownsClient: true, Throw.IfNullOrWhitespace(tenantId), Throw.IfNullOrWhitespace(userId))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosChatMessageStore"/> class using a TokenCredential for authentication with hierarchical partition keys.
/// Initializes a new instance of the <see cref="CosmosChatHistoryProvider"/> class using a TokenCredential for authentication with hierarchical partition keys.
/// </summary>
/// <param name="accountEndpoint">The Cosmos DB account endpoint URI.</param>
/// <param name="tokenCredential">The TokenCredential to use for authentication (e.g., DefaultAzureCredential, ManagedIdentityCredential).</param>
@@ -231,13 +231,13 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
/// <param name="sessionId">The session identifier for hierarchical partitioning.</param>
/// <exception cref="ArgumentNullException">Thrown when any required parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosChatMessageStore(string accountEndpoint, TokenCredential tokenCredential, string databaseId, string containerId, string tenantId, string userId, string sessionId)
public CosmosChatHistoryProvider(string accountEndpoint, TokenCredential tokenCredential, string databaseId, string containerId, string tenantId, string userId, string sessionId)
: this(new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential)), databaseId, containerId, Throw.IfNullOrWhitespace(sessionId), ownsClient: true, Throw.IfNullOrWhitespace(tenantId), Throw.IfNullOrWhitespace(userId))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosChatMessageStore"/> class using an existing <see cref="CosmosClient"/> with hierarchical partition keys.
/// Initializes a new instance of the <see cref="CosmosChatHistoryProvider"/> class using an existing <see cref="CosmosClient"/> with hierarchical partition keys.
/// </summary>
/// <param name="cosmosClient">The <see cref="CosmosClient"/> instance to use for Cosmos DB operations.</param>
/// <param name="databaseId">The identifier of the Cosmos DB database.</param>
@@ -247,43 +247,43 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
/// <param name="sessionId">The session identifier for hierarchical partitioning.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="cosmosClient"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosChatMessageStore(CosmosClient cosmosClient, string databaseId, string containerId, string tenantId, string userId, string sessionId)
public CosmosChatHistoryProvider(CosmosClient cosmosClient, string databaseId, string containerId, string tenantId, string userId, string sessionId)
: this(cosmosClient, databaseId, containerId, Throw.IfNullOrWhitespace(sessionId), ownsClient: false, Throw.IfNullOrWhitespace(tenantId), Throw.IfNullOrWhitespace(userId))
{
}
/// <summary>
/// Creates a new instance of the <see cref="CosmosChatMessageStore"/> class from previously serialized state.
/// Creates a new instance of the <see cref="CosmosChatHistoryProvider"/> class from previously serialized state.
/// </summary>
/// <param name="cosmosClient">The <see cref="CosmosClient"/> instance to use for Cosmos DB operations.</param>
/// <param name="serializedStoreState">A <see cref="JsonElement"/> representing the serialized state of the message store.</param>
/// <param name="serializedState">A <see cref="JsonElement"/> representing the serialized state of the provider.</param>
/// <param name="databaseId">The identifier of the Cosmos DB database.</param>
/// <param name="containerId">The identifier of the Cosmos DB container.</param>
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
/// <returns>A new instance of <see cref="CosmosChatMessageStore"/> initialized from the serialized state.</returns>
/// <returns>A new instance of <see cref="CosmosChatHistoryProvider"/> initialized from the serialized state.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="cosmosClient"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when the serialized state cannot be deserialized.</exception>
public static CosmosChatMessageStore CreateFromSerializedState(CosmosClient cosmosClient, JsonElement serializedStoreState, string databaseId, string containerId, JsonSerializerOptions? jsonSerializerOptions = null)
public static CosmosChatHistoryProvider CreateFromSerializedState(CosmosClient cosmosClient, JsonElement serializedState, string databaseId, string containerId, JsonSerializerOptions? jsonSerializerOptions = null)
{
Throw.IfNull(cosmosClient);
Throw.IfNullOrWhitespace(databaseId);
Throw.IfNullOrWhitespace(containerId);
if (serializedStoreState.ValueKind is not JsonValueKind.Object)
if (serializedState.ValueKind is not JsonValueKind.Object)
{
throw new ArgumentException("Invalid serialized state", nameof(serializedStoreState));
throw new ArgumentException("Invalid serialized state", nameof(serializedState));
}
var state = serializedStoreState.Deserialize<StoreState>(jsonSerializerOptions);
var state = serializedState.Deserialize<State>(jsonSerializerOptions);
if (state?.ConversationIdentifier is not { } conversationId)
{
throw new ArgumentException("Invalid serialized state", nameof(serializedStoreState));
throw new ArgumentException("Invalid serialized state", nameof(serializedState));
}
// Use the internal constructor with all parameters to ensure partition key logic is centralized
return state.UseHierarchicalPartitioning && state.TenantId != null && state.UserId != null
? new CosmosChatMessageStore(cosmosClient, databaseId, containerId, conversationId, ownsClient: false, state.TenantId, state.UserId)
: new CosmosChatMessageStore(cosmosClient, databaseId, containerId, conversationId, ownsClient: false);
? new CosmosChatHistoryProvider(cosmosClient, databaseId, containerId, conversationId, ownsClient: false, state.TenantId, state.UserId)
: new CosmosChatHistoryProvider(cosmosClient, databaseId, containerId, conversationId, ownsClient: false);
}
/// <inheritdoc />
@@ -524,7 +524,7 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
}
#pragma warning restore CA1513
var state = new StoreState
var state = new State
{
ConversationIdentifier = this.ConversationId,
TenantId = this._tenantId,
@@ -632,7 +632,7 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
}
}
private sealed class StoreState
private sealed class State
{
public string ConversationIdentifier { get; set; } = string.Empty;
public string? TenantId { get; set; }
@@ -23,9 +23,9 @@ public static class CosmosDBChatExtensions
/// <returns>The configured <see cref="ChatClientAgentOptions"/>.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
[RequiresUnreferencedCode("The CosmosChatMessageStore uses JSON serialization which is incompatible with trimming.")]
[RequiresDynamicCode("The CosmosChatMessageStore uses JSON serialization which is incompatible with NativeAOT.")]
public static ChatClientAgentOptions WithCosmosDBMessageStore(
[RequiresUnreferencedCode("The CosmosChatHistoryProvider uses JSON serialization which is incompatible with trimming.")]
[RequiresDynamicCode("The CosmosChatHistoryProvider uses JSON serialization which is incompatible with NativeAOT.")]
public static ChatClientAgentOptions WithCosmosDBChatHistoryProvider(
this ChatClientAgentOptions options,
string connectionString,
string databaseId,
@@ -36,7 +36,7 @@ public static class CosmosDBChatExtensions
throw new ArgumentNullException(nameof(options));
}
options.ChatMessageStoreFactory = (context, ct) => new ValueTask<ChatMessageStore>(new CosmosChatMessageStore(connectionString, databaseId, containerId));
options.ChatHistoryProviderFactory = (context, ct) => new ValueTask<ChatHistoryProvider>(new CosmosChatHistoryProvider(connectionString, databaseId, containerId));
return options;
}
@@ -51,9 +51,9 @@ public static class CosmosDBChatExtensions
/// <returns>The configured <see cref="ChatClientAgentOptions"/>.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> or <paramref name="tokenCredential"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
[RequiresUnreferencedCode("The CosmosChatMessageStore uses JSON serialization which is incompatible with trimming.")]
[RequiresDynamicCode("The CosmosChatMessageStore uses JSON serialization which is incompatible with NativeAOT.")]
public static ChatClientAgentOptions WithCosmosDBMessageStoreUsingManagedIdentity(
[RequiresUnreferencedCode("The CosmosChatHistoryProvider uses JSON serialization which is incompatible with trimming.")]
[RequiresDynamicCode("The CosmosChatHistoryProvider uses JSON serialization which is incompatible with NativeAOT.")]
public static ChatClientAgentOptions WithCosmosDBChatHistoryProviderUsingManagedIdentity(
this ChatClientAgentOptions options,
string accountEndpoint,
string databaseId,
@@ -70,7 +70,7 @@ public static class CosmosDBChatExtensions
throw new ArgumentNullException(nameof(tokenCredential));
}
options.ChatMessageStoreFactory = (context, ct) => new ValueTask<ChatMessageStore>(new CosmosChatMessageStore(accountEndpoint, tokenCredential, databaseId, containerId));
options.ChatHistoryProviderFactory = (context, ct) => new ValueTask<ChatHistoryProvider>(new CosmosChatHistoryProvider(accountEndpoint, tokenCredential, databaseId, containerId));
return options;
}
@@ -84,9 +84,9 @@ public static class CosmosDBChatExtensions
/// <returns>The configured <see cref="ChatClientAgentOptions"/>.</returns>
/// <exception cref="ArgumentNullException">Thrown when any required parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
[RequiresUnreferencedCode("The CosmosChatMessageStore uses JSON serialization which is incompatible with trimming.")]
[RequiresDynamicCode("The CosmosChatMessageStore uses JSON serialization which is incompatible with NativeAOT.")]
public static ChatClientAgentOptions WithCosmosDBMessageStore(
[RequiresUnreferencedCode("The CosmosChatHistoryProvider uses JSON serialization which is incompatible with trimming.")]
[RequiresDynamicCode("The CosmosChatHistoryProvider uses JSON serialization which is incompatible with NativeAOT.")]
public static ChatClientAgentOptions WithCosmosDBChatHistoryProvider(
this ChatClientAgentOptions options,
CosmosClient cosmosClient,
string databaseId,
@@ -97,7 +97,7 @@ public static class CosmosDBChatExtensions
throw new ArgumentNullException(nameof(options));
}
options.ChatMessageStoreFactory = (context, ct) => new ValueTask<ChatMessageStore>(new CosmosChatMessageStore(cosmosClient, databaseId, containerId));
options.ChatHistoryProviderFactory = (context, ct) => new ValueTask<ChatHistoryProvider>(new CosmosChatHistoryProvider(cosmosClient, databaseId, containerId));
return options;
}
}
@@ -21,7 +21,7 @@
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework Cosmos DB NoSQL Integration</Title>
<Description>Provides Cosmos DB NoSQL implementations for Microsoft Agent Framework storage abstractions including ChatMessageStore and CheckpointStore.</Description>
<Description>Provides Cosmos DB NoSQL implementations for Microsoft Agent Framework storage abstractions including ChatHistoryProvider and CheckpointStore.</Description>
</PropertyGroup>
<ItemGroup>
@@ -205,7 +205,7 @@ public static class OpenAIAssistantClientExtensions
Description = options.Description ?? assistantMetadata.Description,
ChatOptions = options.ChatOptions,
AIContextProviderFactory = options.AIContextProviderFactory,
ChatMessageStoreFactory = options.ChatMessageStoreFactory,
ChatHistoryProviderFactory = options.ChatHistoryProviderFactory,
UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs
};
@@ -10,16 +10,16 @@ using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows;
internal sealed class WorkflowMessageStore : ChatMessageStore
internal sealed class WorkflowChatHistoryProvider : ChatHistoryProvider
{
private int _bookmark;
private readonly List<ChatMessage> _chatMessages = [];
public WorkflowMessageStore()
public WorkflowChatHistoryProvider()
{
}
public WorkflowMessageStore(StoreState state)
public WorkflowChatHistoryProvider(StoreState state)
{
this.ImportStoreState(Throw.IfNull(state));
}
@@ -82,7 +82,7 @@ internal sealed class WorkflowHostAgent : AIAgent
// For workflow threads, messages are added directly via the internal AddMessages method
// The MessageStore methods are used for agent invocation scenarios
workflowThread.MessageStore.AddMessages(messages);
workflowThread.ChatHistoryProvider.AddMessages(messages);
return workflowThread;
}
@@ -37,7 +37,7 @@ internal sealed class WorkflowThread : AgentThread
this._checkpointManager = checkpointManager ?? new(this._inMemoryCheckpointManager = new());
this.RunId = Throw.IfNullOrEmpty(runId);
this.MessageStore = new WorkflowMessageStore();
this.ChatHistoryProvider = new WorkflowChatHistoryProvider();
}
public WorkflowThread(Workflow workflow, JsonElement serializedThread, IWorkflowExecutionEnvironment executionEnvironment, CheckpointManager? checkpointManager = null, bool includeExceptionDetails = false, bool includeWorkflowOutputsInResponse = false, JsonSerializerOptions? jsonSerializerOptions = null)
@@ -70,7 +70,7 @@ internal sealed class WorkflowThread : AgentThread
this.RunId = threadState.RunId;
this.LastCheckpoint = threadState.LastCheckpoint;
this.MessageStore = new WorkflowMessageStore(threadState.MessageStoreState);
this.ChatHistoryProvider = new WorkflowChatHistoryProvider(threadState.ChatHistoryProviderState);
}
public CheckpointInfo? LastCheckpoint { get; set; }
@@ -81,7 +81,7 @@ internal sealed class WorkflowThread : AgentThread
ThreadState info = new(
this.RunId,
this.LastCheckpoint,
this.MessageStore.ExportStoreState(),
this.ChatHistoryProvider.ExportStoreState(),
this._inMemoryCheckpointManager);
return marshaller.Marshal(info);
@@ -100,7 +100,7 @@ internal sealed class WorkflowThread : AgentThread
RawRepresentation = raw
};
this.MessageStore.AddMessages(update.ToChatMessage());
this.ChatHistoryProvider.AddMessages(update.ToChatMessage());
return update;
}
@@ -117,7 +117,7 @@ internal sealed class WorkflowThread : AgentThread
RawRepresentation = raw
};
this.MessageStore.AddMessages(update.ToChatMessage());
this.ChatHistoryProvider.AddMessages(update.ToChatMessage());
return update;
}
@@ -156,7 +156,7 @@ internal sealed class WorkflowThread : AgentThread
try
{
this.LastResponseId = Guid.NewGuid().ToString("N");
List<ChatMessage> messages = this.MessageStore.GetFromBookmark().ToList();
List<ChatMessage> messages = this.ChatHistoryProvider.GetFromBookmark().ToList();
#pragma warning disable CA2007 // Analyzer misfiring and not seeing .ConfigureAwait(false) below.
await using Checkpointed<StreamingRun> checkpointed =
@@ -240,7 +240,7 @@ internal sealed class WorkflowThread : AgentThread
finally
{
// Do we want to try to undo the step, and not update the bookmark?
this.MessageStore.UpdateBookmark();
this.ChatHistoryProvider.UpdateBookmark();
}
}
@@ -249,17 +249,17 @@ internal sealed class WorkflowThread : AgentThread
public string RunId { get; }
/// <inheritdoc/>
public WorkflowMessageStore MessageStore { get; }
public WorkflowChatHistoryProvider ChatHistoryProvider { get; }
internal sealed class ThreadState(
string runId,
CheckpointInfo? lastCheckpoint,
WorkflowMessageStore.StoreState messageStoreState,
WorkflowChatHistoryProvider.StoreState chatHistoryProviderState,
InMemoryCheckpointManager? checkpointManager = null)
{
public string RunId { get; } = runId;
public CheckpointInfo? LastCheckpoint { get; } = lastCheckpoint;
public WorkflowMessageStore.StoreState MessageStoreState { get; } = messageStoreState;
public WorkflowChatHistoryProvider.StoreState ChatHistoryProviderState { get; } = chatHistoryProviderState;
public InMemoryCheckpointManager? CheckpointManager { get; } = checkpointManager;
}
}
@@ -83,7 +83,7 @@ internal static partial class WorkflowsJsonUtilities
[JsonSerializable(typeof(EdgeConnection))]
// Workflow-as-Agent
[JsonSerializable(typeof(WorkflowMessageStore.StoreState))]
[JsonSerializable(typeof(WorkflowChatHistoryProvider.StoreState))]
[JsonSerializable(typeof(WorkflowThread.ThreadState))]
// Message Types
@@ -78,7 +78,7 @@ public sealed partial class ChatClientAgent : AIAgent
/// <param name="chatClient">The chat client to use when running the agent.</param>
/// <param name="options">
/// Configuration options that control all aspects of the agent's behavior, including chat settings,
/// message store factories, context provider factories, and other advanced configurations.
/// chat history provider factories, context provider factories, and other advanced configurations.
/// </param>
/// <param name="loggerFactory">
/// Optional logger factory for creating loggers used by the agent and its components.
@@ -208,7 +208,7 @@ public sealed partial class ChatClientAgent : AIAgent
ChatOptions? chatOptions,
List<ChatMessage> inputMessagesForChatClient,
IList<ChatMessage>? aiContextProviderMessages,
IList<ChatMessage>? chatMessageStoreMessages,
IList<ChatMessage>? chatHistoryProviderMessages,
ChatClientAgentContinuationToken? continuationToken) =
await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false);
@@ -231,7 +231,7 @@ public sealed partial class ChatClientAgent : AIAgent
}
catch (Exception ex)
{
await NotifyMessageStoreOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatMessageStoreMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyChatHistoryProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
throw;
}
@@ -246,7 +246,7 @@ public sealed partial class ChatClientAgent : AIAgent
}
catch (Exception ex)
{
await NotifyMessageStoreOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatMessageStoreMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyChatHistoryProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
throw;
}
@@ -273,7 +273,7 @@ public sealed partial class ChatClientAgent : AIAgent
}
catch (Exception ex)
{
await NotifyMessageStoreOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatMessageStoreMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyChatHistoryProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
throw;
}
@@ -286,7 +286,7 @@ public sealed partial class ChatClientAgent : AIAgent
await this.UpdateThreadWithTypeAndConversationIdAsync(safeThread, chatResponse.ConversationId, cancellationToken).ConfigureAwait(false);
// To avoid inconsistent state we only notify the thread of the input messages if no error occurs after the initial request.
await NotifyMessageStoreOfNewMessagesAsync(safeThread, GetInputMessages(inputMessages, continuationToken), chatMessageStoreMessages, aiContextProviderMessages, chatResponse.Messages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyChatHistoryProviderOfNewMessagesAsync(safeThread, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatResponse.Messages, chatOptions, cancellationToken).ConfigureAwait(false);
// Notify the AIContextProvider of all new messages.
await NotifyAIContextProviderOfSuccessAsync(safeThread, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
@@ -304,8 +304,8 @@ public sealed partial class ChatClientAgent : AIAgent
/// <inheritdoc/>
public override async ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
{
ChatMessageStore? messageStore = this._agentOptions?.ChatMessageStoreFactory is not null
? await this._agentOptions.ChatMessageStoreFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false)
ChatHistoryProvider? chatHistoryProvider = this._agentOptions?.ChatHistoryProviderFactory is not null
? await this._agentOptions.ChatHistoryProviderFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false)
: null;
AIContextProvider? contextProvider = this._agentOptions?.AIContextProviderFactory is not null
@@ -314,7 +314,7 @@ public sealed partial class ChatClientAgent : AIAgent
return new ChatClientAgentThread
{
MessageStore = messageStore,
ChatHistoryProvider = chatHistoryProvider,
AIContextProvider = contextProvider
};
}
@@ -329,8 +329,8 @@ public sealed partial class ChatClientAgent : AIAgent
/// </returns>
/// <remarks>
/// <para>
/// This method creates threads that rely on server-side conversation storage, where the chat history
/// is maintained by the underlying AI service rather than in local message stores.
/// This method creates an <see cref="AgentThread"/> that relies on server-side chat history storage, where the chat history
/// is maintained by the underlying AI service rather than by a local <see cref="ChatHistoryProvider"/>.
/// </para>
/// <para>
/// Agent threads created with this method will only work with <see cref="ChatClientAgent"/>
@@ -351,28 +351,28 @@ public sealed partial class ChatClientAgent : AIAgent
}
/// <summary>
/// Creates a new agent thread instance using an existing <see cref="ChatMessageStore"/> to continue a conversation.
/// Creates a new agent thread instance using an existing <see cref="ChatHistoryProvider"/> to continue a conversation.
/// </summary>
/// <param name="chatMessageStore">The <see cref="ChatMessageStore"/> instance to use for managing the conversation's message history.</param>
/// <param name="chatHistoryProvider">The <see cref="ChatHistoryProvider"/> instance to use for managing the conversation's message history.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>
/// A value task representing the asynchronous operation. The task result contains a new <see cref="AgentThread"/> instance configured to work with the provided <paramref name="chatMessageStore"/>.
/// A value task representing the asynchronous operation. The task result contains a new <see cref="AgentThread"/> instance configured to work with the provided <paramref name="chatHistoryProvider"/>.
/// </returns>
/// <remarks>
/// <para>
/// This method creates threads that do not support server-side conversation storage.
/// Some AI services require server-side conversation storage to function properly, and creating a thread
/// with a <see cref="ChatMessageStore"/> may not be compatible with these services.
/// with a <see cref="ChatHistoryProvider"/> may not be compatible with these services.
/// </para>
/// <para>
/// Where a service requires server-side conversation storage, use <see cref="GetNewThreadAsync(string, CancellationToken)"/>.
/// </para>
/// <para>
/// If the agent detects, during the first run, that the underlying AI service requires server-side conversation storage,
/// the thread will throw an exception to indicate that it cannot continue using the provided <see cref="ChatMessageStore"/>.
/// the thread will throw an exception to indicate that it cannot continue using the provided <see cref="ChatHistoryProvider"/>.
/// </para>
/// </remarks>
public async ValueTask<AgentThread> GetNewThreadAsync(ChatMessageStore chatMessageStore, CancellationToken cancellationToken = default)
public async ValueTask<AgentThread> GetNewThreadAsync(ChatHistoryProvider chatHistoryProvider, CancellationToken cancellationToken = default)
{
AIContextProvider? contextProvider = this._agentOptions?.AIContextProviderFactory is not null
? await this._agentOptions.AIContextProviderFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false)
@@ -380,7 +380,7 @@ public sealed partial class ChatClientAgent : AIAgent
return new ChatClientAgentThread()
{
MessageStore = Throw.IfNull(chatMessageStore),
ChatHistoryProvider = Throw.IfNull(chatHistoryProvider),
AIContextProvider = contextProvider
};
}
@@ -388,9 +388,9 @@ public sealed partial class ChatClientAgent : AIAgent
/// <inheritdoc/>
public override async ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<ChatMessageStore>>? chatMessageStoreFactory = this._agentOptions?.ChatMessageStoreFactory is null ?
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<ChatHistoryProvider>>? chatHistoryProviderFactory = this._agentOptions?.ChatHistoryProviderFactory is null ?
null :
(jse, jso, ct) => this._agentOptions.ChatMessageStoreFactory.Invoke(new() { SerializedState = jse, JsonSerializerOptions = jso }, ct);
(jse, jso, ct) => this._agentOptions.ChatHistoryProviderFactory.Invoke(new() { SerializedState = jse, JsonSerializerOptions = jso }, ct);
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<AIContextProvider>>? aiContextProviderFactory = this._agentOptions?.AIContextProviderFactory is null ?
null :
@@ -399,7 +399,7 @@ public sealed partial class ChatClientAgent : AIAgent
return await ChatClientAgentThread.DeserializeAsync(
serializedThread,
jsonSerializerOptions,
chatMessageStoreFactory,
chatHistoryProviderFactory,
aiContextProviderFactory,
cancellationToken).ConfigureAwait(false);
}
@@ -422,7 +422,7 @@ public sealed partial class ChatClientAgent : AIAgent
ChatOptions? chatOptions,
List<ChatMessage> inputMessagesForChatClient,
IList<ChatMessage>? aiContextProviderMessages,
IList<ChatMessage>? chatMessageStoreMessages,
IList<ChatMessage>? chatHistoryProviderMessages,
ChatClientAgentContinuationToken? _) =
await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false);
@@ -442,7 +442,7 @@ public sealed partial class ChatClientAgent : AIAgent
}
catch (Exception ex)
{
await NotifyMessageStoreOfFailureAsync(safeThread, ex, inputMessages, chatMessageStoreMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyChatHistoryProviderOfFailureAsync(safeThread, ex, inputMessages, chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
throw;
}
@@ -460,7 +460,7 @@ public sealed partial class ChatClientAgent : AIAgent
}
// Only notify the thread of new messages if the chatResponse was successful to avoid inconsistent message state in the thread.
await NotifyMessageStoreOfNewMessagesAsync(safeThread, inputMessages, chatMessageStoreMessages, aiContextProviderMessages, chatResponse.Messages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyChatHistoryProviderOfNewMessagesAsync(safeThread, inputMessages, chatHistoryProviderMessages, aiContextProviderMessages, chatResponse.Messages, chatOptions, cancellationToken).ConfigureAwait(false);
// Notify the AIContextProvider of all new messages.
await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
@@ -672,7 +672,7 @@ public sealed partial class ChatClientAgent : AIAgent
ChatOptions? ChatOptions,
List<ChatMessage> InputMessagesForChatClient,
IList<ChatMessage>? AIContextProviderMessages,
IList<ChatMessage>? ChatMessageStoreMessages,
IList<ChatMessage>? ChatHistoryProviderMessages,
ChatClientAgentContinuationToken? ContinuationToken
)> PrepareThreadAndMessagesAsync(
AgentThread? thread,
@@ -703,20 +703,20 @@ public sealed partial class ChatClientAgent : AIAgent
List<ChatMessage> inputMessagesForChatClient = [];
IList<ChatMessage>? aiContextProviderMessages = null;
IList<ChatMessage>? chatMessageStoreMessages = null;
IList<ChatMessage>? chatHistoryProviderMessages = null;
// Populate the thread messages only if we are not continuing an existing response as it's not allowed
if (chatOptions?.ContinuationToken is null)
{
ChatMessageStore? chatMessageStore = ResolveChatMessageStore(typedThread, chatOptions);
ChatHistoryProvider? chatHistoryProvider = ResolveChatHistoryProvider(typedThread, chatOptions);
// Add any existing messages from the chatMessageStore to the messages to be sent to the chat client.
if (chatMessageStore is not null)
// Add any existing messages from the thread to the messages to be sent to the chat client.
if (chatHistoryProvider is not null)
{
var invokingContext = new ChatMessageStore.InvokingContext(inputMessages);
var storeMessages = await chatMessageStore.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
inputMessagesForChatClient.AddRange(storeMessages);
chatMessageStoreMessages = storeMessages as IList<ChatMessage> ?? storeMessages.ToList();
var invokingContext = new ChatHistoryProvider.InvokingContext(inputMessages);
var providerMessages = await chatHistoryProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
inputMessagesForChatClient.AddRange(providerMessages);
chatHistoryProviderMessages = providerMessages as IList<ChatMessage> ?? providerMessages.ToList();
}
// Add the input messages before getting context from AIContextProvider.
@@ -770,7 +770,7 @@ public sealed partial class ChatClientAgent : AIAgent
chatOptions.ConversationId = typedThread.ConversationId;
}
return (typedThread, chatOptions, inputMessagesForChatClient, aiContextProviderMessages, chatMessageStoreMessages, continuationToken);
return (typedThread, chatOptions, inputMessagesForChatClient, aiContextProviderMessages, chatHistoryProviderMessages, continuationToken);
}
private async Task UpdateThreadWithTypeAndConversationIdAsync(ChatClientAgentThread thread, string? responseConversationId, CancellationToken cancellationToken)
@@ -791,78 +791,78 @@ public sealed partial class ChatClientAgent : AIAgent
else
{
// If the service doesn't use service side chat history storage (i.e. we got no id back from invocation), and
// the thread has no MessageStore yet, we should update the thread with the custom MessageStore or
// default InMemoryMessageStore so that it has somewhere to store the chat history.
thread.MessageStore ??= this._agentOptions?.ChatMessageStoreFactory is not null
? await this._agentOptions.ChatMessageStoreFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false)
: new InMemoryChatMessageStore();
// the thread has no ChatHistoryProvider yet, we should update the thread with the custom ChatHistoryProvider or
// default InMemoryChatHistoryProvider so that it has somewhere to store the chat history.
thread.ChatHistoryProvider ??= this._agentOptions?.ChatHistoryProviderFactory is not null
? await this._agentOptions.ChatHistoryProviderFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false)
: new InMemoryChatHistoryProvider();
}
}
private static Task NotifyMessageStoreOfFailureAsync(
private static Task NotifyChatHistoryProviderOfFailureAsync(
ChatClientAgentThread thread,
Exception ex,
IEnumerable<ChatMessage> requestMessages,
IEnumerable<ChatMessage>? chatMessageStoreMessages,
IEnumerable<ChatMessage>? chatHistoryProviderMessages,
IEnumerable<ChatMessage>? aiContextProviderMessages,
ChatOptions? chatOptions,
CancellationToken cancellationToken)
{
ChatMessageStore? chatMessageStore = ResolveChatMessageStore(thread, chatOptions);
ChatHistoryProvider? provider = ResolveChatHistoryProvider(thread, chatOptions);
// Only notify the message store if we have one.
// Only notify the provider if we have one.
// If we don't have one, it means that the chat history is service managed and the underlying service is responsible for storing messages.
if (chatMessageStore is not null)
if (provider is not null)
{
var invokedContext = new ChatMessageStore.InvokedContext(requestMessages, chatMessageStoreMessages)
var invokedContext = new ChatHistoryProvider.InvokedContext(requestMessages, chatHistoryProviderMessages!)
{
AIContextProviderMessages = aiContextProviderMessages,
InvokeException = ex
};
return chatMessageStore.InvokedAsync(invokedContext, cancellationToken).AsTask();
return provider.InvokedAsync(invokedContext, cancellationToken).AsTask();
}
return Task.CompletedTask;
}
private static Task NotifyMessageStoreOfNewMessagesAsync(
private static Task NotifyChatHistoryProviderOfNewMessagesAsync(
ChatClientAgentThread thread,
IEnumerable<ChatMessage> requestMessages,
IEnumerable<ChatMessage>? chatMessageStoreMessages,
IEnumerable<ChatMessage>? chatHistoryProviderMessages,
IEnumerable<ChatMessage>? aiContextProviderMessages,
IEnumerable<ChatMessage> responseMessages,
ChatOptions? chatOptions,
CancellationToken cancellationToken)
{
ChatMessageStore? chatMessageStore = ResolveChatMessageStore(thread, chatOptions);
ChatHistoryProvider? provider = ResolveChatHistoryProvider(thread, chatOptions);
// Only notify the message store if we have one.
// Only notify the provider if we have one.
// If we don't have one, it means that the chat history is service managed and the underlying service is responsible for storing messages.
if (chatMessageStore is not null)
if (provider is not null)
{
var invokedContext = new ChatMessageStore.InvokedContext(requestMessages, chatMessageStoreMessages)
var invokedContext = new ChatHistoryProvider.InvokedContext(requestMessages, chatHistoryProviderMessages!)
{
AIContextProviderMessages = aiContextProviderMessages,
ResponseMessages = responseMessages
};
return chatMessageStore.InvokedAsync(invokedContext, cancellationToken).AsTask();
return provider.InvokedAsync(invokedContext, cancellationToken).AsTask();
}
return Task.CompletedTask;
}
private static ChatMessageStore? ResolveChatMessageStore(ChatClientAgentThread thread, ChatOptions? chatOptions)
private static ChatHistoryProvider? ResolveChatHistoryProvider(ChatClientAgentThread thread, ChatOptions? chatOptions)
{
ChatMessageStore? chatMessageStore = thread.MessageStore;
ChatHistoryProvider? provider = thread.ChatHistoryProvider;
// If someone provided an override ChatMessageStore via AdditionalProperties, we should use that instead of the one on the thread.
if (chatOptions?.AdditionalProperties?.TryGetValue(out ChatMessageStore? overrideChatMessageStore) is true)
// If someone provided an override ChatHistoryProvider via AdditionalProperties, we should use that instead of the one on the thread.
if (chatOptions?.AdditionalProperties?.TryGetValue(out ChatHistoryProvider? overrideProvider) is true)
{
chatMessageStore = overrideChatMessageStore;
provider = overrideProvider;
}
return chatMessageStore;
return provider;
}
private static ChatClientAgentContinuationToken? WrapContinuationToken(ResponseContinuationToken? continuationToken, IEnumerable<ChatMessage>? inputMessages = null, List<ChatResponseUpdate>? responseUpdates = null)
@@ -39,10 +39,10 @@ public sealed class ChatClientAgentOptions
public ChatOptions? ChatOptions { get; set; }
/// <summary>
/// Gets or sets a factory function to create an instance of <see cref="ChatMessageStore"/>
/// which will be used to store chat messages for this agent.
/// Gets or sets a factory function to create an instance of <see cref="ChatHistoryProvider"/>
/// which will be used to provide chat history for this agent.
/// </summary>
public Func<ChatMessageStoreFactoryContext, CancellationToken, ValueTask<ChatMessageStore>>? ChatMessageStoreFactory { get; set; }
public Func<ChatHistoryProviderFactoryContext, CancellationToken, ValueTask<ChatHistoryProvider>>? ChatHistoryProviderFactory { get; set; }
/// <summary>
/// Gets or sets a factory function to create an instance of <see cref="AIContextProvider"/>
@@ -75,7 +75,7 @@ public sealed class ChatClientAgentOptions
Name = this.Name,
Description = this.Description,
ChatOptions = this.ChatOptions?.Clone(),
ChatMessageStoreFactory = this.ChatMessageStoreFactory,
ChatHistoryProviderFactory = this.ChatHistoryProviderFactory,
AIContextProviderFactory = this.AIContextProviderFactory,
};
@@ -97,14 +97,14 @@ public sealed class ChatClientAgentOptions
}
/// <summary>
/// Context object passed to the <see cref="ChatMessageStoreFactory"/> to create a new instance of <see cref="ChatMessageStore"/>.
/// Context object passed to the <see cref="ChatHistoryProviderFactory"/> to create a new instance of <see cref="ChatHistoryProvider"/>.
/// </summary>
public sealed class ChatMessageStoreFactoryContext
public sealed class ChatHistoryProviderFactoryContext
{
/// <summary>
/// Gets or sets the serialized state of the chat message store, if any.
/// Gets or sets the serialized state of the <see cref="ChatHistoryProvider"/>, if any.
/// </summary>
/// <value><see langword="default"/> if there is no state, e.g. when the <see cref="ChatMessageStore"/> is first created.</value>
/// <value><see langword="default"/> if there is no state, e.g. when the <see cref="ChatHistoryProvider"/> is first created.</value>
public JsonElement SerializedState { get; set; }
/// <summary>
@@ -15,7 +15,7 @@ namespace Microsoft.Agents.AI;
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class ChatClientAgentThread : AgentThread
{
private ChatMessageStore? _messageStore;
private ChatHistoryProvider? _chatHistoryProvider;
/// <summary>
/// Initializes a new instance of the <see cref="ChatClientAgentThread"/> class.
@@ -29,14 +29,14 @@ public sealed class ChatClientAgentThread : AgentThread
/// </summary>
/// <remarks>
/// <para>
/// Note that either <see cref="ConversationId"/> or <see cref="MessageStore "/> may be set, but not both.
/// If <see cref="MessageStore "/> is not null, setting <see cref="ConversationId"/> will throw an
/// Note that either <see cref="ConversationId"/> or <see cref="ChatHistoryProvider "/> may be set, but not both.
/// If <see cref="ChatHistoryProvider "/> is not null, setting <see cref="ConversationId"/> will throw an
/// <see cref="InvalidOperationException "/> exception.
/// </para>
/// <para>
/// This property may be null in the following cases:
/// <list type="bullet">
/// <item><description>The thread stores messages via the <see cref="ChatMessageStore"/> and not in the agent service.</description></item>
/// <item><description>The thread stores messages via the <see cref="AI.ChatHistoryProvider"/> and not in the agent service.</description></item>
/// <item><description>This thread object is new and a server managed thread has not yet been created in the agent service.</description></item>
/// </list>
/// </para>
@@ -46,7 +46,7 @@ public sealed class ChatClientAgentThread : AgentThread
/// to fork the thread with each iteration.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">Attempted to set a conversation ID but a <see cref="MessageStore"/> is already set.</exception>
/// <exception cref="InvalidOperationException">Attempted to set a conversation ID but a <see cref="ChatHistoryProvider"/> is already set.</exception>
public string? ConversationId
{
get;
@@ -57,12 +57,12 @@ public sealed class ChatClientAgentThread : AgentThread
return;
}
if (this._messageStore is not null)
if (this._chatHistoryProvider is not null)
{
// If we have a message store already, we shouldn't switch the thread to use a conversation id
// If we have a ChatHistoryProvider already, we shouldn't switch the thread to use a conversation id
// since it means that the thread contents will essentially be deleted, and the thread will not work
// with the original agent anymore.
throw new InvalidOperationException("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.");
throw new InvalidOperationException("Only the ConversationId or ChatHistoryProvider may be set, but not both and switching from one to another is not supported.");
}
field = Throw.IfNullOrWhitespace(value);
@@ -70,40 +70,40 @@ public sealed class ChatClientAgentThread : AgentThread
}
/// <summary>
/// Gets or sets the <see cref="ChatMessageStore"/> used by this thread, for cases where messages should be stored in a custom location.
/// Gets or sets the <see cref="AI.ChatHistoryProvider"/> used by this thread, for cases where messages should be stored in a custom location.
/// </summary>
/// <remarks>
/// <para>
/// Note that either <see cref="ConversationId"/> or <see cref="MessageStore "/> may be set, but not both.
/// If <see cref="ConversationId"/> is not null, and <see cref="MessageStore "/> is set, <see cref="ConversationId"/>
/// Note that either <see cref="ConversationId"/> or <see cref="ChatHistoryProvider "/> may be set, but not both.
/// If <see cref="ConversationId"/> is not null, and <see cref="ChatHistoryProvider "/> is set, <see cref="ConversationId"/>
/// will be reverted to null, and vice versa.
/// </para>
/// <para>
/// This property may be null in the following cases:
/// <list type="bullet">
/// <item><description>The thread stores messages in the agent service and just has an id to the remove thread, instead of in an <see cref="ChatMessageStore"/>.</description></item>
/// <item><description>This thread object is new it is not yet clear whether it will be backed by a server managed thread or an <see cref="ChatMessageStore"/>.</description></item>
/// <item><description>The thread stores messages in the agent service and just has an id to the remove thread, instead of in an <see cref="AI.ChatHistoryProvider"/>.</description></item>
/// <item><description>This thread object is new it is not yet clear whether it will be backed by a server managed thread or an <see cref="AI.ChatHistoryProvider"/>.</description></item>
/// </list>
/// </para>
/// </remarks>
public ChatMessageStore? MessageStore
public ChatHistoryProvider? ChatHistoryProvider
{
get => this._messageStore;
get => this._chatHistoryProvider;
internal set
{
if (this._messageStore is null && value is null)
if (this._chatHistoryProvider is null && value is null)
{
return;
}
if (!string.IsNullOrWhiteSpace(this.ConversationId))
{
// If we have a conversation id already, we shouldn't switch the thread to use a message store
// If we have a conversation id already, we shouldn't switch the thread to use a ChatHistoryProvider
// since it means that the thread will not work with the original agent anymore.
throw new InvalidOperationException("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.");
throw new InvalidOperationException("Only the ConversationId or ChatHistoryProvider may be set, but not both and switching from one to another is not supported.");
}
this._messageStore = Throw.IfNull(value);
this._chatHistoryProvider = Throw.IfNull(value);
}
}
@@ -117,9 +117,9 @@ public sealed class ChatClientAgentThread : AgentThread
/// </summary>
/// <param name="serializedThreadState">A <see cref="JsonElement"/> representing the serialized state of the thread.</param>
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
/// <param name="chatMessageStoreFactory">
/// An optional factory function to create a custom <see cref="ChatMessageStore"/> from its serialized state.
/// If not provided, the default in-memory message store will be used.
/// <param name="chatHistoryProviderFactory">
/// An optional factory function to create a custom <see cref="AI.ChatHistoryProvider"/> from its serialized state.
/// If not provided, the default <see cref="InMemoryChatHistoryProvider"/> will be used.
/// </param>
/// <param name="aiContextProviderFactory">
/// An optional factory function to create a custom <see cref="AIContextProvider"/> from its serialized state.
@@ -130,7 +130,7 @@ public sealed class ChatClientAgentThread : AgentThread
internal static async Task<ChatClientAgentThread> DeserializeAsync(
JsonElement serializedThreadState,
JsonSerializerOptions? jsonSerializerOptions = null,
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<ChatMessageStore>>? chatMessageStoreFactory = null,
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<ChatHistoryProvider>>? chatHistoryProviderFactory = null,
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<AIContextProvider>>? aiContextProviderFactory = null,
CancellationToken cancellationToken = default)
{
@@ -152,14 +152,14 @@ public sealed class ChatClientAgentThread : AgentThread
{
thread.ConversationId = threadId;
// Since we have an ID, we should not have a chat message store and we can return here.
// Since we have an ID, we should not have a ChatHistoryProvider and we can return here.
return thread;
}
thread._messageStore =
chatMessageStoreFactory is not null
? await chatMessageStoreFactory.Invoke(state?.StoreState ?? default, jsonSerializerOptions, cancellationToken).ConfigureAwait(false)
: new InMemoryChatMessageStore(state?.StoreState ?? default, jsonSerializerOptions); // default to an in-memory store
thread._chatHistoryProvider =
chatHistoryProviderFactory is not null
? await chatHistoryProviderFactory.Invoke(state?.ChatHistoryProviderState ?? default, jsonSerializerOptions, cancellationToken).ConfigureAwait(false)
: new InMemoryChatHistoryProvider(state?.ChatHistoryProviderState ?? default, jsonSerializerOptions); // default to an in-memory ChatHistoryProvider
return thread;
}
@@ -167,14 +167,14 @@ public sealed class ChatClientAgentThread : AgentThread
/// <inheritdoc/>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
JsonElement? storeState = this._messageStore?.Serialize(jsonSerializerOptions);
JsonElement? chatHistoryProviderState = this._chatHistoryProvider?.Serialize(jsonSerializerOptions);
JsonElement? aiContextProviderState = this.AIContextProvider?.Serialize(jsonSerializerOptions);
var state = new ThreadState
{
ConversationId = this.ConversationId,
StoreState = storeState is { ValueKind: not JsonValueKind.Undefined } ? storeState : null,
ChatHistoryProviderState = chatHistoryProviderState is { ValueKind: not JsonValueKind.Undefined } ? chatHistoryProviderState : null,
AIContextProviderState = aiContextProviderState is { ValueKind: not JsonValueKind.Undefined } ? aiContextProviderState : null,
};
@@ -185,20 +185,20 @@ public sealed class ChatClientAgentThread : AgentThread
public override object? GetService(Type serviceType, object? serviceKey = null) =>
base.GetService(serviceType, serviceKey)
?? this.AIContextProvider?.GetService(serviceType, serviceKey)
?? this.MessageStore?.GetService(serviceType, serviceKey);
?? this.ChatHistoryProvider?.GetService(serviceType, serviceKey);
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay =>
this.ConversationId is { } conversationId ? $"ConversationId = {conversationId}" :
this._messageStore is InMemoryChatMessageStore inMemoryStore ? $"Count = {inMemoryStore.Count}" :
this._messageStore is { } store ? $"Store = {store.GetType().Name}" :
this._chatHistoryProvider is InMemoryChatHistoryProvider inMemoryChatHistoryProvider ? $"Count = {inMemoryChatHistoryProvider.Count}" :
this._chatHistoryProvider is { } chatHistoryProvider ? $"ChatHistoryProvider = {chatHistoryProvider.GetType().Name}" :
"Count = 0";
internal sealed class ThreadState
{
public string? ConversationId { get; set; }
public JsonElement? StoreState { get; set; }
public JsonElement? ChatHistoryProviderState { get; set; }
public JsonElement? AIContextProviderState { get; set; }
}