Skip to content

Agent example: document intake

The job: contracts, agreements and letters arrive as PDFs, some digital and some scanned. The agent reads each one, pulls out the key facts, flags risks, and writes a one-page review note in Word for the legal or purchase team.

Tools used: POST /pdf for exact text, POST /ocr for scans, structured JSON on /v1/chat/completions, and file creation on POST /generate.

import base64, json, os, time
from pathlib import Path
import requests
from openai import OpenAI
from pydantic import BaseModel
BASE = "https://dummydomain"
HEADERS = {"Authorization": "Bearer " + os.environ["API_KEY"]}
client = OpenAI(base_url=BASE + "/v1", api_key=os.environ["API_KEY"])
def attach(path: Path) -> list[dict]:
return [{"name": path.name, "data": base64.b64encode(path.read_bytes()).decode()}]
def ocr(path: Path, deadline_s: int = 600) -> str:
r = requests.post(f"{BASE}/ocr", headers=HEADERS, timeout=60,
json={"attachments": attach(path), "pages": "all", "view": False})
r.raise_for_status()
job_id, give_up = r.json()["job_id"], time.time() + deadline_s
while time.time() < give_up:
job = requests.get(f"{BASE}/ocr/jobs/{job_id}", headers=HEADERS, timeout=30)
job.raise_for_status()
job = job.json()
if job.get("status") == "done":
return job["result"]["text"]
if job.get("status") in ("failed", "error", "cancelled"):
raise RuntimeError(f"OCR job {job_id} ended as {job['status']}")
time.sleep(5)
raise TimeoutError(f"OCR job {job_id} did not finish in {deadline_s} seconds")
def read_text(path: Path) -> str:
r = requests.post(f"{BASE}/pdf", headers=HEADERS, timeout=120,
json={"attachments": attach(path), "mode": "text", "pages": "all"})
r.raise_for_status()
text = r.json()["files"][0]["text"]
return text if len(text.strip()) > 200 else ocr(path) # little text means a scan
class Review(BaseModel):
document_type: str
parties: list[str]
key_dates: list[str]
amounts_inr: list[str]
summary: str
risks: list[str]
def review(text: str) -> Review:
reply = client.chat.completions.create(
model="Smart",
messages=[{"role": "system", "content": "You review business documents for an Indian company. "
"Use only the document text. Dates as DD-MM-YYYY. List a risk only if the text supports it."},
{"role": "user", "content": "Document text:\n\n" + text}],
response_format={"type": "json_schema",
"json_schema": {"name": "Review", "schema": Review.model_json_schema(), "strict": True}},
)
return Review.model_validate_json(reply.choices[0].message.content)
def write_note(name: str, r: Review) -> Path | None:
prompt = (f"Create a Word document: a one-page review note for '{name}'. Use these facts only, "
"with headings for Summary, Parties, Key dates, Amounts and Risks:\n" + r.model_dump_json(indent=2))
made = requests.post(f"{BASE}/generate", headers=HEADERS, timeout=600,
json={"model": "Smart", "prompt": prompt, "stream": False})
made.raise_for_status()
art = made.json().get("office_artifact")
if not art:
return None # no file was made; keep the JSON and tell a person
file = requests.get(f"{BASE}/office/artifacts/{art['artifact_id']}/download", headers=HEADERS, timeout=120)
file.raise_for_status()
out = Path(art["filename"])
out.write_bytes(file.content)
return out
pdf = Path("agreement.pdf")
result = review(read_text(pdf))
print(json.dumps(result.model_dump(), indent=2, ensure_ascii=False))
print("Review note:", write_note(pdf.name, result) or "not created")

A real run of the code above, on made-up sample files. Press Replay to watch the steps in order.

Recorded on 07-10-2026 against a ZenithAI test server, running the code above. The times are real.

What the agent did

  1. POST /pdf0.1 s

    Read the text inside the PDF, with no model: 750 characters found.

    Request and response
    Request
    {
      "attachments": [
        {
          "name": "agreement.pdf",
          "data": "<file, 1,656 bytes>"
        }
      ],
      "mode": "text",
      "pages": "all"
    }
    Response 200
    {
      "files": [
        {
          "chars": 750,
          "name": "agreement.pdf",
          "page_texts": [
            "SERVICE AGREEMENT\nThis agreement is made on 01-10-2026 between Sharma Traders, Chandni Chowk, Delhi\n(the Client) and Gupta Electricals, Lajpat Rai Market, Delhi (the Contractor).\n1. Scope: the Contractor will maintain all electrical fittings at the Client’s warehouse.\n2. Fee: Rs 2,40,000 per year, paid quarterly in advance within 7 days of invoice.\n3. Term: 12 months from 01-10-2026. The agreement renews automatically for another\n12 months unless either party gives notice.\n4. Penalty: a late visit costs the Contractor Rs 5,000 per day, with no upper limit.\n5. Termination: either party may end this agreement with 90 days written notice.\n6. Jurisdiction: courts at Delhi only.\nSigned: R. Sharma ..."
          ],
          "pages": 1,
          "pages_processed": 1,
          "text": "SERVICE AGREEMENT\nThis agreement is made on 01-10-2026 between Sharma Traders, Chandni Chowk, Delhi\n(the Client) and Gupta Electricals, Lajpat Rai Market, Delhi (the Contractor).\n1. Scope: the Contractor will maintain all electrical fittings at the Client’s warehouse.\n2. Fee: Rs 2,40,000 per year, paid quarterly in advance within 7 days of invoice.\n3. Term: 12 months from 01-10-2026. The agreement renews automatically for another\n12 months unless either party gives notice.\n4. Penalty: a late visit costs the Contractor Rs 5,000 per day, with no upper limit.\n5. Termination: either party may end this agreement with 90 days written notice.\n6. Jurisdiction: courts at Delhi only.\nSigned: R. Sharma ...",
          "truncated": false
        }
      ],
      "mode": "text",
      "ms": 10,
      "ok": true
    }
  2. POST /v1/chat/completionsSmart1.3 s

    Smart returned JSON that matches the Review schema.

    Request and response
    Request
    {
      "messages": [
        {
          "role": "system",
          "content": "You review business documents for an Indian company. Use only the document text. Dates as DD-MM-YYYY. List a risk only if the text supports it."
        },
        {
          "role": "user",
          "content": "Document text:\n\nSERVICE AGREEMENT\nThis agreement is made on 01-10-2026 between Sharma Traders, Chandni Chowk, Delhi\n(the Client) and Gupta Electricals, Lajpat Rai Market, Delhi (the Contractor).\n1. Scope: the Contractor will maintain all electrical fittings at the Client’s warehouse.\n2. Fee: Rs 2,40,000 per year, paid quarterly in advance within 7 days of invoice.\n3. Term: 12 months from 01-10-2026. The agreement renews automatically for another\n12 months unless either party gives notice.\n4. Penalty: a late visit costs the Contractor Rs 5,000 per day, with no upper limit.\n5. Termination: either party may end this agreement with 90 days written notice.\n6. Jurisdiction: courts at Delhi only.\nS ..."
        }
      ],
      "response_format": {
        "type": "json_schema",
        "json_schema": {
          "name": "Review",
          "schema": {
            "properties": {
              "document_type": {
                "title": "Document Type",
                "type": "string"
              },
              "parties": {
                "items": {
                  "type": "string"
                },
                "title": "Parties",
                "type": "array"
              },
              "key_dates": {
                "items": {
                  "type": "string"
                },
                "title": "Key Dates",
                "type": "array"
              },
              "amounts_inr": {
                "items": {
                  "type": "string"
                },
                "title": "Amounts Inr",
                "type": "array"
              },
              "summary": {
                "title": "Summary",
                "type": "string"
              },
              "risks": {
                "items": {
                  "type": "string"
                },
                "title": "Risks",
                "type": "array"
              }
            },
            "required": [
              "document_type",
              "parties",
              "key_dates",
              "amounts_inr",
              "summary",
              "risks"
            ],
            "title": "Review",
            "type": "object"
          },
          "strict": true
        }
      }
    }
    Response 200
    {
      "choices": [
        {
          "finish_reason": "stop",
          "index": 0,
          "message": {
            "content": "{\"document_type\":\"SERVICE AGREEMENT\",\"parties\":[\"Sharma Traders\",\"Gupta Electricals\"],\"key_dates\":[\"01-10-2026\"],\"amounts_inr\":[\"Rs 2,40,000 per year\",\"Rs 5,000 per day\"],\"summary\":\"Service agreement for electrical maintenance at the Client's warehouse for a term of 12 months starting 01-10-2026, with automatic renewal and a 90-day termination notice period.\",\"risks\":[\"Late visits cost the Contractor Rs 5,000 per day with no upper limit.\"]}",
            "role": "assistant"
          }
        }
      ],
      "object": "chat.completion",
      "usage": {
        "completion_tokens": 135,
        "prompt_tokens": 440,
        "total_tokens": 575
      }
    }
  3. POST /generateSmart8.1 s

    Made a Word file: Service Agreement Review Note Sharma Traders Gupta Electricals.docx.

    Request and response
    Request
    {
      "prompt": "Create a Word document: a one-page review note for 'agreement.pdf'. Use these facts only, with headings for Summary, Parties, Key dates, Amounts and Risks:\n{\n  \"document_type\": \"SERVICE AGREEMENT\",\n  \"parties\": [\n    \"Sharma Traders\",\n    \"Gupta Electricals\"\n  ],\n  \"key_dates\": [\n    \"01-10-2026\"\n  ],\n  \"amounts_inr\": [\n    \"Rs 2,40,000 per year\",\n    \"Rs 5,000 per day\"\n  ],\n  \"summary\": \"Service agreement for electrical maintenance at the Client's warehouse for a term of 12 months starting 01-10-2026, with automatic renewal and a 90-day termination notice period.\",\n  \"risks\": [\n    \"Late visits cost the Contractor Rs 5,000 per day with no upper limit.\"\n  ]\n}",
      "stream": false
    }
    Response 200
    {
      "duration_ms": 7322,
      "office_artifact": {
        "artifact_id": "FrLReGO1qz6Zj_kMbJWjt2QSn0XIYUjD",
        "available": true,
        "created_at": "2026-10-07T00:29:41.853426Z",
        "expires_at": "2026-10-14T00:29:41.853426Z",
        "filename": "Service Agreement Review Note Sharma Traders Gupta Electricals.docx",
        "kind": "docx_report",
        "logical_pages": 1,
        "mime": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "page_count_mode": "logical",
        "size": 39069,
        "title": "Service Agreement Review Note: Sharma Traders & Gupta Electricals"
      },
      "response": "Created Service Agreement Review Note Sharma Traders Gupta Electricals.docx."
    }
  4. GET /office/artifacts/{id}/download0.1 s

    Downloaded the file.

    Request and response
    Request
    (empty)
    Response 200
    <application/vnd.openxmlformats-officedocument.wordprocessingml.document, 39,069 bytes>

