diff --git a/src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs b/src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs index d6fe530..7f50246 100644 --- a/src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs +++ b/src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs @@ -31,8 +31,10 @@ internal static class AbstractionMapper if (stream is null) return null; + var responseId = stream.CreatedAtString ?? Guid.NewGuid().ToString("N"); var chatMessage = ToChatMessage(stream.Message); chatMessage.CreatedAt = stream.CreatedAt; + chatMessage.MessageId = responseId; return new ChatResponse(chatMessage) { @@ -41,7 +43,7 @@ internal static class AbstractionMapper CreatedAt = stream.CreatedAt, ModelId = usedModel ?? stream.Model, RawRepresentation = stream, - ResponseId = stream.CreatedAtString ?? Guid.NewGuid().ToString("N"), + ResponseId = responseId, Usage = ParseOllamaChatResponseUsage(stream) }; } @@ -348,6 +350,7 @@ public static ChatResponseUpdate ToChatResponseUpdate(ChatResponseStream? respon AdditionalProperties = ParseOllamaChatResponseProps(done), CreatedAt = done.CreatedAt, FinishReason = done.DoneReason is null ? null : new ChatFinishReason(done.DoneReason), + MessageId = responseId, RawRepresentation = response, ResponseId = responseId, ModelId = done.Model @@ -359,6 +362,7 @@ public static ChatResponseUpdate ToChatResponseUpdate(ChatResponseStream? respon // no need to set "Contents" as we set the text CreatedAt = response?.CreatedAt, FinishReason = response?.Done == true ? ChatFinishReason.Stop : null, + MessageId = responseId, RawRepresentation = response, ResponseId = responseId, ModelId = response?.Model diff --git a/test/AbstractionMapperTests.cs b/test/AbstractionMapperTests.cs index 13dca23..02c6ae0 100644 --- a/test/AbstractionMapperTests.cs +++ b/test/AbstractionMapperTests.cs @@ -930,6 +930,7 @@ public void Maps_Known_Properties() response.CreatedAt.ShouldBe(new DateTimeOffset(2023, 08, 04, 08, 52, 19, 385, 406, TimeSpan.FromHours(-7))); response.FinishReason.ShouldBe(ChatFinishReason.Stop); response.Messages[0].AuthorName.ShouldBeNull(); + response.Messages[0].MessageId.ShouldBe(ollamaCreatedStamp); response.Messages[0].RawRepresentation.ShouldBe(stream.Message); response.Messages[0].Role.ShouldBe(Microsoft.Extensions.AI.ChatRole.Assistant); response.Messages[0].Text.ShouldBe("Hi."); @@ -974,6 +975,7 @@ public void Maps_Known_Properties() ((TextContent)streamingChatCompletion.Contents[0]).Text.ShouldBe("Hi."); streamingChatCompletion.CreatedAt.ShouldBe(new DateTimeOffset(2023, 08, 04, 08, 52, 19, 385, 406, TimeSpan.FromHours(-7))); streamingChatCompletion.FinishReason.ShouldBe(ChatFinishReason.Stop); + streamingChatCompletion.MessageId.ShouldBe("12345"); streamingChatCompletion.RawRepresentation.ShouldBe(stream); streamingChatCompletion.ResponseId.ShouldBe("12345"); streamingChatCompletion.Role.ShouldBe(Microsoft.Extensions.AI.ChatRole.Assistant); @@ -1004,6 +1006,7 @@ public void Maps_Thinking_Tokens() ((TextReasoningContent)streamingChatCompletion.Contents[0]).Text.ShouldBe("Beer."); streamingChatCompletion.CreatedAt.ShouldBe(new DateTimeOffset(2023, 08, 04, 08, 52, 19, 385, 406, TimeSpan.FromHours(-7))); streamingChatCompletion.FinishReason.ShouldBe(ChatFinishReason.Stop); + streamingChatCompletion.MessageId.ShouldBe("12345"); streamingChatCompletion.RawRepresentation.ShouldBe(stream); streamingChatCompletion.ResponseId.ShouldBe("12345"); streamingChatCompletion.Role.ShouldBe(Microsoft.Extensions.AI.ChatRole.Assistant); @@ -1068,6 +1071,58 @@ public void Maps_ToolCalls() chatMessage.Text.ShouldBe("It seems the sun will be out all day."); } + /// + /// Verifies that MEAI keeps updates from separate provider calls in separate messages. + /// + [Test] + public void Separates_Updates_With_Different_MessageIds() + { + var updates = new[] + { + AbstractionMapper.ToChatResponseUpdate(new ChatResponseStream + { + Message = new Message { Role = OllamaSharp.Models.Chat.ChatRole.Assistant, Content = "First" } + }, "response-1"), + AbstractionMapper.ToChatResponseUpdate(new ChatResponseStream + { + Message = new Message { Role = OllamaSharp.Models.Chat.ChatRole.Assistant, Content = "Second" } + }, "response-2") + }; + + var response = updates.ToChatResponse(); + + response.Messages.Count.ShouldBe(2); + response.Messages[0].MessageId.ShouldBe("response-1"); + response.Messages[0].Text.ShouldBe("First"); + response.Messages[1].MessageId.ShouldBe("response-2"); + response.Messages[1].Text.ShouldBe("Second"); + } + + /// + /// Verifies that MEAI combines chunks that belong to the same logical message. + /// + [Test] + public void Combines_Updates_With_The_Same_MessageId() + { + var updates = new[] + { + AbstractionMapper.ToChatResponseUpdate(new ChatResponseStream + { + Message = new Message { Role = OllamaSharp.Models.Chat.ChatRole.Assistant, Content = "Hel" } + }, "response-1"), + AbstractionMapper.ToChatResponseUpdate(new ChatResponseStream + { + Message = new Message { Role = OllamaSharp.Models.Chat.ChatRole.Assistant, Content = "lo" } + }, "response-1") + }; + + var response = updates.ToChatResponse(); + + response.Messages.Count.ShouldBe(1); + response.Messages[0].MessageId.ShouldBe("response-1"); + response.Messages[0].Text.ShouldBe("Hello"); + } + /// /// Verifies that performance metrics from ChatDoneResponseStream (LoadDuration, TotalDuration, etc.) /// are correctly mapped to the AdditionalProperties dictionary. @@ -1107,6 +1162,7 @@ public void Maps_Performance_Metrics_To_AdditionalProperties() // Assert - AdditionalProperties contains performance metrics const double NANOSECONDS_PER_MILLISECOND = 1_000_000; + streamingChatCompletion.MessageId.ShouldBe(responseId); streamingChatCompletion.AdditionalProperties.ShouldNotBeNull(); streamingChatCompletion.AdditionalProperties.ShouldContainKey(Application.LoadDuration); streamingChatCompletion.AdditionalProperties[Application.LoadDuration].ShouldBe(TimeSpan.FromMilliseconds(123_456_789 / NANOSECONDS_PER_MILLISECOND)); @@ -1127,6 +1183,13 @@ public void Maps_Performance_Metrics_To_AdditionalProperties() usageContent.Details.InputTokenCount.ShouldBe(42); usageContent.Details.OutputTokenCount.ShouldBe(15); usageContent.Details.TotalTokenCount.ShouldBe(42 + 15); + + var combinedResponse = new[] { streamingChatCompletion }.ToChatResponse(); + combinedResponse.Messages.Single().AdditionalProperties.ShouldNotBeNull(); + combinedResponse.Messages.Single().AdditionalProperties.ShouldContainKey(Application.TotalDuration); + combinedResponse.Usage.ShouldNotBeNull(); + combinedResponse.Usage.InputTokenCount.ShouldBe(42); + combinedResponse.Usage.OutputTokenCount.ShouldBe(15); } } } @@ -1273,4 +1336,4 @@ public void Maps_Response() } #pragma warning restore CS8602 // Dereference of a possibly null reference. -#pragma warning restore CS8604 // Possible null reference argument. \ No newline at end of file +#pragma warning restore CS8604 // Possible null reference argument.