- For a general-purpose assistant, that is fine: let the agent try, watch what it does.
- For an enterprise RAG process, it is dangerous. The answers feed real decisions, so we have to know every step and control the flow between the steps.
This article applies that position to the step where “let the model decide” is most tempting: picking the right parsing method for each document. We build that choice as a dispatcher we control. It reads the PDF’s nature, plans the methods that fit, executes them in order, and synthesizes every output into one enriched corpus for retrieval, generation and evaluation. Each decision is explicit and logged, so the plan can be read and checked before anything runs.
This article extends the document parsing brick of Enterprise Document Intelligence, the series that builds an enterprise RAG system from four bricks. It closes that brick by composing the methods the series built one at a time: fitz for the text layer, Azure Document Intelligence and Docling for tables, a vision LLM for charts and diagrams, EasyOCR for pages with no text layer, image captioning for what the pipeline would otherwise skip, and two ways of recovering a table of contents, from the printed sommaire or from body typography alone.
🧭 New to the series? Start with the map: Prompt, Context, Loop sets out the three engineering layers every RAG system is built on, the prompt (the call itself), the context (what fills the model’s window), the loop (when the next call fires and when it stops), and walks the whole series through that lens, article by article. It is the shortest way to see what is covered and where this one sits.
📓 The runnable notebook runs parse_pdf_agentic() on the attention paper (data/paper/1706.03762v7.pdf), prints the detected nature, the four-step plan the dispatcher produced, and the merged corpus dict with a 15-row native toc_df and a 1048-row line_df: doc-intel/notebooks-vol1.
Every RAG vendor now labels their document-parsing loop agentic. Open the code and it is almost always the same thing: a rule-based dispatcher that reads a few file signals, picks an ordered plan of methods, runs each in sequence, and folds the outputs. The LLMs live inside individual leaves (a heading validation loop, a vision reader on figures, an OCR post-processor). No LLM at the dispatch layer decides what to run next. No feedback loop where an agent watches an output and re-plans.
That is exactly what the dispatcher in this article does. So calling it agentic is a stretch, and calling it agentic without quotes would be selling the same buzzword-inflation the rest of the market sells. The quotes stay.
The honest picture, function by function:
detect_document_nature(pdf_path): six deterministic flags read from-line_df/-span_df.-is_scanned,-has_native_outline,-has_sommaire,-is_composite,-has_rich_figures,-has_tables_signal. Docstring says it plainly:- “deterministic; no LLM”.plan_parsing_methods(nature): pure Python-if / elifon the nature label. Every branch returns a hard-coded ordered list of-MethodStep. No LLM.parse_pdf_agentic(pdf_path, llm_parse=…): loops over the plan and calls one adapter per method. The-llm_parsekwarg is- forwardedto methods that use one (the body-structure loop, the future vision reader). The dispatcher itself makes zero LLM calls.synthesize_parsing_outputs(step_outputs): DataFrame merges + a-_pick_richerheuristic. No LLM.
What true agentic parsing would add: an LLM that reads each method’s output, decides whether the corpus is done or a method is worth re-running with different parameters, and adds or drops methods from the plan on the fly. Tool use in the ReAct sense. Feedback loops. That belongs to Volume 3 (Agentic Bricks) of the series, where each brick gets an agent wrapper that observes, plans, and acts. This article stops one step short.
So this article builds the honest version of the pattern that everyone calls agentic today: rule-based routing plus LLM leaves. It is enough to close the parsing brick with a single parse_pdf_agentic(path) call that returns an enriched corpus. Volume 3 will add the real agent on top.
Think of the documents where a single parsing method is never enough: a 200-page contract with rate tables, a quarterly report full of charts and footnotes, a grant application, a patent, a dense NIPS paper with equations and results tables. Each one defeats a different parser. Fitz gets the text but misses the table cells. Docling gets the tables but the outline is only two levels deep. Azure Layout is strong on both but has no font size. Mistral OCR returns markdown for free but only when you run it against a scanned page. The team needs each of these tools on different documents, sometimes on different pages of the same document.
The reflex the series has been building for eight articles is one method per problem. That reflex is correct at the individual level and it does not scale to the document. A production caller does not want to write a switch statement over parsers. They want to call one function and get back a corpus dict filled to the level the document deserves. That is what this article closes with.
Two regimes coexist and it is worth naming them before the code lands. The first is what this article builds: the “agentic” one above. Read the document once, pick a plan, execute every step, fold the outputs. The document gets everything it deserves in one pass, even if that pass costs several LLM calls and one OCR run. The second is adaptive parsing (a later article in the series): the caller does not enrich anything up front; instead, the retrieval brick asks for the pages it needs and parsing runs on demand. The first is ex ante, the second is lazy. Both belong in the pipeline. This article is only about the first.
The loop has four stages. Each is deterministic, cheap, and inspectable; the choice of what to run is not an LLM decision. The LLMs enter each individual method at the level of its own contract (fixed schema, injected callable, cached JSON), never at the dispatcher layer.
In:a PDF path. Optionally a pre-computed nature or plan. Optionally anllm_parsecallable for methods that use one.Out:an enriched corpus dict withline_df,span_df,toc_df,image_df,reference_df,table_df, plus thenatureandplanused to produce it, so the run is fully reproducible.
The first pass reads the file’s nature: a small Pydantic model with categorical flags. Cheap signals only. File probe plus a single line_df and span_df build. No LLM.
The six flags are read from the following signals:
is_scanned:-line_dfis empty or its row count sits well below-page_count. No extractable text layer, OCR is required.has_native_outline:-doc.get_toc()returns a non-empty list.has_sommaire: an early page carries five or more dot-leader lines (-Title ....... 12), the signal Article 5septies (TOC reconstruction from a sommaire) reads.is_composite:-detect_document_boundaries(from the body-structure module of Article 5octies, TOC reconstruction from body typography) fires on a numbering re-init, style rupture or cover page.has_rich_figures: image density above the median for a prose doc.has_tables_signal: a cheap grid detector finds rows of three or more short whitespace-separated fields.
plan_parsing_methods reads the nature and returns a list of MethodStep values. Each step names one parsing method, gives a one-line rationale, and carries an optional flag saying whether the dispatcher may skip it on error.
Two rules that keep the plan honest:
- Every plan starts withLine and span frames feed every downstream method; there is no case where skipping them saves time.-
fitz_native. - Optional flags are used sparingly.-
image_pipelineand-vision_llm_figuresare opt-out because they call out to a heavy tool; the TOC and layout methods are required because they are the load-bearing outputs.
The open-source parsing landscape is wide and it keeps growing, so treat this as a catalog, not a fixed list. Here is one identity card per method: its family, its licence, an at-a-glance strip (where it runs, its speed, whether it calls an LLM, whether it exposes typography), the input it takes, the output it returns, and where it shines or breaks. Read them as a set, not a sequence. Every card shares the same template, so you can compare across them and pick the right one for a given document. Every method here is open source, and the licence sits on each card. The family is colour-coded: blue for native text parsers, teal for layout and table models, amber for OCR readers, violet for structure and TOC recovery.
Native text parsers (blue) read the text layer directly, no model. fitz is the cheap baseline, PyMuPDF4LLM turns the same read into Markdown for RAG, pdfplumber gives you exact coordinates and ruled tables, and pdfminer.six is the classic low-level extractor underneath.
Layout and table models (teal) run a deep-learning layout pass and hand back structure. They differ on Markdown versus cell-level tables, and on how much GPU they want.
OCR readers (amber) read pixels when there is no text layer. They differ on accuracy and on whether they recover tables.
Structure and TOC recovery (violet) rebuild the outline. Read the native outline first; recover it from a printed contents page or from body headings when the file has neither; or partition the whole document into typed elements with Unstructured.
How to read a card in one glance. The vitals strip is the comparison shortcut. Style tells you whether the body-typography signals from Article 5octies (TOC reconstruction from body typography) will have anything to latch onto when you chain the body-structure loop after this method. LLM tells you if the method opens a socket. Runs and Speed set the budget. The diagram and rows below fill in the exact input and output.
Each card is also saved as its own PNG under book_1/_figures/05_9_agentic_parsing_synthesis/en/identity_cards/, so a single method drops into a slide deck, an evaluation report, or a LinkedIn carousel without cropping a grid.
The dispatcher runs each step through a small _run_step(step) shim that adapts to the method’s own signature. Every parsing module (fitz, azure_layout, docling, easyocr, mistral_ocr, toc, toc.body_structure, vision, images) is already ready for this call; the dispatcher owns the glue, not the logic.
Two things worth spelling out about the execution stage. First, each method carries its own error handling. When an optional step raises, the dispatcher captures the exception into _error on the step output and keeps going. A required step failure aborts the run so the caller sees the real error immediately. Second, every method’s raw output ends up in step_outputs alongside the merged corpus. An audit does not have to guess what each method returned; it can read the trace end to end.
The last stage folds the per-step outputs into one dict with six frames: line_df, span_df, toc_df, image_df, reference_df, table_df, plus a sources list saying which method contributed each frame.
The merging rule is simple: preserve every native frame verbatim, and when two methods produced the same key, keep the strictly-more-informative one (higher row count with a compatible column set). Anything else concatenates. The full column-level reconciliation between, say, Docling table cells and Azure Layout cells lives in the individual modules; the dispatcher does not re-implement it.
Here is what the dispatcher returns on data/paper/1706.03762v7.pdf, a 15-page NIPS paper with a native outline. The nature comes back as native-with-outline. The plan lists four steps: fitz_native (mandatory, cheap), fitz_native_toc (mandatory, free from doc.get_toc()), toc_body_structure (advisory, catches the level-3 subsections the outline missed) and image_pipeline (optional). The merged corpus dict carries a 15-row toc_df from the native outline, a 1048-row line_df from fitz, and a 3480-row span_df from build_span_df. The image and reference frames are empty on this run because the paper has no charts and the reference-extraction method has not been wired into the plan yet.
That run cost one line_df build, one span_df build, one native TOC read and one body-structure loop. No vision LLM, no OCR, no Docling. The plan matched the document.
Agentic parsing is not free. On a 15-page paper the four-step plan costs a few hundred milliseconds. On a 300-page contract with tables, figures and no native outline the plan grows to seven or eight methods, some of which call an LLM; a full run can take a minute and cost real cents. This is fine for documents you plan to use to their full extent. It is wasteful for corpus-scale ingestion where most pages are never queried.
The rule I follow: run agentic parsing on chosen documents, the contracts a reviewer will actually walk end to end, the papers a search result surfaced and a reader clicked. For everything else, the adaptive path (a later article) parses on demand, driven by the questions the pipeline actually receives. The two regimes coexist; the caller picks which one the document goes through.
This article closes brick 1. A caller who has a document and wants everything it offers calls parse_pdf_agentic(pdf_path) and gets back an enriched corpus dict with line_df, span_df, toc_df, image_df, reference_df and table_df, each filled to the level the document supports. Every method the earlier articles introduced now has a place in one dispatcher and one enriched corpus. The choice of methods is deterministic; every LLM lives inside its own module with its own contract, not at the dispatcher layer, so the plan is inspectable and the execution auditable. The synthesized dict is what Part III (retrieval) and Part IV (generation) read next.
Two follow-ups. First, the module comes with stubs for azure_layout, docling_local, easyocr_scan, mistral_ocr, vision_llm_figures and image_pipeline. They call the existing method modules but do not yet carry a full end-to-end integration test on real documents; those tests are on the same ticket. Second, the adaptive parsing article (a later Vol.1 or Vol.2 piece) will describe the counterpart regime where parsing runs lazily, driven by retrieval demand.
Case 4 of Article 5octies (TOC reconstruction from body typography), the body-structure loop, is one of the methods the dispatcher picks from. The parser matrix from that article (fitz / Docling / Azure DI / Mistral OCR / EasyOCR) is exactly the matrix this article’s plan reads. The two articles form the closing pair of the brick.
Earlier in the parsing brick:
- Beyond extract_text: the two layers of a PDF that drive RAG quality. The four-brick pipeline that reads this article’s corpus at the retrieval and chunking steps.
- Stop returning flat text from a PDF: the relational tables RAG needs. The
line_df/page_df/toc_dfshape this article merges every method into. - When PyMuPDF can’t see the table: parse PDFs for RAG with Azure Layout. One of the methods the dispatcher picks from.
- Parse PDFs for RAG locally with Docling: rich tables, no cloud upload. Another method, for when tables need native cell extraction.
- Vision LLMs are PDF parsers too: reading charts and diagrams for RAG. The vision path in the plan, triggered by
has_rich_figures. - Parse scanned PDFs for RAG with EasyOCR: free OCR gives you words, not a document. The OCR path in the plan, triggered by
is_scanned. - Making a PDF’s images searchable for RAG, without paying to read them all. The image pipeline the plan always finishes with.
- Reconstructing the table of contents a PDF forgot to ship. The sommaire cascade (Cases 1-3) the plan reads for
native-no-outlinefiles.
External sources (worked examples and prior art):
- Vaswani et al., Attention Is All You Need, arXiv:1706.03762, NeurIPS 2017. The paper we run the dispatcher on in Section 3.4 (arXiv non-exclusive distribution).
- PyMuPDF (fitz) documentation,
page.get_text("dict"). The API_call_fitz_nativereads. - Sculley et al., Hidden Technical Debt in Machine Learning Systems, NIPS 2015. The “glue code” pattern the dispatcher walks a fine line around.