Agentic RAG LangGraph · Bedrock · Tools

Agentic Multi-Source RAG

A LangGraph tool-calling agent that decides whether to retrieve, searches a private knowledge base and the live web as tools, chains them multi-hop, and streams a grounded, cited answer. The pattern behind “answer this — and go find what you need.”

Decide Search (KB · Web) Multi-hop Grounded answer
Retrieval as a tool Model-decided retrieval KB + live web Multi-hop tool chains Cited & grounded Live NDJSON streaming
127.0.0.1:8200 · Chat

Chat

qwen.qwen3-235b-a22b 🌐 Tavily 128 chunks
What’s Acme’s refund policy, and when do they report Q2 earnings?
Agent trajectory
Needs evidence — retrieving2 sources
Not chit-chat: one private-doc question, one current-events question.
search_knowledge_baseKB4 passages
“Acme refund policy”
search_webWEB5 results
“Acme Corp Q2 2026 earnings date” — KB had no date, so hop to the web.
Composing a grounded answer…
Acme offers a 30-day money-back guarantee on all plans, refunded to the original payment method within 5–7 business days [1][2]. Their Q2 2026 earnings are scheduled for July 30, 2026, after market close [3].
[1]KBrefund-policy.pdf · p.2
“Customers may request a full refund within thirty (30) days of purchase…”
[3]WEBAcme Investor Relations
Acme Corp to report Q2 2026 results on Thursday, July 30, 2026.
investors.acme.example/q2-2026

The Chat view — the agent’s decision, its KB → web tool trajectory, and the streamed answer with a shared citation space.

What it is

Retrieval the model chooses, not a pipeline that always fires

Classic RAG retrieves on every turn. Here retrieval is a set of tools the model calls on its own initiative — over two sources, chained across as many hops as the question needs — and skips entirely when the question doesn’t need evidence. The answer is grounded and cited; follow-ups keep context.

Retrieval as a tool

The model is handed search_knowledge_base and search_web and calls them itself, writing its own queries — not a fixed retrieve step wired into the graph.

Model-decided retrieval

Greetings, thanks, opinions and arithmetic are answered directly — no search. Only questions that need evidence trigger a tool call, shown as an “answered directly” chip when skipped.

Two knowledge sources

A private dense-vector KB of your uploaded docs, and the live web (Tavily or DuckDuckGo). The model picks the right one — or both.

One shared citation space

KB and web hits pass through a single registry that hands out global [n] numbers, so [3] means the same passage whatever its origin or hop.

Multi-hop tool chains

The graph loops agent ⇄ tools: search, read the results, spot a gap, search again — KB → web or web → refined web — before committing to an answer.

Bounded, never runaway

A per-turn hop budget removes the tools on the last round to force a final grounded answer, and a recursion_limit backstops it — so an eager agent always terminates.

Per-thread memory

Each conversation is a thread persisted by a LangGraph SqliteSaver checkpointer — follow-ups keep context across requests and restarts.

Live streaming trajectory

Nodes emit NDJSON events; the UI renders each tool step as it runs, the sources as they land, and the answer streaming token by token.

Build your knowledge base

Upload .pdf / .txt / .md or paste text, tune chunking, and build a persisted dense index with local sentence-transformers embeddings.

Grounded & verifiable

When tools ran, every factual sentence cites its source and the model won’t invent a number that wasn’t returned — with a Test connection check in Settings.

Password-protected settings

Model, Bedrock connection, embedding model and the optional Tavily key live server-side in SQLite, edited behind a password — never in the browser.

Also a CLI

The same agent drives a streaming terminal loop that shares the thread store with the web UI — --no-web for KB-only, --show-sources to print citations.

Why it’s useful

Answers that pull from your docs and the world — only when needed

Whenever the right answer might live in private documents, on the public web, or both, this is the shape of the work. It’s cheaper than retrieving on every turn, more current than a static index, and every claim traces back to a source you can open.

Research assistant over private + public

Ask across your uploaded reports and the live web in one turn — the agent grounds the private parts in your KB and fills current facts from the web.

