Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
fc68839
Add memory projects with common abstractions and mem zero implementation
westey-m Mar 28, 2025
f0d6dbf
Add user preferences component
westey-m Mar 28, 2025
6e926e8
Update AgentThread and ChatCompletionAgent to support thread extensions
westey-m Mar 28, 2025
c13a3c1
Add memory support to open ai assistant agent
westey-m Mar 28, 2025
c28c0ae
Add vector data memory store.
westey-m Apr 7, 2025
a70282e
Merge commit
westey-m Apr 8, 2025
27cb090
Fix bug in VectorDataTextMemoryStore and add integration test with ve…
westey-m Apr 9, 2025
0169f6e
Rename extension, add experimental attributes and mark memory package…
westey-m Apr 9, 2025
a219464
Move extension classes into SK abstractions
westey-m Apr 9, 2025
5f29283
Make some tweaks to mem0 and text memory store
westey-m Apr 11, 2025
33fcb06
Add TextRagComponent, TextRagStore and test to use it.
westey-m Apr 11, 2025
059607c
Add comments to tests.
westey-m Apr 11, 2025
9e1c57e
Rename user preferences to user facts and fix a bug
westey-m Apr 11, 2025
7150cac
Add agents with memory ADR
westey-m Apr 11, 2025
09e3e76
Split mem0 into client and component, test against service and add in…
westey-m Apr 14, 2025
d89e058
Add support for suspend and resume
westey-m Apr 14, 2025
4dccaa7
Add support for loading extensions from DI.
westey-m Apr 14, 2025
d78f987
Simplify suspend/resume
westey-m Apr 14, 2025
d60aa00
Update ADR with onsuspend and onresume and decisions to make list.
westey-m Apr 14, 2025
699d7ac
Allow RAG via plugin for TextRagComponent
westey-m Apr 14, 2025
50669ac
Merge branch 'main' into agent-memory-explore-v2
westey-m Apr 16, 2025
32562c2
Update ConversationStateExtensions to use MEAI types.
westey-m Apr 17, 2025
059e4b4
Fix typos
westey-m Apr 17, 2025
8ad185c
Fix typos
westey-m Apr 17, 2025
9e488a9
Fix experimental flags
westey-m Apr 17, 2025
61dba2f
Fix warning about preview dependency.
westey-m Apr 17, 2025
267b85c
Merge branch 'main' into agent-memory-explore-v2
westey-m Apr 17, 2025
018c821
Rename ThreadExtensionsManager to StateExtensions
westey-m Apr 17, 2025
081472c
Update ADR header
westey-m Apr 17, 2025
3a48d3f
Add unit tests, fix bugs, rename long props. Seal manager
westey-m Apr 17, 2025
e53fbec
Rename onaiinvoke, add threadid to onnewmessage, update experimental …
westey-m Apr 22, 2025
f6c7aa0
Update AzureAIAgent to work with memory.
westey-m Apr 22, 2025
72e6604
Rename conversation state extension to part
westey-m Apr 23, 2025
49e8ec2
Update integration tests to be general purpose
westey-m Apr 23, 2025
46e9959
use string.concat for instructions concatenation.
westey-m Apr 23, 2025
389c54e
Add mem0 tests and fix bugs.
westey-m Apr 23, 2025
41467ed
Add exclude from coverage for mem0 component
westey-m Apr 23, 2025
9a7990e
Add more tests for mem0 and improve thread handling.
westey-m Apr 24, 2025
058a7da
Improve AzureAI and OpenAIAssistant Agent tests
westey-m Apr 24, 2025
cd62b9f
Add mem0 unit tests and some small improvements.
westey-m Apr 25, 2025
54f527f
Move default context prompt to const variable.
westey-m Apr 25, 2025
79e30d5
Update Bedrock Agent to support conversation state and improve tests.
westey-m Apr 25, 2025
6f0c5f0
Move text rag to core and clean up.
westey-m Apr 25, 2025
3fdf5e2
Add unit tests for TextRagComponent and rename a few settings
westey-m Apr 28, 2025
3a2eaac
Merge branch 'main' into agent-memory-explore-v2
westey-m Apr 28, 2025
2c50f7e
Add TextRagStore improvements.
westey-m Apr 29, 2025
a382e35
Merge branch 'agent-memory-explore-v2' of https://github.com/westey-m…
westey-m Apr 29, 2025
34bbed2
Simplify context formatting and alow formatting override.
westey-m Apr 30, 2025
e89ee4f
Seal TextRagComponent and TextRagStore
westey-m Apr 30, 2025
64618aa
Add more tests and options for TextRagStore
westey-m Apr 30, 2025
ad3100b
Add support for text hydration
westey-m May 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions docs/decisions/00NN-agents-with-memory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
---
# These are optional elements. Feel free to remove any of them.
status: accepted
contact: westey-m
date: 2025-04-17
deciders: westey-m, markwallace-microsoft, alliscode, TaoChenOSU, moonbox3, crickman
consulted: westey-m, markwallace-microsoft, alliscode, TaoChenOSU, moonbox3, crickman
informed: westey-m, markwallace-microsoft, alliscode, TaoChenOSU, moonbox3, crickman
---

