Doc Ai logo

Doc Ai

Organization
sarvamai
doc-ai

Write correct Sarvam Document AI code (Sarvam Vision) — the doc_ai.parse() / .extract() / .classify() / .split() job lifecycle for OCR, table conversion, schema-based field extraction, document classification, and splitting multi-document PDFs, from PDFs, images, and scanned documents. Use this skill when building document digitization, structured-data extraction, document routing/triage, or RAG-ingestion pipelines in Python or JS/TS. Not for translating a document's content into another language — use translate/document for that. For live document intelligence in chat via MCP, use sarvam-mcp instead.

Overview

Publishersarvamai
Repositoryskills
Skill namedoc-ai
Stars
72
Forks
24
Bundled files
Instructions only
LicenseApache-2.0
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by sarvamai on GitHub. Read the source before you install it.

Installation

Install the Doc Ai AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/sarvamai/skills/tree/main/doc-ai
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/sarvamai/skills.git /tmp/skills
mkdir -p .claude/skills
cp -r /tmp/skills/doc-ai .claude/skills/doc-ai
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Doc Ai in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Doc Ai on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Doc Ai is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Document AI — Sarvam Vision

Live in-chat document work → sarvam-mcp (sarvam_tools_vision_parse / _extract / _classify / _split). This skill = SDK code.

Translating a document into another language? → translate/document — a different API (client.document_translation.*), not this one.

SDK floor for the code below: Python sarvamai>=0.1.40, JS sarvamai@>=1.1.13. Older versions have doc_ai.digitise() instead of parse() and no classify()/split().

[!IMPORTANT] Auth: api-subscription-key header — NOT Authorization: Bearer. Base URL: https://api.sarvam.ai (NOT /v1 — that prefix is only for the OpenAI-compatible chat endpoint)

What this API does

Sarvam Vision is a 3B-parameter document-intelligence model, exposed through the Document AI API (SDK group: doc_ai / docAi). Four job types share one lifecycle (create → poll → fetch):

JobWhat it returnsBest for
Parse (doc_ai.parse)Full-page OCR: text + layout + tables, as HTML, Markdown, or JSONArchival, search, RAG ingestion
Extract (doc_ai.extract)Only the fields you define, as structured JSON/CSV/XLSXKYC, invoices, forms
Classify (doc_ai.classify)A label per document (or per page) from categories you define, or a suggested typeRouting, triage
Split (doc_ai.split)A multi-document PDF divided into child documents with original page numbersScanned bundles, mixed batches

Parse and Extract fetch output with get_download_url / get_results; Classify and Split use a different call shape (a JSON request object, see below) and fetch with get_results.

Supports PDF, PNG, JPG, and ZIP (flat archive of page images) across 23 languages (22 Indian + English).

Quick Start — Parse (Python)

python
import os, time
from sarvamai import SarvamAI

client = SarvamAI(api_subscription_key=os.environ["SARVAM_API_KEY"])

# `file` is a list of (filename, fileobj, content_type) tuples — even for one document
with open("document.pdf", "rb") as f:
    job = client.doc_ai.parse(
        file=[("document.pdf", f, "application/pdf")],
        language="hi-IN",
        # `parse()` has no output_format kwarg (output is HTML by default) — send it as a raw form field:
        request_options={"additional_body_parameters": {"output_format": "md"}},   # "html", "md", or "json"
    )

TERMINAL = {"completed", "partially_completed", "failed", "rejected"}
while True:
    st = client.doc_ai.get_status(job_id=job.job_id)
    if st.status.lower() in TERMINAL:
        break
    time.sleep(5)

dl = client.doc_ai.get_download_url(job_id=job.job_id)
print("download:", dl.method, dl.url)   # a ZIP: primary file + per-page metadata JSON

Quick Start — Parse (JavaScript/TypeScript)

typescript
import fs from "fs";
import { SarvamAIClient } from "sarvamai";

const client = new SarvamAIClient({ apiSubscriptionKey: process.env.SARVAM_API_KEY });

// `file` is an array, even for one document
// `parse()` has no output_format option in JS — output is HTML. For md/json, POST /doc-ai/v1/job/parse
// directly with an `output_format` form field.
const job = await client.docAi.parse({
  file: [fs.createReadStream("document.pdf")],
  language: "hi-IN",
});

const TERMINAL = new Set(["completed", "partially_completed", "failed", "rejected"]);
let st;
while (true) {
  st = await client.docAi.getStatus(job.job_id);   // positional — NOT { job_id }
  if (TERMINAL.has(st.status.toLowerCase())) break;
  await new Promise((resolve) => setTimeout(resolve, 5000));
}

const dl = await client.docAi.getDownloadUrl(job.job_id);   // also positional
console.log("download:", dl.method, dl.url);

Extract (schema-based fields)

Same lifecycle, but pass a schema (or a saved config_id) alongside the file:

python
import json
schema = {
    "type": "object",
    "properties": {
        "policy_number": {"type": "string", "description": "Insurance policy number"},
        "sum_insured":   {"type": "number", "description": "Total sum insured, in INR"},
    },
}

with open("insurance-policy.pdf", "rb") as f:
    job = client.doc_ai.extract(
        file=[("insurance-policy.pdf", f, "application/pdf")],
        schema=json.dumps(schema),   # JSON string, not a dict — or pass config_id="cfg_abc123" instead
        language="en-IN",
        output_format="json",
    )
# ... poll get_status() same as above, then:
results = client.doc_ai.get_results(job_id=job.job_id)
print(results.result)   # {"policy_number": ..., "sum_insured": ...}

Classify and Split

Both take the file(s) plus a required request object that holds the options. They return an acceptance object (status="accepted"), then you poll and fetch get_results.

python
cats = [
    {"type": "invoice", "description": "A supplier bill with totals and payment details."},
    {"type": "bank_statement", "description": "A bank account statement."},
]

# Classify — scope "document" (default) gives one label; "page" gives one per page
with open("bundle.pdf", "rb") as f:
    job = client.doc_ai.classify(
        file=[("bundle.pdf", f, "application/pdf")],
        request={"config": {"scope": "page", "categories": cats}},
    )

# Split — methods: "semantic" (default), "fixed_pages", "page_ranges", "triggers"
with open("bundle.pdf", "rb") as f:
    job = client.doc_ai.split(
        file=[("bundle.pdf", f, "application/pdf")],
        request={"config": {"method": "semantic", "classify_segments": True, "categories": cats}},
    )

# ... poll get_status() as above, then:
r = client.doc_ai.get_results(job_id=job.job_id)          # include="details" adds pages/evidence
for seg in r.result.segments:                              # Split
    print(seg.pages, seg.type, seg.review_required)
# Classify: r.result.type (scope "document") or r.result.decisions[i].type / .input_page (scope "page")
typescript
const job = await client.docAi.split({
  file: [fs.createReadStream("bundle.pdf")],
  request: { config: { method: "fixed_pages", pages_per_segment: 2, classify_segments: false } },
});
// ... poll getStatus(job.job_id), then:
const r: any = await client.docAi.getResults(job.job_id, { include: "details" });
console.log(r.result.segments.map((s: any) => [s.pages, s.type]));

Gotchas

GotchaDetail
file is always a listEven for a single document: file=[(name, fileobj, content_type)] (Python) / file: [stream] (JS). A bare stream in JS fails with TypeError: request.file is not iterable.
job_id is positional in JSgetStatus(job.job_id), getDownloadUrl(job.job_id), getResults(job.job_id) — NOT getStatus({ job_id }). Passing an object sends [object Object] as the path segment and the service rejects it as a non-UUID. Python keeps the kwarg form: get_status(job_id=...).
schema is a JSON stringSent as a multipart form field: json.dumps(schema) in Python, JSON.stringify(schema) in JS — not a raw object/dict.
JS uses wire names, not camelCaseUnlike the rest of the JS SDK, doc_ai params/fields are snake_case on the wire: output_format (not outputFormat), job_id, pages_processed. Only the method names (parse, getStatus) are camelCased.
language, not language_codeDocument AI takes language. Passing language_code (the STT/translate convention) is silently ignored.
output_format values"html" (default), "md", or "json" for Parse; "json" (default), "csv", or "xlsx" for Extract. "markdown" returns 400 — use "md". The SDK's parse() does not expose the field: Python needs request_options={"additional_body_parameters": {...}}; JS silently returns HTML (use REST for md/json). extract() takes output_format normally.
Don't use get_results for Parse jobsParse output comes from get_download_url (a ZIP). In Python get_results() on a Parse job raises a pydantic ValidationError — the API tags it type: "digitise", which the SDK's result union doesn't accept. get_results is for Extract, Classify and Split.
Classify/Split need request, and config is requiredclassify(file=...) without request={"config": {...}} fails with 400 missing property 'config'. To let the API suggest a document type, send request={"config": {}}. In JS, request is a property of the options object next to file.
Don't use execution: "auto" via the SDKThe API answers with 200 and the finished results body, but the SDK types the response as the 201 accepted object. Python raises ValidationError (the job was still created — you lose its job_id); JS returns the results at runtime but typed as if status_url/results_url exist. Submit normally and poll.
Split: set classify_segments deliberatelyIt defaults to true and then requires categories — omitting them returns 400 at '/config': missing property 'categories'. Pass "classify_segments": False when you are not supplying categories. Segments come back with type: None and classification_status: "not_requested" in that case.
Split triggers: match takes equals / prefix / regexNot text: {"type": "text", "match": {"text": "INVOICE"}} returns 400 additional properties 'text' not allowed. Use {"type": "text", "match": {"prefix": "INVOICE"}}. Each marker page starts a new document.
Classify a bundle → type is nullA file containing several documents returns classification_status: "mixed", type: null, and MULTIPLE_DOCUMENTS_DETECTED in review_reasons — not a wrong label. Split first, or use scope: "page".
completed ≠ trustworthyFor Classify and Split, check review_required (and each segment's review_required) before acting on a label. confidence can be null.
Idempotency keyPython idempotency_key="..."; JS "Idempotency-Key": "..." in the options object. Same key + same request returns the same job_id (idempotency_replayed: true); same key + a changed request raises 409 ConflictError.
10-page cap (Parse/Extract)PDF and ZIP uploads are capped at 10 pages/images per job — exceeding it returns 400 invalid_request_error. Split larger documents before uploading.
Legacy document_intelligence groupdocument_intelligence.create_job()/upload_file()/start()/wait_until_complete()/download_output() still works for existing integrations, but new code should use doc_ai.parse()/.extract() — one call creates and submits the job, no separate upload/start step.
Terminal statescompleted, partially_completed, failed, rejected. Only fetch output/results when completed or partially_completed. Classify/Split jobs start as accepted.
digitise was renamed parsePython 0.1.40 / JS 1.1.13 removed doc_ai.digitise() / docAi.digitise() in favour of parse(); params are otherwise unchanged. The REST path is /doc-ai/v1/job/parse (/digitise still answers).

Full Docs

Fetch schema rules, table-extraction details, and rate limits from:

Frequently asked questions

What does the Doc Ai AI skill do?

Write correct Sarvam Document AI code (Sarvam Vision) — the doc_ai.parse() / .extract() / .classify() / .split() job lifecycle for OCR, table conversion, schema-based field extraction, document classification, and splitting multi-document PDFs, from PDFs, images, and scanned documents. Use this skill when building document digitization, structured-data extraction, document routing/triage, or RAG-ingestion pipelines in Python or JS/TS. Not for translating a document's content into another language — use translate/document for that. For live document intelligence in chat via MCP, use sarvam-m...

Why use Doc Ai on TypingMind?

Because you install it once and use it with any model. Doc Ai is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Doc Ai in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/sarvamai/skills/tree/main/doc-ai. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Doc Ai?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Doc Ai?

As many as you like. As long as a model supports skills, you can use Doc Ai with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Doc Ai AI skill free?

Yes. It is published on GitHub by sarvamai under the Apache-2.0 license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