#!/usr/bin/env python3 """ Index des connaissances Funk dans Qdrant (hermes_memory). Exécuter avec : /srv/data/hermes/hermes-agent/venv/bin/python index_knowledge.py """ import requests import uuid from qdrant_client import QdrantClient from qdrant_client.models import PointStruct, VectorParams, Distance EMBEDDINGS_URL = "http://192.168.10.20:1234/v1/embeddings" EMBEDDINGS_MODEL = "qwen3-8b" QDRANT_HOST = "localhost" QDRANT_PORT = 6333 COLLECTION = "hermes_memory" VECTOR_DIM = 4096 # --- Connaissances à indexer --- KNOWLEDGE = [ { "category": "architecture", "tags": ["homelab", "funk", "réseau", "machines"], "text": "Funk homelab: storage-01 (192.168.10.1 LAN cluster, 192.168.1.200 LAN domestique, AlmaLinux 9.7) est passerelle + bastion + données. gpu-01 (192.168.10.20, AlmaLinux 9.7) héberge llama-server avec RX 6700XT. Cluster Kubernetes Talos sur compute-01 (192.168.10.11), compute-02 (192.168.10.12), compute-03 (192.168.10.13). storage-01 et gpu-01 sont hors du cluster k8s.", }, { "category": "architecture", "tags": ["réseau", "kubernetes", "metallb", "dns"], "text": "Réseau Kubernetes: CNI Flannel pods 10.42.0.0/16, services 10.43.0.0/16, MetalLB pool 192.168.10.200-230. Domaine lab.local avec wildcard DNS via dnsmasq sur storage-01. Ingress: .lab.local vers IP MetalLB de Traefik.", }, { "category": "litellm", "tags": ["litellm", "proxy", "modèle", "switch"], "text": "LiteLLM tourne sur storage-01 port 4000. master_key doit valoir exactement lm-studio. Hermes utilise l'alias hermes-default. Pour switcher: sudo hermes-switch qwen (Qwen2.5-7B local gratuit) ou sudo hermes-switch claude (Claude Sonnet 4.6 ~1-2$ par session). Le script modifie /etc/litellm/config.yaml et redémarre le service.", }, { "category": "litellm", "tags": ["litellm", "config", "modèles", "anthropic"], "text": "LiteLLM config /etc/litellm/config.yaml expose: hermes-default (alias switchable), qwen2.5-7b-instruct (gpu-01:1234), claude-sonnet-4-6 (API Anthropic), claude-opus-4-7 (API Anthropic). ANTHROPIC_API_KEY dans l'environnement systemd via vault Ansible.", }, { "category": "hermes", "tags": ["hermes", "config", "LM_API_KEY", "provider"], "text": "LM_API_KEY doit valoir exactement lm-studio pour activer le provider LM Studio dans Hermes. Toute autre valeur échoue silencieusement. Le fichier /srv/data/hermes/.env peut écraser les variables systemd — Ansible gère les deux via lineinfile.", }, { "category": "hermes", "tags": ["hermes", "config", "config.yaml", "provider"], "text": "config.yaml Hermes doit contenir model.provider: lmstudio ET model.base_url: http://127.0.0.1:4000/v1. Sans provider, l'inférence échoue avec AuthError même si base_url est correct. model.default: hermes-default pointe sur l'alias LiteLLM.", }, { "category": "hermes", "tags": ["hermes", "cli", "path", "git"], "text": "Toujours lancer hermes -z depuis /srv/data/hermes pour éviter l'erreur PermissionError sur .git. Hermes remonte l'arborescence à la recherche d'un .git et échoue s'il tombe sur /home/ansible/.git sans permission. HERMES_HOME=/srv/data/hermes, données sur RAID5.", }, { "category": "hermes", "tags": ["hermes", "mémoire", "sessions", "qdrant"], "text": "Hermes a deux systèmes de mémoire: mémoire native dans /srv/data/hermes/memories/ (texte, automatique, recherche par mots-clés) et Qdrant (vectoriel, sémantique, skill mlops/qdrant installé). Le skill qdrant-vector-search est actif. qdrant-client v1.18 dans le venv /srv/data/hermes/hermes-agent/venv/.", }, { "category": "hermes", "tags": ["hermes", "coût", "tokens", "claude"], "text": "Coût Hermes avec Claude Sonnet 4.6: system prompt ~15000 tokens par échange, contexte s'accumule. Une session de debug ~375k tokens input = ~1.20$. Utiliser Qwen local (gratuit) pour les tâches courantes, Claude uniquement pour le debug complexe ponctuel.", }, { "category": "gpu", "tags": ["rocm", "amd", "gfx1031", "HSA", "llama-server"], "text": "gpu-01 RX 6700XT (gfx1031) non officiellement supportée par ROCm. Toujours définir HSA_OVERRIDE_GFX_VERSION=10.3.0 dans l'environnement llama-server et tout service ROCm. Sans cette variable ROCm échoue. Défini dans le service systemd llama-server via Ansible.", }, { "category": "gpu", "tags": ["llama-server", "embeddings", "pooling", "vecteurs"], "text": "llama-server doit être démarré avec --embeddings --pooling mean pour exposer /v1/embeddings compatible OpenAI. Sans --pooling mean: erreur 'Pooling type none is not OAI compatible'. Qwen2.5-7B produit des vecteurs de dimension 3584. Endpoint: http://192.168.10.20:1234/v1/embeddings.", }, { "category": "gpu", "tags": ["modèles", "qwen", "gemma", "performance"], "text": "Modèle actif gpu-01: Qwen2.5-14B-Instruct-Q4_K_M (~35 tok/s génération, ~200 tok/s prefill, ROCm natif). Contexte limité à 16384 tokens (VRAM: 8GB modèle + 3GB KV cache = 11GB sur 12GB). Alias API: qwen2.5-14b-instruct. Ne pas utiliser Gemma 4: crash ROCm >800 tokens. Dimension embeddings: 5120.", }, { "category": "postgresql", "tags": ["postgresql", "pgdg", "PGDATA", "RAID5"], "text": "PostgreSQL 16 installé depuis repo officiel pgdg sur storage-01. PGDATA sur RAID5: /srv/data/postgres. Override systemd dans /etc/systemd/system/postgresql-16.service.d/pgdata.conf. Auth: peer en local, scram-sha-256 pour connexions réseau. Bases: hermes et litellm avec utilisateurs dédiés.", }, { "category": "qdrant", "tags": ["qdrant", "api", "client", "python"], "text": "Qdrant v1.13.4 sur storage-01 port 6333 (HTTP) et 6334 (gRPC). Données: /srv/data/qdrant. qdrant-client v1.18 installé dans /srv/data/hermes/hermes-agent/venv/. Utiliser client.query_points() et non client.search() (supprimé en v1.14+). Collection hermes_memory: dim=3584, distance=COSINE.", }, { "category": "firewall", "tags": ["nftables", "firewall", "ports", "ansible"], "text": "storage-01 utilise nftables géré centralement par le rôle gateway Ansible (nftables.conf.j2). Ne jamais ajouter de tâches firewalld dans les autres rôles. Ports ouverts cluster (192.168.10.0/24): 5432 PostgreSQL, 6333-6334 Qdrant, 8080 Hermes gateway, 9000-9001 MinIO. SSH ouvert depuis LAN domestique et cluster.", }, { "category": "ansible", "tags": ["ansible", "vault", "secrets", "variables"], "text": "Secrets Ansible dans group_vars/all/vault.yml: vault_anthropic_api_key, vault_pg_hermes_password, vault_pg_litellm_password. Rôles idempotents: common, gateway, dnsmasq, nfs_server, postgresql, qdrant, minio, litellm, hermes_agent, llama_server, rocm. Playbooks: storage-01.yml et gpu-01.yml.", }, { "category": "ansible", "tags": ["ansible", "tags", "déploiement", "commandes"], "text": "Commandes Ansible: cd ansible/ && ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags postgresql. Pour gpu-01: ansible-playbook -i inventory.yml playbooks/gpu-01.yml --tags llama_server. Dry-run: ajouter --check. Collections requises: ansible.posix, community.general, community.postgresql.", }, ] def get_embedding(text: str) -> list[float]: response = requests.post( EMBEDDINGS_URL, json={"model": EMBEDDINGS_MODEL, "input": [text]}, timeout=30, ) response.raise_for_status() return response.json()["data"][0]["embedding"] def main(): client = QdrantClient(QDRANT_HOST, port=QDRANT_PORT) # Recréer la collection proprement existing = [c.name for c in client.get_collections().collections] if COLLECTION in existing: client.delete_collection(COLLECTION) print(f"Collection '{COLLECTION}' supprimée (recréation propre)") client.create_collection( collection_name=COLLECTION, vectors_config=VectorParams(size=VECTOR_DIM, distance=Distance.COSINE), ) print(f"Collection '{COLLECTION}' créée (dim={VECTOR_DIM}, cosine)") points = [] for i, item in enumerate(KNOWLEDGE): print(f"[{i+1}/{len(KNOWLEDGE)}] Embedding: {item['text'][:60]}...") embedding = get_embedding(item["text"]) points.append( PointStruct( id=i + 1, vector=embedding, payload={ "text": item["text"], "category": item["category"], "tags": item["tags"], "source": "funk-sessions-2026-05", }, ) ) client.upsert(collection_name=COLLECTION, points=points) print(f"\nOK — {len(points)} chunks indexés dans '{COLLECTION}'") # Test rapide print("\n--- Test recherche: 'erreur hermes modele provider' ---") query_vec = get_embedding("erreur hermes modele provider") results = client.query_points( collection_name=COLLECTION, query=query_vec, limit=3, ) for r in results.points: print(f" Score={r.score:.4f} [{r.payload['category']}] {r.payload['text'][:80]}...") if __name__ == "__main__": main()