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%

docs: README détaillé (architecture, 13 outils, API, auth v0.5, RAG, sandbox, config, déploiement mld, exploitation, historique, contact)

Simon-Pierre Boucher committed 17 days ago (Sep 7, 2026) parent f7b72d8

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