Apply ZIP update: 205-didactopus-learner-state-progression-update.zip [2026-03-14T13:20:33]
This commit is contained in:
parent
adac1e1637
commit
ed60c1d8f2
|
|
@ -6,18 +6,7 @@ build-backend = "setuptools.build_meta"
|
||||||
name = "didactopus"
|
name = "didactopus"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = ["pydantic>=2.7", "pyyaml>=6.0"]
|
||||||
"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"
|
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from datetime import datetime, timezone
|
||||||
from .learner_state import LearnerState, EvidenceEvent, MasteryRecord
|
from .learner_state import LearnerState, EvidenceEvent, MasteryRecord
|
||||||
|
|
||||||
|
def _parse_ts(ts: str) -> datetime:
|
||||||
|
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||||||
|
|
||||||
def apply_evidence(
|
def apply_evidence(
|
||||||
state: LearnerState,
|
state: LearnerState,
|
||||||
event: EvidenceEvent,
|
event: EvidenceEvent,
|
||||||
|
|
@ -19,13 +23,34 @@ def apply_evidence(
|
||||||
)
|
)
|
||||||
state.records.append(rec)
|
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))
|
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(
|
rec.confidence = min(
|
||||||
1.0,
|
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.evidence_count += 1
|
||||||
rec.last_updated = event.timestamp
|
rec.last_updated = event.timestamp
|
||||||
state.history.append(event)
|
state.history.append(event)
|
||||||
return state
|
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
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,14 @@ def recommend_next_concepts(
|
||||||
for concept in concepts:
|
for concept in concepts:
|
||||||
cid = concept.get("id")
|
cid = concept.get("id")
|
||||||
prereqs = list(concept.get("prerequisites", []) or [])
|
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:
|
if ready:
|
||||||
existing = state.get_record(cid, dimension)
|
existing = state.get_record(cid, dimension)
|
||||||
if existing is None or existing.score < min_score or existing.confidence < min_confidence:
|
if existing is None or existing.score < min_score or existing.confidence < min_confidence:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue