.NET: [BREAKING] Add support for multiple AIContextProviders on a ChatClientAgent (#3863)

* Add support for multiple AIContextProviders on a ChatClientAgent

* Address PR comments and fix tests

* Address PR comments.
This commit is contained in:
westey
2026-02-12 14:15:54 +00:00
committed by GitHub
parent f44fe17479
commit 210d0b8828
16 changed files with 586 additions and 73 deletions
@@ -34,7 +34,7 @@ AIAgent agent = new AzureOpenAIClient(
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
AIContextProvider = new ChatHistoryMemoryProvider(
AIContextProviders = [new ChatHistoryMemoryProvider(
vectorStore,
collectionName: "chathistory",
vectorDimensions: 3072,
@@ -48,7 +48,7 @@ AIAgent agent = new AzureOpenAIClient(
storageScope: new() { UserId = "UID1", SessionId = Guid.NewGuid().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 sessions.
searchScope: new() { UserId = "UID1" }))
searchScope: new() { UserId = "UID1" }))]
});
// Start a new session for the agent conversation.
@@ -36,7 +36,7 @@ AIAgent agent = new AzureOpenAIClient(
// If each session should have its own Mem0 scope, you can create a new id per session via the stateInitializer, e.g.:
// new Mem0Provider(mem0HttpClient, stateInitializer: _ => new(new Mem0ProviderScope() { ThreadId = Guid.NewGuid().ToString() }))
// In our case we are storing memories scoped by application and user instead so that memories are retained across threads.
AIContextProvider = new Mem0Provider(mem0HttpClient, stateInitializer: _ => new(new Mem0ProviderScope() { ApplicationId = "getting-started-agents", UserId = "sample-user" }))
AIContextProviders = [new Mem0Provider(mem0HttpClient, stateInitializer: _ => new(new Mem0ProviderScope() { ApplicationId = "getting-started-agents", UserId = "sample-user" }))]
});
AgentSession session = await agent.CreateSessionAsync();
@@ -33,7 +33,7 @@ ChatClient chatClient = new AzureOpenAIClient(
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = "You are a friendly assistant. Always address the user by their name." },
AIContextProvider = new UserInfoMemory(chatClient.AsIChatClient())
AIContextProviders = [new UserInfoMemory(chatClient.AsIChatClient())]
});
// Create a new session for the conversation.
@@ -62,7 +62,7 @@ AIAgent agent = azureOpenAIClient
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
AIContextProvider = new TextSearchProvider(SearchAdapter, textSearchOptions),
AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)],
// Since we are using ChatCompletion which stores chat history locally, we can also add a message filter
// that removes messages produced by the TextSearchProvider before they are added to the chat history, so that
// we don't bloat chat history with all the search result messages.
@@ -71,7 +71,7 @@ AIAgent agent = azureOpenAIClient
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief." },
AIContextProvider = new TextSearchProvider(SearchAdapter, textSearchOptions)
AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)]
});
AgentSession session = await agent.CreateSessionAsync();
@@ -29,7 +29,7 @@ AIAgent agent = new AzureOpenAIClient(
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
AIContextProvider = new TextSearchProvider(MockSearchAsync, textSearchOptions)
AIContextProviders = [new TextSearchProvider(MockSearchAsync, textSearchOptions)]
});
AgentSession session = await agent.CreateSessionAsync();
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to inject additional AI context into a ChatClientAgent using a custom AIContextProvider component that is attached to the agent.
// The sample also shows how to combine the results from multiple providers into a single class, in order to attach multiple of these to an agent.
// This sample shows how to inject additional AI context into a ChatClientAgent using custom AIContextProvider components that are attached to the agent.
// Multiple providers can be attached to an agent, and they will be called in sequence, each receiving the accumulated context from the previous one.
// This mechanism can be used for various purposes, such as injecting RAG search results or memories into the agent's context.
// Also note that Agent Framework already provides built-in AIContextProviders for many of these scenarios.
@@ -52,12 +52,12 @@ AIAgent agent = new AzureOpenAIClient(
// You may want to store these messages, depending on their content and your requirements.
StorageInputMessageFilter = messages => messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.AIContextProvider && m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory)
}),
// Add an AI context provider that maintains a todo list for the agent and one that provides upcoming calendar entries.
// Wrap these in an AI context provider that aggregates the other two.
AIContextProvider = new AggregatingAIContextProvider([
// Add multiple AI context providers: one that maintains a todo list and one that provides upcoming calendar entries.
// The agent will call each provider in sequence, accumulating context from each.
AIContextProviders = [
new TodoListAIContextProvider(),
new CalendarSearchAIContextProvider(loadNextThreeCalendarEvents)
]),
],
});
// Invoke the agent and output the text result.
@@ -178,30 +178,4 @@ namespace SampleApp
};
}
}
/// <summary>
/// An <see cref="AIContextProvider"/> which aggregates multiple AI context providers into one.
/// Tools and messages from all providers are combined, and instructions are concatenated.
/// </summary>
internal sealed class AggregatingAIContextProvider : AIContextProvider
{
private readonly List<AIContextProvider> _providers;
public AggregatingAIContextProvider(List<AIContextProvider> providers)
{
this._providers = providers;
}
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
// Invoke all the sub providers.
var currentAIContext = context.AIContext;
foreach (var provider in this._providers)
{
currentAIContext = await provider.InvokingAsync(new InvokingContext(context.Agent, context.Session, currentAIContext), cancellationToken);
}
return currentAIContext;
}
}
}