RC2026

Document AI

·
DEMO

Reft

Drag-and-drop builder for document-AI pipelines: background runs, models compared side by side, and an assistant that edits the canvas.

Artifacts

Stack

Document AIReact FlowFastAPI + CeleryLLM ToolsDesign System

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.

The Product

Draw a document-AI pipeline once, run it in the background, and read each model's answer side by side.

Reft pipeline editor, recreated with sample data in the Reft design system. A node palette on the left lists sources (Local File), processors (Unstructured Partition, PDF Splitter, Chunker), enrichments (VLM Structured Extraction, OCR, Image Description, Table Extraction, Summarizer) and destinations (Local Folder). On the dotted canvas, a Local File source feeds a PDF Splitter, which fans out to two VLM Extraction nodes, Mistral OCR and Gemini 2.5 Flash, that both feed a Local Folder. The pipeline Invoice extraction is marked Ready, and a strip at the bottom shows the last run completed for invoice_7731.pdf with 2 of 2 providers in 41 seconds and a View results button.
A pipeline on the canvas: a file source, a PDF splitter and two extraction nodes running in parallel into one output folder, with the last run's result below.
Product features
#1

Build

Ask in plain English and the assistant builds or edits the pipeline through the same checked canvas actions as a manual edit, and every turn can be undone.

Reft Assistant panel beside the canvas, recreated with sample data. The first request, Create a PDF extraction pipeline with Mistral, was answered with 7 actions applied; the second, Add Gemini 2.5 Flash next to Mistral so I can compare them, added a second VLM Structured Extraction node wired to the PDF Splitter and the Local Folder with 4 actions applied. On the canvas the new Gemini node is selected and its new edges are drawn in teal. The panel header has undo and redo, and a hint says Cmd+Z undoes a whole assistant turn.
#2

Define

Each extraction node takes a provider, a model and an output schema, built visually or as JSON and started from templates such as invoices, receipts and bank statements.

Reft node settings for a VLM Structured Extraction node, recreated with sample data. Provider Mistral AI and model Mistral OCR (Latest) are chosen, and the output schema, started from the Invoice template and marked valid, is shown in Visual mode with a Code toggle: invoice_number, invoice_date (string, date format), vendor_name, total_due (number) and line_items, an array of objects with description, quantity and amount, each with its type and a short description.
#3

Run

Runs execute in the background, and a failed run keeps its error and its input, so it can be re-run on its own or in bulk.

Reft Jobs page, recreated with sample data. Tabs filter 31 runs by completed, failed, processing and queued; the table lists each run's ID, pipeline and file, providers, status and time, including one run that completed with 1 of 2 providers failed. Two failed runs are selected for Re-run failed. The selected run's panel shows its kept input file, an error saying the Unstructured API was unreachable after 2 health checks, per-node results (Local File done, Unstructured Partition failed, the rest not reached) and Re-run and Open pipeline buttons.
#4

Compare

When two extractors share a source, both run at once and their outputs open side by side, in tabs or as a JSON diff that counts changed, added and removed fields.

Reft run page for invoice_7731.pdf, recreated with sample data. Two providers ran in parallel and both finished; the view is set to Diff (with Side by side and Tabs alongside), and a summary reads 3 changed, 0 added, 1 removed from Mistral to Gemini. The Mistral OCR and Gemini 2.5 Flash JSON outputs sit in two panes with download buttons; the invoice date, the vendor name and one line item's description are highlighted as changed, and a quantity present only in Mistral's output is marked removed.

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

Execution Flow

5 steps
  1. Step 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.

  2. Step 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.

  3. Step 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.

  4. Step 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.

  5. Step 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.

  6. Done

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.

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.

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.

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.

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.

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.

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.

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.

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.

More from the Archive

View All