Skip to content

Commit 043fa0c

Browse files
authored
Merge pull request #663 from westey-m/tutorials-thirdpartystoarge-memory
Add third party storage and memory tutorials
2 parents 780a834 + eada7de commit 043fa0c

7 files changed

Lines changed: 392 additions & 8 deletions

File tree

agent-framework/tutorials/agents/TOC.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414
href: agent-as-function-tool.md
1515
- name: Exposing an agent as an MCP tool
1616
href: agent-as-mcp-tool.md
17-
- name: Persisting conversations
18-
href: persisted-conversation.md
1917
- name: Enabling observability for agents
2018
href: enable-observability.md
21-
19+
- name: Persisting conversations
20+
href: persisted-conversation.md
21+
- name: Third Party chat history storage
22+
href: third-party-chat-history-storage.md
23+
- name: Adding memory to agents
24+
href: memory.md

agent-framework/tutorials/agents/agent-as-mcp-tool.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,4 @@ Tutorial coming soon.
7878
## Next steps
7979

8080
> [!div class="nextstepaction"]
81-
> [Persisting Conversations](./persisted-conversation.md)
81+
> [Enabling observability for agents](./enable-observability.md)

agent-framework/tutorials/agents/enable-observability.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ Because he wanted to improve his "arrr-ticulation"! ?????
120120
## Next steps
121121

