Handling Attachments and File Uploads in ChatDev Multi-Agent Workflows
ChatDev treats files as first-class assets through the AttachmentStore class, which registers local files or byte streams, deduplicates by SHA-256, and embeds references into MessageBlock objects for seamless agent-to-agent transfer.
The OpenBMB/ChatDev framework enables complex multi-agent workflows where files must persist across execution boundaries. Handling attachments and file uploads in ChatDev multi-agent workflows relies on a filesystem-backed storage layer that couples immutable metadata with MIME-type-aware message blocks. This architecture allows agents to share images, PDFs, and binaries through a consistent API that abstracts physical file paths.
Core Attachment Architecture
ChatDev implements file handling through three layered abstractions defined in utils/attachments.py and entity/messages.py.
AttachmentRef and Immutable Metadata
The AttachmentRef dataclass stores immutable properties including a unique attachment_id, MIME type, SHA-256 hash, file size, and physical location. This reference serves as the canonical identity for any file within the system. According to the source code in entity/messages.py at line 43, AttachmentRef objects are lightweight and hashable, making them suitable for indexing and comparison operations.
AttachmentRecord and Message Coupling
AttachmentRecord binds an AttachmentRef to a specific MessageBlockType (e.g., file, image) and optional user-supplied descriptions. Defined in utils/attachments.py at line 18, this record acts as the bridge between raw storage and conversational content. The method as_message_block() converts the record into a MessageBlock instance that can be inserted directly into a message's content list.
AttachmentStore and Workflow Scoping
The AttachmentStore class provides the primary interface for registration, retrieval, and persistence. Each workflow execution receives a dedicated store instance backed by a per-run directory structure (<run-root>/attachments/<attachment_id>/). The store maintains an in-memory index and writes persistent records to attachments_manifest.json via the _save_manifest method.
Registering Files and Binary Data
The store exposes three registration helpers in utils/attachments.py (lines 72-84) to accommodate different input sources:
register_file– Ingests existing local files from disk. Acceptsfile_path,kind(defaults toMessageBlockType.FILE),display_name,mime_type, and adeduplicateboolean.register_bytes– Accepts in-memory binary data (e.g., generated PNGs). Requiresdata,mime_type, anddisplay_name.register_remote_file– References files hosted by external providers (e.g., OpenAI file IDs). Usesremote_file_id,name,mime_type, andsize.
All methods compute a SHA-256 hash for deduplication, copy the file into the per-run attachments directory, and return an AttachmentRecord.
from pathlib import Path
from utils.attachments import AttachmentStore
from entity.messages import MessageBlockType
store = AttachmentStore(Path("/tmp/run/attachments"))
record = store.register_file(
"/home/user/report.pdf",
kind=MessageBlockType.FILE,
display_name="Project Report.pdf",
mime_type="application/pdf",
deduplicate=True,
)
Embedding Attachments in Agent Messages
Once registered, attachments enter the conversation flow through MessageBlock objects. The AttachmentRecord.as_message_block() method constructs a block with the appropriate type derived from the MIME type via MessageBlockType.from_mime_type (see lines 30-40 in entity/messages.py).
from entity.messages import Message, MessageRole
msg = Message(
role=MessageRole.ASSISTANT,
content=[record.as_message_block()],
)
This design allows multimodal content to traverse the message history exactly like text tokens, ensuring that downstream agents receive both the metadata and the file reference.
Runtime Context and Dependency Injection
Every workflow run receives its AttachmentStore through the runtime context, eliminating the need for agents to manage filesystem paths manually.
- RuntimeBuilder instantiates the store during workflow initialization. Lines 31-36 in
workflow/runtime/runtime_builder.pycreate theattachmentssubdirectory undercode_workspaceand initialize the store. - RuntimeContext holds the store as a dependency injected into all nodes and tools. Line 25 in
workflow/runtime/runtime_context.pyexposesattachment_storeas a context attribute.
Agents and tools access the store uniformly via context["attachment_store"] or context.attachment_store, depending on the context implementation.
HTTP Upload Handling
The FastAPI server handles multipart uploads through AttachmentService.save_upload_file (lines 46-70 in server/services/attachment_service.py). The service:
- Streams
UploadFilechunks to a temporary directory. - Detects MIME types via
upload.content_typeormimetypes.guess_type. - Registers the temporary file with the session-specific
AttachmentStoreusingregister_filewithkind=MessageBlockType.from_mime_type(...). - Cleans up temporary files after registration.
This ensures that files uploaded via HTTP become immediately available to the workflow's agents through the same store interface.
Cross-Session Propagation and Deduplication
When tools or sub-graphs forward attachments between different sessions or agents, AttachmentService.build_attachment_blocks (lines 95-103 in server/services/attachment_service.py) orchestrates the transfer:
- Retrieves the source
AttachmentRecordfrom the origin store. - Copies the underlying file to a target store via
ingest_recordwhen stores reside in different workflow roots. - Returns a list of
MessageBlockobjects ready for new messages.
blocks = attachment_service.build_attachment_blocks(
session_id="session_123",
attachment_ids=["a1b2c3"],
target_store=other_context.attachment_store,
)
Deduplication occurs at registration time through the _hash_index dictionary. When deduplicate=True, the store checks for existing SHA-256 hashes; if found, it returns the existing AttachmentRecord instead of copying duplicate data. Only records marked with persist=True are written to attachments_manifest.json via _save_manifest, which is reloaded on store startup via _load_manifest.
Tool Integration
File-related tools in functions/function_calling/file.py expect an AttachmentStore in their tool_context. The _require_store helper at lines 42-45 validates the store's presence before tools attempt file operations. This pattern ensures that all file-aware tools operate within the workflow's scoped storage, maintaining isolation between concurrent runs.
def generate_chart(context: ToolContext) -> List[MessageBlock]:
img_bytes = create_plot() # matplotlib or similar
store = context["attachment_store"]
record = store.register_bytes(
img_bytes,
mime_type="image/png",
display_name="sales_chart.png",
description="Sales performance chart",
)
return [record.as_message_block()]
Summary
- AttachmentStore provides the central API for registering local files, byte streams, and remote references while handling SHA-256 deduplication.
- AttachmentRef and AttachmentRecord separate immutable metadata from message-specific presentation logic.
- The runtime builder injects a scoped store into RuntimeContext, making file operations available to all workflow nodes without hardcoded paths.
- AttachmentService bridges HTTP uploads and cross-session transfers through
save_upload_fileandbuild_attachment_blocks. - Deduplication and manifest persistence optimize storage and enable workflow resumption across executions.
Frequently Asked Questions
How does ChatDev prevent duplicate file storage across workflow runs?
The AttachmentStore maintains a _hash_index mapping SHA-256 hashes to existing records. When deduplicate=True is passed to register_file or register_bytes, the store checks this index and returns the existing AttachmentRecord if the hash matches, avoiding redundant copies.
Can agents access attachments from previous workflow sessions?
Yes, through the ingest_record method. When AttachmentService.build_attachment_blocks copies an attachment to a target store in a different session, it uses ingest_record with copy_file=True to physically transfer the file while preserving metadata, enabling cross-session file sharing.
What MIME types automatically map to specific message block types?
The MessageBlockType.from_mime_type method at lines 30-40 in entity/messages.py maps common patterns (e.g., image/* to MessageBlockType.IMAGE, application/pdf to MessageBlockType.FILE). Tools can override this by explicitly specifying the kind parameter during registration.
How do I register a file that exists only in memory?
Use AttachmentStore.register_bytes, which accepts a bytes object, mime_type, and display_name. This method writes the data to the store's directory structure and returns an AttachmentRecord identical to disk-based registrations, as shown in the tool integration examples.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →