Conversational RAG Condense · Retrieve · Generate

Conversational RAG with Memory

A LangGraph chat over your documents where follow-ups just work. It condenses each follow-up into a standalone question, keeps per-thread memory through a checkpointer, and streams a grounded, cited answer. The pattern behind “chat with my docs — and remember what we just said.”

Condense Retrieve Generate (stream) Thread memory
Condense-then-retrieve Thread memory (checkpointer) Grounded citations Live token streaming
127.0.0.1:8100 · Chat

Chat

qwen.qwen3-235b-a22b 128 chunks indexed
New
Pricing & plans3 turns · 2m
Onboarding steps1 turn · 1h
Refund policy4 turns · Tue
And what about its pricing?
Standalone question What is the pricing of the Pro plan?
Condensed follow-uprewritten
Retrieved 2 passagesdense
Generating grounded answerstreaming
The Pro plan is $29 per user / month billed annually 1, and includes unlimited projects and priority support 2. Monthly billing is $35 per user
1pricing.md · chunk 3sim 0.58

Pro — $29/user/mo billed annually ($35 monthly). Includes unlimited projects…

2plans.pdf · p.2sim 0.51

Priority support and SSO are available on Pro and Enterprise tiers…

The Chat view — the conversation sidebar (memory threads), the amber standalone-question chip, the condense → retrieve → generate trajectory, and a streamed, cited answer.

What it is

Retrieval that remembers the conversation

Plain RAG retrieves on whatever you just typed — so “and its pricing?” matches on the word “pricing” and loses the thread. This app first rewrites the follow-up into a standalone question using the conversation, retrieves on that, and answers grounded in the sources with [n] citations — while a checkpointer keeps each conversation’s memory separate and durable.

Condense-then-retrieve

The condense node rewrites each follow-up into a self-contained query, resolving “it / that / the second one” against the history — so retrieval sees intent, not pronouns. Turn one is passed through untouched.

Thread memory via checkpointer

The graph compiles with a LangGraph SqliteSaver. Every run is keyed by thread_id, so history accumulates per conversation and survives restarts — no history re-sent from the browser.

Grounded answers with citations

Generation is instructed to use only the retrieved sources and end each factual sentence with a [n] marker — every claim traces back to a passage, and “not in the sources” is said plainly.

Dense semantic retrieval

Local sentence-transformers embeddings with cosine similarity over a normalized matrix — fast, offline, and persisted to disk so the index survives restarts.

Streaming, end to end

Nodes emit events through get_stream_writer()stage, condensed, sources, text_delta, usage. One stream_mode="custom" feed drives both the browser and the CLI.

Multiple conversations

Start, switch and delete independent memory threads from a sidebar. Reopen any thread and its full history reloads from the checkpointer — no client-side transcript needed.

Multi-format ingestion

Upload .pdf, .txt or .md, or paste text. A from-scratch recursive splitter chunks each document with tunable size & overlap.

Same graph in the terminal

chat.py drives the exact same graph with streaming in the shell. Because it shares threads.db, a conversation you start in the CLI shows up in the web sidebar, and vice versa.

Password-protected settings

Model and Bedrock connection live server-side in SQLite behind a PBKDF2 password — never in the browser. Config seeds from .env once, then the DB is the source of truth.

Why it’s useful

The natural way to ask about a corpus

Real questions come in threads: an answer, then “why?”, then “what about the other one?”. Anywhere people explore documents by conversation rather than one-shot search, condense-then-retrieve plus durable memory is the shape of the work — and every answer stays auditable back to a source.

Support & product assistants

Answer from help docs and policies, then handle the inevitable follow-ups without losing what “it” refers to.

Internal knowledge base Q&A

Let a team interrogate runbooks, wikis and specs conversationally — with citations so answers can be trusted and checked.

Research & literature exploration

Dig into a paper set turn by turn — “summarize this”, “how does it compare?”, “what were the limitations?” — with context carried forward.

Onboarding & training

New hires ask their way through policies and playbooks; each person’s thread remembers where their conversation left off.

Contract & policy lookup

Ask about terms and clauses in long PDFs and keep drilling in — the standalone rewrite pulls the right passage each turn.

Tech stack

What it’s built on

A small, dependency-light Python backend and a vanilla-JS frontend with no build step. Retrieval is a local embedding model; the only remote call is the LLM on Bedrock.

Orchestration
LangGraph 1.xStateGraphSqliteSaver checkpointeradd_messages reducerstream_mode="custom"
Model / inference
Amazon BedrockAnthropic MantleClaudeQwen (OpenAI-compat)
Backend
FastAPIUvicornPydanticNDJSON streaming
Ingestion & retrieval
pypdfsentence-transformerscosine similarityrecursive chunker
Storage
SQLite settingsthreads.db (checkpoints)npy + json indexPBKDF2 auth
Frontend & CLI
Vanilla JSNo build stepStreaming fetch readerchat.py (terminal)
How it works

Three nodes, one memory

The graph is condense → retrieve → generate, compiled with a checkpointer that threads memory through every turn.

          ┌─────────────── thread memory (SqliteSaver, keyed by thread_id) ───────────────┐
          │                                                                               │
  START ──▼──▶ condense ──▶ retrieve ──▶ generate ──▶ END
              rewrite follow-up   dense search    stream grounded
              → standalone Q      (top-k)         answer + [n] cites

Condense the follow-up

On turn one the question is used as-is. On later turns the condense node feeds a bounded window of prior turns to the model and asks for a single standalone question — resolving pronouns and references before anything is retrieved. The rewrite is streamed to the UI as the amber “standalone question” chip.

Retrieve on intent, not keywords

The standalone question is embedded with the same local sentence-transformers model the index was built with, and scored by cosine similarity over the normalized embedding matrix. The top-k passages become the numbered SOURCES, streamed to the browser as source cards.

Generate a grounded, cited answer

The generate node sends the recent history plus a SOURCES + QUESTION prompt and streams tokens back. The system prompt forbids outside knowledge and requires a [n] citation on every factual sentence; if the sources don’t cover it, it says so.

Persist the turn to the thread

The messages channel uses the add_messages reducer, so the new human turn and the model’s reply append to the thread automatically. Because the graph runs with a SqliteSaver checkpointer keyed by thread_id, the next turn reloads the full history with no transcript sent from the client — and it all survives a restart.

Stream it everywhere

Every node calls get_stream_writer() to emit stage, condensed, sources, text_delta and usage events. The FastAPI server forwards them to the browser as NDJSON; chat.py renders the same events in the terminal — one graph, two front-ends.

⚡ The browser never sends the chat history — it sends one question and a thread_id. Memory lives in the checkpointer, so a page reload (or a server restart) loses nothing.
Get started

Run it in two commands

Ships with a pre-seeded app_config.db (copied from the sibling RAG Pipeline), 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:8100

# then, in the browser:
1. Knowledge Base → upload .pdf / .txt / .md (or paste), Build Index
2. Chat           → ask a question, then a follow-up — memory + condense kick in
3. Settings       → model & Bedrock connection (password-protected)

# …or the same graph in the terminal:
python chat.py                   # streaming CLI; shares threads.db with the web app
📄 This page is served by the app itself at /overview — e.g. http://127.0.0.1:8100/overview.