Skip to content

Commit c427020

Browse files
DeanChensjcopybara-github
authored andcommitted
fix: resolve tool confirmation resumption failure in production
Ensure requested tool confirmations are correctly serialized and deserialized in the session service. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 951775012
1 parent 597fac3 commit c427020

3 files changed

Lines changed: 187 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ classifiers = [
3131
]
3232
dynamic = [ "version" ]
3333
dependencies = [
34+
"aiohttp!=3.14.2",
3435
"aiosqlite>=0.21",
3536
"authlib>=1.6.6,<2",
3637
"click>=8.1.8,<9",

tests/unittests/flows/llm_flows/test_request_confirmation.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717

1818
from google.adk.agents.llm_agent import LlmAgent
1919
from google.adk.events.event import Event
20+
from google.adk.events.event_actions import EventActions
2021
from google.adk.flows.llm_flows import functions
2122
from google.adk.flows.llm_flows.request_confirmation import request_processor
2223
from google.adk.models.llm_request import LlmRequest
24+
from google.adk.tools.function_tool import FunctionTool
2325
from google.adk.tools.tool_confirmation import ToolConfirmation
2426
from google.genai import types
2527
import pytest
@@ -407,3 +409,141 @@ async def test_request_confirmation_processor_finds_user_confirmation_in_default
407409

408410
assert len(events) == 1
409411
assert events[0] == expected_event
412+
413+
414+
@pytest.mark.asyncio
415+
async def test_request_confirmation_processor_dynamic_success():
416+
"""Test successful processing of dynamic tool confirmation (require_confirmation=False)."""
417+
agent = LlmAgent(
418+
name="test_agent",
419+
tools=[FunctionTool(mock_tool, require_confirmation=False)],
420+
)
421+
invocation_context = await testing_utils.create_invocation_context(
422+
agent=agent
423+
)
424+
llm_request = LlmRequest()
425+
426+
original_function_call = types.FunctionCall(
427+
name=MOCK_TOOL_NAME, args={"param1": "test"}, id=MOCK_FUNCTION_CALL_ID
428+
)
429+
430+
# 1. Event with the original tool call
431+
invocation_context.session.events.append(
432+
Event(
433+
author="agent",
434+
content=types.Content(
435+
parts=[types.Part(function_call=original_function_call)]
436+
),
437+
)
438+
)
439+
440+
# 2. Event with the tool's response requesting confirmation dynamically.
441+
# This event needs to have actions.requested_tool_confirmations.
442+
tool_confirmation_request = ToolConfirmation(
443+
confirmed=False, hint="dynamic hint"
444+
)
445+
original_response_event = Event(
446+
author="user",
447+
content=types.Content(
448+
parts=[
449+
types.Part(
450+
function_response=types.FunctionResponse(
451+
name=MOCK_TOOL_NAME,
452+
id=MOCK_FUNCTION_CALL_ID,
453+
response={"status": "waiting_for_confirm"},
454+
)
455+
)
456+
]
457+
),
458+
actions=EventActions(
459+
requested_tool_confirmations={
460+
MOCK_FUNCTION_CALL_ID: tool_confirmation_request
461+
}
462+
),
463+
)
464+
invocation_context.session.events.append(original_response_event)
465+
466+
# 3. Confirmation request event from the agent to the client.
467+
tool_confirmation_args = {
468+
"originalFunctionCall": original_function_call.model_dump(
469+
exclude_none=True, by_alias=True
470+
),
471+
"toolConfirmation": tool_confirmation_request.model_dump(
472+
by_alias=True, exclude_none=True
473+
),
474+
}
475+
invocation_context.session.events.append(
476+
Event(
477+
author="agent",
478+
content=types.Content(
479+
parts=[
480+
types.Part(
481+
function_call=types.FunctionCall(
482+
name=functions.REQUEST_CONFIRMATION_FUNCTION_CALL_NAME,
483+
args=tool_confirmation_args,
484+
id=MOCK_CONFIRMATION_FUNCTION_CALL_ID,
485+
)
486+
)
487+
]
488+
),
489+
)
490+
)
491+
492+
# 4. Event with the user's confirmation response.
493+
user_confirmation = ToolConfirmation(confirmed=True)
494+
invocation_context.session.events.append(
495+
Event(
496+
author="user",
497+
content=types.Content(
498+
parts=[
499+
types.Part(
500+
function_response=types.FunctionResponse(
501+
name=functions.REQUEST_CONFIRMATION_FUNCTION_CALL_NAME,
502+
id=MOCK_CONFIRMATION_FUNCTION_CALL_ID,
503+
response={
504+
"response": user_confirmation.model_dump_json()
505+
},
506+
)
507+
)
508+
]
509+
),
510+
)
511+
)
512+
513+
expected_event = Event(
514+
author="agent",
515+
content=types.Content(
516+
parts=[
517+
types.Part(
518+
function_response=types.FunctionResponse(
519+
name=MOCK_TOOL_NAME,
520+
id=MOCK_FUNCTION_CALL_ID,
521+
response={"result": "Mock tool result with test"},
522+
)
523+
)
524+
]
525+
),
526+
)
527+
528+
with patch(
529+
"google.adk.flows.llm_flows.functions.handle_function_call_list_async"
530+
) as mock_handle_function_call_list_async:
531+
mock_handle_function_call_list_async.return_value = expected_event
532+
533+
events = []
534+
async for event in request_processor.run_async(
535+
invocation_context, llm_request
536+
):
537+
events.append(event)
538+
539+
assert len(events) == 1
540+
assert events[0] == expected_event
541+
542+
mock_handle_function_call_list_async.assert_called_once()
543+
args, _ = mock_handle_function_call_list_async.call_args
544+
545+
assert list(args[1]) == [original_function_call] # function_calls
546+
assert args[3] == {MOCK_FUNCTION_CALL_ID} # tools_to_confirm
547+
assert (
548+
args[4][MOCK_FUNCTION_CALL_ID] == user_confirmation
549+
) # tool_confirmation_dict

tests/unittests/sessions/test_session_service.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from google.adk.sessions.in_memory_session_service import InMemorySessionService
3333
from google.adk.sessions.sqlite_session_service import SqliteSessionService
3434
from google.adk.sessions.vertex_ai_session_service import VertexAiSessionService
35+
from google.adk.tools.tool_confirmation import ToolConfirmation
3536
from google.genai import types
3637
import pytest
3738
from sqlalchemy import delete
@@ -726,6 +727,51 @@ async def test_append_event_complete(session_service):
726727
)
727728

