Skip to content

Commit 4ef927f

Browse files
committed
Coalesce TextReasoningContent
1 parent 167e0b1 commit 4ef927f

File tree

2 files changed

+83
-38
lines changed

2 files changed

+83
-38
lines changed

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

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -180,53 +180,60 @@ static async Task<ChatResponse> ToChatResponseAsync(
180180
}
181181
}
182182

183-
/// <summary>Coalesces sequential <see cref="TextContent"/> content elements.</summary>
183+
/// <summary>Coalesces sequential <see cref="AIContent"/> content elements.</summary>
184184
internal static void CoalesceTextContent(List<AIContent> contents)
185185
{
186-
StringBuilder? coalescedText = null;
186+
Coalesce<TextContent>(contents, static text => new(text));
187+
Coalesce<TextReasoningContent>(contents, static text => new(text));
187188

188-
// Iterate through all of the items in the list looking for contiguous items that can be coalesced.
189-
int start = 0;
190-
while (start < contents.Count - 1)
189+
// This implementation relies on TContent's ToString returning its exact text.
190+
static void Coalesce<TContent>(List<AIContent> contents, Func<string, TContent> fromText)
191+
where TContent : AIContent
191192
{
192-
// We need at least two TextContents in a row to be able to coalesce.
193-
if (contents[start] is not TextContent firstText)
194-
{
195-
start++;
196-
continue;
197-
}
198-
199-
if (contents[start + 1] is not TextContent secondText)
200-
{
201-
start += 2;
202-
continue;
203-
}
193+
StringBuilder? coalescedText = null;
204194

205-
// Append the text from those nodes and continue appending subsequent TextContents until we run out.
206-
// We null out nodes as their text is appended so that we can later remove them all in one O(N) operation.
207-
coalescedText ??= new();
208-
_ = coalescedText.Clear().Append(firstText.Text).Append(secondText.Text);
209-
contents[start + 1] = null!;
210-
int i = start + 2;
211-
for (; i < contents.Count && contents[i] is TextContent next; i++)
195+
// Iterate through all of the items in the list looking for contiguous items that can be coalesced.
196+
int start = 0;
197+
while (start < contents.Count - 1)
212198
{
213-
_ = coalescedText.Append(next.Text);
214-
contents[i] = null!;
199+
// We need at least two TextContents in a row to be able to coalesce.
200+
if (contents[start] is not TContent firstText)
201+
{
202+
start++;
203+
continue;
204+
}
205+
206+
if (contents[start + 1] is not TContent secondText)
207+
{
208+
start += 2;
209+
continue;
210+
}
211+
212+
// Append the text from those nodes and continue appending subsequent TextContents until we run out.
213+
// We null out nodes as their text is appended so that we can later remove them all in one O(N) operation.
214+
coalescedText ??= new();
215+
_ = coalescedText.Clear().Append(firstText).Append(secondText);
216+
contents[start + 1] = null!;
217+
int i = start + 2;
218+
for (; i < contents.Count && contents[i] is TContent next; i++)
219+
{
220+
_ = coalescedText.Append(next);
221+
contents[i] = null!;
222+
}
223+
224+
// Store the replacement node. We inherit the properties of the first text node. We don't
225+
// currently propagate additional properties from the subsequent nodes. If we ever need to,
226+
// we can add that here.
227+
var newContent = fromText(coalescedText.ToString());
228+
contents[start] = newContent;
229+
newContent.AdditionalProperties = firstText.AdditionalProperties?.Clone();
230+
231+
start = i;
215232
}
216233

217-
// Store the replacement node.
218-
contents[start] = new TextContent(coalescedText.ToString())
219-
{
220-
// We inherit the properties of the first text node. We don't currently propagate additional
221-
// properties from the subsequent nodes. If we ever need to, we can add that here.
222-
AdditionalProperties = firstText.AdditionalProperties?.Clone(),
223-
};
224-
225-
start = i;
234+
// Remove all of the null slots left over from the coalescing process.
235+
_ = contents.RemoveAll(u => u is null);
226236
}
227-
228-
// Remove all of the null slots left over from the coalescing process.
229-
_ = contents.RemoveAll(u => u is null);
230237
}
231238

232239
/// <summary>Finalizes the <paramref name="response"/> object.</summary>

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,44 @@ void AddGap()
145145
}
146146
}
147147

148+
[Theory]
149+
[InlineData(false)]
150+
[InlineData(true)]
151+
public async Task ToChatResponse_CoalescesTextContentAndTextReasoningContentSeparately(bool useAsync)
152+
{
153+
ChatResponseUpdate[] updates =
154+
{
155+
new(null, "A"),
156+
new(null, "B"),
157+
new(null, "C"),
158+
new() { Contents = [new TextReasoningContent("D")] },
159+
new() { Contents = [new TextReasoningContent("E")] },
160+
new() { Contents = [new TextReasoningContent("F")] },
161+
new(null, "G"),
162+
new(null, "H"),
163+
new() { Contents = [new TextReasoningContent("I")] },
164+
new() { Contents = [new TextReasoningContent("J")] },
165+
new(null, "K"),
166+
new() { Contents = [new TextReasoningContent("L")] },
167+
new(null, "M"),
168+
new(null, "N"),
169+
new() { Contents = [new TextReasoningContent("O")] },
170+
new() { Contents = [new TextReasoningContent("P")] },
171+
};
172+
173+
ChatResponse response = useAsync ? await YieldAsync(updates).ToChatResponseAsync() : updates.ToChatResponse();
174+
ChatMessage message = Assert.Single(response.Messages);
175+
Assert.Equal(8, message.Contents.Count);
176+
Assert.Equal("ABC", Assert.IsType<TextContent>(message.Contents[0]).Text);
177+
Assert.Equal("DEF", Assert.IsType<TextReasoningContent>(message.Contents[1]).Text);
178+
Assert.Equal("GH", Assert.IsType<TextContent>(message.Contents[2]).Text);
179+
Assert.Equal("IJ", Assert.IsType<TextReasoningContent>(message.Contents[3]).Text);
180+
Assert.Equal("K", Assert.IsType<TextContent>(message.Contents[4]).Text);
181+
Assert.Equal("L", Assert.IsType<TextReasoningContent>(message.Contents[5]).Text);
182+
Assert.Equal("MN", Assert.IsType<TextContent>(message.Contents[6]).Text);
183+
Assert.Equal("OP", Assert.IsType<TextReasoningContent>(message.Contents[7]).Text);
184+
}
185+
148186
[Fact]
149187
public async Task ToChatResponse_UsageContentExtractedFromContents()
150188
{

0 commit comments

Comments
 (0)