Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -366,24 +366,71 @@ Start immediately with Q1. Do not include any introduction or explanation.
|
|
| 366 |
def grade_student_answers(questions, student_answers, grade_level, topic, subject=None):
|
| 367 |
"""Grade all student answers and provide AI feedback - NO STRICT FORMAT REQUIRED"""
|
| 368 |
|
| 369 |
-
if not questions
|
| 370 |
-
return " No questions
|
| 371 |
-
|
| 372 |
feedback_list = []
|
| 373 |
-
|
| 374 |
for i, question_text in enumerate(questions):
|
| 375 |
-
student_answer = student_answers.get(i,
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
continue
|
| 380 |
-
|
| 381 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
grading_prompt = f"""You are an experienced UNEB examiner for {grade_level} students. Subject: {subject or 'General'}
|
| 383 |
|
| 384 |
Question {i+1}: {question_text}
|
| 385 |
|
| 386 |
-
Student's Answer: {
|
| 387 |
|
| 388 |
Evaluate this answer:
|
| 389 |
- Is it correct, partially correct, or incorrect?
|
|
@@ -392,10 +439,10 @@ Evaluate this answer:
|
|
| 392 |
- If incorrect, briefly explain the correct approach
|
| 393 |
|
| 394 |
Accept answers in ANY format/style as long as they address the question. Be constructive and encouraging."""
|
| 395 |
-
|
| 396 |
grade_response, _ = ask_ai(grading_prompt, temperature=0.3)
|
| 397 |
feedback_list.append(f"Q{i+1}:\n{grade_response}\n")
|
| 398 |
-
|
| 399 |
full_feedback = "\n" + "="*60 + "\n".join(feedback_list)
|
| 400 |
return full_feedback
|
| 401 |
|
|
@@ -408,6 +455,7 @@ class StudentSession:
|
|
| 408 |
self.current_topic = None
|
| 409 |
self.current_grade = None
|
| 410 |
self.last_feedback = None
|
|
|
|
| 411 |
|
| 412 |
def clear_session(self):
|
| 413 |
self.current_questions = []
|
|
@@ -640,6 +688,11 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 640 |
|
| 641 |
# Global state for questions
|
| 642 |
questions_state = gr.State(value=[])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
|
| 644 |
# Student name input
|
| 645 |
with gr.Group():
|
|
@@ -691,10 +744,7 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 691 |
num_questions_input = gr.Slider(minimum=1, maximum=100, step=1, value=20, label="Number of Questions")
|
| 692 |
generate_btn = gr.Button(" Generate Questions", variant="primary", size="lg")
|
| 693 |
|
| 694 |
-
|
| 695 |
-
value="",
|
| 696 |
-
elem_classes="question-display"
|
| 697 |
-
)
|
| 698 |
|
| 699 |
status_output = gr.Textbox(
|
| 700 |
label="Status",
|
|
@@ -727,7 +777,7 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 727 |
generate_btn.click(
|
| 728 |
fn=generate_and_display,
|
| 729 |
inputs=[grade_input, subject_input, topic_input, num_questions_input],
|
| 730 |
-
outputs=[
|
| 731 |
)
|
| 732 |
# Download handler - returns file path for gr.DownloadButton
|
| 733 |
def download_qns():
|
|
@@ -850,11 +900,13 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 850 |
|
| 851 |
# Check which method was used
|
| 852 |
if canvas_input is not None:
|
| 853 |
-
|
|
|
|
| 854 |
return f" Canvas submission received with {len(session.current_questions)} questions. Processing..."
|
| 855 |
|
| 856 |
elif upload_input is not None:
|
| 857 |
-
|
|
|
|
| 858 |
return f" Photo submission received with {len(session.current_questions)} questions. Processing..."
|
| 859 |
|
| 860 |
elif typed_input and typed_input.strip():
|
|
@@ -942,6 +994,20 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 942 |
lines=20,
|
| 943 |
elem_classes="feedback-box"
|
| 944 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 945 |
|
| 946 |
with gr.Row():
|
| 947 |
download_feedback_btn = gr.DownloadButton(" Download Feedback (TXT)")
|
|
@@ -967,6 +1033,8 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 967 |
)
|
| 968 |
|
| 969 |
session.last_feedback = feedback
|
|
|
|
|
|
|
| 970 |
return feedback
|
| 971 |
|
| 972 |
correction_btn.click(
|
|
@@ -991,6 +1059,40 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 991 |
inputs=[],
|
| 992 |
outputs=download_feedback_btn
|
| 993 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 994 |
|
| 995 |
# ===== TAB 4: START NEW SESSION =====
|
| 996 |
with gr.Tab(" New Session"):
|
|
|
|
| 366 |
def grade_student_answers(questions, student_answers, grade_level, topic, subject=None):
|
| 367 |
"""Grade all student answers and provide AI feedback - NO STRICT FORMAT REQUIRED"""
|
| 368 |
|
| 369 |
+
if not questions:
|
| 370 |
+
return " No questions to grade."
|
| 371 |
+
|
| 372 |
feedback_list = []
|
| 373 |
+
|
| 374 |
for i, question_text in enumerate(questions):
|
| 375 |
+
student_answer = student_answers.get(i, None)
|
| 376 |
+
|
| 377 |
+
# No answer submitted for this question
|
| 378 |
+
if student_answer is None:
|
| 379 |
+
# Ask AI for a short guidance/hint for this question (best-effort)
|
| 380 |
+
hint_prompt = f"""You are an experienced UNEB examiner for {grade_level} students.
|
| 381 |
+
|
| 382 |
+
Question {i+1}: {question_text}
|
| 383 |
+
|
| 384 |
+
The student did not submit an answer. Provide a very brief (2-3 line) approach or hint that would help the student answer this question."""
|
| 385 |
+
hint_resp, _ = ask_ai(hint_prompt, temperature=0.3)
|
| 386 |
+
if not hint_resp or "All AI services failed" in hint_resp:
|
| 387 |
+
hint_resp = "No answer submitted. Expected: show step-by-step working and final answer; include units where applicable."
|
| 388 |
+
feedback_list.append(f"Q{i+1}: [NOT ANSWERED]\nGuidance: {hint_resp}\n")
|
| 389 |
continue
|
| 390 |
+
|
| 391 |
+
# Handle image answers (canvas/uploaded photos)
|
| 392 |
+
try:
|
| 393 |
+
from PIL import Image as PILImage
|
| 394 |
+
is_image = isinstance(student_answer, PILImage.Image)
|
| 395 |
+
except Exception:
|
| 396 |
+
is_image = False
|
| 397 |
+
|
| 398 |
+
if is_image:
|
| 399 |
+
grading_prompt = f"""You are an experienced UNEB examiner for {grade_level} students. Subject: {subject or 'General'}
|
| 400 |
+
|
| 401 |
+
Question {i+1}: {question_text}
|
| 402 |
+
|
| 403 |
+
The student submitted handwritten work as an image. Analyse the image, extract the student's answer for this question, and then:
|
| 404 |
+
- State whether the answer is correct, partially correct, or incorrect
|
| 405 |
+
- Provide a mark/score
|
| 406 |
+
- Give brief constructive feedback and the correct approach if needed
|
| 407 |
+
|
| 408 |
+
If you cannot analyse images, state that image analysis requires a vision-capable model."""
|
| 409 |
+
|
| 410 |
+
grade_response, _ = ask_ai(grading_prompt, temperature=0.3, image=student_answer)
|
| 411 |
+
feedback_list.append(f"Q{i+1}:\n{grade_response}\n")
|
| 412 |
+
continue
|
| 413 |
+
|
| 414 |
+
# Treat as text answer
|
| 415 |
+
text_ans = str(student_answer).strip()
|
| 416 |
+
if not text_ans:
|
| 417 |
+
# empty string case
|
| 418 |
+
hint_prompt = f"""You are an experienced UNEB examiner for {grade_level} students.
|
| 419 |
+
|
| 420 |
+
Question {i+1}: {question_text}
|
| 421 |
+
|
| 422 |
+
The student submitted an empty/text-missing answer. Provide a very brief (2-3 line) approach or hint that would help the student answer this question."""
|
| 423 |
+
hint_resp, _ = ask_ai(hint_prompt, temperature=0.3)
|
| 424 |
+
if not hint_resp or "All AI services failed" in hint_resp:
|
| 425 |
+
hint_resp = "No answer submitted. Expected: show step-by-step working and final answer; include units where applicable."
|
| 426 |
+
feedback_list.append(f"Q{i+1}: [NOT ANSWERED]\nGuidance: {hint_resp}\n")
|
| 427 |
+
continue
|
| 428 |
+
|
| 429 |
grading_prompt = f"""You are an experienced UNEB examiner for {grade_level} students. Subject: {subject or 'General'}
|
| 430 |
|
| 431 |
Question {i+1}: {question_text}
|
| 432 |
|
| 433 |
+
Student's Answer: {text_ans}
|
| 434 |
|
| 435 |
Evaluate this answer:
|
| 436 |
- Is it correct, partially correct, or incorrect?
|
|
|
|
| 439 |
- If incorrect, briefly explain the correct approach
|
| 440 |
|
| 441 |
Accept answers in ANY format/style as long as they address the question. Be constructive and encouraging."""
|
| 442 |
+
|
| 443 |
grade_response, _ = ask_ai(grading_prompt, temperature=0.3)
|
| 444 |
feedback_list.append(f"Q{i+1}:\n{grade_response}\n")
|
| 445 |
+
|
| 446 |
full_feedback = "\n" + "="*60 + "\n".join(feedback_list)
|
| 447 |
return full_feedback
|
| 448 |
|
|
|
|
| 455 |
self.current_topic = None
|
| 456 |
self.current_grade = None
|
| 457 |
self.last_feedback = None
|
| 458 |
+
self.correction_conversation = [] # list of {'role':'user'|'assistant','content':str}
|
| 459 |
|
| 460 |
def clear_session(self):
|
| 461 |
self.current_questions = []
|
|
|
|
| 688 |
|
| 689 |
# Global state for questions
|
| 690 |
questions_state = gr.State(value=[])
|
| 691 |
+
# Shared question display (visible across tabs so students don't switch panels)
|
| 692 |
+
questions_display = gr.HTML(
|
| 693 |
+
value="",
|
| 694 |
+
elem_classes="question-display"
|
| 695 |
+
)
|
| 696 |
|
| 697 |
# Student name input
|
| 698 |
with gr.Group():
|
|
|
|
| 744 |
num_questions_input = gr.Slider(minimum=1, maximum=100, step=1, value=20, label="Number of Questions")
|
| 745 |
generate_btn = gr.Button(" Generate Questions", variant="primary", size="lg")
|
| 746 |
|
| 747 |
+
# (questions_display is shared above - no local questions_output here)
|
|
|
|
|
|
|
|
|
|
| 748 |
|
| 749 |
status_output = gr.Textbox(
|
| 750 |
label="Status",
|
|
|
|
| 777 |
generate_btn.click(
|
| 778 |
fn=generate_and_display,
|
| 779 |
inputs=[grade_input, subject_input, topic_input, num_questions_input],
|
| 780 |
+
outputs=[questions_display, questions_state, status_output]
|
| 781 |
)
|
| 782 |
# Download handler - returns file path for gr.DownloadButton
|
| 783 |
def download_qns():
|
|
|
|
| 900 |
|
| 901 |
# Check which method was used
|
| 902 |
if canvas_input is not None:
|
| 903 |
+
# Map the same canvas image to all question indices so each question gets graded
|
| 904 |
+
session.current_answers = {i: canvas_input for i in range(len(session.current_questions))}
|
| 905 |
return f" Canvas submission received with {len(session.current_questions)} questions. Processing..."
|
| 906 |
|
| 907 |
elif upload_input is not None:
|
| 908 |
+
# Map the uploaded image to all question indices so AI can analyse each part
|
| 909 |
+
session.current_answers = {i: upload_input for i in range(len(session.current_questions))}
|
| 910 |
return f" Photo submission received with {len(session.current_questions)} questions. Processing..."
|
| 911 |
|
| 912 |
elif typed_input and typed_input.strip():
|
|
|
|
| 994 |
lines=20,
|
| 995 |
elem_classes="feedback-box"
|
| 996 |
)
|
| 997 |
+
|
| 998 |
+
# Conversation area for follow-up questions about feedback
|
| 999 |
+
conversation_display = gr.Textbox(
|
| 1000 |
+
label="Correction Conversation",
|
| 1001 |
+
lines=12,
|
| 1002 |
+
interactive=False
|
| 1003 |
+
)
|
| 1004 |
+
|
| 1005 |
+
followup_input = gr.Textbox(
|
| 1006 |
+
label="Ask a follow-up question",
|
| 1007 |
+
placeholder="Ask about a specific question or the feedback...",
|
| 1008 |
+
lines=2
|
| 1009 |
+
)
|
| 1010 |
+
followup_btn = gr.Button(" Ask Follow-up")
|
| 1011 |
|
| 1012 |
with gr.Row():
|
| 1013 |
download_feedback_btn = gr.DownloadButton(" Download Feedback (TXT)")
|
|
|
|
| 1033 |
)
|
| 1034 |
|
| 1035 |
session.last_feedback = feedback
|
| 1036 |
+
# initialize conversation with assistant feedback
|
| 1037 |
+
session.correction_conversation = [{'role':'assistant', 'content': feedback}]
|
| 1038 |
return feedback
|
| 1039 |
|
| 1040 |
correction_btn.click(
|
|
|
|
| 1059 |
inputs=[],
|
| 1060 |
outputs=download_feedback_btn
|
| 1061 |
)
|
| 1062 |
+
|
| 1063 |
+
# Follow-up handler: student asks a question about the feedback
|
| 1064 |
+
def send_followup(question_text):
|
| 1065 |
+
if not session.last_feedback:
|
| 1066 |
+
return "Please generate corrections first.", None
|
| 1067 |
+
|
| 1068 |
+
if not question_text or not question_text.strip():
|
| 1069 |
+
return "Please type a follow-up question.", None
|
| 1070 |
+
|
| 1071 |
+
# Build a conversational prompt referencing the previous feedback
|
| 1072 |
+
convo_context = ''
|
| 1073 |
+
# include last assistant feedback
|
| 1074 |
+
convo_context += session.last_feedback + "\n\n"
|
| 1075 |
+
followup_prompt = f"You are an experienced UNEB examiner. The student received the following feedback:\n\n{convo_context}\nNow the student asks: {question_text}\n\nAnswer concisely, refer to the specific question number if applicable, and be encouraging."
|
| 1076 |
+
|
| 1077 |
+
resp, _ = ask_ai(followup_prompt, temperature=0.4)
|
| 1078 |
+
|
| 1079 |
+
# Append to session conversation
|
| 1080 |
+
session.correction_conversation.append({'role':'user', 'content': question_text})
|
| 1081 |
+
session.correction_conversation.append({'role':'assistant', 'content': resp})
|
| 1082 |
+
|
| 1083 |
+
# Build display text combining conversation
|
| 1084 |
+
display = ''
|
| 1085 |
+
for msg in session.correction_conversation:
|
| 1086 |
+
prefix = 'Student:' if msg['role']=='user' else 'AI:'
|
| 1087 |
+
display += f"{prefix} {msg['content']}\n\n"
|
| 1088 |
+
|
| 1089 |
+
return '', display
|
| 1090 |
+
|
| 1091 |
+
followup_btn.click(
|
| 1092 |
+
fn=send_followup,
|
| 1093 |
+
inputs=[followup_input],
|
| 1094 |
+
outputs=[followup_input, conversation_display]
|
| 1095 |
+
)
|
| 1096 |
|
| 1097 |
# ===== TAB 4: START NEW SESSION =====
|
| 1098 |
with gr.Tab(" New Session"):
|