SAGEA LogoDocs & API
Studio

Document QnA

Extract a document once with ARVA OCR, then ask questions over the markdown with a SAGE model. This guide covers the pipeline, chunking, citations, and follow-up turns.

In this guide

  • Extract with ARVA and ask with SAGE using grounded prompts
  • Chunk 100+ page documents with per-page ask and merge
  • Add page-number citations from OCR blocks
  • Run follow-up turns and a Nepali invoice example

Extract once, ask many times

Run OCR a single time and reuse the markdown for every question. This keeps answers consistent and avoids paying for repeated extraction.

Keep the system prompt strict so the model answers only from the document. Pass the markdown as context in the user message, then ask a focused question.

import os
import requests
 
# 1. Extract with ARVA
ocr = requests.post(
    "https://api.sagea.space/v1/ocr/process",
    headers={"Authorization": f"Bearer {os.environ['SAGEA_API_KEY']}"},
    files={"document": open("invoice_npr24500.pdf", "rb")},
    data={"model": "arva-ocr", "output_format": "markdown"},
    timeout=120,
).json()
 
context = ocr["text"]
 
# 2. Ask with SAGE
chat = requests.post(
    "https://api.sagea.space/v1/chat/completions",
    headers={
        "Authorization": f"Bearer {os.environ['SAGEA_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "model": "sage-2-4-actus",
        "messages": [
            {"role": "system", "content": "Answer only from the document below. If the answer is missing, say so."},
            {"role": "user", "content": f"Document:\n{context}\n\nQuestion: What is the total amount and PAN?"},
        ],
    },
    timeout=60,
).json()
 
print(chat["choices"][0]["message"]["content"])

For schemas and auth details, see OCR Process and Chat Completions.

Chunk 100+ page documents

Large PDFs overflow context when sent whole. Ask per page or per section, then merge the partial answers with a final SAGE call.

A practical pattern is 1 to 5 pages per chunk, with a short overlap for tables that span pages. Keep each chunk self-contained by prefixing it with the document title and page range.

chunks = [pages_1_5, pages_6_10, pages_11_15]  # markdown strings from ARVA
partials = []
 
for i, chunk in enumerate(chunks):
    resp = ask_sage(
        model="sage-2-5-celer",
        system="Answer only from the document excerpt. Return JSON with answer and page.",
        user=f"Excerpt pages {i*5+1} to {i*5+5}:\n{chunk}\n\nQuestion: List all invoice totals.",
    )
    partials.append(resp)
 
final = ask_sage(
    model="sage-2-4-actus",
    system="Merge partial answers. Deduplicate and keep page numbers.",
    user=f"Partials:\n{partials}\n\nReturn one combined list.",
)

Use sage-2-5-celer for the fast per-page sweep and sage-2-4-actus for the merge when accuracy matters. For background jobs over many files, see Batch.

Cite pages and handle follow-ups

Readers trust answers with page numbers. Request OCR with bounding boxes, keep the blocks array alongside the markdown, then ask the model to cite the page it used.

# Keep blocks for citations
blocks = ocr["blocks"]  # each block has page, type, confidence
prompt = (
    "Document:\n" + context + "\n\n"
    "Question: What is the VAT amount?\n"
    "Cite the page number, for example Page 2."
)
answer = ask_sage(
    model="sage-2-4-actus",
    system="Answer only from the document. Always include a page citation.",
    user=prompt,
)

For follow-up turns, reuse the same extracted context instead of re-running OCR. Append the prior answer to the message history so pronouns like "that total" still resolve.

messages = [
    {"role": "system", "content": "Answer only from the document below."},
    {"role": "user", "content": f"Document:\n{context}\n\nQuestion: What is the total?"},
    {"role": "assistant", "content": "Rs. 24,500 on Page 1."},
    {"role": "user", "content": "What is the VAT portion of that total?"},
]

If a follow-up drifts off-topic, restate the boundary in the system message and ask the user to upload a new source. See Build an agent for memory patterns.

Nepali invoice example

A Kathmandu supplier sends a two-page Nepali invoice to Aarav Sharma for Rs. 24,500 including VAT. You need the total, PAN, and invoice date with citations.

answer = ask_sage(
    model="sage-2-4-actus",
    system="Answer only from the document. Reply in Nepali and English. Include page citations.",
    user=(
        f"Document:\n{context}\n\n"
        "Questions:\n"
        "1. कुल रकम कति हो? (What is the total?)\n"
        "2. PAN नम्बर के हो?\n"
        "3. बिल मिति के हो?\n"
        "Cite pages, for example Page 1."
    ),
)
print(answer)

Expected shape: कुल रकम: Rs. 24,500 (Page 1) plus PAN and date lines. If VAT and subtotal disagree, ask a second pass to show the arithmetic. Related cookbook: Invoice extraction.

Best practices

  • Extract once at 300 DPI markdown, then reuse the text for all turns.
  • Keep the system prompt grounded: answer only from the document or say it is missing.
  • Chunk by pages for 100+ page docs, then merge with a dedicated call.
  • Persist blocks with page numbers so every answer can cite its source.
  • Prefer sage-2-5-celer for sweeps and sage-2-4-actus for final answers.
  • Re-extract when confidence is low instead of forcing an answer from noise.

What's next

On this page