LlamaIndex Screenshot Tool and Image Reader

Capture web pages in LlamaIndex with a tested FunctionTool and a ScreenshotReader that returns ImageDocuments for multimodal LLMs. Code and limits.

By Simon Chiu · Updated

There are two ways to bring web page screenshots into LlamaIndex, and this page gives you a tested version of each:

  1. capture_page, a FunctionTool. An agent calls it with a URL and gets back a public image URL and the image size. Use it when the agent needs a capture as evidence or as input for a later step.
  2. ScreenshotReader, a reader. You pass URLs and get ImageDocument objects that hold the image bytes. Use it to feed pages to a multimodal LLM or a multimodal index.

Both call the RenderScreenshot API through our Python SDK, so no browser runs in your Python process. This page is part of our guide to giving AI agents web vision.

Last tested: 2026-10-15 with Python 3.12, llama-index-core 0.14.25, renderscreenshot 1.0.0 (Python SDK from GitHub). The tool schema, tool calls, reader output and chat-message conversion ran against a stubbed API client; one real API call with an invalid key checked the error path. The agent example was constructed with LlamaIndex's MockFunctionCallingLLM but not run against a real LLM.

What you can build

  • Visual QA over a sitemap. Read every page of a staging site as an image and ask a multimodal model which pages show a broken layout.
  • A research agent with receipts. The agent cites a screenshot URL for every claim it makes about a web page.
  • Dashboard digests. Capture internal status pages (with cookies set in your code) and summarize them each morning.

Prerequisites

pip install "llama-index-core==0.14.25" "git+https://github.com/Render-Screenshot/rs-python.git"
export RS_API_KEY=rs_live_...

The Python SDK installs from GitHub until it is on PyPI (Python SDK docs). Get a key by creating a free account.

The module

Save as rs_llamaindex.py. This is the file we tested.

"""LlamaIndex helpers for the RenderScreenshot API: a FunctionTool and an image reader."""

import base64
import os
from typing import Annotated, List, Optional

from llama_index.core.readers.base import BaseReader
from llama_index.core.schema import ImageDocument
from llama_index.core.tools import FunctionTool
from renderscreenshot import Client, RenderScreenshotError, TakeOptions

_client: Optional[Client] = None


def get_client() -> Client:
    global _client
    if _client is None:
        _client = Client(os.environ['RS_API_KEY'], timeout=60.0)
    return _client


def capture_page(
    url: Annotated[str, 'Public http(s) URL to capture'],
    full_page: Annotated[bool, 'Capture the whole scrollable page'] = False,
    dark_mode: Annotated[bool, 'Render with prefers-color-scheme: dark'] = False,
) -> str:
    """Capture a web page and return a public image URL with its pixel size."""
    options = TakeOptions.url(url).preset('full_page' if full_page else 'desktop_hd').format('webp')
    if dark_mode:
        options = options.dark_mode()
    try:
        shot = get_client().take_json(options)
    except RenderScreenshotError as e:
        return f'Capture failed ({e.http_status}): {e.message}'
    return f"{shot.get('url')} ({shot.get('width')}x{shot.get('height')})"


screenshot_tool = FunctionTool.from_defaults(
    fn=capture_page,
    name='capture_page',
    description=(
        'Capture a screenshot of a public web page. Returns an image URL and its size. '
        'Use it to check layout or visual state, not to read text.'
    ),
)


class ScreenshotReader(BaseReader):
    """Load web pages as ImageDocuments for multimodal indexes and LLMs."""

    def __init__(self, preset: str = 'desktop_hd', client: Optional[Client] = None):
        self.preset = preset
        self.client = client

    def load_data(self, urls: List[str]) -> List[ImageDocument]:
        client = self.client or get_client()
        docs = []
        for url in urls:
            # take() returns the image bytes. Passing them in avoids a second download:
            # ImageDocument(image_url=...) fetches and verifies the URL when it is built.
            data = client.take(TakeOptions.url(url).preset(self.preset).format('webp'))
            docs.append(ImageDocument(
                image=base64.b64encode(data).decode('ascii'),
                image_mimetype='image/webp',
                metadata={'source_url': url, 'preset': self.preset},
            ))
        return docs

Two things we learned while testing it:

  • Do not add from __future__ import annotations. With it, FunctionTool.from_defaults could not resolve the Annotated hints and raised PydanticUserError: capture_page is not fully defined when it built the schema. Without it, the parameter descriptions flow into the tool schema the model sees.
  • ImageDocument(image_url=...) downloads the image. Our first reader passed the CDN URL. LlamaIndex 0.14.25 fetched and verified it during construction and raised The specified URL is not an accessible image for a URL that was not reachable. The bytes-based version above makes one request per page.

