RC2026

Document AI

·
DEMO

Reft

Drag-and-drop pipeline builder for document AI, with an assistant that edits the canvas.

Artifacts

Stack

Document AIReact FlowFastAPI + CeleryLLM ToolsDesign System

The Product

Screens & Illustrations
Reft pipeline editor from a demo recording: the Reft Assistant panel shows a request that built a Local File, PDF Splitter, VLM Structured Extraction and Local Folder pipeline with 7 actions applied, and a second request that added an output schema for a credit-card statement, shown in the extraction node's schema builder.
From a demo recording: one request built the pipeline (7 actions applied); a second wrote the output schema.
Illustration of the Reft pipeline editor with synthetic data: a node palette on the left; a canvas where a local file feeds a PDF splitter, which fans out to parallel Mistral OCR and Gemini 2.5 Flash extraction nodes that both feed one Local Folder destination; and the Reft Assistant panel, which answers a plain-English request with a short reply, the 10 canvas actions it applied and an Undo button.
Illustration: one request becomes 10 canvas actions, and the two extraction nodes run in parallel.
Illustration of a completed Reft parallel run on a synthetic invoice, INV-2041 from Acme Supplies Ltd: a JSON diff strip counts 2 changed, 1 added and 0 removed fields, and the Mistral OCR and Gemini 2.5 Flash outputs sit side by side, with the differing payment terms and first line-item description highlighted in both columns and Gemini's extra delivery line marked as added.
Illustration: one invoice, two providers side by side; the JSON diff counts 2 changed, 1 added, 0 removed.
Reft's design-system page: the Reft Teal brand scale, the neutral scale for headings, text, borders and background, and four status signal colours.
Reft's /design-system page: one teal accent on a monochrome scale, plus four status colours.
Reft's design-system page: status badges for completed, failed, warning, processing and queued, and a file moving through upload, OCR, extract and index.
Status badges and the file-progress pattern from the same page.

Synopsis

Reft is a drag-and-drop builder for document-AI pipelines, built solo as a personal project between December 2025 and February 2026. A pipeline is drawn on a canvas (a file source, Unstructured partitioning, PDF splitting, schema-based extraction with Mistral, Gemini or OpenAI, and an output node) and runs on Celery workers in the background. Extraction nodes that share a source run in parallel, so two models' answers can be compared field by field, and a chat assistant builds or edits the pipeline from a plain-English request. The backend started from the open-source FastAPI Best Architecture template (MIT), which supplied login, RBAC and the admin layer; the pipeline, assistant, provider and Celery-task modules, the frontend and the design system were built for Reft. What came next: REFT v2 (July 2026; built locally, unpublished) gave every extracted field a page and source quote, checked the fields with deterministic Python rules and sent only failing fields back for repair. The same evidence-first idea is now public in docextract, measured at value F1 0.913 across all 370 ExtractBench documents.

Architecture

System Layers

Client (React 19 + React Flow)

TanStack Start and Router, a React Flow canvas, Zustand stores for the pipeline and the assistant, TanStack Query for data and run polling, and pdf.js for document previews, styled with Tailwind 4 and Radix primitives in the Reft design system. Hosted on Netlify, which proxies API calls to the backend.

Iglobe

API (FastAPI on an MIT template)

The backend started from the open-source FastAPI Best Architecture template, which brought JWT login, RBAC, the admin layer and Celery wiring. Added on top: pipeline, run, upload, result and comparison endpoints and the assistant endpoint, all behind JWT auth, with every pipeline and run checked against the signed-in owner. Served by Uvicorn behind Caddy (automatic HTTPS) in Docker Compose on an Azure VM.

IIserver

Workers (Celery + Redis)

A single Celery task, execute_pipeline, turns the saved canvas JSON into a graph, orders the nodes with Kahn's algorithm (a cycle fails the run) and runs every node on the outputs of upstream nodes. Run state lives in PostgreSQL, with per-node success and any error message stored on the run.

IIIlayers

Models (Mistral, Gemini, OpenAI)

Extraction adapters share one interface: declared capabilities, OCR and schema-constrained extraction. Mistral OCR fills a JSON schema in a single annotation call, Gemini 2.5 Flash reads the PDF directly and answers in JSON-schema mode, and GPT-4o reads images under a strict schema. Unstructured partitioning runs as a self-hosted API on a dedicated Azure VM, and the assistant reaches any model through LiteLLM.

IVcpu

Data (PostgreSQL 16 + Azure Blob)

A pipeline is one row holding the canvas (nodes and edges) as JSONB. A run row records status (pending, processing, completed, failed), output files per provider, a JSONB result summary and the error message, indexed by pipeline and status and by user and time. When Azure is configured, source uploads and single-run inputs and outputs are stored in Azure Blob Storage and shared through read-only signed links; otherwise a local volume is used.

Vdatabase

Core Engineering

Field Notes
01.

Canvas with typed connections

Nodes come from a palette grouped into sources, processors, enrichments and destinations, and every node type has a dedicated settings form: Unstructured strategy and OCR languages, pages per split and image DPI, provider, model and output schema. One table of allowed links decides which node types may connect; React Flow rejects any other drag, and the store applies the same table to edges the assistant adds. Saving checks for exactly one source, no unconnected nodes and every node configured, and the API re-checks the source and destination counts.

02.

Schema builder

Extraction output is defined in a schema builder with a visual mode (string, number, boolean, array and object fields; date, email and URI formats) and a JSON mode, synced both ways and validated as fields change. Templates cover invoices, receipts, contracts, purchase orders, bank statements and ID documents.

03.

Background runs and failure handling

Pressing Run validates the upload (PDF, PNG, JPEG, TIFF, BMP or WebP, up to 100 MB), creates a pending run, queues execute_pipeline and answers at once with the run ID; the editor polls every two seconds until the run completes or fails. The Mistral and Gemini adapters retry dropped connections in-process, a 429 or 5xx from Unstructured sends the task back to Celery for another attempt, and any other error fails the run with the message saved. Inputs are kept, so failed runs can be re-run one at a time or in bulk from the Jobs page.

04.

Parallel extraction and comparison

When two or more extraction nodes hang off the same file or splitter, the worker launches the extraction calls together with asyncio.gather, and the synchronous Mistral and Gemini SDK calls run in threads (asyncio.to_thread) so the requests overlap. Each provider's output is saved as a separate file, the run counts as completed if at least one provider succeeds, and every failure is recorded. The run page shows the outputs side by side, in tabs, or as a JSON diff that counts added, removed and changed fields.

05.

One interface for every model provider

Each adapter declares what the provider supports (OCR, annotations, vision, file types) and implements OCR and schema-constrained extraction; a factory hands out adapters and refuses unfinished ones (Anthropic is stubbed and shown as “coming soon”). The user's JSON Schema is rewritten per provider: closed objects for the strict modes of Mistral and OpenAI, required lists filled in, and keywords such as $schema and $defs dropped for Gemini and OpenAI.

06.

Assistant that edits the canvas

The Reft Assistant sends the request, the conversation and the current canvas (nodes, edges, selected node) to the API. The system prompt carries the node catalogue, the allowed-link table, the implemented providers and models and each node type's defaults, and the model (any LiteLLM model, Gemini Flash by default) must answer in JSON: a message plus a list of actions. A parser keeps only known actions with valid payloads (add, remove or update a node, add or remove an edge, select a node, clear the canvas), 20 per turn by default. The client snapshots the canvas first (up to 50 snapshots, with keyboard undo and redo), maps the assistant's node IDs to real ones and applies every change through the same store methods as manual edits.

07.

Results viewers

