diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index 4bb67980439..cc911fd64bb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -3,6 +3,7 @@ ## NOT YET RELEASED - Added new `ChatResponseFormat.ForJsonSchema` overloads that export a JSON schema from a .NET type. +- Updated `TextReasoningContent` to include `ProtectedData` for representing encrypted/redacted content. ## 9.9.0 diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index b9909857be2..933c6412c20 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -186,17 +186,17 @@ static async Task ToChatResponseAsync( /// Coalesces sequential content elements. internal static void CoalesceTextContent(IList contents) { - Coalesce(contents, mergeSingle: false, static (contents, start, end) => - new(MergeText(contents, start, end)) - { - AdditionalProperties = contents[start].AdditionalProperties?.Clone() - }); - - Coalesce(contents, mergeSingle: false, static (contents, start, end) => - new(MergeText(contents, start, end)) - { - AdditionalProperties = contents[start].AdditionalProperties?.Clone() - }); + Coalesce( + contents, + mergeSingle: false, + canMerge: null, + static (contents, start, end) => new(MergeText(contents, start, end)) { AdditionalProperties = contents[start].AdditionalProperties?.Clone() }); + + Coalesce( + contents, + mergeSingle: false, + canMerge: static (r1, r2) => string.IsNullOrEmpty(r1.ProtectedData), // we allow merging if the first item has no ProtectedData, even if the second does + static (contents, start, end) => new(MergeText(contents, start, end)) { AdditionalProperties = contents[start].AdditionalProperties?.Clone() }); static string MergeText(IList contents, int start, int end) { @@ -209,7 +209,11 @@ static string MergeText(IList contents, int start, int end) return sb.ToString(); } - static void Coalesce(IList contents, bool mergeSingle, Func, int, int, TContent> merge) + static void Coalesce( + IList contents, + bool mergeSingle, + Func? canMerge, + Func, int, int, TContent> merge) where TContent : AIContent { // Iterate through all of the items in the list looking for contiguous items that can be coalesced. @@ -224,9 +228,11 @@ static void Coalesce(IList contents, bool mergeSingle, Func // Iterate until we find a non-coalescable item. int i = start + 1; - while (i < contents.Count && TryAsCoalescable(contents[i], out _)) + TContent prev = firstContent; + while (i < contents.Count && TryAsCoalescable(contents[i], out TContent? next) && (canMerge is null || canMerge(prev, next))) { i++; + prev = next; } // If there's only one item in the run, and we don't want to merge single items, skip it. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs index ccf84af2e3d..345cb430dbd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs @@ -38,6 +38,22 @@ public string Text set => _text = value; } + /// Gets or sets an optional opaque blob of data associated with this reasoning content. + /// + /// + /// This property is used to store data from a provider that should be roundtripped back to the provider but that is not + /// intended for human consumption. It is often encrypted or otherwise redacted information that is only intended to be + /// sent back to the provider and not displayed to the user. It's possible for a to contain + /// only and have an empty property. This data also may be associated with + /// the corresponding , acting as a validation signature for it. + /// + /// + /// Note that whereas can be provider agnostic, + /// is provider-specific, and is likely to only be understood by the provider that created it. + /// + /// + public string? ProtectedData { get; set; } + /// public override string ToString() => Text; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 034b5787eab..24239ee52b3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -2430,6 +2430,10 @@ { "Member": "string Microsoft.Extensions.AI.TextReasoningContent.Text { get; set; }", "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.TextReasoningContent.ProtectedData { get; set; }", + "Stage": "Stable" } ] }, diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index c025ae14d8a..0849b0c414f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -17,6 +17,7 @@ #pragma warning disable S907 // "goto" statement should not be used #pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable S3254 // Default parameter values should not be passed as arguments #pragma warning disable S3604 // Member initializer values should not be redundant #pragma warning disable SA1202 // Elements should be ordered by access #pragma warning disable SA1204 // Static elements should appear before instance elements @@ -149,8 +150,12 @@ internal static IEnumerable ToChatMessages(IEnumerable)message.Contents).AddRange(ToAIContents(messageItem.Content)); break; - case ReasoningResponseItem reasoningItem when reasoningItem.GetSummaryText() is string summary: - message.Contents.Add(new TextReasoningContent(summary) { RawRepresentation = outputItem }); + case ReasoningResponseItem reasoningItem: + message.Contents.Add(new TextReasoningContent(reasoningItem.GetSummaryText()) + { + ProtectedData = reasoningItem.EncryptedContent, + RawRepresentation = outputItem, + }); break; case FunctionCallResponseItem functionCall: @@ -627,7 +632,9 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable