"""
Board AI Engine.
Handles the interactive whiteboard: XML generation, parsing,
icon/page resolution, TTS, and board state management.
Now supports MULTIPLE SUBJECTS dynamically.
Each subject loads its own folder, pages_base_url, etc.
"""
import re
import json
import requests
from config import GPT_URL, MAX_CHAT_HISTORY
from board.tts import TTSEngine
from board.icons import IconResolver
from board.pages import resolve_page_tags
from subjects.loader import subject_loader
from json_processor import BoardProcessor
class BoardEngine:
"""
Board AI Engine for a specific user session.
Supports any subject dynamically.
"""
def __init__(self):
self.gpt_url = GPT_URL
self.board_processor = BoardProcessor()
self.tts_engine = TTSEngine()
self.icon_resolver = IconResolver()
# Per-user state: { username: { subject_id, conversation_history, last_sequence } }
self._user_sessions = {}
print("✅ BoardEngine initialized (multi-subject)")
# ─── User session management ───
def _get_user_session(self, username):
"""Get or create a user's board session."""
if username not in self._user_sessions:
self._user_sessions[username] = {
"subject_id": None,
"conversation_history": [],
"last_sequence": [],
}
return self._user_sessions[username]
def set_subject(self, username, subject_id):
"""Set the active subject for a user's board session."""
us = self._get_user_session(username)
# If switching subjects, clear history
if us["subject_id"] and us["subject_id"] != subject_id:
us["conversation_history"] = []
us["last_sequence"] = []
print(f" 🔄 Board subject switched: {us['subject_id']} → {subject_id} for {username}")
us["subject_id"] = subject_id
print(f" 📋 Board subject set: {subject_id} for {username}")
def get_subject(self, username):
"""Get the active subject for a user."""
us = self._get_user_session(username)
return us.get("subject_id")
# ─── GPT call ───
def _call_gpt5(self, user_message, system_prompt, temperature=0.7, max_tokens=4000):
payload = {
"user_input": user_message,
"chat_history": [
{"role": "system", "content": system_prompt}
],
"temperature": temperature,
"top_p": 0.95,
"max_completion_tokens": max_tokens
}
try:
response = requests.post(self.gpt_url, json=payload, timeout=120)
response.raise_for_status()
return response.json().get("assistant_response", "")
except requests.exceptions.Timeout:
print(" ❌ GPT timeout")
return None
except requests.exceptions.ConnectionError:
print(" ❌ GPT connection error")
return None
except Exception as e:
print(f" ❌ GPT error: {e}")
return None
# ─── Chat history formatting ───
def _format_chat_history(self, username):
us = self._get_user_session(username)
history = us.get("conversation_history", [])
if not history:
return ""
recent = history[-10:]
parts = []
for msg in recent:
if msg["role"] == "user":
parts.append(f"الطالب: {msg['content']}")
elif msg["role"] == "assistant":
parts.append(f"المدرس: {msg['content']}")
return (
"\n\n══ سجل المحادثة السابقة ══\n"
+ "\n".join(parts)
+ "\n══ نهاية السجل ══"
)
# ─── Step 1: Route message ───
def _route_message(self, user_message, username):
us = self._get_user_session(username)
subject_id = us["subject_id"]
if not subject_id:
return "main.txt"
subject_data = subject_loader.load(subject_id)
if not subject_data:
return "main.txt"
structure = subject_data.get("structure.txt", "")
p_files = subject_data.get("_p_files", [])
chat_history_text = self._format_chat_history(username)
p_files_desc = "\n".join([f"- {f}: الفصل {i+1}" for i, f in enumerate(p_files)])
routing_prompt = f"""أنت نظام توجيه ذكي لمساعد تعليمي.
مهمتك: تحليل رسالة الطالب واختيار الملف المناسب للرد.
الملفات المتاحة:
- main.txt: للتحيات، الأسئلة العامة، أي شيء لا يتعلق بفصل محدد
{p_files_desc}
فهرس الكتاب (للمساعدة في التوجيه):
{structure}
{chat_history_text}
تعليمات:
1. إذا كانت الرسالة تحية أو سؤال عام → main.txt
2. إذا كانت تسأل عن موضوع في فصل محدد → الملف المناسب
3. استخدم الفهرس لتحديد الفصل الصحيح
4. مهم جداً: إذا قال الطالب "اشرح أكثر" أو "وضح" أو أي طلب متابعة، ارجع لسجل المحادثة لتعرف الموضوع الحالي واختر نفس الملف
5. أجب فقط باسم الملف (مثال: p1.txt أو main.txt) بدون أي كلام إضافي
رسالة الطالب: {user_message}
الملف المناسب:"""
chosen = self._call_gpt5(
user_message, routing_prompt,
temperature=0.2, max_tokens=50
)
if not chosen:
return "main.txt"
chosen = chosen.strip().lower()
valid_files = ["main.txt"] + p_files
for v in valid_files:
if v in chosen:
return v
return "main.txt"
# ─── Step 2: Generate XML response ───
def _generate_xml_response(self, user_message, chosen_file, username):
us = self._get_user_session(username)
subject_id = us["subject_id"]
if not subject_id:
return None
subject_data = subject_loader.load(subject_id)
if not subject_data:
return None
file_content = subject_data.get(chosen_file, "")
chat_history_text = self._format_chat_history(username)
system_prompt = f"""انتي مدرسة خبيرة ومحترفة في هذه المادة. تشرح على سبورة تفاعلية رقمية.
═══ صيغة الرد ═══
يجب أن تردي بصيغة XML خاصة تحتوي على:
1. عناصر السبورة - ما سيُرسم/يُضاف على السبورة أولاً
2. نص الكلام - النص الذي سيُقرأ بصوت عالٍ للطالب بعد رسم العناصر (عربي طبيعي)
═══ عناصر السبورة المتاحة (داخل ) ═══
• محتوى الملاحظة
• نص مباشر على السبورة بدون خلفية
•
الأنواع: circle, triangle, star, arrow-right, arrow-left, arrow-up, arrow-down,
rectangle, diamond, hexagon, square, oval, arrow-double-h, checkmark, cross,
heart, cloud, lightning, speech, process, decision
•
سيتم البحث عن أيقونة مرسومة يدوياً (مثل: ball, car, force, spring, weight, rope, pulley)
• رقم_الصفحة
لعرض صفحة محددة من الكتاب كصورة على السبورة
مثال: 12 لعرض الصفحة 12 من الكتاب
استخدمها عندما تحتاج تعرض للطالب صفحة معينة من الكتاب
═══ قواعد مهمة جداً ═══
1. اشرح خطوة بخطوة: ابدأ بـ ثم ثم ثم وهكذا
2. اجعل الشرح متدرجاً كأنك تشرح على سبورة حقيقية أمام الطلاب
3. = ما يظهر على السبورة أولاً (ملاحظات، نصوص، أشكال، صور، صفحات الكتاب)
4. = الكلام المسموع بعد رسم العناصر (طبيعي، ودود، واضح، يشرح ما تم رسمه)
5. لا تضع كل شيء دفعة واحدة - اجعله تسلسلياً
7.