Architectures IA locales pour PME suisses : RTX Spark, DGX Spark, RTX PRO 6000, H100. TCO réel, ingénierie RAG, conformité nLPD. Procédures opérationnelles publiées progressivement.
View the Project on GitHub doit4everyone/ia-locale-pme-suisse
| Retour au sommaire | Guide de déploiement |
Statut : validés en lab sur VM-RAG-LAB, Ubuntu Server 26.04 LTS, septembre 2026. Tests DENY, groupes imbriqués et synchronisation SMB réalisés avec corpus réel (Parties 1 et 2). Extension Entra ID validée en lab (§11). Synthèse des réunions Teams validée en lab (§12).
Quelle version installer : toujours la dernière Release publiée, qui contient tous les correctifs de sécurité. La Release v2.12.0 garde les scripts des Parties 1 et 2 tels qu’ils ont été validés avant la Partie 3 : c’est une référence historique, à ne pas installer. Les modifications de la Partie 3 sont compatibles : sans document SharePoint et avec
ENTRA_ENABLED=false, la stack SMB se comporte de la même façon.
Ces scripts constituent le pipeline RAG décrit dans le Guide de déploiement stack IA locale. Ils sont publiés à titre documentaire pour permettre la compréhension et la réimplémentation. Les valeurs sensibles (domaines, IPs, mots de passe) ont été remplacées par des placeholders.
Rédigés à titre documentaire, sans dépendance à aucun constructeur, revendeur ou intégrateur cité.
Utilisateur (navigateur, https://<nom de la stack>)
↓
Nginx (seul service qui publie des ports : 80, 443, 5678 ; §9.8)
↓
Open WebUI (authentification LDAP, identité signée transmise à la RAG API)
↓
RAG API FastAPI (main.py + auth.py)
├→ Qdrant → chunks filtrés par ACL NTFS
├→ Ollama ou vLLM (§2) → génération LLM locale
└→ Microsoft Graph → groupes Entra ID (optionnel, §11)
↓
Journal nLPD (/var/log/rag/rag-queries.jsonl)
n8n (toutes les heures) → POST /teams/sync (teams.py + teams_graph.py)
├→ Microsoft Graph → transcriptions Teams du groupe d'adhésion
├→ Ollama ou vLLM → compte rendu
└→ email → brouillon à l'organisateur (§12)
Les scripts indexer.py et acl_resolver.py sont stockés sur l’hôte Ubuntu, dans /root/rag-pipeline, monté en lecture seule dans le conteneur rag-api. La synchronisation horaire (/admin/sync) les exécute dans le conteneur. Ils peuvent aussi être lancés manuellement depuis l’hôte, avec leur environnement virtuel.
| Script | Rôle | Exécution |
|---|---|---|
main.py |
RAG API FastAPI : retrieval hybride BM25+vectoriel, génération, journalisation nLPD, endpoint /admin/sync |
Dans le conteneur rag-api |
auth.py |
Résolution des SID de l’utilisateur (compte et groupes, tokenGroups) via LDAP, filtrage des chunks par ACL |
Dans le conteneur rag-api |
indexer.py |
Parcours SMB, extraction de texte, embedding, écriture Qdrant avec payload ACL | Sur l’hôte, via /admin/sync ou manuel |
acl_resolver.py |
Lecture des ACL NTFS via smbcacls --numeric (SID), mise à jour autorises[] et interdits[] dans Qdrant, contrôle de migration |
Sur l’hôte, via /admin/sync ou manuel |
teams.py |
Synthèse des réunions Teams : lecture du VTT, contrôle de longueur, prompt, contrôles déterministes, brouillon | Dans le conteneur rag-api |
teams_graph.py |
Récupération des transcriptions Teams via Microsoft Graph, fichier d’état | Dans le conteneur rag-api |
../teams-test/ |
Fichiers et scripts de test de la synthèse Teams | Voir §12.7 du guide |
sp_indexer.py |
Indexation SharePoint Online avec traduction des permissions, déchiffrement Purview par mip-service |
Dans le conteneur rag-api, via /admin/sync ou manuel |
mip-service/ |
Service de déchiffrement Purview (.NET 8, SDK MIP, Ubuntu 24.04) : /dechiffrer, /droits |
Conteneur mip-service, profil Compose purview, voir §14 |
../purview/ |
Sonde de déchiffrement, tests de mip-service |
Voir §14.7 du guide |
../sharepoint/ |
Inventaires des permissions, accord Sites.Selected site par site, sonde |
Voir §13.2 et §13.3 du guide |
Set-AccesIndexationRAG.ps1 |
Lecture seule du compte d’indexation sur les dossiers du partage, par un groupe dédié, héritage coupé compris ; affiche et contrôle les permissions du partage | Sur le serveur de fichiers, voir §5.2.4 et §16.5 du guide |
tests-apres-modification.md |
Procédure de tests après toute modification : démarrage, identité, qualité, refus, cloisonnement, synchronisation | Sur la VM, voir §8.9 et §16 |
test_cloisonnement.py |
Test de non-divulgation : mode accès (filtres, quelques secondes) ou complet (questions) | Dans le conteneur rag-api, voir §16 |
test_revalidation_bm25.py |
Tests unitaires des droits sur la recherche par mots-clés (droit retiré, refus ajouté, point supprimé, Qdrant indisponible, recherche sans groupes), Qdrant simulé ; un échec bloque deploy.sh |
docker exec -w /app rag-api python3 /rag-pipeline/test_revalidation_bm25.py, voir §16.5.1 |
test_echec_droits.py |
Tests unitaires des échecs d’écriture des droits : acl_resolver.py (trois issues, seconde tentative, code 2), documents retirés de la recherche par la RAG API, passages incomplets, registre (écriture, marqueur, validation), âge maximal des droits, fichier SharePoint trop volumineux ; Qdrant simulé ; un échec bloque deploy.sh |
docker exec -w /app rag-api python3 /rag-pipeline/test_echec_droits.py, voir §16.5.2 |
test_revocation_bm25.py |
Test en lab : retire quelques secondes le droit sur un document dans Qdrant, sans reconstruire l’index, et vérifie que la recherche ne le renvoie plus ; droits remis ensuite | Dans le conteneur rag-api, voir §16.5.1 |
test_citations.py |
Tests unitaires du contrôle des citations (accents, ambiguïté, source inventée) et de la lecture du verdict du juge (schéma complet exigé), sans modèle ni Qdrant ; obligatoire dans deploy.sh depuis la 2.21.10 |
docker exec -w /app rag-api python3 /rag-pipeline/test_citations.py, voir §0 étape 16 |
test_sid.py |
Tests unitaires du contrôle d’accès par SID (2.22.0) : sorties réelles de smbcacls --numeric, tokenGroups (lecture refusée = aucun groupe), décision d’accès, refus sur les identités absentes de tokenGroups (2.22.1) ; obligatoire dans deploy.sh |
docker exec -w /app rag-api python3 /rag-pipeline/test_sid.py, voir guide §16.5.3 |
diag_sid.py |
Contrôle des SID d’un compte : tokenGroups lisible, groupes et noms, SID retenus pour l’accès ; ne modifie rien |
docker exec -w /app rag-api python3 /rag-pipeline/diag_sid.py <compte@domaine> |
s10/mesure_s10.py |
Campagne de mesure du §10 : pose le jeu de questions comme Open WebUI, évalue chaque réponse (document principal, éléments attendus et interdits, refus, ancrage), rapport JSON | Dans le conteneur rag-api, voir §10.3 |
s10/charge_s10.py |
Mesure de charge du §10.12 : 1 à N questions posées en même temps, après une mesure de référence de chaque question seule ; gain de chaque série, débit, écarts | docker exec -it rag-api python3 /rag-pipeline/s10/charge_s10.py /rag-pipeline/s10/jeu_questions_s10.json --paliers 1,2,4,6,8 --etiquette ma-configuration |
s10/mesure_juge.py |
Ce que le juge détecte (§10.12) : réponses réelles et versions volontairement fausses (chiffre, fait inventé, documents intervertis), jugées plusieurs fois | docker exec -w /app rag-api python3 /rag-pipeline/s10/mesure_juge.py /rag-pipeline/s10/jeu_questions_s10.json --etiquette reference |
s10/jeu_questions_s10.json |
Jeu de 17 questions du corpus de démonstration, avec leurs critères ; à adapter à votre corpus | Lu par mesure_s10.py |
nginx/rag.conf |
Frontal HTTPS de la stack : Open WebUI sur le port 443, n8n sur le port 5678, /sante pour la supervision, refus des requêtes par adresse IP ou autre nom |
Conteneur nginx, deploy.sh remplace <NOM-STACK>, voir §9.8 |
cas_cloisonnement.exemple.json |
Exemple de fichier de cas, à adapter puis copier en /rag-pipeline/cas_cloisonnement.json |
Voir §16.3 |
Dans le dépôt, les scripts contenant des valeurs d’exemple sont préfixés anon_ (api/anon_main.py, api/anon_auth.py, anon_indexer.py, anon_acl_resolver.py). deploy.sh les renomme à l’installation.
RAG API FastAPI. Endpoints :
POST /query : requête RAG authentifiée par tokenPOST /v1/chat/completions : endpoint compatible OpenAI, utilisé par Open WebUIGET /health et GET /stats : supervisionPOST /admin/sync : lance indexer.py et acl_resolver.py en sous-processusPOST /teams/summarize : compte rendu à partir d’une transcription VTT fournie (tests), réservé à ADMIN_TOKENPOST /teams/sync : récupération des nouvelles transcriptions Teams et production des brouillons, réservé à ADMIN_TOKEN, appelé par n8nFonctionnalités :
autorises[] et interdits[])chunk_index ± radius sur le document le mieux classé : les chunks voisins sont récupérés dans l’ordre du document, le chunk pertinent reste dans le contexte. Contrôle ACL appliqué après le scrollchunk_index pour que l’extension de contexte fonctionne même quand BM25 gagne le RRF. BM25 capture les termes exacts (noms de fichiers,
acronymes, commandes) là où la recherche vectorielle seule échoue sur les reformulations.TOP_K configurable (défaut 20), validé en lab pour améliorer le recall sur les gros fichiers .mdwarmup_judge() : ping du juge Ollama à chaque requête pour le maintenir chargé en mémoire (sans objet avec vLLM, dont les modèles restent chargés)JUDGE_KEEP_ALIVE : durée de rétention du juge après chaque appel (défaut 2h)[Document X]qwen3:4b-instruct avec Ollama, le modèle principal avec vLLM)LLM_API, EMBED_API) : l’API native d’Ollama (défaut) ou l’API compatible OpenAI de vLLM (§2). Avec vLLM, les vecteurs d’embedding sont ramenés à une longueur de 1, comme avec Ollama, et un refus du serveur (fenêtre dépassée, HTTP 400) est écrit au journalasyncio.Lock contre les synchronisations simultanéesVariables d’environnement requises :
LLM_API=ollama # « openai » avec vLLM (§2)
EMBED_API=ollama # « openai » avec vLLM
LLM_BASE_URL=http://<IP-SERVEUR-MODELES>:11434 # vLLM : port 8000
EMBED_BASE_URL=http://<IP-SERVEUR-MODELES>:11434 # vLLM : port 8001
JUDGE_BASE_URL= # vide : même serveur que LLM_BASE_URL
VLLM_API_KEY= # avec vLLM : la clé --api-key des serveurs
LLM_MODEL=qwen3:14b
JUDGE_MODEL=qwen3:4b-instruct # qwen3:14b avec vLLM
JUDGE_KEEP_ALIVE=2h # Ollama seulement : rétention du juge en mémoire après chaque appel.
# Format : "5m", "2h", "-1" (indéfiniment).
# Passer à -1 une fois le GPU installé.
EMBED_MODEL=nomic-embed-text
QDRANT_HOST=http://qdrant:6333
QDRANT_COLLECTION=documents
DOCUMENTATION_COLLECTION=documentation # Collection pour la documentation technique
DOCUMENTATION_PATHS= # Dossiers racine routés vers DOCUMENTATION_COLLECTION (vide = aucun ; lab : DOIT4EVERYONE)
ORG_NAME=<Nom de l'organisation>
API_TOKEN=<token fort>
ADMIN_TOKEN=<token fort distinct>
LOG_FILE=/var/log/rag/rag-queries.jsonl
REPORT_DIR=/var/log/rag
LDAP_HOST=<DC-FQDN>
LDAP_PORT=636
LDAP_USE_TLS=true
LDAP_BASE_DN=DC=votre-domaine,DC=ch
LDAP_BIND_DN=CN=svc-rag,OU=Services,DC=votre-domaine,DC=ch
LDAP_BIND_PWD=<mot-de-passe>
LDAP_DOMAIN=DOMAINE
LDAP_CA_CERT=/etc/ssl/certs/ad-chain.pem
Résolution de l’identité Active Directory via LDAP, en SID depuis la 2.22.0 (§16.5.3 du guide). Appelé par main.py à chaque requête.
Fonctionnalités :
X-OpenWebUI-User-Jwt, vérifié avec OWUI_JWT_SECRET ; en-têtes simples refusés depuis la 2.21.0)CERT_REQUIRED si le certificat CA est présent et non vide, CERT_NONE avec avertissement dans les logs si absenttokenGroups : groupes imbriqués et groupe principal compris ; tokenGroups illisible, aucun groupe (accès refusé)S-1-5-32-…) ignorés par défaut ; ACL_SID_ETENDUS=1 les garde et ajoute « Tout le monde » et « Utilisateurs authentifiés »sid_domaine), transmis à acl_resolver.py pour son rapport (2.22.2), jamais pour une décision d’accèsENTRA_ENABLED=true, ajoute aux groupes AD les identifiants Entra de l’utilisateur, obtenus via Microsoft Graph (transitiveMemberOf), au format entra:usr:<id> et entra:grp:<id>. Authentification de l’application par certificat (MSAL). Si Graph échoue, les groupes AD sont conservés et le résultat n’est pas mis en cache. Détails et validation : guide §11.Variables de l’extension Entra ID :
ENTRA_ENABLED=false
ENTRA_TENANT_ID=<ID-ANNUAIRE>
ENTRA_CLIENT_ID=<ID-APPLICATION>
ENTRA_CERT_PATH=/etc/rag-certs/rag-identity.crt
ENTRA_KEY_PATH=/etc/rag-certs/rag-identity.key
ENTRA_CERT_THUMBPRINT=<EMPREINTE-CERTIFICAT>
Validation en lab : avant la 2.22.0, la résolution récursive par
memberOfavait été testée avec un groupeGRP-Clients-Niveau2imbriqué dansGRP-Clients(guide §9.4.5). Depuis la 2.22.0,tokenGroupsdonne les mêmes groupes imbriqués, plus le groupe principal : validé le 22 octobre 2026, un compte qui n’a que « Utilisateurs du domaine » voit un document autorisé à ce seul groupe (guide §16.5.3).
Indexeur de fichiers SMB. Parcourt un partage monté sur l’hôte et écrit les chunks dans Qdrant avec les métadonnées nécessaires au filtrage.
Fonctionnalités :
.docx, .pdf, .pptx, .txt, .md
pdfplumber. Les PDF scannés sans couche texte produisent un avertissement et tombent en quarantaine.EMBED_API, nomic-embed-text par défaut, configurable). Avec vLLM, un texte plus long que la fenêtre du serveur, refusé par celui-ci, est raccourci avec un avertissementcontent_hash + paramètres d’indexation + chunker_version. Les fichiers non modifiés sont ignorés sans appel au serveur de modèles. ACL reportées sur les fichiers modifiés.source (keyword) + chunk_index (integer)ORG_OWNERSur la détection d’organisation :
org_nameest une donnée déduite, utile pour la navigation et les facettes de recherche. Elle ne doit pas servir de critère d’accès. Le cloisonnement repose surautorises[], lu depuis les ACL paracl_resolver.py.
~$*, *.tmp, *.lnk, .DS_Store, Thumbs.db, desktop.ini, DfsrPrivate, System Volume Information, $RECYCLE.BINUtilisation manuelle :
cd /root/rag-pipeline
source .venv/bin/activate
# Charger les variables d'environnement
set -a && source /root/rag-stack/.env && set +a
# Indexer
python indexer.py --corpus /mnt/corpus-root
# Réindexer tous les fichiers en conservant les ACL
python indexer.py --corpus /mnt/corpus-root --force
# Réindexer depuis zéro (ACL perdues, relancer acl_resolver.py)
python indexer.py --corpus /mnt/corpus-root --reset
Variables d’environnement :
OLLAMA_URL=http://<IP-SERVEUR-MODELES>:11434 # le serveur d'EMBEDDING ; vLLM : port 8001
EMBED_API=ollama # « openai » avec vLLM
VLLM_API_KEY= # avec vLLM
QDRANT_URL=http://localhost:6333
ORG_OWNER=<Nom de l'organisation>
EMBED_MODEL=nomic-embed-text
CHUNK_SIZE=150
CHUNK_OVERLAP=20
MIN_CHUNK_WORDS=8 # Seuil minimal en mots pour conserver un chunk (8 préserve les sections courtes des .md)
DOCUMENTATION_COLLECTION=documentation
DOCUMENTATION_PATHS= # même valeur que dans le .env de la stack
Résolveur d’ACL NTFS. Lit les permissions de chaque fichier via smbcacls --numeric et met à jour le payload Qdrant.
Fonctionnalités :
smbcacls --numeric (paquet smbclient requis) : SID, type (0 autorisation, 1 refus), drapeaux et masque en hexadécimal ; une ligne non reconnue rend le fichier illisibleauth.py (2.22.0)documents_non_migres_sid)documents_refus_pour_tous), y compris un SID d’une autre machine ou d’un autre domaine depuis la 2.22.2 (SID du domaine transmis par la RAG API, SID_DOMAINE)autorises[] (ALLOW) et interdits[] (DENY explicites)source_type smb (ou sans ce champ) sont concernés : les chunks SharePoint ne sont jamais supprimés comme orphelins. La suppression se fait par filtre, sans limite du nombre de chunks par fichierValidation en lab (DENY explicite) :
test-client, membre deGRP-Clients(autorises[]) mais visé par un ACE de refus nominatif (interdits[]), ne voit pas le document interdit, même via le chemin d’extension de contexte. Un compte sans DENY dans le même groupe y accède normalement. Résultats documentés dans le guide §9.4.5.
Utilisation manuelle :
cd /root/rag-pipeline
source .venv/bin/activate
set -a && source /root/rag-stack/.env && set +a
python acl_resolver.py \
--share //<NOM-FILESERVER>/FileService \
--mount /mnt/corpus-root
Variables d’environnement :
QDRANT_URL=http://localhost:6333
SMB_USER=svc-rag
SMB_PASSWORD=<mot-de-passe>
SMB_DOMAIN=DOMAINE
Indexation des bibliothèques SharePoint Online (Partie 3, guide §13). Réutilise l’extraction, le découpage et les embeddings d’indexer.py.
entra:usr:, entra:grp: et entra:tous-internesmip-service si MIP_URL est renseignée, chaque chunk portant alors chiffre, mip_etiquette_id et mip_proprietaire (§14)/dev/shm, en mémoireSP_EXCLUDE_DRIVES)cTag ; permissions recalculées à chaque passage--dry-run : analyse complète, rien n’est écritVariables d’environnement :
SP_CLIENT_ID=<ID-APPLICATION-SHAREPOINT>
SP_CERT_THUMBPRINT=<EMPREINTE-CERTIFICAT-SHAREPOINT>
SP_KEY_PATH=/etc/rag-certs/rag-sharepoint.key
SP_SITES=<URL des sites, séparées par des virgules>
SP_EXCLUDE_DRIVES='Preservation Hold Library'
SP_MAX_FILE_MB=50
Utilisation manuelle :
docker exec -e PYTHONPATH=/app -w /app rag-api python3 /rag-pipeline/sp_indexer.py --dry-run
docker exec -e PYTHONPATH=/app -w /app rag-api python3 /rag-pipeline/sp_indexer.py
Synthèse des réunions Teams (Partie 3, guide §12). Après chaque réunion planifiée et transcrite, dont l’organisateur est membre du groupe d’adhésion, un compte rendu est produit par le modèle local et envoyé en brouillon à l’organisateur seul.
teams_graph.py :
GroupMember.Read.All)getAllTranscripts pour chaque membre, avec l’application RAG-Teams-Reader (OnlineMeetingTranscript.Read.All et stratégie d’accès applicatif attribuée au même groupe)teams.py :
LLM_API=openai), la fenêtre est celle du serveur, et SUMMARY_NUM_CTX ne doit pas la dépasserVariables d’environnement :
TEAMS_CLIENT_ID=<ID-APPLICATION-TEAMS>
TEAMS_CERT_PATH=/etc/rag-certs/rag-teams.crt
TEAMS_KEY_PATH=/etc/rag-certs/rag-teams.key
TEAMS_CERT_THUMBPRINT=<EMPREINTE-CERTIFICAT-TEAMS>
TEAMS_GROUP_ID=<ID-OBJET-GROUPE-ADHESION>
TEAMS_LOOKBACK_HOURS=48
TEAMS_MAX_PAR_SYNC=3
TEAMS_STATE_FILE=/var/log/rag/teams_state.json
# Optionnel : SUMMARY_MODEL (défaut : LLM_MODEL), SUMMARY_NUM_CTX (défaut : 16384)
Tests (fichiers dans scripts/teams-test/) : ces fichiers servent uniquement à valider l’installation et à évaluer les comptes rendus. Ils ne sont pas nécessaires au fonctionnement du pipeline. Les deux scripts s’exécutent dans le conteneur rag-api et doivent être copiés dans /root/rag-pipeline, le dossier monté dans le conteneur, le temps des tests.
# Synthèse sur la transcription fictive, avec contrôles automatiques
docker cp reunion-test-2026-09-29.vtt rag-api:/tmp/
docker exec rag-api python3 /rag-pipeline/test_teams_summary.py /tmp/reunion-test-2026-09-29.vtt
# Accès Graph : jeton, groupe, transcriptions, contenu
docker exec -e PYTHONPATH=/app -w /app rag-api python3 /rag-pipeline/test_teams_graph.py
Le workflow n8n correspondant est n8n-teams-sync.json.
Dans le conteneur rag-api (api/requirements.txt) : depuis la 2.21.0, toutes les dépendances sont figées à la version exacte (pip freeze du conteneur validé) ; l’image de base python:3.11-slim est figée par son empreinte dans le Dockerfile. Mettre à jour une version, c’est revalider (§16.8). Les dépendances directes :
# API
fastapi
uvicorn[standard]
python-dotenv
httpx
# Identité signée transmise par Open WebUI
PyJWT
# Vector store
qdrant-client
# Retrieval hybride BM25
# Index en mémoire : ~1 Mo pour 1 000 chunks, ~200 Mo pour 50 000 chunks.
# Au-delà de 200 000 chunks, migrer vers Qdrant BM42 (voir §10).
rank-bm25
# Résolution LDAP
ldap3
# Extraction de texte
python-docx
pdfplumber
python-pptx
# Microsoft Graph, authentification par certificat (extension Entra ID, Teams)
msal
Sur l’hôte (/root/rag-pipeline/.venv) :
pip install qdrant-client python-docx pdfplumber python-pptx requests
Paquet système (dans le Dockerfile) :
smbclient # fournit smbcacls pour acl_resolver.py
Structure du payload stocké pour chaque chunk :
{
"text": "...",
"source": "CLIENTS/ClientA/contrat.docx",
"source_id": "md5-tronqué",
"org_name": "ClientA",
"detection_niveau": 2,
"embed_model": "nomic-embed-text",
"chunk_index": 0,
"chunk_size": 150,
"chunker_version": 2,
"content_hash": "3c7db804...",
"autorises": ["DOMAINE\\GRP-Clients", "DOMAINE\\Admins du domaine"],
"interdits": [],
"acl_updated_at": "2026-09-14T10:06:02Z"
}
Les chunks issus de SharePoint (Partie 3) porteront un champ source_type égal à sharepoint. Les chunks SMB n’ont pas ce champ : acl_resolver.py les traite comme smb.
Le champ autorises est mis à jour par acl_resolver.py indépendamment du contenu. Le champ interdits porte les DENY explicites NTFS, prioritaires sur autorises.
Scripts publiés sous licence MIT. Libres de réutilisation, d’adaptation et de redistribution avec attribution.
Validé sur VM-RAG-LAB, Ubuntu Server 26.04 LTS (resolute), Docker 29.7.2, septembre 2026.
ℹ️ Références, structuration et aide à la rédaction assistées par IA, avec validation humaine finale.