PyDA Course
متوسط نصف موجّه ⏱ 90 دقيقة ⚡ +100 XP +15 نقطة لكل خطوة

مساعد المدرس بالذكاء الاصطناعي

EducationAI AgentsNLP

مدرِّس يعرف الإجابة الصحيحة فقط هو تطبيق اختبارات. يبني هذا المشروع النوع الآخر: مدرِّس تكرار متباعد يتذكر ما تضعف فيه, يزيد الفاصل بين المراجعات حين تؤدي جيدًا ويقصّره حين لا تؤدي. محرك الجدولة إعادة تنفيذ نظيفة لخوارزمية SM-2 ، خوارزمية مستخدمة على نطاق واسع لا تعتمد إلا على interval (أيام منذ آخر مراجعة) وعلى درجة quality (0–5) توفرها أنت بعد كل محاولة. طبقة استمرار تحفظ حالة المجموعة الكاملة إلى JSON حتى يصمد التقدم عبر الجلسات, وطبقة LLM اختيارية تصوغ «تلميحًا مدرسيًا» من جملة واحدة يدفعك دون أن يكشف الإجابة. الجوهر Python نقي; طبقة LLM حقيقية لكنها اختيارية ، المدرِّس يعمل بكامل وظائفه دون أي مفتاح API على الإطلاق.

هذا يفترض إتقان القواميس والقوائم, وJSON أساسيًا, ولا خلفية في تعلّم الآلة; لا شيء هنا مُقيَّم؛ هذا المشروع اختياري وغير مُقيَّم ، راجع مشاريع من العالم الحقيقي للاطلاع على القائمة الكاملة والنامية.

🎯 ما ستفعله

  1. تعريف نموذج بيانات البطاقة وفحص أي البطاقات مستحقة الآن.
  2. تنفيذ مجدول فترات SM-2 مبسط يعيد نسخة بطاقة محدّثة.
  3. تشغيل جلسة تدريب تراجع البطاقات المستحقة فقط, وتقرأ درجات النفس, وتحدّث المجموعة.
  4. حفظ التقدم إلى JSON والتحقق من رحلة حفظ/تحميل ذهابًا وإيابًا.
  5. تأليف مطالبة LLM اختيارية لتلميح المدرِّس وتخطي استدعاء الواجهة البرمجية بأدب حين لا يوجد مفتاح.

أين تُشغّل هذا

محليًا باستخدام uv هو المسار الموصى به ، يكتب هذا المشروع ويقرأ progress.json وليس له تبعيات خارجية, فـuv init جديد وطرفية محلية كل ما تحتاج إليه.

Google Colab وKaggle Notebooks وBinder ستشغّل كل خطوة: لا تبعيات pip للمدرِّس إطلاقًا, فكل خلية كود تعمل دون تعديل. التحفظ الوحيد: progress.json يعيش في نظام ملفات الدفتر المؤقت ، نزّله بين الجلسات إن أردت الاستمرار عبر تشغيلات Colab.

Open In Colab Open In Kaggle Binder

الإعداد

كل ما يلزم قبل أول تشغيل تدريب: مشروع بـjson (لا حاجة لـuv add) ومجموعة بداية حالتها الجدولية في منتصف قوس تعلم منطقي.

أنشئ المشروع

uv init ai-tutor
cd ai-tutor

لا حاجة لـuv add ، json في المكتبة القياسية, ومحرك SM-2 في خمسة أسطر حساب.

حمّل مجموعة البداية

👟 تلميح البداية : ابنِ new_card كمعمل صغير (factory) كي تبدأ كل بطاقة بقيم افتراضية سليمة (interval=0, due=0) مع السماح بتجاوزات العالم الحقيقي للبطاقات التي تعلّمتها مسبقًا.

# tutor.py
import json

def new_card(card_id, front, back, hint, interval=0, due=0):
    return {"id": card_id, "front": front, "back": back, "hint": hint,
            "interval": interval, "due": due}

