docs: README détaillé (architecture, 13 outils, API, auth v0.5, RAG, sandbox, config, déploiement mld, exploitation, historique, contact)
1 changed file +482 −48
modified
README.md
+482 −48
@@ -1,74 +1,508 @@ | ||
| 1 | −# UQO-Chat | |
| 1 | +# UQO-Chat — tuteur IA pour IMM1003 · IMM1033 | |
| 2 | 2 | |
| 3 | −**Tuteur IA pour les cours IMM1003 — Éléments d'évaluation immobilière et IMM1033 — Méthodes du coût (UQO).** | |
| 4 | −Production : https://www.uqo-chat.app · Spécification complète : [`CLAUDE.md`](CLAUDE.md). | |
| 3 | +> **Tuteur IA des cours IMM1003 — Éléments d'évaluation immobilière et IMM1033 — Méthodes du coût en évaluation | |
| 4 | +> immobilière (Université du Québec en Outaouais).** Il explique la matière en citant les notes de cours officielles, | |
| 5 | +> exécute du Python dans un bac à sable isolé, produit et modifie des classeurs Excel à formules vivantes, calcule | |
| 6 | +> avec des outils d'évaluation déterministes, cherche des données de marché actuelles, analyse les fichiers déposés, | |
| 7 | +> génère des graphiques, des documents Word et des quiz interactifs. | |
| 5 | 8 | |
| 6 | −Le tuteur explique la matière en citant les notes de cours officielles (RAG), exécute du Python | |
| 7 | −dans un bac à sable isolé, produit des classeurs Excel à formules vivantes, cherche des données | |
| 8 | −de marché actuelles (Firecrawl), analyse les fichiers déposés et génère des quiz interactifs. | |
| 9 | −Tous les modèles (Claude, GPT) passent par OpenRouter avec repli automatique. | |
| 9 | +| | | | |
| 10 | +|---|---| | |
| 11 | +| **Production** | https://www.uqo-chat.app | | |
| 12 | +| **Version** | v0.5 (auth par invitation) — `backend/pyproject.toml` et `frontend/package.json` portent encore `0.1.0` | | |
| 13 | +| **App `mld`** | `uqo-chat` · API `:8190` · sandbox `:8191` · nœud actuel : `mld status` (au 2026-09-06 : **M4M64a**, `~/apps/uqo-chat`) | | |
| 14 | +| **Processus PM2** | `uqo-chat-api` · `uqo-chat-sandbox` · `uqo-chat-ngrok` | | |
| 15 | +| **Santé** | `GET /api/v1/ready` → `{"ok":true,"chunks":714}` · `GET /api/v1/health` → `{"ok":true,"service":"uqo-chat"}` | | |
| 16 | +| **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 | | |
| 17 | +| **Spécification produit** | [`CLAUDE.md`](CLAUDE.md) (résumé de la spec d'origine en 28 sections, règles d'or, écarts assumés) | | |
| 18 | +| **Auteur / contact** | **Simon-Pierre Boucher** — contact@spboucher.ai | | |
| 10 | 19 | |
| 11 | −## Architecture (déploiement MacLustr) | |
| 20 | +--- | |
| 21 | + | |
| 22 | +## Sommaire | |
| 23 | + | |
| 24 | +1. [Vue d'ensemble](#1-vue-densemble) | |
| 25 | +2. [Fonctionnalités](#2-fonctionnalités) | |
| 26 | +3. [Les 13 outils du tuteur](#3-les-13-outils-du-tuteur) | |
| 27 | +4. [Architecture](#4-architecture) | |
| 28 | +5. [Arborescence du dépôt](#5-arborescence-du-dépôt) | |
| 29 | +6. [Backend (FastAPI)](#6-backend-fastapi) | |
| 30 | +7. [Authentification, rôles et comptes](#7-authentification-rôles-et-comptes) | |
| 31 | +8. [RAG sur les notes de cours](#8-rag-sur-les-notes-de-cours) | |
| 32 | +9. [Bac à sable Python (`sandbox-runner`)](#9-bac-à-sable-python-sandbox-runner) | |
| 33 | +10. [Frontend (React PWA)](#10-frontend-react-pwa) | |
| 34 | +11. [Configuration (variables d'environnement)](#11-configuration-variables-denvironnement) | |
| 35 | +12. [Développement local](#12-développement-local) | |
| 36 | +13. [Tests, lint, CI](#13-tests-lint-ci) | |
| 37 | +14. [Déploiement sur le cluster MacLustr (`mld`)](#14-déploiement-sur-le-cluster-maclustr-mld) | |
| 38 | +15. [Exploitation](#15-exploitation) | |
| 39 | +16. [Coûts LLM](#16-coûts-llm) | |
| 40 | +17. [Vie privée, sécurité, Loi 25](#17-vie-privée-sécurité-loi-25) | |
| 41 | +18. [Écarts assumés par rapport à la spécification](#18-écarts-assumés-par-rapport-à-la-spécification) | |
| 42 | +19. [Feuille de route](#19-feuille-de-route) | |
| 43 | +20. [Historique des versions](#20-historique-des-versions) | |
| 44 | +21. [Pièges connus](#21-pièges-connus) | |
| 45 | +22. [Droits et licence](#22-droits-et-licence) | |
| 46 | +23. [Contact](#23-contact) | |
| 47 | + | |
| 48 | +--- | |
| 49 | + | |
| 50 | +## 1. Vue d'ensemble | |
| 51 | + | |
| 52 | +UQO-Chat est une application web (PWA) destinée aux étudiant·es des deux cours d'évaluation immobilière donnés | |
| 53 | +par Simon-Pierre Boucher à l'UQO. Le professeur inscrit les courriels des étudiant·es ; chacun·e reçoit une | |
| 54 | +invitation, choisit un mot de passe et converse en français avec un tuteur qui : | |
| 55 | + | |
| 56 | +- **s'appuie d'abord sur la matière du cours** : recherche BM25 dans le HTML des sites de notes interactives | |
| 57 | + (https://www.uqo-imm1003.app, https://www.uqo-imm1033.app) avec citations cliquables vers la séance et la section ; | |
| 58 | +- **calcule juste** : calculateurs déterministes d'évaluation (méthode du coût, dépréciation, terrain, | |
| 59 | + capitalisation, comparables, six fonctions du dollar, conversions d'unités) plutôt que de l'arithmétique « de tête » ; | |
| 60 | +- **produit des livrables** : classeurs Excel à formules vivantes (6 gabarits ou spec libre), modification de | |
| 61 | + classeurs déposés, graphiques PNG, documents Word, quiz interactifs ; | |
| 62 | +- **exécute du code** : Python (pandas, numpy, matplotlib…) dans un service séparé sous `sandbox-exec`, sans réseau ; | |
| 63 | +- **va chercher le présent** : données de marché, taux, coûts, règlements via Firecrawl (cache 6 h) ; | |
| 64 | +- **est piloté par le professeur** : tableau de bord (activité anonymisée, étudiants et invitations, contenu | |
| 65 | + ingéré, consignes et annonces par cours, réglages), page admin des coûts. | |
| 66 | + | |
| 67 | +Tous les appels de modèles passent par **OpenRouter** (`anthropic/claude-fable-5.1` par défaut, repli | |
| 68 | +`openai/gpt-5.5`, `anthropic/claude-opus-4.6` pour la réflexion approfondie, `openai/gpt-5.4-nano` pour les titres | |
| 69 | +et classifications, `anthropic/claude-sonnet-4.6` pour la vision), en **streaming SSE** avec boucle agentique | |
| 70 | +(jusqu'à 12 itérations d'outils, outils exécutés en parallèle, 16 000 jetons de sortie). | |
| 71 | + | |
| 72 | +## 2. Fonctionnalités | |
| 73 | + | |
| 74 | +### Pour l'étudiant·e | |
| 75 | +- Conversations par cours (IMM1003 / IMM1033), titrées automatiquement, regroupées par date dans le tiroir, | |
| 76 | + renommage, suppression, régénération d'une réponse, arrêt d'un tour en cours, rétroaction 👍/👎 par message. | |
| 77 | +- Réponses en Markdown + **KaTeX** (formules), blocs de code colorés (**shiki** allégé), garde-fou `math-guard` | |
| 78 | + pour que « 185 000 $ » en prose ne soit pas pris pour du LaTeX. | |
| 79 | +- **Cartes d'outils** riches : source de cours (extrait + lien), calculateur d'évaluation (tableau des étapes), | |
| 80 | + calculateur financier, conversion d'unités, **éditeur Python** (onglets Code / Sortie / Graphiques, modifier et | |
| 81 | + ré-exécuter, télécharger `.py`), **aperçu Excel multi-feuilles** avec formules `ƒ` et bouton « Modifier avec le | |
| 82 | + tuteur », graphique, document Word, analyse de fichier, recherche web (sources), **quiz interactif** (correction | |
| 83 | + immédiate, explication, score enregistré). | |
| 84 | +- Dépôt de fichiers (xlsx, csv, pdf, docx, pptx, images ; 20 Mo ; 20 / jour) conservés 7 jours (30 si épinglés), | |
| 85 | + panneau des artefacts de la conversation, liens de téléchargement signés. | |
| 86 | +- Préférences : tutoiement / vouvoiement, langue, thème ; consentement Loi 25, export et suppression du compte. | |
| 87 | +- PWA installable (app shell + cache lecture ≈ 2 Mo), mobile-first 375 px, héros dégradé, composer flottant. | |
| 88 | + | |
| 89 | +### Pour le professeur (`/professeur`) | |
| 90 | +- **Activité** : messages, conversations, étudiant·es actifs, outils appelés, sujets, rétroactions — agrégés et | |
| 91 | + anonymisés (identifiants hachés), sans contenu de message. | |
| 92 | +- **Étudiants** : ajout en lot (collage de courriels), rôles, statut *invité / activé*, **invitation Resend** | |
| 93 | + « Bienvenue sur UQO-Chat — choisis ton mot de passe », relance individuelle ou « Inviter les N jamais invité·es », | |
| 94 | + mot de passe défini manuellement, suppression. | |
| 95 | +- **Contenu** : ingestion de fichiers (PDF, DOCX, PPTX, MD) ou d'un site de notes, activation / désactivation d'un | |
| 96 | + document, réindexation. | |
| 97 | +- **Réglages par cours** : consignes additionnelles injectées dans le prompt, annonce affichée aux étudiant·es, | |
| 98 | + échéances, modèle. | |
| 99 | +- Promotion d'un compte au rôle professeur / admin. | |
| 100 | + | |
| 101 | +### Pour l'admin (`/admin`) | |
| 102 | +- Coûts LLM (par jour, par modèle, par étudiant·e haché), budget mensuel et projection. | |
| 103 | + | |
| 104 | +## 3. Les 13 outils du tuteur | |
| 105 | + | |
| 106 | +Chaque outil = `backend/app/tools/<nom>.py` + schéma JSON `app/tools/schemas/<nom>.json` + test + carte React | |
| 107 | +`frontend/src/components/tools/<nom>-card.tsx`. Les arguments sont **coercés avec tolérance** (`tools/coerce.py` : | |
| 108 | +« 425 000 $ », « +3 % », « Oui » → nombres / booléens) et les appels tronqués par le modèle sont **réparés ou refusés** | |
| 109 | +plutôt qu'exécutés à moitié. | |
| 110 | + | |
| 111 | +| Outil | Rôle | Détails | | |
| 112 | +|---|---|---| | |
| 113 | +| `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` | | |
| 114 | +| `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é | | |
| 115 | +| `financial_calc` | Six fonctions du dollar, VAN / TRI, âge-vie | `numpy-financial` | | |
| 116 | +| `unit_convert` | m² ↔ pi², arpent, acre, hectare, $/m² ↔ $/pi² | unités québécoises incluses | | |
| 117 | +| `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>` | | |
| 118 | +| `make_chart` | Graphique rapide à partir de données | matplotlib in-process, palette UQO, PNG | | |
| 119 | +| `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) | | |
| 120 | +| `inspect_excel` | Lire un classeur (déposé ou généré) | feuilles, dimensions, en-têtes, formules, aperçu | | |
| 121 | +| `edit_excel` | **Modifier** un classeur existant | ajouter colonne / ligne / feuille / graphique, corriger une cellule, formater ; versions `_v2`, `_v3`… (`tools/excel_ops.py`) | | |
| 122 | +| `create_docx` | Document Word depuis Markdown | fiche de révision, plan d'étude, gabarit de rapport (`python-docx`) | | |
| 123 | +| `analyze_file` | Lire un fichier déposé | xlsx / csv / pdf (`pdfplumber`) / docx / pptx / image (modèle vision) | | |
| 124 | +| `web_search` | Données actuelles seulement | Firecrawl, cache 6 h, 30 / jour / étudiant·e | | |
| 125 | +| `generate_quiz` | Quiz interactif | questions à choix, explication, enregistrement des tentatives | | |
| 126 | + | |
| 127 | +Le prompt système (`app/llm/prompts/system_tutor.md` + `guardrails.md` + `course_imm10xx.md` + consignes du | |
| 128 | +professeur + `TOOL_HINTS` dans `agent.py`) impose : chercher la matière avant de répondre, enchaîner les outils | |
| 129 | +(ex. `search_course_content → appraisal_calc → create_excel → make_chart`), **modifier** un fichier existant avec | |
| 130 | +`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 | |
| 131 | +en cours. | |
| 132 | + | |
| 133 | +## 4. Architecture | |
| 134 | + | |
| 135 | +``` | |
| 136 | +Internet ── ngrok (www.uqo-chat.app, TLS) ──► uqo-chat-api :8190 FastAPI + SSE + SPA React (PM2) | |
| 137 | + │ | |
| 138 | + ├──► uqo-chat-sandbox :8191 sandbox-runner (PM2) — python -I -B sous sandbox-exec, | |
| 139 | + │ sans réseau, écriture confinée, rlimits, timeout ≤ 60 s | |
| 140 | + ├──► SQLite data/uqochat.db (SQLAlchemy async ; Postgres via DATABASE_URL) | |
| 141 | + │ index BM25 reconstruit en mémoire au démarrage | |
| 142 | + ├──► fichiers data/files/<user>/<id>.<ext> (TTL 7 j / 30 j épinglés, purge horaire) | |
| 143 | + └──► HTTPS sortant : OpenRouter (LLM), Firecrawl (web), Resend (courriels) | |
| 144 | +``` | |
| 145 | + | |
| 146 | +- **Un seul point d'entrée LLM** : `app/llm/openrouter.py` (`LLMClient`, SSE, repli de modèle, comptage des jetons | |
| 147 | + et des coûts → table `llm_usage`). `app/llm/router.py` choisit le modèle (primaire / raisonnement / rapide / vision). | |
| 148 | +- **Boucle agentique** : `app/llm/agent.py` — construit le prompt (cours, préférences, fichiers, échéances, | |
| 149 | + annonce), streame les deltas au client (`token`, `tool_call`, `tool_result`, `done`, `error`), exécute les outils en | |
| 150 | + parallèle via `app/tools/registry.py` (`ToolContext` = utilisateur, conversation, fichiers, cours), persiste | |
| 151 | + messages et `tool_calls`, enregistre usage et analytics. | |
| 152 | +- **Aucun code étudiant / LLM ne s'exécute dans le processus API** : toujours `sandbox-runner` | |
| 153 | + (`app/sandbox/client.py`, jeton `SANDBOX_TOKEN`). | |
| 154 | +- **Maintenance** (`main.py`, toutes les heures) : purge des fichiers expirés, anonymisation des messages de plus de | |
| 155 | + `MESSAGE_RETENTION_MONTHS` (12). | |
| 156 | +- **Sécurité HTTP** : CORS restreint, en-têtes `nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy`, | |
| 157 | + `Permissions-Policy`, HSTS en production, **CSP** stricte sur le HTML (`script-src 'self'`, `frame-ancestors 'none'`). | |
| 158 | +- **Frontend servi par l'API** : `frontend/dist` monté sur `/assets` + repli SPA (`GET`/`HEAD` sur toute route | |
| 159 | + → `index.html`), ce qui permet les vérifications d'uptime en `HEAD`. | |
| 160 | + | |
| 161 | +Les manifestes **Kubernetes** (`k8s/`) et `docker-compose.yml` reproduisent la cible décrite dans la spécification | |
| 162 | +(Postgres + pgvector, Redis, MinIO, sandbox durci `network_mode: none`, NetworkPolicy deny-all, HPA) pour un futur | |
| 163 | +cluster ; la production actuelle tourne sur un nœud Mac du cluster via `mld`. | |
| 164 | + | |
| 165 | +## 5. Arborescence du dépôt | |
| 12 | 166 | |
| 13 | 167 | ``` |
| 14 | −Internet ── ngrok (www.uqo-chat.app) ──► uqo-chat-api :8190 FastAPI + SSE + SPA React (PM2) | |
| 15 | − │ | |
| 16 | − ├──► uqo-chat-sandbox :8191 (PM2, sandbox-exec : sans réseau) | |
| 17 | − ├──► SQLite data/uqochat.db (index BM25 en mémoire) | |
| 18 | − ├──► fichiers data/files/ (TTL 7 j / 30 j épinglés) | |
| 19 | − └──► OpenRouter · Firecrawl (HTTPS sortant) | |
| 168 | +uqo-chat/ | |
| 169 | +├── README.md · CLAUDE.md · Makefile · .env.example · docker-compose.yml | |
| 170 | +├── .github/workflows/ ci.yml (ruff + pytest + tsc + build + trivy) · deploy.yml (images ghcr + kubectl, non utilisé en prod) | |
| 171 | +├── deploy/uqo-chat.manifest.json gabarit du manifeste mld (les secrets sont dans M1M32:~/dispatch/apps/uqo-chat.json) | |
| 172 | +├── backend/ Python 3.12 · FastAPI · SQLAlchemy async | |
| 173 | +│ ├── pyproject.toml dépendances (fastapi, uvicorn, pydantic, sqlalchemy, aiosqlite, asyncpg, httpx[http2], | |
| 174 | +│ │ structlog, python-jose, passlib, openpyxl, pandas, numpy(-financial), matplotlib, | |
| 175 | +│ │ pdfplumber, python-docx, python-pptx, selectolax, markdown-it-py, aiosmtplib, orjson) | |
| 176 | +│ ├── Dockerfile | |
| 177 | +│ ├── app/ | |
| 178 | +│ │ ├── main.py application, middlewares, SPA, tâche de maintenance | |
| 179 | +│ │ ├── db.py moteur async, SessionLocal, init_db (create_all + _add_missing_columns) | |
| 180 | +│ │ ├── models/ User, MagicLink, Course, Conversation, Message, ToolCall, StoredFile, Quiz, QuizAttempt, | |
| 181 | +│ │ │ CourseDocument, CourseChunk, AnalyticsEvent, LLMUsage, AppSetting | |
| 182 | +│ │ ├── api/v1/ health, auth, conversations, chat, files, quiz, courses, professor, admin | |
| 183 | +│ │ ├── core/ config (Settings), logging (structlog, masquage content/text/email), security (JWT, | |
| 184 | +│ │ │ PBKDF2, hachage d'identifiants), ratelimit (mémoire), cache | |
| 185 | +│ │ ├── llm/ openrouter.py, agent.py, router.py, schemas.py, prompts/*.md | |
| 186 | +│ │ ├── rag/ ingest.py (HTML des sites / PDF / DOCX / PPTX / MD → chunks), chunking.py, retriever.py (BM25) | |
| 187 | +│ │ ├── sandbox/client.py client HTTP du sandbox-runner | |
| 188 | +│ │ ├── services/ users, invites, mail (Resend + SMTP), conversations, files, courses, analytics, costs | |
| 189 | +│ │ └── tools/ registry, all, coerce, excel_spec, excel_ops, 13 outils, schemas/*.json, excel_templates/ (6) | |
| 190 | +│ ├── scripts/ seed.py (cours + professeur), ingest.py (--course --site | --path) | |
| 191 | +│ └── tests/ 41 tests (auth/invitations respx, SSE OpenRouter, RAG, préfiltre sandbox, outils) | |
| 192 | +├── sandbox-runner/ service FastAPI minimal : POST /run {code, files_in, timeout_s} · runner.py (sandbox-exec, | |
| 193 | +│ rlimits, prélude matplotlib Agg, capture des figures, troncature 50 Ko) · Dockerfile · requirements.txt | |
| 194 | +├── frontend/ React 18 · Vite 5 · TypeScript · Tailwind 3 · PWA | |
| 195 | +│ ├── src/app/router.tsx /connexion · /mot-de-passe · /confidentialite · /professeur · /admin · / · /c/:id | |
| 196 | +│ ├── src/components/ chat/ (chat-view, composer, message-bubble, streaming-text, code-block, tool-call-timeline, | |
| 197 | +│ │ empty-state) · tools/ (13 cartes + tool-card, artifact-chips) · layout/ (app-shell, sidebar) · ui/ | |
| 198 | +│ ├── src/features/ auth/ (login, set-password, privacy, settings) · conversations/artifacts-panel · | |
| 199 | +│ │ professor/ (professor-page, admin-costs-page) | |
| 200 | +│ ├── src/stores/ zustand : auth, chat, ui (dont ui.draft pour « Modifier avec le tuteur ») | |
| 201 | +│ ├── src/lib/ api.ts (fetch + SSE), types.ts, format.ts, math-guard.ts (+ test), cn.ts | |
| 202 | +│ ├── src/theme/uqo.ts · src/i18n/fr-CA.json · src/index.css · tailwind.config.ts · vite.config.ts (PWA, proxy /api) | |
| 203 | +│ └── public/ uqo-logo.png, uqo-logo-white.png, icons/ (favicon, 192/512, maskable, apple-touch, og.png) | |
| 204 | +├── k8s/ base/ (namespace, api, web, worker, sandbox-runner, postgres, redis, minio, services, | |
| 205 | +│ networkpolicies, hpa) · overlays/staging|prod · ngrok/ (operator, ingress) | |
| 206 | +├── content/ (ignoré) copies locales des sites de notes pour l'ingestion | |
| 207 | +└── data/ (ignoré) uqochat.db + files/ | |
| 20 | 208 | ``` |
| 21 | 209 | |
| 22 | −Les manifestes Kubernetes (`k8s/`) et `docker-compose.yml` reproduisent la cible décrite dans la | |
| 23 | −spécification (Postgres + pgvector, Redis, MinIO, sandbox durci) pour un futur cluster ; la | |
| 24 | −production actuelle tourne sur un nœud Mac du cluster via `mld` (voir `deploy/`). | |
| 210 | +## 6. Backend (FastAPI) | |
| 211 | + | |
| 212 | +Préfixe commun **`/api/v1`**. Authentification par **JWT** (`Authorization: Bearer`, 24 h) + jeton de | |
| 213 | +rafraîchissement (30 j). Documentation OpenAPI disponible **en développement seulement** (`/api/docs`). | |
| 214 | + | |
| 215 | +| Groupe | Routes | Notes | | |
| 216 | +|---|---|---| | |
| 217 | +| Santé | `GET /health` · `GET /ready` | `ready` renvoie le nombre de passages indexés | | |
| 218 | +| 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 | | |
| 219 | +| Moi | `GET /me` · `PATCH /me/preferences` · `POST /me/password` · `POST /me/consent` · `DELETE /me` | suppression = anonymisation + purge des fichiers | | |
| 220 | +| Cours | `GET /courses` | cours actifs, annonces, échéances | | |
| 221 | +| Conversations | `GET/POST /conversations` · `GET/PATCH/DELETE /conversations/{id}` · `POST /conversations/{id}/messages/{mid}/feedback` | | | |
| 222 | +| 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` | | |
| 223 | +| 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 | | |
| 224 | +| Quiz | `GET /quiz/{id}` · `POST /quiz/{id}/answers` | | | |
| 225 | +| 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` | | |
| 226 | +| Admin | `GET /costs` | rôle `admin` | | |
| 227 | + | |
| 228 | +**Modèle de données** (SQLAlchemy, `create_all` au démarrage + ajout des colonnes manquantes) : | |
| 229 | + | |
| 230 | +| Table | Contenu | | |
| 231 | +|---|---| | |
| 232 | +| `users` | courriel, rôle (`student` / `professor` / `admin`), mot de passe PBKDF2, préférences JSON, consentement, `invited_at`, dernier accès | | |
| 233 | +| `magic_links` | jetons à usage unique (`purpose` = `invite` / `reset` / legacy), expiration | | |
| 234 | +| `courses` | IMM1003, IMM1033 : libellé, consignes du professeur, annonce, échéances, modèle | | |
| 235 | +| `conversations` · `messages` · `tool_calls` | fil, rôle, contenu, jetons, coût, rétroaction, appels d'outils (args, résultat, durée) | | |
| 236 | +| `files` | fichiers déposés / générés : propriétaire, conversation, type, taille, hachage, épinglé, expiration | | |
| 237 | +| `quizzes` · `quiz_attempts` | quiz générés et tentatives | | |
| 238 | +| `course_documents` · `course_chunks` | documents ingérés (site, PDF…) et passages BM25 | | |
| 239 | +| `analytics_events` | événements anonymisés (identifiant haché) | | |
| 240 | +| `llm_usage` | usage par appel : modèle, jetons entrée / sortie, coût USD | | |
| 241 | +| `app_settings` | réglages clé-valeur modifiables à chaud | | |
| 242 | + | |
| 243 | +**Limites par étudiant·e** (config) : 60 messages / h, 400 / jour, 10 exécutions sandbox / h, 30 recherches web / | |
| 244 | +jour, 20 dépôts / jour, 20 Mo par fichier. Dépassement → `429` avec message français. | |
| 245 | + | |
| 246 | +**Journalisation** : `structlog` JSON en production ; les clés `content`, `text`, `email`… sont masquées | |
| 247 | +(`core/logging.py`). Jamais de contenu de message dans les logs. | |
| 248 | + | |
| 249 | +## 7. Authentification, rôles et comptes | |
| 250 | + | |
| 251 | +Flux **v0.5** (le code d'accès au cours des versions précédentes est supprimé) : | |
| 252 | + | |
| 253 | +1. Le professeur ajoute les courriels dans **Étudiants** (`POST /professor/students`, création en lot). Les comptes | |
| 254 | + créés par le professeur sont acceptés même hors `@uqo.ca` ; sinon `ALLOWED_EMAIL_DOMAINS` s'applique. | |
| 255 | +2. Invitation (`send_invitations`, `POST /students/{id}/invite`, `POST /students/invite-all`) : courriel **Resend** | |
| 256 | + « Bienvenue sur UQO-Chat — choisis ton mot de passe » avec lien `/mot-de-passe?token=…` (`purpose=invite`, | |
| 257 | + `INVITE_TTL_DAYS` = 14). Gabarits HTML aux couleurs UQO dans `services/mail.py` (Resend `/emails` et | |
| 258 | + `/emails/batch` ≤ 100 ; repli SMTP `aiosmtplib`). | |
| 259 | +3. L'étudiant·e choisit son mot de passe (`GET /auth/password-token` valide le jeton, `POST /auth/set-password`), | |
| 260 | + puis se connecte (`POST /auth/login`, PBKDF2). | |
| 261 | +4. « Première connexion ou mot de passe oublié » → `POST /auth/forgot` : lien `purpose=reset` de | |
| 262 | + `RESET_TTL_MINUTES` = 60, envoyé **uniquement** aux adresses inscrites ou listées dans `PROFESSOR_EMAILS` / | |
| 263 | + `ADMIN_EMAILS` / `INVITED_EMAILS` (sinon `404` avec un indice : « adresse non inscrite » ou « adresse non admise »). | |
| 264 | + Limitation de débit : 20 demandes / h par IP, 5 / h par adresse. En dev, la réponse contient `dev_link`. | |
| 265 | +5. Sans `RESEND_API_KEY` ni SMTP, les liens sont affichés dans le tableau de bord pour copie manuelle. | |
| 266 | + | |
| 267 | +Rôles : `student` (chat), `professor` (tableau de bord, étudiants, contenu, réglages), `admin` (coûts, promotion). | |
| 268 | +Les adresses de `PROFESSOR_EMAILS` / `ADMIN_EMAILS` obtiennent leur rôle à la connexion. | |
| 269 | + | |
| 270 | +État des comptes en production au 2026-09-06 : le professeur (`boucsi02@uqo.ca`) a son mot de passe ; l'admin | |
| 271 | +(`spbou4@icloud.com`) a reçu son courriel « choisis ton mot de passe » ; **19 étudiant·es sont inscrits sans avoir été | |
| 272 | +invités** — le professeur déclenche l'envoi via « Inviter les 19 jamais invité·es ». | |
| 273 | + | |
| 274 | +## 8. RAG sur les notes de cours | |
| 275 | + | |
| 276 | +- **Source** : le HTML généré des sites de notes interactives (`dist/<cours>/seance/NN/index.html`, dépôts | |
| 277 | + `uqo-imm1003` / `uqo-imm1033`), copié sur le nœud dans `~/apps/uqo-chat/content/<cours>/`. Aussi PDF, DOCX, PPTX, | |
| 278 | + MD déposés par le professeur. | |
| 279 | +- **Ingestion** (`rag/ingest.py`) : `selectolax` nettoie le HTML (scripts, SVG, widgets, navigation), reconvertit le | |
| 280 | + KaTeX en `$…$` via l'annotation MathML, découpe par section (`chunking.py`, chevauchement), calcule un hachage par | |
| 281 | + passage, écrit `course_documents` + `course_chunks` avec l'URL publique (`SITE_URLS`) et l'ancre. | |
| 282 | +- **Index** : **BM25 en mémoire** (`rag/retriever.py`), reconstruit au démarrage (`/ready` renvoie le nombre de | |
| 283 | + passages : 714 pour les deux cours) et après chaque ingestion. | |
| 284 | +- **Embeddings** : optionnels via un endpoint OpenAI-compatible (`EMBEDDINGS_BASE_URL`, `MODEL_EMBEDDINGS`) ; | |
| 285 | + OpenRouter n'en expose aucun (vérifié 2026-09-05), donc BM25 seul en production. | |
| 286 | +- **Après un rebuild des sites de notes**, ré-ingérer (voir §15). | |
| 287 | + | |
| 288 | +## 9. Bac à sable Python (`sandbox-runner`) | |
| 25 | 289 | |
| 26 | −## Démarrage local | |
| 290 | +Service FastAPI minimal (`server.py`) : `GET /healthz`, `POST /run {code ≤ 200 000 car., files_in[], timeout_s 1–60}`, | |
| 291 | +jeton `SANDBOX_TOKEN` en `Authorization: Bearer`, `SANDBOX_MAX_PARALLEL` = 4 exécutions simultanées (pool de threads + | |
| 292 | +sémaphore). | |
| 293 | + | |
| 294 | +`runner.py` — couches d'isolation : | |
| 295 | + | |
| 296 | +| Couche | Détail | | |
| 297 | +|---|---| | |
| 298 | +| macOS | profil **`sandbox-exec`** (seatbelt) : réseau interdit, écriture uniquement dans le répertoire de la course, lecture de `~` bloquée | | |
| 299 | +| Linux (conteneur) | pod durci attendu (NetworkPolicy deny-all, rootfs lecture seule, `cap_drop ALL`, `pids_limit 64`, 512 Mo) | | |
| 300 | +| 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 | | |
| 301 | +| 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 | | |
| 302 | + | |
| 303 | +Les fichiers déposés **et** les artefacts déjà générés dans la conversation (classeurs, graphiques, documents) | |
| 304 | +sont copiés dans `inputs/` avant l'exécution ; les fichiers produits reviennent comme artefacts. | |
| 305 | + | |
| 306 | +## 10. Frontend (React PWA) | |
| 307 | + | |
| 308 | +- **Pile** : React 18, Vite 5, TypeScript 5, Tailwind 3, zustand, TanStack Query, react-router 6, react-markdown + | |
| 309 | + remark-gfm + remark-math + rehype-katex, `shiki/core` (langages ciblés ; le paquet complet précachait 10 Mo), Radix | |
| 310 | + (dialog, dropdown, tooltip), lucide-react, recharts, `@microsoft/fetch-event-source` (SSE avec en-têtes). | |
| 311 | +- **Routes** : `/connexion`, `/mot-de-passe` (choix / réinitialisation), `/confidentialite`, `/professeur` (garde | |
| 312 | + rôle), `/admin` (garde rôle), `/` et `/c/:id` (chat), `*` → `/`. | |
| 313 | +- **Marque** : couleur `#0F6180` alignée sur le mot-symbole officiel UQO (`public/uqo-logo*.png`, recoloré depuis | |
| 314 | + 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. | |
| 315 | +- **Mobile-first** : cible 375 px ; tiroir groupé par date, bulles et composer flottant, squelettes de chargement, | |
| 316 | + bouton de défilement dégagé du composer, lien « mot de passe oublié » sous le champ sans retour à la ligne. | |
| 317 | +- **PWA** (`vite-plugin-pwa`) : app shell + cache lecture (≈ 2 Mo précachés), manifest, icônes maskable. | |
| 318 | +- **`lib/math-guard.ts`** : applique la règle Pandoc pour ne rendre en LaTeX que les `$…$` plausibles (test vitest). | |
| 319 | +- **Build** : `tsc --noEmit && vite build` → `frontend/dist` servi par l'API en production. | |
| 320 | + | |
| 321 | +## 11. Configuration (variables d'environnement) | |
| 322 | + | |
| 323 | +Lues par `pydantic-settings` depuis `.env` (racine ou `backend/`) puis l'environnement ; gabarit complet dans | |
| 324 | +`.env.example`. Les valeurs de production sont dans le manifeste `M1M32:~/dispatch/apps/uqo-chat.json` (jamais dans | |
| 325 | +le dépôt). | |
| 326 | + | |
| 327 | +| Groupe | Variables (défaut) | | |
| 328 | +|---|---| | |
| 329 | +| App | `APP_ENV` (development), `APP_URL`, `PORT` (8190), `CORS_ORIGINS`, `LOG_LEVEL`, `DATA_DIR` (`./data`), `FRONTEND_DIST`, `COURSES` (IMM1003,IMM1033), `TERM_LABEL` | | |
| 330 | +| 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) | | |
| 331 | +| Embeddings (optionnel) | `EMBEDDINGS_BASE_URL`, `EMBEDDINGS_API_KEY`, `MODEL_EMBEDDINGS` | | |
| 332 | +| Web | `FIRECRAWL_API_KEY`, `FIRECRAWL_BASE_URL`, `WEB_SEARCH_CACHE_TTL_S` (21600) | | |
| 333 | +| Données | `DATABASE_URL` (vide = SQLite `DATA_DIR/uqochat.db`), `REDIS_URL` (vide = mémoire) | | |
| 334 | +| 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` | | |
| 335 | +| 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) | | |
| 336 | +| 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` | | |
| 337 | +| 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) | | |
| 338 | +| Frontend (build) | `VITE_API_URL` (/api/v1), `VITE_USE_OFFICIAL_LOGO`, `VITE_COURSES` | | |
| 339 | + | |
| 340 | +Le domaine `uqo-chat.app` est vérifié chez Resend (DNS GoDaddy) ; le CNAME `www` pointe vers ngrok. | |
| 341 | + | |
| 342 | +## 12. Développement local | |
| 27 | 343 | |
| 28 | 344 | ```bash |
| 29 | −cp .env.example .env # OPENROUTER_API_KEY, FIRECRAWL_API_KEY, RESEND_API_KEY… | |
| 30 | −make setup # venvs uv (backend, sandbox) + npm install | |
| 345 | +git clone gitsrv:~/srv/git/uqo-chat.git && cd uqo-chat | |
| 346 | +cp .env.example .env # OPENROUTER_API_KEY, FIRECRAWL_API_KEY, RESEND_API_KEY, JWT_SECRET, PROFESSOR_EMAILS… | |
| 347 | +make setup # uv venv 3.12 backend + sandbox-runner, npm install | |
| 348 | +make migrate # schéma (create_all) | |
| 349 | +make seed PROF=prof@uqo.ca # cours + compte professeur | |
| 31 | 350 | make sandbox & # :8191 |
| 32 | −make api & # :8190 (sert frontend/dist s'il existe) | |
| 33 | −make web # :5173 avec proxy /api | |
| 351 | +make api & # :8190 (sert frontend/dist s'il existe ; /api/docs en dev) | |
| 352 | +make web # :5173, proxy /api → 8190 | |
| 34 | 353 | make ingest COURSE=imm1033 SITE=~/Desktop/Academique/UQO/UQO_COURS/_Site_web/dist/imm1033 |
| 35 | −make test && make lint | |
| 354 | +make ingest COURSE=imm1003 SITE=~/Desktop/Academique/UQO/UQO_COURS/_Site_web/dist/imm1003 | |
| 36 | 355 | ``` |
| 37 | 356 | |
| 38 | −Connexion : le professeur inscrit les courriels des étudiant·es (onglet **Étudiants**) ; chacun·e reçoit | |
| 39 | −un courriel Resend « Bienvenue sur UQO-Chat » avec un lien `/mot-de-passe?token=…` (valide | |
| 40 | −`INVITE_TTL_DAYS`) pour choisir son mot de passe, puis se connecte avec courriel + mot de passe. | |
| 41 | −« Première connexion ou mot de passe oublié » renvoie un lien (`RESET_TTL_MINUTES`). Seules les adresses | |
| 42 | −inscrites (ou listées dans `PROFESSOR_EMAILS` / `ADMIN_EMAILS` / `INVITED_EMAILS`) peuvent demander un lien. | |
| 43 | −Sans `RESEND_API_KEY` (ni SMTP), les liens sont affichés dans le tableau de bord pour être copiés ; en dev, | |
| 44 | −`/auth/forgot` renvoie `dev_link`. | |
| 357 | +Prérequis : Python 3.12 via `uv`, Node 22, macOS pour `sandbox-exec` (sur Linux, utiliser `docker compose up` | |
| 358 | +qui isole le sandbox avec `network_mode: none`). En dev, `POST /auth/forgot` renvoie `dev_link` pour se connecter | |
| 359 | +sans courriel. | |
| 360 | + | |
| 361 | +## 13. Tests, lint, CI | |
| 362 | + | |
| 363 | +```bash | |
| 364 | +make test # pytest (41 tests) + tsc --noEmit | |
| 365 | +make lint # ruff (E, F, I, B, UP ; ligne 100) + tsc | |
| 366 | +cd frontend && npm test # vitest (math-guard) | |
| 367 | +python3 /tmp/uqo-qa/qa.py # QA Playwright : connexion + chat + captures 375/1440 + détection d'overflow (script local) | |
| 368 | +``` | |
| 369 | + | |
| 370 | +Tests backend : invitations et mots de passe (`respx` simule Resend), flux SSE OpenRouter, RAG (ingestion HTML + | |
| 371 | +BM25), préfiltre du sandbox, coercition et normalisation de specs Excel, taux de la grille de comparables, | |
| 372 | +`create_excel`, `financial_calc`, nouveaux outils (`make_chart`, `create_docx`…). | |
| 373 | + | |
| 374 | +CI GitHub Actions (`ci.yml`, si le dépôt est poussé sur GitHub) : ruff + pytest, `npm ci && npm run build`, | |
| 375 | +construction des deux images Docker et analyse Trivy. `deploy.yml` (images ghcr + `kubectl apply -k`) correspond à la | |
| 376 | +cible Kubernetes et **n'est pas** utilisé pour la production actuelle. | |
| 377 | + | |
| 378 | +## 14. Déploiement sur le cluster MacLustr (`mld`) | |
| 379 | + | |
| 380 | +L'app est orchestrée par **maclustr-dispatch (`mld`)** depuis la passerelle **M1M32**. | |
| 381 | + | |
| 382 | +Manifeste (`deploy/uqo-chat.manifest.json` = gabarit ; le vrai, avec secrets : `M1M32:~/dispatch/apps/uqo-chat.json`) : | |
| 45 | 383 | |
| 46 | −## Déploiement | |
| 384 | +| Clé | Valeur | | |
| 385 | +|---|---| | |
| 386 | +| `domain` / `port` / `health_path` | `www.uqo-chat.app` / `8190` / `/api/v1/ready` | | |
| 387 | +| `requires` | runtimes `pm2`, `ngrok`, `uv`, `uv-python@3.12` · 3 Go RAM · ports 8190, 8191 (≈ 900 Mo observés) | | |
| 388 | +| `placement` | `prefer: M4M64a`, `avoid: M3U96b` (réservé hfmarketdata), `M1M32` (passerelle) | | |
| 389 | +| `sync_excludes` | `.venv/`, `data/`, `content/`, `.env`, `frontend/node_modules/`, caches, `.git/` | | |
| 390 | +| `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) | | |
| 391 | +| `ngrok` | PM2 `uqo-chat-ngrok` → `https://www.uqo-chat.app` | | |
| 392 | +| `hooks.post_sync` | `uv venv` + `uv pip install -e .` (backend), `uv pip install -r requirements.txt` (sandbox), `mkdir -p data/files` | | |
| 393 | + | |
| 394 | +Procédure depuis le laptop (le dépôt = source de vérité ; **ne jamais éditer la copie du nœud**) : | |
| 47 | 395 | |
| 48 | 396 | ```bash |
| 49 | −make build # frontend/dist | |
| 50 | −~/Desktop/cluster-skill/mld stage $PWD uqo-chat # laptop → passerelle | |
| 51 | −~/Desktop/cluster-skill/mld deploy uqo-chat # nœud choisi par score (--node X pour forcer) | |
| 397 | +make test lint build # vérifie puis produit frontend/dist | |
| 398 | +git add -A && git commit -m "…" && git push origin main | |
| 399 | +~/Desktop/cluster-skill/mld stage $PWD uqo-chat # laptop → M1M32:~/dispatch/stage/uqo-chat/dir | |
| 400 | +~/Desktop/cluster-skill/mld deploy uqo-chat # nœud par score (ou --node M4M64a) ; santé locale + publique ; registre | |
| 401 | +# raccourci équivalent : make deploy [NODE=M4M64a] | |
| 52 | 402 | ``` |
| 53 | 403 | |
| 54 | −Le manifeste de production (`M1M32:~/dispatch/apps/uqo-chat.json`) contient les secrets ; la copie | |
| 55 | −du dépôt (`deploy/uqo-chat.manifest.json`) n'a que des gabarits. Après un premier déploiement, | |
| 56 | −ingérer le matériel de cours sur le nœud : | |
| 404 | +`data/` (SQLite + fichiers) et `content/` (sites ingérés) sont **exclus de la synchronisation** : ils persistent sur | |
| 405 | +le nœud. Un `mld move uqo-chat --to <nœud>` déplace l'app mais il faut alors recopier `data/` et `content/` | |
| 406 | +manuellement (ou ré-ingérer). | |
| 407 | + | |
| 408 | +## 15. Exploitation | |
| 57 | 409 | |
| 58 | 410 | ```bash |
| 59 | −scp -r ~/Desktop/Academique/UQO/UQO_COURS/_Site_web/dist/imm10{03,33} <nœud>:~/apps/uqo-chat/content/ | |
| 60 | −ssh <nœud> 'cd ~/apps/uqo-chat/backend && for c in imm1003 imm1033; do \ | |
| 411 | +NODE=M4M64a # vérifier avec : ~/Desktop/cluster-skill/mld status | grep uqo-chat | |
| 412 | +ssh $NODE 'pm2 ls | grep uqo-chat; pm2 logs uqo-chat-api --lines 100 --nostream' | |
| 413 | +ssh $NODE 'pm2 restart uqo-chat-api' # après changement de manifeste / env | |
| 414 | +curl -s https://www.uqo-chat.app/api/v1/ready # {"ok":true,"chunks":714} | |
| 415 | + | |
| 416 | +# Ré-ingestion des notes de cours après un rebuild des sites (dépôts uqo-imm1003 / uqo-imm1033) | |
| 417 | +scp -r ~/Desktop/Academique/UQO/UQO_COURS/_Site_web/dist/imm10{03,33} $NODE:~/apps/uqo-chat/content/ | |
| 418 | +ssh $NODE 'cd ~/apps/uqo-chat/backend && for c in imm1003 imm1033; do \ | |
| 61 | 419 | DATA_DIR=~/apps/uqo-chat/data .venv/bin/python -m scripts.ingest --course $c --site ../content/$c; done' |
| 62 | −pm2 restart uqo-chat-api | |
| 420 | +ssh $NODE 'pm2 restart uqo-chat-api' | |
| 421 | + | |
| 422 | +# Sauvegarde des données | |
| 423 | +ssh $NODE 'sqlite3 ~/apps/uqo-chat/data/uqochat.db ".backup /tmp/uqochat-$(date +%F).db"' | |
| 63 | 424 | ``` |
| 64 | 425 | |
| 65 | −## Écarts assumés par rapport à la spécification | |
| 426 | +- Registre `mld` (`M1M32:~/dispatch/registry.json`) : nœud, IP LAN, port, santé, processus ; consommé par les apps | |
| 427 | + de monitoring MacLustr (macOS / iOS). | |
| 428 | +- Réglages modifiables à chaud en base (`app_settings`) et depuis l'onglet **Réglages** du professeur. | |
| 429 | +- Changer de modèle : `MODEL_*` dans le manifeste M1M32, `mld deploy uqo-chat` (ou `pm2 restart` avec env mis à jour). | |
| 430 | + Vérifier le slug sur https://openrouter.ai/models avant. | |
| 431 | + | |
| 432 | +## 16. Coûts LLM | |
| 66 | 433 | |
| 67 | −| Spécification | Réalisation actuelle | Pourquoi | | |
| 434 | +- Chaque appel est enregistré dans `llm_usage` (modèle, jetons, coût USD) ; `/admin` agrège par jour, modèle et | |
| 435 | + étudiant·e (haché). Budget `LLM_MONTHLY_BUDGET_USD` = 2 000 $ US ; dépassement → refus poli avec message. | |
| 436 | +- Ordres de grandeur mesurés : tour simple avec RAG ≈ 0,12 $ US (≈ 30 k jetons d'entrée avec outils et passages) ; | |
| 437 | + tour riche (4 outils, classeur + graphique) ≈ 0,80 $ US. | |
| 438 | + | |
| 439 | +## 17. Vie privée, sécurité, Loi 25 | |
| 440 | + | |
| 441 | +- Consentement explicite à la première connexion (`POST /me/consent`), page `/confidentialite`, export et | |
| 442 | + **suppression du compte** (`DELETE /me` : anonymisation des messages, purge des fichiers). | |
| 443 | +- Analytics du professeur **anonymisées** (identifiants hachés, `core/security.hash_user_id`), aucun contenu de | |
| 444 | + message ; logs sans contenu ni courriel. | |
| 445 | +- Rétention : fichiers 7 j (30 j épinglés), messages anonymisés après 12 mois (tâche horaire). | |
| 446 | +- Secrets uniquement dans le manifeste M1M32 et `.env` local (ignoré par git) ; `SecretStr` côté config. | |
| 447 | +- Code étudiant / LLM jamais exécuté dans l'API ; sandbox sans réseau ; CSP stricte ; JWT signés ; mots de passe PBKDF2 ; | |
| 448 | + liens à usage unique et expirants ; limitation de débit sur `/auth/forgot` et sur les messages / outils. | |
| 449 | +- Logo UQO : l'autorisation formelle du Service des communications de l'UQO reste à obtenir (`VITE_USE_OFFICIAL_LOGO`). | |
| 450 | + | |
| 451 | +## 18. Écarts assumés par rapport à la spécification | |
| 452 | + | |
| 453 | +| Spécification (CLAUDE.md d'origine) | Réalisation actuelle | Pourquoi | | |
| 68 | 454 | |---|---|---| |
| 69 | −| 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 | | |
| 70 | −| Embeddings `openai/text-embedding-3-large` via OpenRouter | BM25 (index en mémoire) + embeddings optionnels via un endpoint OpenAI-compatible (`EMBEDDINGS_BASE_URL`) | OpenRouter n'expose aucun modèle d'embeddings (vérifié 2026-09-05) | | |
| 71 | −| Lien magique SMTP à chaque connexion | Invitation Resend → mot de passe choisi par l'étudiant·e (PBKDF2), « mot de passe oublié » par courriel ; liste blanche gérée par le professeur | Une seule étape par courriel, puis connexion classique ; Resend (`RESEND_API_KEY`) plutôt qu'un SMTP UQO | | |
| 72 | −| Pods sandbox gVisor / Job k8s | Service `sandbox-runner` séparé, `sandbox-exec` macOS (réseau interdit, écriture confinée) + rlimits + timeout | Équivalent local ; `sandbox-runner/Dockerfile` + NetworkPolicy prêts pour k8s | | |
| 73 | −| Alembic | `create_all` au démarrage | Une migration initiale sera ajoutée au premier changement de schéma | | |
| 74 | −| Export PDF de conversation, mode hors-ligne complet | Non faits (PWA : app shell + cache lecture) | Phase 2/3 | | |
| 455 | +| 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 | | |
| 456 | +| 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) | | |
| 457 | +| 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 | | |
| 458 | +| Pods sandbox gVisor / Job k8s | Service `sandbox-runner` + `sandbox-exec` macOS + rlimits + timeout | Équivalent local ; Dockerfile + NetworkPolicy prêts pour k8s | | |
| 459 | +| Alembic | `create_all` + `_add_missing_columns` | Migration initiale à écrire au premier changement de schéma incompatible | | |
| 460 | +| Kubernetes + ngrok operator | `mld` + PM2 + agent ngrok sur un nœud Mac | Infrastructure réelle = cluster MacLustr | | |
| 461 | +| Export PDF de conversation, hors-ligne complet | Non faits (PWA : app shell + cache lecture) | Phase 2 / 3 | | |
| 462 | + | |
| 463 | +## 19. Feuille de route | |
| 464 | + | |
| 465 | +Export PDF d'une conversation ; file d'attente hors-ligne (Background Sync) ; évaluation pédagogique automatique en | |
| 466 | +CI (40 questions par cours, juge `MODEL_FAST`) ; tests adverses complets du sandbox ; Alembic ; migration éventuelle | |
| 467 | +Postgres + pgvector ; autorisation officielle du logo UQO ; alignement des numéros de version (`pyproject`, | |
| 468 | +`package.json`) sur v0.5. | |
| 469 | + | |
| 470 | +## 20. Historique des versions | |
| 471 | + | |
| 472 | +| Date | Commit | Version | Contenu | | |
| 473 | +|---|---|---|---| | |
| 474 | +| 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 | | |
| 475 | +| 2026-09-06 | `afb5ca4` | | `HEAD` accepté sur les routes SPA (vérifications d'uptime) | | |
| 476 | +| 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 | | |
| 477 | +| 2026-09-06 | `9e6193a` | | Grille de comparables : ajustements par formule (sujet + taux unitaires) ; correction du collage de courriels en lot | | |
| 478 | +| 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 | | |
| 479 | +| 2026-09-06 | `7a51d30` · `d896f91` | | `anthropic/claude-fable-5.1` modèle par défaut ; budget mensuel 2 000 $ US | | |
| 480 | +| 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 ») | | |
| 481 | +| 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) | | |
| 482 | +| 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) | | |
| 483 | +| 2026-09-06 | — | | README détaillé (ce document) | | |
| 484 | + | |
| 485 | +## 21. Pièges connus | |
| 486 | + | |
| 487 | +- `sandbox-exec` bloque bien le réseau et la lecture de `~`, mais `RLIMIT_AS` casse numpy sur macOS → désactivé sur Darwin. | |
| 488 | +- `shiki` complet précache 10 Mo dans la PWA → utiliser `shiki/core` avec les langages ciblés. | |
| 489 | +- Les montants « 185 000 $ » en prose cassent `remark-math` → `frontend/src/lib/math-guard.ts` (règle Pandoc). | |
| 490 | +- Les gros arguments d'outils étaient tronqués à 4 k jetons de sortie (« JSON invalide ») → 16 k + réparation JSON + | |
| 491 | + refus d'exécuter un appel tronqué. | |
| 492 | +- Le manifeste `deploy/` du dépôt n'a que des gabarits `{{…}}` : ne pas le copier tel quel sur M1M32 sans renseigner | |
| 493 | + les secrets. | |
| 494 | +- Après `mld move`, `data/` et `content/` ne suivent pas (exclus du sync) : les recopier ou ré-ingérer. | |
| 495 | +- PM2 sous zsh : `pm2` n'est pas dans le PATH des hooks → les hooks exportent `~/.local/bin` et `/opt/homebrew/bin`. | |
| 496 | + | |
| 497 | +## 22. Droits et licence | |
| 498 | + | |
| 499 | +© 2026 Simon-Pierre Boucher. Dépôt privé, tous droits réservés. Contenu pédagogique indexé : notes de cours | |
| 500 | +IMM1003 / IMM1033 © Simon-Pierre Boucher, UQO. Le nom et le logo UQO appartiennent à l'Université du Québec en | |
| 501 | +Outaouais. Modèles fournis par OpenRouter (Anthropic, OpenAI) ; recherche web par Firecrawl ; courriels par Resend. | |
| 502 | + | |
| 503 | +## 23. Contact | |
| 504 | + | |
| 505 | +**Simon-Pierre Boucher** | |
| 506 | +Courriel : **contact@spboucher.ai** | |
| 507 | +Git personnel : https://git.spboucher.ai (spbgit, dépôt `uqo-chat`) | |
| 508 | +Application : https://www.uqo-chat.app · notes de cours : https://www.uqo-imm1003.app · https://www.uqo-imm1033.app | |
| 75 | 509 | |