"""Anonymised analytics for the professor dashboard (no raw message content stored).""" from __future__ import annotations import json import re from datetime import UTC, datetime, timedelta from typing import Any from sqlalchemy import func, select from app.core.logging import get_logger from app.db import SessionLocal from app.llm.openrouter import get_llm from app.llm.router import router from app.models import AnalyticsEvent, Conversation, Message, User log = get_logger("analytics") CLASSIFY_PROMPT = """Classe la question d'un étudiant en évaluation immobilière. Réponds en JSON strict : {"topic": "", "reformulation": ""}""" async def classify_and_record(user_id_hash: str, course: str, question: str) -> None: """Background: classify topic with MODEL_FAST and store an anonymised sample.""" topic, sample = "", "" try: plan = router.plan("fast") text, _ = await get_llm().complete( [{"role": "system", "content": CLASSIFY_PROMPT}, {"role": "user", "content": question[:1500]}], plan.models, temperature=0.0, max_tokens=150, response_format={"type": "json_object"}, user_id_hash=user_id_hash) m = re.search(r"\{.*\}", text, re.S) data = json.loads(m.group(0)) if m else {} topic = str(data.get("topic", ""))[:120] sample = str(data.get("reformulation", ""))[:400] except Exception as exc: # noqa: BLE001 log.warning("classify_failed", error=str(exc)) async with SessionLocal() as session: session.add(AnalyticsEvent(user_id_hash=user_id_hash, course_code=course, event_type="question", topic=topic, question_sample=sample)) await session.commit() async def record_event(user_id_hash: str, course: str, event_type: str, topic: str = "") -> None: async with SessionLocal() as session: session.add(AnalyticsEvent(user_id_hash=user_id_hash, course_code=course, event_type=event_type, topic=topic)) await session.commit() async def dashboard(days: int = 30) -> dict[str, Any]: since = datetime.now(UTC).replace(tzinfo=None) - timedelta(days=days) async with SessionLocal() as session: day = func.date(Message.created_at) per_day = await session.execute( select(day, func.count()).where(Message.created_at >= since, Message.role == "user") .group_by(day).order_by(day)) active_students = await session.scalar( select(func.count(func.distinct(AnalyticsEvent.user_id_hash))) .where(AnalyticsEvent.created_at >= since)) total_users = await session.scalar(select(func.count(User.id))) hours = await session.execute( select(func.strftime("%H", Message.created_at), func.count()) .where(Message.created_at >= since, Message.role == "user") .group_by(func.strftime("%H", Message.created_at))) topics = await session.execute( select(AnalyticsEvent.topic, AnalyticsEvent.course_code, func.count()) .where(AnalyticsEvent.created_at >= since, AnalyticsEvent.event_type == "question", AnalyticsEvent.topic != "") .group_by(AnalyticsEvent.topic, AnalyticsEvent.course_code) .order_by(func.count().desc()).limit(15)) samples = await session.execute( select(AnalyticsEvent.question_sample, AnalyticsEvent.topic, AnalyticsEvent.course_code, AnalyticsEvent.created_at) .where(AnalyticsEvent.created_at >= since, AnalyticsEvent.question_sample != "") .order_by(AnalyticsEvent.created_at.desc()).limit(40)) feedback = await session.execute( select(Message.feedback, func.count()).where(Message.created_at >= since, Message.feedback.is_not(None)) .group_by(Message.feedback)) conv_count = await session.scalar(select(func.count(Conversation.id)) .where(Conversation.created_at >= since)) tool_usage = await session.execute( select(AnalyticsEvent.topic, func.count()) .where(AnalyticsEvent.created_at >= since, AnalyticsEvent.event_type == "tool") .group_by(AnalyticsEvent.topic)) return { "days": days, "messages_per_day": [{"day": str(d), "count": int(n)} for d, n in per_day], "active_students": int(active_students or 0), "total_users": int(total_users or 0), "conversations": int(conv_count or 0), "peak_hours": [{"hour": int(h), "count": int(n)} for h, n in hours if h is not None], "top_topics": [{"topic": t, "course": c, "count": int(n)} for t, c, n in topics], "question_samples": [{"question": q, "topic": t, "course": c, "at": a.isoformat()} for q, t, c, a in samples], "feedback": {str(f): int(n) for f, n in feedback}, "tool_usage": [{"tool": t, "count": int(n)} for t, n in tool_usage], }