@@ -343,3 +343,126 @@ async def test_last_invocation_with_tool_call_keeps_user_prompt():
343343
344344 assert "user_prompt_2" in texts
345345 assert "final_answer_2" in texts
346+
347+
348+ @pytest .mark .asyncio
349+ async def test_filter_with_remove_amount ():
350+ """Tests that remove_amount correctly removes additional invocations."""
351+ plugin = ContextFilterPlugin (num_invocations_to_keep = 2 , remove_amount = 1 )
352+ contents = [
353+ _create_content ("user" , "user_prompt_1" ),
354+ _create_content ("model" , "model_response_1" ),
355+ _create_content ("user" , "user_prompt_2" ),
356+ _create_content ("model" , "model_response_2" ),
357+ _create_content ("user" , "user_prompt_3" ),
358+ _create_content ("model" , "model_response_3" ),
359+ ]
360+ llm_request = LlmRequest (contents = contents )
361+
362+ await plugin .before_model_callback (
363+ callback_context = mock .create_autospec (CallbackContext , instance = True ),
364+ llm_request = llm_request ,
365+ )
366+
367+ # With num_invocations_to_keep=2 and remove_amount=1, keeps last 2.
368+ assert len (llm_request .contents ) == 4
369+ assert llm_request .contents [0 ].parts [0 ].text == "user_prompt_2"
370+ assert llm_request .contents [1 ].parts [0 ].text == "model_response_2"
371+ assert llm_request .contents [2 ].parts [0 ].text == "user_prompt_3"
372+ assert llm_request .contents [3 ].parts [0 ].text == "model_response_3"
373+
374+
375+ @pytest .mark .asyncio
376+ async def test_filter_with_higher_remove_amount ():
377+ """Tests remove_amount with a higher value to remove more invocations."""
378+ plugin = ContextFilterPlugin (num_invocations_to_keep = 3 , remove_amount = 2 )
379+ contents = [
380+ _create_content ("user" , "user_prompt_1" ),
381+ _create_content ("model" , "model_response_1" ),
382+ _create_content ("user" , "user_prompt_2" ),
383+ _create_content ("model" , "model_response_2" ),
384+ _create_content ("user" , "user_prompt_3" ),
385+ _create_content ("model" , "model_response_3" ),
386+ _create_content ("user" , "user_prompt_4" ),
387+ _create_content ("model" , "model_response_4" ),
388+ _create_content ("user" , "user_prompt_5" ),
389+ _create_content ("model" , "model_response_5" ),
390+ ]
391+ llm_request = LlmRequest (contents = contents )
392+
393+ await plugin .before_model_callback (
394+ callback_context = mock .create_autospec (CallbackContext , instance = True ),
395+ llm_request = llm_request ,
396+ )
397+
398+ # With num_invocations_to_keep=3 and remove_amount=2, keeps last 3.
399+ assert len (llm_request .contents ) == 6
400+ assert llm_request .contents [0 ].parts [0 ].text == "user_prompt_3"
401+ assert llm_request .contents [1 ].parts [0 ].text == "model_response_3"
402+ assert llm_request .contents [2 ].parts [0 ].text == "user_prompt_4"
403+ assert llm_request .contents [3 ].parts [0 ].text == "model_response_4"
404+ assert llm_request .contents [4 ].parts [0 ].text == "user_prompt_5"
405+ assert llm_request .contents [5 ].parts [0 ].text == "model_response_5"
406+
407+
408+ def test_invalid_remove_amount ():
409+ """Tests that initializing with remove_amount < 1 raises ValueError."""
410+ with pytest .raises (ValueError , match = "remove_amount must be at least 1" ):
411+ ContextFilterPlugin (num_invocations_to_keep = 1 , remove_amount = 0 )
412+
413+ with pytest .raises (ValueError , match = "remove_amount must be at least 1" ):
414+ ContextFilterPlugin (num_invocations_to_keep = 1 , remove_amount = - 1 )
415+
416+
417+ @pytest .mark .asyncio
418+ async def test_filter_remove_amount_with_multiple_user_turns ():
419+ """Tests remove_amount with multiple user turns in invocations."""
420+ plugin = ContextFilterPlugin (num_invocations_to_keep = 2 , remove_amount = 1 )
421+ contents = [
422+ _create_content ("user" , "user_prompt_1" ),
423+ _create_content ("model" , "model_response_1" ),
424+ _create_content ("user" , "user_prompt_2a" ),
425+ _create_content ("user" , "user_prompt_2b" ),
426+ _create_content ("model" , "model_response_2" ),
427+ _create_content ("user" , "user_prompt_3" ),
428+ _create_content ("model" , "model_response_3" ),
429+ ]
430+ llm_request = LlmRequest (contents = contents )
431+
432+ await plugin .before_model_callback (
433+ callback_context = mock .create_autospec (CallbackContext , instance = True ),
434+ llm_request = llm_request ,
435+ )
436+
437+ # Should keep last 2 invocations including multiple user turns
438+ assert len (llm_request .contents ) == 5
439+ assert llm_request .contents [0 ].parts [0 ].text == "user_prompt_2a"
440+ assert llm_request .contents [1 ].parts [0 ].text == "user_prompt_2b"
441+ assert llm_request .contents [2 ].parts [0 ].text == "model_response_2"
442+ assert llm_request .contents [3 ].parts [0 ].text == "user_prompt_3"
443+ assert llm_request .contents [4 ].parts [0 ].text == "model_response_3"
444+
445+
446+ @pytest .mark .asyncio
447+ async def test_filter_bypass_when_under_remove_threshold ():
448+ """Tests that filtering is bypassed when total invocations are between keep limit and keep+remove limit."""
449+ plugin = ContextFilterPlugin (num_invocations_to_keep = 2 , remove_amount = 2 )
450+ contents = [
451+ _create_content ("user" , "user_prompt_1" ),
452+ _create_content ("model" , "model_response_1" ),
453+ _create_content ("user" , "user_prompt_2" ),
454+ _create_content ("model" , "model_response_2" ),
455+ _create_content ("user" , "user_prompt_3" ),
456+ _create_content ("model" , "model_response_3" ),
457+ ]
458+ llm_request = LlmRequest (contents = contents )
459+ original_contents = list (llm_request .contents )
460+
461+ await plugin .before_model_callback (
462+ callback_context = mock .create_autospec (CallbackContext , instance = True ),
463+ llm_request = llm_request ,
464+ )
465+
466+ # With num_invocations_to_keep=2 and remove_amount=2, threshold is 4.
467+ # We have 3 invocations, so no filtering should occur.
468+ assert llm_request .contents == original_contents
0 commit comments