# UQO-Chat **Tuteur IA pour les cours IMM1003 — Éléments d'évaluation immobilière et IMM1033 — Méthodes du coût (UQO).** Production : https://www.uqo-chat.app · Spécification complète : [`CLAUDE.md`](CLAUDE.md). Le tuteur explique la matière en citant les notes de cours officielles (RAG), exécute du Python dans un bac à sable isolé, produit des classeurs Excel à formules vivantes, cherche des données de marché actuelles (Firecrawl), analyse les fichiers déposés et génère des quiz interactifs. Tous les modèles (Claude, GPT) passent par OpenRouter avec repli automatique. ## Architecture (déploiement MacLustr) ``` Internet ── ngrok (www.uqo-chat.app) ──► uqo-chat-api :8190 FastAPI + SSE + SPA React (PM2) │ ├──► uqo-chat-sandbox :8191 (PM2, sandbox-exec : sans réseau) ├──► SQLite data/uqochat.db (index BM25 en mémoire) ├──► fichiers data/files/ (TTL 7 j / 30 j épinglés) └──► OpenRouter · Firecrawl (HTTPS sortant) ``` Les manifestes Kubernetes (`k8s/`) et `docker-compose.yml` reproduisent la cible décrite dans la spécification (Postgres + pgvector, Redis, MinIO, sandbox durci) pour un futur cluster ; la production actuelle tourne sur un nœud Mac du cluster via `mld` (voir `deploy/`). ## Démarrage local ```bash cp .env.example .env # OPENROUTER_API_KEY, FIRECRAWL_API_KEY, RESEND_API_KEY… make setup # venvs uv (backend, sandbox) + npm install make sandbox & # :8191 make api & # :8190 (sert frontend/dist s'il existe) make web # :5173 avec proxy /api make ingest COURSE=imm1033 SITE=~/Desktop/Academique/UQO/UQO_COURS/_Site_web/dist/imm1033 make test && make lint ``` Connexion : le professeur inscrit les courriels des étudiant·es (onglet **Étudiants**) ; chacun·e reçoit un courriel Resend « Bienvenue sur UQO-Chat » avec un lien `/mot-de-passe?token=…` (valide `INVITE_TTL_DAYS`) pour choisir son mot de passe, puis se connecte avec courriel + mot de passe. « Première connexion ou mot de passe oublié » renvoie un lien (`RESET_TTL_MINUTES`). Seules les adresses inscrites (ou listées dans `PROFESSOR_EMAILS` / `ADMIN_EMAILS` / `INVITED_EMAILS`) peuvent demander un lien. Sans `RESEND_API_KEY` (ni SMTP), les liens sont affichés dans le tableau de bord pour être copiés ; en dev, `/auth/forgot` renvoie `dev_link`. ## Déploiement ```bash make build # frontend/dist ~/Desktop/cluster-skill/mld stage $PWD uqo-chat # laptop → passerelle ~/Desktop/cluster-skill/mld deploy uqo-chat # nœud choisi par score (--node X pour forcer) ``` Le manifeste de production (`M1M32:~/dispatch/apps/uqo-chat.json`) contient les secrets ; la copie du dépôt (`deploy/uqo-chat.manifest.json`) n'a que des gabarits. Après un premier déploiement, ingérer le matériel de cours sur le nœud : ```bash scp -r ~/Desktop/Academique/UQO/UQO_COURS/_Site_web/dist/imm10{03,33} :~/apps/uqo-chat/content/ ssh 'cd ~/apps/uqo-chat/backend && for c in imm1003 imm1033; do \ DATA_DIR=~/apps/uqo-chat/data .venv/bin/python -m scripts.ingest --course $c --site ../content/$c; done' pm2 restart uqo-chat-api ``` ## Écarts assumés par rapport à la spécification | Spécification | Réalisation actuelle | Pourquoi | |---|---|---| | PostgreSQL + pgvector, Redis, MinIO, arq | SQLite (SQLAlchemy async), cache/limiteur en mémoire, fichiers sur disque, tâches asyncio | Déploiement mono-nœud sans dépendances ; `DATABASE_URL` accepte déjà Postgres, `k8s/` décrit la cible complète | | Embeddings `openai/text-embedding-3-large` via OpenRouter | BM25 (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) | | 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 | | 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 | | Alembic | `create_all` au démarrage | Une migration initiale sera ajoutée au premier changement de schéma | | Export PDF de conversation, mode hors-ligne complet | Non faits (PWA : app shell + cache lecture) | Phase 2/3 |