mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
fix(sdk): handle base64 reads with unknown file extensions (#3663)
Closes #3657 --- Binary file reads now respect the backend-declared `encoding` before deciding whether content should be line-numbered as text. This prevents base64 payloads for extensions outside the multimodal block map, like `.docx`, from being treated as plain text. ## Changes - Route `FilesystemMiddleware` read results with `encoding="base64"` through binary `content_blocks` even when `_get_file_type` returns `text`. - Fall back to a generic `file` block for unknown binary extensions while still using `mimetypes.guess_type` to preserve the best available media type. - Keep existing image/audio/video handling unchanged by using the mapped block type whenever the extension is recognized.
This commit is contained in:
@@ -900,12 +900,19 @@ class FilesystemMiddleware(AgentMiddleware[FilesystemState, ContextT, ResponseT]
|
||||
)
|
||||
|
||||
file_type = _get_file_type(validated_path)
|
||||
encoding = read_result.file_data.get("encoding", "utf-8")
|
||||
content = read_result.file_data["content"]
|
||||
|
||||
if file_type != "text":
|
||||
# Route on the backend-declared encoding first: `"base64"` means the
|
||||
# content is binary and must never be line-numbered as text, even
|
||||
# when the extension is absent from `_EXTENSION_TO_FILE_TYPE` (#3657).
|
||||
# The extension map is only consulted to pick the multimodal block
|
||||
# type; unknown binary extensions fall back to the generic `"file"`.
|
||||
if encoding == "base64" or file_type != "text":
|
||||
block_type = file_type if file_type != "text" else "file"
|
||||
mime_type = mimetypes.guess_type("file" + Path(validated_path).suffix)[0] or "application/octet-stream"
|
||||
return ToolMessage(
|
||||
content_blocks=cast("list[ContentBlock]", [{"type": file_type, "base64": content, "mime_type": mime_type}]),
|
||||
content_blocks=cast("list[ContentBlock]", [{"type": block_type, "base64": content, "mime_type": mime_type}]),
|
||||
name="read_file",
|
||||
tool_call_id=tool_call_id,
|
||||
additional_kwargs={"read_file_path": validated_path, "read_file_media_type": mime_type},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import mimetypes
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -1218,6 +1219,53 @@ class TestFilesystemMiddleware:
|
||||
assert result.content[0]["mime_type"] == "image/png"
|
||||
assert result.content[0]["base64"] == "<base64_data>"
|
||||
|
||||
def test_read_file_base64_unknown_extension_returns_file_block(self):
|
||||
"""Binary reads route on `encoding`, not the extension map (#3657).
|
||||
|
||||
A backend may return `encoding="base64"` for a file whose extension is
|
||||
absent from `_EXTENSION_TO_FILE_TYPE` (e.g. `.docx`). The base64 payload
|
||||
must be emitted as a generic `file` content block, never line-numbered
|
||||
as text into the LLM context.
|
||||
"""
|
||||
|
||||
class DocxBackend(StateBackend):
|
||||
def read(self, path, *, offset=0, limit=100):
|
||||
return ReadResult(
|
||||
file_data={
|
||||
"content": "<docx_base64_data>",
|
||||
"encoding": "base64",
|
||||
}
|
||||
)
|
||||
|
||||
middleware = FilesystemMiddleware(backend=DocxBackend())
|
||||
state = FilesystemState(messages=[], files={})
|
||||
runtime = ToolRuntime(
|
||||
state=state,
|
||||
context=None,
|
||||
tool_call_id="docx-read-1",
|
||||
store=None,
|
||||
stream_writer=lambda _: None,
|
||||
config={},
|
||||
)
|
||||
|
||||
read_file_tool = next(tool for tool in middleware.tools if tool.name == "read_file")
|
||||
result = read_file_tool.invoke({"file_path": "/app/report.docx", "runtime": runtime})
|
||||
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.status == "success"
|
||||
# Binary content_blocks, not a line-numbered text string.
|
||||
assert isinstance(result.content, list)
|
||||
assert result.content[0]["type"] == "file"
|
||||
assert result.content[0]["base64"] == "<docx_base64_data>"
|
||||
# The block carries the best media type `mimetypes` can resolve for the
|
||||
# extension, falling back to `application/octet-stream`. The resolved
|
||||
# value is platform-dependent (`.docx` maps to the full Office type on
|
||||
# macOS/Linux but to `application/octet-stream` on Windows, where
|
||||
# `mimetypes` reads the registry), so mirror the production logic rather
|
||||
# than hardcoding a single value.
|
||||
expected_mime = mimetypes.guess_type("file.docx")[0] or "application/octet-stream"
|
||||
assert result.content[0]["mime_type"] == expected_mime
|
||||
|
||||
def test_read_file_image_returns_error_when_download_fails(self):
|
||||
"""Image reads should return a clear backend error string."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user