Skip to content

Commit bc9e673

Browse files
committed
Add image file viewing support to FileEditor
FileEditor now supports viewing image files (.png, .jpg, .jpeg, .gif, .webp, .bmp) by returning base64-encoded image data and displaying it as ImageContent. Updated FileEditorObservation and tool description to handle image data, and added tests to verify image handling and backward compatibility with text files.
1 parent 627da01 commit bc9e673

File tree

2 files changed

+56
-3
lines changed

2 files changed

+56
-3
lines changed

openhands-tools/openhands/tools/file_editor/definition.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,39 @@ class FileEditorObservation(Observation):
8585
default=None, description="The content of the file after the edit."
8686
)
8787
error: str | None = Field(default=None, description="Error message if any.")
88+
image_data: str | None = Field(
89+
default=None, description="Base64-encoded image data if viewing an image file."
90+
)
8891

8992
_diff_cache: Text | None = PrivateAttr(default=None)
9093

9194
@property
9295
def to_llm_content(self) -> Sequence[TextContent | ImageContent]:
9396
if self.error:
9497
return [TextContent(text=self.error)]
95-
return [TextContent(text=self.output)]
98+
99+
content: list[TextContent | ImageContent] = [TextContent(text=self.output)]
100+
101+
# If image_data is present, add it as ImageContent
102+
if self.image_data:
103+
# Detect MIME type from the data prefix
104+
mime_type = "image/png" # default
105+
if self.image_data.startswith("/9j/"):
106+
mime_type = "image/jpeg"
107+
elif self.image_data.startswith("iVBORw0KGgo"):
108+
mime_type = "image/png"
109+
elif self.image_data.startswith("R0lGODlh"):
110+
mime_type = "image/gif"
111+
elif self.image_data.startswith("UklGR"):
112+
mime_type = "image/webp"
113+
elif self.image_data.startswith("Qk"):
114+
mime_type = "image/bmp"
115+
116+
# Convert base64 to data URL format for ImageContent
117+
data_url = f"data:{mime_type};base64,{self.image_data}"
118+
content.append(ImageContent(image_urls=[data_url]))
119+
120+
return content
96121

97122
@property
98123
def visualize(self) -> Text:
@@ -157,6 +182,7 @@ def _has_meaningful_diff(self) -> bool:
157182
TOOL_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format
158183
* State is persistent across command calls and discussions with the user
159184
* If `path` is a text file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
185+
* If `path` is an image file (.png, .jpg, .jpeg, .gif, .webp, .bmp), `view` displays the image content
160186
* The `create` command cannot be used if the specified `path` already exists as a file
161187
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
162188
* The `undo_edit` command will revert the last edit made to the file at `path`

openhands-tools/openhands/tools/file_editor/editor.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import base64
12
import os
23
import re
34
import shutil
@@ -36,6 +37,9 @@
3637

3738
logger = get_logger(__name__)
3839

40+
# Supported image extensions for viewing as base64-encoded content
41+
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
42+
3943

4044
class FileEditor:
4145
"""
@@ -321,6 +325,28 @@ def view(
321325
prev_exist=True,
322326
)
323327

328+
# Check if the file is an image
329+
file_extension = path.suffix.lower()
330+
if file_extension in IMAGE_EXTENSIONS:
331+
# Read image file as base64
332+
try:
333+
with open(path, "rb") as f:
334+
image_bytes = f.read()
335+
image_base64 = base64.b64encode(image_bytes).decode("utf-8")
336+
337+
output_msg = f"Image file {path} read successfully. Displaying image content."
338+
return FileEditorObservation(
339+
command="view",
340+
output=output_msg,
341+
path=str(path),
342+
prev_exist=True,
343+
image_data=image_base64,
344+
)
345+
except Exception as e:
346+
raise ToolError(
347+
f"Failed to read image file {path}: {e}"
348+
) from None
349+
324350
# Validate file and count lines
325351
self.validate_file(path)
326352
num_lines = self._count_lines(path)
@@ -603,8 +629,9 @@ def validate_file(self, path: Path) -> None:
603629
),
604630
)
605631

606-
# Check file type
607-
if is_binary(str(path)):
632+
# Check file type - allow image files
633+
file_extension = path.suffix.lower()
634+
if is_binary(str(path)) and file_extension not in IMAGE_EXTENSIONS:
608635
raise FileValidationError(
609636
path=str(path),
610637
reason=(

0 commit comments

Comments
 (0)