DECK = [
    new_card("py-1", "What does `zip(a, b)` do?",
             "Pairs items from a and b into tuples, stopping at the shorter.",
             "Sounds like a zipper."),
    new_card("py-2", "When does `dict.get(k, d)` return `d`?",
             "When `k` is missing from the dict.",
             "Think of a safe default value.", interval=1, due=2),
    new_card("py-3", "What does `sorted(d.items())` return?",
             "A new list of `(key, value)` tuples, sorted by key.",
             "It's the items view, but sorted.", interval=6, due=5),
    new_card("py-4", "Name one difference between a list and a tuple.",
             "Lists are mutable; tuples are not.",
             "One uses [], the other uses ().", interval=6, due=8),
]

print(len(DECK), "cards")
today = 5
due = [c for c in DECK if c["due"] <= today]
print(len(due), "due now", [c["id"] for c in due])

تلتقط المجموعة مزيجًا واقعيًا: py-1 لم تُراجع قط (interval=0, due=0), وpy-2 روجعت قبل أيام (due=2, مستحقة الآن متأخرة), وpy-3 مستحقة اليوم, وpy-4 مجدولة للمستقبل. مسند «الآن» هو due <= today ، بطاقة متأخرة وبطاقة مستحقة اليوم كلتاهما تُحسب.

🎯 الناتج المتوقع : 4 cards ثم 3 due now ['py-1', 'py-2', 'py-3'].

🩹 إذا لم يعمل : إن طبع due البطاقات الخاطئة, تحقق مما إذا كانت due حقل عدد صحيح خام (لا datetime) ، يستخدم هذا المشروع عدّاد أيام, لا تاريخًا. إن كان العدد خاطئًا, فلعل <= ينبغي أن تكون < ، بطاقة متأخرة (مستحقة يوم 2, اليوم 5) تُحسب, وبطاقة مستحقة اليوم (اليوم 5) تُحسب, لكن بطاقة مستقبلية (اليوم 8) لا تُحسب.

الخطوة 2: نفّذ مجدول SM-2

المجدول دالة نقيّة: سلّمها interval الحالي ودرجة النفس quality (0–5, حيث 3 فما فوق تعني «عرفت») فتعيد الفاصل التالي ، لا تأثيرات جانبية, لا إدخال/إخراج.

2.1 اكتب sm2_interval

👟 تلميح البداية : ترجم قواعد SM-2: الدرجة دون 3 تُعيد إلى تكرار قصير; interval يساوي 0 ينتقل إلى يوم واحد; interval يساوي 1 ينتقل إلى 6 أيام; وإلا ضاعف الفاصل الحالي ونصفّه.

# tutor.py (continued)
def sm2_interval(interval: int, quality: int) -> int:
    if quality < 3:
        return 0
    if interval == 0:
        return 1
    if interval == 1:
        return 6
    return round(interval * 2.5)

print(sm2_interval(0, 5), sm2_interval(1, 5), sm2_interval(6, 4), sm2_interval(6, 2))

الدرجة دون 3 تعني «لم أعرف» ، تُعاد البطاقة لتكرار قصير. حالما تكون الدرجة ≥ 3, ينمو الفاصل: بطاقة لم تُرَ قط (0 → يوم واحد) → بطاقة يوم واحد (1 → 6 أيام) → بطاقة ستة أيام (6 → 15 يومًا). النمو ليس خطيًا: round(interval * 2.5) يجعل بطاقة نجحت عليها ثلاث مرات متتالية تنمو أسرع بكثير من المراجعات الأولى ، وهذا هو جوهر السبب في أن التكرار المتباعد يوفر الوقت.

🎯 الناتج المتوقع : 1 6 15 0 ، تنجح المراجعة الأولى, وتفتح الثانية القوس الطويل, وتنمّي الثالثة الفاصل مجددًا, ومراجعة فاشلة على بطاقة ناضجة تُصفرّها.

🩹 إذا لم يعمل : إن طبع sm2_interval(0, 3) 0 بدل 1, ففحص quality >= 3 وُضع بعد فحص interval == 0 (إرجاع مبكر). إن طبع عائمًا كـ15.0, فـround() أُزيل.

2.2 اكتب دالة review

👟 تلميح البداية : تأخذ review بطاقة ودرجة جودة وعدّاد اليوم الحالي; وتعيد نسخة محدّثة ، لا تأثيرات جانبية.

