import os import json from flask import Flask, session, request, redirect, url_for, render_template_string, flash, jsonify from openai import OpenAI from database import init_db, register_user, authenticate_user, get_db_connection app = Flask(__name__) # Secure session key app.secret_key = "examforge_nigerian_cbt_secure_secret" # Verify/Initialize the SQLite CBT database tables init_db() # BASE PREMIUM HTML/CSS LAYOUT BASE_LAYOUT = """ {{ title }} | ExamForge CBT
ExamForge CBT
{% if session.get('user_id') %} {{ session.get('role').upper() }} Log Out {% else %} Offline Prototype {% endif %}
{% block content %}{% endblock %}
""" # LOGIN VIEW TEMPLATE LOGIN_TEMPLATE = """ {% extends "base" %} {% block content %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, msg in messages %}
{{ msg }}
{% endfor %} {% endif %} {% endwith %}

{% if current_role == 'student' %}Student Login{% else %}Teacher Access{% endif %}

{% if current_role == 'student' %} Input custom ID format: STU-[CLASS]-[NAME]-[NUM] {% else %} Input custom ID format: TCH-[SUBJECT]-[NAME]-[NUM] {% endif %}

Need an offline CBT profile? Register Profile

{% endblock %} """ # REGISTRATION VIEW TEMPLATE REGISTER_TEMPLATE = """ {% extends "base" %} {% block content %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, msg in messages %}
{{ msg }}
{% endfor %} {% endif %} {% endwith %}
{{ current_role }} enrollment registry Back to login

CBT Profile Registry

{% if current_role == 'student' %}
{% else %}
{% endif %}

Nigeria CBT Coding Engine: The backend will instantly secure your educational records and automatically generate your official ID starting with {{ current_role.upper()[:3] }}-.

{% endblock %} """ # REGISTRATION SUCCESS VIEW TEMPLATE REGISTER_SUCCESS_TEMPLATE = """ {% extends "base" %} {% block content %}

Registered!

Account registered successfully inside secure local SQLite database.

Assigned Academic CBT ID {{ user_id }}

Write this ID down or copy it! It is required to log in along with your 4-digit PIN password.

Proceed with Login
{% endblock %} """ # TEACHER DASHBOARD VIEW TEMPLATE (RIGID QUESTION GENERATOR ENGINE) TEACHER_TEMPLATE = """ {% extends "base" %} {% block content %}
CBT TEACHER CONSOLE

Welcome Back, {{ user.full_name }}

Admin Custom ID: {{ user.id }}

Designated Specialty

{{ user.class_or_subject }}

CBT Database Status

Role Level: Subject Master
Topic Focus: {% if session.get('exam_topic') %}{{ session.get('exam_topic') }}{% else %}Not Generatd{% endif %}
Active Cache: {% if session.get('active_exam') %}Yes (3 Questions){% else %}Empty Dashboard{% endif %}

NVIDIA NIM AI CORE

This module utilizes NVIDIA cloud-accelerated Meta Llama 3.3 70B models inside security metrics. Exams generated adapt straight to West African Examination Syllabus metrics.

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, msg in messages %}
{{ msg }}
{% endfor %} {% endif %} {% endwith %}

Rigid AI Question Generator Engine

{% if session.get('active_exam') %}

Active Exam Generated for Students

{{ session.get('exam_type') }} CODE

STUDENT CODESPACE PREVIEW: Registered students logging in can now view and write this exam on their portals.

{% for q in session.get('active_exam') %}
{{ loop.index }}

{{ q.question }}

{% for letter, val in q.options.items() %}
{{ letter }}: {{ val }} {% if letter == q.correct %} ✓ Correct {% endif %}
{% endfor %}
{% endfor %}
{% endif %}
{% endblock %} """ # STUDENT DASHBOARD / EXAM INTERFACE VIEW TEMPLATE STUDENT_TEMPLATE = """ {% extends "base" %} {% block content %}
Nigeria Academic Center

Welcome, {{ user.full_name }}

CBT Student ID: {{ user.id }}

Assigned Classroom

{{ user.class_or_subject }}

Exam History

Status Check: Good Standing
Class: {{ user.class_or_subject }}

Principal Quote

"Only absolute focus and dedication yields academic success. Make your parents proud and study with perfect discipline!"

{% if not active_exam %}

No Active Exam Session Published

There are currently no active exams launched inside our SQLite CBT server database. Wait for your specialty subject teacher to deploy a batch of questions using their dashboard.

{% else %}
{{ session.get('exam_type') }} CORE FOCUS

Classroom CBT: {{ session.get('exam_topic') }}

Timer Untimed Session
{% for q in active_exam %}
{{ loop.index }}

{{ q.question }}

{% for letter, val in q.options.items() %} {% endfor %}
{% if not loop.last %}
{% endif %} {% endfor %}
{% endif %}
{% endblock %} """ # RESULTS / MOTIVATIONAL PRINCIPAL SPEECH VIEW TEMPLATE (LOCALIZED EVALUATION SPEECH ENGINE) RESULTS_TEMPLATE = """ {% extends "base" %} {% block content %}

Exam Grading Outcome

Graded evaluation score from local CBT SQLite cache

{{ score }} / 3 Syllabus Score

Principal's Office evaluation Evaluation Speech

"{{ speech }}"

Principal Mrs. Victoria Omotola

Llama 3.3 Engine

Question Explanations

{% for q in exam_questions %}
{{ loop.index }}

{{ q.question }}

Correct Option: {{ q.correct }} | Your Choice: {{ user_answers[q.id|string] if user_answers[q.id|string] else 'Unanswered' }}

