Files

842 lines
30 KiB
Python
Raw Permalink Normal View History

2025-06-17 12:58:00 +05:30
"""
Shared utils.
"""
import copy
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
import dataclasses
import hashlib
fix(providers): normalize string tool-call arguments across all providers (TS + Python) (#3514) ## Summary Models — and some MCP transports — occasionally emit tool-call arguments as a **JSON string instead of an object/dict**. The most visible trigger is `COMPOSIO_MULTI_EXECUTE_TOOL` on the Vercel AI SDK, where streaming fails with: > `messages.3.content.1.tool_use.input: Input should be a valid dictionary` Until now only a handful of providers guarded against this, each with its own slightly different inline check, leaving most providers vulnerable and behaviour inconsistent across the SDK. This PR centralizes the coercion into **one helper per language** and routes **every** provider through it, in both the TypeScript and Python SDKs. Closes https://github.com/ComposioHQ/composio/issues/2406 ## What changed **TypeScript** — new `normalizeToolArguments` in `@composio/core` (exported), used by every provider: `vercel`, `cloudflare`, `openai-agents`, `openai` (ChatCompletions + Responses), `anthropic`, `google`, `langchain`, `llamaindex`, `claude-agent-sdk`, `mastra`. **Python** — new `normalize_tool_arguments` in `composio.utils.shared`, used by every provider: `openai`, `openai-responses`, `anthropic`, `google`, `langchain`, `langgraph`, `crewai`, `autogen`, `llamaindex`, `gemini`, `google-adk`, `openai-agents`, `claude-agent-sdk`. Shared semantics (identical in both languages): | Input | Result | | --- | --- | | object / dict | returned unchanged | | JSON string | parsed to object | | empty / whitespace string | `{}` | | `null` / `undefined` / `None` | `{}` | | array, primitive, unparseable string, JSON that isn't an object | **typed error** (`ComposioInvalidToolArgumentsError` / `InvalidParams`) with the original parse error as cause | The typed error replaces the previous grab-bag of behaviours: a raw `SyntaxError`/`JSONDecodeError`, or — worse — silently forwarding a malformed string downstream. ## Why this supersedes the open PRs This consolidates and extends three open PRs that each addressed a slice of the problem inconsistently. Their authors are credited as co-authors on the relevant commits: - **#3489** (LlamaIndex + Claude Agent SDK, TS) — @srijanarya - **#3438** (Anthropic, Google, LangChain, TS) — @aptsalt - **#3437** (Google ADK empty schemas + name fix, Python) — @pragnyanramtha — its empty-`input_parameters` / missing-description handling and the `gemini` → `google_adk` provider-name fix are folded in here. Compared to the three combined, this PR additionally: covers **every** provider in **both** SDKs (not a subset of one), defines a single source of truth instead of per-provider snippets, normalizes empty/`null` payloads to `{}`, and raises an actionable typed error instead of leaking `SyntaxError` or forwarding a bad string. ## Tests - Exhaustive unit tests for both helpers (object passthrough, JSON-string parse, empty/null → `{}`, malformed/non-object → typed error). - Per-provider regression tests across the touched TypeScript providers (string path, malformed-string path, empty-payload path). - Full `@composio/core` + touched-provider TS suites pass; `typecheck` and `lint` clean. Python `ruff` clean and new test green. ## Changeset Patch bump for all affected TypeScript packages (`@composio/core` + the providers). Python follows its own versioning, so no changeset there. --------- Co-authored-by: srijanarya <74669415+srijanarya@users.noreply.github.com> Co-authored-by: Deepak Singh Kandari <deepaksinghkandari07@gmail.com> Co-authored-by: Pragnyan Ramtha <pragnyanramtha@gmail.com>
2026-06-16 12:49:37 +04:00
import json
import keyword
2025-06-17 12:58:00 +05:30
import typing as t
import uuid
fix(py): handle 4+ option and typeless anyOf/oneOf unions in tool signatures (#3708) ## Summary `get_signature_format_from_schema_params` in `python/composio/utils/shared.py` builds the `__signature__` for the tool wrappers used by the langchain, langgraph, llamaindex, and autogen providers. Its `oneOf`/`anyOf` handling hardcoded a ladder for exactly 1, 2, or 3 union members and raised `ValueError("Invalid 'oneOf' schema")` for any union with four or more options. It also indexed `PYDANTIC_TYPE_TO_PYTHON_TYPE[ptype.get("type")]` directly, so any combiner option missing a `type` key produced `None` and raised `KeyError(None)`. Both shapes are valid JSON Schema and appear in real tool input schemas, so a tool with a 4-way union, or a combiner option without an explicit `type`, makes those four providers raise at tool-wrap time. This replaces the ladder with a single map + `functools.reduce` path that mirrors the existing `_build_union_from_options` in `schema_converter.py`, keeping the 1/2/3-member and nullable `[type, null]` outputs identical to before. Fixes # ## Changes - `python/composio/utils/shared.py`: in `get_signature_format_from_schema_params`, replace the 1/2/3-member `oneOf`/`anyOf` ladder (which raised `ValueError` for 4+ members) with a map-each-option-to-a-Python-type + `functools.reduce` Union build that supports any member count. Unknown/missing option types map to `typing.Any` instead of raising `KeyError`. Adds `from functools import reduce` and removes three now-unnecessary `# type: ignore` comments. - `python/tests/test_schema_parser.py`: add `TestGetSignatureFormatFromSchemaParams` (8 cases) covering 4- and 5-member unions, an `anyOf` option missing `type`, an all-typeless `anyOf`, and regressions for the previously supported single/2/3-member and nullable `[type, null]` shapes. ## 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_schema_parser.py::TestGetSignatureFormatFromSchemaParams -v # 8 passed uv run pytest tests/test_schema_parser.py -q # 75 passed make chk # ruff + mypy -> clean, no new ignores ``` To confirm the tests guard the fix, the new test class was also run against the unmodified `shared.py`: 4 of the 8 fail there (`ValueError`/`KeyError` on the 4+/typeless cases), and all 8 pass with this change. The nullable `anyOf [type, null]` case still resolves to `Union[str, Any, NoneType]` (its pre-existing behavior); collapsing `null` into `Optional` lives in `schema_converter` and is intentionally left out of this minimal fix, with the test documenting current behavior. ## Screenshots (if applicable) N/A ## 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 - [x] I added tests or explain why not applicable - [ ] I added a changeset if this change affects published packages ## Additional context No linked issue: this is a self-identified, reproduced bug in the older schema->signature path. The fix deliberately mirrors `schema_converter._build_union_from_options` so the two schema paths agree on N-ary union handling. The same bug family exists in `python/composio/utils/openapi.py` (`function_signature_from_jsonschema`, used by the google_adk provider) but is kept out of this PR to stay single-purpose; happy to follow up on it separately.
2026-07-02 13:09:41 +05:30
from functools import reduce
2025-06-17 12:58:00 +05:30
from inspect import Parameter
from pydantic import BaseModel, Field, create_model
from pydantic.fields import FieldInfo
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
from composio.exceptions import InvalidParams, InvalidSchemaError
from composio.utils.json_schema import dereference_json_schema
2025-06-17 12:58:00 +05:30
from composio.utils.logging import get as get_logger
from composio.utils.schema_converter import (
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
_EXPLICIT_DEFAULT_FIELDS_ATTRIBUTE,
CONTAINER_TYPE,
FALLBACK_VALUES,
2026-02-06 02:08:50 +04:00
PYDANTIC_TYPE_TO_PYTHON_TYPE,
_mark_explicit_default_fields,
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
_with_exact_validation,
apply_object_policy,
2026-02-06 02:08:50 +04:00
json_schema_to_pydantic_type,
)
2025-06-17 12:58:00 +05:30
logger = get_logger(__name__)
# Re-export for backward compatibility
__all__ = [
"json_schema_to_pydantic_type",
"PYDANTIC_TYPE_TO_PYTHON_TYPE",
"CONTAINER_TYPE",
"FALLBACK_VALUES",
"json_schema_to_pydantic_field",
"json_schema_to_fields_dict",
"json_schema_to_model",
"pydantic_model_from_param_schema",
"get_signature_format_from_schema_params",
"get_pydantic_signature_format_from_schema_params",
"generate_request_id",
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
"ToolSchemaAliases",
"alias_tool_input_schema",
"restore_tool_arguments",
"substitute_reserved_python_keywords",
"reinstate_reserved_python_keywords",
fix(providers): normalize string tool-call arguments across all providers (TS + Python) (#3514) ## Summary Models — and some MCP transports — occasionally emit tool-call arguments as a **JSON string instead of an object/dict**. The most visible trigger is `COMPOSIO_MULTI_EXECUTE_TOOL` on the Vercel AI SDK, where streaming fails with: > `messages.3.content.1.tool_use.input: Input should be a valid dictionary` Until now only a handful of providers guarded against this, each with its own slightly different inline check, leaving most providers vulnerable and behaviour inconsistent across the SDK. This PR centralizes the coercion into **one helper per language** and routes **every** provider through it, in both the TypeScript and Python SDKs. Closes https://github.com/ComposioHQ/composio/issues/2406 ## What changed **TypeScript** — new `normalizeToolArguments` in `@composio/core` (exported), used by every provider: `vercel`, `cloudflare`, `openai-agents`, `openai` (ChatCompletions + Responses), `anthropic`, `google`, `langchain`, `llamaindex`, `claude-agent-sdk`, `mastra`. **Python** — new `normalize_tool_arguments` in `composio.utils.shared`, used by every provider: `openai`, `openai-responses`, `anthropic`, `google`, `langchain`, `langgraph`, `crewai`, `autogen`, `llamaindex`, `gemini`, `google-adk`, `openai-agents`, `claude-agent-sdk`. Shared semantics (identical in both languages): | Input | Result | | --- | --- | | object / dict | returned unchanged | | JSON string | parsed to object | | empty / whitespace string | `{}` | | `null` / `undefined` / `None` | `{}` | | array, primitive, unparseable string, JSON that isn't an object | **typed error** (`ComposioInvalidToolArgumentsError` / `InvalidParams`) with the original parse error as cause | The typed error replaces the previous grab-bag of behaviours: a raw `SyntaxError`/`JSONDecodeError`, or — worse — silently forwarding a malformed string downstream. ## Why this supersedes the open PRs This consolidates and extends three open PRs that each addressed a slice of the problem inconsistently. Their authors are credited as co-authors on the relevant commits: - **#3489** (LlamaIndex + Claude Agent SDK, TS) — @srijanarya - **#3438** (Anthropic, Google, LangChain, TS) — @aptsalt - **#3437** (Google ADK empty schemas + name fix, Python) — @pragnyanramtha — its empty-`input_parameters` / missing-description handling and the `gemini` → `google_adk` provider-name fix are folded in here. Compared to the three combined, this PR additionally: covers **every** provider in **both** SDKs (not a subset of one), defines a single source of truth instead of per-provider snippets, normalizes empty/`null` payloads to `{}`, and raises an actionable typed error instead of leaking `SyntaxError` or forwarding a bad string. ## Tests - Exhaustive unit tests for both helpers (object passthrough, JSON-string parse, empty/null → `{}`, malformed/non-object → typed error). - Per-provider regression tests across the touched TypeScript providers (string path, malformed-string path, empty-payload path). - Full `@composio/core` + touched-provider TS suites pass; `typecheck` and `lint` clean. Python `ruff` clean and new test green. ## Changeset Patch bump for all affected TypeScript packages (`@composio/core` + the providers). Python follows its own versioning, so no changeset there. --------- Co-authored-by: srijanarya <74669415+srijanarya@users.noreply.github.com> Co-authored-by: Deepak Singh Kandari <deepaksinghkandari07@gmail.com> Co-authored-by: Pragnyan Ramtha <pragnyanramtha@gmail.com>
2026-06-16 12:49:37 +04:00
"normalize_tool_arguments",
"validate_and_serialize_tool_arguments",
]
2025-06-17 12:58:00 +05:30
reserved_names = ["validate"]
_OBJ_MARKER = "-_object_-"
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
_ARR_MARKER = "-_array_-"
_MAX_PROVIDER_ALIAS_LENGTH = 64
_MISSING_ARGUMENT = object()
fix(providers): normalize string tool-call arguments across all providers (TS + Python) (#3514) ## Summary Models — and some MCP transports — occasionally emit tool-call arguments as a **JSON string instead of an object/dict**. The most visible trigger is `COMPOSIO_MULTI_EXECUTE_TOOL` on the Vercel AI SDK, where streaming fails with: > `messages.3.content.1.tool_use.input: Input should be a valid dictionary` Until now only a handful of providers guarded against this, each with its own slightly different inline check, leaving most providers vulnerable and behaviour inconsistent across the SDK. This PR centralizes the coercion into **one helper per language** and routes **every** provider through it, in both the TypeScript and Python SDKs. Closes https://github.com/ComposioHQ/composio/issues/2406 ## What changed **TypeScript** — new `normalizeToolArguments` in `@composio/core` (exported), used by every provider: `vercel`, `cloudflare`, `openai-agents`, `openai` (ChatCompletions + Responses), `anthropic`, `google`, `langchain`, `llamaindex`, `claude-agent-sdk`, `mastra`. **Python** — new `normalize_tool_arguments` in `composio.utils.shared`, used by every provider: `openai`, `openai-responses`, `anthropic`, `google`, `langchain`, `langgraph`, `crewai`, `autogen`, `llamaindex`, `gemini`, `google-adk`, `openai-agents`, `claude-agent-sdk`. Shared semantics (identical in both languages): | Input | Result | | --- | --- | | object / dict | returned unchanged | | JSON string | parsed to object | | empty / whitespace string | `{}` | | `null` / `undefined` / `None` | `{}` | | array, primitive, unparseable string, JSON that isn't an object | **typed error** (`ComposioInvalidToolArgumentsError` / `InvalidParams`) with the original parse error as cause | The typed error replaces the previous grab-bag of behaviours: a raw `SyntaxError`/`JSONDecodeError`, or — worse — silently forwarding a malformed string downstream. ## Why this supersedes the open PRs This consolidates and extends three open PRs that each addressed a slice of the problem inconsistently. Their authors are credited as co-authors on the relevant commits: - **#3489** (LlamaIndex + Claude Agent SDK, TS) — @srijanarya - **#3438** (Anthropic, Google, LangChain, TS) — @aptsalt - **#3437** (Google ADK empty schemas + name fix, Python) — @pragnyanramtha — its empty-`input_parameters` / missing-description handling and the `gemini` → `google_adk` provider-name fix are folded in here. Compared to the three combined, this PR additionally: covers **every** provider in **both** SDKs (not a subset of one), defines a single source of truth instead of per-provider snippets, normalizes empty/`null` payloads to `{}`, and raises an actionable typed error instead of leaking `SyntaxError` or forwarding a bad string. ## Tests - Exhaustive unit tests for both helpers (object passthrough, JSON-string parse, empty/null → `{}`, malformed/non-object → typed error). - Per-provider regression tests across the touched TypeScript providers (string path, malformed-string path, empty-payload path). - Full `@composio/core` + touched-provider TS suites pass; `typecheck` and `lint` clean. Python `ruff` clean and new test green. ## Changeset Patch bump for all affected TypeScript packages (`@composio/core` + the providers). Python follows its own versioning, so no changeset there. --------- Co-authored-by: srijanarya <74669415+srijanarya@users.noreply.github.com> Co-authored-by: Deepak Singh Kandari <deepaksinghkandari07@gmail.com> Co-authored-by: Pragnyan Ramtha <pragnyanramtha@gmail.com>
2026-06-16 12:49:37 +04:00
def normalize_tool_arguments(arguments: t.Any) -> t.Dict[str, t.Any]:
"""Coerce model-supplied tool arguments into a dict.
Models (and some MCP transports) occasionally emit tool-call arguments as a
JSON string instead of a dict, which breaks execution with errors such as
``tool_use.input: Input should be a valid dictionary``. This is the single
coercion every provider routes through so behaviour is identical everywhere.
See https://github.com/ComposioHQ/composio/issues/2406.
- ``None`` becomes ``{}`` (some models send no arguments for no-arg tools).
- A dict is returned unchanged.
- A string is JSON-parsed; an empty / whitespace-only string becomes ``{}``.
- Anything that does not resolve to a dict (lists, primitives, unparseable
strings, JSON that parses to a non-object) raises :class:`InvalidParams`.
:param arguments: Raw arguments as received from the model / framework.
:return: The normalized arguments as a dict.
:raises InvalidParams: If the arguments cannot be resolved to a dict.
"""
if arguments is None:
return {}
if isinstance(arguments, str):
stripped = arguments.strip()
if not stripped:
return {}
try:
parsed = json.loads(stripped)
except json.JSONDecodeError as e:
raise InvalidParams(
f"Tool arguments were provided as a string that is not valid JSON: {e}"
) from e
return _as_dict(parsed)
return _as_dict(arguments)
def _as_dict(value: t.Any) -> t.Dict[str, t.Any]:
if isinstance(value, dict):
return value
raise InvalidParams(
f"Tool arguments must resolve to an object, received {type(value).__name__}"
)
def validate_and_serialize_tool_arguments(
args_schema: t.Type[BaseModel],
arguments: t.Dict[str, t.Any],
) -> t.Dict[str, t.Any]:
"""Validate provider arguments and serialize only values the backend should see.
Pydantic models use ``None`` for optional fields that were not supplied, so a
plain ``model_dump()`` changes omission into an explicit null. Conversely,
``exclude_unset=True`` also drops defaults declared by the source JSON Schema.
Generated models track fields with declared defaults. Combining that metadata
recursively with each selected model's ``model_fields_set`` preserves aliases,
explicit nulls, defaults, and validated dynamic extras without replaying the
source schema during execution.
"""
validated = args_schema.model_validate(arguments)
return t.cast(
t.Dict[str, t.Any],
_serialize_present_arguments(
validated,
validated.model_dump(mode="python", by_alias=True),
arguments,
),
)
def _model_field_aliases(
model: t.Type[BaseModel],
) -> t.Dict[str, t.Tuple[str, FieldInfo]]:
fields = {}
for name, field in model.model_fields.items():
alias = field.serialization_alias or field.alias
fields[alias if isinstance(alias, str) else name] = (name, field)
return fields
def _serialize_present_arguments(
validated: t.Any,
serialized: t.Any,
supplied: t.Any,
) -> t.Any:
"""Recursively remove values Pydantic added for omitted optional fields."""
if supplied is _MISSING_ARGUMENT:
return serialized
if isinstance(validated, BaseModel) and isinstance(serialized, dict):
fields = _model_field_aliases(type(validated))
included_fields = set(validated.model_fields_set)
included_fields.update(
getattr(type(validated), _EXPLICIT_DEFAULT_FIELDS_ATTRIBUTE, ())
)
supplied_values = supplied if isinstance(supplied, dict) else {}
result: t.Dict[str, t.Any] = {}
for key, value in serialized.items():
field = fields.get(key)
if field is not None:
name, _ = field
if name not in included_fields:
continue
child_validated = getattr(validated, name)
if key in supplied_values:
child_supplied = supplied_values[key]
elif name in supplied_values:
child_supplied = supplied_values[name]
else:
child_supplied = _MISSING_ARGUMENT
else:
extra = validated.model_extra
if extra is None or key not in extra:
continue
child_validated = extra[key]
child_supplied = supplied_values.get(key, _MISSING_ARGUMENT)
result[key] = _serialize_present_arguments(
child_validated,
value,
child_supplied,
)
return result
if isinstance(serialized, dict):
supplied_values = supplied if isinstance(supplied, dict) else {}
validated_values = validated if isinstance(validated, dict) else {}
return {
key: _serialize_present_arguments(
validated_values.get(key, _MISSING_ARGUMENT),
value,
supplied_values.get(key, _MISSING_ARGUMENT),
)
for key, value in serialized.items()
if key in supplied_values or key in validated_values
}
if isinstance(serialized, (list, tuple)):
supplied_items = supplied if isinstance(supplied, (list, tuple)) else ()
validated_items = validated if isinstance(validated, (list, tuple)) else ()
items = [
_serialize_present_arguments(
validated_items[index]
if index < len(validated_items)
else _MISSING_ARGUMENT,
value,
supplied_items[index]
if index < len(supplied_items)
else _MISSING_ARGUMENT,
)
for index, value in enumerate(serialized)
]
return tuple(items) if isinstance(serialized, tuple) else items
return serialized
def _make_safe_name(name: str) -> str:
"""Append ``_rs`` to a Python keyword so it can be used as a parameter name."""
return f"{name}_rs"
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
def _make_python_identifier(name: str) -> str:
if keyword.iskeyword(name):
return _make_safe_name(name)
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
safe = "".join(
char if (char.isascii() and (char.isalnum() or char == "_")) else "_"
for char in name
)
if not safe:
safe = "param"
if safe[0].isdigit() or safe[0] == "_":
safe = f"param_{safe.lstrip('_') or 'value'}"
if keyword.iskeyword(safe):
safe = _make_safe_name(safe)
if len(safe) > _MAX_PROVIDER_ALIAS_LENGTH:
hash_suffix = hashlib.sha256(name.encode()).hexdigest()[:8]
safe = (
f"{safe[: _MAX_PROVIDER_ALIAS_LENGTH - len(hash_suffix) - 1]}_{hash_suffix}"
)
return safe
@dataclasses.dataclass(frozen=True)
class ToolSchemaAliases:
"""Provider-visible schema plus mapping back to backend argument names."""
schema: t.Dict[str, t.Any]
aliases: t.Dict[str, t.Any]
def restore_arguments(self, arguments: dict) -> dict:
return restore_tool_arguments(arguments, self.aliases)
def alias_tool_input_schema(schema: t.Dict) -> ToolSchemaAliases:
"""Alias tool input schema keys so they are valid Python parameter names.
Returns a :class:`ToolSchemaAliases` object containing a deep-copied schema
for provider/framework exposure and a reverse alias map for restoring model
arguments before calling the backend executor.
Python keywords keep the historical ``_rs`` suffix (``from`` becomes
``from_rs``). Other names that cannot be used as Python identifiers are
converted to Pydantic-safe identifiers and long aliases are capped at 64
characters so the same provider-facing schema is accepted by
Anthropic-style tool schema validators. Internal JSON Schema references are
inlined before aliasing so referenced object properties are exposed through
the same safe names. If two properties would expose the same alias, an
:class:`InvalidSchemaError` is raised instead of guessing.
"""
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
aliased_schema = t.cast(
t.Dict[str, t.Any],
dereference_json_schema(
copy.deepcopy(schema),
on_unresolved="sentinel",
),
)
schema_params, aliases = _alias_schema_properties(aliased_schema)
return ToolSchemaAliases(schema=schema_params, aliases=aliases)
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
def _alias_schema_properties(schema: t.Dict[str, t.Any]) -> t.Tuple[dict, dict]:
properties = schema.get("properties")
if not isinstance(properties, dict):
return schema, {}
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
aliases: t.Dict[str, t.Any] = {}
aliased_properties: t.Dict[str, t.Any] = {}
for original_name, property_schema in properties.items():
safe_name = _make_python_identifier(original_name)
if safe_name in aliased_properties:
raise InvalidSchemaError(
"Tool input schema property names produce a duplicate Python "
f"parameter alias {safe_name!r}"
)
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
nested_object_aliases: t.Dict[str, t.Any] = {}
nested_array_aliases: t.Dict[str, t.Any] = {}
if isinstance(property_schema, dict):
property_schema, nested_object_aliases = _alias_nested_object_schema(
property_schema
)
property_schema, nested_array_aliases = _alias_nested_array_schema(
property_schema
)
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
aliased_properties[safe_name] = property_schema
if safe_name != original_name or nested_object_aliases or nested_array_aliases:
aliases[safe_name] = original_name
if nested_object_aliases:
aliases[f"{safe_name}{_OBJ_MARKER}"] = nested_object_aliases
if nested_array_aliases:
aliases[f"{safe_name}{_ARR_MARKER}"] = nested_array_aliases
schema["properties"] = aliased_properties
if aliases and "required" in schema:
reverse = {
original: safe
for safe, original in aliases.items()
if not safe.endswith(_OBJ_MARKER) and not safe.endswith(_ARR_MARKER)
}
schema["required"] = [reverse.get(r, r) for r in schema["required"]]
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
return schema, aliases
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
def _alias_nested_object_schema(
property_schema: t.Dict[str, t.Any],
) -> t.Tuple[dict, dict]:
if not isinstance(property_schema.get("properties"), dict):
return property_schema, {}
return _alias_schema_properties(property_schema)
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
def _alias_nested_array_schema(
property_schema: t.Dict[str, t.Any],
) -> t.Tuple[dict, dict]:
items_schema = property_schema.get("items")
if not isinstance(items_schema, dict):
return property_schema, {}
aliased_items, aliases = _alias_schema_properties(items_schema)
if aliases:
property_schema["items"] = aliased_items
return property_schema, aliases
def restore_tool_arguments(request: dict, aliases: dict) -> dict:
"""Restore provider-visible argument aliases back to backend schema names.
Modifies *request* in-place and returns it.
"""
alias_keys = [
key
for key in aliases
if not key.endswith(_OBJ_MARKER) and not key.endswith(_ARR_MARKER)
]
for clean_key in sorted(alias_keys, reverse=True):
if clean_key not in request:
continue
original_value = request.pop(clean_key)
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
object_aliases = aliases.get(f"{clean_key}{_OBJ_MARKER}", {})
array_aliases = aliases.get(f"{clean_key}{_ARR_MARKER}", {})
if object_aliases and isinstance(original_value, dict):
original_value = restore_tool_arguments(
request=original_value,
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
aliases=object_aliases,
)
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
if array_aliases and isinstance(original_value, list):
original_value = [
restore_tool_arguments(item, array_aliases)
if isinstance(item, dict)
else item
for item in original_value
]
request[aliases.get(clean_key, clean_key)] = original_value
return request
2025-06-17 12:58:00 +05:30
fix(py): centralize tool schema aliasing (#3652) This PR: - supersedes https://github.com/ComposioHQ/composio/pull/3625, https://github.com/ComposioHQ/composio/pull/3629, and the Python SDK/provider-side fix proposed by https://github.com/ComposioHQ/composio/pull/3504 - adds `alias_tool_input_schema` / `restore_tool_arguments` for provider-visible schemas and backend argument restoration - keeps `substitute_reserved_python_keywords` / `reinstate_reserved_python_keywords` as compatibility wrappers - preserves the existing Python keyword alias style (`from` -> `from_rs`), aliases invalid Python parameter names like `$top`, avoids leading-underscore aliases so Pydantic-backed providers can build models, and caps aliases at 64 characters for Anthropic-style tool schema validators - dereferences internal `$ref` / `$defs` before aliasing so referenced object properties are exposed through the same safe provider-visible names - restores Gemini manual `handle_response` arguments with the per-tool alias map before execution - wraps Google ADK tools with aliased callable signatures and restores original backend argument names - wraps Anthropic and Claude Agent SDK tool schemas with the same shared alias map and restores original backend argument names before execution - fixes the package-local Claude Agent SDK provider tests so they run without `pytest-asyncio` and keep using the provider-visible schema conversion hook - adds dependency-light core tests for helper behavior, `$ref` aliasing, Pydantic model compatibility, Gemini manual response, Google ADK wrapping, Anthropic wrapping, and Claude Agent SDK wrapping Local validation: - `uv run --project python pytest python/tests/test_tool_schema_aliasing.py python/tests/test_schema_parser.py python/tests/test_json_schema.py python/tests/test_imports.py -q` -> 112 passed - `PYTHONPATH=python uv run --project python --with-editable ./python/providers/claude_agent_sdk pytest python/providers/claude_agent_sdk/tests/test_provider.py -q --timeout=20` -> 16 passed - `uv run --project python --with-editable ./python/providers/langchain pytest --ignore-glob='python/tests/test_type_inference*.py' python/tests/ -q` -> 742 passed, 31 skipped - `uv run --project python --with-editable ./python --with-editable ./python/providers/langchain python <LangChain $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python --with-editable ./python --with-editable ./python/providers/langgraph python <LangGraph $top wrap smoke>` -> wrapped args schema field `param_top` - `uv run --project python ruff check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `uv run --project python ruff format --check --config python/config/ruff.toml python/composio/utils/shared.py python/tests/test_tool_schema_aliasing.py` -> passed - `git diff --check` -> passed Note: #3500's hosted MCP emission-layer report is already closed; this PR covers the Python SDK/provider surface rather than changing hosted MCP schema emission.
2026-06-25 01:37:12 +04:00
def substitute_reserved_python_keywords(
schema: t.Dict,
) -> t.Tuple[dict, dict]:
"""Replace unsafe JSON schema property names with Python parameter aliases.
Backward-compatible wrapper around :func:`alias_tool_input_schema`.
"""
aliased = alias_tool_input_schema(schema=schema)
return aliased.schema, aliased.aliases
def reinstate_reserved_python_keywords(
request: dict,
keywords: dict,
) -> dict:
"""Reverse the substitution performed by :func:`substitute_reserved_python_keywords`.
Modifies *request* **in-place** and returns it.
"""
return restore_tool_arguments(request=request, aliases=keywords)
def _coerce_default_value(
default: t.Any,
json_schema: t.Dict[str, t.Any],
) -> t.Any:
"""
Coerce a default value to match the expected type from JSON schema.
Handles common mismatches where string defaults should be boolean/int/float.
This fixes issues where API returns stringified defaults like "true" instead of true.
Coercion precedence: boolean > integer > float. This means values like "1" and "0"
become booleans when both bool and int are expected types.
:param default: The default value from the JSON schema.
:param json_schema: The JSON schema property definition.
:return: The coerced default value, or original if no coercion possible.
"""
if default is None or not isinstance(default, str):
return default
# Collect expected types from schema
expected_types: t.Set[t.Any] = set()
if "type" in json_schema:
py_type = PYDANTIC_TYPE_TO_PYTHON_TYPE.get(json_schema["type"])
if py_type is not None:
expected_types.add(py_type)
for combiner in ("anyOf", "oneOf", "allOf"):
for option in json_schema.get(combiner, []):
if isinstance(option, dict):
option_type = option.get("type")
if isinstance(option_type, str):
py_type = PYDANTIC_TYPE_TO_PYTHON_TYPE.get(option_type)
if py_type is not None:
expected_types.add(py_type)
# If string is expected, no coercion needed
if str in expected_types:
return default
# Boolean coercion (takes precedence over int for "1"/"0")
if bool in expected_types:
lower_default = default.lower()
if lower_default in ("true", "yes", "1"):
return True
if lower_default in ("false", "no", "0"):
return False
# Integer coercion
if int in expected_types:
try:
return int(default)
except ValueError:
pass
# Float coercion
if float in expected_types:
try:
return float(default)
except ValueError:
pass
return default
2025-06-17 12:58:00 +05:30
def json_schema_to_pydantic_field(
name: str,
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
json_schema: t.Union[t.Dict[str, t.Any], bool],
2025-06-17 12:58:00 +05:30
required: t.List[str],
skip_default: bool = False,
*,
root_schema: t.Optional[t.Dict[str, t.Any]] = None,
2025-06-17 12:58:00 +05:30
) -> t.Tuple[str, t.Type, FieldInfo]:
"""
Converts a JSON schema property to a Pydantic field definition.
:param name: The field name.
:param json_schema: The JSON schema property.
:param required: List of required properties.
:param root_schema: Full schema document used to resolve local references.
2025-06-17 12:58:00 +05:30
:return: A Pydantic field definition.
"""
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
# A property schema may be the literal `true`/`false` (JSON Schema
# draft-06+); those carry no metadata, so read annotations from an empty
# object and let `json_schema_to_pydantic_type` decide the type.
schema_object = json_schema if isinstance(json_schema, dict) else {}
description = schema_object.get("description")
if "oneOf" in schema_object:
2025-06-17 12:58:00 +05:30
description = " | ".join(
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
[option.get("description", "") for option in schema_object["oneOf"]]
2025-06-17 12:58:00 +05:30
)
description = f"Any of the following options(separated by |): {description}"
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
examples = schema_object.get("examples", [])
default = schema_object.get("default")
2025-06-17 12:58:00 +05:30
# Coerce default value to match expected type from schema
if default is not None:
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
default = _coerce_default_value(default, schema_object)
2025-06-17 12:58:00 +05:30
# Check if the field name is a reserved Pydantic name
original_name = name
2025-06-17 12:58:00 +05:30
if name in reserved_names:
name = f"{name}_"
alias = original_name
2025-06-17 12:58:00 +05:30
else:
alias = None
field = {
"description": description,
"examples": examples,
"alias": alias,
}
if not skip_default:
field["default"] = ... if original_name in required else default
2025-06-17 12:58:00 +05:30
return (
name,
t.cast(
t.Type,
json_schema_to_pydantic_type(
json_schema=json_schema,
root_schema=root_schema,
2025-06-17 12:58:00 +05:30
),
),
Field(**field), # type: ignore
)
def json_schema_to_fields_dict(json_schema: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
"""
Converts a JSON schema to a dictionary of param name, and a tuple of type & Field.
:param json_schema: The JSON schema to convert.
:return: dict<str, tuple<<class 'type'>, Field>>
Example Output:
```python
{
'owner': (<class 'str'>, FieldInfo(default=Ellipsis, description='The account owner of the repository.', extra={'examples': ([],)})),
'repo': (<class 'str'>, FieldInfo(default=Ellipsis, description='The name of the repository without the `.git` extension.', extra={'examples': ([],)}))}
}
```
"""
field_definitions = {}
for name, prop in json_schema.get("properties", {}).items():
updated_name, pydantic_type, pydantic_field = json_schema_to_pydantic_field(
name,
prop,
json_schema.get("required", []),
root_schema=json_schema,
2025-06-17 12:58:00 +05:30
)
field_definitions[updated_name] = (pydantic_type, pydantic_field)
return field_definitions # type: ignore
def json_schema_to_model(
json_schema: t.Dict[str, t.Any],
skip_default: bool = False,
) -> t.Type[BaseModel]:
"""
Converts a JSON schema to a Pydantic BaseModel class.
:param json_schema: The JSON schema to convert.
:param skip_default: Skip the default values when building field object
:return: Pydantic `BaseModel` type
"""
model_name = json_schema.get("title")
if model_name is None:
model_name = "GeneratedModel"
2025-06-17 12:58:00 +05:30
field_definitions = {}
for name, prop in json_schema.get("properties", {}).items():
updated_name, pydantic_type, pydantic_field = json_schema_to_pydantic_field(
name,
prop,
json_schema.get("required", []),
skip_default=skip_default,
root_schema=json_schema,
2025-06-17 12:58:00 +05:30
)
field_definitions[updated_name] = (pydantic_type, pydantic_field)
# The dynamic-key policy is shared with `json_schema_to_pydantic_type` so the
# two entry points cannot disagree about which arguments survive conversion.
base_model = create_model(model_name, **field_definitions) # type: ignore
if skip_default:
setattr(base_model, _EXPLICIT_DEFAULT_FIELDS_ATTRIBUTE, frozenset())
else:
_mark_explicit_default_fields(base_model, json_schema, json_schema)
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
# Exact acceptance is shared with `json_schema_to_pydantic_type` so every
# Python entry point agrees with the Zod and Effect converters about which
# inputs a schema admits.
return _with_exact_validation(
apply_object_policy(
json_schema,
base_model,
model_name=model_name,
),
json_schema,
json_schema,
)
2025-06-17 12:58:00 +05:30
def pydantic_model_from_param_schema(param_schema: t.Dict) -> t.Type:
"""
Dynamically creates a Pydantic model from a schema dictionary.
:param param_schema: Schema with 'title', 'properties', and optionally 'required' keys.
:return: A Pydantic model class for the defined schema.
:raised ValueError: Invalid 'type' for property or recursive model creation.
Note: Requires global `schema_type_python_type_dict` for type mapping and
`fallback_values` for default values.
"""
required_fields = {}
optional_fields = {}
if "title" not in param_schema:
raise ValueError(f"Missing 'title' in param_schema: {param_schema}")
param_title = str(param_schema["title"]).replace(" ", "")
required_props = param_schema.get("required", [])
if param_schema.get("type") == "array":
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
return t.cast(t.Type, json_schema_to_pydantic_type(param_schema))
2025-06-17 12:58:00 +05:30
for prop_name, prop_info in param_schema.get("properties", {}).items():
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
prop_object = prop_info if isinstance(prop_info, dict) else {}
prop_type = prop_object.get("type")
prop_title = prop_object.get("title", prop_name).replace(" ", "")
fallback = (
FALLBACK_VALUES.get(prop_type) if isinstance(prop_type, str) else None
)
prop_default = prop_object.get("default", fallback)
signature_prop_type = t.cast(
t.Type,
json_schema_to_pydantic_type(
json_schema=prop_info,
root_schema=param_schema,
),
)
2025-06-17 12:58:00 +05:30
field_kwargs = {
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
"description": prop_object.get(
"description", prop_object.get("desc", prop_title)
2025-06-17 12:58:00 +05:30
),
}
# Add alias if the field name is a reserved Pydantic name
if prop_name in reserved_names:
field_kwargs["alias"] = prop_name
field_kwargs["title"] = f"{prop_name}_"
else:
field_kwargs["title"] = prop_title
if prop_name in required_props:
2025-06-17 12:58:00 +05:30
required_fields[prop_name] = (
signature_prop_type,
Field(..., **field_kwargs),
)
else:
optional_fields[prop_name] = (
signature_prop_type,
Field(default=prop_default, **field_kwargs),
)
if not required_fields and not optional_fields:
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
# Nothing to materialize: let the shared converter pick the annotation
# so `allOf`/`anyOf`/`$ref` and typeless assertions keep the same exact
# validation and permissive materialization as the other entry points.
return t.cast(t.Type, json_schema_to_pydantic_type(param_schema))
2025-06-17 12:58:00 +05:30
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
model = create_model( # type: ignore
2025-06-17 12:58:00 +05:30
param_title,
**required_fields,
**optional_fields,
)
fix(sdk): preserve primitive JSON Schema semantics (#4316) ## Summary - preserve boolean, empty, null, type-array, enum, const, and scalar-constraint semantics across every Python conversion entry point - intersect Zod enum and const values with declared types and constraints, including compound JSON values - default unversioned exact validation to Draft 7 and apply inclusive and numeric exclusive bounds independently - run one byte-identical corpus through Python, Zod, and Effect so accepted and rejected inputs stay aligned - keep exact JSON Schema acceptance separate from Pydantic default materialization ## Review follow-up (second push) - Python: exact Draft 7 acceptance now wraps all three entry points (`json_schema_to_pydantic_type`, `json_schema_to_model`, `pydantic_model_from_param_schema`), so they can no longer disagree - Python: draft-4 boolean `exclusiveMinimum`/`exclusiveMaximum` (OpenAPI 3.0 style) no longer crash conversion — exact validation falls back to Draft 4, and the library input is translated to the numeric spelling - Python: ECMA-only regex patterns (look-around) no longer crash pydantic model builds — Rust-incompatible patterns fall back to Python `re` - Python: type arrays with sibling constraints no longer raise `TypeError` on valid input — constraints are scoped per member before the library sees them - Python: integral floats satisfy `integer`, `const` intersects `enum`, annotation-only schemas accept anything, and an optional property with an empty `enum` tolerates absence - Zod: typeless scalar constraints apply per instance type, and string lengths count Unicode code points instead of UTF-16 code units - Effect: draft-4 boolean exclusive bounds are enforced instead of silently ignored - `multipleOf` uses decimal scaling in all three converters (declared `divergesFromJsonSchema` on the corpus case) - shared corpus grows by 13 primitive cases; new property-based tests check acceptance against real Draft 7 oracles (hypothesis + `jsonschema` in Python, fast-check + Ajv in TypeScript) ## Verification - Python `make chk` (ruff + mypy) - Python pytest: 1,572 passed (5 langchain-extra tests need an env this sandbox lacks; unchanged from base) - `@composio/json-schema-to-zod`: 187 passed incl. 300-run fast-check property test; typecheck + build - `@composio/json-schema-to-effect-schema`: 133 passed; typecheck - `@composio/core` corpus ingress tests: 61 passed - shared Python/TypeScript corpus files are byte-identical (shasum-verified) - `git diff --check` ## Contributor context This replaces four narrow proposals after independent local reproduction: - [#4301](https://github.com/ComposioHQ/composio/pull/4301) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4301) - [#4302](https://github.com/ComposioHQ/composio/pull/4302) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4302) - [#4303](https://github.com/ComposioHQ/composio/pull/4303) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4303) - [#4307](https://github.com/ComposioHQ/composio/pull/4307) · [Glen](https://app.tryglen.com/ComposioHQ/composio/pull/4307) --------- Co-authored-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
2026-09-04 14:19:02 +02:00
return _with_exact_validation(model, param_schema, param_schema)
2025-06-17 12:58:00 +05:30
def get_signature_format_from_schema_params(
schema_params: t.Dict,
skip_default: bool = False,
) -> t.List[Parameter]:
"""
Get function parameters signature(with pydantic field definition as default values)
from schema parameters. Works like:
def demo_function(
owner: str,
repo: str),
)
:param schema_params: A dictionary object containing schema params, with keys [properties, required etc.].
:return: List of required and optional parameters
Output Format:
[
<Parameter "owner: str">,
<Parameter "repo: str">
]
"""
default_parameters = []
none_default_parameters = []
required_params = schema_params.get("required", [])
schema_params_object = schema_params.get("properties", {})
for param_name, param_schema in schema_params_object.items():
param_type = param_schema.get("type", None)
param_oneOf = param_schema.get("oneOf", None)
param_anyOf = param_schema.get("anyOf", None)
param_allOf = param_schema.get("allOf", None)
if param_allOf is not None and len(param_allOf) == 1:
param_type = param_allOf[0].get("type", None)
if param_oneOf is not None or param_anyOf is not None:
param_types = [ptype.get("type") for ptype in (param_oneOf or param_anyOf)]
fix(py): handle 4+ option and typeless anyOf/oneOf unions in tool signatures (#3708) ## Summary `get_signature_format_from_schema_params` in `python/composio/utils/shared.py` builds the `__signature__` for the tool wrappers used by the langchain, langgraph, llamaindex, and autogen providers. Its `oneOf`/`anyOf` handling hardcoded a ladder for exactly 1, 2, or 3 union members and raised `ValueError("Invalid 'oneOf' schema")` for any union with four or more options. It also indexed `PYDANTIC_TYPE_TO_PYTHON_TYPE[ptype.get("type")]` directly, so any combiner option missing a `type` key produced `None` and raised `KeyError(None)`. Both shapes are valid JSON Schema and appear in real tool input schemas, so a tool with a 4-way union, or a combiner option without an explicit `type`, makes those four providers raise at tool-wrap time. This replaces the ladder with a single map + `functools.reduce` path that mirrors the existing `_build_union_from_options` in `schema_converter.py`, keeping the 1/2/3-member and nullable `[type, null]` outputs identical to before. Fixes # ## Changes - `python/composio/utils/shared.py`: in `get_signature_format_from_schema_params`, replace the 1/2/3-member `oneOf`/`anyOf` ladder (which raised `ValueError` for 4+ members) with a map-each-option-to-a-Python-type + `functools.reduce` Union build that supports any member count. Unknown/missing option types map to `typing.Any` instead of raising `KeyError`. Adds `from functools import reduce` and removes three now-unnecessary `# type: ignore` comments. - `python/tests/test_schema_parser.py`: add `TestGetSignatureFormatFromSchemaParams` (8 cases) covering 4- and 5-member unions, an `anyOf` option missing `type`, an all-typeless `anyOf`, and regressions for the previously supported single/2/3-member and nullable `[type, null]` shapes. ## 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_schema_parser.py::TestGetSignatureFormatFromSchemaParams -v # 8 passed uv run pytest tests/test_schema_parser.py -q # 75 passed make chk # ruff + mypy -> clean, no new ignores ``` To confirm the tests guard the fix, the new test class was also run against the unmodified `shared.py`: 4 of the 8 fail there (`ValueError`/`KeyError` on the 4+/typeless cases), and all 8 pass with this change. The nullable `anyOf [type, null]` case still resolves to `Union[str, Any, NoneType]` (its pre-existing behavior); collapsing `null` into `Optional` lives in `schema_converter` and is intentionally left out of this minimal fix, with the test documenting current behavior. ## Screenshots (if applicable) N/A ## 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 - [x] I added tests or explain why not applicable - [ ] I added a changeset if this change affects published packages ## Additional context No linked issue: this is a self-identified, reproduced bug in the older schema->signature path. The fix deliberately mirrors `schema_converter._build_union_from_options` so the two schema paths agree on N-ary union handling. The same bug family exists in `python/composio/utils/openapi.py` (`function_signature_from_jsonschema`, used by the google_adk provider) but is kept out of this PR to stay single-purpose; happy to follow up on it separately.
2026-07-02 13:09:41 +05:30
# Map each option to a Python type, falling back to t.Any for options
# that are missing a "type" key or use an unrecognized type, then build
# a Union for any count of members (no 1/2/3-member cap).
mapped_types: t.List[t.Any] = [
_annotation_from_json_schema_type(ptype) for ptype in param_types
fix(py): handle 4+ option and typeless anyOf/oneOf unions in tool signatures (#3708) ## Summary `get_signature_format_from_schema_params` in `python/composio/utils/shared.py` builds the `__signature__` for the tool wrappers used by the langchain, langgraph, llamaindex, and autogen providers. Its `oneOf`/`anyOf` handling hardcoded a ladder for exactly 1, 2, or 3 union members and raised `ValueError("Invalid 'oneOf' schema")` for any union with four or more options. It also indexed `PYDANTIC_TYPE_TO_PYTHON_TYPE[ptype.get("type")]` directly, so any combiner option missing a `type` key produced `None` and raised `KeyError(None)`. Both shapes are valid JSON Schema and appear in real tool input schemas, so a tool with a 4-way union, or a combiner option without an explicit `type`, makes those four providers raise at tool-wrap time. This replaces the ladder with a single map + `functools.reduce` path that mirrors the existing `_build_union_from_options` in `schema_converter.py`, keeping the 1/2/3-member and nullable `[type, null]` outputs identical to before. Fixes # ## Changes - `python/composio/utils/shared.py`: in `get_signature_format_from_schema_params`, replace the 1/2/3-member `oneOf`/`anyOf` ladder (which raised `ValueError` for 4+ members) with a map-each-option-to-a-Python-type + `functools.reduce` Union build that supports any member count. Unknown/missing option types map to `typing.Any` instead of raising `KeyError`. Adds `from functools import reduce` and removes three now-unnecessary `# type: ignore` comments. - `python/tests/test_schema_parser.py`: add `TestGetSignatureFormatFromSchemaParams` (8 cases) covering 4- and 5-member unions, an `anyOf` option missing `type`, an all-typeless `anyOf`, and regressions for the previously supported single/2/3-member and nullable `[type, null]` shapes. ## 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_schema_parser.py::TestGetSignatureFormatFromSchemaParams -v # 8 passed uv run pytest tests/test_schema_parser.py -q # 75 passed make chk # ruff + mypy -> clean, no new ignores ``` To confirm the tests guard the fix, the new test class was also run against the unmodified `shared.py`: 4 of the 8 fail there (`ValueError`/`KeyError` on the 4+/typeless cases), and all 8 pass with this change. The nullable `anyOf [type, null]` case still resolves to `Union[str, Any, NoneType]` (its pre-existing behavior); collapsing `null` into `Optional` lives in `schema_converter` and is intentionally left out of this minimal fix, with the test documenting current behavior. ## Screenshots (if applicable) N/A ## 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 - [x] I added tests or explain why not applicable - [ ] I added a changeset if this change affects published packages ## Additional context No linked issue: this is a self-identified, reproduced bug in the older schema->signature path. The fix deliberately mirrors `schema_converter._build_union_from_options` so the two schema paths agree on N-ary union handling. The same bug family exists in `python/composio/utils/openapi.py` (`function_signature_from_jsonschema`, used by the google_adk provider) but is kept out of this PR to stay single-purpose; happy to follow up on it separately.
2026-07-02 13:09:41 +05:30
]
if len(mapped_types) == 1:
annotation = mapped_types[0]
2025-06-17 12:58:00 +05:30
else:
fix(py): handle 4+ option and typeless anyOf/oneOf unions in tool signatures (#3708) ## Summary `get_signature_format_from_schema_params` in `python/composio/utils/shared.py` builds the `__signature__` for the tool wrappers used by the langchain, langgraph, llamaindex, and autogen providers. Its `oneOf`/`anyOf` handling hardcoded a ladder for exactly 1, 2, or 3 union members and raised `ValueError("Invalid 'oneOf' schema")` for any union with four or more options. It also indexed `PYDANTIC_TYPE_TO_PYTHON_TYPE[ptype.get("type")]` directly, so any combiner option missing a `type` key produced `None` and raised `KeyError(None)`. Both shapes are valid JSON Schema and appear in real tool input schemas, so a tool with a 4-way union, or a combiner option without an explicit `type`, makes those four providers raise at tool-wrap time. This replaces the ladder with a single map + `functools.reduce` path that mirrors the existing `_build_union_from_options` in `schema_converter.py`, keeping the 1/2/3-member and nullable `[type, null]` outputs identical to before. Fixes # ## Changes - `python/composio/utils/shared.py`: in `get_signature_format_from_schema_params`, replace the 1/2/3-member `oneOf`/`anyOf` ladder (which raised `ValueError` for 4+ members) with a map-each-option-to-a-Python-type + `functools.reduce` Union build that supports any member count. Unknown/missing option types map to `typing.Any` instead of raising `KeyError`. Adds `from functools import reduce` and removes three now-unnecessary `# type: ignore` comments. - `python/tests/test_schema_parser.py`: add `TestGetSignatureFormatFromSchemaParams` (8 cases) covering 4- and 5-member unions, an `anyOf` option missing `type`, an all-typeless `anyOf`, and regressions for the previously supported single/2/3-member and nullable `[type, null]` shapes. ## 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_schema_parser.py::TestGetSignatureFormatFromSchemaParams -v # 8 passed uv run pytest tests/test_schema_parser.py -q # 75 passed make chk # ruff + mypy -> clean, no new ignores ``` To confirm the tests guard the fix, the new test class was also run against the unmodified `shared.py`: 4 of the 8 fail there (`ValueError`/`KeyError` on the 4+/typeless cases), and all 8 pass with this change. The nullable `anyOf [type, null]` case still resolves to `Union[str, Any, NoneType]` (its pre-existing behavior); collapsing `null` into `Optional` lives in `schema_converter` and is intentionally left out of this minimal fix, with the test documenting current behavior. ## Screenshots (if applicable) N/A ## 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 - [x] I added tests or explain why not applicable - [ ] I added a changeset if this change affects published packages ## Additional context No linked issue: this is a self-identified, reproduced bug in the older schema->signature path. The fix deliberately mirrors `schema_converter._build_union_from_options` so the two schema paths agree on N-ary union handling. The same bug family exists in `python/composio/utils/openapi.py` (`function_signature_from_jsonschema`, used by the google_adk provider) but is kept out of this PR to stay single-purpose; happy to follow up on it separately.
2026-07-02 13:09:41 +05:30
annotation = reduce(lambda a, b: t.Union[a, b], mapped_types)
2025-06-17 12:58:00 +05:30
param_default = param_schema.get("default", "")
elif isinstance(param_type, list):
annotation = _annotation_from_json_schema_type(param_type)
scalar_type = param_type[0] if len(param_type) == 1 else None
if isinstance(scalar_type, str) and scalar_type in FALLBACK_VALUES:
param_default = param_schema.get(
"default", FALLBACK_VALUES[scalar_type]
)
else:
param_default = param_schema.get("default", "")
2025-06-17 12:58:00 +05:30
elif param_type in PYDANTIC_TYPE_TO_PYTHON_TYPE:
annotation = PYDANTIC_TYPE_TO_PYTHON_TYPE[param_type]
param_default = param_schema.get("default", FALLBACK_VALUES[param_type])
else:
annotation = pydantic_model_from_param_schema(param_schema)
if param_type is None or param_type == "null":
param_default = None
else:
param_default = param_schema.get("default", FALLBACK_VALUES[param_type])
default = param_default
required = param_schema.get("required", False) or param_name in required_params
if required:
default = Parameter.empty
if skip_default:
default = Parameter.empty
parameter = Parameter(
name=param_name,
kind=Parameter.POSITIONAL_OR_KEYWORD,
annotation=annotation,
default=default,
)
if required:
default_parameters.append(parameter)
continue
none_default_parameters.append(parameter)
return default_parameters + none_default_parameters
def _annotation_from_json_schema_type(schema_type: t.Any) -> t.Any:
"""Convert a JSON Schema ``type`` value to a Python annotation.
JSON Schema Draft 2020-12 and OpenAPI 3.1 allow ``type`` to be an array,
including inside ``anyOf`` and ``oneOf`` branches. Keep that form from
reaching the scalar dictionary lookup, where a list would be unhashable.
"""
if isinstance(schema_type, list):
mapped_types = [_annotation_from_json_schema_type(item) for item in schema_type]
if not mapped_types:
return t.Any
if len(mapped_types) == 1:
return mapped_types[0]
return reduce(lambda left, right: t.Union[left, right], mapped_types)
if isinstance(schema_type, str):
return PYDANTIC_TYPE_TO_PYTHON_TYPE.get(schema_type, t.Any)
return t.Any
2025-06-17 12:58:00 +05:30
def get_pydantic_signature_format_from_schema_params(
schema_params: t.Dict,
skip_default: bool = False,
) -> t.List[Parameter]:
"""
Get function parameters signature(with pydantic field definition as default values)
from schema parameters. Works like:
def demo_function(
owner: str=Field(..., description='The account owner of the repository.'),
repo: str=Field(..., description='The name of the repository without the `.git` extension.'),
)
:param schema_params: A dictionary object containing schema params, with keys [properties, required etc.].
:return: List of required and optional parameters
Example Output Format:
```python
[
<Parameter "owner: str = FieldInfo(
default=Ellipsis,
description='The account owner of the repository.',
extra={'examples': ([],)})">,
<Parameter "repo: str = FieldInfo(
default=Ellipsis,
description='The name of the repository without the `.git` extension.',
extra={'examples': ([],)})">
]
```
"""
all_parameters = []
field_definitions = json_schema_to_fields_dict(schema_params)
for param_name, (param_dtype, parame_field) in field_definitions.items():
param = Parameter(
name=param_name,
kind=Parameter.POSITIONAL_OR_KEYWORD,
annotation=param_dtype,
default=Parameter.empty if skip_default else parame_field.default,
)
all_parameters.append(param)
return all_parameters
def generate_request_id() -> str:
"""Generate a unique request ID."""
return str(uuid.uuid4())