heerjtdev commited on
Commit
514240d
·
verified ·
1 Parent(s): 31baf0a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +302 -76
app.py CHANGED
@@ -179,6 +179,228 @@
179
 
180
 
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  import gradio as gr
183
  import fitz # PyMuPDF
184
  import torch
@@ -201,9 +423,11 @@ class OnnxBgeEmbeddings(Embeddings):
201
  def __init__(self, model_name="BAAI/bge-large-en-v1.5"):
202
  print(f"🔄 Loading Embeddings: {model_name}...")
203
  self.tokenizer = AutoTokenizer.from_pretrained(model_name)
204
- # Note: export=True will re-convert on every restart.
205
- # For production, you'd want to save this permanently, but this works for now.
206
- self.model = ORTModelForFeatureExtraction.from_pretrained(model_name, export=True)
 
 
207
 
208
  def _process_batch(self, texts):
209
  inputs = self.tokenizer(texts, padding=True, truncation=True, max_length=512, return_tensors="pt")
@@ -223,7 +447,6 @@ class OnnxBgeEmbeddings(Embeddings):
223
  # ---------------------------------------------------------
224
  # 2. LLM Evaluator Class (Llama-3.2-1B ONNX)
225
  # ---------------------------------------------------------
226
-
227
  class LLMEvaluator:
228
  def __init__(self):
229
  self.repo_id = "onnx-community/Llama-3.2-1B-Instruct"
@@ -231,39 +454,31 @@ class LLMEvaluator:
231
 
232
  print(f"🔄 Preparing LLM: {self.repo_id}...")
233
 
234
- # [FIXED DOWNLOADER]
235
- print(f"📥 Downloading FP16 model + data to {self.local_dir}...")
236
- snapshot_download(
237
- repo_id=self.repo_id,
238
- local_dir=self.local_dir,
239
- local_dir_use_symlinks=False,
240
- allow_patterns=[
241
- "config.json",
242
- "generation_config.json",
243
- "tokenizer*",
244
- "special_tokens_map.json",
245
- "*.jinja",
246
- "onnx/model_fp16.onnx*" # WILDCARD '*' ensures we get .onnx AND .onnx_data
247
- ]
248
- )
249
- print("✅ Download complete.")
250
 
251
  self.tokenizer = AutoTokenizer.from_pretrained(self.local_dir)
252
 
253
- # [CRITICAL FIX]
254
- # Separating 'subfolder' and 'file_name' is required by Optimum
255
  self.model = ORTModelForCausalLM.from_pretrained(
256
  self.local_dir,
257
- subfolder="onnx", # Point to the subfolder
258
- file_name="model_fp16.onnx", # Just the filename
259
  use_cache=True,
260
  use_io_binding=False
261
  )
262
 
263
- def evaluate(self, context, question, student_answer):
264
- # Prompt Engineering for Llama 3
265
  messages = [
266
- {"role": "system", "content": "You are a strict academic. Grade the student answer based ONLY on the provided context."},
267
  {"role": "user", "content": f"""
268
  ### CONTEXT:
269
  {context}
@@ -274,39 +489,32 @@ class LLMEvaluator:
274
  ### STUDENT ANSWER:
275
  {student_answer}
276
 
277
- ### INSTRUCTIONS:
278
- 1. Is the answer correct?
279
- 2. Score out of 10.
280
- 3. Explanation.
 
 
 
 
 
281
  """}
282
  ]
283
 
284
- # Format input using the chat template
285
- input_text = self.tokenizer.apply_chat_template(
286
- messages,
287
- tokenize=False,
288
- add_generation_prompt=True
289
- )
290
-
291
  inputs = self.tokenizer(input_text, return_tensors="pt")
292
 
293
- # Generate response
294
  with torch.no_grad():
295
  outputs = self.model.generate(
296
  **inputs,
297
- max_new_tokens=256,
298
- temperature=0.3,
299
- do_sample=True,
300
- top_p=0.9
301
  )
302
 
303
- # Decode response
304
- response = self.tokenizer.decode(
305
- outputs[0][inputs.input_ids.shape[1]:],
306
- skip_special_tokens=True
307
- )
308
  return response
309
-
310
  # ---------------------------------------------------------
311
  # 3. Main Application Logic
312
  # ---------------------------------------------------------