122122
> [!div class="nextstepaction"]
123-
> [Having a multi-turn conversation with an agent](./multi-turn-conversation.md)
123+
> [Persisting conversations](./persisted-conversation.md)
124124
125125
::: zone-end
126126
::: zone pivot="programming-language-python"
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
---
2+
title: Adding Memory to an Agent
3+
description: How to add memory to an agent using an AIContextProvider.
4+
zone_pivot_groups: programming-languages
5+
author: westey-m
6+
ms.topic: tutorial
7+
ms.author: westey
8+
ms.date: 09/25/2025
9+
ms.service: semantic-kernel
10+
---
11+
12+
# Adding Memory to an Agent
13+
14+
This tutorial shows how to add memory to an agent by implementing an `AIContextProvider` and attaching it to the agent.
15+
16+
> [!IMPORTANT]
17+
> Not all agent types support `AIContextProvider`. In this step we are using a `ChatClientAgent`, which does support `AIContextProvider`.
18+
19+
::: zone pivot="programming-language-csharp"
20+
21+
## Prerequisites
22+
23+
For prerequisites and installing nuget packages, see the [Create and run a simple agent](./run-agent.md) step in this tutorial.
24+
25+
## Creating an AIContextProvider
26+
27+
`AIContextProvider` is an abstract class that you can inherit from, and which can be associated with the `AgentThread` for a `ChatClientAgent`.
28+
It allows you to:
29+
30+
1. run custom logic before and after the agent invokes the underlying inference service
31+
1. provide additional context to the agent before it invokes the underlying inference service
32+
1. inspect all messages provided to and produced by the agent
33+
34+
### Pre and post invocation events
35+
36+
The `AIContextProvider` class has two methods that you can override to run custom logic before and after the agent invokes the underlying inference service:
37+
38+
- `InvokingAsync` - called before the agent invokes the underlying inference service. You can provide additional context to the agent by returning an `AIContext` object. This context will be merged with the agent's existing context before invoking the underlying service. It is possible to provide instructions, tools, and messages to add to the request.
39+
- `InvokedAsync` - called after the agent has received a response from the underlying inference service. You can inspect the request and response messages, and update the state of the context provider.
40+
41+
### Serialization
42+
43+
`AIContextProvider` instances are created and attached to an `AgentThread` when the thread is created, and when a thread is resumed from a serialized state.
44+
45+
The `AIContextProvider` instance may have its own state that needs to be persisted between invocations of the agent. E.g. a memory component that remembers information about the user may have memories as part of its state.
46+
47+
To allow persisting threads, you need to implement the `SerializeAsync` method of the `AIContextProvider` class. You also need to provide a constructor that takes a `JsonElement` parameter, which can be used to deserialize the state when resuming a thread.
48+
49+
### Sample AIContextProvider implementation
50+
51+
Let's look at an example of a custom memory component that remembers a user's name and age, and provides it to the agent before each invocation.
52+
53+
First we'll create a model class to hold the memories.
54+
55+
```csharp
56+
internal sealed class UserInfo
57+
{
58+
public string? UserName { get; set; }
59+
public int? UserAge { get; set; }
60+
}
61+
```
62+
63+
Then we can implement the `AIContextProvider` to manage the memories.
64+
The `UserInfoMemory` class below contains the following behavior:
65+
66+
1. It uses a `IChatClient` to look for the user's name and age in user messages when new messages are added to the thread at the end of each run.
67+
1. It provides any current memories to the agent before each invocation.
68+
1. If not memories are available, it instructs the agent to ask the user for the missing information, and not to answer any questions until the information is provided.
69+
1. It also implements serialization to allow persisting the memories as part of the thread state.
70+
71+
```csharp
72+
internal sealed class UserInfoMemory : AIContextProvider
73+
{
74+
private readonly IChatClient _chatClient;
75+
public UserInfoMemory(IChatClient chatClient, UserInfo? userInfo = null)
76+
{
77+
this._chatClient = chatClient;
78+
this.UserInfo = userInfo ?? new UserInfo();
79+
}
80+
81+
public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
82+
{
83+
this._chatClient = chatClient;
84+
this.UserInfo = serializedState.ValueKind == JsonValueKind.Object ?
85+
serializedState.Deserialize<UserInfo>(jsonSerializerOptions)! :
86+
new UserInfo();
87+
}
88+
89+
public UserInfo UserInfo { get; set; }
90+
91+
public override async ValueTask InvokedAsync(
92+
InvokedContext context,
93+
CancellationToken cancellationToken = default)
94+
{
95+
if ((this.UserInfo.UserName is null || this.UserInfo.UserAge is null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
96+
{
97+
var result = await this._chatClient.GetResponseAsync<UserInfo>(
98+
context.RequestMessages,
99+
new ChatOptions()
100+
{
101+
Instructions = "Extract the user's name and age from the message if present. If not present return nulls."
102+
},
103+
cancellationToken: cancellationToken);
104+
this.UserInfo.UserName ??= result.Result.UserName;
105+
this.UserInfo.UserAge ??= result.Result.UserAge;
106+
}
107+
}
108+
109+
public override ValueTask<AIContext> InvokingAsync(
110+
InvokingContext context,
111+
CancellationToken cancellationToken = default)
112+
{
113+
StringBuilder instructions = new();
114+
instructions
115+
.AppendLine(
116+
this.UserInfo.UserName is null ?
117+
"Ask the user for their name and politely decline to answer any questions until they provide it." :
118+
$"The user's name is {this.UserInfo.UserName}.")
119+
.AppendLine(
120+
this.UserInfo.UserAge is null ?
121+
"Ask the user for their age and politely decline to answer any questions until they provide it." :
122+
$"The user's age is {this.UserInfo.UserAge}.");
123+
return new ValueTask<AIContext>(new AIContext
124+
{
125+
Instructions = instructions.ToString()
126+
});
127+
}
128+
129+
public override ValueTask<JsonElement?> SerializeAsync(
130+
JsonSerializerOptions? jsonSerializerOptions = null,
131+
CancellationToken cancellationToken = default)
132+
{
133+
return new ValueTask<JsonElement?>(JsonSerializer.SerializeToElement(this.UserInfo, jsonSerializerOptions));
134+
}
135+
}
136+
```
137+
138+
## Using the AIContextProvider with an agent
139+
140+
To use the custom `AIContextProvider`, you need to provide an `AIContextProviderFactory` when creating the agent. This factory allows the agent to create a new instance of the desired `AIContextProvider` for each thread.
141+
142+
When creating a `ChatClientAgent` it is possible to provide a `ChatClientAgentOptions` object that allows providing the `AIContextProviderFactory` in addition to all other agent options.
143+
144+
```csharp
145+
ChatClient chatClient = new AzureOpenAIClient(
146+
new Uri("https://<myresource>.openai.azure.com"),
147+
new AzureCliCredential())
148+
.GetChatClient("gpt-4o-mini");
149+
150+
AIAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions()
151+
{
152+
Instructions = "You are a friendly assistant. Always address the user by their name.",
153+
AIContextProviderFactory = ctx => new UserInfoMemory(
154+
chatClient.AsIChatClient(),
155+
ctx.SerializedState,
156+
ctx.JsonSerializerOptions)
157+
});
158+
```
159+
160+
When creating a new thread, the `AIContextProvider` will be created by `GetNewThread`
161+
and attached to the thread. Once memories are extracted it is therefore possible to access the memory component via the thread's `GetService` method and inspect the memories.
162+
163+
```csharp
164+
// Create a new thread for the conversation.
165+
AgentThread thread = agent.GetNewThread();
166+
167+
Console.WriteLine(await agent.RunAsync("Hello, what is the square root of 9?", thread));
168+
Console.WriteLine(await agent.RunAsync("My name is Ruaidhrí", thread));
169+
Console.WriteLine(await agent.RunAsync("I am 20 years old", thread));
170+
171+
// Access the memory component via the thread's GetService method.
172+
var userInfo = deserializedThread.GetService<UserInfoMemory>()?.UserInfo;
173+
Console.WriteLine($"MEMORY - User Name: {userInfo?.UserName}");
174+
Console.WriteLine($"MEMORY - User Age: {userInfo?.UserAge}");
175+
```
176+
177+
::: zone-end
178+
::: zone pivot="programming-language-python"
179+
180+
Tutorial coming soon.
181+
182+
::: zone-end

agent-framework/tutorials/agents/persisted-conversation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ zone_pivot_groups: programming-languages
55
author: westey-m
66
ms.topic: tutorial
77
ms.author: westey
8-
ms.date: 09/18/2025
8+
ms.date: 09/25/2025
99
ms.service: semantic-kernel
1010
---
1111

@@ -87,4 +87,4 @@ Tutorial coming soon.
8787
## Next steps
8888

8989
> [!div class="nextstepaction"]
90-
> [Enabling observability for agents](./enable-observability.md)
90+
> [Third Party chat history storage](./third-party-chat-history-storage.md)

agent-framework/tutorials/agents/run-agent.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Before you begin, ensure you have the following prerequisites:
3232
3333
## Installing Nuget packages
3434

35-
To use the AgentFramework with Azure OpenAI, you need to install the following NuGet packages:
35+
To use the Microsoft Agent Framework with Azure OpenAI, you need to install the following NuGet packages:
3636

3737
```powershell
3838
dotnet add package Azure.Identity

0 commit comments

Comments
 (0)