mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 05:24:43 +02:00
feat(rag): RAG Hermes — Qdrant + rag-ingest/rag-query + skill funk-ai
Indexation de admin/ (284 chunks) dans Qdrant via embeddings Qwen3-8B. rag-query utilisable en CLI et depuis le profil funk-ai de Hermes. Note: modèle d'embedding générique — qualité limitée, voir admin/ia/rag.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3a013834aa
commit
eae6e76645
9 changed files with 687 additions and 0 deletions
194
admin/ia/rag.md
Normal file
194
admin/ia/rag.md
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
# RAG — Documentation Funk
|
||||
|
||||
Le RAG (Retrieval-Augmented Generation) permet à Hermes de répondre en s'appuyant
|
||||
sur la documentation du repo (`admin/`) plutôt que sur sa seule mémoire de modèle.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Hermes (funk-ai)
|
||||
│ terminal: rag-query "comment relancer dnsmasq ?"
|
||||
▼
|
||||
/usr/local/bin/rag-query (storage-01)
|
||||
│ 1. Embed la question → POST http://192.168.10.20:1234/v1/embeddings
|
||||
│ 2. Recherche vectorielle → POST http://127.0.0.1:6333/collections/funk-docs/points/search
|
||||
▼
|
||||
Qdrant (storage-01:6333)
|
||||
│ Retourne les N chunks les plus proches sémantiquement
|
||||
▼
|
||||
rag-query affiche les extraits + fichier source + score
|
||||
│
|
||||
▼
|
||||
Hermes formule sa réponse en citant les sources
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Composants déployés
|
||||
|
||||
| Composant | Emplacement | Rôle |
|
||||
|---|---|---|
|
||||
| Qdrant | storage-01:6333/6334 | Base vectorielle — stocke les embeddings |
|
||||
| `rag-ingest` | `/usr/local/bin/rag-ingest` | Indexe les docs dans Qdrant |
|
||||
| `rag-query` | `/usr/local/bin/rag-query` | Interroge Qdrant depuis la ligne de commande |
|
||||
| Skill `rag-docs` | Profil `funk-ai` Hermes | Enseigne à Hermes comment utiliser `rag-query` |
|
||||
| Docs indexées | `/srv/data/rag/docs/` (RAID5) | Copie locale du dossier `admin/` |
|
||||
| Collection Qdrant | `funk-docs` | 284 chunks — dimension 4096 (Cosine) |
|
||||
|
||||
---
|
||||
|
||||
## Modèle d'embedding utilisé
|
||||
|
||||
**Qwen3-8B** via llama-server GPU (port 1234), avec les flags `--embeddings --pooling mean`.
|
||||
|
||||
C'est le modèle de chat principal, réutilisé pour les embeddings.
|
||||
|
||||
### Limitation connue
|
||||
|
||||
Qwen3-8B est un modèle génératif, **pas un modèle d'embedding dédié**.
|
||||
Ses représentations vectorielles ont peu de discrimination sémantique :
|
||||
les scores de similarité cosinus sont uniformément hauts (0.90–0.95)
|
||||
quelle que soit la pertinence du résultat.
|
||||
|
||||
Conséquence : les résultats retournés ne sont pas triés par pertinence réelle —
|
||||
le fichier avec le plus de tokens (ex : `incidents.md`) remonte souvent en premier.
|
||||
|
||||
### Quand améliorer
|
||||
|
||||
Pour une recherche sémantique précise, utiliser un modèle d'embedding dédié :
|
||||
|
||||
| Modèle | Taille | Dimension | Notes |
|
||||
|---|---|---|---|
|
||||
| `nomic-embed-text-v1.5` | ~274 MB | 768 | Rapide, bon équilibre qualité/taille |
|
||||
| `bge-m3` | ~1.2 GB | 1024 | Multilingue (français natif) — meilleur choix pour ce repo |
|
||||
|
||||
Déploiement si besoin :
|
||||
```bash
|
||||
# Sur gpu-01 — télécharger le modèle
|
||||
ssh gpu-01 "lms get nomic-ai/nomic-embed-text-v1.5-GGUF"
|
||||
|
||||
# Ajouter une instance llama-server sur port 1238 (CPU, embedding only)
|
||||
# Modifier ansible/roles/rag/defaults/main.yml :
|
||||
# embed_url: "http://192.168.10.20:1238/v1/embeddings"
|
||||
# embed_model: "nomic-embed-text-v1.5"
|
||||
# Puis re-indexer : rag-ingest /srv/data/rag/docs/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Via Hermes (funk-ai)
|
||||
|
||||
Le skill `rag-docs` est chargé dans le profil `funk-ai`. Hermes l'utilise
|
||||
automatiquement pour les questions sur le cluster :
|
||||
|
||||
```
|
||||
# Dans le TUI Hermes :
|
||||
"Comment je relance dnsmasq ?"
|
||||
→ Hermes appelle rag-query en interne et cite les sources
|
||||
```
|
||||
|
||||
### En ligne de commande
|
||||
|
||||
```bash
|
||||
# Depuis storage-01
|
||||
rag-query "ma question"
|
||||
rag-query "ma question" --top 3 # limiter les résultats (défaut: 5)
|
||||
|
||||
# Exemples
|
||||
rag-query "comment relancer llama-server gpu"
|
||||
rag-query "nftables firewall port cluster"
|
||||
rag-query "hermes profils configuration"
|
||||
rag-query "alertmanager webhook pipeline"
|
||||
rag-query "monitoring grafana dashboard"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mise à jour de l'index
|
||||
|
||||
L'index doit être re-généré après chaque modification de la doc `admin/`.
|
||||
|
||||
### Via Ansible (recommandé)
|
||||
|
||||
```bash
|
||||
cd ansible/
|
||||
../.venv/bin/ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags rag
|
||||
```
|
||||
|
||||
Ansible synchronise `admin/` vers `/srv/data/rag/docs/` puis relance automatiquement
|
||||
`rag-ingest` (handler Ansible).
|
||||
|
||||
### Manuellement
|
||||
|
||||
```bash
|
||||
# Synchroniser les docs (depuis le poste perso)
|
||||
rsync -av --delete admin/ storage-01:/srv/data/rag/docs/
|
||||
|
||||
# Ré-indexer (depuis storage-01)
|
||||
ssh storage-01 "rag-ingest /srv/data/rag/docs/"
|
||||
```
|
||||
|
||||
L'ingestion prend ~5-10 minutes pour 284 chunks (embeddings via GPU).
|
||||
Elle est idempotente — re-lancer ne crée pas de doublons (IDs stables basés sur MD5 du fichier+section).
|
||||
|
||||
---
|
||||
|
||||
## Administration Qdrant
|
||||
|
||||
```bash
|
||||
# Collections existantes
|
||||
curl -s http://storage-01:6333/collections | python3 -m json.tool
|
||||
|
||||
# Stats de la collection funk-docs
|
||||
curl -s http://storage-01:6333/collections/funk-docs | python3 -m json.tool
|
||||
|
||||
# Nombre de points indexés
|
||||
curl -s http://storage-01:6333/collections/funk-docs \
|
||||
| python3 -c "import sys,json; print(json.load(sys.stdin)['result']['points_count'])"
|
||||
|
||||
# Vider et re-créer la collection (si re-indexation propre nécessaire)
|
||||
curl -s -X DELETE http://storage-01:6333/collections/funk-docs
|
||||
rag-ingest /srv/data/rag/docs/
|
||||
|
||||
# Statut du service
|
||||
ssh storage-01 "systemctl status qdrant --no-pager"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Structure des fichiers
|
||||
|
||||
```
|
||||
ansible/roles/rag/
|
||||
├── defaults/main.yml # URLs Qdrant/embed, collection, répertoires
|
||||
├── tasks/main.yml # Déploiement scripts + docs + skill Hermes
|
||||
├── handlers/main.yml # Handler : Run rag-ingest + Restart hermes-agent
|
||||
└── files/
|
||||
├── rag-ingest # Script Python d'ingestion
|
||||
├── rag-query # Script Python de requête
|
||||
└── rag-docs/SKILL.md # Skill Hermes
|
||||
|
||||
hermes-skills/funk/rag-docs/
|
||||
└── SKILL.md # Source versionnée du skill (copié dans files/)
|
||||
|
||||
/srv/data/rag/ (RAID5 storage-01)
|
||||
├── docs/ # Copie de admin/ — source des chunks
|
||||
└── (Qdrant stocke dans /srv/data/qdrant/)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Points d'attention
|
||||
|
||||
| Sujet | Détail |
|
||||
|---|---|
|
||||
| Qualité embeddings | Qwen3-8B = modèle chat réutilisé — scores peu discriminants. Fonctionnel mais pas optimal. |
|
||||
| Modèle dédié | `nomic-embed-text` ou `bge-m3` diviseraient les faux positifs par ~3 |
|
||||
| Contention GPU | `rag-ingest` sollicite le GPU (port 1234) pendant ~5-10 min — éviter pendant une session d'inférence active |
|
||||
| Re-indexation | Obligatoire après modification de `admin/` — pas de sync automatique |
|
||||
| Qdrant persistance | Données dans `/srv/data/qdrant/` sur RAID5 — survivent aux redémarrages |
|
||||
| Score minimum | `MIN_SCORE = 0.60` dans `rag-query` — tous les résultats sont au-dessus avec ce modèle |
|
||||
|
|
@ -12,5 +12,6 @@
|
|||
- { role: minio, tags: [minio] }
|
||||
- { role: litellm, tags: [litellm] }
|
||||
- { role: hermes_agent, tags: [hermes_agent] }
|
||||
- { role: rag, tags: [rag] }
|
||||
- { role: node_exporter, tags: [node_exporter] }
|
||||
- { role: alertmanager_webhook, tags: [alertmanager_webhook] }
|
||||
|
|
|
|||
8
ansible/roles/rag/defaults/main.yml
Normal file
8
ansible/roles/rag/defaults/main.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
rag_data_dir: /srv/data/rag
|
||||
rag_docs_dir: /srv/data/rag/docs
|
||||
|
||||
qdrant_url: "http://127.0.0.1:6333"
|
||||
embed_url: "http://192.168.10.20:1234/v1/embeddings"
|
||||
embed_model: "qwen3-8b"
|
||||
rag_collection: "funk-docs"
|
||||
97
ansible/roles/rag/files/rag-docs/SKILL.md
Normal file
97
ansible/roles/rag/files/rag-docs/SKILL.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
---
|
||||
name: rag-docs
|
||||
description: "Interroge la documentation Funk via RAG pour répondre aux questions sur le cluster, les services, les configs et les procédures admin."
|
||||
version: 1.0.0
|
||||
author: Funk Lab
|
||||
license: MIT
|
||||
platforms: [linux]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [rag, documentation, funk, cluster, recherche]
|
||||
---
|
||||
|
||||
# Documentation RAG — Cluster Funk
|
||||
|
||||
## Quand utiliser ce skill
|
||||
|
||||
Pour toute question sur :
|
||||
- Procédures admin (comment faire X, commandes utiles)
|
||||
- Configuration d'un service (dnsmasq, litellm, hermes, nftables, llama-server...)
|
||||
- Architecture du cluster Funk
|
||||
- Dépannage d'un composant (diagnostics, causes connues)
|
||||
- Alertes et monitoring
|
||||
|
||||
**Ne pas utiliser** pour l'état en temps réel (logs live, métriques actuelles) — utiliser Terminal directement.
|
||||
|
||||
---
|
||||
|
||||
## Commande
|
||||
|
||||
```bash
|
||||
rag-query "ta question"
|
||||
```
|
||||
|
||||
Interroge la base vectorielle Qdrant avec la question, retourne les passages
|
||||
de documentation les plus proches sémantiquement (score ≥ 0.60).
|
||||
|
||||
Options :
|
||||
```bash
|
||||
rag-query "question" --top 3 # limiter à 3 résultats (défaut: 5)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern obligatoire
|
||||
|
||||
**Étape 1** — Interroger la base RAG :
|
||||
```
|
||||
terminal: rag-query "comment relancer dnsmasq après un reboot ?"
|
||||
```
|
||||
|
||||
**Étape 2** — Utiliser le contexte retourné pour formuler la réponse, en citant la source :
|
||||
```
|
||||
D'après la documentation Funk (admin/infra/dnsmasq.md § Dépannage) :
|
||||
sudo systemctl restart dnsmasq
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exemples
|
||||
|
||||
```bash
|
||||
# Procédure de redémarrage d'un service
|
||||
terminal: rag-query "comment redémarrer llama-server gpu ?"
|
||||
|
||||
# Dépannage
|
||||
terminal: rag-query "dnsmasq failed après reboot causes"
|
||||
|
||||
# Configuration
|
||||
terminal: rag-query "nftables règles firewall cluster pod cidr"
|
||||
|
||||
# Architecture
|
||||
terminal: rag-query "hermes profils litellm modèles disponibles"
|
||||
|
||||
# Alertes
|
||||
terminal: rag-query "GPUTemperatureCritical seuil alertmanager"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interpréter les résultats
|
||||
|
||||
Chaque résultat indique :
|
||||
- **source** : `admin/ia/hermes.md § Configuration`
|
||||
- **score** : pertinence sémantique (0.60–1.00) — un score > 0.75 est très pertinent
|
||||
- **texte** : extrait du document
|
||||
|
||||
Si le score est < 0.65 ou les résultats hors sujet : répondre depuis ta connaissance générale
|
||||
et le mentionner (`"Je n'ai pas trouvé de documentation spécifique sur ce point"`).
|
||||
|
||||
---
|
||||
|
||||
## Règles
|
||||
|
||||
- Toujours citer la source (`admin/ia/hermes.md § section`)
|
||||
- Un seul appel `rag-query` suffit pour une question — ne pas enchaîner plusieurs requêtes
|
||||
- Ne jamais inventer des commandes non trouvées dans les résultats RAG
|
||||
156
ansible/roles/rag/files/rag-ingest
Normal file
156
ansible/roles/rag/files/rag-ingest
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Indexe les fichiers Markdown de la doc Funk dans Qdrant.
|
||||
Usage: rag-ingest [docs_dir]
|
||||
docs_dir : répertoire contenant les .md (défaut: /srv/data/rag/docs)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import hashlib
|
||||
import re
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
QDRANT_URL = os.environ.get("QDRANT_URL", "http://127.0.0.1:6333")
|
||||
EMBED_URL = os.environ.get("EMBED_URL", "http://192.168.10.20:1234/v1/embeddings")
|
||||
EMBED_MODEL = os.environ.get("EMBED_MODEL", "qwen3-8b")
|
||||
COLLECTION = os.environ.get("RAG_COLLECTION", "funk-docs")
|
||||
VECTOR_DIM = 4096
|
||||
CHUNK_MAX = 2000
|
||||
|
||||
|
||||
def _request(method, url, data=None):
|
||||
body = json.dumps(data).encode() if data is not None else None
|
||||
req = urllib.request.Request(
|
||||
url, data=body,
|
||||
headers={"Content-Type": "application/json"} if body else {},
|
||||
method=method
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def ensure_collection():
|
||||
try:
|
||||
_request("GET", f"{QDRANT_URL}/collections/{COLLECTION}")
|
||||
print(f"Collection '{COLLECTION}' existante")
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
print(f"Création de la collection '{COLLECTION}'...")
|
||||
_request("PUT", f"{QDRANT_URL}/collections/{COLLECTION}", {
|
||||
"vectors": {"size": VECTOR_DIM, "distance": "Cosine"}
|
||||
})
|
||||
print("Collection créée")
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def embed(text):
|
||||
result = _request("POST", EMBED_URL, {"model": EMBED_MODEL, "input": text})
|
||||
return result["data"][0]["embedding"]
|
||||
|
||||
|
||||
def chunk_markdown(rel_path, content):
|
||||
"""Découpe un fichier Markdown par sections H2, puis H3 si trop grand."""
|
||||
chunks = []
|
||||
|
||||
# Séparer par H2
|
||||
parts = re.split(r'\n(?=## )', content)
|
||||
|
||||
# Intro avant le premier H2
|
||||
if parts and not parts[0].strip().startswith('## '):
|
||||
intro = parts[0].strip()
|
||||
if len(intro) > 80:
|
||||
title = intro.split('\n')[0].lstrip('#').strip() or rel_path
|
||||
chunks.append({"file": rel_path, "section": title, "text": intro[:CHUNK_MAX]})
|
||||
parts = parts[1:]
|
||||
|
||||
for part in parts:
|
||||
if not part.strip():
|
||||
continue
|
||||
h2_title = part.split('\n')[0].lstrip('#').strip()
|
||||
|
||||
if len(part) <= CHUNK_MAX:
|
||||
chunks.append({"file": rel_path, "section": h2_title, "text": part.strip()})
|
||||
else:
|
||||
# Découper par H3
|
||||
subs = re.split(r'\n(?=### )', part)
|
||||
for sub in subs:
|
||||
if not sub.strip():
|
||||
continue
|
||||
h3_title = sub.split('\n')[0].lstrip('#').strip()
|
||||
label = f"{h2_title} — {h3_title}" if h3_title != h2_title else h2_title
|
||||
chunks.append({"file": rel_path, "section": label, "text": sub.strip()[:CHUNK_MAX]})
|
||||
|
||||
return chunks
|
||||
|
||||
|
||||
def point_id(file_path, section):
|
||||
"""ID stable uint64 = MD5(file::section)."""
|
||||
h = hashlib.md5(f"{file_path}::{section}".encode()).hexdigest()
|
||||
return int(h[:16], 16)
|
||||
|
||||
|
||||
def ingest(docs_dir):
|
||||
ensure_collection()
|
||||
|
||||
md_files = []
|
||||
for root, dirs, files in os.walk(docs_dir):
|
||||
dirs[:] = sorted(d for d in dirs if not d.startswith('.'))
|
||||
for f in sorted(files):
|
||||
if f.endswith('.md'):
|
||||
md_files.append(os.path.join(root, f))
|
||||
|
||||
print(f"{len(md_files)} fichiers Markdown trouvés dans {docs_dir}")
|
||||
|
||||
total = 0
|
||||
errors = 0
|
||||
|
||||
for filepath in md_files:
|
||||
rel = os.path.relpath(filepath, docs_dir)
|
||||
try:
|
||||
with open(filepath, encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except OSError as e:
|
||||
print(f" SKIP {rel}: {e}")
|
||||
continue
|
||||
|
||||
chunks = chunk_markdown(rel, content)
|
||||
if not chunks:
|
||||
continue
|
||||
|
||||
points = []
|
||||
for chunk in chunks:
|
||||
print(f" embed {rel} § {chunk['section'][:55]}")
|
||||
try:
|
||||
vector = embed(chunk['text'])
|
||||
except Exception as e:
|
||||
print(f" ERREUR embedding: {e}")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
points.append({
|
||||
"id": point_id(chunk['file'], chunk['section']),
|
||||
"vector": vector,
|
||||
"payload": {
|
||||
"file": chunk['file'],
|
||||
"section": chunk['section'],
|
||||
"text": chunk['text'],
|
||||
}
|
||||
})
|
||||
|
||||
if points:
|
||||
_request("PUT", f"{QDRANT_URL}/collections/{COLLECTION}/points?wait=true", {"points": points})
|
||||
total += len(points)
|
||||
print(f" → {len(points)} chunks indexés ({rel})")
|
||||
|
||||
print(f"\nTerminé : {total} chunks dans '{COLLECTION}'" + (f" ({errors} erreurs)" if errors else ""))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
docs_dir = sys.argv[1] if len(sys.argv) > 1 else "/srv/data/rag/docs"
|
||||
if not os.path.isdir(docs_dir):
|
||||
print(f"ERREUR : {docs_dir} n'existe pas", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
ingest(docs_dir)
|
||||
76
ansible/roles/rag/files/rag-query
Normal file
76
ansible/roles/rag/files/rag-query
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Interroge la base vectorielle Qdrant avec une question en langage naturel.
|
||||
Usage: rag-query "ma question" [--top N]
|
||||
Retourne les passages de documentation les plus pertinents.
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
QDRANT_URL = os.environ.get("QDRANT_URL", "http://127.0.0.1:6333")
|
||||
EMBED_URL = os.environ.get("EMBED_URL", "http://192.168.10.20:1234/v1/embeddings")
|
||||
EMBED_MODEL = os.environ.get("EMBED_MODEL", "qwen3-8b")
|
||||
COLLECTION = os.environ.get("RAG_COLLECTION", "funk-docs")
|
||||
MIN_SCORE = 0.60
|
||||
|
||||
|
||||
def _post(url, data):
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps(data).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def embed(text):
|
||||
result = _post(EMBED_URL, {"model": EMBED_MODEL, "input": text})
|
||||
return result["data"][0]["embedding"]
|
||||
|
||||
|
||||
def search(question, top=5):
|
||||
vector = embed(question)
|
||||
result = _post(f"{QDRANT_URL}/collections/{COLLECTION}/points/search", {
|
||||
"vector": vector,
|
||||
"limit": top,
|
||||
"with_payload": True,
|
||||
"score_threshold": MIN_SCORE,
|
||||
})
|
||||
return result["result"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: rag-query \"question\" [--top N]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
question = sys.argv[1]
|
||||
top = 5
|
||||
if "--top" in sys.argv:
|
||||
idx = sys.argv.index("--top")
|
||||
try:
|
||||
top = int(sys.argv[idx + 1])
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
|
||||
try:
|
||||
results = search(question, top)
|
||||
except Exception as e:
|
||||
print(f"ERREUR: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not results:
|
||||
print(f"Aucun résultat pertinent pour : {question}")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"=== Contexte RAG — {len(results)} résultat(s) pour : {question} ===\n")
|
||||
for i, r in enumerate(results, 1):
|
||||
p = r["payload"]
|
||||
score = r["score"]
|
||||
print(f"--- [{i}] {p['file']} § {p['section']} (score: {score:.3f}) ---")
|
||||
print(p["text"][:700])
|
||||
print()
|
||||
12
ansible/roles/rag/handlers/main.yml
Normal file
12
ansible/roles/rag/handlers/main.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
- name: Run rag-ingest
|
||||
ansible.builtin.command:
|
||||
cmd: /usr/local/bin/rag-ingest {{ rag_docs_dir }}
|
||||
changed_when: true
|
||||
async: 900
|
||||
poll: 15
|
||||
|
||||
- name: Restart hermes-agent
|
||||
ansible.builtin.systemd:
|
||||
name: hermes-agent
|
||||
state: restarted
|
||||
46
ansible/roles/rag/tasks/main.yml
Normal file
46
ansible/roles/rag/tasks/main.yml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
- name: Create RAG directories on RAID5
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
loop:
|
||||
- "{{ rag_data_dir }}"
|
||||
- "{{ rag_docs_dir }}"
|
||||
|
||||
- name: Deploy rag-ingest script
|
||||
ansible.builtin.copy:
|
||||
src: rag-ingest
|
||||
dest: /usr/local/bin/rag-ingest
|
||||
mode: '0755'
|
||||
notify: Run rag-ingest
|
||||
|
||||
- name: Deploy rag-query script
|
||||
ansible.builtin.copy:
|
||||
src: rag-query
|
||||
dest: /usr/local/bin/rag-query
|
||||
mode: '0755'
|
||||
|
||||
- name: Sync admin docs to RAG docs dir
|
||||
ansible.builtin.copy:
|
||||
src: "{{ inventory_dir }}/../admin/"
|
||||
dest: "{{ rag_docs_dir }}/"
|
||||
mode: '0644'
|
||||
notify: Run rag-ingest
|
||||
|
||||
- name: Deploy RAG skill to Hermes global skills
|
||||
ansible.builtin.copy:
|
||||
src: rag-docs/
|
||||
dest: /srv/data/hermes/skills/funk/rag-docs/
|
||||
owner: hermes
|
||||
group: hermes
|
||||
mode: '0644'
|
||||
|
||||
- name: Deploy RAG skill to funk-ai profile
|
||||
ansible.builtin.copy:
|
||||
src: rag-docs/
|
||||
dest: /srv/data/hermes/profiles/funk-ai/skills/funk/rag-docs/
|
||||
owner: hermes
|
||||
group: hermes
|
||||
mode: '0644'
|
||||
notify: Restart hermes-agent
|
||||
97
hermes-skills/funk/rag-docs/SKILL.md
Normal file
97
hermes-skills/funk/rag-docs/SKILL.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
---
|
||||
name: rag-docs
|
||||
description: "Interroge la documentation Funk via RAG pour répondre aux questions sur le cluster, les services, les configs et les procédures admin."
|
||||
version: 1.0.0
|
||||
author: Funk Lab
|
||||
license: MIT
|
||||
platforms: [linux]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [rag, documentation, funk, cluster, recherche]
|
||||
---
|
||||
|
||||
# Documentation RAG — Cluster Funk
|
||||
|
||||
## Quand utiliser ce skill
|
||||
|
||||
Pour toute question sur :
|
||||
- Procédures admin (comment faire X, commandes utiles)
|
||||
- Configuration d'un service (dnsmasq, litellm, hermes, nftables, llama-server...)
|
||||
- Architecture du cluster Funk
|
||||
- Dépannage d'un composant (diagnostics, causes connues)
|
||||
- Alertes et monitoring
|
||||
|
||||
**Ne pas utiliser** pour l'état en temps réel (logs live, métriques actuelles) — utiliser Terminal directement.
|
||||
|
||||
---
|
||||
|
||||
## Commande
|
||||
|
||||
```bash
|
||||
rag-query "ta question"
|
||||
```
|
||||
|
||||
Interroge la base vectorielle Qdrant avec la question, retourne les passages
|
||||
de documentation les plus proches sémantiquement (score ≥ 0.60).
|
||||
|
||||
Options :
|
||||
```bash
|
||||
rag-query "question" --top 3 # limiter à 3 résultats (défaut: 5)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern obligatoire
|
||||
|
||||
**Étape 1** — Interroger la base RAG :
|
||||
```
|
||||
terminal: rag-query "comment relancer dnsmasq après un reboot ?"
|
||||
```
|
||||
|
||||
**Étape 2** — Utiliser le contexte retourné pour formuler la réponse, en citant la source :
|
||||
```
|
||||
D'après la documentation Funk (admin/infra/dnsmasq.md § Dépannage) :
|
||||
sudo systemctl restart dnsmasq
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exemples
|
||||
|
||||
```bash
|
||||
# Procédure de redémarrage d'un service
|
||||
terminal: rag-query "comment redémarrer llama-server gpu ?"
|
||||
|
||||
# Dépannage
|
||||
terminal: rag-query "dnsmasq failed après reboot causes"
|
||||
|
||||
# Configuration
|
||||
terminal: rag-query "nftables règles firewall cluster pod cidr"
|
||||
|
||||
# Architecture
|
||||
terminal: rag-query "hermes profils litellm modèles disponibles"
|
||||
|
||||
# Alertes
|
||||
terminal: rag-query "GPUTemperatureCritical seuil alertmanager"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interpréter les résultats
|
||||
|
||||
Chaque résultat indique :
|
||||
- **source** : `admin/ia/hermes.md § Configuration`
|
||||
- **score** : pertinence sémantique (0.60–1.00) — un score > 0.75 est très pertinent
|
||||
- **texte** : extrait du document
|
||||
|
||||
Si le score est < 0.65 ou les résultats hors sujet : répondre depuis ta connaissance générale
|
||||
et le mentionner (`"Je n'ai pas trouvé de documentation spécifique sur ce point"`).
|
||||
|
||||
---
|
||||
|
||||
## Règles
|
||||
|
||||
- Toujours citer la source (`admin/ia/hermes.md § section`)
|
||||
- Un seul appel `rag-query` suffit pour une question — ne pas enchaîner plusieurs requêtes
|
||||
- Ne jamais inventer des commandes non trouvées dans les résultats RAG
|
||||
Loading…
Add table
Add a link
Reference in a new issue