Skip to content

Commit ad0dac3

Browse files
authored
.NET: [BREAKING] Add ability to mark the source of Agent request messages and use that for filtering (microsoft#3540)
* Add ability to mark the source of Agent request messages and use that for filtering * Add support for source, in addition to source type, and add unit tests for automatic stamping * Address PR comments. * Add merge fixes * Address PR comments
1 parent 390f933 commit ad0dac3

34 files changed

Lines changed: 1719 additions & 385 deletions

File tree

dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ protected override async Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessag
6262
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
6363

6464
// Notify the session of the input and output messages.
65-
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, messages, storeMessages)
65+
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, messages)
6666
{
6767
ResponseMessages = responseMessages
6868
};
@@ -94,7 +94,7 @@ protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingA
9494
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
9595

9696
// Notify the session of the input and output messages.
97-
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, messages, storeMessages)
97+
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, messages)
9898
{
9999
ResponseMessages = responseMessages
100100
};

dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step03_CustomMemory/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonS
104104

105105
public UserInfo UserInfo { get; set; }
106106

107-
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
107+
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
108108
{
109109
// Try and extract the user name and age from the message if we don't have it already and it's a user message.
110110
if ((this.UserInfo.UserName is null || this.UserInfo.UserAge is null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
@@ -122,7 +122,7 @@ public override async ValueTask InvokedAsync(InvokedContext context, Cancellatio
122122
}
123123
}
124124

125-
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
125+
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
126126
{
127127
StringBuilder instructions = new();
128128

dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyChatHistoryStorage/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public VectorChatHistoryProvider(VectorStore vectorStore, JsonElement serialized
8989

9090
public string? SessionDbKey { get; private set; }
9191

92-
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
92+
protected override async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
9393
{
9494
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
9595
await collection.EnsureCollectionExistsAsync(cancellationToken);
@@ -107,7 +107,7 @@ public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(Invoking
107107
return messages;
108108
}
109109

110-
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
110+
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
111111
{
112112
// Don't store messages if the request failed.
113113
if (context.InvokeException is not null)
@@ -122,7 +122,7 @@ public override async ValueTask InvokedAsync(InvokedContext context, Cancellatio
122122

123123
// Add both request and response messages to the store
124124
// Optionally messages produced by the AIContextProvider can also be persisted (not shown).
125-
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
125+
var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
126126

127127
await collection.UpsertAsync(allNewMessages.Select(x => new ChatHistoryItem()
128128
{

dotnet/samples/GettingStarted/Agents/Agent_Step20_AdditionalAIContext/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public TodoListAIContextProvider(JsonElement jsonElement, JsonSerializerOptions?
9292
}
9393
}
9494

95-
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
95+
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
9696
{
9797
StringBuilder outputMessageBuilder = new();
9898
outputMessageBuilder.AppendLine("Your todo list contains the following items:");
@@ -132,7 +132,7 @@ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptio
132132
/// </summary>
133133
internal sealed class CalendarSearchAIContextProvider(Func<Task<string[]>> loadNextThreeCalendarEvents) : AIContextProvider
134134
{
135-
public override async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
135+
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
136136
{
137137
var events = await loadNextThreeCalendarEvents();
138138

@@ -179,7 +179,7 @@ public AggregatingAIContextProvider(ProviderFactory[] providerFactories, JsonEle
179179
.ToList();
180180
}
181181

182-
public override async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
182+
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
183183
{
184184
// Invoke all the sub providers.
185185
var tasks = this._providers.Select(provider => provider.InvokingAsync(context, cancellationToken).AsTask());

dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs

Lines changed: 99 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
using System;
44
using System.Collections.Generic;
5+
using System.Linq;
56
using System.Text.Json;
67
using System.Threading;
78
using System.Threading.Tasks;
@@ -31,6 +32,25 @@ namespace Microsoft.Agents.AI;
3132
/// </remarks>
3233
public abstract class AIContextProvider
3334
{
35+
private readonly string _sourceName;
36+
37+
/// <summary>
38+
/// Initializes a new instance of the <see cref="AIContextProvider"/> class.
39+
/// </summary>
40+
protected AIContextProvider()
41+
{
42+
this._sourceName = this.GetType().FullName!;
43+
}
44+
45+
/// <summary>
46+
/// Initializes a new instance of the <see cref="AIContextProvider"/> class with the specified source name.
47+
/// </summary>
48+
/// <param name="sourceName">The source name to stamp on <see cref="ChatMessage.AdditionalProperties"/> for each messages produced by the <see cref="AIContextProvider"/>.</param>
49+
protected AIContextProvider(string sourceName)
50+
{
51+
this._sourceName = sourceName;
52+
}
53+
3454
/// <summary>
3555
/// Called at the start of agent invocation to provide additional context.
3656
/// </summary>
@@ -48,7 +68,81 @@ public abstract class AIContextProvider
4868
/// </list>
4969
/// </para>
5070
/// </remarks>
51-
public abstract ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default);
71+
public async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
72+
{
73+
var aiContext = await this.InvokingCoreAsync(context, cancellationToken).ConfigureAwait(false);
74+
if (aiContext.Messages is null)
75+
{
76+
return aiContext;
77+
}
78+
79+
aiContext.Messages = aiContext.Messages.Select(message =>
80+
{
81+
if (message.AdditionalProperties != null
82+
// Check if the message was already tagged with this provider's source type
83+
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out var messageSourceType)
84+
&& messageSourceType is AgentRequestMessageSourceType typedMessageSourceType
85+
&& typedMessageSourceType == AgentRequestMessageSourceType.AIContextProvider
86+
// Check if the message was already tagged with this provider's source
87+
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out var messageSource)
88+
&& messageSource is string typedMessageSource
89+
&& typedMessageSource == this._sourceName)
90+
{
91+
return message;
92+
}
93+
94+
message = message.Clone();
95+
message.AdditionalProperties ??= new();
96+
message.AdditionalProperties[AgentRequestMessageSourceType.AdditionalPropertiesKey] = AgentRequestMessageSourceType.AIContextProvider;
97+
message.AdditionalProperties[AgentRequestMessageSource.AdditionalPropertiesKey] = this._sourceName;
98+
return message;
99+
}).ToList();
100+
101+
return aiContext;
102+
}
103+
104+
/// <summary>
105+
/// Called at the start of agent invocation to provide additional context.
106+
/// </summary>
107+
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
108+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
109+
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="AIContext"/> with additional context to be used by the agent during this invocation.</returns>
110+
/// <remarks>
111+
/// <para>
112+
/// Implementers can load any additional context required at this time, such as:
113+
/// <list type="bullet">
114+
/// <item><description>Retrieving relevant information from knowledge bases</description></item>
115+
/// <item><description>Adding system instructions or prompts</description></item>
116+
/// <item><description>Providing function tools for the current invocation</description></item>
117+
/// <item><description>Injecting contextual messages from conversation history</description></item>
118+
/// </list>
119+
/// </para>
120+
/// </remarks>
121+
protected abstract ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default);
122+
123+
/// <summary>
124+
/// Called at the end of the agent invocation to process the invocation results.
125+
/// </summary>
126+
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</param>
127+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
128+
/// <returns>A task that represents the asynchronous operation.</returns>
129+
/// <remarks>
130+
/// <para>
131+
/// Implementers can use the request and response messages in the provided <paramref name="context"/> to:
132+
/// <list type="bullet">
133+
/// <item><description>Update internal state based on conversation outcomes</description></item>
134+
/// <item><description>Extract and store memories or preferences from user messages</description></item>
135+
/// <item><description>Log or audit conversation details</description></item>
136+
/// <item><description>Perform cleanup or finalization tasks</description></item>
137+
/// </list>
138+
/// </para>
139+
/// <para>
140+
/// This method is called regardless of whether the invocation succeeded or failed.
141+
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
142+
/// </para>
143+
/// </remarks>
144+
public ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
145+
=> this.InvokedCoreAsync(context, cancellationToken);
52146

53147
/// <summary>
54148
/// Called at the end of the agent invocation to process the invocation results.
@@ -71,7 +165,7 @@ public abstract class AIContextProvider
71165
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
72166
/// </para>
73167
/// </remarks>
74-
public virtual ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
168+
protected virtual ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
75169
=> default;
76170

77171
/// <summary>
@@ -117,7 +211,7 @@ public virtual JsonElement Serialize(JsonSerializerOptions? jsonSerializerOption
117211
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
118212

119213
/// <summary>
120-
/// Contains the context information provided to <see cref="InvokingAsync(InvokingContext, CancellationToken)"/>.
214+
/// Contains the context information provided to <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>.
121215
/// </summary>
122216
/// <remarks>
123217
/// This class provides context about the invocation before the underlying AI model is invoked, including the messages
@@ -163,7 +257,7 @@ public InvokingContext(
163257
}
164258

165259
/// <summary>
166-
/// Contains the context information provided to <see cref="InvokedAsync(InvokedContext, CancellationToken)"/>.
260+
/// Contains the context information provided to <see cref="InvokedCoreAsync(InvokedContext, CancellationToken)"/>.
167261
/// </summary>
168262
/// <remarks>
169263
/// This class provides context about a completed agent invocation, including both the
@@ -178,18 +272,15 @@ public sealed class InvokedContext
178272
/// <param name="agent">The agent being invoked.</param>
179273
/// <param name="session">The session associated with the agent invocation.</param>
180274
/// <param name="requestMessages">The caller provided messages that were used by the agent for this invocation.</param>
181-
/// <param name="aiContextProviderMessages">The messages provided by the <see cref="AIContextProvider"/> for this invocation, if any.</param>
182275
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
183276
public InvokedContext(
184277
AIAgent agent,
185278
AgentSession? session,
186-
IEnumerable<ChatMessage> requestMessages,
187-
IEnumerable<ChatMessage>? aiContextProviderMessages)
279+
IEnumerable<ChatMessage> requestMessages)
188280
{
189281
this.Agent = Throw.IfNull(agent);
190282
this.Session = session;
191283
this.RequestMessages = Throw.IfNull(requestMessages);
192-
this.AIContextProviderMessages = aiContextProviderMessages;
193284
}
194285

195286
/// <summary>
@@ -211,15 +302,6 @@ public InvokedContext(
211302
/// </value>
212303
public IEnumerable<ChatMessage> RequestMessages { get; set { field = Throw.IfNull(value); } }
213304

214-
/// <summary>
215-
/// Gets the messages provided by the <see cref="AIContextProvider"/> for this invocation, if any.
216-
/// </summary>
217-
/// <value>
218-
/// A collection of <see cref="ChatMessage"/> instances that were provided by the <see cref="AIContextProvider"/>,
219-
/// and were used by the agent as part of the invocation.
220-
/// </value>
221-
public IEnumerable<ChatMessage>? AIContextProviderMessages { get; set; }
222-
223305
/// <summary>
224306
/// Gets the collection of response messages generated during this invocation if the invocation succeeded.
225307
/// </summary>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Microsoft.Extensions.AI;
4+
5+
namespace Microsoft.Agents.AI;
6+
7+
/// <summary>
8+
/// Provides a constant for the key used to store the source of the agent request message.
9+
/// </summary>
10+
public static class AgentRequestMessageSource
11+
{
12+
/// <summary>
13+
/// Provides the key used in <see cref="ChatMessage.AdditionalProperties"/> to store the source of the agent request message.
14+
/// </summary>
15+
public static readonly string AdditionalPropertiesKey = "Agent.RequestMessageSource";
16+
}

0 commit comments

Comments
 (0)