|
2 | 2 |
|
3 | 3 | using System; |
4 | 4 | using System.Collections.Generic; |
5 | | -using System.Text; |
| 5 | +using System.Runtime.CompilerServices; |
6 | 6 | using System.Threading; |
7 | 7 | using System.Threading.Tasks; |
8 | 8 | using Microsoft.Extensions.AI; |
@@ -116,16 +116,15 @@ public static AgentRunResponse ToAgentRunResponse( |
116 | 116 | { |
117 | 117 | _ = Throw.IfNull(updates); |
118 | 118 |
|
119 | | - AgentRunResponse response = new(); |
| 119 | + AgentRunResponseDetails additionalDetails = new(); |
| 120 | + ChatResponse chatResponse = |
| 121 | + AsChatResponseUpdatesWithAdditionalDetails(updates, additionalDetails) |
| 122 | + .ToChatResponse(); |
120 | 123 |
|
121 | | - foreach (var update in updates) |
| 124 | + return new AgentRunResponse(chatResponse) |
122 | 125 | { |
123 | | - ProcessUpdate(update, response); |
124 | | - } |
125 | | - |
126 | | - FinalizeResponse(response); |
127 | | - |
128 | | - return response; |
| 126 | + AgentId = additionalDetails.AgentId, |
| 127 | + }; |
129 | 128 | } |
130 | 129 |
|
131 | 130 | /// <summary> |
@@ -159,193 +158,52 @@ static async Task<AgentRunResponse> ToAgentRunResponseAsync( |
159 | 158 | IAsyncEnumerable<AgentRunResponseUpdate> updates, |
160 | 159 | CancellationToken cancellationToken) |
161 | 160 | { |
162 | | - AgentRunResponse response = new(); |
| 161 | + AgentRunResponseDetails additionalDetails = new(); |
| 162 | + ChatResponse chatResponse = await |
| 163 | + AsChatResponseUpdatesWithAdditionalDetailsAsync(updates, additionalDetails, cancellationToken) |
| 164 | + .ToChatResponseAsync(cancellationToken) |
| 165 | + .ConfigureAwait(false); |
163 | 166 |
|
164 | | - await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false)) |
| 167 | + return new AgentRunResponse(chatResponse) |
165 | 168 | { |
166 | | - ProcessUpdate(update, response); |
167 | | - } |
168 | | - |
169 | | - FinalizeResponse(response); |
170 | | - |
171 | | - return response; |
| 169 | + AgentId = additionalDetails.AgentId, |
| 170 | + }; |
172 | 171 | } |
173 | 172 | } |
174 | 173 |
|
175 | | - /// <summary>Coalesces sequential <see cref="AIContent"/> content elements.</summary> |
176 | | - internal static void CoalesceTextContent(List<AIContent> contents) |
| 174 | + private static IEnumerable<ChatResponseUpdate> AsChatResponseUpdatesWithAdditionalDetails( |
| 175 | + IEnumerable<AgentRunResponseUpdate> updates, |
| 176 | + AgentRunResponseDetails additionalDetails) |
177 | 177 | { |
178 | | - Coalesce<TextContent>(contents, static text => new(text)); |
179 | | - Coalesce<TextReasoningContent>(contents, static text => new(text)); |
180 | | - |
181 | | - // This implementation relies on TContent's ToString returning its exact text. |
182 | | - static void Coalesce<TContent>(List<AIContent> contents, Func<string, TContent> fromText) |
183 | | - where TContent : AIContent |
| 178 | + foreach (var update in updates) |
184 | 179 | { |
185 | | - StringBuilder? coalescedText = null; |
186 | | - |
187 | | - // Iterate through all of the items in the list looking for contiguous items that can be coalesced. |
188 | | - int start = 0; |
189 | | - while (start < contents.Count - 1) |
190 | | - { |
191 | | - // We need at least two TextContents in a row to be able to coalesce. |
192 | | - if (contents[start] is not TContent firstText) |
193 | | - { |
194 | | - start++; |
195 | | - continue; |
196 | | - } |
197 | | - |
198 | | - if (contents[start + 1] is not TContent secondText) |
199 | | - { |
200 | | - start += 2; |
201 | | - continue; |
202 | | - } |
203 | | - |
204 | | - // Append the text from those nodes and continue appending subsequent TextContents until we run out. |
205 | | - // We null out nodes as their text is appended so that we can later remove them all in one O(N) operation. |
206 | | - coalescedText ??= new(); |
207 | | - _ = coalescedText.Clear().Append(firstText).Append(secondText); |
208 | | - contents[start + 1] = null!; |
209 | | - int i = start + 2; |
210 | | - for (; i < contents.Count && contents[i] is TContent next; i++) |
211 | | - { |
212 | | - _ = coalescedText.Append(next); |
213 | | - contents[i] = null!; |
214 | | - } |
215 | | - |
216 | | - // Store the replacement node. We inherit the properties of the first text node. We don't |
217 | | - // currently propagate additional properties from the subsequent nodes. If we ever need to, |
218 | | - // we can add that here. |
219 | | - var newContent = fromText(coalescedText.ToString()); |
220 | | - contents[start] = newContent; |
221 | | - newContent.AdditionalProperties = firstText.AdditionalProperties?.Clone(); |
222 | | - |
223 | | - start = i; |
224 | | - } |
225 | | - |
226 | | - // Remove all of the null slots left over from the coalescing process. |
227 | | - _ = contents.RemoveAll(u => u is null); |
| 180 | + UpdateAdditionalDetails(update, additionalDetails); |
| 181 | + yield return update.AsChatResponseUpdate(); |
228 | 182 | } |
229 | 183 | } |
230 | 184 |
|
231 | | - /// <summary>Finalizes the <paramref name="response"/> object.</summary> |
232 | | - private static void FinalizeResponse(AgentRunResponse response) |
| 185 | + private static async IAsyncEnumerable<ChatResponseUpdate> AsChatResponseUpdatesWithAdditionalDetailsAsync( |
| 186 | + IAsyncEnumerable<AgentRunResponseUpdate> updates, |
| 187 | + AgentRunResponseDetails additionalDetails, |
| 188 | + [EnumeratorCancellation] CancellationToken cancellationToken) |
233 | 189 | { |
234 | | - int count = response.Messages.Count; |
235 | | - for (int i = 0; i < count; i++) |
| 190 | + await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false)) |
236 | 191 | { |
237 | | - CoalesceTextContent((List<AIContent>)response.Messages[i].Contents); |
| 192 | + UpdateAdditionalDetails(update, additionalDetails); |
| 193 | + yield return update.AsChatResponseUpdate(); |
238 | 194 | } |
239 | 195 | } |
240 | 196 |
|
241 | | - /// <summary>Processes the <see cref="AgentRunResponseUpdate"/>, incorporating its contents into <paramref name="response"/>.</summary> |
242 | | - /// <param name="update">The update to process.</param> |
243 | | - /// <param name="response">The <see cref="AgentRunResponse"/> object that should be updated based on <paramref name="update"/>.</param> |
244 | | - private static void ProcessUpdate(AgentRunResponseUpdate update, AgentRunResponse response) |
| 197 | + private static void UpdateAdditionalDetails(AgentRunResponseUpdate update, AgentRunResponseDetails details) |
245 | 198 | { |
246 | | - // If there is no message created yet, or if the last update we saw had a different |
247 | | - // message ID or role than the newest update, create a new message. |
248 | | - ChatMessage message; |
249 | | - var isNewMessage = false; |
250 | | - if (response.Messages.Count == 0) |
251 | | - { |
252 | | - isNewMessage = true; |
253 | | - } |
254 | | - else if (update.MessageId is { Length: > 0 } updateMessageId |
255 | | - && response.Messages[response.Messages.Count - 1].MessageId is string lastMessageId |
256 | | - && updateMessageId != lastMessageId) |
257 | | - { |
258 | | - isNewMessage = true; |
259 | | - } |
260 | | - else if (update.Role is { } updateRole |
261 | | - && response.Messages[response.Messages.Count - 1].Role is { } lastRole |
262 | | - && updateRole != lastRole) |
263 | | - { |
264 | | - isNewMessage = true; |
265 | | - } |
266 | | - |
267 | | - if (isNewMessage) |
268 | | - { |
269 | | - message = new(ChatRole.Assistant, []); |
270 | | - response.Messages.Add(message); |
271 | | - } |
272 | | - else |
273 | | - { |
274 | | - message = response.Messages[response.Messages.Count - 1]; |
275 | | - } |
276 | | - |
277 | | - // Some members on AgentRunResponseUpdate map to members of ChatMessage. |
278 | | - // Incorporate those into the latest message; in cases where the message |
279 | | - // stores a single value, prefer the latest update's value over anything |
280 | | - // stored in the message. |
281 | | - if (update.AuthorName is not null) |
282 | | - { |
283 | | - message.AuthorName = update.AuthorName; |
284 | | - } |
285 | | - |
286 | | - if (message.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > message.CreatedAt)) |
287 | | - { |
288 | | - message.CreatedAt = update.CreatedAt; |
289 | | - } |
290 | | - |
291 | | - if (update.Role is ChatRole role) |
292 | | - { |
293 | | - message.Role = role; |
294 | | - } |
295 | | - |
296 | | - if (update.MessageId is { Length: > 0 }) |
297 | | - { |
298 | | - // Note that this must come after the message checks earlier, as they depend |
299 | | - // on this value for change detection. |
300 | | - message.MessageId = update.MessageId; |
301 | | - } |
302 | | - |
303 | | - foreach (var content in update.Contents) |
304 | | - { |
305 | | - switch (content) |
306 | | - { |
307 | | - // Usage content is treated specially and propagated to the response's Usage. |
308 | | - case UsageContent usage: |
309 | | - (response.Usage ??= new()).Add(usage.Details); |
310 | | - break; |
311 | | - |
312 | | - default: |
313 | | - message.Contents.Add(content); |
314 | | - break; |
315 | | - } |
316 | | - } |
317 | | - |
318 | | - // Other members on a AgentRunResponseUpdate map to members of the AgentRunResponse. |
319 | | - // Update the response object with those, preferring the values from later updates. |
320 | | - |
321 | 199 | if (update.AgentId is { Length: > 0 }) |
322 | 200 | { |
323 | | - response.AgentId = update.AgentId; |
324 | | - } |
325 | | - |
326 | | - if (update.ResponseId is { Length: > 0 }) |
327 | | - { |
328 | | - response.ResponseId = update.ResponseId; |
329 | | - } |
330 | | - |
331 | | - if (response.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > response.CreatedAt)) |
332 | | - { |
333 | | - response.CreatedAt = update.CreatedAt; |
| 201 | + details.AgentId = update.AgentId; |
334 | 202 | } |
| 203 | + } |
335 | 204 |
|
336 | | - if (update.AdditionalProperties is not null) |
337 | | - { |
338 | | - if (response.AdditionalProperties is null) |
339 | | - { |
340 | | - response.AdditionalProperties = new(update.AdditionalProperties); |
341 | | - } |
342 | | - else |
343 | | - { |
344 | | - foreach (var item in update.AdditionalProperties) |
345 | | - { |
346 | | - response.AdditionalProperties[item.Key] = item.Value; |
347 | | - } |
348 | | - } |
349 | | - } |
| 205 | + private sealed class AgentRunResponseDetails |
| 206 | + { |
| 207 | + public string? AgentId { get; set; } |
350 | 208 | } |
351 | 209 | } |
0 commit comments