Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,17 +186,17 @@ static async Task<ChatResponse> ToChatResponseAsync(
/// <summary>Coalesces sequential <see cref="AIContent"/> content elements.</summary>
internal static void CoalesceTextContent(IList<AIContent> contents)
{
Coalesce<TextContent>(contents, mergeSingle: false, static (contents, start, end) =>
new(MergeText(contents, start, end))
{
AdditionalProperties = contents[start].AdditionalProperties?.Clone()
});

Coalesce<TextReasoningContent>(contents, mergeSingle: false, static (contents, start, end) =>
new(MergeText(contents, start, end))
{
AdditionalProperties = contents[start].AdditionalProperties?.Clone()
});
Coalesce<TextContent>(
contents,
mergeSingle: false,
canMerge: null,
static (contents, start, end) => new(MergeText(contents, start, end)) { AdditionalProperties = contents[start].AdditionalProperties?.Clone() });

Coalesce<TextReasoningContent>(
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<AIContent> contents, int start, int end)
{
Expand All @@ -209,7 +209,11 @@ static string MergeText(IList<AIContent> contents, int start, int end)
return sb.ToString();
}

static void Coalesce<TContent>(IList<AIContent> contents, bool mergeSingle, Func<IList<AIContent>, int, int, TContent> merge)
static void Coalesce<TContent>(
IList<AIContent> contents,
bool mergeSingle,
Func<TContent, TContent, bool>? canMerge,
Func<IList<AIContent>, int, int, TContent> merge)
where TContent : AIContent
{
// Iterate through all of the items in the list looking for contiguous items that can be coalesced.
Expand All @@ -224,9 +228,11 @@ static void Coalesce<TContent>(IList<AIContent> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@ public string Text
set => _text = value;
}

/// <summary>Gets or sets an optional opaque blob of data associated with this reasoning content.</summary>
/// <remarks>
/// <para>
/// 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 <see cref="TextReasoningContent"/> to contain
/// only <see cref="ProtectedData"/> and have an empty <see cref="Text"/> property. This data also may be associated with
/// the corresponding <see cref="Text"/>, acting as a validation signature for it.
/// </para>
/// <para>
/// Note that whereas <see cref="Text"/> can be provider agnostic, <see cref="ProtectedData"/>
/// is provider-specific, and is likely to only be understood by the provider that created it.
/// </para>
/// </remarks>
public string? ProtectedData { get; set; }

/// <inheritdoc/>
public override string ToString() => Text;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -149,8 +150,12 @@ internal static IEnumerable<ChatMessage> ToChatMessages(IEnumerable<ResponseItem
((List<AIContent>)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:
Expand Down Expand Up @@ -627,7 +632,9 @@ internal static IEnumerable<ResponseItem> ToOpenAIResponseItems(IEnumerable<Chat
break;

case TextReasoningContent reasoningContent:
yield return ResponseItem.CreateReasoningItem(reasoningContent.Text);
yield return OpenAIResponsesModelFactory.ReasoningResponseItem(
encryptedContent: reasoningContent.ProtectedData,
summaryText: reasoningContent.Text);
break;

case FunctionCallContent callContent:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public void Constructor_String_PropsDefault(string? text)
TextReasoningContent c = new(text);
Assert.Null(c.RawRepresentation);
Assert.Null(c.AdditionalProperties);
Assert.Null(c.ProtectedData);
Assert.Equal(text ?? string.Empty, c.Text);
}

Expand Down Expand Up @@ -46,5 +47,11 @@ public void Constructor_PropsRoundtrip()
c.Text = string.Empty;
Assert.Equal(string.Empty, c.Text);
Assert.Equal(string.Empty, c.ToString());

Assert.Null(c.ProtectedData);
c.ProtectedData = "protected";
Assert.Equal("protected", c.ProtectedData);
c.ProtectedData = null;
Assert.Null(c.ProtectedData);
}
}
Loading