rag-document-qa / app_gradio.py
Amrita P
feat: add readme
3a4a51d
Raw
History Blame Contribute Delete
7.37 kB
"""
Gradio UI for the RAG Document Q&A pipeline.
Run:
source venv/bin/activate
python app_gradio.py
Opens at http://127.0.0.1:7860
"""
import os
import time
from pathlib import Path
import gradio as gr
from dotenv import load_dotenv
from ingestion.embedder import Embedder
from ingestion.pipeline import IngestionPipeline
from retrieval.index import VectorIndex
from retrieval.searcher import search
from generation.generator import Generator
load_dotenv()
# ---------------------------------------------------------------------------
# Startup — same order as main.py lifespan
# ---------------------------------------------------------------------------
print("Loading embedder model...")
embedder = Embedder()
print(f"Initialising FAISS index (dim={embedder.dimension})...")
index = VectorIndex(dimension=embedder.dimension)
print("Building ingestion pipeline...")
pipeline = IngestionPipeline(
embedder=embedder,
index=index,
strategy="recursive_character",
chunk_size=500,
overlap=100,
)
print("Initialising Groq generator...")
generator = Generator()
print("All components ready.\n")
# ---------------------------------------------------------------------------
# Callbacks
# ---------------------------------------------------------------------------
def ingest_files(files) -> tuple[str, str]:
"""Ingest uploaded PDFs and return a results markdown string + updated stats."""
if not files:
return "No files selected.", _stats_line()
lines: list[str] = []
for file in files:
path = Path(file if isinstance(file, str) else file.name)
t0 = time.perf_counter()
result = pipeline.ingest_pdf(path)
elapsed_ms = round((time.perf_counter() - t0) * 1000)
if result.error:
lines.append(f"**{path.name}** — Error: {result.error}")
else:
lines.append(
f"**{result.file}** — "
f"{result.pages} pages · **{result.chunks} chunks** · {elapsed_ms} ms"
)
return "\n\n".join(lines), _stats_line()
def answer_question(question: str, history: list[dict], expand_query: bool):
"""Retrieve chunks then stream the answer token-by-token into the chat history."""
if not question.strip():
yield history, ""
return
if index.size == 0:
history = history + [
{"role": "user", "content": question},
{
"role": "assistant",
"content": (
"No documents have been indexed yet. "
"Upload one or more PDFs in the **Upload PDFs** tab first."
),
},
]
yield history, ""
return
search_resp = search(
query=question,
embedder=embedder,
index=index,
k=8,
expand_query=expand_query,
)
results = search_resp.chunks
source_lines = [
f"- **[Source {i}]** `{r.metadata.get('source', '?')}` "
f"p. {r.metadata.get('page_num', '?')} "
f"— relevance {r.score:.3f}"
for i, r in enumerate(results, start=1)
]
sources_block = "---\n**Retrieved context:**\n" + "\n".join(source_lines)
history = history + [
{"role": "user", "content": question},
{"role": "assistant", "content": ""},
]
yield history, ""
accumulated = ""
try:
for chunk in generator.generate_answer_stream(question, results, max_score=search_resp.max_score):
accumulated += chunk
history[-1]["content"] = accumulated
yield history, ""
except Exception as exc:
history[-1]["content"] = accumulated + f"\n\n*Error generating answer: {exc}*"
yield history, ""
return
history[-1]["content"] = accumulated + "\n\n" + sources_block
yield history, ""
def clear_chat() -> tuple[list, str]:
return [], ""
def _stats_line() -> str:
return f"Index: **{index.size} vectors** across **{pipeline.chunk_count} chunks ingested**"
# ---------------------------------------------------------------------------
# UI layout
# ---------------------------------------------------------------------------
with gr.Blocks(title="RAG Document Q&A", theme=gr.themes.Soft()) as demo:
gr.Markdown(
"# RAG Document Q&A\n"
"Upload PDF documents, then ask questions. "
"Answers are grounded in the documents and cite sources inline."
)
with gr.Tab("Upload PDFs"):
gr.Markdown(
"Select one or more PDF files. Each is extracted, chunked, embedded, "
"and added to the shared FAISS index."
)
with gr.Row():
with gr.Column(scale=2):
file_input = gr.File(
label="PDF files",
file_types=[".pdf"],
file_count="multiple",
height=160,
)
ingest_btn = gr.Button("Ingest", variant="primary", size="lg")
with gr.Column(scale=3):
ingest_output = gr.Markdown(label="Ingestion results", value="Results will appear here.")
index_stats = gr.Markdown(value=_stats_line())
ingest_btn.click(
fn=ingest_files,
inputs=file_input,
outputs=[ingest_output, index_stats],
)
gr.Markdown("*Note: If you change retrieval settings, re-upload your PDFs to re-index with new chunking parameters.*")
with gr.Tab("Ask Questions"):
gr.Markdown(
"Type a question about your uploaded documents. "
"Press **Enter** or click **Ask**. "
"Answers include `[Source N]` citations linked to specific pages."
)
chatbot = gr.Chatbot(
label="Conversation",
height=500,
type="messages",
render_markdown=True,
placeholder="Ingest some PDFs, then ask a question.",
)
with gr.Row():
question_box = gr.Textbox(
placeholder="e.g. What retrieval method does RAG use?",
label="",
lines=1,
scale=5,
submit_btn=False,
)
ask_btn = gr.Button("Ask", variant="primary", scale=1, min_width=80)
with gr.Accordion("Advanced Retrieval Options", open=False):
expand_cb = gr.Checkbox(
label="Enable Adaptive Query Expansion (LLM rephrases your question only when initial search confidence is low)",
value=True
)
clear_btn = gr.Button("Clear conversation", size="sm", variant="secondary")
# Submit on button click or Enter key
ask_btn.click(
fn=answer_question,
inputs=[question_box, chatbot, expand_cb],
outputs=[chatbot, question_box],
)
question_box.submit(
fn=answer_question,
inputs=[question_box, chatbot, expand_cb],
outputs=[chatbot, question_box],
)
clear_btn.click(fn=clear_chat, outputs=[chatbot, question_box])
if __name__ == "__main__":
demo.launch(
server_name=os.environ.get("GRADIO_SERVER_NAME", "127.0.0.1"),
server_port=int(os.environ.get("GRADIO_SERVER_PORT", "7860")),
show_error=True,
)