Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.agno.com/llms.txt

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

The shape AP teams need: a header (vendor, totals, dates) and a list of line items. The schema is the contract with downstream systems.
from typing import List, Optional

from agno.agent import Agent
from agno.media import File
from agno.models.openai import OpenAIResponses
from pydantic import BaseModel, Field


class LineItem(BaseModel):
    description: str = Field(..., description="Line description as printed")
    quantity: Optional[float] = None
    unit_price: Optional[float] = None
    amount: Optional[float] = Field(None, description="Line total in invoice currency")


class Invoice(BaseModel):
    invoice_number: Optional[str] = None
    vendor: Optional[str] = None
    vendor_tax_id: Optional[str] = None
    bill_to: Optional[str] = None
    invoice_date: Optional[str] = Field(None, description="ISO 8601 if possible")
    due_date: Optional[str] = None
    currency: Optional[str] = Field(None, description="ISO 4217, e.g. USD, EUR")
    subtotal: Optional[float] = None
    tax: Optional[float] = None
    total: Optional[float] = None
    lines: List[LineItem] = Field(default_factory=list)


agent = Agent(
    model=OpenAIResponses(id="gpt-5.5"),
    instructions=(
        "Extract every field and every line item from the attached invoice. "
        "Numbers stay as numbers. Use ISO 8601 for dates when the format is "
        "unambiguous. Null for missing fields. Do not invent line items."
    ),
    output_schema=Invoice,
)

invoice = agent.run(
    "Extract this invoice.",
    files=[File(url="https://example.com/invoice-1042.pdf")],
).content
# Invoice(invoice_number='1042', vendor='Acme Corp', invoice_date='2026-04-12',
#         total=1296.0, currency='USD', lines=[LineItem(description='Pro plan',
#         quantity=12, unit_price=99.0, amount=1188.0), LineItem(...)])
The hard part is the missing-field discipline. A hallucinated total corrupts the AP ledger. Two lines in the instructions (“use exactly what the document shows”, “null if missing”) cut the hallucination rate substantially on noisy scans.

Persist the row

The agent’s job ends at a validated Invoice. The next step is a normal INSERT. Pydantic .model_dump() gives you a dict you can hand to any driver.
from sqlalchemy import create_engine, text

engine = create_engine("postgresql+psycopg://ai:ai@localhost:5532/ap")

with engine.begin() as conn:
    invoice_id = conn.execute(
        text(
            "INSERT INTO invoices (invoice_number, vendor, invoice_date, "
            "due_date, currency, subtotal, tax, total) "
            "VALUES (:invoice_number, :vendor, :invoice_date, :due_date, "
            ":currency, :subtotal, :tax, :total) RETURNING id"
        ),
        invoice.model_dump(exclude={"lines"}),
    ).scalar_one()

    for line in invoice.lines:
        conn.execute(
            text(
                "INSERT INTO invoice_lines (invoice_id, description, "
                "quantity, unit_price, amount) "
                "VALUES (:invoice_id, :description, :quantity, "
                ":unit_price, :amount)"
            ),
            {"invoice_id": invoice_id, **line.model_dump()},
        )
Two writes per invoice. The schema decides the table layout; the agent decides the values.

Receipts

Receipts are invoices with a thinner header. Drop due_date, vendor_tax_id, and bill_to. Keep the same line-item shape. The same agent and the same instructions work; only output_schema changes.
class Receipt(BaseModel):
    merchant: Optional[str] = None
    purchase_date: Optional[str] = None
    currency: Optional[str] = None
    total: Optional[float] = None
    lines: List[LineItem] = Field(default_factory=list)
For phone-camera receipts (skewed, low light), the input becomes an image rather than a PDF. See multimodal inputs for the input argument.

Confidence on noisy scans

Production AP sees faxed copies, partial scans, and mixed-language invoices. When you need a flag for “send this to a human”, wrap each value in a confidence carrier. The pattern is identical to the data labeling pattern and feeds the routing logic in human routing and eval.

Next steps

TaskGuide
Process a folder of invoicesBatch and durability
Route low-confidence invoices to AP reviewHuman routing and eval
Extract contract clauses with the same primitiveContracts

Developer Resources