# tutor.py (continued)
def review(card: dict, quality: int, today: int) -> dict:
    updated = dict(card)
    if quality >= 3:
        updated["interval"] = sm2_interval(card["interval"], quality)
        updated["due"] = today + updated["interval"]
    else:
        updated["interval"] = max(1, card["interval"])
        updated["due"] = today
    updated["last"] = today
    return updated

c = review({"id": "x", "interval": 0, "due": 0}, 5, 0)
print(c["interval"], c["due"])
c = review({"id": "x", "interval": 1, "due": 0}, 5, 0)
print(c["interval"], c["due"])
c = review({"id": "x", "interval": 6, "due": 0}, 2, 0)
print(c["interval"], c["due"])

dict(card) ينشئ نسخة سطحية ، لا تُحوَّر المجموعة الأصلية, ما يعني أن المتصل يقرر إن أراد حفظ التغيير (تصميم متعمد قابل للمراحل). الدرجة ≥ 3 تنمّي الفاصل وتدفع due للأمام أيامًا بقدره. الدرجة دون 3 تُعيد ضبط البطاقة: يبقى الفاصل على الأقل 1 (كي تبقى البطاقة في التداول) وتستحق البطاقة فورًا اليوم ، لا غدًا ، لأن المتعلم لم يُثبت بعد أنه يعرفها.

🎯 الناتج المتوقع : 1 1 ثم 6 6 ثم 6 0 ، الحالات الثلاث: المراجعة الأولى, والثانية, والفاشلة.

🩹 إذا لم يعمل : إن طبع السطر الثالث 0 0 بدل 6 0, ففرع «الفشل» يُصفّر الفاصل بدل إبقاء max(1, card["interval"]). إن فشل c["due"] عند النمو بـTypeError, فـcard["due"] كانت سلسلة ، تأكد من بقاء due عددًا صحيحًا طوال الطريق.

2.3 تحقّق من المجدول

✅ قائمة التحقق

  • sm2_interval حتمية: نفس interval + نفس quality → نفس النتيجة كل مرة.
  • ✅ تعيد review قاموسًا جديدًا ، المتصل هو من يقرر إن يقبل التحديث.
  • ✅ الدرجة دون 3 تُعيد ضبط البطاقة لكن لا إلى interval=0 أبدًا ، تبقى البطاقة في التداول.

🤔 سؤال (أسئلة) سقراطي(ة) :

  • تعيد review نسخة بدل تحوير البطاقة في مكانها. لماذا هذا مهم لجلسة تراجع بطاقات عديدة ، وما خطر review مُحوِّرة إن أردت لاحقًا إظهار المتعلم «كيف ستتغيّر مجموعتك» قبل القبول؟
  • تقول SM-2 «كرر فورًا عند الفشل», وتضبط هذه التطبيقة due = today. لماذا due = today خيار أفضل من due = tomorrow ، وماذا يحدث في دماغ المتعلم حين تظهر بطاقة فاشلة مجددًا في الجلسة نفسها؟

الخطوة 3: جلسة التدريب

الآن يؤدي المدرِّس وظيفته: يفلتر البطاقات المستحقة, ويطالب المتعلم بإجابة ودرجة جودة, ويطبّق review, ويحدّث المجموعة.

3.1 ابنِ حلقة التفاعل

👟 تلميح البداية : تأخذ run_session المجموعة وعدّاد اليوم وكائنًا قابلاً للاستدعاء ask. الكائن هو ما يطالب المتعلم ، وهو ما يجعل الجلسة قابلة للاختبار.

# tutor.py (continued)
def run_session(cards: list[dict], today: int, ask) -> list[dict]:
    due = [i for i, c in enumerate(cards) if c["due"] <= today]
    print(f"{len(due)} due today")
    for idx in due:
        card = cards[idx]
        print("Q:", card["front"])
        _ = ask()           # learner's typed answer (free response)
        print("A:", card["back"])
        quality = int(ask()) # grade: 0 (blackout) to 5 (instant recall)
        cards[idx] = review(card, quality, today)
    return cards

cards[idx] = review(...) هو التحوير المتعمد الوحيد في المشروع كله: يُحدَّث المجموع في مكانها, وهذا ما تريده في جلسة حقيقية ينجو فيها نفس القائمة عبر عمر السكربت. تمرير ask بدل استخدام input() مباشرة هو ما يجعل الحلقة حتمية ، يستدعيها السكربت بنفس تسلسل الإجابات في كل مرة.

🎯 الناتج المتوقع (مُقيَّد) : 3 due today ، تُراجع البطاقات الثلاث ذات due <= 5, واحدة تلو الأخرى.

🩹 إذا لم يعمل : إن كان العدد خاطئًا, فقائمة due رُشحت ضد قيمة today مختلفة. إن استُدعي ask() مرة واحدة لكل بطاقة (بدل مرتين ، إجابة + درجة), فالحلقة تقصّر عند سطر _ = ask().

3.2 محاكِ جلسة بإجابات ثابتة

👟 تلميح البداية : ابنِ مكررًا مجهريًا يغذّي ask ، إجابة حرة واحدة, ثم درجة صحيحة واحدة, لكل بطاقة مستحقة ، فتكون الجلسة حتمية بالكامل.

# tutor.py (continued)
script = iter([
    "pear",  "5",    # py-1: correct, confident
    "pear",  "2",    # py-2: wrong answer
    "pear",  "4",    # py-3: close, thoughtful
])

run_session(DECK, today=5, ask=script.__next__)
print("after session:")
for c in DECK:
    print("  ", c["id"], "interval", c["interval"], "due", c["due"])
print("still due now:", [c["id"] for c in DECK if c["due"] <= 5])

list.__iter__ يعطي نفس القيم في نفس الترتيب في كل تشغيل ، هكذا تحصل على ناتج متوقع قابل للإنجاب من حلقة تكون في الاستخدام الحقيقي input(). بعد الجلسة تنمو py-1 (0 → يوم واحد, مستحقة يوم 6), وتُعاد ضبط py-2 (ما زالت مستحقة اليوم يوم 5), وتقفز py-3 (6 → 15 يومًا, مستحقة يوم 20). يسأل السطر الأخير «من ما زال مستحقًا؟» ، وتتحقق py-2 الفاشلة فقط.

🎯 الناتج المتوقع :

3 due today
Q: What does `zip(a, b)` do?
A: Pairs items from a and b into tuples, stopping at the shorter.
Q: When does `dict.get(k, d)` return `d`?
A: When `k` is missing from the dict.
Q: What does `sorted(d.items())` return?
A: A new list of `(key, value)` tuples, sorted by key.
after session:
   py-1 interval 1 due 6
   py-2 interval 1 due 5
   py-3 interval 15 due 20
   py-4 interval 6 due 8
still due now: ['py-2']

🩹 إذا لم يعمل : إن أظهر still due now ['py-2', 'py-3'], فـdue الجديدة لـpy-3 حُسبت 5 + 15 = 20 ، وهي ليست <= 5 ، فلتحقق أن due مضبوطة إلى today + interval, لا today + quality.

3.3 تحقّق من الجلسة

✅ قائمة التحقق

  • ✅ 3 بطاقات مستحقة بالضبط, وبعد الجلسة تبقى 1 (py-2) مستحقة.
  • ✅ مراجعة أولى ناجحة (py-1) تضبط interval=1 وdue=6.
  • ✅ مراجعة فاشلة (py-2) تُعيد due إلى today, لا إلى today + 1.

🤔 سؤال (أسئلة) سقراطي(ة) :

  • يُظهر السكربت «pear» ثلاث مرات كإجابة المتعلم ، المتعلم الحقيقي سيكتب كلمة مختلفة لكل بطاقة. لماذا يجعل iter المُقيَّد الناتج حتميًا, وماذا يغيّر input() داخل الحلقة حيال ذلك؟
  • بعد الجلسة ما زالت py-2 مستحقة. ماذا ستفعل في استدعاء run_session التالي كي يرى المتعلم البطاقة الفاشلة مجددًا دون أن يعيد رؤية بطاقات يعرفها؟

الخطوة 4: ثبّت التقدم ولخّص

