Extract structured data¶
extract() fills a Pydantic schema you define from a document — and tells
you, field by field, where each value came from and whether it checks
out. Define the shape; the extraction cites its sources.
One instance from a document¶
mode="one" (the default) fills a single instance from the whole
document — headline facts, a contract's key terms, a form's fields:
from pydantic import BaseModel
from ingestlib.operations import extract
class Financials(BaseModel):
company: str
fiscal_year: int
total_net_sales: float
report = extract("finance-10k.pdf", schema=Financials)
item = report.items[0]
print(item.value) # Financials(company='Apple Inc.', ...)
print(item.citation) # p.1
print(item.fields["total_net_sales"].grounded) # True
item.value is a real instance of your class — validated, typed,
IDE-completable. Field descriptions are part of the prompt, so
Field(description=...) is the place to say what a value means:
from pydantic import Field
class Financials(BaseModel):
total_net_sales: float = Field(
description="Total net sales in millions of dollars, most recent fiscal year"
)
Every instance in a batch¶
mode="many" finds all instances across the pages — all the receipts in
a scanned expense bundle, every line item, each policy in a filing:
from ingestlib.operations import parse
class Receipt(BaseModel):
merchant: str
total: float
currency: str
result = parse("expenses.pdf") # a 16-page scan of receipts
report = extract(result, schema=Receipt, mode="many")
for item in report.items:
print(item.value.merchant, item.value.total, item.citation)
# BART 20.0 p.10
# Hilton 214.6 p.2
# …
Long documents are read in overlapping page windows, and items that appear in two windows are deduplicated by value — each real-world instance comes back once.
Provenance on every field¶
Each extracted item carries a FieldValue per schema field:
total = item.fields["total"]
total.pages # [10] — where it was read
total.region_ids # {10: [3]} — the parse regions on that page
total.grounded # True — the value appears in the cited text
total.confidence # 0.9 — honest, see below
The citations are verified, not trusted: a citation naming a region
that doesn't exist in the parse is dropped (and logged), and every value
is checked against its cited source text — numeric forms are normalized,
so 383285.0 grounds against a printed 383,285.
Confidence follows from that verification:
- A field with no valid citation is capped at 0.3
- A field whose value isn't found in the cited text is capped at 0.5
and flagged
grounded=False - A cited, grounded field keeps the model's own confidence
Treat grounded=False as "verify before use" — a reworded or derived
value can be correct without matching the source text verbatim. See
Limits for the honest edges.
Parse first, or not¶
Extract accepts the same inputs as classify and split, with the same trade-off:
- A
ParseResult— full OCR quality and region-level citations. The right choice for scans (their text layers are garbage or missing) and whenever you want bounding-box provenance for a UI. - A raw path — reads the native text layer directly, page-level
citations (
region_idsstays empty), no OCR server involved. The cheap path for born-digital documents.
report = extract(parse("scan.pdf"), schema=Receipt, mode="many") # regions
report = extract("born-digital.pdf", schema=Financials) # pages only
A document with no readable text on either path raises the same clear
error as the other operations: run parse() first.
Target pages¶
Know where the data lives? Same page targeting as classify:
report = extract("filing.pdf", schema=Financials, target_pages="30-45")
Free-text guidance rides along with instructions:
report = extract(result, schema=Receipt, mode="many",
instructions="Only meal and transport receipts; skip lodging.")
Documents are capped at 100 pages per call — use target_pages to reach
into anything larger.
Persist and reload¶
Extraction results store beside the document's other artifacts, keyed by schema name:
from ingestlib.storage import artifacts
doc_id = artifacts.save_parse(result)
artifacts.save_extract(doc_id, report)
loaded = artifacts.load_extract(doc_id, Receipt) # values revalidate into Receipt
Async¶
Inside an event loop, use aextract — the sync form will tell you so if
you forget:
from ingestlib.operations import aextract
report = await aextract(result, schema=Receipt, mode="many")
Next: Your categories & sections — constrain classify and split to your domain.