# Agents with Memory

## What do we mean by Memory?

By memory we mean the capability to remember information and skills that are learned during
a conversation and re-use those later in the same conversation or later in a subsequent conversation.

## Context and Problem Statement

Today we support multiple agent types with different characteristics:

1. In process vs remote.
1. Remote agents that store and maintain conversation state in the service vs those that require the caller to provide conversation state on each invocation.

We need to support advanced memory capabilities across this range of agent types.

### Memory Scope

Another aspect of memory that is important to consider is the scope of different memory types.
Most agent implementations have instructions and skills but the agent is not tied to a single conversation.
On each invocation of the agent, the agent is told which conversation to participate in, during that invocation.

Memories about a user or about a conversation with a user is therefore extracted from one of these conversation and recalled
during the same or another conversation with the same user.
These memories will typically contain information that the user would not like to share with other users of the system.

Other types of memories also exist which are not tied to a specific user or conversation.
E.g. an Agent may learn how to do something and be able to do that in many conversations with different users.
With these type of memories there is of cousrse risk in leaking personal information between different users which is important to guard against.

### Packaging memory capabilities

All of the above memory types can be supported for any agent by attaching software components to conversation threads.
This is achieved via a simple mechanism of:

1. Inspecting and using messages as they are passed to and from the agent.
1. Passing additional context to the agent per invocation.

With our current `AgentThread` implementation, when an agent is invoked, all input and output messages are already passed to the `AgentThread`
and can be made available to any components attached to the `AgentThread`.
Where agents are remote/external and manage conversation state in the service, passing the messages to the `AgentThread` may not have any
affect on the thread in the service. This is OK, since the service will have already updated the thread during the remote invocation.
It does however, still allow us to subscribe to messages in any attached components.

For the second requirement of getting additional context per invocation, the agent may ask the thread passed to it, to in turn ask
each of the components attached to it, to provide context to pass to the Agent.
This enables the component to provide memories that it contains to the Agent as needed.

Different memory capabilities can be built using separate components. Each component would have the following characteristics:

1. May store some context that can be provided to the agent per invocation.
1. May inspect messages from the conversation to learn from the conversation and build its context.
1. May register plugins to allow the agent to directly store, retrieve, update or clear memories.

### Suspend / Resume

Building a service to host an agent comes with challenges.
It's hard to build a stateful service, but service consumers expect an experience that looks stateful from the outside.
E.g. on each invocation, the user expects that the service can continue a conversation they are having.

This means that where the the service is exposing a local agent with local conversation state management (e.g. via `ChatHistory`)
that conversation state needs to be loaded and persisted for each invocation of the service.

It also means that any memory components that may have some in-memory state will need to be loaded and persisted too.

For cases like this, the `OnSuspend` and `OnResume` methods allow notification of the components that they need to save or reload their state.
It is up to each of these components to decide how and where to save state to or load state from.

## Proposed interface for Memory Components

The types of events that Memory Components require are not unique to memory, and can be used to package up other capabilities too.
The suggestion is therefore to create a more generally named type that can be used for other scenarios as well and can even
be used for non-agent scenarios too.

This type should live in the `Microsoft.SemanticKernel.Abstractions` nuget, since these components can be used by systems other than just agents.

