fix: Filter out None values from enum lists in schema generation

Close #3552

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 834967220
This commit is contained in:
George Weale
2025-11-20 17:02:45 -08:00
committed by Copybara-Service
parent 31cfa3b82b
commit 848fdbef7c
2 changed files with 43 additions and 24 deletions
+20 -24
View File
@@ -632,10 +632,8 @@ 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.
def _schema_to_dict(schema: types.Schema | dict[str, Any]) -> dict:
"""Recursively converts a schema object or dict to a pure-python dict.
Args:
schema: The schema to convert.
@@ -643,38 +641,36 @@ def _schema_to_dict(schema: types.Schema) -> dict:
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)
schema_dict = (
schema.model_dump(exclude_none=True)
if isinstance(schema, types.Schema)
else dict(schema)
)
enum_values = schema_dict.get("enum")
if isinstance(enum_values, (list, tuple)):
schema_dict["enum"] = [value for value in enum_values if value is not None]
# ---- normalise this level ------------------------------------------------
if "type" in schema_dict:
# schema_dict["type"] can be an Enum or a str
if "type" in schema_dict and schema_dict["type"] is not None:
t = schema_dict["type"]
schema_dict["type"] = (t.value if isinstance(t, types.Type) else t).lower()
schema_dict["type"] = (
t.value if isinstance(t, types.Type) else str(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"])
items = schema_dict["items"]
schema_dict["items"] = (
_schema_to_dict(items)
if isinstance(items, (types.Schema, dict))
else 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):
if isinstance(value, (types.Schema, dict)):
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
+23
View File
@@ -26,6 +26,7 @@ from google.adk.models.lite_llm import _message_to_generate_content_response
from google.adk.models.lite_llm import _model_response_to_chunk
from google.adk.models.lite_llm import _model_response_to_generate_content_response
from google.adk.models.lite_llm import _parse_tool_calls_from_text
from google.adk.models.lite_llm import _schema_to_dict
from google.adk.models.lite_llm import _split_message_content_and_tool_calls
from google.adk.models.lite_llm import _to_litellm_response_format
from google.adk.models.lite_llm import _to_litellm_role
@@ -286,6 +287,28 @@ def test_to_litellm_response_format_handles_genai_schema_instance():
)
def test_schema_to_dict_filters_none_enum_values():
# Use model_construct to bypass strict enum validation.
top_level_schema = types.Schema.model_construct(
type=types.Type.STRING,
enum=["ACTIVE", None, "INACTIVE"],
)
nested_schema = types.Schema.model_construct(
type=types.Type.OBJECT,
properties={
"status": types.Schema.model_construct(
type=types.Type.STRING, enum=["READY", None, "DONE"]
),
},
)
assert _schema_to_dict(top_level_schema)["enum"] == ["ACTIVE", "INACTIVE"]
assert _schema_to_dict(nested_schema)["properties"]["status"]["enum"] == [
"READY",
"DONE",
]
MULTIPLE_FUNCTION_CALLS_STREAM = [
ModelResponse(
choices=[