diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseExtensions.cs index c5ffd07837..01328cdab1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseExtensions.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; @@ -116,16 +116,15 @@ public static class AgentRunResponseExtensions { _ = Throw.IfNull(updates); - AgentRunResponse response = new(); + AgentRunResponseDetails additionalDetails = new(); + ChatResponse chatResponse = + AsChatResponseUpdatesWithAdditionalDetails(updates, additionalDetails) + .ToChatResponse(); - foreach (var update in updates) + return new AgentRunResponse(chatResponse) { - ProcessUpdate(update, response); - } - - FinalizeResponse(response); - - return response; + AgentId = additionalDetails.AgentId, + }; } /// @@ -159,193 +158,52 @@ public static class AgentRunResponseExtensions IAsyncEnumerable updates, CancellationToken cancellationToken) { - AgentRunResponse response = new(); + AgentRunResponseDetails additionalDetails = new(); + ChatResponse chatResponse = await + AsChatResponseUpdatesWithAdditionalDetailsAsync(updates, additionalDetails, cancellationToken) + .ToChatResponseAsync(cancellationToken) + .ConfigureAwait(false); - await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false)) + return new AgentRunResponse(chatResponse) { - ProcessUpdate(update, response); - } - - FinalizeResponse(response); - - return response; + AgentId = additionalDetails.AgentId, + }; } } - /// Coalesces sequential content elements. - internal static void CoalesceTextContent(List contents) + private static IEnumerable AsChatResponseUpdatesWithAdditionalDetails( + IEnumerable updates, + AgentRunResponseDetails additionalDetails) { - Coalesce(contents, static text => new(text)); - Coalesce(contents, static text => new(text)); - - // This implementation relies on TContent's ToString returning its exact text. - static void Coalesce(List contents, Func fromText) - where TContent : AIContent + foreach (var update in updates) { - StringBuilder? coalescedText = null; - - // Iterate through all of the items in the list looking for contiguous items that can be coalesced. - int start = 0; - while (start < contents.Count - 1) - { - // We need at least two TextContents in a row to be able to coalesce. - if (contents[start] is not TContent firstText) - { - start++; - continue; - } - - if (contents[start + 1] is not TContent secondText) - { - start += 2; - continue; - } - - // Append the text from those nodes and continue appending subsequent TextContents until we run out. - // We null out nodes as their text is appended so that we can later remove them all in one O(N) operation. - coalescedText ??= new(); - _ = coalescedText.Clear().Append(firstText).Append(secondText); - contents[start + 1] = null!; - int i = start + 2; - for (; i < contents.Count && contents[i] is TContent next; i++) - { - _ = coalescedText.Append(next); - contents[i] = null!; - } - - // Store the replacement node. We inherit the properties of the first text node. We don't - // currently propagate additional properties from the subsequent nodes. If we ever need to, - // we can add that here. - var newContent = fromText(coalescedText.ToString()); - contents[start] = newContent; - newContent.AdditionalProperties = firstText.AdditionalProperties?.Clone(); - - start = i; - } - - // Remove all of the null slots left over from the coalescing process. - _ = contents.RemoveAll(u => u is null); + UpdateAdditionalDetails(update, additionalDetails); + yield return update.AsChatResponseUpdate(); } } - /// Finalizes the object. - private static void FinalizeResponse(AgentRunResponse response) + private static async IAsyncEnumerable AsChatResponseUpdatesWithAdditionalDetailsAsync( + IAsyncEnumerable updates, + AgentRunResponseDetails additionalDetails, + [EnumeratorCancellation] CancellationToken cancellationToken) { - int count = response.Messages.Count; - for (int i = 0; i < count; i++) + await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false)) { - CoalesceTextContent((List)response.Messages[i].Contents); + UpdateAdditionalDetails(update, additionalDetails); + yield return update.AsChatResponseUpdate(); } } - /// Processes the , incorporating its contents into . - /// The update to process. - /// The object that should be updated based on . - private static void ProcessUpdate(AgentRunResponseUpdate update, AgentRunResponse response) + private static void UpdateAdditionalDetails(AgentRunResponseUpdate update, AgentRunResponseDetails details) { - // If there is no message created yet, or if the last update we saw had a different - // message ID or role than the newest update, create a new message. - ChatMessage message; - var isNewMessage = false; - if (response.Messages.Count == 0) - { - isNewMessage = true; - } - else if (update.MessageId is { Length: > 0 } updateMessageId - && response.Messages[response.Messages.Count - 1].MessageId is string lastMessageId - && updateMessageId != lastMessageId) - { - isNewMessage = true; - } - else if (update.Role is { } updateRole - && response.Messages[response.Messages.Count - 1].Role is { } lastRole - && updateRole != lastRole) - { - isNewMessage = true; - } - - if (isNewMessage) - { - message = new(ChatRole.Assistant, []); - response.Messages.Add(message); - } - else - { - message = response.Messages[response.Messages.Count - 1]; - } - - // Some members on AgentRunResponseUpdate map to members of ChatMessage. - // Incorporate those into the latest message; in cases where the message - // stores a single value, prefer the latest update's value over anything - // stored in the message. - if (update.AuthorName is not null) - { - message.AuthorName = update.AuthorName; - } - - if (message.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > message.CreatedAt)) - { - message.CreatedAt = update.CreatedAt; - } - - if (update.Role is ChatRole role) - { - message.Role = role; - } - - if (update.MessageId is { Length: > 0 }) - { - // Note that this must come after the message checks earlier, as they depend - // on this value for change detection. - message.MessageId = update.MessageId; - } - - foreach (var content in update.Contents) - { - switch (content) - { - // Usage content is treated specially and propagated to the response's Usage. - case UsageContent usage: - (response.Usage ??= new()).Add(usage.Details); - break; - - default: - message.Contents.Add(content); - break; - } - } - - // Other members on a AgentRunResponseUpdate map to members of the AgentRunResponse. - // Update the response object with those, preferring the values from later updates. - if (update.AgentId is { Length: > 0 }) { - response.AgentId = update.AgentId; - } - - if (update.ResponseId is { Length: > 0 }) - { - response.ResponseId = update.ResponseId; - } - - if (response.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > response.CreatedAt)) - { - response.CreatedAt = update.CreatedAt; - } - - if (update.AdditionalProperties is not null) - { - if (response.AdditionalProperties is null) - { - response.AdditionalProperties = new(update.AdditionalProperties); - } - else - { - foreach (var item in update.AdditionalProperties) - { - response.AdditionalProperties[item.Key] = item.Value; - } - } + details.AgentId = update.AgentId; } } + + private sealed class AgentRunResponseDetails + { + public string? AgentId { get; set; } + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs index a653cf80f5..cc3247416a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs @@ -207,7 +207,7 @@ public class AgentRunResponseUpdateExtensionsTests Assert.Equal("Hello, world!", Assert.IsType(Assert.Single(Assert.Single(response.Messages).Contents)).Text); } - [Theory] + [Theory(Skip = "Reactive once M.E.AI 9.10 is imported")] [InlineData(false)] [InlineData(true)] public async Task ToAgentRunResponse_AlternativeTimestampsAsync(bool useAsync) @@ -275,7 +275,7 @@ public class AgentRunResponseUpdateExtensionsTests } } - [Theory] + [Theory(Skip = "Reactive once M.E.AI 9.10 is imported")] [MemberData(nameof(ToAgentRunResponse_TimestampFolding_MemberData))] public async Task ToAgentRunResponse_TimestampFoldingAsync(bool useAsync, string? timestamp1, string? timestamp2, string? expectedTimestamp) {