# --- 2a. Les 22 textes francais (themes reels de la serie) ---------
TEXTES = {
"txt-incident-derive-montage": (
"incident-derive-montage.txt",
{"theme": "incidents"},
"""Incident : la derive de montage disque. Le conteneur Qdrant tournait depuis des semaines quand
le volume hote a commence a deriver : le point de montage partage avec d'autres services GenAI
a glisse vers un repertoire different, et Qdrant a continue d'ecrire sur un montage partiellement
detache. Symptome observe : la collection semblait intacte (les compteurs repondaient) mais les
recherches renvoyaient des resultats vides par intermittence. Diagnostic : comparer le chemin reel
du montage vu par le processus avec celui declare dans la configuration. Correctif de fond :
disque virtuel dedie (VHDX) par service, jamais de repertoire hote partage entre stacks.""" ,
),
"txt-incident-perte-donnees": (
"incident-perte-donnees.txt",
{"theme": "incidents"},
"""Perte de donnees par split-brain. Deux instances Qdrant ont un moment pointe
vers le meme stockage avec des etats d'index differents : la premiere a cree la collection,
la seconde l'a recree par-dessus en croyant le stockage vierge. Resultat : plusieurs jours
d'indexation perdus, sans erreur visible ni cote client ni cote serveur. Lecons : une seule
instance proprietaire d'un volume donne ; un verrou d'unicite au niveau de l'orchestrateur
Docker ; une sonde qui compare le compte de points attendu au compte reel apres chaque
redemarrage, pas seulement le healthz.""" ,
),
"txt-incident-sauvegardes": (
"incident-sauvegardes.txt",
{"theme": "incidents"},
"""Incident : sauvegardes a moitie cablees. La tache planifiee copiait bien le repertoire
de stockage Qdrant, mais jamais le fichier de configuration des collections ni les cles de
chiffrement associees. Au moment de restaurer, les donnees etaient la et illisibles : la
restauration d'un dump sans sa configuration est une sauvegarde decorative. Regle qui en est
sortie : une sauvegarde n'est valide qu'apres une restauration TESTEE sur une instance
sur une instance temoin, avec un scenario de verification ecrit (compte de points, recherche temoin,
comparaison de metadonnees).""" ,
),
"txt-infra-docker-wsl2": (
"infra-docker-wsl2.txt",
{"theme": "infrastructure"},
"""Deployer Qdrant sous Windows passe par WSL2 et Docker Desktop. Le point critique n'est pas
l'image (qdrant/qdrant se lance en une commande) mais le stockage : le filesystem VHDX de WSL2
grossit par paliers et ne rend jamais l'espace automatiquement. Recommandations pratiques :
un disque virtuel isole par service pour eviter qu'un service affame les autres ; un plafond
de taille configure des le depart ; une surveillance du taux d'occupation du VHDX lui-meme,
pas seulement du filesystem invitant, car les deux derivent differemment.""" ,
),
"txt-infra-quantization": (
"infra-quantization.txt",
{"theme": "infrastructure"},
"""La quantization (TurboQuant chez Qdrant) compresse les vecteurs stockes au prix d'une perte
de precision controlable. Parametres en jeu : quantization scalaire sur les vecteurs, avec
recherche hybride qui continue d'interroger les vecteurs originaux quand le score est proche de
la frontiere (rescoring). Gain mesure sur un corpus d'essai : de l'ordre de deux tiers de
memoire en moins pour une perte de rappel marginale. La bonne question n'est pas "faut-il
quantizer" mais "a partir de quelle taille de corpus le compromis devient-il rentable" : en
dessous de quelques centaines de milliers de vecteurs, la RAM economisee ne justifie pas la
complexite operationnelle.""" ,
),
"txt-embeddings-qwen": (
"embeddings-qwen.txt",
{"theme": "embeddings"},
"""Le service d'embeddings de la serie est auto-heberge : un modele qwen3-4b quantize AWQ
servant des vecteurs de 2560 dimensions. Il a remplace une API commerciale proprietaire
(1536 dimensions, facturee au token). Le calcul economique est plus subtil que "auto-heberge
= gratuit" : le cout se deplace vers la VRAM occupee en permanence, la maintenance de l'image
GPU et la surveillance de la derive de versions. Ce qui motive le choix est surtout la
souverainete : pas de dependance a un fournisseur qui peut changer de tarification ou de
modele du jour au lendemain, et la garantie que les donnees indexees ne sortent pas.""" ,
),
"txt-embeddings-dimensions": (
"embeddings-dimensions.txt",
{"theme": "embeddings"},
"""Le nombre de dimensions d'un embedding est un compromis, pas un score de qualite. Plus de
dimensions : plus de capacite a separer des concepts proches, mais plus de RAM par point,
des recherches plus lentes et un cout d'API superieur si le service est facture. Moins de
dimensions : l'inverse, avec un plancher en dessous duquel des distinctions utiles
disparaissent (des requetes differentes tombent sur les memes voisins). Les modeles recents
permettent de tronquer a la demande (Matryoshka) : indexer gros, interroger petit. La
meilleure pratique reste de mesurer sur SON corpus : le rappel a k dimensions donnees est la
seule metrique qui compte.""" ,
),
"txt-chunking-overlap": (
"chunking-overlap.txt",
{"theme": "chunking"},
"""Le decoupage en partitions chevauchees : quand un document est coupe en morceaux de mille
tokens, chaque morceau recommence avec les cent derniers tokens du precedent. Pourquoi :
une phrase coupée en deux au milieu perd son sens des deux cotes ; le chevauchement garantit
qu'au moins une partition contient chaque transition complete. Le cout : de la redondance
stockee (quelques pour cent du corpus) et un risque de doublons dans les resultats si le
moteur de recherche ne deduplique pas. Regle pratique : un chevauchement de l'ordre d'une
phrase a deux phrases, pas plus -- un chevauchement massif fabrique des doublons qui
polluent le haut du classement.""" ,
),
"txt-chunking-granularite": (
"chunking-granularite.txt",
{"theme": "chunking"},
"""Granularite contre rappel : des partitions courtes (un paragraphe) rendent les citations
precises -- la preuve ramenee au juge est courte et pertinente -- mais multiplient les
fragments qui manquent le contexte global. Des partitions longues capturent le contexte mais
diluent le signal : le vecteur moyen d'un long passage ressemble a beaucoup de choses a la
fois, et le reranking devient necessaire. Le compromis se mesure, il ne se decrete pas :
sur un gold-set etiquete, faire varier la taille de partition et tracer la courbe rappel
en fonction de la taille est l'exercice de reference avant tout passage en production.""" ,
),
"txt-hnsw-parametres": (
"hnsw-parametres.txt",
{"theme": "hnsw"},
"""HNSW (Hierarchical Navigable Small World) est l'index approximatif au coeur de Qdrant.
Deux parametres gouvernent la construction : m, le nombre de liens par noeud (plus m est
grand, plus le graphe est dense, plus la memoire explose, meilleures sont les connexions) ;
ef_construct, la taille de la liste de candidats pendant la construction (plus elle est
grande, plus la construction est lente, meilleure est la qualite du graphe). Valeurs par
defaut raisonnables : m=16, ef_construct=100. Augmenter m est pertinent quand les vecteurs
sont tres proches les uns des autres ; la plupart des autres cas n'en tirent rien.""" ,
),
"txt-hnsw-ef-compromis": (
"hnsw-ef-compromis.txt",
{"theme": "hnsw"},
"""Le parametre ef a la requete est le levier du compromis exactitude/vitesse d'HNSW :
il fixe la taille de la file de candidats explores pendant la recherche. Avec ef faible
(8, 16), la recherche est rapide et survole le graphe : le rappel chute. Avec ef eleve
(256, 512), la recherche ralentit et converge vers le resultat exact de la force brute.
La mesure de reference : calculer le classement exact par force brute en dehors du serveur,
puis comparer recall@k du serveur pour chaque ef. La courbe typique sature : au-dela d'un
certain ef, le rappel n'augmente plus mais le temps continue de croitre -- c'est ce point
de saturation qu'il faut viser, pas le maximum.""" ,
),
"txt-grounding-sddd": (
"grounding-sddd.txt",
{"theme": "grounding"},
"""La methode SDDD (Specification-Driven Design and Development) exige de croiser trois
sources avant d'agir : le code (lecture directe des sources), la conversation (historique des
decisions deja prises) et la recherche semantique (index vectoriel du depot). Une affirmation
n'est recevable que si au moins une source la verifie ; une contradiction entre sources est
un signal d'arret, pas un detail. Le grounding est la mise en oeuvre technique de cette
exigence : avant de repondre "le module X ne fait pas Y", l'agent DOIT avoir interroge la
memoire -- sinon il hallucine un etat du projet. La memoire semantique est donc une
infrastructure de verification, pas une convenance.""" ,
),
"txt-grounding-hallucination": (
"grounding-hallucination.txt",
{"theme": "grounding"},
"""L'echec typique d'un agent sans memoire externe : il re-explore le meme terrain a chaque
session, re-pose des questions deja tranchees, et finit par affirmer un etat du projet qui
n'existe plus. C'est l'hallucination par absence de contexte, distincte de l'hallucination
de modelisation : le modele n'invente pas par nature, il comble un vide. Le traitement est
structurel, pas promptuel : ancrer chaque tache dans une recherche prealable ("qu'a-t-on
deja fait autour de X") dont les resultats alimentent le contexte. La qualite du grounding
depend alors directement de la qualite du retrieval -- d'ou les notebooks de mesure de
cette serie.""" ,
),
"txt-agents-memoire-long-terme": (
"agents-memoire-long-terme.txt",
{"theme": "agents"},
"""Une flotte d'agents (Claude Code, Roo Code, sur plusieurs machines) produit du contenu
indexe ET le consomme : chaque session enrichit la memoire commune, chaque tache commence
par l'interroger. La memoire long-terme evite deux derivees symetriques : la perte de
continuite (refaire ce qui a ete defait, re-decider ce qui a ete decide) et la dilution
(un contexte resume a chaque session qui finit par ne plus rien contenir). L'horizon memoire
d'un agent brut est la session ; l'horizon memoire de la flotte est l'historique complet,
durable sur disque, sauvegarde.""" ,
),
"txt-agents-mcp": (
"agents-mcp.txt",
{"theme": "agents"},
"""Le serveur MCP roo-state-manager est le pont entre les agents et Qdrant : il indexe les
conversations et les depots au fil de l'eau, et expose des outils de recherche directement
dans l'agent (codebase_search, recherche semantique). Le protocole MCP normalise ce pont :
n'importe quel client compatible peut brancher la meme memoire sans integration dediee. Le
point d'architecture important : l'agent ne parle jamais a Qdrant directement -- il parle a
un serveur qui possede la logique d'indexation (chunking, embeddings, schemas de payload).
C'est exactement la separation que Kernel Memory industrialise cote documents.""" ,
),
"txt-tokenisation-bpe": (
"tokenisation-bpe.txt",
{"theme": "tokenisation"},
"""La tokenisation BPE (Byte Pair Encoding) decoupe le texte en unites statistiques
apprises : on commence par des caracteres, on fusionne iterativement les paires les plus
frequentes jusqu'a atteindre la taille de vocabulaire cible. Consequence directe pour le
chunking : la "taille en tokens" d'un texte depend du tokenizer, pas seulement du texte --
un mot rare se fragmente en many pieces, un mot commun reste entier. Budgeter un contexte,
dimensionner une partition, estimer un cout d'API : toutes ces operations passent par le
compteur du tokenizer reel, jamais par une approximation en mots.""" ,
),
"txt-tokenisation-cout": (
"tokenisation-cout.txt",
{"theme": "tokenisation"},
"""Le token est l'unite de compte de toute la chaine : budget de contexte du modele,
facturation des API, taille des partitions d'indexation. Les pieges classiques : comparer
des comptes de tokens entre tokenizers differents (ils ne comptent pas la meme chose) ;
dimensionner un pipeline sur de l'anglais puis l'exposer au francais, dont les tokens sont
en moyenne plus fragmentes ; oublier que les limites de contexte comptent aussi la SORTIE
du modele. Un reflexe sain pour tout pipeline documentaire : mesurer le distribution des
tailles en tokens du corpus reel avant de fixer les parametres de decoupage.""" ,
),
"txt-payload-filtering": (
"payload-filtering.txt",
{"theme": "vectordb"},
"""Filtrer cote serveur plutot qu'apres la requete : Qdrant associe a chaque point un payload
JSON indexe, et la recherche combine filtre payload + plus proches voisins dans une seule
operation. L'alternative naive -- recuperer top-k puis filtrer en client -- est incorrecte :
si les k premiers sont tous exclus par le filtre, la reponse est vide alors que des points
valides existent plus loin dans le classement. Avec un index payload sur les champs filtres,
le cout est marginal. La lecon pour une memoire d'agents : indexer des le depart les
metadonnees qu'on voudra filtrer (source, date, type, projet) -- les ajouter apres coup
demande une re-indexation complete.""" ,
),
"txt-vectordb-collections": (
"vectordb-collections.txt",
{"theme": "vectordb"},
"""Le modele de donnees de Qdrant : des points (un vecteur + un payload JSON) regroupes en
collections ; chaque collection porte sa propre configuration (dimension des vecteurs,
metrique de distance, parametres HNSW, quantization). Une collection = un schema d'embedding :
melanger des vecteurs de dimensions differentes dans une collection est impossible, et
melanger des vecteurs de MODELES differents corrompt silencieusement la recherche -- chaque
changement de modele d'embedding impose une collection neuve et une re-indexation. Regle
de nommage qui sauve des heures : suffixer les collections par le modele d'embedding.""" ,
),
"txt-kernelmemory-couche": (
"kernelmemory-couche.txt",
{"theme": "kernelmemory"},
"""Ce que Kernel Memory ajoute par-dessus une base vectorielle : l'ETL documentaire. Decodage
des formats (PDF, Word, Markdown, HTML, images avec OCR), decoupage en partitions avec
chevauchement, file d'attente de pipeline asynchrone (extract, partition, gen_embeddings,
save_records), ecriture dans le store vectoriel AVEC les metadonnees de provenance, et une
API de recherche qui renvoie des citations. La base vectorielle reste le socle -- KM ne la
remplace pas, il l'alimente et l'interroge. Le contrat est explicite : on lui donne des
documents et une question, il rend des passages sources.""" ,
),
"txt-kernelmemory-citations": (
"kernelmemory-citations.txt",
{"theme": "kernelmemory"},
"""La citation est l'unite de traite de Kernel Memory : chaque resultat porte son document
d'origine, son fichier, sa partition, le passage exact et un score de pertinence. Cette
trace document vers partition vers passage est portee par le pipeline d'ingestion, pas
reconstruite apres coup -- c'est ce qui distingue KM d'un upsert Qdrant ecrit a la main.
En pratique, la citation rend la reponse verifiable : un lecteur peut ouvrir le fichier
source et verifier que le passage dit bien ce que la reponse affirme. Sans cette trace,
le RAG est une opinion ; avec, c'est un argument.""" ,
),
"txt-kernelmemory-formats": (
"kernelmemory-formats.txt",
{"theme": "kernelmemory"},
"""Les formats reconnus par le service Kernel Memory couvrent les besoins bureautiques
(pdf, docx, pptx, xlsx, rtf, odt) et le web (html, md, csv, json, xml), plus quelques
sources (js, sh) -- mais PAS les extensions de code les plus courantes (.py, .cs, .java).
Ingerer un fichier non reconnu ne leve pas d'erreur a l'upload : le message echoue en
queue d'attente (poison queue) et le document reste etat "non complete" indefiniment.
Parade : suffixer .txt (le contenu circule intact) et garder la provenance reelle dans
les tags. Verifiez toujours le statut d'upload -- le 202 n'est pas une promesse
d'indexation.""" ,
),
}
# --- 2b. PDF reel du depot ------------------------------------------
PDF_CANDIDATES = [
Path.cwd() / "slides" / "01-introduction" / "pptx-reference" / "slides.pdf",
Path.cwd().parent / "slides" / "01-introduction" / "pptx-reference" / "slides.pdf",
]
PDF_SRC = next((p for p in PDF_CANDIDATES if p.is_file()), None)
# --- 2c. Code source reel du depot ----------------------------------
CODE_CANDIDATES = [
Path.cwd() / "scripts" / "genai-stack" / "genai.py",
Path.cwd() / "scripts" / "notebook_tools" / "audit_pip_install_cells.py",
]
CODE_SRCS = [p for p in CODE_CANDIDATES if p.is_file()]
# --- 2d. Ecriture du corpus sur disque ------------------------------
uploaded = [] # (document_id, chemin, tags)
for doc_id, (fname, tags, body) in TEXTES.items():
f = corpus_dir / fname
f.write_text(body.strip() + "\n", encoding="utf-8")
uploaded.append((doc_id, f, tags))
if PDF_SRC:
pdf_dst = corpus_dir / "cours-introduction-ia.pdf"
shutil.copyfile(PDF_SRC, pdf_dst)
uploaded.append(("pdf-cours-intro", pdf_dst, {"theme": "cours", "format": "pdf"}))
for src in CODE_SRCS:
dst = corpus_dir / (src.stem + ".py.txt") # .py non reconnu par KM -> suffixe .txt
dst.write_text(src.read_text(encoding="utf-8", errors="replace"), encoding="utf-8")
uploaded.append((f"code-{src.stem}", dst, {"theme": "code", "lang": "python"}))
n_txt = sum(1 for _, f, _ in uploaded if f.suffix == ".txt" and f.name.startswith(("incident", "infra", "embeddings", "chunking", "hnsw", "grounding", "agents", "tokenisation", "payload", "vectordb", "kernelmemory")))
n_pdf = sum(1 for _, f, _ in uploaded if f.suffix == ".pdf")
n_code = sum(1 for _, f, _ in uploaded if f.name.endswith(".py.txt"))
print(f"Corpus pret dans {corpus_dir.name}/ : {len(uploaded)} documents")
print(f" - {n_txt} textes francais (11 themes : incidents, infra, embeddings, chunking, hnsw, grounding, agents, tokenisation, vectordb, kernelmemory)")
print(f" - {n_pdf} PDF reel ({PDF_SRC.name if PDF_SRC else 'ABSENT'}, {PDF_SRC.stat().st_size // 1024 if PDF_SRC else 0} Ko)")
print(f" - {n_code} fichiers source Python ({', '.join(s.name for s in CODE_SRCS)})")