-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
405 lines (322 loc) Β· 14 KB
/
app.py
File metadata and controls
405 lines (322 loc) Β· 14 KB
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
from flask import Flask, render_template, request, jsonify, session, Response, stream_with_context
from groq import Groq
from dotenv import load_dotenv
import os
import pypdf
import docx
import uuid
import json
import time
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 1. Add override=True to FORCE the app to use the .env file
env_path = os.path.join(BASE_DIR, ".env")
load_dotenv(env_path, override=True)
app = Flask(__name__)
app.secret_key = "exambot_secret_2024"
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
print("\n" + "="*40)
print(f"Checking for .env at: {env_path}")
if GROQ_API_KEY:
print("SUCCESS: Groq API key loaded.")
else:
print("FAILED: API Key is NONE. The .env file was not found or is empty!")
print("="*40 + "\n")
client = Groq(api_key=GROQ_API_KEY)
UPLOAD_FOLDER = os.path.join(BASE_DIR, "uploads")
HISTORY_FOLDER = os.path.join(BASE_DIR, "chat_history")
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(HISTORY_FOLDER, exist_ok=True)
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
ALLOWED_EXTENSIONS = {"pdf", "txt", "docx"}
BASE_SYSTEM_PROMPT = """You are ExamBot β an expert AI study assistant for B.Tech CSE students in India (LPU, AKTU, VTU, IIT, etc.).
Your job:
- Carefully analyze uploaded syllabus or study material
- Tell the student exactly how to start studying step by step
- Identify the most important topics for the exam
- Create a practical day-wise or time-based study plan
- Explain concepts clearly with examples
- Provide exam-oriented answers and writing strategies
STRICT RULES:
- Do not answer anything outside academic scope
- If question is irrelevant, respond: "This is outside academic scope. Ask questions related to your syllabus or exam preparation."
- Always be specific and mention actual topic names
- Do not give generic advice
OUTPUT FORMAT (MANDATORY):
1. Quick Summary
2. Where to Start (First topic + reason)
3. Important Topics (Mark as β High / β Medium / β Low priority)
4. Study Plan (Based on time left for exam)
5. Concept Explanation (if required)
6. Practice Questions
7. Exam Writing Strategy
8. Common Mistakes
9. Revision Strategy
LOGIC:
- Adapt based on time left:
- 1β2 days β Crash plan
- 3β7 days β Smart preparation
- 7+ days β Full preparation
- Assume beginner level unless specified
- Use simple explanations + examples + analogies
CONCEPT EXPLANATION FORMAT:
Definition β Simple Explanation β Example β Exam Answer Format
EXAM FOCUS:
- Highlight frequently asked topics
- Provide structured answers for 5/10 mark questions
- Include keywords and diagrams where needed
LEARNING SUPPORT:
- Generate practice questions after each topic
- Include expected exam questions
PERSONALIZATION:
- Use user's syllabus, weak areas, and exam pattern if provided
"""
def allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
# βββ TEXT EXTRACTION ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def extract_text_from_pdf(filepath):
"""Extract text from PDF using pypdf"""
text = ""
try:
reader = pypdf.PdfReader(filepath)
for page in reader.pages:
t = page.extract_text()
if t:
text += t + "\n"
except Exception as e:
text = f"[PDF read error: {e}]"
return text
def extract_text_from_docx(filepath):
"""Extract text from Word document"""
try:
doc = docx.Document(filepath)
return "\n".join([p.text for p in doc.paragraphs if p.text.strip()])
except Exception as e:
return f"[DOCX read error: {e}]"
def extract_text_from_txt(filepath):
"""Extract text from plain text file"""
try:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
return f.read()
except Exception as e:
return f"[TXT read error: {e}]"
def extract_text(filepath, filename):
"""Route to correct extractor based on file extension"""
ext = filename.rsplit(".", 1)[1].lower()
if ext == "pdf":
return extract_text_from_pdf(filepath)
elif ext == "docx":
return extract_text_from_docx(filepath)
elif ext == "txt":
return extract_text_from_txt(filepath)
return ""
def get_syllabus_context():
"""Re-read the uploaded file properly and return text for AI context"""
if "file_path" not in session or "original_filename" not in session:
return ""
try:
return extract_text(session["file_path"], session["original_filename"])[:15000]
except Exception:
return ""
def save_chat_history(session_id, history):
filepath = os.path.join(HISTORY_FOLDER, f"{session_id}.json")
with open(filepath, "w", encoding="utf-8") as f:
json.dump(history, f, indent=2)
def load_chat_history(session_id):
filepath = os.path.join(HISTORY_FOLDER, f"{session_id}.json")
if os.path.exists(filepath):
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
return []
def groq_text(system_prompt, user_prompt, max_tokens=1500):
response = client.chat.completions.create(
model=GROQ_MODEL,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
temperature=0.7,
max_tokens=max_tokens,
)
return response.choices[0].message.content
def groq_stream(system_prompt, history, max_tokens=1024):
return client.chat.completions.create(
model=GROQ_MODEL,
messages=[
{"role": "system", "content": system_prompt},
*history,
],
temperature=0.7,
max_tokens=max_tokens,
stream=True,
)
# βββ ROUTES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.route("/")
def index():
if "session_id" not in session:
session["session_id"] = str(uuid.uuid4())
return render_template("index.html")
@app.route("/upload", methods=["POST"])
def upload_file():
"""Handle file upload, extract text, and return AI study plan"""
if "file" not in request.files:
return jsonify({"error": "No file uploaded"}), 400
file = request.files["file"]
days = request.form.get("days", "7")
if file.filename == "":
return jsonify({"error": "No file selected"}), 400
if not allowed_file(file.filename):
return jsonify({"error": "Only PDF, DOCX, and TXT files are allowed"}), 400
# Save with unique name to avoid conflicts
unique_name = str(uuid.uuid4()) + "_" + file.filename
filepath = os.path.join(app.config["UPLOAD_FOLDER"], unique_name)
file.save(filepath)
# Extract text from file
extracted_text = extract_text(filepath, file.filename)
if not extracted_text.strip():
return jsonify({"error": "Could not extract text from this file. Make sure it's a text-based PDF (not scanned)."}), 400
# Store in session (store path + original name for re-reading later)
session["file_path"] = filepath
session["original_filename"] = file.filename
session["session_id"] = str(uuid.uuid4())
session["history"] = []
session.modified = True
# Use up to 25000 chars for initial analysis
content_for_analysis = extracted_text[:25000]
analysis_prompt = f"""The student uploaded their study material: "{file.filename}"
CONTENT OF THE FILE:
---
{content_for_analysis}
---
The student has {days} days before their exam.
Please do the following:
1. π List ALL topics/units found in the material
2. β Identify TOP 5 most important topics for the exam
3. π Tell them EXACTLY which topic to start with first and why
4. π
Create a day-by-day study plan for {days} days
5. π‘ Give 3-4 specific exam tips for this subject
6. βοΈ Mention what type of exam questions are asked on these topics
Be very specific, practical, and use the actual topic names from the material."""
try:
reply = groq_text(BASE_SYSTEM_PROMPT, analysis_prompt, max_tokens=1500)
# Save full history to file
full_history = [
{"role": "user", "content": analysis_prompt},
{"role": "assistant", "content": reply}
]
save_chat_history(session["session_id"], full_history)
# Add summarized history to session
summarized_prompt = f"Student uploaded {file.filename} with {days} days left."
session["history"].append({"role": "user", "content": summarized_prompt})
session["history"].append({"role": "assistant", "content": reply})
session.modified = True
return jsonify({"reply": reply, "filename": file.filename})
except Exception as e:
return jsonify({"error": f"Groq API error: {str(e)}"}), 500
@app.route("/chat", methods=["POST"])
def chat():
"""Handle follow-up questions β always with syllabus context via streaming"""
data = request.get_json()
if not data:
return jsonify({"reply": "Invalid request"}), 400
user_message = data.get("message", "").strip()
if not user_message:
return jsonify({"reply": "Please type a message"}), 400
if "session_id" not in session:
session["session_id"] = str(uuid.uuid4())
session["history"] = []
if "history" not in session:
session["history"] = []
session_id = session["session_id"]
# Update full history on disk
full_history = load_chat_history(session_id)
full_history.append({"role": "user", "content": user_message})
save_chat_history(session_id, full_history)
session["history"].append({"role": "user", "content": user_message})
# Truncate session history to keep it small (e.g. last 8 messages + context)
if len(session["history"]) > 8:
# Keep first two (summarized prompt + reply) and the rest
if len(session["history"]) >= 2:
session["history"] = [session["history"][0], session["history"][1]] + session["history"][-6:]
else:
session["history"] = session["history"][-8:]
session.modified = True
# Always include syllabus in system prompt for context
system_prompt = BASE_SYSTEM_PROMPT
syllabus = get_syllabus_context()
if syllabus:
system_prompt += f"\n\nSTUDENT'S UPLOADED STUDY MATERIAL:\n{syllabus}\n\nAlways refer to this material when answering the student's questions."
def generate():
try:
stream = groq_stream(system_prompt, session["history"], max_tokens=1024)
full_reply = ""
for chunk in stream:
if chunk.choices[0].delta.content is not None:
content = chunk.choices[0].delta.content
full_reply += content
yield content
# Once streaming is complete, save the assistant's reply to history
full_history = load_chat_history(session_id)
full_history.append({"role": "assistant", "content": full_reply})
save_chat_history(session_id, full_history)
session["history"].append({"role": "assistant", "content": full_reply})
if len(session["history"]) > 8:
if len(session["history"]) >= 2:
session["history"] = [session["history"][0], session["history"][1]] + session["history"][-6:]
else:
session["history"] = session["history"][-8:]
session.modified = True
except Exception as e:
yield f"Error: {str(e)}"
return Response(stream_with_context(generate()), mimetype="text/plain")
@app.route("/api/history", methods=["GET"])
def get_history_list():
"""List all saved JSON chat files"""
history_list = []
if os.path.exists(HISTORY_FOLDER):
for filename in os.listdir(HISTORY_FOLDER):
if filename.endswith(".json"):
session_id = filename.replace(".json", "")
filepath = os.path.join(HISTORY_FOLDER, filename)
try:
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
if data and len(data) > 0:
# Use first user message as title
title = data[0]["content"]
if title.startswith("The student uploaded"):
title = "Document Analysis: " + title.split('"')[1]
else:
title = title[:50] + "..." if len(title) > 50 else title
# Use modified time as timestamp
mtime = os.path.getmtime(filepath)
history_list.append({
"session_id": session_id,
"title": title,
"timestamp": mtime
})
except Exception:
pass
# Sort by timestamp descending
history_list.sort(key=lambda x: x["timestamp"], reverse=True)
return jsonify(history_list)
@app.route("/api/history/<session_id>", methods=["GET"])
def load_history(session_id):
"""Load a specific past chat into current session"""
data = load_chat_history(session_id)
if data:
session["session_id"] = session_id
# Reconstruct a truncated session history for context
if len(data) > 8:
session["history"] = [data[0], data[1]] + data[-6:]
else:
session["history"] = data
session.modified = True
return jsonify({"status": "success", "history": data})
return jsonify({"error": "History not found"}), 404
@app.route("/clear", methods=["POST"])
def clear():
session.clear()
return jsonify({"status": "cleared"})
if __name__ == "__main__":
app.run(debug=True, port=5000)