"""Image view tool — load an image from a file path or URL for the LLM to analyse.
The image is returned as base64 and injected into the conversation so the LLM
can actually see it (not just read a text description of it).
"""
import base64
import mimetypes
from pathlib import Path
import httpx
from .base import Tool, ToolResult
_TIMEOUT = 30
_SUPPORTED = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
class ImageViewTool(Tool):
name = "image_view"
description = (
"Load an image from a local file path or HTTP/HTTPS URL so you can see and analyse it. "
"Call this whenever the user mentions an image file or URL, or when you need to "
"inspect visual content. The image becomes visible to you in the next message."
)
parameters = {
"type": "object",
"properties": {
"source": {
"type": "string",
"description": "Absolute file path (e.g. /home/user/photo.jpg) or HTTP/HTTPS URL",
},
},
"required": ["source"],
}
async def execute(self, params: dict) -> ToolResult:
source = params["source"].strip()
try:
if source.startswith(("http://", "https://")):
raw, mime = await self._fetch_url(source)
else:
raw, mime = self._read_file(source)
b64 = base64.b64encode(raw).decode()
size_kb = len(raw) // 1024
return ToolResult(
success=True,
output=f"Image loaded ({size_kb} KB, {mime}). It will appear in the next turn.",
metadata={"base64": b64, "mime": mime, "is_image": True},
)
except Exception as e:
return ToolResult(success=False, output=f"Failed to load image: {e}", error=str(e))
async def _fetch_url(self, url: str) -> tuple[bytes, str]:
async with httpx.AsyncClient(timeout=_TIMEOUT, follow_redirects=True) as client:
r = await client.get(url)
r.raise_for_status()
mime = r.headers.get("content-type", "image/jpeg").split(";")[0].strip()
return r.content, mime
def _read_file(self, path_str: str) -> tuple[bytes, str]:
path = Path(path_str).expanduser().resolve()
if not path.exists():
raise FileNotFoundError(f"File not found: {path}")
if path.suffix.lower() not in _SUPPORTED:
raise ValueError(f"Unsupported image format: {path.suffix}")
mime = mimetypes.guess_type(str(path))[0] or "image/jpeg"
return path.read_bytes(), mime