مدرِّس ينسى المتعلم بين الجلسات ما هو إلا تطبيق اختبارات بخطوات إضافية. تحفظ هذه الخطوة المجموعة إلى JSON وتتحقق من الرحلة ذهابًا وإيابًا.

4.1 احفظ وحمّل

👟 تلميح البداية : اكتب save_deck وload_deck ، دالتين مجهريتين, كل واحدة سطر واحد من json.dump/json.load, مع إبقاء صيغة البيانات مفتوحة للفحص لاحقًا.

# tutor.py (continued)
def save_deck(cards: list[dict], path: str = "progress.json") -> None:
    with open(path, "w") as f:
        json.dump(cards, f, indent=2)
    print(f"saved {len(cards)} cards to {path}")

def load_deck(path: str = "progress.json") -> list[dict]:
    with open(path) as f:
        return json.load(f)

save_deck(DECK)
loaded = load_deck()
print("round-trip ok:", loaded == DECK)

indent=2 هو خيار التنسيق الذي يجعل JSON مقروءًا بشريًا في طرفية وصديق الفروقات في git ، قرار سطر واحد يدفع ثمنه كل مرة يقرأ فيها أحدٌ الملف يدويًا. فحص round-trip ok: True اختبار تكامل بسيط لكنه حقيقي: الملف على القرص يساوي المجموعة في الذاكرة, ما يعني أن الحفظ + التحميل لم يفقدا شيئًا أو يعيدا ترتيبه.

🎯 الناتج المتوقع : saved 4 cards to progress.json وround-trip ok: True.

🩹 إذا لم يعمل : إن طبع فحص الرحلة False, تحقق هل أُضيف last بعد حفظ JSON (الملف المحفوظ لن يحمله, لكن المجموعة في الذاكرة تحمله ، احفظ إما بعد تحديث last أو قبل). إن أخطأ json.dump بـTypeError: Object of type ndarray is not JSON serializable, فكانت إحدى قيم الفاصل أو الاستحقاق عددًا صحيحًا من numpy ، لفّها بـint(...).

4.2 تحقّق من الاستمرار

✅ قائمة التحقق

  • ✅ يوجد progress.json ويحوي 4 كائنات بطاقات, كل منها بحقول interval وdue وlast.
  • ✅ يُعيد load_deck() قائمة تساوي المجموعة المحفوظة.
  • ✅ حذف المجموعة من الذاكرة وإعادة التحميل ينتج نفس الحالة.

🤔 سؤال (أسئلة) سقراطي(ة) :

  • صيغة الاستمرار هذه تخزن المجموعة كلها كل مرة. حين تنمو المجموعة إلى 1000 بطاقة, هل هذا إسراف؟ ما تغيير json.dump الذي يكلّم صفًا واحدًا (بهيكل مختلف) يتيح تحديث بطاقة واحدة دون إعادة كتابة الملف كله؟
  • أُضيف حقل last داخل review فقط, فلن تحمله البطاقات التي لم تُراجع خلال الجلسة. كيف ستؤثر تلك اللامتسقة على ميزة «سجل المراجعات» مستقبلًا ، وما أبسط إصلاح؟

الخطوة 5: تلميح مدرِّس LLM اختياري

يقول لك مجدول متى تراجع; تلميح المدرِّس يقول لك كيف تفكر. تبني هذه الخطوة مطالبة حتمية وتُرسلها, عند وجود مفتاح, إلى LLM. يعمل المدرِّس تمامًا بدونها.

5.1 ألّف مطالبة التلميح

👟 تلميح البداية : ابنِ مطالبة تعطي LLM البطاقة والإجابة الخاطئة وتطلب دفعة ، لا الحل.

# tutor.py (continued)
def tutor_prompt(card: dict, wrong_answer: str) -> str:
    return (
        f"The learner answered '{wrong_answer}' for flashcard '{card['id']}'. "
        f"The card asks '{card['front']}' and the correct answer is "
        f"'{card['back']}'. Write a one-sentence hint nudging them toward the "
        f"answer without giving it away."
    )

missed = DECK[1]   # py-2, which was answered wrong
print(tutor_prompt(missed, "When the key equals the default"))