```csharp
namespace Microsoft.SemanticKernel;

public abstract class ConversationStateExtension
{
public virtual IReadOnlyCollection<AIFunction> AIFunctions => Array.Empty<AIFunction>();

public virtual Task OnThreadCreatedAsync(string? threadId, CancellationToken cancellationToken = default);
public virtual Task OnThreadDeleteAsync(string? threadId, CancellationToken cancellationToken = default);

// OnThreadCheckpointAsync not included in initial release, maybe in future.
public virtual Task OnThreadCheckpointAsync(string? threadId, CancellationToken cancellationToken = default);

public virtual Task OnNewMessageAsync(string? threadId, ChatMessage newMessage, CancellationToken cancellationToken = default);
public abstract Task<string> OnModelInvokeAsync(ICollection<ChatMessage> newMessages, CancellationToken cancellationToken = default);

public virtual Task OnSuspendAsync(string? threadId, CancellationToken cancellationToken = default);
public virtual Task OnResumeAsync(string? threadId, CancellationToken cancellationToken = default);
}
```

> TODO: Decide about the correct namespace for `ConversationStateExtension`

## Managing multiple components

To manage multiple components I propose that we have a `ConversationStateExtensionsManager`.
This class allows registering components and delegating new message notifications, ai invocation calls, etc. to the contained components.

## Integrating with agents

I propose to add a `ConversationStateExtensionsManager` to the `AgentThread` class, allowing us to attach components to any `AgentThread`.

When an `Agent` is invoked, we will call `OnModelInvokeAsync` on each component via the `ConversationStateExtensionsManager` to get
a combined set of context to pass to the agent for this invocation. This will be internal to the `Agent` class and transparent to the user.

```csharp
var additionalInstructions = await currentAgentThread.OnModelInvokeAsync(messages, cancellationToken).ConfigureAwait(false);
```

## Usage examples

### Multiple threads using the same memory component

```csharp
// Create a vector store for storing memories.
var vectorStore = new InMemoryVectorStore();
// Create a memory store that is tired to a "Memories" collection in the vector store and stores memories under the "user/12345" namespace.
using var textMemoryStore = new VectorDataTextMemoryStore<string>(vectorStore, textEmbeddingService, "Memories", "user/12345", 1536);

// Create a memory component to will pull user facts from the conversation, store them in the vector store
// and pass them to the agent as additional instructions.
var userFacts = new UserFactsMemoryComponent(this.Fixture.Agent.Kernel, textMemoryStore);

// Create a thread and attach a Memory Component.
var agentThread1 = new ChatHistoryAgentThread();
agentThread1.ThreadExtensionsManager.Add(userFacts);
var asyncResults1 = agent.InvokeAsync("Hello, my name is Caoimhe.", agentThread1);

// Create a second thread and attach a Memory Component.
var agentThread2 = new ChatHistoryAgentThread();
agentThread2.ThreadExtensionsManager.Add(userFacts);
var asyncResults2 = agent.InvokeAsync("What is my name?.", agentThread2);
// Expected response contains Caoimhe.
```

### Using a RAG component

```csharp
// Create Vector Store and Rag Store/Component
var vectorStore = new InMemoryVectorStore();
using var ragStore = new TextRagStore<string>(vectorStore, textEmbeddingService, "Memories", 1536, "group/g2");
var ragComponent = new TextRagComponent(ragStore, new TextRagComponentOptions());

// Upsert docs into vector store.
await ragStore.UpsertDocumentsAsync(
[
new TextRagDocument("The financial results of Contoso Corp for 2023 is as follows:\nIncome EUR 174 000 000\nExpenses EUR 152 000 000")
{
SourceName = "Contoso 2023 Financial Report",
SourceReference = "https://www.consoso.com/reports/2023.pdf",
Namespaces = ["group/g2"]
}
]);

// Create a new agent thread and register the Rag component
var agentThread = new ChatHistoryAgentThread();
agentThread.ThreadExtensionsManager.RegisterThreadExtension(ragComponent);

// Inovke the agent.
var asyncResults1 = agent.InvokeAsync("What was the income of Contoso for 2023", agentThread);
// Expected response contains the 174M income from the document.
```

## Decisions to make

### Extension base class name

1. ConversationStateExtension
1. Long
1. MemoryComponent
1. Too specific

Chose ConversationStateExtension.

