Teacher-AI / app /progress.py
Moaaz2os's picture
Upload 6 files
7e0eb7b verified
Raw
History Blame Contribute Delete
9.56 kB
"""
progress.py — Student progress engine.
Handles:
- recording attempts
- updating mastery scores (decay + boost)
- streak tracking
- computing weak/strong topics
- generating weekly report
- generating personalised study plan
- next difficulty recommendation (adaptive)
"""
from datetime import datetime, date, timedelta
from database import db
import math
# ─────────────────────────────────────────
# MASTERY FORMULA
# mastery = correct / total (weighted)
# decay = 0.95 per day inactive
# boost = +0.05 per consecutive correct
# range = [0.0, 1.0]
# ─────────────────────────────────────────
DECAY_PER_DAY = 0.95
BOOST_ON_CORRECT = 0.05
MIN_MASTERY = 0.0
MAX_MASTERY = 1.0
def record_attempt(
student_id: int,
question: str,
topic: str,
error_type: str,
response: str,
difficulty: int,
mode: str = "text",
explain_level: str = "normal",
) -> int:
"""Insert attempt row, update mastery + error log + streak. Returns attempt id."""
with db() as conn:
cur = conn.execute(
"""INSERT INTO attempts
(student_id, question, topic, error_type, response, difficulty, mode, explain_level)
VALUES (?,?,?,?,?,?,?,?)""",
(student_id, question, topic, error_type, response, difficulty, mode, explain_level),
)
attempt_id = cur.lastrowid
# ── update mastery ──
conn.execute(
"""INSERT INTO topic_mastery (student_id, topic, attempts, correct, mastery_score)
VALUES (?,?,1,0,0.0)
ON CONFLICT(student_id, topic) DO UPDATE SET
attempts = attempts + 1,
last_updated = datetime('now')""",
(student_id, topic),
)
# treat "None detected" errors as correct
is_correct = error_type in (None, "", "None detected")
if is_correct:
conn.execute(
"""UPDATE topic_mastery SET
correct = correct + 1,
mastery_score = MIN(1.0, mastery_score + ?),
last_updated = datetime('now')
WHERE student_id=? AND topic=?""",
(BOOST_ON_CORRECT, student_id, topic),
)
else:
# error: log it and slightly decrease mastery
conn.execute(
"""INSERT INTO error_log (student_id, topic, error_type, count, last_seen)
VALUES (?,?,?,1,datetime('now'))
ON CONFLICT(student_id, topic, error_type) DO UPDATE SET
count = count + 1,
last_seen = datetime('now')""",
(student_id, topic, error_type),
)
conn.execute(
"""UPDATE topic_mastery SET
mastery_score = MAX(0.0, mastery_score - 0.03),
last_updated = datetime('now')
WHERE student_id=? AND topic=?""",
(student_id, topic),
)
# ── streak ──
row = conn.execute(
"SELECT last_active FROM students WHERE id=?", (student_id,)
).fetchone()
today_str = date.today().isoformat()
last = row["last_active"] if row else None
new_streak_sql = "streak + 1" if last != today_str else "streak"
conn.execute(
f"UPDATE students SET last_active=?, streak={new_streak_sql} WHERE id=?",
(today_str, student_id),
)
return attempt_id
def get_next_difficulty(student_id: int, topic: str, current: int = 5) -> int:
"""Adaptive difficulty: harder if mastery > 0.7, easier if < 0.35."""
with db() as conn:
row = conn.execute(
"SELECT mastery_score, attempts FROM topic_mastery WHERE student_id=? AND topic=?",
(student_id, topic),
).fetchone()
if not row or row["attempts"] < 3:
return current # not enough data yet
score = row["mastery_score"]
if score >= 0.70:
return min(10, current + 1)
elif score <= 0.35:
return max(1, current - 1)
return current
def get_student_stats(student_id: int) -> dict:
"""Full stats object for dashboard."""
with db() as conn:
student = conn.execute(
"SELECT * FROM students WHERE id=?", (student_id,)
).fetchone()
if not student:
return {}
mastery_rows = conn.execute(
"SELECT topic, attempts, correct, mastery_score FROM topic_mastery WHERE student_id=? ORDER BY mastery_score DESC",
(student_id,),
).fetchall()
error_rows = conn.execute(
"SELECT topic, error_type, count FROM error_log WHERE student_id=? ORDER BY count DESC LIMIT 10",
(student_id,),
).fetchall()
total_attempts = conn.execute(
"SELECT COUNT(*) as n FROM attempts WHERE student_id=?", (student_id,)
).fetchone()["n"]
# attempts last 7 days grouped by day
daily = conn.execute(
"""SELECT date(created_at) as day, COUNT(*) as n
FROM attempts WHERE student_id=? AND created_at >= date('now','-6 days')
GROUP BY day ORDER BY day""",
(student_id,),
).fetchall()
# topic distribution
topic_dist = conn.execute(
"""SELECT topic, COUNT(*) as n FROM attempts
WHERE student_id=? GROUP BY topic ORDER BY n DESC""",
(student_id,),
).fetchall()
mastery = [dict(r) for r in mastery_rows]
errors = [dict(r) for r in error_rows]
weak = [m for m in mastery if m["mastery_score"] < 0.4 and m["attempts"] >= 2]
strong = [m for m in mastery if m["mastery_score"] >= 0.7]
overall_accuracy = (
sum(m["correct"] for m in mastery) / max(1, sum(m["attempts"] for m in mastery))
) * 100
return {
"student": dict(student),
"total_attempts": total_attempts,
"overall_accuracy": round(overall_accuracy, 1),
"mastery": mastery,
"weak_topics": weak,
"strong_topics": strong,
"top_errors": errors,
"daily_activity": [dict(r) for r in daily],
"topic_distribution": [dict(r) for r in topic_dist],
}
def get_weekly_report(student_id: int) -> dict:
"""Generate human-readable weekly progress report."""
with db() as conn:
rows = conn.execute(
"""SELECT topic, error_type, created_at FROM attempts
WHERE student_id=? AND created_at >= date('now','-7 days')
ORDER BY created_at""",
(student_id,),
).fetchall()
prev_rows = conn.execute(
"""SELECT COUNT(*) as n FROM attempts
WHERE student_id=? AND created_at >= date('now','-14 days')
AND created_at < date('now','-7 days')""",
(student_id,),
).fetchone()
this_week = len(rows)
last_week = prev_rows["n"] if prev_rows else 0
growth_pct = round(((this_week - last_week) / max(1, last_week)) * 100)
topic_counts: dict[str, int] = {}
for r in rows:
topic_counts[r["topic"]] = topic_counts.get(r["topic"], 0) + 1
most_practiced = max(topic_counts, key=topic_counts.get) if topic_counts else "—"
stats = get_student_stats(student_id)
insights = []
if stats.get("weak_topics"):
wt = stats["weak_topics"][0]["topic"]
insights.append(f"Focus area this week: {wt} needs more practice.")
if stats.get("strong_topics"):
st = stats["strong_topics"][0]["topic"]
insights.append(f"Great progress in {st} — keep it up!")
if growth_pct > 0:
insights.append(f"You solved {growth_pct}% more questions than last week!")
elif growth_pct < 0:
insights.append("Try to solve a few more questions this week to maintain momentum.")
return {
"questions_this_week": this_week,
"questions_last_week": last_week,
"growth_pct": growth_pct,
"most_practiced": most_practiced,
"insights": insights,
"accuracy": stats.get("overall_accuracy", 0),
"streak": stats.get("student", {}).get("streak", 0),
}
def generate_study_plan(student_id: int) -> list[dict]:
"""Return ordered list of recommended topics + question counts."""
stats = get_student_stats(student_id)
plan = []
for w in stats.get("weak_topics", [])[:3]:
plan.append({
"topic": w["topic"],
"priority": "high",
"reason": f"Mastery only {round(w['mastery_score']*100)}% — needs work",
"questions": 5,
})
for m in stats.get("mastery", []):
if 0.4 <= m["mastery_score"] < 0.7 and len(plan) < 5:
plan.append({
"topic": m["topic"],
"priority": "medium",
"reason": f"Mastery at {round(m['mastery_score']*100)}% — almost there",
"questions": 3,
})
if not plan:
plan.append({
"topic": "General STEM",
"priority": "normal",
"reason": "Start solving questions to unlock your personalised plan",
"questions": 5,
})
return plan