Support & internal ops

Answer from policy and product docs, and reach for the web only when the KB is silent or stale — no forced retrieval on “hi, can you help?”

Competitive & market analysis

Combine an internal brief with fresh web results, then chain follow-up searches to fill the gaps — each fact cited to its origin.

Current-events grounding

For anything recent, the model reaches past its training cutoff to the live web and cites the pages — instead of guessing from memory.

A teaching reference for agentic RAG

A compact, readable implementation of tool-decided retrieval, multi-source citations and multi-hop loops on LangGraph — easy to fork and extend.

Tech stack

What it’s built on

A small, dependency-light Python backend and a vanilla-JS frontend with no build step. The only heavyweight is the model itself.

Orchestration
LangGraph 1.xStateGraphagent ⇄ tools loopSqliteSaver checkpointerstream_mode="custom"
Model / inference
Amazon BedrockAnthropic MantleClaude (streamed tool use)Qwen (OpenAI-compat)
Knowledge sources
sentence-transformers KBcosine retrievalTavilyDuckDuckGo
Backend
FastAPIUvicornPydanticNDJSON streaming
Ingestion & storage
pypdfSQLitepersisted dense indexPBKDF2 auth
Frontend
Vanilla JSNo build stepStreaming fetch readerManrope · JetBrains Mono
How it works

One graph, an agent that loops through tools

Two nodes: agent runs one LLM turn with the tools available, and tools executes whatever it asked for — a conditional edge loops them until the model answers.

              ┌───────────────── loop: multi-hop ─────────────────┐
              ▼                                                   │
START ─▶ agent ──has tool calls?──▶ tools ──▶ search_knowledge_base · search_web
              │                                    (results, cited [n])
   no tool calls │
              ▼
      grounded, cited answer ─▶ END          chit-chat: answer directly, no retrieval

Decide — retrieve, or just answer

The agent node runs one streamed LLM turn with the two tools available. If it returns no tool calls, that is the answer — the chit-chat / general-knowledge path — and the graph ends. The system prompt tells it to skip retrieval when the question needs no evidence.

Call a tool — with its own query

When it decides to retrieve, the model emits a tool call and writes the search query itself, resolving pronouns from the conversation (“and its pricing?” → “Acme Cloud pricing”). It picks search_knowledge_base for your docs, search_web for current or general facts — or both.

Execute & register sources

The tools node runs each call — dense cosine search over the KB, or a Tavily / DuckDuckGo web query — and funnels every hit through one SourceRegistry. Each hit gets the next global [n], so the numbers the model reads are exactly the ones the UI shows.

Loop for more — until it’s enough

A conditional edge routes back to agent after every tools round, so the model can read what it found and search again — that’s the multi-hop chain. A per-turn hop budget drops the tools on the final round to force an answer, and a recursion_limit guarantees termination.

Answer, grounded and cited

On the turn with no tool calls, the model streams the answer, citing every factual sentence with the [n] markers from the shared registry — refusing to invent a citation that wasn’t returned. If nothing was found, it says so plainly.

Remember & stream

The whole exchange — tool calls, results, answer — persists per thread_id via the SqliteSaver checkpointer, so follow-ups keep context. Nodes emit stage, tool_call, tool_result, text_delta, no_retrieval and usage events as NDJSON; the browser and CLI render the same stream.

⚡ The pipeline never forces a search. Ask “hi” and it just replies; ask something real and it retrieves, chains, and cites — the model draws the line.
Get started

Run it in two commands

Ships with a pre-seeded app_config.db, so the Bedrock region, key, model and Tavily key carry over — it runs out of the box.

# install & launch
pip install -r requirements.txt
python server.py                 # → http://127.0.0.1:8200

# then, in the browser:
1. Knowledge Base → upload .pdf / .txt / .md (optional — web works without it)
2. Chat          → try “hi” (no search) vs. a real question (KB + web, multi-hop)
3. Settings      → model, Bedrock & Tavily connection (password: bala@143)

# or drive the same agent from the terminal
python chat.py --show-sources