Skip to content

Commit be5828f

Browse files
google-genai-botcopybara-github
authored andcommitted
ADK changes
PiperOrigin-RevId: 949756197
1 parent 64b758a commit be5828f

6 files changed

Lines changed: 196 additions & 431 deletions

File tree

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

Lines changed: 32 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
from __future__ import annotations
1515

16+
import json
1617
import logging
1718
from typing import Any
1819
from typing import AsyncGenerator
@@ -26,157 +27,68 @@
2627
from ...agents.readonly_context import ReadonlyContext
2728
from ...events.event import Event
2829
from ...models.llm_request import LlmRequest
29-
from ...tools.base_tool import BaseTool
3030
from ...tools.tool_confirmation import ToolConfirmation
31-
from ...tools.tool_context import ToolContext
3231
from ._base_llm_processor import BaseLlmRequestProcessor
3332
from .functions import REQUEST_CONFIRMATION_FUNCTION_CALL_NAME
3433

3534
if TYPE_CHECKING:
3635
pass
3736

3837

39-
logger = logging.getLogger("google_adk." + __name__)
38+
logger = logging.getLogger('google_adk.' + __name__)
4039

4140

4241
def _parse_tool_confirmation(response: dict[str, Any]) -> ToolConfirmation:
43-
"""Parses ToolConfirmation from a function response dict."""
44-
return ToolConfirmation.from_response_dict(response)
42+
"""Parse ToolConfirmation from a function response dict.
4543
44+
Handles both the direct dict format and the ADK client's
45+
``{'response': json_string}`` wrapper format.
4646
47-
async def _resolve_confirmation_targets(
48-
invocation_context: InvocationContext,
47+
"""
48+
if response and len(response.values()) == 1 and 'response' in response.keys():
49+
return ToolConfirmation.model_validate(json.loads(response['response']))
50+
return ToolConfirmation.model_validate(response)
51+
52+
53+
def _resolve_confirmation_targets(
4954
events: list[Event],
5055
confirmation_fc_ids: set[str],
5156
confirmations_by_fc_id: dict[str, ToolConfirmation],
52-
tools_dict: dict[str, BaseTool],
5357
) -> tuple[dict[str, ToolConfirmation], dict[str, types.FunctionCall]]:
54-
"""Find original function calls for confirmed tools and validate them.
58+
"""Find original function calls for confirmed tools.
5559
5660
Scans events for ``adk_request_confirmation`` function calls whose IDs
5761
are in *confirmation_fc_ids*, extracts the ``originalFunctionCall`` from
58-
their args, validates that they are registered, actually require confirmation,
59-
and match the original function calls in history, and maps each confirmation
60-
to the original FC ID.
62+
their args, and maps each confirmation to the original FC ID.
6163
6264
Args:
63-
invocation_context: Current invocation context.
6465
events: Session events to scan.
6566
confirmation_fc_ids: IDs of ``adk_request_confirmation`` function calls.
6667
confirmations_by_fc_id: Mapping of confirmation FC ID ->
6768
``ToolConfirmation``.
68-
tools_dict: Dictionary of registered tools.
6969
7070
Returns:
7171
Tuple of ``(tool_confirmation_dict, original_fcs_dict)`` where both
7272
are keyed by the ORIGINAL function call IDs.
73-
74-
Raises:
75-
ValueError: If validation of any confirmation target fails.
7673
"""
7774
tool_confirmation_dict: dict[str, ToolConfirmation] = {}
7875
original_fcs_dict: dict[str, types.FunctionCall] = {}
7976

80-
history_fcs = {
81-
fc.id: (fc, ev)
82-
for ev in events
83-
for fc in ev.get_function_calls()
84-
if fc.id and fc.name != REQUEST_CONFIRMATION_FUNCTION_CALL_NAME
85-
}
86-
history_fr_events = {
87-
fr.id: ev for ev in events for fr in ev.get_function_responses() if fr.id
88-
}
89-
9077
for event in events:
9178
event_function_calls = event.get_function_calls()
9279
if not event_function_calls:
9380
continue
9481

9582
for function_call in event_function_calls:
96-
if not function_call.id or function_call.id not in confirmation_fc_ids:
83+
if function_call.id not in confirmation_fc_ids:
9784
continue
9885

9986
args = function_call.args
100-
if not args or "originalFunctionCall" not in args:
87+
if 'originalFunctionCall' not in args:
10188
continue
10289
original_function_call = types.FunctionCall(
103-
**args["originalFunctionCall"]
104-
)
105-
if not original_function_call.id:
106-
raise ValueError("Original function call ID is missing.")
107-
tool_name = original_function_call.name
108-
if not tool_name:
109-
raise ValueError("Original function call name is missing.")
110-
111-
# Check 1: Is the tool registered?
112-
original_fc_info = history_fcs.get(original_function_call.id)
113-
if not original_fc_info:
114-
raise ValueError(
115-
f"Original function call for ID '{original_function_call.id}' not"
116-
" found in session history."
117-
)
118-
original_fc_in_history, original_fc_event = original_fc_info
119-
120-
# If this tool call was authored by another agent, skip it to let that
121-
# agent's processor handle it.
122-
agent = invocation_context.agent
123-
if agent and original_fc_event.author != agent.name:
124-
continue
125-
126-
tool = tools_dict.get(tool_name)
127-
if not tool:
128-
raise ValueError(
129-
f"Tool '{original_function_call.name}' is not registered."
130-
)
131-
132-
# Check 2: Does the tool require confirmation for these arguments?
133-
# We check if it is either statically required, or if it was dynamically
134-
# requested in the session history.
135-
temp_tool_context = ToolContext(
136-
invocation_context=invocation_context,
137-
function_call_id=original_function_call.id,
90+
**args['originalFunctionCall']
13891
)
139-
requires_confirmation = await tool.check_require_confirmation(
140-
original_function_call.args or {}, temp_tool_context
141-
)
142-
143-
requested_in_history = False
144-
if not requires_confirmation:
145-
# Search the history for the response event of the original tool call
146-
original_response_event = history_fr_events.get(
147-
original_function_call.id
148-
)
149-
if (
150-
original_response_event
151-
and original_response_event.actions.requested_tool_confirmations
152-
):
153-
requested_in_history = (
154-
original_function_call.id
155-
in original_response_event.actions.requested_tool_confirmations
156-
)
157-
158-
if not requires_confirmation and not requested_in_history:
159-
raise ValueError(
160-
f"Tool '{original_function_call.name}' does not require"
161-
" confirmation."
162-
)
163-
164-
# Check 3: Does the original function call match name and arguments?
165-
if original_fc_in_history.name != original_function_call.name:
166-
raise ValueError(
167-
f"Function call name mismatch for ID '{original_function_call.id}':"
168-
f" history has '{original_fc_in_history.name}', confirmation has"
169-
f" '{original_function_call.name}'."
170-
)
171-
172-
hist_args = original_fc_in_history.args or {}
173-
conf_args = original_function_call.args or {}
174-
if hist_args != conf_args:
175-
raise ValueError(
176-
"Function call arguments mismatch for ID"
177-
f" '{original_function_call.id}'."
178-
)
179-
18092
tool_confirmation_dict[original_function_call.id] = (
18193
confirmations_by_fc_id[function_call.id]
18294
)
@@ -203,9 +115,10 @@ async def run_async(
203115
# Step 1: Find the last user-authored event and parse confirmation
204116
# responses from it.
205117
confirmations_by_fc_id: dict[str, ToolConfirmation] = {}
118+
confirmation_event_index = -1
206119
for k in range(len(events) - 1, -1, -1):
207120
event = events[k]
208-
if not event.author or event.author != "user":
121+
if not event.author or event.author != 'user':
209122
continue
210123
responses = event.get_function_responses()
211124
if not responses:
@@ -214,45 +127,29 @@ async def run_async(
214127
for function_response in responses:
215128
if function_response.name != REQUEST_CONFIRMATION_FUNCTION_CALL_NAME:
216129
continue
217-
if not function_response.id or function_response.response is None:
218-
continue
219130
confirmations_by_fc_id[function_response.id] = _parse_tool_confirmation(
220131
function_response.response
221132
)
133+
confirmation_event_index = k
222134
break
223135

224136
if not confirmations_by_fc_id:
225137
return
226138

227-
# Resolve all canonical tools and build tools_dict
228-
tools_dict = {}
229-
if agent is not None and hasattr(agent, "canonical_tools"):
230-
tools_dict = {
231-
tool.name: tool
232-
for tool in await agent.canonical_tools(
233-
ReadonlyContext(invocation_context)
234-
)
235-
}
236-
237139
# Step 2: Resolve confirmation targets using extracted helper.
238140
confirmation_fc_ids = set(confirmations_by_fc_id.keys())
239141
tools_to_resume_with_confirmation, tools_to_resume_with_args = (
240-
await _resolve_confirmation_targets(
241-
invocation_context,
242-
events,
243-
confirmation_fc_ids,
244-
confirmations_by_fc_id,
245-
tools_dict,
142+
_resolve_confirmation_targets(
143+
events, confirmation_fc_ids, confirmations_by_fc_id
246144
)
247145
)
248146

249147
if not tools_to_resume_with_confirmation:
250148
return
251149

252150
# Step 3: Remove tools that have already been confirmed (dedup).
253-
for event in reversed(events):
254-
if event.author == "user":
255-
break
151+
for i in range(len(events) - 1, confirmation_event_index, -1):
152+
event = events[i]
256153
fr_list = event.get_function_responses()
257154
if not fr_list:
258155
continue
@@ -270,9 +167,14 @@ async def run_async(
270167
# Step 4: Re-execute the confirmed tools.
271168
if function_response_event := await functions.handle_function_call_list_async(
272169
invocation_context,
273-
list(tools_to_resume_with_args.values()),
274-
tools_dict,
275-
set(tools_to_resume_with_confirmation.keys()),
170+
tools_to_resume_with_args.values(),
171+
{
172+
tool.name: tool
173+
for tool in await agent.canonical_tools(
174+
ReadonlyContext(invocation_context)
175+
)
176+
},
177+
tools_to_resume_with_confirmation.keys(),
276178
tools_to_resume_with_confirmation,
277179
):
278180
yield function_response_event

src/google/adk/tools/base_tool.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,6 @@ async def process_llm_request(
168168
# Use the consolidated logic in LlmRequest.append_tools
169169
llm_request.append_tools([self])
170170

171-
async def check_require_confirmation(
172-
self, args: dict[str, Any], tool_context: ToolContext
173-
) -> bool:
174-
"""Returns whether the tool requires confirmation for the given args."""
175-
return False
176-
177171
@property
178172
def _api_variant(self) -> GoogleLLMVariant:
179173
return get_google_llm_variant()

src/google/adk/tools/function_tool.py

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import logging
1919
from typing import Any
2020
from typing import Callable
21-
from typing import cast
2221
from typing import get_args
2322
from typing import get_origin
2423
from typing import get_type_hints
@@ -195,52 +194,36 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]:
195194
args[param_name], list
196195
):
197196
item_type = get_list_inner_type(target_type)
198-
if item_type is not None:
199-
try:
200-
converted_args[param_name] = [
201-
item_type.model_validate(item)
202-
if isinstance(item, dict)
203-
else item
204-
for item in args[param_name]
205-
]
206-
except Exception as e:
207-
logger.warning(
208-
f"Failed to convert argument '{param_name}' to"
209-
f' list[{item_type.__name__}]: {e}'
210-
)
211-
pass
197+
try:
198+
converted_args[param_name] = [
199+
item_type.model_validate(item)
200+
if isinstance(item, dict)
201+
else item
202+
for item in args[param_name]
203+
]
204+
except Exception as e:
205+
logger.warning(
206+
f"Failed to convert argument '{param_name}' to"
207+
f' list[{item_type.__name__}]: {e}'
208+
)
209+
pass
212210

213211
return converted_args
214212

215-
def _prepare_invocation_args(
216-
self, args: dict[str, Any], tool_context: ToolContext
217-
) -> dict[str, Any]:
218-
"""Prepare args for function invocation (preprocesses, injects context and filters)."""
219-
args_to_call = self._preprocess_args(args)
220-
signature = inspect.signature(self.func)
221-
valid_params = set(signature.parameters.keys())
222-
if self._context_param_name in valid_params:
223-
args_to_call[self._context_param_name] = tool_context
224-
return {k: v for k, v in args_to_call.items() if k in valid_params}
225-
226-
@override
227-
async def check_require_confirmation(
228-
self, args: dict[str, Any], tool_context: ToolContext
229-
) -> bool:
230-
if callable(self._require_confirmation):
231-
args_to_call = self._prepare_invocation_args(args, tool_context)
232-
return cast(
233-
bool,
234-
await self._invoke_callable(self._require_confirmation, args_to_call),
235-
)
236-
return bool(self._require_confirmation)
237-
238213
@override
239214
async def run_async(
240215
self, *, args: dict[str, Any], tool_context: ToolContext
241216
) -> Any:
242217
# Preprocess arguments (includes Pydantic model conversion)
243-
args_to_call = self._prepare_invocation_args(args, tool_context)
218+
args_to_call = self._preprocess_args(args)
219+
220+
signature = inspect.signature(self.func)
221+
valid_params = {param for param in signature.parameters}
222+
if self._context_param_name in valid_params:
223+
args_to_call[self._context_param_name] = tool_context
224+
225+
# Filter args_to_call to only include valid parameters for the function
226+
args_to_call = {k: v for k, v in args_to_call.items() if k in valid_params}
244227

245228
# Before invoking the function, we check for if the list of args passed in
246229
# has all the mandatory arguments or not.
@@ -259,9 +242,12 @@ async def run_async(
259242
You could retry calling this tool, but it is IMPORTANT for you to provide all the mandatory parameters."""
260243
return {'error': error_str}
261244

262-
require_confirmation = await self.check_require_confirmation(
263-
args, tool_context
264-
)
245+
if isinstance(self._require_confirmation, Callable):
246+
require_confirmation = await self._invoke_callable(
247+
self._require_confirmation, args_to_call
248+
)
249+
else:
250+
require_confirmation = bool(self._require_confirmation)
265251

266252
if require_confirmation:
267253
if not tool_context.tool_confirmation:

0 commit comments

Comments
 (0)