A @tool-decorated function captures a screenshot and hands the PNG bytes back to the agent. The vision model sees the actual image on the next turn.
from praisonaiagents import Agent, toolfrom praisonaiagents.tools import multimodal_content, text_part, image_part@tooldef take_screenshot() -> dict: """Capture a screenshot and return it as an image the agent can see.""" import subprocess, base64 result = subprocess.run( ["python", "-c", "import PIL.ImageGrab; img = PIL.ImageGrab.grab(); " "import io; buf=io.BytesIO(); img.save(buf, 'PNG'); " "print(buf.getvalue().hex())"], capture_output=True, text=True ) png_bytes = bytes.fromhex(result.stdout.strip()) return multimodal_content( text_part("Here is the current screenshot:"), image_part(png_bytes), )agent = Agent( instructions="You are a screen-reading assistant. Take a screenshot and describe what you see.", tools=[take_screenshot], llm="gpt-4o",)agent.start("Take a screenshot and describe what is on screen.")
The user asks for a screenshot summary; the tool returns multimodal content the model can read.
2
Remote image via URL
No downloading needed — pass url= directly and the model fetches it.
from praisonaiagents import Agent, toolfrom praisonaiagents.tools import multimodal_content, text_part, image_part@tooldef fetch_chart(symbol: str) -> dict: """Return a stock chart image for the given symbol.""" chart_url = f"https://charts.example.com/{symbol}.png" return multimodal_content( text_part(f"Stock chart for {symbol}:"), image_part(url=chart_url), )agent = Agent( instructions="You are a financial analyst. Fetch charts and describe trends.", tools=[fetch_chart], llm="gpt-4o",)agent.start("Show me the chart for AAPL and describe the recent trend.")
3
Bare list (lightweight shape)
Skip the helper — return a plain list of part-dicts. The runtime normalises it automatically.
from praisonaiagents import Agent, tool@tooldef render_diagram(code: str) -> list: """Render a Mermaid diagram and return the image.""" import base64, io # ... render to PNG bytes ... png_bytes = b"" # replace with real rendering b64 = base64.b64encode(png_bytes).decode() return [ {"type": "text", "text": "Rendered diagram:"}, {"type": "image", "data": b64, "mime": "image/png"}, ]agent = Agent( instructions="Render diagrams and explain them.", tools=[render_diagram], llm="gpt-4o",)agent.start("Render a simple flowchart and explain what it shows.")
---Let your tools hand back screenshots, charts, or rendered pages — the agent sees the picture, not a description of it.
from praisonaiagents import Agent, toolfrom praisonaiagents.tools import multimodal_content, text_part, image_part@tooldef show_chart() -> dict: return multimodal_content(text_part("Chart:"), image_part(b"...png bytes..."))agent = Agent(name="analyst", instructions="Interpret visuals from tools.", tools=[show_chart])agent.start("What does the chart show?")
The user asks a visual question; the tool returns image bytes the vision model reads on the next turn.The runtime emits two messages automatically:
A tool message satisfying the tool_call_id contract (containing a short text fallback).
A follow-up user message carrying the image_url parts.
This two-step delivery is required because most providers reject image_url parts inside tool-role messages. You don’t need to do anything — it’s automatic. When an assistant turn contains multiple tool calls, all tool replies stay consecutive and the media user message is appended only after the full batch flushes.
from praisonaiagents import Agent, toolfrom praisonaiagents.tools import multimodal_content, text_part, image_partimport io@tooldef screenshot(region: str = "full") -> dict: """Capture a screenshot and return it so the agent can see the screen.""" try: from PIL import ImageGrab, Image img = ImageGrab.grab() # Resize to keep under 5 MB limit img.thumbnail((1280, 800), Image.LANCZOS) buf = io.BytesIO() img.save(buf, format="PNG", optimize=True) png_bytes = buf.getvalue() return multimodal_content( text_part(f"Screenshot ({region}):"), image_part(png_bytes), ) except Exception as e: return {"error": str(e)}agent = Agent( instructions="You are a UI assistant. Screenshot the screen and describe the UI.", tools=[screenshot], llm="gpt-4o",)agent.start("What application is currently open?")
MCP servers often return image blocks with a mimeType key. Return the block as-is — the runtime normalises mimeType → mime automatically.
from praisonaiagents import Agent, tool@tooldef mcp_screenshot() -> dict: """Return an MCP-style image content block directly.""" import base64 # MCP servers return blocks like this: mcp_block = { "type": "image", "data": base64.b64encode(b"...png bytes...").decode(), "mimeType": "image/png", # note: mimeType, not mime } return mcp_block # runtime normalises it — no extra wrapping neededagent = Agent( instructions="Use the MCP screenshot tool and describe what you see.", tools=[mcp_screenshot], llm="gpt-4o",)agent.start("Take a screenshot using MCP and describe the screen.")
The runtime enforces a 5 MB cap per image part (MULTIMODAL_IMAGE_BYTE_LIMIT = 5_000_000). Images over this limit are silently dropped with a warning — the model only sees the text fallback.Resize before returning:
from PIL import Imageimport iodef resize_to_safe(img: Image.Image, max_bytes: int = 4_000_000) -> bytes: """Resize image until it fits under the byte limit.""" quality = 90 scale = 1.0 while True: buf = io.BytesIO() w, h = int(img.width * scale), int(img.height * scale) resized = img.resize((w, h), Image.LANCZOS) resized.save(buf, "PNG", optimize=True) data = buf.getvalue() if len(data) <= max_bytes or scale < 0.2: return data scale *= 0.8
Use a vision-capable LLM
Image parts are only perceived by models that support vision. Use:
gpt-4o / gpt-4o-mini (OpenAI)
claude-3-5-sonnet-20241022 or any Claude 3+ model (Anthropic)
gemini-1.5-pro / gemini-1.5-flash (Google)
Plain text models only see the text fallback string — they do not raise an error.
A caption improves grounding and is the only signal a non-vision model gets. Always pair image_part with text_part:
return multimodal_content( text_part("Screenshot of the dashboard at 14:32:"), # ← always include image_part(png_bytes),)
This also helps when the image is dropped due to the 5 MB cap — the agent still gets useful context.
Files aren't pixels
file_part is currently surfaced to the model as a text annotation [file: name (mime)]. Most LLM providers cannot ingest arbitrary binary files inline.For PDFs or other documents you want the model to see, rasterise to images first:
from pdf2image import convert_from_bytesimport iodef pdf_to_image(pdf_bytes: bytes) -> bytes: """Convert first page of PDF to PNG.""" images = convert_from_bytes(pdf_bytes, first_page=1, last_page=1) buf = io.BytesIO() images[0].save(buf, "PNG") return buf.getvalue()return multimodal_content( text_part("First page of the PDF document:"), image_part(pdf_to_image(pdf_bytes)), # image, not file_part)
Text parts returned by external or untrusted tools are automatically wrapped in a prompt-injection fence — the same protection used for text-only tool output. You don’t need to do anything, but know that if you mark a tool as external, its text parts will be fenced before the model sees them.