Skip to content

Commit 0ec81c8

Browse files
shethaaditAdit Sheth
andauthored
.Net: Fix for : OpenAIResponsesAgent don't generate ReasoningContent. (#13048)
Fix for : OpenAIResponsesAgent don't generate ReasoningContent. Fixes #13046 --------- Co-authored-by: Adit Sheth <[email protected]>
1 parent 652575a commit 0ec81c8

File tree

3 files changed

+189
-2
lines changed

3 files changed

+189
-2
lines changed

dotnet/src/Agents/OpenAI/Extensions/OpenAIResponseExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ private static ChatMessageContentItemCollection ToChatMessageContentItemCollecti
202202
{
203203
if (part is ReasoningSummaryTextPart text)
204204
{
205-
collection.Add(new TextContent(text.Text, innerContent: text));
205+
collection.Add(new ReasoningContent(text.Text) { InnerContent = text });
206206
}
207207
}
208208
return collection;

dotnet/src/Agents/UnitTests/Extensions/ResponseItemExtensionsTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ public void VerifyToChatMessageContentFromReasoning()
110110

111111
// Assert
112112
Assert.NotNull(messageContent);
113-
Assert.Equal("Foo", messageContent.Content);
113+
Assert.Single(messageContent.Items);
114+
var reasoningContent = messageContent.Items[0] as ReasoningContent;
115+
Assert.NotNull(reasoningContent);
116+
Assert.Equal("Foo", reasoningContent.Text);
114117
}
115118
}

dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIResponseExtensionsTests.cs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,83 @@ public void VerifyToFunctionCallContent()
155155
Assert.NotNull(content.Arguments);
156156
}
157157

158+
/// <summary>
159+
/// Verify that ReasoningResponseItem with SummaryParts generates ReasoningContent correctly.
160+
/// </summary>
161+
[Fact]
162+
public void VerifyToChatMessageContentWithReasoningResponseItem()
163+
{
164+
// Arrange
165+
var reasoningResponseItem = this.CreateReasoningResponseItem("Let me think about this step by step...");
166+
167+
// Act
168+
ChatMessageContent? chatMessageContent = reasoningResponseItem.ToChatMessageContent();
169+
170+
// Assert
171+
Assert.NotNull(chatMessageContent);
172+
Assert.Equal(AuthorRole.Assistant, chatMessageContent.Role);
173+
Assert.Single(chatMessageContent.Items);
174+
175+
var reasoningContent = chatMessageContent.Items[0] as ReasoningContent;
176+
Assert.NotNull(reasoningContent);
177+
Assert.Equal("Let me think about this step by step...", reasoningContent.Text);
178+
}
179+
180+
/// <summary>
181+
/// Verify that ReasoningResponseItem converts to correct ChatMessageContentItemCollection with ReasoningContent.
182+
/// </summary>
183+
[Fact]
184+
public void VerifyToChatMessageContentItemCollectionWithReasoningResponseItem()
185+
{
186+
// Arrange
187+
var reasoningResponseItem = this.CreateReasoningResponseItem("Analyzing the problem...");
188+
189+
// Act
190+
ChatMessageContentItemCollection collection = reasoningResponseItem.ToChatMessageContentItemCollection();
191+
192+
// Assert
193+
Assert.NotNull(collection);
194+
Assert.Single(collection);
195+
Assert.IsType<ReasoningContent>(collection[0]);
196+
197+
var reasoningContent = collection[0] as ReasoningContent;
198+
Assert.Equal("Analyzing the problem...", reasoningContent?.Text);
199+
}
200+
201+
/// <summary>
202+
/// Verify that OpenAIResponse with both ReasoningResponseItem and MessageResponseItem generates correct content types.
203+
/// </summary>
204+
[Fact]
205+
public void VerifyToChatMessageContentWithMixedResponseItems()
206+
{
207+
// Arrange
208+
var reasoningResponseItem = this.CreateReasoningResponseItem("Thinking about the answer...");
209+
var messageResponseItem = ResponseItem.CreateAssistantMessageItem("Here is my response.");
210+
211+
OpenAIResponse mockResponse = this.CreateMockOpenAIResponse("gpt-4o-mini",
212+
[
213+
reasoningResponseItem,
214+
messageResponseItem
215+
]);
216+
217+
// Act
218+
ChatMessageContent chatMessageContent = mockResponse.ToChatMessageContent();
219+
220+
// Assert
221+
Assert.NotNull(chatMessageContent);
222+
Assert.Equal(2, chatMessageContent.Items.Count);
223+
224+
// First item should be ReasoningContent
225+
Assert.IsType<ReasoningContent>(chatMessageContent.Items[0]);
226+
var reasoningContent = chatMessageContent.Items[0] as ReasoningContent;
227+
Assert.Equal("Thinking about the answer...", reasoningContent?.Text);
228+
229+
// Second item should be TextContent
230+
Assert.IsType<TextContent>(chatMessageContent.Items[1]);
231+
var textContent = chatMessageContent.Items[1] as TextContent;
232+
Assert.Equal("Here is my response.", textContent?.Text);
233+
}
234+
158235
#region private
159236
private OpenAIResponse CreateMockOpenAIResponse(string model, IEnumerable<ResponseItem> outputItems)
160237
{
@@ -250,5 +327,112 @@ private OpenAIResponse CreateMockOpenAIResponse(string id, DateTimeOffset create
250327
throw new InvalidOperationException("Constructor not found.");
251328
}
252329

330+
private ReasoningResponseItem CreateReasoningResponseItem(string? reasoningText = null, IReadOnlyList<ReasoningSummaryPart>? summaryParts = null)
331+
{
332+
Type reasoningResponseItemType = typeof(ReasoningResponseItem);
333+
Type reasoningSummaryTextPartType = typeof(ReasoningSummaryTextPart);
334+
335+
// If reasoningText is provided and summaryParts is not, create summaryParts with the text
336+
if (reasoningText != null && summaryParts == null)
337+
{
338+
// Try to find any public static factory method or constructor that can create ReasoningSummaryTextPart
339+
var createTextPartMethod = typeof(ReasoningSummaryPart).GetMethod(
340+
"CreateTextPart",
341+
BindingFlags.Static | BindingFlags.Public,
342+
null,
343+
[typeof(string)],
344+
null);
345+
346+
if (createTextPartMethod != null)
347+
{
348+
var textPart = createTextPartMethod.Invoke(null, [reasoningText]) as ReasoningSummaryTextPart;
349+
summaryParts = textPart != null ? new List<ReasoningSummaryPart> { textPart } : new List<ReasoningSummaryPart>();
350+
}
351+
else
352+
{
353+
// Try to find constructor - search for all constructors
354+
var textPartConstructors = reasoningSummaryTextPartType.GetConstructors(
355+
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
356+
357+
ConstructorInfo? textPartConstructor = null;
358+
foreach (var ctor in textPartConstructors)
359+
{
360+
var parameters = ctor.GetParameters();
361+
if (parameters.Length >= 1 && parameters[0].ParameterType == typeof(string))
362+
{
363+
textPartConstructor = ctor;
364+
break;
365+
}
366+
}
367+
368+
if (textPartConstructor != null)
369+
{
370+
var ctorParams = textPartConstructor.GetParameters();
371+
var args = new object?[ctorParams.Length];
372+
args[0] = reasoningText;
373+
// Fill in any additional parameters with null or default values
374+
for (int i = 1; i < ctorParams.Length; i++)
375+
{
376+
args[i] = null;
377+
}
378+
379+
var textPart = textPartConstructor.Invoke(args) as ReasoningSummaryTextPart;
380+
summaryParts = textPart != null ? new List<ReasoningSummaryPart> { textPart } : new List<ReasoningSummaryPart>();
381+
}
382+
else
383+
{
384+
throw new InvalidOperationException("Could not find a way to create ReasoningSummaryTextPart.");
385+
}
386+
}
387+
}
388+
389+
// Convert null summaryParts to empty list for method calls
390+
var partsToPass = summaryParts ?? new List<ReasoningSummaryPart>();
391+
392+
// Try to find a static factory method first
393+
var createReasoningItemMethod = typeof(ResponseItem).GetMethod(
394+
"CreateReasoningItem",
395+
BindingFlags.Static | BindingFlags.Public,
396+
null,
397+
[typeof(IEnumerable<ReasoningSummaryPart>)],
398+
null);
399+
400+
if (createReasoningItemMethod != null)
401+
{
402+
return (ReasoningResponseItem)createReasoningItemMethod.Invoke(null, [partsToPass])!;
403+
}
404+
405+
// If no factory method, look for constructors
406+
var constructors = reasoningResponseItemType.GetConstructors(
407+
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
408+
409+
foreach (var ctor in constructors)
410+
{
411+
var parameters = ctor.GetParameters();
412+
413+
// Look for constructor that takes IReadOnlyList<ReasoningSummaryPart> or similar
414+
if (parameters.Length >= 1)
415+
{
416+
var firstParamType = parameters[0].ParameterType;
417+
if (firstParamType.IsAssignableFrom(typeof(List<ReasoningSummaryPart>)) ||
418+
firstParamType.IsAssignableFrom(typeof(IReadOnlyList<ReasoningSummaryPart>)) ||
419+
firstParamType.IsAssignableFrom(typeof(IEnumerable<ReasoningSummaryPart>)))
420+
{
421+
var args = new object?[parameters.Length];
422+
args[0] = partsToPass;
423+
// Fill in any additional parameters with null
424+
for (int i = 1; i < parameters.Length; i++)
425+
{
426+
args[i] = null;
427+
}
428+
429+
return (ReasoningResponseItem)ctor.Invoke(args);
430+
}
431+
}
432+
}
433+
434+
throw new InvalidOperationException("Constructor not found for ReasoningResponseItem.");
435+
}
436+
253437
#endregion
254438
}

0 commit comments

Comments
 (0)