File size: 6,330 Bytes
e7a3876
 
 
 
7b50136
 
 
e7a3876
 
 
7b50136
e7a3876
 
 
 
7b50136
e7a3876
 
 
 
 
7b50136
 
 
 
 
e7a3876
 
7b50136
 
 
 
 
 
e7a3876
 
 
7b50136
 
e7a3876
 
 
7b50136
e7a3876
7b50136
 
e7a3876
 
7b50136
e7a3876
7b50136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e7a3876
7b50136
 
 
 
 
 
 
 
e7a3876
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b50136
 
e7a3876
 
7b50136
 
 
 
 
e7a3876
 
7b50136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e7a3876
 
7b50136
 
 
 
 
e7a3876
7b50136
 
 
 
 
 
 
 
 
 
 
 
 
e7a3876
7b50136
 
e7a3876
7b50136
 
 
 
 
e7a3876
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
"""

Gradio UI β€” RAG Document Q&A

Deployed on HuggingFace Spaces.



Supports:

  - Pre-loaded demo documents (Attention Is All You Need, RAG paper)

  - User PDF uploads β€” upload any PDF and query it instantly

"""

import os
import tempfile
from pathlib import Path
import gradio as gr
from src.generation.rag_chain import RAGChain
from src.retrieval.vector_store import VectorStore
from src.ingestion.pdf_loader import load_pdf
from src.ingestion.chunker import chunk_pages
from src.retrieval.embedder import Embedder
from src.utils.logger import logger


# ------------------------------------------------------------
# Startup β€” ingest demo documents if store is empty
# ------------------------------------------------------------

def ensure_demo_ingested():
    store = VectorStore()
    if store.collection.count() > 0:
        logger.info(f"Vector store has {store.collection.count()} chunks β€” skipping demo ingestion")
        return store.collection.count()

    logger.info("Ingesting demo documents...")
    from src.ingestion.pdf_loader import load_pdfs_from_dir
    from src.ingestion.chunker import chunk_pages

    pages = load_pdfs_from_dir("data/raw")
    if not pages:
        logger.warning("No demo PDFs found in data/raw/")
        return 0

    chunks = chunk_pages(pages)
    embedder = Embedder()
    embeddings = embedder.embed_texts([c["text"] for c in chunks])
    store.add_chunks(chunks, embeddings)
    logger.info(f"Demo ingestion complete β€” {len(chunks)} chunks")
    return len(chunks)


demo_chunks = ensure_demo_ingested()
chain = RAGChain()
embedder = Embedder()


# ------------------------------------------------------------
# PDF upload handler
# ------------------------------------------------------------

def ingest_pdf(file) -> str:
    """

    Ingest a user-uploaded PDF into the vector store.

    Adds to existing chunks β€” doesn't reset the store.

    """
    if file is None:
        return "No file uploaded."

    try:
        path = Path(file.name)
        logger.info(f"User uploaded: {path.name}")

        pages = load_pdf(path)
        if not pages:
            return f"Could not extract text from {path.name}. Is it a scanned PDF?"

        chunks = chunk_pages(pages)
        embeddings = embedder.embed_texts([c["text"] for c in chunks])

        store = VectorStore()
        store.add_chunks(chunks, embeddings)

        total = store.collection.count()
        return (
            f"βœ… **{path.name}** ingested successfully!\n\n"
            f"- Pages extracted: {len(pages)}\n"
            f"- Chunks created: {len(chunks)}\n"
            f"- Total chunks in store: {total}\n\n"
            f"You can now ask questions about this document."
        )

    except Exception as e:
        logger.error(f"Ingestion error: {e}")
        return f"❌ Error ingesting file: {str(e)}"


# ------------------------------------------------------------
# Query handler
# ------------------------------------------------------------

def answer_question(question: str) -> tuple[str, str]:
    if not question.strip():
        return "Please enter a question.", ""

    result = chain.query(question)
    answer = result["answer"]

    sources_lines = ["**Sources retrieved:**\n"]
    for i, s in enumerate(result["sources"], start=1):
        sources_lines.append(
            f"{i}. `{s['source']}` β€” Page {s['page']} (similarity: {s['score']})"
        )
    sources_md = "\n".join(sources_lines)
    return answer, sources_md


# ------------------------------------------------------------
# Gradio UI
# ------------------------------------------------------------

with gr.Blocks(title="RAG Document Q&A", theme=gr.themes.Soft()) as demo:

    gr.Markdown("""

    # RAG Document Q&A

    Ask questions about documents using Retrieval-Augmented Generation.

    

    **Pre-loaded:** Attention Is All You Need + RAG paper (Lewis et al., 2020)

    

    **Or upload your own PDF** and query it instantly.

    """)

    with gr.Tab("Ask a question"):
        with gr.Row():
            question_box = gr.Textbox(
                label="Your question",
                placeholder="e.g. What is the attention mechanism? How does RAG work?",
                lines=2,
            )

        submit_btn = gr.Button("Ask", variant="primary", size="lg")

        with gr.Row():
            answer_box = gr.Textbox(
                label="Answer",
                lines=8,
                interactive=False,
            )
            sources_box = gr.Markdown(label="Sources")

        gr.Examples(
            examples=[
                "What is the attention mechanism in transformers?",
                "What is multi-head attention?",
                "How does RAG combine retrieval and generation?",
                "What datasets were used to evaluate RAG?",
                "What is the encoder-decoder architecture?",
            ],
            inputs=question_box,
        )

        submit_btn.click(
            fn=answer_question,
            inputs=[question_box],
            outputs=[answer_box, sources_box],
        )

    with gr.Tab("Upload your PDF"):
        gr.Markdown("""

        ### Upload a PDF to query

        Upload any PDF document and it will be ingested into the vector store.

        You can then ask questions about it in the **Ask a question** tab.

        

        **Note:** Uploaded documents are added to the existing store alongside the demo papers.

        Scanned PDFs (image-only) are not supported β€” the PDF must have extractable text.

        """)

        file_upload = gr.File(
            label="Upload PDF",
            file_types=[".pdf"],
        )
        upload_btn = gr.Button("Ingest PDF", variant="primary")
        upload_status = gr.Markdown(label="Status")

        upload_btn.click(
            fn=ingest_pdf,
            inputs=[file_upload],
            outputs=[upload_status],
        )

    gr.Markdown(
        "_Built with sentence-transformers, ChromaDB, and Groq (Llama 3.1 8B). "
        "[View source on GitHub](https://github.com/OmUniyal/rag-document-qa)_"
    )

if __name__ == "__main__":
    demo.launch()