study-partner / schema.py
nz-nz's picture
Deploy Recall study-partner app (stub-mode demo)
7563305 verified
Raw
History Blame Contribute Delete
3 kB
"""
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())
)