@@ -314,8 +522,9 @@ class VectorSystem:
314
  def __init__(self):
315
  self.vector_store = None
316
  self.embeddings = OnnxBgeEmbeddings()
317
- self.llm = LLMEvaluator() # Initialize LLM
318
- self.all_chunks = []
 
319
 
320
  def process_file(self, file_obj):
321
  if file_obj is None: return "No file uploaded."
@@ -329,36 +538,52 @@ class VectorSystem:
329
  else:
330
  return "❌ Error: Only .pdf and .txt supported."
331
 
332
- text_splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=150)
333
  self.all_chunks = text_splitter.split_text(text)
 
334
 
335
  if not self.all_chunks: return "File empty."
336
 
337
- metadatas = [{"id": i} for i in range(len(self.all_chunks))]
 
338
  self.vector_store = FAISS.from_texts(self.all_chunks, self.embeddings, metadatas=metadatas)
339
- return f"✅ Indexed {len(self.all_chunks)} chunks."
 
340
  except Exception as e:
341
  return f"Error: {str(e)}"
342
 
343
- def process_query(self, question, student_answer):
344
  if not self.vector_store: return "⚠️ Please upload a file first.", ""
345
  if not question: return "⚠️ Enter a question.", ""
346
 
347
- # 1. Retrieve
348
- results = self.vector_store.similarity_search_with_score(question, k=3)
 
349
 
350
- # Prepare context for LLM
351
- context_text = "\n\n".join([doc.page_content for doc, _ in results])
 
352
 
353
- # Prepare Evidence Output for UI
354
- evidence_display = "### 📚 Retrieved Context:\n"
355
- for i, (doc, score) in enumerate(results):
356
- evidence_display += f"**Chunk {i+1}** (Score: {score:.4f}):\n> {doc.page_content}\n\n"
 
 
 
 
 
 
 
357
 
358
- # 2. Evaluate (if answer provided)
 
 
 
 
359
  llm_feedback = "Please enter a student answer to grade."
360
  if student_answer:
361
- llm_feedback = self.llm.evaluate(context_text, question, student_answer)
362
 
363
  return evidence_display, llm_feedback
364
 
@@ -367,27 +592,28 @@ system = VectorSystem()
367
 
368
  # --- GRADIO UI ---
369
  with gr.Blocks(title="EduGenius AI Grader") as demo:
370
- gr.Markdown("# 🧠 EduGenius: RAG + LLM Grading")
371
- gr.Markdown("Powered by **BGE-Large** (Retrieval) and **Llama-3.2-1B** (Evaluation) - All ONNX Optimized.")
372
-
373
  with gr.Row():
374
  with gr.Column(scale=1):
375
- pdf_input = gr.File(label="1. Upload Chapter (PDF/TXT)")
376
  upload_btn = gr.Button("Index Content", variant="primary")
377
- status_msg = gr.Textbox(label="System Status", interactive=False)
378
 
379
  with gr.Column(scale=2):
380
- q_input = gr.Textbox(label="2. Question")
381
- a_input = gr.Textbox(label="3. Student Answer")
 
 
 
382
  run_btn = gr.Button("Retrieve & Grade", variant="secondary")
383
 
384
  with gr.Row():
385
- evidence_box = gr.Markdown(label="Context")
386
- grade_box = gr.Markdown(label="LLM Evaluation")
387
 
388
  upload_btn.click(system.process_file, inputs=[pdf_input], outputs=[status_msg])
389
- run_btn.click(system.process_query, inputs=[q_input, a_input], outputs=[evidence_box, grade_box])
390
 
391
  if __name__ == "__main__":
392
  demo.launch()
393
-
 
179
 
180
 
181
 
