-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Expand file tree
/
Copy pathProgram.cs
More file actions
182 lines (150 loc) · 8.75 KB
/
Copy pathProgram.cs
File metadata and controls
182 lines (150 loc) · 8.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to add a basic custom memory component to an agent.
// The memory component subscribes to all messages added to the conversation and
// extracts the user's name and age if provided.
// The component adds a prompt to ask for this information if it is not already known
// and provides it to the model before each invocation if known.
using System.Text;
using System.Text.Json;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using SampleApp;
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
var projectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
// Create a separate IChatClient for the memory component to use for structured extraction.
// The memory component calls the model with a ResponseFormat (JSON schema) to extract user info.
// Using a dedicated client here avoids mixing side-channel extraction calls with the agent's
// conversation history, and avoids the chicken-and-egg problem of needing an IChatClient
// before the main agent is constructed.
IChatClient extractionClient =
new AIProjectClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(model);
// Create the agent with instructions and the custom memory context provider.
// The memory component is attached to all sessions created by the agent. Here each new memory
// component will have its own user info object, so each session will have its own memory.
// In real world applications/services, where the user info would be persisted in a database,
// and preferably shared between multiple sessions used by the same user, ensure that the
// factory reads the user id from the current context and scopes the memory component
// and its storage to that user id.
AIAgent agent = projectClient.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new ChatOptions
{
ModelId = model,
Instructions = "You are a friendly assistant. Always address the user by their name.",
},
AIContextProviders = [new UserInfoMemory(extractionClient)]
});
// Create a new session for the conversation.
AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(">> Use session with blank memory\n");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Hello, what is the square root of 9?", session));
Console.WriteLine(await agent.RunAsync("My name is Ruaidhrí", session));
Console.WriteLine(await agent.RunAsync("I am 20 years old", session));
// We can serialize the session. The serialized state will include the state of the memory component.
JsonElement sessionElement = await agent.SerializeSessionAsync(session);
Console.WriteLine("\n>> Use deserialized session with previously created memories\n");
// Later we can deserialize the session and continue the conversation with the previous memory component state.
var deserializedSession = await agent.DeserializeSessionAsync(sessionElement);
Console.WriteLine(await agent.RunAsync("What is my name and age?", deserializedSession));
Console.WriteLine("\n>> Read memories using memory component\n");
// It's possible to access the memory component via the agent's GetService method.
var userInfo = agent.GetService<UserInfoMemory>()?.GetUserInfo(deserializedSession);
// Output the user info that was captured by the memory component.
Console.WriteLine($"MEMORY - User Name: {userInfo?.UserName}");
Console.WriteLine($"MEMORY - User Age: {userInfo?.UserAge}");
Console.WriteLine("\n>> Use new session with previously created memories\n");
// It is also possible to set the memories using a memory component on an individual session.
// This is useful if we want to start a new session, but have it share the same memories as a previous session.
var newSession = await agent.CreateSessionAsync();
if (userInfo is not null && agent.GetService<UserInfoMemory>() is UserInfoMemory newSessionMemory)
{
newSessionMemory.SetUserInfo(newSession, userInfo);
}
// Invoke the agent and output the text result.
// This time the agent should remember the user's name and use it in the response.
Console.WriteLine(await agent.RunAsync("What is my name and age?", newSession));
namespace SampleApp
{
/// <summary>
/// Sample memory component that can remember a user's name and age.
/// </summary>
internal sealed class UserInfoMemory : AIContextProvider
{
private readonly ProviderSessionState<UserInfo> _sessionState;
private IReadOnlyList<string>? _stateKeys;
private readonly IChatClient _chatClient;
public UserInfoMemory(IChatClient chatClient, Func<AgentSession?, UserInfo>? stateInitializer = null)
{
this._sessionState = new ProviderSessionState<UserInfo>(
stateInitializer ?? (_ => new UserInfo()),
this.GetType().Name);
this._chatClient = chatClient;
}
public override IReadOnlyList<string> StateKeys => this._stateKeys ??= [this._sessionState.StateKey];
public UserInfo GetUserInfo(AgentSession session)
=> this._sessionState.GetOrInitializeState(session);
public void SetUserInfo(AgentSession session, UserInfo userInfo)
=> this._sessionState.SaveState(session, userInfo);
protected override async ValueTask StoreAIContextAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
var userInfo = this._sessionState.GetOrInitializeState(context.Session);
// Try and extract the user name and age from the message if we don't have it already and it's a user message.
if ((userInfo.UserName is null || userInfo.UserAge is null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
{
// The Foundry Responses API requires the model name in the request body.
// Retrieve it from the client's metadata so callers don't need to pass it separately.
var modelId = this._chatClient.GetService<ChatClientMetadata>()?.DefaultModelId
?? throw new InvalidOperationException(
"Could not retrieve DefaultModelId from the extraction IChatClient. " +
"Ensure the client was created with a model ID (e.g., via projectClient.AsAIAgent(...)).");
var result = await this._chatClient.GetResponseAsync<UserInfo>(
context.RequestMessages,
new ChatOptions()
{
ModelId = modelId,
Instructions = "Extract the user's name and age from the message if present. If not present return nulls."
},
cancellationToken: cancellationToken);
userInfo.UserName ??= result.Result.UserName;
userInfo.UserAge ??= result.Result.UserAge;
}
this._sessionState.SaveState(context.Session, userInfo);
}
protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var userInfo = this._sessionState.GetOrInitializeState(context.Session);
StringBuilder instructions = new();
// If we don't already know the user's name and age, add instructions to ask for them, otherwise just provide what we have to the context.
instructions
.AppendLine(
userInfo.UserName is null ?
"Ask the user for their name and politely decline to answer any questions until they provide it." :
$"The user's name is {userInfo.UserName}.")
.AppendLine(
userInfo.UserAge is null ?
"Ask the user for their age and politely decline to answer any questions until they provide it." :
$"The user's age is {userInfo.UserAge}.");
return new ValueTask<AIContext>(new AIContext
{
Instructions = instructions.ToString()
});
}
}
internal sealed class UserInfo
{
public string? UserName { get; set; }
public int? UserAge { get; set; }
}
}