{% endfor %}
{% endblock %} """ # HELPER RENDER WRAP FOR STRIP INTERFACE RENDERS def render_template(template_name, **context): layout = BASE_LAYOUT if template_name == "login": body = LOGIN_TEMPLATE elif template_name == "register": body = REGISTER_TEMPLATE elif template_name == "success": body = REGISTER_SUCCESS_TEMPLATE elif template_name == "teacher_dashboard": body = TEACHER_TEMPLATE elif template_name == "student_dashboard": body = STUDENT_TEMPLATE elif template_name == "results": body = RESULTS_TEMPLATE else: body = "" combined = layout.replace("{% block content %}{% endblock %}", body) return render_template_string(combined, **context) # ---- CRITICAL ROUTING PIPELINE 1: INSTITUTIONAL SYSTEM LOGINS (/) ---- @app.route("/", methods=["GET", "POST"]) def index(): # If already logged in, route immediately if "user_id" in session: if session.get("role") == "student": return redirect(url_for("student_dashboard")) else: return redirect(url_for("teacher_dashboard")) if request.method == "POST": user_id = request.form.get("user_id", "").strip().upper() pin = request.form.get("pin", "").strip() role = request.form.get("role", "student").strip().lower() # Enforce server-side rigid password PIN limits if len(pin) != 4 or not pin.isdigit(): flash("Strict Security Reject: Password PIN must be exactly 4 numeric digits.", "error") return redirect(url_for("login", role=role)) # Enforce structural integrity checks on ID starting values if not user_id.startswith("STU-") and not user_id.startswith("TCH-"): flash("Rejected Identification Format: Missing STU- or TCH- prefix.", "error") return redirect(url_for("login", role=role)) # Perform authentication inside database.py secure SQLite methods user = authenticate_user(user_id, pin) if user: # Enforce that login page role matches actual db role if user['role'].lower() != role: flash(f"System Conflict: ID belongs to a {user['role']} profile, not {role}.", "error") return redirect(url_for("login", role=role)) # Store inside session variables session["user_id"] = user["id"] session["full_name"] = user["full_name"] session["role"] = user["role"].lower() session["class_or_subject"] = user["class_or_subject"] if session["role"] == "student": return redirect(url_for("student_dashboard")) else: return redirect(url_for("teacher_dashboard")) else: flash("Invalid Custom ID or secure Password PIN. Verify and try again.", "error") return redirect(url_for("login", role=role)) role = request.args.get("role", "student") if role not in ["student", "teacher"]: role = "student" return render_template("login", title="Sign In", current_role=role, prefilled_id=request.args.get("id", "")) @app.route("/login") def login(): role = request.args.get("role", "student") if role not in ["student", "teacher"]: role = "student" return render_template("login", title="Sign In", current_role=role, prefilled_id=request.args.get("id", "")) # ---- REGISTRATION CHANNELS ---- @app.route("/register", methods=["GET", "POST"]) def register(): if "user_id" in session: return redirect(url_for("index")) if request.method == "POST": full_name = request.form.get("full_name", "").strip() role = request.form.get("role", "student").strip().lower() class_or_subject = request.form.get("class_or_subject", "").strip() pin = request.form.get("pin", "").strip() if len(pin) != 4 or not pin.isdigit(): flash("Error: Security Password PIN must be exactly 4 numeric digits.", "error") return redirect(url_for("register", role=role)) try: generated_id = register_user(full_name, role, class_or_subject, pin) return redirect(url_for("register_success", user_id=generated_id, role=role)) except ValueError as e: flash(str(e), "error") return redirect(url_for("register", role=role)) role = request.args.get("role", "student") if role not in ["student", "teacher"]: role = "student" return render_template("register", title="Create Profile", current_role=role) @app.route("/register/success") def register_success(): user_id = request.args.get("user_id") role = request.args.get("role", "student") if not user_id: return redirect(url_for("login")) return render_template("success", title="Registration Finished", user_id=user_id, role=role) # ---- CRITICAL ROUTING PIPELINE 2: RIGID AI QUESTION GENERATOR ENGINE (/teacher) ---- @app.route("/teacher") def teacher_dashboard(): if "user_id" not in session or session.get("role") != "teacher": return redirect(url_for("login", role="teacher")) user_info = { "id": session.get("user_id"), "full_name": session.get("full_name"), "role": session.get("role"), "class_or_subject": session.get("class_or_subject") } return render_template("teacher_dashboard", title="Teacher Panel", user=user_info) @app.route("/teacher/generate", methods=["POST"]) def generate_exam_questions(): if "user_id" not in session or session.get("role") != "teacher": return redirect(url_for("login", role="teacher")) topic = request.form.get("topic", "").strip() exam_type = request.form.get("exam_type", "WAEC").strip() if not topic: flash("Generation Error: You must supply a syllabus topic.", "error") return redirect(url_for("teacher_dashboard")) try: # Initialize OpenAI Client pointing strictly to cloud-hosted NVIDIA NIM API Endpoint (Nvidia Nim) # Key must remain hidden in environment client = OpenAI( base_url="https://nvidia.com", api_key=os.environ.get("NVIDIA_API_KEY", "PLACEHOLDER_KEY") ) prompt = f""" Choose exactly 3 complex multiple choice questions (MCQ) on the topic "{topic}" conforming strictly to the Nigerian {exam_type} secondary syllabus metrics. Return ONLY a valid, raw, unformatted JSON object. Do not wrap your output in markdown ```json blocks or include any extra text outside the JSON object itself. The JSON format must match this model EXACTLY: {{ "questions": [ {{ "id": 1, "question": "What is the primary product of light reaction during photosynthesis?", "options": {{ "A": "Oxygen", "B": "Glucose", "C": "Water", "D": "Carbon dioxide" }}, "correct": "A" }}, {{ "id": 2, "question": "Which of the following describes the dark reaction of photosynthesis?", "options": {{ "A": "It requires direct sunlight", "B": "It occurs inside the thylakoid membrane", "C": "It is light-independent and builds sucrose", "D": "It releases high-density hydrogen gas" }}, "correct": "C" }}, {{ "id": 3, "question": "Spirogyra cells thrive inside which ecosystem element?", "options": {{ "A": "Deep oceanic abyssal trenches", "B": "Flowing fresh ponds or streams", "C": "Arid Sahara desert dunes", "D": "Deciduous tropical moist tree barks" }}, "correct": "B" }} ] }} """ # Dispatch completion payload targeting our model Meta Llama-3.3-70b-instruct response = client.chat.completions.create( model="meta/llama-3.3-70b-instruct", messages=[ {"role": "system", "content": "You are an expert curriculum examiner generating strict Nigerian school board questions. Your outputs are always pure JSON blocks only, never including wrapping characters."}, {"role": "user", "content": prompt} ], temperature=0.2, max_tokens=1024 ) response_text = response.choices[0].message.content.strip() # Clean potential markdown wrappers if some leakage occurs if response_text.startswith("```"): lines = response_text.splitlines() if lines[0].startswith("```"): lines = lines[1:] if lines[-1].startswith("```"): lines = lines[:-1] response_text = "\n".join(lines).strip() # Parse generated string elements parsed_data = json.loads(response_text) questions_array = parsed_data.get("questions", []) if len(questions_array) != 3: raise ValueError("Syllabus Error: Core Model returned invalid question depth.") # Cache exactly within fully resilient Flask cache session to bypass SQLite wiping on serverless deployments session["active_exam"] = questions_array session["exam_topic"] = topic session["exam_type"] = exam_type flash(f"CBT Setup Confirmed! Generated 3 premium {exam_type} questions on '{topic}' successfully.", "success") except Exception as e: print("Llama NIM Generator Error Detail:", str(e)) flash(f"API Error: Llama CBT generator failed. Please double-check your secure NVIDIA_API_KEY. (Detail: {str(e)})", "error") return redirect(url_for("teacher_dashboard")) # ---- STUDENT PORTAL ---- @app.route("/student") def student_dashboard(): if "user_id" not in session or session.get("role") != "student": return redirect(url_for("login", role="student")) user_info = { "id": session.get("user_id"), "full_name": session.get("full_name"), "role": session.get("role"), "class_or_subject": session.get("class_or_subject") } # Fetch active exam questions from in-memory session active_exam = session.get("active_exam", None) return render_template("student_dashboard", title="Student Console", user=user_info, active_exam=active_exam) # ---- CRITICAL ROUTING PIPELINE 3: LOCALIZED PRINCIPAL EVALUATION SPEECH ENGINE (/results) ---- @app.route("/results", methods=["POST"]) def submit_exam_and_view_results(): if "user_id" not in session or session.get("role") != "student": return redirect(url_for("login", role="student")) active_exam = session.get("active_exam") if not active_exam: flash("Error: No active exam exists to validate.", "error") return redirect(url_for("student_dashboard")) # Tabulate the final score from frontend radio button submissions parameters score = 0 user_answers = {} for q in active_exam: q_id = str(q["id"]) submitted_val = request.form.get(f"ans_q_{q_id}", "").strip() user_answers[q_id] = submitted_val if submitted_val == q["correct"]: score += 1 topic = session.get("exam_topic", "General Studies") # Fire evaluation payload to the cloud AI completion server for the localized principal speech # Core system roleplay requirement:Mrs Victoria Victoria Omotola, Caring secondary principal. speech_output = "Weldone on completing your assessments. You must remain up and doing, let's keep working. No slacking!" try: client = OpenAI( base_url="https://nvidia.com", api_key=os.environ.get("NVIDIA_API_KEY", "PLACEHOLDER_KEY") ) principal_prompt = f""" Assess this student's outcome on an exam. - Scoring grade: {score} out of 3 questions correct - Topic of evaluation: "{topic}" Write a motivational assessment speech of EXACTLY 3 sentences. You MUST roleplay as Mrs. Victoria Omotola, a strict but deeply caring secondary school Principal in Lagos, Nigeria. You MUST explicitly inject authentic colloquial Nigerian administrative school idioms and words (such as "up and doing", "no slacking", "weldone", "carry first"). Do not write any introductory greetings or extra sentences. Stick strictly to exactly 3 sentences. """ response = client.chat.completions.create( model="meta/llama-3.3-70b-instruct", messages=[ {"role": "system", "content": "You are a Nigerian secondary school principal Mrs. Victoria Omotola with rich native style. Your output is always exactly 3 sentences of deeply encouraging and structured evaluation."}, {"role": "user", "content": principal_prompt} ], temperature=0.7, max_tokens=300 ) speech_output = response.choices[0].message.content.strip() except Exception as e: print("Mrs Omotola Evaluator Error Detail:", str(e)) # Keep user answer details in temporary session to show review list session["user_answers"] = user_answers session["last_score"] = score session["principal_speech"] = speech_output return render_template( "results", title="Graded Exam", score=score, speech=speech_output, exam_questions=active_exam, user_answers=user_answers ) # ---- CRITICAL ROUTING PIPELINE 4: LIVE RE-VALUATION & ANSWER EXPLAINER API (/explain/) ---- @app.route("/explain/") def explain_incorrect_answers(question_id): if "user_id" not in session: return jsonify({"error": "Unauthorized"}), 401 active_exam = session.get("active_exam") user_answers = session.get("user_answers", {}) if not active_exam: return jsonify({"explanation": "Syllabus active exam expired."}), 404 # Locate the target question index target_q = None for q in active_exam: if q["id"] == question_id: target_q = q break if not target_q: return jsonify({"explanation": "Question target not found inside workspace cache."}), 404 correct_option = target_q["correct"] correct_desc = target_q["options"].get(correct_option, "") user_selection = user_answers.get(str(question_id), "None") user_desc = target_q["options"].get(user_selection, "No Option Chosen") explanation_text = "NVIDIA NIM Llama explanation loading..." try: client = OpenAI( base_url="https://nvidia.com", api_key=os.environ.get("NVIDIA_API_KEY", "PLACEHOLDER_KEY") ) explain_prompt = f""" Analyze this multiple choice question on the school topic "{session.get('exam_topic')}": Question: "{target_q['question']}" Correct answer was option "{correct_option}" which is: "{correct_desc}" Student selected option "{user_selection}" which is: "{user_desc}" Deliver a concise, conversational explanation of exactly 2-3 sentences max. Plainly explain: 1. Why the correct option "{correct_option}" is scientifically or mathematically correct. 2. In simple, friendly Nigerian student-facing terms, why the chosen option "{user_selection}" is incorrect. Keep the tone helpful and encouraging. Do not output anything other than the explanation text. """ response = client.chat.completions.create( model="meta/llama-3.3-70b-instruct", messages=[ {"role": "system", "content": "You are a professional educational curriculum tutor. You explain concepts simply, concisely and directly in 3 sentences max."}, {"role": "user", "content": explain_prompt} ], temperature=0.3, max_tokens=400 ) explanation_text = response.choices[0].message.content.strip() except Exception as e: explanation_text = f"Explanation Generation Failed. Ensure your NVIDIA API metrics key is set correctly in PowerShell environment parameters. (Error: {str(e)})" return jsonify({"explanation": explanation_text}) # ---- LOGOUT PORTAL CHANNELS ---- @app.route("/logout") def logout_user(): session.clear() flash("Session terminated securely.", "success") return redirect(url_for("login")) if __name__ == "__main__": print("----------------------------------------------------------------") print(" EXAMFORGE CBT LOCAL LIVE FLASK COMPLIANT SERVER ") print("----------------------------------------------------------------") app.run(host="0.0.0.0", port=3000, debug=True)