IA locale pour PME suisse, guide décisionnel et procédures

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

Scripts pipeline RAG local

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é.


Architecture

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.


Scripts disponibles

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.


main.py

RAG API FastAPI. Endpoints :

Fonctionnalités :

Variables 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

auth.py

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 :

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 memberOf avait été testée avec un groupe GRP-Clients-Niveau2 imbriqué dans GRP-Clients (guide §9.4.5). Depuis la 2.22.0, tokenGroups donne 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).


indexer.py

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 :

Sur la détection d’organisation : org_name est 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 sur autorises[], lu depuis les ACL par acl_resolver.py.

Utilisation 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

acl_resolver.py

Résolveur d’ACL NTFS. Lit les permissions de chaque fichier via smbcacls --numeric et met à jour le payload Qdrant.

Fonctionnalités :

Validation en lab (DENY explicite) : test-client, membre de GRP-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

sp_indexer.py

Indexation des bibliothèques SharePoint Online (Partie 3, guide §13). Réutilise l’extraction, le découpage et les embeddings d’indexer.py.

Variables 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

teams.py et teams_graph.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 :

teams.py :

Variables 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.


Dépendances

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

Payload Qdrant

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.


Licence

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.