Use the tool in an agent

import asyncio

from llama_index.core.agent.workflow import FunctionAgent
from llama_index.llms.openai import OpenAI  # pip install llama-index-llms-openai

from rs_llamaindex import screenshot_tool

agent = FunctionAgent(
    tools=[screenshot_tool],
    llm=OpenAI(model='gpt-4o-mini'),
    system_prompt='You review web pages. Cite the screenshot URL for every finding.',
)


async def main():
    print(await agent.run('Capture https://example.com and tell me the image size.'))

asyncio.run(main())

This is the schema LlamaIndex built from the function, as printed in our test:

url        string   "Public http(s) URL to capture"
full_page  boolean  default False  "Capture the whole scrollable page"
dark_mode  boolean  default False  "Render with prefers-color-scheme: dark"

Send pages to a multimodal model

The reader's documents convert straight into an image block for a chat message:

from llama_index.core.llms import ChatMessage, ImageBlock, TextBlock

from rs_llamaindex import ScreenshotReader

docs = ScreenshotReader(preset='desktop_hd').load_data(['https://example.com'])
msg = ChatMessage(role='user', blocks=[
    ImageBlock(image=docs[0].image_resource.data, image_mimetype='image/webp'),
    TextBlock(text='Is the main call to action visible above the fold?'),
])
# response = your_multimodal_llm.chat([msg])

In our test, two stubbed pages produced two ImageDocument objects with image/webp data and source_url metadata, and the message held an ImageBlock and a TextBlock. A 1920x1080 capture costs a fixed number of visual tokens no matter what the page shows; our screenshot vs DOM vs accessibility tree study gives the numbers.

Parameter mapping

LlamaIndex input SDK call API field Notes
url TakeOptions.url(url) url Public http(s) URL
full_page=True .preset('full_page') preset Scrolls to trigger lazy loading (see presets)
full_page=False .preset('desktop_hd') preset 1920x1080
dark_mode=True .dark_mode() browser.dark_mode Only affects pages that use prefers-color-scheme
Reader preset .preset(...) preset Any built-in preset, such as mobile
(fixed) .format('webp') output.format Smaller than PNG

Errors, retries and limits

  • Tool errors return text. A real call with an invalid key returned Capture failed (401): {...} to the agent instead of raising, so the agent can report it.
  • Reader errors raise. A batch load should fail loudly. Wrap load_data in your own retry if you load many URLs; retry on HTTP 408, 429 and 5xx only. In SDK 1.0.0 we saw a 401 reported with retryable=True, so base the decision on e.http_status.
  • Large batches: for more than a handful of URLs, use the API's batch endpoint through client.batch(...) and build documents from the results.
  • Cost: each capture uses credits, reported in the X-Credits-Used header. Cache hits on repeated URLs are faster; see caching.

Troubleshooting

Symptom Cause Fix
PydanticUserError: capture_page is not fully defined from __future__ import annotations in the tool module Remove it, or define the schema with fn_schema
ValueError: The specified URL is not an accessible image ImageDocument(image_url=...) fetched an unreachable URL Pass bytes with image= as in the reader above
KeyError: 'RS_API_KEY' Key not set export RS_API_KEY=rs_live_...
Model ignores the screenshot The tool returns a URL, not pixels Use the reader and an ImageBlock for visual questions
Tall full-page images look blurry to the model The model scales large images down Use desktop_hd slices or element captures
ModuleNotFoundError: llama_index.llms.openai LLM integrations are separate packages pip install llama-index-llms-openai (or your provider's package)

Frequently asked questions

How do I load a web page as an image in LlamaIndex?
Use a reader that captures the page and returns an ImageDocument. The ScreenshotReader on this page calls the RenderScreenshot API and puts the image bytes in the document, so a multimodal LLM or index can use them.
Why not pass the screenshot URL to ImageDocument(image_url=...)?
In llama-index-core 0.14.25, ImageDocument downloads and verifies an image_url when the document is constructed. That is a second network request per page, and it fails if the URL is not reachable yet. Passing the bytes avoids both problems.
Does the FunctionTool return the image to the model?
No. It returns a short text result with the image URL and size, which keeps agent context small. Use the reader when a model needs the pixels.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.