Skip to content

fix(python-sdk): handle anyOf(string, array<file>) for file_uploadable#3353

Draft
abhishekpatil4 wants to merge 1 commit into
nextfrom
fix/file-uploadable-anyof-array-multi
Draft

fix(python-sdk): handle anyOf(string, array<file>) for file_uploadable#3353
abhishekpatil4 wants to merge 1 commit into
nextfrom
fix/file-uploadable-anyof-array-multi

Conversation

@abhishekpatil4
Copy link
Copy Markdown
Collaborator

Summary

The Python SDK's file_uploadable resolver was breaking on tools whose schema accepts both a single path and a list of paths (e.g. the new GMAIL_SEND_EMAIL.attachment schema):

"attachment": {
  "anyOf": [
    { "type": "string", "format": "path", "file_uploadable": true },
    { "type": "array",  "items": { "type": "string", "format": "path", "file_uploadable": true } }
  ]
}

When the model picked the array branch and sent [\"/p1\", \"/p2\", \"/p3\"], the SDK always returned the first matching anyOf variant from _find_uploadable_schema_variant (the string one), then called FileUploadable.from_path(file=<list>) and crashed with:

argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'list'

This affects every tool exposing anyOf(file, array<file>) — Gmail multi-attachment, Slack SLACK_UPLOAD_OR_CREATE_A_FILE_IN_SLACK when the model batches files into one call, etc.

Fixes #

Changes

  • _find_uploadable_schema_variant is now value-aware. When a runtime value is passed in, it prefers the anyOf/oneOf/allOf variant whose JSON-Schema type matches the value's Python type (array for list, string for str, object for dict). Falls back to first match if no value or no type match — preserves existing behavior for callers that pass value=None.
  • New array-of-file_uploadable branch in _substitute_file_uploads_recursively: when the picker returns an array variant whose items are file_uploadable, iterate the list and upload each file via FileUploadable.from_path, replacing the parameter with [{name, mimetype, s3key}, ...]. The existing security/allowlist/before-hook kwargs (sensitive_file_upload_protection, file_upload_path_deny_segments, file_upload_allowlist, before_file_upload) are threaded through to each upload.

Type of change

  • Bug fix
  • New feature
  • Refactor/Chore
  • Documentation
  • Breaking change

How Has This Been Tested?

Repro before the fix:

composio = Composio(provider=OpenAIAgentsProvider(), dangerously_allow_auto_upload_download_files=True)
tools = composio.tools.get(user_id=\"<user>\", tools=[\"GMAIL_SEND_EMAIL\"])
agent = Agent(name=\"Email\", instructions=\"...\", tools=tools)
await Runner.run(starting_agent=agent, input=\"send an email to x@y.com with these attachments /a.png /b.jpg /c.png\")

Before:

  • Model calls GMAIL_SEND_EMAIL once with attachment=['/a.png','/b.jpg','/c.png']
  • Tool returns successful=false with \"argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'list'\"

After:

  • Same model call, but the SDK now uploads all three files individually and forwards attachment=[{name, mimetype, s3key}, ...] to the backend executor.
  • Email is sent with all three attachments.

Manual verification done against backend.composio.dev with the live GMAIL_SEND_EMAIL schema. Existing single-path call sites (attachment='/a.png') are unaffected because the picker still returns the string variant for non-list values.

I'd like to add unit tests in python/tests/test_auto_upload_download_files.py covering both branches (single path vs list) before this leaves draft — happy to do that here or as a follow-up depending on what reviewers prefer.

Checklist

  • I have read the Code of Conduct and this PR adheres to it
  • I ran linters/tests locally and they passed (local manual repro only — please run CI)
  • I updated documentation as needed (no public API surface change)
  • I added tests or explain why not applicable (see note above — happy to add before un-drafting)
  • I added a changeset if this change affects published packages (python pkg — let me know if a changeset is required for python-only fixes; I didn't see one in recent python-sdk fix commits)

Additional context

Originally surfaced while debugging a multi-attachment Gmail send. The toolkit-side attachment schema was already updated to anyOf(string<file>, array<file>); this PR fixes the matching SDK-side resolver so the new schema is actually usable end-to-end.

Made with Cursor

When a tool's `file_uploadable` parameter has an `anyOf` accepting both a
single path and an array of paths (e.g. the new `GMAIL_SEND_EMAIL`
`attachment` schema), the model often picks the array branch and sends
`["/p1", "/p2", "/p3"]`. The SDK previously always returned the first
matching anyOf variant from `_find_uploadable_schema_variant`, which is
the singular string variant, then called `FileUploadable.from_path` on
the list — crashing with:

    argument should be a str or an os.PathLike object where __fspath__
    returns a str, not 'list'

The variant picker is now value-aware: when called with a runtime
`value`, it prefers the variant whose JSON-Schema `type` matches the
value (`array` for lists, `string` for strings, `object` for dicts).
A new array-of-`file_uploadable` branch in `_substitute_file_uploads_recursively`
iterates the list and uploads each file, replacing the parameter with
`[{name, mimetype, s3key}, ...]`.

This unblocks Gmail multi-attachment, Slack multi-file upload, and any
other tool that exposes `anyOf(file, array<file>)`. Single-path callers
are unaffected because `_find_uploadable_schema_variant` falls back to
the first candidate when the value is `None` or doesn't match any
variant type.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 6, 2026 6:16pm

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant