Skip to content

Commit f1f17e6

Browse files
committed
Merged PR 48301: Fix grouping of ChatResponseUpdate into ChatMessage (#6074)
Fix grouping of ChatResponseUpdate into ChatMessage (#6074) * For Ollama client, ensure ToChatResponseAsync coalesces text chunks into a single message * Fix OpenAI case by not treating empty-string response IDs as message boundaries
2 parents c2f0174 + f663faf commit f1f17e6

File tree

6 files changed

+39
-6
lines changed

6 files changed

+39
-6
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon
171171
// response ID than the newest update, create a new message.
172172
ChatMessage message;
173173
if (response.Messages.Count == 0 ||
174-
(update.ResponseId is string updateId && response.ResponseId is string responseId && updateId != responseId))
174+
(update.ResponseId is { Length: > 0 } updateId && response.ResponseId is string responseId && updateId != responseId))
175175
{
176176
message = new ChatMessage(ChatRole.Assistant, []);
177177
response.Messages.Add(message);
@@ -213,7 +213,7 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon
213213
// Other members on a ChatResponseUpdate map to members of the ChatResponse.
214214
// Update the response object with those, preferring the values from later updates.
215215

216-
if (update.ResponseId is not null)
216+
if (update.ResponseId is { Length: > 0 })
217217
{
218218
// Note that this must come after the message checks earlier, as they depend
219219
// on this value for change detection.

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

+6
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ public IList<AIContent> Contents
9999
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
100100

101101
/// <summary>Gets or sets the ID of the response of which this update is a part.</summary>
102+
/// <remarks>
103+
/// This value is used when <see cref="ChatResponseExtensions.ToChatResponseAsync(IAsyncEnumerable{ChatResponseUpdate}, System.Threading.CancellationToken)"/>
104+
/// groups <see cref="ChatResponseUpdate"/> instances into <see cref="ChatMessage"/> instances.
105+
/// The value must be unique to each call to the underlying provider, and must be shared by
106+
/// all updates that are part of the same response.
107+
/// </remarks>
102108
public string? ResponseId { get; set; }
103109

104110
/// <summary>Gets or sets the chat thread ID associated with the chat response of which this update is a part.</summary>

src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
132132
await OllamaUtilities.ThrowUnsuccessfulOllamaResponseAsync(httpResponse, cancellationToken).ConfigureAwait(false);
133133
}
134134

135+
// Ollama doesn't set a response ID on streamed chunks, so we need to generate one.
136+
var responseId = Guid.NewGuid().ToString("N");
137+
135138
using var httpResponseStream = await httpResponse.Content
136139
#if NET
137140
.ReadAsStreamAsync(cancellationToken)
@@ -160,7 +163,7 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
160163
CreatedAt = DateTimeOffset.TryParse(chunk.CreatedAt, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset createdAt) ? createdAt : null,
161164
FinishReason = ToFinishReason(chunk),
162165
ModelId = modelId,
163-
ResponseId = chunk.CreatedAt,
166+
ResponseId = responseId,
164167
Role = chunk.Message?.Role is not null ? new ChatRole(chunk.Message.Role) : null,
165168
};
166169

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public async Task ToChatResponse_SuccessfullyCreatesResponse(bool useAsync)
5151

5252
Assert.Equal("123", response.ChatThreadId);
5353

54-
ChatMessage message = response.Messages.Last();
54+
ChatMessage message = response.Messages.Single();
5555
Assert.Equal(new ChatRole("human"), message.Role);
5656
Assert.Equal("Someone", message.AuthorName);
5757
Assert.Null(message.AdditionalProperties);

test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs

+19
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,25 @@ public virtual async Task GetStreamingResponseAsync_UsageDataAvailable()
130130
Assert.Equal(usage.Details.InputTokenCount + usage.Details.OutputTokenCount, usage.Details.TotalTokenCount);
131131
}
132132

133+
[ConditionalFact]
134+
public virtual async Task GetStreamingResponseAsync_AppendToHistory()
135+
{
136+
SkipIfNotEnabled();
137+
138+
List<ChatMessage> history = [new(ChatRole.User, "Explain in 100 words how AI works")];
139+
140+
var streamingResponse = _chatClient.GetStreamingResponseAsync(history);
141+
142+
Assert.Single(history);
143+
await history.AddMessagesAsync(streamingResponse);
144+
Assert.Equal(2, history.Count);
145+
Assert.Equal(ChatRole.Assistant, history[1].Role);
146+
147+
var singleTextContent = (TextContent)history[1].Contents.Single();
148+
Assert.NotEmpty(singleTextContent.Text);
149+
Assert.Equal(history[1].Text, singleTextContent.Text);
150+
}
151+
133152
protected virtual string? GetModel_MultiModal_DescribeImage() => null;
134153

135154
[ConditionalFact]

test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,12 @@ public async Task BasicRequestResponse_Streaming()
171171
using IChatClient client = new OllamaChatClient("http://localhost:11434", "llama3.1", httpClient);
172172

173173
List<ChatResponseUpdate> updates = [];
174-
await foreach (var update in client.GetStreamingResponseAsync("hello", new()
174+
var streamingResponse = client.GetStreamingResponseAsync("hello", new()
175175
{
176176
MaxOutputTokens = 20,
177177
Temperature = 0.5f,
178-
}))
178+
});
179+
await foreach (var update in streamingResponse)
179180
{
180181
updates.Add(update);
181182
}
@@ -201,6 +202,10 @@ public async Task BasicRequestResponse_Streaming()
201202
Assert.Equal(11, usage.Details.InputTokenCount);
202203
Assert.Equal(20, usage.Details.OutputTokenCount);
203204
Assert.Equal(31, usage.Details.TotalTokenCount);
205+
206+
var chatResponse = await streamingResponse.ToChatResponseAsync();
207+
Assert.Single(Assert.Single(chatResponse.Messages).Contents);
208+
Assert.Equal("Hello! How are you today? Is there something I can help you with or would you like to", chatResponse.Text);
204209
}
205210

206211
[Fact]

0 commit comments

Comments
 (0)