Skip to content

Add message cancel/export and file upload/download to client.chat - #23

Open
ahmeda-cominty wants to merge 5 commits into
devfrom
feat/chat-file-export-cancel-support
Open

ahmeda-cominty wants to merge 5 commits into
devfrom
feat/chat-file-export-cancel-support

Conversation

@ahmeda-cominty

@ahmeda-cominty ahmeda-cominty commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

ENG-951: Add file upload and download support to the Cominty SDK
ENG-950: Add message export support (Export Step Output) to the Cominty SDK
ENG-949: Add support for cancelling a chat message to the Cominty SDK

Summary

Adds three things to client.chat: stopping a running message (cancel), exporting a finished message as a document (export), and file transfer (upload_file / download_file). Nothing is removed or renamed: this is a minor release (1.1.0).

from pathlib import Path

run = await client.chat.start(agent_id=AGENT_ID, message="Write a long market report.")

# Stop it
message = await client.chat.cancel(run.message_id)  # status == MessageStatus.cancelled

# Export a finished answer
reply = await (await client.chat.start(agent_id=AGENT_ID, message="Write a brief.")).result()
pdf = await client.chat.export(reply.id, format="pdf")  # bytes, "docx" also works
Path("brief.pdf").write_bytes(pdf)

# Give the agent files, then read one back
report = await client.chat.upload_file(Path("q3-report.pdf"))  # name and mimetype inferred
data = await client.chat.upload_file(b"a,b\n1,2\n", filename="data.csv")  # bytes need a filename
run = await client.chat.start(
    agent_id=AGENT_ID, message="Compare these two files.", file_ids=[report.id, data.id]
)
content = await client.chat.download_file(report.id)

