12
12
# See the License for the specific language governing permissions and
13
13
# limitations under the License.
14
14
15
+ from google .adk .tools ._gemini_schema_util import _sanitize_schema_formats_for_gemini
15
16
from google .adk .tools ._gemini_schema_util import _to_gemini_schema
16
17
from google .adk .tools ._gemini_schema_util import _to_snake_case
17
18
from google .genai .types import Schema
@@ -31,7 +32,7 @@ def test_to_gemini_schema_not_dict(self):
31
32
def test_to_gemini_schema_empty_dict (self ):
32
33
result = _to_gemini_schema ({})
33
34
assert isinstance (result , Schema )
34
- assert result .type is None
35
+ assert result .type is Type . OBJECT
35
36
assert result .properties is None
36
37
37
38
def test_to_gemini_schema_dict_with_only_object_type (self ):
@@ -64,10 +65,8 @@ def test_to_gemini_schema_array_string_types(self):
64
65
"nonnullable_string" : {"type" : ["string" ]},
65
66
"nullable_string" : {"type" : ["string" , "null" ]},
66
67
"nullable_number" : {"type" : ["null" , "integer" ]},
67
- "object_nullable" : {"type" : "null" }, # invalid
68
- "multi_types_nullable" : {
69
- "type" : ["string" , "null" , "integer" ]
70
- }, # invalid
68
+ "object_nullable" : {"type" : "null" },
69
+ "multi_types_nullable" : {"type" : ["string" , "null" , "integer" ]},
71
70
"empty_default_object" : {},
72
71
},
73
72
}
@@ -85,14 +84,14 @@ def test_to_gemini_schema_array_string_types(self):
85
84
assert gemini_schema .properties ["nullable_number" ].type == Type .INTEGER
86
85
assert gemini_schema .properties ["nullable_number" ].nullable
87
86
88
- assert gemini_schema .properties ["object_nullable" ].type is None
87
+ assert gemini_schema .properties ["object_nullable" ].type == Type . OBJECT
89
88
assert gemini_schema .properties ["object_nullable" ].nullable
90
89
91
- assert gemini_schema .properties ["multi_types_nullable" ].type is None
90
+ assert gemini_schema .properties ["multi_types_nullable" ].type == Type . STRING
92
91
assert gemini_schema .properties ["multi_types_nullable" ].nullable
93
92
94
- assert gemini_schema .properties ["empty_default_object" ].type is None
95
- assert not gemini_schema .properties ["empty_default_object" ].nullable
93
+ assert gemini_schema .properties ["empty_default_object" ].type == Type . OBJECT
94
+ assert gemini_schema .properties ["empty_default_object" ].nullable is None
96
95
97
96
def test_to_gemini_schema_nested_objects (self ):
98
97
openapi_schema = {
@@ -382,6 +381,136 @@ def test_to_gemini_schema_property_ordering(self):
382
381
gemini_schema = _to_gemini_schema (openapi_schema )
383
382
assert gemini_schema .property_ordering == ["name" , "age" ]
384
383
384
+ def test_sanitize_schema_formats_for_gemini (self ):
385
+ schema = {
386
+ "type" : "object" ,
387
+ "description" : "Test schema" , # Top-level description
388
+ "properties" : {
389
+ "valid_int" : {"type" : "integer" , "format" : "int32" },
390
+ "invalid_format_prop" : {"type" : "integer" , "format" : "unsigned" },
391
+ "valid_string" : {"type" : "string" , "format" : "date-time" },
392
+ "camelCaseKey" : {"type" : "string" },
393
+ "prop_with_extra_key" : {
394
+ "type" : "boolean" ,
395
+ "unknownInternalKey" : "discard_this_value" ,
396
+ },
397
+ },
398
+ "required" : ["valid_int" ],
399
+ "additionalProperties" : False , # This is an unsupported top-level key
400
+ "unknownTopLevelKey" : (
401
+ "discard_me_too"
402
+ ), # Another unsupported top-level key
403
+ }
404
+ sanitized = _sanitize_schema_formats_for_gemini (schema )
405
+
406
+ # Check description is preserved
407
+ assert sanitized ["description" ] == "Test schema"
408
+
409
+ # Check properties and their sanitization
410
+ assert "properties" in sanitized
411
+ sanitized_props = sanitized ["properties" ]
412
+
413
+ assert "valid_int" in sanitized_props
414
+ assert sanitized_props ["valid_int" ]["type" ] == "integer"
415
+ assert sanitized_props ["valid_int" ]["format" ] == "int32"
416
+
417
+ assert "invalid_format_prop" in sanitized_props
418
+ assert sanitized_props ["invalid_format_prop" ]["type" ] == "integer"
419
+ assert (
420
+ "format" not in sanitized_props ["invalid_format_prop" ]
421
+ ) # Invalid format removed
422
+
423
+ assert "valid_string" in sanitized_props
424
+ assert sanitized_props ["valid_string" ]["type" ] == "string"
425
+ assert sanitized_props ["valid_string" ]["format" ] == "date-time"
426
+
427
+ # Check camelCase keys not changed for properties
428
+ assert "camel_case_key" not in sanitized_props
429
+ assert "camelCaseKey" in sanitized_props
430
+ assert sanitized_props ["camelCaseKey" ]["type" ] == "string"
431
+
432
+ # Check removal of unsupported keys within a property definition
433
+ assert "prop_with_extra_key" in sanitized_props
434
+ assert sanitized_props ["prop_with_extra_key" ]["type" ] == "boolean"
435
+ assert (
436
+ "unknown_internal_key" # snake_cased version of unknownInternalKey
437
+ not in sanitized_props ["prop_with_extra_key" ]
438
+ )
439
+
440
+ # Check removal of unsupported top-level fields (after snake_casing)
441
+ assert "additional_properties" not in sanitized
442
+ assert "unknown_top_level_key" not in sanitized
443
+
444
+ # Check original unsupported top-level field names are not there either
445
+ assert "additionalProperties" not in sanitized
446
+ assert "unknownTopLevelKey" not in sanitized
447
+
448
+ # Check required is preserved
449
+ assert sanitized ["required" ] == ["valid_int" ]
450
+
451
+ # Test with a schema that has a list of types for a property
452
+ schema_with_list_type = {
453
+ "type" : "object" ,
454
+ "properties" : {
455
+ "nullable_field" : {"type" : ["string" , "null" ], "format" : "uuid" }
456
+ },
457
+ }
458
+ sanitized_list_type = _sanitize_schema_formats_for_gemini (
459
+ schema_with_list_type
460
+ )
461
+ # format should be removed because 'uuid' is not supported for string
462
+ assert "format" not in sanitized_list_type ["properties" ]["nullable_field" ]
463
+ # type should be processed by _sanitize_schema_type and preserved
464
+ assert sanitized_list_type ["properties" ]["nullable_field" ]["type" ] == [
465
+ "string" ,
466
+ "null" ,
467
+ ]
468
+
469
+ def test_sanitize_schema_formats_for_gemini_nullable (self ):
470
+ openapi_schema = {
471
+ "properties" : {
472
+ "case_id" : {
473
+ "description" : "The ID of the case." ,
474
+ "title" : "Case Id" ,
475
+ "type" : "string" ,
476
+ },
477
+ "next_page_token" : {
478
+ "anyOf" : [{"type" : "string" }, {"type" : "null" }],
479
+ "default" : None ,
480
+ "description" : (
481
+ "The nextPageToken to fetch the next page of results."
482
+ ),
483
+ "title" : "Next Page Token" ,
484
+ },
485
+ },
486
+ "required" : ["case_id" ],
487
+ "title" : "list_alerts_by_caseArguments" ,
488
+ "type" : "object" ,
489
+ }
490
+ openapi_schema = _sanitize_schema_formats_for_gemini (openapi_schema )
491
+ assert openapi_schema == {
492
+ "properties" : {
493
+ "case_id" : {
494
+ "description" : "The ID of the case." ,
495
+ "title" : "Case Id" ,
496
+ "type" : "string" ,
497
+ },
498
+ "next_page_token" : {
499
+ "any_of" : [
500
+ {"type" : "string" },
501
+ {"type" : ["object" , "null" ]},
502
+ ],
503
+ "description" : (
504
+ "The nextPageToken to fetch the next page of results."
505
+ ),
506
+ "title" : "Next Page Token" ,
507
+ },
508
+ },
509
+ "required" : ["case_id" ],
510
+ "title" : "list_alerts_by_caseArguments" ,
511
+ "type" : "object" ,
512
+ }
513
+
385
514
386
515
class TestToSnakeCase :
387
516
0 commit comments