Skip to content

Commit da35ca7

Browse files
DeanChensjcopybara-github
authored andcommitted
fix: harden callable type hint resolution and FunctionNode TypeAdapter handling
When `from __future__ import annotations` is active and a callable imports ADK context types (`Context`, `ToolContext`, `CallbackContext`, `ReadonlyContext`) or other symbols inside `if TYPE_CHECKING:`, `typing.get_type_hints` previously raised `NameError` and discarded all resolved type hints for the entire callable. In addition, `FunctionNode` did not catch `NameError` or `PydanticUserError` when constructing `TypeAdapter` instances and had an unguarded `TypeAdapter(annotated_type)` call at runtime in `_coerce_param`. - Retry `typing.get_type_hints` with ADK context types in `localns` on `NameError`, and add `get_best_effort_type_hints` per-parameter fallback so an unresolvable `TYPE_CHECKING`-only annotation does not wipe out sibling parameter hints in `context_utils`, `FunctionNode`, or `FunctionTool` runtime argument preprocessing and validation. - Share `get_callable_globals`, `resolve_annotation`, and `is_unresolvable` between `_callable_utils` and `_function_tool_declarations`, resolving individual annotations via `typing.get_type_hints` on a single-annotation holder. - Catch `(TypeError, NameError, PydanticSchemaGenerationError, PydanticUserError)` across `FunctionNode.__init__`, `_infer_schemas_from_func_signature`, and `_coerce_param`, and rebuild incomplete `TypeAdapter` instances inside `_build_type_adapter`. Co-authored-by: Shangjie Chen <[email protected]> PiperOrigin-RevId: 1006586318
1 parent 12c7bed commit da35ca7

9 files changed

Lines changed: 774 additions & 134 deletions

‎src/google/adk/tools/_function_tool_declarations.py‎

Lines changed: 21 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,11 @@
2828
import functools
2929
import inspect
3030
import logging
31-
import sys
3231
from types import UnionType
3332
from typing import Any
3433
from typing import Callable
35-
from typing import cast
36-
from typing import ForwardRef
3734
from typing import get_args
3835
from typing import get_origin
39-
from typing import Literal
4036
from typing import Optional
4137
from typing import Type
4238
from typing import Union
@@ -50,8 +46,12 @@
5046

5147
from ..events.event import Event
5248
from ..events.request_input import RequestInput
49+
from ..utils._callable_utils import get_adk_context_localns
5350
from ..utils._callable_utils import get_callable_doc
51+
from ..utils._callable_utils import get_callable_globals
5452
from ..utils._callable_utils import get_type_hints_cached
53+
from ..utils._callable_utils import is_unresolvable
54+
from ..utils._callable_utils import resolve_annotation
5555
from ..utils.variant_utils import get_google_llm_variant
5656
from ..utils.variant_utils import GoogleLLMVariant
5757

@@ -137,102 +137,6 @@ def _collapse_redundant_outer_optional(tp: Any) -> Any:
137137
return tp
138138

139139

