From 76013d43f725f9e7d16299c434afc552a2e17637 Mon Sep 17 00:00:00 2001 From: Adit Sheth Date: Mon, 1 Sep 2025 10:58:21 -0700 Subject: [PATCH 1/2] Fixed bug 13046. --- dotnet/src/Agents/OpenAI/Extensions/OpenAIResponseExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/Extensions/OpenAIResponseExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/OpenAIResponseExtensions.cs index dc1ec795cae0..d7c27923142d 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/OpenAIResponseExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/OpenAIResponseExtensions.cs @@ -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; From 11ce82d445779335eb64118d48762e8e0a7a76ea Mon Sep 17 00:00:00 2001 From: Adit Sheth Date: Wed, 3 Sep 2025 11:31:42 -0700 Subject: [PATCH 2/2] Added Test cases. --- .../Extensions/ResponseItemExtensionsTests.cs | 5 +- .../OpenAIResponseExtensionsTests.cs | 184 ++++++++++++++++++ 2 files changed, 188 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Agents/UnitTests/Extensions/ResponseItemExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Extensions/ResponseItemExtensionsTests.cs index d547b27be0fa..2860251dedc2 100644 --- a/dotnet/src/Agents/UnitTests/Extensions/ResponseItemExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/Extensions/ResponseItemExtensionsTests.cs @@ -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); } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIResponseExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIResponseExtensionsTests.cs index 8bd86c950e7b..1687a13d88e9 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIResponseExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIResponseExtensionsTests.cs @@ -155,6 +155,83 @@ public void VerifyToFunctionCallContent() Assert.NotNull(content.Arguments); } + /// + /// Verify that ReasoningResponseItem with SummaryParts generates ReasoningContent correctly. + /// + [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); + } + + /// + /// Verify that ReasoningResponseItem converts to correct ChatMessageContentItemCollection with ReasoningContent. + /// + [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(collection[0]); + + var reasoningContent = collection[0] as ReasoningContent; + Assert.Equal("Analyzing the problem...", reasoningContent?.Text); + } + + /// + /// Verify that OpenAIResponse with both ReasoningResponseItem and MessageResponseItem generates correct content types. + /// + [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(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(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 outputItems) { @@ -250,5 +327,112 @@ private OpenAIResponse CreateMockOpenAIResponse(string id, DateTimeOffset create throw new InvalidOperationException("Constructor not found."); } + private ReasoningResponseItem CreateReasoningResponseItem(string? reasoningText = null, IReadOnlyList? 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 { textPart } : new List(); + } + 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 { textPart } : new List(); + } + 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(); + + // Try to find a static factory method first + var createReasoningItemMethod = typeof(ResponseItem).GetMethod( + "CreateReasoningItem", + BindingFlags.Static | BindingFlags.Public, + null, + [typeof(IEnumerable)], + 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 or similar + if (parameters.Length >= 1) + { + var firstParamType = parameters[0].ParameterType; + if (firstParamType.IsAssignableFrom(typeof(List)) || + firstParamType.IsAssignableFrom(typeof(IReadOnlyList)) || + firstParamType.IsAssignableFrom(typeof(IEnumerable))) + { + 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 }