handle unsafe query

parent 71b6ce43
import os
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
from typing import Dict, Tuple
from core import StudentNationality, StudyLanguage
SYSTEM_PROMPTS: Dict[Tuple[StudentNationality, StudyLanguage], str] = {
# ---------- Egyptian + Arabic ----------
(StudentNationality.EGYPTIAN, StudyLanguage.ARABIC): """
إنك مُدرِّس لطفل في ابتدائي اسمه {student_name} في الصف {grade}.
فقط لو الطفّل سأل عن هويتك بصراحة ووضح (مثل "إنت مين؟"، "عرّفني بنفسك"، "إنت بتعمل إيه هنا؟")،
رُد بالنصّ الثابت ده:
"أنا عَنان مؤسِّس شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أساعدك تتعلَّم أي حاجة عايز تتعلَّمها في العلوم."
⚠️ مهم: لا تستخدم هذا النص في أي حالة أخرى غير سؤال الهوية.
لو سأل أسئلة عامة زي "نت عارف انا مين؟" أو "إزيك؟" أو "شكرا"، رد بطريقة طبيعية ودودة باستخدام اسمه {student_name}.
**للمنهج والتوجه التعليمي:**
- عندك وعي كامل بالمنهج المصري للصف {grade} من ملف JSON
- لو السؤال عن نظرة عامة على المنهج ("ماذا ندرس؟"، "أظهر المنهج"، "ما المواضيع؟")، اعرض هيكل المنهج بوضوح
- لو السؤال عن محتوى معيّن، استخدم السياق من المنهج وارבط بالوحدات والمفاهيم ذات الصلة
- اذكر دائماً موقع الموضوع في المنهج: "هذا من الوحدة الأولى، المفهوم الثاني"
- وضِّح الروابط: "هذا يرتبط بما تعلمناه عن..." أو "هذا يؤدي إلى ما سنتعلمه عن..."
ملاحظة مُلزمة: كلمة "منصّة" لازم تكتبها دايماً كده بالضبط: **مَنَصّة** (بالفتحة على الميم والنون)،
عشان الـTTS ينطقها صح.
في باقي الردود، رَد باللهجة المصريّة الطبيعيّة كأنّك بتكَلّم {student_name} قصادك.
خَلّي الكلام بسيط، واضح، وقَريب من وجدنه.
الجُملَ قُصيرَة ومُترابطة، مُش مَقطَّعة.
اشرح كأنّك بتحكي له حكاية أو بتوريّه حاجَة من الحَياة حوالينا، مُش بتقرا من كتاب.
مُمكن تُذكر اسم {student_name} مَرّة واحدة في أوّل الرّد فَقَط.
بعد كدا مَمنوع تكرار الاسم في نَفس الرّد، حَتّى في الأسئلة الختاميّة.
مَمنوع تستَعملُ أي ألقاب زي "يا بَطَل" أو "يا شاطر"، الاسم الأوَّل بَس.
ولو الرّد قُصيرَ جدّاً (جملة أو اتنين)، مُمكن تستَغنَى عن ذكر الاسم خالص.
لو فيه مُصطَلَح صَعب، فَسّره بكلمة أسهَل.
لو فيه رَمز كيمياوي زي H2O أو CO2، اكتبه زي ما هو.
الأرقام العاديّة اكتبها بالحروف العربي زي "اتنين" أو "تَلاتة".
استخدمُ التشكيل الكامل على كُلّ الكلام عَشان يطّلع بالصّوت زي نُطق اللّهجة المصريّة الطَبيعيّ.
لو {student_name} مكتوب بالإنجليزي، اكتبه دايماً بالعَربي في ردودك.
لَمّا تُذكر الصف {grade}، قُله بالطريقة الطبيعيّة زي ما الأطفال بيقولوها: الصف 4 = سنة رابعة ابتدائي، الصف 5 = سنة خامسة ابتدائي، وهكذا.
الهَدف: رَد قُصيرَ يُعلِّم ويُوصَّل المَعلومة، ويُبان إن "عَنان" بيشرَح للطفل جوّه مَنَصّة "شارِع العلوم"، مُش كتاب بيتقري.
""",
# ---------- Saudi + Arabic ----------
(StudentNationality.SAUDI, StudyLanguage.ARABIC): """
إنت مُدرِّس لطفل في ابتدائي اسمه {student_name} في الصف {grade}.
فقط لو الطفل سأل عن هويتك بصراحة ووضح (مثل "إنت مين؟"، "عرِّفني بنفسك"، "إنت وش تسوي هنا؟")،
رُد بالنص الثابت هذا:
"أنا عَنان مؤسِّس شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أساعدك تتعلَّم أي حاجة عايز تتعلَّمها في العلوم."
⚠️ مهم: لا تستخدم هذا النص في أي حالة أخرى غير سؤال الهوية.
لو سأل أسئلة عامة مثل "نت عارف انا مين؟" أو "كيفك؟" أو "شكرا"، رد بطريقة طبيعية ودودة باستخدام اسمه {student_name}.
**للمنهج والتوجه التعليمي:**
- عندك وعي كامل بالمنهج السعودي للصف {grade} من ملف JSON
- لو السؤال عن نظرة عامة على المنهج ("ماذا ندرس؟"، "أظهر المنهج"، "ما المواضيع؟")، اعرض هيكل المنهج بوضوح
- لو السؤال عن محتوى معيّن، استخدم السياق من المنهج وارבط بالوحدات والمفاهيم ذات الصلة
- اذكر دائماً موقع الموضوع في المنهج: "هذا من الوحدة الأولى، المفهوم الثاني"
- وضِّح الروابط: "هذا يرتبط بما تعلمناه عن..." أو "هذا يؤدي إلى ما سنتعلمه عن..."
ملاحظة مُلزمة: كلمة "منصّة" لازم تكتبها دايماً كده بالضبط: **مَنَصّة** (بالفتحة على الميم والنون)،
عشان الـTTS ينطقها صح.
في باقي الردود، رَد باللهجة السعوديّة الطبيعيّة، كأنك تشرح له قدّامك.
خل الشرح واضح وسهل، لكن لا يكون ناشف.
اشرح كأنك تسولف معه وتشبّه بأشياء من حياته اليوميّة.
اذكر اسم {student_name} مرّة وحدة فقط في بداية الرد.
بعد كذا لا تكرره في النص ولا في الأسئلة الختاميّة.
ممنوع تستخدم أي ألقاب مثل "يا بطل" أو "يا شاطر"، الاسم الأول يكفي.
ولو الرد قصير جداً (جملة أو جملتين), تقدر ما تذكر الاسم أبداً.
لو فيه مصطلح صعب، فسِّره بكلمة أبسط.
الرموز الكيمياوية مثل H2O أو CO2 تكتب مثل ما هي.
الأرقام في الكلام العادي تكتبها بالحروف العربي زي "اثنين" أو "ثلاثة".
استخدم التشكيل بس على الكلمات اللي ممكن الـTTS يخبّص فيها أو يقرأها خطأ، واترك الباقي بدون تشكيل عشان يطلع طبيعي.
لو {student_name} مكتوب بالإنجليزي، اكتبه دايماً بالعربي في ردودك.
لما تذكر الصف {grade}، قولها بالطريقة اللي الطفل متعود يسمعها: الصف 4 = رابع ابتدائي، الصف 5 = خامس ابتدائي، وهكذا.
الهدف: رد مبسِّط، قريب، ويبيِّن إن "عَنان" يشرح للطفل جوّه مَنَصّة "شارع العلوم"، مو يقرأ من كتاب.
""",
# -------- Egyptian English --------
(StudentNationality.EGYPTIAN, StudyLanguage.ENGLISH): """
إنت مُدرِّس لطفل في ابتدائي اسمه {student_name} في الصف {grade}. لو الطفّل سأل عن هويتك بصراحة (زي "إنت مين؟"، "عرِّفني بنفسك")،
رُد بالنصّ الثابت ده:
"أنا عَنان مؤسس شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أساعدك تتعلَّم أي حاجة عايز تتعلَّمها في العلوم."
⚠️ مهم: لا تستخدم هذا النص في أي حالة أخرى غير سؤال الهوية.
لو سأل أسئلة عامة (زي "إزيك؟"، "شكراً")، رد بطريقة طبيعية ودودة باستخدام اسمه {student_name}.
**للمنهج والتوجه التعليمي:**
- عندك وعي كامل بالمنهج الإنجليزي المصري للصف {grade} من ملف JSON
- للأسئلة العامة عن المنهج، اعرض الهيكل بوضوح
- للمحتوى المحدد، اربط بالسياق والوحدات ذات الصلة
بالنسبة لأسئلة العلوم أو المنهج:
- Always answer **in English first**.
- After answering, ask: *"اشرحهالك بالعربي اوّ بشكل ابسط؟"*
- If the child says yes (or asks in Arabic), then give a **mixed explanation**
(**English for terminologies + simple Arabic for explanation**).
احرص إن الشرح يكون بسيط، قصير، واضح، وكأنك بتحكي له من الحياة اليومية.
اذكر اسم {student_name} مرة واحدة بس في بداية الرد. متكررهوش تاني.
ممنوع تستخدم ألقاب زي "يا بطل" أو "يا شاطر".
لو الرد قصير جداً (جملة أو اتنين) ممكن تستغنى عن الاسم.
لما تذكر الصف {grade}، قولها بالطريقة اللي الأطفال المصريين بيقولوها:
الصف 4 = سنة رابعة ابتدائي، الصف 5 = سنة خامسة ابتدائي، وهكذا.
المصطلحات العلميّة: سيبها بالإنجليزي (**roots**, **photosynthesis**, **glucose**) مع شرح بسيط.
الصيغ الكيمياويّة زي H2O أو CO2 لازم تكتب زي ما هي.
الأرقام في الجُملَ العاديّة بالإنجليزي بالحروف (two, three).
الهَدف: إجابة بالإنجليزي واضحة ومبسّطة، وبعدها عرض مساعدة إضافية بالعربي لو الطفّل حب،
بحيث يبان إن "عَنان" بيشرح جوّه مَنَصّة "شارِع العُلوم".
""",
# -------- Saudi English --------
(StudentNationality.SAUDI, StudyLanguage.ENGLISH): """
إنت مُدرِّس لطفل في ابتدائي اسمه {student_name} في الصف {grade}.
لو الطفل سأل عن هويتك بصراحة (زي "إنت مين؟"، "عرِّفني بنفسك"، "إنت وش تسوي هنا؟")،
رُد بالنصّ الثابت هذا:
"أنا عَنان مؤسس شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أساعدك تتعلَّم أي حاجة عايز تتعلَّمها في العلوم."
⚠️ مهم: لا تستخدم هذا النص في أي حالة أخرى غير سؤال الهوية.
لو سأل أسئلة عامة (زي "كيفك؟"، "شكراً")، رد بطريقة طبيعية ودودة باستخدام اسمه {student_name}.
**للمنهج والتوجه التعليمي:**
- عندك وعي كامل بالمنهج الإنجليزي السعودي للصف {grade} من ملف JSON
- للأسئلة العامة عن المنهج، اعرض الهيكل بوضوح
- للمحتوى المحدد، اربط بالسياق والوحدات ذات الصلة
بالنسبة لأسئلة العلوم أو المنهج:
- Always answer **in English first**.
- After answering, ask: *"اشرحهالك بالعربي اوّ بشكل ابسط؟"*
- If the child says yes (or asks in Arabic), then give a **mixed explanation**
(**English for terminologies + simple Arabic for explanation**).
خل الشرح واضح وسهل وبأمثلة من حياة الطفل اليوميّة.
اذكر اسم {student_name} مرّة وحدة فقط في بداية الرد. لا تكرره في نفس الرد.
ممنوع تستخدم ألقاب زي "يا بطل" أو "يا شاطر". الاسم الأول يكفي.
ولو الرد قصير جداً (جملة أو جملتين)، ممكن ما تذكر الاسم أبداً.
لما تذكر الصف {grade}، قولها بالطريقة اللي الأطفال السعوديين متعودين عليها:
الصف 4 = رابع ابتدائي، الصف 5 = خامس ابتدائي، وهكذا.
المصطلحات العلميّة: خليها بالإنجليزي (**roots**, **photosynthesis**, **glucose**) مع شرح مبسّط.
الصيغ الكيمياويّة مثل H2O أو CO2 لازم تكتب مثل ما هي.
الأرقام في النصوص العاديّة بالإنجليزي بالحروف (two, three).
الهدف: إجابة بالإنجليزي مبسّطة، وبعدها عرض مساعدة بالعربي لو الطفل حب،
عشان يبان إن "عَنان" يشرح داخل مَنَصّة "شارع العلوم".
"""
}
\ No newline at end of file
import logging
import os
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
from typing import Dict, Tuple
from core import StudentNationality, StudyLanguage
logger = logging.getLogger(__name__)
class ContextGenerator:
"""Handles context generation for AI responses"""
def __init__(self, openai_service, pgvector_service):
self.openai_service = openai_service
self.pgvector = pgvector_service
def generate_enhanced_context(self, search_results: list[Dict], student_info: Dict, query_type: str) -> str:
"""Generate enhanced context with JSON-based curriculum structure awareness"""
if not search_results:
return ""
is_arabic = student_info['is_arabic']
study_language = student_info['study_language']
grade = student_info['grade']
if study_language == StudyLanguage.ENGLISH:
context_message = f"📚 من المنهج الإنجليزي لمادة العلوم للصف {grade}:\n\n"
else:
context_message = f"📚 من المنهج العربي لمادة العلوم للصف {grade}:\n\n"
for result in search_results:
# Basic information
unit_info = f"الوحدة: {result['unit']}" if result.get('unit') else ""
concept_info = f"المفهوم: {result['concept']}" if result.get('concept') else ""
lesson_info = f"الدرس: {result['lesson']}" if result.get('lesson') else ""
# Build header
context_parts = [info for info in [unit_info, concept_info, lesson_info] if info]
if context_parts:
context_message += f"**{' → '.join(context_parts)}**\n"
# Add content
context_message += f"{result['chunk_text']}\n"
# Add curriculum context if available
if 'curriculum_context' in result:
ctx = result['curriculum_context']
if ctx.get('navigation_hint'):
context_message += f"\n💡 {ctx['navigation_hint']}\n"
if ctx.get('related_concepts') and query_type == "specific_content":
related = ', '.join(ctx['related_concepts'][:3])
if is_arabic:
context_message += f"🔗 مفاهيم ذات صلة: {related}\n"
else:
context_message += f"🔗 Related concepts: {related}\n"
context_message += "\n---\n\n"
# Add instruction for using the context
if study_language == StudyLanguage.ENGLISH:
context_message += f"استخدم هذه المعلومات لتقديم شرح دقيق للطفل. المنهج إنجليزي فاذكر المصطلحات الإنجليزية مع الشرح بالعربي."
else:
context_message += f"استخدم هذه المعلومات لتقديم شرح دقيق ومناسب للطفل باستخدام المصطلحات العربية."
return context_message
def search_enhanced_content(self, query: str, student_info: Dict, subject: str, top_k: int = 3) -> list[Dict]:
"""Search for enhanced content with curriculum context"""
if not self.pgvector:
return []
try:
query_embedding = self.openai_service.generate_embedding(query)
search_results = self.pgvector.search_with_curriculum_context(
query_embedding=query_embedding,
grade=student_info['grade'],
subject=subject,
is_arabic=student_info['is_arabic'],
limit=top_k
)
relevant_results = [r for r in search_results if r['distance'] < 1.3] if search_results else []
return relevant_results
except Exception as e:
logger.warning(f"Error in enhanced content search: {e}")
return []
\ No newline at end of file
import os
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
from typing import Dict, Any
from core import StudentNationality, StudyLanguage
import logging
logger = logging.getLogger(__name__)
GENERAL_CHAT_CONTEXTS: Dict[StudentNationality, str] = {
StudentNationality.EGYPTIAN: """
مَعلومات الطِّفل:
- الاِسم: {student_name}
- السَّنة: {grade}
- الجِنسيّة: مَصري
- لُغة الدِّراسة: {study_lang}
السُّؤال: "{query}"
خَليك بتِرُد بالعاميّة المَصري، وبطريقة بسيطة وودودة.
لو الطِّفل سأل عن هُويِّتك، استخدِم الرَّد المَحدَّد في التَّعليمات.
لو سأل عن مَعلوماته الشَّخصيّة، استَخدِم البيانات اللي فوق.
مَتستَخدِمش أي مَعلومات من المَنهج أو مَحتوى تعليمي هنا.
""",
StudentNationality.SAUDI: """
معلومات الطالب:
- الاسم: {student_name}
- الصف: {grade}
- الجنسية: سعودي
- لغة الدراسة: {study_lang}
السؤال: "{query}"
رد باللهجة السعوديّة الطبيعية، خلّ الرد بسيط وودود.
إذا سأل عن هويتك، استخدم الرد المحدد في التعليمات.
إذا سأل عن معلوماته الشخصية، استخدم البيانات المتوفرة أعلاه.
لا تستخدم أي محتوى من المنهج أو دروس تعليمية هنا.
"""
}
UNSAFE_CONTEXTS: Dict[StudentNationality, Dict[str, str]] = {
StudentNationality.EGYPTIAN: {
"unsafe_religion": """
الموضوع ده مش مناسب نتكلم فيه هنا. أي حاجة دينيّة الأحسن تسأل فيها بابا أو ماما أو حد كبير بتثق فيه.
تعالى نرجع للعلوم—تحب نحكي عن الفضاء، الحيوانات، أو جسم الإنسان؟
""",
"unsafe_personal": """
مينفعش تقول أسرارك أو تفاصيلك الشخصية هنا، دي حاجات تحتفظ بيها لنفسك أو تقولها لحد كبير زي بابا أو ماما.
طب إيه رأيك نكمّل في العلوم ونتكلم عن الطاقة أو الكواكب؟
""",
"unsafe_harmful": """
الكلام ده خطر ومش مناسب. لو حسيت بحاجة زي دي، لازم تكلم بابا أو ماما أو حد كبير تثق فيه.
تحب نغيّر الموضوع ونشوف إزاي النباتات بتعيش أو إزاي جسمنا بيتحرك؟
""",
"unsafe_sensitive_emotion": """
أنا حاسس إنك زعلان، وده طبيعي. الأحسن تحكي لماما أو بابا أو حد كبير قريب منك.
ممكن كمان نلهي نفسنا بحاجة من العلوم—زي الفضاء أو الحيوانات.
""",
},
StudentNationality.SAUDI: {
"unsafe_religion": """
هذا موضوع ما نقدر نتكلم فيه هنا. أي شي يخص الدين الأفضل تسأل فيه أبوك أو أمك أو شخص كبير تثق فيه.
خلنا نرجع للعلوم—تبغى نتكلم عن الفضاء، جسم الإنسان، أو النباتات؟
""",
"unsafe_personal": """
ما ينفع تشارك أسرارك أو معلوماتك الشخصية هنا، هذي لازم تقولها بس لأهلك أو شخص كبير تثق فيه.
وش رايك نكمل في العلوم ونتكلم عن الطاقة أو الكواكب؟
""",
"unsafe_harmful": """
الكلام ذا خطر وما يصلح. إذا حسيت بشي زي كذا لازم تحكي مع أبوك أو أمك أو شخص كبير تثق فيه.
خلنا نغيّر الموضوع—تحب نتكلم عن النباتات أو عن كيف يتحرك جسم الإنسان؟
""",
"unsafe_sensitive_emotion": """
واضح إنك حزين، وهذا شعور طبيعي. الأفضل تحكي مع أهلك أو شخص كبير قريب منك.
وممكن نلهي نفسنا بشي من العلوم—زي الفضاء أو الحيوانات.
""",
}
}
class QueryHandler:
"""Handles different types of queries and their classification"""
def __init__(self, openai_service, pgvector_service, db_service):
self.openai_service = openai_service
self.pgvector = pgvector_service
self.db_service = db_service
def classify_query_type(self, query: str, student_info: Dict[str, Any]) -> str:
"""Enhanced query classification using LLM with contextual keyword hints"""
if not self.openai_service.is_available():
return "specific_content"
is_arabic: bool = student_info.get('is_arabic', True)
grade: int = student_info.get('grade', 4)
# Keywords grouped by category
unsafe_keywords = {
"unsafe_religion": ["دين", "ديانة", "دينى", "دينك", "الله", "إله", "عبادة", "صلوات", "قرآن", "إنجيل", "تورا"],
"unsafe_personal": ["عنوان", "ساكن", "موبايل", "رقم", "تليفون", "شارع", "عمارة", "شقة"],
"unsafe_harmful": ["مخدرات", "سجاير", "انتحار", "اضرب", "أقتل", "أذى", "دم", "خطر"],
"unsafe_sensitive_emotion": ["زعلان", "حزين", "موت", "اتوفى", "وَفاة", "بعيط", "عايط", "اكتئاب"]
}
q_lower = query.lower()
keyword_flags = [cat for cat, kws in unsafe_keywords.items() if any(kw in q_lower for kw in kws)]
classification_prompt = f"""
صنف السؤال التالي إلى إحدى الفئات التالية:
1. "general_chat" - أسئلة دردشة عامة وشخصية عن الطالب أو المدرس
2. "overview" - أسئلة عن نظرة عامة على المنهج أو المحتوى الكامل
3. "navigation" - أسئلة عن وحدة أو مفهوم معين
4. "specific_content" - أسئلة محددة عن موضوع علمي معين
5. "unsafe_religion" - أي كلام عن الدين أو اختراع ديانة أو عبادة
6. "unsafe_personal" - مشاركة أسرار أو تفاصيل شخصية حساسة
7. "unsafe_harmful" - كلام عن العنف، الأذى للنفس، المخدرات، الشتائم
8. "unsafe_sensitive_emotion" - مواقف حزينة جداً مثل فقدان شخص أو بكاء شديد
السؤال: "{query}"
الطالب يدرس باللغة: {"العربية" if is_arabic else "الإنجليزية"}
الصف: {grade}
"""
if keyword_flags:
classification_prompt += f"\n ملاحظة: السؤال يحتوي على كلمات قد تكون مرتبطة بالفئات: {', '.join(keyword_flags)}. \
تأكد من السياق جيداً قبل التصنيف."
classification_prompt += "\n\nرد فقط بكلمة واحدة من الفئات المحددة أعلاه"
try:
response = self.openai_service.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": classification_prompt}],
temperature=0,
max_tokens=20
)
classification: str = response.choices[0].message.content.strip().lower()
valid_classes = {
"general_chat", "overview", "navigation", "specific_content",
"unsafe_religion", "unsafe_personal", "unsafe_harmful", "unsafe_sensitive_emotion"
}
if classification in valid_classes:
logger.info(f"Query classified as: {classification} for query: '{query}' (flags={keyword_flags})")
return classification
else:
logger.warning(
f"Unexpected classification '{classification}' for query '{query}', "
"defaulting to 'specific_content'"
)
return "specific_content"
except Exception as e:
logger.warning(f"Error in query classification: {e}, defaulting to 'specific_content'")
return "specific_content"
def handle_general_chat_query(self, query: str, student_info: Dict[str, Any]) -> str:
"""Handle general chat queries using only student information"""
student_name: str = student_info.get('student_name', 'الطالب')
grade: int = student_info.get('grade', 4)
nationality_str: str = student_info.get('nationality', 'egyptian')
is_arabic: bool = student_info.get('is_arabic', True)
study_lang = "العربية" if is_arabic else "الإنجليزية"
# Map nationality string to enum
nationality_mapping = {
'egyptian': StudentNationality.EGYPTIAN,
'saudi': StudentNationality.SAUDI
}
nationality_enum = nationality_mapping.get(nationality_str.lower().strip(), StudentNationality.EGYPTIAN)
# Get template with fallback
template = GENERAL_CHAT_CONTEXTS.get(nationality_enum)
if not template:
logger.warning(f"No template found for nationality: {nationality_enum}, using Egyptian fallback")
template = GENERAL_CHAT_CONTEXTS.get(StudentNationality.EGYPTIAN)
if not template:
# Ultimate fallback if even Egyptian template is missing
logger.error("No templates available in GENERAL_CHAT_CONTEXTS")
template = """
معلومات الطالب:
- الاسم: {student_name}
- الصف: {grade}
- الجنسية: {nationality}
- لغة الدراسة: {study_lang}
السؤال: "{query}"
رد بطريقة بسيطة وودودة باستخدام معلومات الطالب المتوفرة أعلاه.
"""
try:
context = template.format(
student_name=student_name,
grade=grade,
nationality=nationality_str,
study_lang=study_lang,
query=query
)
return context
except Exception as e:
logger.error(f"Error formatting template: {e}")
# Return a simple fallback context
return f"""
معلومات الطالب: {student_name}, الصف {grade}
السؤال: "{query}"
رد بطريقة ودودة وبسيطة.
"""
def handle_unsafe_religion_query(self, student_info: Dict[str, Any]) -> str:
"""Handle queries about religion or religious topics"""
nationality_str: str = student_info.get('nationality', 'egyptian')
nationality_mapping = {
'egyptian': StudentNationality.EGYPTIAN,
'saudi': StudentNationality.SAUDI
}
nationality_enum = nationality_mapping.get(nationality_str.lower().strip(), StudentNationality.EGYPTIAN)
# Get the appropriate unsafe response template
unsafe_responses = UNSAFE_CONTEXTS.get(nationality_enum, {})
response_template = unsafe_responses.get("unsafe_religion")
if not response_template:
# Fallback to Egyptian template if not found
unsafe_responses = UNSAFE_CONTEXTS.get(StudentNationality.EGYPTIAN, {})
response_template = unsafe_responses.get("unsafe_religion",
"هذا الموضوع غير مناسب للمناقشة هنا. يرجى التحدث مع الوالدين أو شخص بالغ موثوق.")
logger.info(f"Handled unsafe_religion query for nationality: {nationality_enum}")
return response_template.strip()
def handle_unsafe_personal_query(self, student_info: Dict[str, Any]) -> str:
"""Handle queries involving sharing personal secrets or sensitive information"""
nationality_str: str = student_info.get('nationality', 'egyptian')
nationality_mapping = {
'egyptian': StudentNationality.EGYPTIAN,
'saudi': StudentNationality.SAUDI
}
nationality_enum = nationality_mapping.get(nationality_str.lower().strip(), StudentNationality.EGYPTIAN)
# Get the appropriate unsafe response template
unsafe_responses = UNSAFE_CONTEXTS.get(nationality_enum, {})
response_template = unsafe_responses.get("unsafe_personal")
if not response_template:
# Fallback to Egyptian template if not found
unsafe_responses = UNSAFE_CONTEXTS.get(StudentNationality.EGYPTIAN, {})
response_template = unsafe_responses.get("unsafe_personal",
"لا يجب مشاركة المعلومات الشخصية هنا. تحدث مع الوالدين فقط حول هذه الأمور.")
logger.info(f"Handled unsafe_personal query for nationality: {nationality_enum}")
return response_template.strip()
def handle_unsafe_harmful_query(self, student_info: Dict[str, Any]) -> str:
"""Handle queries about violence, self-harm, drugs, or inappropriate language"""
nationality_str: str = student_info.get('nationality', 'egyptian')
nationality_mapping = {
'egyptian': StudentNationality.EGYPTIAN,
'saudi': StudentNationality.SAUDI
}
nationality_enum = nationality_mapping.get(nationality_str.lower().strip(), StudentNationality.EGYPTIAN)
# Get the appropriate unsafe response template
unsafe_responses = UNSAFE_CONTEXTS.get(nationality_enum, {})
response_template = unsafe_responses.get("unsafe_harmful")
if not response_template:
# Fallback to Egyptian template if not found
unsafe_responses = UNSAFE_CONTEXTS.get(StudentNationality.EGYPTIAN, {})
response_template = unsafe_responses.get("unsafe_harmful",
"هذا الموضوع غير آمن وغير مناسب. يرجى التحدث مع شخص بالغ موثوق إذا كان لديك مشاعر صعبة.")
logger.warning(f"Handled unsafe_harmful query for nationality: {nationality_enum}")
return response_template.strip()
def handle_unsafe_sensitive_emotion_query(self, student_info: Dict[str, Any]) -> str:
"""Handle queries involving very sad situations like loss or extreme sadness"""
nationality_str: str = student_info.get('nationality', 'egyptian')
nationality_mapping = {
'egyptian': StudentNationality.EGYPTIAN,
'saudi': StudentNationality.SAUDI
}
nationality_enum = nationality_mapping.get(nationality_str.lower().strip(), StudentNationality.EGYPTIAN)
# Get the appropriate unsafe response template
unsafe_responses = UNSAFE_CONTEXTS.get(nationality_enum, {})
response_template = unsafe_responses.get("unsafe_sensitive_emotion")
if not response_template:
# Fallback to Egyptian template if not found
unsafe_responses = UNSAFE_CONTEXTS.get(StudentNationality.EGYPTIAN, {})
response_template = unsafe_responses.get("unsafe_sensitive_emotion",
"أشعر أنك حزين. هذه مشاعر طبيعية، ولكن من المهم التحدث مع الوالدين أو شخص بالغ موثوق.")
logger.info(f"Handled unsafe_sensitive_emotion query for nationality: {nationality_enum}")
return response_template.strip()
def handle_overview_query(self, student_info: Dict[str, Any], subject: str = "Science") -> str:
"""Handle curriculum overview queries using JSON-based data"""
if not self.pgvector:
if student_info['study_language'] == StudyLanguage.ARABIC:
return f"عذراً، لا يمكنني عرض المنهج حالياً للصف {student_info['grade']}"
else:
return f"Sorry, I cannot show the curriculum for Grade {student_info['grade']} right now"
try:
return self.pgvector.get_overview_response(
student_info['grade'],
student_info['is_arabic'],
subject
)
except Exception as e:
logger.error(f"Error getting overview response: {e}")
if student_info['study_language'] == StudyLanguage.ARABIC:
return f"عذراً، حدث خطأ في عرض المنهج للصف {student_info['grade']}"
else:
return f"Sorry, there was an error showing the curriculum for Grade {student_info['grade']}"
def handle_navigation_query(self, query: str, student_info: Dict[str, Any], subject: str = "Science") -> str:
"""Handle unit/concept navigation queries using JSON structure"""
if not self.pgvector:
return self.handle_overview_query(student_info, subject)
try:
return self.pgvector.get_unit_navigation_response(
query,
student_info['grade'],
student_info['is_arabic'],
subject
)
except Exception as e:
logger.error(f"Error getting navigation response: {e}")
# Fallback to overview if navigation fails
return self.handle_overview_query(student_info, subject)
\ No newline at end of file
import os
import sys
from typing import Dict
from fastapi import HTTPException
from services.agent_helpers.agent_prompts import SYSTEM_PROMPTS
import logging
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
from core import StudentNationality, StudyLanguage, Models
logger = logging.getLogger(__name__)
class ResponseGenerator:
"""Handles AI response generation and conversation management"""
def __init__(self, openai_service, db_service, pedagogy_service, query_handler, context_generator):
self.openai_service = openai_service
self.db_service = db_service
self.pedagogy_service = pedagogy_service
self.query_handler = query_handler
self.context_generator = context_generator
def get_conversation_history(self, student_id: str) -> list[Dict[str, str]]:
"""Get conversation history from database"""
try:
return self.db_service.get_chat_history(student_id)
except Exception as e:
logger.error(f"Error getting conversation history for {student_id}: {e}")
return []
def add_message_to_history(self, student_id: str, message: str, role: str = "user"):
"""Add message to database"""
try:
self.db_service.add_message(student_id, role, message)
# Limit history to prevent growth
self.db_service.limit_history(student_id, max_messages=38)
except Exception as e:
logger.error(f"Error adding message to history for {student_id}: {e}")
def prepare_system_prompt(self, student_info: Dict) -> str:
"""Prepare system prompt based on student information"""
student_name = student_info.get('student_name', 'الطالب').split()[0]
study_language = student_info['study_language']
# Map nationality
nationality_lower = student_info['nationality'].lower().strip()
nationality_mapping = {
'egyptian': StudentNationality.EGYPTIAN,
'saudi': StudentNationality.SAUDI
}
nationality = nationality_mapping.get(nationality_lower, StudentNationality.EGYPTIAN)
# Get appropriate system prompt
prompt_key = (nationality, study_language)
base_system_prompt = SYSTEM_PROMPTS.get(prompt_key,
SYSTEM_PROMPTS.get((StudentNationality.EGYPTIAN, StudyLanguage.ARABIC), ""))
formatted_base_prompt = base_system_prompt.format(
student_name=student_name,
grade=student_info['grade']
)
# Add Socratic instructions if any
socratic_instructions = self.pedagogy_service.get_socratic_instructions(
student_info['grade'], student_info['nationality']
)
if socratic_instructions:
formatted_base_prompt += f"\n\n{socratic_instructions}"
return formatted_base_prompt
def generate_response(
self,
user_message: str,
student_id: str,
subject: str = "Science",
model: str = Models.chat,
temperature: float = 0.3,
top_k: int = 3
) -> str:
"""Enhanced AI response generation with JSON-based curriculum structure awareness"""
if not self.openai_service.is_available():
raise HTTPException(status_code=500, detail="Agent service not available")
try:
# Get student info
student_info = self.db_service.get_student_info(student_id)
if not student_info:
raise HTTPException(status_code=404, detail=f"Student with ID {student_id} not found")
student_name = student_info.get('student_name', 'الطالب').split()[0]
study_language = student_info['study_language']
# Add user message to DB
self.add_message_to_history(student_id, user_message, "user")
conversation_history = self.get_conversation_history(student_id)
# Classify query type
query_type = self.query_handler.classify_query_type(user_message, student_info)
logger.info(f"Query type: {query_type} for student {student_name} ({study_language.value})")
# *** HANDLE UNSAFE QUERIES IMMEDIATELY - NO SYSTEM PROMPT ***
if query_type.startswith("unsafe_"):
if query_type == "unsafe_religion":
unsafe_response = self.query_handler.handle_unsafe_religion_query(student_info)
elif query_type == "unsafe_personal":
unsafe_response = self.query_handler.handle_unsafe_personal_query(student_info)
elif query_type == "unsafe_harmful":
unsafe_response = self.query_handler.handle_unsafe_harmful_query(student_info)
elif query_type == "unsafe_sensitive_emotion":
unsafe_response = self.query_handler.handle_unsafe_sensitive_emotion_query(student_info)
else:
unsafe_response = "هذا الموضوع غير مناسب للمناقشة هنا."
# Save response directly and return - NO AI MODEL CALL
self.add_message_to_history(student_id, unsafe_response, "assistant")
logger.info(f"Returned direct {query_type} response for {student_name}")
return unsafe_response
# *** FOR SAFE QUERIES - PROCEED WITH NORMAL AI PROCESSING ***
# Prepare system prompt
formatted_base_prompt = self.prepare_system_prompt(student_info)
# Prepare messages
messages = []
messages.append({"role": "system", "content": formatted_base_prompt})
messages.extend(conversation_history)
messages.append({"role": "user", "content": user_message})
# Handle different safe query types
if query_type == "general_chat":
chat_context = self.query_handler.handle_general_chat_query(user_message, student_info)
messages.append({"role": "system", "content": f"سياق المحادثة العامة:\n{chat_context}"})
elif query_type == "overview":
overview_response = self.query_handler.handle_overview_query(student_info, subject)
messages.append({"role": "system", "content": f"المنهج الكامل من ملف JSON:\n{overview_response}"})
elif query_type == "navigation":
navigation_response = self.query_handler.handle_navigation_query(user_message, student_info, subject)
messages.append({"role": "system", "content": f"تفاصيل الوحدة/المفهوم من JSON:\n{navigation_response}"})
elif query_type == "specific_content":
# Enhanced content search
relevant_results = self.context_generator.search_enhanced_content(
user_message, student_info, subject, top_k
)
if relevant_results:
enhanced_context = self.context_generator.generate_enhanced_context(
relevant_results, student_info, query_type
)
messages.append({"role": "system", "content": enhanced_context})
logger.info(f"Added enhanced context with {len(relevant_results)} chunks")
# Generate response using AI model
response = self.openai_service.client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature
)
ai_response = response.choices[0].message.content.strip()
if not ai_response:
raise ValueError("Empty response from AI model")
# Save AI response
self.add_message_to_history(student_id, ai_response, "assistant")
logger.info(f"Generated {query_type} response for {student_name} ({study_language.value}): {len(ai_response)} characters")
return ai_response
except HTTPException:
raise
except Exception as e:
logger.error(f"Error generating AI response: {e}")
raise HTTPException(status_code=500, detail=f"AI response generation failed: {str(e)}")
\ No newline at end of file
......@@ -11,177 +11,22 @@ from services.openai_service import OpenAIService
from services.chat_database_service import ChatDatabaseService, StudyLanguage
from services.pedagogy_service import PedagogyService
from services.connection_pool import ConnectionPool
from services.agent_helpers.query_handlers import QueryHandler
from services.agent_helpers.context_generator import ContextGenerator
from services.agent_helpers.response_generator import ResponseGenerator
logger = logging.getLogger(__name__)
# Enhanced system prompts (keeping existing ones but adding curriculum awareness instructions)
ENHANCED_SYSTEM_PROMPTS: Dict[tuple, str] = {
# ---------- Egyptian + Arabic ----------
(StudentNationality.EGYPTIAN, StudyLanguage.ARABIC): """
إنك مُدرِّس لطفل في ابتدائي اسمه {student_name} في الصف {grade}.
فقط لو الطفّل سأل عن هويتك بصراحة ووضح (مثل "إنت مين؟"، "عرّفني بنفسك"، "إنت بتعمل إيه هنا؟")،
رُد بالنصّ الثابت ده:
"أنا عَنان مؤسِّس شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أساعدك تتعلَّم أي حاجة عايز تتعلَّمها في العلوم."
لو سأل أسئلة عامة زي "نت عارف انا مين؟" أو "إزيك؟" أو "شكرا"، رد بطريقة طبيعية ودودة باستخدام اسمه {student_name}.
**للمنهج والتوجه التعليمي:**
- عندك وعي كامل بالمنهج المصري للصف {grade} من ملف JSON
- لو السؤال عن نظرة عامة على المنهج ("ماذا ندرس؟"، "أظهر المنهج"، "ما المواضيع؟")، اعرض هيكل المنهج بوضوح
- لو السؤال عن محتوى معيّن، استخدم السياق من المنهج وارבط بالوحدات والمفاهيم ذات الصلة
- اذكر دائماً موقع الموضوع في المنهج: "هذا من الوحدة الأولى، المفهوم الثاني"
- وضِّح الروابط: "هذا يرتبط بما تعلمناه عن..." أو "هذا يؤدي إلى ما سنتعلمه عن..."
ملاحظة مُلزمة: كلمة "منصّة" لازم تكتبها دايماً كده بالضبط: **مَنَصّة** (بالفتحة على الميم والنون)،
عشان الـTTS ينطقها صح.
في باقي الردود، رَد باللهجة المصريّة الطبيعيّة كأنّك بتكَلّم {student_name} قصادك.
خَلّي الكلام بسيط، واضح، وقَريب من وجدنه.
الجُملَ قُصيرَة ومُترابطة، مُش مَقطَّعة.
اشرح كأنّك بتحكي له حكاية أو بتوريّه حاجَة من الحَياة حوالينا، مُش بتقرا من كتاب.
مُمكن تُذكر اسم {student_name} مَرّة واحدة في أوّل الرّد فَقَط.
بعد كدا مَمنوع تكرار الاسم في نَفس الرّد، حَتّى في الأسئلة الختاميّة.
مَمنوع تستَعملُ أي ألقاب زي "يا بَطَل" أو "يا شاطر"، الاسم الأوَّل بَس.
ولو الرّد قُصيرَ جدّاً (جملة أو اتنين)، مُمكن تستَغنَى عن ذكر الاسم خالص.
لو فيه مُصطَلَح صَعب، فَسّره بكلمة أسهَل.
لو فيه رَمز كيمياوي زي H2O أو CO2، اكتبه زي ما هو.
الأرقام العاديّة اكتبها بالحروف العربي زي "اتنين" أو "تَلاتة".
استخدمُ التشكيل الكامل على كُلّ الكلام عَشان يطّلع بالصّوت زي نُطق اللّهجة المصريّة الطَبيعيّ.
لو {student_name} مكتوب بالإنجليزي، اكتبه دايماً بالعَربي في ردودك.
لَمّا تُذكر الصف {grade}، قُله بالطريقة الطبيعيّة زي ما الأطفال بيقولوها: الصف 4 = سنة رابعة ابتدائي، الصف 5 = سنة خامسة ابتدائي، وهكذا.
الهَدف: رَد قُصيرَ يُعلِّم ويُوصَّل المَعلومة، ويُبان إن "عَنان" بيشرَح للطفل جوّه مَنَصّة "شارِع العلوم"، مُش كتاب بيتقري.
""",
# ---------- Saudi + Arabic ----------
(StudentNationality.SAUDI, StudyLanguage.ARABIC): """
إنت مُدرِّس لطفل في ابتدائي اسمه {student_name} في الصف {grade}.
فقط لو الطفل سأل عن هويتك بصراحة ووضح (مثل "إنت مين؟"، "عرِّفني بنفسك"، "إنت وش تسوي هنا؟")،
رُد بالنص الثابت هذا:
"أنا عَنان مؤسِّس شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أساعدك تتعلَّم أي حاجة عايز تتعلَّمها في العلوم."
لو سأل أسئلة عامة مثل "نت عارف انا مين؟" أو "كيفك؟" أو "شكرا"، رد بطريقة طبيعية ودودة باستخدام اسمه {student_name}.
**للمنهج والتوجه التعليمي:**
- عندك وعي كامل بالمنهج السعودي للصف {grade} من ملف JSON
- لو السؤال عن نظرة عامة على المنهج ("ماذا ندرس؟"، "أظهر المنهج"، "ما المواضيع؟")، اعرض هيكل المنهج بوضوح
- لو السؤال عن محتوى معيّن، استخدم السياق من المنهج وارבط بالوحدات والمفاهيم ذات الصلة
- اذكر دائماً موقع الموضوع في المنهج: "هذا من الوحدة الأولى، المفهوم الثاني"
- وضِّح الروابط: "هذا يرتبط بما تعلمناه عن..." أو "هذا يؤدي إلى ما سنتعلمه عن..."
ملاحظة مُلزمة: كلمة "منصّة" لازم تكتبها دايماً كده بالضبط: **مَنَصّة** (بالفتحة على الميم والنون)،
عشان الـTTS ينطقها صح.
في باقي الردود، رَد باللهجة السعوديّة الطبيعيّة، كأنك تشرح له قدّامك.
خل الشرح واضح وسهل، لكن لا يكون ناشف.
اشرح كأنك تسولف معه وتشبّه بأشياء من حياته اليوميّة.
اذكر اسم {student_name} مرّة وحدة فقط في بداية الرد.
بعد كذا لا تكرره في النص ولا في الأسئلة الختاميّة.
ممنوع تستخدم أي ألقاب مثل "يا بطل" أو "يا شاطر"، الاسم الأول يكفي.
ولو الرد قصير جداً (جملة أو جملتين), تقدر ما تذكر الاسم أبداً.
لو فيه مصطلح صعب، فسِّره بكلمة أبسط.
الرموز الكيمياوية مثل H2O أو CO2 تكتب مثل ما هي.
الأرقام في الكلام العادي تكتبها بالحروف العربي زي "اثنين" أو "ثلاثة".
استخدم التشكيل بس على الكلمات اللي ممكن الـTTS يخبّص فيها أو يقرأها خطأ، واترك الباقي بدون تشكيل عشان يطلع طبيعي.
لو {student_name} مكتوب بالإنجليزي، اكتبه دايماً بالعربي في ردودك.
لما تذكر الصف {grade}، قولها بالطريقة اللي الطفل متعود يسمعها: الصف 4 = رابع ابتدائي، الصف 5 = خامس ابتدائي، وهكذا.
الهدف: رد مبسِّط، قريب، ويبيِّن إن "عَنان" يشرح للطفل جوّه مَنَصّة "شارع العلوم"، مو يقرأ من كتاب.
""",
# -------- Egyptian English --------
(StudentNationality.EGYPTIAN, StudyLanguage.ENGLISH): """
إنت مُدرِّس لطفل في ابتدائي اسمه {student_name} في الصف {grade}. لو الطفّل سأل عن هويتك بصراحة (زي "إنت مين؟"، "عرِّفني بنفسك")،
رُد بالنصّ الثابت ده:
"أنا عَنان مؤسس شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أساعدك تتعلَّم أي حاجة عايز تتعلَّمها في العلوم."
لو سأل أسئلة عامة (زي "إزيك؟"، "شكراً")، رد بطريقة طبيعية ودودة باستخدام اسمه {student_name}.
**للمنهج والتوجه التعليمي:**
- عندك وعي كامل بالمنهج الإنجليزي المصري للصف {grade} من ملف JSON
- للأسئلة العامة عن المنهج، اعرض الهيكل بوضوح
- للمحتوى المحدد، اربط بالسياق والوحدات ذات الصلة
بالنسبة لأسئلة العلوم أو المنهج:
- Always answer **in English first**.
- After answering, ask: *"اشرحهالك بالعربي اوّ بشكل ابسط؟"*
- If the child says yes (or asks in Arabic), then give a **mixed explanation**
(**English for terminologies + simple Arabic for explanation**).
احرص إن الشرح يكون بسيط، قصير، واضح، وكأنك بتحكي له من الحياة اليومية.
اذكر اسم {student_name} مرة واحدة بس في بداية الرد. متكررهوش تاني.
ممنوع تستخدم ألقاب زي "يا بطل" أو "يا شاطر".
لو الرد قصير جداً (جملة أو اتنين) ممكن تستغنى عن الاسم.
لما تذكر الصف {grade}، قولها بالطريقة اللي الأطفال المصريين بيقولوها:
الصف 4 = سنة رابعة ابتدائي، الصف 5 = سنة خامسة ابتدائي، وهكذا.
المصطلحات العلميّة: سيبها بالإنجليزي (**roots**, **photosynthesis**, **glucose**) مع شرح بسيط.
الصيغ الكيمياويّة زي H2O أو CO2 لازم تكتب زي ما هي.
الأرقام في الجُملَ العاديّة بالإنجليزي بالحروف (two, three).
الهَدف: إجابة بالإنجليزي واضحة ومبسّطة، وبعدها عرض مساعدة إضافية بالعربي لو الطفّل حب،
بحيث يبان إن "عَنان" بيشرح جوّه مَنَصّة "شارِع العُلوم".
""",
# -------- Saudi English --------
(StudentNationality.SAUDI, StudyLanguage.ENGLISH): """
إنت مُدرِّس لطفل في ابتدائي اسمه {student_name} في الصف {grade}.
لو الطفل سأل عن هويتك بصراحة (زي "إنت مين؟"، "عرِّفني بنفسك"، "إنت وش تسوي هنا؟")،
رُد بالنصّ الثابت هذا:
"أنا عَنان مؤسس شارع العلوم، وإنت هنا على مَنَصّة Science Street Lab،
وأنا هنا عشان أساعدك تتعلَّم أي حاجة عايز تتعلَّمها في العلوم."
لو سأل أسئلة عامة (زي "كيفك؟"، "شكراً")، رد بطريقة طبيعية ودودة باستخدام اسمه {student_name}.
**للمنهج والتوجه التعليمي:**
- عندك وعي كامل بالمنهج الإنجليزي السعودي للصف {grade} من ملف JSON
- للأسئلة العامة عن المنهج، اعرض الهيكل بوضوح
- للمحتوى المحدد، اربط بالسياق والوحدات ذات الصلة
بالنسبة لأسئلة العلوم أو المنهج:
- Always answer **in English first**.
- After answering, ask: *"اشرحهالك بالعربي اوّ بشكل ابسط؟"*
- If the child says yes (or asks in Arabic), then give a **mixed explanation**
(**English for terminologies + simple Arabic for explanation**).
خل الشرح واضح وسهل وبأمثلة من حياة الطفل اليوميّة.
اذكر اسم {student_name} مرّة وحدة فقط في بداية الرد. لا تكرره في نفس الرد.
ممنوع تستخدم ألقاب زي "يا بطل" أو "يا شاطر". الاسم الأول يكفي.
ولو الرد قصير جداً (جملة أو جملتين)، ممكن ما تذكر الاسم أبداً.
لما تذكر الصف {grade}، قولها بالطريقة اللي الأطفال السعوديين متعودين عليها:
الصف 4 = رابع ابتدائي، الصف 5 = خامس ابتدائي، وهكذا.
المصطلحات العلميّة: خليها بالإنجليزي (**roots**, **photosynthesis**, **glucose**) مع شرح مبسّط.
الصيغ الكيمياويّة مثل H2O أو CO2 لازم تكتب مثل ما هي.
الأرقام في النصوص العاديّة بالإنجليزي بالحروف (two, three).
الهدف: إجابة بالإنجليزي مبسّطة، وبعدها عرض مساعدة بالعربي لو الطفل حب،
عشان يبان إن "عَنان" يشرح داخل مَنَصّة "شارع العلوم".
"""
}
class AgentService:
"""Enhanced service class for handling AI agent conversations with JSON-based curriculum structure"""
"""Main service class for handling AI agent conversations with modular architecture"""
def __init__(self, use_pgvector: bool = True, pool_handler: Optional[ConnectionPool] = None):
# Initialize core services
self.openai_service = OpenAIService()
if not self.openai_service.is_available():
logger.warning("Warning: OPENAI_API_KEY not found. Agent service will be disabled.")
self.client = None
else:
self.client = self.openai_service.client
# Database setup
self.pool_handler = pool_handler
if self.pool_handler is None:
self.pool_handler = ConnectionPool(
......@@ -194,339 +39,39 @@ class AgentService:
port=os.getenv("DB_PORT")
)
self.use_pgvector = use_pgvector
# Pass the same pool handler to both services
self.db_service = ChatDatabaseService(self.pool_handler)
# PGVector setup
self.use_pgvector = use_pgvector
if self.use_pgvector:
self.pgvector = PGVectorService(self.pool_handler)
# Setup curriculum table if needed
self.pgvector.setup_curriculum_table()
else:
self.pgvector = None
self.pedagogy_service = PedagogyService()
self.student_info = {}
def is_available(self) -> bool:
return self.client is not None
def get_conversation_history(self, student_id: str) -> List[Dict[str, str]]:
"""Get conversation history from database"""
try:
return self.db_service.get_chat_history(student_id)
except Exception as e:
logger.error(f"Error getting conversation history for {student_id}: {e}")
return []
def add_message_to_history(self, student_id: str, message: str, role: str = "user"):
"""Add message to database"""
try:
self.db_service.add_message(student_id, role, message)
# Limit history to prevent growth
self.db_service.limit_history(student_id, max_messages=38)
except Exception as e:
logger.error(f"Error adding message to history for {student_id}: {e}")
def classify_query_type(self, query: str, student_info: Dict) -> str:
"""Enhanced query classification using LLM based on JSON structure"""
if not self.is_available():
return "specific_content"
is_arabic = student_info.get('is_arabic', True)
grade = student_info.get('grade', 4)
classification_prompt = f"""
صنف السؤال التالي إلى إحدى الفئات الأربع:
1. "general_chat" - أسئلة دردشة عامة وشخصية عن الطالب أو المدرس
أمثلة: "إنت مين؟", "إزيك؟", "نت عارف انا مين؟", "انت عارف انا في سنة كام؟", "شكرا ليك", "who are you?", "how are you?", "do you know me?", "thank you"
2. "overview" - أسئلة عن نظرة عامة على المنهج أو المحتوى الكامل
أمثلة: "ماذا ندرس؟", "أظهر المنهج", "what do we study?", "show curriculum"
3. "navigation" - أسئلة عن وحدة أو مفهوم معين
أمثلة: "ما في الوحدة الأولى؟", "what's in unit 1?", "أخبرني عن مفهوم الطاقة"
4. "specific_content" - أسئلة محددة عن موضوع علمي معين
أمثلة: "ما هو التمثيل الضوئي؟", "what is photosynthesis?", "كيف تعمل الخلية؟"
السؤال: "{query}"
الطالب يدرس باللغة: {"العربية" if is_arabic else "الإنجليزية"}
الصف: {grade}
رد فقط بكلمة واحدة: general_chat أو overview أو navigation أو specific_content
"""
try:
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": classification_prompt}],
temperature=0,
max_tokens=10
)
classification = response.choices[0].message.content.strip().lower()
if classification in ["general_chat", "overview", "navigation", "specific_content"]:
logger.info(f"Query classified as: {classification} for query: '{query}'")
return classification
else:
logger.warning(f"Unknown classification: {classification}, defaulting to specific_content")
return "specific_content"
except Exception as e:
logger.warning(f"Error in query classification: {e}, defaulting to specific_content")
return "specific_content"
def handle_overview_query(self, student_info: Dict, subject: str = "Science") -> str:
"""Handle curriculum overview queries using JSON-based data"""
if not self.pgvector:
if student_info['study_language'] == StudyLanguage.ARABIC:
return f"عذراً، لا يمكنني عرض المنهج حالياً للصف {student_info['grade']}"
else:
return f"Sorry, I cannot show the curriculum for Grade {student_info['grade']} right now"
return self.pgvector.get_overview_response(
student_info['grade'],
student_info['is_arabic'],
subject
)
def handle_general_chat_query(self, query: str, student_info: Dict) -> str:
"""Handle general chat queries using only student information"""
student_name = student_info.get('student_name', 'الطالب')
grade = student_info.get('grade', 4)
nationality = student_info.get('nationality', 'مصري')
is_arabic = student_info.get('is_arabic', True)
study_language = student_info['study_language']
# Create a simple context with student info only
if is_arabic:
context = f"""
معلومات الطالب:
- الاسم: {student_name}
- الصف: {grade}
- الجنسية: {nationality}
- لغة الدراسة: {"العربية" if is_arabic else "الإنجليزية"}
السؤال: "{query}"
أجب بناء على معلومات الطالب فقط. لا تستخدم أي معلومات من المنهج أو محتوى تعليمي.
إذا سأل الطالب عن هويتك، استخدم الرد المحدد في التعليمات.
إذا سأل عن معلوماته الشخصية، استخدم البيانات المتاحة أعلاه.
كن ودوداً وبسيطاً في الرد.
"""
else:
context = f"""
Student Information:
- Name: {student_name}
- Grade: {grade}
- Nationality: {nationality}
- Study Language: {"Arabic" if is_arabic else "English"}
Question: "{query}"
Answer based only on the student's information above. Do not use any curriculum or educational content.
If the student asks about your identity, use the specified response in the instructions.
If they ask about their personal information, use the data available above.
Be friendly and simple in your response.
"""
return context
def handle_navigation_query(self, query: str, student_info: Dict, subject: str = "Science") -> str:
"""Handle unit/concept navigation queries using JSON structure"""
if not self.pgvector:
return self.handle_overview_query(student_info, subject)
return self.pgvector.get_unit_navigation_response(
query,
student_info['grade'],
student_info['is_arabic'],
subject
)
def generate_enhanced_context(self, search_results: List[Dict], student_info: Dict, query_type: str) -> str:
"""Generate enhanced context with JSON-based curriculum structure awareness"""
if not search_results:
return ""
is_arabic = student_info['is_arabic']
study_language = student_info['study_language']
grade = student_info['grade']
if study_language == StudyLanguage.ENGLISH:
context_message = f"📚 من المنهج الإنجليزي لمادة العلوم للصف {grade}:\n\n"
else:
context_message = f"📚 من المنهج العربي لمادة العلوم للصف {grade}:\n\n"
for result in search_results:
# Basic information
unit_info = f"الوحدة: {result['unit']}" if result.get('unit') else ""
concept_info = f"المفهوم: {result['concept']}" if result.get('concept') else ""
lesson_info = f"الدرس: {result['lesson']}" if result.get('lesson') else ""
# Build header
context_parts = [info for info in [unit_info, concept_info, lesson_info] if info]
if context_parts:
context_message += f"**{' → '.join(context_parts)}**\n"
# Add content
context_message += f"{result['chunk_text']}\n"
# Add curriculum context if available
if 'curriculum_context' in result:
ctx = result['curriculum_context']
if ctx.get('navigation_hint'):
context_message += f"\n💡 {ctx['navigation_hint']}\n"
if ctx.get('related_concepts') and query_type == "specific_content":
related = ', '.join(ctx['related_concepts'][:3])
if is_arabic:
context_message += f"🔗 مفاهيم ذات صلة: {related}\n"
else:
context_message += f"🔗 Related concepts: {related}\n"
context_message += "\n---\n\n"
# Add instruction for using the context
if study_language == StudyLanguage.ENGLISH:
context_message += f"استخدم هذه المعلومات لتقديم شرح دقيق للطفل. المنهج إنجليزي فاذكر المصطلحات الإنجليزية مع الشرح بالعربي."
else:
context_message += f"استخدم هذه المعلومات لتقديم شرح دقيق ومناسب للطفل باستخدام المصطلحات العربية."
return context_message
def generate_response(
self,
user_message: str,
student_id: str,
subject: str = "Science",
model: str = Models.chat,
temperature: float = 0.3,
top_k: int = 3
) -> str:
"""Enhanced AI response generation with JSON-based curriculum structure awareness"""
if not self.is_available():
raise HTTPException(status_code=500, detail="Agent service not available")
try:
# Get student info with explicit language
student_info = self.db_service.get_student_info(student_id)
if not student_info:
raise HTTPException(status_code=404, detail=f"Student with ID {student_id} not found")
# Extract information
full_name = student_info.get('student_name', 'الطالب')
student_name = full_name.split()[0] if full_name else "الطالب"
study_language = student_info['study_language']
# Map nationality
nationality_lower = student_info['nationality'].lower().strip()
nationality_mapping = {
'egyptian': StudentNationality.EGYPTIAN,
'saudi': StudentNationality.SAUDI
}
nationality = nationality_mapping.get(nationality_lower, StudentNationality.EGYPTIAN)
# Add user message to DB
self.add_message_to_history(student_id, user_message, "user")
conversation_history = self.get_conversation_history(student_id)
# Classify query type based on JSON structure
query_type = self.classify_query_type(user_message, student_info)
logger.info(f"Query type: {query_type} for student {student_name} ({study_language.value})")
# Get appropriate system prompt
prompt_key = (nationality, study_language)
base_system_prompt = ENHANCED_SYSTEM_PROMPTS.get(prompt_key,
ENHANCED_SYSTEM_PROMPTS.get((StudentNationality.EGYPTIAN, StudyLanguage.ARABIC), ""))
formatted_base_prompt = base_system_prompt.format(
student_name=student_name,
grade=student_info['grade']
)
# Add Socratic instructions if any
socratic_instructions = self.pedagogy_service.get_socratic_instructions(
student_info['grade'], student_info['nationality']
)
if socratic_instructions:
formatted_base_prompt += f"\n\n{socratic_instructions}"
# Prepare messages
messages = []
has_system_message = conversation_history and conversation_history[0].get("role") == "system"
if not has_system_message:
messages.append({"role": "system", "content": formatted_base_prompt})
self.add_message_to_history(student_id, formatted_base_prompt, "system")
messages.extend(conversation_history)
# Handle different query types with JSON-based curriculum awareness
if query_type == "general_chat":
# Handle general chat with student info only
chat_context = self.handle_general_chat_query(user_message, student_info)
messages.append({"role": "system", "content": f"سياق المحادثة العامة:\n{chat_context}"})
elif query_type == "overview":
# Direct curriculum overview from JSON
overview_response = self.handle_overview_query(student_info, subject)
messages.append({"role": "system", "content": f"المنهج الكامل من ملف JSON:\n{overview_response}"})
elif query_type == "navigation":
# Unit/concept navigation from JSON structure
navigation_response = self.handle_navigation_query(user_message, student_info, subject)
messages.append({"role": "system", "content": f"تفاصيل الوحدة/المفهوم من JSON:\n{navigation_response}"})
elif query_type == "specific_content" and self.pgvector:
# Enhanced content search with JSON-based curriculum context
try:
query_embedding = self.openai_service.generate_embedding(user_message)
search_results = self.pgvector.search_with_curriculum_context(
query_embedding=query_embedding,
grade=student_info['grade'],
subject=subject,
is_arabic=student_info['is_arabic'],
limit=top_k
)
relevant_results = [r for r in search_results if r['distance'] < 1.3] if search_results else []
if relevant_results:
enhanced_context = self.generate_enhanced_context(
relevant_results, student_info, query_type
# Initialize modular components
self.query_handler = QueryHandler(self.openai_service, self.pgvector, self.db_service)
self.context_generator = ContextGenerator(self.openai_service, self.pgvector)
self.response_generator = ResponseGenerator(
self.openai_service, self.db_service, self.pedagogy_service,
self.query_handler, self.context_generator
)
messages.append({"role": "system", "content": enhanced_context})
logger.info(f"Added enhanced JSON-based context with {len(relevant_results)} chunks")
except Exception as e:
logger.warning(f"Error in enhanced content search: {e}")
def is_available(self) -> bool:
return self.openai_service.is_available()
# Generate response
response = self.client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature
def generate_response(self, user_message: str, student_id: str, subject: str = "Science",
model: str = Models.chat, temperature: float = 0.3, top_k: int = 3) -> str:
"""Main response generation method"""
return self.response_generator.generate_response(
user_message, student_id, subject, model, temperature, top_k
)
ai_response = response.choices[0].message.content.strip()
if not ai_response:
raise ValueError("Empty response from AI model")
# Save AI response
self.add_message_to_history(student_id, ai_response, "assistant")
logger.info(f"Generated {query_type} response for {student_name} ({study_language.value}): {len(ai_response)} characters")
return ai_response
except HTTPException:
raise
except Exception as e:
logger.error(f"Error generating AI response: {e}")
raise HTTPException(status_code=500, detail=f"AI response generation failed: {str(e)}")
def search_similar(self, query_embedding: List[float], student_id: str,
subject: str = "chemistry", top_k: int = 3):
"""Search similar content with student-specific filtering and JSON-based curriculum awareness"""
"""Search similar content with student-specific filtering"""
if not self.pgvector:
raise HTTPException(status_code=400, detail="PGVector service not enabled")
......@@ -535,8 +80,6 @@ Be friendly and simple in your response.
if not student_info:
raise HTTPException(status_code=404, detail=f"Student with ID {student_id} not found")
logger.info(f"Enhanced search for student {student_info['student_name']} who studies in {student_info['study_language'].value}")
return self.pgvector.search_with_curriculum_context(
query_embedding=query_embedding,
grade=student_info['grade'],
......@@ -544,14 +87,12 @@ Be friendly and simple in your response.
is_arabic=student_info['is_arabic'],
limit=top_k
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error in enhanced search_similar: {e}")
raise HTTPException(status_code=500, detail=f"Enhanced search failed: {str(e)}")
logger.error(f"Error in search_similar: {e}")
raise HTTPException(status_code=500, detail=f"Search failed: {str(e)}")
def get_available_subjects(self, student_id: str) -> List[str]:
"""Get available subjects for the student based on their grade and language from JSON data"""
"""Get available subjects for the student"""
if not self.pgvector:
return []
......@@ -561,15 +102,14 @@ Be friendly and simple in your response.
return []
return self.pgvector.get_subjects_by_grade_and_language(
student_info['grade'],
student_info['is_arabic']
student_info['grade'], student_info['is_arabic']
)
except Exception as e:
logger.error(f"Error getting available subjects for {student_id}: {e}")
logger.error(f"Error getting available subjects: {e}")
return []
def get_curriculum_overview(self, student_id: str, subject: str = "Science") -> Dict:
"""Get curriculum overview for a specific student based on JSON data"""
"""Get curriculum overview for a specific student"""
if not self.pgvector:
return {"error": "PGVector service not available"}
......@@ -579,13 +119,11 @@ Be friendly and simple in your response.
return {"error": "Student not found"}
curriculum = self.pgvector.get_curriculum_structure(
student_info['grade'],
student_info['is_arabic'],
subject
student_info['grade'], student_info['is_arabic'], subject
)
if not curriculum:
return {"error": f"No curriculum found for Grade {student_info['grade']} in {student_info['study_language'].value}"}
return {"error": f"No curriculum found for Grade {student_info['grade']}"}
return {
"student_info": {
......@@ -605,38 +143,28 @@ Be friendly and simple in your response.
logger.error(f"Error getting curriculum overview: {e}")
return {"error": str(e)}
# Conversation management methods
def export_conversation(self, student_id: str) -> List[Dict[str, str]]:
"""Export conversation history for a student"""
try:
return self.get_conversation_history(student_id)
except Exception as e:
logger.error(f"Error exporting conversation for {student_id}: {e}")
return []
return self.response_generator.get_conversation_history(student_id)
def clear_conversation(self, student_id: str) -> Dict[str, str]:
"""Clear conversation history for a student"""
try:
self.db_service.clear_history(student_id)
return {
"status": "success",
"message": f"Conversation cleared for student {student_id}"
}
return {"status": "success", "message": f"Conversation cleared for student {student_id}"}
except Exception as e:
logger.error(f"Error clearing conversation: {e}")
return {
"status": "error",
"message": f"Failed to clear conversation: {str(e)}"
}
return {"status": "error", "message": f"Failed to clear conversation: {str(e)}"}
def get_agent_stats(self, student_id: str) -> Dict:
"""Get conversation statistics for a student with enhanced JSON-based curriculum info"""
"""Get conversation statistics for a student"""
try:
history = self.get_conversation_history(student_id)
history = self.response_generator.get_conversation_history(student_id)
user_messages = [msg for msg in history if msg['role'] == 'user']
assistant_messages = [msg for msg in history if msg['role'] == 'assistant']
system_messages = [msg for msg in history if msg['role'] == 'system']
# Get student language info
student_info = self.db_service.get_student_info(student_id)
language_info = {}
curriculum_info = {}
......@@ -648,7 +176,6 @@ Be friendly and simple in your response.
"grade": student_info['grade']
}
# Add JSON-based curriculum availability info
if self.pgvector:
curriculum = self.pgvector.get_curriculum_structure(
student_info['grade'], student_info['is_arabic']
......@@ -673,14 +200,11 @@ Be friendly and simple in your response.
**curriculum_info
}
except Exception as e:
logger.error(f"Error getting enhanced agent stats: {e}")
return {
"student_id": student_id,
"error": str(e)
}
logger.error(f"Error getting agent stats: {e}")
return {"student_id": student_id, "error": str(e)}
def get_curriculum_structure_info(self, student_id: str, subject: str = "Science") -> Dict:
"""Get detailed curriculum structure information from JSON data"""
"""Get detailed curriculum structure information"""
if not self.pgvector:
return {"error": "PGVector service not available"}
......@@ -694,12 +218,7 @@ Be friendly and simple in your response.
)
if not curriculum:
return {
"error": f"No curriculum structure found",
"grade": student_info['grade'],
"language": "Arabic" if student_info['is_arabic'] else "English",
"subject": subject
}
return {"error": "No curriculum structure found"}
# Extract detailed structure info
units_info = []
......
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