depiedencap-rag/docs/rag/LOCAL_DEV.md
grgmro 39cffbc1bb depiedencap-rag — export public initial
Plugin Discourse RAG (grain post, rerank cross-encoder, citations
whitelistées, gate déterministe, réécriture de requête multi-tour)
+ documentation du chantier (design, éval, sécurité, pipeline).

Export sanitisé depuis le monorepo privé via scripts/export_rag_public.py
(aucun endpoint interne, IP, compte démo ou chemin d'infrastructure).
WIP : agent en test sur groupe restreint, pas encore public sur le forum.
2026-09-17 09:26:45 +02:00

195 lines
9.5 KiB
Markdown

# Forum local — recette de redéploiement (réplique du VPS)
> **But** : un Discourse local = copie exacte de `depiedencap.org`
> (données + config IA + vecteurs), pour développer/tester le RAG sans
> toucher la prod. IA : embeddings en **local** (bge-m3 CPU), chat LLM sur
> le **mini PC** via Tailscale (config déjà dans le dump).
> Marche sur laptop **et** PC fixe (Tailscale requis dans les deux cas).
## 0. Prérequis machine
| Besoin | Minimum | Vérifier |
|---|---|---|
| Docker Engine | installé + daemon | `docker info` |
| RAM dispo | ~6 Go | Discourse ~3-4 Go + Ollama ~2 Go |
| Disque | **~60 Go libres** | tar 10 Go + uploads ~19 Go + DB ~5 Go + image ~6 Go + marge |
| Tailscale | connecté | `tailscale status` → mini PC `<MINI_PC>` |
| Ollama (local, embeddings dev) | CPU suffit | `ollama --version` |
| Ce repo | cloné (`scripts/` du chantier) | `git status` propre |
Install (Ubuntu/Tuxedo — sudo, par l'utilisateur) :
```bash
# ⚠️ Tuxedo OS : get.docker.com détecte mal la distrib (écrit un dépôt
# Debian trixie → paquets cassés). Utiliser les dépôts Ubuntu :
sudo apt install -y docker.io
sudo usermod -aG docker "$USER" # re-login ensuite (ou `newgrp docker`)
docker info
# Ollama (bge-m3 local ; RTX 2060 détectée auto si driver NVIDIA présent,
# sinon CPU — suffisant dans les deux cas)
curl -fsSL https://ollama.com/install.sh | sh
ollama pull bge-m3
# Ollama doit écouter au-delà de 127.0.0.1 (le conteneur passe par
# host.docker.internal → gateway docker0) :
sudo mkdir -p /etc/systemd/system/ollama.service.d # ← dossier à créer d'abord
printf '[Service]\nEnvironment="OLLAMA_HOST=0.0.0.0"\n' | sudo tee /etc/systemd/system/ollama.service.d/override.conf
sudo systemctl daemon-reload && sudo systemctl restart ollama
curl -s http://localhost:11434/api/tags # doit lister bge-m3
```
## 1. Récupérer le dernier backup prod
```bash
# Liste des archives sur le NAS (chaîne hebdo, rétention glissante)
ssh <nas> 'ls -lht <NAS_BACKUP_DIR>/ | head'
# Copier le plus récent (~10 Go, patience). Référence 07/09/2026 :
# depiedencap-2026-09-07-010010-v20260817054353.tar.gz
scp <nas>:<NAS_BACKUP_DIR>/depiedencap-AAAA-MM-JJ-*.tar.gz ~/dpec-restore/
```
> La version Discourse exacte est dans le `meta.json` du tar :
> `tar -xzf depiedencap-*.tar.gz -O meta.json | head` → si le restore se
> plaint de version, pinner `version:` dans `app.yml` en conséquence.
## 2. Installer discourse_docker en local
```bash
sudo git clone https://github.com/discourse/discourse_docker.git /var/discourse
cd /var/discourse
sudo cp <clone>/rag/local/app.yml containers/app.yml # template prêt, sans secrets
```
`rag/local/app.yml` — diff vs prod : hostname `localhost`, **pas de SMTP**
(aucun `DISCOURSE_SMTP_*`), port 80 seul, `--add-host
host.docker.internal:host-gateway` (pour joindre l'Ollama local depuis le
conteneur), hook `after_code` qui recopie les 3 plugins depuis `/shared`.
```bash
sudo ./launcher bootstrap app
sudo ./launcher start app
```
## 3. Restaurer le dump
```bash
sudo cp ~/dpec-restore/depiedencap-*.tar.gz /var/discourse/shared/standalone/backups/default/
sudo chown 1000:www-data /var/discourse/shared/standalone/backups/default/*.tar.gz
docker exec app bash -lc 'cd /var/www/discourse && su discourse -c "bundle exec script/discourse enable_restore"'
docker exec app bash -lc 'cd /var/www/discourse && su discourse -c "bundle exec script/discourse restore depiedencap-AAAA-MM-JJ-....tar.gz"'
```
Le restore fait **déjà** deux choses tout seul : `Remapping
'https://depiedencap.org' to 'http://localhost'` (cooked réécrits) et
« Disabling outgoing emails for non-staff users ». Reste à durcir :
```bash
# ⚠️ E-mails : passer à "yes" (couvre aussi staff/digests). En pratique
# aucun mail ne peut partir : le SMTP prod était en env vars, absent ici.
docker exec -u postgres app psql discourse -c \
"UPDATE site_settings SET value='yes' WHERE name='disable_emails';"
```
Puis les 3 plugins custom (le .tar.gz ne contient pas le code). Le hook
`after_code` de `app.yml` les attend dans `/shared/depiedencap-plugins/` :
```bash
sudo mkdir -p /var/discourse/shared/standalone/depiedencap-plugins
cd <clone>/scripts
sudo cp -r discourse_plugin/depiedencap-ai-citations \
discourse_plugin/depiedencap-onboarding \
plugins/discourse-depiedencap-locale \
/var/discourse/shared/standalone/depiedencap-plugins/
cd /var/discourse && sudo ./launcher rebuild app # ~10-15 min
```
> Thèmes/picker/PWA : dans le dump. `discourse-ai` est **inclus dans
> l'image** officielle — avec `ai_posts_embeddings` (grain post) et son
> index HNSW+binary-quantization natifs : le chantier post-centric peut
> s'appuyer dessus au lieu d'une table custom.
## 4. Recâbler l'IA
La config IA est **dans le dump** : `llm_models` → `http://<MINI_PC>:32134`
(mini PC, joignable via Tailscale — laissé tel quel pour le chat),
`embedding_definitions` id=1 → **à repointer** vers l'Ollama local
(ci-dessous), `allowed_internal_hosts` → ajouter le gateway docker0.
| Composant | Local dev | Comment |
|---|---|---|
| Chat LLM | mini PC `:32134` | inchangé — marche dès le restore |
| Embeddings | **bge-m3 local** `host.docker.internal:11434` | repointer `EmbeddingDefinition` (ci-dessous) |
| pgvector / vecteurs | locale (restaurés) | backfill posts → ici, jamais en prod |
Repointer les embeddings vers l'Ollama local — `embedding_definitions`
est une **table** (id=1 = bge-m3 prod) — SQL direct ou rails runner :
```bash
docker exec -u postgres app psql discourse -c \
"UPDATE embedding_definitions SET url='http://host.docker.internal:11434/v1/embeddings' WHERE id=1;"
docker exec -u postgres app psql discourse -c \
"UPDATE site_settings SET value='<MINI_PC>|172.17.0.1|host.docker.internal' WHERE name='allowed_internal_hosts';"
# ⚠️ allowed_internal_hosts matche le NOM D'HÔTE (pas l'IP résolue) —
# sans « host.docker.internal », les appels embeddings échouent avec
# « FinalDestination: all resolved IPs were disallowed » (canal sem vide).
# Vérif depuis l'intérieur du conteneur :
docker exec app curl -s -m 5 -X POST http://host.docker.internal:11434/v1/embeddings \
-H 'Content-Type: application/json' -d '{"model":"bge-m3","input":"test"}' | head -c 120
# → doit renvoyer du JSON {"object":"list","data":[{"embedding":[...
# (ou garder le mini PC :32134 — marche aussi, juste partagé avec la prod)
```
## 5. Vérifs post-restore
```bash
curl -s http://localhost | head -3 # forum up
docker exec app rails runner 'puts Post.count' # ~425k attendus
docker exec app rails runner 'puts AiAgent.find_by(name: "Guide Depiedencap").present?'
docker exec app rails runner 'puts ActiveRecord::Base.connection.execute("SELECT count(*) FROM ai_topics_embeddings").first' # 22 340
curl -s http://<MINI_PC>:32134/api/tags | head # mini PC joignable
curl -s http://localhost:11434/api/tags | head # bge-m3 local
```
Login local : ton compte admin de prod fonctionne tel quel (même hash en
base) — **mot de passe**, pas de lien magique (emails coupés).
Checklist « ne pas casser la prod par procuration » :
- [ ] `disable_emails = yes` (aucun mail réel ne part — digests, D9…)
- [ ] Pas de domaine public pointant sur l'instance locale (localhost only)
- [ ] Backups locaux : `enable_backups` peut être laissé — écrira en local
- [ ] Le compte `demo` existe dans le dump — comportement identique, OK
- [ ] **Redémarrer sidekiq après un restore** : `docker exec app kill -TERM $(docker exec app pgrep -f 'sidekiq.*discourse')`
(respawn auto via `unicorn_launcher`, ~40 s). Sinon le process garde
`@readonly_mode = true` en mémoire → tous les `Jobs::Scheduled` no-opent
silencieusement (`success` dans `scheduler_stats`, ~100 ms, rien fait) :
pas de rebakes, pas de backfill embeddings, pas de cleanups. Symptôme
observé : `ai_posts_embeddings` figé malgré job actif toutes les 5 min.
## 6. Différences assumées vs prod
| Écart | Impact |
|---|---|
| Pas de Cloudflare/TLS | Tester les liens en HTTP `localhost` |
| Uploads `optimized/` régénérés à la demande | Premières images lentes, normal |
| SMTP coupé | Onboarding email non testable en local (le MP d'accueil, si) |
| UX bot (suggestions, overlay cold-start, endpoint `/depiedencap-ai/llm-warmup`) | Restreint au compte **`demo`** (`shouldRender` + 403 API) — se connecter en `demo` pour les voir. Identique à la prod, pas un bug. |
| bge-m3 CPU local | Vecteurs identiques (même modèle), juste plus lents |
| `demo-<token>.html` statique | Fichier public, présent dans uploads — marche |
## 7. Dépannage rapide
| Symptôme | Piste |
|---|---|
| Restore échoue « version » | Pinner `version:` app.yml = version du dump |
| `Permission denied` sur le tar | `chown 1000:www-data` (§3) |
| Bot muet / erreur TCP | Mini PC éteint ? `curl :32134/api/tags` ; plugin → message humoristique attendu |
| Embeddings vides | `OLLAMA_HOST=0.0.0.0` + URL `host.docker.internal` (§4) |
| `//localhost/` cassés dans les posts | remap §3 pas (encore) joué |
| Jobs planifiés « success » mais rien ne se passe | readonly résiduel côté sidekiq → restart sidekiq (§5) |
| Bandeaux « thème comporte des erreurs » après restore | bundles JS au vieux format → `Theme.find_each(&:update_javascript_cache!)` + vider `js_asset_info` du cache |
| Job/backfill embeddings figé | verifier `scheduler_stats` (`duration_ms` ~100 ms = no-op) ; verrou `cluster_concurrency:Jobs::EmbeddingsBackfill` (redis **sans** namespace, ttl 120 s) |