Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

231 lines
6.7 KiB
Python

from __future__ import annotations
from enum import Enum
import time
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field
_KNOWLEDGE_TYPE_LEGACY: dict[str, str] = {
"记忆型": "memory",
"概念型": "concept",
"程序型": "procedure",
"设计型": "design",
}
_ERROR_TYPE_LEGACY: dict[str, str] = {
"知识结构性": "structural",
"理解偏差型": "deviation",
"应用错误": "application",
"元认知型": "metacognitive",
}
class KnowledgeType(str, Enum):
MEMORY = "memory"
CONCEPT = "concept"
PROCEDURE = "procedure"
DESIGN = "design"
@classmethod
def _missing_(cls, value: object) -> KnowledgeType | None:
mapped = _KNOWLEDGE_TYPE_LEGACY.get(str(value))
return cls(mapped) if mapped else None
class ErrorType(str, Enum):
KNOWLEDGE_STRUCTURAL = "structural"
UNDERSTANDING_DEVIATION = "deviation"
APPLICATION_ERROR = "application"
METACOGNITIVE = "metacognitive"
@classmethod
def _missing_(cls, value: object) -> ErrorType | None:
mapped = _ERROR_TYPE_LEGACY.get(str(value))
return cls(mapped) if mapped else None
# Stages removed in the Mastery Path simplification are mapped onto the nearest
# surviving stage so progress persisted by the older engine still deserializes.
_STAGE_LEGACY: dict[str, str] = {
"diagnostic_phase1": "diagnostic",
"diagnostic_phase2": "diagnostic",
"metacognitive_intro": "explain",
"plan": "explain",
"pretest": "explain",
"practice_quiz": "practice",
"module_test": "review",
}
class LearningStage(str, Enum):
"""The Mastery Path loop: diagnose once, then per knowledge point teach and
check understanding, then practice the module, diagnose errors, and schedule
spaced review."""
DIAGNOSTIC = "diagnostic"
EXPLAIN = "explain"
FEYNMAN_CHECK = "feynman_check"
PRACTICE = "practice"
ERROR_DIAGNOSIS = "error_diagnosis"
REVIEW = "review"
COMPLETED = "completed"
@classmethod
def _missing_(cls, value: object) -> LearningStage | None:
mapped = _STAGE_LEGACY.get(str(value))
return cls(mapped) if mapped else None
class KnowledgePoint(BaseModel):
model_config = ConfigDict(extra="ignore")
id: str
name: str
type: KnowledgeType
module_id: str
class LearningModule(BaseModel):
model_config = ConfigDict(extra="ignore")
id: str
name: str
order: int
pass_threshold: float = 0.7
knowledge_points: list[KnowledgePoint] = Field(default_factory=list)
class DiagnosticResult(BaseModel):
model_config = ConfigDict(extra="ignore")
total_questions: int = 0
correct_count: int = 0
module_mastery: dict[str, float] = Field(default_factory=dict)
class QuizAttempt(BaseModel):
model_config = ConfigDict(extra="ignore")
question_id: str
knowledge_point_id: str
module_id: str = ""
is_correct: bool
user_answer: Any = None
error_type: ErrorType | None = None
self_attribution: str = ""
mastery_estimate: float = 0.0
timestamp: float = Field(default_factory=time.time)
class RetryAttempt(BaseModel):
model_config = ConfigDict(extra="ignore")
timestamp: float
is_correct: bool
attempt_number: int
class ErrorRecord(BaseModel):
model_config = ConfigDict(extra="ignore")
id: str
question_id: str
knowledge_point_id: str
module_id: str
error_type: ErrorType
self_attribution: str = ""
ai_confirmation: str = ""
retry_history: list[RetryAttempt] = Field(default_factory=list)
status: Literal["active", "retrying", "review", "graduated"] = "active"
created_at: float = Field(default_factory=time.time)
class RepetitionState(BaseModel):
model_config = ConfigDict(extra="ignore")
interval_index: int = 0
consecutive_correct: int = 0
consecutive_wrong: int = 0
next_review_at: float
class ReviewTask(BaseModel):
model_config = ConfigDict(extra="ignore")
id: str
knowledge_point_id: str
knowledge_type: KnowledgeType
due_at: float
priority: int
state: RepetitionState
class PendingQuestion(BaseModel):
"""A question posed to the learner and awaiting their answer.
Persisted so grading is deterministic across turns: the expected answer
lives here server-side and never round-trips through the model. The tutor
poses a question with ``mastery_quiz`` (storing this), the learner answers
on a later turn, and ``mastery_grade`` scores the stored answer.
"""
model_config = ConfigDict(extra="ignore")
question_id: str
knowledge_point_id: str
module_id: str = ""
prompt: str = ""
question_type: str = "short"
expected_answer: str = ""
options: list[str] = Field(default_factory=list)
created_at: float = Field(default_factory=time.time)
class LearningProgress(BaseModel):
model_config = ConfigDict(extra="ignore")
book_id: str
diagnostic: DiagnosticResult | None = None
modules: list[LearningModule] = Field(default_factory=list)
current_module_id: str = ""
current_stage: LearningStage = LearningStage.DIAGNOSTIC
current_kp_index: int = 0
mastery_levels: dict[str, float] = Field(default_factory=dict)
# Qualitative gate for CONCEPT / DESIGN knowledge points: True once the
# tutor judges the learner's explanation sufficient (``mastery_assess``).
# The quantitative ``mastery_levels`` gate covers MEMORY / PROCEDURE.
qualitative_mastery: dict[str, bool] = Field(default_factory=dict)
knowledge_types: dict[str, KnowledgeType] = Field(default_factory=dict)
quiz_attempts: list[QuizAttempt] = Field(default_factory=list)
error_records: list[ErrorRecord] = Field(default_factory=list)
repetition_states: dict[str, RepetitionState] = Field(default_factory=dict)
review_queue: list[ReviewTask] = Field(default_factory=list)
# A single outstanding question; grading reads its expected answer so the
# model never has to recall it across turns.
pending_question: PendingQuestion | None = None
feynman_retries: dict[str, int] = Field(default_factory=dict)
feynman_explanations: dict[str, str] = Field(default_factory=dict)
stage_failure_counts: dict[str, int] = Field(default_factory=dict)
stage_failure_notes: dict[str, str] = Field(default_factory=dict)
version: int = 0
created_at: float = Field(default_factory=time.time)
updated_at: float = Field(default_factory=time.time)
__all__ = [
"KnowledgeType",
"ErrorType",
"LearningStage",
"KnowledgePoint",
"LearningModule",
"DiagnosticResult",
"QuizAttempt",
"RetryAttempt",
"ErrorRecord",
"RepetitionState",
"ReviewTask",
"PendingQuestion",
"LearningProgress",
]