Arabic-Lessons / app.py
Ahmed-El-Sharkawy's picture
Update app.py
60fbdcb verified
raw
history blame
16 kB
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()