Skip to content

Read any document

Name the document type, then pull its fields.

A shared inbox gets receipts, leases and doctor's notes, and each kind needs different fields. This guide lets Sage2 name the document first, then extract only the fields that kind of document has.

Documents that fit none of your types go to other. Close calls come back as null. Both reach a person instead of the wrong form.

  • Choice
  • Extract
Try
  1. Fields read for each type
    • receipt: merchant, total, date
    • lease: tenant, monthly_rent, term, start_date
    • doctors_note: patient, doctor, restriction, restriction_length
  2. Name the kind of documentChoice
    • receiptunder 0.01%
    • lease99.99%
    • doctors_noteunder 0.01%
    • otherunder 0.01%
    4 ms
  3. Read the fields that kind of document hasExtract
    tenant
    Leo Brandt
    monthly_rent
    €1,250.00
    term
    P3Y (3 years)
    start_date
    2027-02-01
    63 ms
  4. Your codeFiled as a lease

    The application saves 4 fields: tenant, monthly rent, term, start date.

The type question took 4 ms. Your code then sent the lease field set to Extract: 63 ms.

Get the code

Set SAGE_API_KEY (get a free key), then run it. Python: pip install requests. TypeScript: Node 22+. Save as recipe.mts and run npx tsx recipe.mts.

A null answer is not an error. Sage2 isn't sure, and each step below says what to do with it. Limits, errors and retries: Limits and errors.

import os
import requests