182
+ # import gradio as gr
183
+ # import fitz # PyMuPDF
184
+ # import torch
185
+ # import os
186
+
187
+ # # --- LANGCHAIN & RAG IMPORTS ---
188
+ # from langchain_text_splitters import RecursiveCharacterTextSplitter
189
+ # from langchain_community.vectorstores import FAISS
190
+ # from langchain_core.embeddings import Embeddings
191
+
192
+ # # --- ONNX & MODEL IMPORTS ---
193
+ # from transformers import AutoTokenizer
194
+ # from optimum.onnxruntime import ORTModelForFeatureExtraction, ORTModelForCausalLM
195
+ # from huggingface_hub import snapshot_download
196
+
197
+ # # ---------------------------------------------------------
198
+ # # 1. Custom ONNX Embedding Class (BGE-Large)
199
+ # # ---------------------------------------------------------
200
+ # class OnnxBgeEmbeddings(Embeddings):
201
+ # def __init__(self, model_name="BAAI/bge-large-en-v1.5"):
202
+ # print(f"🔄 Loading Embeddings: {model_name}...")
203
+ # self.tokenizer = AutoTokenizer.from_pretrained(model_name)
204
+ # # Note: export=True will re-convert on every restart.
205
+ # # For production, you'd want to save this permanently, but this works for now.
206
+ # self.model = ORTModelForFeatureExtraction.from_pretrained(model_name, export=True)
207
+
208
+ # def _process_batch(self, texts):
209
+ # inputs = self.tokenizer(texts, padding=True, truncation=True, max_length=512, return_tensors="pt")
210
+ # with torch.no_grad():
211
+ # outputs = self.model(**inputs)
212
+ # # CLS pooling for BGE
213
+ # embeddings = outputs.last_hidden_state[:, 0]
214
+ # embeddings = torch.nn.functional.normalize(embeddings, p=2, dim=1)
215
+ # return embeddings.numpy().tolist()
216
+
217
+ # def embed_documents(self, texts):
218
+ # return self._process_batch(texts)
219
+
220
+ # def embed_query(self, text):
221
+ # return self._process_batch(["Represent this sentence for searching relevant passages: " + text])[0]
222
+
223
+ # # ---------------------------------------------------------
224
+ # # 2. LLM Evaluator Class (Llama-3.2-1B ONNX)
225
+ # # ---------------------------------------------------------
226
+
227
+ # class LLMEvaluator:
228
+ # def __init__(self):
229
+ # self.repo_id = "onnx-community/Llama-3.2-1B-Instruct"
230
+ # self.local_dir = "onnx_llama_local"
231
+
232
+ # print(f"🔄 Preparing LLM: {self.repo_id}...")
233
+
234
+ # # [FIXED DOWNLOADER]
235
+ # print(f"📥 Downloading FP16 model + data to {self.local_dir}...")
236
+ # snapshot_download(
237
+ # repo_id=self.repo_id,
238
+ # local_dir=self.local_dir,
239
+ # local_dir_use_symlinks=False,
240
+ # allow_patterns=[
241
+ # "config.json",
242
+ # "generation_config.json",
243
+ # "tokenizer*",
244
+ # "special_tokens_map.json",
245
+ # "*.jinja",
246
+ # "onnx/model_fp16.onnx*" # WILDCARD '*' ensures we get .onnx AND .onnx_data
247
+ # ]
248
+ # )
249
+ # print("✅ Download complete.")
250
+
251
+ # self.tokenizer = AutoTokenizer.from_pretrained(self.local_dir)
252
+
253
+ # # [CRITICAL FIX]
254
+ # # Separating 'subfolder' and 'file_name' is required by Optimum
255
+ # self.model = ORTModelForCausalLM.from_pretrained(
256
+ # self.local_dir,
257
+ # subfolder="onnx", # Point to the subfolder
258
+ # file_name="model_fp16.onnx", # Just the filename
259
+ # use_cache=True,
260
+ # use_io_binding=False
261
+ # )
262
+
263
+ # def evaluate(self, context, question, student_answer):
264
+ # # Prompt Engineering for Llama 3
265
+ # messages = [
266
+ # {"role": "system", "content": "You are a strict academic. Grade the student answer based ONLY on the provided context."},
267
+ # {"role": "user", "content": f"""
268
+ # ### CONTEXT:
269
+ # {context}
270
+
271
+ # ### QUESTION:
272
+ # {question}
273
+
274
+ # ### STUDENT ANSWER:
275
+ # {student_answer}
276
+
277
+ # ### INSTRUCTIONS:
278
+ # 1. Is the answer correct?
279
+ # 2. Score out of 10.
280
+ # 3. Explanation.
281
+ # """}
282
+ # ]
283
+
284
+ # # Format input using the chat template
285
+ # input_text = self.tokenizer.apply_chat_template(
286
+ # messages,
287
+ # tokenize=False,
288
+ # add_generation_prompt=True
289
+ # )
290
+
291
+ # inputs = self.tokenizer(input_text, return_tensors="pt")
292
+
293
+ # # Generate response
294
+ # with torch.no_grad():
295
+ # outputs = self.model.generate(
296
+ # **inputs,
297
+ # max_new_tokens=256,
298
+ # temperature=0.3,
299
+ # do_sample=True,
300
+ # top_p=0.9
301
+ # )
302
+
303
+ # # Decode response
304
+ # response = self.tokenizer.decode(
305
+ # outputs[0][inputs.input_ids.shape[1]:],
306
+ # skip_special_tokens=True
307
+ # )
308
+ # return response
309
+
310
+ # # ---------------------------------------------------------
311
+ # # 3. Main Application Logic
312
+ # # ---------------------------------------------------------
313
+ # class VectorSystem:
314
+ # def __init__(self):
315
+ # self.vector_store = None
316
+ # self.embeddings = OnnxBgeEmbeddings()
317
+ # self.llm = LLMEvaluator() # Initialize LLM
318
+ # self.all_chunks = []
319
+
320
+ # def process_file(self, file_obj):
321
+ # if file_obj is None: return "No file uploaded."
322
+ # try:
323
+ # text = ""
324
+ # if file_obj.name.endswith('.pdf'):
325
+ # doc = fitz.open(file_obj.name)
326
+ # for page in doc: text += page.get_text()
327
+ # elif file_obj.name.endswith('.txt'):
328
+ # with open(file_obj.name, 'r', encoding='utf-8') as f: text = f.read()
329
+ # else:
330
+ # return "❌ Error: Only .pdf and .txt supported."
331
+
332
+ # text_splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=150)
333
+ # self.all_chunks = text_splitter.split_text(text)
334
+
335
+ # if not self.all_chunks: return "File empty."
336
+
337
+ # metadatas = [{"id": i} for i in range(len(self.all_chunks))]
338
+ # self.vector_store = FAISS.from_texts(self.all_chunks, self.embeddings, metadatas=metadatas)
339
+ # return f"✅ Indexed {len(self.all_chunks)} chunks."
340
+ # except Exception as e:
341
+ # return f"Error: {str(e)}"
342
+
343
+ # def process_query(self, question, student_answer):
344
+ # if not self.vector_store: return "⚠️ Please upload a file first.", ""
345
+ # if not question: return "⚠️ Enter a question.", ""
346
+
347
+ # # 1. Retrieve
348
+ # results = self.vector_store.similarity_search_with_score(question, k=3)
349
+
350
+ # # Prepare context for LLM
351
+ # context_text = "\n\n".join([doc.page_content for doc, _ in results])
352
+
353
+ # # Prepare Evidence Output for UI
354
+ # evidence_display = "### 📚 Retrieved Context:\n"
355
+ # for i, (doc, score) in enumerate(results):
356
+ # evidence_display += f"**Chunk {i+1}** (Score: {score:.4f}):\n> {doc.page_content}\n\n"
357
+
358
+ # # 2. Evaluate (if answer provided)
359
+ # llm_feedback = "Please enter a student answer to grade."
360
+ # if student_answer:
361
+ # llm_feedback = self.llm.evaluate(context_text, question, student_answer)
362
+
363
+ # return evidence_display, llm_feedback
364
+
365
+ # # Initialize
366
+ # system = VectorSystem()
367
+
368
+ # # --- GRADIO UI ---
369
+ # with gr.Blocks(title="EduGenius AI Grader") as demo:
370
+ # gr.Markdown("# 🧠 EduGenius: RAG + LLM Grading")
371
+ # gr.Markdown("Powered by **BGE-Large** (Retrieval) and **Llama-3.2-1B** (Evaluation) - All ONNX Optimized.")
372
+
373
+ # with gr.Row():
374
+ # with gr.Column(scale=1):
375
+ # pdf_input = gr.File(label="1. Upload Chapter (PDF/TXT)")
376
+ # upload_btn = gr.Button("Index Content", variant="primary")
377
+ # status_msg = gr.Textbox(label="System Status", interactive=False)
378
+
379
+ # with gr.Column(scale=2):
380
+ # q_input = gr.Textbox(label="2. Question")
381
+ # a_input = gr.Textbox(label="3. Student Answer")
382
+ # run_btn = gr.Button("Retrieve & Grade", variant="secondary")
383
+
384
+ # with gr.Row():
385
+ # evidence_box = gr.Markdown(label="Context")
386
+ # grade_box = gr.Markdown(label="LLM Evaluation")
387
+
388
+ # upload_btn.click(system.process_file, inputs=[pdf_input], outputs=[status_msg])
389
+ # run_btn.click(system.process_query, inputs=[q_input, a_input], outputs=[evidence_box, grade_box])
390
+
391
+ # if __name__ == "__main__":
392
+ # demo.launch()
393
+
394
+
395
+
396
+
397
+
398
+
399
+
400
+
401
+
402
+
403
+
404
  import gradio as gr
