sanjikiren commited on
Commit
3d43b16
·
0 Parent(s):

Added Complete backend model with hybrid search and LLAMA 8b instant model via groq

Browse files
Files changed (9) hide show
  1. .gitignore +6 -0
  2. README.md +0 -0
  3. app/__init__.py +0 -0
  4. app/ingest.py +77 -0
  5. app/llm.py +29 -0
  6. app/main.py +52 -0
  7. app/retrieval.py +37 -0
  8. frontend/index.html +156 -0
  9. requirements.txt +0 -0
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ models/
6
+ data/
README.md ADDED
File without changes
app/__init__.py ADDED
File without changes
app/ingest.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fitz
2
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
3
+ from sentence_transformers import SentenceTransformer
4
+ import numpy as np
5
+ import faiss
6
+ import pickle
7
+ import os
8
+ from rank_bm25 import BM25Okapi
9
+
10
+ #0.Embedding Models and Database Paths
11
+ EMBEDDING_MODEL = "all-MiniLM-L6-v2"
12
+ FAISS_INDEX_PATH = "models/faiss.index"
13
+ CHUNKS_PATH = "models/chunks.pkl"
14
+ BM25_PATH = "models/bm25.pkl"
15
+
16
+ #1.First we Extract the text from the pdf using pymudf(imported as fitz here) and store it in a list.
17
+ def extract_text(pdf_path:str) -> str:
18
+ doc = fitz.open(pdf_path)
19
+ all_text = []
20
+
21
+ for page in doc:
22
+ text = page.get_text()
23
+ all_text.append(text)
24
+ return "\n".join(all_text)
25
+
26
+ #2.Now we split the text into chunks(Chunking)
27
+
28
+ def chunk_text(text:str)->list:
29
+ splitter = RecursiveCharacterTextSplitter(
30
+ chunk_size = 500,
31
+ chunk_overlap = 50,
32
+ separators= ["\n\n","\n","."," "]
33
+ )
34
+ chunks = splitter.split_text(text)
35
+ return chunks
36
+
37
+ #3.Embed the text and save it to the Faiss database.
38
+
39
+ def build_index(chunks:list):
40
+ print("Loading Embedding Model")
41
+ model = SentenceTransformer(EMBEDDING_MODEL)
42
+
43
+ print("Embedding the chunks standby")
44
+ embeddings = model.encode(chunks,show_progress_bar = True)
45
+ embeddings = np.array(embeddings,dtype = "float32")
46
+
47
+ #3.1building the FAISS index
48
+ dimension = embeddings.shape[1] #we create a np array which gets all the vectors of a chunk in a straight line and all the chunks as rows(downwards)
49
+ index = faiss.IndexFlatL2(dimension)
50
+ index.add(embeddings)
51
+
52
+ #3.2save the faiss index
53
+ faiss.write_index(index,FAISS_INDEX_PATH)
54
+
55
+ #3.3save chunks
56
+ with open(CHUNKS_PATH,"wb") as f:
57
+ pickle.dump(chunks,f)
58
+
59
+ print(f"FAISS index saved - {len(chunks)}chunks indexed")
60
+
61
+ #3.4 Tokenize and save index for the BM25
62
+ tokenized = [chunk.lower().split() for chunk in chunks]
63
+ bm25 = BM25Okapi(tokenized)
64
+
65
+ with open(BM25_PATH,"wb") as f:
66
+ pickle.dump(bm25,f)
67
+
68
+ print("BM25 index saved")
69
+
70
+ return index,chunks
71
+
72
+ if __name__ == "__main__":
73
+ text = extract_text("data\physics.pdf")
74
+ chunks = chunk_text(text)
75
+ build_index(chunks)
76
+
77
+
app/llm.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from groq import Groq
2
+ from dotenv import load_dotenv
3
+ import os
4
+
5
+ load_dotenv()
6
+
7
+ client = Groq(api_key=os.getenv("GROQ_API_KEY"))
8
+ MODEL = "llama-3.1-8b-instant"
9
+
10
+ def generate_answer(query: str, chunks: list) -> str:
11
+ context = "\n\n".join(chunks)
12
+
13
+ response = client.chat.completions.create(
14
+ model=MODEL,
15
+ messages=[
16
+ {
17
+ "role": "system",
18
+ "content": "You are a helpful JEE/NEET study assistant. Answer using ONLY the context provided. If the answer isn't in the context, say so."
19
+ },
20
+ {
21
+ "role": "user",
22
+ "content": f"Context:\n{context}\n\nQuestion: {query}"
23
+ }
24
+ ],
25
+ max_tokens=300,
26
+ temperature=0.3,
27
+ )
28
+
29
+ return response.choices[0].message.content
app/main.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI,UploadFile,File
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.responses import FileResponse
4
+ from pydantic import BaseModel
5
+ from contextlib import asynccontextmanager
6
+ from app.ingest import extract_text,chunk_text,build_index
7
+ from app.retrieval import retrieve
8
+ from app.llm import generate_answer
9
+ import shutil
10
+ import os
11
+
12
+ @asynccontextmanager
13
+ async def lifespan(app:FastAPI):
14
+ print("RAG Assistant ready to rock and roll")
15
+ yield
16
+
17
+ app = FastAPI(lifespan = lifespan)
18
+
19
+ class Query(BaseModel):
20
+ question:str
21
+
22
+ @app.get("/health")
23
+ def get_health():
24
+ return {"status":"ok"}
25
+
26
+ @app.post("/upload")
27
+ async def upload_pdf(file:UploadFile = File(...)):
28
+ pdf_path = f"data/{file.filename}"
29
+ with open(pdf_path,"wb") as f:
30
+ shutil.copyfileobj(file.file,f)
31
+
32
+ text = extract_text(pdf_path)
33
+ chunks = chunk_text(text)
34
+ build_index(chunks)
35
+
36
+ return {"Message":f"Indexed len{chunks} chunks from {file.filename}"}
37
+
38
+ @app.post("/ask")
39
+ def ask(query: Query):
40
+ chunks = retrieve(query.question)
41
+ answer = generate_answer(query.question,chunks)
42
+ return {
43
+ "question":query.question,
44
+ "answer" : answer,
45
+ "sources":chunks[:2]
46
+ }
47
+
48
+ app.mount("/static",StaticFiles(directory="frontend"),name="static")
49
+
50
+ @app.get("/")
51
+ def serve():
52
+ return FileResponse("frontend/index.html")
app/retrieval.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sentence_transformers import SentenceTransformer
2
+ import faiss
3
+ import numpy as np
4
+ import pickle
5
+
6
+ EMBEDDING_MODEL = "all-MiniLM-L6-v2"
7
+ FAISS_INDEX_PATH = "models/faiss.index"
8
+ CHUNKS_PATH = "models/chunks.pkl"
9
+ BM25_PATH = "models/bm25.pkl"
10
+ TOP_K = 5
11
+
12
+ def retrieve(query:str)->str:
13
+ with open(CHUNKS_PATH,"rb") as f:
14
+ chunks = pickle.load(f)
15
+
16
+ with open(BM25_PATH,"rb") as f:
17
+ bm25 = pickle.load(f)
18
+
19
+ faiss_index = faiss.read_index(FAISS_INDEX_PATH)
20
+ model = SentenceTransformer(EMBEDDING_MODEL)
21
+
22
+ embedded_query = np.array(model.encode([query]),dtype = "float32")
23
+
24
+ _, indices = faiss_index.search(embedded_query,TOP_K)
25
+ faiss_chunks = [chunks[i] for i in indices[0].tolist()]
26
+
27
+ bm25_chunks = bm25.get_top_n(query.lower().split(),chunks,n=TOP_K)
28
+
29
+ combined = list(dict.fromkeys(faiss_chunks + bm25_chunks))
30
+ return combined[:TOP_K]
31
+
32
+ #if __name__ == "__main__":
33
+ #retrieve("What is the speed of light")
34
+
35
+
36
+
37
+
frontend/index.html ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>JEE/NEET Study Assistant</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&family=Instrument+Sans:wght@300;400;500&display=swap" rel="stylesheet">
8
+ <style>
9
+ *{margin:0;padding:0;box-sizing:border-box}
10
+ :root{--bg:#06060a;--surface:#0d0d14;--card:#10101a;--border:#1a1a2e;--text:#f0f0f8;--muted:#4a4a6a;--dim:#2a2a3e;--accent:#7c83ff;--accent-glow:rgba(124,131,255,0.12);--bull:#00e676;--bear:#ff3d57}
11
+ body{background:var(--bg);color:var(--text);font-family:'Instrument Sans',sans-serif;height:100vh;display:flex;flex-direction:column;overflow:hidden}
12
+ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse 80% 50% at 50% 0%,rgba(124,131,255,0.06) 0%,transparent 60%);pointer-events:none;z-index:0}
13
+ .header{padding:16px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;position:relative;z-index:1;flex-shrink:0}
14
+ .brand{display:flex;flex-direction:column;gap:2px}
15
+ .brand-tag{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:0.25em;color:var(--muted);text-transform:uppercase}
16
+ .brand-name{font-family:'Syne',sans-serif;font-size:20px;font-weight:800;letter-spacing:-0.02em;color:var(--text)}
17
+ .brand-name em{font-style:normal;color:var(--accent)}
18
+ .header-right{display:flex;align-items:center;gap:12px}
19
+ .upload-btn{display:flex;align-items:center;gap:6px;padding:7px 14px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--muted);font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.08em;cursor:pointer;transition:all 0.2s}
20
+ .upload-btn:hover{border-color:var(--accent);color:var(--accent)}
21
+ .status-dot{width:6px;height:6px;border-radius:50%;background:var(--bull);animation:pulse 1.8s ease-in-out infinite}
22
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
23
+ .status-text{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--bull);letter-spacing:0.08em}
24
+ input[type="file"]{display:none}
25
+ .chat-area{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:16px;position:relative;z-index:1}
26
+ .chat-area::-webkit-scrollbar{width:4px}
27
+ .chat-area::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
28
+ .empty-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;opacity:0.5}
29
+ .empty-icon{font-family:'Syne',sans-serif;font-size:48px;font-weight:800;color:var(--accent);letter-spacing:-0.04em}
30
+ .empty-text{font-size:14px;color:var(--muted);text-align:center;line-height:1.6}
31
+ .suggestions{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:8px}
32
+ .suggestion{padding:6px 14px;border:1px solid var(--border);border-radius:20px;font-size:12px;color:var(--muted);cursor:pointer;transition:all 0.2s}
33
+ .suggestion:hover{border-color:var(--accent);color:var(--accent)}
34
+ .message{display:flex;gap:12px;max-width:780px;animation:msgIn 0.3s ease}
35
+ @keyframes msgIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
36
+ .message.user{align-self:flex-end;flex-direction:row-reverse}
37
+ .message.assistant{align-self:flex-start}
38
+ .avatar{width:28px;height:28px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:500;flex-shrink:0;margin-top:2px}
39
+ .message.user .avatar{background:rgba(124,131,255,0.15);color:var(--accent);border:1px solid rgba(124,131,255,0.2)}
40
+ .message.assistant .avatar{background:rgba(0,230,118,0.1);color:var(--bull);border:1px solid rgba(0,230,118,0.2)}
41
+ .bubble{padding:12px 16px;border-radius:10px;font-size:14px;line-height:1.6;max-width:100%}
42
+ .message.user .bubble{background:rgba(124,131,255,0.08);border:1px solid rgba(124,131,255,0.15);color:var(--text);border-radius:10px 2px 10px 10px}
43
+ .message.assistant .bubble{background:var(--card);border:1px solid var(--border);color:var(--text);border-radius:2px 10px 10px 10px}
44
+ .sources{margin-top:10px;padding-top:10px;border-top:1px solid var(--border)}
45
+ .sources-label{font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted);letter-spacing:0.15em;text-transform:uppercase;margin-bottom:6px}
46
+ .source-item{font-size:12px;color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:6px 10px;margin-bottom:4px;line-height:1.4;cursor:pointer;transition:border-color 0.2s;max-height:40px;overflow:hidden}
47
+ .source-item:hover{border-color:var(--accent)}
48
+ .source-item.expanded{max-height:none;color:var(--text)}
49
+ .typing{display:flex;gap:4px;padding:4px 0}
50
+ .typing span{width:6px;height:6px;border-radius:50%;background:var(--muted);animation:typing 1.2s ease-in-out infinite}
51
+ .typing span:nth-child(2){animation-delay:0.2s}
52
+ .typing span:nth-child(3){animation-delay:0.4s}
53
+ @keyframes typing{0%,100%{opacity:0.3;transform:translateY(0)}50%{opacity:1;transform:translateY(-3px)}}
54
+ .input-area{padding:16px 24px;border-top:1px solid var(--border);position:relative;z-index:1;flex-shrink:0}
55
+ .input-wrap{display:flex;gap:10px;align-items:flex-end;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 12px;transition:border-color 0.2s}
56
+ .input-wrap:focus-within{border-color:var(--accent)}
57
+ textarea{flex:1;background:transparent;border:none;outline:none;color:var(--text);font-family:'Instrument Sans',sans-serif;font-size:14px;resize:none;max-height:120px;line-height:1.5}
58
+ textarea::placeholder{color:var(--muted)}
59
+ .send-btn{width:32px;height:32px;border-radius:6px;background:var(--accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:opacity 0.2s;color:#fff;font-size:16px}
60
+ .send-btn:hover{opacity:0.85}
61
+ .send-btn:disabled{opacity:0.3;cursor:not-allowed}
62
+ .input-hint{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--muted);margin-top:6px;letter-spacing:0.05em}
63
+ .toast{position:fixed;bottom:80px;right:24px;padding:10px 16px;border-radius:8px;font-size:13px;font-family:'JetBrains Mono',monospace;letter-spacing:0.05em;z-index:100;display:none}
64
+ .toast.success{background:rgba(0,230,118,0.1);border:1px solid rgba(0,230,118,0.3);color:var(--bull)}
65
+ .toast.error{background:rgba(255,61,87,0.1);border:1px solid rgba(255,61,87,0.3);color:var(--bear)}
66
+ </style>
67
+ </head>
68
+ <body>
69
+ <div class="header">
70
+ <div class="brand">
71
+ <div class="brand-tag">JEE · NEET · Study Assistant</div>
72
+ <div class="brand-name">Study<em>.</em>AI</div>
73
+ </div>
74
+ <div class="header-right">
75
+ <div style="display:flex;align-items:center;gap:6px">
76
+ <div class="status-dot"></div>
77
+ <div class="status-text">READY</div>
78
+ </div>
79
+ <label class="upload-btn" for="pdf-upload">↑ UPLOAD PDF</label>
80
+ <input type="file" id="pdf-upload" accept=".pdf" onchange="uploadPDF(this)">
81
+ </div>
82
+ </div>
83
+ <div class="chat-area" id="chat-area">
84
+ <div class="empty-state" id="empty-state">
85
+ <div class="empty-icon">Study.</div>
86
+ <div class="empty-text">Ask any question from your NCERT textbook.<br>Upload a PDF or start asking from the indexed material.</div>
87
+ <div class="suggestions">
88
+ <div class="suggestion" onclick="ask('What is the speed of light?')">What is the speed of light?</div>
89
+ <div class="suggestion" onclick="ask('Explain dimensional analysis')">Explain dimensional analysis</div>
90
+ <div class="suggestion" onclick="ask('What are SI units?')">What are SI units?</div>
91
+ <div class="suggestion" onclick="ask('What is significant figures?')">What is significant figures?</div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ <div class="input-area">
96
+ <div class="input-wrap">
97
+ <textarea id="question-input" placeholder="Ask a question from your textbook..." rows="1"
98
+ onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendQuestion()}"
99
+ oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px'"></textarea>
100
+ <button class="send-btn" id="send-btn" onclick="sendQuestion()">↑</button>
101
+ </div>
102
+ <div class="input-hint">Press Enter to send · Shift+Enter for new line · Upload PDF to index new material</div>
103
+ </div>
104
+ <div class="toast" id="toast"></div>
105
+ <script>
106
+ let isLoading=false;
107
+ function showToast(msg,type='success'){const t=document.getElementById('toast');t.textContent=msg;t.className=`toast ${type}`;t.style.display='block';setTimeout(()=>t.style.display='none',3000)}
108
+ function hideEmpty(){const e=document.getElementById('empty-state');if(e)e.remove()}
109
+ function addMessage(role,content,sources=[]){
110
+ hideEmpty();
111
+ const area=document.getElementById('chat-area');
112
+ const div=document.createElement('div');
113
+ div.className=`message ${role}`;
114
+ const avatar=role==='user'?'YOU':'AI';
115
+ let sourcesHtml='';
116
+ if(sources.length>0){sourcesHtml=`<div class="sources"><div class="sources-label">Source excerpts</div>${sources.map(s=>`<div class="source-item" onclick="this.classList.toggle('expanded')">${s}</div>`).join('')}</div>`}
117
+ div.innerHTML=`<div class="avatar">${avatar}</div><div class="bubble">${content}${sourcesHtml}</div>`;
118
+ area.appendChild(div);
119
+ area.scrollTop=area.scrollHeight;
120
+ }
121
+ function addTyping(){
122
+ hideEmpty();
123
+ const area=document.getElementById('chat-area');
124
+ const div=document.createElement('div');
125
+ div.className='message assistant';div.id='typing-indicator';
126
+ div.innerHTML=`<div class="avatar">AI</div><div class="bubble"><div class="typing"><span></span><span></span><span></span></div></div>`;
127
+ area.appendChild(div);area.scrollTop=area.scrollHeight;
128
+ }
129
+ function removeTyping(){const t=document.getElementById('typing-indicator');if(t)t.remove()}
130
+ async function ask(question){document.getElementById('question-input').value=question;sendQuestion()}
131
+ async function sendQuestion(){
132
+ if(isLoading)return;
133
+ const input=document.getElementById('question-input');
134
+ const question=input.value.trim();
135
+ if(!question)return;
136
+ input.value='';input.style.height='auto';
137
+ isLoading=true;document.getElementById('send-btn').disabled=true;
138
+ addMessage('user',question);addTyping();
139
+ try{
140
+ const res=await fetch('/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question})});
141
+ const data=await res.json();
142
+ removeTyping();addMessage('assistant',data.answer,data.sources||[]);
143
+ }catch(e){removeTyping();addMessage('assistant','Something went wrong. Is the server running?')}
144
+ isLoading=false;document.getElementById('send-btn').disabled=false;input.focus();
145
+ }
146
+ async function uploadPDF(input){
147
+ const file=input.files[0];if(!file)return;
148
+ showToast('Indexing PDF...','success');
149
+ const formData=new FormData();formData.append('file',file);
150
+ try{const res=await fetch('/upload',{method:'POST',body:formData});const data=await res.json();showToast(`✓ ${data.message}`,'success')}
151
+ catch(e){showToast('Upload failed','error')}
152
+ input.value='';
153
+ }
154
+ </script>
155
+ </body>
156
+ </html>
requirements.txt ADDED
Binary file (3.17 kB). View file