diff --git a/google/genai/_mcp_utils.py b/google/genai/_mcp_utils.py index cc7a1642b..f72938ae1 100644 --- a/google/genai/_mcp_utils.py +++ b/google/genai/_mcp_utils.py @@ -57,11 +57,7 @@ def mcp_to_gemini_tool(tool: McpTool) -> types.Tool: function_declarations=[{ "name": tool.name, "description": tool.description, - "parameters": types.Schema.from_json_schema( - json_schema=types.JSONSchema( - **_filter_to_supported_schema(tool.inputSchema) - ) - ), + "parameters_json_schema": tool.inputSchema, }] ) diff --git a/google/genai/tests/types/test_schema_from_json_schema.py b/google/genai/tests/types/test_schema_from_json_schema.py deleted file mode 100644 index 04a72c45e..000000000 --- a/google/genai/tests/types/test_schema_from_json_schema.py +++ /dev/null @@ -1,417 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import logging -import pydantic - -from ... import types - - -def _get_not_none_fields(model: pydantic.BaseModel) -> list[str]: - """Returns field names in a Pydantic model whose values are not None.""" - return [ - field for field, value in model.model_dump().items() if value is not None - ] - - -def test_empty_json_schema_conversion(): - """Test conversion of empty JSONSchema to Schema.""" - json_schema = types.JSONSchema() - gemini_api_schema = types.Schema.from_json_schema(json_schema=json_schema) - vertex_ai_schema = types.Schema.from_json_schema( - json_schema=json_schema, api_option='VERTEX_AI' - ) - - assert gemini_api_schema == types.Schema() - assert vertex_ai_schema == types.Schema() - - -def test_not_null_type_conversion(): - """Test conversion of JSONSchema.type to Schema.type""" - json_schema_types = [ - 'string', - 'number', - 'integer', - 'boolean', - 'array', - 'object', - ] - schema_types = [ - 'STRING', - 'NUMBER', - 'INTEGER', - 'BOOLEAN', - 'ARRAY', - 'OBJECT', - ] - for json_schema_type, expected_type in zip(json_schema_types, schema_types): - json_schema1 = types.JSONSchema(type=types.JSONSchemaType(json_schema_type)) - json_schema2 = types.JSONSchema(type=json_schema_type) - gemini_api_schema1 = types.Schema.from_json_schema(json_schema=json_schema1) - vertex_ai_schema1 = types.Schema.from_json_schema( - json_schema=json_schema1, api_option='VERTEX_AI' - ) - gemini_api_schema2 = types.Schema.from_json_schema(json_schema=json_schema2) - vertex_ai_schema2 = types.Schema.from_json_schema( - json_schema=json_schema2, api_option='VERTEX_AI' - ) - - gemini_api_not_none_field_name1 = _get_not_none_fields(gemini_api_schema1) - vertex_api_not_none_field_name1 = _get_not_none_fields(vertex_ai_schema1) - gemini_api_not_none_field_name2 = _get_not_none_fields(gemini_api_schema2) - vertex_ai_not_none_field_name2 = _get_not_none_fields(vertex_ai_schema2) - - assert gemini_api_schema1.type == expected_type - assert vertex_ai_schema1.type == expected_type - assert gemini_api_schema2.type == expected_type - assert vertex_ai_schema2.type == expected_type - assert gemini_api_not_none_field_name1 == ['type'] - assert vertex_api_not_none_field_name1 == ['type'] - assert gemini_api_not_none_field_name2 == ['type'] - assert vertex_ai_not_none_field_name2 == ['type'] - - -def test_nullable_conversion(): - """Test conversion of JSONSchema.nullable to Schema.nullable""" - json_schema1 = types.JSONSchema( - type=[types.JSONSchemaType('string'), types.JSONSchemaType('null')], - ) - json_schema2 = types.JSONSchema( - type=['string', 'null'], - ) - gemini_api_schema1 = types.Schema.from_json_schema(json_schema=json_schema1) - vertex_ai_schema1 = types.Schema.from_json_schema( - json_schema=json_schema1, api_option='VERTEX_AI' - ) - gemini_api_schema2 = types.Schema.from_json_schema(json_schema=json_schema2) - vertex_ai_schema2 = types.Schema.from_json_schema( - json_schema=json_schema2, api_option='VERTEX_AI' - ) - gemini_api_not_none_field_names1 = _get_not_none_fields(gemini_api_schema1) - vertex_ai_not_none_field_names1 = _get_not_none_fields(vertex_ai_schema1) - gemini_api_not_none_field_names2 = _get_not_none_fields(gemini_api_schema2) - vertex_ai_not_none_field_names2 = _get_not_none_fields(vertex_ai_schema2) - - assert gemini_api_schema1.nullable - assert vertex_ai_schema1.nullable - assert gemini_api_schema2.nullable - assert vertex_ai_schema2.nullable - assert set(gemini_api_not_none_field_names1) == set(['type', 'nullable']) - assert set(vertex_ai_not_none_field_names1) == set(['type', 'nullable']) - assert set(gemini_api_not_none_field_names2) == set(['type', 'nullable']) - assert set(vertex_ai_not_none_field_names2) == set(['type', 'nullable']) - - -def test_nullable_in_union_like_type_conversion(): - """Test conversion of JSONSchema.nullable to Schema.nullable""" - json_schema1 = types.JSONSchema( - type=[ - types.JSONSchemaType('string'), - types.JSONSchemaType('null'), - types.JSONSchemaType('object'), - types.JSONSchemaType('number'), - types.JSONSchemaType('array'), - types.JSONSchemaType('boolean'), - types.JSONSchemaType('integer'), - ], - ) - gemini_api_schema1 = types.Schema.from_json_schema(json_schema=json_schema1) - vertex_ai_schema1 = types.Schema.from_json_schema( - json_schema=json_schema1, api_option='VERTEX_AI' - ) - gemini_api_not_none_field_names1 = _get_not_none_fields(gemini_api_schema1) - vertex_ai_not_none_field_names1 = _get_not_none_fields(vertex_ai_schema1) - json_schema2 = types.JSONSchema( - type=[ - 'string', - 'null', - 'object', - 'number', - 'array', - 'boolean', - 'integer', - ] - ) - gemini_api_schema2 = types.Schema.from_json_schema(json_schema=json_schema2) - vertex_ai_schema2 = types.Schema.from_json_schema( - json_schema=json_schema2, api_option='VERTEX_AI' - ) - expected_schema = types.Schema( - nullable=True, - any_of=[ - types.Schema(type='STRING'), - types.Schema(type='OBJECT'), - types.Schema(type='NUMBER'), - types.Schema(type='ARRAY'), - types.Schema(type='BOOLEAN'), - types.Schema(type='INTEGER'), - ], - ) - - assert gemini_api_schema1 == expected_schema - assert vertex_ai_schema1 == expected_schema - assert gemini_api_schema2 == expected_schema - assert vertex_ai_schema2 == expected_schema - - -def test_union_like_type_conversion_suite1(): - """Test conversion of JSONSchema.type to Schema.any_of""" - json_schema = types.JSONSchema( - type=[ - types.JSONSchemaType('string'), - types.JSONSchemaType('object'), - types.JSONSchemaType('null'), - ], - description='description', - default='default', - max_length=10, - min_length=5, - enum=['value1', 'value2'], - format='format', - pattern='pattern', - title='title', - min_properties=1, - max_properties=2, - required=['field1', 'field2'], - properties={ - 'field1': types.JSONSchema(type='string'), - 'field2': types.JSONSchema(type='integer'), - }, - ) - actual_gemini_api_schema = types.Schema.from_json_schema( - json_schema=json_schema - ) - actual_vertex_ai_schema = types.Schema.from_json_schema( - json_schema=json_schema, api_option='VERTEX_AI' - ) - expected_schema = types.Schema( - nullable=True, - any_of=[ - types.Schema( - type='STRING', - description='description', - max_length=10, - min_length=5, - enum=['value1', 'value2'], - format='format', - pattern='pattern', - title='title', - ), - types.Schema( - type='OBJECT', - properties={ - 'field1': types.Schema(type='STRING'), - 'field2': types.Schema(type='INTEGER'), - }, - required=['field1', 'field2'], - min_properties=1, - max_properties=2, - title='title', - description='description', - ), - ], - ) - - assert actual_gemini_api_schema == expected_schema - assert actual_vertex_ai_schema == expected_schema - - -def test_union_like_type_conversion_suite2(): - """Test conversion of JSONSchema.type to Schema.any_of""" - json_schema = types.JSONSchema( - type=[ - types.JSONSchemaType('integer'), - types.JSONSchemaType('array'), - ], - description='description', - items=types.JSONSchema(type='integer', maximum=2, minimum=1), - min_items=1, - max_items=2, - title='title', - enum=['1', '2'], - maximum=2, - minimum=1, - ) - actual_gemini_api_schema = types.Schema.from_json_schema( - json_schema=json_schema - ) - actual_vertex_ai_schema = types.Schema.from_json_schema( - json_schema=json_schema, api_option='VERTEX_AI' - ) - expected_schema = types.Schema( - any_of=[ - types.Schema( - type='INTEGER', - description='description', - maximum=2, - minimum=1, - enum=['1', '2'], - title='title', - ), - types.Schema( - type='ARRAY', - items=types.Schema(type='INTEGER', maximum=2, minimum=1), - min_items=1, - max_items=2, - title='title', - description='description', - ), - ], - ) - - assert actual_gemini_api_schema == expected_schema - assert actual_vertex_ai_schema == expected_schema - - -def test_array_type_conversion(): - """Test conversion of JSONSchema.items to Schema.items""" - json_schema = types.JSONSchema( - type=types.JSONSchemaType('array'), - items=types.JSONSchema( - type='object', - properties={ - 'field1': types.JSONSchema(type='string'), - 'field2': types.JSONSchema(type='integer'), - }, - required=['field1', 'field2'], - min_properties=1, - max_properties=2, - title='title', - description='description', - ), - ) - gemini_api_schema = types.Schema.from_json_schema(json_schema=json_schema) - vertex_ai_schema = types.Schema.from_json_schema( - json_schema=json_schema, api_option='VERTEX_AI' - ) - expected_schema = types.Schema( - type='ARRAY', - items=types.Schema( - type='OBJECT', - properties={ - 'field1': types.Schema(type='STRING'), - 'field2': types.Schema(type='INTEGER'), - }, - required=['field1', 'field2'], - min_properties=1, - max_properties=2, - title='title', - description='description', - ), - ) - - assert gemini_api_schema == expected_schema - assert vertex_ai_schema == expected_schema - - -def test_complex_object_type_conversion(): - """Test conversion of JSONSchema.properties to Schema.properties""" - json_schema = types.JSONSchema( - type=types.JSONSchemaType('object'), - properties={ - 'field1': types.JSONSchema( - type=['string', 'array', 'null'], - description='description1', - max_length=20, - min_length=15, - enum=['value1', 'value2'], - format='format', - pattern='pattern', - title='title1', - items=types.JSONSchema(type='integer', maximum=2, minimum=1), - min_items=1, - max_items=2, - ), - 'field2': types.JSONSchema(type='integer'), - }, - required=['field1', 'field2'], - min_properties=1, - max_properties=2, - title='title', - description='description', - ) - gemini_api_schema = types.Schema.from_json_schema(json_schema=json_schema) - vertex_ai_schema = types.Schema.from_json_schema( - json_schema=json_schema, api_option='VERTEX_AI' - ) - expected_schema = types.Schema( - type='OBJECT', - properties={ - 'field1': types.Schema( - nullable=True, - any_of=[ - types.Schema( - type='STRING', - description='description1', - max_length=20, - min_length=15, - enum=['value1', 'value2'], - format='format', - pattern='pattern', - title='title1', - ), - types.Schema( - type='ARRAY', - items=types.Schema(type='INTEGER', maximum=2, minimum=1), - min_items=1, - max_items=2, - title='title1', - description='description1', - ), - ], - ), - 'field2': types.Schema(type='INTEGER'), - }, - required=['field1', 'field2'], - min_properties=1, - max_properties=2, - title='title', - description='description', - ) - - assert gemini_api_schema == expected_schema - assert vertex_ai_schema == expected_schema - - -def test_from_json_schema_logs_only_once(caplog): - """Test that the info message is logged only once across multiple from_json_schema calls.""" - from ... import types as types_module - - types_module._from_json_schema_warning_logged = False - - caplog.set_level(logging.INFO, logger='google_genai.types') - - json_schema1 = types_module.JSONSchema(type='string') - schema1 = types_module.Schema.from_json_schema(json_schema=json_schema1) - - assert len(caplog.records) == 1 - assert 'Json Schema is now supported natively' in caplog.text - assert 'response_json_schema' in caplog.text - - json_schema2 = types_module.JSONSchema(type='number') - schema2 = types_module.Schema.from_json_schema(json_schema=json_schema2) - - assert len(caplog.records) == 1 - - json_schema3 = types_module.JSONSchema(type='object') - schema3 = types_module.Schema.from_json_schema(json_schema=json_schema3) - - assert len(caplog.records) == 1 - - assert schema1.type == types_module.Type('STRING') - assert schema2.type == types_module.Type('NUMBER') - assert schema3.type == types.Type('OBJECT') - - types_module._from_json_schema_warning_logged = False diff --git a/google/genai/tests/types/test_schema_json_schema.py b/google/genai/tests/types/test_schema_json_schema.py deleted file mode 100644 index 576c87ee2..000000000 --- a/google/genai/tests/types/test_schema_json_schema.py +++ /dev/null @@ -1,468 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - - -import logging -import pydantic - -from ... import types - - -def _get_not_none_fields(model: pydantic.BaseModel) -> list[str]: - """Returns field names in a Pydantic model whose values are not None.""" - return [ - field for field, value in model.model_dump().items() if value is not None - ] - - -def test_empty_schema_conversion(): - """Test conversion of empty Schema to JSONSchema.""" - schema = types.Schema() - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert json_schema == types.JSONSchema() - assert not_none_field_names == [] - - -def test_not_null_type_conversion(): - """Test conversion of Schema.type to JSONSchema.type.""" - schema_types = [ - 'OBJECT', - 'ARRAY', - 'STRING', - 'NUMBER', - 'BOOLEAN', - 'INTEGER', - ] - json_schema_types = [ - 'object', - 'array', - 'string', - 'number', - 'boolean', - 'integer', - ] - for schema_type, expected_type in zip(schema_types, json_schema_types): - schema = types.Schema(type=schema_type) - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - assert json_schema.type == types.JSONSchemaType(expected_type) - assert not_none_field_names == ['type'] - - -def test_unspecified_type_conversion(): - """Test conversion of Schema.type to JSONSchema.type.""" - schema = types.Schema(type='TYPE_UNSPECIFIED') - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert json_schema.type is None - assert not_none_field_names == [] - - -def test_nullable_conversion(): - """Test conversion of Schema.nullable to JSONSchema.type.""" - schema = types.Schema(type='STRING', nullable=True) - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert set(json_schema.type) == set([ - types.JSONSchemaType('null'), - types.JSONSchemaType('string') - ]) - assert not_none_field_names == ['type'] - - -def test_property_conversion(): - """Test conversion of Schema.properties to JSONSchema.properties.""" - schema = types.Schema( - type='OBJECT', - properties={ - 'key1': types.Schema(type='STRING'), - 'key2': types.Schema(type='NUMBER'), - }, - ) - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert json_schema.properties == { - 'key1': types.JSONSchema(type=types.JSONSchemaType('string')), - 'key2': types.JSONSchema(type=types.JSONSchemaType('number')), - } - assert json_schema.type == types.JSONSchemaType('object') - assert not_none_field_names == ['type', 'properties'] - - -def test_complex_property_conversion(): - """Test conversion of complex Schema.properties to JSONSchema.properties.""" - schema = types.Schema( - type='OBJECT', - properties={ - 'key1': types.Schema( - type='OBJECT', - properties={ - 'key2': types.Schema(type='STRING'), - 'key3': types.Schema(type='NUMBER'), - }, - ), - 'key2': types.Schema(type='ARRAY', items=types.Schema(type='STRING')), - }, - ) - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert json_schema.properties == { - 'key1': types.JSONSchema( - type=types.JSONSchemaType('object'), - properties={ - 'key2': types.JSONSchema(type=types.JSONSchemaType('string')), - 'key3': types.JSONSchema(type=types.JSONSchemaType('number')), - }, - ), - 'key2': types.JSONSchema( - type=types.JSONSchemaType('array'), - items=types.JSONSchema(type=types.JSONSchemaType('string')), - ), - } - assert json_schema.type == types.JSONSchemaType('object') - assert not_none_field_names == ['type', 'properties'] - - -def test_items_conversion(): - """Test conversion of Schema.items to JSONSchema.items.""" - schema = types.Schema( - type='ARRAY', - items=types.Schema(type='STRING'), - ) - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert json_schema.type == types.JSONSchemaType('array') - assert json_schema.items == types.JSONSchema( - type=types.JSONSchemaType('string') - ) - assert not_none_field_names == ['type', 'items'] - - -def test_complex_items_conversion(): - """Test conversion of complex Schema.items to JSONSchema.items.""" - schema = types.Schema( - type='ARRAY', - items=types.Schema( - type='OBJECT', - properties={ - 'key1': types.Schema(type='STRING'), - 'key2': types.Schema(type='NUMBER'), - }, - ), - ) - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert json_schema.type == types.JSONSchemaType('array') - assert json_schema.items == types.JSONSchema( - type=types.JSONSchemaType('object'), - properties={ - 'key1': types.JSONSchema(type=types.JSONSchemaType('string')), - 'key2': types.JSONSchema(type=types.JSONSchemaType('number')), - }, - ) - assert not_none_field_names == ['type', 'items'] - - -def test_any_of_conversion(): - """Test conversion of Schema.any_of to JSONSchema.any_of.""" - schema = types.Schema( - type='OBJECT', - any_of=[ - types.Schema(type='STRING'), - types.Schema(type='NUMBER'), - ], - ) - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert json_schema.type == types.JSONSchemaType('object') - assert json_schema.any_of == [ - types.JSONSchema(type=types.JSONSchemaType('string')), - types.JSONSchema(type=types.JSONSchemaType('number')), - ] - assert not_none_field_names == ['type', 'any_of'] - - -def test_complex_any_of_conversion(): - """Test conversion of complex Schema.any_of to JSONSchema.any_of.""" - schema = types.Schema( - type='OBJECT', - any_of=[ - types.Schema( - type='OBJECT', - properties={ - 'key1': types.Schema(type='STRING'), - 'key2': types.Schema(type='NUMBER'), - }, - ), - types.Schema(type='ARRAY', items=types.Schema(type='STRING')), - ], - ) - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert json_schema.type == types.JSONSchemaType('object') - assert json_schema.any_of == [ - types.JSONSchema( - type=types.JSONSchemaType('object'), - properties={ - 'key1': types.JSONSchema(type=types.JSONSchemaType('string')), - 'key2': types.JSONSchema(type=types.JSONSchemaType('number')), - }, - ), - types.JSONSchema( - type=types.JSONSchemaType('array'), - items=types.JSONSchema(type=types.JSONSchemaType('string')), - ), - ] - assert not_none_field_names == ['type', 'any_of'] - - -def test_example_conversion(): - """Test conversion of Schema.direct to JSONSchema.direct.""" - schema = types.Schema( - example='this is an example', - ) - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert not_none_field_names == [] - - -def test_property_ordering_conversion(): - """Test conversion of Schema.property_ordering to JSONSchema.property_ordering.""" - schema = types.Schema( - property_ordering=['a', 'b'], - ) - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert not_none_field_names == [] - - -def test_direct_conversion(): - """Test Schema fiedls that do not need to be converted.""" - schema = types.Schema( - pattern='^[a-z]+$', - default=1, - max_length=10, - title='title', - min_length=2, - min_properties=3, - max_properties=7, - description='description', - enum=['enum1', 'enum2'], - format='email', - max_items=199, - maximum=300, - min_items=6, - minimum=40, - required=['required1', 'required2'], - ) - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert json_schema.pattern == '^[a-z]+$' - assert json_schema.default == 1 - assert json_schema.max_length == 10 - assert json_schema.title == 'title' - assert json_schema.min_length == 2 - assert json_schema.min_properties == 3 - assert json_schema.max_properties == 7 - assert json_schema.description == 'description' - assert json_schema.enum == ['enum1', 'enum2'] - assert json_schema.format == 'email' - assert json_schema.max_items == 199 - assert json_schema.maximum == 300 - assert json_schema.min_items == 6 - assert json_schema.minimum == 40 - assert json_schema.required == ['required1', 'required2'] - assert not_none_field_names.sort() == [ - 'pattern', - 'default', - 'max_length', - 'title', - 'min_length', - 'min_properties', - 'max_properties', - 'description', - 'enum', - 'format', - 'max_items', - 'maximum', - 'min_items', - 'minimum', - 'required', - ].sort() - - -def test_complex_any_of_conversion(): - schema = types.Schema( - type=types.Type.OBJECT, - title='Fruit Basket', - description='A structured representation of a fruit basket', - properties={ - 'fruit': types.Schema( - type=types.Type.ARRAY, - description='An ordered list of the fruit in the basket', - items=types.Schema( - any_of=[ - types.Schema( - title='Apple', - description='Describes an apple', - type=types.Type.OBJECT, - properties={ - 'type': types.Schema( - type=types.Type.STRING, - description='Always "apple"', - ), - 'variety': types.Schema( - type=types.Type.STRING, - description=( - 'The variety of apple (e.g., "Granny' - ' Smith")' - ), - ), - }, - property_ordering=['type', 'variety'], - required=['type', 'variety'], - ), - types.Schema( - title='Orange', - description='Describes an orange', - type=types.Type.OBJECT, - properties={ - 'type': types.Schema( - type=types.Type.STRING, - description='Always "orange"', - ), - 'variety': types.Schema( - type=types.Type.STRING, - description=( - 'The variety of orange (e.g.,"Navel' - ' orange")' - ), - ), - }, - property_ordering=['type', 'variety'], - required=['type', 'variety'], - ), - ], - ), - ), - }, - required=['fruit'], - ) - json_schema = schema.json_schema - not_none_field_names = _get_not_none_fields(json_schema) - - assert json_schema.type == types.JSONSchemaType('object') - assert json_schema.title == 'Fruit Basket' - assert json_schema.description == 'A structured representation of a fruit basket' - assert json_schema.properties == { - 'fruit': types.JSONSchema( - type=types.JSONSchemaType('array'), - description='An ordered list of the fruit in the basket', - items=types.JSONSchema( - any_of=[ - types.JSONSchema( - title='Apple', - description='Describes an apple', - type=types.JSONSchemaType('object'), - properties={ - 'type': types.JSONSchema( - type=types.JSONSchemaType('string'), - description='Always "apple"', - ), - 'variety': types.JSONSchema( - type=types.JSONSchemaType('string'), - description=( - 'The variety of apple (e.g., "Granny' - ' Smith")' - ), - ), - }, - required=['type', 'variety'], - ), - types.JSONSchema( - title='Orange', - description='Describes an orange', - type=types.JSONSchemaType('object'), - properties={ - 'type': types.JSONSchema( - type=types.JSONSchemaType('string'), - description='Always "orange"', - ), - 'variety': types.JSONSchema( - type=types.JSONSchemaType('string'), - description=( - 'The variety of orange (e.g.,"Navel orange")' - ), - ), - }, - required=['type', 'variety'], - ), - ], - ), - ), - } - assert json_schema.required == ['fruit'] - assert not_none_field_names == [ - 'type', - 'title', - 'description', - 'properties', - 'required', - ] - - -def test_json_schema_logs_only_once(caplog): - """Test that the info message is logged only once across multiple json_schema calls.""" - from ... import types as types_module - - types_module._json_schema_warning_logged = False - - caplog.set_level(logging.INFO, logger='google_genai.types') - - schema1 = types_module.Schema(type='STRING') - json_schema1 = schema1.json_schema - - assert len(caplog.records) == 1 - assert 'Json Schema is now supported natively' in caplog.text - assert 'response_json_schema' in caplog.text - - schema2 = types_module.Schema(type='NUMBER') - json_schema2 = schema2.json_schema - - assert len(caplog.records) == 1 - - schema3 = types_module.Schema(type='OBJECT') - json_schema3 = schema3.json_schema - - assert len(caplog.records) == 1 - - assert json_schema1.type == types_module.JSONSchemaType('string') - assert json_schema2.type == types_module.JSONSchemaType('number') - assert json_schema3.type == types_module.JSONSchemaType('object') - - types_module._json_schema_warning_logged = False diff --git a/google/genai/types.py b/google/genai/types.py index f99fbd070..793cce05a 100644 --- a/google/genai/types.py +++ b/google/genai/types.py @@ -896,22 +896,6 @@ class ResourceScope(_common.CaseInSensitiveEnum): "https://aiplatform.googleapis.com/publishers/google/models/gemini-3-pro-preview""" -class JSONSchemaType(Enum): - """The type of the data supported by JSON Schema. - - The values of the enums are lower case strings, while the values of the enums - for the Type class are upper case strings. - """ - - NULL = 'null' - BOOLEAN = 'boolean' - OBJECT = 'object' - ARRAY = 'array' - NUMBER = 'number' - INTEGER = 'integer' - STRING = 'string' - - class FeatureSelectionPreference(_common.CaseInSensitiveEnum): """Options for feature selection preference.""" @@ -2471,173 +2455,6 @@ class HttpOptionsDict(TypedDict, total=False): HttpOptionsOrDict = Union[HttpOptions, HttpOptionsDict] -class JSONSchema(_common.BaseModel): - """A subset of JSON Schema according to 2020-12 JSON Schema draft. - - Represents a subset of a JSON Schema object that is used by the Gemini model. - The difference between this class and the Schema class is that this class is - compatible with OpenAPI 3.1 schema objects. And the Schema class is used to - make API call to Gemini model. - """ - - type: Optional[Union[JSONSchemaType, list[JSONSchemaType]]] = Field( - default=None, - description="""Validation succeeds if the type of the instance matches the type represented by the given type, or matches at least one of the given types.""", - ) - format: Optional[str] = Field( - default=None, - description='Define semantic information about a string instance.', - ) - title: Optional[str] = Field( - default=None, - description=( - 'A preferably short description about the purpose of the instance' - ' described by the schema.' - ), - ) - description: Optional[str] = Field( - default=None, - description=( - 'An explanation about the purpose of the instance described by the' - ' schema.' - ), - ) - default: Optional[Any] = Field( - default=None, - description=( - 'This keyword can be used to supply a default JSON value associated' - ' with a particular schema.' - ), - ) - items: Optional['JSONSchema'] = Field( - default=None, - description=( - 'Validation succeeds if each element of the instance not covered by' - ' prefixItems validates against this schema.' - ), - ) - min_items: Optional[int] = Field( - default=None, - description=( - 'An array instance is valid if its size is greater than, or equal to,' - ' the value of this keyword.' - ), - ) - max_items: Optional[int] = Field( - default=None, - description=( - 'An array instance is valid if its size is less than, or equal to,' - ' the value of this keyword.' - ), - ) - enum: Optional[list[Any]] = Field( - default=None, - description=( - 'Validation succeeds if the instance is equal to one of the elements' - ' in this keyword’s array value.' - ), - ) - properties: Optional[dict[str, 'JSONSchema']] = Field( - default=None, - description=( - 'Validation succeeds if, for each name that appears in both the' - ' instance and as a name within this keyword’s value, the child' - ' instance for that name successfully validates against the' - ' corresponding schema.' - ), - ) - required: Optional[list[str]] = Field( - default=None, - description=( - 'An object instance is valid against this keyword if every item in' - ' the array is the name of a property in the instance.' - ), - ) - min_properties: Optional[int] = Field( - default=None, - description=( - 'An object instance is valid if its number of properties is greater' - ' than, or equal to, the value of this keyword.' - ), - ) - max_properties: Optional[int] = Field( - default=None, - description=( - 'An object instance is valid if its number of properties is less' - ' than, or equal to, the value of this keyword.' - ), - ) - minimum: Optional[float] = Field( - default=None, - description=( - 'Validation succeeds if the numeric instance is greater than or equal' - ' to the given number.' - ), - ) - maximum: Optional[float] = Field( - default=None, - description=( - 'Validation succeeds if the numeric instance is less than or equal to' - ' the given number.' - ), - ) - min_length: Optional[int] = Field( - default=None, - description=( - 'A string instance is valid against this keyword if its length is' - ' greater than, or equal to, the value of this keyword.' - ), - ) - max_length: Optional[int] = Field( - default=None, - description=( - 'A string instance is valid against this keyword if its length is' - ' less than, or equal to, the value of this keyword.' - ), - ) - pattern: Optional[str] = Field( - default=None, - description=( - 'A string instance is considered valid if the regular expression' - ' matches the instance successfully.' - ), - ) - additional_properties: Optional[Any] = Field( - default=None, - description="""Can either be a boolean or an object; controls the presence of additional properties.""", - ) - any_of: Optional[list['JSONSchema']] = Field( - default=None, - description=( - 'An instance validates successfully against this keyword if it' - ' validates successfully against at least one schema defined by this' - ' keyword’s value.' - ), - ) - unique_items: Optional[bool] = Field( - default=None, - description="""Boolean value that indicates whether the items in an array are unique.""", - ) - ref: Optional[str] = Field( - default=None, - alias='$ref', - description="""Allows indirect references between schema nodes.""", - ) - defs: Optional[dict[str, 'JSONSchema']] = Field( - default=None, - alias='$defs', - description="""Schema definitions to be used with $ref.""", - ) - one_of: Optional[list['JSONSchema']] = Field( - default=None, - description=( - 'An instance validates successfully against this keyword if it' - ' validates successfully against exactly one schema defined by this' - " keyword's value." - ), - ) - - class Schema(_common.BaseModel): """Schema is used to define the format of input/output data. @@ -2745,482 +2562,6 @@ class Schema(_common.BaseModel): default=None, description="""Optional. Data type of the schema field.""" ) - @property - def json_schema(self) -> 'JSONSchema': - """Converts the Schema object to a JSONSchema object, that is compatible with 2020-12 JSON Schema draft. - - Note: Conversion of fields that are not included in the JSONSchema class - are ignored. - Json Schema is now supported natively by both Gemini Enterprise Agent - Platform and Gemini API. Users - are recommended to pass/receive Json Schema directly to/from the API. For - example: - 1. the counter part of GenerateContentConfig.response_schema is - GenerateContentConfig.response_json_schema, which accepts [JSON - Schema](https://json-schema.org/) - 2. the counter part of FunctionDeclaration.parameters is - FunctionDeclaration.parameters_json_schema, which accepts [JSON - Schema](https://json-schema.org/) - 3. the counter part of FunctionDeclaration.response is - FunctionDeclaration.response_json_schema, which accepts [JSON - Schema](https://json-schema.org/) - """ - - global _json_schema_warning_logged - if not _json_schema_warning_logged: - info_message = """ -Note: Conversion of fields that are not included in the JSONSchema class are -ignored. -Json Schema is now supported natively by both Gemini Enterprise Agent Platform and Gemini API. Users -are recommended to pass/receive Json Schema directly to/from the API. For example: -1. the counter part of GenerateContentConfig.response_schema is - GenerateContentConfig.response_json_schema, which accepts [JSON - Schema](https://json-schema.org/) -2. the counter part of FunctionDeclaration.parameters is - FunctionDeclaration.parameters_json_schema, which accepts [JSON - Schema](https://json-schema.org/) -3. the counter part of FunctionDeclaration.response is - FunctionDeclaration.response_json_schema, which accepts [JSON - Schema](https://json-schema.org/) -""" - logger.info(info_message) - _json_schema_warning_logged = True - - json_schema_field_names: set[str] = set(JSONSchema.model_fields.keys()) - schema_field_names: tuple[str] = ( - 'items', - ) # 'additional_properties' to come - list_schema_field_names: tuple[str] = ( - 'any_of', # 'one_of', 'all_of', 'not' to come - ) - dict_schema_field_names: tuple[str] = ('properties',) # 'defs' to come - - def convert_schema(schema: Union['Schema', dict[str, Any]]) -> 'JSONSchema': - if isinstance(schema, pydantic.BaseModel): - schema_dict = schema.model_dump(exclude_none=True) - else: - schema_dict = schema - json_schema = JSONSchema() - for field_name, field_value in schema_dict.items(): - if field_value is None: - continue - elif field_name == 'nullable': - if json_schema.type is None: - json_schema.type = JSONSchemaType.NULL - elif isinstance(json_schema.type, JSONSchemaType): - current_type: JSONSchemaType = json_schema.type - json_schema.type = [current_type, JSONSchemaType.NULL] - elif isinstance(json_schema.type, list): - json_schema.type.append(JSONSchemaType.NULL) - elif field_name not in json_schema_field_names: - continue - elif field_name == 'type': - if field_value == Type.TYPE_UNSPECIFIED: - continue - json_schema_type = JSONSchemaType(field_value.lower()) - if json_schema.type is None: - json_schema.type = json_schema_type - elif isinstance(json_schema.type, JSONSchemaType): - existing_type: JSONSchemaType = json_schema.type - json_schema.type = [existing_type, json_schema_type] - elif isinstance(json_schema.type, list): - json_schema.type.append(json_schema_type) - elif field_name in schema_field_names: - schema_field_value: 'JSONSchema' = convert_schema(field_value) - setattr(json_schema, field_name, schema_field_value) - elif field_name in list_schema_field_names: - list_schema_field_value: list['JSONSchema'] = [ - convert_schema(this_field_value) - for this_field_value in field_value - ] - setattr(json_schema, field_name, list_schema_field_value) - elif field_name in dict_schema_field_names: - dict_schema_field_value: dict[str, 'JSONSchema'] = { - key: convert_schema(value) for key, value in field_value.items() - } - setattr(json_schema, field_name, dict_schema_field_value) - else: - setattr(json_schema, field_name, field_value) - - return json_schema - - return convert_schema(self) - - @classmethod - def from_json_schema( - cls, - *, - json_schema: 'JSONSchema', - api_option: Literal['VERTEX_AI', 'GEMINI_API'] = 'GEMINI_API', - raise_error_on_unsupported_field: bool = False, - ) -> 'Schema': - """Converts a JSONSchema object to a Schema object. - - Note: Conversion of fields that are not included in the JSONSchema class - are ignored. - Json Schema is now supported natively by both Gemini Enterprise Agent - Platform and Gemini API. Users - are recommended to pass/receive Json Schema directly to/from the API. For - example: - 1. the counter part of GenerateContentConfig.response_schema is - GenerateContentConfig.response_json_schema, which accepts [JSON - Schema](https://json-schema.org/) - 2. the counter part of FunctionDeclaration.parameters is - FunctionDeclaration.parameters_json_schema, which accepts [JSON - Schema](https://json-schema.org/) - 3. the counter part of FunctionDeclaration.response is - FunctionDeclaration.response_json_schema, which accepts [JSON - Schema](https://json-schema.org/) - The JSONSchema is compatible with 2020-12 JSON Schema draft, specified by - OpenAPI 3.1. - - Args: - json_schema: JSONSchema object to be converted. - api_option: API option to be used. If set to 'VERTEX_AI', the - JSONSchema will be converted to a Schema object that is compatible - with Gemini Enterprise Agent Platform API. If set to 'GEMINI_API', - the JSONSchema will be converted to a Schema object that is - compatible with Gemini API. Default is 'GEMINI_API'. - raise_error_on_unsupported_field: If set to True, an error will be - raised if the JSONSchema contains any unsupported fields. Default is - False. - - Returns: - Schema object that is compatible with the specified API option. - Raises: - ValueError: If the JSONSchema contains any unsupported fields and - raise_error_on_unsupported_field is set to True. Or if the JSONSchema - is not compatible with the specified API option. - """ - global _from_json_schema_warning_logged - if not _from_json_schema_warning_logged: - info_message = """ -Note: Conversion of fields that are not included in the JSONSchema class are ignored. -Json Schema is now supported natively by both Gemini Enterprise Agent Platform and Gemini API. Users -are recommended to pass/receive Json Schema directly to/from the API. For example: -1. the counter part of GenerateContentConfig.response_schema is - GenerateContentConfig.response_json_schema, which accepts [JSON - Schema](https://json-schema.org/) -2. the counter part of FunctionDeclaration.parameters is - FunctionDeclaration.parameters_json_schema, which accepts [JSON - Schema](https://json-schema.org/) -3. the counter part of FunctionDeclaration.response is - FunctionDeclaration.response_json_schema, which accepts [JSON - Schema](https://json-schema.org/) -""" - logger.info(info_message) - _from_json_schema_warning_logged = True - - google_schema_field_names: set[str] = set(cls.model_fields.keys()) - schema_field_names: tuple[str, ...] = ( - 'items', - ) # 'additional_properties' to come - list_schema_field_names: tuple[str, ...] = ( - 'any_of', # 'one_of', 'all_of', 'not' to come - ) - dict_schema_field_names: tuple[str, ...] = ('properties',) - - related_field_names_by_type: dict[str, tuple[str, ...]] = { - JSONSchemaType.NUMBER.value: ( - 'description', - 'enum', - 'format', - 'maximum', - 'minimum', - 'title', - ), - JSONSchemaType.STRING.value: ( - 'description', - 'enum', - 'format', - 'max_length', - 'min_length', - 'pattern', - 'title', - ), - JSONSchemaType.OBJECT.value: ( - 'any_of', - 'description', - 'max_properties', - 'min_properties', - 'properties', - 'required', - 'title', - ), - JSONSchemaType.ARRAY.value: ( - 'description', - 'items', - 'max_items', - 'min_items', - 'title', - ), - JSONSchemaType.BOOLEAN.value: ( - 'description', - 'title', - ), - } - # Treat `INTEGER` like `NUMBER`. - related_field_names_by_type[JSONSchemaType.INTEGER.value] = ( - related_field_names_by_type[JSONSchemaType.NUMBER.value] - ) - - # placeholder for potential gemini api unsupported fields - gemini_api_unsupported_field_names: tuple[str, ...] = () - - def _resolve_ref( - ref_path: str, root_schema_dict: dict[str, Any] - ) -> dict[str, Any]: - """Helper to resolve a $ref path.""" - current = root_schema_dict - for part in ref_path.lstrip('#/').split('/'): - if part == '$defs': - part = 'defs' - current = current[part] - current.pop('title', None) - if 'properties' in current and current['properties'] is not None: - for prop_schema in current['properties'].values(): - if isinstance(prop_schema, dict): - prop_schema.pop('title', None) - - return current - - def normalize_json_schema_type( - json_schema_type: Optional[ - Union[JSONSchemaType, Sequence[JSONSchemaType], str, Sequence[str]] - ], - ) -> tuple[list[str], bool]: - """Returns (non_null_types, nullable)""" - if json_schema_type is None: - return [], False - type_sequence: Sequence[Union[JSONSchemaType, str]] - if isinstance(json_schema_type, str) or not isinstance( - json_schema_type, Sequence - ): - type_sequence = [json_schema_type] - else: - type_sequence = json_schema_type - non_null_types = [] - nullable = False - for type_value in type_sequence: - if isinstance(type_value, JSONSchemaType): - type_value = type_value.value - if type_value == JSONSchemaType.NULL.value: - nullable = True - else: - non_null_types.append(type_value) - return non_null_types, nullable - - def raise_error_if_cannot_convert( - json_schema_dict: dict[str, Any], - api_option: Literal['VERTEX_AI', 'GEMINI_API'], - raise_error_on_unsupported_field: bool, - ) -> None: - """Raises an error if the JSONSchema cannot be converted to the specified Schema object.""" - if not raise_error_on_unsupported_field: - return - for field_name, field_value in json_schema_dict.items(): - if field_value is None: - continue - if field_name not in google_schema_field_names and field_name not in [ - 'ref', - 'defs', - ]: - raise ValueError( - f'JSONSchema field "{field_name}" is not supported by the Schema' - ' object. And the "raise_error_on_unsupported_field" argument is' - ' set to True. If you still want to convert it into the Schema' - f' object, please either remove the field "{field_name}" from the' - ' JSONSchema object, leave the' - ' "raise_error_on_unsupported_field" unset, or try using' - ' response_json_schema instead.' - ) - if ( - field_name in gemini_api_unsupported_field_names - and api_option == 'GEMINI_API' - ): - raise ValueError( - f'The "{field_name}" field is not supported by the Schema ' - 'object for GEMINI_API.' - ) - - def copy_schema_fields( - json_schema_dict: dict[str, Any], - related_fields_to_copy: tuple[str, ...], - sub_schema_in_any_of: dict[str, Any], - ) -> None: - """Copies the fields from json_schema_dict to sub_schema_in_any_of.""" - for field_name in related_fields_to_copy: - sub_schema_in_any_of[field_name] = json_schema_dict.get( - field_name, None - ) - - def convert_json_schema( - current_json_schema: 'JSONSchema', - root_json_schema_dict: dict[str, Any], - api_option: Literal['VERTEX_AI', 'GEMINI_API'], - raise_error_on_unsupported_field: bool, - visited_refs: Optional[set[str]] = None, - ) -> 'Schema': - if visited_refs is None: - visited_refs = set() - - schema = Schema() - json_schema_dict = current_json_schema.model_dump() - - ref = json_schema_dict.get('ref') - if ref: - if ref in visited_refs: - return Schema() - visited_refs.add(ref) - json_schema_dict = _resolve_ref(ref, root_json_schema_dict) - - raise_error_if_cannot_convert( - json_schema_dict=json_schema_dict, - api_option=api_option, - raise_error_on_unsupported_field=raise_error_on_unsupported_field, - ) - - # At the highest level of the logic, there are two passes: - # Pass 1: the JSONSchema.type is union-like, - # e.g. ['null', 'string', 'array']. - # for this case, we need to split the JSONSchema into multiple - # sub-schemas, and copy them into the any_of field of the Schema. - # And when we copy the non-type fields into any_of field, - # we only copy the fields related to the specific type. - # Detailed logic is commented below with `Pass 1` keyword tag. - # Pass 2: the JSONSchema.type is not union-like, - # e.g. 'string', ['string'], ['null', 'string']. - # for this case, no splitting is needed. Detailed - # logic is commented below with `Pass 2` keyword tag. - # - # - # Pass 1: the JSONSchema.type is union-like - # e.g. ['null', 'string', 'array']. - non_null_types, nullable = normalize_json_schema_type( - json_schema_dict.get('type', None) - ) - is_union_like_type = len(non_null_types) > 1 - if len(non_null_types) > 1: - logger.warning( - 'JSONSchema type is union-like, e.g. ["null", "string", "array"]. ' - 'Converting it into multiple sub-schemas, and copying them into ' - 'the any_of field of the Schema. The value of `default` field is ' - 'ignored because it is ambiguous to tell which sub-schema it ' - 'belongs to.' - ) - reformed_json_schema = JSONSchema() - # start splitting the JSONSchema into multiple sub-schemas - any_of = [] - if nullable: - schema.nullable = True - for normalized_type in non_null_types: - sub_schema_in_any_of = {'type': normalized_type} - related_field_names = related_field_names_by_type.get(normalized_type) - if related_field_names is not None: - copy_schema_fields( - json_schema_dict=json_schema_dict, - related_fields_to_copy=related_field_names, - sub_schema_in_any_of=sub_schema_in_any_of, - ) - any_of.append(JSONSchema(**sub_schema_in_any_of)) - reformed_json_schema.any_of = any_of - json_schema_dict = reformed_json_schema.model_dump() - - # Pass 2: the JSONSchema.type is not union-like, - # e.g. 'string', ['string'], ['null', 'string']. - for field_name, field_value in json_schema_dict.items(): - if field_value is None or field_name == 'defs': - continue - if field_name in schema_field_names: - if field_name == 'items' and not field_value: - continue - schema_field_value: 'Schema' = convert_json_schema( - current_json_schema=JSONSchema(**field_value), - root_json_schema_dict=root_json_schema_dict, - api_option=api_option, - raise_error_on_unsupported_field=raise_error_on_unsupported_field, - visited_refs=visited_refs, - ) - setattr(schema, field_name, schema_field_value) - elif field_name in list_schema_field_names: - list_schema_field_value: list['Schema'] = [ - convert_json_schema( - current_json_schema=JSONSchema(**this_field_value), - root_json_schema_dict=root_json_schema_dict, - api_option=api_option, - raise_error_on_unsupported_field=raise_error_on_unsupported_field, - visited_refs=visited_refs, - ) - for this_field_value in field_value - ] - setattr(schema, field_name, list_schema_field_value) - if not schema.type and not is_union_like_type and not schema.any_of: - schema.type = Type('OBJECT') - elif field_name in dict_schema_field_names: - dict_schema_field_value: dict[str, 'Schema'] = { - key: convert_json_schema( - current_json_schema=JSONSchema(**value), - root_json_schema_dict=root_json_schema_dict, - api_option=api_option, - raise_error_on_unsupported_field=raise_error_on_unsupported_field, - visited_refs=visited_refs, - ) - for key, value in field_value.items() - } - setattr(schema, field_name, dict_schema_field_value) - elif field_name == 'type': - non_null_types, nullable = normalize_json_schema_type(field_value) - if nullable: - schema.nullable = True - if non_null_types: - schema.type = Type(non_null_types[0]) - else: - if ( - hasattr(schema, field_name) - and field_name != 'additional_properties' - ): - setattr(schema, field_name, field_value) - - if ( - schema.type == 'ARRAY' - and schema.items - and not schema.items.model_dump(exclude_unset=True) - ): - schema.items = None - - if schema.any_of and len(schema.any_of) == 2: - nullable_part = None - type_part = None - for part in schema.any_of: - # A schema representing `None` will either be of type NULL or just be nullable. - part_dict = part.model_dump(exclude_unset=True) - if part_dict == {'nullable': True} or part_dict == {'type': 'NULL'}: - nullable_part = part - else: - type_part = part - - # If we found both parts, unwrap them into a single schema. - if nullable_part and type_part: - default_value = schema.default - schema = type_part - schema.nullable = True - # Carry the default value over to the unwrapped schema - if default_value is not None: - schema.default = default_value - - if ref: - visited_refs.remove(ref) - return schema - - # This is the initial call to the recursive function. - root_schema_dict = json_schema.model_dump() - return convert_json_schema( - current_json_schema=json_schema, - root_json_schema_dict=root_schema_dict, - api_option=api_option, - raise_error_on_unsupported_field=raise_error_on_unsupported_field, - ) - class SchemaDict(TypedDict, total=False): """Schema is used to define the format of input/output data.