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
215ea76f
Commit
215ea76f
authored
Feb 21, 2026
by
salma
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
atom change and reasoning
parent
8b41479b
Changes
16
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
660 additions
and
130 deletions
+660
-130
apply_test_schema.py
self_hosted_env/voice_agent/apply_test_schema.py
+33
-41
container.py
self_hosted_env/voice_agent/core/container.py
+0
-57
main.py
self_hosted_env/voice_agent/main.py
+2
-1
frontend.py
self_hosted_env/voice_agent/routers/frontend.py
+11
-1
reasoning.py
self_hosted_env/voice_agent/routers/reasoning.py
+57
-0
__init__.py
self_hosted_env/voice_agent/services/__init__.py
+2
-1
agent_prompts.py
...d_env/voice_agent/services/agent_helpers/agent_prompts.py
+12
-12
query_handlers.py
..._env/voice_agent/services/agent_helpers/query_handlers.py
+2
-2
reasoning_prompts.py
...v/voice_agent/services/agent_helpers/reasoning_prompts.py
+20
-0
openai_service.py
self_hosted_env/voice_agent/services/openai_service.py
+11
-3
pgvector_service.py
self_hosted_env/voice_agent/services/pgvector_service.py
+54
-1
reasoning_service.py
self_hosted_env/voice_agent/services/reasoning_service.py
+112
-0
audio-recorder.html
self_hosted_env/voice_agent/static/audio-recorder.html
+2
-2
index.html
self_hosted_env/voice_agent/static/index.html
+19
-9
reasoning_CSV_uploader.html
...hosted_env/voice_agent/static/reasoning_CSV_uploader.html
+66
-0
reasoning_interface.html
self_hosted_env/voice_agent/static/reasoning_interface.html
+257
-0
No files found.
self_hosted_env/voice_agent/apply_test_schema.py
View file @
215ea76f
...
...
@@ -23,30 +23,46 @@ CREATE TABLE IF NOT EXISTS chat_history (
FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE
);
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_chat_history_student_id ON chat_history(student_id);
CREATE INDEX IF NOT EXISTS idx_chat_history_created_at ON chat_history(created_at);
CREATE INDEX IF NOT EXISTS idx_students_nationality ON students(nationality);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_students_grade ON students(grade);
CREATE INDEX IF NOT EXISTS idx_students_grade_language ON students(grade, language);
-- Insert d
ummy data for testing
-- Insert d
ata (Preserving OLD students and adding NEW ones up to Grade 12)
INSERT INTO students (student_id, student_name, grade, language, nationality) VALUES
--
Arabic Grade 4: One Egyptian, One Saudi
--
THE ORIGINAL STUDENTS (Do not change IDs)
('student_001', 'Ahmed Ali', 4, TRUE, 'EGYPTIAN'),
('student_002', 'Khalid Al-Rashid', 4, TRUE, 'SAUDI'),
-- Arabic Grade 6: One Egyptian, One Saudi
('student_003', 'Fatima Hassan', 6, TRUE, 'EGYPTIAN'),
('student_004', 'Nora Al-Zahrani', 6, TRUE, 'SAUDI'),
-- English Grade 5: One Egyptian, One Saudi
('student_005', 'Mona Adel', 5, FALSE, 'EGYPTIAN'),
('student_006', 'Sara Al-Mutairi', 5, FALSE, 'SAUDI'),
-- English Grade 6: One Egyptian, One Saudi
('student_007', 'Omar Youssef', 6, FALSE, 'EGYPTIAN'),
('student_008', 'Abdullah Al-Harbi', 6, FALSE, 'SAUDI')
('student_008', 'Abdullah Al-Harbi', 6, FALSE, 'SAUDI'),
-- NEW STUDENTS: PREP & SECONDARY (Grades 7-12)
-- Grade 7 (Prep 1)
('student_009', 'Mostafa Mahmoud', 7, TRUE, 'EGYPTIAN'),
('student_010', 'Yasmine Fahmy', 7, FALSE, 'EGYPTIAN'),
-- Grade 8 (Prep 2)
('student_011', 'Hassan Kareem', 8, TRUE, 'EGYPTIAN'),
('student_012', 'Laila Ibrahim', 8, FALSE, 'EGYPTIAN'),
-- Grade 9 (Prep 3)
('student_013', 'Zaid Al-Qahtani', 9, TRUE, 'SAUDI'),
('student_014', 'Mariam Soliman', 9, FALSE, 'EGYPTIAN'),
-- Grade 10 (Secondary 1)
('student_015', 'Tarek Hegazy', 10, TRUE, 'EGYPTIAN'),
('student_016', 'Salma Ezzat', 10, FALSE, 'EGYPTIAN'),
-- Grade 11 (Secondary 2)
('student_017', 'Faisal Al-Otaibi', 11, TRUE, 'SAUDI'),
('student_018', 'Habiba Ahmed', 11, FALSE, 'EGYPTIAN'),
-- Grade 12 (Secondary 3)
('student_019', 'Youssef Mansour', 12, TRUE, 'EGYPTIAN'),
('student_020', 'Jana Wael', 12, FALSE, 'EGYPTIAN')
ON CONFLICT (student_id) DO NOTHING;
"""
...
...
@@ -56,7 +72,6 @@ DO $$
DECLARE
rec RECORD;
BEGIN
-- drop all tables in public schema
FOR rec IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
EXECUTE 'DROP TABLE IF EXISTS "' || rec.tablename || '" CASCADE';
END LOOP;
...
...
@@ -64,11 +79,6 @@ END $$;
"""
def
setup_database
(
drop_existing_tables
:
bool
=
False
):
"""
Sets up the database schema and tables.
Args:
drop_existing_tables: If True, drops all existing tables before creating them.
"""
try
:
conn
=
psycopg2
.
connect
(
host
=
os
.
getenv
(
"POSTGRES_HOST"
,
"localhost"
),
...
...
@@ -83,40 +93,22 @@ def setup_database(drop_existing_tables: bool = False):
if
drop_existing_tables
:
print
(
"Dropping all existing tables..."
)
cur
.
execute
(
drop_all_tables_sql
)
print
(
"All tables dropped."
)
print
(
"Setting up schema and inserting data..."
)
print
(
"Setting up schema and inserting
hardcoded
data..."
)
cur
.
execute
(
schema_sql
)
print
(
"Database setup complete. Verifying data..."
)
# Verifications: Select from students and chat_history tables
print
(
"
\n
Students table rows:"
)
cur
.
execute
(
"SELECT * FROM students ORDER BY id;"
)
students
=
cur
.
fetchall
()
for
row
in
students
:
print
(
row
)
print
(
"
\n
Chat_history table rows:"
)
cur
.
execute
(
"SELECT * FROM chat_history ORDER BY id;"
)
chat_history
=
cur
.
fetchall
()
for
row
in
chat_history
:
print
(
"Database setup complete."
)
cur
.
execute
(
"SELECT student_id, student_name, grade FROM students ORDER BY grade ASC;"
)
for
row
in
cur
.
fetchall
():
print
(
row
)
except
psycopg2
.
OperationalError
as
e
:
print
(
f
"Database connection failed: {e}"
)
except
Exception
as
e
:
print
(
f
"An error occurred: {e}"
)
finally
:
if
'conn'
in
locals
()
and
conn
:
conn
.
close
()
print
(
"Database connection closed."
)
if
__name__
==
"__main__"
:
_drop_env
=
os
.
getenv
(
"DROP_TEST_SCHEMA"
,
"False"
)
drop_existing_tables
=
str
(
_drop_env
)
.
strip
()
.
lower
()
in
(
"1"
,
"true"
,
"yes"
,
"y"
,
"t"
)
print
(
"**************************************************"
)
print
(
f
"Drop existing tables: {drop_existing_tables}"
)
print
(
"**************************************************"
)
setup_database
(
drop_existing_tables
=
drop_existing_tables
)
\ No newline at end of file
self_hosted_env/voice_agent/core/container.py
deleted
100644 → 0
View file @
8b41479b
import
os
from
pathlib
import
Path
import
sys
sys
.
path
.
append
(
os
.
path
.
abspath
(
os
.
path
.
join
(
os
.
path
.
dirname
(
__file__
),
'..'
)))
class
DIContainer
:
def
__init__
(
self
):
from
.
import
AppConfig
from
repositories
import
MinIOStorageRepository
from
services
import
(
AudioService
,
ChatService
,
HealthService
,
ResponseService
,
ResponseManager
,
OpenAIService
,
AgentService
,
ConnectionPool
,
LanguageSegmentationService
,
DataIngestionService
,
MCQService
,
PGVectorService
,
ChatDatabaseService
)
self
.
config
=
AppConfig
.
from_env
()
self
.
storage_repo
=
MinIOStorageRepository
(
self
.
config
)
self
.
response_manager
=
ResponseManager
()
# Initialize OpenAI and Agent services
self
.
openai_service
=
OpenAIService
()
self
.
pool_handler
=
ConnectionPool
(
dbname
=
os
.
getenv
(
"POSTGRES_DB"
),
user
=
os
.
getenv
(
"POSTGRES_USER"
),
password
=
os
.
getenv
(
"POSTGRES_PASSWORD"
),
host
=
os
.
getenv
(
"DB_HOST"
),
port
=
int
(
os
.
getenv
(
"DB_PORT"
))
)
print
(
os
.
getenv
(
"DB_HOST"
),
os
.
getenv
(
"POSTGRES_DB"
),
os
.
getenv
(
"POSTGRES_USER"
))
self
.
chat_db_service
=
ChatDatabaseService
(
self
.
pool_handler
)
self
.
pgvector_service
=
PGVectorService
(
self
.
pool_handler
)
self
.
agent_service
=
AgentService
(
use_pgvector
=
True
,
pgvector
=
self
.
pgvector_service
,
db_service
=
self
.
chat_db_service
)
self
.
mcq_service
=
MCQService
(
self
.
pgvector_service
,
self
.
chat_db_service
)
self
.
data_ingestion_service
=
DataIngestionService
(
pool_handler
=
self
.
pool_handler
)
# Initialize services
self
.
audio_service
=
AudioService
(
self
.
storage_repo
,
self
.
config
.
minio_bucket
)
self
.
segmentation_service
=
LanguageSegmentationService
()
self
.
chat_service
=
ChatService
(
self
.
storage_repo
,
self
.
response_manager
,
self
.
config
,
self
.
openai_service
,
self
.
agent_service
,
self
.
segmentation_service
)
self
.
response_service
=
ResponseService
(
self
.
response_manager
,
self
.
audio_service
)
self
.
health_service
=
HealthService
(
self
.
storage_repo
,
self
.
config
)
\ No newline at end of file
self_hosted_env/voice_agent/main.py
View file @
215ea76f
...
...
@@ -10,7 +10,7 @@ from core.container import DIContainer
from
services
import
WebSocketManager
,
redis_client
,
redis_listener
# Import Routers
from
routers
import
chat
,
quiz
,
multiplayer
,
curriculum
,
frontend
,
system
from
routers
import
chat
,
quiz
,
multiplayer
,
curriculum
,
frontend
,
system
,
reasoning
logger
=
logging
.
getLogger
(
"uvicorn.error"
)
...
...
@@ -87,6 +87,7 @@ def create_app() -> FastAPI:
app
.
include_router
(
multiplayer
.
router
)
app
.
include_router
(
curriculum
.
router
)
app
.
include_router
(
system
.
router
)
app
.
include_router
(
reasoning
.
router
)
return
app
...
...
self_hosted_env/voice_agent/routers/frontend.py
View file @
215ea76f
...
...
@@ -43,4 +43,14 @@ async def serve_test_yourself_interface():
@
router
.
get
(
"/curriculum-upload"
)
async
def
serve_curriculum_upload
():
"""Serve the curriculum upload HTML file"""
return
serve_html
(
"curriculum_PDF_uploader.html"
)
\ No newline at end of file
return
serve_html
(
"curriculum_PDF_uploader.html"
)
@
router
.
get
(
"/reasoning-upload"
)
async
def
serve_reasoning_upload
():
"""Serve the reasoning questions CSV uploader HTML file"""
return
serve_html
(
"reasoning_CSV_uploader.html"
)
@
router
.
get
(
"/reasoning-interface"
)
async
def
serve_reasoning_interface
():
"""Serve the interactive Reasoning Challenge interface"""
return
serve_html
(
"reasoning_interface.html"
)
\ No newline at end of file
self_hosted_env/voice_agent/routers/reasoning.py
0 → 100644
View file @
215ea76f
from
fastapi
import
APIRouter
,
UploadFile
,
File
,
Form
,
Request
,
Response
import
base64
router
=
APIRouter
(
tags
=
[
"Reasoning Mode"
])
@
router
.
post
(
"/reasoning/upload"
)
async
def
upload_reasoning_csv
(
request
:
Request
,
grade
:
int
=
Form
(
...
),
file
:
UploadFile
=
File
(
...
)
):
content
=
await
file
.
read
()
success
=
request
.
app
.
state
.
container
.
reasoning_service
.
bulk_upload_questions
(
content
,
grade
)
return
{
"status"
:
"success"
if
success
else
"error"
}
@
router
.
post
(
"/reasoning/ask"
)
async
def
ask_reasoning
(
request
:
Request
,
student_id
:
str
=
Form
(
...
)):
container
=
request
.
app
.
state
.
container
text
,
audio_bytes
=
container
.
reasoning_service
.
get_atom_challenge
(
student_id
)
encoded_text
=
base64
.
b64encode
(
text
.
encode
(
'utf-8'
))
.
decode
(
'utf-8'
)
return
Response
(
content
=
audio_bytes
,
media_type
=
"audio/wav"
,
headers
=
{
"X-Response-Text"
:
encoded_text
,
"X-Response-Type"
:
"reasoning_ask"
,
"Access-Control-Expose-Headers"
:
"X-Response-Text, X-Response-Type"
# Add this!
}
)
@
router
.
post
(
"/reasoning/evaluate"
)
async
def
evaluate_reasoning
(
request
:
Request
,
student_id
:
str
=
Form
(
...
),
file
:
UploadFile
=
File
(
...
)
):
container
=
request
.
app
.
state
.
container
audio_content
=
await
file
.
read
()
filename
=
file
.
filename
# نحصل على الاسم الأصلي مثل answer.webm
feedback
,
audio_bytes
,
student_said
=
container
.
reasoning_service
.
evaluate_student_answer
(
student_id
,
audio_content
,
filename
)
encoded_feedback
=
base64
.
b64encode
(
feedback
.
encode
(
'utf-8'
))
.
decode
(
'utf-8'
)
encoded_said
=
base64
.
b64encode
(
student_said
.
encode
(
'utf-8'
))
.
decode
(
'utf-8'
)
return
Response
(
content
=
audio_bytes
,
media_type
=
"audio/wav"
,
headers
=
{
"X-Response-Text"
:
encoded_feedback
,
"X-Student-Said"
:
encoded_said
,
"X-Response-Type"
:
"reasoning_eval"
,
"Access-Control-Expose-Headers"
:
"X-Response-Text, X-Student-Said, X-Response-Type"
}
)
\ No newline at end of file
self_hosted_env/voice_agent/services/__init__.py
View file @
215ea76f
...
...
@@ -13,4 +13,5 @@ from .segmentation_service import LanguageSegmentationService
from
.data_ingestion_service
import
DataIngestionService
from
.websocket_service
import
WebSocketManager
from
.redis_client
import
redis_client
,
redis_listener
,
get_room_key
,
get_room_channel
from
.mcq_service
import
MCQService
\ No newline at end of file
from
.mcq_service
import
MCQService
from
.reasoning_service
import
ReasoningService
\ No newline at end of file
self_hosted_env/voice_agent/services/agent_helpers/agent_prompts.py
View file @
215ea76f
...
...
@@ -16,10 +16,10 @@ SYSTEM_PROMPTS: Dict[Tuple[StudentNationality, StudyLanguage], str] = {
فقط لو الطفّل سأل عن هويتك بصراحة ووضح (مثل "إنت مين؟"، "عرّفني بنفسك"، "إنت بتعمل إيه هنا؟")،
رُد بالنصّ الثابت ده:
"أنا
عَنان مؤسِّس
شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
"أنا
Atom من
شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أَساعدك تتعلِّم أي حاجة عايز تتعلِّمها في العلوم."
⚠️ مهم: لا تستخدم هذا النص في أي حالة أخرى غير سؤال الهوية.
⚠️ مهم جدًا: مَمنوع تقول أبداً إنك AI أو برنامج أو روبوت. إنت
عنان
وبس.
⚠️ مهم جدًا: مَمنوع تقول أبداً إنك AI أو برنامج أو روبوت. إنت
Atom
وبس.
لو سأل أسئلة عامة زي "نت عارف انا مين؟" أو "إزيك؟" أو "شكرا"، رد بطريقة طبيعية ودودة باستخدام اسمه {student_name}.
...
...
@@ -78,7 +78,7 @@ SYSTEM_PROMPTS: Dict[Tuple[StudentNationality, StudyLanguage], str] = {
لَمّا تُذكر الصف {grade}، قُله بالطريقة الطبيعيّة زي ما الأطفال بيقولوها: الصف 4 = سنة رابعة ابتدائي، الصف 5 = سنة خامسة ابتدائي، وهكذا.
الهَدف: رَد قُصيرَ يُعلِّم ويُوصَّل المَعلومة، ويُبان إن "
عَنان
" بيشرَح للطفل جوّه مَنَصّة "شارِع العلوم"، مُش كتاب بيتقري.
الهَدف: رَد قُصيرَ يُعلِّم ويُوصَّل المَعلومة، ويُبان إن "
Atom
" بيشرَح للطفل جوّه مَنَصّة "شارِع العلوم"، مُش كتاب بيتقري.
"""
,
# ---------- Saudi + Arabic ----------
...
...
@@ -92,10 +92,10 @@ SYSTEM_PROMPTS: Dict[Tuple[StudentNationality, StudyLanguage], str] = {
فقط لو الطفّل سأل عن هويتك بصراحة ووضح (مثل "إنت مين؟"، "عرّفني بنفسك"، "إنت بتعمل إيه هنا؟")،
رُد بالنصّ الثابت ده:
"أنا
عَنان مؤسِّس
شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
"أنا
Atom من
شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أَساعدك تتعلِّم أي حاجة عايز تتعلِّمها في العلوم."
⚠️ مهم: لا تستخدم هذا النص في أي حالة أخرى غير سؤال الهوية.
⚠️ مهم جدًا: مَمنوع تقول أبداً إنك AI أو برنامج أو روبوت. إنت
عنان
وبس.
⚠️ مهم جدًا: مَمنوع تقول أبداً إنك AI أو برنامج أو روبوت. إنت
Atom
وبس.
لو سأل أسئلة عامة زي "نت عارف انا مين؟" أو "إزيك؟" أو "شكرا"، رد بطريقة طبيعية ودودة باستخدام اسمه {student_name}.
...
...
@@ -154,7 +154,7 @@ SYSTEM_PROMPTS: Dict[Tuple[StudentNationality, StudyLanguage], str] = {
لَمّا تُذكر الصف {grade}، قُله بالطريقة الطبيعيّة زي ما الأطفال بيقولوها: الصف 4 = سنة رابعة ابتدائي، الصف 5 = سنة خامسة ابتدائي، وهكذا.
الهَدف: رَد قُصيرَ يُعلِّم ويُوصَّل المَعلومة، ويُبان إن "
عَنان
" بيشرَح للطفل جوّه مَنَصّة "شارِع العلوم"، مُش كتاب بيتقري.
الهَدف: رَد قُصيرَ يُعلِّم ويُوصَّل المَعلومة، ويُبان إن "
Atom
" بيشرَح للطفل جوّه مَنَصّة "شارِع العلوم"، مُش كتاب بيتقري.
"""
,
# -------- Egyptian English --------
...
...
@@ -169,10 +169,10 @@ SYSTEM_PROMPTS: Dict[Tuple[StudentNationality, StudyLanguage], str] = {
لو الطفّل سأل عن هويتك بصراحة (زي "إنت مين؟"، "عرِّفني بنفسك")،
رُد بالنصّ الثابت ده:
"أنا
عَنان مؤسس
شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
"أنا
Atom من
شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أَساعدك تتعلِّم أي حاجة عايز تتعلِّمها في العلوم."
⚠️ مهم: لا تستخدم هذا النص في أي حالة أخرى غير سؤال الهوية.
⚠️ مهم جدًا: مَمنوع تقول أبداً إنك AI أو برنامج أو روبوت. إنت
عنان
وبس.
⚠️ مهم جدًا: مَمنوع تقول أبداً إنك AI أو برنامج أو روبوت. إنت
Atom
وبس.
...
...
@@ -230,7 +230,7 @@ SYSTEM_PROMPTS: Dict[Tuple[StudentNationality, StudyLanguage], str] = {
الأرقام في الجُملَ العاديّة بالإنجليزي بالحروف (two, three).
الهَدف: إجابة بالإنجليزي واضحة ومبسّطة، وبعدها عرض مساعدة إضافية بالعربي لو الطفّل حب،
بحيث يبان إن "
عَنان
" بيشرح جوّه مَنَصّة "شارِع العُلوم".
بحيث يبان إن "
Atom
" بيشرح جوّه مَنَصّة "شارِع العُلوم".
"""
,
# -------- Saudi English --------
...
...
@@ -245,10 +245,10 @@ SYSTEM_PROMPTS: Dict[Tuple[StudentNationality, StudyLanguage], str] = {
لو الطفّل سأل عن هويتك بصراحة (زي "إنت مين؟"، "عرِّفني بنفسك")،
رُد بالنصّ الثابت ده:
"أنا
عَنان مؤسس
شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
"أنا
Atom من
شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أَساعدك تتعلِّم أي حاجة عايز تتعلِّمها في العلوم."
⚠️ مهم: لا تستخدم هذا النص في أي حالة أخرى غير سؤال الهوية.
⚠️ مهم جدًا: مَمنوع تقول أبداً إنك AI أو برنامج أو روبوت. إنت
عنان
وبس.
⚠️ مهم جدًا: مَمنوع تقول أبداً إنك AI أو برنامج أو روبوت. إنت
Atom
وبس.
...
...
@@ -306,7 +306,7 @@ SYSTEM_PROMPTS: Dict[Tuple[StudentNationality, StudyLanguage], str] = {
الأرقام في الجُملَ العاديّة بالإنجليزي بالحروف (two, three).
الهَدف: إجابة بالإنجليزي واضحة ومبسّطة، وبعدها عرض مساعدة إضافية بالعربي لو الطفّل حب،
بحيث يبان إن "
عَنان
" بيشرح جوّه مَنَصّة "شارِع العُلوم".
بحيث يبان إن "
Atom
" بيشرح جوّه مَنَصّة "شارِع العُلوم".
"""
}
self_hosted_env/voice_agent/services/agent_helpers/query_handlers.py
View file @
215ea76f
...
...
@@ -22,7 +22,7 @@ GENERAL_CHAT_CONTEXTS: Dict[StudentNationality, str] = {
خَليك بتِرُد بالعاميّة المَصري، وبطريقة بسيطة وودودة.
- لو الطِّفل سأل: "إنت مين؟" → رد بالهوية المخصصة ليك (أنا
عَنان
...).
- لو الطِّفل سأل: "إنت مين؟" → رد بالهوية المخصصة ليك (أنا
Atom
...).
- لو الطِّفل سأل: "أنا مين؟" أو "إنت عارف أنا مين؟" → رد باستخدام بيانات الطالب اللي فوق (الاسم + الصف)، مثلاً:
"أيوه طبعًا، إنت/أنتِ (اسم الطالب بالعربي) في سنة (سنة الطالب بالعربي). عايز نكمّل النهارده في موضوع معين في العلوم؟"
- لو السُّؤال له علاقة بالعلوم أو بالمنهج → جاوب عليه.
...
...
@@ -41,7 +41,7 @@ GENERAL_CHAT_CONTEXTS: Dict[StudentNationality, str] = {
السؤال: "{query}"
- لو الطِّفل سأل: "إنت مين؟" → رد بالهوية المخصصة ليك (أنا
عَنان
...).
- لو الطِّفل سأل: "إنت مين؟" → رد بالهوية المخصصة ليك (أنا
Atom
...).
- لو الطِّفل سأل: "أنا مين؟" أو "إنت عارف أنا مين؟" → رد باستخدام بيانات الطالب اللي فوق (الاسم + الصف)، مثلاً:
"أيوه طبعًا، إنت/أنتِ (اسم الطالب بالعربي) في سنة (سنة الطالب بالعربي). عايز نكمّل النهارده في موضوع معين في العلوم؟"
- لو السُّؤال له علاقة بالعلوم أو بالمنهج → جاوب عليه.
...
...
self_hosted_env/voice_agent/services/agent_helpers/reasoning_prompts.py
0 → 100644
View file @
215ea76f
REASONING_SYSTEM_BASE
=
"""
إنت Atom مُدرِّس العلوم في مَنَصّة Science Street Lab.
تتحدث بلهجة مصرية بسيطة وقصيرة جداً.
⚠️ قواعد صارمة:
- ادخل في السؤال فوراً بدون مقدمات مثل .
- السؤال القادم من قاعدة البيانات يبدأ بكلمة (علل) أو (ماذا يحدث)، فلا تضف قبلها "ليه" أو "علل" مرة أخرى.
- ممنوع الرغي، اجعل كلامك كله لا يتعدى جملتين.
"""
ASK_PROMPT
=
REASONING_SYSTEM_BASE
+
"""
اطرح السؤال التالي على {student_name} كما هو مكتوب بالضبط: "{question_text}"
"""
EVALUATE_PROMPT
=
REASONING_SYSTEM_BASE
+
"""
قيم إجابة الطفل على سؤال: "{question_text}"
الإجابة النموذجية: "{model_answer}"
الطفل قال: "{student_answer}"
صحح له بلهجة مصرية "مختصرة جداً". إذا أصاب قل "برافو" ووضح السبب باختصار. إذا أخطأ صحح له المعلومة في جملة واحدة.
"""
\ No newline at end of file
self_hosted_env/voice_agent/services/openai_service.py
View file @
215ea76f
...
...
@@ -60,7 +60,7 @@ class OpenAIService(BaseTTSService):
if
not
self
.
is_available
():
raise
HTTPException
(
status_code
=
500
,
detail
=
"OpenAI service not available"
)
voice
=
"
alloy
"
voice
=
"
echo
"
try
:
print
(
f
"Generating TTS audio with OpenAI: {text[:50]}..."
)
...
...
@@ -69,8 +69,16 @@ class OpenAIService(BaseTTSService):
model
=
Models
.
tts
,
voice
=
voice
,
input
=
text
,
response_format
=
"wav"
)
response_format
=
"wav"
,
speed
=
1.2
,
instructions
=
"""
اتكلم كأنك روبوت صغير كيوت.
نبرة خفيفة ومرحة.
إيقاع واضح ومنظم.
فيه لمسة روبوت بسيطة.
من غير مبالغة في المشاعر.
"""
,
)
audio_bytes
=
response
.
content
print
(
"OpenAI TTS generation successful."
)
...
...
self_hosted_env/voice_agent/services/pgvector_service.py
View file @
215ea76f
...
...
@@ -717,4 +717,57 @@ class PGVectorService:
cur
.
executemany
(
insert_query
,
data_to_insert
)
conn
.
commit
()
logger
.
info
(
f
"Successfully inserted {len(mcq_list)} MCQs with vectors."
)
\ No newline at end of file
logger
.
info
(
f
"Successfully inserted {len(mcq_list)} MCQs with vectors."
)
def
setup_reasoning_tables
(
self
):
"""Automatically creates the reasoning bank and session tables."""
with
self
.
pool_handler
.
get_connection
()
as
conn
:
with
conn
.
cursor
()
as
cur
:
cur
.
execute
(
"""
CREATE TABLE IF NOT EXISTS reasoning_questions (
id SERIAL PRIMARY KEY,
grade INTEGER NOT NULL,
question_text TEXT NOT NULL,
model_answer TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""
)
cur
.
execute
(
"""
CREATE TABLE IF NOT EXISTS reasoning_sessions (
student_id VARCHAR(100) PRIMARY KEY,
current_question_id INTEGER REFERENCES reasoning_questions(id),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""
)
conn
.
commit
()
def
get_random_reasoning_question
(
self
,
grade
:
int
):
with
self
.
pool_handler
.
get_connection
()
as
conn
:
with
conn
.
cursor
(
cursor_factory
=
RealDictCursor
)
as
cur
:
cur
.
execute
(
"SELECT id, question_text, model_answer FROM reasoning_questions WHERE grade =
%
s ORDER BY RANDOM() LIMIT 1"
,
(
grade
,)
)
return
cur
.
fetchone
()
def
update_reasoning_session
(
self
,
student_id
:
str
,
question_id
:
int
):
with
self
.
pool_handler
.
get_connection
()
as
conn
:
with
conn
.
cursor
()
as
cur
:
cur
.
execute
(
"""INSERT INTO reasoning_sessions (student_id, current_question_id)
VALUES (
%
s,
%
s)
ON CONFLICT (student_id) DO UPDATE SET current_question_id = EXCLUDED.current_question_id"""
,
(
student_id
,
question_id
)
)
conn
.
commit
()
def
get_active_reasoning_question
(
self
,
student_id
:
str
):
with
self
.
pool_handler
.
get_connection
()
as
conn
:
with
conn
.
cursor
(
cursor_factory
=
RealDictCursor
)
as
cur
:
cur
.
execute
(
"""SELECT q.question_text, q.model_answer
FROM reasoning_sessions s
JOIN reasoning_questions q ON s.current_question_id = q.id
WHERE s.student_id =
%
s"""
,
(
student_id
,)
)
return
cur
.
fetchone
()
\ No newline at end of file
self_hosted_env/voice_agent/services/reasoning_service.py
0 → 100644
View file @
215ea76f
import
logging
import
csv
import
io
import
base64
from
fastapi
import
HTTPException
from
core
import
Models
from
services.agent_helpers.reasoning_prompts
import
ASK_PROMPT
,
EVALUATE_PROMPT
logger
=
logging
.
getLogger
(
__name__
)
class
ReasoningService
:
def
__init__
(
self
,
pgvector
,
db_service
,
openai_service
,
tts_service
):
self
.
pgvector
=
pgvector
self
.
db_service
=
db_service
self
.
openai
=
openai_service
self
.
tts
=
tts_service
# Initialize tables automatically
self
.
pgvector
.
setup_reasoning_tables
()
def
bulk_upload_questions
(
self
,
csv_content
:
bytes
,
grade
:
int
):
try
:
decoded
=
csv_content
.
decode
(
'utf-8-sig'
)
reader
=
csv
.
DictReader
(
io
.
StringIO
(
decoded
))
with
self
.
pgvector
.
pool_handler
.
get_connection
()
as
conn
:
with
conn
.
cursor
()
as
cur
:
count
=
0
for
row
in
reader
:
row
=
{
k
.
strip
():
v
for
k
,
v
in
row
.
items
()
if
k
}
q_text
=
row
.
get
(
'السؤال'
)
or
row
.
get
(
'question_text'
)
a_text
=
row
.
get
(
'الاجابة'
)
or
row
.
get
(
'model_answer'
)
if
q_text
and
a_text
:
cur
.
execute
(
"INSERT INTO reasoning_questions (grade, question_text, model_answer) VALUES (
%
s,
%
s,
%
s)"
,
(
grade
,
q_text
.
strip
(),
a_text
.
strip
())
)
count
+=
1
conn
.
commit
()
print
(
f
"Successfully uploaded {count} questions for grade {grade}"
)
return
True
except
Exception
as
e
:
logger
.
error
(
f
"Failed to upload reasoning CSV: {e}"
)
return
False
def
get_atom_challenge
(
self
,
student_id
:
str
):
# 1. Retrieve student info
student_info
=
self
.
db_service
.
get_student_info
(
student_id
)
if
not
student_info
:
logger
.
error
(
f
"Student {student_id} not found"
)
raise
HTTPException
(
status_code
=
404
,
detail
=
"Student not found"
)
grade
=
student_info
.
grade
student_name
=
student_info
.
student_name
# 2. Get random question
question_data
=
self
.
pgvector
.
get_random_reasoning_question
(
grade
)
if
not
question_data
:
logger
.
error
(
f
"No questions found for grade {grade}"
)
raise
HTTPException
(
status_code
=
404
,
detail
=
"No questions found for this grade."
)
# 3. Save session reference
self
.
pgvector
.
update_reasoning_session
(
student_id
,
question_data
[
'id'
])
# 4. Atomize the question
prompt
=
ASK_PROMPT
.
format
(
student_name
=
student_name
,
question_text
=
question_data
[
'question_text'
])
ai_response
=
self
.
openai
.
client
.
chat
.
completions
.
create
(
model
=
Models
.
chat
,
messages
=
[{
"role"
:
"user"
,
"content"
:
prompt
}]
)
atom_text
=
ai_response
.
choices
[
0
]
.
message
.
content
self
.
db_service
.
add_message
(
student_id
,
'assistant'
,
atom_text
)
audio_bytes
=
self
.
tts
.
generate_speech
(
atom_text
)
return
atom_text
,
audio_bytes
def
evaluate_student_answer
(
self
,
student_id
:
str
,
audio_content
:
bytes
,
filename
:
str
):
student_info
=
self
.
db_service
.
get_student_info
(
student_id
)
active_q
=
self
.
pgvector
.
get_active_reasoning_question
(
student_id
)
if
not
active_q
:
raise
HTTPException
(
status_code
=
400
,
detail
=
"No active reasoning session."
)
student_said
=
self
.
openai
.
transcribe_audio
(
audio_content
,
filename
)
self
.
db_service
.
add_message
(
student_id
,
'user'
,
student_said
)
recent_history
=
self
.
db_service
.
get_chat_history
(
student_id
,
limit
=
5
)
history_context
=
"
\n
"
.
join
([
f
"{m['role']}: {m['content']}"
for
m
in
recent_history
])
prompt
=
EVALUATE_PROMPT
.
format
(
student_name
=
student_info
.
student_name
,
question_text
=
active_q
[
'question_text'
],
model_answer
=
active_q
[
'model_answer'
],
student_answer
=
student_said
)
messages
=
[
{
"role"
:
"system"
,
"content"
:
"Recent context:
\n
"
+
history_context
},
{
"role"
:
"user"
,
"content"
:
prompt
}
]
ai_eval
=
self
.
openai
.
client
.
chat
.
completions
.
create
(
model
=
Models
.
chat
,
messages
=
messages
,
temperature
=
0.3
)
feedback_text
=
ai_eval
.
choices
[
0
]
.
message
.
content
self
.
db_service
.
add_message
(
student_id
,
'assistant'
,
feedback_text
)
audio_bytes
=
self
.
tts
.
generate_speech
(
feedback_text
)
return
feedback_text
,
audio_bytes
,
student_said
\ No newline at end of file
self_hosted_env/voice_agent/static/audio-recorder.html
View file @
215ea76f
...
...
@@ -300,7 +300,7 @@
const
msgDiv
=
document
.
createElement
(
'div'
);
msgDiv
.
className
=
`message
${
sender
}
-message`
;
const
senderName
=
sender
===
'user'
?
'أنت'
:
'
عنان
'
;
const
senderName
=
sender
===
'user'
?
'أنت'
:
'
Atom
'
;
msgDiv
.
innerHTML
=
`<strong>
${
senderName
}
:</strong> <div class="message-content"></div>`
;
msgDiv
.
querySelector
(
'.message-content'
).
innerHTML
=
text
;
...
...
@@ -383,7 +383,7 @@
}
async
getAgentResponse
(
studentId
)
{
this
.
uiManager
.
showStatus
(
'جاري جلب رد
عنان
...'
,
StatusType
.
PROCESSING
);
this
.
uiManager
.
showStatus
(
'جاري جلب رد
Atom
...'
,
StatusType
.
PROCESSING
);
try
{
const
response
=
await
this
.
apiClient
.
fetchAudioResponse
(
studentId
);
const
responseType
=
response
.
headers
.
get
(
'X-Response-Type'
);
...
...
self_hosted_env/voice_agent/static/index.html
View file @
215ea76f
...
...
@@ -61,18 +61,28 @@
.link-list
a
.test-yourself
:hover
{
background-color
:
#218838
;
}
.link-list
a
.live-quiz
{
background-color
:
#fd7e14
;
}
.link-list
a
.live-quiz
:hover
{
background-color
:
#e36a04
;
}
.link-list
a
.reasoning
{
background-color
:
#512da8
;
}
/* Deep Purple for Challenge */
.link-list
a
.reasoning
:hover
{
background-color
:
#4527a0
;
}
.link-list
a
.admin-tool
{
background-color
:
#343a40
;
}
/* Dark Slate for Admin tools */
.link-list
a
.admin-tool
:hover
{
background-color
:
#23272b
;
}
</style>
</head>
<body>
<div
class=
"container"
>
<h1>
SSLabs AI Feature Hub
</h1>
<ul
class=
"link-list"
>
<li><a
href=
"/chat-interface"
class=
"chat"
>
Voice Chat Interface
</a></li>
<li><a
href=
"/test-yourself"
class=
"test-yourself"
>
Test Yourself (Single Player)
</a></li>
<li><a
href=
"/live-quiz"
class=
"live-quiz"
>
Live Quiz Challenge (Multiplayer)
</a></li>
<li><a
href=
"/quiz-interface"
class=
"dynamic-quiz"
>
Dynamic Quiz Generator (for CSV)
</a></li>
<li><a
href=
"/curriculum-upload"
class=
"upload"
>
Curriculum PDF Uploader
</a></li>
</ul>
</div>
<h1>
SSLabs AI Feature Hub
</h1>
<ul
class=
"link-list"
>
<!-- Student Activities -->
<li><a
href=
"/chat-interface"
class=
"chat"
>
Voice Chat Interface
</a></li>
<li><a
href=
"/test-yourself"
class=
"test-yourself"
>
Test Yourself (Single Player)
</a></li>
<li><a
href=
"/reasoning-interface"
class=
"reasoning"
>
Reasoning Challenge (Atom)
</a></li>
<li><a
href=
"/live-quiz"
class=
"live-quiz"
>
Live Quiz Challenge (Multiplayer)
</a></li>
<!-- Teacher/Admin Tools -->
<li><a
href=
"/quiz-interface"
class=
"dynamic-quiz"
>
Dynamic Quiz Generator
</a></li>
<li><a
href=
"/reasoning-upload"
class=
"admin-tool"
>
Reasoning CSV Ingestion
</a></li>
<li><a
href=
"/curriculum-upload"
class=
"upload"
>
Curriculum PDF Uploader
</a></li>
</ul>
</div>
</body>
</html>
\ No newline at end of file
self_hosted_env/voice_agent/static/reasoning_CSV_uploader.html
0 → 100644
View file @
215ea76f
<!DOCTYPE html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<title>
Reasoning Bank Ingestion
</title>
<style>
body
{
font-family
:
sans-serif
;
max-width
:
500px
;
margin
:
50px
auto
;
padding
:
20px
;
background
:
#f4f7f6
;
}
.card
{
background
:
white
;
padding
:
30px
;
border-radius
:
12px
;
box-shadow
:
0
4px
10px
rgba
(
0
,
0
,
0
,
0.1
);
}
h1
{
color
:
#6f42c1
;
text-align
:
center
;
margin-bottom
:
25px
;
}
label
{
display
:
block
;
margin
:
15px
0
5px
;
font-weight
:
bold
;
}
input
{
width
:
100%
;
padding
:
12px
;
border
:
1px
solid
#ddd
;
border-radius
:
8px
;
box-sizing
:
border-box
;
}
button
{
width
:
100%
;
padding
:
14px
;
margin-top
:
25px
;
border
:
none
;
border-radius
:
8px
;
background
:
#6f42c1
;
color
:
white
;
font-weight
:
bold
;
cursor
:
pointer
;
}
.status
{
margin-top
:
15px
;
padding
:
12px
;
border-radius
:
8px
;
display
:
none
;
text-align
:
center
;
}
.success
{
background
:
#d4edda
;
color
:
#155724
;
}
.error
{
background
:
#f8d7da
;
color
:
#721c24
;
}
</style>
</head>
<body>
<div
class=
"card"
>
<h1>
Reasoning Ingestion
</h1>
<label>
Grade Number:
</label>
<input
type=
"number"
id=
"gradeNumber"
placeholder=
"e.g. 4 or 7"
min=
"1"
>
<label>
CSV File (السؤال, الاجابة):
</label>
<input
type=
"file"
id=
"csvFile"
accept=
".csv"
>
<button
id=
"uploadBtn"
>
Upload Bank
</button>
<div
id=
"status"
class=
"status"
></div>
</div>
<script>
document
.
getElementById
(
'uploadBtn'
).
onclick
=
async
()
=>
{
const
file
=
document
.
getElementById
(
'csvFile'
).
files
[
0
];
const
grade
=
document
.
getElementById
(
'gradeNumber'
).
value
;
const
status
=
document
.
getElementById
(
'status'
);
if
(
!
grade
||
!
file
)
{
alert
(
"Please fill all fields"
);
return
;
}
const
formData
=
new
FormData
();
formData
.
append
(
'file'
,
file
);
formData
.
append
(
'grade'
,
grade
);
status
.
style
.
display
=
'block'
;
status
.
textContent
=
'Uploading...'
;
status
.
className
=
'status'
;
try
{
const
response
=
await
fetch
(
'/reasoning/upload'
,
{
method
:
'POST'
,
body
:
formData
});
const
data
=
await
response
.
json
();
if
(
data
.
status
===
'success'
)
{
status
.
textContent
=
'✅ Success! Bank updated.'
;
status
.
className
=
'status success'
;
}
else
{
status
.
textContent
=
'❌ Upload failed. Check CSV headers.'
;
status
.
className
=
'status error'
;
}
}
catch
(
e
)
{
status
.
textContent
=
'❌ Server Error. Check if backend is running.'
;
status
.
className
=
'status error'
;
}
};
</script>
</body>
</html>
\ No newline at end of file
self_hosted_env/voice_agent/static/reasoning_interface.html
0 → 100644
View file @
215ea76f
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