diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs index 3e52a5f01a..1c1943d385 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs @@ -45,18 +45,19 @@ protected override async Task RunCoreAsync(IEnumerable responseMessages = CloneAndToUpperCase(messages, this.Name).ToList(); // Notify the thread of the input and output messages. - var invokedContext = new ChatMessageStore.InvokedContext(messages, storeMessages) + var invokedContext = new AIContextProvider.InvokedContext(messages, null) { + ChatHistoryMessages = historyProviderAIContext.Messages, ResponseMessages = responseMessages }; - await typedThread.MessageStore.InvokedAsync(invokedContext, cancellationToken); + await typedThread.ChatHistoryProvider.InvokedAsync(invokedContext, cancellationToken); return new AgentResponse { @@ -77,18 +78,19 @@ protected override async IAsyncEnumerable RunCoreStreamingA } // Get existing messages from the store - var invokingContext = new ChatMessageStore.InvokingContext(messages); - var storeMessages = await typedThread.MessageStore.InvokingAsync(invokingContext, cancellationToken); + var invokingContext = new AIContextProvider.InvokingContext(messages); + var historyProviderAIContext = await typedThread.ChatHistoryProvider.InvokingAsync(invokingContext, cancellationToken); // Clone the input messages and turn them into response messages with upper case text. List responseMessages = CloneAndToUpperCase(messages, this.Name).ToList(); // Notify the thread of the input and output messages. - var invokedContext = new ChatMessageStore.InvokedContext(messages, storeMessages) + var invokedContext = new AIContextProvider.InvokedContext(messages, null) { + ChatHistoryMessages = historyProviderAIContext.Messages, ResponseMessages = responseMessages }; - await typedThread.MessageStore.InvokedAsync(invokedContext, cancellationToken); + await typedThread.ChatHistoryProvider.InvokedAsync(invokedContext, cancellationToken); foreach (var message in responseMessages) { diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs index a4904ecf77..cadd2fc7bd 100644 --- a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs @@ -66,7 +66,7 @@ // Since we are using ChatCompletion which stores chat history locally, we can also add a message removal policy // that removes messages produced by the TextSearchProvider before they are added to the chat history, so that // we don't bloat chat history with all the search result messages. - ChatMessageStoreFactory = (ctx, ct) => new ValueTask(new InMemoryChatMessageStore(ctx.SerializedState, ctx.JsonSerializerOptions) + ChatHistoryProviderFactory = (ctx, ct) => new ValueTask(new InMemoryChatHistoryProvider(ctx.SerializedState, ctx.JsonSerializerOptions) .WithAIContextProviderMessageRemoval()), }); diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs index a03b3bb349..89c5eab0d7 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs @@ -31,11 +31,11 @@ { ChatOptions = new() { Instructions = "You are good at telling jokes." }, Name = "Joker", - ChatMessageStoreFactory = (ctx, ct) => new ValueTask( - // Create a new chat message store for this agent that stores the messages in a vector store. - // Each thread must get its own copy of the VectorChatMessageStore, since the store + ChatHistoryProviderFactory = (ctx, ct) => new ValueTask( + // Create a new chat history provider for this agent that stores the chat history in a vector store. + // Each thread must get its own copy of the VectorStoreChatHistoryProvider, since the store // also contains the id that the thread is stored under. - new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions)) + new VectorStoreChatHistoryProvider(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions)) }); // Start a new thread for the agent conversation. @@ -61,20 +61,20 @@ // Run the agent with the thread that stores conversation history in the vector store a second time. Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread)); -// We can access the VectorChatMessageStore via the thread's GetService method if we need to read the key under which threads are stored. -var messageStore = resumedThread.GetService()!; -Console.WriteLine($"\nThread is stored in vector store under key: {messageStore.ThreadDbKey}"); +// We can access the VectorStoreChatHistoryProvider via the thread's GetService method if we need to read the key under which threads are stored. +var chatHistoryProvider = resumedThread.GetService()!; +Console.WriteLine($"\nThread is stored in vector store under key: {chatHistoryProvider.ThreadDbKey}"); namespace SampleApp { /// - /// A sample implementation of that stores chat messages in a vector store. + /// A sample implementation of that stores and provides chat history from a vector store. /// - internal sealed class VectorChatMessageStore : ChatMessageStore + internal sealed class VectorStoreChatHistoryProvider : AIContextProvider { private readonly VectorStore _vectorStore; - public VectorChatMessageStore(VectorStore vectorStore, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null) + public VectorStoreChatHistoryProvider(VectorStore vectorStore, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null) { this._vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore)); @@ -87,7 +87,7 @@ public VectorChatMessageStore(VectorStore vectorStore, JsonElement serializedSto public string? ThreadDbKey { get; private set; } - public override async ValueTask> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) + public override async ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) { var collection = this._vectorStore.GetCollection("ChatHistory"); await collection.EnsureCollectionExistsAsync(cancellationToken); @@ -102,7 +102,7 @@ public override async ValueTask> InvokingAsync(Invoking var messages = records.ConvertAll(x => JsonSerializer.Deserialize(x.SerializedMessage!)!) ; messages.Reverse(); - return messages; + return new() { Messages = messages }; } public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Program.cs index a80dd0fed0..25ddee3b25 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Program.cs @@ -15,7 +15,7 @@ var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; -// Construct the agent, and provide a factory to create an in-memory chat message store with a reducer that keeps only the last 2 non-system messages. +// Construct the agent, and provide a factory to create an in-memory chat history provider with a reducer that keeps only the last 2 non-system messages. AIAgent agent = new AzureOpenAIClient( new Uri(endpoint), new AzureCliCredential()) @@ -24,7 +24,7 @@ { ChatOptions = new() { Instructions = "You are good at telling jokes." }, Name = "Joker", - ChatMessageStoreFactory = (ctx, ct) => new ValueTask(new InMemoryChatMessageStore(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions)) + ChatHistoryProviderFactory = (ctx, ct) => new ValueTask(new InMemoryChatHistoryProvider(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions)) }); AgentThread thread = await agent.GetNewThreadAsync(); diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs index f104f12890..75de702c97 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs @@ -176,6 +176,19 @@ public InvokedContext(IEnumerable requestMessages, IEnumerable public IEnumerable RequestMessages { get; set { field = Throw.IfNull(value); } } + /// + /// Gets the messages retrieved from the chat history provider for this invocation, if any. + /// + /// + /// Note that if chat history is stored in the underlying AI service, this property will be null. + /// Only chat history retrieved via a chat history provider will be provided here. + /// + /// + /// A collection of instances that were retrieved from the chat history, + /// and were used by the agent as part of the invocation. + /// + public IEnumerable? ChatHistoryMessages { get; set; } + /// /// Gets the messages provided by the for this invocation, if any. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProviderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProviderExtensions.cs new file mode 100644 index 0000000000..0d828dc510 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProviderExtensions.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.Agents.AI; + +/// +/// Contains extension methods for the class. +/// +public static class AIContextProviderExtensions +{ + /// + /// Adds message filtering to an existing , so that data passed to to and from it + /// can be filtered, updated or replaced. + /// + /// The underlying AI context provider to be wrapped. Cannot be null. + /// An optional filter function to apply to the AI context before it is returned. If null, no filter is applied at this + /// stage. + /// An optional filter function to apply to the invocation context before it is consumed. If null, no + /// filter is applied at this stage. + /// The with filtering applied. + public static AIContextProvider WithMessageFilters( + this AIContextProvider innerAIContextProvider, + Func? invokingContextFilter = null, + Func? invokedContextFilter = null) + { + return new MessageFilteringAIContextProvider( + innerAIContextProvider, + invokingContextFilter, + invokedContextFilter); + } + + /// + /// Decorates the provided so that it does not receive messages produced by any . + /// + /// The underlying AI context provider to add the filter to. Cannot be null. + /// A new instance that filters out messages. + public static AIContextProvider WithAIContextProviderMessageRemoval(this AIContextProvider innerAIContextProvider) + { + return new MessageFilteringAIContextProvider( + innerAIContextProvider, + invokedContextFilter: (ctx) => + { + ctx.AIContextProviderMessages = null; + return ctx; + }); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentAbstractionsJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentAbstractionsJsonUtilities.cs index 937d871c56..3d420fb573 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentAbstractionsJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentAbstractionsJsonUtilities.cs @@ -82,7 +82,7 @@ private static JsonSerializerOptions CreateDefaultOptions() [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; diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentThread.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentThread.cs index 318307ec43..63cc423227 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentThread.cs @@ -68,7 +68,7 @@ public virtual JsonElement Serialize(JsonSerializerOptions? jsonSerializerOption /// is . /// /// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the , - /// including itself or any services it might be wrapping. For example, to access a if available for the instance, + /// including itself or any services it might be wrapping. For example, to access an if available for the instance, /// may be used to request it. /// public virtual object? GetService(Type serviceType, object? serviceKey = null) diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatMessageStore.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatMessageStore.cs deleted file mode 100644 index 54cee063d7..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatMessageStore.cs +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.AI; - -/// -/// Provides an abstract base class for storing and managing chat messages associated with agent conversations. -/// -/// -/// -/// defines the contract for persistent storage of chat messages in agent conversations. -/// Implementations are responsible for managing message persistence, retrieval, and any necessary optimization -/// strategies such as truncation, summarization, or archival. -/// -/// -/// Key responsibilities include: -/// -/// Storing chat messages with proper ordering and metadata preservation -/// Retrieving messages in chronological order for agent context -/// Managing storage limits through truncation, summarization, or other strategies -/// Supporting serialization for thread persistence and migration -/// -/// -/// -public abstract class ChatMessageStore -{ - /// - /// 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. - /// - /// Contains the request context including the caller provided messages that will be used by the agent for this invocation. - /// The to monitor for cancellation requests. The default is . - /// - /// A task that represents the asynchronous operation. The task result contains a collection of - /// instances in ascending chronological order (oldest first). - /// - /// - /// - /// Messages are returned in chronological order to maintain proper conversation flow and context for the agent. - /// The oldest messages appear first in the collection, followed by more recent messages. - /// - /// - /// If the total message history becomes very large, implementations should apply appropriate strategies to manage - /// storage constraints, such as: - /// - /// Truncating older messages while preserving recent context - /// Summarizing message groups to maintain essential context - /// Implementing sliding window approaches for message retention - /// Archiving old messages while keeping active conversation context - /// - /// - /// - /// Each store instance should be associated with a single conversation thread to ensure proper message isolation - /// and context management. - /// - /// - public abstract ValueTask> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default); - - /// - /// Called at the end of the agent invocation to add new messages to the store. - /// - /// Contains the invocation context including request messages, response messages, and any exception that occurred. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous add operation. - /// - /// - /// 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 - /// return messages in the correct chronological order. - /// - /// - /// Implementations may perform additional processing during message addition, such as: - /// - /// Validating message content and metadata - /// Applying storage optimizations or compression - /// Triggering background maintenance operations - /// Updating indices or search capabilities - /// - /// - /// - /// This method is called regardless of whether the invocation succeeded or failed. - /// To check if the invocation was successful, inspect the property. - /// - /// - public abstract ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default); - - /// - /// Serializes the current object's state to a using the specified serialization options. - /// - /// The JSON serialization options to use. - /// A representation of the object's state. - public abstract JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null); - - /// Asks the for an object of the specified type . - /// The type of object being requested. - /// An optional key that can be used to help identify the target service. - /// The found object, otherwise . - /// is . - /// - /// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the , - /// including itself or any services it might be wrapping. - /// - public virtual object? GetService(Type serviceType, object? serviceKey = null) - { - _ = Throw.IfNull(serviceType); - - return serviceKey is null && serviceType.IsInstanceOfType(this) - ? this - : null; - } - - /// Asks the for an object of type . - /// The type of the object to be retrieved. - /// An optional key that can be used to help identify the target service. - /// The found object, otherwise . - /// - /// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the , - /// including itself or any services it might be wrapping. - /// - public TService? GetService(object? serviceKey = null) - => this.GetService(typeof(TService), serviceKey) is TService service ? service : default; - - /// - /// Contains the context information provided to . - /// - /// - /// 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. - /// - public sealed class InvokingContext - { - /// - /// Initializes a new instance of the class with the specified request messages. - /// - /// The new messages to be used by the agent for this invocation. - /// is . - public InvokingContext(IEnumerable requestMessages) - { - this.RequestMessages = requestMessages ?? throw new ArgumentNullException(nameof(requestMessages)); - } - - /// - /// Gets the caller provided messages that will be used by the agent for this invocation. - /// - /// - /// A collection of instances representing new messages that were provided by the caller. - /// - public IEnumerable RequestMessages { get; set { field = Throw.IfNull(value); } } - } - - /// - /// Contains the context information provided to . - /// - /// - /// This class provides context about a completed agent invocation, including both the - /// request messages that were used and the response messages that were generated. It also indicates - /// whether the invocation succeeded or failed. - /// - public sealed class InvokedContext - { - /// - /// Initializes a new instance of the class with the specified request messages. - /// - /// The caller provided messages that were used by the agent for this invocation. - /// The messages retrieved from the for this invocation. - /// is . - public InvokedContext(IEnumerable requestMessages, IEnumerable chatMessageStoreMessages) - { - this.RequestMessages = Throw.IfNull(requestMessages); - this.ChatMessageStoreMessages = Throw.IfNull(chatMessageStoreMessages); - } - - /// - /// Gets the caller provided messages that were used by the agent for this invocation. - /// - /// - /// A collection of instances representing new messages that were provided by the caller. - /// This does not include any supplied messages. - /// - public IEnumerable RequestMessages { get; set { field = Throw.IfNull(value); } } - - /// - /// Gets the messages retrieved from the for this invocation, if any. - /// - /// - /// A collection of instances that were retrieved from the , - /// and were used by the agent as part of the invocation. - /// - public IEnumerable ChatMessageStoreMessages { get; set { field = Throw.IfNull(value); } } - - /// - /// Gets or sets the messages provided by the for this invocation, if any. - /// - /// - /// A collection of instances that were provided by the , - /// and were used by the agent as part of the invocation. - /// - public IEnumerable? AIContextProviderMessages { get; set; } - - /// - /// Gets the collection of response messages generated during this invocation if the invocation succeeded. - /// - /// - /// A collection of instances representing the response, - /// or if the invocation failed or did not produce response messages. - /// - public IEnumerable? ResponseMessages { get; set; } - - /// - /// Gets the that was thrown during the invocation, if the invocation failed. - /// - /// - /// The exception that caused the invocation to fail, or if the invocation succeeded. - /// - public Exception? InvokeException { get; set; } - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatMessageStoreExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatMessageStoreExtensions.cs deleted file mode 100644 index a205fc1d9e..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatMessageStoreExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.AI; - -/// -/// Contains extension methods for the class. -/// -public static class ChatMessageStoreExtensions -{ - /// - /// 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. - /// - /// The store to add the message filter to. - /// An optional filter function to apply to messages produced by the store. If null, no filter is applied at this - /// stage. - /// 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. - /// The with filtering applied. - public static ChatMessageStore WithMessageFilters( - this ChatMessageStore store, - Func, IEnumerable>? invokingMessagesFilter = null, - Func? invokedMessagesFilter = null) - { - return new ChatMessageStoreMessageFilter( - innerChatMessageStore: store, - invokingMessagesFilter: invokingMessagesFilter, - invokedMessagesFilter: invokedMessagesFilter); - } - - /// - /// Decorates the provided chat message store so that it does not store messages produced by any . - /// - /// The store to add the message filter to. - /// A new instance that filters out messages so they do not get stored. - public static ChatMessageStore WithAIContextProviderMessageRemoval(this ChatMessageStore store) - { - return new ChatMessageStoreMessageFilter( - innerChatMessageStore: store, - invokedMessagesFilter: (ctx) => - { - ctx.AIContextProviderMessages = null; - return ctx; - }); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatMessageStoreMessageFilter.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatMessageStoreMessageFilter.cs deleted file mode 100644 index e58f233067..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatMessageStoreMessageFilter.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.AI; - -/// -/// A decorator that allows filtering the messages -/// passed into and out of an inner . -/// -public sealed class ChatMessageStoreMessageFilter : ChatMessageStore -{ - private readonly ChatMessageStore _innerChatMessageStore; - private readonly Func, IEnumerable>? _invokingMessagesFilter; - private readonly Func? _invokedMessagesFilter; - - /// - /// Initializes a new instance of the class. - /// - /// 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 - /// additional filtering. - /// The underlying chat message store to be wrapped. Cannot be null. - /// An optional filter function to apply to messages before they are invoked. If null, no filter is applied at this - /// stage. - /// An optional filter function to apply to the invocation context after messages have been invoked. If null, no - /// filter is applied at this stage. - /// Thrown if innerChatMessageStore is null. - public ChatMessageStoreMessageFilter( - ChatMessageStore innerChatMessageStore, - Func, IEnumerable>? invokingMessagesFilter = null, - Func? invokedMessagesFilter = null) - { - this._innerChatMessageStore = Throw.IfNull(innerChatMessageStore); - - if (invokingMessagesFilter == null && invokedMessagesFilter == null) - { - throw new ArgumentException("At least one filter function, invokingMessagesFilter or invokedMessagesFilter, must be provided."); - } - - this._invokingMessagesFilter = invokingMessagesFilter; - this._invokedMessagesFilter = invokedMessagesFilter; - } - - /// - public override async ValueTask> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) - { - var messages = await this._innerChatMessageStore.InvokingAsync(context, cancellationToken).ConfigureAwait(false); - return this._invokingMessagesFilter != null ? this._invokingMessagesFilter(messages) : messages; - } - - /// - public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) - { - if (this._invokedMessagesFilter != null) - { - context = this._invokedMessagesFilter(context); - } - - return this._innerChatMessageStore.InvokedAsync(context, cancellationToken); - } - - /// - public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) - { - return this._innerChatMessageStore.Serialize(jsonSerializerOptions); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryAgentThread.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryAgentThread.cs index 13fcc134f0..732ecff316 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryAgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryAgentThread.cs @@ -9,16 +9,16 @@ namespace Microsoft.Agents.AI; /// -/// Provides an abstract base class for agent threads that maintain all conversation state in local memory. +/// Provides an abstract base class for any that maintains all chat history in local memory. /// /// /// -/// is designed for scenarios where conversation state should be stored locally +/// 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. +/// maintaining full control over the chat history. /// /// -/// In-memory threads do not persist conversation data across application restarts +/// does not persist chat history across application restarts /// unless explicitly serialized and restored. /// /// @@ -28,31 +28,31 @@ public abstract class InMemoryAgentThread : AgentThread /// /// Initializes a new instance of the class. /// - /// - /// An optional instance to use for storing chat messages. - /// If , a new empty message store will be created. + /// + /// An optional instance to use for storing chat messages. + /// If , a new empty will be created. /// /// - /// 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 chat history between instances or providing pre-configured + /// chat history providers with specific reduction or processing logic. /// - protected InMemoryAgentThread(InMemoryChatMessageStore? messageStore = null) + protected InMemoryAgentThread(InMemoryChatHistoryProvider? chatHistoryProvider = null) { - this.MessageStore = messageStore ?? []; + this.ChatHistoryProvider = chatHistoryProvider ?? []; } /// /// Initializes a new instance of the class. /// - /// The initial messages to populate the conversation history. + /// The initial messages to populate the chat history. /// is . /// - /// This constructor is useful for initializing threads with existing conversation history or - /// for migrating conversations from other storage systems. + /// This constructor is useful for initializing threads with existing chat history or + /// for migrating chat history from other storage systems. /// protected InMemoryAgentThread(IEnumerable messages) { - this.MessageStore = [.. messages]; + this.ChatHistoryProvider = [.. messages]; } /// @@ -60,20 +60,20 @@ protected InMemoryAgentThread(IEnumerable messages) /// /// A representing the serialized state of the thread. /// Optional settings for customizing the JSON deserialization process. - /// - /// Optional factory function to create the from its serialized state. + /// + /// Optional factory function to create the from its serialized state. /// If not provided, a default factory will be used that creates a basic in-memory store. /// /// The is not a JSON object. /// The is invalid or cannot be deserialized to the expected type. /// - /// This constructor enables restoration of in-memory threads from previously saved state, allowing + /// This constructor enables restoration of in-memory chat history from previously saved state, allowing /// conversations to be resumed across application restarts or migrated between different instances. /// protected InMemoryAgentThread( JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null, - Func? messageStoreFactory = null) + Func? chatHistoryProviderFactory = null) { if (serializedThreadState.ValueKind != JsonValueKind.Object) { @@ -83,15 +83,15 @@ protected InMemoryAgentThread( 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?.StoreState ?? default, jsonSerializerOptions) ?? + new InMemoryChatHistoryProvider(state?.StoreState ?? default, jsonSerializerOptions); } /// - /// Gets or sets the used by this thread. + /// Gets or sets the used by this thread. /// - public InMemoryChatMessageStore MessageStore { get; } + public InMemoryChatHistoryProvider ChatHistoryProvider { get; } /// /// Serializes the current object's state to a using the specified serialization options. @@ -100,7 +100,7 @@ protected InMemoryAgentThread( /// A representation of the object's state. public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) { - var storeState = this.MessageStore.Serialize(jsonSerializerOptions); + var storeState = this.ChatHistoryProvider.Serialize(jsonSerializerOptions); var state = new InMemoryAgentThreadState { @@ -112,10 +112,10 @@ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptio /// 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 { diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatMessageStore.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs similarity index 80% rename from dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatMessageStore.cs rename to dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs index 1fb1b568ae..c6d0ea6201 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatMessageStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs @@ -14,12 +14,12 @@ namespace Microsoft.Agents.AI; /// -/// Provides an in-memory implementation of with support for message reduction and collection semantics. +/// Provides an in-memory chat history storage implementation of with support for message reduction and collection semantics. /// /// /// -/// stores chat messages entirely in local memory, providing fast access and manipulation -/// capabilities. It implements both for agent integration and +/// stores chat messages entirely in local memory, providing fast access and manipulation +/// capabilities. It implements both for agent integration and /// for direct collection manipulation. /// /// @@ -29,40 +29,40 @@ namespace Microsoft.Agents.AI; /// [DebuggerDisplay("Count = {Count}")] [DebuggerTypeProxy(typeof(DebugView))] -public sealed class InMemoryChatMessageStore : ChatMessageStore, IList, IReadOnlyList +public sealed class InMemoryChatHistoryProvider : AIContextProvider, IList, IReadOnlyList { private List _messages; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// This constructor creates a basic in-memory store without message reduction capabilities. /// Messages will be stored exactly as added without any automatic processing or reduction. /// - public InMemoryChatMessageStore() + public InMemoryChatHistoryProvider() { this._messages = []; } /// - /// Initializes a new instance of the class from previously serialized state. + /// Initializes a new instance of the class from previously serialized state. /// - /// A representing the serialized state of the message store. + /// A representing the serialized state of the message provider. /// Optional settings for customizing the JSON deserialization process. /// The is not a valid JSON object or cannot be deserialized. /// - /// This constructor enables restoration of message stores from previously saved state, allowing - /// conversation history to be preserved across application restarts or migrated between instances. + /// This constructor enables restoration of chat history from previously saved state, allowing + /// conversations to be preserved across application restarts or migrated between instances. /// The store will be configured with default settings and message reduction before retrieval. /// - public InMemoryChatMessageStore(JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null) + public InMemoryChatHistoryProvider(JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null) : this(null, serializedStoreState, jsonSerializerOptions, ChatReducerTriggerEvent.BeforeMessagesRetrieval) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// A instance used to process, reduce, or optimize chat messages. @@ -77,20 +77,20 @@ public InMemoryChatMessageStore(JsonElement serializedStoreState, JsonSerializer /// Message reducers enable automatic management of message storage by implementing strategies to /// keep memory usage under control while preserving important conversation context. /// - public InMemoryChatMessageStore(IChatReducer chatReducer, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval) + public InMemoryChatHistoryProvider(IChatReducer chatReducer, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval) : this(chatReducer, default, null, reducerTriggerEvent) { Throw.IfNull(chatReducer); } /// - /// Initializes a new instance of the class, with an existing state from a serialized JSON element. + /// Initializes a new instance of the class, with an existing state from a serialized JSON element. /// /// An optional instance used to process or reduce chat messages. If null, no reduction logic will be applied. /// A representing the serialized state of the store. /// Optional settings for customizing the JSON deserialization process. /// The event that should trigger the reducer invocation. - public InMemoryChatMessageStore(IChatReducer? chatReducer, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval) + public InMemoryChatHistoryProvider(IChatReducer? chatReducer, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval) { this.ChatReducer = chatReducer; this.ReducerTriggerEvent = reducerTriggerEvent; @@ -99,7 +99,7 @@ public InMemoryChatMessageStore(IChatReducer? chatReducer, JsonElement serialize { var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions; var state = serializedStoreState.Deserialize( - jso.GetTypeInfo(typeof(StoreState))) as StoreState; + jso.GetTypeInfo(typeof(State))) as State; if (state?.Messages is { } messages) { this._messages = messages; @@ -134,7 +134,7 @@ public ChatMessage this[int index] } /// - public override async ValueTask> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) + public override async ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) { _ = Throw.IfNull(context); @@ -143,7 +143,7 @@ public override async ValueTask> InvokingAsync(Invoking this._messages = (await this.ChatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList(); } - return this._messages; + return new() { Messages = this._messages }; } /// @@ -169,13 +169,13 @@ public override async ValueTask InvokedAsync(InvokedContext context, Cancellatio /// 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))); } /// @@ -218,13 +218,13 @@ public IEnumerator GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - internal sealed class StoreState + internal sealed class State { public List Messages { get; set; } = []; } /// - /// Defines the events that can trigger a reducer in the . + /// Defines the events that can trigger a reducer in the . /// public enum ChatReducerTriggerEvent { @@ -241,7 +241,7 @@ public enum ChatReducerTriggerEvent BeforeMessagesRetrieval } - private sealed class DebugView(InMemoryChatMessageStore store) + private sealed class DebugView(InMemoryChatHistoryProvider store) { [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public ChatMessage[] Items => store._messages.ToArray(); diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/MessageFilteringAIContextProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/MessageFilteringAIContextProvider.cs new file mode 100644 index 0000000000..f63ed9d4e3 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/MessageFilteringAIContextProvider.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// An decorator that allows filtering the data +/// passed into and out of an inner . +/// +public sealed class MessageFilteringAIContextProvider : AIContextProvider +{ + private readonly AIContextProvider _innerAIContextProvider; + private readonly Func? _invokingContextFilter; + private readonly Func? _invokedContextFilter; + + /// + /// Initializes a new instance of the class. + /// + /// Use this constructor to customize how context is filtered before and after invocation by + /// providing appropriate filter functions. If no filters are provided, the context provider operates without + /// additional filtering. + /// The underlying AI context provider to be wrapped. Cannot be null. + /// An optional filter function to apply to the AI context before it is returned. If null, no filter is applied at this + /// stage. + /// An optional filter function to apply to the invocation context before it is consumed. If null, no + /// filter is applied at this stage. + /// Thrown if innerAIContextProvider is null. + public MessageFilteringAIContextProvider( + AIContextProvider innerAIContextProvider, + Func? invokingContextFilter = null, + Func? invokedContextFilter = null) + { + this._innerAIContextProvider = Throw.IfNull(innerAIContextProvider); + + if (invokingContextFilter == null && invokedContextFilter == null) + { + throw new ArgumentException("At least one filter function, invokingContextFilter or invokedContextFilter, must be provided."); + } + + this._invokingContextFilter = invokingContextFilter; + this._invokedContextFilter = invokedContextFilter; + } + + /// + public override async ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) + { + var aiContext = await this._innerAIContextProvider.InvokingAsync(context, cancellationToken).ConfigureAwait(false); + return this._invokingContextFilter != null ? this._invokingContextFilter(aiContext) : aiContext; + } + + /// + public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) + { + if (this._invokedContextFilter != null) + { + context = this._invokedContextFilter(context); + } + + return this._innerAIContextProvider.InvokedAsync(context, cancellationToken); + } + + /// + public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) + { + return this._innerAIContextProvider.Serialize(jsonSerializerOptions); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/PersistentAgentsClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/PersistentAgentsClientExtensions.cs index 55c3c4f0bf..ea8e0e9030 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/PersistentAgentsClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/PersistentAgentsClientExtensions.cs @@ -225,7 +225,7 @@ public static ChatClientAgent AsAIAgent( Description = options.Description ?? persistentAgentMetadata.Description, ChatOptions = options.ChatOptions, AIContextProviderFactory = options.AIContextProviderFactory, - ChatMessageStoreFactory = options.ChatMessageStoreFactory, + ChatHistoryProviderFactory = options.ChatHistoryProviderFactory, UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs }; diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs index 8e03a33be3..36ca72cb01 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs @@ -852,7 +852,7 @@ private static ChatClientAgentOptions CreateChatClientAgentOptions(AgentVersion if (options is not null) { agentOptions.AIContextProviderFactory = options.AIContextProviderFactory; - agentOptions.ChatMessageStoreFactory = options.ChatMessageStoreFactory; + agentOptions.ChatHistoryProviderFactory = options.ChatHistoryProviderFactory; agentOptions.UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs; } diff --git a/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatMessageStore.cs b/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatHistoryProvider.cs similarity index 87% rename from dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatMessageStore.cs rename to dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatHistoryProvider.cs index 5c2c23ff9e..7694ec1e9e 100644 --- a/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatMessageStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatHistoryProvider.cs @@ -15,11 +15,11 @@ namespace Microsoft.Agents.AI; /// -/// Provides a Cosmos DB implementation of the abstract class. +/// Provides a Cosmos DB implementation of the abstract class for storing chat history. /// -[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 : AIContextProvider, IDisposable { private readonly CosmosClient _cosmosClient; private readonly Container _container; @@ -73,17 +73,17 @@ private static JsonSerializerOptions CreateDefaultJsonOptions() public int? MessageTtlSeconds { get; set; } = 86400; /// - /// Gets the conversation ID associated with this message store. + /// Gets the conversation ID associated with this chat history provider. /// public string ConversationId { get; init; } /// - /// Gets the database ID associated with this message store. + /// Gets the database ID associated with this chat history provider. /// public string DatabaseId { get; init; } /// - /// Gets the container ID associated with this message store. + /// Gets the container ID associated with this chat history provider. /// public string ContainerId { get; init; } @@ -97,7 +97,7 @@ private static JsonSerializerOptions CreateDefaultJsonOptions() /// Whether this instance owns the CosmosClient and should dispose it. /// Optional tenant identifier for hierarchical partitioning. /// Optional user identifier for hierarchical partitioning. - 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 @@ internal CosmosChatMessageStore(CosmosClient cosmosClient, string databaseId, st } /// - /// Initializes a new instance of the class using a connection string. + /// Initializes a new instance of the class using a connection string. /// /// The Cosmos DB connection string. /// The identifier of the Cosmos DB database. /// The identifier of the Cosmos DB container. /// Thrown when any required parameter is null. /// Thrown when any string parameter is null or whitespace. - public CosmosChatMessageStore(string connectionString, string databaseId, string containerId) + public CosmosChatHistoryProvider(string connectionString, string databaseId, string containerId) : this(connectionString, databaseId, containerId, Guid.NewGuid().ToString("N")) { } /// - /// Initializes a new instance of the class using a connection string. + /// Initializes a new instance of the class using a connection string. /// /// The Cosmos DB connection string. /// The identifier of the Cosmos DB database. @@ -142,13 +142,13 @@ public CosmosChatMessageStore(string connectionString, string databaseId, string /// The unique identifier for this conversation thread. /// Thrown when any required parameter is null. /// Thrown when any string parameter is null or whitespace. - 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) { } /// - /// Initializes a new instance of the class using TokenCredential for authentication. + /// Initializes a new instance of the class using TokenCredential for authentication. /// /// The Cosmos DB account endpoint URI. /// The TokenCredential to use for authentication (e.g., DefaultAzureCredential, ManagedIdentityCredential). @@ -156,13 +156,13 @@ public CosmosChatMessageStore(string connectionString, string databaseId, string /// The identifier of the Cosmos DB container. /// Thrown when any required parameter is null. /// Thrown when any string parameter is null or whitespace. - 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")) { } /// - /// Initializes a new instance of the class using a TokenCredential for authentication. + /// Initializes a new instance of the class using a TokenCredential for authentication. /// /// The Cosmos DB account endpoint URI. /// The TokenCredential to use for authentication (e.g., DefaultAzureCredential, ManagedIdentityCredential). @@ -171,26 +171,26 @@ public CosmosChatMessageStore(string accountEndpoint, TokenCredential tokenCrede /// The unique identifier for this conversation thread. /// Thrown when any required parameter is null. /// Thrown when any string parameter is null or whitespace. - 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) { } /// - /// Initializes a new instance of the class using an existing . + /// Initializes a new instance of the class using an existing . /// /// The instance to use for Cosmos DB operations. /// The identifier of the Cosmos DB database. /// The identifier of the Cosmos DB container. /// Thrown when is null. /// Thrown when any string parameter is null or whitespace. - public CosmosChatMessageStore(CosmosClient cosmosClient, string databaseId, string containerId) + public CosmosChatHistoryProvider(CosmosClient cosmosClient, string databaseId, string containerId) : this(cosmosClient, databaseId, containerId, Guid.NewGuid().ToString("N")) { } /// - /// Initializes a new instance of the class using an existing . + /// Initializes a new instance of the class using an existing . /// /// The instance to use for Cosmos DB operations. /// The identifier of the Cosmos DB database. @@ -198,13 +198,13 @@ public CosmosChatMessageStore(CosmosClient cosmosClient, string databaseId, stri /// The unique identifier for this conversation thread. /// Thrown when is null. /// Thrown when any string parameter is null or whitespace. - 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) { } /// - /// Initializes a new instance of the class using a connection string with hierarchical partition keys. + /// Initializes a new instance of the class using a connection string with hierarchical partition keys. /// /// The Cosmos DB connection string. /// The identifier of the Cosmos DB database. @@ -214,13 +214,13 @@ public CosmosChatMessageStore(CosmosClient cosmosClient, string databaseId, stri /// The session identifier for hierarchical partitioning. /// Thrown when any required parameter is null. /// Thrown when any string parameter is null or whitespace. - 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)) { } /// - /// Initializes a new instance of the class using a TokenCredential for authentication with hierarchical partition keys. + /// Initializes a new instance of the class using a TokenCredential for authentication with hierarchical partition keys. /// /// The Cosmos DB account endpoint URI. /// The TokenCredential to use for authentication (e.g., DefaultAzureCredential, ManagedIdentityCredential). @@ -231,13 +231,13 @@ public CosmosChatMessageStore(string connectionString, string databaseId, string /// The session identifier for hierarchical partitioning. /// Thrown when any required parameter is null. /// Thrown when any string parameter is null or whitespace. - 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)) { } /// - /// Initializes a new instance of the class using an existing with hierarchical partition keys. + /// Initializes a new instance of the class using an existing with hierarchical partition keys. /// /// The instance to use for Cosmos DB operations. /// The identifier of the Cosmos DB database. @@ -247,23 +247,23 @@ public CosmosChatMessageStore(string accountEndpoint, TokenCredential tokenCrede /// The session identifier for hierarchical partitioning. /// Thrown when is null. /// Thrown when any string parameter is null or whitespace. - 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)) { } /// - /// Creates a new instance of the class from previously serialized state. + /// Creates a new instance of the class from previously serialized state. /// /// The instance to use for Cosmos DB operations. - /// A representing the serialized state of the message store. + /// A representing the serialized state of the chat history provider. /// The identifier of the Cosmos DB database. /// The identifier of the Cosmos DB container. /// Optional settings for customizing the JSON deserialization process. - /// A new instance of initialized from the serialized state. + /// A new instance of initialized from the serialized state. /// Thrown when is null. /// Thrown when the serialized state cannot be deserialized. - public static CosmosChatMessageStore CreateFromSerializedState(CosmosClient cosmosClient, JsonElement serializedStoreState, string databaseId, string containerId, JsonSerializerOptions? jsonSerializerOptions = null) + public static CosmosChatHistoryProvider CreateFromSerializedState(CosmosClient cosmosClient, JsonElement serializedStoreState, string databaseId, string containerId, JsonSerializerOptions? jsonSerializerOptions = null) { Throw.IfNull(cosmosClient); Throw.IfNullOrWhitespace(databaseId); @@ -282,12 +282,12 @@ public static CosmosChatMessageStore CreateFromSerializedState(CosmosClient cosm // 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); } /// - public override async ValueTask> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) + public override async ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) { #pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks if (this._disposed) @@ -343,7 +343,7 @@ public override async ValueTask> InvokingAsync(Invoking messages.Reverse(); } - return messages; + return new() { Messages = messages }; } /// diff --git a/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosDBChatExtensions.cs b/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosDBChatExtensions.cs index 45c0d09536..236fcf4782 100644 --- a/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosDBChatExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosDBChatExtensions.cs @@ -23,9 +23,9 @@ public static class CosmosDBChatExtensions /// The configured . /// Thrown when is null. /// Thrown when any string parameter is null or whitespace. - [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 ChatClientAgentOptions WithCosmosDBMessageStore( throw new ArgumentNullException(nameof(options)); } - options.ChatMessageStoreFactory = (context, ct) => new ValueTask(new CosmosChatMessageStore(connectionString, databaseId, containerId)); + options.ChatHistoryProviderFactory = (context, ct) => new ValueTask(new CosmosChatHistoryProvider(connectionString, databaseId, containerId)); return options; } @@ -50,9 +50,9 @@ public static ChatClientAgentOptions WithCosmosDBMessageStore( /// The configured . /// Thrown when is null. /// Thrown when any string parameter is null or whitespace. - [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, @@ -63,7 +63,7 @@ public static ChatClientAgentOptions WithCosmosDBMessageStoreUsingManagedIdentit throw new ArgumentNullException(nameof(options)); } - options.ChatMessageStoreFactory = (context, ct) => new ValueTask(new CosmosChatMessageStore(accountEndpoint, new DefaultAzureCredential(), databaseId, containerId)); + options.ChatHistoryProviderFactory = (context, ct) => new ValueTask(new CosmosChatHistoryProvider(accountEndpoint, new DefaultAzureCredential(), databaseId, containerId)); return options; } @@ -77,9 +77,9 @@ public static ChatClientAgentOptions WithCosmosDBMessageStoreUsingManagedIdentit /// The configured . /// Thrown when any required parameter is null. /// Thrown when any string parameter is null or whitespace. - [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, @@ -90,7 +90,7 @@ public static ChatClientAgentOptions WithCosmosDBMessageStore( throw new ArgumentNullException(nameof(options)); } - options.ChatMessageStoreFactory = (context, ct) => new ValueTask(new CosmosChatMessageStore(cosmosClient, databaseId, containerId)); + options.ChatHistoryProviderFactory = (context, ct) => new ValueTask(new CosmosChatHistoryProvider(cosmosClient, databaseId, containerId)); return options; } } diff --git a/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj b/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj index 7e13ec5998..6cc828ff5c 100644 --- a/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj +++ b/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj @@ -21,7 +21,7 @@ Microsoft Agent Framework Cosmos DB NoSQL Integration - Provides Cosmos DB NoSQL implementations for Microsoft Agent Framework storage abstractions including ChatMessageStore and CheckpointStore. + Provides Cosmos DB NoSQL implementations for Microsoft Agent Framework storage abstractions including a ChatHistoryProvider and CheckpointStore. diff --git a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs index 291ff56091..88f20a146b 100644 --- a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs @@ -238,7 +238,7 @@ public static ChatClientAgent AsAIAgent( Description = options.Description ?? assistantMetadata.Description, ChatOptions = options.ChatOptions, AIContextProviderFactory = options.AIContextProviderFactory, - ChatMessageStoreFactory = options.ChatMessageStoreFactory, + ChatHistoryProviderFactory = options.ChatHistoryProviderFactory, UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs }; diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowMessageStore.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowMessageStore.cs index 87cef04e76..77228ddb8c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowMessageStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowMessageStore.cs @@ -10,7 +10,7 @@ namespace Microsoft.Agents.AI.Workflows; -internal sealed class WorkflowMessageStore : ChatMessageStore +internal sealed class WorkflowMessageStore : AIContextProvider { private int _bookmark; private readonly List _chatMessages = []; @@ -46,8 +46,8 @@ internal sealed class StoreState internal void AddMessages(params IEnumerable messages) => this._chatMessages.AddRange(messages); - public override ValueTask> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) - => new(this._chatMessages.AsReadOnly()); + public override ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) + => new(new AIContext { Messages = this._chatMessages.AsReadOnly() }); public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index 4a42241b3c..cf115a520d 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs @@ -78,7 +78,7 @@ public ChatClientAgent(IChatClient chatClient, string? instructions = null, stri /// The chat client to use when running the agent. /// /// 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. /// /// /// Optional logger factory for creating loggers used by the agent and its components. @@ -208,7 +208,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA ChatOptions? chatOptions, List inputMessagesForChatClient, IList? aiContextProviderMessages, - IList? chatMessageStoreMessages, + IList? chatHistoryProviderMessages, ChatClientAgentContinuationToken? continuationToken) = await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false); @@ -231,8 +231,8 @@ protected override async IAsyncEnumerable RunCoreStreamingA } catch (Exception ex) { - await NotifyMessageStoreOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatMessageStoreMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); - await NotifyAIContextProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false); + await NotifyChatHistoryProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); + await NotifyAIContextProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); throw; } @@ -246,8 +246,8 @@ protected override async IAsyncEnumerable RunCoreStreamingA } catch (Exception ex) { - await NotifyMessageStoreOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatMessageStoreMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); - await NotifyAIContextProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false); + await NotifyChatHistoryProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); + await NotifyAIContextProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); throw; } @@ -273,8 +273,8 @@ protected override async IAsyncEnumerable RunCoreStreamingA } catch (Exception ex) { - await NotifyMessageStoreOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatMessageStoreMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); - await NotifyAIContextProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false); + await NotifyChatHistoryProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); + await NotifyAIContextProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false); throw; } } @@ -286,10 +286,10 @@ protected override async IAsyncEnumerable RunCoreStreamingA 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, cancellationToken).ConfigureAwait(false); + await NotifyChatHistoryProviderOfNewMessagesAsync(safeThread, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false); // Notify the AIContextProvider of all new messages. - await NotifyAIContextProviderOfSuccessAsync(safeThread, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false); + await NotifyAIContextProviderOfSuccessAsync(safeThread, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false); } /// @@ -304,8 +304,8 @@ protected override async IAsyncEnumerable RunCoreStreamingA /// public override async ValueTask 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) + AIContextProvider? historyProvider = 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 override async ValueTask GetNewThreadAsync(CancellationToken return new ChatClientAgentThread { - MessageStore = messageStore, + ChatHistoryProvider = historyProvider, AIContextProvider = contextProvider }; } @@ -329,12 +329,12 @@ public override async ValueTask GetNewThreadAsync(CancellationToken /// /// /// - /// 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 threads that rely on server-side chat history storage, where the chat history + /// is maintained by the underlying AI service rather than a local chat history provider. /// /// - /// Agent threads created with this method will only work with - /// instances that support server-side conversation storage through their underlying . + /// Agent threads created with this method will only work with a + /// instance that supports server-side chat history storage through its underlying . /// /// public async ValueTask GetNewThreadAsync(string conversationId, CancellationToken cancellationToken = default) @@ -351,28 +351,28 @@ public async ValueTask GetNewThreadAsync(string conversationId, Can } /// - /// Creates a new agent thread instance using an existing to continue a conversation. + /// Creates a new agent thread instance using an existing chat history provider to continue a conversation. /// - /// The instance to use for managing the conversation's message history. + /// The instance to use for managing the conversation's chat history. /// The to monitor for cancellation requests. /// - /// A value task representing the asynchronous operation. The task result contains a new instance configured to work with the provided . + /// A value task representing the asynchronous operation. The task result contains a new instance configured to work with the provided . /// /// /// - /// This method creates threads that do not support server-side conversation storage. + /// This method creates threads that do not support server-side chat history storage. /// Some AI services require server-side conversation storage to function properly, and creating a thread - /// with a may not be compatible with these services. + /// with a chat history provider may not be compatible with these services. /// /// /// Where a service requires server-side conversation storage, use . /// /// /// 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 . + /// the thread will throw an exception to indicate that it cannot continue using the provided . /// /// - public async ValueTask GetNewThreadAsync(ChatMessageStore chatMessageStore, CancellationToken cancellationToken = default) + public async ValueTask GetNewThreadAsync(AIContextProvider 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 async ValueTask GetNewThreadAsync(ChatMessageStore chatMessa return new ChatClientAgentThread() { - MessageStore = Throw.IfNull(chatMessageStore), + ChatHistoryProvider = Throw.IfNull(chatHistoryProvider), AIContextProvider = contextProvider }; } @@ -388,9 +388,9 @@ public async ValueTask GetNewThreadAsync(ChatMessageStore chatMessa /// public override async ValueTask DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) { - Func>? chatMessageStoreFactory = this._agentOptions?.ChatMessageStoreFactory is null ? + Func>? 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>? aiContextProviderFactory = this._agentOptions?.AIContextProviderFactory is null ? null : @@ -399,7 +399,7 @@ public override async ValueTask DeserializeThreadAsync(JsonElement return await ChatClientAgentThread.DeserializeAsync( serializedThread, jsonSerializerOptions, - chatMessageStoreFactory, + chatHistoryProviderFactory, aiContextProviderFactory, cancellationToken).ConfigureAwait(false); } @@ -422,7 +422,7 @@ private async Task RunCoreAsync inputMessagesForChatClient, IList? aiContextProviderMessages, - IList? chatMessageStoreMessages, + IList? chatHistoryProviderMessages, ChatClientAgentContinuationToken? _) = await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false); @@ -442,8 +442,8 @@ private async Task RunCoreAsync RunCoreAsync RunCoreAsync inputMessages, + IEnumerable? chatHistoryProviderMessages, IList? aiContextProviderMessages, IEnumerable responseMessages, CancellationToken cancellationToken) { if (thread.AIContextProvider is not null) { - await thread.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProviderMessages) { ResponseMessages = responseMessages }, + await thread.AIContextProvider.InvokedAsync( + new(inputMessages, aiContextProviderMessages) + { + ChatHistoryMessages = chatHistoryProviderMessages, + ResponseMessages = responseMessages + }, cancellationToken).ConfigureAwait(false); } } @@ -496,12 +502,18 @@ private static async Task NotifyAIContextProviderOfFailureAsync( ChatClientAgentThread thread, Exception ex, IEnumerable inputMessages, + IEnumerable? chatHistoryProviderMessages, IList? aiContextProviderMessages, CancellationToken cancellationToken) { if (thread.AIContextProvider is not null) { - await thread.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProviderMessages) { InvokeException = ex }, + await thread.AIContextProvider.InvokedAsync( + new(inputMessages, aiContextProviderMessages) + { + ChatHistoryMessages = chatHistoryProviderMessages, + InvokeException = ex + }, cancellationToken).ConfigureAwait(false); } } @@ -672,7 +684,7 @@ private async Task ChatOptions? ChatOptions, List InputMessagesForChatClient, IList? AIContextProviderMessages, - IList? ChatMessageStoreMessages, + IList? chatHistoryProviderMessages, ChatClientAgentContinuationToken? ContinuationToken )> PrepareThreadAndMessagesAsync( AgentThread? thread, @@ -703,18 +715,38 @@ private async Task List inputMessagesForChatClient = []; IList? aiContextProviderMessages = null; - IList? chatMessageStoreMessages = []; + IList? 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) { // Add any existing messages from the thread to the messages to be sent to the chat client. - if (typedThread.MessageStore is not null) + if (typedThread.ChatHistoryProvider is not null) { - var invokingContext = new ChatMessageStore.InvokingContext(inputMessages); - var storeMessages = await typedThread.MessageStore.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false); - inputMessagesForChatClient.AddRange(storeMessages); - chatMessageStoreMessages = storeMessages as IList ?? storeMessages.ToList(); + var invokingContext = new AIContextProvider.InvokingContext(inputMessages); + var aiContext = await typedThread.ChatHistoryProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false); + + if (aiContext.Messages is { Count: > 0 }) + { + inputMessagesForChatClient.AddRange(aiContext.Messages); + chatHistoryProviderMessages = aiContext.Messages; + } + + if (aiContext.Tools is { Count: > 0 }) + { + chatOptions ??= new(); + chatOptions.Tools ??= []; + foreach (AITool tool in aiContext.Tools) + { + chatOptions.Tools.Add(tool); + } + } + + if (aiContext.Instructions is not null) + { + chatOptions ??= new(); + chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? aiContext.Instructions : $"{chatOptions.Instructions}\n{aiContext.Instructions}"; + } } // Add the input messages before getting context from AIContextProvider. @@ -768,7 +800,7 @@ private async Task 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) @@ -789,60 +821,60 @@ private async Task UpdateThreadWithTypeAndConversationIdAsync(ChatClientAgentThr 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 requestMessages, - IEnumerable? chatMessageStoreMessages, + IEnumerable? chatHistoryProviderMessages, IEnumerable? aiContextProviderMessages, CancellationToken cancellationToken) { - var messageStore = thread.MessageStore; + var historyProvider = thread.ChatHistoryProvider; - // Only notify the message store if we have one. + // Only notify the chat history 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 (messageStore is not null) + if (historyProvider is not null) { - var invokedContext = new ChatMessageStore.InvokedContext(requestMessages, chatMessageStoreMessages!) + var invokedContext = new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages!) { - AIContextProviderMessages = aiContextProviderMessages, + ChatHistoryMessages = chatHistoryProviderMessages, InvokeException = ex }; - return messageStore.InvokedAsync(invokedContext, cancellationToken).AsTask(); + return historyProvider.InvokedAsync(invokedContext, cancellationToken).AsTask(); } return Task.CompletedTask; } - private static Task NotifyMessageStoreOfNewMessagesAsync( + private static Task NotifyChatHistoryProviderOfNewMessagesAsync( ChatClientAgentThread thread, IEnumerable requestMessages, - IEnumerable? chatMessageStoreMessages, + IEnumerable? chatHistoryProviderMessages, IEnumerable? aiContextProviderMessages, IEnumerable responseMessages, CancellationToken cancellationToken) { - var messageStore = thread.MessageStore; + var historyProvider = thread.ChatHistoryProvider; - // Only notify the message store if we have one. + // Only notify the chat history 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 (messageStore is not null) + if (historyProvider is not null) { - var invokedContext = new ChatMessageStore.InvokedContext(requestMessages, chatMessageStoreMessages!) + var invokedContext = new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages) { - AIContextProviderMessages = aiContextProviderMessages, + ChatHistoryMessages = chatHistoryProviderMessages, ResponseMessages = responseMessages }; - return messageStore.InvokedAsync(invokedContext, cancellationToken).AsTask(); + return historyProvider.InvokedAsync(invokedContext, cancellationToken).AsTask(); } return Task.CompletedTask; @@ -863,7 +895,7 @@ private static Task NotifyMessageStoreOfNewMessagesAsync( InputMessages = inputMessages?.Any() is true ? inputMessages : null, // Save all updates received so far to the continuation token so they can be provided to the - // message store and context provider in the last successful streaming resumption run. + // chat history provider and context provider in the last successful streaming resumption run. // That's necessary for scenarios where a streaming run is interrupted after some updates were received. ResponseUpdates = responseUpdates?.Count > 0 ? responseUpdates : null }; diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs index 719e863f0c..258a11edb4 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs @@ -39,10 +39,11 @@ public sealed class ChatClientAgentOptions public ChatOptions? ChatOptions { get; set; } /// - /// Gets or sets a factory function to create an instance of - /// which will be used to store chat messages for this agent. + /// Gets or sets a factory function to create a chat history provider + /// which will be used to provide chat history for this agent, if the underlying AI service + /// that the agent uses does not require chat history to be stored in the service. /// - public Func>? ChatMessageStoreFactory { get; set; } + public Func>? ChatHistoryProviderFactory { get; set; } /// /// Gets or sets a factory function to create an instance of @@ -75,7 +76,7 @@ public ChatClientAgentOptions Clone() Name = this.Name, Description = this.Description, ChatOptions = this.ChatOptions?.Clone(), - ChatMessageStoreFactory = this.ChatMessageStoreFactory, + ChatHistoryProviderFactory = this.ChatHistoryProviderFactory, AIContextProviderFactory = this.AIContextProviderFactory, }; @@ -97,14 +98,14 @@ public sealed class AIContextProviderFactoryContext } /// - /// Context object passed to the to create a new instance of . + /// Context object passed to the to create a new instance of a chat history . /// - public sealed class ChatMessageStoreFactoryContext + public sealed class ChatHistoryProviderFactoryContext { /// - /// Gets or sets the serialized state of the chat message store, if any. + /// Gets or sets the serialized state of the chat history store, if any. /// - /// if there is no state, e.g. when the is first created. + /// if there is no state, e.g. when the is first created. public JsonElement SerializedState { get; set; } /// diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs index 06326d1ed2..16f66c9df4 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs @@ -15,7 +15,7 @@ namespace Microsoft.Agents.AI; [DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ChatClientAgentThread : AgentThread { - private ChatMessageStore? _messageStore; + private AIContextProvider? _chatHistoryProvider; /// /// Initializes a new instance of the class. @@ -25,28 +25,27 @@ internal ChatClientAgentThread() } /// - /// Gets or sets the ID of the underlying service thread to support cases where the chat history is stored by the agent service. + /// Gets or sets the ID of the underlying service thread to support cases where the chat history is stored by the underlying AI service that the agent uses. /// /// /// - /// Note that either or may be set, but not both. - /// If is not null, setting will throw an + /// Note that either or may be set, but not both. + /// If is not null, setting will throw an /// exception. /// /// /// This property may be null in the following cases: /// - /// The thread stores messages via the and not in the agent service. - /// This thread object is new and a server managed thread has not yet been created in the agent service. + /// The thread stores messages via the and not in the underlying AI service. + /// This object is new and server managed chat history has not yet been created in the underlying AI service. /// /// /// - /// The id may also change over time where the id is pointing at a - /// agent service managed thread, and the default behavior of a service is + /// The id may also change over time if the id is pointing at AI service managed chat history, and the default behavior of a service is /// to fork the thread with each iteration. /// /// - /// Attempted to set a conversation ID but a is already set. + /// Attempted to set a conversation ID but a is already set. public string? ConversationId { get; @@ -57,12 +56,12 @@ internal set 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 chat history provider 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 +69,40 @@ internal set } /// - /// Gets or sets the used by this thread, for cases where messages should be stored in a custom location. + /// Gets or sets the chat history provider used by this thread, for cases where messages are not stored in the underlying AI service that the agent uses. /// /// /// - /// Note that either or may be set, but not both. - /// If is not null, and is set, + /// Note that either or may be set, but not both. + /// If is not null, and is set, /// will be reverted to null, and vice versa. /// /// /// This property may be null in the following cases: /// - /// The thread stores messages in the agent service and just has an id to the remove thread, instead of in an . - /// This thread object is new it is not yet clear whether it will be backed by a server managed thread or an . + /// Chat history is stored in the underlying AI service and just has an id to the remote chat history. + /// This object is new it is not yet clear whether it will be backed by AI service managed chat history or a . /// /// /// - public ChatMessageStore? MessageStore + public AIContextProvider? 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 chat history provider // 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 +116,9 @@ internal set /// /// A representing the serialized state of the thread. /// Optional settings for customizing the JSON deserialization process. - /// - /// An optional factory function to create a custom from its serialized state. - /// If not provided, the default in-memory message store will be used. + /// + /// An optional factory function to create a custom chat history provider from its serialized state. + /// If not provided, the default in-memory chat history provider will be used. /// /// /// An optional factory function to create a custom from its serialized state. @@ -130,7 +129,7 @@ internal set internal static async Task DeserializeAsync( JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null, - Func>? chatMessageStoreFactory = null, + Func>? chatHistoryProviderFactory = null, Func>? aiContextProviderFactory = null, CancellationToken cancellationToken = default) { @@ -152,14 +151,14 @@ internal static async Task DeserializeAsync( { 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 chat history provider 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 store return thread; } @@ -167,14 +166,14 @@ chatMessageStoreFactory is not null /// 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 +184,20 @@ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptio 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 inMemoryStore ? $"Count = {inMemoryStore.Count}" : + this._chatHistoryProvider is { } store ? $"Store = {store.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; } } diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs index ee782dce52..a65db60761 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs @@ -63,7 +63,7 @@ public static ChatClientAgent BuildAIAgent( /// A builder for creating pipelines of . /// /// 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. /// /// /// Optional logger factory for creating loggers used by the agent and its components. diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs index 2bec0b366e..d71386197b 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs @@ -39,12 +39,12 @@ public async Task> GetChatHistoryAsync(AgentThread thread) { var typedThread = (ChatClientAgentThread)thread; - if (typedThread.MessageStore is null) + if (typedThread.ChatHistoryProvider is null) { return []; } - return (await typedThread.MessageStore.InvokingAsync(new([]))).ToList(); + return (await typedThread.ChatHistoryProvider.InvokingAsync(new([]))).Messages!.ToList(); } public Task CreateChatClientAgentAsync( diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs index ddb015eb17..a5d7bf385e 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs +++ b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs @@ -48,12 +48,12 @@ public async Task> GetChatHistoryAsync(AgentThread thread) return await this.GetChatHistoryFromResponsesChainAsync(chatClientThread.ConversationId); } - if (chatClientThread.MessageStore is null) + if (chatClientThread.ChatHistoryProvider is null) { return []; } - return (await chatClientThread.MessageStore.InvokingAsync(new([]))).ToList(); + return (await chatClientThread.ChatHistoryProvider.InvokingAsync(new([]))).Messages!.ToList(); } private async Task> GetChatHistoryFromResponsesChainAsync(string conversationId) diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/ChatMessageStoreMessageFilterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/ChatMessageStoreMessageFilterTests.cs deleted file mode 100644 index ab10c377ae..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/ChatMessageStoreMessageFilterTests.cs +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; -using Moq; - -namespace Microsoft.Agents.AI.Abstractions.UnitTests; - -/// -/// Contains tests for the class. -/// -public sealed class ChatMessageStoreMessageFilterTests -{ - [Fact] - public void Constructor_WithNullInnerStore_ThrowsArgumentNullException() - { - // Arrange, Act & Assert - Assert.Throws(() => new ChatMessageStoreMessageFilter(null!)); - } - - [Fact] - public void Constructor_WithOnlyInnerStore_Throws() - { - // Arrange - var innerStoreMock = new Mock(); - - // Act & Assert - Assert.Throws(() => new ChatMessageStoreMessageFilter(innerStoreMock.Object)); - } - - [Fact] - public void Constructor_WithAllParameters_CreatesInstance() - { - // Arrange - var innerStoreMock = new Mock(); - - IEnumerable InvokingFilter(IEnumerable msgs) => msgs; - ChatMessageStore.InvokedContext InvokedFilter(ChatMessageStore.InvokedContext ctx) => ctx; - - // Act - var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, InvokingFilter, InvokedFilter); - - // Assert - Assert.NotNull(filter); - } - - [Fact] - public async Task InvokingAsync_WithNoOpFilters_ReturnsInnerStoreMessagesAsync() - { - // Arrange - var innerStoreMock = new Mock(); - var expectedMessages = new List - { - new(ChatRole.User, "Hello"), - new(ChatRole.Assistant, "Hi there!") - }; - var context = new ChatMessageStore.InvokingContext([new ChatMessage(ChatRole.User, "Test")]); - - innerStoreMock - .Setup(s => s.InvokingAsync(context, It.IsAny())) - .ReturnsAsync(expectedMessages); - - var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, x => x, x => x); - - // Act - var result = (await filter.InvokingAsync(context, CancellationToken.None)).ToList(); - - // Assert - Assert.Equal(2, result.Count); - Assert.Equal("Hello", result[0].Text); - Assert.Equal("Hi there!", result[1].Text); - innerStoreMock.Verify(s => s.InvokingAsync(context, It.IsAny()), Times.Once); - } - - [Fact] - public async Task InvokingAsync_WithInvokingFilter_AppliesFilterAsync() - { - // Arrange - var innerStoreMock = new Mock(); - var innerMessages = new List - { - new(ChatRole.User, "Hello"), - new(ChatRole.Assistant, "Hi there!"), - new(ChatRole.User, "How are you?") - }; - var context = new ChatMessageStore.InvokingContext([new ChatMessage(ChatRole.User, "Test")]); - - innerStoreMock - .Setup(s => s.InvokingAsync(context, It.IsAny())) - .ReturnsAsync(innerMessages); - - // Filter to only user messages - IEnumerable InvokingFilter(IEnumerable msgs) => msgs.Where(m => m.Role == ChatRole.User); - - var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, InvokingFilter); - - // Act - var result = (await filter.InvokingAsync(context, CancellationToken.None)).ToList(); - - // Assert - Assert.Equal(2, result.Count); - Assert.All(result, msg => Assert.Equal(ChatRole.User, msg.Role)); - innerStoreMock.Verify(s => s.InvokingAsync(context, It.IsAny()), Times.Once); - } - - [Fact] - public async Task InvokingAsync_WithInvokingFilter_CanModifyMessagesAsync() - { - // Arrange - var innerStoreMock = new Mock(); - var innerMessages = new List - { - new(ChatRole.User, "Hello"), - new(ChatRole.Assistant, "Hi there!") - }; - var context = new ChatMessageStore.InvokingContext([new ChatMessage(ChatRole.User, "Test")]); - - innerStoreMock - .Setup(s => s.InvokingAsync(context, It.IsAny())) - .ReturnsAsync(innerMessages); - - // Filter that transforms messages - IEnumerable InvokingFilter(IEnumerable msgs) => - msgs.Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}")); - - var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, InvokingFilter); - - // Act - var result = (await filter.InvokingAsync(context, CancellationToken.None)).ToList(); - - // Assert - Assert.Equal(2, result.Count); - Assert.Equal("[FILTERED] Hello", result[0].Text); - Assert.Equal("[FILTERED] Hi there!", result[1].Text); - } - - [Fact] - public async Task InvokedAsync_WithInvokedFilter_AppliesFilterAsync() - { - // Arrange - var innerStoreMock = new Mock(); - var requestMessages = new List { new(ChatRole.User, "Hello") }; - var chatMessageStoreMessages = new List { new(ChatRole.System, "System") }; - var responseMessages = new List { new(ChatRole.Assistant, "Response") }; - var context = new ChatMessageStore.InvokedContext(requestMessages, chatMessageStoreMessages) - { - ResponseMessages = responseMessages - }; - - ChatMessageStore.InvokedContext? capturedContext = null; - innerStoreMock - .Setup(s => s.InvokedAsync(It.IsAny(), It.IsAny())) - .Callback((ctx, ct) => capturedContext = ctx) - .Returns(default(ValueTask)); - - // Filter that modifies the context - ChatMessageStore.InvokedContext InvokedFilter(ChatMessageStore.InvokedContext ctx) - { - var modifiedRequestMessages = ctx.RequestMessages.Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}")).ToList(); - return new ChatMessageStore.InvokedContext(modifiedRequestMessages, ctx.ChatMessageStoreMessages) - { - ResponseMessages = ctx.ResponseMessages, - AIContextProviderMessages = ctx.AIContextProviderMessages, - InvokeException = ctx.InvokeException - }; - } - - var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, invokedMessagesFilter: InvokedFilter); - - // Act - await filter.InvokedAsync(context, CancellationToken.None); - - // Assert - Assert.NotNull(capturedContext); - Assert.Single(capturedContext.RequestMessages); - Assert.Equal("[FILTERED] Hello", capturedContext.RequestMessages.First().Text); - innerStoreMock.Verify(s => s.InvokedAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public void Serialize_DelegatesToInnerStore() - { - // Arrange - var innerStoreMock = new Mock(); - var expectedJson = JsonSerializer.SerializeToElement("data", TestJsonSerializerContext.Default.String); - - innerStoreMock - .Setup(s => s.Serialize(It.IsAny())) - .Returns(expectedJson); - - var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, x => x, x => x); - - // Act - var result = filter.Serialize(); - - // Assert - Assert.Equal(expectedJson.GetRawText(), result.GetRawText()); - innerStoreMock.Verify(s => s.Serialize(null), Times.Once); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/ChatMessageStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/ChatMessageStoreTests.cs deleted file mode 100644 index 883941458c..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/ChatMessageStoreTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.AI.Abstractions.UnitTests; - -/// -/// Contains tests for the class. -/// -public class ChatMessageStoreTests -{ - #region GetService Method Tests - - [Fact] - public void GetService_RequestingExactStoreType_ReturnsStore() - { - var store = new TestChatMessageStore(); - var result = store.GetService(typeof(TestChatMessageStore)); - Assert.NotNull(result); - Assert.Same(store, result); - } - - [Fact] - public void GetService_RequestingBaseStoreType_ReturnsStore() - { - var store = new TestChatMessageStore(); - var result = store.GetService(typeof(ChatMessageStore)); - Assert.NotNull(result); - Assert.Same(store, result); - } - - [Fact] - public void GetService_RequestingUnrelatedType_ReturnsNull() - { - var store = new TestChatMessageStore(); - var result = store.GetService(typeof(string)); - Assert.Null(result); - } - - [Fact] - public void GetService_WithServiceKey_ReturnsNull() - { - var store = new TestChatMessageStore(); - var result = store.GetService(typeof(TestChatMessageStore), "some-key"); - Assert.Null(result); - } - - [Fact] - public void GetService_WithNullServiceType_ThrowsArgumentNullException() - { - var store = new TestChatMessageStore(); - Assert.Throws(() => store.GetService(null!)); - } - - [Fact] - public void GetService_Generic_ReturnsCorrectType() - { - var store = new TestChatMessageStore(); - var result = store.GetService(); - Assert.NotNull(result); - Assert.Same(store, result); - } - - [Fact] - public void GetService_Generic_ReturnsNullForUnrelatedType() - { - var store = new TestChatMessageStore(); - var result = store.GetService(); - Assert.Null(result); - } - - #endregion - - private sealed class TestChatMessageStore : ChatMessageStore - { - public override ValueTask> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) - => new(Array.Empty()); - - public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) - => default; - - public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) - => default; - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryAgentThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryAgentThreadTests.cs index 906db4d30c..ca851c612c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryAgentThreadTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryAgentThreadTests.cs @@ -16,29 +16,29 @@ public class InMemoryAgentThreadTests #region Constructor and Property Tests [Fact] - public void Constructor_SetsDefaultMessageStore() + public void Constructor_SetsDefaultChatHistoryProvider() { // Arrange & Act var thread = new TestInMemoryAgentThread(); // Assert - Assert.NotNull(thread.GetMessageStore()); - Assert.Empty(thread.GetMessageStore()); + Assert.NotNull(thread.GetChatHistoryProvider()); + Assert.Empty(thread.GetChatHistoryProvider()); } [Fact] - public void Constructor_WithMessageStore_SetsProperty() + public void Constructor_WithChatHistoryProvider_SetsProperty() { // Arrange - InMemoryChatMessageStore store = [new(ChatRole.User, "Hello")]; + InMemoryChatHistoryProvider store = [new(ChatRole.User, "Hello")]; // Act var thread = new TestInMemoryAgentThread(store); // Assert - Assert.Same(store, thread.GetMessageStore()); - Assert.Single(thread.GetMessageStore()); - Assert.Equal("Hello", thread.GetMessageStore()[0].Text); + Assert.Same(store, thread.GetChatHistoryProvider()); + Assert.Single(thread.GetChatHistoryProvider()); + Assert.Equal("Hello", thread.GetChatHistoryProvider()[0].Text); } [Fact] @@ -51,16 +51,16 @@ public void Constructor_WithMessages_SetsProperty() var thread = new TestInMemoryAgentThread(messages); // Assert - Assert.NotNull(thread.GetMessageStore()); - Assert.Single(thread.GetMessageStore()); - Assert.Equal("Hi", thread.GetMessageStore()[0].Text); + Assert.NotNull(thread.GetChatHistoryProvider()); + Assert.Single(thread.GetChatHistoryProvider()); + Assert.Equal("Hi", thread.GetChatHistoryProvider()[0].Text); } [Fact] public void Constructor_WithSerializedState_SetsProperty() { // Arrange - InMemoryChatMessageStore store = [new(ChatRole.User, "TestMsg")]; + InMemoryChatHistoryProvider store = [new(ChatRole.User, "TestMsg")]; var storeState = store.Serialize(); var threadStateWrapper = new InMemoryAgentThread.InMemoryAgentThreadState { StoreState = storeState }; var json = JsonSerializer.SerializeToElement(threadStateWrapper, TestJsonSerializerContext.Default.InMemoryAgentThreadState); @@ -69,9 +69,9 @@ public void Constructor_WithSerializedState_SetsProperty() var thread = new TestInMemoryAgentThread(json); // Assert - Assert.NotNull(thread.GetMessageStore()); - Assert.Single(thread.GetMessageStore()); - Assert.Equal("TestMsg", thread.GetMessageStore()[0].Text); + Assert.NotNull(thread.GetChatHistoryProvider()); + Assert.Single(thread.GetChatHistoryProvider()); + Assert.Equal("TestMsg", thread.GetChatHistoryProvider()[0].Text); } [Fact] @@ -130,15 +130,14 @@ public void Serialize_ReturnsEmptyMessages_WhenNoMessages() #region GetService Tests [Fact] - public void GetService_RequestingChatMessageStore_ReturnsChatMessageStore() + public void GetService_RequestingInMemoryChatHistoryProvider_ReturnsProvider() { // Arrange var thread = new TestInMemoryAgentThread(); // Act & Assert - Assert.NotNull(thread.GetService(typeof(ChatMessageStore))); - Assert.Same(thread.GetMessageStore(), thread.GetService(typeof(ChatMessageStore))); - Assert.Same(thread.GetMessageStore(), thread.GetService(typeof(InMemoryChatMessageStore))); + Assert.NotNull(thread.GetService(typeof(InMemoryChatHistoryProvider))); + Assert.Same(thread.GetChatHistoryProvider(), thread.GetService(typeof(InMemoryChatHistoryProvider))); } #endregion @@ -147,9 +146,9 @@ public void GetService_RequestingChatMessageStore_ReturnsChatMessageStore() private sealed class TestInMemoryAgentThread : InMemoryAgentThread { public TestInMemoryAgentThread() { } - public TestInMemoryAgentThread(InMemoryChatMessageStore? store) : base(store) { } + public TestInMemoryAgentThread(InMemoryChatHistoryProvider? store) : base(store) { } public TestInMemoryAgentThread(IEnumerable messages) : base(messages) { } public TestInMemoryAgentThread(JsonElement serializedThreadState) : base(serializedThreadState) { } - public InMemoryChatMessageStore GetMessageStore() => this.MessageStore; + public InMemoryChatHistoryProvider GetChatHistoryProvider() => this.ChatHistoryProvider; } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatHistoryProviderTests.cs similarity index 80% rename from dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatHistoryProviderTests.cs index 43bfacca79..0d163b7606 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatHistoryProviderTests.cs @@ -14,24 +14,24 @@ namespace Microsoft.Agents.AI.Abstractions.UnitTests; /// -/// Contains tests for the class. +/// Contains tests for the class. /// -public class InMemoryChatMessageStoreTests +public class InMemoryChatHistoryProviderTests { [Fact] public void Constructor_Throws_ForNullReducer() => // Arrange & Act & Assert - Assert.Throws(() => new InMemoryChatMessageStore(null!)); + Assert.Throws(() => new InMemoryChatHistoryProvider(null!)); [Fact] public void Constructor_DefaultsToBeforeMessageRetrieval_ForNotProvidedTriggerEvent() { // Arrange & Act var reducerMock = new Mock(); - var store = new InMemoryChatMessageStore(reducerMock.Object); + var store = new InMemoryChatHistoryProvider(reducerMock.Object); // Assert - Assert.Equal(InMemoryChatMessageStore.ChatReducerTriggerEvent.BeforeMessagesRetrieval, store.ReducerTriggerEvent); + Assert.Equal(InMemoryChatHistoryProvider.ChatReducerTriggerEvent.BeforeMessagesRetrieval, store.ReducerTriggerEvent); } [Fact] @@ -39,11 +39,11 @@ public void Constructor_Arguments_SetOnPropertiesCorrectly() { // Arrange & Act var reducerMock = new Mock(); - var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded); + var store = new InMemoryChatHistoryProvider(reducerMock.Object, InMemoryChatHistoryProvider.ChatReducerTriggerEvent.AfterMessageAdded); // Assert Assert.Same(reducerMock.Object, store.ChatReducer); - Assert.Equal(InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded, store.ReducerTriggerEvent); + Assert.Equal(InMemoryChatHistoryProvider.ChatReducerTriggerEvent.AfterMessageAdded, store.ReducerTriggerEvent); } [Fact] @@ -57,7 +57,7 @@ public async Task InvokedAsyncAddsMessagesAsync() { new(ChatRole.Assistant, "Hi there!") }; - var messageStoreMessages = new List() + var historyProviderMessages = new List() { new(ChatRole.System, "original instructions") }; @@ -66,9 +66,9 @@ public async Task InvokedAsyncAddsMessagesAsync() new(ChatRole.System, "additional context") }; - var store = new InMemoryChatMessageStore(); - store.Add(messageStoreMessages[0]); - var context = new ChatMessageStore.InvokedContext(requestMessages, messageStoreMessages) + var store = new InMemoryChatHistoryProvider(); + store.Add(historyProviderMessages[0]); + var context = new AIContextProvider.InvokedContext(requestMessages, historyProviderMessages) { AIContextProviderMessages = aiContextProviderMessages, ResponseMessages = responseMessages @@ -85,9 +85,9 @@ public async Task InvokedAsyncAddsMessagesAsync() [Fact] public async Task InvokedAsyncWithEmptyDoesNotFailAsync() { - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); - var context = new ChatMessageStore.InvokedContext([], []); + var context = new AIContextProvider.InvokedContext([], []); await store.InvokedAsync(context, CancellationToken.None); Assert.Empty(store); @@ -96,14 +96,15 @@ public async Task InvokedAsyncWithEmptyDoesNotFailAsync() [Fact] public async Task InvokingAsyncReturnsAllMessagesAsync() { - var store = new InMemoryChatMessageStore + var store = new InMemoryChatHistoryProvider { new ChatMessage(ChatRole.User, "Test1"), new ChatMessage(ChatRole.Assistant, "Test2") }; - var context = new ChatMessageStore.InvokingContext([]); - var result = (await store.InvokingAsync(context, CancellationToken.None)).ToList(); + var context = new AIContextProvider.InvokingContext([]); + var aiContext = await store.InvokingAsync(context, CancellationToken.None); + var result = aiContext.Messages?.ToList() ?? []; Assert.Equal(2, result.Count); Assert.Contains(result, m => m.Text == "Test1"); @@ -115,7 +116,7 @@ public async Task DeserializeConstructorWithEmptyElementAsync() { var emptyObject = JsonSerializer.Deserialize("{}", TestJsonSerializerContext.Default.JsonElement); - var newStore = new InMemoryChatMessageStore(emptyObject); + var newStore = new InMemoryChatHistoryProvider(emptyObject); Assert.Empty(newStore); } @@ -123,14 +124,14 @@ public async Task DeserializeConstructorWithEmptyElementAsync() [Fact] public async Task SerializeAndDeserializeConstructorRoundtripsAsync() { - var store = new InMemoryChatMessageStore + var store = new InMemoryChatHistoryProvider { new ChatMessage(ChatRole.User, "A"), new ChatMessage(ChatRole.Assistant, "B") }; var jsonElement = store.Serialize(); - var newStore = new InMemoryChatMessageStore(jsonElement); + var newStore = new InMemoryChatHistoryProvider(jsonElement); Assert.Equal(2, newStore.Count); Assert.Equal("A", newStore[0].Text); @@ -147,13 +148,13 @@ public async Task SerializeAndDeserializeConstructorRoundtripsWithCustomAIConten }; options.AddAIContentType(typeDiscriminatorId: "testContent"); - var store = new InMemoryChatMessageStore + var store = new InMemoryChatHistoryProvider { new ChatMessage(ChatRole.User, [new TestAIContent("foo data")]), }; var jsonElement = store.Serialize(options); - var newStore = new InMemoryChatMessageStore(jsonElement, options); + var newStore = new InMemoryChatHistoryProvider(jsonElement, options); Assert.Single(newStore); var actualTestAIContent = Assert.IsType(newStore[0].Contents[0]); @@ -163,14 +164,14 @@ public async Task SerializeAndDeserializeConstructorRoundtripsWithCustomAIConten [Fact] public async Task SerializeAndDeserializeWorksWithExperimentalContentTypesAsync() { - var store = new InMemoryChatMessageStore + var store = new InMemoryChatHistoryProvider { new ChatMessage(ChatRole.User, [new FunctionApprovalRequestContent("call123", new FunctionCallContent("call123", "some_func"))]), new ChatMessage(ChatRole.Assistant, [new FunctionApprovalResponseContent("call123", true, new FunctionCallContent("call123", "some_func"))]) }; var jsonElement = store.Serialize(); - var newStore = new InMemoryChatMessageStore(jsonElement); + var newStore = new InMemoryChatHistoryProvider(jsonElement); Assert.Equal(2, newStore.Count); Assert.IsType(newStore[0].Contents[0]); @@ -180,10 +181,10 @@ public async Task SerializeAndDeserializeWorksWithExperimentalContentTypesAsync( [Fact] public async Task InvokedAsyncWithEmptyMessagesDoesNotChangeStoreAsync() { - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); var messages = new List(); - var context = new ChatMessageStore.InvokedContext(messages, []); + var context = new AIContextProvider.InvokedContext(messages, []); await store.InvokedAsync(context, CancellationToken.None); Assert.Empty(store); @@ -193,7 +194,7 @@ public async Task InvokedAsyncWithEmptyMessagesDoesNotChangeStoreAsync() public async Task InvokedAsync_WithNullContext_ThrowsArgumentNullExceptionAsync() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); // Act & Assert await Assert.ThrowsAsync(() => store.InvokedAsync(null!, CancellationToken.None).AsTask()); @@ -203,7 +204,7 @@ public async Task InvokedAsync_WithNullContext_ThrowsArgumentNullExceptionAsync( public void DeserializeContructor_WithNullSerializedState_CreatesEmptyStore() { // Act - var store = new InMemoryChatMessageStore(new JsonElement()); + var store = new InMemoryChatHistoryProvider(new JsonElement()); // Assert Assert.Empty(store); @@ -218,7 +219,7 @@ public async Task DeserializeContructor_WithEmptyMessages_DoesNotAddMessagesAsyn TestJsonSerializerContext.Default.IDictionaryStringObject); // Act - var store = new InMemoryChatMessageStore(stateWithEmptyMessages); + var store = new InMemoryChatHistoryProvider(stateWithEmptyMessages); // Assert Assert.Empty(store); @@ -233,7 +234,7 @@ public async Task DeserializeConstructor_WithNullMessages_DoesNotAddMessagesAsyn TestJsonSerializerContext.Default.DictionaryStringObject); // Act - var store = new InMemoryChatMessageStore(stateWithNullMessages); + var store = new InMemoryChatHistoryProvider(stateWithNullMessages); // Assert Assert.Empty(store); @@ -254,7 +255,7 @@ public async Task DeserializeConstructor_WithValidMessages_AddsMessagesAsync() TestJsonSerializerContext.Default.DictionaryStringObject); // Act - var store = new InMemoryChatMessageStore(serializedState); + var store = new InMemoryChatHistoryProvider(serializedState); // Assert Assert.Equal(2, store.Count); @@ -266,7 +267,7 @@ public async Task DeserializeConstructor_WithValidMessages_AddsMessagesAsync() public void IndexerGet_ReturnsCorrectMessage() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); var message1 = new ChatMessage(ChatRole.User, "First"); var message2 = new ChatMessage(ChatRole.Assistant, "Second"); store.Add(message1); @@ -281,7 +282,7 @@ public void IndexerGet_ReturnsCorrectMessage() public void IndexerSet_UpdatesMessage() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); var originalMessage = new ChatMessage(ChatRole.User, "Original"); var newMessage = new ChatMessage(ChatRole.User, "Updated"); store.Add(originalMessage); @@ -298,7 +299,7 @@ public void IndexerSet_UpdatesMessage() public void IsReadOnly_ReturnsFalse() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); // Act & Assert Assert.False(store.IsReadOnly); @@ -308,7 +309,7 @@ public void IsReadOnly_ReturnsFalse() public void IndexOf_ReturnsCorrectIndex() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); var message1 = new ChatMessage(ChatRole.User, "First"); var message2 = new ChatMessage(ChatRole.Assistant, "Second"); var message3 = new ChatMessage(ChatRole.User, "Third"); @@ -325,7 +326,7 @@ public void IndexOf_ReturnsCorrectIndex() public void Insert_InsertsMessageAtCorrectIndex() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); var message1 = new ChatMessage(ChatRole.User, "First"); var message2 = new ChatMessage(ChatRole.Assistant, "Second"); var insertMessage = new ChatMessage(ChatRole.User, "Inserted"); @@ -346,7 +347,7 @@ public void Insert_InsertsMessageAtCorrectIndex() public void RemoveAt_RemovesMessageAtIndex() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); var message1 = new ChatMessage(ChatRole.User, "First"); var message2 = new ChatMessage(ChatRole.Assistant, "Second"); var message3 = new ChatMessage(ChatRole.User, "Third"); @@ -367,7 +368,7 @@ public void RemoveAt_RemovesMessageAtIndex() public void Clear_RemovesAllMessages() { // Arrange - var store = new InMemoryChatMessageStore + var store = new InMemoryChatHistoryProvider { new ChatMessage(ChatRole.User, "First"), new ChatMessage(ChatRole.Assistant, "Second") @@ -384,7 +385,7 @@ public void Clear_RemovesAllMessages() public void Contains_ReturnsTrueForExistingMessage() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); var message1 = new ChatMessage(ChatRole.User, "First"); var message2 = new ChatMessage(ChatRole.Assistant, "Second"); store.Add(message1); @@ -398,7 +399,7 @@ public void Contains_ReturnsTrueForExistingMessage() public void CopyTo_CopiesMessagesToArray() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); var message1 = new ChatMessage(ChatRole.User, "First"); var message2 = new ChatMessage(ChatRole.Assistant, "Second"); store.Add(message1); @@ -419,7 +420,7 @@ public void CopyTo_CopiesMessagesToArray() public void Remove_RemovesSpecificMessage() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); var message1 = new ChatMessage(ChatRole.User, "First"); var message2 = new ChatMessage(ChatRole.Assistant, "Second"); var message3 = new ChatMessage(ChatRole.User, "Third"); @@ -441,7 +442,7 @@ public void Remove_RemovesSpecificMessage() public void Remove_ReturnsFalseForNonExistentMessage() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); var message1 = new ChatMessage(ChatRole.User, "First"); var message2 = new ChatMessage(ChatRole.Assistant, "Second"); store.Add(message1); @@ -458,7 +459,7 @@ public void Remove_ReturnsFalseForNonExistentMessage() public void GetEnumerator_Generic_ReturnsAllMessages() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); var message1 = new ChatMessage(ChatRole.User, "First"); var message2 = new ChatMessage(ChatRole.Assistant, "Second"); store.Add(message1); @@ -478,7 +479,7 @@ public void GetEnumerator_Generic_ReturnsAllMessages() public void GetEnumerator_NonGeneric_ReturnsAllMessages() { // Arrange - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); var message1 = new ChatMessage(ChatRole.User, "First"); var message2 = new ChatMessage(ChatRole.Assistant, "Second"); store.Add(message1); @@ -517,10 +518,10 @@ public async Task AddMessagesAsync_WithReducer_AfterMessageAdded_InvokesReducerA .Setup(r => r.ReduceAsync(It.Is>(x => x.SequenceEqual(originalMessages)), It.IsAny())) .ReturnsAsync(reducedMessages); - var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded); + var store = new InMemoryChatHistoryProvider(reducerMock.Object, InMemoryChatHistoryProvider.ChatReducerTriggerEvent.AfterMessageAdded); // Act - var context = new ChatMessageStore.InvokedContext(originalMessages, []); + var context = new AIContextProvider.InvokedContext(originalMessages, []); await store.InvokedAsync(context, CancellationToken.None); // Assert @@ -548,7 +549,7 @@ public async Task GetMessagesAsync_WithReducer_BeforeMessagesRetrieval_InvokesRe .Setup(r => r.ReduceAsync(It.Is>(x => x.SequenceEqual(originalMessages)), It.IsAny())) .ReturnsAsync(reducedMessages); - var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.BeforeMessagesRetrieval); + var store = new InMemoryChatHistoryProvider(reducerMock.Object, InMemoryChatHistoryProvider.ChatReducerTriggerEvent.BeforeMessagesRetrieval); // Add messages directly to the store for this test foreach (var msg in originalMessages) { @@ -556,8 +557,9 @@ public async Task GetMessagesAsync_WithReducer_BeforeMessagesRetrieval_InvokesRe } // Act - var invokingContext = new ChatMessageStore.InvokingContext(Array.Empty()); - var result = (await store.InvokingAsync(invokingContext, CancellationToken.None)).ToList(); + var invokingContext = new AIContextProvider.InvokingContext(Array.Empty()); + var aiContext = await store.InvokingAsync(invokingContext, CancellationToken.None); + var result = aiContext.Messages?.ToList() ?? []; // Assert Assert.Single(result); @@ -576,10 +578,10 @@ public async Task AddMessagesAsync_WithReducer_ButWrongTrigger_DoesNotInvokeRedu var reducerMock = new Mock(); - var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.BeforeMessagesRetrieval); + var store = new InMemoryChatHistoryProvider(reducerMock.Object, InMemoryChatHistoryProvider.ChatReducerTriggerEvent.BeforeMessagesRetrieval); // Act - var context = new ChatMessageStore.InvokedContext(originalMessages, []); + var context = new AIContextProvider.InvokedContext(originalMessages, []); await store.InvokedAsync(context, CancellationToken.None); // Assert @@ -599,14 +601,15 @@ public async Task GetMessagesAsync_WithReducer_ButWrongTrigger_DoesNotInvokeRedu var reducerMock = new Mock(); - var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded) + var store = new InMemoryChatHistoryProvider(reducerMock.Object, InMemoryChatHistoryProvider.ChatReducerTriggerEvent.AfterMessageAdded) { originalMessages[0] }; // Act - var invokingContext = new ChatMessageStore.InvokingContext(Array.Empty()); - var result = (await store.InvokingAsync(invokingContext, CancellationToken.None)).ToList(); + var invokingContext = new AIContextProvider.InvokingContext(Array.Empty()); + var aiContext = await store.InvokingAsync(invokingContext, CancellationToken.None); + var result = aiContext.Messages?.ToList() ?? []; // Assert Assert.Single(result); diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/MessageFilteringAIContextProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/MessageFilteringAIContextProviderTests.cs new file mode 100644 index 0000000000..b52b7197e5 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/MessageFilteringAIContextProviderTests.cs @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Moq; + +namespace Microsoft.Agents.AI.Abstractions.UnitTests; + +/// +/// Contains tests for the class. +/// +public sealed class MessageFilteringAIContextProviderTests +{ + [Fact] + public void Constructor_WithNullInnerStore_ThrowsArgumentNullException() + { + // Arrange, Act & Assert + Assert.Throws(() => new MessageFilteringAIContextProvider(null!)); + } + + [Fact] + public void Constructor_WithOnlyInnerStore_Throws() + { + // Arrange + var innerProviderMock = new Mock(); + + // Act & Assert + Assert.Throws(() => new MessageFilteringAIContextProvider(innerProviderMock.Object)); + } + + [Fact] + public void Constructor_WithAllParameters_CreatesInstance() + { + // Arrange + var innerProviderMock = new Mock(); + + AIContext InvokingFilter(AIContext ctx) => ctx; + AIContextProvider.InvokedContext InvokedFilter(AIContextProvider.InvokedContext ctx) => ctx; + + // Act + var filter = new MessageFilteringAIContextProvider(innerProviderMock.Object, InvokingFilter, InvokedFilter); + + // Assert + Assert.NotNull(filter); + } + + [Fact] + public async Task InvokingAsync_WithNoOpFilters_ReturnsInnerStoreMessagesAsync() + { + // Arrange + var innerProviderMock = new Mock(); + var expectedMessages = new List + { + new(ChatRole.User, "Hello"), + new(ChatRole.Assistant, "Hi there!") + }; + var context = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "Test")]); + + innerProviderMock + .Setup(s => s.InvokingAsync(context, It.IsAny())) + .ReturnsAsync(new AIContext { Messages = expectedMessages }); + + var filter = new MessageFilteringAIContextProvider(innerProviderMock.Object, x => x, x => x); + + // Act + var aiContext = await filter.InvokingAsync(context, CancellationToken.None); + var result = aiContext.Messages?.ToList() ?? []; + + // Assert + Assert.Equal(2, result.Count); + Assert.Equal("Hello", result[0].Text); + Assert.Equal("Hi there!", result[1].Text); + innerProviderMock.Verify(s => s.InvokingAsync(context, It.IsAny()), Times.Once); + } + + [Fact] + public async Task InvokingAsync_WithInvokingFilter_AppliesFilterAsync() + { + // Arrange + var innerProviderMock = new Mock(); + var innerMessages = new List + { + new(ChatRole.User, "Hello"), + new(ChatRole.Assistant, "Hi there!"), + new(ChatRole.User, "How are you?") + }; + var context = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "Test")]); + + innerProviderMock + .Setup(s => s.InvokingAsync(context, It.IsAny())) + .ReturnsAsync(new AIContext { Messages = innerMessages }); + + // Filter to only user messages + AIContext InvokingFilter(AIContext ctx) + { + var filteredMessages = ctx.Messages?.Where(m => m.Role == ChatRole.User).ToList(); + return new AIContext { Messages = filteredMessages, Instructions = ctx.Instructions, Tools = ctx.Tools }; + } + + var filter = new MessageFilteringAIContextProvider(innerProviderMock.Object, InvokingFilter); + + // Act + var aiContext = await filter.InvokingAsync(context, CancellationToken.None); + var result = aiContext.Messages?.ToList() ?? []; + + // Assert + Assert.Equal(2, result.Count); + Assert.All(result, msg => Assert.Equal(ChatRole.User, msg.Role)); + innerProviderMock.Verify(s => s.InvokingAsync(context, It.IsAny()), Times.Once); + } + + [Fact] + public async Task InvokingAsync_WithInvokingFilter_CanModifyMessagesAsync() + { + // Arrange + var innerProviderMock = new Mock(); + var innerMessages = new List + { + new(ChatRole.User, "Hello"), + new(ChatRole.Assistant, "Hi there!") + }; + var context = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "Test")]); + + innerProviderMock + .Setup(s => s.InvokingAsync(context, It.IsAny())) + .ReturnsAsync(new AIContext { Messages = innerMessages }); + + // Filter that transforms messages + AIContext InvokingFilter(AIContext ctx) + { + var transformedMessages = ctx.Messages?.Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}")).ToList(); + return new AIContext { Messages = transformedMessages, Instructions = ctx.Instructions, Tools = ctx.Tools }; + } + + var filter = new MessageFilteringAIContextProvider(innerProviderMock.Object, InvokingFilter); + + // Act + var aiContext = await filter.InvokingAsync(context, CancellationToken.None); + var result = aiContext.Messages?.ToList() ?? []; + + // Assert + Assert.Equal(2, result.Count); + Assert.Equal("[FILTERED] Hello", result[0].Text); + Assert.Equal("[FILTERED] Hi there!", result[1].Text); + } + + [Fact] + public async Task InvokedAsync_WithInvokedFilter_AppliesFilterAsync() + { + // Arrange + var innerProviderMock = new Mock(); + var requestMessages = new List { new(ChatRole.User, "Hello") }; + var aiContextProviderMessages = new List { new(ChatRole.System, "System") }; + var responseMessages = new List { new(ChatRole.Assistant, "Response") }; + var context = new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages) + { + ResponseMessages = responseMessages + }; + + AIContextProvider.InvokedContext? capturedContext = null; + innerProviderMock + .Setup(s => s.InvokedAsync(It.IsAny(), It.IsAny())) + .Callback((ctx, ct) => capturedContext = ctx) + .Returns(default(ValueTask)); + + // Filter that modifies the context + AIContextProvider.InvokedContext InvokedFilter(AIContextProvider.InvokedContext ctx) + { + var modifiedRequestMessages = ctx.RequestMessages.Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}")).ToList(); + return new AIContextProvider.InvokedContext(modifiedRequestMessages, ctx.AIContextProviderMessages) + { + ResponseMessages = ctx.ResponseMessages, + ChatHistoryMessages = ctx.ChatHistoryMessages, + InvokeException = ctx.InvokeException + }; + } + + var filter = new MessageFilteringAIContextProvider(innerProviderMock.Object, invokedContextFilter: InvokedFilter); + + // Act + await filter.InvokedAsync(context, CancellationToken.None); + + // Assert + Assert.NotNull(capturedContext); + Assert.Single(capturedContext.RequestMessages); + Assert.Equal("[FILTERED] Hello", capturedContext.RequestMessages.First().Text); + innerProviderMock.Verify(s => s.InvokedAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public void Serialize_DelegatesToInnerStore() + { + // Arrange + var innerProviderMock = new Mock(); + var expectedJson = JsonSerializer.SerializeToElement("data", TestJsonSerializerContext.Default.String); + + innerProviderMock + .Setup(s => s.Serialize(It.IsAny())) + .Returns(expectedJson); + + var filter = new MessageFilteringAIContextProvider(innerProviderMock.Object, x => x, x => x); + + // Act + var result = filter.Serialize(); + + // Assert + Assert.Equal(expectedJson.GetRawText(), result.GetRawText()); + innerProviderMock.Verify(s => s.Serialize(null), Times.Once); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/TestJsonSerializerContext.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/TestJsonSerializerContext.cs index 1f6f9bb578..397cd48c86 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/TestJsonSerializerContext.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/TestJsonSerializerContext.cs @@ -22,5 +22,5 @@ namespace Microsoft.Agents.AI.Abstractions.UnitTests; [JsonSerializable(typeof(InMemoryAgentThread.InMemoryAgentThreadState))] [JsonSerializable(typeof(ServiceIdAgentThread.ServiceIdAgentThreadState))] [JsonSerializable(typeof(ServiceIdAgentThreadTests.EmptyObject))] -[JsonSerializable(typeof(InMemoryChatMessageStoreTests.TestAIContent))] +[JsonSerializable(typeof(InMemoryChatHistoryProviderTests.TestAIContent))] internal sealed partial class TestJsonSerializerContext : JsonSerializerContext; diff --git a/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatMessageStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs similarity index 74% rename from dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatMessageStoreTests.cs rename to dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs index 9410e68f1b..363a05043b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatMessageStoreTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs @@ -14,7 +14,7 @@ namespace Microsoft.Agents.AI.CosmosNoSql.UnitTests; /// -/// Contains tests for . +/// Contains tests for . /// /// Test Modes: /// - Default Mode: Cleans up all test data after each test run (deletes database) @@ -39,7 +39,7 @@ namespace Microsoft.Agents.AI.CosmosNoSql.UnitTests; /// - Reset to cleanup mode: $env:COSMOS_PRESERVE_CONTAINERS=""; dotnet test tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/ /// [Collection("CosmosDB")] -public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable +public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable { // Cosmos DB Emulator connection settings private const string EmulatorEndpoint = "https://localhost:8081"; @@ -154,7 +154,7 @@ public void Constructor_WithConnectionString_ShouldCreateInstance() this.SkipIfEmulatorNotAvailable(); // Act - using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, "test-conversation"); + using var store = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, "test-conversation"); // Assert Assert.NotNull(store); @@ -171,7 +171,7 @@ public void Constructor_WithConnectionStringNoConversationId_ShouldCreateInstanc this.SkipIfEmulatorNotAvailable(); // Act - using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId); + using var store = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId); // Assert Assert.NotNull(store); @@ -186,7 +186,7 @@ public void Constructor_WithNullConnectionString_ShouldThrowArgumentException() { // Arrange & Act & Assert Assert.Throws(() => - new CosmosChatMessageStore((string)null!, s_testDatabaseId, TestContainerId, "test-conversation")); + new CosmosChatHistoryProvider((string)null!, s_testDatabaseId, TestContainerId, "test-conversation")); } [SkippableFact] @@ -197,7 +197,7 @@ public void Constructor_WithEmptyConversationId_ShouldThrowArgumentException() this.SkipIfEmulatorNotAvailable(); Assert.Throws(() => - new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, "")); + new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, "")); } #endregion @@ -211,10 +211,10 @@ public async Task InvokedAsync_WithSingleMessage_ShouldAddMessageAsync() // Arrange this.SkipIfEmulatorNotAvailable(); var conversationId = Guid.NewGuid().ToString(); - using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, conversationId); + using var store = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversationId); var message = new ChatMessage(ChatRole.User, "Hello, world!"); - var context = new ChatMessageStore.InvokedContext([message], []) + var context = new AIContextProvider.InvokedContext([message], []) { ResponseMessages = [] }; @@ -226,9 +226,9 @@ public async Task InvokedAsync_WithSingleMessage_ShouldAddMessageAsync() await Task.Delay(100); // Assert - var invokingContext = new ChatMessageStore.InvokingContext([]); - var messages = await store.InvokingAsync(invokingContext); - var messageList = messages.ToList(); + var invokingContext = new AIContextProvider.InvokingContext([]); + var aiContext = await store.InvokingAsync(invokingContext); + var messageList = aiContext.Messages?.ToList() ?? []; // Simple assertion - if this fails, we know the deserialization is the issue if (messageList.Count == 0) @@ -277,7 +277,7 @@ public async Task InvokedAsync_WithMultipleMessages_ShouldAddAllMessagesAsync() // Arrange this.SkipIfEmulatorNotAvailable(); var conversationId = Guid.NewGuid().ToString(); - using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, conversationId); + using var store = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversationId); var requestMessages = new[] { new ChatMessage(ChatRole.User, "First message"), @@ -293,7 +293,7 @@ public async Task InvokedAsync_WithMultipleMessages_ShouldAddAllMessagesAsync() new ChatMessage(ChatRole.Assistant, "Response message") }; - var context = new ChatMessageStore.InvokedContext(requestMessages, []) + var context = new AIContextProvider.InvokedContext(requestMessages, []) { AIContextProviderMessages = aiContextProviderMessages, ResponseMessages = responseMessages @@ -303,9 +303,9 @@ public async Task InvokedAsync_WithMultipleMessages_ShouldAddAllMessagesAsync() await store.InvokedAsync(context); // Assert - var invokingContext = new ChatMessageStore.InvokingContext([]); - var retrievedMessages = await store.InvokingAsync(invokingContext); - var messageList = retrievedMessages.ToList(); + var invokingContext = new AIContextProvider.InvokingContext([]); + var aiContext = await store.InvokingAsync(invokingContext); + var messageList = aiContext.Messages?.ToList() ?? []; Assert.Equal(5, messageList.Count); Assert.Equal("First message", messageList[0].Text); Assert.Equal("Second message", messageList[1].Text); @@ -324,14 +324,14 @@ public async Task InvokingAsync_WithNoMessages_ShouldReturnEmptyAsync() { // Arrange this.SkipIfEmulatorNotAvailable(); - using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, Guid.NewGuid().ToString()); + using var store = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, Guid.NewGuid().ToString()); // Act - var invokingContext = new ChatMessageStore.InvokingContext([]); - var messages = await store.InvokingAsync(invokingContext); + var invokingContext = new AIContextProvider.InvokingContext([]); + var aiContext = await store.InvokingAsync(invokingContext); // Assert - Assert.Empty(messages); + Assert.Empty(aiContext.Messages ?? []); } [SkippableFact] @@ -343,25 +343,25 @@ public async Task InvokingAsync_WithConversationIsolation_ShouldOnlyReturnMessag var conversation1 = Guid.NewGuid().ToString(); var conversation2 = Guid.NewGuid().ToString(); - using var store1 = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, conversation1); - using var store2 = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, conversation2); + using var store1 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversation1); + using var store2 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversation2); - var context1 = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Message for conversation 1")], []); - var context2 = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Message for conversation 2")], []); + var context1 = new AIContextProvider.InvokedContext([new ChatMessage(ChatRole.User, "Message for conversation 1")], []); + var context2 = new AIContextProvider.InvokedContext([new ChatMessage(ChatRole.User, "Message for conversation 2")], []); await store1.InvokedAsync(context1); await store2.InvokedAsync(context2); // Act - var invokingContext1 = new ChatMessageStore.InvokingContext([]); - var invokingContext2 = new ChatMessageStore.InvokingContext([]); + var invokingContext1 = new AIContextProvider.InvokingContext([]); + var invokingContext2 = new AIContextProvider.InvokingContext([]); - var messages1 = await store1.InvokingAsync(invokingContext1); - var messages2 = await store2.InvokingAsync(invokingContext2); + var aiContext1 = await store1.InvokingAsync(invokingContext1); + var aiContext2 = await store2.InvokingAsync(invokingContext2); // Assert - var messageList1 = messages1.ToList(); - var messageList2 = messages2.ToList(); + var messageList1 = aiContext1.Messages?.ToList() ?? []; + var messageList2 = aiContext2.Messages?.ToList() ?? []; Assert.Single(messageList1); Assert.Single(messageList2); Assert.Equal("Message for conversation 1", messageList1[0].Text); @@ -379,7 +379,7 @@ public async Task FullWorkflow_AddAndGet_ShouldWorkCorrectlyAsync() // Arrange this.SkipIfEmulatorNotAvailable(); var conversationId = $"test-conversation-{Guid.NewGuid():N}"; // Use unique conversation ID - using var originalStore = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, conversationId); + using var originalStore = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversationId); var messages = new[] { @@ -391,19 +391,19 @@ public async Task FullWorkflow_AddAndGet_ShouldWorkCorrectlyAsync() }; // Act 1: Add messages - var invokedContext = new ChatMessageStore.InvokedContext(messages, []); + var invokedContext = new AIContextProvider.InvokedContext(messages, []); await originalStore.InvokedAsync(invokedContext); // Act 2: Verify messages were added - var invokingContext = new ChatMessageStore.InvokingContext([]); - var retrievedMessages = await originalStore.InvokingAsync(invokingContext); - var retrievedList = retrievedMessages.ToList(); + var invokingContext = new AIContextProvider.InvokingContext([]); + var aiContext = await originalStore.InvokingAsync(invokingContext); + var retrievedList = aiContext.Messages?.ToList() ?? []; Assert.Equal(5, retrievedList.Count); // Act 3: Create new store instance for same conversation (test persistence) - using var newStore = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, conversationId); - var persistedMessages = await newStore.InvokingAsync(invokingContext); - var persistedList = persistedMessages.ToList(); + using var newStore = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversationId); + var persistedAiContext = await newStore.InvokingAsync(invokingContext); + var persistedList = persistedAiContext.Messages?.ToList() ?? []; // Assert final state Assert.Equal(5, persistedList.Count); @@ -424,7 +424,7 @@ public void Dispose_AfterUse_ShouldNotThrow() { // Arrange this.SkipIfEmulatorNotAvailable(); - var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, Guid.NewGuid().ToString()); + var store = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, Guid.NewGuid().ToString()); // Act & Assert store.Dispose(); // Should not throw @@ -436,7 +436,7 @@ public void Dispose_MultipleCalls_ShouldNotThrow() { // Arrange this.SkipIfEmulatorNotAvailable(); - var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, Guid.NewGuid().ToString()); + var store = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, Guid.NewGuid().ToString()); // Act & Assert store.Dispose(); // First call @@ -455,7 +455,7 @@ public void Constructor_WithHierarchicalConnectionString_ShouldCreateInstance() this.SkipIfEmulatorNotAvailable(); // Act - using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "user-456", "session-789"); + using var store = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "user-456", "session-789"); // Assert Assert.NotNull(store); @@ -473,7 +473,7 @@ public void Constructor_WithHierarchicalEndpoint_ShouldCreateInstance() // Act TokenCredential credential = new DefaultAzureCredential(); - using var store = new CosmosChatMessageStore(EmulatorEndpoint, credential, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "user-456", "session-789"); + using var store = new CosmosChatHistoryProvider(EmulatorEndpoint, credential, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "user-456", "session-789"); // Assert Assert.NotNull(store); @@ -490,7 +490,7 @@ public void Constructor_WithHierarchicalCosmosClient_ShouldCreateInstance() this.SkipIfEmulatorNotAvailable(); using var cosmosClient = new CosmosClient(EmulatorEndpoint, EmulatorKey); - using var store = new CosmosChatMessageStore(cosmosClient, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "user-456", "session-789"); + using var store = new CosmosChatHistoryProvider(cosmosClient, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "user-456", "session-789"); // Assert Assert.NotNull(store); @@ -507,7 +507,7 @@ public void Constructor_WithHierarchicalNullTenantId_ShouldThrowArgumentExceptio this.SkipIfEmulatorNotAvailable(); Assert.Throws(() => - new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, null!, "user-456", "session-789")); + new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, null!, "user-456", "session-789")); } [SkippableFact] @@ -518,7 +518,7 @@ public void Constructor_WithHierarchicalEmptyUserId_ShouldThrowArgumentException this.SkipIfEmulatorNotAvailable(); Assert.Throws(() => - new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "", "session-789")); + new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "", "session-789")); } [SkippableFact] @@ -529,7 +529,7 @@ public void Constructor_WithHierarchicalWhitespaceSessionId_ShouldThrowArgumentE this.SkipIfEmulatorNotAvailable(); Assert.Throws(() => - new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "user-456", " ")); + new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "user-456", " ")); } [SkippableFact] @@ -542,10 +542,10 @@ public async Task InvokedAsync_WithHierarchicalPartitioning_ShouldAddMessageWith const string UserId = "user-456"; const string SessionId = "session-789"; // Test hierarchical partitioning constructor with connection string - using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId); + using var store = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId); var message = new ChatMessage(ChatRole.User, "Hello from hierarchical partitioning!"); - var context = new ChatMessageStore.InvokedContext([message], []); + var context = new AIContextProvider.InvokedContext([message], []); // Act await store.InvokedAsync(context); @@ -554,9 +554,9 @@ public async Task InvokedAsync_WithHierarchicalPartitioning_ShouldAddMessageWith await Task.Delay(100); // Assert - var invokingContext = new ChatMessageStore.InvokingContext([]); - var messages = await store.InvokingAsync(invokingContext); - var messageList = messages.ToList(); + var invokingContext = new AIContextProvider.InvokingContext([]); + var aiContext = await store.InvokingAsync(invokingContext); + var messageList = aiContext.Messages?.ToList() ?? []; Assert.Single(messageList); Assert.Equal("Hello from hierarchical partitioning!", messageList[0].Text); @@ -594,7 +594,7 @@ public async Task InvokedAsync_WithHierarchicalMultipleMessages_ShouldAddAllMess const string UserId = "user-batch"; const string SessionId = "session-batch"; // Test hierarchical partitioning constructor with connection string - using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId); + using var store = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId); var messages = new[] { new ChatMessage(ChatRole.User, "First hierarchical message"), @@ -602,7 +602,7 @@ public async Task InvokedAsync_WithHierarchicalMultipleMessages_ShouldAddAllMess new ChatMessage(ChatRole.User, "Third hierarchical message") }; - var context = new ChatMessageStore.InvokedContext(messages, []); + var context = new AIContextProvider.InvokedContext(messages, []); // Act await store.InvokedAsync(context); @@ -611,9 +611,9 @@ public async Task InvokedAsync_WithHierarchicalMultipleMessages_ShouldAddAllMess await Task.Delay(100); // Assert - var invokingContext = new ChatMessageStore.InvokingContext([]); - var retrievedMessages = await store.InvokingAsync(invokingContext); - var messageList = retrievedMessages.ToList(); + var invokingContext = new AIContextProvider.InvokingContext([]); + var aiContext = await store.InvokingAsync(invokingContext); + var messageList = aiContext.Messages?.ToList() ?? []; Assert.Equal(3, messageList.Count); Assert.Equal("First hierarchical message", messageList[0].Text); @@ -633,12 +633,12 @@ public async Task InvokingAsync_WithHierarchicalPartitionIsolation_ShouldIsolate const string SessionId = "session-isolation"; // Different userIds create different hierarchical partitions, providing proper isolation - using var store1 = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId1, SessionId); - using var store2 = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId2, SessionId); + using var store1 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId1, SessionId); + using var store2 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId2, SessionId); // Add messages to both stores - var context1 = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Message from user 1")], []); - var context2 = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Message from user 2")], []); + var context1 = new AIContextProvider.InvokedContext([new ChatMessage(ChatRole.User, "Message from user 1")], []); + var context2 = new AIContextProvider.InvokedContext([new ChatMessage(ChatRole.User, "Message from user 2")], []); await store1.InvokedAsync(context1); await store2.InvokedAsync(context2); @@ -647,14 +647,14 @@ public async Task InvokingAsync_WithHierarchicalPartitionIsolation_ShouldIsolate await Task.Delay(100); // Act & Assert - var invokingContext1 = new ChatMessageStore.InvokingContext([]); - var invokingContext2 = new ChatMessageStore.InvokingContext([]); + var invokingContext1 = new AIContextProvider.InvokingContext([]); + var invokingContext2 = new AIContextProvider.InvokingContext([]); - var messages1 = await store1.InvokingAsync(invokingContext1); - var messageList1 = messages1.ToList(); + var aiContext1 = await store1.InvokingAsync(invokingContext1); + var messageList1 = aiContext1.Messages?.ToList() ?? []; - var messages2 = await store2.InvokingAsync(invokingContext2); - var messageList2 = messages2.ToList(); + var aiContext2 = await store2.InvokingAsync(invokingContext2); + var messageList2 = aiContext2.Messages?.ToList() ?? []; // With true hierarchical partitioning, each user sees only their own messages Assert.Single(messageList1); @@ -673,9 +673,9 @@ public async Task SerializeDeserialize_WithHierarchicalPartitioning_ShouldPreser const string UserId = "user-serialize"; const string SessionId = "session-serialize"; - using var originalStore = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId); + using var originalStore = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId); - var context = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Test serialization message")], []); + var context = new AIContextProvider.InvokedContext([new ChatMessage(ChatRole.User, "Test serialization message")], []); await originalStore.InvokedAsync(context); // Act - Serialize the store state @@ -687,15 +687,15 @@ public async Task SerializeDeserialize_WithHierarchicalPartitioning_ShouldPreser { TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; - using var deserializedStore = CosmosChatMessageStore.CreateFromSerializedState(cosmosClient, serializedState, s_testDatabaseId, HierarchicalTestContainerId, serializerOptions); + using var deserializedStore = CosmosChatHistoryProvider.CreateFromSerializedState(cosmosClient, serializedState, s_testDatabaseId, HierarchicalTestContainerId, serializerOptions); // Wait a moment for eventual consistency await Task.Delay(100); // Assert - The deserialized store should have the same functionality - var invokingContext = new ChatMessageStore.InvokingContext([]); - var messages = await deserializedStore.InvokingAsync(invokingContext); - var messageList = messages.ToList(); + var invokingContext = new AIContextProvider.InvokingContext([]); + var aiContext = await deserializedStore.InvokingAsync(invokingContext); + var messageList = aiContext.Messages?.ToList() ?? []; Assert.Single(messageList); Assert.Equal("Test serialization message", messageList[0].Text); @@ -713,12 +713,12 @@ public async Task HierarchicalAndSimplePartitioning_ShouldCoexistAsync() const string SessionId = "coexist-session"; // Create simple store using simple partitioning container and hierarchical store using hierarchical container - using var simpleStore = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, SessionId); - using var hierarchicalStore = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-coexist", "user-coexist", SessionId); + using var simpleStore = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, SessionId); + using var hierarchicalStore = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-coexist", "user-coexist", SessionId); // Add messages to both - var simpleContext = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Simple partitioning message")], []); - var hierarchicalContext = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Hierarchical partitioning message")], []); + var simpleContext = new AIContextProvider.InvokedContext([new ChatMessage(ChatRole.User, "Simple partitioning message")], []); + var hierarchicalContext = new AIContextProvider.InvokedContext([new ChatMessage(ChatRole.User, "Hierarchical partitioning message")], []); await simpleStore.InvokedAsync(simpleContext); await hierarchicalStore.InvokedAsync(hierarchicalContext); @@ -727,13 +727,13 @@ public async Task HierarchicalAndSimplePartitioning_ShouldCoexistAsync() await Task.Delay(100); // Act & Assert - var invokingContext = new ChatMessageStore.InvokingContext([]); + var invokingContext = new AIContextProvider.InvokingContext([]); - var simpleMessages = await simpleStore.InvokingAsync(invokingContext); - var simpleMessageList = simpleMessages.ToList(); + var simpleAiContext = await simpleStore.InvokingAsync(invokingContext); + var simpleMessageList = simpleAiContext.Messages?.ToList() ?? []; - var hierarchicalMessages = await hierarchicalStore.InvokingAsync(invokingContext); - var hierarchicalMessageList = hierarchicalMessages.ToList(); + var hierarchicalAiContext = await hierarchicalStore.InvokingAsync(invokingContext); + var hierarchicalMessageList = hierarchicalAiContext.Messages?.ToList() ?? []; // Each should only see its own messages since they use different containers Assert.Single(simpleMessageList); @@ -750,7 +750,7 @@ public async Task MaxMessagesToRetrieve_ShouldLimitAndReturnMostRecentAsync() this.SkipIfEmulatorNotAvailable(); const string ConversationId = "max-messages-test"; - using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, ConversationId); + using var store = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, ConversationId); // Add 10 messages var messages = new List(); @@ -760,7 +760,7 @@ public async Task MaxMessagesToRetrieve_ShouldLimitAndReturnMostRecentAsync() await Task.Delay(10); // Small delay to ensure different timestamps } - var context = new ChatMessageStore.InvokedContext(messages, []); + var context = new AIContextProvider.InvokedContext(messages, []); await store.InvokedAsync(context); // Wait for eventual consistency @@ -768,9 +768,9 @@ public async Task MaxMessagesToRetrieve_ShouldLimitAndReturnMostRecentAsync() // Act - Set max to 5 and retrieve store.MaxMessagesToRetrieve = 5; - var invokingContext = new ChatMessageStore.InvokingContext([]); - var retrievedMessages = await store.InvokingAsync(invokingContext); - var messageList = retrievedMessages.ToList(); + var invokingContext = new AIContextProvider.InvokingContext([]); + var aiContext = await store.InvokingAsync(invokingContext); + var messageList = aiContext.Messages?.ToList() ?? []; // Assert - Should get the 5 most recent messages (6-10) in ascending order Assert.Equal(5, messageList.Count); @@ -789,7 +789,7 @@ public async Task MaxMessagesToRetrieve_Null_ShouldReturnAllMessagesAsync() this.SkipIfEmulatorNotAvailable(); const string ConversationId = "max-messages-null-test"; - using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, ConversationId); + using var store = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, ConversationId); // Add 10 messages var messages = new List(); @@ -798,16 +798,16 @@ public async Task MaxMessagesToRetrieve_Null_ShouldReturnAllMessagesAsync() messages.Add(new ChatMessage(ChatRole.User, $"Message {i}")); } - var context = new ChatMessageStore.InvokedContext(messages, []); + var context = new AIContextProvider.InvokedContext(messages, []); await store.InvokedAsync(context); // Wait for eventual consistency await Task.Delay(100); // Act - No limit set (default null) - var invokingContext = new ChatMessageStore.InvokingContext([]); - var retrievedMessages = await store.InvokingAsync(invokingContext); - var messageList = retrievedMessages.ToList(); + var invokingContext = new AIContextProvider.InvokingContext([]); + var aiContext = await store.InvokingAsync(invokingContext); + var messageList = aiContext.Messages?.ToList() ?? []; // Assert - Should get all 10 messages Assert.Equal(10, messageList.Count); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentOptionsTests.cs index 896a4ceba5..783d4c8be6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentOptionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentOptionsTests.cs @@ -23,7 +23,7 @@ public void DefaultConstructor_InitializesWithNullValues() Assert.Null(options.Name); Assert.Null(options.Description); Assert.Null(options.ChatOptions); - Assert.Null(options.ChatMessageStoreFactory); + Assert.Null(options.ChatHistoryProviderFactory); Assert.Null(options.AIContextProviderFactory); } @@ -37,7 +37,7 @@ public void Constructor_WithNullValues_SetsPropertiesCorrectly() Assert.Null(options.Name); Assert.Null(options.Description); Assert.Null(options.AIContextProviderFactory); - Assert.Null(options.ChatMessageStoreFactory); + Assert.Null(options.ChatHistoryProviderFactory); Assert.NotNull(options.ChatOptions); Assert.Null(options.ChatOptions.Instructions); Assert.Null(options.ChatOptions.Tools); @@ -117,8 +117,8 @@ public void Clone_CreatesDeepCopyWithSameValues() const string Description = "Test description"; var tools = new List { AIFunctionFactory.Create(() => "test") }; - static ValueTask ChatMessageStoreFactoryAsync( - ChatClientAgentOptions.ChatMessageStoreFactoryContext ctx, CancellationToken ct) => new(new Mock().Object); + static ValueTask ChatHistoryProviderFactoryAsync( + ChatClientAgentOptions.AIContextProviderFactoryContext ctx, CancellationToken ct) => new(new Mock().Object); static ValueTask AIContextProviderFactoryAsync( ChatClientAgentOptions.AIContextProviderFactoryContext ctx, CancellationToken ct) => new(new Mock().Object); @@ -129,7 +129,7 @@ static ValueTask AIContextProviderFactoryAsync( Description = Description, ChatOptions = new() { Tools = tools }, Id = "test-id", - ChatMessageStoreFactory = ChatMessageStoreFactoryAsync, + ChatHistoryProviderFactory = ChatHistoryProviderFactoryAsync, AIContextProviderFactory = AIContextProviderFactoryAsync }; @@ -141,7 +141,7 @@ static ValueTask AIContextProviderFactoryAsync( Assert.Equal(original.Id, clone.Id); Assert.Equal(original.Name, clone.Name); Assert.Equal(original.Description, clone.Description); - Assert.Same(original.ChatMessageStoreFactory, clone.ChatMessageStoreFactory); + Assert.Same(original.ChatHistoryProviderFactory, clone.ChatHistoryProviderFactory); Assert.Same(original.AIContextProviderFactory, clone.AIContextProviderFactory); // ChatOptions should be cloned, not the same reference @@ -170,7 +170,7 @@ public void Clone_WithoutProvidingChatOptions_ClonesCorrectly() Assert.Equal(original.Name, clone.Name); Assert.Equal(original.Description, clone.Description); Assert.Null(original.ChatOptions); - Assert.Null(clone.ChatMessageStoreFactory); + Assert.Null(clone.ChatHistoryProviderFactory); Assert.Null(clone.AIContextProviderFactory); } diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs index 546dc258cd..faa6edb53a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs @@ -222,7 +222,7 @@ public async Task RunAsyncSetsAuthorNameOnAllResponseMessagesAsync(string? autho } /// - /// Verify that RunAsync works with existing thread and can retreive messages if the thread has a MessageStore. + /// Verify that RunAsync works with existing thread and can retreive messages if the thread has a ChatHistoryProvider. /// [Fact] public async Task RunAsyncRetrievesMessagesFromThreadWhenThreadStoresMessagesThreadAsync() @@ -426,10 +426,10 @@ public async Task RunAsyncSetsConversationIdOnThreadWhenReturnedByChatClientAsyn } /// - /// Verify that RunAsync uses the ChatMessageStore factory when the chat client returns no conversation id. + /// Verify that RunAsync uses the ChatHistoryProvider factory when the chat client returns no conversation id. /// [Fact] - public async Task RunAsyncUsesChatMessageStoreWhenNoConversationIdReturnedByChatClientAsync() + public async Task RunAsyncUsesChatHistoryProviderWhenNoConversationIdReturnedByChatClientAsync() { // Arrange Mock mockService = new(); @@ -438,12 +438,12 @@ public async Task RunAsyncUsesChatMessageStoreWhenNoConversationIdReturnedByChat It.IsAny>(), It.IsAny(), It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); - Mock>> mockFactory = new(); - mockFactory.Setup(f => f(It.IsAny(), It.IsAny())).ReturnsAsync(new InMemoryChatMessageStore()); + Mock>> mockFactory = new(); + mockFactory.Setup(f => f(It.IsAny(), It.IsAny())).ReturnsAsync(new InMemoryChatHistoryProvider()); ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" }, - ChatMessageStoreFactory = mockFactory.Object + ChatHistoryProviderFactory = mockFactory.Object }); // Act @@ -451,18 +451,18 @@ public async Task RunAsyncUsesChatMessageStoreWhenNoConversationIdReturnedByChat await agent.RunAsync([new(ChatRole.User, "test")], thread); // Assert - var messageStore = Assert.IsType(thread!.MessageStore); - Assert.Equal(2, messageStore.Count); - Assert.Equal("test", messageStore[0].Text); - Assert.Equal("response", messageStore[1].Text); - mockFactory.Verify(f => f(It.IsAny(), It.IsAny()), Times.Once); + var historyProvider = Assert.IsType(thread!.ChatHistoryProvider); + Assert.Equal(2, historyProvider.Count); + Assert.Equal("test", historyProvider[0].Text); + Assert.Equal("response", historyProvider[1].Text); + mockFactory.Verify(f => f(It.IsAny(), It.IsAny()), Times.Once); } /// - /// Verify that RunAsync uses the default InMemoryChatMessageStore when the chat client returns no conversation id. + /// Verify that RunAsync uses the default InMemoryChatHistoryProvider when the chat client returns no conversation id. /// [Fact] - public async Task RunAsyncUsesDefaultInMemoryChatMessageStoreWhenNoConversationIdReturnedByChatClientAsync() + public async Task RunAsyncUsesDefaultInMemoryChatHistoryProviderWhenNoConversationIdReturnedByChatClientAsync() { // Arrange Mock mockService = new(); @@ -481,17 +481,17 @@ public async Task RunAsyncUsesDefaultInMemoryChatMessageStoreWhenNoConversationI await agent.RunAsync([new(ChatRole.User, "test")], thread); // Assert - var messageStore = Assert.IsType(thread!.MessageStore); - Assert.Equal(2, messageStore.Count); - Assert.Equal("test", messageStore[0].Text); - Assert.Equal("response", messageStore[1].Text); + var historyProvider = Assert.IsType(thread!.ChatHistoryProvider); + Assert.Equal(2, historyProvider.Count); + Assert.Equal("test", historyProvider[0].Text); + Assert.Equal("response", historyProvider[1].Text); } /// - /// Verify that RunAsync uses the ChatMessageStore factory when the chat client returns no conversation id. + /// Verify that RunAsync uses the ChatHistoryProvider factory when the chat client returns no conversation id. /// [Fact] - public async Task RunAsyncUsesChatMessageStoreFactoryWhenProvidedAndNoConversationIdReturnedByChatClientAsync() + public async Task RunAsyncUsesChatHistoryProviderFactoryWhenProvidedAndNoConversationIdReturnedByChatClientAsync() { // Arrange Mock mockService = new(); @@ -501,21 +501,21 @@ public async Task RunAsyncUsesChatMessageStoreFactoryWhenProvidedAndNoConversati It.IsAny(), It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); - Mock mockChatMessageStore = new(); - mockChatMessageStore.Setup(s => s.InvokingAsync( - It.IsAny(), - It.IsAny())).ReturnsAsync([new ChatMessage(ChatRole.User, "Existing Chat History")]); - mockChatMessageStore.Setup(s => s.InvokedAsync( - It.IsAny(), + Mock mockChatHistoryProvider = new(); + mockChatHistoryProvider.Setup(s => s.InvokingAsync( + It.IsAny(), + It.IsAny())).ReturnsAsync(new AIContext() { Messages = [new ChatMessage(ChatRole.User, "Existing Chat History")] }); + mockChatHistoryProvider.Setup(s => s.InvokedAsync( + It.IsAny(), It.IsAny())).Returns(new ValueTask()); - Mock>> mockFactory = new(); - mockFactory.Setup(f => f(It.IsAny(), It.IsAny())).ReturnsAsync(mockChatMessageStore.Object); + Mock>> mockFactory = new(); + mockFactory.Setup(f => f(It.IsAny(), It.IsAny())).ReturnsAsync(mockChatHistoryProvider.Object); ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" }, - ChatMessageStoreFactory = mockFactory.Object + ChatHistoryProviderFactory = mockFactory.Object }); // Act @@ -523,29 +523,29 @@ public async Task RunAsyncUsesChatMessageStoreFactoryWhenProvidedAndNoConversati await agent.RunAsync([new(ChatRole.User, "test")], thread); // Assert - Assert.IsType(thread!.MessageStore, exactMatch: false); + Assert.IsType(thread!.ChatHistoryProvider, exactMatch: false); mockService.Verify( x => x.GetResponseAsync( It.Is>(msgs => msgs.Count() == 2 && msgs.Any(m => m.Text == "Existing Chat History") && msgs.Any(m => m.Text == "test")), It.IsAny(), It.IsAny()), Times.Once); - mockChatMessageStore.Verify(s => s.InvokingAsync( - It.Is(x => x.RequestMessages.Count() == 1), + mockChatHistoryProvider.Verify(s => s.InvokingAsync( + It.Is(x => x.RequestMessages.Count() == 1), It.IsAny()), Times.Once); - mockChatMessageStore.Verify(s => s.InvokedAsync( - It.Is(x => x.RequestMessages.Count() == 1 && x.ChatMessageStoreMessages.Count() == 1 && x.ResponseMessages!.Count() == 1), + mockChatHistoryProvider.Verify(s => s.InvokedAsync( + It.Is(x => x.RequestMessages.Count() == 1 && x.ChatHistoryMessages!.Count() == 1 && x.ResponseMessages!.Count() == 1), It.IsAny()), Times.Once); - mockFactory.Verify(f => f(It.IsAny(), It.IsAny()), Times.Once); + mockFactory.Verify(f => f(It.IsAny(), It.IsAny()), Times.Once); } /// - /// Verify that RunAsync notifies the ChatMessageStore on failure. + /// Verify that RunAsync notifies the ChatHistoryProvider on failure. /// [Fact] - public async Task RunAsyncNotifiesChatMessageStoreOnFailureAsync() + public async Task RunAsyncNotifiesChatHistoryProviderOnFailureAsync() { // Arrange Mock mockService = new(); @@ -555,15 +555,18 @@ public async Task RunAsyncNotifiesChatMessageStoreOnFailureAsync() It.IsAny(), It.IsAny())).Throws(new InvalidOperationException("Test Error")); - Mock mockChatMessageStore = new(); + Mock mockChatHistoryProvider = new(); + mockChatHistoryProvider.Setup(s => s.InvokingAsync( + It.IsAny(), + It.IsAny())).ReturnsAsync(new AIContext() { Messages = [new ChatMessage(ChatRole.User, "Existing Chat History")] }); - Mock>> mockFactory = new(); - mockFactory.Setup(f => f(It.IsAny(), It.IsAny())).ReturnsAsync(mockChatMessageStore.Object); + Mock>> mockFactory = new(); + mockFactory.Setup(f => f(It.IsAny(), It.IsAny())).ReturnsAsync(mockChatHistoryProvider.Object); ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" }, - ChatMessageStoreFactory = mockFactory.Object + ChatHistoryProviderFactory = mockFactory.Object }); // Act @@ -571,19 +574,19 @@ public async Task RunAsyncNotifiesChatMessageStoreOnFailureAsync() await Assert.ThrowsAsync(() => agent.RunAsync([new(ChatRole.User, "test")], thread)); // Assert - Assert.IsType(thread!.MessageStore, exactMatch: false); - mockChatMessageStore.Verify(s => s.InvokedAsync( - It.Is(x => x.RequestMessages.Count() == 1 && x.ResponseMessages == null && x.InvokeException!.Message == "Test Error"), + Assert.IsType(thread!.ChatHistoryProvider, exactMatch: false); + mockChatHistoryProvider.Verify(s => s.InvokedAsync( + It.Is(x => x.RequestMessages.Count() == 1 && x.ResponseMessages == null && x.InvokeException!.Message == "Test Error"), It.IsAny()), Times.Once); - mockFactory.Verify(f => f(It.IsAny(), It.IsAny()), Times.Once); + mockFactory.Verify(f => f(It.IsAny(), It.IsAny()), Times.Once); } /// - /// Verify that RunAsync throws when a ChatMessageStore Factory is provided and the chat client returns a conversation id. + /// Verify that RunAsync throws when a ChatHistoryProviderFactory is provided and the chat client returns a conversation id. /// [Fact] - public async Task RunAsyncThrowsWhenChatMessageStoreFactoryProvidedAndConversationIdReturnedByChatClientAsync() + public async Task RunAsyncThrowsWhenChatHistoryProviderFactoryProvidedAndConversationIdReturnedByChatClientAsync() { // Arrange Mock mockService = new(); @@ -592,18 +595,18 @@ public async Task RunAsyncThrowsWhenChatMessageStoreFactoryProvidedAndConversati It.IsAny>(), It.IsAny(), It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" }); - Mock>> mockFactory = new(); - mockFactory.Setup(f => f(It.IsAny(), It.IsAny())).ReturnsAsync(new InMemoryChatMessageStore()); + Mock>> mockFactory = new(); + mockFactory.Setup(f => f(It.IsAny(), It.IsAny())).ReturnsAsync(new InMemoryChatHistoryProvider()); ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" }, - ChatMessageStoreFactory = mockFactory.Object + ChatHistoryProviderFactory = mockFactory.Object }); // Act & Assert ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread; var exception = await Assert.ThrowsAsync(() => agent.RunAsync([new(ChatRole.User, "test")], thread)); - Assert.Equal("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.", exception.Message); + Assert.Equal("Only the ConversationId or ChatHistoryProvider may be set, but not both and switching from one to another is not supported.", exception.Message); } /// @@ -668,11 +671,11 @@ public async Task RunAsyncInvokesAIContextProviderAndUsesResultAsync() Assert.Contains(capturedTools, t => t.Name == "context provider function"); // Verify that the thread was updated with the ai context provider, input and response messages - var messageStore = Assert.IsType(thread!.MessageStore); - Assert.Equal(3, messageStore.Count); - Assert.Equal("user message", messageStore[0].Text); - Assert.Equal("context provider message", messageStore[1].Text); - Assert.Equal("response", messageStore[2].Text); + var historyProvider = Assert.IsType(thread!.ChatHistoryProvider); + Assert.Equal(3, historyProvider.Count); + Assert.Equal("user message", historyProvider[0].Text); + Assert.Equal("context provider message", historyProvider[1].Text); + Assert.Equal("response", historyProvider[2].Text); mockProvider.Verify(p => p.InvokingAsync(It.IsAny(), It.IsAny()), Times.Once); mockProvider.Verify(p => p.InvokedAsync(It.Is(x => @@ -1566,10 +1569,10 @@ public async Task VerifyChatClientAgentStreamingAsync() } /// - /// Verify that RunStreamingAsync uses the ChatMessageStore factory when the chat client returns no conversation id. + /// Verify that RunStreamingAsync uses the ChatHistoryProviderfactory when the chat client returns no conversation id. /// [Fact] - public async Task RunStreamingAsyncUsesChatMessageStoreWhenNoConversationIdReturnedByChatClientAsync() + public async Task RunStreamingAsyncUsesChatHistoryProviderWhenNoConversationIdReturnedByChatClientAsync() { // Arrange Mock mockService = new(); @@ -1583,12 +1586,12 @@ public async Task RunStreamingAsyncUsesChatMessageStoreWhenNoConversationIdRetur It.IsAny>(), It.IsAny(), It.IsAny())).Returns(ToAsyncEnumerableAsync(returnUpdates)); - Mock>> mockFactory = new(); - mockFactory.Setup(f => f(It.IsAny(), It.IsAny())).ReturnsAsync(new InMemoryChatMessageStore()); + Mock>> mockFactory = new(); + mockFactory.Setup(f => f(It.IsAny(), It.IsAny())).ReturnsAsync(new InMemoryChatHistoryProvider()); ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" }, - ChatMessageStoreFactory = mockFactory.Object + ChatHistoryProviderFactory = mockFactory.Object }); // Act @@ -1596,18 +1599,18 @@ public async Task RunStreamingAsyncUsesChatMessageStoreWhenNoConversationIdRetur await agent.RunStreamingAsync([new(ChatRole.User, "test")], thread).ToListAsync(); // Assert - var messageStore = Assert.IsType(thread!.MessageStore); - Assert.Equal(2, messageStore.Count); - Assert.Equal("test", messageStore[0].Text); - Assert.Equal("what?", messageStore[1].Text); - mockFactory.Verify(f => f(It.IsAny(), It.IsAny()), Times.Once); + var historyProvider = Assert.IsType(thread!.ChatHistoryProvider); + Assert.Equal(2, historyProvider.Count); + Assert.Equal("test", historyProvider[0].Text); + Assert.Equal("what?", historyProvider[1].Text); + mockFactory.Verify(f => f(It.IsAny(), It.IsAny()), Times.Once); } /// - /// Verify that RunStreamingAsync throws when a ChatMessageStore factory is provided and the chat client returns a conversation id. + /// Verify that RunStreamingAsync throws when a ChatHistoryProvider factory is provided and the chat client returns a conversation id. /// [Fact] - public async Task RunStreamingAsyncThrowsWhenChatMessageStoreFactoryProvidedAndConversationIdReturnedByChatClientAsync() + public async Task RunStreamingAsyncThrowsWhenChatHistoryProviderFactoryProvidedAndConversationIdReturnedByChatClientAsync() { // Arrange Mock mockService = new(); @@ -1621,18 +1624,18 @@ public async Task RunStreamingAsyncThrowsWhenChatMessageStoreFactoryProvidedAndC It.IsAny>(), It.IsAny(), It.IsAny())).Returns(ToAsyncEnumerableAsync(returnUpdates)); - Mock>> mockFactory = new(); - mockFactory.Setup(f => f(It.IsAny(), It.IsAny())).ReturnsAsync(new InMemoryChatMessageStore()); + Mock>> mockFactory = new(); + mockFactory.Setup(f => f(It.IsAny(), It.IsAny())).ReturnsAsync(new InMemoryChatHistoryProvider()); ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" }, - ChatMessageStoreFactory = mockFactory.Object + ChatHistoryProviderFactory = mockFactory.Object }); // Act & Assert ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread; var exception = await Assert.ThrowsAsync(async () => await agent.RunStreamingAsync([new(ChatRole.User, "test")], thread).ToListAsync()); - Assert.Equal("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.", exception.Message); + Assert.Equal("Only the ConversationId or ChatHistoryProvider may be set, but not both and switching from one to another is not supported.", exception.Message); } /// @@ -1704,11 +1707,11 @@ public async Task RunStreamingAsyncInvokesAIContextProviderAndUsesResultAsync() Assert.Contains(capturedTools, t => t.Name == "context provider function"); // Verify that the thread was updated with the input, ai context provider, and response messages - var messageStore = Assert.IsType(thread!.MessageStore); - Assert.Equal(3, messageStore.Count); - Assert.Equal("user message", messageStore[0].Text); - Assert.Equal("context provider message", messageStore[1].Text); - Assert.Equal("response", messageStore[2].Text); + var historyProvider = Assert.IsType(thread!.ChatHistoryProvider); + Assert.Equal(3, historyProvider.Count); + Assert.Equal("user message", historyProvider[0].Text); + Assert.Equal("context provider message", historyProvider[1].Text); + Assert.Equal("response", historyProvider[2].Text); mockProvider.Verify(p => p.InvokingAsync(It.IsAny(), It.IsAny()), Times.Once); mockProvider.Verify(p => p.InvokedAsync(It.Is(x => diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentThreadTests.cs index 57af3b6449..29d46623e6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentThreadTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentThreadTests.cs @@ -24,7 +24,7 @@ public void ConstructorSetsDefaults() // Assert Assert.Null(thread.ConversationId); - Assert.Null(thread.MessageStore); + Assert.Null(thread.ChatHistoryProvider); } [Fact] @@ -39,52 +39,52 @@ public void SetConversationIdRoundtrips() // Assert Assert.Equal(ConversationId, thread.ConversationId); - Assert.Null(thread.MessageStore); + Assert.Null(thread.ChatHistoryProvider); } [Fact] - public void SetChatMessageStoreRoundtrips() + public void SetChatHistoryProviderRoundtrips() { // Arrange var thread = new ChatClientAgentThread(); - var messageStore = new InMemoryChatMessageStore(); + var historyProvider = new InMemoryChatHistoryProvider(); // Act - thread.MessageStore = messageStore; + thread.ChatHistoryProvider = historyProvider; // Assert - Assert.Same(messageStore, thread.MessageStore); + Assert.Same(historyProvider, thread.ChatHistoryProvider); Assert.Null(thread.ConversationId); } [Fact] - public void SetConversationIdThrowsWhenMessageStoreIsSet() + public void SetConversationIdThrowsWhenChatHistoryProviderIsSet() { // Arrange var thread = new ChatClientAgentThread { - MessageStore = new InMemoryChatMessageStore() + ChatHistoryProvider = new InMemoryChatHistoryProvider() }; // Act & Assert var exception = Assert.Throws(() => thread.ConversationId = "new-thread-id"); - Assert.Equal("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.", exception.Message); - Assert.NotNull(thread.MessageStore); + Assert.Equal("Only the ConversationId or ChatHistoryProvider may be set, but not both and switching from one to another is not supported.", exception.Message); + Assert.NotNull(thread.ChatHistoryProvider); } [Fact] - public void SetChatMessageStoreThrowsWhenConversationIdIsSet() + public void SetChatHistoryProviderThrowsWhenConversationIdIsSet() { // Arrange var thread = new ChatClientAgentThread { ConversationId = "existing-thread-id" }; - var store = new InMemoryChatMessageStore(); + var store = new InMemoryChatHistoryProvider(); // Act & Assert - var exception = Assert.Throws(() => thread.MessageStore = store); - Assert.Equal("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.", exception.Message); + var exception = Assert.Throws(() => thread.ChatHistoryProvider = store); + Assert.Equal("Only the ConversationId or ChatHistoryProvider may be set, but not both and switching from one to another is not supported.", exception.Message); Assert.NotNull(thread.ConversationId); } @@ -98,7 +98,7 @@ public async Task VerifyDeserializeWithMessagesAsync() // Arrange var json = JsonSerializer.Deserialize(""" { - "storeState": { "messages": [{"authorName": "testAuthor"}] } + "chatHistoryProviderState": { "messages": [{"authorName": "testAuthor"}] } } """, TestJsonSerializerContext.Default.JsonElement); @@ -108,10 +108,10 @@ public async Task VerifyDeserializeWithMessagesAsync() // Assert Assert.Null(thread.ConversationId); - var messageStore = thread.MessageStore as InMemoryChatMessageStore; - Assert.NotNull(messageStore); - Assert.Single(messageStore); - Assert.Equal("testAuthor", messageStore[0].AuthorName); + var historyProvider = thread.ChatHistoryProvider as InMemoryChatHistoryProvider; + Assert.NotNull(historyProvider); + Assert.Single(historyProvider); + Assert.Equal("testAuthor", historyProvider[0].AuthorName); } [Fact] @@ -129,7 +129,7 @@ public async Task VerifyDeserializeWithIdAsync() // Assert Assert.Equal("TestConvId", thread.ConversationId); - Assert.Null(thread.MessageStore); + Assert.Null(thread.ChatHistoryProvider); } [Fact] @@ -148,7 +148,7 @@ public async Task VerifyDeserializeWithAIContextProviderAsync() var thread = await ChatClientAgentThread.DeserializeAsync(json, aiContextProviderFactory: (_, _, _) => new(mockProvider.Object)); // Assert - Assert.Null(thread.MessageStore); + Assert.Null(thread.ChatHistoryProvider); Assert.Same(thread.AIContextProvider, mockProvider.Object); } @@ -195,8 +195,8 @@ public void VerifyThreadSerializationWithId() public void VerifyThreadSerializationWithMessages() { // Arrange - InMemoryChatMessageStore store = [new(ChatRole.User, "TestContent") { AuthorName = "TestAuthor" }]; - var thread = new ChatClientAgentThread { MessageStore = store }; + InMemoryChatHistoryProvider store = [new(ChatRole.User, "TestContent") { AuthorName = "TestAuthor" }]; + var thread = new ChatClientAgentThread { ChatHistoryProvider = store }; // Act var json = thread.Serialize(); @@ -206,10 +206,10 @@ public void VerifyThreadSerializationWithMessages() Assert.False(json.TryGetProperty("conversationId", out _)); - Assert.True(json.TryGetProperty("storeState", out var storeStateProperty)); - Assert.Equal(JsonValueKind.Object, storeStateProperty.ValueKind); + Assert.True(json.TryGetProperty("chatHistoryProviderState", out var chatHistoryProviderStateProperty)); + Assert.Equal(JsonValueKind.Object, chatHistoryProviderStateProperty.ValueKind); - Assert.True(storeStateProperty.TryGetProperty("messages", out var messagesProperty)); + Assert.True(chatHistoryProviderStateProperty.TryGetProperty("messages", out var messagesProperty)); Assert.Equal(JsonValueKind.Array, messagesProperty.ValueKind); Assert.Single(messagesProperty.EnumerateArray()); @@ -264,11 +264,11 @@ public void VerifyThreadSerializationWithCustomOptions() new Dictionary { ["Key"] = "TestValue" }, TestJsonSerializerContext.Default.DictionaryStringObject); - var messageStoreMock = new Mock(); - messageStoreMock + var historyProviderMock = new Mock(); + historyProviderMock .Setup(m => m.Serialize(options)) .Returns(storeStateElement); - thread.MessageStore = messageStoreMock.Object; + thread.ChatHistoryProvider = historyProviderMock.Object; // Act var json = thread.Serialize(options); @@ -278,13 +278,13 @@ public void VerifyThreadSerializationWithCustomOptions() Assert.False(json.TryGetProperty("conversationId", out var idProperty)); - Assert.True(json.TryGetProperty("storeState", out var storeStateProperty)); - Assert.Equal(JsonValueKind.Object, storeStateProperty.ValueKind); + Assert.True(json.TryGetProperty("chatHistoryProviderState", out var chatHistoryProviderStateProperty)); + Assert.Equal(JsonValueKind.Object, chatHistoryProviderStateProperty.ValueKind); - Assert.True(storeStateProperty.TryGetProperty("Key", out var keyProperty)); + Assert.True(chatHistoryProviderStateProperty.TryGetProperty("Key", out var keyProperty)); Assert.Equal("TestValue", keyProperty.GetString()); - messageStoreMock.Verify(m => m.Serialize(options), Times.Once); + historyProviderMock.Verify(m => m.Serialize(options), Times.Once); } #endregion Serialize Tests @@ -311,19 +311,19 @@ public void GetService_RequestingAIContextProvider_ReturnsAIContextProvider() } [Fact] - public void GetService_RequestingChatMessageStore_ReturnsChatMessageStore() + public void GetService_RequestingChatHistoryProvider_ReturnsChatHistoryProvider() { // Arrange var thread = new ChatClientAgentThread(); - var messageStore = new InMemoryChatMessageStore(); - thread.MessageStore = messageStore; + var historyProvider = new InMemoryChatHistoryProvider(); + thread.ChatHistoryProvider = historyProvider; // Act - var result = thread.GetService(typeof(ChatMessageStore)); + var result = thread.GetService(typeof(InMemoryChatHistoryProvider)); // Assert Assert.NotNull(result); - Assert.Same(messageStore, result); + Assert.Same(historyProvider, result); } #endregion diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_BackgroundResponsesTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_BackgroundResponsesTests.cs index 79af3add1d..135a8e76df 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_BackgroundResponsesTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_BackgroundResponsesTests.cs @@ -336,11 +336,11 @@ public async Task RunAsync_WhenContinuationTokenProvided_SkipsThreadMessagePopul // Arrange List capturedMessages = []; - // Create a mock message store that would normally provide messages - var mockMessageStore = new Mock(); - mockMessageStore - .Setup(ms => ms.InvokingAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync([new(ChatRole.User, "Message from message store")]); + // Create a mock chat history provider that would normally provide messages + var mockChatHistoryProvider = new Mock(); + mockChatHistoryProvider + .Setup(ms => ms.InvokingAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new AIContext() { Messages = [new(ChatRole.User, "Message from chat history provider")] }); // Create a mock AI context provider that would normally provide context var mockContextProvider = new Mock(); @@ -364,10 +364,10 @@ public async Task RunAsync_WhenContinuationTokenProvided_SkipsThreadMessagePopul ChatClientAgent agent = new(mockChatClient.Object); - // Create a thread with both message store and AI context provider + // Create a thread with both chat history provider and AI context provider ChatClientAgentThread thread = new() { - MessageStore = mockMessageStore.Object, + ChatHistoryProvider = mockChatHistoryProvider.Object, AIContextProvider = mockContextProvider.Object }; @@ -384,9 +384,9 @@ public async Task RunAsync_WhenContinuationTokenProvided_SkipsThreadMessagePopul // With continuation token, thread message population should be skipped Assert.Empty(capturedMessages); - // Verify that message store was never called due to continuation token - mockMessageStore.Verify( - ms => ms.InvokingAsync(It.IsAny(), It.IsAny()), + // Verify that chat history provider was never called due to continuation token + mockChatHistoryProvider.Verify( + ms => ms.InvokingAsync(It.IsAny(), It.IsAny()), Times.Never); // Verify that AI context provider was never called due to continuation token @@ -401,11 +401,11 @@ public async Task RunStreamingAsync_WhenContinuationTokenProvided_SkipsThreadMes // Arrange List capturedMessages = []; - // Create a mock message store that would normally provide messages - var mockMessageStore = new Mock(); - mockMessageStore - .Setup(ms => ms.InvokingAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync([new(ChatRole.User, "Message from message store")]); + // Create a mock chat history provider that would normally provide messages + var mockChatHistoryProvider = new Mock(); + mockChatHistoryProvider + .Setup(ms => ms.InvokingAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new AIContext() { Messages = [new(ChatRole.User, "Message from chat history provider")] }); // Create a mock AI context provider that would normally provide context var mockContextProvider = new Mock(); @@ -429,10 +429,10 @@ public async Task RunStreamingAsync_WhenContinuationTokenProvided_SkipsThreadMes ChatClientAgent agent = new(mockChatClient.Object); - // Create a thread with both message store and AI context provider + // Create a thread with both chat history provider and AI context provider ChatClientAgentThread thread = new() { - MessageStore = mockMessageStore.Object, + ChatHistoryProvider = mockChatHistoryProvider.Object, AIContextProvider = mockContextProvider.Object }; @@ -448,9 +448,9 @@ public async Task RunStreamingAsync_WhenContinuationTokenProvided_SkipsThreadMes // With continuation token, thread message population should be skipped Assert.Empty(capturedMessages); - // Verify that message store was never called due to continuation token - mockMessageStore.Verify( - ms => ms.InvokingAsync(It.IsAny(), It.IsAny()), + // Verify that chat history provider was never called due to continuation token + mockChatHistoryProvider.Verify( + ms => ms.InvokingAsync(It.IsAny(), It.IsAny()), Times.Never); // Verify that AI context provider was never called due to continuation token @@ -610,7 +610,7 @@ public async Task RunStreamingAsync_WhenResponseUpdatesPresentInContinuationToke } [Fact] - public async Task RunStreamingAsync_WhenResumingStreaming_UsesUpdatesFromInitialRunForContextProviderAndMessageStoreAsync() + public async Task RunStreamingAsync_WhenResumingStreaming_UsesUpdatesFromInitialRunForContextProviderAndChatHistoryProviderAsync() { // Arrange ChatResponseUpdate[] returnUpdates = @@ -631,10 +631,10 @@ public async Task RunStreamingAsync_WhenResumingStreaming_UsesUpdatesFromInitial ChatClientAgent agent = new(mockChatClient.Object); List capturedMessagesAddedToStore = []; - var mockMessageStore = new Mock(); - mockMessageStore - .Setup(ms => ms.InvokedAsync(It.IsAny(), It.IsAny())) - .Callback((ctx, ct) => capturedMessagesAddedToStore.AddRange(ctx.ResponseMessages ?? [])) + var mockChatHistoryProvider = new Mock(); + mockChatHistoryProvider + .Setup(ms => ms.InvokedAsync(It.IsAny(), It.IsAny())) + .Callback((ctx, ct) => capturedMessagesAddedToStore.AddRange(ctx.ResponseMessages ?? [])) .Returns(new ValueTask()); AIContextProvider.InvokedContext? capturedInvokedContext = null; @@ -646,7 +646,7 @@ public async Task RunStreamingAsync_WhenResumingStreaming_UsesUpdatesFromInitial ChatClientAgentThread thread = new() { - MessageStore = mockMessageStore.Object, + ChatHistoryProvider = mockChatHistoryProvider.Object, AIContextProvider = mockContextProvider.Object }; @@ -662,7 +662,7 @@ public async Task RunStreamingAsync_WhenResumingStreaming_UsesUpdatesFromInitial await agent.RunStreamingAsync(thread, options: runOptions).ToListAsync(); // Assert - mockMessageStore.Verify(ms => ms.InvokedAsync(It.IsAny(), It.IsAny()), Times.Once); + mockChatHistoryProvider.Verify(ms => ms.InvokedAsync(It.IsAny(), It.IsAny()), Times.Once); Assert.Single(capturedMessagesAddedToStore); Assert.Contains("once upon a time", capturedMessagesAddedToStore[0].Text); @@ -673,7 +673,7 @@ public async Task RunStreamingAsync_WhenResumingStreaming_UsesUpdatesFromInitial } [Fact] - public async Task RunStreamingAsync_WhenResumingStreaming_UsesInputMessagesFromInitialRunForContextProviderAndMessageStoreAsync() + public async Task RunStreamingAsync_WhenResumingStreaming_UsesInputMessagesFromInitialRunForContextProviderAndChatHistoryProviderAsync() { // Arrange Mock mockChatClient = new(); @@ -687,10 +687,10 @@ public async Task RunStreamingAsync_WhenResumingStreaming_UsesInputMessagesFromI ChatClientAgent agent = new(mockChatClient.Object); List capturedMessagesAddedToStore = []; - var mockMessageStore = new Mock(); - mockMessageStore - .Setup(ms => ms.InvokedAsync(It.IsAny(), It.IsAny())) - .Callback((ctx, ct) => capturedMessagesAddedToStore.AddRange(ctx.RequestMessages)) + var mockChatHistoryProvider = new Mock(); + mockChatHistoryProvider + .Setup(ms => ms.InvokedAsync(It.IsAny(), It.IsAny())) + .Callback((ctx, ct) => capturedMessagesAddedToStore.AddRange(ctx.RequestMessages)) .Returns(new ValueTask()); AIContextProvider.InvokedContext? capturedInvokedContext = null; @@ -702,7 +702,7 @@ public async Task RunStreamingAsync_WhenResumingStreaming_UsesInputMessagesFromI ChatClientAgentThread thread = new() { - MessageStore = mockMessageStore.Object, + ChatHistoryProvider = mockChatHistoryProvider.Object, AIContextProvider = mockContextProvider.Object }; @@ -718,7 +718,7 @@ public async Task RunStreamingAsync_WhenResumingStreaming_UsesInputMessagesFromI await agent.RunStreamingAsync(thread, options: runOptions).ToListAsync(); // Assert - mockMessageStore.Verify(ms => ms.InvokedAsync(It.IsAny(), It.IsAny()), Times.Once); + mockChatHistoryProvider.Verify(ms => ms.InvokedAsync(It.IsAny(), It.IsAny()), Times.Once); Assert.Single(capturedMessagesAddedToStore); Assert.Contains("Tell me a story", capturedMessagesAddedToStore[0].Text); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_DeserializeThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_DeserializeThreadTests.cs index 98e5b0ed1a..73281113a0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_DeserializeThreadTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_DeserializeThreadTests.cs @@ -46,19 +46,19 @@ public async Task DeserializeThread_UsesAIContextProviderFactory_IfProvidedAsync } [Fact] - public async Task DeserializeThread_UsesChatMessageStoreFactory_IfProvidedAsync() + public async Task DeserializeThread_UsesChatHistoryProviderFactory_IfProvidedAsync() { // Arrange var mockChatClient = new Mock(); - var mockMessageStore = new Mock(); + var mockHistoryProvider = new Mock(); var factoryCalled = false; var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions { ChatOptions = new() { Instructions = "Test instructions" }, - ChatMessageStoreFactory = (_, _) => + ChatHistoryProviderFactory = (_, _) => { factoryCalled = true; - return new ValueTask(mockMessageStore.Object); + return new ValueTask(mockHistoryProvider.Object); } }); @@ -72,9 +72,9 @@ public async Task DeserializeThread_UsesChatMessageStoreFactory_IfProvidedAsync( var thread = await agent.DeserializeThreadAsync(json); // Assert - Assert.True(factoryCalled, "ChatMessageStoreFactory was not called."); + Assert.True(factoryCalled, "ChatHistoryProviderFactory was not called."); Assert.IsType(thread); var typedThread = (ChatClientAgentThread)thread; - Assert.Same(mockMessageStore.Object, typedThread.MessageStore); + Assert.Same(mockHistoryProvider.Object, typedThread.ChatHistoryProvider); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_GetNewThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_GetNewThreadTests.cs index e6cc7e90e9..d2c3a254ad 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_GetNewThreadTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_GetNewThreadTests.cs @@ -39,19 +39,19 @@ public async Task GetNewThread_UsesAIContextProviderFactory_IfProvidedAsync() } [Fact] - public async Task GetNewThread_UsesChatMessageStoreFactory_IfProvidedAsync() + public async Task GetNewThread_UsesChatHistoryProviderFactory_IfProvidedAsync() { // Arrange var mockChatClient = new Mock(); - var mockMessageStore = new Mock(); + var mockHistoryProvider = new Mock(); var factoryCalled = false; var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions { ChatOptions = new() { Instructions = "Test instructions" }, - ChatMessageStoreFactory = (_, _) => + ChatHistoryProviderFactory = (_, _) => { factoryCalled = true; - return new ValueTask(mockMessageStore.Object); + return new ValueTask(mockHistoryProvider.Object); } }); @@ -59,27 +59,27 @@ public async Task GetNewThread_UsesChatMessageStoreFactory_IfProvidedAsync() var thread = await agent.GetNewThreadAsync(); // Assert - Assert.True(factoryCalled, "ChatMessageStoreFactory was not called."); + Assert.True(factoryCalled, "ChatHistoryProviderFactory was not called."); Assert.IsType(thread); var typedThread = (ChatClientAgentThread)thread; - Assert.Same(mockMessageStore.Object, typedThread.MessageStore); + Assert.Same(mockHistoryProvider.Object, typedThread.ChatHistoryProvider); } [Fact] - public async Task GetNewThread_UsesChatMessageStore_FromTypedOverloadAsync() + public async Task GetNewThread_UsesChatHistoryProvider_FromTypedOverloadAsync() { // Arrange var mockChatClient = new Mock(); - var mockMessageStore = new Mock(); + var mockHistoryProvider = new Mock(); var agent = new ChatClientAgent(mockChatClient.Object); // Act - var thread = await agent.GetNewThreadAsync(mockMessageStore.Object); + var thread = await agent.GetNewThreadAsync(mockHistoryProvider.Object); // Assert Assert.IsType(thread); var typedThread = (ChatClientAgentThread)thread; - Assert.Same(mockMessageStore.Object, typedThread.MessageStore); + Assert.Same(mockHistoryProvider.Object, typedThread.ChatHistoryProvider); } [Fact] diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestEchoAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestEchoAgent.cs index b971736b74..cf63843def 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestEchoAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestEchoAgent.cs @@ -26,7 +26,7 @@ public override ValueTask GetNewThreadAsync(CancellationToken cance private static ChatMessage UpdateThread(ChatMessage message, InMemoryAgentThread? thread = null) { - thread?.MessageStore.Add(message); + thread?.ChatHistoryProvider.Add(message); return message; } diff --git a/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs index 0fb9745d2d..3cb69e6cc5 100644 --- a/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs +++ b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs @@ -32,12 +32,12 @@ public async Task> GetChatHistoryAsync(AgentThread thread) { var typedThread = (ChatClientAgentThread)thread; - if (typedThread.MessageStore is null) + if (typedThread.ChatHistoryProvider is null) { return []; } - return (await typedThread.MessageStore.InvokingAsync(new([]))).ToList(); + return (await typedThread.ChatHistoryProvider.InvokingAsync(new([]))).Messages!.ToList(); } public Task CreateChatClientAgentAsync( diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs index c57e1c460d..fbf06fdcfd 100644 --- a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs +++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs @@ -50,12 +50,12 @@ public async Task> GetChatHistoryAsync(AgentThread thread) return [.. previousMessages, responseMessage]; } - if (typedThread.MessageStore is null) + if (typedThread.ChatHistoryProvider is null) { return []; } - return (await typedThread.MessageStore.InvokingAsync(new([]))).ToList(); + return (await typedThread.ChatHistoryProvider.InvokingAsync(new([]))).Messages!.ToList(); } private static ChatMessage ConvertToChatMessage(ResponseItem item)