Skip to content

Commit 8128b84

Browse files
DeanChensjcopybara-github
authored andcommitted
fix(tools): support zero-argument nodes in NodeTool
A FunctionNode taking no arguments (or only a context parameter) no longer fails with "does not have an input_schema defined" -- it now omits parameters_json_schema from its FunctionDeclaration, matching FunctionTool. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 974086964
1 parent a213e76 commit 8128b84

2 files changed

Lines changed: 85 additions & 18 deletions

File tree

src/google/adk/tools/_node_tool.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ def __init__(
6666
if orig_output_schema is not None:
6767
node.output_schema = orig_output_schema
6868

69-
if not getattr(node, 'input_schema', None):
69+
# A FunctionNode has already inferred its schema by here, and that yields
70+
# None only when the function has nothing to bind.
71+
if not isinstance(node, FunctionNode) and not getattr(
72+
node, 'input_schema', None
73+
):
7074
raise ValueError(
7175
f"Node '{node.name}' does not have an input_schema defined."
7276
' NodeTool requires an explicit Pydantic input_schema on the wrapped'
@@ -84,26 +88,27 @@ def __init__(
8488

8589
@override
8690
def _get_declaration(self) -> types.FunctionDeclaration:
87-
schema = schema_to_json_schema(self.node.input_schema)
88-
89-
# The GenAI API strictly requires parameters_json_schema to be an 'object'
90-
# type schema. If the node has a primitive input schema (e.g., str, int),
91-
# we wrap it into an object schema with a 'request' property.
92-
if isinstance(schema, dict) and schema.get('type') != 'object':
93-
schema = {
94-
'type': 'object',
95-
'properties': {
96-
'request': schema,
97-
},
98-
'required': ['request'],
99-
}
100-
10191
decl = types.FunctionDeclaration(
10292
name=self.name,
10393
description=self.description,
104-
parameters_json_schema=schema,
10594
)
10695

96+
input_schema = getattr(self.node, 'input_schema', None)
97+
if input_schema is not None:
98+
schema = schema_to_json_schema(input_schema)
99+
# The GenAI API strictly requires parameters_json_schema to be an 'object'
100+
# type schema. If the node has a primitive input schema (e.g., str, int),
101+
# we wrap it into an object schema with a 'request' property.
102+
if isinstance(schema, dict) and schema.get('type') != 'object':
103+
schema = {
104+
'type': 'object',
105+
'properties': {
106+
'request': schema,
107+
},
108+
'required': ['request'],
109+
}
110+
decl.parameters_json_schema = schema
111+
107112
output_schema = getattr(self.node, 'output_schema', None)
108113
if output_schema:
109114
decl.response_json_schema = schema_to_json_schema(output_schema)
@@ -121,7 +126,7 @@ async def run_async(
121126

122127
from pydantic import BaseModel
123128

124-
input_schema = self.node.input_schema
129+
input_schema = getattr(self.node, 'input_schema', None)
125130
node_input: Any
126131
if inspect.isclass(input_schema) and issubclass(input_schema, BaseModel):
127132
try:
@@ -130,7 +135,11 @@ async def run_async(
130135
except Exception as e:
131136
return f'Error validating input for node: {e}'
132137
else:
133-
schema = schema_to_json_schema(input_schema)
138+
schema = (
139+
schema_to_json_schema(input_schema)
140+
if input_schema is not None
141+
else None
142+
)
134143
if isinstance(schema, dict) and schema.get('type') != 'object':
135144
node_input = args.get('request')
136145
else:

tests/unittests/workflow/test_node_tool.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,3 +1072,61 @@ def echo_func(node_input: str):
10721072
assert func_response_events[0].content.parts[
10731073
0
10741074
].function_response.response == {'result': 'Echo: hello_world'}
1075+
1076+
1077+
@pytest.mark.parametrize('has_context', [False, True])
1078+
@pytest.mark.asyncio
1079+
async def test_node_tool_wraps_zero_argument_function(
1080+
request: pytest.FixtureRequest,
1081+
has_context: bool,
1082+
):
1083+
"""NodeTool supports FunctionNode taking no arguments or only context."""
1084+
1085+
if has_context:
1086+
1087+
@node
1088+
def get_constant(ctx: Context) -> str:
1089+
"""Returns a constant value."""
1090+
return 'constant_val'
1091+
1092+
else:
1093+
1094+
@node
1095+
def get_constant() -> str:
1096+
"""Returns a constant value."""
1097+
return 'constant_val'
1098+
1099+
tool = NodeTool(node=get_constant)
1100+
decl = tool._get_declaration()
1101+
assert decl.name == 'get_constant'
1102+
assert decl.parameters_json_schema is None
1103+
1104+
parent_agent = LlmAgent(
1105+
name='parent_agent',
1106+
model=testing_utils.MockModel.create(
1107+
responses=[
1108+
types.Part.from_function_call(
1109+
name='get_constant',
1110+
args={},
1111+
),
1112+
types.Part.from_text(text='Finished.'),
1113+
]
1114+
),
1115+
tools=[get_constant],
1116+
)
1117+
app = App(
1118+
name=f'{request.function.__name__}_{has_context}',
1119+
root_agent=parent_agent,
1120+
)
1121+
runner = testing_utils.InMemoryRunner(app=app)
1122+
events = await runner.run_async(testing_utils.get_user_content('Run'))
1123+
1124+
func_response_events = [
1125+
e
1126+
for e in events
1127+
if e.content and e.content.parts and e.content.parts[0].function_response
1128+
]
1129+
assert len(func_response_events) == 1
1130+
assert func_response_events[0].content.parts[
1131+
0
1132+
].function_response.response == {'result': 'constant_val'}

0 commit comments

Comments
 (0)