.NET: [BREAKING] Refactor providers to move common functionality to base (#3900)

* Move common functionality to provider base classes

Co-Authored-By: Copilot <175728472+Copilot@users.noreply.github.com>

* Address PR comments.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
westey
2026-02-16 19:02:47 +00:00
committed by GitHub
co-authored by Copilot
parent aab621f5eb
commit 54a67d96cd
23 changed files with 1593 additions and 482 deletions
@@ -86,27 +86,25 @@ namespace SampleApp
/// <summary>
/// Sample memory component that can remember a user's name and age.
/// </summary>
internal sealed class UserInfoMemory : AIContextProvider
internal sealed class UserInfoMemory : AIContextProvider<UserInfo>
{
private readonly IChatClient _chatClient;
private readonly Func<AgentSession?, UserInfo> _stateInitializer;
public UserInfoMemory(IChatClient chatClient, Func<AgentSession?, UserInfo>? stateInitializer = null)
: base(stateInitializer ?? (_ => new UserInfo()), null, null, null, null)
{
this._chatClient = chatClient;
this._stateInitializer = stateInitializer ?? (_ => new UserInfo());
}
public UserInfo GetUserInfo(AgentSession session)
=> session.StateBag.GetValue<UserInfo>(nameof(UserInfoMemory)) ?? new UserInfo();
=> this.GetOrInitializeState(session);
public void SetUserInfo(AgentSession session, UserInfo userInfo)
=> session.StateBag.SetValue(nameof(UserInfoMemory), userInfo);
=> this.SaveState(session, userInfo);
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override async ValueTask StoreAIContextAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
var userInfo = context.Session?.StateBag.GetValue<UserInfo>(nameof(UserInfoMemory))
?? this._stateInitializer.Invoke(context.Session);
var userInfo = this.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))
@@ -123,20 +121,14 @@ namespace SampleApp
userInfo.UserAge ??= result.Result.UserAge;
}
context.Session?.StateBag.SetValue(nameof(UserInfoMemory), userInfo);
this.SaveState(context.Session, userInfo);
}
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var inputContext = context.AIContext;
var userInfo = context.Session?.StateBag.GetValue<UserInfo>(nameof(UserInfoMemory))
?? this._stateInitializer.Invoke(context.Session);
var userInfo = this.GetOrInitializeState(context.Session);
StringBuilder instructions = new();
if (!string.IsNullOrEmpty(inputContext.Instructions))
{
instructions.AppendLine(inputContext.Instructions);
}
// 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
@@ -151,9 +143,7 @@ namespace SampleApp
return new ValueTask<AIContext>(new AIContext
{
Instructions = instructions.ToString(),
Messages = inputContext.Messages,
Tools = inputContext.Tools
Instructions = instructions.ToString()
});
}
}
@@ -76,45 +76,23 @@ namespace SampleApp
/// State (the session DB key) is stored in the <see cref="AgentSession.StateBag"/> so it roundtrips
/// automatically with session serialization.
/// </summary>
internal sealed class VectorChatHistoryProvider : ChatHistoryProvider
internal sealed class VectorChatHistoryProvider : ChatHistoryProvider<VectorChatHistoryProvider.State>
{
private readonly VectorStore _vectorStore;
private readonly Func<AgentSession?, State> _stateInitializer;
private readonly string _stateKey;
/// <inheritdoc />
public override string StateKey => this._stateKey;
public VectorChatHistoryProvider(
VectorStore vectorStore,
Func<AgentSession?, State>? stateInitializer = null,
string? stateKey = null)
: base(stateInitializer: stateInitializer ?? (_ => new State(Guid.NewGuid().ToString("N"))), stateKey: stateKey, jsonSerializerOptions: null, provideOutputMessageFilter: null, storeInputMessageFilter: null)
{
this._vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore));
this._stateInitializer = stateInitializer ?? (_ => new State(Guid.NewGuid().ToString("N")));
this._stateKey = stateKey ?? base.StateKey;
}
public string GetSessionDbKey(AgentSession session)
=> this.GetOrInitializeState(session).SessionDbKey;
private State GetOrInitializeState(AgentSession? session)
{
if (session?.StateBag.TryGetValue<State>(this._stateKey, out var state) is true && state is not null)
{
return state;
}
state = this._stateInitializer(session);
if (session is not null)
{
session.StateBag.SetValue(this._stateKey, state);
}
return state;
}
protected override async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var state = this.GetOrInitializeState(context.Session);
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
@@ -129,29 +107,17 @@ namespace SampleApp
var messages = records.ConvertAll(x => JsonSerializer.Deserialize<ChatMessage>(x.SerializedMessage!)!);
messages.Reverse();
return messages
.Select(message => message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, this.GetType().FullName!))
.Concat(context.RequestMessages);
return messages;
}
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override async ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
// Don't store messages if the request failed.
if (context.InvokeException is not null)
{
return;
}
var state = this.GetOrInitializeState(context.Session);
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
// Add both request and response messages to the store, excluding messages that came from chat history.
// Optionally messages produced by the AIContextProvider can also be persisted (not shown).
var allNewMessages = context.RequestMessages
.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory)
.Concat(context.ResponseMessages ?? []);
var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
await collection.UpsertAsync(allNewMessages.Select(x => new ChatHistoryItem()
{