@@ -84,27 +84,145 @@ public async Task ReduceAsync_PreservesSystemMessage()
8484 }
8585
8686 [ Fact ]
87- public async Task ReduceAsync_IgnoresFunctionCallsAndResults ( )
87+ public async Task ReduceAsync_PreservesCompleteToolCallSequence ( )
8888 {
8989 using var chatClient = new TestChatClient ( ) ;
90- var reducer = new SummarizingChatReducer ( chatClient , targetCount : 3 , threshold : 0 ) ;
90+
91+ // Target 2 messages, but this would split a function call sequence
92+ var reducer = new SummarizingChatReducer ( chatClient , targetCount : 2 , threshold : 0 ) ;
9193
9294 List < ChatMessage > messages =
9395 [
96+ new ChatMessage ( ChatRole . User , "What's the time?" ) ,
97+ new ChatMessage ( ChatRole . Assistant , "Let me check" ) ,
9498 new ChatMessage ( ChatRole . User , "What's the weather?" ) ,
95- new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "call1" , "get_weather" , new Dictionary < string , object ? > { [ "location" ] = "Seattle" } ) ] ) ,
96- new ChatMessage ( ChatRole . Tool , [ new FunctionResultContent ( "call1" , "Sunny, 72°F " ) ] ) ,
97- new ChatMessage ( ChatRole . Assistant , "The weather in Seattle is sunny and 72°F." ) ,
98- new ChatMessage ( ChatRole . User , "Thanks! " ) ,
99+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "call1" , "get_weather" ) , new TestUserInputRequestContent ( "uir1" ) ] ) ,
100+ new ChatMessage ( ChatRole . Tool , [ new FunctionResultContent ( "call1" , "Sunny" ) ] ) ,
101+ new ChatMessage ( ChatRole . User , [ new TestUserInputResponseContent ( "uir1" ) ] ) ,
102+ new ChatMessage ( ChatRole . Assistant , "It's sunny " ) ,
99103 ] ;
100104
105+ chatClient . GetResponseAsyncCallback = ( msgs , _ , _ ) =>
106+ {
107+ Assert . DoesNotContain ( msgs , m => m . Contents . Any ( c => c is FunctionCallContent or FunctionResultContent or TestUserInputRequestContent or TestUserInputResponseContent ) ) ;
108+ return Task . FromResult ( new ChatResponse ( new ChatMessage ( ChatRole . Assistant , "Asked about time" ) ) ) ;
109+ } ;
110+
101111 var result = await reducer . ReduceAsync ( messages , CancellationToken . None ) ;
112+ var resultList = result . ToList ( ) ;
102113
103- // Function calls/results should be ignored, which means there aren't enough messages to generate a summary.
114+ // Should have: summary + function call + function result + user input response + last reply
115+ Assert . Equal ( 5 , resultList . Count ) ;
116+
117+ // Verify the complete sequence is preserved
118+ Assert . Collection ( resultList ,
119+ m => Assert . Contains ( "Asked about time" , m . Text ) ,
120+ m =>
121+ {
122+ Assert . Contains ( m . Contents , c => c is FunctionCallContent ) ;
123+ Assert . Contains ( m . Contents , c => c is TestUserInputRequestContent ) ;
124+ } ,
125+ m => Assert . Contains ( m . Contents , c => c is FunctionResultContent ) ,
126+ m => Assert . Contains ( m . Contents , c => c is TestUserInputResponseContent ) ,
127+ m => Assert . Contains ( "sunny" , m . Text ) ) ;
128+ }
129+
130+ [ Fact ]
131+ public async Task ReduceAsync_PreservesUserMessageWhenWithinThreshold ( )
132+ {
133+ using var chatClient = new TestChatClient ( ) ;
134+
135+ // Target 3 messages with threshold of 2
136+ // This allows us to keep anywhere from 3 to 5 messages
137+ var reducer = new SummarizingChatReducer ( chatClient , targetCount : 3 , threshold : 2 ) ;
138+
139+ List < ChatMessage > messages =
140+ [
141+ new ChatMessage ( ChatRole . User , "First question" ) ,
142+ new ChatMessage ( ChatRole . Assistant , "First answer" ) ,
143+ new ChatMessage ( ChatRole . User , "Second question" ) ,
144+ new ChatMessage ( ChatRole . Assistant , "Second answer" ) ,
145+ new ChatMessage ( ChatRole . User , "Third question" ) ,
146+ new ChatMessage ( ChatRole . Assistant , "Third answer" ) ,
147+ ] ;
148+
149+ chatClient . GetResponseAsyncCallback = ( msgs , _ , _ ) =>
150+ {
151+ var msgList = msgs . ToList ( ) ;
152+
153+ // Should summarize messages 0-1 (First question and answer)
154+ // The reducer should find the User message at index 2 within the threshold
155+ Assert . Equal ( 3 , msgList . Count ) ; // 2 messages to summarize + system prompt
156+ return Task . FromResult ( new ChatResponse ( new ChatMessage ( ChatRole . Assistant , "Summary of first exchange" ) ) ) ;
157+ } ;
158+
159+ var result = await reducer . ReduceAsync ( messages , CancellationToken . None ) ;
104160 var resultList = result . ToList ( ) ;
105- Assert . Equal ( 3 , resultList . Count ) ; // Function calls get removed in the summarized chat.
106- Assert . DoesNotContain ( resultList , m => m . Contents . Any ( c => c is FunctionCallContent ) ) ;
107- Assert . DoesNotContain ( resultList , m => m . Contents . Any ( c => c is FunctionResultContent ) ) ;
161+
162+ // Should have: summary + 4 kept messages (from "Second question" onward)
163+ Assert . Equal ( 5 , resultList . Count ) ;
164+
165+ // Verify the summary is first
166+ Assert . Contains ( "Summary" , resultList [ 0 ] . Text ) ;
167+
168+ // Verify we kept the User message at index 2 and everything after
169+ Assert . Collection ( resultList . Skip ( 1 ) ,
170+ m => Assert . Contains ( "Second question" , m . Text ) ,
171+ m => Assert . Contains ( "Second answer" , m . Text ) ,
172+ m => Assert . Contains ( "Third question" , m . Text ) ,
173+ m => Assert . Contains ( "Third answer" , m . Text ) ) ;
174+ }
175+
176+ [ Fact ]
177+ public async Task ReduceAsync_ExcludesToolCallsFromSummarizedPortion ( )
178+ {
179+ using var chatClient = new TestChatClient ( ) ;
180+
181+ // Target 3 messages - this will cause function calls in older messages to be summarized (excluded)
182+ // while function calls in recent messages are kept
183+ var reducer = new SummarizingChatReducer ( chatClient , targetCount : 3 , threshold : 0 ) ;
184+
185+ List < ChatMessage > messages =
186+ [
187+ new ChatMessage ( ChatRole . User , "What's the weather in Seattle?" ) ,
188+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "call1" , "get_weather" , new Dictionary < string , object ? > { [ "location" ] = "Seattle" } ) , new TestUserInputRequestContent ( "uir2" ) ] ) ,
189+ new ChatMessage ( ChatRole . Tool , [ new FunctionResultContent ( "call1" , "Sunny, 72°F" ) ] ) ,
190+ new ChatMessage ( ChatRole . User , [ new TestUserInputResponseContent ( "uir2" ) ] ) ,
191+ new ChatMessage ( ChatRole . Assistant , "It's sunny and 72°F in Seattle." ) ,
192+ new ChatMessage ( ChatRole . User , "What about New York?" ) ,
193+ new ChatMessage ( ChatRole . Assistant , [ new FunctionCallContent ( "call2" , "get_weather" , new Dictionary < string , object ? > { [ "location" ] = "New York" } ) ] ) ,
194+ new ChatMessage ( ChatRole . Tool , [ new FunctionResultContent ( "call2" , "Rainy, 65°F" ) ] ) ,
195+ new ChatMessage ( ChatRole . Assistant , "It's rainy and 65°F in New York." ) ,
196+ ] ;
197+
198+ chatClient . GetResponseAsyncCallback = ( msgs , _ , _ ) =>
199+ {
200+ var msgList = msgs . ToList ( ) ;
201+
202+ Assert . Equal ( 4 , msgList . Count ) ; // 3 non-function messages + system prompt
203+ Assert . DoesNotContain ( msgList , m => m . Contents . Any ( c => c is FunctionCallContent or FunctionResultContent or TestUserInputRequestContent or TestUserInputResponseContent ) ) ;
204+ Assert . Contains ( msgList , m => m . Text . Contains ( "What's the weather in Seattle?" ) ) ;
205+ Assert . Contains ( msgList , m => m . Text . Contains ( "sunny and 72°F in Seattle" ) ) ;
206+ Assert . Contains ( msgList , m => m . Text . Contains ( "What about New York?" ) ) ;
207+ Assert . Contains ( msgList , m => m . Role == ChatRole . System ) ;
208+
209+ return Task . FromResult ( new ChatResponse ( new ChatMessage ( ChatRole . Assistant , "User asked about weather in Seattle and New York." ) ) ) ;
210+ } ;
211+
212+ var result = await reducer . ReduceAsync ( messages , CancellationToken . None ) ;
213+ var resultList = result . ToList ( ) ;
214+
215+ // Should have: summary + 3 kept messages (the last 3 messages with function calls)
216+ Assert . Equal ( 4 , resultList . Count ) ;
217+
218+ Assert . Contains ( "User asked about weather" , resultList [ 0 ] . Text ) ;
219+ Assert . Contains ( resultList , m => m . Contents . Any ( c => c is FunctionCallContent fc && fc . CallId == "call2" ) ) ;
220+ Assert . Contains ( resultList , m => m . Contents . Any ( c => c is FunctionResultContent fr && fr . CallId == "call2" ) ) ;
221+ Assert . DoesNotContain ( resultList , m => m . Contents . Any ( c => c is FunctionCallContent fc && fc . CallId == "call1" ) ) ;
222+ Assert . DoesNotContain ( resultList , m => m . Contents . Any ( c => c is FunctionResultContent fr && fr . CallId == "call1" ) ) ;
223+ Assert . DoesNotContain ( resultList , m => m . Contents . Any ( c => c is TestUserInputRequestContent ) ) ;
224+ Assert . DoesNotContain ( resultList , m => m . Contents . Any ( c => c is TestUserInputResponseContent ) ) ;
225+ Assert . DoesNotContain ( resultList , m => m . Text . Contains ( "sunny and 72°F in Seattle" ) ) ;
108226 }
109227
110228 [ Theory ]
@@ -121,7 +239,7 @@ public async Task ReduceAsync_RespectsTargetAndThresholdCounts(int targetCount,
121239 var messages = new List < ChatMessage > ( ) ;
122240 for ( int i = 0 ; i < messageCount ; i ++ )
123241 {
124- messages . Add ( new ChatMessage ( i % 2 == 0 ? ChatRole . User : ChatRole . Assistant , $ "Message { i } ") ) ;
242+ messages . Add ( new ChatMessage ( ChatRole . Assistant , $ "Message { i } ") ) ;
125243 }
126244
127245 var summarizationCalled = false ;
@@ -266,4 +384,20 @@ need frequent exercise. The user then asked about whether they're good around ki
266384 m => Assert . StartsWith ( "Golden retrievers get along" , m . Text , StringComparison . Ordinal ) ,
267385 m => Assert . StartsWith ( "Do they make good lap dogs" , m . Text , StringComparison . Ordinal ) ) ;
268386 }
387+
388+ private sealed class TestUserInputRequestContent : UserInputRequestContent
389+ {
390+ public TestUserInputRequestContent ( string id )
391+ : base ( id )
392+ {
393+ }
394+ }
395+
396+ private sealed class TestUserInputResponseContent : UserInputResponseContent
397+ {
398+ public TestUserInputResponseContent ( string id )
399+ : base ( id )
400+ {
401+ }
402+ }
269403}
0 commit comments