How to Ingest a PDF into OpenAI Agents SDK with EnConvert

Use EnConvert to ingest a PDF into OpenAI Agents SDK as heading-aware chunks with section metadata your agent can retrieve and reason over in one API call

Get API key

A developer building an OpenAI Agents SDK agent that reads PDFs needs heading-aware chunks with section metadata the agent can retrieve and reason over, not flat extracted text that loses document structure. Standard extraction libraries like PyPDF2 or pdfplumber return text page by page, producing one undifferentiated block per page with no heading hierarchy, no section boundaries, and no chunk metadata a retrieval layer can filter on. For an agent reading a technical manual, a legal contract, or a research paper, that flat page text produces passages the agent cannot locate within the document or filter by section. EnConvert’s Ingest endpoint takes the PDF upload and returns heading-aware JSONL chunks, each carrying its full headings_path breadcrumb, section name, and chunk index in metadata, so RAG-ready PDF chunks are ready for the agent to retrieve and reason over in one call.

Example

python
import json
import os
import re
import time

import requests
from agents import Agent, Runner, function_tool

# The Agents SDK reads OPENAI_API_KEY from the environment.
HEADERS = {"X-API-Key": os.environ["ENCONVERT_API_KEY"]}

# Step 1: Upload the PDF (multipart field name is "files")
with open("/path/to/your/technical-manual.pdf", "rb") as f:
    upload = requests.post(
        "https://api.enconvert.com/v2/ingest/files",
        headers=HEADERS,
        files={"files": ("technical-manual.pdf", f, "application/pdf")},
        timeout=60,
    )
upload.raise_for_status()
job_id = upload.json()["job_id"]

# Step 2: Poll until the job completes
while True:
    poll = requests.get(
        f"https://api.enconvert.com/v2/ingest/{job_id}",
        headers=HEADERS,
        timeout=60,
    )
    poll.raise_for_status()
    job = poll.json()
    if job["status"] == "completed":
        output_url = job["output_url"]
        break
    if job["status"] in ("failed", "canceled"):
        raise RuntimeError(
            f"Ingest job {job_id} {job['status']}: {job.get('error_message')}"
        )
    time.sleep(2)

# Step 3: Download the JSONL output (signed URL, plain GET, no API key)
jsonl = requests.get(output_url, timeout=60)
jsonl.raise_for_status()

# One chunk per line. Split on "\n" only: the text can hold other line separators.
chunks = [
    json.loads(line)
    for line in jsonl.content.decode("utf-8").split("\n")
    if line.strip()
]
print(f"Loaded {len(chunks)} chunks ready for retrieval")


# Step 4: Expose the chunks to the agent as a function tool
@function_tool
def search_manual(query: str, section: str | None = None) -> str:
    """Search the ingested PDF and return relevant passages with their
    section location. Optionally filter by a heading breadcrumb substring.

    Args:
        query: Keywords describing the information needed.
        section: Optional substring of the heading breadcrumb to filter by.
    """
    terms = set(re.findall(r"\w+", query.lower()))
    scored = []

    for chunk in chunks:
        metadata = chunk["metadata"]
        # headings_path is a list of strings (may be empty), e.g. ["Install", "Torque"]
        breadcrumb = " > ".join(metadata["headings_path"])

        if section and section.lower() not in breadcrumb.lower():
            continue

        words = set(re.findall(r"\w+", f"{breadcrumb} {chunk['content']}".lower()))
        score = len(terms & words)
        if score:
            scored.append((score, breadcrumb, metadata["chunk_index"], chunk["content"]))

    if not scored:
        return "No matching passages found."

    scored.sort(key=lambda item: item[0], reverse=True)
    return "\n\n".join(
        f"[{breadcrumb or 'no heading'} | chunk {chunk_index}]\n{content}"
        for _, breadcrumb, chunk_index, content in scored[:3]
    )


# Step 5: Run the agent
agent = Agent(
    name="Manual Assistant",
    instructions=(
        "Answer questions about the technical manual. Use search_manual to find "
        "passages, cite the section each answer comes from, and say so if the "
        "manual does not cover the question."
    ),
    tools=[search_manual],
)

# Replace with a question about your own document
result = Runner.run_sync(agent, "What are the safety requirements before installation?")
print(result.final_output)

What you get back

Ingest returns the PDF as heading-aware JSONL chunks, where each chunk carries its full headings_path breadcrumb, section name, and chunk index, so the agent knows exactly where in the document a retrieved result came from. Extracting text with PyPDF2 or pdfplumber gives one flat block per page, with no heading hierarchy, no section boundaries, and no metadata the agent can use to locate or filter results.

Get free API key — 500 ops per month, no card required.