Spaces:
Runtime error
Runtime error
| # app.py | |
| import os | |
| import json | |
| import random | |
| import sqlite3 | |
| import gradio as gr | |
| from agents import AnalyzerAgent, CoachAgent, PredictiveAgent | |
| # Config | |
| QUESTIONS_JSON = "questions.json" | |
| DB_FILE = "exam.db" | |
| MEDIA_DIR = "media" | |
| MAX_SLOTS = 10 # number of question rows to display (adjust as needed) | |
| os.makedirs(MEDIA_DIR, exist_ok=True) | |
| # Agents | |
| analyzer = AnalyzerAgent() | |
| coach = CoachAgent() | |
| predictor = PredictiveAgent() | |
| # ------------------------- | |
| # DB / JSON helpers | |
| # ------------------------- | |
| def load_questions_json(): | |
| if not os.path.exists(QUESTIONS_JSON) or os.path.getsize(QUESTIONS_JSON) == 0: | |
| return [] | |
| try: | |
| with open(QUESTIONS_JSON, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except Exception: | |
| return [] | |
| def init_db_and_sync(): | |
| conn = sqlite3.connect(DB_FILE) | |
| cur = conn.cursor() | |
| cur.execute(""" | |
| CREATE TABLE IF NOT EXISTS questions ( | |
| id INTEGER PRIMARY KEY, | |
| text TEXT, | |
| choices TEXT, | |
| answer TEXT, | |
| subject TEXT, | |
| paper INTEGER, | |
| year INTEGER, | |
| image TEXT, | |
| source TEXT | |
| ) | |
| """) | |
| conn.commit() | |
| conn.close() | |
| sync_json_to_db() | |
| def sync_json_to_db(): | |
| questions = load_questions_json() | |
| if not questions: | |
| return | |
| conn = sqlite3.connect(DB_FILE) | |
| cur = conn.cursor() | |
| for q in questions: | |
| try: | |
| qid = int(q.get("id") or 0) | |
| text = q.get("text", "") | |
| choices = json.dumps(q.get("choices", []), ensure_ascii=False) | |
| answer = q.get("answer", "") | |
| subject = q.get("subject", "") | |
| paper = int(q.get("paper", 2) or 2) | |
| year = int(q.get("year", 0) or 0) | |
| image = q.get("image") | |
| source = q.get("source", "") | |
| if qid: | |
| cur.execute("INSERT OR IGNORE INTO questions (id,text,choices,answer,subject,paper,year,image,source) VALUES (?,?,?,?,?,?,?,?,?)", | |
| (qid, text, choices, answer, subject, paper, year, image, source)) | |
| else: | |
| cur.execute("INSERT INTO questions (text,choices,answer,subject,paper,year,image,source) VALUES (?,?,?,?,?,?,?,?)", | |
| (text, choices, answer, subject, paper, year, image, source)) | |
| except Exception: | |
| continue | |
| conn.commit() | |
| conn.close() | |
| def get_pool(subject, paper=2): | |
| conn = sqlite3.connect(DB_FILE) | |
| cur = conn.cursor() | |
| cur.execute("SELECT id, text, choices, answer, subject, paper, year, image, source FROM questions WHERE subject=? AND paper=?", (subject, paper)) | |
| rows = cur.fetchall() | |
| conn.close() | |
| pool = [] | |
| for r in rows: | |
| try: | |
| choices = json.loads(r[2]) if r[2] else [] | |
| except Exception: | |
| choices = [] | |
| pool.append({ | |
| "id": r[0], | |
| "text": r[1], | |
| "choices": choices, | |
| "answer": r[3], | |
| "subject": r[4], | |
| "paper": r[5], | |
| "year": r[6], | |
| "image": r[7], | |
| "source": r[8] | |
| }) | |
| return pool | |
| # initialize DB & sync once at startup | |
| init_db_and_sync() | |
| # ------------------------- | |
| # Exam functions | |
| # ------------------------- | |
| def start_exam(subject, num_questions): | |
| """ | |
| Returns values mapping to outputs: | |
| - first MAX_SLOTS strings: markdown question text (include choice lines A/B/C/D) | |
| - next MAX_SLOTS strings: image file paths or empty "" | |
| - next MAX_SLOTS values: dropdown default value (None) | |
| - final output: selected questions (state) | |
| """ | |
| # ensure any new JSON items are synced | |
| sync_json_to_db() | |
| pool = get_pool(subject, paper=2) | |
| if not pool: | |
| # return empty placeholders | |
| markdowns = [f"Q{i+1}: (no question available)" for i in range(MAX_SLOTS)] | |
| images = ["" for _ in range(MAX_SLOTS)] | |
| defaults = [None for _ in range(MAX_SLOTS)] | |
| return markdowns + images + defaults + [[]] | |
| selected = random.sample(pool, min(num_questions, len(pool))) | |
| markdowns = [] | |
| images = [] | |
| defaults = [] | |
| for i in range(MAX_SLOTS): | |
| if i < len(selected): | |
| q = selected[i] | |
| # Build question text + choice lines | |
| text_block = q["text"].strip() | |
| # If choices exist, append them as labeled lines (A., B., ...) | |
| if q.get("choices"): | |
| letters = ["A", "B", "C", "D"] | |
| choice_lines = [] | |
| for j, opt in enumerate(q["choices"]): | |
| if j < 4: | |
| # Avoid double-prepending if already includes letter | |
| opt_str = opt.strip() | |
| if opt_str.upper().startswith(tuple([l + "." for l in letters])): | |
| choice_lines.append(opt_str) | |
| else: | |
| choice_lines.append(f"{letters[j]}. {opt_str}") | |
| text_block += "\n\n" + "\n".join(choice_lines) | |
| else: | |
| text_block += "\n\n(A/B/C/D)" | |
| markdowns.append(text_block) | |
| images.append(q.get("image") or "") | |
| defaults.append(None) | |
| else: | |
| markdowns.append("") # blank row | |
| images.append("") | |
| defaults.append(None) | |
| # Return markdowns (MAX) + images (MAX) + defaults (MAX) + [selected] | |
| return markdowns + images + defaults + [selected] | |
| def submit_exam(*args): | |
| """ | |
| args: first MAX_SLOTS dropdown answers (strings or None), last arg = selected questions list | |
| returns: score_text, wrong_details_str, ai_advice_str | |
| """ | |
| if len(args) < MAX_SLOTS + 1: | |
| return "Submission error (bad input format).", "", "" | |
| answers = list(args[:MAX_SLOTS]) | |
| selected = args[MAX_SLOTS] or [] | |
| total = len(selected) | |
| correct = 0 | |
| wrong_list = [] | |
| wrong_ids = [] | |
| for i, q in enumerate(selected): | |
| ans = (answers[i] or "").strip() | |
| expected = str(q.get("answer") or "").strip() | |
| is_correct = False | |
| if expected == "": | |
| is_correct = False | |
| else: | |
| # If expected is single letter (A/B/C/D) | |
| if len(expected) == 1 and expected.upper() in ["A","B","C","D"]: | |
| if ans.upper() == expected.upper(): | |
| is_correct = True | |
| else: | |
| # compare free-text | |
| if ans.lower() == expected.lower(): | |
| is_correct = True | |
| if is_correct: | |
| correct += 1 | |
| else: | |
| # Prepare readable correct label | |
| corr_display = expected | |
| if expected and len(expected) == 1 and q.get("choices"): | |
| idx = ord(expected.upper()) - ord("A") | |
| if 0 <= idx < len(q["choices"]): | |
| corr_display = f"{expected.upper()}. {q['choices'][idx]}" | |
| wrong_list.append(f"Q{i+1} (id:{q['id']}): Your = {ans or '(no answer)'} | Correct = {corr_display}") | |
| wrong_ids.append(q['id']) | |
| score_text = f"Score: {correct} / {total} (Correct {correct}, Wrong {total - correct})" | |
| # Ask coach agent for advice | |
| try: | |
| context = {"subject": selected[0]["subject"] if total>0 else None, | |
| "total": total, "correct": correct, "wrong": total-correct, "wrong_ids": wrong_ids} | |
| advice = coach.coach(json.dumps(context, ensure_ascii=False)) | |
| except Exception as e: | |
| advice = f"(AI coach not available) {e}" | |
| wrong_str = "\n".join(wrong_list) if wrong_list else "All correct!" | |
| return score_text, wrong_str, advice | |
| # ------------------------- | |
| # Build UI (side-by-side layout) | |
| # ------------------------- | |
| # Prepare subjects list from DB | |
| conn = sqlite3.connect(DB_FILE) | |
| cur = conn.cursor() | |
| cur.execute("SELECT DISTINCT subject FROM questions") | |
| rows = cur.fetchall() | |
| conn.close() | |
| SUBJECTS = [r[0] for r in rows] or ["BM", "English", "Math", "History", "Science", "MoralStudies"] | |
| with gr.Blocks(title="SPM Paper 2 Simulator — Side-by-Side UI") as demo: | |
| gr.Markdown("# 🧾 SPM Paper 2 — Simulator (Form 5)") | |
| gr.Markdown("Each question is displayed with its answer selector to the right (select A/B/C/D). Diagrams (if any) show below the question text.") | |
| with gr.Row(): | |
| subject_dd = gr.Dropdown(SUBJECTS, label="Subject", value=SUBJECTS[0]) | |
| num_q = gr.Slider(1, MAX_SLOTS, value=10, step=1, label="Number of Questions") | |
| start_btn = gr.Button("Start Simulation") | |
| # Create MAX_SLOTS rows, each row has Markdown (left), Image (below left), Dropdown (right) | |
| q_markdowns = [] | |
| q_images = [] | |
| q_dropdowns = [] | |
| for i in range(MAX_SLOTS): | |
| with gr.Row(): | |
| md = gr.Markdown(f"Q{i+1}: (not loaded)") | |
| dd = gr.Dropdown(choices=["A","B","C","D"], label=f"Select answer Q{i+1}", value=None) | |
| img = gr.Image(type="filepath", visible=False) | |
| q_markdowns.append(md) | |
| q_images.append(img) | |
| q_dropdowns.append(dd) | |
| # state to hold selected question dicts | |
| selected_state = gr.State([]) | |
| # Outputs mapping: [*q_markdowns, *q_images, *q_dropdowns, selected_state] | |
| start_btn.click( | |
| fn=start_exam, | |
| inputs=[subject_dd, num_q], | |
| outputs=[*q_markdowns, *q_images, *q_dropdowns, selected_state] | |
| ) | |
| submit_btn = gr.Button("Submit Answers") | |
| score_out = gr.Textbox(label="Score Summary") | |
| wrong_out = gr.Textbox(label="Wrong Answers", lines=8) | |
| advice_out = gr.Textbox(label="AI Advice", lines=6) | |
| submit_btn.click( | |
| fn=submit_exam, | |
| inputs=[*q_dropdowns, selected_state], | |
| outputs=[score_out, wrong_out, advice_out] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860) | |