File size: 2,996 Bytes
7563305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
"""
Recall — shared data contract (§4 of the main plan).

This is the single source of truth all three modules build against.
We use plain dicts (Gradio gr.State friendly) + light dataclasses/factories
so nobody is blocked on each other. DO NOT change these shapes without a
team sync — content_pipeline, learning_engine, and app.py all depend on them.
"""
from __future__ import annotations

import uuid
from typing import Optional, TypedDict


# ---- Core types ------------------------------------------------------------

class Card(TypedDict):
    id: str
    question: str
    answer: str            # reference answer
    topic: str             # short tag, e.g. "Photosynthesis"
    source_chunk: str      # text it came from (grounding / explanations)
    difficulty: int        # 1 (easy) .. 3 (hard)
    parent_id: Optional[str]  # set when this card is a generated follow-up


class CardState(TypedDict):
    card_id: str
    ease: float            # SM-2-style, starts 2.5
    interval: int          # positions until it reappears in the queue
    reps: int
    lapses: int
    last_grade: int        # 0..5 from the grader


class GradeResult(TypedDict):
    score: int             # 0..5
    correct: bool          # score >= 3
    explanation: str       # shown to the user
    missed_concept: str    # seed for follow-up generation


class Session(TypedDict):
    deck: list[Card]
    states: dict[str, CardState]   # card_id -> CardState
    queue: list[str]               # ordered card_ids to serve
    history: list[dict]            # {card_id, user_answer, grade}
    streak: int


# ---- Factories (use these instead of building dicts by hand) ---------------

def new_card(
    question: str,
    answer: str,
    topic: str = "General",
    source_chunk: str = "",
    difficulty: int = 1,
    parent_id: Optional[str] = None,
    card_id: Optional[str] = None,
) -> Card:
    return Card(
        id=card_id or str(uuid.uuid4()),
        question=question,
        answer=answer,
        topic=topic,
        source_chunk=source_chunk,
        difficulty=difficulty,
        parent_id=parent_id,
    )


def new_card_state(card_id: str) -> CardState:
    return CardState(
        card_id=card_id, ease=2.5, interval=1, reps=0, lapses=0, last_grade=0
    )


def new_grade(score: int, explanation: str, missed_concept: str = "") -> GradeResult:
    score = max(0, min(5, int(score)))
    return GradeResult(
        score=score,
        correct=score >= 3,
        explanation=explanation,
        missed_concept=missed_concept,
    )


def validate_card(card: dict) -> bool:
    """Cheap guard so a malformed model card never crashes the deck."""
    required = ("id", "question", "answer", "topic", "source_chunk",
                "difficulty", "parent_id")
    return (
        isinstance(card, dict)
        and all(k in card for k in required)
        and bool(str(card.get("question", "")).strip())
        and bool(str(card.get("answer", "")).strip())
    )