Spaces:
Sleeping
Sleeping
| 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() | |