تضمين الإجابة الخاطئة في المطالبة يعطي LLM شيئًا يصحّحه ، يمكنه كتابة «الافتراضي d يُعاد فقط عند إخفاق, لا إصابة» بدل شرح عام. قيد «جملة واحدة» يمنع التلميح من التحول إلى محاضرة.

🎯 الناتج المتوقع : جملة واحدة تبدأ The learner answered 'When the key equals the default' for flashcard 'py-2'. The card asks 'When does \dict.get(k, d)` return `d`?’ and the correct answer is ‘When `k` is missing from the dict.’. Write a one-sentence hint nudging them toward the answer without giving it away.` ، لاحظ: المطالبة الكاملة, لا استجابة LLM.

🩹 إذا لم يعمل : إن كان معرّف البطاقة py-1 بدل py-2, فاستُخدم DECK[1] ضد مجموعة غير مرتبة ، تحقق من ترتيب البطاقات, أو غيّر الفهرس ليطابق البطاقة الفاشلة من الخطوة 3.

5.2 نفّذ التلميح (اختياري)

👟 تلميح البداية : افحص OPENAI_API_KEY أولًا; وإن غاب, احفظ المطالبة للاستخدام اليدوي. لا تعطّل المدرِّس أبدًا على مفتاح مفقود.

# tutor.py (continued)
def maybe_hint(prompt: str) -> None:
    import getpass, os
    key = os.environ.get("OPENAI_API_KEY") or getpass.getpass("OpenAI key (blank to skip): ")
    if not key:
        with open("hint_prompt.txt", "w") as f:
            f.write(prompt)
        print("no key — prompt saved to hint_prompt.txt")
        return
    print("key present — a real API call would go here")

maybe_hint(tutor_prompt(DECK[1], "When the key equals the default"))

نفس النمط المتقهقر الأنيق للطبقات الاختيارية السابقة: متغير البيئة يخدم CI, وgetpass يخدم طرفية, والسلسلة الفارغة هي منحدر الخروج. حفظ المطالبة يعني أن خطوة التلميح ليست المعنى الحقيقي أبدًا ، الصقها في أي نموذج, احصل على تلميح, انسخه مجددًا إلى ناتج حلقة التدريب المطبوع.

🎯 الناتج المتوقع : no key ، prompt saved to hint_prompt.txt.

🩹 إذا لم يعمل : إن كان الملف فارغًا, فسلسلة المطالبة استُهلكت بأول print (تُقيَّم f-strings مرة واحدة) ، أعد اسنادها, لا تطبعها مرتين. إن طبع GetPassWarning, فالطرفية غير تفاعلية ولا OPENAI_API_KEY مضبوط ، صدّر المتغير بدل ذلك.

5.3 تحقّق من طبقة المدرِّس

✅ قائمة التحقق

  • ✅ تُعيد tutor_prompt دائمًا سلسلة تتضمن حقول البطاقة الفعلية ، لا نصوص بديلة عامة.
  • ✅ بلا مفتاح, يوجد hint_prompt.txt ويحوي سلسلة المطالبة الحرفية.
  • ✅ تعمل طبقة تلميح المدرِّس في نهاية المسار, فالفشل هنا لا يعطّل المجدول أو الاستمرار أبدًا.

