Files

129 lines
3.9 KiB
Python
Raw Permalink Normal View History

2025-06-17 12:58:00 +05:30
"""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)
fix(py): handle typeless and empty combiner schemas in openapi signatures (#3710) ### Summary `function_signature_from_jsonschema` in `python/composio/utils/openapi.py` builds the `__signature__` for tools wrapped by the `google_adk` provider (`providers/google_adk/composio_google_adk/provider.py:66`). It already falls back to `Any` for a top-level property with no `type`, but the nested combiner helpers do not, so real combiner shapes crash at wrap time: - a `oneOf`/`anyOf` option with no `type` key -> `KeyError: 'type'` - an empty `oneOf`/`anyOf` list -> `TypeError: Cannot take a Union of no types` - an empty `allOf` list -> `KeyError: 'type'` (merges to `{}`) This extends the existing "default to Any" intent to nested options. Follow-up to #3708, which fixed the same bug family in `shared.py` and noted this `openapi.py` case for a separate PR. Fixes # ### Changes - `_type_to_parameter`: read `schema.get("type")` instead of `schema["type"]`; when the type is missing, return `t.Any` (mirrors the top-level fallback). An unknown *string* type still raises `InvalidSchemaError`. - `_handle_composite_type`: return `t.Any` for an empty option list instead of building an empty `Union`. Empty `allOf` merges to `{}` and resolves to `Any` via the above. - New `python/tests/test_openapi.py` (11 tests): this builder had none. Asserts the five former crashes now resolve to `Any`, and that existing shapes are unchanged. ### Type of change - [x] Bug fix - [ ] New feature - [ ] Refactor/Chore - [ ] Documentation - [ ] Breaking change ### How Has This Been Tested? From `python/`: ``` uv run pytest tests/test_openapi.py -q # 11 passed make chk # ruff + mypy -> clean ``` To confirm the tests guard the fix, the five former crashes (typeless `anyOf`/`oneOf` option, empty `anyOf`/`oneOf`/`allOf`) raise on the unmodified file and now resolve to `Any`; regressions cover `anyOf:[str,null]` -> `Optional[str]`, a 4-member union, enum, array, object, and a typeless property. ### 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 (internal util) - [x] I added tests (new test_openapi.py) - [ ] I added a changeset (Python-only, not needed) Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
2026-07-02 13:10:35 +05:30
p_type = schema.get("type")
fix(py): handle list-valued JSON Schema type in openapi signatures (#3822) `_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>
2026-07-16 01:04:26 +05:30
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
)
]
2025-06-17 12:58:00 +05:30
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)
fix(py): handle typeless and empty combiner schemas in openapi signatures (#3710) ### Summary `function_signature_from_jsonschema` in `python/composio/utils/openapi.py` builds the `__signature__` for tools wrapped by the `google_adk` provider (`providers/google_adk/composio_google_adk/provider.py:66`). It already falls back to `Any` for a top-level property with no `type`, but the nested combiner helpers do not, so real combiner shapes crash at wrap time: - a `oneOf`/`anyOf` option with no `type` key -> `KeyError: 'type'` - an empty `oneOf`/`anyOf` list -> `TypeError: Cannot take a Union of no types` - an empty `allOf` list -> `KeyError: 'type'` (merges to `{}`) This extends the existing "default to Any" intent to nested options. Follow-up to #3708, which fixed the same bug family in `shared.py` and noted this `openapi.py` case for a separate PR. Fixes # ### Changes - `_type_to_parameter`: read `schema.get("type")` instead of `schema["type"]`; when the type is missing, return `t.Any` (mirrors the top-level fallback). An unknown *string* type still raises `InvalidSchemaError`. - `_handle_composite_type`: return `t.Any` for an empty option list instead of building an empty `Union`. Empty `allOf` merges to `{}` and resolves to `Any` via the above. - New `python/tests/test_openapi.py` (11 tests): this builder had none. Asserts the five former crashes now resolve to `Any`, and that existing shapes are unchanged. ### Type of change - [x] Bug fix - [ ] New feature - [ ] Refactor/Chore - [ ] Documentation - [ ] Breaking change ### How Has This Been Tested? From `python/`: ``` uv run pytest tests/test_openapi.py -q # 11 passed make chk # ruff + mypy -> clean ``` To confirm the tests guard the fix, the five former crashes (typeless `anyOf`/`oneOf` option, empty `anyOf`/`oneOf`/`allOf`) raise on the unmodified file and now resolve to `Any`; regressions cover `anyOf:[str,null]` -> `Optional[str]`, a 4-member union, enum, array, object, and a typeless property. ### 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 (internal util) - [x] I added tests (new test_openapi.py) - [ ] I added a changeset (Python-only, not needed) Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
2026-07-02 13:10:35 +05:30
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
2025-06-17 12:58:00 +05:30
raise InvalidSchemaError(f"Invalid property type {p_type}: {schema!r}")
def _handle_composite_type(schemas: t.List[t.Dict]) -> t.Any:
fix(py): handle typeless and empty combiner schemas in openapi signatures (#3710) ### Summary `function_signature_from_jsonschema` in `python/composio/utils/openapi.py` builds the `__signature__` for tools wrapped by the `google_adk` provider (`providers/google_adk/composio_google_adk/provider.py:66`). It already falls back to `Any` for a top-level property with no `type`, but the nested combiner helpers do not, so real combiner shapes crash at wrap time: - a `oneOf`/`anyOf` option with no `type` key -> `KeyError: 'type'` - an empty `oneOf`/`anyOf` list -> `TypeError: Cannot take a Union of no types` - an empty `allOf` list -> `KeyError: 'type'` (merges to `{}`) This extends the existing "default to Any" intent to nested options. Follow-up to #3708, which fixed the same bug family in `shared.py` and noted this `openapi.py` case for a separate PR. Fixes # ### Changes - `_type_to_parameter`: read `schema.get("type")` instead of `schema["type"]`; when the type is missing, return `t.Any` (mirrors the top-level fallback). An unknown *string* type still raises `InvalidSchemaError`. - `_handle_composite_type`: return `t.Any` for an empty option list instead of building an empty `Union`. Empty `allOf` merges to `{}` and resolves to `Any` via the above. - New `python/tests/test_openapi.py` (11 tests): this builder had none. Asserts the five former crashes now resolve to `Any`, and that existing shapes are unchanged. ### Type of change - [x] Bug fix - [ ] New feature - [ ] Refactor/Chore - [ ] Documentation - [ ] Breaking change ### How Has This Been Tested? From `python/`: ``` uv run pytest tests/test_openapi.py -q # 11 passed make chk # ruff + mypy -> clean ``` To confirm the tests guard the fix, the five former crashes (typeless `anyOf`/`oneOf` option, empty `anyOf`/`oneOf`/`allOf`) raise on the unmodified file and now resolve to `Any`; regressions cover `anyOf:[str,null]` -> `Optional[str]`, a 4-member union, enum, array, object, and a typeless property. ### 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 (internal util) - [x] I added tests (new test_openapi.py) - [ ] I added a changeset (Python-only, not needed) Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
2026-07-02 13:10:35 +05:30
if not schemas:
# An empty oneOf/anyOf has no options to union; fall back to Any.
return t.Any
2025-06-17 12:58:00 +05:30
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
2025-06-17 12:58:00 +05:30
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