DEPLOIEMENT-V1.md
Rama main · réplica de lectura del repositorio original.
# Vektor V1 — Journal de déploiement complet (step by step)
Assistant personnel auto-hébergé « Jarvis » : omnicanal (Telegram + Alexa),
conscient de l'infrastructure (RAG + outils live en lecture seule), cerveau
LLM local (Ollama sur VPS).
- **Bot Telegram** : `https://t.me/<nom-du-bot>`
- **Endpoint Alexa** : `https://vektor.example.ch/api/alexa`
- **Hébergement** : VPS (`/opt/vektor`), 4 conteneurs Docker
- **Sécurité** : lecture seule, whitelist stricte, aucun port public inutile
---
## 1. Architecture finale
```
Telegram (polling) ─┐
├─→ FastAPI (/api/chat, /api/alexa, /api/status)
Alexa (EU, HTTPS) ──┘ │
├─→ LangGraph → Ollama (qwen2.5:14b, keep_alive=-1)
├─→ RAG pgvector (doc infra, 7 blocs)
├─→ Outils live LECTURE SEULE :
│ • API PVE (token vektor-ro@pve!vektor, PVEAuditor)
│ • SSH à commande forcée (script vektor-status unique)
│ • Checks HTTP (jellyfin, sonarr, radarr, prowlarr, qbit…)
└─→ PostgreSQL (mémoire conversationnelle multi-canal)
Réseau : API sur le réseau Docker de Caddy (accès interne par nom,
aucun port publié) ; Telegram appelle l'API par le nom de service Docker
(`http://vektor-api:8000`, JAMAIS 127.0.0.1 = loopback du conteneur bot) ;
Ollama ponté par socat host-network lié uniquement à la passerelle Docker.
```
### Conteneurs
| Conteneur | Rôle | Exposition |
|---|---|---|
| `vektor-postgres` | PostgreSQL 16 + pgvector, mémo + RAG | 127.0.0.1:55432 |
| `vektor-api` | FastAPI + LangGraph + outils | réseau Docker interne + 127.0.0.1:8000 |
| `vektor-telegram` | Bot polling (python-telegram-bot) | aucun (sortant uniquement) |
| `vektor-ollama-bridge` | socat host → passerelle Docker pour Ollama | passerelle Docker uniquement |
### Fichiers
```
vektor/
├── app/
│ ├── main.py # FastAPI : /health, /api/chat, /api/alexa, /api/status, /api/forget
│ ├── graph.py # run_agent : routage → LLM (Ollama, keep_alive) + prompts FR
│ ├── tools.py # outils live : PVE, Docker, checks HTTP, /status complet
│ ├── alexa.py # vérif signature Amazon + SSML + réponses progressives
│ ├── telegram.py # bot : 11 commandes + chat libre, whitelist, messages longs,
│ │ # auto-test des URLs au démarrage (log + alerte Telegram)
│ ├── memory.py # PostgreSQL : conversations, messages, RAG
│ ├── rag.py # indexation/recherche pgvector
│ └── config.py # Settings pydantic (env)
├── knowledge/homelab.md # doc infra NON SECRÈTE (topologie, rôles, IPs LAN)
├── scripts/ # benchmark modèles, tests keep_alive, netdiag
├── compose.yml
├── Dockerfile
├── .env.example # modèle SANS valeurs réelles
└── DEPLOIEMENT-V1.md # ce fichier
```
---
## 2. Étapes du déploiement (ordre réel)
### Étape 1 — Inventaire préalable
Vérifié sur le VPS : 12 vCPU, 23 Go RAM, ~40 Go libres, Ollama déjà présent
(modèles `qwen2.5:14b`, `llama3.1:8b`), Caddy existant en Docker.
### Étape 2 — Stack de base
`compose.yml` : PostgreSQL pgvector + API FastAPI. Premier échec : le build
Docker ne résolvait pas PyPI (DNS du réseau de build) → corrigé avec un
réseau de build utilisant le DNS de l'hôte.
### Étape 3 — Accès à Ollama depuis les conteneurs
L'IP NetBird d'Ollama est injoignable depuis un réseau Docker bridgé
(drop silencieux, diagnostiqué par `netdiag.py`). Solution : pont socat en
`network_mode: host`, lié **uniquement** à l'IP passerelle Docker
(172.20.0.1) + règle UFW scopée au subnet Docker. Ollama reste inatteignable
du WAN et du LAN.
### Étape 4 — API et sécurité
- `/api/chat` protégé par `X-Vektor-Token` (401 sans token).
- Chaîne testée de bout en bout : RAG, réponse LLM réelle, routage live.
### Étape 5 — Telegram
1. Constat : le token fourni au départ était celui du bot d'admin existant
(`homelab_bot`) → conflit `getUpdates` (un token = un polling).
2. Création d'un **bot dédié** via @BotFather : `<nom-du-bot>`.
3. Piège de saisie : token recopié depuis un screenshot avec un caractère
ambigu (`V` lu `v`) → `401 Unauthorized`. Toujours coller le token en
texte, jamais depuis une image.
4. Code : whitelist d'IDs stricte (silence total pour les inconnus),
`/start`, `/help`, `/status`, `/forget`, indicateur « écrit… »,
découpage des messages > 4096 caractères.
### Étape 6 — Outils infrastructure (lecture seule)
1. **API PVE** : token `vektor-ro@pve!vektor`, rôle `PVEAuditor`.
Piège : avec privilege separation (`privsep 1` par défaut), `/lxc`
renvoie vide → `pveum acl modify ... --privsep 0` (le token hérite du
rôle de son utilisateur, même pattern que le pve-exporter existant).
2. **SSH à commande forcée** : clé ed25519 dédiée sur le PVE avec
`command="/usr/local/bin/vektor-status"`, no-pty, no-forwarding.
Le script statique renvoie CTs + inventaire Docker + `memory.peak`.
Preuve : toute commande arbitraire est remplacée par le script.
3. **Checks HTTP** : jellyfin, sonarr, radarr, prowlarr, qbittorrent, garmin.
4. Droits : `chown 10001` sur les secrets montés (conteneur non-root).
### Étape 7 — Choix du modèle (benchmark réel)
| Modèle | Verdict |
|---|---|
| `llama3.1:8b` | rapides mais réponses faibles en FR, refus aberrants |
| `qwen2.5:7b` | rapide MAIS hallucination grave (swap financier ≠ mémoire) |
| `qwen2.5:14b` | ✅ retenu : raisonnement sysadmin, bon français |
`keep_alive=-1` (résident en RAM) : finis les 74 s de chargement à froid.
⚠️ Un seul modèle résident à la fois (~9,2 Go) — deux simultanés = VPS à
880 Mo dispo. Déchargement d'urgence :
`curl http://127.0.0.1:11434/api/generate -d '{"model":"...","keep_alive":0}'`
### Étape 8 — Routage intelligent
- Intention d'**état** → outils live (0,1 s), réponse **non déformée** par le LLM.
- Intention d'**explication** (« pourquoi », « explique ») → LLM.
- Alexa n'envoie que le **slot nu** (« proxmox » sans « état ») → règle
`bare_subject` : sujet infra court (≤ 5 mots) = demande d'état.
### Étape 9 — Alexa, partie serveur
1. `alexa.py` : vérification complète des requêtes Amazon
(signature RSA-SHA1 du corps, certificat validé : HTTPS, port 443,
host `s3.amazonaws.com` ou `*.amazonaws.com`, SAN `echo-api.amazon.com`
**ou** `echo-api.amazonaws.com`, dates de validité, cache 6 h),
anti-rejeu 150 s, `applicationId` dans allowlist.
2. `main.py` : LaunchRequest, Help, Stop/Cancel, Fallback, VektorQueryIntent,
SessionEnded ; mémoire partagée Telegram ↔ Alexa via `sessionAttributes`.
3. Réponses **progressives** (`VoicePlayer.Speak` vers l'apiEndpoint
régional EU de la requête) : le Echo parle pendant que le LLM calcule
(Alexa coupe à ~8 s sinon). Boucle de phrases toutes les 6 s.
4. Exposition : DNS `vektor.example.ch` **grey cloud** (obligatoire :
Amazon refuse le proxy orange de Cloudflare) → Caddy → réseau Docker
interne. Certificat Let's Encrypt automatique.
### Étape 10 — Alexa, côté console Amazon (manuel)
1. Créer la skill : **Custom** · **Français (FR)** · **Provision your own**
(⚠️ PAS « Alexa-Hosted » : le template Lambda d'Amazon répondrait à sa
place et n'appellerait jamais notre serveur — première grosse erreur
corrigée en recréant `vektor2`).
2. JSON Editor : coller l'interaction model (invocation `vektor`,
`VektorQueryIntent` avec `AMAZON.SearchQuery` **toujours avec carrier
phrase** — un slot seul est refusé au build) + `AMAZON.FallbackIntent`.
3. Build → vert.
4. Endpoint : HTTPS `https://vektor.example.ch/api/alexa`,
« certificate from a trusted certificate authority ».
5. Onglet **Test** → On (sinon invisible sur les appareils).
6. Distribution : corriger le nom d'invocation affiché (le template
pré-remplit « le génie des salutations »…), description, puis
désactiver/réactiver la skill dans l'app pour resynchroniser.
7. Skill ID → `.env` VPS (`ALEXA_SKILL_ID=...`).
### Étape 11 — La chaîne de déverrouillage (le vrai feuilleton)
Chaque erreur générique « Un problème est survenu » cachait UNE couche :
| # | Symptôme | Cause réelle | Correctif |
|---|---|---|---|
| 1 | Le template répond « Intent: … Slots: … » | skill Alexa-Hosted (Lambda démo) | recréer en « Provision your own » |
| 2 | 400 signature | certificats EU : host régional `*.amazonaws.com`, chemin non standard, SAN `echo-api.amazon.com` | allowlist alignée sur le vérificateur ask-sdk |
| 3 | 400 « requête périmée » | **horloge VPS en retard de 3 min 22 s, NTP cassé** | `chrony` + `ch.pool.ntp.org` (offset final 0,00004 s) |
| 4 | « pas fourni de réponse valide » | slot nu → routage LLM lent → timeout 8 s | `bare_subject` → live instantané + progressives |
| 5 | 400 « applicationId inconnu » | l'ANCIENNE skill pointait aussi sur l'endpoint | allowlist multi-IDs, puis retour à l'ID unique |
**Leçon majeure** : `docker compose restart` ne relit NI le `.env` NI le
code → toujours `docker compose up -d --build` après modification.
Le log de diagnostic (motif exact + valeurs reçues) a remplacé toutes les
devinettes : `logger.warning("Requete Alexa rejetee: %s", exc)`.
### Étape 12 — Voix naturelle
`_voice_friendly()` (canal vocal uniquement) : listes Python supprimées,
CPU→« processeur », RAM→« mémoire », Uptime→« en marche depuis »,
`G`→« giga octets sur », `%`→« pour cent », décimales à la française
(11,2), lignes transformées en phrases. Telegram garde le format brut.
---
## 3. État final de la V1 (testé et prouvé)
| Capacité | État |
|---|---|
| Telegram `/start` `/help` `/status` `/forget` + chat libre | ✅ live |
| « Etat de Proxmox » → CPU/RAM/swap/uptime live | ✅ 0,1 s |
| Inventaire Docker des 6 CTs (SSH forcé) | ✅ |
| Checks services (jellyfin, *arr, qbit…) | ✅ |
| Questions générales → qwen2.5:14b | ✅ ~15 s (CPU) |
| Alexa simulateur : launch + question → réponse vocale | ✅ |
| Mémoire partagée Telegram ↔ Alexa | ✅ |
| Refus sans signature / mauvais ID / requête périmée | ✅ testés |
| Secrets | `.env` chmod 600 hors Git, aucun secret dans le RAG |
### Limites connues (V1)
- Echo **physique** : skill activée et fiche correcte, mais l'appareil ne
déclenche pas encore la skill (« je ne sais pas comment vous aider »).
Pistes restantes : resynchro complète (disable/enable + reboot), compte
Echo = compte dev, marketplace du compte dev = FR. Le simulateur, lui,
fonctionne de bout en bout — le serveur n'est pas en cause.
- Latence LLM ~15 s (CPU) : leviers futurs = petit GPU local avec Ollama
(aucun changement d'architecture) ou streaming Telegram.
- Réponses Alexa tronquées à 600 caractères (limite vocale assumée).
## 4. Exploitation
```bash
# état
ssh vps 'docker ps --filter name=vektor'
ssh vps 'docker logs vektor-api --tail 20'
# redémarrer proprement après modif code/env (JAMAIS simple restart)
ssh vps 'cd /opt/vektor && docker compose up -d --build'
# décharger le modèle si RAM VPS critique
ssh vps 'curl -s http://127.0.0.1:11434/api/generate -d "{\"model\":\"qwen2.5:14b\",\"keep_alive\":0}"'
```
### Checklist post-déploiement (depuis v1.3.6 — à exécuter À CHAQUE mise en prod)
`/health` en 200 ne prouve RIEN d'autre que l'API tourne : le crash du bot
Telegram est invisible dedans, et une commande muette (bot → API rompu)
aussi. Cette checklist est née du bug v1.3.6 : les commandes étendues
répondaient côté API (sondes vertes sur les endpoints) alors que la
commande Telegram était muette depuis leur création — personne ne les
avait testées en vrai.
```bash
# 1. Build + recreate — un simple restart ne relit NI le code NI l'env
ssh vps 'cd /opt/vektor && docker compose --profile telegram up -d --build --force-recreate api telegram'
# 2. compose.yml intact : le tar de déploiement ne doit JAMAIS le contenir
# (comparer au MD5 de référence, sinon les volumes/env compose sont perdus)
ssh vps 'md5sum /opt/vektor/compose.yml /root/compose-vektor-REF.yml'
# 3. Volumes (knowledge-live surtout) + état des 4 conteneurs
ssh vps 'docker inspect vektor-api --format "{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}"'
# attendu : <chemin-prod>/knowledge-live -> /app/knowledge
ssh vps 'docker ps --filter name=vektor' # vektor-telegram Up — son crash ne se voit PAS dans /health
# 4. AUTO-TEST du bot (v1.3.7) : URLs réellement utilisées + token vérifié,
# avec retries pour couvrir la course de démarrage api/telegram
ssh vps 'docker logs vektor-telegram | grep AUTO-TEST'
# attendu : « 10/10 commandes -> API OK (token vérifié via sonde DNS) »
# sinon : log ERROR détaillé + message 🚨 envoyé sur Telegram
# 5. URLs effectives du bot — anti-régression v1.3.6
ssh vps 'docker exec vektor-telegram python -c "import app.telegram as t; print(t.DNS_URL, t.SEEDS_URL, t.BACKUPS_URL, t.MODEL_URL, t.MONITORING_URL, t.RELOAD_URL)"'
# RÈGLE : aucune URL en 127.0.0.1/localhost — c'est le loopback du
# CONTENEUR telegram, où aucune API n'écoute (= commande muette).
# Défauts du code et env compose doivent viser le service `vektor-api`.
# 6. Test RÉEL des commandes, depuis le conteneur telegram (comme le bot)
ssh vps 'docker exec vektor-telegram python -c "
import os, httpx
H = {\"X-Vektor-Token\": os.environ[\"VEKTOR_API_TOKEN\"]}
for ep in (\"dns\", \"status\", \"seeds\", \"backups\", \"model\", \"monitoring\"):
r = httpx.get(f\"http://vektor-api:8000/api/{ep}\", headers=H, timeout=90)
print(ep, r.status_code, r.json().get(\"report\", \"\").splitlines()[0][:60])
"'
# attendu : 200 partout avec une première ligne de rapport cohérente
# 7. reload-doc + chat LLM avec tool-calling (la 2e inférence + l'outil)
ssh vps 'docker exec vektor-telegram python -c "
import os, httpx
H = {\"X-Vektor-Token\": os.environ[\"VEKTOR_API_TOKEN\"]}
print(httpx.post(\"http://vektor-api:8000/api/reload-doc\", headers=H, timeout=60).status_code)
r = httpx.post(\"http://vektor-api:8000/api/chat\", headers=H, timeout=300,
json={\"user_id\": \"test\", \"channel\": \"web\", \"text\": \"sonde les résolveurs DNS\"})
print(r.status_code, r.json()[\"response\"][:120])
"'
# 8. Canal Telegram : polling seul, pas de webhook, 0 update en attente
ssh vps 'docker exec vektor-telegram python -c "
import os, httpx
t = os.environ[\"TELEGRAM_BOT_TOKEN\"]
print(httpx.get(f\"https://api.telegram.org/bot{t}/getMe\", timeout=10).json()[\"ok\"])
print(httpx.get(f\"https://api.telegram.org/bot{t}/getWebhookInfo\", timeout=10).json()[\"result\"])
"'
ssh vps 'docker logs vektor-telegram 2>&1 | grep -cE "Traceback|ERROR|Conflict"' # attendu : 0
# 9. Env critique (toute modif : sed ciblé + backup .env.bak-<date>, jamais
# réécrire le .env entier — c'est comme ça qu'une variable a disparu)
ssh vps 'grep -E "VEKTOR_DNS_RESOLVERS|OLLAMA_NETBIRD_HOST|VEKTOR_HC|VEKTOR_SERVICES" /opt/vektor/.env'
# 10. Healthchecks : 0 check down. Un check down = job mort OU ping HC perdu
# (le job peut réussir sans pinger : vérifier le script côté PVE)
# → /api/monitoring doit afficher tous les checks up
```
Avant tout push : suite pytest locale verte (le contrat « défauts d'URL =
service API » est verrouillé par `tests/test_telegram_urls.py`, l'auto-test
par `tests/test_telegram_autotest.py`).
### Leçons v1.3.6 → v1.3.8
| Symptôme | Cause réelle | Correctif durable |
|---|---|---|
| Commandes muettes (/dns, /seeds…) sans aucune erreur | défauts d'URL `127.0.0.1` = loopback du conteneur bot | défauts = service Docker `vektor-api` + auto-test au démarrage (log + Telegram) |
| `/seeds` : « authentification impossible » | creds qBit mortes après rotation du mot de passe | scripts en appel direct (bypass whitelist LAN), plus de creds stockées |
| Check Healthchecks down alors que le job réussit | ping HC perdu dans une réécriture du script | le ping HC fait partie du contrat du script, pas une option |
| IPs du rapport /dns prises pour celles des résolveurs | la sonde montre la réponse POUR le domaine sondé (example.com = Cloudflare) | ligne = résolveur (son adresse) : sonde → réponse |
Sauvegardes : `/opt/vektor` (compose + .env) et le volume `postgres-data`
sont couverts par les backups VPS existants ; la mémoire conversationnelle
vit dans PostgreSQL (restaurable).
## 5. Feuille de route V2
1. Echo physique (résolution compte/marketplace).
2. Outils d'écriture **avec confirmation obligatoire** (restart d'un service…).
3. Streaming des réponses Telegram (perçu 3× plus rapide).
4. Adaptateur web (canal déjà prévu : `channel: "web"`).
5. GPU local pour le LLM via NetBird.