diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index 16eed49db93..e6fb9d4dafb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -171,7 +171,7 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon // response ID than the newest update, create a new message. ChatMessage message; if (response.Messages.Count == 0 || - (update.ResponseId is string updateId && response.ResponseId is string responseId && updateId != responseId)) + (update.ResponseId is { Length: > 0 } updateId && response.ResponseId is string responseId && updateId != responseId)) { message = new ChatMessage(ChatRole.Assistant, []); response.Messages.Add(message); @@ -213,7 +213,7 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon // Other members on a ChatResponseUpdate map to members of the ChatResponse. // Update the response object with those, preferring the values from later updates. - if (update.ResponseId is not null) + if (update.ResponseId is { Length: > 0 }) { // Note that this must come after the message checks earlier, as they depend // on this value for change detection. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs index 24610ac76fc..346a5ed0d65 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs @@ -99,6 +99,12 @@ public IList Contents public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } /// Gets or sets the ID of the response of which this update is a part. + /// + /// This value is used when + /// groups instances into instances. + /// The value must be unique to each call to the underlying provider, and must be shared by + /// all updates that are part of the same response. + /// public string? ResponseId { get; set; } /// Gets or sets the chat thread ID associated with the chat response of which this update is a part. diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs index 0af538b9802..4fede2b6ceb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs @@ -132,6 +132,9 @@ public async IAsyncEnumerable GetStreamingResponseAsync( await OllamaUtilities.ThrowUnsuccessfulOllamaResponseAsync(httpResponse, cancellationToken).ConfigureAwait(false); } + // Ollama doesn't set a response ID on streamed chunks, so we need to generate one. + var responseId = Guid.NewGuid().ToString("N"); + using var httpResponseStream = await httpResponse.Content #if NET .ReadAsStreamAsync(cancellationToken) @@ -160,7 +163,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( CreatedAt = DateTimeOffset.TryParse(chunk.CreatedAt, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset createdAt) ? createdAt : null, FinishReason = ToFinishReason(chunk), ModelId = modelId, - ResponseId = chunk.CreatedAt, + ResponseId = responseId, Role = chunk.Message?.Role is not null ? new ChatRole(chunk.Message.Role) : null, }; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index 00e074ab276..4c20074301c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -51,7 +51,7 @@ public async Task ToChatResponse_SuccessfullyCreatesResponse(bool useAsync) Assert.Equal("123", response.ChatThreadId); - ChatMessage message = response.Messages.Last(); + ChatMessage message = response.Messages.Single(); Assert.Equal(new ChatRole("human"), message.Role); Assert.Equal("Someone", message.AuthorName); Assert.Null(message.AdditionalProperties); diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index 55b840eea5f..81e4d1044e3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -130,6 +130,25 @@ public virtual async Task GetStreamingResponseAsync_UsageDataAvailable() Assert.Equal(usage.Details.InputTokenCount + usage.Details.OutputTokenCount, usage.Details.TotalTokenCount); } + [ConditionalFact] + public virtual async Task GetStreamingResponseAsync_AppendToHistory() + { + SkipIfNotEnabled(); + + List history = [new(ChatRole.User, "Explain in 100 words how AI works")]; + + var streamingResponse = _chatClient.GetStreamingResponseAsync(history); + + Assert.Single(history); + await history.AddMessagesAsync(streamingResponse); + Assert.Equal(2, history.Count); + Assert.Equal(ChatRole.Assistant, history[1].Role); + + var singleTextContent = (TextContent)history[1].Contents.Single(); + Assert.NotEmpty(singleTextContent.Text); + Assert.Equal(history[1].Text, singleTextContent.Text); + } + protected virtual string? GetModel_MultiModal_DescribeImage() => null; [ConditionalFact] diff --git a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs index 8f7499aa272..16df3bc52ea 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs @@ -171,11 +171,12 @@ public async Task BasicRequestResponse_Streaming() using IChatClient client = new OllamaChatClient("http://localhost:11434", "llama3.1", httpClient); List updates = []; - await foreach (var update in client.GetStreamingResponseAsync("hello", new() + var streamingResponse = client.GetStreamingResponseAsync("hello", new() { MaxOutputTokens = 20, Temperature = 0.5f, - })) + }); + await foreach (var update in streamingResponse) { updates.Add(update); } @@ -201,6 +202,10 @@ public async Task BasicRequestResponse_Streaming() Assert.Equal(11, usage.Details.InputTokenCount); Assert.Equal(20, usage.Details.OutputTokenCount); Assert.Equal(31, usage.Details.TotalTokenCount); + + var chatResponse = await streamingResponse.ToChatResponseAsync(); + Assert.Single(Assert.Single(chatResponse.Messages).Contents); + Assert.Equal("Hello! How are you today? Is there something I can help you with or would you like to", chatResponse.Text); } [Fact]