Arabic-Lessons / app.py
Ahmed-El-Sharkawy's picture
Update app.py
60fbdcb verified
import os, sys, time, asyncio, json, re
from typing import List, Dict, Optional
import gradio as gr
from dotenv import load_dotenv
import base64
from openai import OpenAI
if sys.platform.startswith("win"):
try:
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
except Exception:
pass
load_dotenv()
APP_Name = os.getenv("APP_Name", "منصة دروس تفاعلية بالعربية")
APP_Version = os.getenv("APP_Version", "0.2.0")
API_KEY = os.getenv("API_KEY", "")
MODELS = [m.strip() for m in os.getenv("Models", "").split(",") if m.strip()] or [
"QwQ-32B",
"zai-org/GLM-4.5-Air",
]
MODEL_INFO = {
"QwQ-32B": "QwQ-32B — مُعلّم استدلالي قوي للإجابات المفصّلة.",
"zai-org/GLM-4.5-Air": "GLM-4.5-Air — سريع وفعّال للشرح خطوة بخطوة.",
}
LOGO_PATH = "download.jpeg"
COMPANY_LOGO = LOGO_PATH
OWNER_NAME = "ENG. Ahmed Yasser El Sharkawy"
BASE_URL = "https://genai.ghaymah.systems"
client = OpenAI(api_key=API_KEY, base_url=BASE_URL) if API_KEY else None
CSS = """
:root { direction: rtl; }
* { font-family: "Tajawal", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.app-header{display:flex;align-items:center;gap:12px;justify-content:center;margin:6px 0 16px}
.app-header img{height:60px;border-radius:12px}
.app-title{font-weight:800;font-size:28px;line-height:1.1}
.app-sub{opacity:.7;font-size:14px}
.gradio-container { direction: rtl; }
.markdown-body { direction: rtl; text-align: right; }
"""
SYSTEM_SEED = (
"أنت معلّم عربي متميز. قدّم شروحًا تعليمية دقيقة ومبسطة، وراعِ مستوى الطالب،"
" وقدّم الحلول خطوة بخطوة عند الطلب. استخدم LaTeX للمعادلات بين $$...$$."
)
BACKOFF = [3, 6, 12]
def logo_data_uri(path: str) -> str:
if not os.path.exists(path):
return ""
ext = os.path.splitext(path)[1].lower()
mime = {
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".webp": "image/webp", ".gif": "image/gif"
}.get(ext, "image/png")
with open(path, "rb") as f:
b64 = base64.b64encode(f.read()).decode("utf-8")
return f"data:{mime};base64,{b64}"
DIALECT_MAP = {
"فصحى": "استخدم العربية الفصحى الواضحة.",
"مصري": "استخدم لهجة مصرية خفيفة مفهومة على نطاق واسع دون إسراف.",
"شامي": "استخدم لهجة شامية مبسطة ومفهومة.",
"خليجي": "استخدم لهجة خليجية مبسطة ومفهومة.",
"مغربي": "استخدم لهجة مغاربية مبسطة ومفهومة، وتوضيح المصطلحات إن لزم.",
}
SUBJECT_MAP = {
"رياضيات": "math",
"فيزياء": "physics",
"كيمياء": "chemistry",
"أحياء": "biology",
}
LEVEL_MAP = {
"ابتدائي": "elementary",
"إعدادي": "middle",
"ثانوي": "high",
"جامعي": "university",
}
STYLE_MAP = {
"خطوة بخطوة": "step_by_step",
"تلميحات أولًا": "hints_first",
"إجابة نهائية فقط": "final_only",
}
SCHEMA_GUIDE = {
"mode": "solve|explain|practice",
"subject": "math|physics|chemistry|biology",
"grade": "elementary|middle|high|university",
"dialect": "fosha|masri|shami|khaleeji|maghrebi",
"answer_style": "step_by_step|hints_first|final_only",
"steps": [{"title": "", "explanation": "", "math": ""}],
"final_answer": "",
"tips": [""],
"common_mistakes": [""],
"similar_exercises": [""],
"confidence": 0.0,
}
DIALECT_CODE = {
"فصحى": "fosha",
"مصري": "masri",
"شامي": "shami",
"خليجي": "khaleeji",
"مغربي": "maghrebi",
}
def build_messages(
prompt: str,
subject_ar: str,
level_ar: str,
dialect_ar: str,
style_ar: str,
mode: str,
) -> List[Dict[str, str]]:
subject = SUBJECT_MAP.get(subject_ar, "math")
grade = LEVEL_MAP.get(level_ar, "university")
answer_style = STYLE_MAP.get(style_ar, "step_by_step")
dialect_note = DIALECT_MAP.get(dialect_ar, DIALECT_MAP["فصحى"])
dialect_code = DIALECT_CODE.get(dialect_ar, "masri")
system = (
f"{SYSTEM_SEED} {dialect_note} ركّز على الدقة والمنطق التربوي."
" إذا طلب الطالب برهانًا أو اشتقاقًا فاذكر الأفكار الرئيسية بإيجاز."
" تجنّب الحشو واطرح أسئلة فاحصة عند الحاجة."
)
user = (
"أنت الآن داخل منصة دروس عربية. أعد لي إخراجًا بصيغة JSON فقط دون أي نص زائد.\n"
"اتبع هذا المخطط الحرفي للمفاتيح: {"
"\"mode\", \"subject\", \"grade\", \"dialect\", \"answer_style\", \"steps\", \"final_answer\", \"tips\", \"common_mistakes\", \"similar_exercises\", \"confidence\"} .\n"
"- اكتب الشرح بالعربية وفق اللهجة المطلوبة.\n"
"- اكتب المعادلات داخل الحقل math بين $$ بهذه الصيغة: $$..$$.\n"
"- راعِ المستوى الدراسي.\n"
f"- subject='{subject}', grade='{grade}', dialect='{dialect_code}', answer_style='{answer_style}', mode='{mode}'.\n"
f"- مهمة الطالب: {prompt}\n"
"أعد JSON الصحيح فقط."
)
return [{"role": "system", "content": system}, {"role": "user", "content": user}]
def safe_chat_complete(model: str, messages: List[Dict], max_tokens: int = 1200) -> str:
if not client:
return "⚠️ لم يتم العثور على API_KEY في .env"
attempt = 0
while True:
try:
resp = client.chat.completions.create(
model=model,
messages=messages,
max_tokens=max_tokens,
temperature=0.2,
timeout=90,
)
return resp.choices[0].message.content or ""
except Exception as e:
msg = str(e)
if ("429" in msg or "Rate" in msg) and attempt < len(BACKOFF):
time.sleep(BACKOFF[attempt]); attempt += 1
continue
return f"فشل الطلب مع النموذج `{model}`: {e}"
def extract_json(text: str) -> Optional[Dict]:
if not text:
return None
try:
return json.loads(text)
except Exception:
pass
m = re.search(r"\{[\s\S]*\}", text)
if m:
try:
return json.loads(m.group(0))
except Exception:
return None
return None
def format_lesson(payload: Dict) -> str:
"""حوّل استجابة JSON إلى Markdown سهل القراءة مع بطاقة ملخص.
متسامح مع تنسيقات steps/tips المختلفة.
"""
if not isinstance(payload, dict):
return payload if isinstance(payload, str) else "تعذر تنسيق الرد."
subject = payload.get("subject", "math") or "math"
grade = payload.get("grade", "") or ""
answer_style = payload.get("answer_style", "") or ""
dialect_code = payload.get("dialect", "fosha") or "fosha"
mode = payload.get("mode", "solve") or "solve"
steps_raw = payload.get("steps", []) or []
final_answer = payload.get("final_answer", "") or ""
tips = payload.get("tips", []) or []
mistakes = payload.get("common_mistakes", []) or []
similars = payload.get("similar_exercises", []) or []
if isinstance(tips, str): tips = [tips]
if isinstance(mistakes, str): mistakes = [mistakes]
if isinstance(similars, str): similars = [similars]
steps: List[Dict[str, str]] = []
for i, st in enumerate(steps_raw, 1):
if isinstance(st, dict):
steps.append({
"title": st.get("title", f"الخطوة {i}") or f"الخطوة {i}",
"explanation": st.get("explanation", "") or "",
"math": st.get("math", "") or "",
})
elif isinstance(st, str):
steps.append({"title": f"الخطوة {i}", "explanation": st, "math": ""})
else:
continue
subject_icon = {"math": "🧮", "physics": "🧪", "chemistry": "⚗️", "biology": "🧬"}.get(subject, "📘")
# خرائط عرض عربية
SUBJECT_AR = {"math": "رياضيات", "physics": "فيزياء", "chemistry": "كيمياء", "biology": "أحياء"}
STYLE_AR = {"step_by_step": "خطوة بخطوة", "hints_first": "تلميحات أولًا", "final_only": "إجابة نهائية فقط"}
DIALECT_AR = {"fosha": "فصحى", "masri": "مصري", "shami": "شامي", "khaleeji": "خليجي", "maghrebi": "مغربي"}
MODE_AR = {"solve": "حل مسألة", "explain": "شرح مفهوم", "practice": "إنشاء تمارين"}
md: List[str] = []
# تفاصيل الشرح
if steps:
md.append("#### الخطوات")
for i, st in enumerate(steps, 1):
title = st.get("title", f"الخطوة {i}")
expl = st.get("explanation", "")
math = st.get("math", "")
md.append(f"**{i}. {title}**\n\n{expl}")
if math:
md.append(f"\\[ {math.replace('$$','')} \\]")
if final_answer:
md.append("#### الإجابة النهائية")
md.append(final_answer)
if tips:
md.append("#### نصائح للمذاكرة")
for t in tips:
md.append(f"- {t}")
if mistakes:
md.append("#### أخطاء شائعة")
for m in mistakes:
md.append(f"- {m}")
if similars:
md.append("#### تمارين مشابهة للتدريب")
for s in similars[:5]:
md.append(f"- {s}")
return "\n\n".join(md)
# Gradio
with gr.Blocks(title=f"{APP_Name} v{APP_Version}", css=CSS, theme=gr.themes.Soft()) as demo:
# MathJax لدعم LaTeX
gr.HTML(
"""
<script>
if (!window.MathJax) {
window.MathJax = {tex: {inlineMath: [['$','$'], ['\\(','\\)']]}};
}
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
"""
)
header_logo_src = logo_data_uri(COMPANY_LOGO)
logo_html = f"<img src='{header_logo_src}' alt='logo'>" if header_logo_src else ""
gr.HTML(f"""
<div class="app-header">
{logo_html}
<div class="app-header-text">
<div class="app-title">{APP_Name}</div>
<div class="app-sub">v{APP_Version}{OWNER_NAME}</div>
</div>
</div>
""")
with gr.Row():
with gr.Column(scale=3):
chat = gr.Chatbot(label="جلسة الدرس", height=520, type="messages", value=[])
user_in = gr.Textbox(label="سؤال الطالب / المسألة", placeholder="اكتب السؤال هنا… مثلاً: احسب قيمة التعبير 2x+3 عند x=5", lines=2)
with gr.Row():
send_btn = gr.Button("إرسال ✨", variant="primary")
clear_btn = gr.Button("مسح المحادثة")
gr.Markdown(
"""
> **ملاحظة:** تُستخدم هذه المنصة لأغراض تعليمية. تحقّق دائمًا من النتائج خاصة في المسائل المتقدمة.
جرّب كتابة: **حلّل العبارة التربيعية x^2 - 5x + 6** أو **اشرح قانون نيوتن الثاني**.
"""
)
with gr.Column(scale=2, min_width=340):
model_choice = gr.Radio(
choices=MODELS,
value=MODELS[0],
label="النموذج",
info=" GLM-4.5-Air",
)
info_md = gr.Markdown(MODEL_INFO.get(MODELS[0], ""))
def _update_info(m: str) -> str:
title = f"**{m}**"
desc = MODEL_INFO.get(m, "")
return f"{title}\n\n{desc}"
model_choice.change(_update_info, model_choice, info_md)
subject_dd = gr.Dropdown(["رياضيات", "فيزياء", "كيمياء", "أحياء"], value="رياضيات", label="المادة")
level_dd = gr.Dropdown(["ابتدائي", "إعدادي", "ثانوي", "جامعي"], value="جامعي", label="المستوى الدراسي")
dialect_dd = gr.Dropdown(["فصحى", "مصري", "شامي", "خليجي", "مغربي"], value="مصري", label="الأسلوب/اللهجة")
style_dd = gr.Radio(["خطوة بخطوة", "تلميحات أولًا", "إجابة نهائية فقط"], value="خطوة بخطوة", label="نمط الشرح")
mode_dd = gr.Radio(["حل مسألة", "شرح مفهوم", "إنشاء تمارين"], value="حل مسألة", label="نوع الدرس")
ex_label = gr.Markdown("**أمثلة سريعة**")
examples = gr.Dropdown(
[
"أوجد قيمة x: 2x + 3 = 11",
"اشتق الدالة f(x)=x^3 - 4x",
"احسب عجلة جسم كتلته 2كج تؤثر عليه قوة 10ن",
"ما الفرق بين الرابطة الأيونية والتساهمية؟",
], label="اختر مثالًا ثم اضغط إدراج")
insert_btn = gr.Button("إدراج المثال")
# gr.Image(LOGO_PATH, show_label=False, container=False)
state = gr.State({"history": []})
def on_insert(ex):
return gr.update(value=ex or "")
insert_btn.click(on_insert, examples, user_in)
def on_submit(msg, chat_messages):
if not msg:
return "", (chat_messages or [])
updated = (chat_messages or []) + [{"role": "user", "content": msg}]
return "", updated
def bot_step(chat_messages, chosen_model, st, subject_ar, level_ar, dialect_ar, style_ar, mode_label):
if not chat_messages:
return chat_messages, st
last_user = None
for m in reversed(chat_messages):
if m.get("role") == "user":
last_user = m.get("content")
break
if not last_user:
return chat_messages, st
mode = "solve" if mode_label == "حل مسألة" else ("explain" if mode_label == "شرح مفهوم" else "practice")
msgs = build_messages(last_user, subject_ar, level_ar, dialect_ar, style_ar, mode)
reply_raw = safe_chat_complete(chosen_model, msgs, max_tokens=1400)
payload = extract_json(reply_raw)
if payload is None:
pretty = reply_raw or "تعذر الحصول على رد."
else:
pretty = format_lesson(payload)
updated = (chat_messages or []) + [{"role": "assistant", "content": pretty}]
st = st or {"history": []}
st["history"] = (st.get("history") or []) + [{
"user": last_user,
"model": chosen_model,
"subject": subject_ar,
"level": level_ar,
"dialect": dialect_ar,
"style": style_ar,
"mode": mode_label,
"raw": reply_raw,
}]
return updated, st
def on_clear():
return [], {"history": []}
user_in.submit(on_submit, [user_in, chat], [user_in, chat]) \
.then(bot_step, [chat, model_choice, state, subject_dd, level_dd, dialect_dd, style_dd, mode_dd], [chat, state])
send_btn.click(on_submit, [user_in, chat], [user_in, chat]) \
.then(bot_step, [chat, model_choice, state, subject_dd, level_dd, dialect_dd, style_dd, mode_dd], [chat, state])
clear_btn.click(on_clear, outputs=[chat, state])
if __name__ == "__main__":
demo.queue()
demo.launch()