Multi-Modal & Browser Agents Basics
9 examples to get you started with Multi-Modal & Browser Agents - 6 basic and 3 intermediate.
Search across all documentation pages
9 examples to get you started with Multi-Modal & Browser Agents - 6 basic and 3 intermediate.
These sketches are conceptual and provider-agnostic. They show control flow and message shape, not a full production SDK.
pip install playwright then browser install - verify at build).Turn a PNG on disk into a data URL the model can read.
import base64
from pathlib import Path
def image_data_url(path: str, mime: str = "image/png") -> str:
raw = Path(path).read_bytes()
b64 = base64.b64encode(raw).decode("ascii")
return f"data:{mime};base64,{b64}"
url = image_data_url("fixtures/checkout-error.png")Related: How Vision Models Extend an Agent Beyond Text - why images are observations
One user message with text + image parts; no tools yet.
def describe_screenshot(client, model: str, data_url: str) -> str:
resp = client.chat.completions.create(
model=model, # must support vision - verify at build
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Describe the UI and any error in 3 bullets."},
{"type": "image_url", "image_url": {"url": data_url}},
],
}],
max_tokens=300,
)
return resp.choices[0].message.contentmax_tokens so a verbose model does not essay every screenshot.Related: Combining Vision and Tool Use for Document-Processing Agents - structured extract next
Vision is more useful when the host parses fields, not free prose.
import json
from pydantic import BaseModel, ValidationError
class UiFinding(BaseModel):
page_type: str
has_error: bool
error_text: str | None
primary_cta: str | None
def extract_ui(client, model: str, data_url: str) -> UiFinding:
prompt = (
"Return JSON only with keys: page_type, has_error, "
"error_text, primary_cta. No markdown."
)
text = describe_with_prompt(client, model, data_url, prompt)
return UiFinding.model_validate(json.loads(text))ValidationError, retry once with the error message and the same image.page_type once your domain is known.Related: Tool Use and Function Calling Basics - tools after extract
Make vision an observation channel inside a tool loop.
def tool_screenshot(page) -> dict:
png = page.screenshot(type="png", full_page=False)
b64 = base64.b64encode(png).decode("ascii")
return {
"ok": True,
"note": f"viewport screenshot, url={page.url}",
"image_data_url": f"data:image/png;base64,{b64}",
}full_page=False is the default for cost control.image_data_url as an image part on the next model call.Related: Building a Browser-Controlling Agent with Playwright - full tool set
No clicks yet; prove the observe path works.
def run_observe_only(goal: str, page, client, model: str, max_turns: int = 3):
messages = [{"role": "system", "content": "You inspect pages. Call screenshot when needed."}]
messages.append({"role": "user", "content": goal})
for _ in range(max_turns):
out = client.chat.completions.create(model=model, messages=messages, tools=[screenshot_tool_schema])
msg = out.choices[0].message
if not msg.tool_calls:
return msg.content
for call in msg.tool_calls:
obs = tool_screenshot(page)
messages.append(tool_message_with_image(call.id, obs))
return {"status": "max_turns"}tool_message_with_image so image parts survive into history.Related: Stopping Conditions: How an Agent Knows When It's Done - host stop rules
Browser agents need a real page before vision helps.
from playwright.sync_api import sync_playwright
def open_page(url: str):
p = sync_playwright().start()
browser = p.chromium.launch(headless=True)
context = browser.new_context(viewport={"width": 1280, "height": 720})
page = context.new_page()
page.goto(url, wait_until="domcontentloaded", timeout=30_000)
return p, browser, pagedomcontentloaded over networkidle on modern SPAs (idle may never come).finally block in real code.Related: Handling Dynamic Pages, Popups, and Login Flows in Browser Agents - waits and popups
Do not spend vision tokens when the DOM already answers.
def observe(page, client, model: str, question: str) -> str:
try:
snap = page.locator("body").aria_snapshot() # API name may vary - verify
if len(snap) > 50:
return client_text_only(client, model, question, snap)
except Exception:
pass
obs = tool_screenshot(page)
return describe_with_image(client, model, question, obs["image_data_url"])Related: Vision-Grounded Actions: Clicking What the Model Actually Sees - when pixels must drive action
Old images confuse the model and burn context.
def prune_images(messages: list, keep_last: int = 1) -> list:
images = [i for i, m in enumerate(messages) if message_has_image(m)]
drop = set(images[:-keep_last]) if keep_last else set(images)
pruned = []
for i, m in enumerate(messages):
if i in drop:
pruned.append(replace_image_with_text(m, "[prior screenshot omitted]"))
else:
pruned.append(m)
return prunedRelated: Multi-Modal & Browser Agents Best Practices - cost and context rules
Prove observe reliability before granting act privileges.
checklist = {
"vision_model": "returns UiFinding on 5 fixtures",
"screenshot_tool": "image + url note each call",
"viewport": "fixed 1280x720",
"max_turns": 5,
"cleanup": "browser.close in finally",
"no_secrets_in_shots": True,
}
assert all(checklist.values())Related: Building a Browser-Controlling Agent with Playwright - navigate, click, read | Managed Browser Agent Platforms vs Self-Hosted Playwright - host choice
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.
Reviewed by Chris St. John·Last updated Jul 16, 2026