8000 Merge branch 'main' into google-search-display · UNIONTIC/adk-python@463797b · GitHub
[go: up one dir, main page]

Skip to content 8000

Commit 463797b

Browse files
authored
Merge branch 'main' into google-search-display
2 parents dc53678 + 21d2047 commit 463797b

File tree

4 files changed

+127
-36
lines changed

4 files changed

+127
-36
lines changed

src/google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,12 @@
1414

1515
import inspect
1616
from textwrap import dedent
17-
from typing import Any
18-
from typing import Dict
19-
from typing import List
20-
from typing import Optional
21-
from typing import Union
17+
from typing import Any, Dict, List, Optional, Union
2218

2319
from fastapi.encoders import jsonable_encoder
24-
from fastapi.openapi.models import Operation
25-
from fastapi.openapi.models import Parameter
26-
from fastapi.openapi.models import Schema
20+
from fastapi.openapi.models import Operation, Parameter, Schema
2721

28-
from ..common.common import ApiParameter
29-
from ..common.common import PydocHelper
30-
from ..common.common import to_snake_case
22+
from ..common.common import ApiParameter, PydocHelper, to_snake_case
3123

3224

3325
class OperationParser:
@@ -110,7 +102,8 @@ def _process_request_body(self):
110102
description = request_body.description or ''
111103

112104
if schema and schema.type == 'object':
113-
for prop_name, prop_details in schema.properties.items():
105+
properties = schema.properties or {}
106+
for prop_name, prop_details in properties.items():
114107
self.params.append(
115108
ApiParameter(
116109
original_name=prop_name,

src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import List
1818
from typing import Literal
1919
from typing import Optional
20+
from typing import Sequence
2021
from typing import Tuple
2122
from typing import Union
2223

@@ -59,6 +60,40 @@ def snake_to_lower_camel(snake_case_string: str):
5960
])
6061

6162

