""" Points ledger and daily summary. Points are ALWAYS additive — never deducted. """ import json import logging import os from db import get_cursor import llm import prompts logger = logging.getLogger(__name__) _SYLLABUS_PATH = os.path.join(os.path.dirname(__file__), "syllabus_full_a1_c2.json") _a1_a2_concepts: list[dict] | None = None POINT_VALUES = { "daily_open": 5, "saved_lesson": 10, "exercise_done": 5, "dialogue_turn": 3, "pronunciation": 5, "word_explored": 1, "photo_exercise": 8, } def try_daily_open(user_id: str) -> int: """Award daily-open points once per calendar day. Returns points awarded (0 if already awarded).""" try: with get_cursor() as cur: cur.execute( "SELECT COUNT(*) AS n FROM points " "WHERE user_id = %s AND reason = 'daily_open' AND earned_at::date = CURRENT_DATE", (user_id,), ) if cur.fetchone()["n"] > 0: return 0 amount = POINT_VALUES["daily_open"] cur.execute( "INSERT INTO points (user_id, reason, amount) VALUES (%s, %s, %s)", (user_id, "daily_open", amount), ) return amount except Exception as e: logger.warning("try_daily_open failed: %s", e) return 0 def add_points(user_id: str, reason: str) -> int: """Award points for an action. Returns points awarded.""" amount = POINT_VALUES.get(reason, 2) try: with get_cursor() as cur: cur.execute( "INSERT INTO points (user_id, reason, amount) VALUES (%s, %s, %s)", (user_id, reason, amount), ) except Exception as e: logger.warning("add_points failed: %s", e) return amount def get_total_points(user_id: str) -> int: try: with get_cursor() as cur: cur.execute( "SELECT COALESCE(SUM(amount), 0) AS total FROM points WHERE user_id = %s", (user_id,), ) return int(cur.fetchone()["total"]) except Exception: return 0 def get_daily_stats(user_id: str) -> dict: try: with get_cursor() as cur: cur.execute( """SELECT (SELECT COUNT(*) FROM pages WHERE user_id=%s AND date=CURRENT_DATE) AS pages_today, (SELECT COUNT(*) FROM exercises WHERE user_id=%s AND created_at::date=CURRENT_DATE) AS exercises_today, (SELECT COUNT(*) FROM points WHERE user_id=%s AND reason='dialogue_turn' AND earned_at::date=CURRENT_DATE) AS dialogue_turns, (SELECT COUNT(*) FROM points WHERE user_id=%s AND reason='word_explored' AND earned_at::date=CURRENT_DATE) AS words_clicked, (SELECT COALESCE(SUM(amount),0) FROM points WHERE user_id=%s) AS total_points""", (user_id,)*5, ) return {k: int(v) for k, v in dict(cur.fetchone()).items()} except Exception: return {"pages_today":0,"exercises_today":0,"dialogue_turns":0,"words_clicked":0,"total_points":0} def _load_a1_a2_concepts() -> list[dict]: global _a1_a2_concepts if _a1_a2_concepts is None: try: with open(_SYLLABUS_PATH, encoding="utf-8") as f: concepts = json.load(f)["concepts"] _a1_a2_concepts = [c for c in concepts if c.get("cefr_level") in ("A1", "A2")] except Exception as e: logger.warning("_load_a1_a2_concepts failed: %s", e) _a1_a2_concepts = [] return _a1_a2_concepts def get_concepts_progress() -> dict: """Concepts the Coach Agent has identified as covered, plus the next one up in syllabus order — powers the Summary tab's strengths + next focus.""" try: with get_cursor() as cur: cur.execute("SELECT id FROM concepts WHERE covered_on IS NOT NULL") covered_ids = {r["id"] for r in cur.fetchall()} except Exception: covered_ids = set() a1_a2 = _load_a1_a2_concepts() covered = [c["name"] for c in a1_a2 if c["id"] in covered_ids] next_concept = next((c["name"] for c in a1_a2 if c["id"] not in covered_ids), None) return { "covered": covered, "next": next_concept, "covered_count": len(covered), "total_count": len(a1_a2), } def get_daily_summary(user_id: str) -> str: stats = get_daily_stats(user_id) concepts = get_concepts_progress() result = llm.chat([ {"role": "system", "content": prompts.DAILY_SUMMARY_SYSTEM}, {"role": "user", "content": prompts.daily_summary_user(stats, concepts)}, ]) if result.startswith("⚠"): return _fallback(stats, concepts) return result def _fallback(stats: dict, concepts: dict | None = None) -> str: total = stats.get("total_points", 0) pages = stats.get("pages_today", 0) ex = stats.get("exercises_today", 0) lines = [f"You've earned **{total} points** — great work!"] if pages: lines.append(f"You saved {pages} lesson{'s' if pages > 1 else ''} today.") if ex: lines.append(f"You completed {ex} exercise{'s' if ex > 1 else ''}.") covered = (concepts or {}).get("covered") or [] if covered: lines.append(f"You're building skills in: {', '.join(covered[-3:])}.") next_concept = (concepts or {}).get("next") if next_concept: lines.append(f"Ready to practice next: {next_concept}.") else: lines.append("Ready to explore next: more vocabulary and dialogue practice. 🇫🇷") return "\n\n".join(lines)