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 @@ -202,7 +202,7 @@ private static ChatMessageContentItemCollection ToChatMessageContentItemCollecti
{
if (part is ReasoningSummaryTextPart text)
{
collection.Add(new TextContent(text.Text, innerContent: text));
collection.Add(new ReasoningContent(text.Text) { InnerContent = text });
}
}
return collection;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ public void VerifyToChatMessageContentFromReasoning()

// Assert
Assert.NotNull(messageContent);
Assert.Equal("Foo", messageContent.Content);
Assert.Single(messageContent.Items);
var reasoningContent = messageContent.Items[0] as ReasoningContent;
Assert.NotNull(reasoningContent);
Assert.Equal("Foo", reasoningContent.Text);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,83 @@ public void VerifyToFunctionCallContent()
Assert.NotNull(content.Arguments);
}

/// <summary>
/// Verify that ReasoningResponseItem with SummaryParts generates ReasoningContent correctly.
/// </summary>
[Fact]
public void VerifyToChatMessageContentWithReasoningResponseItem()
{
// Arrange
var reasoningResponseItem = this.CreateReasoningResponseItem("Let me think about this step by step...");

// Act
ChatMessageContent? chatMessageContent = reasoningResponseItem.ToChatMessageContent();

// Assert
Assert.NotNull(chatMessageContent);
Assert.Equal(AuthorRole.Assistant, chatMessageContent.Role);
Assert.Single(chatMessageContent.Items);

var reasoningContent = chatMessageContent.Items[0] as ReasoningContent;
Assert.NotNull(reasoningContent);
Assert.Equal("Let me think about this step by step...", reasoningContent.Text);
}

/// <summary>
/// Verify that ReasoningResponseItem converts to correct ChatMessageContentItemCollection with ReasoningContent.
/// </summary>
[Fact]
public void VerifyToChatMessageContentItemCollectionWithReasoningResponseItem()
{
// Arrange
var reasoningResponseItem = this.CreateReasoningResponseItem("Analyzing the problem...");

// Act
ChatMessageContentItemCollection collection = reasoningResponseItem.ToChatMessageContentItemCollection();

// Assert
Assert.NotNull(collection);
Assert.Single(collection);
Assert.IsType<ReasoningContent>(collection[0]);

var reasoningContent = collection[0] as ReasoningContent;
Assert.Equal("Analyzing the problem...", reasoningContent?.Text);
}

/// <summary>
/// Verify that OpenAIResponse with both ReasoningResponseItem and MessageResponseItem generates correct content types.
/// </summary>
[Fact]
public void VerifyToChatMessageContentWithMixedResponseItems()
{
// Arrange
var reasoningResponseItem = this.CreateReasoningResponseItem("Thinking about the answer...");
var messageResponseItem = ResponseItem.CreateAssistantMessageItem("Here is my response.");

OpenAIResponse mockResponse = this.CreateMockOpenAIResponse("gpt-4o-mini",
[
reasoningResponseItem,
messageResponseItem
]);

// Act
ChatMessageContent chatMessageContent = mockResponse.ToChatMessageContent();

// Assert
Assert.NotNull(chatMessageContent);
Assert.Equal(2, chatMessageContent.Items.Count);

// First item should be ReasoningContent
Assert.IsType<ReasoningContent>(chatMessageContent.Items[0]);
var reasoningContent = chatMessageContent.Items[0] as ReasoningContent;
Assert.Equal("Thinking about the answer...", reasoningContent?.Text);

// Second item should be TextContent
Assert.IsType<TextContent>(chatMessageContent.Items[1]);
var textContent = chatMessageContent.Items[1] as TextContent;
Assert.Equal("Here is my response.", textContent?.Text);
}

#region private
private OpenAIResponse CreateMockOpenAIResponse(string model, IEnumerable<ResponseItem> outputItems)
{
Expand Down Expand Up @@ -250,5 +327,112 @@ private OpenAIResponse CreateMockOpenAIResponse(string id, DateTimeOffset create
throw new InvalidOperationException("Constructor not found.");
}

private ReasoningResponseItem CreateReasoningResponseItem(string? reasoningText = null, IReadOnlyList<ReasoningSummaryPart>? summaryParts = null)
{
Type reasoningResponseItemType = typeof(ReasoningResponseItem);
Type reasoningSummaryTextPartType = typeof(ReasoningSummaryTextPart);

// If reasoningText is provided and summaryParts is not, create summaryParts with the text
if (reasoningText != null && summaryParts == null)
{
// Try to find any public static factory method or constructor that can create ReasoningSummaryTextPart
var createTextPartMethod = typeof(ReasoningSummaryPart).GetMethod(
"CreateTextPart",
BindingFlags.Static | BindingFlags.Public,
null,
[typeof(string)],
null);

if (createTextPartMethod != null)
{
var textPart = createTextPartMethod.Invoke(null, [reasoningText]) as ReasoningSummaryTextPart;
summaryParts = textPart != null ? new List<ReasoningSummaryPart> { textPart } : new List<ReasoningSummaryPart>();
}
else
{
// Try to find constructor - search for all constructors
var textPartConstructors = reasoningSummaryTextPartType.GetConstructors(
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

ConstructorInfo? textPartConstructor = null;
foreach (var ctor in textPartConstructors)
{
var parameters = ctor.GetParameters();
if (parameters.Length >= 1 && parameters[0].ParameterType == typeof(string))
{
textPartConstructor = ctor;
break;
}
}

if (textPartConstructor != null)
{
var ctorParams = textPartConstructor.GetParameters();
var args = new object?[ctorParams.Length];
args[0] = reasoningText;
// Fill in any additional parameters with null or default values
for (int i = 1; i < ctorParams.Length; i++)
{
args[i] = null;
}

var textPart = textPartConstructor.Invoke(args) as ReasoningSummaryTextPart;
summaryParts = textPart != null ? new List<ReasoningSummaryPart> { textPart } : new List<ReasoningSummaryPart>();
}
else
{
throw new InvalidOperationException("Could not find a way to create ReasoningSummaryTextPart.");
}
}
}

// Convert null summaryParts to empty list for method calls
var partsToPass = summaryParts ?? new List<ReasoningSummaryPart>();

// Try to find a static factory method first
var createReasoningItemMethod = typeof(ResponseItem).GetMethod(
"CreateReasoningItem",
BindingFlags.Static | BindingFlags.Public,
null,
[typeof(IEnumerable<ReasoningSummaryPart>)],
null);

if (createReasoningItemMethod != null)
{
return (ReasoningResponseItem)createReasoningItemMethod.Invoke(null, [partsToPass])!;
}

// If no factory method, look for constructors
var constructors = reasoningResponseItemType.GetConstructors(
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

foreach (var ctor in constructors)
{
var parameters = ctor.GetParameters();

// Look for constructor that takes IReadOnlyList<ReasoningSummaryPart> or similar
if (parameters.Length >= 1)
{
var firstParamType = parameters[0].ParameterType;
if (firstParamType.IsAssignableFrom(typeof(List<ReasoningSummaryPart>)) ||
firstParamType.IsAssignableFrom(typeof(IReadOnlyList<ReasoningSummaryPart>)) ||
firstParamType.IsAssignableFrom(typeof(IEnumerable<ReasoningSummaryPart>)))
{
var args = new object?[parameters.Length];
args[0] = partsToPass;
// Fill in any additional parameters with null
for (int i = 1; i < parameters.Length; i++)
{
args[i] = null;
}

return (ReasoningResponseItem)ctor.Invoke(args);
}
}
}

throw new InvalidOperationException("Constructor not found for ReasoningResponseItem.");
}

#endregion
}
Loading