Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 89 additions & 19 deletions python/composio/core/models/_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,25 +776,46 @@ def process_schema_recursively(self, schema: t.Dict) -> t.Dict:
self.enhance_schema_descriptions(schema)
return schema

def _find_uploadable_schema_variant(self, schema: t.Dict) -> t.Optional[t.Dict]:
"""Find a schema variant that contains file_uploadable properties."""
# Check anyOf variants
if "anyOf" in schema:
for variant in schema["anyOf"]:
if self._has_file_property(variant, "file_uploadable"):
return variant
def _find_uploadable_schema_variant(
self, schema: t.Dict, value: t.Any = None
) -> t.Optional[t.Dict]:
"""Find a schema variant that contains file_uploadable properties.

When ``value`` is provided, prefer a variant whose JSON-Schema ``type``
matches the runtime type of the value. This makes
``anyOf(string<file>, array<file>)`` work for both single paths and
lists of paths instead of always returning the first match (which
previously caused ``os.fspath(list)`` errors when the model sent an
array of paths to a tool whose schema accepted both shapes).
"""
for key in ("anyOf", "oneOf", "allOf"):
if key not in schema:
continue

# Check oneOf variants
if "oneOf" in schema:
for variant in schema["oneOf"]:
if self._has_file_property(variant, "file_uploadable"):
return variant
candidates = [
v
for v in schema[key]
if self._has_file_property(v, "file_uploadable")
]
if not candidates:
continue

# Check allOf - merge all variants
if "allOf" in schema:
for variant in schema["allOf"]:
if self._has_file_property(variant, "file_uploadable"):
return variant
if value is not None:
value_type = (
"array"
if isinstance(value, list)
else "object"
if isinstance(value, dict)
else "string"
if isinstance(value, str)
else None
)
if value_type is not None:
for c in candidates:
if c.get("type") == value_type:
return c

return candidates[0]

return None

Expand Down Expand Up @@ -835,8 +856,15 @@ def _substitute_file_uploads_recursively(
).model_dump()
continue

# Check anyOf/oneOf/allOf for file_uploadable
uploadable_variant = self._find_uploadable_schema_variant(param_schema)
# Check anyOf/oneOf/allOf for file_uploadable. Pass the runtime
# value so the picker can match an array-of-files variant when the
# model sends a list, instead of always returning the first
# (string) variant — which previously caused ``os.fspath(list)``
# errors for tools like ``GMAIL_SEND_EMAIL`` whose ``attachment``
# field accepts ``anyOf(string<file>, array<file>)``.
uploadable_variant = self._find_uploadable_schema_variant(
param_schema, value=request[_param]
)
if uploadable_variant is not None:
# If the variant itself is file_uploadable
if uploadable_variant.get("file_uploadable", False):
Expand All @@ -856,6 +884,48 @@ def _substitute_file_uploads_recursively(
).model_dump()
continue

# Array-of-file_uploadable variant: model sent a list of paths
# and the schema permits an array branch with uploadable items.
if (
uploadable_variant.get("type") == "array"
and isinstance(request[_param], list)
and isinstance(uploadable_variant.get("items"), dict)
and self._has_file_property(
uploadable_variant["items"], "file_uploadable"
)
):
items_schema = uploadable_variant["items"]
processed: t.List[t.Any] = []
for item in request[_param]:
if item is None or item == "":
continue
if items_schema.get("file_uploadable", False):
processed.append(
FileUploadable.from_path(
client=self._client,
file=item,
tool=tool.slug,
toolkit=tool.toolkit.slug,
sensitive_file_upload_protection=self._sensitive_file_upload_protection,
file_upload_path_deny_segments=self._file_upload_path_deny_segments,
file_upload_allowlist=self._file_upload_allowlist,
before_file_upload=before_file_upload,
).model_dump()
)
elif isinstance(item, dict):
processed.append(
self._substitute_file_uploads_recursively(
schema=items_schema,
request=item,
tool=tool,
before_file_upload=before_file_upload,
)
)
else:
processed.append(item)
request[_param] = processed
continue

# If the variant has nested properties with file_uploadable
if (
isinstance(request[_param], dict)
Expand Down
Loading