One LangGraph pipeline that folds three retrieval patterns into a single self-correcting graph — it routes each question to the right source, grades what it retrieves, corrects itself with a rewrite and a web fallback, and verifies the answer is grounded before it ships. The full production version — runnable, with loop guards.
The Chat view — a live graph trajectory (route → grade → correct → verify), then the grounded, cited answer with its merged KB + web sources.
Most RAG retrieves once and hopes for the best. This graph makes retrieval a decision it revisits: it picks the right source, throws out passages that don't help, rewrites and reaches for the web when it comes up short, and refuses to ship an answer it can't ground in the sources — all inside a single LangGraph with bounded loops.
An LLM router sends each question to the cheapest source that can answer it — the private vectorstore, the live web_search, or a direct answer with no retrieval at all.
Every retrieved passage is graded relevant / off-topic on its own. Off-topic chunks never reach the generator, so a lucky-but-wrong match can't poison the answer.
When too few passages survive grading, the graph rewrites the question into a sharper, higher-signal query and tries again — the corrective move that rescues a bad first retrieval.
If the private corpus comes up short, the rewritten query goes to the live web (Tavily or DuckDuckGo). Web hits join the same citation space as KB passages.
Before an answer ships, a judge checks every claim is supported by the sources. A hallucination sends it back to regenerate — strictly from the evidence.
A second judge asks whether the answer actually resolves the question. If not, the graph rewrites and retries instead of handing back something off-target.
max_rewrites bounds the corrective loop; max_generations bounds regeneration; a recursion_limit backstops both. Every grader fails open, so a flaky judge can't trap the graph.
A LangGraph SqliteSaver checkpointer keeps each conversation's history server-side, so follow-ups resolve references and it all survives restarts.
Nodes emit NDJSON events; the UI renders the route, the per-passage grade chips, the corrective rewrite & web hop, and the Self-RAG verdicts as a live timeline — answer tokens stream in.
Upload .pdf, .md or .txt (or paste text); recursive chunking + sentence-transformers embeddings build the dense store the router can route to.
Model, embedding and Bedrock connection live server-side in SQLite, edited behind a password — never in the browser.
A streaming CLI (chat.py) drives the identical graph and prints the full trajectory — and shares threads.db with the web UI.
Plain RAG will confidently answer from an irrelevant chunk. This pipeline is built for the cases where that's not acceptable — where the answer has to be grounded, sourced, and worth trusting, even if it means a second pass.
Answer over private policies, reports and wikis with citations — and a groundedness gate that blocks confident hallucinations before they reach a user.
One assistant that knows when to read your documents and when to hit the live web — the router decides per question, so users don't have to.
Where every answer must trace to a source. The self-check + citations make replies auditable, and off-topic retrievals are filtered out, not paraphrased.
When the corpus doesn't cover a question, the corrective rewrite + web fallback quietly fills the gap instead of returning "I couldn't find anything."
A clean, commented example of routing, CRAG grading and Self-RAG verification wired into one graph — the canonical patterns, in one runnable place.
A small Python backend and a vanilla-JS frontend with no build step. Five tiny LLM "judges" steer a seven-node graph; the only heavyweight is the model itself.
Seven nodes and a handful of conditional edges. The graders decide the branches; the loop guards decide when to stop.
START │ ▼ route_question ─"direct"────────────▶ direct_answer ─────────────▶ END │ "web_search" ▲ │ └────────────▶ web_search ──┐ │ │ "vectorstore" │ │ ▼ ▼ │ retrieve ─▶ grade_documents ─────▶ generate ─▶ (Self-RAG grade) │ too few relevant │ │ ▼ │ grounded & useful ─▶ END transform_query ◀───────────┤ not grounded ─▶ generate (≤ max_generations) │ └─ not useful ─▶ transform_query (≤ max_rewrites) ▼ web_search ─▶ generate
One LLM call classifies the turn as vectorstore, web_search, or direct. Hard constraints are enforced in code — an empty KB never routes to the vectorstore, a disabled web tool never routes to the web — so the router can't pick an impossible branch.
The vectorstore route pulls the top-k passages, and a grader scores each one relevant or off-topic independently. Only survivors reach the generator. If fewer than a threshold survive, the graph flags a correction.
The correction rewrites the question into a sharper query, then searches the live web as a fallback source. KB and web hits flow into one citation space, so [3] means the same passage wherever it came from. The rewrite counter starts a fresh retrieval attempt.
The generator answers strictly from the selected sources, citing [n] for every factual claim, and streams token by token — natively for anthropic.*, over the OpenAI-compatible SSE endpoint for other families like Qwen.
Two more judges run on the draft. Groundedness checks it invents no facts absent from the sources — a failure regenerates. Answer relevance checks it resolves the question — a failure rewrites and retries. Only when both pass does the turn end.
max_generations caps consecutive regenerations; max_rewrites caps rewrites per turn; a recursion_limit backstops both, and every grader returns the permissive verdict when its reply can't be parsed. The graph is guaranteed to halt.
Nodes call get_stream_writer() to emit events — route, retrieve, doc_grade, grade_summary, transform, web, generate_start, text_delta, self_check, decision, sources, usage. The server forwards them as NDJSON; the UI renders the live trajectory.
Ships with a pre-seeded app_config.db and a sample index/, so the Bedrock region, key, model and a working knowledge base carry over — it runs out of the box.
# install & launch pip install -r requirements.txt python server.py # → http://127.0.0.1:8300 # then, in the browser: 1. Knowledge Base → upload .pdf / .md / .txt, Build index # optional 2. Chat → watch route → grade → correct → verify live 3. Settings → model & Bedrock connection (password: bala@143) # this overview page is served at: http://127.0.0.1:8300/overview
GET /overview (and /overview.html) — open http://127.0.0.1:8300/overview while it's running.