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%
ZIP tar.gz
NameLast commitUpdated
.github feat: UQO-Chat v0.1.0 — tuteur IA IMM1003/IMM1033 (FastAPI +... 18 days ago
backend feat(auth): whitelist + Resend invitations replace the course access... 17 days ago
deploy feat: UQO-Chat v0.1.0 — tuteur IA IMM1003/IMM1033 (FastAPI +... 18 days ago
frontend ui(login): forgot-password link under the password field (no wrap at... 17 days ago
k8s feat: UQO-Chat v0.1.0 — tuteur IA IMM1003/IMM1033 (FastAPI +... 18 days ago
sandbox-runner feat: UQO-Chat v0.1.0 — tuteur IA IMM1003/IMM1033 (FastAPI +... 18 days ago
.env.example feat(auth): whitelist + Resend invitations replace the course access... 17 days ago
.gitignore feat: UQO-Chat v0.1.0 — tuteur IA IMM1003/IMM1033 (FastAPI +... 18 days ago
CLAUDE.md feat(auth): whitelist + Resend invitations replace the course access... 17 days ago
docker-compose.yml feat: UQO-Chat v0.1.0 — tuteur IA IMM1003/IMM1033 (FastAPI +... 18 days ago
Makefile feat: UQO-Chat v0.1.0 — tuteur IA IMM1003/IMM1033 (FastAPI +... 18 days ago
README.md docs: README détaillé (architecture, 13 outils, API, auth v0.5, RAG,... 17 days ago
README.md source

# UQO-Chat — tuteur IA pour IMM1003 · IMM1033

Tuteur IA des cours IMM1003 — Éléments d'évaluation immobilière et IMM1033 — Méthodes du coût en évaluation immobilière (Université du Québec en Outaouais). Il explique la matière en citant les notes de cours officielles, exécute du Python dans un bac à sable isolé, produit et modifie des classeurs Excel à formules vivantes, calcule avec des outils d'évaluation déterministes, cherche des données de marché actuelles, analyse les fichiers déposés, génère des graphiques, des documents Word et des quiz interactifs.

Production https://www.uqo-chat.app
Version v0.5 (auth par invitation) — backend/pyproject.toml et frontend/package.json portent encore 0.1.0
App mld uqo-chat · API :8190 · sandbox :8191 · nœud actuel : mld status (au 2026-09-06 : M4M64a, ~/apps/uqo-chat)
Processus PM2 uqo-chat-api · uqo-chat-sandbox · uqo-chat-ngrok
Santé GET /api/v1/ready{"ok":true,"chunks":714} · GET /api/v1/health{"ok":true,"service":"uqo-chat"}
Dépôt gitsrv:~/srv/git/uqo-chat.git (spbgit, git.spboucher.ai) — source de vérité = ce dépôt (laptop) + spbgit, jamais la copie du nœud
Spécification produit CLAUDE.md (résumé de la spec d'origine en 28 sections, règles d'or, écarts assumés)
Auteur / contact Simon-Pierre Bouchercontact@spboucher.ai

# Sommaire

  1. Vue d'ensemble
  2. Fonctionnalités
  3. Les 13 outils du tuteur
  4. Architecture
  5. Arborescence du dépôt
  6. Backend (FastAPI)
  7. Authentification, rôles et comptes
  8. RAG sur les notes de cours
  9. Bac à sable Python (sandbox-runner)
  10. Frontend (React PWA)
  11. Configuration (variables d'environnement)
  12. Développement local
  13. Tests, lint, CI
  14. Déploiement sur le cluster MacLustr (mld)
  15. Exploitation
  16. Coûts LLM
  17. Vie privée, sécurité, Loi 25
  18. Écarts assumés par rapport à la spécification
  19. Feuille de route
  20. Historique des versions
  21. Pièges connus
  22. Droits et licence
  23. Contact

# 1. Vue d'ensemble

UQO-Chat est une application web (PWA) destinée aux étudiant·es des deux cours d'évaluation immobilière donnés par Simon-Pierre Boucher à l'UQO. Le professeur inscrit les courriels des étudiant·es ; chacun·e reçoit une invitation, choisit un mot de passe et converse en français avec un tuteur qui :

  • s'appuie d'abord sur la matière du cours : recherche BM25 dans le HTML des sites de notes interactives (https://www.uqo-imm1003.app, https://www.uqo-imm1033.app) avec citations cliquables vers la séance et la section ;
  • calcule juste : calculateurs déterministes d'évaluation (méthode du coût, dépréciation, terrain, capitalisation, comparables, six fonctions du dollar, conversions d'unités) plutôt que de l'arithmétique « de tête » ;
  • produit des livrables : classeurs Excel à formules vivantes (6 gabarits ou spec libre), modification de classeurs déposés, graphiques PNG, documents Word, quiz interactifs ;
  • exécute du code : Python (pandas, numpy, matplotlib…) dans un service séparé sous sandbox-exec, sans réseau ;
  • va chercher le présent : données de marché, taux, coûts, règlements via Firecrawl (cache 6 h) ;
  • est piloté par le professeur : tableau de bord (activité anonymisée, étudiants et invitations, contenu ingéré, consignes et annonces par cours, réglages), page admin des coûts.

Tous les appels de modèles passent par OpenRouter (anthropic/claude-fable-5.1 par défaut, repli openai/gpt-5.5, anthropic/claude-opus-4.6 pour la réflexion approfondie, openai/gpt-5.4-nano pour les titres et classifications, anthropic/claude-sonnet-4.6 pour la vision), en streaming SSE avec boucle agentique (jusqu'à 12 itérations d'outils, outils exécutés en parallèle, 16 000 jetons de sortie).

# 2. Fonctionnalités

# Pour l'étudiant·e

  • Conversations par cours (IMM1003 / IMM1033), titrées automatiquement, regroupées par date dans le tiroir, renommage, suppression, régénération d'une réponse, arrêt d'un tour en cours, rétroaction 👍/👎 par message.
  • Réponses en Markdown + KaTeX (formules), blocs de code colorés (shiki allégé), garde-fou math-guard pour que « 185 000 $ » en prose ne soit pas pris pour du LaTeX.
  • Cartes d'outils riches : source de cours (extrait + lien), calculateur d'évaluation (tableau des étapes), calculateur financier, conversion d'unités, éditeur Python (onglets Code / Sortie / Graphiques, modifier et ré-exécuter, télécharger .py), aperçu Excel multi-feuilles avec formules ƒ et bouton « Modifier avec le tuteur », graphique, document Word, analyse de fichier, recherche web (sources), quiz interactif (correction immédiate, explication, score enregistré).
  • Dépôt de fichiers (xlsx, csv, pdf, docx, pptx, images ; 20 Mo ; 20 / jour) conservés 7 jours (30 si épinglés), panneau des artefacts de la conversation, liens de téléchargement signés.
  • Préférences : tutoiement / vouvoiement, langue, thème ; consentement Loi 25, export et suppression du compte.
  • PWA installable (app shell + cache lecture ≈ 2 Mo), mobile-first 375 px, héros dégradé, composer flottant.

# Pour le professeur (/professeur)

  • Activité : messages, conversations, étudiant·es actifs, outils appelés, sujets, rétroactions — agrégés et anonymisés (identifiants hachés), sans contenu de message.
  • Étudiants : ajout en lot (collage de courriels), rôles, statut invité / activé, invitation Resend « Bienvenue sur UQO-Chat — choisis ton mot de passe », relance individuelle ou « Inviter les N jamais invité·es », mot de passe défini manuellement, suppression.
  • Contenu : ingestion de fichiers (PDF, DOCX, PPTX, MD) ou d'un site de notes, activation / désactivation d'un document, réindexation.
  • Réglages par cours : consignes additionnelles injectées dans le prompt, annonce affichée aux étudiant·es, échéances, modèle.
  • Promotion d'un compte au rôle professeur / admin.

# Pour l'admin (/admin)

  • Coûts LLM (par jour, par modèle, par étudiant·e haché), budget mensuel et projection.

# 3. Les 13 outils du tuteur

Chaque outil = backend/app/tools/<nom>.py + schéma JSON app/tools/schemas/<nom>.json + test + carte React frontend/src/components/tools/<nom>-card.tsx. Les arguments sont coercés avec tolérance (tools/coerce.py : « 425 000 $ », « +3 % », « Oui » → nombres / booléens) et les appels tronqués par le modèle sont réparés ou refusés plutôt qu'exécutés à moitié.

Outil Rôle Détails
search_course_content Matière du cours (à consulter d'abord) BM25 sur 714 passages des deux sites de notes ; renvoie extraits + URL seance/NN#ancre
appraisal_calc 12 calculateurs d'évaluation déterministes méthode du coût, ventilation de la dépréciation anti-double-comptage, coût indexé / unitaire, terrain (extraction, allocation, résiduelle, lotissement), capitalisation directe, MRB, grille de comparables, âge effectif par le marché
financial_calc Six fonctions du dollar, VAN / TRI, âge-vie numpy-financial
unit_convert m² ↔ pi², arpent, acre, hectare, $/m² ↔ $/pi² unités québécoises incluses
execute_python Calculs libres, statistiques, régressions, graphiques via sandbox-runner ; les fichiers déposés et les artefacts déjà générés sont disponibles dans inputs/<nom>
make_chart Graphique rapide à partir de données matplotlib in-process, palette UQO, PNG
create_excel Nouveau classeur à formules vivantes gabarits comparables_ajustes, methode_du_cout, age_vie, tableau_amortissement, six_fonctions, sensibilite, ou spec libre normalisée (tools/excel_spec.py) ; la grille de comparables calcule les ajustements par formule (sujet + taux + échelles ordinales + tendance / mois)
inspect_excel Lire un classeur (déposé ou généré) feuilles, dimensions, en-têtes, formules, aperçu
edit_excel Modifier un classeur existant ajouter colonne / ligne / feuille / graphique, corriger une cellule, formater ; versions _v2, _v3… (tools/excel_ops.py)
create_docx Document Word depuis Markdown fiche de révision, plan d'étude, gabarit de rapport (python-docx)
analyze_file Lire un fichier déposé xlsx / csv / pdf (pdfplumber) / docx / pptx / image (modèle vision)
web_search Données actuelles seulement Firecrawl, cache 6 h, 30 / jour / étudiant·e
generate_quiz Quiz interactif questions à choix, explication, enregistrement des tentatives

Le prompt système (app/llm/prompts/system_tutor.md + guardrails.md + course_imm10xx.md + consignes du professeur + TOOL_HINTS dans agent.py) impose : chercher la matière avant de répondre, enchaîner les outils (ex. search_course_content → appraisal_calc → create_excel → make_chart), modifier un fichier existant avec edit_excel plutôt que d'en recréer un, pédagogie socratique, français québécois, pas de solution complète aux TP en cours.

# 4. Architecture

text
Internet ── ngrok (www.uqo-chat.app, TLS) ──► uqo-chat-api :8190   FastAPI + SSE + SPA React (PM2)

                                                   ├──► uqo-chat-sandbox :8191   sandbox-runner (PM2) — python -I -B sous sandbox-exec,
                                                   │                             sans réseau, écriture confinée, rlimits, timeout ≤ 60 s
                                                   ├──► SQLite  data/uqochat.db  (SQLAlchemy async ; Postgres via DATABASE_URL)
                                                   │      index BM25 reconstruit en mémoire au démarrage
                                                   ├──► fichiers data/files/<user>/<id>.<ext>  (TTL 7 j / 30 j épinglés, purge horaire)
                                                   └──► HTTPS sortant : OpenRouter (LLM), Firecrawl (web), Resend (courriels)
  • Un seul point d'entrée LLM : app/llm/openrouter.py (LLMClient, SSE, repli de modèle, comptage des jetons et des coûts → table llm_usage). app/llm/router.py choisit le modèle (primaire / raisonnement / rapide / vision).
  • Boucle agentique : app/llm/agent.py — construit le prompt (cours, préférences, fichiers, échéances, annonce), streame les deltas au client (token, tool_call, tool_result, done, error), exécute les outils en parallèle via app/tools/registry.py (ToolContext = utilisateur, conversation, fichiers, cours), persiste messages et tool_calls, enregistre usage et analytics.
  • Aucun code étudiant / LLM ne s'exécute dans le processus API : toujours sandbox-runner (app/sandbox/client.py, jeton SANDBOX_TOKEN).
  • Maintenance (main.py, toutes les heures) : purge des fichiers expirés, anonymisation des messages de plus de MESSAGE_RETENTION_MONTHS (12).
  • Sécurité HTTP : CORS restreint, en-têtes nosniff, X-Frame-Options: DENY, Referrer-Policy, Permissions-Policy, HSTS en production, CSP stricte sur le HTML (script-src 'self', frame-ancestors 'none').
  • Frontend servi par l'API : frontend/dist monté sur /assets + repli SPA (GET/HEAD sur toute route → index.html), ce qui permet les vérifications d'uptime en HEAD.

Les manifestes Kubernetes (k8s/) et docker-compose.yml reproduisent la cible décrite dans la spécification (Postgres + pgvector, Redis, MinIO, sandbox durci network_mode: none, NetworkPolicy deny-all, HPA) pour un futur cluster ; la production actuelle tourne sur un nœud Mac du cluster via mld.

# 5. Arborescence du dépôt

text
uqo-chat/
├── README.md · CLAUDE.md · Makefile · .env.example · docker-compose.yml
├── .github/workflows/         ci.yml (ruff + pytest + tsc + build + trivy) · deploy.yml (images ghcr + kubectl, non utilisé en prod)
├── deploy/uqo-chat.manifest.json   gabarit du manifeste mld (les secrets sont dans M1M32:~/dispatch/apps/uqo-chat.json)
├── backend/                   Python 3.12 · FastAPI · SQLAlchemy async
│   ├── pyproject.toml         dépendances (fastapi, uvicorn, pydantic, sqlalchemy, aiosqlite, asyncpg, httpx[http2],
│   │                          structlog, python-jose, passlib, openpyxl, pandas, numpy(-financial), matplotlib,
│   │                          pdfplumber, python-docx, python-pptx, selectolax, markdown-it-py, aiosmtplib, orjson)
│   ├── Dockerfile
│   ├── app/
│   │   ├── main.py            application, middlewares, SPA, tâche de maintenance
│   │   ├── db.py              moteur async, SessionLocal, init_db (create_all + _add_missing_columns)
│   │   ├── models/            User, MagicLink, Course, Conversation, Message, ToolCall, StoredFile, Quiz, QuizAttempt,
│   │   │                      CourseDocument, CourseChunk, AnalyticsEvent, LLMUsage, AppSetting
│   │   ├── api/v1/            health, auth, conversations, chat, files, quiz, courses, professor, admin
│   │   ├── core/              config (Settings), logging (structlog, masquage content/text/email), security (JWT,
│   │   │                      PBKDF2, hachage d'identifiants), ratelimit (mémoire), cache
│   │   ├── llm/               openrouter.py, agent.py, router.py, schemas.py, prompts/*.md
│   │   ├── rag/               ingest.py (HTML des sites / PDF / DOCX / PPTX / MD → chunks), chunking.py, retriever.py (BM25)
│   │   ├── sandbox/client.py  client HTTP du sandbox-runner
│   │   ├── services/          users, invites, mail (Resend + SMTP), conversations, files, courses, analytics, costs
│   │   └── tools/             registry, all, coerce, excel_spec, excel_ops, 13 outils, schemas/*.json, excel_templates/ (6)
│   ├── scripts/               seed.py (cours + professeur), ingest.py (--course --site | --path)
│   └── tests/                 41 tests (auth/invitations respx, SSE OpenRouter, RAG, préfiltre sandbox, outils)
├── sandbox-runner/            service FastAPI minimal : POST /run {code, files_in, timeout_s} · runner.py (sandbox-exec,
│                              rlimits, prélude matplotlib Agg, capture des figures, troncature 50 Ko) · Dockerfile · requirements.txt
├── frontend/                  React 18 · Vite 5 · TypeScript · Tailwind 3 · PWA
│   ├── src/app/router.tsx     /connexion · /mot-de-passe · /confidentialite · /professeur · /admin · / · /c/:id
│   ├── src/components/        chat/ (chat-view, composer, message-bubble, streaming-text, code-block, tool-call-timeline,
│   │                          empty-state) · tools/ (13 cartes + tool-card, artifact-chips) · layout/ (app-shell, sidebar) · ui/
│   ├── src/features/          auth/ (login, set-password, privacy, settings) · conversations/artifacts-panel ·
│   │                          professor/ (professor-page, admin-costs-page)
│   ├── src/stores/            zustand : auth, chat, ui (dont ui.draft pour « Modifier avec le tuteur »)
│   ├── src/lib/               api.ts (fetch + SSE), types.ts, format.ts, math-guard.ts (+ test), cn.ts
│   ├── src/theme/uqo.ts · src/i18n/fr-CA.json · src/index.css · tailwind.config.ts · vite.config.ts (PWA, proxy /api)
│   └── public/                uqo-logo.png, uqo-logo-white.png, icons/ (favicon, 192/512, maskable, apple-touch, og.png)
├── k8s/                       base/ (namespace, api, web, worker, sandbox-runner, postgres, redis, minio, services,
│                              networkpolicies, hpa) · overlays/staging|prod · ngrok/ (operator, ingress)
├── content/                   (ignoré) copies locales des sites de notes pour l'ingestion
└── data/                      (ignoré) uqochat.db + files/

# 6. Backend (FastAPI)

Préfixe commun /api/v1. Authentification par JWT (Authorization: Bearer, 24 h) + jeton de rafraîchissement (30 j). Documentation OpenAPI disponible en développement seulement (/api/docs).

Groupe Routes Notes
Santé GET /health · GET /ready ready renvoie le nombre de passages indexés
Auth POST /auth/login · POST /auth/forgot · GET /auth/password-token · POST /auth/set-password · POST /auth/verify (legacy lien magique) · POST /auth/refresh · POST /auth/logout · GET /auth/config voir §7
Moi GET /me · PATCH /me/preferences · POST /me/password · POST /me/consent · DELETE /me suppression = anonymisation + purge des fichiers
Cours GET /courses cours actifs, annonces, échéances
Conversations GET/POST /conversations · GET/PATCH/DELETE /conversations/{id} · POST /conversations/{id}/messages/{mid}/feedback
Chat POST /chat/{id}/messages (SSE) · POST /chat/{id}/stop · POST /chat/{id}/messages/{mid}/regenerate (SSE) événements token, tool_call, tool_result, title, usage, done, error
Fichiers POST /files (201) · GET /files/{id} · GET /files/{id}/link · POST /files/{id}/pin · GET /conversations/{id}/files · POST /tools/python/run python/run = ré-exécution depuis l'éditeur de la carte Python
Quiz GET /quiz/{id} · POST /quiz/{id}/answers
Professeur GET /dashboard · GET /settings · PATCH /settings/{course} · GET/POST /content (202) · PATCH/DELETE /content/{doc_id} · POST /promote · GET/POST /students · POST /students/invite-all · PATCH/DELETE /students/{id} · POST /students/{id}/invite · PUT /students/{id}/password rôle professor ou admin
Admin GET /costs rôle admin

Modèle de données (SQLAlchemy, create_all au démarrage + ajout des colonnes manquantes) :

Table Contenu
users courriel, rôle (student / professor / admin), mot de passe PBKDF2, préférences JSON, consentement, invited_at, dernier accès
magic_links jetons à usage unique (purpose = invite / reset / legacy), expiration
courses IMM1003, IMM1033 : libellé, consignes du professeur, annonce, échéances, modèle
conversations · messages · tool_calls fil, rôle, contenu, jetons, coût, rétroaction, appels d'outils (args, résultat, durée)
files fichiers déposés / générés : propriétaire, conversation, type, taille, hachage, épinglé, expiration
quizzes · quiz_attempts quiz générés et tentatives
course_documents · course_chunks documents ingérés (site, PDF…) et passages BM25
analytics_events événements anonymisés (identifiant haché)
llm_usage usage par appel : modèle, jetons entrée / sortie, coût USD
app_settings réglages clé-valeur modifiables à chaud

Limites par étudiant·e (config) : 60 messages / h, 400 / jour, 10 exécutions sandbox / h, 30 recherches web / jour, 20 dépôts / jour, 20 Mo par fichier. Dépassement → 429 avec message français.

Journalisation : structlog JSON en production ; les clés content, text, email… sont masquées (core/logging.py). Jamais de contenu de message dans les logs.

# 7. Authentification, rôles et comptes

Flux v0.5 (le code d'accès au cours des versions précédentes est supprimé) :

  1. Le professeur ajoute les courriels dans Étudiants (POST /professor/students, création en lot). Les comptes créés par le professeur sont acceptés même hors @uqo.ca ; sinon ALLOWED_EMAIL_DOMAINS s'applique.
  2. Invitation (send_invitations, POST /students/{id}/invite, POST /students/invite-all) : courriel Resend « Bienvenue sur UQO-Chat — choisis ton mot de passe » avec lien /mot-de-passe?token=… (purpose=invite, INVITE_TTL_DAYS = 14). Gabarits HTML aux couleurs UQO dans services/mail.py (Resend /emails et /emails/batch ≤ 100 ; repli SMTP aiosmtplib).
  3. L'étudiant·e choisit son mot de passe (GET /auth/password-token valide le jeton, POST /auth/set-password), puis se connecte (POST /auth/login, PBKDF2).
  4. « Première connexion ou mot de passe oublié » → POST /auth/forgot : lien purpose=reset de RESET_TTL_MINUTES = 60, envoyé uniquement aux adresses inscrites ou listées dans PROFESSOR_EMAILS / ADMIN_EMAILS / INVITED_EMAILS (sinon 404 avec un indice : « adresse non inscrite » ou « adresse non admise »). Limitation de débit : 20 demandes / h par IP, 5 / h par adresse. En dev, la réponse contient dev_link.
  5. Sans RESEND_API_KEY ni SMTP, les liens sont affichés dans le tableau de bord pour copie manuelle.

Rôles : student (chat), professor (tableau de bord, étudiants, contenu, réglages), admin (coûts, promotion). Les adresses de PROFESSOR_EMAILS / ADMIN_EMAILS obtiennent leur rôle à la connexion.

État des comptes en production au 2026-09-06 : le professeur (boucsi02@uqo.ca) a son mot de passe ; l'admin (spbou4@icloud.com) a reçu son courriel « choisis ton mot de passe » ; 19 étudiant·es sont inscrits sans avoir été invités — le professeur déclenche l'envoi via « Inviter les 19 jamais invité·es ».

# 8. RAG sur les notes de cours

  • Source : le HTML généré des sites de notes interactives (dist/<cours>/seance/NN/index.html, dépôts uqo-imm1003 / uqo-imm1033), copié sur le nœud dans ~/apps/uqo-chat/content/<cours>/. Aussi PDF, DOCX, PPTX, MD déposés par le professeur.
  • Ingestion (rag/ingest.py) : selectolax nettoie le HTML (scripts, SVG, widgets, navigation), reconvertit le KaTeX en $…$ via l'annotation MathML, découpe par section (chunking.py, chevauchement), calcule un hachage par passage, écrit course_documents + course_chunks avec l'URL publique (SITE_URLS) et l'ancre.
  • Index : BM25 en mémoire (rag/retriever.py), reconstruit au démarrage (/ready renvoie le nombre de passages : 714 pour les deux cours) et après chaque ingestion.
  • Embeddings : optionnels via un endpoint OpenAI-compatible (EMBEDDINGS_BASE_URL, MODEL_EMBEDDINGS) ; OpenRouter n'en expose aucun (vérifié 2026-09-05), donc BM25 seul en production.
  • Après un rebuild des sites de notes, ré-ingérer (voir §15).

# 9. Bac à sable Python (sandbox-runner)

Service FastAPI minimal (server.py) : GET /healthz, POST /run {code ≤ 200 000 car., files_in[], timeout_s 1–60}, jeton SANDBOX_TOKEN en Authorization: Bearer, SANDBOX_MAX_PARALLEL = 4 exécutions simultanées (pool de threads + sémaphore).

runner.py — couches d'isolation :

Couche Détail
macOS profil sandbox-exec (seatbelt) : réseau interdit, écriture uniquement dans le répertoire de la course, lecture de ~ bloquée
Linux (conteneur) pod durci attendu (NetworkPolicy deny-all, rootfs lecture seule, cap_drop ALL, pids_limit 64, 512 Mo)
Toujours python -I -B, rlimits (CPU, fichiers, processus ; RLIMIT_AS 512 Mo désactivé sur Darwin car il casse numpy), délai mur, sortie tronquée à 50 Ko, ≤ 10 fichiers / 20 Mo en sortie
Prélude chdir dans le répertoire de travail, matplotlib en Agg (130 dpi, grille), figures ouvertes sauvegardées automatiquement et renvoyées en base64

Les fichiers déposés et les artefacts déjà générés dans la conversation (classeurs, graphiques, documents) sont copiés dans inputs/ avant l'exécution ; les fichiers produits reviennent comme artefacts.

# 10. Frontend (React PWA)

  • Pile : React 18, Vite 5, TypeScript 5, Tailwind 3, zustand, TanStack Query, react-router 6, react-markdown + remark-gfm + remark-math + rehype-katex, shiki/core (langages ciblés ; le paquet complet précachait 10 Mo), Radix (dialog, dropdown, tooltip), lucide-react, recharts, @microsoft/fetch-event-source (SSE avec en-têtes).
  • Routes : /connexion, /mot-de-passe (choix / réinitialisation), /confidentialite, /professeur (garde rôle), /admin (garde rôle), / et /c/:id (chat), */.
  • Marque : couleur #0F6180 alignée sur le mot-symbole officiel UQO (public/uqo-logo*.png, recoloré depuis la source du dépôt de cours ; VITE_USE_OFFICIAL_LOGO), icônes PWA et image OG régénérées, police système.
  • Mobile-first : cible 375 px ; tiroir groupé par date, bulles et composer flottant, squelettes de chargement, bouton de défilement dégagé du composer, lien « mot de passe oublié » sous le champ sans retour à la ligne.
  • PWA (vite-plugin-pwa) : app shell + cache lecture (≈ 2 Mo précachés), manifest, icônes maskable.
  • lib/math-guard.ts : applique la règle Pandoc pour ne rendre en LaTeX que les $…$ plausibles (test vitest).
  • Build : tsc --noEmit && vite buildfrontend/dist servi par l'API en production.

# 11. Configuration (variables d'environnement)

Lues par pydantic-settings depuis .env (racine ou backend/) puis l'environnement ; gabarit complet dans .env.example. Les valeurs de production sont dans le manifeste M1M32:~/dispatch/apps/uqo-chat.json (jamais dans le dépôt).

Groupe Variables (défaut)
App APP_ENV (development), APP_URL, PORT (8190), CORS_ORIGINS, LOG_LEVEL, DATA_DIR (./data), FRONTEND_DIST, COURSES (IMM1003,IMM1033), TERM_LABEL
LLM OPENROUTER_API_KEY, OPENROUTER_BASE_URL, MODEL_TUTOR_PRIMARY (anthropic/claude-fable-5.1), MODEL_TUTOR_FALLBACK (openai/gpt-5.5), MODEL_REASONING (anthropic/claude-opus-4.6), MODEL_FAST (openai/gpt-5.4-nano), MODEL_VISION (anthropic/claude-sonnet-4.6), LLM_MAX_TOOL_ITERATIONS (12), LLM_TIMEOUT_SECONDS (120), LLM_MAX_OUTPUT_TOKENS (16000), LLM_TURN_TIMEOUT_SECONDS (300), LLM_MONTHLY_BUDGET_USD (2000)
Embeddings (optionnel) EMBEDDINGS_BASE_URL, EMBEDDINGS_API_KEY, MODEL_EMBEDDINGS
Web FIRECRAWL_API_KEY, FIRECRAWL_BASE_URL, WEB_SEARCH_CACHE_TTL_S (21600)
Données DATABASE_URL (vide = SQLite DATA_DIR/uqochat.db), REDIS_URL (vide = mémoire)
Sandbox SANDBOX_URL (http://127.0.0.1:8191), SANDBOX_TOKEN, SANDBOX_TIMEOUT_S (30), SANDBOX_HEAVY_TIMEOUT_S (60) ; côté runner : SANDBOX_MAX_PARALLEL, SANDBOX_PYTHON
Auth JWT_SECRET, JWT_TTL_HOURS (24), REFRESH_TTL_DAYS (30), ALLOWED_EMAIL_DOMAINS (uqo.ca), INVITED_EMAILS, PROFESSOR_EMAILS, ADMIN_EMAILS, INVITE_TTL_DAYS (14), RESET_TTL_MINUTES (60)
Courriel RESEND_API_KEY, RESEND_BASE_URL, MAIL_FROM (UQO-Chat no-reply@uqo-chat.app), MAIL_REPLY_TO, repli SMTP_HOST/PORT/USER/PASSWORD/FROM
Limites RATE_MESSAGES_PER_HOUR (60), RATE_MESSAGES_PER_DAY (400), RATE_SANDBOX_PER_HOUR (10), RATE_WEBSEARCH_PER_DAY (30), RATE_UPLOADS_PER_DAY (20), UPLOAD_MAX_MB (20), FILE_TTL_DAYS (7), FILE_PINNED_TTL_DAYS (30), MESSAGE_RETENTION_MONTHS (12)
Frontend (build) VITE_API_URL (/api/v1), VITE_USE_OFFICIAL_LOGO, VITE_COURSES

Le domaine uqo-chat.app est vérifié chez Resend (DNS GoDaddy) ; le CNAME www pointe vers ngrok.

# 12. Développement local

bash
git clone gitsrv:~/srv/git/uqo-chat.git && cd uqo-chat
cp .env.example .env            # OPENROUTER_API_KEY, FIRECRAWL_API_KEY, RESEND_API_KEY, JWT_SECRET, PROFESSOR_EMAILS…
make setup                      # uv venv 3.12 backend + sandbox-runner, npm install
make migrate                    # schéma (create_all)
make seed PROF=prof@uqo.ca      # cours + compte professeur
make sandbox &                  # :8191
make api &                      # :8190 (sert frontend/dist s'il existe ; /api/docs en dev)
make web                        # :5173, proxy /api → 8190
make ingest COURSE=imm1033 SITE=~/Desktop/Academique/UQO/UQO_COURS/_Site_web/dist/imm1033
make ingest COURSE=imm1003 SITE=~/Desktop/Academique/UQO/UQO_COURS/_Site_web/dist/imm1003

Prérequis : Python 3.12 via uv, Node 22, macOS pour sandbox-exec (sur Linux, utiliser docker compose up qui isole le sandbox avec network_mode: none). En dev, POST /auth/forgot renvoie dev_link pour se connecter sans courriel.

# 13. Tests, lint, CI

bash
make test    # pytest (41 tests) + tsc --noEmit
make lint    # ruff (E, F, I, B, UP ; ligne 100) + tsc
cd frontend && npm test        # vitest (math-guard)
python3 /tmp/uqo-qa/qa.py      # QA Playwright : connexion + chat + captures 375/1440 + détection d'overflow (script local)

Tests backend : invitations et mots de passe (respx simule Resend), flux SSE OpenRouter, RAG (ingestion HTML + BM25), préfiltre du sandbox, coercition et normalisation de specs Excel, taux de la grille de comparables, create_excel, financial_calc, nouveaux outils (make_chart, create_docx…).

CI GitHub Actions (ci.yml, si le dépôt est poussé sur GitHub) : ruff + pytest, npm ci && npm run build, construction des deux images Docker et analyse Trivy. deploy.yml (images ghcr + kubectl apply -k) correspond à la cible Kubernetes et n'est pas utilisé pour la production actuelle.

# 14. Déploiement sur le cluster MacLustr (mld)

L'app est orchestrée par maclustr-dispatch (mld) depuis la passerelle M1M32.

Manifeste (deploy/uqo-chat.manifest.json = gabarit ; le vrai, avec secrets : M1M32:~/dispatch/apps/uqo-chat.json) :

Clé Valeur
domain / port / health_path www.uqo-chat.app / 8190 / /api/v1/ready
requires runtimes pm2, ngrok, uv, uv-python@3.12 · 3 Go RAM · ports 8190, 8191 (≈ 900 Mo observés)
placement prefer: M4M64a, avoid: M3U96b (réservé hfmarketdata), M1M32 (passerelle)
sync_excludes .venv/, data/, content/, .env, frontend/node_modules/, caches, .git/
processes PM2 uqo-chat-sandbox (uvicorn server:app 127.0.0.1:8191, max_memory_restart 2G) · uqo-chat-api (uvicorn app.main:app 0.0.0.0:8190, keep-alive 75 s, max_memory_restart 3G, env de production)
ngrok PM2 uqo-chat-ngrokhttps://www.uqo-chat.app
hooks.post_sync uv venv + uv pip install -e . (backend), uv pip install -r requirements.txt (sandbox), mkdir -p data/files

Procédure depuis le laptop (le dépôt = source de vérité ; ne jamais éditer la copie du nœud) :

bash
make test lint build                                # vérifie puis produit frontend/dist
git add -A && git commit -m "…" && git push origin main
~/Desktop/cluster-skill/mld stage $PWD uqo-chat     # laptop → M1M32:~/dispatch/stage/uqo-chat/dir
~/Desktop/cluster-skill/mld deploy uqo-chat         # nœud par score (ou --node M4M64a) ; santé locale + publique ; registre
# raccourci équivalent : make deploy [NODE=M4M64a]

data/ (SQLite + fichiers) et content/ (sites ingérés) sont exclus de la synchronisation : ils persistent sur le nœud. Un mld move uqo-chat --to <nœud> déplace l'app mais il faut alors recopier data/ et content/ manuellement (ou ré-ingérer).

# 15. Exploitation

bash
NODE=M4M64a   # vérifier avec : ~/Desktop/cluster-skill/mld status | grep uqo-chat
ssh $NODE 'pm2 ls | grep uqo-chat; pm2 logs uqo-chat-api --lines 100 --nostream'
ssh $NODE 'pm2 restart uqo-chat-api'                     # après changement de manifeste / env
curl -s https://www.uqo-chat.app/api/v1/ready            # {"ok":true,"chunks":714}

# Ré-ingestion des notes de cours après un rebuild des sites (dépôts uqo-imm1003 / uqo-imm1033)
scp -r ~/Desktop/Academique/UQO/UQO_COURS/_Site_web/dist/imm10{03,33} $NODE:~/apps/uqo-chat/content/
ssh $NODE 'cd ~/apps/uqo-chat/backend && for c in imm1003 imm1033; do \
  DATA_DIR=~/apps/uqo-chat/data .venv/bin/python -m scripts.ingest --course $c --site ../content/$c; done'
ssh $NODE 'pm2 restart uqo-chat-api'

# Sauvegarde des données
ssh $NODE 'sqlite3 ~/apps/uqo-chat/data/uqochat.db ".backup /tmp/uqochat-$(date +%F).db"'
  • Registre mld (M1M32:~/dispatch/registry.json) : nœud, IP LAN, port, santé, processus ; consommé par les apps de monitoring MacLustr (macOS / iOS).
  • Réglages modifiables à chaud en base (app_settings) et depuis l'onglet Réglages du professeur.
  • Changer de modèle : MODEL_* dans le manifeste M1M32, mld deploy uqo-chat (ou pm2 restart avec env mis à jour). Vérifier le slug sur https://openrouter.ai/models avant.

# 16. Coûts LLM

  • Chaque appel est enregistré dans llm_usage (modèle, jetons, coût USD) ; /admin agrège par jour, modèle et étudiant·e (haché). Budget LLM_MONTHLY_BUDGET_USD = 2 000 $ US ; dépassement → refus poli avec message.
  • Ordres de grandeur mesurés : tour simple avec RAG ≈ 0,12 $ US (≈ 30 k jetons d'entrée avec outils et passages) ; tour riche (4 outils, classeur + graphique) ≈ 0,80 $ US.

# 17. Vie privée, sécurité, Loi 25

  • Consentement explicite à la première connexion (POST /me/consent), page /confidentialite, export et suppression du compte (DELETE /me : anonymisation des messages, purge des fichiers).
  • Analytics du professeur anonymisées (identifiants hachés, core/security.hash_user_id), aucun contenu de message ; logs sans contenu ni courriel.
  • Rétention : fichiers 7 j (30 j épinglés), messages anonymisés après 12 mois (tâche horaire).
  • Secrets uniquement dans le manifeste M1M32 et .env local (ignoré par git) ; SecretStr côté config.
  • Code étudiant / LLM jamais exécuté dans l'API ; sandbox sans réseau ; CSP stricte ; JWT signés ; mots de passe PBKDF2 ; liens à usage unique et expirants ; limitation de débit sur /auth/forgot et sur les messages / outils.
  • Logo UQO : l'autorisation formelle du Service des communications de l'UQO reste à obtenir (VITE_USE_OFFICIAL_LOGO).

# 18. Écarts assumés par rapport à la spécification

Spécification (CLAUDE.md d'origine) Réalisation actuelle Pourquoi
PostgreSQL + pgvector, Redis, MinIO, arq SQLite (SQLAlchemy async), cache / limiteur en mémoire, fichiers sur disque, tâches asyncio Déploiement mono-nœud sans dépendances ; DATABASE_URL accepte déjà Postgres, k8s/ décrit la cible complète
Embeddings openai/text-embedding-3-large via OpenRouter BM25 en mémoire + embeddings optionnels via endpoint OpenAI-compatible OpenRouter n'expose aucun modèle d'embeddings (vérifié 2026-09-05)
Lien magique SMTP à chaque connexion Invitation Resend → mot de passe choisi (PBKDF2) ; « mot de passe oublié » par courriel ; liste blanche gérée par le professeur Une seule étape par courriel puis connexion classique ; Resend plutôt qu'un SMTP UQO
Pods sandbox gVisor / Job k8s Service sandbox-runner + sandbox-exec macOS + rlimits + timeout Équivalent local ; Dockerfile + NetworkPolicy prêts pour k8s
Alembic create_all + _add_missing_columns Migration initiale à écrire au premier changement de schéma incompatible
Kubernetes + ngrok operator mld + PM2 + agent ngrok sur un nœud Mac Infrastructure réelle = cluster MacLustr
Export PDF de conversation, hors-ligne complet Non faits (PWA : app shell + cache lecture) Phase 2 / 3

# 19. Feuille de route

Export PDF d'une conversation ; file d'attente hors-ligne (Background Sync) ; évaluation pédagogique automatique en CI (40 questions par cours, juge MODEL_FAST) ; tests adverses complets du sandbox ; Alembic ; migration éventuelle Postgres + pgvector ; autorisation officielle du logo UQO ; alignement des numéros de version (pyproject, package.json) sur v0.5.

# 20. Historique des versions

Date Commit Version Contenu
2026-09-06 cd08d44 v0.1.0 Création : FastAPI + agent OpenRouter, 7 outils, sandbox, RAG, React PWA ; déployé mld sur M4M64a ; www.uqo-chat.app
2026-09-06 afb5ca4 HEAD accepté sur les routes SPA (vérifications d'uptime)
2026-09-06 f261ace v0.2 Outils robustes (coercition, comparables flexibles, normalisation de spec, réparation JSON, sortie 16 k), comptes étudiants + liens d'invitation + code d'accès dans le tableau de bord
2026-09-06 9e6193a Grille de comparables : ajustements par formule (sujet + taux unitaires) ; correction du collage de courriels en lot
2026-09-06 b3a57c0 Connexion par mot de passe (PBKDF2), changement de mot de passe, mots de passe fixés par le professeur, migration de colonnes
2026-09-06 7a51d30 · d896f91 anthropic/claude-fable-5.1 modèle par défaut ; budget mensuel 2 000 $ US
2026-09-06 1b07883 · a783830 v0.3 5 nouveaux outils (inspect_excel, edit_excel, appraisal_calc, unit_convert, make_chart, create_docx), artefacts visibles du sandbox, cartes refaites (éditeur Python, aperçu Excel multi-feuilles, « Modifier avec le tuteur »)
2026-09-06 d0aea63 · 1b12758 v0.4 Mot-symbole officiel UQO + bleu #0F6180, passe « premium mobile » (héros, tiroir par date, bulles, composer flottant, squelettes, icônes PWA / OG)
2026-09-06 d9fc592 · f7b72d8 v0.5 Liste blanche + invitations Resend remplacent le code d'accès ; lien « mot de passe oublié » sous le champ (375 px)
2026-09-06 README détaillé (ce document)

# 21. Pièges connus

  • sandbox-exec bloque bien le réseau et la lecture de ~, mais RLIMIT_AS casse numpy sur macOS → désactivé sur Darwin.
  • shiki complet précache 10 Mo dans la PWA → utiliser shiki/core avec les langages ciblés.
  • Les montants « 185 000 $ » en prose cassent remark-mathfrontend/src/lib/math-guard.ts (règle Pandoc).
  • Les gros arguments d'outils étaient tronqués à 4 k jetons de sortie (« JSON invalide ») → 16 k + réparation JSON + refus d'exécuter un appel tronqué.
  • Le manifeste deploy/ du dépôt n'a que des gabarits {{…}} : ne pas le copier tel quel sur M1M32 sans renseigner les secrets.
  • Après mld move, data/ et content/ ne suivent pas (exclus du sync) : les recopier ou ré-ingérer.
  • PM2 sous zsh : pm2 n'est pas dans le PATH des hooks → les hooks exportent ~/.local/bin et /opt/homebrew/bin.

# 22. Droits et licence

© 2026 Simon-Pierre Boucher. Dépôt privé, tous droits réservés. Contenu pédagogique indexé : notes de cours IMM1003 / IMM1033 © Simon-Pierre Boucher, UQO. Le nom et le logo UQO appartiennent à l'Université du Québec en Outaouais. Modèles fournis par OpenRouter (Anthropic, OpenAI) ; recherche web par Firecrawl ; courriels par Resend.

# 23. Contact

Simon-Pierre Boucher Courriel : contact@spboucher.ai Git personnel : https://git.spboucher.ai (spbgit, dépôt uqo-chat) Application : https://www.uqo-chat.app · notes de cours : https://www.uqo-imm1003.app · https://www.uqo-imm1033.app