Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
A
AI Tutor
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Salma Mohammed Hamed
AI Tutor
Commits
8bbfd066
Commit
8bbfd066
authored
Nov 04, 2025
by
SalmaMohammedHamedMustafa
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add dropdown menu to the frontend of the quiz
parent
66689c39
Changes
5
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
489 additions
and
361 deletions
+489
-361
main.py
self_hosted_env/voice_agent/main.py
+48
-17
agent_service.py
self_hosted_env/voice_agent/services/agent_service.py
+156
-118
pgvector_service.py
self_hosted_env/voice_agent/services/pgvector_service.py
+87
-16
setup_mcq_table.py
self_hosted_env/voice_agent/setup_mcq_table.py
+25
-14
dynamic_quiz_interface.html
...hosted_env/voice_agent/static/dynamic_quiz_interface.html
+173
-196
No files found.
self_hosted_env/voice_agent/main.py
View file @
8bbfd066
...
...
@@ -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
():
...
...
self_hosted_env/voice_agent/services/agent_service.py
View file @
8bbfd066
This diff is collapsed.
Click to expand it.
self_hosted_env/voice_agent/services/pgvector_service.py
View file @
8bbfd066
...
...
@@ -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
self_hosted_env/voice_agent/setup_mcq_table.py
View file @
8bbfd066
...
...
@@ -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
self_hosted_env/voice_agent/static/dynamic_quiz_interface.html
View file @
8bbfd066
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment