Krippa commited on
Commit
eb22b1f
Β·
0 Parent(s):

Deploy RAG app with Groq LFS

Browse files
Files changed (8) hide show
  1. .gitattributes +1 -0
  2. .gitignore +12 -0
  3. README.md +36 -0
  4. app.py +176 -0
  5. build_index.py +99 -0
  6. faiss_index/chunks.json +0 -0
  7. faiss_index/index.faiss +3 -0
  8. requirements.txt +5 -0
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.faiss filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Data
2
+ data/
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+ .env
9
+
10
+ # IDE
11
+ .vscode/
12
+ .idea/
README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SQL Books RAG
3
+ emoji: πŸ“š
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: "6.6.0"
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ # πŸ“š SQL Books RAG
13
+
14
+ A Retrieval-Augmented Generation (RAG) system that answers SQL questions using content from 5 SQL textbooks.
15
+
16
+ ## How It Works
17
+
18
+ 1. **Retrieval** – Your question is embedded using `all-MiniLM-L6-v2` and matched against a FAISS index of ~500-word chunks extracted from the books.
19
+ 2. **Generation** – The top-5 most relevant chunks are fed as context to `Llama 3.1-8B` via the Groq API to produce a detailed, grounded answer.
20
+
21
+ ## Data Sources
22
+
23
+ | Book | Author |
24
+ |------|--------|
25
+ | Practical SQL: A Beginner's Guide to Storytelling with Data | Anthony DeBarros |
26
+ | SQL for Data Scientists | Renee M. Teate |
27
+ | SQL for Data Analysis | Cathy Tanimura |
28
+ | The Art of SQL | StΓ©phane Faroult |
29
+ | Learning SQL: Generate, Manipulate, and Retrieve Data | Alan Beaulieu |
30
+
31
+ ## Tech Stack
32
+
33
+ - **Embeddings**: `sentence-transformers/all-MiniLM-L6-v2`
34
+ - **Vector Store**: FAISS (IndexFlatL2)
35
+ - **LLM**: `Llama 3.1-8B-Instant` via Groq API (free tier)
36
+ - **UI**: Gradio
app.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py - SQL Books RAG application powered by Gradio.
3
+ Retrieves relevant chunks from a FAISS index and generates answers
4
+ using Llama 3 via the Groq API (free tier).
5
+ """
6
+
7
+ import json
8
+ import os
9
+
10
+ import faiss
11
+ import gradio as gr
12
+ import numpy as np
13
+ import requests
14
+ from sentence_transformers import SentenceTransformer
15
+
16
+ # -- Configuration -------------------------------------------------------------
17
+ INDEX_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "faiss_index")
18
+ EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
19
+ GEN_MODEL = "llama-3.1-8b-instant"
20
+ API_URL = "https://api.groq.com/openai/v1/chat/completions"
21
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
22
+ TOP_K = 5
23
+
24
+ # -- Load resources once at startup --------------------------------------------
25
+ print("Loading embedding model ...")
26
+ embedder = SentenceTransformer(EMBED_MODEL)
27
+
28
+ print("Loading FAISS index ...")
29
+ index = faiss.read_index(os.path.join(INDEX_DIR, "index.faiss"))
30
+
31
+ print("Loading chunk metadata ...")
32
+ with open(os.path.join(INDEX_DIR, "chunks.json"), "r", encoding="utf-8") as f:
33
+ chunks = json.load(f)
34
+
35
+ print(f"Ready: {index.ntotal} vectors, {len(chunks)} chunks")
36
+ print(f"LLM: {GEN_MODEL} via Groq API")
37
+ print("App ready.")
38
+
39
+
40
+ # -- RAG pipeline ---------------------------------------------------------------
41
+ def retrieve(query: str, top_k: int = TOP_K):
42
+ """Embed the query and retrieve the top-k most similar chunks."""
43
+ query_vec = embedder.encode([query]).astype("float32")
44
+ distances, indices = index.search(query_vec, top_k)
45
+ results = []
46
+ for dist, idx in zip(distances[0], indices[0]):
47
+ if idx < len(chunks):
48
+ results.append({
49
+ "text": chunks[idx]["text"],
50
+ "source": chunks[idx]["source"],
51
+ "distance": float(dist),
52
+ })
53
+ return results
54
+
55
+
56
+ def generate_answer(query: str, context_chunks: list) -> str:
57
+ """Build a prompt from retrieved context and generate an answer via Groq API."""
58
+ context = "\n\n".join(
59
+ f"[Source: {c['source']}]\n{c['text'][:800]}" for c in context_chunks
60
+ )
61
+
62
+ system_message = (
63
+ "You are a helpful SQL tutor. Answer the user's question using ONLY the "
64
+ "provided context from SQL textbooks. Give clear, detailed explanations with "
65
+ "examples where appropriate. If the context doesn't contain enough information, "
66
+ "say so honestly. Format your answer using markdown."
67
+ )
68
+
69
+ user_message = (
70
+ f"## Context from SQL Textbooks\n\n{context}\n\n"
71
+ f"---\n\n## Question\n{query}"
72
+ )
73
+
74
+ headers = {
75
+ "Authorization": f"Bearer {GROQ_API_KEY}",
76
+ "Content-Type": "application/json",
77
+ }
78
+ payload = {
79
+ "model": GEN_MODEL,
80
+ "messages": [
81
+ {"role": "system", "content": system_message},
82
+ {"role": "user", "content": user_message},
83
+ ],
84
+ "max_tokens": 1024,
85
+ "temperature": 0.3,
86
+ }
87
+
88
+ try:
89
+ response = requests.post(API_URL, headers=headers, json=payload, timeout=60)
90
+ response.raise_for_status()
91
+ result = response.json()
92
+ return result["choices"][0]["message"]["content"].strip()
93
+ except requests.exceptions.HTTPError:
94
+ return f"⚠️ API error ({response.status_code}): {response.text}"
95
+ except Exception as e:
96
+ return f"⚠️ Generation error: {e}"
97
+
98
+
99
+ def rag_query(question: str):
100
+ """Full RAG pipeline: retrieve, generate, format output."""
101
+ if not question.strip():
102
+ return "Please enter a question.", ""
103
+
104
+ # Retrieve
105
+ retrieved = retrieve(question)
106
+
107
+ # Generate
108
+ answer = generate_answer(question, retrieved)
109
+
110
+ # Format sources
111
+ sources_text = "\n\n---\n\n".join(
112
+ f"**Source {i+1}** - *{r['source']}*\n\n{r['text'][:500]}{'...' if len(r['text']) > 500 else ''}"
113
+ for i, r in enumerate(retrieved)
114
+ )
115
+
116
+ return answer, sources_text
117
+
118
+
119
+ # -- Gradio UI ------------------------------------------------------------------
120
+ DESCRIPTION = """
121
+ # πŸ“š SQL Books RAG
122
+
123
+ Ask any question about SQL and get answers grounded in content from **5 SQL textbooks**:
124
+
125
+ - *Practical SQL: A Beginner's Guide to Storytelling with Data*
126
+ - *SQL for Data Scientists* (Renee M. Teate)
127
+ - *SQL for Data Analysis: Advanced Techniques for Transforming Data into Insights*
128
+ - *The Art of SQL*
129
+ - *Learning SQL: Generate, Manipulate, and Retrieve Data*
130
+
131
+ Powered by **FAISS** retrieval + **Llama 3.1** generation.
132
+ """
133
+
134
+ EXAMPLES = [
135
+ "What is a JOIN in SQL?",
136
+ "Explain the difference between INNER JOIN and LEFT JOIN",
137
+ "How do window functions work in SQL?",
138
+ "What is a subquery and when should I use one?",
139
+ "How do I use GROUP BY with HAVING?",
140
+ "What are common table expressions (CTEs)?",
141
+ ]
142
+
143
+ my_theme = gr.themes.Soft(
144
+ primary_hue="indigo",
145
+ secondary_hue="blue",
146
+ )
147
+
148
+ with gr.Blocks(title="SQL Books RAG", theme=my_theme) as demo:
149
+ gr.Markdown(DESCRIPTION)
150
+
151
+ with gr.Row():
152
+ with gr.Column(scale=3):
153
+ question = gr.Textbox(
154
+ label="Your SQL Question",
155
+ placeholder="e.g. What is a JOIN in SQL?",
156
+ lines=2,
157
+ )
158
+ submit_btn = gr.Button("Ask", variant="primary", size="lg")
159
+ with gr.Column(scale=1):
160
+ gr.Markdown("### Try these examples")
161
+ for ex in EXAMPLES:
162
+ gr.Button(ex, size="sm").click(
163
+ fn=lambda e=ex: e, outputs=question
164
+ )
165
+
166
+ answer_box = gr.Markdown(label="Answer")
167
+
168
+ with gr.Accordion("Retrieved Source Chunks", open=False):
169
+ sources_box = gr.Markdown()
170
+
171
+ submit_btn.click(fn=rag_query, inputs=question, outputs=[answer_box, sources_box])
172
+ question.submit(fn=rag_query, inputs=question, outputs=[answer_box, sources_box])
173
+
174
+
175
+ if __name__ == "__main__":
176
+ demo.launch()
build_index.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ build_index.py β€” Extract text from SQL PDFs, chunk it, embed it, and save a FAISS index.
3
+ Run this once locally before deploying to Hugging Face.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import fitz # PyMuPDF
9
+ import numpy as np
10
+ from sentence_transformers import SentenceTransformer
11
+
12
+ # ── Configuration ──────────────────────────────────────────────────────────────
13
+ DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
14
+ INDEX_DIR = os.path.join(os.path.dirname(__file__), "faiss_index")
15
+ CHUNK_SIZE = 500 # approximate tokens (β‰ˆwords for English)
16
+ CHUNK_OVERLAP = 50 # overlap between consecutive chunks
17
+ EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
18
+
19
+
20
+ # ── PDF Extraction ─────────────────────────────────────────────────────────────
21
+ def extract_text_from_pdf(pdf_path: str) -> str:
22
+ """Extract all text from a PDF using PyMuPDF."""
23
+ doc = fitz.open(pdf_path)
24
+ text = ""
25
+ for page in doc:
26
+ text += page.get_text()
27
+ doc.close()
28
+ return text
29
+
30
+
31
+ # ── Chunking ───────────────────────────────────────────────────────────────────
32
+ def chunk_text(text: str, source: str, chunk_size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP):
33
+ """Split text into overlapping word-level chunks with metadata."""
34
+ words = text.split()
35
+ chunks = []
36
+ start = 0
37
+ while start < len(words):
38
+ end = start + chunk_size
39
+ chunk_words = words[start:end]
40
+ chunk_text_str = " ".join(chunk_words)
41
+ # Skip very short chunks (< 30 words)
42
+ if len(chunk_words) >= 30:
43
+ chunks.append({
44
+ "text": chunk_text_str,
45
+ "source": source,
46
+ "chunk_id": len(chunks),
47
+ })
48
+ start += chunk_size - overlap
49
+ return chunks
50
+
51
+
52
+ # ── Main ───────────────────────────────────────────────────────────────────────
53
+ def main():
54
+ os.makedirs(INDEX_DIR, exist_ok=True)
55
+
56
+ # 1. Extract & chunk all PDFs
57
+ all_chunks = []
58
+ pdf_files = [f for f in os.listdir(DATA_DIR) if f.lower().endswith(".pdf")]
59
+ print(f"Found {len(pdf_files)} PDFs in {DATA_DIR}")
60
+
61
+ for pdf_file in sorted(pdf_files):
62
+ pdf_path = os.path.join(DATA_DIR, pdf_file)
63
+ print(f" Processing: {pdf_file} ...", end=" ", flush=True)
64
+ text = extract_text_from_pdf(pdf_path)
65
+ chunks = chunk_text(text, source=pdf_file)
66
+ all_chunks.extend(chunks)
67
+ print(f"{len(chunks)} chunks")
68
+
69
+ print(f"\nTotal chunks: {len(all_chunks)}")
70
+
71
+ # 2. Generate embeddings
72
+ print(f"\nLoading embedding model: {EMBED_MODEL}")
73
+ model = SentenceTransformer(EMBED_MODEL)
74
+
75
+ texts = [c["text"] for c in all_chunks]
76
+ print("Generating embeddings ...")
77
+ embeddings = model.encode(texts, show_progress_bar=True, batch_size=64)
78
+ embeddings = np.array(embeddings).astype("float32")
79
+ print(f"Embeddings shape: {embeddings.shape}")
80
+
81
+ # 3. Build FAISS index
82
+ import faiss
83
+
84
+ dimension = embeddings.shape[1]
85
+ index = faiss.IndexFlatL2(dimension)
86
+ index.add(embeddings)
87
+ print(f"FAISS index size: {index.ntotal} vectors, dimension {dimension}")
88
+
89
+ # 4. Save
90
+ faiss.write_index(index, os.path.join(INDEX_DIR, "index.faiss"))
91
+ with open(os.path.join(INDEX_DIR, "chunks.json"), "w", encoding="utf-8") as f:
92
+ json.dump(all_chunks, f, ensure_ascii=False, indent=2)
93
+
94
+ print(f"\n[OK] Index saved to {INDEX_DIR}/index.faiss")
95
+ print(f"[OK] Chunks saved to {INDEX_DIR}/chunks.json")
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main()
faiss_index/chunks.json ADDED
The diff for this file is too large to render. See raw diff
 
faiss_index/index.faiss ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1e6a7d0dcfbcb0f0a0a266ae938ef77e79b9b067dcb3752e36d85739055183e9
3
+ size 1826349
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=6.0.0
2
+ sentence-transformers
3
+ faiss-cpu
4
+ pymupdf
5
+ requests