from dataclasses import dataclass, field from typing import Dict, List, Optional @dataclass class BKTParams: p_init: float = 0.10 p_transit: float = 0.30 p_slip: float = 0.10 p_guess: float = 0.20 @dataclass class ObservationRecord: concept: str correct: bool mastery_before: float mastery_after: float bloom_level: int = 1 question_id: str = "" class BayesianKnowledgeTracing: def __init__(self, default_params: Optional[BKTParams] = None): self.default_params = default_params or BKTParams() self.concept_params: Dict[str, BKTParams] = {} self._mastery: Dict[str, float] = {} self.history: List[ObservationRecord] = [] def get_mastery(self, concept: str) -> float: return self._mastery.get( concept, self.concept_params.get(concept, self.default_params).p_init ) def set_params(self, concept: str, params: BKTParams): self.concept_params[concept] = params def update( self, concept: str, correct: bool, bloom_level: int = 1, question_id: str = "", ) -> float: params = self.concept_params.get(concept, self.default_params) p_l = self.get_mastery(concept) if correct: p_obs_l1 = 1.0 - params.p_slip p_obs_l0 = params.p_guess else: p_obs_l1 = params.p_slip p_obs_l0 = 1.0 - params.p_guess p_obs = p_obs_l1 * p_l + p_obs_l0 * (1.0 - p_l) if p_obs < 1e-12: p_obs = 1e-12 p_l_given_obs = (p_obs_l1 * p_l) / p_obs p_l_next = p_l_given_obs + (1.0 - p_l_given_obs) * params.p_transit p_l_next = min(max(p_l_next, 0.0), 1.0) self._mastery[concept] = p_l_next self.history.append(ObservationRecord( concept=concept, correct=correct, mastery_before=p_l, mastery_after=p_l_next, bloom_level=bloom_level, question_id=question_id, )) return p_l_next def get_topic_mastery(self, concepts: List[str]) -> float: if not concepts: return 0.0 return sum(self.get_mastery(c) for c in concepts) / len(concepts) def all_masteries(self) -> Dict[str, float]: return dict(self._mastery) def concept_history(self, concept: str) -> List[ObservationRecord]: return [r for r in self.history if r.concept == concept] def summary(self) -> str: lines = ["BKT Mastery Summary:"] for concept, mastery in sorted(self._mastery.items(), key=lambda x: -x[1]): hist = self.concept_history(concept) attempts = len(hist) correct = sum(1 for r in hist if r.correct) lines.append( f" {concept:<30} {mastery:.3f} ({correct}/{attempts} correct)" ) return "\n".join(lines)