add dropdown menu to the frontend of the quiz

parent 66689c39
......@@ -338,25 +338,27 @@ def create_app() -> FastAPI:
@app.post("/mcq/generate")
async def generate_mcqs_handler(
request: Request,
grade: int = Form(...),
curriculum: str = Form(...),
grade: str = Form(...), # Changed to str
subject: str = Form(...),
unit: str = Form(...),
concept: str = Form(...),
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
try:
generated_questions = container.agent_service.generate_and_store_mcqs(
curriculum=curriculum,
grade=grade,
subject=subject,
unit=unit,
concept=concept,
num_questions=count,
is_arabic=is_arabic
is_arabic=is_arabic,
)
return {
"status": "success",
......@@ -364,7 +366,7 @@ def create_app() -> FastAPI:
"questions": generated_questions
}
except HTTPException as e:
raise e # Re-raise FastAPI specific exceptions
raise e
except Exception as e:
logger.error(f"Error in generate_mcqs_handler: {e}")
raise HTTPException(status_code=500, detail=str(e))
......@@ -372,27 +374,27 @@ def create_app() -> FastAPI:
@app.get("/mcq")
async def get_mcqs_handler(
request: Request,
grade: int,
curriculum: str,
grade: str,
subject: str,
unit: str,
concept: str,
is_arabic: bool,
# Make limit optional. If not provided, it will be None.
limit: Optional[int] = None
):
"""
Retrieves existing MCQs for a specific topic and language from the database.
If no limit is provided, retrieves all questions.
Retrieves existing MCQs for a specific topic, now filtering by curriculum.
"""
container = request.app.state.container
try:
questions = container.agent_service.pgvector.get_mcqs(
curriculum=curriculum,
grade=grade,
subject=subject,
unit=unit,
concept=concept,
is_arabic=is_arabic,
limit=limit # Pass the limit (which could be None)
limit=limit
)
return {
"status": "success",
......@@ -406,7 +408,8 @@ def create_app() -> FastAPI:
@app.post("/quiz/dynamic")
async def get_dynamic_quiz_handler(
request: Request,
grade: int = Form(...),
curriculum: str = Form(...),
grade: str = Form(...),
subject: str = Form(...),
unit: str = Form(...),
concept: str = Form(...),
......@@ -414,15 +417,12 @@ def create_app() -> FastAPI:
count: int = Form(5)
):
"""
Generates a dynamic quiz for a topic.
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).
Generates a dynamic quiz, now using curriculum as a key identifier.
"""
container = request.app.state.container
try:
quiz_questions = container.agent_service.get_dynamic_quiz(
curriculum=curriculum,
grade=grade,
subject=subject,
unit=unit,
......@@ -436,7 +436,7 @@ def create_app() -> FastAPI:
"quiz": quiz_questions
}
except HTTPException as e:
raise e # Re-raise FastAPI specific exceptions
raise e
except Exception as e:
logger.error(f"Error in get_dynamic_quiz_handler: {e}")
raise HTTPException(status_code=500, detail=str(e))
......@@ -459,6 +459,37 @@ def create_app() -> FastAPI:
except Exception as e:
print(f"Error serving quiz interface: {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")
async def audio_response_options():
......
......@@ -527,26 +527,37 @@ class PGVectorService:
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:
return
with self.pool_handler.get_connection() as conn:
with conn.cursor() as cur:
# --- UPDATED QUERY ---
# --- UPDATED INSERT QUERY WITH ALL NEW COLUMNS ---
insert_query = """
INSERT INTO mcq_questions (
grade, is_arabic, subject, unit, concept, question_text,
correct_answer, wrong_answer_1, wrong_answer_2, wrong_answer_3
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
curriculum, grade, subject, unit, concept, question_text,
question_type, difficulty_level, is_arabic, correct_answer,
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 = [
(
q['grade'], q['is_arabic'], q['subject'], q['unit'], q['concept'],
q['question_text'], q['correct_answer'], q['wrong_answer_1'],
q['wrong_answer_2'], q['wrong_answer_3']
q.get('curriculum'), q.get('grade'), q.get('subject'), q.get('unit'), q.get('concept'),
q.get('question_text'), q.get('question_type'), q.get('difficulty_level'),
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
]
......@@ -554,23 +565,23 @@ class PGVectorService:
conn.commit()
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.
"""
with self.pool_handler.get_connection() as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Dynamically build the query based on the limit
# --- UPDATED SELECT AND WHERE CLAUSE ---
query = """
SELECT id, question_text, correct_answer, wrong_answer_1, wrong_answer_2, wrong_answer_3
SELECT *
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
"""
params = (grade, subject, unit, concept, is_arabic)
params = (curriculum, grade, subject, unit, concept, is_arabic)
if limit is not None:
query += " LIMIT %s;"
......@@ -579,4 +590,64 @@ class PGVectorService:
query += ";"
cur.execute(query, params)
return cur.fetchall()
\ No newline at end of file
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()
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:
conn = psycopg2.connect(
......@@ -24,30 +24,41 @@ def setup_mcq_table(drop_existing_table: bool = False):
cur.execute("DROP TABLE IF EXISTS mcq_questions CASCADE;")
print("Table dropped.")
print("Creating mcq_questions table...")
# --- THIS IS THE UPDATED TABLE SCHEMA ---
print("Creating mcq_questions table with the NEW COMPREHENSIVE schema...")
# --- THIS IS THE FULLY UPDATED TABLE SCHEMA ---
cur.execute("""
CREATE TABLE IF NOT EXISTS mcq_questions (
id SERIAL PRIMARY KEY,
grade INTEGER NOT NULL,
is_arabic BOOLEAN NOT NULL, -- <-- ADDED THIS LINE
curriculum TEXT,
grade TEXT NOT NULL,
subject TEXT NOT NULL,
unit TEXT NOT NULL,
concept TEXT NOT NULL,
question_text TEXT NOT NULL,
question_type TEXT,
difficulty_level INTEGER,
is_arabic BOOLEAN NOT NULL,
correct_answer TEXT NOT NULL,
wrong_answer_1 TEXT NOT NULL,
wrong_answer_2 TEXT NOT NULL,
wrong_answer_3 TEXT NOT NULL,
wrong_answer_1 TEXT,
wrong_answer_2 TEXT,
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
);
""")
print("Creating indexes on mcq_questions table...")
# --- THIS IS THE UPDATED INDEX ---
# --- UPDATED INDEX TO INCLUDE CURRICULUM ---
cur.execute("""
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.")
......@@ -60,7 +71,7 @@ def setup_mcq_table(drop_existing_table: bool = False):
print("Database connection closed.")
if __name__ == "__main__":
# To apply the changes, it's best to drop and recreate the table.
# Be careful if you have existing data you want to keep!
print("Creating MCQ table...")
setup_mcq_table(drop_existing_table=False)
\ No newline at end of file
# To apply the new schema, run this script.
# Set drop_existing_table=True to ensure a clean recreation.
print("Setting up the new MCQ table structure...")
setup_mcq_table(drop_existing_table=True)
\ 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