Skip to content

Commit d0c8454

Browse files
DeanChensjcopybara-github
authored andcommitted
fix(tools): return validation error dict when Workflow entry node rejects tool args in NodeTool
When `NodeTool` wraps a `Workflow` whose `input_schema` is a JSON Schema dict (such as a workflow starting with a `FunctionNode` in `node_input` binding mode) or a non-object schema with a missing `request` argument, `BaseNode._validate_input_data` on the outer `Workflow` does not validate the inner entry node's parameters before `_run_node_in_tool_context` starts. As a result, `ValidationError` or `WorkflowDataError` raised by the `START` successor escaped `NodeTool.run_async`'s validation handler instead of returning an `{'error': ...}` dict for the LLM to self-correct. Add `_validate_tool_input` to validate and coerce tool arguments against the wrapped node and its `START` entry nodes before starting the workflow run, while keeping downstream internal workflow errors propagating to `on_tool_error` callbacks. Also skip `START` successors with `_requires_all_predecessors` (such as `JoinNode`) during schema resolution and tool-argument pre-validation so workflows with a `START -> JoinNode` edge can be wrapped by `NodeTool`. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 999764136
1 parent cb48b91 commit d0c8454

2 files changed

Lines changed: 350 additions & 14 deletions

File tree

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

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import Any
1818

1919
from google.genai import types
20+
from pydantic import TypeAdapter
2021
from pydantic import ValidationError
2122
from typing_extensions import override
2223

@@ -41,6 +42,26 @@ def _to_json_schema(schema: SchemaType) -> dict[str, Any]:
4142
return schema_to_json_schema(schema)
4243

4344

45+
def _start_entry_nodes(workflow: BaseNode) -> list[BaseNode]:
46+
"""START successors that receive the tool's input directly.
47+
48+
Wait-for-all nodes (e.g. ``JoinNode``) are excluded: a ``START -> JoinNode``
49+
edge only registers START as one of the join's predecessors, so the join
50+
neither contributes to the tool's input schema nor validates its arguments.
51+
"""
52+
from ..workflow._base_node import START
53+
54+
graph = getattr(workflow, 'graph', None)
55+
if graph is None:
56+
return []
57+
return [
58+
e.to_node
59+
for e in graph.edges
60+
if e.from_node.name == START.name
61+
and not e.to_node._requires_all_predecessors
62+
]
63+
64+
4465
def _resolve_node_input_schema(
4566
node: BaseNode,
4667
) -> tuple[bool, SchemaType | None]:
@@ -54,7 +75,6 @@ def _resolve_node_input_schema(
5475
ambiguous.
5576
"""
5677
from ..agents.llm_agent import LlmAgent
57-
from ..workflow._base_node import START
5878
from ..workflow._function_node import FunctionNode
5979
from ..workflow._parallel_worker import _ParallelWorker
6080
from ..workflow._tool_node import _ToolNode
@@ -106,11 +126,7 @@ def _resolve_node_input_schema(
106126
return True, str
107127

108128
if isinstance(node, Workflow):
109-
if node.graph is None:
110-
return False, None
111-
start_nodes = [
112-
e.to_node for e in node.graph.edges if e.from_node.name == START.name
113-
]
129+
start_nodes = _start_entry_nodes(node)
114130
if not start_nodes:
115131
return False, None
116132

@@ -178,6 +194,54 @@ def _build_node_declaration(
178194
return decl
179195

180196

197+
def _arg_validation_error(name: str, detail: Any) -> dict[str, str]:
198+
"""Builds a tool argument validation error dict."""
199+
return {
200+
'error': (
201+
f'Invoking `{name}()` failed due to argument validation'
202+
f' errors:\n{detail}\nYou could retry calling this tool with'
203+
' corrected argument types.'
204+
)
205+
}
206+
207+
208+
def _validate_tool_input(node: BaseNode, data: Any) -> Any:
209+
"""Validates tool input against a node and its workflow entry nodes."""
210+
from ..workflow._function_node import FunctionNode
211+
from ..workflow._parallel_worker import _ParallelWorker
212+
from ..workflow._workflow import Workflow
213+
214+
input_schema = getattr(node, 'input_schema', None)
215+
if input_schema is not None and not isinstance(
216+
input_schema, (dict, types.Schema)
217+
):
218+
if data is None:
219+
TypeAdapter(input_schema).validate_python(None)
220+
return node._validate_input_data(data)
221+
222+
data = node._validate_input_data(data)
223+
if (
224+
isinstance(node, FunctionNode)
225+
and node.parameter_binding != 'node_input'
226+
and node.input_schema is None
227+
):
228+
hint = node._type_hints.get('node_input')
229+
if hint not in (None, Any, object):
230+
return node._coerce_param('node_input', data, hint)
231+
elif isinstance(node, _ParallelWorker):
232+
items = data if isinstance(data, list) else [data]
233+
validated = [_validate_tool_input(node._node, item) for item in items]
234+
return validated if isinstance(data, list) else validated[0]
235+
elif isinstance(node, Workflow):
236+
validated = [
237+
_validate_tool_input(n, data) for n in _start_entry_nodes(node)
238+
]
239+
if len(validated) == 1:
240+
return validated[0]
241+
242+
return data
243+
244+
181245
class NodeTool(BaseTool):
182246
"""A tool wrapper that executes a BaseNode (e.g. a Workflow or loop node)."""
183247

@@ -248,22 +312,20 @@ async def run_async(
248312
input_schema = getattr(self.node, 'input_schema', None)
249313
schema = _to_json_schema(input_schema) if input_schema is not None else None
250314
if isinstance(schema, dict) and schema.get('type') != 'object':
315+
if 'request' not in args:
316+
return _arg_validation_error(
317+
self.name, 'Missing required argument "request".'
318+
)
251319
node_input = args.get('request')
252320
else:
253321
node_input = args
254322

255323
try:
256-
node_input = self.node._validate_input_data(node_input)
324+
node_input = _validate_tool_input(self.node, node_input)
257325
except (ValidationError, WorkflowDataError) as e:
258326
# Same shape as FunctionTool's argument validation errors, so the
259327
# model can correct its arguments and retry.
260-
return {
261-
'error': (
262-
f'Invoking `{self.name}()` failed due to argument validation'
263-
f' errors:\n{e}\nYou could retry calling this tool with'
264-
' corrected argument types.'
265-
)
266-
}
328+
return _arg_validation_error(self.name, e)
267329

268330
res = await _run_node_in_tool_context(
269331
self.node,

0 commit comments

Comments
 (0)