> ## Documentation Index
> Fetch the complete documentation index at: https://langwatch.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Capturing Documents

> LangWatch captures the PDF files and other documents your Python application sends to a model and shows them on the trace.

Send the file the way your model provider expects and keep the call inside a trace. There is no upload step and no extra API call: LangWatch reads the document out of the messages at ingestion, stores it, and leaves a reference in its place.

## Send the document with the model call

The examples below all call `langwatch.setup()`. The OpenAI and Anthropic ones run inside a `@langwatch.trace()` entry point; the Google ADK one lets the instrumentor make the trace. Read [Integration Guide](/docs/integration/python/guide) first if you have not set that up yet.

### OpenAI

Put the document in a `file` part, as `file_data`. That is the shape Chat Completions accepts for PDF files.

```python theme={null}
import base64
import langwatch
from openai import OpenAI

langwatch.setup()


def data_url(path: str, media_type: str) -> str:
    encoded = base64.b64encode(open(path, "rb").read()).decode("utf-8")
    return f"data:{media_type};base64,{encoded}"


@langwatch.trace(name="read invoice")
def read_invoice(prompt: str, path: str) -> str:
    client = OpenAI()
    langwatch.get_current_trace().autotrack_openai_calls(client)

    completion = client.chat.completions.create(
        model="gpt-5-mini",
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": prompt},
                    {
                        "type": "file",
                        "file": {
                            "filename": "invoice.pdf",
                            "file_data": data_url(path, "application/pdf"),
                        },
                    },
                ],
            }
        ],
    )
    return completion.choices[0].message.content or ""


read_invoice("What is the invoice number and the total due?", "invoice.pdf")
```

`file_data` accepts a base64 data URL, as above, or raw base64. With raw base64 the media type comes from the `filename` extension, so always send a filename.

### Anthropic

Put the document in a `document` content block with a base64 source.

```python theme={null}
import base64
import langwatch
from anthropic import Anthropic
from openinference.instrumentation.anthropic import AnthropicInstrumentor

langwatch.setup(instrumentors=[AnthropicInstrumentor()])


@langwatch.trace(name="read invoice")
def read_invoice(prompt: str, path: str) -> str:
    client = Anthropic()
    encoded = base64.b64encode(open(path, "rb").read()).decode("utf-8")

    message = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=1024,
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": prompt},
                    {
                        "type": "document",
                        "source": {
                            "type": "base64",
                            "media_type": "application/pdf",
                            "data": encoded,
                        },
                    },
                ],
            }
        ],
    )
    return message.content[0].text


read_invoice("What is the invoice number and the total due?", "invoice.pdf")
```

### Google Gemini and Vertex

Put the document in an inline data part with the PDF media type. The rest of the agent setup does not change.

```python theme={null}
import asyncio

import langwatch
from google.adk import Agent, Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
from openinference.instrumentation.google_adk import GoogleADKInstrumentor

langwatch.setup(instrumentors=[GoogleADKInstrumentor()])

APP_NAME = "document-reader"
USER_ID = "caller-1"
SESSION_ID = "session-1"


def ask(prompt: str, path: str) -> str:
    agent = Agent(
        name="document_agent",
        model="gemini-2.5-flash",
        instruction="Answer the user's question about the attached document directly.",
    )
    session_service = InMemorySessionService()
    asyncio.run(
        session_service.create_session(
            app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID
        )
    )
    runner = Runner(agent=agent, app_name=APP_NAME, session_service=session_service)

    message = types.Content(
        role="user",
        parts=[
            types.Part(text=prompt),
            types.Part.from_bytes(
                data=open(path, "rb").read(), mime_type="application/pdf"
            ),
        ],
    )

    for event in runner.run(
        user_id=USER_ID, session_id=SESSION_ID, new_message=message
    ):
        if event.is_final_response():
            return event.content.parts[0].text
    return ""


ask("What is the invoice number and the total due?", "invoice.pdf")
```

<Note>
  The instrumentor makes the trace for this call, so no `@langwatch.trace()`
  wrapper is needed. If you add one to attach metadata, switch the run to
  `Runner.run_async`: the synchronous `Runner.run` starts its own thread, which
  begins with an empty OpenTelemetry context, and the agent spans open a second
  trace. See the [Google ADK integration](/docs/integration/python/integrations/google-ai).
</Note>

## Document shapes LangWatch understands

Send any of these and the attachment is captured.

| Source                   | Shape                                                                                                |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| OpenAI                   | `{"type": "file", "file": {"filename": "x.pdf", "file_data": "data:application/pdf;base64,..."}}`    |
| Anthropic                | `{"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": "..."}}` |
| Anthropic                | `{"type": "document", "source": {"type": "url", "url": "https://..."}}`                              |
| Google Gemini and Vertex | `{"inline_data": {"mime_type": "application/pdf", "data": "..."}}`                                   |
| AG-UI                    | `{"type": "document", "source": {"type": "data", "value": "...", "mimeType": "application/pdf"}}`    |

A part that names only a provider-hosted file, such as an OpenAI `file_id` with no `file_data`, holds no bytes. LangWatch passes it through unchanged and stores no attachment.

## What happens to the bytes

LangWatch moves the document out of the span at ingestion. The bytes go into content-addressed object storage and the message part becomes a `/api/files/<projectId>/<id>` reference, around 60 characters where the base64 was.

Storage is addressed by SHA-256, so identical bytes are stored once. For example, the same contract sent on ten traces costs one stored object.

On read, LangWatch serves PDF files with their original media type, along with images, audio and video. Every other document type, such as CSV, JSON, Markdown and plain text, is served as a download.

## Where the document appears

The trace list draws a paperclip next to the input preview, so you can spot a run with a file without opening it.

Open the trace and the Summary tab shows an attachment chip, named after the file, or after its media type when the message sends no filename. Click it to open the file. The Conversation tab shows the same chip in the message it belongs to, and the Trace tab shows the full message payload.

<Frame caption="The trace drawer Summary tab, with an application/pdf attachment chip below the input text and the model answer reading the invoice number and total from the file">
  <img src="https://mintcdn.com/langwatch/0_fIM0yG2oBnnIbz/images/integration/multimodal/trace-drawer-pdf.png?fit=max&auto=format&n=0_fIM0yG2oBnnIbz&q=85&s=50e69631febf582807d4733aafc10a4b" alt="LangWatch trace drawer showing a PDF attachment chip below the input question" width="1600" height="1000" data-path="images/integration/multimodal/trace-drawer-pdf.png" />
</Frame>

## Related

* [Capturing Images](/docs/integration/python/tutorials/capturing-images), for pictures and screenshots
* [Capturing Audio and Voice](/docs/integration/python/tutorials/capturing-audio), for recordings and voice agents
* [Multimodal Evaluation](/docs/evaluations/experiments/multimodal-evaluation), to score document parsing with a vision model