140-
def _get_callable_globals(func: Callable[..., Any]) -> dict[str, Any]:
141-
"""Extracts the globals dictionary from a callable."""
142-
while True:
143-
if isinstance(func, functools.partial):
144-
func = func.func
145-
elif hasattr(func, '__wrapped__'):
146-
func = inspect.unwrap(func)
147-
if isinstance(func, functools.partial):
148-
func = func.func
149-
else:
150-
break
151-
else:
152-
break
153-
if hasattr(func, '__globals__'):
154-
return func.__globals__
155-
if hasattr(func, '__call__') and hasattr(func.__call__, '__globals__'):
156-
return cast(dict[str, Any], func.__call__.__globals__)
157-
module_name = getattr(func, '__module__', None)
158-
if module_name and module_name in sys.modules:
159-
return cast(
160-
dict[str, Any], getattr(sys.modules[module_name], '__dict__', {})
161-
)
162-
return {}
163-
164-
165-
def _resolve_annotation(ann: Any, globalns: dict[str, Any]) -> Any:
166-
"""Resolves string, ForwardRef, and generic type annotations in globalns.
167-
168-
Paired with `_is_unresolvable`; both functions must cover the same type
169-
shapes (Annotated, containers/generics, ForwardRef, str).
170-
"""
171-
while isinstance(ann, str):
172-
if globalns:
173-
try:
174-
resolved = eval(ann, globalns)
175-
if resolved == ann:
176-
break
177-
ann = resolved
178-
except Exception:
179-
break
180-
else:
181-
break
182-
183-
if isinstance(ann, ForwardRef):
184-
if globalns:
185-
try:
186-
resolved = eval(ann.__forward_arg__, globalns)
187-
return _resolve_annotation(resolved, globalns)
188-
except Exception:
189-
return ann
190-
return ann
191-
192-
origin = get_origin(ann)
193-
if origin is not None:
194-
if origin is Literal:
195-
return ann
196-
args = get_args(ann)
197-
if not args:
198-
return ann
199-
if origin is Annotated:
200-
resolved_inner = _resolve_annotation(args[0], globalns)
201-
if resolved_inner is not args[0]:
202-
return Annotated[(resolved_inner, *args[1:])]
203-
return ann
204-
resolved_args = tuple(_resolve_annotation(arg, globalns) for arg in args)
205-
if resolved_args != args:
206-
try:
207-
if _is_union_type(origin):
208-
return Union[resolved_args]
209-
if len(resolved_args) == 1:
210-
return origin[resolved_args[0]]
211-
return origin[resolved_args]
212-
except Exception:
213-
return ann
214-
return ann
215-
216-
217-
def _is_unresolvable(ann: Any) -> bool:
218-
"""Returns True if ann contains an unresolvable string or ForwardRef.
219-
220-
Paired with `_resolve_annotation`; both functions must cover the same type
221-
shapes (Annotated, containers/generics, ForwardRef, str).
222-
"""
223-
if isinstance(ann, (str, ForwardRef)):
224-
return True
225-
origin = get_origin(ann)
226-
if origin is not None:
227-
if origin is Literal:
228-
return False
229-
args = get_args(ann)
230-
if origin is Annotated:
231-
return bool(args and _is_unresolvable(args[0]))
232-
return any(_is_unresolvable(arg) for arg in args)
233-
return False
234-
235-
236140
def _get_function_fields(
237141
func: Callable[..., Any],
238142
ignore_params: Optional[list[str]] = None,
@@ -262,7 +166,8 @@ def _get_function_fields(
262166
# reference at runtime. Fall back to resolving individual annotations below.
263167
type_hints = {}
264168

265-
func_globals = _get_callable_globals(func)
169+
func_globals = get_callable_globals(func)
170+
func_localns = get_adk_context_localns(func)
266171

267172
for name, param in sig.parameters.items():
268173
if name in ignore_params:
@@ -283,10 +188,10 @@ def _get_function_fields(
283188
else:
284189
ann = Any
285190

286-
if _is_unresolvable(ann):
287-
ann = _resolve_annotation(ann, func_globals)
191+
if is_unresolvable(ann):
192+
ann = resolve_annotation(ann, func_globals, func_localns)
288193

289-
if _is_unresolvable(ann):
194+
if is_unresolvable(ann):
290195
logger.warning(
291196
'Parameter %r of %r has unresolvable type annotation %r; dropping'
292197
' parameter schema.',
@@ -469,14 +374,20 @@ def _build_response_json_schema(
469374
type_hints = get_type_hints_cached(func, include_extras=True)
470375
return_annotation = type_hints.get('return', return_annotation)
471376
except (TypeError, NameError, AttributeError):
472-
func_globals = _get_callable_globals(func)
473-
return_annotation = _resolve_annotation(return_annotation, func_globals)
377+
func_globals = get_callable_globals(func)
378+
func_localns = get_adk_context_localns(func)
379+
return_annotation = resolve_annotation(
380+
return_annotation, func_globals, func_localns
381+
)
474382

475-
if _is_unresolvable(return_annotation):
476-
func_globals = _get_callable_globals(func)
477-
return_annotation = _resolve_annotation(return_annotation, func_globals)
383+
if is_unresolvable(return_annotation):
384+
func_globals = get_callable_globals(func)
385+
func_localns = get_adk_context_localns(func)
386+
return_annotation = resolve_annotation(
387+
return_annotation, func_globals, func_localns
388+
)
478389

479-
if _is_unresolvable(return_annotation):
390+
if is_unresolvable(return_annotation):
480391
return None
481392

482393
return_annotation = _get_generator_output_annotation(return_annotation)

‎src/google/adk/tools/function_tool.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,9 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]:
261261
signature = self._spec.signature
262262
else:
263263
signature = None
264-
return _schema_utils.preprocess_args(args, signature, self._spec.type_hints)
264+
return _schema_utils.preprocess_args(
265+
args, signature, self._spec.get_best_effort_type_hints()
266+
)
265267

266268
def _preprocess_args_with_validation(
267269
self, args: dict[str, Any]
@@ -293,7 +295,7 @@ def _preprocess_args_with_validation(
293295
return preprocessed_args, []
294296

295297
signature = self._spec.signature
296-
type_hints = self._spec.type_hints
298+
type_hints = self._spec.get_best_effort_type_hints()
297299

298300
return self._validate_args(
299301
preprocessed_args,

0 commit comments

Comments
 (0)