polyglot-tutor / src /tutor /domain /models.py
blizzarman's picture
feat: M0 walking skeleton (app, service protocols, CI/CD, Space deploy)
00d5ae5
Raw
History Blame Contribute Delete
3.14 kB
"""Core domain entities, persistence- and framework-agnostic.
Languages are first-class data (`source_lang` / `target_lang`, ISO 639-1) rather
than constants: English is the launch target, but the model must not need a
migration to add Spanish or German later.
"""
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any
from uuid import uuid4
from pydantic import BaseModel, Field
def _new_id() -> str:
return uuid4().hex
def _now() -> datetime:
return datetime.now(UTC)
class CEFRLevel(StrEnum):
A1 = "A1"
A2 = "A2"
B1 = "B1"
B2 = "B2"
C1 = "C1"
C2 = "C2"
@property
def rank(self) -> int:
"""0-based ordinal position (A1=0 ... C2=5), for ordinal metrics and adjacency."""
return list(CEFRLevel).index(self)
def distance(self, other: "CEFRLevel") -> int:
"""Absolute level distance; `<= 1` is the classic 'adjacent accuracy' criterion."""
return abs(self.rank - other.rank)
class ExerciseType(StrEnum):
READING_QA = "reading_qa" # M1 β€” comprehension questions on a CEFR-graded text
DICTATION = "dictation" # M2 β€” TTS audio, learner types what they hear
WRITING = "writing" # M3 β€” free writing, LLM correction with typed errors
PRONUNCIATION = "pronunciation" # M5 β€” read-aloud with pronunciation scoring
class Learner(BaseModel):
id: str = Field(default_factory=_new_id)
display_name: str
source_lang: str = "fr"
target_lang: str = "en"
created_at: datetime = Field(default_factory=_now)
class Exercise(BaseModel):
"""A generated, cacheable exercise.
`payload` is type-specific content (text, questions, audio reference...).
Generated exercises are content-addressed upstream (hash of source text +
prompt version) so LLM cost stays ~0 and evals are reproducible.
"""
id: str = Field(default_factory=_new_id)
type: ExerciseType
target_lang: str
cefr_level: CEFRLevel
payload: dict[str, Any] = Field(default_factory=dict)
created_at: datetime = Field(default_factory=_now)
class Attempt(BaseModel):
"""One learner answer to one exercise, with its scoring/feedback."""
id: str = Field(default_factory=_new_id)
learner_id: str
exercise_id: str
response: dict[str, Any] = Field(default_factory=dict)
score: float | None = Field(default=None, ge=0.0, le=1.0)
feedback: dict[str, Any] = Field(default_factory=dict)
created_at: datetime = Field(default_factory=_now)
class ReviewItem(BaseModel):
"""An atomic, schedulable knowledge item (vocab word, recurring error, ...).
`scheduler_state` is an opaque dict owned by the SRS engine (FSRS-ready in
M4) so the domain model is not coupled to one algorithm's parameters.
"""
id: str = Field(default_factory=_new_id)
learner_id: str
kind: str # "vocab" | "error" | ... (free-form until M4 hardens it)
content: dict[str, Any] = Field(default_factory=dict)
due_at: datetime | None = None
scheduler_state: dict[str, Any] = Field(default_factory=dict)
created_at: datetime = Field(default_factory=_now)