AI_math / tutor /progress_store.py
NSamson1's picture
Create tutor/progress_store.py
0bc4913 verified
"""tutor/progress_store.py โ€” SQLite-backed learner progress store."""
from __future__ import annotations
import json, sqlite3, time, uuid
from pathlib import Path
from typing import Any, Dict, Optional
_SCHEMA = """
PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS learners (
learner_id TEXT PRIMARY KEY,
display_name TEXT,
created_at REAL
);
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
learner_id TEXT,
started_at REAL,
ended_at REAL,
state_json TEXT,
lang TEXT
);
CREATE TABLE IF NOT EXISTS responses (
response_id TEXT PRIMARY KEY,
learner_id TEXT,
session_id TEXT,
item_id TEXT,
skill TEXT,
difficulty REAL,
is_correct INTEGER,
latency_ms INTEGER,
ts REAL
);
"""
class ProgressStore:
def __init__(self, db_path=":memory:"):
self._path = str(db_path)
self._conn = sqlite3.connect(self._path, check_same_thread=False)
self._conn.executescript(_SCHEMA)
self._conn.commit()
# โ”€โ”€ Learners โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def add_learner(self, learner_id: str, display_name: str = "") -> None:
self._conn.execute(
"INSERT OR IGNORE INTO learners VALUES (?,?,?)",
(learner_id, display_name or learner_id, time.time()))
self._conn.commit()
# โ”€โ”€ Sessions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def start_session(self, learner_id: str, state: Dict, lang: str = "en") -> str:
sid = str(uuid.uuid4())
self._conn.execute(
"INSERT INTO sessions VALUES (?,?,?,?,?,?)",
(sid, learner_id, time.time(), None, json.dumps(state), lang))
self._conn.commit()
return sid
def end_session(self, session_id: str, state: Dict) -> None:
self._conn.execute(
"UPDATE sessions SET ended_at=?, state_json=? WHERE session_id=?",
(time.time(), json.dumps(state), session_id))
self._conn.commit()
def load_latest_state(self, learner_id: str) -> Optional[Dict]:
row = self._conn.execute(
"SELECT state_json FROM sessions WHERE learner_id=? "
"ORDER BY started_at DESC LIMIT 1", (learner_id,)).fetchone()
if row and row[0]:
try:
return json.loads(row[0])
except Exception:
pass
return None
# โ”€โ”€ Responses โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def log_response(self, learner_id: str, session_id: str, item_id: str,
skill: str, difficulty: float, is_correct: bool,
latency_ms: int) -> None:
self._conn.execute(
"INSERT INTO responses VALUES (?,?,?,?,?,?,?,?,?)",
(str(uuid.uuid4()), learner_id, session_id, item_id,
skill, difficulty, int(is_correct), latency_ms, time.time()))
self._conn.commit()
# โ”€โ”€ Reports โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def skill_summary(self, learner_id: str) -> Dict[str, Any]:
rows = self._conn.execute(
"SELECT skill, COUNT(*) as n, SUM(is_correct) as c "
"FROM responses WHERE learner_id=? GROUP BY skill",
(learner_id,)).fetchall()
return {r[0]: {"attempts": r[1], "correct": r[2],
"accuracy": round(r[2]/r[1], 3) if r[1] else 0}
for r in rows}