<< All versions

Skill v1.0.1

currentAutomated scan100/100
gaik-project/gaik-toolkit/parsing-documents

+5 new

──Details
PublishedOctober 28, 2026 at 05:11 PM
Content Hashsha256:440d9e5bf3e267f2...
Git SHA1add4142482d
Bump Typepatch
Compare with v1.0.0
──Files
Files (1 file, 9.6 KB)
SKILL.md9.6 KBactive
SKILL.md · 185 lines · 9.6 KB

name: parsing-documents description: >- Converts PDFs, scans, and Word documents into text or markdown with the gaik toolkit's parsers, choosing the parser that will not silently destroy the structure the downstream task depends on. Use when reading a PDF or DOCX into text, pulling tables out of a document, running OCR on scans, feeding documents into a RAG pipeline or an LLM, deciding between PyMuPDF, Docling, and vision-LLM parsing, or when a parse appeared to succeed but the tables, columns, or whole pages came out wrong or empty. Also use when document parsing is costing more time or money than expected. Covers parser selection, per-page verification, and escalation from cheap local parsing to vision models. metadata: version: "1.0.1"


Parsing documents with gaik

bash
pip install "gaik[parser]==0.8.5" # PyMuPDF, python-docx, Docling
pip install "gaik[multimodal-parser]==0.8.5" # multi-provider vision parsing
# Add llm-google for shared Google/Vertex configs, or llm-litellm for LiteLLM.

Choose by what must survive, not by file type

A parse that returns fluent, plausible text can still have destroyed the one thing the task needed. Decide first what has to still be true afterwards, then pick:

What must surviveParserCost
Plain prose, simple single-column layoutPyMuPDFParserfree, local, milliseconds
A Word document's textDocxParserfree, local
Text on scans / no text layerDoclingParser (OCR)slow on CPU, free
Table structure — merged cells, multi-row headersMultimodalParser (HTML tables by default), or VisionParser with a custom_prompt that asks for HTMLAPI calls
Images explained in place, for RAG chunksVisionPlusParserDocling + API call
Docling quality without the local installDoclingApiClientParsera Docling service (api_base=, password=)

Escalate only when a check fails — start at the cheapest row that could plausibly work, verify (below), and move down one row if it did not. When the text is headed for a search index, the searching-documents skill picks up from here.

Why the cheap path scores zero on tables

Measured on a public benchmark's table split (40 documents, GriTS and TEDS scored against ground-truth HTML table trees):

ParserOutput formatTable structure score
Vision-LLM parsing (MultimodalParser)HTML <table>0.90 – 0.96
Docling, serialized as HTMLHTML <table>0.89
Docling, as shipped (use_markdown=True)markdown pipe table0.00
PyMuPDFplain text0.00

The zeros are not "much worse" — they are structurally unable to score, and that is the transferable point. The output format decides what can survive. A markdown pipe table has no way to express a merged cell or a two-row header, so a document containing one comes back looking clean and quietly wrong. Plain text loses column boundaries entirely.

So the rule is not "always use vision". It is: if the tables carry merged cells or stacked headers, the parser must emit HTML — and among the paths that do, vision-LLM parsing led the specialized parser, with the gap widest on the messiest layouts.

Treat those numbers as a dated snapshot on one corpus, not a constant. What generalizes is the format argument; re-measure the ranking on documents that look like yours.

Parsing quality is usually a traceability decision, not an accuracy one

The expensive counterexample, measured on an extraction task: feeding the model the native PDF, plain extracted text, or model-produced HTML gave the same F1. Parsing cost 22–25× the wall time and was 93% of total spend, and bought no accuracy at all.

What it did buy was bounding boxes. When the parser returns coordinates for each element, a citation can be matched to a box afterwards, and a human reviewer clicks a highlighted region on the page. The model cannot invent that, and both halves stay independently checkable.

Decide on that basis:

  • The deliverable is a reviewer clicking a highlighted box → pay for the rich parse.
  • Page-level evidence is enough, or nothing is reviewed by hand → do not. Feed the model

the document and skip the parsing bill.

Use the API correctly

The class method and the module-level function of the same name do not return the same type, which is the easiest mistake to make here:

python
from gaik.software_components.parsers import PyMuPDFParser, parse_pdf
text = PyMuPDFParser().parse_pdf("doc.pdf") # -> str
result = parse_pdf("doc.pdf") # -> dict
text = result["text_content"]

Every parse_document returns a dict, and the key differs by parser:

python
from gaik.software_components.parsers import (
DocxParser, DoclingParser, VisionParser, VisionPlusParser, MultimodalParser,
)
from gaik.software_components.llm import get_llm_config
DocxParser().parse_docx("doc.docx") # -> str
DoclingParser().parse_document("scan.pdf")["text_content"] # OCR
config = get_llm_config("azure") # explicit provider
VisionPlusParser(vision_config=config).parse_document(
"doc.pdf"
)["parsed_markdown"] # note: different key
VisionParser(openai_config=config).convert_image("page.jpg") # -> str
MultimodalParser(api_config=config).parse("doc.pdf") # -> ParseResult

VisionPlusParser and DoclingApiClientParser take required keyword arguments — vision_config=, and api_base= plus password= — and a bare constructor call fails.

MultimodalParser takes keyword arguments only. Use api_config=get_llm_config(...) for the shared provider interface; the legacy model_provider flags read credentials from the environment. It has no config parameter. VisionParser uses openai_config and VisionPlusParser uses vision_config for the same shared dictionary. Choose native openai, azure, google, vertex, anthropic, anthropic_foundry, aitta, openai_compatible, or optional litellm through get_llm_config. The configured model must accept images. Shared multimodal parsing renders PDF pages as PNG images, so native PDF uploads are not required.

ParseResult is a plain dataclass with raw_markdown, clean_markdown, html (populated only when create_html=True) and usage; it has no save() method, so write the files yourself. DoclingParser has no parse() method.

Set merge_table=True when a table runs across a page break — it instructs the model to stitch the halves back together, which no local parser can do.

For which environment variables each provider needs, read references/parser-selection.md.

Verify before building on the output

Parsers fail quietly far more often than they raise, so check the output rather than the exception. Three checks catch nearly everything:

1. Emptiness, per page — never per document. In one measured corpus 18 of 66 pages had no text layer, spread across half the documents. A document-level if not text check passes such a document as normal and those pages simply never reach the model: no error, no warning, no missing file. Loop the pages and assert each one produced characters; report which page numbers came back empty.

2. Table structure, if tables matter. Search the output for <table> (or pipe rows). If the source has a merged cell and the output has no <table>, the structure is already gone — escalate rather than patch the text.

3. A known token round-trip. Pick a handful of values you can see in the document — a total, an invoice number, a date — and assert they appear in the parsed text. This catches column-collapse and page-drop, which both otherwise read as fine prose.

Gotchas

  • parse_document returns text_content on PyMuPDFParser and DoclingParser, but

parsed_markdown on VisionPlusParser and DoclingApiClientParser. Same method name, different key — read the dict, don't assume.

  • Docling on CPU runs roughly 20–30 s/page. A GPU makes it faster but does not change

its accuracy, so never reach for Docling to improve a quality result you measured on CPU — the number will be identical.

  • DoclingParser requires the parser extra, not parser-cpu. Without Docling the class

still imports (it prints a warning); parse_document then fails with ModuleNotFoundError: psutil or ValueError: Docling is not available.

  • VisionParser's default prompt asks for markdown pipe tables, which cannot hold a merged

cell. For table structure, pass a custom_prompt that asks for HTML <table> markup, or use MultimodalParser, whose prompts already do.

  • VisionParser accepts all shared provider configs, including native Google/Vertex,

Anthropic, Aitta and optional LiteLLM. The selected model must support image inputs. MultimodalParser(api_config=...) uses the same interface. LiteLLM requires gaik[llm-litellm] and a provider-prefixed model identifier; capabilities still depend on that backend/model.

  • Audio transcription components require OpenAI/Azure audio configs. Aitta, native

Google/Anthropic, and an arbitrary compatible chat endpoint do not acquire audio support through the shared chat client. Use separate audio and text stage configs.

  • On Windows, write parsed output with encoding="utf-8" explicitly. Path.write_text()

defaults to the platform codepage, which raises on characters a document parser routinely produces — and a crashed write downstream looks exactly like a bad parse.

  • A parser returning fluent text is not evidence it read the whole document. Only the

per-page check is.

← v1.0.0All versions