From e5d9d74c2da0302d5e2ca01e174a7b1cbe4c03e0 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 7 Nov 2025 11:46:35 +0000
Subject: [PATCH] .NET: Chathistory memory provider add (#1867)
* Add ChatHistoryMemoryProvider with unit tests
* Set new project to not packable.
* Fix bugs
* Add serialization support.
* Update dotnet/src/Microsoft.Agents.AI.VectorDataMemory/ChatHistoryMemoryProvider.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Remove unnecessary line
* Convert ChatHistoryMemoryProvider to use Dynamic collections.
* Sealing options and scope classes.
* Add sample, add scope to logs and improve scope validation
* Move ChatHistoryMemoryProvider to MAAI project.
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
dotnet/agent-framework-dotnet.slnx | 1 +
.../Agent_Step18_TextSearchRag/Program.cs | 2 +
...nt_Step21_ChatHistoryMemoryProvider.csproj | 23 +
.../Program.cs | 60 +++
.../Microsoft.Agents.AI.Mem0/Mem0Provider.cs | 8 +-
.../Microsoft.Agents.AI/AgentJsonUtilities.cs | 1 +
.../ChatClient/ChatClientAgentThread.cs | 4 +-
.../Memory/ChatHistoryMemoryProvider.cs | 483 ++++++++++++++++++
.../ChatHistoryMemoryProviderOptions.cs | 56 ++
.../Memory/ChatHistoryMemoryProviderScope.cs | 53 ++
.../Memory/ChatHistoryMemoryProviderTests.cs | 396 ++++++++++++++
11 files changed, 1081 insertions(+), 6 deletions(-)
create mode 100644 dotnet/samples/GettingStarted/Agents/Agent_Step21_ChatHistoryMemoryProvider/Agent_Step21_ChatHistoryMemoryProvider.csproj
create mode 100644 dotnet/samples/GettingStarted/Agents/Agent_Step21_ChatHistoryMemoryProvider/Program.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProviderOptions.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProviderScope.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 03f8a910d3..56211e457f 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -66,6 +66,7 @@
+
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs
index 65f3a9e98f..c56be11fc5 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs
@@ -4,6 +4,8 @@
// capabilities to an AI agent. The provider runs a search against an external knowledge base
// before each model invocation and injects the results into the model context.
+// Also see the AgentWithRAG folder for more advanced RAG scenarios.
+
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step21_ChatHistoryMemoryProvider/Agent_Step21_ChatHistoryMemoryProvider.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step21_ChatHistoryMemoryProvider/Agent_Step21_ChatHistoryMemoryProvider.csproj
new file mode 100644
index 0000000000..1caf270c49
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step21_ChatHistoryMemoryProvider/Agent_Step21_ChatHistoryMemoryProvider.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net9.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step21_ChatHistoryMemoryProvider/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step21_ChatHistoryMemoryProvider/Program.cs
new file mode 100644
index 0000000000..9e4c27cebb
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step21_ChatHistoryMemoryProvider/Program.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to create and use a simple AI agent that stores chat messages in a vector store using the ChatHistoryMemoryProvider.
+// It can then use the chat history from prior conversations to inform responses in new conversations.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.VectorData;
+using Microsoft.SemanticKernel.Connectors.InMemory;
+using OpenAI;
+
+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";
+var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
+
+// Create a vector store to store the chat messages in.
+// For demonstration purposes, we are using an in-memory vector store.
+// Replace this with a vector store implementation of your choice that can persist the chat history long term.
+VectorStore vectorStore = new InMemoryVectorStore(new InMemoryVectorStoreOptions()
+{
+ EmbeddingGenerator = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
+ .GetEmbeddingClient(embeddingDeploymentName)
+ .AsIEmbeddingGenerator()
+});
+
+// Create the agent and add the ChatHistoryMemoryProvider to store chat messages in the vector store.
+AIAgent agent = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(new ChatClientAgentOptions
+ {
+ Instructions = "You are good at telling jokes.",
+ Name = "Joker",
+ AIContextProviderFactory = (ctx) => new ChatHistoryMemoryProvider(
+ vectorStore,
+ collectionName: "chathistory",
+ vectorDimensions: 3072,
+ // Configure the scope values under which chat messages will be stored.
+ // In this case, we are using a fixed user ID and a unique thread ID for each new thread.
+ storageScope: new() { UserId = "UID1", ThreadId = new Guid().ToString() },
+ // Configure the scope which would be used to search for relevant prior messages.
+ // In this case, we are searching for any messages for the user across all threads.
+ searchScope: new() { UserId = "UID1" })
+ });
+
+// Start a new thread for the agent conversation.
+AgentThread thread = agent.GetNewThread();
+
+// Run the agent with the thread that stores conversation history in the vector store.
+Console.WriteLine(await agent.RunAsync("I like jokes about Pirates. Tell me a joke about a pirate.", thread));
+
+// Start a second thread. Since we configured the search scope to be across all threads for the user,
+// the agent should remember that the user likes pirate jokes.
+AgentThread thread2 = agent.GetNewThread();
+
+// Run the agent with the second thread.
+Console.WriteLine(await agent.RunAsync("Tell me a joke that I might like.", thread2));
diff --git a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs
index 4aae5de59b..d18ed2b460 100644
--- a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs
@@ -153,7 +153,7 @@ public sealed class Mem0Provider : AIContextProvider
if (this._logger is not null)
{
this._logger.LogInformation(
- "Mem0AIContextProvider: Retrieved {Count} memories. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'",
+ "Mem0AIContextProvider: Retrieved {Count} memories. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
memories.Count,
this._searchScope.ApplicationId,
this._searchScope.AgentId,
@@ -162,7 +162,7 @@ public sealed class Mem0Provider : AIContextProvider
if (outputMessageText is not null)
{
this._logger.LogTrace(
- "Mem0AIContextProvider: Search Results\nInput:{Input}\nOutput:{MessageText}\nApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'",
+ "Mem0AIContextProvider: Search Results\nInput:{Input}\nOutput:{MessageText}\nApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
queryText,
outputMessageText,
this._searchScope.ApplicationId,
@@ -185,7 +185,7 @@ public sealed class Mem0Provider : AIContextProvider
{
this._logger?.LogError(
ex,
- "Mem0AIContextProvider: Failed to search Mem0 for memories due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'",
+ "Mem0AIContextProvider: Failed to search Mem0 for memories due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
this._searchScope.ApplicationId,
this._searchScope.AgentId,
this._searchScope.ThreadId,
@@ -211,7 +211,7 @@ public sealed class Mem0Provider : AIContextProvider
{
this._logger?.LogError(
ex,
- "Mem0AIContextProvider: Failed to send messages to Mem0 due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'",
+ "Mem0AIContextProvider: Failed to send messages to Mem0 due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
this._storageScope.ApplicationId,
this._storageScope.AgentId,
this._storageScope.ThreadId,
diff --git a/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs
index 36bef4a2af..c400a1cb6c 100644
--- a/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs
+++ b/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs
@@ -68,6 +68,7 @@ internal static partial class AgentJsonUtilities
// Agent abstraction types
[JsonSerializable(typeof(ChatClientAgentThread.ThreadState))]
[JsonSerializable(typeof(TextSearchProvider.TextSearchProviderState))]
+ [JsonSerializable(typeof(ChatHistoryMemoryProvider.ChatHistoryMemoryProviderState))]
[ExcludeFromCodeCoverage]
internal sealed partial class JsonContext : JsonSerializerContext;
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs
index baa36c0054..ad224e6777 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs
@@ -166,8 +166,8 @@ public class ChatClientAgentThread : AgentThread
var state = new ThreadState
{
ConversationId = this.ConversationId,
- StoreState = storeState,
- AIContextProviderState = aiContextProviderState
+ StoreState = storeState is { ValueKind: not JsonValueKind.Undefined } ? storeState : null,
+ AIContextProviderState = aiContextProviderState is { ValueKind: not JsonValueKind.Undefined } ? aiContextProviderState : null,
};
return JsonSerializer.SerializeToElement(state, AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ThreadState)));
diff --git a/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs
new file mode 100644
index 0000000000..6d90c877e8
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs
@@ -0,0 +1,483 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.VectorData;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// A context provider that stores all chat history in a vector store and is able to
+/// retrieve related chat history later to augment the current conversation.
+///
+///
+///
+/// This provider stores chat messages in a vector store and retrieves relevant previous messages
+/// to provide as context during agent invocations. It uses the VectorStore and VectorStoreCollection
+/// abstractions to work with any compatible vector store implementation.
+///
+///
+/// Messages are stored during the method and retrieved during the
+/// method using semantic similarity search.
+///
+///
+/// Behavior is configurable through . When
+/// is selected the provider
+/// exposes a function tool that the model can invoke to retrieve relevant memories on demand instead of
+/// injecting them automatically on each invocation.
+///
+///
+public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
+{
+ private const string DefaultContextPrompt = "## Memories\nConsider the following memories when answering user questions:";
+ private const int DefaultMaxResults = 3;
+ private const string DefaultFunctionToolName = "Search";
+ private const string DefaultFunctionToolDescription = "Allows searching for related previous chat history to help answer the user question.";
+
+ private readonly VectorStore _vectorStore;
+ private readonly VectorStoreCollection