🤔 سؤال (أسئلة) سقراطي(ة) :

  • تطلب مطالبة المدرِّس «دفعة دون إعطاء الإجابة». ما التغيير الذي يجعل المطالبة قابلة للحقن بأمان عبر أنواع البطاقات ، ولماذا يهم الاقتباس الصريح (هروب ') في wrong_answer حين تدخل السلسلة مطالبة؟
  • مسار مفتاح getpass تفاعلي; مسار OPENAI_API_KEY ليس كذلك. لأي نوع من التوزيع يكون مسار متغير البيئة فعلًا أأمن ، وما الخطأ الشائع الذي يجعل كليهما غير آمن بنفس القدر؟

⚠️ مآزق شائعة

  • تحوير البطاقة بدل إرجاع النسخة. review دالة نقيّة بالتصميم ، ناتجها قاموس جديد. إن حوّرت البطاقة الأصلية داخل review, فلن تستطيع حلقة الجلسة إظهار فرق قبل/بعد, وتفقد القدرة على «تراجع» درجة خاطئة.
  • استخدام due = today + quality بدل today + interval. الفاصل ينمو; الدرجة عدد صحيح صغير من 0–5. مزجهما ينتج تواريخ استحقاق مستقبلية غير معقولة, والخطأ غير مرئي حتى الجلسة التالية.
  • استدعاء ask() داخل input() للحلقة القابلة للتمرين. input() في سكربت اختبار يعلّق. مرّر ask ككائن قابل للاستدعاء وابنِ غلاف input حقيقيًا للاستخدام التفاعلي ، هذا النمط هو ما يجعل المدرِّس قابلًا للاختبار والاستخدام معًا.
  • الحفظ قبل ضبط last. حقل last يُضاف فقط أثناء review, فلن تحمله البطاقات التي لم تُراجع في جلسة في الملف المحفوظ ، وسترى عملية تحميل لاحقة لاتساق مخطط. إمّا تهيّئ last في new_card أو احفظ بعد جلسة كاملة.
  • الحفظ كقائمة مسطّحة. قائمة قواميس تكفي لمجموعة صغيرة, لكن متعلمين اثنين لا يمكنهما مشاركة الملف نفسه, ولا توجد بيانات وصفية لكل مجموعة. قاموس مفاتيحه أسماء المجموعات ترقية قليلة الجهد تحصّن الصيغة مستقبلًا.

ما بنيته للتو

مدرِّس يتذكر ما تضعف فيه, ويجدول المراجعات برياضيات حقيقية, ويثبّت حالته بأمانة, ويستطيع صوغ تلميح دافع حين يتوفر LLM. محرك SM-2 دالة خمسة أسطر; والباقي أنابيب بيانات نظيفة ، مجموعة قواميس, وحلقة تفلتر وتحوّر, وملف JSON. الشكل القابل للنقل ، دالة نقيّة لمنطق الجوهر, حلقة جلسة قابلة للتحوير لتحديثات الحالة, JSON للاستمرار, وLLM اختياري للذكاء ، هو نفس الهيكل العظمي خلف أدوات التخطيط وتتبّع العادات وأي أداة خفيفة تحمل حالة.

شغّل نسخة أكمل دون أي إعداد محلي

examples/ai-tutor/ في مستودع الدورة هو المدرِّس كاملًا كدفتر ، نفس مجموعة البداية, والجلسة المُقيَّدة, وفحص الاستمرار, وتلميح LLM الاختياري, كلها قابلة للتشغيل في Colab/Kaggle/Binder. استنسخ المستودع أو افتحه في Codespace.

إلى أين تذهب من هنا

  • أضف سطر أرجعة افتراضية hint = card["hint"]: حين يخطئ المتعلم بطاقة ولا يوجد مفتاح API, اطبع التلميح المخزّن فورًا ، يساعد المدرِّس الآن حتى دون LLM.
  • ابنِ دالة stats() تقرأ progress.json وتطبع أطول سلسلة نجاح للمتعلم, ومتوسط الدرجات, والبطاقات المستحقة في الأيام السبعة القادمة.
  • أضف مجموعة ثانية (كمجموعة مفردات) وعلم --deck كي يخدم نفس CLI مواضيع متعددة.
  • خزّن سجل جلسة كملف JSON ثانٍ بقائمة صفوف {id, quality, timestamp} ، البيانات الأولية لمخطط تقدم لاحقًا.

شارك مشروعك مع الصف

بنيت شيئًا تفخر به؟ examples/student-projects/ معرض لمشاريع قدّمها طلاب آخرون ، وREADME الخاص به يحوي شرحًا كاملًا وودودًا للمبتدئين لإضافة مشروعك عبر pull request, حتى لو لم تستخدم git من قبل قط: عمل fork للمستودع, وإنشاء فرع, وتثبيت ملفاتك, وفتح الـ PR, خطوة بخطوة. لا يُفترض أي خبرة سابقة بـ git.

مرحبًا بك في كتابة Python خارج المتصفح. 🎓

أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.