# 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/` - **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 : ``. 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 : /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-, 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.