chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,672 @@
|
||||
"""
|
||||
Shared utils.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
import hashlib
|
||||
import json
|
||||
import keyword
|
||||
import typing as t
|
||||
import uuid
|
||||
from functools import reduce
|
||||
from inspect import Parameter
|
||||
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from composio.exceptions import InvalidParams, InvalidSchemaError
|
||||
from composio.utils.json_schema import dereference_json_schema
|
||||
from composio.utils.logging import get as get_logger
|
||||
from composio.utils.schema_converter import (
|
||||
CONTAINER_TYPE,
|
||||
FALLBACK_VALUES,
|
||||
PYDANTIC_TYPE_TO_PYTHON_TYPE,
|
||||
json_schema_to_pydantic_type,
|
||||
)
|
||||
|
||||
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",
|
||||
"ToolSchemaAliases",
|
||||
"alias_tool_input_schema",
|
||||
"restore_tool_arguments",
|
||||
"substitute_reserved_python_keywords",
|
||||
"reinstate_reserved_python_keywords",
|
||||
"normalize_tool_arguments",
|
||||
]
|
||||
|
||||
reserved_names = ["validate"]
|
||||
|
||||
_OBJ_MARKER = "-_object_-"
|
||||
_ARR_MARKER = "-_array_-"
|
||||
_MAX_PROVIDER_ALIAS_LENGTH = 64
|
||||
|
||||
|
||||
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 _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"
|
||||
|
||||
|
||||
def _make_python_identifier(name: str) -> str:
|
||||
if keyword.iskeyword(name):
|
||||
return _make_safe_name(name)
|
||||
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
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, {}
|
||||
|
||||
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}"
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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"]]
|
||||
|
||||
return schema, aliases
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
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,
|
||||
aliases=object_aliases,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def json_schema_to_pydantic_field(
|
||||
name: str,
|
||||
json_schema: t.Dict[str, t.Any],
|
||||
required: t.List[str],
|
||||
skip_default: bool = False,
|
||||
) -> 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.
|
||||
:return: A Pydantic field definition.
|
||||
"""
|
||||
description = json_schema.get("description")
|
||||
if "oneOf" in json_schema:
|
||||
description = " | ".join(
|
||||
[option.get("description", "") for option in json_schema["oneOf"]]
|
||||
)
|
||||
description = f"Any of the following options(separated by |): {description}"
|
||||
|
||||
examples = json_schema.get("examples", [])
|
||||
default = json_schema.get("default")
|
||||
|
||||
# Coerce default value to match expected type from schema
|
||||
if default is not None:
|
||||
default = _coerce_default_value(default, json_schema)
|
||||
|
||||
# Check if the field name is a reserved Pydantic name
|
||||
original_name = name
|
||||
if name in reserved_names:
|
||||
name = f"{name}_"
|
||||
alias = original_name
|
||||
else:
|
||||
alias = None
|
||||
|
||||
field = {
|
||||
"description": description,
|
||||
"examples": examples,
|
||||
"alias": alias,
|
||||
}
|
||||
if not skip_default:
|
||||
field["default"] = ... if original_name in required else default
|
||||
|
||||
return (
|
||||
name,
|
||||
t.cast(
|
||||
t.Type,
|
||||
json_schema_to_pydantic_type(
|
||||
json_schema=json_schema,
|
||||
),
|
||||
),
|
||||
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", [])
|
||||
)
|
||||
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")
|
||||
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,
|
||||
)
|
||||
field_definitions[updated_name] = (pydantic_type, pydantic_field)
|
||||
return create_model(model_name, **field_definitions) # type: ignore
|
||||
|
||||
|
||||
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":
|
||||
# print("param_schema inside array - ", param_schema)
|
||||
item_schema = param_schema.get("items")
|
||||
if item_schema:
|
||||
ItemType = t.cast(
|
||||
t.Type,
|
||||
json_schema_to_pydantic_type(
|
||||
json_schema=item_schema,
|
||||
),
|
||||
)
|
||||
return t.List[ItemType] # type: ignore
|
||||
return t.List
|
||||
|
||||
for prop_name, prop_info in param_schema.get("properties", {}).items():
|
||||
prop_type = prop_info.get("type")
|
||||
prop_title = prop_info.get("title", prop_name).replace(" ", "")
|
||||
prop_default = prop_info.get("default", FALLBACK_VALUES.get(prop_type))
|
||||
if (
|
||||
prop_type is not None
|
||||
and prop_type in PYDANTIC_TYPE_TO_PYTHON_TYPE
|
||||
and prop_type not in CONTAINER_TYPE
|
||||
):
|
||||
signature_prop_type = PYDANTIC_TYPE_TO_PYTHON_TYPE[prop_type]
|
||||
elif prop_type is None:
|
||||
# Schema uses anyOf/allOf/oneOf/$ref instead of a top-level "type" key.
|
||||
# Delegate to json_schema_to_pydantic_type which handles all combiners.
|
||||
signature_prop_type = t.cast(
|
||||
t.Type,
|
||||
json_schema_to_pydantic_type(json_schema=prop_info),
|
||||
)
|
||||
else:
|
||||
signature_prop_type = pydantic_model_from_param_schema(prop_info)
|
||||
|
||||
field_kwargs = {
|
||||
"description": prop_info.get(
|
||||
"description", prop_info.get("desc", prop_title)
|
||||
),
|
||||
}
|
||||
|
||||
# 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:
|
||||
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:
|
||||
return t.Dict
|
||||
|
||||
return create_model( # type: ignore
|
||||
param_title,
|
||||
**required_fields,
|
||||
**optional_fields,
|
||||
)
|
||||
|
||||
|
||||
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)]
|
||||
# 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] = [
|
||||
PYDANTIC_TYPE_TO_PYTHON_TYPE.get(ptype, t.Any) for ptype in param_types
|
||||
]
|
||||
if len(mapped_types) == 1:
|
||||
annotation = mapped_types[0]
|
||||
else:
|
||||
annotation = reduce(lambda a, b: t.Union[a, b], mapped_types)
|
||||
param_default = param_schema.get("default", "")
|
||||
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 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())
|
||||
Reference in New Issue
Block a user