What the program printed

{
  "document_type": "SERVICE AGREEMENT",
  "parties": [
    "Sharma Traders",
    "Gupta Electricals"
  ],
  "key_dates": [
    "01-10-2026"
  ],
  "amounts_inr": [
    "Rs 2,40,000 per year",
    "Rs 5,000 per day"
  ],
  "summary": "Service agreement for electrical maintenance at the Client's warehouse for a term of 12 months starting 01-10-2026, with automatic renewal and a 90-day termination notice period.",
  "risks": [
    "Late visits cost the Contractor Rs 5,000 per day with no upper limit."
  ]
}
Review note: Service Agreement Review Note Sharma Traders Gupta Electricals.docx

File made: Service Agreement Review Note Sharma Traders Gupta Electricals.docx (38 KB)

  1. Exact first, OCR only when needed. /pdf uses no model, so digital PDFs are read exactly and quickly. Only scans go to OCR.
  2. OCR is a job. Start it, then check it every five seconds, with a deadline.
  3. The review is JSON. Your code can store it, search it, or send risky contracts to a senior person first.
  4. The Word file is checked. If the response has no office_artifact, no file was made, and the agent says so instead of pretending.

Vendor onboarding papers, bank statements, insurance claims, tender documents, purchase orders and HR files. For tables, such as a bank statement, ask /pdf for "mode": "tables" and work with the rows directly.

Common questions

How does the agent know if a PDF is scanned?

It first asks POST /pdf for the text inside the file. A digital PDF returns plenty of text. A scan returns little or none, because it is only a picture of a page, so the agent sends it to OCR instead.

How long does OCR take on a scanned PDF?

It depends on the number of pages and how busy your server is. OCR runs as a job, so your code checks it every few seconds. Always set a deadline, for example ten minutes, so a stuck job does not hold up the queue.