diff --git a/dotnet/src/SemanticKernel.Core/AI/ChatCompletion/ChatHistoryReducerExtensions.cs b/dotnet/src/SemanticKernel.Core/AI/ChatCompletion/ChatHistoryReducerExtensions.cs index 2a01909a2c5c..4b8fa61b4511 100644 --- a/dotnet/src/SemanticKernel.Core/AI/ChatCompletion/ChatHistoryReducerExtensions.cs +++ b/dotnet/src/SemanticKernel.Core/AI/ChatCompletion/ChatHistoryReducerExtensions.cs @@ -104,7 +104,8 @@ public static int LocateSafeReductionIndex( int offsetCount = 0, bool hasSystemMessage = false) { - targetCount -= hasSystemMessage ? 1 : 0; + // The system message counts toward the target, but at least one other message is always retained + targetCount = Math.Max(1, targetCount - (hasSystemMessage ? 1 : 0)); // Compute the index of the truncation threshold int thresholdIndex = chatHistory.Count - (thresholdCount ?? 0) - targetCount; diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistorySummarizationReducerTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistorySummarizationReducerTests.cs index 0588055efc19..5db41bc01dd1 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistorySummarizationReducerTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistorySummarizationReducerTests.cs @@ -245,6 +245,27 @@ public async Task VerifySystemMessageIsNotReducedAsync() Assert.Contains(messages, m => m.Role == AuthorRole.System); } + /// + /// Validate history summarized and the latest message retained when the target count is one and a system message is present. + /// + [Fact] + public async Task VerifySystemMessageWithSingleTargetCountAsync() + { + // Arrange + Mock mockCompletionService = this.CreateMockCompletionService(); + IReadOnlyList sourceHistory = MockChatHistoryGenerator.CreateSimpleHistory(20, includeSystemMessage: true).ToArray(); + ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 1); + + // Act + IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + + // Assert + ChatMessageContent[] messages = VerifyReducedHistory(reducedHistory, 3); + Assert.Equal(AuthorRole.System, messages[0].Role); + VerifySummarization(messages[1]); + Assert.Same(sourceHistory[^1], messages[2]); + } + private static ChatMessageContent[] VerifyReducedHistory(IEnumerable? reducedHistory, int expectedCount) { Assert.NotNull(reducedHistory); diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTruncationReducerTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTruncationReducerTests.cs index 650648dadc17..73fe57d688d7 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTruncationReducerTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTruncationReducerTests.cs @@ -155,6 +155,25 @@ public async Task VerifySystemMessageIsNotReducedAsync() Assert.Contains(reducedHistory!, m => m.Role == AuthorRole.System); } + /// + /// Validate history reduced to the system message and the latest message when the target count is one. + /// + [Fact] + public async Task VerifySystemMessageWithSingleTargetCountAsync() + { + // Arrange + IReadOnlyList sourceHistory = MockChatHistoryGenerator.CreateSimpleHistory(20, includeSystemMessage: true).ToArray(); + ChatHistoryTruncationReducer reducer = new(1); + + // Act + IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + + // Assert + VerifyReducedHistory(reducedHistory, 2); + Assert.Equal(AuthorRole.System, reducedHistory!.First().Role); + Assert.Same(sourceHistory[^1], reducedHistory!.Last()); + } + private static void VerifyReducedHistory(IEnumerable? reducedHistory, int expectedCount) { Assert.NotNull(reducedHistory);