Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -105,130 +105,213 @@ def ask_ai(prompt, temperature=0.7, max_retries=2, image=None):
|
|
| 105 |
|
| 106 |
return " All AI services failed. Please try again later.", "error"
|
| 107 |
|
| 108 |
-
# ---------- 3. Uganda Primary Curriculum Syllabus (P6 & P7 Only) ----------
|
|
|
|
|
|
|
| 109 |
syllabus_topics = {
|
| 110 |
-
"Primary 6":
|
| 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 |
-
# ---------- 4. Generate Practice Questions (Customizable) ----------
|
| 137 |
-
def generate_sample_questions(grade_level, topic, num_questions=10):
|
| 138 |
-
"""Fallback: Generate sample questions locally when AI is unavailable"""
|
| 139 |
-
samples = {
|
| 140 |
-
"Integers & Operations": [
|
| 141 |
-
"Q1. Calculate the sum of -20 and -15.",
|
| 142 |
-
"Q2. A farmer bought 40 oranges and then sold 25 of them. What is the difference between the number of oranges bought and sold?",
|
| 143 |
-
"Q3. Simplify: 36 - (-10) + 5. Q10. Find the value of -2(8) + 15.",
|
| 144 |
-
"Q11. A car is parked at -10 meters. If it moves up a slope of 15 meters, what is its final position?",
|
| 145 |
-
"Q12. Calculate the product of -4 and 5.",
|
| 146 |
-
"Q13. A boat descends 12 meters below sea level, then rises 8 meters. What is its final position relative to sea level?",
|
| 147 |
-
"Q16. Simplify: 2(-3) + 5(-2).",
|
| 148 |
-
"Q17. A rabbit hops 7 meters forward and then 4 meters backward. What is the net distance covered by the rabbit?",
|
| 149 |
-
"Q18. Calculate the value of 2/3(-18) + 5.",
|
| 150 |
-
"Q19. A plane descends 300 meters and then ascends 200 meters. What is the plane's final position relative to the initial descent point?",
|
| 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 |
-
"Q8. What is the area of a triangle with base 8 cm and height 6 cm?",
|
| 185 |
-
"Q9. Calculate the perimeter of a rectangle with length 10 cm and width 6 cm.",
|
| 186 |
-
"Q10. What are supplementary angles?",
|
| 187 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
}
|
| 189 |
|
| 190 |
-
|
|
|
|
| 191 |
topic_key = None
|
| 192 |
-
for key in
|
| 193 |
if key.lower() in topic.lower() or topic.lower() in key.lower():
|
| 194 |
topic_key = key
|
| 195 |
break
|
| 196 |
|
| 197 |
if topic_key:
|
| 198 |
-
qs =
|
| 199 |
else:
|
| 200 |
-
|
|
|
|
| 201 |
|
| 202 |
return "\n\n".join(qs)
|
| 203 |
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
"""Generate multiple questions with robust text parsing (not JSON)"""
|
| 207 |
|
| 208 |
if not topic:
|
| 209 |
return None, "⚠️ Please select a topic first!"
|
| 210 |
|
| 211 |
-
prompt = f"""Generate exactly {num_questions} UNEB-style
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
Q1. [Question text here]
|
| 215 |
-
Q2. [Question text here]
|
| 216 |
-
Q3. [Question text here]
|
| 217 |
-
... and so on up to Q{num_questions}
|
| 218 |
-
|
| 219 |
-
Each question should be:
|
| 220 |
-
- Clear and exam-like
|
| 221 |
-
- Appropriate difficulty for {grade_level}
|
| 222 |
-
- Self-contained (includes all necessary information)
|
| 223 |
-
- Solvable in 2-5 minutes
|
| 224 |
-
|
| 225 |
-
Start immediately with Q1. Do not include any introduction or explanation."""
|
| 226 |
|
| 227 |
response, source = ask_ai(prompt, temperature=0.6)
|
| 228 |
|
| 229 |
-
# Fallback: if AI fails,
|
| 230 |
-
if "All AI services failed" in response or response.
|
| 231 |
-
response = generate_sample_questions(grade_level, topic, num_questions)
|
| 232 |
|
| 233 |
# Parse questions robustly using regex to support multi-digit numbers (Q1..Q10..)
|
| 234 |
questions = []
|
|
@@ -267,7 +350,7 @@ Start immediately with Q1. Do not include any introduction or explanation."""
|
|
| 267 |
return questions, formatted
|
| 268 |
|
| 269 |
# ---------- 5. Grade/Mark Student Answers ----------
|
| 270 |
-
def grade_student_answers(questions, student_answers, grade_level, topic):
|
| 271 |
"""Grade all student answers and provide feedback"""
|
| 272 |
|
| 273 |
if not questions or not student_answers:
|
|
@@ -283,7 +366,7 @@ def grade_student_answers(questions, student_answers, grade_level, topic):
|
|
| 283 |
continue
|
| 284 |
|
| 285 |
# Create grading prompt
|
| 286 |
-
grading_prompt = f"""You are an experienced UNEB examiner for {grade_level} students.
|
| 287 |
|
| 288 |
Question: {question_text}
|
| 289 |
|
|
@@ -323,17 +406,19 @@ class StudentSession:
|
|
| 323 |
session = StudentSession()
|
| 324 |
|
| 325 |
# ---------- 7. Download Functions ----------
|
| 326 |
-
def download_questions_file(questions_list, topic, grade_level):
|
| 327 |
"""Download questions as text file"""
|
| 328 |
if not questions_list:
|
| 329 |
return None
|
| 330 |
|
| 331 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 332 |
-
|
|
|
|
| 333 |
|
| 334 |
content = f"""UNEB EXAM PRACTICE QUESTIONS
|
| 335 |
{'='*50}
|
| 336 |
Grade Level: {grade_level}
|
|
|
|
| 337 |
Topic: {topic}
|
| 338 |
Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 339 |
Total Questions: {len(questions_list)}
|
|
@@ -363,7 +448,7 @@ Good luck!
|
|
| 363 |
return filepath
|
| 364 |
|
| 365 |
|
| 366 |
-
def download_questions_pdf(questions_list, topic, grade_level):
|
| 367 |
"""Generate a simple PDF with the questions, return filepath."""
|
| 368 |
try:
|
| 369 |
from reportlab.lib.pagesizes import A4
|
|
@@ -374,7 +459,8 @@ def download_questions_pdf(questions_list, topic, grade_level):
|
|
| 374 |
return None
|
| 375 |
|
| 376 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 377 |
-
|
|
|
|
| 378 |
filepath = f"downloads/{filename}"
|
| 379 |
os.makedirs("downloads", exist_ok=True)
|
| 380 |
|
|
@@ -386,7 +472,7 @@ def download_questions_pdf(questions_list, topic, grade_level):
|
|
| 386 |
|
| 387 |
elems.append(Paragraph("UNEB EXAM PRACTICE QUESTIONS", styles['Title']))
|
| 388 |
elems.append(Spacer(1, 4*mm))
|
| 389 |
-
meta = f"Grade Level: {grade_level} Topic: {topic} Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 390 |
elems.append(Paragraph(meta, normal))
|
| 391 |
elems.append(Spacer(1, 6*mm))
|
| 392 |
|
|
@@ -402,18 +488,20 @@ def download_questions_pdf(questions_list, topic, grade_level):
|
|
| 402 |
except Exception:
|
| 403 |
return None
|
| 404 |
|
| 405 |
-
def download_feedback_file(feedback_text, topic, grade_level):
|
| 406 |
"""Download AI feedback/corrections"""
|
| 407 |
if not feedback_text:
|
| 408 |
return None
|
| 409 |
|
| 410 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 411 |
-
|
|
|
|
| 412 |
|
| 413 |
content = f"""AI CORRECTION & FEEDBACK
|
| 414 |
{'='*50}
|
| 415 |
Student: {session.student_name}
|
| 416 |
Grade Level: {grade_level}
|
|
|
|
| 417 |
Topic: {topic}
|
| 418 |
Date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 419 |
{'='*50}
|
|
@@ -473,7 +561,7 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 473 |
|
| 474 |
gr.Markdown("""
|
| 475 |
# UNEB Exam Practice
|
| 476 |
-
## Primary 6 & 7
|
| 477 |
""")
|
| 478 |
|
| 479 |
# Global state for questions
|
|
@@ -496,7 +584,7 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 496 |
with gr.Tabs():
|
| 497 |
# ===== TAB 1: GENERATE QUESTIONS =====
|
| 498 |
with gr.Tab("1️⃣ Generate Questions"):
|
| 499 |
-
gr.Markdown("### Step 1: Generate Practice Questions")
|
| 500 |
|
| 501 |
with gr.Row():
|
| 502 |
grade_input = gr.Dropdown(
|
|
@@ -504,20 +592,30 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 504 |
label="Grade Level",
|
| 505 |
value="Primary 7"
|
| 506 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
topic_input = gr.Dropdown(
|
| 508 |
label="Topic",
|
| 509 |
-
choices=syllabus_topics["Primary 7"],
|
| 510 |
-
value=syllabus_topics["Primary 7"][0]
|
| 511 |
)
|
| 512 |
|
| 513 |
-
# Update topics when grade changes
|
| 514 |
-
def update_topics(grade):
|
| 515 |
-
|
|
|
|
|
|
|
|
|
|
| 516 |
|
| 517 |
-
grade_input.change(update_topics, grade_input, topic_input)
|
|
|
|
| 518 |
|
| 519 |
with gr.Row():
|
| 520 |
-
|
|
|
|
| 521 |
|
| 522 |
questions_output = gr.Markdown(
|
| 523 |
value="",
|
|
@@ -535,8 +633,8 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 535 |
copy_btn = gr.Button(" Copy Questions")
|
| 536 |
|
| 537 |
# Generate questions handler
|
| 538 |
-
def generate_and_display(grade, topic):
|
| 539 |
-
questions_list, formatted_text = generate_practice_questions(grade, topic, num_questions=
|
| 540 |
|
| 541 |
if questions_list is None:
|
| 542 |
return "", formatted_text, questions_state.value, " Generation failed"
|
|
@@ -544,28 +642,30 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 544 |
session.current_questions = questions_list
|
| 545 |
session.current_grade = grade
|
| 546 |
session.current_topic = topic
|
|
|
|
|
|
|
| 547 |
|
| 548 |
# For Markdown display, keep spacing and simple formatting
|
| 549 |
md_text = "\n\n".join([f"**{i+1}.** {q.split('.',1)[1].strip() if '.' in q else q}" for i, q in enumerate(questions_list)])
|
| 550 |
-
return md_text, md_text, questions_list, f" Generated {len(questions_list)} questions on {topic}"
|
| 551 |
|
| 552 |
generate_btn.click(
|
| 553 |
fn=generate_and_display,
|
| 554 |
-
inputs=[grade_input, topic_input],
|
| 555 |
outputs=[questions_output, questions_output, questions_state, status_output]
|
| 556 |
-
)
|
| 557 |
-
|
| 558 |
# Download handler - returns file path for gr.DownloadButton
|
| 559 |
def download_qns():
|
| 560 |
if not session.current_questions:
|
| 561 |
return None
|
| 562 |
try:
|
| 563 |
# Prefer PDF export; fall back to plain text if PDF library missing
|
| 564 |
-
|
|
|
|
| 565 |
if pdf_path:
|
| 566 |
return pdf_path
|
| 567 |
# Fallback to text
|
| 568 |
-
filepath = download_questions_file(session.current_questions, session.current_topic, session.current_grade)
|
| 569 |
return filepath
|
| 570 |
except Exception as e:
|
| 571 |
return None
|
|
@@ -710,7 +810,8 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 710 |
session.current_questions,
|
| 711 |
session.current_answers,
|
| 712 |
session.current_grade,
|
| 713 |
-
session.current_topic
|
|
|
|
| 714 |
)
|
| 715 |
|
| 716 |
session.last_feedback = feedback
|
|
@@ -727,7 +828,8 @@ with gr.Blocks(title="UNEB Exam Prep - Primary 6 & 7", theme=gr.themes.Soft(), c
|
|
| 727 |
if not session.last_feedback:
|
| 728 |
return None
|
| 729 |
try:
|
| 730 |
-
|
|
|
|
| 731 |
return filepath
|
| 732 |
except Exception as e:
|
| 733 |
return None
|
|
|
|
| 105 |
|
| 106 |
return " All AI services failed. Please try again later.", "error"
|
| 107 |
|
| 108 |
+
# ---------- 3. Uganda Primary Curriculum Syllabus (P6 & P7 Only, by Subject) ----------
|
| 109 |
+
# Topics below are expanded and aligned to typical NCDC/UNEB primary curriculum themes
|
| 110 |
+
# (see https://ncdc.go.ug/ and Ministry of Education resources for official documents).
|
| 111 |
syllabus_topics = {
|
| 112 |
+
"Primary 6": {
|
| 113 |
+
"Mathematics": [
|
| 114 |
+
"Whole Numbers - Addition & Subtraction",
|
| 115 |
+
"Whole Numbers - Multiplication & Division",
|
| 116 |
+
"Factors, Multiples and Prime Numbers",
|
| 117 |
+
"Fractions & Decimals",
|
| 118 |
+
"Money & Making Change",
|
| 119 |
+
"Measurement - Length, Mass & Capacity",
|
| 120 |
+
"Time - Hours, Minutes & Conversion",
|
| 121 |
+
"Geometry - Shapes, Symmetry & Angles (basic)",
|
| 122 |
+
"Data Handling - Tables, Bar Graphs & Pictograms",
|
| 123 |
+
"Ratio & Proportion",
|
| 124 |
+
"Introduction to Algebra - Simple Equations",
|
| 125 |
+
"Basic Percentages and Problem Solving"
|
| 126 |
+
],
|
| 127 |
+
"English": [
|
| 128 |
+
"Reading Comprehension - Short Passages",
|
| 129 |
+
"Grammar - Tenses, Parts of Speech, Sentence Structure",
|
| 130 |
+
"Vocabulary Building & Spelling",
|
| 131 |
+
"Composition - Story and Letter Writing",
|
| 132 |
+
"Punctuation & Capitalization",
|
| 133 |
+
"Clarity in Expression - Cohesion and Coherence",
|
| 134 |
+
"Cloze Tests & Short Answer Questions",
|
| 135 |
+
"Listening and Speaking Basics"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
],
|
| 137 |
+
"Social Studies": [
|
| 138 |
+
"Local Community - Roles, Services & Leaders",
|
| 139 |
+
"Local History & Traditions",
|
| 140 |
+
"Civics - Rights, Responsibilities & Good Citizenship",
|
| 141 |
+
"Map Skills - Directions, Symbols, Scale (basic)",
|
| 142 |
+
"Resources and Local Economy - Farming, Trade, Markets",
|
| 143 |
+
"Culture, Customs and Heritage",
|
| 144 |
+
"Environment & Conservation - Local Examples",
|
| 145 |
+
"Health, Sanitation and Community Wellbeing",
|
| 146 |
+
"Basic Local Government Structures and Participation",
|
| 147 |
+
"Road Safety and Community Rules"
|
| 148 |
],
|
| 149 |
+
"Science": [
|
| 150 |
+
"Living & Non-Living Things - Characteristics",
|
| 151 |
+
"Plants - Parts and Functions",
|
| 152 |
+
"Animals - Habitats and Adaptations",
|
| 153 |
+
"Human Body - Health, Nutrition & Hygiene",
|
| 154 |
+
"Materials & Their Properties",
|
| 155 |
+
"Forces and Motion - Simple Examples",
|
| 156 |
+
"Light, Heat and Sound (basic concepts)",
|
| 157 |
+
"Environment and Natural Resources",
|
| 158 |
+
"Simple Experiments and Observations"
|
| 159 |
+
]
|
| 160 |
+
},
|
| 161 |
+
"Primary 7": {
|
| 162 |
+
"Mathematics": [
|
| 163 |
+
"Integers & Operations",
|
| 164 |
+
"Fractions - Addition, Subtraction, Multiplication & Division",
|
| 165 |
+
"Decimals & Percentages",
|
| 166 |
+
"Ratio, Rate & Proportion",
|
| 167 |
+
"Algebraic Expressions & Simple Equations",
|
| 168 |
+
"Geometry - Angles, Triangles and Quadrilaterals",
|
| 169 |
+
"Mensuration - Area, Perimeter and Volume (basic)",
|
| 170 |
+
"Statistics & Probability - Averages and Data Interpretation",
|
| 171 |
+
"Coordinate Geometry - Introduction",
|
| 172 |
+
"Number Theory - Factors, HCF & LCM",
|
| 173 |
+
"Problem Solving Strategies"
|
| 174 |
],
|
| 175 |
+
"English": [
|
| 176 |
+
"Comprehension - Longer Passages & Questioning",
|
| 177 |
+
"Grammar - Sentence Transformation, Tenses, Agreement",
|
| 178 |
+
"Composition - Stories, Letters, Reports and Dialogues",
|
| 179 |
+
"Cloze & Summary Writing",
|
| 180 |
+
"Vocabulary - Synonyms, Antonyms & Contextual Use",
|
| 181 |
+
"Listening Skills and Oral Expression",
|
| 182 |
+
"Directed Writing and Examination Techniques"
|
|
|
|
|
|
|
|
|
|
| 183 |
],
|
| 184 |
+
"Social Studies": [
|
| 185 |
+
"History - Key Events in Uganda and East Africa (pre-colonial, colonial, independence)",
|
| 186 |
+
"Civics and Governance - Structure of Government, Roles and Rights",
|
| 187 |
+
"Geography - Maps, Physical Features, Weather, Climate and Resources",
|
| 188 |
+
"Economy - Agriculture, Trade, Markets, Production and Consumption",
|
| 189 |
+
"Community Development - Projects, Participation and Leadership",
|
| 190 |
+
"Citizenship Education - Rights, Responsibilities and Human Rights",
|
| 191 |
+
"Culture, National Symbols and Heritage",
|
| 192 |
+
"Local and National Government - Functions and Services",
|
| 193 |
+
"Environmental Issues - Conservation, Deforestation, Pollution",
|
| 194 |
+
"Global Connections - Trade, Aid and Regional Cooperation"
|
| 195 |
+
],
|
| 196 |
+
"Science": [
|
| 197 |
+
"Living Things - Classification, Life Cycles and Ecosystems",
|
| 198 |
+
"Plants and Animals - Structure and Function",
|
| 199 |
+
"Human Body Systems - Digestive, Respiratory, Circulatory (basic)",
|
| 200 |
+
"Health and Disease Prevention",
|
| 201 |
+
"Forces, Magnets and Motion",
|
| 202 |
+
"Energy - Sources and Uses",
|
| 203 |
+
"Materials and Their Uses (including mixtures and separation)",
|
| 204 |
+
"Environment - Habitats, Conservation and Sustainable Use",
|
| 205 |
+
"Simple Scientific Investigation and Reporting"
|
| 206 |
+
]
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
# ---------- 4. Generate Practice Questions (Customizable) ----------
|
| 211 |
+
def generate_sample_questions(grade_level, subject, topic, num_questions=10):
|
| 212 |
+
"""Fallback: Generate sample questions locally when AI is unavailable.
|
| 213 |
+
This function is subject-aware and supplies simple sample questions for common topics."""
|
| 214 |
+
# Basic sample banks keyed by subject/topic
|
| 215 |
+
samples = {
|
| 216 |
+
"Mathematics": {
|
| 217 |
+
"Integers & Operations": [
|
| 218 |
+
"Q1. Calculate the sum of -20 and -15.",
|
| 219 |
+
"Q2. A farmer bought 40 oranges and then sold 25 of them. What is the difference between the number of oranges bought and sold?",
|
| 220 |
+
"Q3. Simplify: 36 - (-10) + 5.",
|
| 221 |
+
"Q4. Find the value of -2(8) + 15.",
|
| 222 |
+
"Q5. A car is parked at -10 meters. If it moves up 15 meters, what is its final position?",
|
| 223 |
+
"Q6. Calculate the product of -4 and 5.",
|
| 224 |
+
"Q7. A boat descends 12 meters, then rises 8 meters. What is its final position relative to sea level?",
|
| 225 |
+
"Q8. Simplify: 2(-3) + 5(-2).",
|
| 226 |
+
"Q9. A rabbit hops 7 meters forward and then 4 meters backward. What is the net distance covered?",
|
| 227 |
+
"Q10. A plane descends 300 meters and then ascends 200 meters. What is the plane's final position?",
|
| 228 |
+
],
|
| 229 |
+
"Fractions - Addition & Subtraction": [
|
| 230 |
+
"Q1. Add 1/4 and 1/3.",
|
| 231 |
+
"Q2. Subtract 2/5 from 3/5.",
|
| 232 |
+
"Q3. What is 1/2 + 1/4 + 1/8?",
|
| 233 |
+
"Q4. Calculate 7/8 - 1/4.",
|
| 234 |
+
"Q5. Find the sum of 2/3 and 1/6.",
|
| 235 |
+
"Q6. Subtract 3/10 from 9/10.",
|
| 236 |
+
"Q7. Add 1/5, 2/5, and 1/5.",
|
| 237 |
+
"Q8. What is 5/6 - 1/3?",
|
| 238 |
+
"Q9. Calculate 3/4 + 2/8.",
|
| 239 |
+
"Q10. Find 11/12 - 1/4.",
|
| 240 |
+
],
|
| 241 |
+
},
|
| 242 |
+
"English": {
|
| 243 |
+
"Comprehension - Passages & Questions": [
|
| 244 |
+
"Q1. Read the passage and answer: What is the main idea of paragraph 2?",
|
| 245 |
+
"Q2. From the passage, extract two reasons the author gives for saving water.",
|
| 246 |
+
"Q3. What does the word 'frugal' mean in the passage?",
|
| 247 |
+
"Q4. Give a title for the passage in not more than five words.",
|
| 248 |
+
"Q5. Why did the character decide to leave home?",
|
| 249 |
+
],
|
| 250 |
+
"Grammar - Sentence Transformation & Tenses": [
|
| 251 |
+
"Q1. Change to passive voice: 'The teacher marked the tests.'",
|
| 252 |
+
"Q2. Fill in the blank with the correct tense: 'She ___ (go) to school yesterday.'",
|
| 253 |
+
"Q3. Correct the sentence: 'He don't like vegetables.'",
|
| 254 |
+
"Q4. Combine the sentences: 'He ran fast. He missed the bus.'",
|
| 255 |
+
"Q5. Rewrite in reported speech: 'She said, "I will come."'",
|
| 256 |
+
]
|
| 257 |
+
},
|
| 258 |
+
"Science": {
|
| 259 |
+
"Living Things - Classification": [
|
| 260 |
+
"Q1. State two differences between plants and animals.",
|
| 261 |
+
"Q2. Name three groups of living organisms.",
|
| 262 |
+
"Q3. How do leaves help plants to survive?",
|
| 263 |
+
"Q4. What is photosynthesis? Give a simple definition.",
|
| 264 |
+
"Q5. Explain why animals need oxygen.",
|
| 265 |
+
],
|
| 266 |
+
"Forces, Magnets and Motion": [
|
| 267 |
+
"Q1. Define force with an example.",
|
| 268 |
+
"Q2. What does a magnet attract?",
|
| 269 |
+
"Q3. Give one example of a push and one example of a pull.",
|
| 270 |
+
]
|
| 271 |
+
},
|
| 272 |
+
"Social Studies": {
|
| 273 |
+
"History - Uganda & East Africa": [
|
| 274 |
+
"Q1. Name one important event in Uganda's history and explain why it is important.",
|
| 275 |
+
"Q2. Who was the first person to unite (example) ...?",
|
| 276 |
+
],
|
| 277 |
+
"Geography - Maps, Weather & Resources": [
|
| 278 |
+
"Q1. Give two uses of a map.",
|
| 279 |
+
"Q2. What are the main types of weather in Uganda?",
|
| 280 |
+
]
|
| 281 |
+
}
|
| 282 |
}
|
| 283 |
|
| 284 |
+
subject_bank = samples.get(subject, {})
|
| 285 |
+
# Try to match topic within subject bank
|
| 286 |
topic_key = None
|
| 287 |
+
for key in subject_bank:
|
| 288 |
if key.lower() in topic.lower() or topic.lower() in key.lower():
|
| 289 |
topic_key = key
|
| 290 |
break
|
| 291 |
|
| 292 |
if topic_key:
|
| 293 |
+
qs = subject_bank[topic_key][:num_questions]
|
| 294 |
else:
|
| 295 |
+
# Generic fallback per subject
|
| 296 |
+
qs = [f"Q{i+1}. Sample {subject} question {i+1} on {topic}." for i in range(num_questions)]
|
| 297 |
|
| 298 |
return "\n\n".join(qs)
|
| 299 |
|
| 300 |
+
def generate_practice_questions(grade_level, subject, topic, num_questions=10):
|
| 301 |
+
"""Generate multiple questions with robust text parsing (not JSON). Subject-aware prompt."""
|
|
|
|
| 302 |
|
| 303 |
if not topic:
|
| 304 |
return None, "⚠️ Please select a topic first!"
|
| 305 |
|
| 306 |
+
prompt = f"""Generate exactly {num_questions} UNEB-style {subject} questions for {grade_level} students on: \"{topic}\"\n\n"
|
| 307 |
+
prompt += "Format your response EXACTLY like this:\nQ1. [Question text here]\nQ2. [Question text here]\nQ3. [Question text here]\n... and so on up to Q{num_questions}\n\n"
|
| 308 |
+
prompt += f"Each question should be:\n- Clear and exam-like\n- Appropriate difficulty for {grade_level}\n- Self-contained (includes all necessary information)\n- Solvable in 2-5 minutes for short-answer questions\n\nStart immediately with Q1. Do not include any introduction or explanation."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
|
| 310 |
response, source = ask_ai(prompt, temperature=0.6)
|
| 311 |
|
| 312 |
+
# Fallback: if AI fails or returns empty, use local generator (now subject-aware)
|
| 313 |
+
if "All AI services failed" in response or not response or response.strip() == "":
|
| 314 |
+
response = generate_sample_questions(grade_level, subject, topic, num_questions)
|
| 315 |
|
| 316 |
# Parse questions robustly using regex to support multi-digit numbers (Q1..Q10..)
|
| 317 |
questions = []
|
|
|
|
| 350 |
return questions, formatted
|
| 351 |
|
| 352 |
# ---------- 5. Grade/Mark Student Answers ----------
|
| 353 |
+
def grade_student_answers(questions, student_answers, grade_level, topic, subject=None):
|
| 354 |
"""Grade all student answers and provide feedback"""
|
| 355 |
|
| 356 |
if not questions or not student_answers:
|
|
|
|
| 366 |
continue
|
| 367 |
|
| 368 |
# Create grading prompt
|
| 369 |
+
grading_prompt = f"""You are an experienced UNEB examiner for {grade_level} students. Subject: {subject or 'General'}
|
| 370 |
|
| 371 |
Question: {question_text}
|
| 372 |
|
|
|
|
| 406 |
session = StudentSession()
|
| 407 |
|
| 408 |
# ---------- 7. Download Functions ----------
|
| 409 |
+
def download_questions_file(questions_list, topic, grade_level, subject=None):
|
| 410 |
"""Download questions as text file"""
|
| 411 |
if not questions_list:
|
| 412 |
return None
|
| 413 |
|
| 414 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 415 |
+
subject_tag = f"_{subject}" if subject else ""
|
| 416 |
+
filename = f"questions_{grade_level}{subject_tag}_{topic}_{timestamp}.txt"
|
| 417 |
|
| 418 |
content = f"""UNEB EXAM PRACTICE QUESTIONS
|
| 419 |
{'='*50}
|
| 420 |
Grade Level: {grade_level}
|
| 421 |
+
Subject: {subject or 'N/A'}
|
| 422 |
Topic: {topic}
|
| 423 |
Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 424 |
Total Questions: {len(questions_list)}
|
|
|
|
| 448 |
return filepath
|
| 449 |
|
| 450 |
|
| 451 |
+
def download_questions_pdf(questions_list, topic, grade_level, subject=None):
|
| 452 |
"""Generate a simple PDF with the questions, return filepath."""
|
| 453 |
try:
|
| 454 |
from reportlab.lib.pagesizes import A4
|
|
|
|
| 459 |
return None
|
| 460 |
|
| 461 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 462 |
+
subject_tag = f"_{subject}" if subject else ""
|
| 463 |
+
filename = f"questions_{grade_level}{subject_tag}_{topic}_{timestamp}.pdf"
|
| 464 |
filepath = f"downloads/{filename}"
|
| 465 |
os.makedirs("downloads", exist_ok=True)
|
| 466 |
|
|
|
|
| 472 |
|
| 473 |
elems.append(Paragraph("UNEB EXAM PRACTICE QUESTIONS", styles['Title']))
|
| 474 |
elems.append(Spacer(1, 4*mm))
|
| 475 |
+
meta = f"Grade Level: {grade_level} Subject: {subject or 'N/A'} Topic: {topic} Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 476 |
elems.append(Paragraph(meta, normal))
|
| 477 |
elems.append(Spacer(1, 6*mm))
|
| 478 |
|
|
|
|
| 488 |
except Exception:
|
| 489 |
return None
|
| 490 |
|
| 491 |
+
def download_feedback_file(feedback_text, topic, grade_level, subject=None):
|
| 492 |
"""Download AI feedback/corrections"""
|
| 493 |
if not feedback_text:
|
| 494 |
return None
|
| 495 |
|
| 496 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 497 |
+
subject_tag = f"_{subject}" if subject else ""
|
| 498 |
+
filename = f"feedback_{grade_level}{subject_tag}_{topic}_{timestamp}.txt"
|
| 499 |
|
| 500 |
content = f"""AI CORRECTION & FEEDBACK
|
| 501 |
{'='*50}
|
| 502 |
Student: {session.student_name}
|
| 503 |
Grade Level: {grade_level}
|
| 504 |
+
Subject: {subject or 'N/A'}
|
| 505 |
Topic: {topic}
|
| 506 |
Date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 507 |
{'='*50}
|
|
|
|
| 561 |
|
| 562 |
gr.Markdown("""
|
| 563 |
# UNEB Exam Practice
|
| 564 |
+
## Primary 6 & 7 — Multiple Subjects
|
| 565 |
""")
|
| 566 |
|
| 567 |
# Global state for questions
|
|
|
|
| 584 |
with gr.Tabs():
|
| 585 |
# ===== TAB 1: GENERATE QUESTIONS =====
|
| 586 |
with gr.Tab("1️⃣ Generate Questions"):
|
| 587 |
+
gr.Markdown("### Step 1: Generate Practice Questions\n\nChoose the Grade, Subject and Topic, then set the Number of Questions (1–100).")
|
| 588 |
|
| 589 |
with gr.Row():
|
| 590 |
grade_input = gr.Dropdown(
|
|
|
|
| 592 |
label="Grade Level",
|
| 593 |
value="Primary 7"
|
| 594 |
)
|
| 595 |
+
subject_input = gr.Dropdown(
|
| 596 |
+
choices=["Mathematics", "English", "Social Studies", "Science"],
|
| 597 |
+
label="Subject",
|
| 598 |
+
value="Mathematics"
|
| 599 |
+
)
|
| 600 |
topic_input = gr.Dropdown(
|
| 601 |
label="Topic",
|
| 602 |
+
choices=syllabus_topics["Primary 7"]["Mathematics"],
|
| 603 |
+
value=syllabus_topics["Primary 7"]["Mathematics"][0]
|
| 604 |
)
|
| 605 |
|
| 606 |
+
# Update topics when grade or subject changes
|
| 607 |
+
def update_topics(grade, subject):
|
| 608 |
+
topics = syllabus_topics.get(grade, {}).get(subject, [])
|
| 609 |
+
if not topics:
|
| 610 |
+
topics = ["General - " + subject]
|
| 611 |
+
return gr.Dropdown(choices=topics, value=topics[0])
|
| 612 |
|
| 613 |
+
grade_input.change(update_topics, [grade_input, subject_input], topic_input)
|
| 614 |
+
subject_input.change(update_topics, [grade_input, subject_input], topic_input)
|
| 615 |
|
| 616 |
with gr.Row():
|
| 617 |
+
num_questions_input = gr.Slider(minimum=1, maximum=100, step=1, value=20, label="Number of Questions")
|
| 618 |
+
generate_btn = gr.Button(" Generate Questions", variant="primary", size="lg")
|
| 619 |
|
| 620 |
questions_output = gr.Markdown(
|
| 621 |
value="",
|
|
|
|
| 633 |
copy_btn = gr.Button(" Copy Questions")
|
| 634 |
|
| 635 |
# Generate questions handler
|
| 636 |
+
def generate_and_display(grade, subject, topic, num_questions):
|
| 637 |
+
questions_list, formatted_text = generate_practice_questions(grade, subject, topic, num_questions=num_questions)
|
| 638 |
|
| 639 |
if questions_list is None:
|
| 640 |
return "", formatted_text, questions_state.value, " Generation failed"
|
|
|
|
| 642 |
session.current_questions = questions_list
|
| 643 |
session.current_grade = grade
|
| 644 |
session.current_topic = topic
|
| 645 |
+
# Also store subject
|
| 646 |
+
session.current_subject = subject
|
| 647 |
|
| 648 |
# For Markdown display, keep spacing and simple formatting
|
| 649 |
md_text = "\n\n".join([f"**{i+1}.** {q.split('.',1)[1].strip() if '.' in q else q}" for i, q in enumerate(questions_list)])
|
| 650 |
+
return md_text, md_text, questions_list, f" Generated {len(questions_list)} {subject} questions on {topic}"
|
| 651 |
|
| 652 |
generate_btn.click(
|
| 653 |
fn=generate_and_display,
|
| 654 |
+
inputs=[grade_input, subject_input, topic_input, num_questions_input],
|
| 655 |
outputs=[questions_output, questions_output, questions_state, status_output]
|
| 656 |
+
)
|
|
|
|
| 657 |
# Download handler - returns file path for gr.DownloadButton
|
| 658 |
def download_qns():
|
| 659 |
if not session.current_questions:
|
| 660 |
return None
|
| 661 |
try:
|
| 662 |
# Prefer PDF export; fall back to plain text if PDF library missing
|
| 663 |
+
subject = getattr(session, 'current_subject', None)
|
| 664 |
+
pdf_path = download_questions_pdf(session.current_questions, session.current_topic, session.current_grade, subject=subject)
|
| 665 |
if pdf_path:
|
| 666 |
return pdf_path
|
| 667 |
# Fallback to text
|
| 668 |
+
filepath = download_questions_file(session.current_questions, session.current_topic, session.current_grade, subject=subject)
|
| 669 |
return filepath
|
| 670 |
except Exception as e:
|
| 671 |
return None
|
|
|
|
| 810 |
session.current_questions,
|
| 811 |
session.current_answers,
|
| 812 |
session.current_grade,
|
| 813 |
+
session.current_topic,
|
| 814 |
+
getattr(session, 'current_subject', None)
|
| 815 |
)
|
| 816 |
|
| 817 |
session.last_feedback = feedback
|
|
|
|
| 828 |
if not session.last_feedback:
|
| 829 |
return None
|
| 830 |
try:
|
| 831 |
+
subject = getattr(session, 'current_subject', None)
|
| 832 |
+
filepath = download_feedback_file(session.last_feedback, session.current_topic, session.current_grade, subject=subject)
|
| 833 |
return filepath
|
| 834 |
except Exception as e:
|
| 835 |
return None
|