add dropdown menu to the frontend of the quiz

parent 66689c39
...@@ -338,25 +338,27 @@ def create_app() -> FastAPI: ...@@ -338,25 +338,27 @@ def create_app() -> FastAPI:
@app.post("/mcq/generate") @app.post("/mcq/generate")
async def generate_mcqs_handler( async def generate_mcqs_handler(
request: Request, request: Request,
grade: int = Form(...), curriculum: str = Form(...),
grade: str = Form(...), # Changed to str
subject: str = Form(...), subject: str = Form(...),
unit: str = Form(...), unit: str = Form(...),
concept: str = Form(...), concept: str = Form(...),
count: int = Form(5), count: int = Form(5),
is_arabic: bool = Form(False) is_arabic: bool = Form(False),
): ):
""" """
Generates and stores a new set of MCQs for a specific topic. Generates and stores a new set of MCQs for a specific topic, using the new schema.
""" """
container = request.app.state.container container = request.app.state.container
try: try:
generated_questions = container.agent_service.generate_and_store_mcqs( generated_questions = container.agent_service.generate_and_store_mcqs(
curriculum=curriculum,
grade=grade, grade=grade,
subject=subject, subject=subject,
unit=unit, unit=unit,
concept=concept, concept=concept,
num_questions=count, num_questions=count,
is_arabic=is_arabic is_arabic=is_arabic,
) )
return { return {
"status": "success", "status": "success",
...@@ -364,7 +366,7 @@ def create_app() -> FastAPI: ...@@ -364,7 +366,7 @@ def create_app() -> FastAPI:
"questions": generated_questions "questions": generated_questions
} }
except HTTPException as e: except HTTPException as e:
raise e # Re-raise FastAPI specific exceptions raise e
except Exception as e: except Exception as e:
logger.error(f"Error in generate_mcqs_handler: {e}") logger.error(f"Error in generate_mcqs_handler: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
...@@ -372,27 +374,27 @@ def create_app() -> FastAPI: ...@@ -372,27 +374,27 @@ def create_app() -> FastAPI:
@app.get("/mcq") @app.get("/mcq")
async def get_mcqs_handler( async def get_mcqs_handler(
request: Request, request: Request,
grade: int, curriculum: str,
grade: str,
subject: str, subject: str,
unit: str, unit: str,
concept: str, concept: str,
is_arabic: bool, is_arabic: bool,
# Make limit optional. If not provided, it will be None.
limit: Optional[int] = None limit: Optional[int] = None
): ):
""" """
Retrieves existing MCQs for a specific topic and language from the database. Retrieves existing MCQs for a specific topic, now filtering by curriculum.
If no limit is provided, retrieves all questions.
""" """
container = request.app.state.container container = request.app.state.container
try: try:
questions = container.agent_service.pgvector.get_mcqs( questions = container.agent_service.pgvector.get_mcqs(
curriculum=curriculum,
grade=grade, grade=grade,
subject=subject, subject=subject,
unit=unit, unit=unit,
concept=concept, concept=concept,
is_arabic=is_arabic, is_arabic=is_arabic,
limit=limit # Pass the limit (which could be None) limit=limit
) )
return { return {
"status": "success", "status": "success",
...@@ -406,7 +408,8 @@ def create_app() -> FastAPI: ...@@ -406,7 +408,8 @@ def create_app() -> FastAPI:
@app.post("/quiz/dynamic") @app.post("/quiz/dynamic")
async def get_dynamic_quiz_handler( async def get_dynamic_quiz_handler(
request: Request, request: Request,
grade: int = Form(...), curriculum: str = Form(...),
grade: str = Form(...),
subject: str = Form(...), subject: str = Form(...),
unit: str = Form(...), unit: str = Form(...),
concept: str = Form(...), concept: str = Form(...),
...@@ -414,15 +417,12 @@ def create_app() -> FastAPI: ...@@ -414,15 +417,12 @@ def create_app() -> FastAPI:
count: int = Form(5) count: int = Form(5)
): ):
""" """
Generates a dynamic quiz for a topic. Generates a dynamic quiz, now using curriculum as a key identifier.
This endpoint ensures freshness by generating a few new questions
and then randomly selects the total requested 'count' from the
entire pool of available questions (new and old).
""" """
container = request.app.state.container container = request.app.state.container
try: try:
quiz_questions = container.agent_service.get_dynamic_quiz( quiz_questions = container.agent_service.get_dynamic_quiz(
curriculum=curriculum,
grade=grade, grade=grade,
subject=subject, subject=subject,
unit=unit, unit=unit,
...@@ -436,7 +436,7 @@ def create_app() -> FastAPI: ...@@ -436,7 +436,7 @@ def create_app() -> FastAPI:
"quiz": quiz_questions "quiz": quiz_questions
} }
except HTTPException as e: except HTTPException as e:
raise e # Re-raise FastAPI specific exceptions raise e
except Exception as e: except Exception as e:
logger.error(f"Error in get_dynamic_quiz_handler: {e}") logger.error(f"Error in get_dynamic_quiz_handler: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
...@@ -460,6 +460,37 @@ def create_app() -> FastAPI: ...@@ -460,6 +460,37 @@ def create_app() -> FastAPI:
print(f"Error serving quiz interface: {e}") print(f"Error serving quiz interface: {e}")
raise HTTPException(status_code=500, detail=f"Error serving interface: {str(e)}") raise HTTPException(status_code=500, detail=f"Error serving interface: {str(e)}")
@app.get("/quiz/options/curricula")
async def get_curricula_options(request: Request):
container = request.app.state.container
options = container.agent_service.pgvector.get_distinct_curricula_from_structure()
return {"options": options}
@app.get("/quiz/options/grades")
async def get_grades_options(request: Request, curriculum: str):
container = request.app.state.container
options = container.agent_service.pgvector.get_distinct_grades_from_structure(curriculum)
return {"options": options}
@app.get("/quiz/options/subjects")
async def get_subjects_options(request: Request, curriculum: str, grade: str):
container = request.app.state.container
options = container.agent_service.pgvector.get_distinct_subjects_from_structure(curriculum, grade)
return {"options": options}
@app.get("/quiz/options/units")
async def get_units_options(request: Request, curriculum: str, grade: str, subject: str):
container = request.app.state.container
options = container.agent_service.pgvector.get_distinct_units_from_structure(curriculum, grade, subject)
return {"options": options}
@app.get("/quiz/options/concepts")
async def get_concepts_options(request: Request, curriculum: str, grade: str, subject: str, unit: str):
container = request.app.state.container
options = container.agent_service.pgvector.get_distinct_concepts_from_structure(curriculum, grade, subject, unit)
return {"options": options}
@app.options("/get-audio-response") @app.options("/get-audio-response")
async def audio_response_options(): async def audio_response_options():
"""Handle preflight CORS requests for audio response endpoint""" """Handle preflight CORS requests for audio response endpoint"""
......
...@@ -527,26 +527,37 @@ class PGVectorService: ...@@ -527,26 +527,37 @@ class PGVectorService:
def insert_mcqs(self, mcq_list: List[Dict]): def insert_mcqs(self, mcq_list: List[Dict]):
""" """
Inserts a batch of MCQs, now including the language flag. Inserts a batch of MCQs, now including ALL new fields from the updated schema.
""" """
if not mcq_list: if not mcq_list:
return return
with self.pool_handler.get_connection() as conn: with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
# --- UPDATED QUERY --- # --- UPDATED INSERT QUERY WITH ALL NEW COLUMNS ---
insert_query = """ insert_query = """
INSERT INTO mcq_questions ( INSERT INTO mcq_questions (
grade, is_arabic, subject, unit, concept, question_text, curriculum, grade, subject, unit, concept, question_text,
correct_answer, wrong_answer_1, wrong_answer_2, wrong_answer_3 question_type, difficulty_level, is_arabic, correct_answer,
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s); wrong_answer_1, wrong_answer_2, wrong_answer_3, wrong_answer_4,
question_image_url, correct_image_url, wrong_image_url_1,
wrong_image_url_2, wrong_image_url_3, wrong_image_url_4, hint
) VALUES (
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
%s, %s, %s, %s, %s, %s, %s
);
""" """
# --- UPDATED DATA PREPARATION --- # --- UPDATED DATA PREPARATION TO MATCH THE NEW SCHEMA ---
# Using .get() provides safety against missing keys from the LLM response
data_to_insert = [ data_to_insert = [
( (
q['grade'], q['is_arabic'], q['subject'], q['unit'], q['concept'], q.get('curriculum'), q.get('grade'), q.get('subject'), q.get('unit'), q.get('concept'),
q['question_text'], q['correct_answer'], q['wrong_answer_1'], q.get('question_text'), q.get('question_type'), q.get('difficulty_level'),
q['wrong_answer_2'], q['wrong_answer_3'] q.get('is_arabic'), q.get('correct_answer'), q.get('wrong_answer_1'),
q.get('wrong_answer_2'), q.get('wrong_answer_3'), q.get('wrong_answer_4'),
q.get('question_image_url'), q.get('correct_image_url'), q.get('wrong_image_url_1'),
q.get('wrong_image_url_2'), q.get('wrong_image_url_3'), q.get('wrong_image_url_4'),
q.get('hint')
) for q in mcq_list ) for q in mcq_list
] ]
...@@ -554,23 +565,23 @@ class PGVectorService: ...@@ -554,23 +565,23 @@ class PGVectorService:
conn.commit() conn.commit()
logger.info(f"Successfully inserted {len(mcq_list)} MCQs into the database.") logger.info(f"Successfully inserted {len(mcq_list)} MCQs into the database.")
def get_mcqs(self, grade: int, subject: str, unit: str, concept: str, is_arabic: bool, limit: Optional[int] = 10) -> List[Dict]: def get_mcqs(self, curriculum: str, grade: str, subject: str, unit: str, concept: str, is_arabic: bool, limit: Optional[int] = 10) -> List[Dict]:
""" """
Retrieves MCQs for a specific topic and language. Retrieves MCQs for a specific topic and language, now filtering by curriculum.
If limit is None, it retrieves all matching questions. If limit is None, it retrieves all matching questions.
""" """
with self.pool_handler.get_connection() as conn: with self.pool_handler.get_connection() as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cur: with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Dynamically build the query based on the limit # --- UPDATED SELECT AND WHERE CLAUSE ---
query = """ query = """
SELECT id, question_text, correct_answer, wrong_answer_1, wrong_answer_2, wrong_answer_3 SELECT *
FROM mcq_questions FROM mcq_questions
WHERE grade = %s AND subject = %s AND unit = %s AND concept = %s AND is_arabic = %s WHERE curriculum = %s AND grade = %s AND subject = %s AND unit = %s AND concept = %s AND is_arabic = %s
ORDER BY created_at DESC ORDER BY created_at DESC
""" """
params = (grade, subject, unit, concept, is_arabic) params = (curriculum, grade, subject, unit, concept, is_arabic)
if limit is not None: if limit is not None:
query += " LIMIT %s;" query += " LIMIT %s;"
...@@ -580,3 +591,63 @@ class PGVectorService: ...@@ -580,3 +591,63 @@ class PGVectorService:
cur.execute(query, params) cur.execute(query, params)
return cur.fetchall() return cur.fetchall()
def get_distinct_curricula_from_structure(self) -> List[str]:
"""Gets distinct curriculum names from the curriculum_structure table."""
with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur:
cur.execute("SELECT DISTINCT curriculum_data->>'title' FROM curriculum_structure ORDER BY 1;")
return [row[0] for row in cur.fetchall() if row[0]]
def get_distinct_grades_from_structure(self, curriculum: str) -> List[str]:
"""Gets distinct grades for a given curriculum from the curriculum_structure table."""
with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur:
# We assume grade is stored as an integer, but return as string for consistency
cur.execute("""
SELECT DISTINCT grade::text FROM curriculum_structure
WHERE curriculum_data->>'title' = %s ORDER BY 1;
""", (curriculum,))
return [row[0] for row in cur.fetchall() if row[0]]
def get_distinct_subjects_from_structure(self, curriculum: str, grade: str) -> List[str]:
"""Gets distinct subjects for a given curriculum and grade."""
with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur:
cur.execute("""
SELECT DISTINCT subject FROM curriculum_structure
WHERE curriculum_data->>'title' = %s AND grade = %s ORDER BY 1;
""", (curriculum, int(grade))) # Grade is an integer in this table
return [row[0] for row in cur.fetchall() if row[0]]
def get_distinct_units_from_structure(self, curriculum: str, grade: str, subject: str) -> List[str]:
"""Gets distinct unit names from the JSONB data in curriculum_structure."""
with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur:
# This query uses jsonb_array_elements to expand the 'units' array
cur.execute("""
SELECT DISTINCT unit->>'name'
FROM curriculum_structure, jsonb_array_elements(curriculum_data->'units') AS unit
WHERE curriculum_data->>'title' = %s AND grade = %s AND subject = %s
ORDER BY 1;
""", (curriculum, int(grade), subject))
return [row[0] for row in cur.fetchall() if row[0]]
def get_distinct_concepts_from_structure(self, curriculum: str, grade: str, subject: str, unit: str) -> List[str]:
"""Gets distinct concept names for a given unit from the JSONB data."""
with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur:
# This is a more complex query that expands both units and concepts
cur.execute("""
SELECT DISTINCT concept->>'name'
FROM curriculum_structure,
jsonb_array_elements(curriculum_data->'units') AS u,
jsonb_array_elements(u->'concepts') AS concept
WHERE curriculum_data->>'title' = %s
AND grade = %s
AND subject = %s
AND u->>'name' = %s
ORDER BY 1;
""", (curriculum, int(grade), subject, unit))
return [row[0] for row in cur.fetchall() if row[0]]
\ No newline at end of file
...@@ -6,7 +6,7 @@ load_dotenv() ...@@ -6,7 +6,7 @@ load_dotenv()
def setup_mcq_table(drop_existing_table: bool = False): def setup_mcq_table(drop_existing_table: bool = False):
""" """
Sets up the mcq_questions table in the database. Sets up the mcq_questions table with the final, comprehensive schema.
""" """
try: try:
conn = psycopg2.connect( conn = psycopg2.connect(
...@@ -24,30 +24,41 @@ def setup_mcq_table(drop_existing_table: bool = False): ...@@ -24,30 +24,41 @@ def setup_mcq_table(drop_existing_table: bool = False):
cur.execute("DROP TABLE IF EXISTS mcq_questions CASCADE;") cur.execute("DROP TABLE IF EXISTS mcq_questions CASCADE;")
print("Table dropped.") print("Table dropped.")
print("Creating mcq_questions table...") print("Creating mcq_questions table with the NEW COMPREHENSIVE schema...")
# --- THIS IS THE UPDATED TABLE SCHEMA --- # --- THIS IS THE FULLY UPDATED TABLE SCHEMA ---
cur.execute(""" cur.execute("""
CREATE TABLE IF NOT EXISTS mcq_questions ( CREATE TABLE IF NOT EXISTS mcq_questions (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
grade INTEGER NOT NULL, curriculum TEXT,
is_arabic BOOLEAN NOT NULL, -- <-- ADDED THIS LINE grade TEXT NOT NULL,
subject TEXT NOT NULL, subject TEXT NOT NULL,
unit TEXT NOT NULL, unit TEXT NOT NULL,
concept TEXT NOT NULL, concept TEXT NOT NULL,
question_text TEXT NOT NULL, question_text TEXT NOT NULL,
question_type TEXT,
difficulty_level INTEGER,
is_arabic BOOLEAN NOT NULL,
correct_answer TEXT NOT NULL, correct_answer TEXT NOT NULL,
wrong_answer_1 TEXT NOT NULL, wrong_answer_1 TEXT,
wrong_answer_2 TEXT NOT NULL, wrong_answer_2 TEXT,
wrong_answer_3 TEXT NOT NULL, wrong_answer_3 TEXT,
wrong_answer_4 TEXT,
question_image_url TEXT, -- Placeholder for MinIO URL
correct_image_url TEXT, -- Placeholder for MinIO URL
wrong_image_url_1 TEXT, -- Placeholder for MinIO URL
wrong_image_url_2 TEXT, -- Placeholder for MinIO URL
wrong_image_url_3 TEXT, -- Placeholder for MinIO URL
wrong_image_url_4 TEXT, -- Placeholder for MinIO URL
hint TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
""") """)
print("Creating indexes on mcq_questions table...") print("Creating indexes on mcq_questions table...")
# --- THIS IS THE UPDATED INDEX --- # --- UPDATED INDEX TO INCLUDE CURRICULUM ---
cur.execute(""" cur.execute("""
CREATE INDEX IF NOT EXISTS idx_mcq_topic CREATE INDEX IF NOT EXISTS idx_mcq_topic
ON mcq_questions(grade, is_arabic, subject, unit, concept); -- <-- ADDED is_arabic ON mcq_questions(curriculum, grade, is_arabic, subject, unit, concept);
""") """)
print("MCQ table setup complete.") print("MCQ table setup complete.")
...@@ -60,7 +71,7 @@ def setup_mcq_table(drop_existing_table: bool = False): ...@@ -60,7 +71,7 @@ def setup_mcq_table(drop_existing_table: bool = False):
print("Database connection closed.") print("Database connection closed.")
if __name__ == "__main__": if __name__ == "__main__":
# To apply the changes, it's best to drop and recreate the table. # To apply the new schema, run this script.
# Be careful if you have existing data you want to keep! # Set drop_existing_table=True to ensure a clean recreation.
print("Creating MCQ table...") print("Setting up the new MCQ table structure...")
setup_mcq_table(drop_existing_table=False) setup_mcq_table(drop_existing_table=True)
\ No newline at end of file \ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment