Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/openai/openai-cookbook/llms.txt

Use this file to discover all available pages before exploring further.

Vast amounts of enterprise data lives in formats that are difficult to work with programmatically: PDFs, scanned documents, free-form text fields, email threads, and hand-written notes. Traditional approaches — regular expressions, rule-based parsers, or OCR — break down when document layouts vary, languages mix, or the data relationships require reasoning to interpret. GPT models handle this naturally. By combining the model’s language understanding with Structured Outputs, you can reliably extract typed, validated data from almost any text source and feed it directly into downstream systems.

Extracting structured data with Structured Outputs

Structured Outputs guarantee that the model’s response conforms to a schema you define. You describe the shape of the data you want using a Pydantic model (Python) or a JSON Schema, pass it as the response_format, and the API returns a parsed object — no post-processing regex needed. The example below extracts invoice fields from a block of unstructured text:
from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

class Invoice(BaseModel):
    invoice_number: str
    vendor_name: str
    total_amount: float
    due_date: str
    line_items: list[str]

completion = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "Extract invoice data from the provided text."},
        {"role": "user", "content": "Invoice #1234 from Acme Corp. Due March 15. Total: $450.00. Items: Widget A x2, Widget B x1."}
    ],
    response_format=Invoice
)

invoice = completion.choices[0].message.parsed
print(invoice.invoice_number)   # "1234"
print(invoice.total_amount)     # 450.0
print(invoice.line_items)       # ["Widget A x2", "Widget B x1"]
client.beta.chat.completions.parse is a convenience wrapper that validates the response against your Pydantic model. If the model produces output that doesn’t match your schema, a ValidationError is raised rather than silently returning malformed data.
The system prompt matters. A focused instruction like "Extract invoice data from the provided text" gives the model a clear task. Avoid vague prompts like "Read this and help me" — specificity improves accuracy, especially when field names could be ambiguous.

Named entity recognition

Named Entity Recognition (NER) classifies spans of text into semantic categories: people, organizations, locations, dates, monetary amounts, and so on. GPT models perform NER without any fine-tuning — you describe the entity types you care about in the prompt, and the model finds and labels them.
from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

class Entity(BaseModel):
    text: str
    label: str  # e.g. "PERSON", "ORG", "DATE", "MONEY"
    start_char: int
    end_char: int

class NERResult(BaseModel):
    entities: list[Entity]

text = (
    "On January 9, 2007, Steve Jobs announced the iPhone at Macworld "
    "in San Francisco. Apple's stock rose 8% that day."
)

completion = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {
            "role": "system",
            "content": (
                "You are a named entity recognition system. Extract all named entities "
                "from the text. Label each entity as one of: PERSON, ORG, LOCATION, DATE, MONEY, PRODUCT. "
                "Include the character offsets (start_char, end_char) for each entity."
            ),
        },
        {"role": "user", "content": text},
    ],
    response_format=NERResult,
)

for entity in completion.choices[0].message.parsed.entities:
    print(f"{entity.text!r:30s}  {entity.label}")
NER is a foundation for text enrichment workflows: you can link extracted entities to a knowledge base (Wikipedia, an internal product catalog, a CRM) to produce annotated documents with structured metadata alongside the original prose.

Document parsing and data normalization

PDFs and scanned documents present two distinct challenges: getting the text out, and making sense of it. GPT-4o’s vision capabilities let you skip the OCR step entirely by encoding document pages as base64 images and sending them directly.
import base64
import fitz  # PyMuPDF
from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

def pdf_page_to_base64(pdf_path: str, page_number: int = 0) -> str:
    doc = fitz.open(pdf_path)
    page = doc[page_number]
    pix = page.get_pixmap(dpi=150)
    img_bytes = pix.tobytes("png")
    return base64.b64encode(img_bytes).decode("utf-8")

class HotelInvoice(BaseModel):
    guest_name: str
    check_in: str
    check_out: str
    room_rate_per_night: float
    total_charged: float
    currency: str

image_data = pdf_page_to_base64("invoice.pdf")

completion = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {
            "role": "system",
            "content": "Extract hotel invoice details from the document image.",
        },
        {
            "role": "user",
            "content": [
                {
                    "type": "image_url",
                    "image_url": {"url": f"data:image/png;base64,{image_data}"},
                }
            ],
        },
    ],
    response_format=HotelInvoice,
)

invoice = completion.choices[0].message.parsed
This approach handles multilingual documents, mixed layouts, and tables without any format-specific parsing logic. GPT-4o adapts to the document’s structure rather than requiring you to specify it in advance.
Set dpi=150 when converting PDF pages to images. Lower resolutions can make small text illegible to the model; higher resolutions increase token consumption without a proportional accuracy gain.

Handling long documents with chunking

GPT models have a finite context window, so documents longer than roughly 100,000 tokens need to be split into chunks and processed in passes. The pattern below uses tiktoken to split on token boundaries while preserving sentence integrity.
import tiktoken
from openai import OpenAI

client = OpenAI()
enc = tiktoken.encoding_for_model("gpt-4o")

def chunk_text(text: str, max_tokens: int = 4000) -> list[str]:
    tokens = enc.encode(text)
    chunks = []
    for i in range(0, len(tokens), max_tokens):
        chunk_tokens = tokens[i : i + max_tokens]
        chunks.append(enc.decode(chunk_tokens))
    return chunks

def extract_from_chunk(chunk: str, question: str) -> str:
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": (
                    "Extract the answer to the question from the provided text. "
                    "If the text does not contain a relevant answer, respond with 'NOT FOUND'."
                ),
            },
            {"role": "user", "content": f"Text:\n{chunk}\n\nQuestion: {question}"},
        ],
    )
    return response.choices[0].message.content

def extract_from_document(document: str, question: str) -> list[str]:
    chunks = chunk_text(document)
    results = [extract_from_chunk(chunk, question) for chunk in chunks]
    return [r for r in results if r.strip() != "NOT FOUND"]
After collecting per-chunk answers, run a final synthesis pass:
def synthesize_answers(answers: list[str], question: str) -> str:
    combined = "\n---\n".join(answers)
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": "Synthesize the following partial answers into a single, coherent response.",
            },
            {"role": "user", "content": f"Question: {question}\n\nPartial answers:\n{combined}"},
        ],
    )
    return response.choices[0].message.content
Chunking on token boundaries can split sentences mid-clause. If extraction accuracy is critical, add a small overlap (e.g. 200 tokens) between chunks so boundary sentences appear in both the preceding and following chunk.

Choosing the right approach

Structured Outputs + Pydantic

Best when the output schema is known in advance and you need validated, typed data. Eliminates post-processing and surfaces schema violations immediately.

Vision input (image/PDF)

Best for scanned documents, PDFs with complex layouts, or multilingual content where OCR would struggle. No preprocessing pipeline required.

NER with function calling

Best when you need entity spans with character offsets — for downstream annotation, linking to a knowledge base, or feeding into a search index.

Chunked extraction

Best for long documents that exceed the context window. Combine with a synthesis pass to produce a unified result from distributed extractions.

Next steps

Structured Outputs

Dive deeper into response schemas, required fields, and how to handle optional properties.

Batch processing

Run extraction pipelines at scale using the Batch API for 50% cost savings on large document sets.

Build docs developers (and LLMs) love