@@ -2991,6 +2991,100 @@ async def _drive_one_llm_call(flow, invocation_context):
29912991 pass
29922992
29932993
2994+ @pytest .mark .asyncio
2995+ async def test_preprocess_final_response_skips_llm_call ():
2996+ """A final response from preprocessing must finish the current step."""
2997+ agent = Agent (
2998+ name = 'root_agent' , model = testing_utils .MockModel .create (responses = [])
2999+ )
3000+ flow = BaseLlmFlowForTesting ()
3001+ invocation_context = await testing_utils .create_invocation_context (
3002+ agent = agent , user_content = 'resume'
3003+ )
3004+ function_response_event = Event (
3005+ invocation_id = invocation_context .invocation_id ,
3006+ author = agent .name ,
3007+ content = types .Content (
3008+ role = 'user' ,
3009+ parts = [
3010+ types .Part .from_function_response (
3011+ name = 'resumed_tool' , response = {'result' : 'done' }
3012+ )
3013+ ],
3014+ ),
3015+ )
3016+ function_response_event .actions .skip_summarization = True
3017+
3018+ async def mock_preprocess (_ctx , _request ):
3019+ yield function_response_event
3020+
3021+ async def fail_if_llm_called (* _args , ** _kwargs ):
3022+ raise AssertionError ('LLM should not be called after a final response' )
3023+ yield # pylint: disable=unreachable
3024+
3025+ with (
3026+ mock .patch .object (flow , '_preprocess_async' , side_effect = mock_preprocess ),
3027+ mock .patch .object (
3028+ flow , '_call_llm_async' , side_effect = fail_if_llm_called
3029+ ),
3030+ ):
3031+ events = [event async for event in flow .run_async (invocation_context )]
3032+
3033+ assert events == [function_response_event ]
3034+
3035+
3036+ @pytest .mark .asyncio
3037+ async def test_preprocess_non_function_response_does_not_skip_llm_call ():
3038+ """Non-function-response events in preprocessing must not skip the LLM call."""
3039+ mock_response = types .GenerateContentResponse (
3040+ candidates = [
3041+ types .Candidate (
3042+ content = types .Content (
3043+ role = 'model' ,
3044+ parts = [types .Part .from_text (text = 'Analysis done.' )],
3045+ ),
3046+ finish_reason = 'STOP' ,
3047+ )
3048+ ]
3049+ )
3050+ agent = Agent (
3051+ name = 'root_agent' ,
3052+ model = testing_utils .MockModel .create (responses = [mock_response ]),
3053+ )
3054+ flow = BaseLlmFlowForTesting ()
3055+ invocation_context = await testing_utils .create_invocation_context (
3056+ agent = agent , user_content = 'test'
3057+ )
3058+ processing_file_event = Event (
3059+ invocation_id = invocation_context .invocation_id ,
3060+ author = agent .name ,
3061+ content = types .Content (
3062+ role = 'model' ,
3063+ parts = [
3064+ types .Part (text = 'Processing input file: `data.csv`' ),
3065+ types .Part (
3066+ executable_code = types .ExecutableCode (
3067+ code = 'import pandas as pd' , language = 'PYTHON'
3068+ )
3069+ ),
3070+ ],
3071+ ),
3072+ )
3073+ assert processing_file_event .is_final_response ()
3074+
3075+ async def mock_preprocess (_ctx , _request ):
3076+ yield processing_file_event
3077+
3078+ with mock .patch .object (
3079+ flow , '_preprocess_async' , side_effect = mock_preprocess
3080+ ):
3081+ events = [event async for event in flow .run_async (invocation_context )]
3082+
3083+ assert len (events ) == 2
3084+ assert events [0 ] == processing_file_event
3085+ assert events [1 ].content .parts [0 ].text == 'Analysis done.'
3086+
3087+
29943088@pytest .mark .asyncio
29953089async def test_cfc_llm_calls_are_counted_against_max_llm_calls ():
29963090 """support_cfc must not exempt a run from the max_llm_calls spend cap."""
0 commit comments