Design

  • cancel(message_id) -> Message: POST /chat/messages/{id}/cancel. The status flips to cancelled immediately. It is not idempotent: a message that is not running (finished, failed, already cancelled, or a user message) raises APIError (400, "Message is not live"), an unknown one NotFoundError.
  • export(message_id, *, format) -> bytes: GET /chat/messages/{id}/export. format is the new public ExportFormat ("pdf" or "docx"), validated locally (InvalidParams). The server sends no file name, so the caller picks one. A message without content raises APIError (400).
  • upload_file(file, *, filename=None, mimetype=None) -> ConversationFile: runs the presigned flow (GET /chat/files/upload, a direct multipart POST to storage, POST /chat/files with the ETag exactly as storage returned it).
    • file is bytes (then filename is required) or a pathlib.Path (name from the path, mimetype guessed, falling back to application/octet-stream).
    • A plain str is rejected with InvalidParams: it is ambiguous between a path and text, and upload_file("hello") used to fail with a baffling FileNotFoundError.
    • A file over 500 MiB (the hosted API's CONVERSATION_FILE_MAX_SIZE) is rejected before any request. The size of a Path comes from stat(), the file is not read.
    • The file is read off the event loop (asyncio.to_thread).
  • download_file(file_id) -> bytes: GET /chat/files/{id} answers with a 5 minute presigned URL, which is then fetched.
  • Message ids are checked as UUIDs on cancel and export, and download_file percent-encodes its id, so an id cannot rewrite the request path (cancel("../threads/x") is refused locally).
  • Storage safety (new AGENTS.md section 7.4):
    • Upload and download use a fresh httpx.AsyncClient, never the main one, so x-cominty-token never reaches the storage host.
    • Storage calls never follow redirects, and any non-2xx answer (a 3xx included) is an error. Before, a 307 would have produced an empty download or a misleading "no ETag" error.
    • Presigned URLs are shown without their query string in error messages. A malformed upload permission raises SDKError and never echoes the presigned fields.
    • Storage errors come back as XML: their Code and Message become the APIError message (for example EntityTooLarge: Your proposed upload exceeds the maximum allowed size).
  • Internals: the timeout, network and HTTP error mapping of AsyncTransport moved into one private _send() (separate refactor commit, no behavior change). New AsyncTransport.request_bytes, upload_to_presigned_url, download_from_presigned_url.

Breaking changes

None. The only behavior change is the message of the ValueError raised by chat.stream() for a malformed id: it is still a ValueError, and it now names the argument (message_id must be a UUID, got 'x').

Also in this PR

  • Examples 12_cancel_message.py, 13_export_message.py, 14_upload_files.py (numbering follows 11_capabilities.py).
  • README: sections "Stop a message", "Export a message as a document", "Give the agent files", and the file_ids row of the parameters table now points to upload_file().
  • CHANGELOG 1.1.0 section.
  • The branch was rebased on dev and its three original commits rewritten to follow the current AGENTS.md (no em dash, no module docstrings in src/ or tests/, Google docstrings on the public surface only, absolute imports across packages, 100 % coverage). The refactor is its own commit.

Test plan

  • uv run invoke code.all (ruff, pyright strict, pytest, 100 % coverage), run on each of the 4 commits
  • Unit tests on Python 3.9 as well (306 passed, 65 new)
  • Unit tests: request URL, method, query and body for each call; bytes, Path, UUID and str inputs; quoted ETag preserved; multipart field order (form fields before the file part); x-cominty-token absent from every storage request; redirects, storage XML errors, missing ETag, timeouts and network errors; invalid ids, filenames, formats and mimetypes before any request; the 500 MiB limit
  • Playground scenarios against production (playground/test_scenario/chat_files.py, not part of this PR): cancel and its error cases, pdf and docx magic bytes, upload then byte-exact download, a file attached to a message and read back by the agent, a 25 MiB upload accepted, a 500 MiB + 1 byte file refused locally
  • Release: uv run invoke release --minor, then push and publish the GitHub Release

@coderabbitai

coderabbitai Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 25b91108-cfe4-4385-81a1-515d26d1ae39

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ahmeda-cominty
ahmeda-cominty requested a review from mous222 August 31, 2026 11:19
@ahmeda-cominty ahmeda-cominty self-assigned this Aug 31, 2026
@ahmeda-cominty

Copy link
Copy Markdown
Contributor Author

@elfominty

@ahmeda-cominty
ahmeda-cominty force-pushed the feat/chat-file-export-cancel-support branch from 7a9e179 to f62293d Compare August 31, 2026 15:53
ahmeda-cominty and others added 3 commits October 16, 2026 12:59
Sends POST /chat/messages/{id}/cancel and returns the Message with its
status set to cancelled. The call is not idempotent: a message that is
no longer live raises APIError (400).

Fixes ENG-949
Move the timeout, network error, and HTTP error mapping out of
AsyncTransport.request() into a private _send() so the upcoming byte
and presigned-URL calls share it. No behavior change.
Sends GET /chat/messages/{id}/export and returns the raw file bytes.
The format is validated locally (ExportFormat), so a bad value raises
InvalidParams before any request. Adds AsyncTransport.request_bytes().

Fixes ENG-950
@elfominty
elfominty force-pushed the feat/chat-file-export-cancel-support branch 2 times, most recently from 5629430 to a00a97e Compare October 16, 2026 11:24
@elfominty
elfominty self-requested a review October 16, 2026 11:28
upload_file() runs the presigned flow (permission, direct upload to
storage, confirmation) and takes bytes or a Path, inferring the name and
mimetype from the path. Anything else, a plain str included, is
rejected as ambiguous. A file over 500 MiB, the hosted API's limit, is
rejected before any request.

download_file() asks the API for a presigned URL, then fetches it.

Storage calls use a fresh client so x-cominty-token never reaches the
storage host. They never follow redirects, treat any non-2xx answer as
an error, and keep presigned signatures out of error messages and out
of raw validation errors. Storage XML errors surface their Code and
Message.

Fixes ENG-951
@elfominty
elfominty force-pushed the feat/chat-file-export-cancel-support branch from a00a97e to 9b17836 Compare October 16, 2026 11:30

This branch has not been deployed

No deployments
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.

2 participants