mirror of
https://github.com/EvolutionAPI/adk-python.git
synced 2025-07-13 15:14:50 -06:00

this fixes https://github.com/google/adk-python/issues/1055 and https://github.com/google/adk-python/issues/881 PiperOrigin-RevId: 766288394
263 lines
7.7 KiB
Python
263 lines
7.7 KiB
Python
# 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.
|
|
|
|
from __future__ import annotations
|
|
|
|
import keyword
|
|
from typing import Any
|
|
from typing import Dict
|
|
from typing import List
|
|
from typing import Optional
|
|
from typing import Union
|
|
|
|
from fastapi.openapi.models import Response
|
|
from fastapi.openapi.models import Schema
|
|
from pydantic import BaseModel
|
|
from pydantic import Field
|
|
from pydantic import model_serializer
|
|
|
|
from ..._gemini_schema_util import _to_snake_case
|
|
|
|
|
|
def rename_python_keywords(s: str, prefix: str = 'param_') -> str:
|
|
"""Renames Python keywords by adding a prefix.
|
|
|
|
Example:
|
|
```
|
|
rename_python_keywords('if') -> 'param_if'
|
|
rename_python_keywords('for') -> 'param_for'
|
|
```
|
|
|
|
Args:
|
|
s: The input string.
|
|
prefix: The prefix to add to the keyword.
|
|
|
|
Returns:
|
|
The renamed string.
|
|
"""
|
|
if keyword.iskeyword(s):
|
|
return prefix + s
|
|
return s
|
|
|
|
|
|
class ApiParameter(BaseModel):
|
|
"""Data class representing a function parameter."""
|
|
|
|
original_name: str
|
|
param_location: str
|
|
param_schema: Union[str, Schema]
|
|
description: Optional[str] = ''
|
|
py_name: Optional[str] = ''
|
|
type_value: type[Any] = Field(default=None, init_var=False)
|
|
type_hint: str = Field(default=None, init_var=False)
|
|
required: bool = False
|
|
|
|
def model_post_init(self, _: Any):
|
|
self.py_name = (
|
|
self.py_name
|
|
if self.py_name
|
|
else rename_python_keywords(_to_snake_case(self.original_name))
|
|
)
|
|
if isinstance(self.param_schema, str):
|
|
self.param_schema = Schema.model_validate_json(self.param_schema)
|
|
|
|
self.description = self.description or self.param_schema.description or ''
|
|
self.type_value = TypeHintHelper.get_type_value(self.param_schema)
|
|
self.type_hint = TypeHintHelper.get_type_hint(self.param_schema)
|
|
return self
|
|
|
|
@model_serializer
|
|
def _serialize(self):
|
|
return {
|
|
'original_name': self.original_name,
|
|
'param_location': self.param_location,
|
|
'param_schema': self.param_schema,
|
|
'description': self.description,
|
|
'py_name': self.py_name,
|
|
}
|
|
|
|
def __str__(self):
|
|
return f'{self.py_name}: {self.type_hint}'
|
|
|
|
def to_arg_string(self):
|
|
"""Converts the parameter to an argument string for function call."""
|
|
return f'{self.py_name}={self.py_name}'
|
|
|
|
def to_dict_property(self):
|
|
"""Converts the parameter to a key:value string for dict property."""
|
|
return f'"{self.py_name}": {self.py_name}'
|
|
|
|
def to_pydoc_string(self):
|
|
"""Converts the parameter to a PyDoc parameter docstr."""
|
|
return PydocHelper.generate_param_doc(self)
|
|
|
|
|
|
class TypeHintHelper:
|
|
"""Helper class for generating type hints."""
|
|
|
|
@staticmethod
|
|
def get_type_value(schema: Schema) -> Any:
|
|
"""Generates the Python type value for a given parameter."""
|
|
param_type = schema.type if schema.type else Any
|
|
|
|
if param_type == 'integer':
|
|
return int
|
|
elif param_type == 'number':
|
|
return float
|
|
elif param_type == 'boolean':
|
|
return bool
|
|
elif param_type == 'string':
|
|
return str
|
|
elif param_type == 'array':
|
|
items_type = Any
|
|
if schema.items and schema.items.type:
|
|
items_type = schema.items.type
|
|
|
|
if items_type == 'object':
|
|
return List[Dict[str, Any]]
|
|
else:
|
|
type_map = {
|
|
'integer': int,
|
|
'number': float,
|
|
'boolean': bool,
|
|
'string': str,
|
|
'object': Dict[str, Any],
|
|
'array': List[Any],
|
|
}
|
|
return List[type_map.get(items_type, 'Any')]
|
|
elif param_type == 'object':
|
|
return Dict[str, Any]
|
|
else:
|
|
return Any
|
|
|
|
@staticmethod
|
|
def get_type_hint(schema: Schema) -> str:
|
|
"""Generates the Python type in string for a given parameter."""
|
|
param_type = schema.type if schema.type else 'Any'
|
|
|
|
if param_type == 'integer':
|
|
return 'int'
|
|
elif param_type == 'number':
|
|
return 'float'
|
|
elif param_type == 'boolean':
|
|
return 'bool'
|
|
elif param_type == 'string':
|
|
return 'str'
|
|
elif param_type == 'array':
|
|
items_type = 'Any'
|
|
if schema.items and schema.items.type:
|
|
items_type = schema.items.type
|
|
|
|
if items_type == 'object':
|
|
return 'List[Dict[str, Any]]'
|
|
else:
|
|
type_map = {
|
|
'integer': 'int',
|
|
'number': 'float',
|
|
'boolean': 'bool',
|
|
'string': 'str',
|
|
}
|
|
return f"List[{type_map.get(items_type, 'Any')}]"
|
|
elif param_type == 'object':
|
|
return 'Dict[str, Any]'
|
|
else:
|
|
return 'Any'
|
|
|
|
|
|
class PydocHelper:
|
|
"""Helper class for generating PyDoc strings."""
|
|
|
|
@staticmethod
|
|
def generate_param_doc(
|
|
param: ApiParameter,
|
|
) -> str:
|
|
"""Generates a parameter documentation string.
|
|
|
|
Args:
|
|
param: ApiParameter - The parameter to generate the documentation for.
|
|
|
|
Returns:
|
|
str: The generated parameter Python documentation string.
|
|
"""
|
|
description = param.description.strip() if param.description else ''
|
|
param_doc = f'{param.py_name} ({param.type_hint}): {description}'
|
|
|
|
if param.param_schema.type == 'object':
|
|
properties = param.param_schema.properties
|
|
if properties:
|
|
param_doc += ' Object properties:\n'
|
|
for prop_name, prop_details in properties.items():
|
|
prop_desc = prop_details.description or ''
|
|
prop_type = TypeHintHelper.get_type_hint(prop_details)
|
|
param_doc += f' {prop_name} ({prop_type}): {prop_desc}\n'
|
|
|
|
return param_doc
|
|
|
|
@staticmethod
|
|
def generate_return_doc(responses: Dict[str, Response]) -> str:
|
|
"""Generates a return value documentation string.
|
|
|
|
Args:
|
|
responses: Dict[str, TypedDict[Response]] - Response in an OpenAPI
|
|
Operation
|
|
|
|
Returns:
|
|
str: The generated return value Python documentation string.
|
|
"""
|
|
return_doc = ''
|
|
|
|
# Only consider 2xx responses for return type hinting.
|
|
# Returns the 2xx response with the smallest status code number and with
|
|
# content defined.
|
|
sorted_responses = sorted(responses.items(), key=lambda item: int(item[0]))
|
|
qualified_response = next(
|
|
filter(
|
|
lambda r: r[0].startswith('2') and r[1].content,
|
|
sorted_responses,
|
|
),
|
|
None,
|
|
)
|
|
if not qualified_response:
|
|
return ''
|
|
response_details = qualified_response[1]
|
|
|
|
description = (response_details.description or '').strip()
|
|
content = response_details.content or {}
|
|
|
|
# Generate return type hint and properties for the first response type.
|
|
# TODO(cheliu): Handle multiple content types.
|
|
for _, schema_details in content.items():
|
|
schema = schema_details.schema_ or {}
|
|
|
|
# Use a dummy Parameter object for return type hinting.
|
|
dummy_param = ApiParameter(
|
|
original_name='', param_location='', param_schema=schema
|
|
)
|
|
return_doc = f'Returns ({dummy_param.type_hint}): {description}'
|
|
|
|
response_type = schema.type or 'Any'
|
|
if response_type != 'object':
|
|
break
|
|
properties = schema.properties
|
|
if not properties:
|
|
break
|
|
return_doc += ' Object properties:\n'
|
|
for prop_name, prop_details in properties.items():
|
|
prop_desc = prop_details.description or ''
|
|
prop_type = TypeHintHelper.get_type_hint(prop_details)
|
|
return_doc += f' {prop_name} ({prop_type}): {prop_desc}\n'
|
|
break
|
|
|
|
return return_doc
|