mirror of
https://github.com/ComposioHQ/composio.git
synced 2026-09-22 11:46:35 +08:00
3b28dc756e
`_type_to_parameter` in `python/composio/utils/openapi.py` assumed a
property's
`type` was a scalar string. JSON Schema Draft 2020-12 and OpenAPI 3.1
can express
a nullable field as a list of types instead, for example
`{"type": ["string", "null"]}`. Passing that list to the scalar
membership check
raised `TypeError: unhashable type: 'list'`, which could break tool
signature
generation in providers such as `google_adk`.
## Changes
- Handle list-valued `type` definitions as unions.
- Re-run every member through `_type_to_parameter` with a copy of the
full
property schema and only `type` replaced. This preserves sibling fields
such
as an array's `items`, so `{"type": ["array", "null"], "items": ...}`
resolves
to `Optional[List[...]]`.
- Preserve scalar error behavior for invalid member types: an unknown
type in a
list raises `InvalidSchemaError` instead of silently becoming `Any`.
- Keep the existing empty-list fallback to `Any`.
- Add regression coverage for nullable arrays and scalar/list
unknown-type
parity alongside the existing list-valued type cases.
## Type of change
- [x] Bug fix
- [ ] New feature
- [ ] Refactor/Chore
- [ ] Documentation
- [ ] Breaking change
## Verification
From `python/`:
- `uv run pytest tests/test_openapi.py -q` — 18 passed.
- `make chk` — Ruff and mypy passed across the Python SDK, providers,
tests,
examples, and scripts.
## Checklist
- [x] I have read the Code of Conduct and this PR adheres to it
- [x] I ran linters/tests locally and they passed
- [ ] I updated documentation as needed (not applicable: internal
helper)
- [x] I added regression tests
- [ ] I added a changeset (not applicable: Python-only change)
---------
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Signed-off-by: jkomyno <alberto@composio.dev>
Co-authored-by: jkomyno <alberto@composio.dev>
129 lines
3.9 KiB
Python
129 lines
3.9 KiB
Python
"""OpenAPI helpers."""
|
|
|
|
import inspect
|
|
import typing as t
|
|
|
|
from composio.exceptions import InvalidSchemaError
|
|
|
|
OPENAPI_TO_PYTHON = {
|
|
"null": None,
|
|
"number": float,
|
|
"integer": int,
|
|
"boolean": bool,
|
|
"string": str,
|
|
}
|
|
|
|
|
|
# pylint: disable=unused-argument
|
|
def _handle_object_type(schema: t.Dict) -> t.Type:
|
|
# Nested objects are not supported ATM
|
|
return t.Dict[str, t.Any]
|
|
|
|
|
|
def _handle_array_type(schema: t.Dict) -> t.Any:
|
|
# This discards the nested objects
|
|
items_type = schema.get("items", {}).get("type")
|
|
if items_type is None:
|
|
return t.List[t.Any]
|
|
|
|
SubT = _type_to_parameter(schema=schema.get("items", {}))
|
|
return t.List[SubT] # type: ignore
|
|
|
|
|
|
def _handle_enum_type(schema: t.Dict) -> t.Any:
|
|
return t.Literal[tuple(schema["enum"])]
|
|
|
|
|
|
def _type_to_parameter(schema: t.Dict[str, t.Any]) -> t.Any:
|
|
if "enum" in schema:
|
|
return _handle_enum_type(schema=schema)
|
|
|
|
p_type = schema.get("type")
|
|
if isinstance(p_type, list):
|
|
# JSON Schema Draft 2020-12 / OpenAPI 3.1 express a nullable field as a
|
|
# list of types, e.g. {"type": ["string", "null"]}, rather than an anyOf.
|
|
if not p_type:
|
|
return t.Any
|
|
return t.Union[
|
|
tuple(
|
|
_type_to_parameter(schema={**schema, "type": member})
|
|
for member in p_type
|
|
)
|
|
]
|
|
|
|
if p_type in OPENAPI_TO_PYTHON:
|
|
return OPENAPI_TO_PYTHON[p_type]
|
|
|
|
if p_type == "object":
|
|
return _handle_object_type(schema=schema)
|
|
|
|
if p_type == "array":
|
|
return _handle_array_type(schema=schema)
|
|
|
|
if p_type is None:
|
|
# No type specified (e.g. a combiner option that is description-only or
|
|
# an explicit Any), mirroring the top-level fallback below.
|
|
return t.Any
|
|
|
|
raise InvalidSchemaError(f"Invalid property type {p_type}: {schema!r}")
|
|
|
|
|
|
def _handle_composite_type(schemas: t.List[t.Dict]) -> t.Any:
|
|
if not schemas:
|
|
# An empty oneOf/anyOf has no options to union; fall back to Any.
|
|
return t.Any
|
|
return t.Union[tuple(map(_type_to_parameter, schemas))]
|
|
|
|
|
|
def _one_of_to_parameter(schema: t.Dict[str, t.Any]) -> t.Any:
|
|
return _handle_composite_type(schemas=schema["oneOf"])
|
|
|
|
|
|
def _any_of_to_parameter(schema: t.Dict[str, t.Any]) -> t.Any:
|
|
return _handle_composite_type(schemas=schema["anyOf"])
|
|
|
|
|
|
def _all_of_to_parameter(schema: t.Dict[str, t.Any]) -> t.Type:
|
|
composite = {}
|
|
for subschema in schema["allOf"]:
|
|
composite.update(subschema)
|
|
return _type_to_parameter(schema=composite)
|
|
|
|
|
|
def function_signature_from_jsonschema(
|
|
schema: t.Dict[str, t.Any],
|
|
skip_default: bool = False,
|
|
) -> t.List[inspect.Parameter]:
|
|
"""Convert json schema to a list of parameters (`inspect.Parameter`)."""
|
|
parameters = []
|
|
required = set(schema.get("required", []))
|
|
for p_name, p_schema in schema.get("properties", {}).items():
|
|
if "oneOf" in p_schema:
|
|
p_type = _one_of_to_parameter(schema=p_schema)
|
|
elif "anyOf" in p_schema:
|
|
p_type = _any_of_to_parameter(schema=p_schema)
|
|
elif "allOf" in p_schema:
|
|
p_type = _all_of_to_parameter(schema=p_schema)
|
|
elif "type" in p_schema:
|
|
p_type = _type_to_parameter(schema=p_schema)
|
|
else:
|
|
# Handle cases where no type is specified (e.g., Pydantic's Any type)
|
|
# This typically happens when using typing.Any in Pydantic models,
|
|
# which intentionally omits the 'type' field in JSON schema
|
|
p_type = t.Any
|
|
|
|
p_val = p_schema.get("default", None)
|
|
if p_name in required or p_schema.get("required", False) or skip_default:
|
|
p_val = inspect.Parameter.empty
|
|
|
|
parameters.append(
|
|
inspect.Parameter(
|
|
name=p_name,
|
|
annotation=p_type,
|
|
default=p_val,
|
|
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
)
|
|
)
|
|
|
|
return parameters
|