Partitioned documents open in a pdf.js viewer that draws every Unstructured element's box on the page; clicking an element in the list highlights the box. Extraction results sit beside the PDF as formatted fields or raw JSON, and output shaped like a bank statement gets account details, credit and debit totals and a transactions table filterable by source page. Results download as JSON, per provider for parallel runs.

08.

REFT.UI design system

A /design-system page in the app documents the visual language: a monochrome neutral scale with a single accent, “Reft Teal” (#0d9488), four status colours, sharp-edged components (rounded-none) with high-contrast borders, uppercase tracked labels, status badges, a file-progress pattern (upload, OCR, extract, index) and Lucide icons at 20 px, always paired with text or set in a square. The shadcn button variants were rewritten to match.

Execution Flow

Dispatch
Execution Trace5 steps
01

Draw the pipeline, or ask for one

Nodes are dragged onto the canvas, or the assistant is asked (“Create a PDF extraction pipeline with Mistral”) and returns actions that pass the same connection checks as manual edits; every assistant turn can be undone.

02

Configure and save

Each node is set up in the side panel; the extraction node takes a provider, a model and an output schema from the schema builder. Saving validates the graph in the browser and again in the API, which stores the canvas as JSONB.

03

Queue the run

The document is uploaded, a pending run is created and execute_pipeline is queued for a Celery worker; the API returns the run ID straight away.

04

Execute in dependency order

The worker sorts the graph and runs every node on the outputs of upstream nodes (partitioning, page splitting, schema extraction), launching extraction nodes that share a source together.

05

Store, poll, review

Outputs are saved as JSON (one file per provider for parallel runs) and the run is marked completed or failed with the error message. The editor, polling every two seconds, shows the finished run with a button into the results viewer; parallel runs also get a provider comparison on the run page.

Complete✓ OK
Reflections
Outcomes
  • “Have the model return actions, not prose. The assistant answers with a message and a list of typed actions; unknown types and malformed payloads are dropped, the prompt lists only implemented providers, and the client applies the remaining actions through the same checked store methods as manual edits, after an undo snapshot. A bad answer costs one undo, not a broken canvas.
  • “One JSON Schema, three dialects. The same schema had to be rewritten per provider (closed objects and filled-in required lists for strict modes, unsupported $ keywords removed), so schema conversion lives in each adapter rather than in the UI.
  • “asyncio.gather does not make blocking calls concurrent. The Mistral and Gemini SDKs are synchronous, so parallel extraction only overlapped requests once each call moved into a thread with asyncio.to_thread.
  • “Fail fast on a dead dependency. The partitioner pings the self-hosted Unstructured API before each document (two 2-second tries by default), so a stopped VM fails the run in seconds with an “unreachable” error instead of risking a five-minute request timeout; responses worth retrying, such as 429 and 5xx, go back to Celery for another attempt.
  • “Comparison is not evaluation. Side-by-side output and JSON diffs show where providers disagree, not which provider is right, and v1 had no labelled documents or scores. That gap set the direction for REFT v2, which scores the share of fields backed by a cited page and quote, and for docextract, which is scored on a public benchmark.
Next Steps
  • Read long documents in full. In the graph, the PDF splitter only records page ranges, extraction receives the whole file, and Mistral's single-call mode reads the first eight pages; feeding the splitter's page windows into extraction and merging the results in page order, as REFT v2 does with two-page windows, would close that gap.
  • Implement the chunker and the LLM enrichment nodes (image description, table extraction, OCR, summarizer), which sit on the palette but are placeholders in the executor.
  • Key parallel results by node instead of provider, so two models from one provider, such as Gemini 2.5 Flash and 2.5 Pro, can be compared in the same run.

One Last Thing

The core challenge: Running one document through several AI models and comparing the results takes a chain of steps (partitioning, page splitting, schema extraction), and each provider accepts files and JSON schemas differently. The goal was a Zapier-style builder for document AI: draw the chain once, run the graph in the background, and read each model's answer side by side.

✦Built with
intention

More from the Archive

View All