Spaces:
Sleeping
Sleeping
| """ | |
| 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, | |
| ) | |