// 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.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using OpenAI.Chat; using SampleApp; 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-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. ChatClient chatClient = new AzureOpenAIClient( new Uri(endpoint), new DefaultAzureCredential()) .GetChatClient(deploymentName); // Create the agent and provide a factory to add our custom memory component 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 = chatClient.AsAIAgent(new ChatClientAgentOptions() { ChatOptions = new() { Instructions = "You are a friendly assistant. Always address the user by their name." }, AIContextProviders = [new UserInfoMemory(chatClient.AsIChatClient())] }); // 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()?.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() 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 { /// /// Sample memory component that can remember a user's name and age. /// internal sealed class UserInfoMemory : AIContextProvider { private readonly ProviderSessionState _sessionState; private IReadOnlyList? _stateKeys; private readonly IChatClient _chatClient; public UserInfoMemory(IChatClient chatClient, Func? stateInitializer = null) { this._sessionState = new ProviderSessionState( stateInitializer ?? (_ => new UserInfo()), this.GetType().Name); this._chatClient = chatClient; } public override IReadOnlyList 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)) { var result = await this._chatClient.GetResponseAsync( context.RequestMessages, new ChatOptions() { 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 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(new AIContext { Instructions = instructions.ToString() }); } } internal sealed class UserInfo { public string? UserName { get; set; } public int? UserAge { get; set; } } }