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( """ """ ) header_logo_src = logo_data_uri(COMPANY_LOGO) logo_html = f"logo" if header_logo_src else "" gr.HTML(f"""
{logo_html}
{APP_Name}
v{APP_Version} • {OWNER_NAME}
""") 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()