### Location for abstractions

1. Microsoft.SemanticKernel.<baseclass>
1. Microsoft.SemanticKernel.Memory.<baseclass>
1. Microsoft.SemanticKernel.Memory.<baseclass> (in separate nuget)

Chose Microsoft.SemanticKernel.<baseclass>.

### Location for memory components

1. A nuget for each component
1. Microsoft.SemanticKernel.Core nuget
1. Microsoft.SemanticKernel.Memory nuget
1. Microsoft.SemanticKernel.ConversationStateExtensions nuget

Chose Microsoft.SemanticKernel.Core nuget
20 changes: 20 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCPClient", "samples\Demos\
{12C7E0C7-A7DF-3BC3-0D4B-1A706BCE6981} = {12C7E0C7-A7DF-3BC3-0D4B-1A706BCE6981}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "memory", "memory", "{4B850B93-46D6-4F25-9DB1-90D1E6E4AB70}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Memory.Abstractions", "src\Memory\Memory.Abstractions\Memory.Abstractions.csproj", "{F5124057-1DA1-4799-9357-D9A635047678}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Memory", "src\Memory\Memory\Memory.csproj", "{A0538079-AB6F-4C7D-9138-A15258583F80}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProcessWithCloudEvents", "ProcessWithCloudEvents", "{7C092DD9-9985-4D18-A817-15317D984149}"
ProjectSection(SolutionItems) = preProject
samples\Demos\ProcessWithCloudEvents\README.md = samples\Demos\ProcessWithCloudEvents\README.md
Expand Down Expand Up @@ -1460,6 +1465,18 @@ Global
{B06770D5-2F3E-4271-9F6B-3AA9E716176F}.Publish|Any CPU.Build.0 = Release|Any CPU
{B06770D5-2F3E-4271-9F6B-3AA9E716176F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B06770D5-2F3E-4271-9F6B-3AA9E716176F}.Release|Any CPU.Build.0 = Release|Any CPU
{F5124057-1DA1-4799-9357-D9A635047678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5124057-1DA1-4799-9357-D9A635047678}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5124057-1DA1-4799-9357-D9A635047678}.Publish|Any CPU.ActiveCfg = Release|Any CPU
{F5124057-1DA1-4799-9357-D9A635047678}.Publish|Any CPU.Build.0 = Release|Any CPU
{F5124057-1DA1-4799-9357-D9A635047678}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5124057-1DA1-4799-9357-D9A635047678}.Release|Any CPU.Build.0 = Release|Any CPU
{A0538079-AB6F-4C7D-9138-A15258583F80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0538079-AB6F-4C7D-9138-A15258583F80}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0538079-AB6F-4C7D-9138-A15258583F80}.Publish|Any CPU.ActiveCfg = Release|Any CPU
{A0538079-AB6F-4C7D-9138-A15258583F80}.Publish|Any CPU.Build.0 = Release|Any CPU
{A0538079-AB6F-4C7D-9138-A15258583F80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0538079-AB6F-4C7D-9138-A15258583F80}.Release|Any CPU.Build.0 = Release|Any CPU
{31F6608A-FD36-F529-A5FC-C954A0B5E29E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{31F6608A-FD36-F529-A5FC-C954A0B5E29E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31F6608A-FD36-F529-A5FC-C954A0B5E29E}.Publish|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -1703,6 +1720,9 @@ Global
{879545ED-D429-49B1-96F1-2EC55FFED31D} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
{12C7E0C7-A7DF-3BC3-0D4B-1A706BCE6981} = {879545ED-D429-49B1-96F1-2EC55FFED31D}
{B06770D5-2F3E-4271-9F6B-3AA9E716176F} = {879545ED-D429-49B1-96F1-2EC55FFED31D}
{4B850B93-46D6-4F25-9DB1-90D1E6E4AB70} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0}
{F5124057-1DA1-4799-9357-D9A635047678} = {4B850B93-46D6-4F25-9DB1-90D1E6E4AB70}
{A0538079-AB6F-4C7D-9138-A15258583F80} = {4B850B93-46D6-4F25-9DB1-90D1E6E4AB70}
{7C092DD9-9985-4D18-A817-15317D984149} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
{31F6608A-FD36-F529-A5FC-C954A0B5E29E} = {7C092DD9-9985-4D18-A817-15317D984149}
{08D84994-794A-760F-95FD-4EFA8998A16D} = {7C092DD9-9985-4D18-A817-15317D984149}
Expand Down
1 change: 1 addition & 0 deletions dotnet/docs/EXPERIMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ You can use the following diagnostic IDs to ignore warnings or errors for a part
| SKEXP0100 | Advanced Semantic Kernel features |
| SKEXP0110 | Semantic Kernel Agents |
| SKEXP0120 | Native-AOT |
| SKEXP0130 | Conversation State |

