A LangGraph map-reduce that fans out one parallel, typed extraction per document, then synthesizes them into a comparison table. The pattern behind “compare these proposals / contracts / papers.”
| Field | Acme Systems | Bluepeak | Cirrus Labs |
|---|---|---|---|
| Vendor | Acme Systems Inc. | Bluepeak Consulting | Cirrus Labs |
| Total cost | $240,000 fixed | ~$310k (cap $360k) | $275,000 fixed |
| Timeline | 16 weeks | 22 weeks | 18 weeks |
| Has SLA | Yes | Yes | Yes |
| Summary | Fastest & fixed price | Deepest modernization | Balanced middle option |
The Compare view — live map-reduce trajectory, the parallel fan-out grid, and the synthesized comparison table.
Give it several proposals, contracts, papers or resumes and a set of fields to pull. It extracts a typed record from each document in parallel, lays them side by side, and writes a recommendation — turning “read ten documents and build a table” into a single, defensible run.
The branch count isn’t known until runtime — fan_out returns one Send per document. Three docs or forty, only the list changes.
Each field is typed (text / number / boolean / list). Extraction runs against a JSON Schema and every value is coerced — a list stays a list, a bool stays a bool.
A join node runs once, after every branch, and reduces the records into a documents × fields table plus a focus-driven narrative.
Over-budget documents aren’t blindly truncated — each branch retrieves the most relevant passages of its own document (TF-IDF cosine), or falls back to truncation.
Branches run concurrently on a thread pool, so N docs finish in about the time of the slowest — with a max_concurrency cap so forty docs drain in waves, not one stampede.
Nodes emit NDJSON events; the UI renders the plan, the fan-out grid filling in per document, and the synthesis streaming token by token.
Start from a preset (proposals, contracts, papers, resumes, products) or define your own typed fields — and pick which documents to include.
Upload .pdf, .docx, .txt or .md, or paste text. Each document is parsed whole and kept in a persisted set.
Copy the comparison as Markdown or download it as CSV — drop it straight into a doc, sheet or ticket.
Model and Bedrock connection live server-side in SQLite, edited behind a password — never in the browser.
Any time the answer is a matrix of “document × attribute,” this is the shape of the work. It’s faster than reading each document in turn, more consistent than doing it by hand, and the structure is auditable — every cell traces to one document.
Line up price, timeline, deliverables, strengths and risks across bids and get a value recommendation.
Surface term length, termination, liability caps and governing law — including clauses buried deep in long PDFs.
Compare research questions, methods, datasets and findings to scan a literature set at a glance.
Extract years of experience, skills, achievements and gaps to shortlist against a role consistently.
Position competing products on pricing, features, integrations and limitations to recommend the right fit.
A small, dependency-light Python backend and a vanilla-JS frontend with no build step. The only heavyweight is the model itself.
Three nodes: plan fans out, extract_document runs per document in parallel, and synthesize joins them once.
START │ ▼ plan ──(dynamic fan-out: one Send per document)──▶ extract_document ✕ N (MAP · parallel) │ ▼ synthesize ──▶ END (REDUCE · runs once)
The number of parallel branches equals len(documents) — unknown at build time. A conditional edge returns a list of Send("extract_document", payload), one per document. LangGraph schedules them in a single superstep.
Each branch builds a JSON Schema from the requested fields and calls the model for structured output: native tool-use for anthropic.*, a strict-JSON prompt for others. Values are coerced to their declared types. Sync nodes run on a thread pool, so the per-document Bedrock calls genuinely overlap.
One document failing to parse never sinks the run — its column is flagged and the rest proceed.
When a document is over the per-document budget, the branch chunks its own text, ranks the chunks against a query derived from the fields (TF-IDF cosine), and feeds only the most relevant passages — in document order. The “metadata filter per document” is inherent: a branch only ever sees its own chunks. Truncation remains as a fallback.
Each branch appends its record to an operator.add channel, so parallel writes gather instead of clobbering. A max_concurrency cap bounds how many branches run at once — so forty documents don’t fire forty simultaneous calls; they drain in waves.
extract_document has a single outgoing edge to synthesize, so it runs exactly once, after every branch joins. It sorts the out-of-order results back into document order, builds the documents × fields table, and streams a focus-driven recommendation.
Nodes call get_stream_writer() to emit events — plan, fanout, extract_start, extract_done, table, synthesize_start, text_delta, usage, done. The server forwards them as NDJSON; every extraction event carries the document id, so the UI routes concurrent events to the right card.
Ships with a pre-seeded app_config.db, so the Bedrock region, key and model carry over — it runs out of the box.
# install & launch pip install -r requirements.txt python server.py # → http://127.0.0.1:8320 # then, in the browser: 1. Documents → upload ≥ 2 files (.pdf / .docx / .txt / .md) 2. Compare → pick a preset, tune typed fields & focus, Run comparison 3. Settings → model & Bedrock connection (password: bala@143)