diff --git a/src/google/adk/tools/_function_parameter_parse_util.py b/src/google/adk/tools/_function_parameter_parse_util.py index 9eda7a078c..108b07a120 100644 --- a/src/google/adk/tools/_function_parameter_parse_util.py +++ b/src/google/adk/tools/_function_parameter_parse_util.py @@ -303,8 +303,9 @@ def _parse_schema_from_parameter( schema.default = param.default schema.type = types.Type.OBJECT schema.properties = {} + required_fields = [] for field_name, field_info in param.annotation.model_fields.items(): - schema.properties[field_name] = _parse_schema_from_parameter( + field_schema = _parse_schema_from_parameter( variant, inspect.Parameter( field_name, @@ -313,6 +314,17 @@ def _parse_schema_from_parameter( ), func_name, ) + + if field_info.description: + field_schema.description = field_info.description + + if field_info.is_required(): + required_fields.append(field_name) + + schema.properties[field_name] = field_schema + + schema.required = required_fields + _raise_if_schema_unsupported(variant, schema) return schema if param.annotation is None: diff --git a/tests/unittests/tools/test_build_function_declaration.py b/tests/unittests/tools/test_build_function_declaration.py index 8be1f86520..8b526378a8 100644 --- a/tests/unittests/tools/test_build_function_declaration.py +++ b/tests/unittests/tools/test_build_function_declaration.py @@ -15,6 +15,7 @@ from enum import Enum from typing import Dict from typing import List +from typing import Optional from google.adk.tools import _automatic_function_calling_util from google.adk.tools.tool_context import ToolContext @@ -23,6 +24,7 @@ # TODO: crewai requires python 3.10 as minimum # from crewai_tools import FileReadTool from pydantic import BaseModel +from pydantic import Field import pytest @@ -154,33 +156,82 @@ class SimpleFunction(BaseModel): def test_nested_basemodel_input(): - class ChildInput(BaseModel): - input_str: str + """Test nested Pydantic models with and without Field annotations.""" - class CustomInput(BaseModel): - child: ChildInput + class ChildInput(BaseModel): + name: str = Field(description='The name of the child') + age: int # No Field annotation + nickname: Optional[str] = Field( + default=None, description='Optional nickname' + ) + email: Optional[str] = None # No Field annotation, Optional with default + + class ParentInput(BaseModel): + title: str = Field(description='The title of the parent') + basic_field: str # No Field annotation + child: ChildInput = Field(description='Child information') + optional_field: Optional[str] = Field( + default='default_value', description='An optional field with default' + ) + status: Optional[str] = None # No Field annotation, Optional with default - def simple_function(input: CustomInput) -> str: + def simple_function(input: ParentInput) -> str: return {'result': input} function_decl = _automatic_function_calling_util.build_function_declaration( func=simple_function ) + # Check top-level structure assert function_decl.name == 'simple_function' assert function_decl.parameters.type == 'OBJECT' assert function_decl.parameters.properties['input'].type == 'OBJECT' + + # Check ParentInput properties with and without Field annotations + parent_props = function_decl.parameters.properties['input'].properties + assert parent_props['title'].type == 'STRING' + assert parent_props['title'].description == 'The title of the parent' + assert parent_props['basic_field'].type == 'STRING' + assert parent_props['basic_field'].description is None # No Field annotation + assert parent_props['child'].type == 'OBJECT' + assert parent_props['child'].description == 'Child information' + assert parent_props['optional_field'].type == 'STRING' assert ( - function_decl.parameters.properties['input'].properties['child'].type - == 'OBJECT' + parent_props['optional_field'].description + == 'An optional field with default' ) + assert parent_props['status'].type == 'STRING' + assert parent_props['status'].description is None # No Field annotation + + # Check ParentInput required fields + parent_required = function_decl.parameters.properties['input'].required + assert 'title' in parent_required + assert 'basic_field' in parent_required + assert 'child' in parent_required + assert 'optional_field' not in parent_required # Has default value assert ( - function_decl.parameters.properties['input'] - .properties['child'] - .properties['input_str'] - .type - == 'STRING' - ) + 'status' not in parent_required + ) # No Field annotation, Optional with default + + # Check ChildInput properties with and without Field annotations + child_props = parent_props['child'].properties + assert child_props['name'].type == 'STRING' + assert child_props['name'].description == 'The name of the child' + assert child_props['age'].type == 'INTEGER' + assert child_props['age'].description is None # No Field annotation + assert child_props['nickname'].type == 'STRING' + assert child_props['nickname'].description == 'Optional nickname' + assert child_props['email'].type == 'STRING' + assert child_props['email'].description is None # No Field annotation + + # Check ChildInput required fields + child_required = parent_props['child'].required + assert 'name' in child_required + assert 'age' in child_required + assert 'nickname' not in child_required # Optional with default None + assert ( + 'email' not in child_required + ) # No Field annotation, Optional with default def test_basemodel_with_nested_basemodel(): @@ -223,7 +274,6 @@ def simple_function( def test_enums(): - class InputEnum(Enum): AGENT = 'agent' TOOL = 'tool'