## Experimental Features Tracking

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" />
<PackageReference Include="System.Linq.Async" />
</ItemGroup>

<ItemGroup>
Expand Down
59 changes: 59 additions & 0 deletions dotnet/src/Agents/Abstractions/AgentThread.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -26,6 +27,52 @@ public abstract class AgentThread
/// </summary>
public virtual bool IsDeleted { get; protected set; } = false;

/// <summary>
/// Gets or sets the container for conversation state part components that manages their lifecycle and interactions.
/// </summary>
[Experimental("SKEXP0110")]
public virtual ConversationStatePartsManager StateParts { get; init; } = new ConversationStatePartsManager();

/// <summary>
/// Called when the current conversion is temporarily suspended and any state should be saved.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An async task.</returns>
/// <remarks>
/// In a service that hosts an agent, that is invoked via calls to the service, this might be at the end of each service call.
/// In a client application, this might be when the user closes the chat window or the application.
/// </remarks>
[Experimental("SKEXP0110")]
public virtual Task OnSuspendAsync(CancellationToken cancellationToken = default)
{
return this.StateParts.OnSuspendAsync(this.Id, cancellationToken);
}

/// <summary>
/// Called when the current conversion is resumed and any state should be restored.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An async task.</returns>
/// <remarks>
/// In a service that hosts an agent, that is invoked via calls to the service, this might be at the start of each service call where a previous conversation is being continued.
/// In a client application, this might be when the user re-opens the chat window to resume a conversation after having previously closed it.
/// </remarks>
[Experimental("SKEXP0110")]
public virtual Task OnResumeAsync(CancellationToken cancellationToken = default)
{
if (this.IsDeleted)
{
throw new InvalidOperationException("This thread has been deleted and cannot be used anymore.");
}

if (this.Id is null)
{
throw new InvalidOperationException("This thread cannot be resumed, since it has not been created.");
}

return this.StateParts.OnResumeAsync(this.Id, cancellationToken);
}

/// <summary>
/// Creates the thread and returns the thread id.
/// </summary>
Expand All @@ -45,6 +92,10 @@ protected internal virtual async Task CreateAsync(CancellationToken cancellation
}

this.Id = await this.CreateInternalAsync(cancellationToken: cancellationToken).ConfigureAwait(false);

#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
await this.StateParts.OnThreadCreatedAsync(this.Id!, cancellationToken).ConfigureAwait(false);
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
}

/// <summary>
Expand All @@ -65,6 +116,10 @@ public virtual async Task DeleteAsync(CancellationToken cancellationToken = defa
throw new InvalidOperationException("This thread cannot be deleted, since it has not been created.");
}

#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
await this.StateParts.OnThreadDeleteAsync(this.Id!, cancellationToken).ConfigureAwait(false);
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

await this.DeleteInternalAsync(cancellationToken).ConfigureAwait(false);

this.IsDeleted = true;
Expand Down Expand Up @@ -92,6 +147,10 @@ internal virtual async Task OnNewMessageAsync(ChatMessageContent newMessage, Can
await this.CreateAsync(cancellationToken).ConfigureAwait(false);
}

#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
await this.StateParts.OnNewMessageAsync(this.Id, newMessage, cancellationToken).ConfigureAwait(false);
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

await this.OnNewMessageInternalAsync(newMessage, cancellationToken).ConfigureAwait(false);
}

Expand Down
1 change: 1 addition & 0 deletions dotnet/src/Agents/AzureAI/Agents.AzureAI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

<ItemGroup>
<ProjectReference Include="..\Abstractions\Agents.Abstractions.csproj" />
<ProjectReference Include="..\..\SemanticKernel.Core\SemanticKernel.Core.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading