SPB Git forge

spb/uqo-chat

Public
14commits 1branches 0releases
1.4 MBsize
maindefault branch
17 days agolast push
Python 64.6% TypeScript 33.7% CSS 0.8%
5.4 KB · 106 lines python
Raw Blame History
1"""Anonymised analytics for the professor dashboard (no raw message content stored)."""23from __future__ import annotations45import json6import re7from datetime import UTC, datetime, timedelta8from typing import Any910from sqlalchemy import func, select1112from app.core.logging import get_logger13from app.db import SessionLocal14from app.llm.openrouter import get_llm15from app.llm.router import router16from app.models import AnalyticsEvent, Conversation, Message, User1718log = get_logger("analytics")1920CLASSIFY_PROMPT = """Classe la question d'un étudiant en évaluation immobilière. Réponds en JSON strict :21{"topic": "<notion du cours en 2-5 mots, ex. 'dépréciation âge-vie', 'principes de la valeur',22'méthode de comparaison', 'valeur du terrain', 'coûts indirects', 'rapport d'évaluation', 'UMPP',23'six fonctions du dollar', 'rôle d'évaluation', 'hors sujet'>",24 "reformulation": "<la question reformulée de façon générique et anonyme, sans nom, adresse, code25permanent ni détail personnel, max 25 mots>"}"""262728async def classify_and_record(user_id_hash: str, course: str, question: str) -> None:29    """Background: classify topic with MODEL_FAST and store an anonymised sample."""30    topic, sample = "", ""31    try:32        plan = router.plan("fast")33        text, _ = await get_llm().complete(34            [{"role": "system", "content": CLASSIFY_PROMPT},35             {"role": "user", "content": question[:1500]}],36            plan.models, temperature=0.0, max_tokens=150,37            response_format={"type": "json_object"}, user_id_hash=user_id_hash)38        m = re.search(r"\{.*\}", text, re.S)39        data = json.loads(m.group(0)) if m else {}40        topic = str(data.get("topic", ""))[:120]41        sample = str(data.get("reformulation", ""))[:400]42    except Exception as exc:  # noqa: BLE00143        log.warning("classify_failed", error=str(exc))44    async with SessionLocal() as session:45        session.add(AnalyticsEvent(user_id_hash=user_id_hash, course_code=course,46                                   event_type="question", topic=topic, question_sample=sample))47        await session.commit()484950async def record_event(user_id_hash: str, course: str, event_type: str, topic: str = "") -> None:51    async with SessionLocal() as session:52        session.add(AnalyticsEvent(user_id_hash=user_id_hash, course_code=course,53                                   event_type=event_type, topic=topic))54        await session.commit()555657async def dashboard(days: int = 30) -> dict[str, Any]:58    since = datetime.now(UTC).replace(tzinfo=None) - timedelta(days=days)59    async with SessionLocal() as session:60        day = func.date(Message.created_at)61        per_day = await session.execute(62            select(day, func.count()).where(Message.created_at >= since, Message.role == "user")63            .group_by(day).order_by(day))64        active_students = await session.scalar(65            select(func.count(func.distinct(AnalyticsEvent.user_id_hash)))66            .where(AnalyticsEvent.created_at >= since))67        total_users = await session.scalar(select(func.count(User.id)))68        hours = await session.execute(69            select(func.strftime("%H", Message.created_at), func.count())70            .where(Message.created_at >= since, Message.role == "user")71            .group_by(func.strftime("%H", Message.created_at)))72        topics = await session.execute(73            select(AnalyticsEvent.topic, AnalyticsEvent.course_code, func.count())74            .where(AnalyticsEvent.created_at >= since, AnalyticsEvent.event_type == "question",75                   AnalyticsEvent.topic != "")76            .group_by(AnalyticsEvent.topic, AnalyticsEvent.course_code)77            .order_by(func.count().desc()).limit(15))78        samples = await session.execute(79            select(AnalyticsEvent.question_sample, AnalyticsEvent.topic, AnalyticsEvent.course_code,80                   AnalyticsEvent.created_at)81            .where(AnalyticsEvent.created_at >= since, AnalyticsEvent.question_sample != "")82            .order_by(AnalyticsEvent.created_at.desc()).limit(40))83        feedback = await session.execute(84            select(Message.feedback, func.count()).where(Message.created_at >= since,85                                                          Message.feedback.is_not(None))86            .group_by(Message.feedback))87        conv_count = await session.scalar(select(func.count(Conversation.id))88                                          .where(Conversation.created_at >= since))89        tool_usage = await session.execute(90            select(AnalyticsEvent.topic, func.count())91            .where(AnalyticsEvent.created_at >= since, AnalyticsEvent.event_type == "tool")92            .group_by(AnalyticsEvent.topic))93    return {94        "days": days,95        "messages_per_day": [{"day": str(d), "count": int(n)} for d, n in per_day],96        "active_students": int(active_students or 0),97        "total_users": int(total_users or 0),98        "conversations": int(conv_count or 0),99        "peak_hours": [{"hour": int(h), "count": int(n)} for h, n in hours if h is not None],100        "top_topics": [{"topic": t, "course": c, "count": int(n)} for t, c, n in topics],101        "question_samples": [{"question": q, "topic": t, "course": c, "at": a.isoformat()}102                             for q, t, c, a in samples],103        "feedback": {str(f): int(n) for f, n in feedback},104        "tool_usage": [{"tool": t, "count": int(n)} for t, n in tool_usage],105    }106