forked from microsoft/agent-framework
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAIContextProviderChatClient.cs
More file actions
217 lines (189 loc) · 8.43 KB
/
Copy pathAIContextProviderChatClient.cs
File metadata and controls
217 lines (189 loc) · 8.43 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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// A delegating chat client that enriches input messages, tools, and instructions by invoking a pipeline of
/// <see cref="AIContextProvider"/> instances before delegating to the inner chat client, and notifies those
/// providers after the inner client completes.
/// </summary>
/// <remarks>
/// <para>
/// This chat client must be used within the context of a running <see cref="AIAgent"/>. It retrieves the current
/// agent and session from <see cref="AIAgent.CurrentRunContext"/>, which is set automatically when an agent's
/// <see cref="AIAgent.RunAsync(IEnumerable{ChatMessage}, AgentSession?, AgentRunOptions?, CancellationToken)"/> or
/// <see cref="AIAgent.RunStreamingAsync(IEnumerable{ChatMessage}, AgentSession?, AgentRunOptions?, CancellationToken)"/> method is called.
/// An <see cref="InvalidOperationException"/> is thrown if no run context is available.
/// </para>
/// </remarks>
internal sealed class AIContextProviderChatClient : DelegatingChatClient
{
private readonly IReadOnlyList<AIContextProvider> _providers;
/// <summary>
/// Initializes a new instance of the <see cref="AIContextProviderChatClient"/> class.
/// </summary>
/// <param name="innerClient">The underlying chat client that will handle the core operations.</param>
/// <param name="providers">The AI context providers to invoke before and after the inner chat client.</param>
public AIContextProviderChatClient(IChatClient innerClient, IReadOnlyList<AIContextProvider> providers)
: base(innerClient)
{
Throw.IfNull(providers);
if (providers.Count == 0)
{
Throw.ArgumentException(nameof(providers), "At least one AIContextProvider must be provided.");
}
this._providers = providers;
}
/// <inheritdoc/>
public override async Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
var runContext = GetRequiredRunContext();
var (enrichedMessages, enrichedOptions) = await this.InvokeProvidersAsync(runContext, messages, options, cancellationToken).ConfigureAwait(false);
ChatResponse response;
try
{
response = await base.GetResponseAsync(enrichedMessages, enrichedOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
await this.NotifyProvidersOfFailureAsync(runContext, enrichedMessages, ex, cancellationToken).ConfigureAwait(false);
throw;
}
await this.NotifyProvidersOfSuccessAsync(runContext, enrichedMessages, response.Messages, cancellationToken).ConfigureAwait(false);
return response;
}
/// <inheritdoc/>
public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var runContext = GetRequiredRunContext();
var (enrichedMessages, enrichedOptions) = await this.InvokeProvidersAsync(runContext, messages, options, cancellationToken).ConfigureAwait(false);
List<ChatResponseUpdate> responseUpdates = [];
IAsyncEnumerator<ChatResponseUpdate> enumerator;
try
{
enumerator = base.GetStreamingResponseAsync(enrichedMessages, enrichedOptions, cancellationToken).GetAsyncEnumerator(cancellationToken);
}
catch (Exception ex)
{
await this.NotifyProvidersOfFailureAsync(runContext, enrichedMessages, ex, cancellationToken).ConfigureAwait(false);
throw;
}
bool hasUpdates;
try
{
hasUpdates = await enumerator.MoveNextAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
await this.NotifyProvidersOfFailureAsync(runContext, enrichedMessages, ex, cancellationToken).ConfigureAwait(false);
throw;
}
while (hasUpdates)
{
var update = enumerator.Current;
responseUpdates.Add(update);
yield return update;
try
{
hasUpdates = await enumerator.MoveNextAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
await this.NotifyProvidersOfFailureAsync(runContext, enrichedMessages, ex, cancellationToken).ConfigureAwait(false);
throw;
}
}
var chatResponse = responseUpdates.ToChatResponse();
await this.NotifyProvidersOfSuccessAsync(runContext, enrichedMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Gets the current <see cref="AgentRunContext"/>, throwing if not available.
/// </summary>
private static AgentRunContext GetRequiredRunContext()
{
return AIAgent.CurrentRunContext
?? throw new InvalidOperationException(
$"{nameof(AIContextProviderChatClient)} can only be used within the context of a running AIAgent. " +
"Ensure that the chat client is being invoked as part of an AIAgent.RunAsync or AIAgent.RunStreamingAsync call.");
}
/// <summary>
/// Invokes each provider's <see cref="AIContextProvider.InvokingAsync"/> in sequence,
/// accumulating context (messages, tools, instructions) from each.
/// </summary>
private async Task<(IEnumerable<ChatMessage> Messages, ChatOptions? Options)> InvokeProvidersAsync(
AgentRunContext runContext,
IEnumerable<ChatMessage> messages,
ChatOptions? options,
CancellationToken cancellationToken)
{
var aiContext = new AIContext
{
Instructions = options?.Instructions,
Messages = messages,
Tools = options?.Tools
};
foreach (var provider in this._providers)
{
var invokingContext = new AIContextProvider.InvokingContext(runContext.Agent, runContext.Session, aiContext);
aiContext = await provider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
}
// Materialize the accumulated context back into messages and options.
// Clone options to avoid mutating the caller's instance across calls.
options = options?.Clone();
var enrichedMessages = aiContext.Messages ?? [];
var tools = aiContext.Tools as IList<AITool> ?? aiContext.Tools?.ToList();
if (options?.Tools is { Count: > 0 } || tools is { Count: > 0 })
{
options ??= new();
options.Tools = tools;
}
if (options?.Instructions is not null || aiContext.Instructions is not null)
{
options ??= new();
options.Instructions = aiContext.Instructions;
}
return (enrichedMessages, options);
}
/// <summary>
/// Notifies each provider of a successful invocation.
/// </summary>
private async Task NotifyProvidersOfSuccessAsync(
AgentRunContext runContext,
IEnumerable<ChatMessage> requestMessages,
IEnumerable<ChatMessage> responseMessages,
CancellationToken cancellationToken)
{
var invokedContext = new AIContextProvider.InvokedContext(runContext.Agent, runContext.Session, requestMessages, responseMessages);
foreach (var provider in this._providers)
{
await provider.InvokedAsync(invokedContext, cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Notifies each provider of a failed invocation.
/// </summary>
private async Task NotifyProvidersOfFailureAsync(
AgentRunContext runContext,
IEnumerable<ChatMessage> requestMessages,
Exception exception,
CancellationToken cancellationToken)
{
var invokedContext = new AIContextProvider.InvokedContext(runContext.Agent, runContext.Session, requestMessages, exception);
foreach (var provider in this._providers)
{
await provider.InvokedAsync(invokedContext, cancellationToken).ConfigureAwait(false);
}
}
}