60 lines
3.1 KiB
Python
60 lines
3.1 KiB
Python
from __future__ import annotations
|
|
from .models import LearnerState, EvidenceEvent, MasteryRecord, PackData
|
|
|
|
def get_record(state: LearnerState, concept_id: str, dimension: str = "mastery") -> MasteryRecord | None:
|
|
for rec in state.records:
|
|
if rec.concept_id == concept_id and rec.dimension == dimension:
|
|
return rec
|
|
return None
|
|
|
|
def apply_evidence(state: LearnerState, event: EvidenceEvent, decay: float = 0.05, reinforcement: float = 0.25) -> LearnerState:
|
|
rec = get_record(state, event.concept_id, event.dimension)
|
|
if rec is None:
|
|
rec = MasteryRecord(concept_id=event.concept_id, dimension=event.dimension, score=0.0, confidence=0.0, evidence_count=0, last_updated=event.timestamp)
|
|
state.records.append(rec)
|
|
weight = max(0.05, min(1.0, event.confidence_hint))
|
|
rec.score = ((rec.score * rec.evidence_count) + (event.score * weight)) / max(1, rec.evidence_count + 1)
|
|
rec.confidence = min(1.0, max(0.0, rec.confidence * (1.0 - decay) + reinforcement * weight + 0.10 * max(0.0, min(1.0, event.score))))
|
|
rec.evidence_count += 1
|
|
rec.last_updated = event.timestamp
|
|
state.history.append(event)
|
|
return state
|
|
|
|
def prereqs_satisfied(state: LearnerState, concept, min_score: float = 0.65, min_confidence: float = 0.45) -> bool:
|
|
for pid in concept.prerequisites:
|
|
rec = get_record(state, pid, concept.masteryDimension)
|
|
if rec is None or rec.score < min_score or rec.confidence < min_confidence:
|
|
return False
|
|
return True
|
|
|
|
def concept_status(state: LearnerState, concept, min_score: float = 0.65, min_confidence: float = 0.45) -> str:
|
|
rec = get_record(state, concept.id, concept.masteryDimension)
|
|
if rec and rec.score >= min_score and rec.confidence >= min_confidence:
|
|
return "mastered"
|
|
if prereqs_satisfied(state, concept, min_score, min_confidence):
|
|
return "active" if rec else "available"
|
|
return "locked"
|
|
|
|
def recommend_next(state: LearnerState, pack: PackData) -> list[dict]:
|
|
cards = []
|
|
for concept in pack.concepts:
|
|
status = concept_status(state, concept)
|
|
rec = get_record(state, concept.id, concept.masteryDimension)
|
|
if status in {"available", "active"}:
|
|
cards.append({
|
|
"id": concept.id,
|
|
"title": f"Work on {concept.title}",
|
|
"minutes": 15 if status == "available" else 10,
|
|
"reason": "Prerequisites are satisfied, so this is the best next unlock." if status == "available" else "You have started this concept, but mastery is not yet secure.",
|
|
"why": [
|
|
"Prerequisite check passed",
|
|
f"Current score: {rec.score:.2f}" if rec else "No evidence recorded yet",
|
|
f"Current confidence: {rec.confidence:.2f}" if rec else "Confidence starts after your first exercise",
|
|
],
|
|
"reward": concept.exerciseReward or f"{concept.title} progress recorded",
|
|
"conceptId": concept.id,
|
|
"scoreHint": 0.82 if status == "available" else 0.76,
|
|
"confidenceHint": 0.72 if status == "available" else 0.55,
|
|
})
|
|
return cards[:4]
|