63+
# TODO: Switch to Gemini `from_json_schema` util when it is released
64+
# in Gemini SDK.
65+
def normalize_json_schema_type(
66+
json_schema_type: Optional[Union[str, Sequence[str]]],
67+
) -> tuple[Optional[str], bool]:
68+
"""Converts a JSON Schema Type into Gemini Schema type.
69+
70+
Adopted and modified from Gemini SDK. This gets the first available schema
71+
type from JSON Schema, and use it to mark Gemini schema type. If JSON Schema
72+
contains a list of types, the first non null type is used.
73+
74+
Remove this after switching to Gemini `from_json_schema`.
75+
"""
76+
if json_schema_type is None:
77+
return None, False
78+
if isinstance(json_schema_type, str):
79+
if json_schema_type == "null":
80+
return None, True
81+
return json_schema_type, False
82+
83+
non_null_types = []
84+
nullable = False
85+
# If json schema type is an array, pick the first non null type.
86+
for type_value in json_schema_type:
87+
if type_value == "null":
88+
nullable = True
89+
else:
90+
non_null_types.append(type_value)
91+
non_null_type = non_null_types[0] if non_null_types else None
92+
return non_null_type, nullable
93+
94+
95+
# TODO: Switch to Gemini `from_json_schema` util when it is released
96+
# in Gemini SDK.
6297
def to_gemini_schema(openapi_schema: Optional[Dict[str, Any]] = None) -> Schema:
6398
"""Converts an OpenAPI schema dictionary to a Gemini Schema object.
6499
@@ -82,13 +117,6 @@ def to_gemini_schema(openapi_schema: Optional[Dict[str, Any]] = None) -> Schema:
82117
if not openapi_schema.get("type"):
83118
openapi_schema["type"] = "object"
84119

85-
# Adding this to avoid "properties: should be non-empty for OBJECT type" error
86-
# See b/385165182
87-
if openapi_schema.get("type", "") == "object" and not openapi_schema.get(
88-
"properties"
89-
):
90-
openapi_schema["properties"] = {"dummy_DO_NOT_GENERATE": {"type": "string"}}
91-
92120
for key, value in openapi_schema.items():
93121
snake_case_key = to_snake_case(key)
94122
# Check if the snake_case_key exists in the Schema model's fields.
@@ -99,7 +127,17 @@ def to_gemini_schema(openapi_schema: Optional[Dict[str, Any]] = None) -> Schema:
99127
# Format: properties[expiration].format: only 'enum' and 'date-time' are
100128
# supported for STRING type
101129
continue
102-
if snake_case_key == "properties" and isinstance(value, dict):
130+
elif snake_case_key == "type":
131+
schema_type, nullable = normalize_json_schema_type(
132+
openapi_schema.get("type", None)
133+
)
134+
# Adding this to force adding a type to an empty dict
135+
# This avoid "... one_of or any_of must specify a type" error
136+
pydantic_schema_data["type"] = schema_type if schema_type else "object"
137+
pydantic_schema_data["type"] = pydantic_schema_data["type"].upper()
138+
if nullable:
139+
pydantic_schema_data["nullable"] = True
140+
elif snake_case_key == "properties" and isinstance(value, dict):
103141
pydantic_schema_data[snake_case_key] = {
104142
k: to_gemini_schema(v) for k, v in value.items()
105143
}

tests/unittests/tools/openapi_tool/openapi_spec_parser/test_operation_parser.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,18 @@ def test_process_request_body_no_name():
164164
assert parser.params[0].param_location == 'body'
165165

166166

167+
def test_process_request_body_empty_object():
168+
"""Test _process_request_body with a schema that is of type object but with no properties."""
169+
operation = Operation(
170+
requestBody=RequestBody(
171+
content={'application/json': MediaType(schema=Schema(type='object'))}
172+
)
173+
)
174+
parser = OperationParser(operation, should_parse=False)
175+
parser._process_request_body()
176+
assert len(parser.params) == 0
177+
178+
167179
def test_dedupe_param_names(sample_operation):
168180
"""Test _dedupe_param_names method."""
169181
parser = OperationParser(sample_operation, should_parse=False)

tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@
1414

1515

1616
import json
17-
from unittest.mock import MagicMock
18-
from unittest.mock import patch
17+
from unittest.mock import MagicMock, patch
1918

20-
from fastapi.openapi.models import MediaType
21-
from fastapi.openapi.models import Operation
19+
from fastapi.openapi.models import MediaType, Operation
2220
from fastapi.openapi.models import Parameter as OpenAPIParameter
2321
from fastapi.openapi.models import RequestBody
2422
from fastapi.openapi.models import Schema as OpenAPISchema
@@ -27,13 +25,13 @@
2725
from google.adk.tools.openapi_tool.common.common import ApiParameter
2826
from google.adk.tools.openapi_tool.openapi_spec_parser.openapi_spec_parser import OperationEndpoint
2927
from google.adk.tools.openapi_tool.openapi_spec_parser.operation_parser import OperationParser
30-
from google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool import RestApiTool
31-
from google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool import snake_to_lower_camel
32-
from google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool import to_gemini_schema
28+
from google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool import (
29+
RestApiTool,
30+
snake_to_lower_camel,
31+
to_gemini_schema,
32+
)
3333
from google.adk.tools.tool_context import ToolContext
34-
from google.genai.types import FunctionDeclaration
35-
from google.genai.types import Schema
36-
from google.genai.types import Type
34+
from google.genai.types import FunctionDeclaration, Schema, Type
3735
import pytest
3836

3937

@@ -790,13 +788,13 @@ def test_to_gemini_schema_empty_dict(self):
790788
result = to_gemini_schema({})
791789
assert isinstance(result, Schema)
792790
assert result.type == Type.OBJECT
793-
assert result.properties == {"dummy_DO_NOT_GENERATE": Schema(type="string")}
791+
assert result.properties is None
794792

795793
def test_to_gemini_schema_dict_with_only_object_type(self):
796794
result = to_gemini_schema({"type": "object"})
797795
assert isinstance(result, Schema)
798796
assert result.type == Type.OBJECT
799-
assert result.properties == {"dummy_DO_NOT_GENERATE": Schema(type="string")}
797+
assert result.properties is None
800798

801799
def test_to_gemini_schema_basic_types(self):
802800
openapi_schema = {
@@ -814,6 +812,42 @@ def test_to_gemini_schema_basic_types(self):
814812
assert gemini_schema.properties["age"].type == Type.INTEGER
815813
assert gemini_schema.properties["is_active"].type == Type.BOOLEAN
816814

815+
def test_to_gemini_schema_array_string_types(self):
816+
openapi_schema = {
817+
"type": "object",
818+
"properties": {
819+
"boolean_field": {"type": "boolean"},
820+
"nonnullable_string": {"type": ["string"]},
821+
"nullable_string": {"type": ["string", "null"]},
822+
"nullable_number": {"type": ["null", "integer"]},
823+
"object_nullable": {"type": "null"},
824+
"multi_types_nullable": {"type": ["string", "null", "integer"]},
825+
"empty_default_object": {},
826+
},
827+
}
828+
gemini_schema = to_gemini_schema(openapi_schema)
829+
assert isinstance(gemini_schema, Schema)
830+
assert gemini_schema.type == Type.OBJECT
831+
assert gemini_schema.properties["boolean_field"].type == Type.BOOLEAN
832+
833+
assert gemini_schema.properties["nonnullable_string"].type == Type.STRING
834+
assert not gemini_schema.properties["nonnullable_string"].nullable
835+
836+
assert gemini_schema.properties["nullable_string"].type == Type.STRING
837+
assert gemini_schema.properties["nullable_string"].nullable
838+
839+
assert gemini_schema.properties["nullable_number"].type == Type.INTEGER
840+
assert gemini_schema.properties["nullable_number"].nullable
841+
842+
assert gemini_schema.properties["object_nullable"].type == Type.OBJECT
843+
assert gemini_schema.properties["object_nullable"].nullable
844+
845+
assert gemini_schema.properties["multi_types_nullable"].type == Type.STRING
846+
assert gemini_schema.properties["multi_types_nullable"].nullable
847+
848+
assert gemini_schema.properties["empty_default_object"].type == Type.OBJECT
849+
assert not gemini_schema.properties["empty_default_object"].nullable
850+
817851
def test_to_gemini_schema_nested_objects(self):
818852
openapi_schema = {
819853
"type": "object",
@@ -895,17 +929,31 @@ def test_to_gemini_schema_required(self):
895929
def test_to_gemini_schema_nested_dict(self):
896930
openapi_schema = {
897931
"type": "object",
898-
"properties": {"metadata": {"key1": "value1", "key2": 123}},
932+
"properties": {
933+
"metadata": {
934+
"type": "object",
935+
"properties": {
936+
"key1": {"type": "object"},
937+
"key2": {"type": "string"},
938+
},
939+
}
940+
},
899941
}
900942
gemini_schema = to_gemini_schema(openapi_schema)
901943
# Since metadata is not properties nor item, it will call to_gemini_schema recursively.
902944
assert isinstance(gemini_schema.properties["metadata"], Schema)
903945
assert (
904946
gemini_schema.properties["metadata"].type == Type.OBJECT
905947
) # add object type by default
906-
assert gemini_schema.properties["metadata"].properties == {
907-
"dummy_DO_NOT_GENERATE": Schema(type="string")
908-
}
948+
assert len(gemini_schema.properties["metadata"].properties) == 2
949+
assert (
950+
gemini_schema.properties["metadata"].properties["key1"].type
951+
== Type.OBJECT
952+
)
953+
assert (
954+
gemini_schema.properties["metadata"].properties["key2"].type
955+
== Type.STRING
956+
)
909957

910958
def test_to_gemini_schema_ignore_title_default_format(self):
911959
openapi_schema = {

0 commit comments

Comments
 (0)
0