Issue #43 adds app.py: a Streamlit web app that lets a user ask a question and instantly compare two answers:
Panel
What it does
Without RAG
Sends the question directly to llama3.2 with no retrieval context
With RAG
Retrieves the top-4 chunks from the CBI speeches corpus via ChromaDB, injects them into the prompt, and generates a grounded answer
The goal is to make the difference between the two approaches visible and interactive — the kind of live demo that is harder to convey in a static report.
How to run
# 1. Start Ollamaollama serve# 2. Confirm corpus is loadedpython scripts/ollama_ready.py# 3. Launch the appuv run streamlit run app.py
The app opens at http://localhost:8501.
The comparison logic
The core of the app is three calls: one to ChatOllama for the baseline, one to ask() for the RAG answer, and one read of the confidence_signal dict to surface how grounded the RAG answer is.
from unittest.mock import MagicMockfrom rag.query import ask, confidence_signal, ABSTAIN_PHRASE# -- Synthetic setup (no Ollama required) -----------------------------------def _make_doc(content, source="cbi_speeches.csv", speaker="Governor Lane"): doc = MagicMock() doc.page_content = content doc.metadata = {"source": source, "speaker": speaker, "date": "2023-06-12"}return docSYNTHETIC_DOCS = [ _make_doc("Inflation in the euro area fell to 2.9% in December 2023."), _make_doc("The Governing Council raised rates by 25 basis points in July 2023."), _make_doc("Price stability remains the primary mandate of the ECB."), _make_doc("Core inflation, excluding energy and food, remained elevated."),]def _make_chain(answer, docs=SYNTHETIC_DOCS): chain = MagicMock() chain.invoke.return_value = {"answer": answer,"source_docs": docs,"context": "","question": "", }return chainQUESTION ="What happened to inflation in the euro area in 2023?"# -- Baseline (no RAG) --- simulated raw LLM responseBASELINE_ANSWER = ("Inflation in the euro area declined significantly in 2023 from its 2022 peak, ""driven by falling energy prices, though core inflation remained sticky. ""The ECB raised interest rates several times during the year.")# -- RAG answer --- grounded in retrieved chunksRAG_ANSWER ="Inflation in the euro area fell to 2.9% in December 2023."rag_chain = _make_chain(RAG_ANSWER)rag_result = ask(rag_chain, QUESTION)print("QUESTION:", QUESTION)print()print("WITHOUT RAG (baseline LLM):")print(f" {BASELINE_ANSWER}")print()print("WITH RAG:")print(f" {rag_result['answer']}")
QUESTION: What happened to inflation in the euro area in 2023?
WITHOUT RAG (baseline LLM):
Inflation in the euro area declined significantly in 2023 from its 2022 peak, driven by falling energy prices, though core inflation remained sticky. The ECB raised interest rates several times during the year.
WITH RAG:
Inflation in the euro area fell to 2.9% in December 2023.
Confidence signal output
The RAG panel also shows the confidence signal so the user knows how grounded the answer is.
When the model cannot find an answer in the corpus, is_uncertain flips to True and the app shows a warning rather than a confident-looking wrong answer.
Code
uncertain_chain = _make_chain(ABSTAIN_PHRASE, docs=[])uncertain_result = ask(uncertain_chain, "What is the capital of Mars?")conf_u = uncertain_result["confidence"]print(f"Answer : {uncertain_result['answer']}")print(f"is_uncertain: {conf_u['is_uncertain']}")print(f"n_sources : {conf_u['n_sources']}")
Answer : I don't know based on the available speeches.
is_uncertain: True
n_sources : 0
Retrieved sources display
The app renders source metadata in an expandable panel.
Code
print(f"Retrieved {len(rag_result['sources'])} chunks:\n")for i, src inenumerate(rag_result["sources"], 1): title = src.get("title") or src.get("source", "unknown") speaker = src.get("speaker", "") date = src.get("date", "") meta =" · ".join(p for p in [speaker, date] if p)print(f" {i}. {title}")if meta:print(f" {meta}")
Retrieved 4 chunks:
1. cbi_speeches.csv
Governor Lane · 2023-06-12
2. cbi_speeches.csv
Governor Lane · 2023-06-12
3. cbi_speeches.csv
Governor Lane · 2023-06-12
4. cbi_speeches.csv
Governor Lane · 2023-06-12
What this shows
The side-by-side format makes three things visible at once:
Hallucination risk — the baseline LLM may produce plausible but unfounded claims; the RAG answer is constrained to retrieved text.
Grounding — the confidence signal and source panel show exactly which documents the RAG answer came from, making the answer auditable.
Failure mode — when the corpus does not contain a relevant answer, the RAG system says so explicitly rather than guessing.
This is the kind of live evidence a non-technical stakeholder can engage with directly.
Source Code
---title: "Live Demo App"subtitle: "Baseline LLM vs RAG — side-by-side comparison"date: todaydate-format: "DD MMMM YYYY"number-sections: false---## What this page coversIssue [#43](https://github.com/almac09/private-rag-assistant/issues/43) adds `app.py`:a Streamlit web app that lets a user ask a question and instantly compare two answers:| Panel | What it does ||-------|-------------|| **Without RAG** | Sends the question directly to `llama3.2` with no retrieval context || **With RAG** | Retrieves the top-4 chunks from the CBI speeches corpus via ChromaDB, injects them into the prompt, and generates a grounded answer |The goal is to make the difference between the two approaches visible and interactive —the kind of live demo that is harder to convey in a static report.---## How to run```powershell# 1. Start Ollamaollama serve# 2. Confirm corpus is loadedpython scripts/ollama_ready.py# 3. Launch the appuv run streamlit run app.py```The app opens at `http://localhost:8501`.---## The comparison logicThe core of the app is three calls: one to `ChatOllama` for the baseline, one to`ask()` for the RAG answer, and one read of the `confidence_signal` dict tosurface how grounded the RAG answer is.```{python}#| code-fold: falsefrom unittest.mock import MagicMockfrom rag.query import ask, confidence_signal, ABSTAIN_PHRASE# -- Synthetic setup (no Ollama required) -----------------------------------def _make_doc(content, source="cbi_speeches.csv", speaker="Governor Lane"): doc = MagicMock() doc.page_content = content doc.metadata = {"source": source, "speaker": speaker, "date": "2023-06-12"}return docSYNTHETIC_DOCS = [ _make_doc("Inflation in the euro area fell to 2.9% in December 2023."), _make_doc("The Governing Council raised rates by 25 basis points in July 2023."), _make_doc("Price stability remains the primary mandate of the ECB."), _make_doc("Core inflation, excluding energy and food, remained elevated."),]def _make_chain(answer, docs=SYNTHETIC_DOCS): chain = MagicMock() chain.invoke.return_value = {"answer": answer,"source_docs": docs,"context": "","question": "", }return chainQUESTION ="What happened to inflation in the euro area in 2023?"# -- Baseline (no RAG) --- simulated raw LLM responseBASELINE_ANSWER = ("Inflation in the euro area declined significantly in 2023 from its 2022 peak, ""driven by falling energy prices, though core inflation remained sticky. ""The ECB raised interest rates several times during the year.")# -- RAG answer --- grounded in retrieved chunksRAG_ANSWER ="Inflation in the euro area fell to 2.9% in December 2023."rag_chain = _make_chain(RAG_ANSWER)rag_result = ask(rag_chain, QUESTION)print("QUESTION:", QUESTION)print()print("WITHOUT RAG (baseline LLM):")print(f" {BASELINE_ANSWER}")print()print("WITH RAG:")print(f" {rag_result['answer']}")```---## Confidence signal outputThe RAG panel also shows the confidence signal so the user knows how groundedthe answer is.```{python}conf = rag_result["confidence"]print(f"is_uncertain : {conf['is_uncertain']}")print(f"n_sources : {conf['n_sources']} chunks retrieved")print(f"has_sources : {conf['has_sources']}")print(f"unique_documents: {conf['unique_documents']}")```---## Uncertain answer exampleWhen the model cannot find an answer in the corpus, `is_uncertain` flips to `True`and the app shows a warning rather than a confident-looking wrong answer.```{python}uncertain_chain = _make_chain(ABSTAIN_PHRASE, docs=[])uncertain_result = ask(uncertain_chain, "What is the capital of Mars?")conf_u = uncertain_result["confidence"]print(f"Answer : {uncertain_result['answer']}")print(f"is_uncertain: {conf_u['is_uncertain']}")print(f"n_sources : {conf_u['n_sources']}")```---## Retrieved sources displayThe app renders source metadata in an expandable panel.```{python}print(f"Retrieved {len(rag_result['sources'])} chunks:\n")for i, src inenumerate(rag_result["sources"], 1): title = src.get("title") or src.get("source", "unknown") speaker = src.get("speaker", "") date = src.get("date", "") meta =" · ".join(p for p in [speaker, date] if p)print(f" {i}. {title}")if meta:print(f" {meta}")```---## What this showsThe side-by-side format makes three things visible at once:1. **Hallucination risk** — the baseline LLM may produce plausible but unfounded claims; the RAG answer is constrained to retrieved text.2. **Grounding** — the confidence signal and source panel show exactly which documents the RAG answer came from, making the answer auditable.3. **Failure mode** — when the corpus does not contain a relevant answer, the RAG system says so explicitly rather than guessing.This is the kind of live evidence a non-technical stakeholder can engage with directly.