From 53df35ee58599e9816bd4b9c42ff48457505e599 Mon Sep 17 00:00:00 2001 From: Thiago Salvatore Date: Thu, 17 Jul 2025 18:16:17 -0300 Subject: [PATCH 1/3] fix(schema to dict): fix serialization of tools with nested schema --- src/google/adk/models/lite_llm.py | 66 +++++++++++++++----------- tests/unittests/models/test_litellm.py | 53 +++++++++++++++++++++ 2 files changed, 90 insertions(+), 29 deletions(-) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 97a9e781..59aa6774 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -311,38 +311,46 @@ TYPE_LABELS = { def _schema_to_dict(schema: types.Schema) -> dict: - """Recursively converts a types.Schema to a dictionary. + """ + Recursively converts a types.Schema to a pure-python dict + with all enum values written as lower-case strings. + """ + # Dump without json encoding so we still get Enum members + schema_dict = schema.model_dump(exclude_none=True) - Args: - schema: The schema to convert. + # ---- normalise this level ------------------------------------------------ + if "type" in schema_dict: + # schema_dict["type"] can be an Enum or a str + t = schema_dict["type"] + schema_dict["type"] = (t.value if isinstance(t, types.Type) else t).lower() - Returns: - The dictionary representation of the schema. - """ + # ---- recurse into `items` ----------------------------------------------- + if "items" in schema_dict: + schema_dict["items"] = _schema_to_dict( + schema.items if isinstance(schema.items, types.Schema) + else types.Schema.model_validate(schema_dict["items"]) + ) - schema_dict = schema.model_dump(exclude_none=True) - if "type" in schema_dict: - schema_dict["type"] = schema_dict["type"].lower() - if "items" in schema_dict: - if isinstance(schema_dict["items"], dict): - schema_dict["items"] = _schema_to_dict( - types.Schema.model_validate(schema_dict["items"]) - ) - elif isinstance(schema_dict["items"]["type"], types.Type): - schema_dict["items"]["type"] = TYPE_LABELS[ - schema_dict["items"]["type"].value - ] - if "properties" in schema_dict: - properties = {} - for key, value in schema_dict["properties"].items(): - if isinstance(value, types.Schema): - properties[key] = _schema_to_dict(value) - else: - properties[key] = value - if "type" in properties[key]: - properties[key]["type"] = properties[key]["type"].lower() - schema_dict["properties"] = properties - return schema_dict + # ---- recurse into `properties` ------------------------------------------ + if "properties" in schema_dict: + new_props = {} + for key, value in schema_dict["properties"].items(): + # value is a dict → rebuild a Schema object and recurse + if isinstance(value, dict): + new_props[key] = _schema_to_dict( + types.Schema.model_validate(value) + ) + # value is already a Schema instance + elif isinstance(value, types.Schema): + new_props[key] = _schema_to_dict(value) + # plain dict without nested schemas + else: + new_props[key] = value + if "type" in new_props[key]: + new_props[key]["type"] = new_props[key]["type"].lower() + schema_dict["properties"] = new_props + + return schema_dict def _function_declaration_to_tool_param( diff --git a/tests/unittests/models/test_litellm.py b/tests/unittests/models/test_litellm.py index b584489d..5e412ad7 100644 --- a/tests/unittests/models/test_litellm.py +++ b/tests/unittests/models/test_litellm.py @@ -673,6 +673,59 @@ function_declaration_test_cases = [ }, }, ), + ( + "nested_properties", + types.FunctionDeclaration( + name="test_function_nested_properties", + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "array_arg": types.Schema( + type=types.Type.ARRAY, + items=types.Schema( + type=types.Type.OBJECT, + properties={ + "nested_key": types.Schema( + type=types.Type.OBJECT, + properties={ + "inner_key": types.Schema( + type=types.Type.STRING, + ) + } + ) + } + ) + ), + }, + ), + ), + { + "type": "function", + "function": { + "name": "test_function_nested_properties", + "description": "", + "parameters": { + "type": "object", + "properties": { + "array_arg": { + "items": { + "type": "object", + "properties": { + "nested_key": { + "type": "object", + "properties": { + "inner_key": {"type": "string"}, + }, + }, + }, + }, + "type": "array", + }, + }, + }, + }, + }, + ), ] From b5850e07577a335286be1020924bfad5d02fadf2 Mon Sep 17 00:00:00 2001 From: Thiago Salvatore Date: Thu, 17 Jul 2025 18:17:09 -0300 Subject: [PATCH 2/3] reformat file structure --- src/google/adk/models/lite_llm.py | 71 +++++++++++++------------- tests/unittests/models/test_litellm.py | 6 +-- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 59aa6774..5ffcf630 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -311,46 +311,45 @@ TYPE_LABELS = { def _schema_to_dict(schema: types.Schema) -> dict: - """ - Recursively converts a types.Schema to a pure-python dict - with all enum values written as lower-case strings. - """ - # Dump without json encoding so we still get Enum members - schema_dict = schema.model_dump(exclude_none=True) + """ + Recursively converts a types.Schema to a pure-python dict + with all enum values written as lower-case strings. + """ + # Dump without json encoding so we still get Enum members + schema_dict = schema.model_dump(exclude_none=True) - # ---- normalise this level ------------------------------------------------ - if "type" in schema_dict: - # schema_dict["type"] can be an Enum or a str - t = schema_dict["type"] - schema_dict["type"] = (t.value if isinstance(t, types.Type) else t).lower() + # ---- normalise this level ------------------------------------------------ + if "type" in schema_dict: + # schema_dict["type"] can be an Enum or a str + t = schema_dict["type"] + schema_dict["type"] = (t.value if isinstance(t, types.Type) else t).lower() - # ---- recurse into `items` ----------------------------------------------- - if "items" in schema_dict: - schema_dict["items"] = _schema_to_dict( - schema.items if isinstance(schema.items, types.Schema) - else types.Schema.model_validate(schema_dict["items"]) - ) + # ---- recurse into `items` ----------------------------------------------- + if "items" in schema_dict: + schema_dict["items"] = _schema_to_dict( + schema.items + if isinstance(schema.items, types.Schema) + else types.Schema.model_validate(schema_dict["items"]) + ) - # ---- recurse into `properties` ------------------------------------------ - if "properties" in schema_dict: - new_props = {} - for key, value in schema_dict["properties"].items(): - # value is a dict → rebuild a Schema object and recurse - if isinstance(value, dict): - new_props[key] = _schema_to_dict( - types.Schema.model_validate(value) - ) - # value is already a Schema instance - elif isinstance(value, types.Schema): - new_props[key] = _schema_to_dict(value) - # plain dict without nested schemas - else: - new_props[key] = value - if "type" in new_props[key]: - new_props[key]["type"] = new_props[key]["type"].lower() - schema_dict["properties"] = new_props + # ---- recurse into `properties` ------------------------------------------ + if "properties" in schema_dict: + new_props = {} + for key, value in schema_dict["properties"].items(): + # value is a dict → rebuild a Schema object and recurse + if isinstance(value, dict): + new_props[key] = _schema_to_dict(types.Schema.model_validate(value)) + # value is already a Schema instance + elif isinstance(value, types.Schema): + new_props[key] = _schema_to_dict(value) + # plain dict without nested schemas + else: + new_props[key] = value + if "type" in new_props[key]: + new_props[key]["type"] = new_props[key]["type"].lower() + schema_dict["properties"] = new_props - return schema_dict + return schema_dict def _function_declaration_to_tool_param( diff --git a/tests/unittests/models/test_litellm.py b/tests/unittests/models/test_litellm.py index 5e412ad7..1328ee81 100644 --- a/tests/unittests/models/test_litellm.py +++ b/tests/unittests/models/test_litellm.py @@ -691,10 +691,10 @@ function_declaration_test_cases = [ "inner_key": types.Schema( type=types.Type.STRING, ) - } + }, ) - } - ) + }, + ), ), }, ), From 2f8bb91e6bdbcc3751dcb994dfc3534a0819444b Mon Sep 17 00:00:00 2001 From: Thiago Salvatore Date: Thu, 17 Jul 2025 18:21:40 -0300 Subject: [PATCH 3/3] add docstring --- src/google/adk/models/lite_llm.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 5ffcf630..ddce6a73 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -314,6 +314,12 @@ def _schema_to_dict(schema: types.Schema) -> dict: """ Recursively converts a types.Schema to a pure-python dict with all enum values written as lower-case strings. + + Args: + schema: The schema to convert. + + Returns: + The dictionary representation of the schema. """ # Dump without json encoding so we still get Enum members schema_dict = schema.model_dump(exclude_none=True)