Commit 215ea76f authored by salma's avatar salma

atom change and reasoning

parent 8b41479b
......@@ -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 dummy data for testing
-- Insert data (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("\nStudents table rows:")
cur.execute("SELECT * FROM students ORDER BY id;")
students = cur.fetchall()
for row in students:
print(row)
print("\nChat_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
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
......@@ -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
......
......@@ -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
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
......@@ -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
......@@ -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" بيشرح جوّه مَنَصّة "شارِع العُلوم".
"""
}
......@@ -22,7 +22,7 @@ GENERAL_CHAT_CONTEXTS: Dict[StudentNationality, str] = {
خَليك بتِرُد بالعاميّة المَصري، وبطريقة بسيطة وودودة.
- لو الطِّفل سأل: "إنت مين؟" → رد بالهوية المخصصة ليك (أنا عَنان...).
- لو الطِّفل سأل: "إنت مين؟" → رد بالهوية المخصصة ليك (أنا Atom...).
- لو الطِّفل سأل: "أنا مين؟" أو "إنت عارف أنا مين؟" → رد باستخدام بيانات الطالب اللي فوق (الاسم + الصف)، مثلاً:
"أيوه طبعًا، إنت/أنتِ (اسم الطالب بالعربي) في سنة (سنة الطالب بالعربي). عايز نكمّل النهارده في موضوع معين في العلوم؟"
- لو السُّؤال له علاقة بالعلوم أو بالمنهج → جاوب عليه.
......@@ -41,7 +41,7 @@ GENERAL_CHAT_CONTEXTS: Dict[StudentNationality, str] = {
السؤال: "{query}"
- لو الطِّفل سأل: "إنت مين؟" → رد بالهوية المخصصة ليك (أنا عَنان...).
- لو الطِّفل سأل: "إنت مين؟" → رد بالهوية المخصصة ليك (أنا Atom...).
- لو الطِّفل سأل: "أنا مين؟" أو "إنت عارف أنا مين؟" → رد باستخدام بيانات الطالب اللي فوق (الاسم + الصف)، مثلاً:
"أيوه طبعًا، إنت/أنتِ (اسم الطالب بالعربي) في سنة (سنة الطالب بالعربي). عايز نكمّل النهارده في موضوع معين في العلوم؟"
- لو السُّؤال له علاقة بالعلوم أو بالمنهج → جاوب عليه.
......
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
......@@ -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.")
......
......@@ -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
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
......@@ -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');
......
......@@ -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
<!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
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>تحدي علل مع أتوم</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f0f2f5; margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.container { background: white; width: 95%; max-width: 600px; padding: 30px; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); text-align: center; }
h1 { color: #6f42c1; margin-bottom: 20px; }
.card { background: #f8f9fa; border: 2px solid #e9ecef; padding: 20px; border-radius: 15px; margin: 20px 0; min-height: 80px; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: bold; color: #333; line-height: 1.6; }
/* شريط الصوت (تم تعديله ليعمل مع أكثر من مشغل) */
.audio-container { margin: 15px 0; display: none; background: #eee; padding: 10px; border-radius: 50px; }
audio { width: 100%; height: 35px; outline: none; }
/* الأزرار */
.btn { border: none; padding: 15px 30px; border-radius: 50px; font-size: 18px; font-weight: bold; cursor: pointer; transition: 0.3s; width: 100%; margin: 10px 0; }
.btn-primary { background: #6f42c1; color: white; }
.btn-record { background: #dc3545; color: white; user-select: none; -webkit-tap-highlight-color: transparent; }
.btn-record.recording { animation: pulse 1.5s infinite; background: #c82333; }
.btn-next { background: #28a745; color: white; }
/* منطقة التغذية الراجعة */
.feedback-area { text-align: right; border-top: 2px solid #eee; padding-top: 20px; margin-top: 20px; display: none; }
.student-transcription { font-style: italic; color: #555; background: #fff3cd; padding: 12px; border-radius: 10px; margin-bottom: 15px; font-size: 16px; border-right: 5px solid #ffc107; text-align: right; }
#atomFeedback { color: #6f42c1; font-weight: bold; font-size: 18px; margin-bottom: 15px; text-align: center; }
/* رسائل الخطأ */
.error-msg { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 10px; border: 1px solid #f5c6cb; margin: 10px 0; display: none; font-weight: bold; }
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7); } 70% { box-shadow: 0 0 0 15px rgba(220, 53, 69, 0); } 100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); } }
#setupView, #questionView { display: none; }
.active { display: block !important; }
input[type="text"] { width: 100%; padding: 15px; border-radius: 10px; border: 2px solid #ddd; margin-bottom: 20px; text-align: center; font-size: 16px; box-sizing: border-box; }
input[type="text"]:focus { border-color: #6f42c1; outline: none; }
</style>
</head>
<body>
<div class="container">
<h1>تحدي أتوم (علل)</h1>
<!-- عرض رسالة الخطأ -->
<div id="errorAlert" class="error-msg"></div>
<!-- VIEW 1: SETUP -->
<div id="setupView" class="active">
<p>مستعد تختبر معلوماتك العلمية؟</p>
<input type="text" id="studentId" placeholder="أدخل رقم الطالب الخاص بك">
<button class="btn btn-primary" onclick="startChallenge()">ابدأ التحدي الآن!</button>
</div>
<!-- VIEW 2: CHALLENGE -->
<div id="questionView">
<div class="card" id="questionText">جاري تحميل السؤال...</div>
<!-- مشغل الصوت الخاص بالسؤال -->
<div id="questionAudioContainer" class="audio-container">
<audio id="questionAudio" controls></audio>
</div>
<div id="actionArea">
<button id="recordBtn" class="btn btn-record"
onmousedown="startRecording()" onmouseup="stopRecording()"
ontouchstart="startRecording()" ontouchend="stopRecording()">
اضغط واستمر في الضغط للإجابة 🎤
</button>
</div>
<!-- منطقة التقييم -->
<div id="feedbackArea" class="feedback-area">
<div class="student-transcription" id="studentSaid"></div>
<div id="atomFeedback"></div>
<!-- مشغل الصوت الخاص بالتقييم -->
<div id="feedbackAudioContainer" class="audio-container">
<audio id="feedbackAudio" controls></audio>
</div>
<button class="btn btn-next" onclick="startChallenge()">السؤال التالي 🚀</button>
</div>
</div>
</div>
<script>
let mediaRecorder;
let audioChunks = [];
function decodeText(base64Str) {
try {
if (!base64Str) return "";
const binaryString = window.atob(base64Str);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return new TextDecoder("utf-8").decode(bytes);
} catch (e) {
console.error("Decoding error:", e);
return "خطأ في معالجة النص";
}
}
function showError(msg) {
const errorAlert = document.getElementById('errorAlert');
errorAlert.textContent = msg;
errorAlert.style.display = 'block';
setTimeout(() => { errorAlert.style.display = 'none'; }, 5000);
}
// دالة محسنة لتشغيل الصوت في أي مشغل نحدده
function playAudio(blob, audioId, containerId) {
const url = URL.createObjectURL(blob);
const player = document.getElementById(audioId);
player.src = url;
document.getElementById(containerId).style.display = 'block';
player.play();
}
async function startChallenge() {
const studentId = document.getElementById('studentId').value;
if(!studentId) { alert("من فضلك أدخل رقم الطالب"); return; }
document.getElementById('setupView').classList.remove('active');
document.getElementById('questionView').classList.add('active');
// إخفاء مناطق الصوت والتقييم القديمة
document.getElementById('feedbackArea').style.display = 'none';
document.getElementById('questionAudioContainer').style.display = 'none';
document.getElementById('feedbackAudioContainer').style.display = 'none';
// إيقاف أي صوت شغال حالياً
document.getElementById('questionAudio').pause();
document.getElementById('feedbackAudio').pause();
document.getElementById('questionText').textContent = "أتوم يختار سؤالاً...";
const formData = new FormData();
formData.append('student_id', studentId);
try {
const response = await fetch('/reasoning/ask', { method: 'POST', body: formData });
if (!response.ok) {
const errData = await response.json();
throw new Error(errData.detail || "فشل في جلب السؤال");
}
const text = decodeText(response.headers.get('X-Response-Text'));
document.getElementById('questionText').textContent = text;
const blob = await response.blob();
// تشغيل صوت السؤال في المشغل الأول
playAudio(blob, 'questionAudio', 'questionAudioContainer');
} catch (e) {
showError("❌ " + e.message);
document.getElementById('setupView').classList.add('active');
document.getElementById('questionView').classList.remove('active');
}
}
async function startRecording() {
audioChunks = [];
try {
// إيقاف صوت السؤال إذا كان يعمل أثناء بدء التسجيل
document.getElementById('questionAudio').pause();
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
let options = {};
if (MediaRecorder.isTypeSupported('audio/webm')) {
options = { mimeType: 'audio/webm' };
} else if (MediaRecorder.isTypeSupported('audio/mp4')) {
options = { mimeType: 'audio/mp4' };
}
mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) audioChunks.push(event.data);
};
mediaRecorder.onstop = sendAnswer;
mediaRecorder.start();
document.getElementById('recordBtn').classList.add('recording');
document.getElementById('recordBtn').textContent = "أتوم يسمعك... تحدث";
} catch (error) {
showError("لا يمكن الوصول للميكروفون");
}
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== "inactive") {
mediaRecorder.stop();
document.getElementById('recordBtn').classList.remove('recording');
document.getElementById('recordBtn').textContent = "جاري تحليل إجابتك...";
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
}
}
async function sendAnswer() {
let actualMimeType = mediaRecorder.mimeType;
if (!actualMimeType) actualMimeType = 'audio/mp4';
const audioBlob = new Blob(audioChunks, { type: actualMimeType });
if (audioBlob.size === 0) {
showError("لم يتم تسجيل أي صوت. اضغط واستمر بالضغط للتحدث.");
document.getElementById('recordBtn').textContent = "اضغط واستمر في الضغط للإجابة 🎤";
return;
}
const studentId = document.getElementById('studentId').value;
let extension = 'webm';
if (actualMimeType.includes('mp4')) extension = 'm4a';
else if (actualMimeType.includes('ogg')) extension = 'ogg';
else if (actualMimeType.includes('wav')) extension = 'wav';
const formData = new FormData();
formData.append('student_id', studentId);
formData.append('file', audioBlob, `answer.${extension}`);
try {
const response = await fetch('/reasoning/evaluate', { method: 'POST', body: formData });
if (!response.ok) {
const errData = await response.json();
throw new Error(errData.detail || "فشل في تقييم الإجابة");
}
const feedbackText = decodeText(response.headers.get('X-Response-Text'));
const studentSaid = decodeText(response.headers.get('X-Student-Said'));
document.getElementById('studentSaid').textContent = "إنت قلت: " + studentSaid;
document.getElementById('atomFeedback').textContent = feedbackText;
document.getElementById('feedbackArea').style.display = 'block';
document.getElementById('recordBtn').textContent = "اضغط واستمر في الضغط للإجابة 🎤";
const blob = await response.blob();
// تشغيل صوت التقييم في المشغل الثاني الخاص بالتقييم
playAudio(blob, 'feedbackAudio', 'feedbackAudioContainer');
} catch (e) {
showError("❌ " + e.message);
document.getElementById('recordBtn').textContent = "اضغط واستمر في الضغط للإجابة 🎤";
}
}
</script>
</body>
</html>
\ 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