405
  import fitz # PyMuPDF
406
  import torch
 
423
  def __init__(self, model_name="BAAI/bge-large-en-v1.5"):
424
  print(f"🔄 Loading Embeddings: {model_name}...")
425
  self.tokenizer = AutoTokenizer.from_pretrained(model_name)
426
+
427
+ # OPTIMIZATION: Removed export=True.
428
+ # Loading a pre-exported model or caching it is much faster.
429
+ # If you don't have the ONNX version, run export=True ONCE, then set to False.
430
+ self.model = ORTModelForFeatureExtraction.from_pretrained(model_name, export=False)
431
 
432
  def _process_batch(self, texts):
433
  inputs = self.tokenizer(texts, padding=True, truncation=True, max_length=512, return_tensors="pt")
 
447
  # ---------------------------------------------------------
448
  # 2. LLM Evaluator Class (Llama-3.2-1B ONNX)
449
  # ---------------------------------------------------------
 
450
  class LLMEvaluator:
451
  def __init__(self):
452
  self.repo_id = "onnx-community/Llama-3.2-1B-Instruct"
 
454
 
455
  print(f"🔄 Preparing LLM: {self.repo_id}...")
456
 
457
+ # Download usually only needs to happen once
458
+ if not os.path.exists(self.local_dir):
459
+ print(f"📥 Downloading FP16 model + data to {self.local_dir}...")
460
+ snapshot_download(
461
+ repo_id=self.repo_id,
462
+ local_dir=self.local_dir,
463
+ local_dir_use_symlinks=False,
464
+ allow_patterns=["config.json", "generation_config.json", "tokenizer*", "special_tokens_map.json", "*.jinja", "onnx/model_fp16.onnx*"]
465
+ )
466
+ print("✅ Download complete.")
 
 
 
 
 
 
467
 
468
  self.tokenizer = AutoTokenizer.from_pretrained(self.local_dir)
469
 
 
 
470
  self.model = ORTModelForCausalLM.from_pretrained(
471
  self.local_dir,
472
+ subfolder="onnx",
473
+ file_name="model_fp16.onnx",
474
  use_cache=True,
475
  use_io_binding=False
476
  )
477
 
478
+ def evaluate(self, context, question, student_answer, max_marks):
479
+ # OPTIMIZATION: Strict Grading Prompt
480
  messages = [
481
+ {"role": "system", "content": "You are a strict academic grader. You must grade accurately based on the context provided."},
482
  {"role": "user", "content": f"""
483
  ### CONTEXT:
484
  {context}
 
489
  ### STUDENT ANSWER:
490
  {student_answer}
491
 
492
+ ### GRADING INSTRUCTIONS:
493
+ 1. The maximum score for this question is {max_marks}.
494
+ 2. If the answer is completely wrong, give 0.
495
+ 3. If the answer is correct but missing details, deduct marks proportionally.
496
+ 4. DO NOT hallucinate a score higher than {max_marks}.
497
+
498
+ ### OUTPUT FORMAT:
499
+ Score: [Your Score] / {max_marks}
500
+ Feedback: [One sentence explanation]
501
  """}
502
  ]
503
 
504
+ input_text = self.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
 
 
 
 
 
 
505
  inputs = self.tokenizer(input_text, return_tensors="pt")
506
 
 
507
  with torch.no_grad():
508
  outputs = self.model.generate(
509
  **inputs,
510
+ max_new_tokens=150, # Reduced to improve speed
511
+ temperature=0.1, # Lower temperature for stricter, less creative grading
512
+ do_sample=False # Greedy decoding is faster and more deterministic for grading
 
513
  )
514
 
515
+ response = self.tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
 
 
 
 
516
  return response
517
+
518
  # ---------------------------------------------------------
519
  # 3. Main Application Logic
520
  # ---------------------------------------------------------
 
522
  def __init__(self):
523
  self.vector_store = None
524
  self.embeddings = OnnxBgeEmbeddings()
525
+ self.llm = LLMEvaluator()
526
+ self.all_chunks = [] # Stores raw text
527
+ self.total_chunks = 0
528
 
529
  def process_file(self, file_obj):
530
  if file_obj is None: return "No file uploaded."
 
538
  else:
539
  return "❌ Error: Only .pdf and .txt supported."
540
 
541
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100)
542
  self.all_chunks = text_splitter.split_text(text)
543
+ self.total_chunks = len(self.all_chunks)
544
 
545
  if not self.all_chunks: return "File empty."
546
 
547
+ # OPTIMIZATION: Store explicit IDs to allow neighbor retrieval
548
+ metadatas = [{"id": i} for i in range(self.total_chunks)]
549
  self.vector_store = FAISS.from_texts(self.all_chunks, self.embeddings, metadatas=metadatas)
550
+
551
+ return f"✅ Indexed {self.total_chunks} chunks."
552
  except Exception as e:
553
  return f"Error: {str(e)}"
554
 
555
+ def process_query(self, question, student_answer, max_marks):
556
  if not self.vector_store: return "⚠️ Please upload a file first.", ""
557
  if not question: return "⚠️ Enter a question.", ""
558
 
559
+ # 1. Retrieve ONLY Top 1 Chunk
560
+ results = self.vector_store.similarity_search_with_score(question, k=1)
561
+ top_doc, score = results[0]
562
 
563
+ # 2. Context Expansion (Preceding + Succeeding Chunks)
564
+ # Get the ID of the best match
565
+ center_id = top_doc.metadata['id']
566
 
567
+ # Calculate indices (handle boundaries)
568
+ start_id = max(0, center_id - 1)
569
+ end_id = min(self.total_chunks - 1, center_id + 1)
570
+
571
+ # Fetch the contiguous text block
572
+ expanded_context = ""
573
+ context_indices = []
574
+
575
+ for i in range(start_id, end_id + 1):
576
+ expanded_context += self.all_chunks[i] + "\n"
577
+ context_indices.append(i)
578
 
579
+ # UI Evidence Display
580
+ evidence_display = f"### 📚 Expanded Context (Chunks {start_id} to {end_id}):\n"
581
+ evidence_display += f"> ... {expanded_context} ..."
582
+
583
+ # 3. Evaluate
584
  llm_feedback = "Please enter a student answer to grade."
585
  if student_answer:
586
+ llm_feedback = self.llm.evaluate(expanded_context, question, student_answer, max_marks)
587
 
588
  return evidence_display, llm_feedback
589
 
 
592
 
593
  # --- GRADIO UI ---
594
  with gr.Blocks(title="EduGenius AI Grader") as demo:
595
+ gr.Markdown("# 🧠 EduGenius: Smart Context Grading")
596
+
 
597
  with gr.Row():
598
  with gr.Column(scale=1):
599
+ pdf_input = gr.File(label="1. Upload Chapter")
600
  upload_btn = gr.Button("Index Content", variant="primary")
601
+ status_msg = gr.Textbox(label="Status", interactive=False)
602
 
603
  with gr.Column(scale=2):
604
+ with gr.Row():
605
+ q_input = gr.Textbox(label="Question", scale=2)
606
+ max_marks = gr.Slider(minimum=1, maximum=20, value=5, step=1, label="Max Marks")
607
+
608
+ a_input = gr.TextArea(label="Student Answer")
609
  run_btn = gr.Button("Retrieve & Grade", variant="secondary")
610
 
611
  with gr.Row():
612
+ evidence_box = gr.Markdown(label="Context Used")
613
+ grade_box = gr.Markdown(label="Grading Result")
614
 
615
  upload_btn.click(system.process_file, inputs=[pdf_input], outputs=[status_msg])
616
+ run_btn.click(system.process_query, inputs=[q_input, a_input, max_marks], outputs=[evidence_box, grade_box])
617
 
618
  if __name__ == "__main__":
619
  demo.launch()