728729

730+
# pylint: disable=redefined-outer-name
731+
@pytest.mark.asyncio
732+
async def test_append_event_with_requested_tool_confirmations(session_service):
733+
"""Tests that EventActions.requested_tool_confirmations is preserved in session service."""
734+
app_name = 'my_app'
735+
user_id = 'user'
736+
737+
session = await session_service.create_session(
738+
app_name=app_name, user_id=user_id
739+
)
740+
event = Event(
741+
invocation_id='invocation',
742+
author='user',
743+
actions=EventActions(
744+
requested_tool_confirmations={
745+
'tool_call_1': ToolConfirmation(
746+
hint='dynamic hint',
747+
confirmed=False,
748+
payload={
749+
'collection_name': 'photos',
750+
'resource_id': 'album_1',
751+
},
752+
)
753+
}
754+
),
755+
)
756+
await session_service.append_event(session=session, event=event)
757+
758+
refreshed_session = await session_service.get_session(
759+
app_name=app_name, user_id=user_id, session_id=session.id
760+
)
761+
assert refreshed_session is not None
762+
assert len(refreshed_session.events) == 1
763+
retrieved_event = refreshed_session.events[0]
764+
assert retrieved_event.actions is not None
765+
assert 'tool_call_1' in retrieved_event.actions.requested_tool_confirmations
766+
tc = retrieved_event.actions.requested_tool_confirmations['tool_call_1']
767+
assert tc.hint == 'dynamic hint'
768+
assert tc.payload == {
769+
'collection_name': 'photos',
770+
'resource_id': 'album_1',
771+
}
772+
assert not tc.confirmed
773+
774+
729775
@pytest.mark.asyncio
730776
async def test_session_last_update_time_updates_on_event(session_service):
731777
app_name = 'my_app'

0 commit comments

Comments
 (0)