9.2 KiB
Vault Obsidian + retrieval GrasBot (v3 — graph + BM25)
Créé : 2026-04-22 (v1 RAG vectoriel) Refondu : 2026-04-22 (v3 — graph + BM25, sans embeddings) Statut : opérationnel (17 projets + 4 compétences + CV + 3 notes techniques + 15 MOCs)
Raison d'être
Avant ce pipeline, GrasBot interrogeait mistral:7b sans aucun contexte —
il répondait de manière générique sur n'importe quoi. Depuis :
- Modèle chat :
qwen3:8b(meilleur en FR, reasoning solide). - Base de connaissance structurée comme vault Obsidian.
- Pipeline de retrieval branché : chaque question récupère les notes pertinentes avant génération.
Pourquoi graph + BM25 plutôt que RAG vectoriel ?
La première version (avril 2026, v2) utilisait ChromaDB + embeddings nomic-embed-text. Ça marchait, mais :
- Vault de taille modeste (~40 notes, ~100 Ko) : la sémantique vectorielle sur-dimensionne le problème.
- Retrieval imprévisible sur vocabulaire précis (une question « compétences
en IA » pouvait ne pas remonter la note
ia.mdsi son embedding était dominé par d'autres concepts). - Chaîne d'installation lourde :
chromadbdépend dechroma-hnswlib, qui nécessite un compilateur C++ sous Windows → blocage fréquent. - Coût en VRAM :
nomic-embed-textmobilisait ~500 Mo et ~1 s par requête, inutile à cette échelle. - Désynchronisation vault / index : étape
index_vault.pyoubliable.
En v3, on exploite directement la structure du vault : frontmatter YAML
(aliases, answers, domains, tags, priority), wikilinks, MOCs. Le retrieval
est déterministe, traçable (on sait pourquoi une note est remontée),
instantané (~50 ms), et ne demande qu'une dépendance : pyyaml.
Résultat : GrasBot cite toujours ses sources, et le top-5 est beaucoup plus prévisible pour une question précise.
Vault — vault-grasbot/
Arborescence :
vault-grasbot/
├── 00-MOC/ # hubs thématiques (MOC-Projets, MOC-Ia, MOC-Technique, ...)
├── 10-Projets/ # 17 projets Strapi (push-swap, minishell, ft-transcendence, ...)
├── 20-Competences/ # 4 compétences Strapi (IA, domotique, web, 3D)
├── 30-Parcours/ # CV curaté manuellement (source: manual)
├── 40-Glossaire/ # (vide, prévu pour le content-type glossaire Strapi)
├── 50-Technique/ # auto-doc : architecture-site, grasbot-retrieval, vault-structure
├── README.md # résumé utilisateur (généré)
└── TAXONOMIE.md # vocabulaire contrôlé (domaines, tags, aliases, answers, priority)
Frontmatter YAML
Chaque note porte une en-tête enrichie :
---
title: "push_swap"
slug: push-swap
type: projet # projet | competence | parcours | moc | technique
source: strapi/projects # strapi/... | pdf/... | manual | vault/generated
domains: [algorithmique, c, ecole-42]
tags: [42-commun, tri, makefile]
aliases:
- push swap
- push_swap
- algo de tri 42
answers:
- "Parle-moi de push-swap"
- "Comment fonctionne push-swap ?"
priority: 5 # 1..10, boost léger au scoring
linked: ["[[MOC-Projets]]"]
related: ["[[minishell]]"]
updated: 2026-04-22
visibility: public
---
Détail des champs et leur usage exact par le retrieval : voir
vault-grasbot/TAXONOMIE.md et la note interne
vault-structure du vault.
Règle de régénération
strapi_extraction/build-vault.py écrase les notes dont source: strapi/* ou source: pdf/*. Il ne touche jamais aux notes
source: manual.
Le drapeau --clean supprime tout le vault avant régénération : à utiliser
uniquement si on veut repartir de zéro (attention aux notes manual).
Génération — strapi_extraction/build-vault.py
Pipeline :
- Lit les
project-*.mdetcompetence-*.mddestrapi_extraction/docs/(eux-mêmes produits pargenerate-docs.jsà partir de l'API Strapi). - Parse titre, slug, description, détails.
- Infère
domains/tagsviaDOMAIN_KEYWORDS/TAG_KEYWORDS(ajustables dans le script). - Génère automatiquement :
aliasesà partir du slug + titre +DOMAIN_ALIASES(synonymes courants par domaine).answersselon le type (projet → « Parle-moi de X », compétence → « Quelles sont ses compétences en X ? », etc.).priorityheuristique (CV=10, MOCs=7, compétences=7, projets=5).
- Calcule les
relatedpar intersection de domaines (top 3). - Écrit chaque note avec frontmatter + corps + section « Liens » en pied.
- Génère les MOCs (un par type + un par domaine significatif).
- Optionnel : convertit le CV PDF via
pypdfsi installé (mais la version manuellecv-grascalvet-fernand.mdavecsource: manualest toujours préservée).
Commandes :
python strapi_extraction/build-vault.py # régénère tout
python strapi_extraction/build-vault.py --dry-run # liste sans écrire
python strapi_extraction/build-vault.py --clean # supprime puis regénère
Retrieval — llm-api/search.py
Module lu par api.py. Fournit :
load_vault()— lecture mémoïsée du vault (frontmatter YAML + body + wikilinks). Filtrevisibility: private.tokenize_fr(text)— tokenisation FR + normalisations (c++→cpp, split sur-/_, stop-words).score_note(note, query, tokens, stats)— score déterministe multi-signaux. Retourne unScoredNote(score, reasons[]).expand_by_graph(seeds, vault)— ajoute les voisins (linked,related, wikilinks du body) avec un score dérivé de 60 %.search(query, top_k)— orchestration : score + expansion + dedupe + top-K.build_prompt(query, notes)— couple(system, user)pour/api/chat.generate(system, user)— appel Ollama/api/chat, retourne le texte.answer(query)— pipeline complet, retourne un dict{response, sources, grounded, model, vault_size}.
Barème de scoring (documentation opérationnelle)
| Signal | Points | Détails |
|---|---|---|
| Alias match | +10 | 1+ aliases de la note apparaissent dans la question |
| Title exact | +8 | Titre complet dans la query (len ≥ 4) |
| Title tokens | +4 | Au moins 2 tokens du titre dans la query |
| Slug | +8 | Tous les tokens du slug sont dans la query |
| Answers full | +12 | ≥ 3 tokens communs avec une question-type |
| Answers partial | +5 | 2 tokens communs |
| Domains | +5 × n | Par domaine strictement matché |
| Tags | +3 × n | Par tag strictement matché |
| BM25 body | 0..5 | Normalisé |
| Priority | (p-5) × 0.3 | Boost léger si déjà scoré |
| MOC-hub | +1.0 | Si note de type moc ET déjà scorée |
| Graph neighbor | 60 % du parent | Via expand_by_graph |
Seuil SEARCH_MIN_SCORE (défaut 1.0) : en-dessous, le mode « sans
contexte pertinent » se déclenche et Qwen3 est invité à ne pas inventer
de faits sur Fernand.
Compatibilité rétro
L'API garde la signature GET /ask?q=.... Le JSON renvoyé a :
response(conservé, consommé paraskAI.js)sources[](enrichi :slug,title,type,score,reasons,url)grounded(bool — nouveau)model(conservé)vault_size(nouveau)
Le champ rag de la v2 est remplacé par grounded (plus explicite).
Commandes utiles
# Régénérer le vault depuis strapi_extraction/docs/
python strapi_extraction\build-vault.py
# Démarrer l'API locale (pas d'indexation préalable à faire)
cd llm-api ; uvicorn api:app --host 0.0.0.0 --port 8000
# Vérifier la config active et la taille du vault
curl http://localhost:8000/health
# Forcer la relecture du vault sans redémarrer uvicorn
curl -X POST http://localhost:8000/reload-vault
# Tester une question en direct
curl "http://localhost:8000/ask?q=parle-moi+de+push-swap"
Fusion avec un vault Obsidian perso
Deux voies :
- Vault séparé (recommandé au début) : on ouvre
vault-grasbot/comme vault Obsidian indépendant. - Fusion : on copie
vault-grasbot/comme sous-dossier d'un vault existant. Les wikilinks restent valides tant que les noms sont uniques. Les notes persos doivent portersource: manual(évite l'écrasement parbuild-vault.py) etvisibility: private(exclues automatiquement du retrieval parload_vault()).
Limites actuelles
- Pas de mémoire conversationnelle : chaque question est indépendante.
- Pas de streaming : la réponse arrive en un bloc après 2-10 s.
- Aliases / answers auto-générés : c'est une base. Les notes
stratégiques (CV, IA, MOCs) méritent un enrichissement manuel en
passant
source: manual. clean-api-data.jsn'extrait pas leshomepagesni lesglossaires: bug préexistant, à corriger pour enrichir40-Glossaire/et la home.- Re-chargement manuel via
POST /reload-vault(pas encore automatisé via file watcher).
Évolutions priorisables
- Corriger
clean-api-data.js(homepages + glossaires). - Afficher les
sourcescitées sous la réponse dansChatBot.js. - Ajouter un badge
groundedpour informer le visiteur de la confiance. - Historique conversationnel court (3-4 tours).
- Streaming Ollama
stream: true(Server-Sent Events côté API). - File watcher sur
vault-grasbot/qui appellePOST /reload-vaultautomatiquement.