Skip to content

Commit 764e2cb

Browse files
sylvesterkaczmarekcopybara-github
authored andcommitted
fix: stop after final preprocessing responses
Merge #6869 Fixes #4472 PiperOrigin-RevId: 974149791
1 parent ada8652 commit 764e2cb

2 files changed

Lines changed: 98 additions & 1 deletion

File tree

src/google/adk/flows/llm_flows/base_llm_flow.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1304,12 +1304,15 @@ async def _run_one_step_async(
13041304
run_config = _require_run_config(invocation_context)
13051305

13061306
# Preprocess before calling the LLM.
1307+
preprocess_yielded_final_response = False
13071308
async with Aclosing(
13081309
self._preprocess_async(invocation_context, llm_request)
13091310
) as agen:
13101311
async for event in agen:
1312+
if event.get_function_responses() and event.is_final_response():
1313+
preprocess_yielded_final_response = True
13111314
yield event
1312-
if invocation_context.end_invocation:
1315+
if invocation_context.end_invocation or preprocess_yielded_final_response:
13131316
return
13141317

13151318
# Resume the LLM agent based on the last event from the current branch.

tests/unittests/flows/llm_flows/test_base_llm_flow.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
29953089
async 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

Comments
 (0)