def sage(path, body):
    r = requests.post(
        "https://sage.levanto.ai" + path,
        headers={"Authorization": f"Bearer {os.environ['SAGE_API_KEY']}"},
        json=body,
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

# From a PDF or a photo? Run OCR first and pass the text.
document = """RESIDENTIAL LEASE
Landlord: Ana Marsh. Tenant: Leo Brandt.
The premises at 8 Quay Lane are let for three (3) years from 1 February 2027.
Monthly rent: 1,250.00 EUR. Deposit: 2,500.00 EUR.
Pets are allowed with written consent."""

pick = sage("/decide", {
    "content": document,
    "question": {
        "id": "document_type",
        "kind": "choice",
        "instructions": "What kind of document is this?",
        "options": [
            {
                "option": "receipt",
                "description": "A proof of purchase from a shop or restaurant",
            },
            {"option": "lease", "description": "A contract to rent a property"},
            {
                "option": "doctors_note",
                "description": "A note from a doctor about a patient",
            },
            {"option": "other", "description": "Any other kind of document"},
        ],
    },
})["result"]
BAR = 0.9  # file a document on its own only at this probability or more
kind = pick["chosen"] if (pick["probability"] or 0) >= BAR else None

FIELDS = {
    "receipt": {
        "merchant": "the shop or restaurant",
        "total": {"type": "money"},
        "date": {"type": "date"},
    },
    "lease": {
        "tenant": "the person renting",
        "monthly_rent": {"type": "money"},
        "term": {"type": "duration"},
        "start_date": {"type": "date"},
    },
    "doctors_note": {
        "patient": "the person examined",
        "doctor": "the doctor",
        "restriction": "what the patient should not do",
        "restriction_length": {"type": "duration"},
    },
}

if kind is None or kind == "other":
    # Not one of your types, or Sage isn't sure enough: no field set fits.
    print("Send it to a person")
else:
    values = sage("/v1/extract", {"content": document, "extract": FIELDS[kind]})["values"]
    print(kind, values)

How it works

  1. OCR

    Turn the file into text

    Extraction reads text. For a PDF, a scan or a photo, run OCR first and pass the text on. Any OCR tool works, such as Tesseract or your cloud provider's OCR.

    To only name the type, Choice can read a photo directly (Images, beta).

    Runs with the helper from Get the code.

    # From a PDF or a photo? Run OCR first and pass the text.
    document = """RESIDENTIAL LEASE
    Landlord: Ana Marsh. Tenant: Leo Brandt.
    The premises at 8 Quay Lane are let for three (3) years from 1 February 2027.
    Monthly rent: 1,250.00 EUR. Deposit: 2,500.00 EUR.
    Pets are allowed with written consent."""
  2. Choice

    Name the document type

    Choice docs

    Ask a Choice question with one option per document type, and describe each option in a few words. Sage2 returns its pick in chosen, with a probability for every option.

    • other is the last option. It catches documents that fit none of your types.
    • null means the top two options are too close to call.

    Keep the pick only at or above your bar, here 0.9. Below it, the code treats the pick as null.

    Runs with the helper from Get the code.

    pick = sage("/decide", {
        "content": document,
        "question": {
            "id": "document_type",
            "kind": "choice",
            "instructions": "What kind of document is this?",
            "options": [
                {
                    "option": "receipt",
                    "description": "A proof of purchase from a shop or restaurant",
                },
                {"option": "lease", "description": "A contract to rent a property"},
                {
                    "option": "doctors_note",
                    "description": "A note from a doctor about a patient",
                },
                {"option": "other", "description": "Any other kind of document"},
            ],
        },
    })["result"]
    BAR = 0.9  # file a document on its own only at this probability or more
    kind = pick["chosen"] if (pick["probability"] or 0) >= BAR else None
    Example Sage2 output
    {
      "id": "document_type",
      "kind": "choice",
      "result": {
        "chosen": "lease",
        "probability": 0.9999,
        "probabilities": [
          {
            "option": "receipt",
            "probability": 0
          },
          {
            "option": "lease",
            "probability": 0.9999
          },
          {
            "option": "doctors_note",
            "probability": 0
          },
          {
            "option": "other",
            "probability": 0
          }
        ]
      },
      "meta": {
        "model": "levanto-sage-2",
        "latency_ms": 4.1,
        "question_count": 1,
        "usage": {
          "billed_input_tokens": 165,
          "image_count": 0,
          "image_tokens": 0,
          "input_tokens": 165,
          "output_tokens": 0
        }
      }
    }
  3. Extract

    Extract that type's fields

    Extract docs

    Keep one field set per type. The Choice answer picks the set you send to /v1/extract. other and null have no field set, so a person files those.

    money comes back as {amount, currency}, date as an ISO date and duration as an ISO 8601 duration. A three-year lease term is P3Y.

    Runs with the helper from Get the code.

    FIELDS = {
        "receipt": {
            "merchant": "the shop or restaurant",
            "total": {"type": "money"},
            "date": {"type": "date"},
        },
        "lease": {
            "tenant": "the person renting",
            "monthly_rent": {"type": "money"},
            "term": {"type": "duration"},
            "start_date": {"type": "date"},
        },
        "doctors_note": {
            "patient": "the person examined",
            "doctor": "the doctor",
            "restriction": "what the patient should not do",
            "restriction_length": {"type": "duration"},
        },
    }
    
    if kind is None or kind == "other":
        # Not one of your types, or Sage isn't sure enough: no field set fits.
        print("Send it to a person")
    else:
        values = sage("/v1/extract", {"content": document, "extract": FIELDS[kind]})["values"]
        print(kind, values)
    Example Sage2 output
    {
      "values": {
        "tenant": "Leo Brandt",
        "monthly_rent": {
          "amount": 1250,
          "currency": "EUR"
        },
        "term": "P3Y",
        "start_date": "2027-02-01"
      },
      "results": {
        "tenant": {
          "value": "Leo Brandt",
          "text": "Leo Brandt",
          "start": 47,
          "end": 57,
          "probability": 0.9912,
          "probabilities": [
            {
              "value": "Leo Brandt",
              "text": "Leo Brandt",
              "start": 47,
              "end": 57,
              "probability": 0.9912
            },
            {
              "value": null,
              "probability": 0.0088
            }
          ]
        },
        "monthly_rent": {
          "value": {
            "amount": 1250,
            "currency": "EUR"
          },
          "text": "1,250.00 EUR",
          "start": 151,
          "end": 163,
          "probability": 0.9906,
          "probabilities": [
            {
              "value": {
                "amount": 1250,
                "currency": "EUR"
              },
              "text": "1,250.00 EUR",
              "start": 151,
              "end": 163,
              "probability": 0.9906
            },
            {
              "value": null,
              "probability": 0.0094
            }
          ]
        },
        "…": "one entry per field"
      },
      "meta": {
        "model": "levanto-sage-2",
        "usage": {
          "input_tokens": 263,
          "output_tokens": 432
        },
        "latency_ms": 62.8
      }
    }

Before you ship

  1. Add a type in two lines. Add an option to the question and a field set to FIELDS. There is no training step.
  2. Set your own bar. Test on a sample of your own documents, then file automatically only above the level you trust.
  3. Use the unknowns. Route other and null to a review queue. Add a type when the same kind of document keeps arriving there.
  4. Fast and small. Each call answers in about 0.1 s. A one-page document reads a few hundred tokens per call.

Sage / Levanto

Start free.

Sign in or create an account.

Continue with Google
or with email

We’ll email you a 6-digit code.