Skip to content

Commit 3176d4c

Browse files
authored
Add TextReasoningContent.ProtectedData (#6784)
1 parent 9a68639 commit 3176d4c

File tree

6 files changed

+57
-16
lines changed

6 files changed

+57
-16
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## NOT YET RELEASED
44

55
- Added new `ChatResponseFormat.ForJsonSchema` overloads that export a JSON schema from a .NET type.
6+
- Updated `TextReasoningContent` to include `ProtectedData` for representing encrypted/redacted content.
67

78
## 9.9.0
89

src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -186,17 +186,17 @@ static async Task<ChatResponse> ToChatResponseAsync(
186186
/// <summary>Coalesces sequential <see cref="AIContent"/> content elements.</summary>
187187
internal static void CoalesceTextContent(IList<AIContent> contents)
188188
{
189-
Coalesce<TextContent>(contents, mergeSingle: false, static (contents, start, end) =>
190-
new(MergeText(contents, start, end))
191-
{
192-
AdditionalProperties = contents[start].AdditionalProperties?.Clone()
193-
});
194-
195-
Coalesce<TextReasoningContent>(contents, mergeSingle: false, static (contents, start, end) =>
196-
new(MergeText(contents, start, end))
197-
{
198-
AdditionalProperties = contents[start].AdditionalProperties?.Clone()
199-
});
189+
Coalesce<TextContent>(
190+
contents,
191+
mergeSingle: false,
192+
canMerge: null,
193+
static (contents, start, end) => new(MergeText(contents, start, end)) { AdditionalProperties = contents[start].AdditionalProperties?.Clone() });
194+
195+
Coalesce<TextReasoningContent>(
196+
contents,
197+
mergeSingle: false,
198+
canMerge: static (r1, r2) => string.IsNullOrEmpty(r1.ProtectedData), // we allow merging if the first item has no ProtectedData, even if the second does
199+
static (contents, start, end) => new(MergeText(contents, start, end)) { AdditionalProperties = contents[start].AdditionalProperties?.Clone() });
200200

201201
static string MergeText(IList<AIContent> contents, int start, int end)
202202
{
@@ -209,7 +209,11 @@ static string MergeText(IList<AIContent> contents, int start, int end)
209209
return sb.ToString();
210210
}
211211

212-
static void Coalesce<TContent>(IList<AIContent> contents, bool mergeSingle, Func<IList<AIContent>, int, int, TContent> merge)
212+
static void Coalesce<TContent>(
213+
IList<AIContent> contents,
214+
bool mergeSingle,
215+
Func<TContent, TContent, bool>? canMerge,
216+
Func<IList<AIContent>, int, int, TContent> merge)
213217
where TContent : AIContent
214218
{
215219
// Iterate through all of the items in the list looking for contiguous items that can be coalesced.
@@ -224,9 +228,11 @@ static void Coalesce<TContent>(IList<AIContent> contents, bool mergeSingle, Func
224228

225229
// Iterate until we find a non-coalescable item.
226230
int i = start + 1;
227-
while (i < contents.Count && TryAsCoalescable(contents[i], out _))
231+
TContent prev = firstContent;
232+
while (i < contents.Count && TryAsCoalescable(contents[i], out TContent? next) && (canMerge is null || canMerge(prev, next)))
228233
{
229234
i++;
235+
prev = next;
230236
}
231237

232238
// If there's only one item in the run, and we don't want to merge single items, skip it.

src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ public string Text
3838
set => _text = value;
3939
}
4040

41+
/// <summary>Gets or sets an optional opaque blob of data associated with this reasoning content.</summary>
42+
/// <remarks>
43+
/// <para>
44+
/// This property is used to store data from a provider that should be roundtripped back to the provider but that is not
45+
/// intended for human consumption. It is often encrypted or otherwise redacted information that is only intended to be
46+
/// sent back to the provider and not displayed to the user. It's possible for a <see cref="TextReasoningContent"/> to contain
47+
/// only <see cref="ProtectedData"/> and have an empty <see cref="Text"/> property. This data also may be associated with
48+
/// the corresponding <see cref="Text"/>, acting as a validation signature for it.
49+
/// </para>
50+
/// <para>
51+
/// Note that whereas <see cref="Text"/> can be provider agnostic, <see cref="ProtectedData"/>
52+
/// is provider-specific, and is likely to only be understood by the provider that created it.
53+
/// </para>
54+
/// </remarks>
55+
public string? ProtectedData { get; set; }
56+
4157
/// <inheritdoc/>
4258
public override string ToString() => Text;
4359

src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2430,6 +2430,10 @@
24302430
{
24312431
"Member": "string Microsoft.Extensions.AI.TextReasoningContent.Text { get; set; }",
24322432
"Stage": "Stable"
2433+
},
2434+
{
2435+
"Member": "string? Microsoft.Extensions.AI.TextReasoningContent.ProtectedData { get; set; }",
2436+
"Stage": "Stable"
24332437
}
24342438
]
24352439
},

src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#pragma warning disable S907 // "goto" statement should not be used
1818
#pragma warning disable S1067 // Expressions should not be too complex
1919
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
20+
#pragma warning disable S3254 // Default parameter values should not be passed as arguments
2021
#pragma warning disable S3604 // Member initializer values should not be redundant
2122
#pragma warning disable SA1202 // Elements should be ordered by access
2223
#pragma warning disable SA1204 // Static elements should appear before instance elements
@@ -149,8 +150,12 @@ internal static IEnumerable<ChatMessage> ToChatMessages(IEnumerable<ResponseItem
149150
((List<AIContent>)message.Contents).AddRange(ToAIContents(messageItem.Content));
150151
break;
151152

152-
case ReasoningResponseItem reasoningItem when reasoningItem.GetSummaryText() is string summary:
153-
message.Contents.Add(new TextReasoningContent(summary) { RawRepresentation = outputItem });
153+
case ReasoningResponseItem reasoningItem:
154+
message.Contents.Add(new TextReasoningContent(reasoningItem.GetSummaryText())
155+
{
156+
ProtectedData = reasoningItem.EncryptedContent,
157+
RawRepresentation = outputItem,
158+
});
154159
break;
155160

156161
case FunctionCallResponseItem functionCall:
@@ -626,7 +631,9 @@ internal static IEnumerable<ResponseItem> ToOpenAIResponseItems(IEnumerable<Chat
626631
break;
627632

628633
case TextReasoningContent reasoningContent:
629-
yield return ResponseItem.CreateReasoningItem(reasoningContent.Text);
634+
yield return OpenAIResponsesModelFactory.ReasoningResponseItem(
635+
encryptedContent: reasoningContent.ProtectedData,
636+
summaryText: reasoningContent.Text);
630637
break;
631638

632639
case FunctionCallContent callContent:

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/TextReasoningContentTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public void Constructor_String_PropsDefault(string? text)
1616
TextReasoningContent c = new(text);
1717
Assert.Null(c.RawRepresentation);
1818
Assert.Null(c.AdditionalProperties);
19+
Assert.Null(c.ProtectedData);
1920
Assert.Equal(text ?? string.Empty, c.Text);
2021
}
2122

@@ -46,5 +47,11 @@ public void Constructor_PropsRoundtrip()
4647
c.Text = string.Empty;
4748
Assert.Equal(string.Empty, c.Text);
4849
Assert.Equal(string.Empty, c.ToString());
50+
51+
Assert.Null(c.ProtectedData);
52+
c.ProtectedData = "protected";
53+
Assert.Equal("protected", c.ProtectedData);
54+
c.ProtectedData = null;
55+
Assert.Null(c.ProtectedData);
4956
}
5057
}

0 commit comments

Comments
 (0)