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