From ed60c1d8f20808882bb9700a9bd6edca47332135 Mon Sep 17 00:00:00 2001 From: welsberr Date: Sat, 14 Mar 2026 13:29:56 -0400 Subject: [PATCH] Apply ZIP update: 205-didactopus-learner-state-progression-update.zip [2026-03-14T13:20:33] --- pyproject.toml | 13 +------------ src/didactopus/progression_engine.py | 29 ++++++++++++++++++++++++++-- src/didactopus/recommendations.py | 9 ++++++++- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 083045d..1fcb487 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,18 +6,7 @@ build-backend = "setuptools.build_meta" name = "didactopus" version = "0.1.0" requires-python = ">=3.10" -dependencies = [ - "pydantic>=2.7", - "fastapi>=0.115", - "uvicorn>=0.30", - "sqlalchemy>=2.0", - "passlib[bcrypt]>=1.7", - "python-jose[cryptography]>=3.3" -] - -[project.scripts] -didactopus-api = "didactopus.api:main" -didactopus-export-svg = "didactopus.export_svg:main" +dependencies = ["pydantic>=2.7", "pyyaml>=6.0"] [tool.setuptools.packages.find] where = ["src"] diff --git a/src/didactopus/progression_engine.py b/src/didactopus/progression_engine.py index 07326cf..9dac940 100644 --- a/src/didactopus/progression_engine.py +++ b/src/didactopus/progression_engine.py @@ -1,6 +1,10 @@ from __future__ import annotations +from datetime import datetime, timezone from .learner_state import LearnerState, EvidenceEvent, MasteryRecord +def _parse_ts(ts: str) -> datetime: + return datetime.fromisoformat(ts.replace("Z", "+00:00")) + def apply_evidence( state: LearnerState, event: EvidenceEvent, @@ -19,13 +23,34 @@ def apply_evidence( ) state.records.append(rec) + prev_score = rec.score + prev_conf = rec.confidence + + # weighted incremental update 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.score = ((prev_score * rec.evidence_count) + (event.score * weight)) / max(1, rec.evidence_count + 1) + + # confidence grows with repeated evidence and quality, but is bounded 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))), + max( + 0.0, + prev_conf * (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 decay_confidence(state: LearnerState, now_ts: str, daily_decay: float = 0.0025) -> LearnerState: + now = _parse_ts(now_ts) + for rec in state.records: + if not rec.last_updated: + continue + then = _parse_ts(rec.last_updated) + delta_days = max(0.0, (now - then).total_seconds() / 86400.0) + factor = max(0.0, 1.0 - daily_decay * delta_days) + rec.confidence = max(0.0, rec.confidence * factor) + return state diff --git a/src/didactopus/recommendations.py b/src/didactopus/recommendations.py index 515980b..7b90483 100644 --- a/src/didactopus/recommendations.py +++ b/src/didactopus/recommendations.py @@ -13,7 +13,14 @@ def recommend_next_concepts( for concept in concepts: cid = concept.get("id") prereqs = list(concept.get("prerequisites", []) or []) - ready = concept_ready(state, cid, prereqs, dimension=dimension, min_score=min_score, min_confidence=min_confidence) + ready = concept_ready( + state, + cid, + prereqs, + dimension=dimension, + min_score=min_score, + min_confidence=min_confidence, + ) if ready: existing = state.get_record(cid, dimension) if existing is None or existing.score < min_score or existing.confidence < min_confidence: