Code/IA/02 - injection_fiches/auto_ingest.py
Stéphan Peccini 6d2e877341
feat(audit): audit qualité complet — 907→0 erreurs ruff + fix multiselect labels
- Correction des 907 erreurs ruff (pathlib, imports, nommage, simplifications, docstrings)
- Fix déduplication labels dans multiselect nœuds d'arrivée (analyse)
- Expansion 1→N label→IDs pour le Sankey (Pays d'opération)
- Ajout CLAUDE.md et document de design de l'audit
- Mise à jour .gitignore (artefacts tests exploratoires)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 11:52:01 +01:00

250 lines
8.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Script d'injection automatique de documents pour PrivateGPT
Ce script parcourt un répertoire spécifié et injecte tous les fichiers
compatibles dans PrivateGPT via son API REST.
"""
import argparse
import logging
import os
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
import requests
# Configuration du logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler("pgpt_auto_ingest.log")
]
)
logger = logging.getLogger(__name__)
# Extensions de fichiers couramment supportées par PrivateGPT
SUPPORTED_EXTENSIONS = {
'.pdf', '.txt', '.md', '.doc', '.docx', '.ppt', '.pptx',
'.xls', '.xlsx', '.csv', '.epub', '.html', '.htm'
}
def parse_arguments():
"""Parse les arguments de ligne de commande."""
parser = argparse.ArgumentParser(
description="Injecte automatiquement tous les fichiers d'un répertoire dans PrivateGPT"
)
parser.add_argument(
"-d", "--directory",
type=str,
required=True,
help="Chemin du répertoire contenant les fichiers à injecter"
)
parser.add_argument(
"-u", "--url",
type=str,
default="http://localhost:8001",
help="URL de l'API PrivateGPT (défaut: http://localhost:8001)"
)
parser.add_argument(
"-r", "--recursive",
action="store_true",
help="Parcourir récursivement les sous-répertoires"
)
parser.add_argument(
"-t", "--threads",
type=int,
default=5,
help="Nombre de threads pour les injections parallèles (défaut: 5)"
)
parser.add_argument(
"--retry",
type=int,
default=3,
help="Nombre de tentatives en cas d'échec (défaut: 3)"
)
parser.add_argument(
"--retry-delay",
type=int,
default=5,
help="Délai entre les tentatives en secondes (défaut: 5)"
)
parser.add_argument(
"--timeout",
type=int,
default=300,
help="Délai d'attente pour chaque requête en secondes (défaut: 300)"
)
parser.add_argument(
"--extensions",
nargs="+",
help="Liste d'extensions spécifiques à injecter (ex: .pdf .txt)"
)
return parser.parse_args()
def find_files(directory: str, recursive: bool = False,
extensions: set[str] = SUPPORTED_EXTENSIONS) -> list[Path]:
"""Trouve tous les fichiers avec les extensions spécifiées dans le répertoire.
Args:
directory: Répertoire à scanner
recursive: Si True, parcourt aussi les sous-répertoires
extensions: Ensemble d'extensions de fichier à inclure
Returns:
Liste des chemins de fichiers trouvés
"""
directory_path = Path(directory)
if not directory_path.exists() or not directory_path.is_dir():
logger.error(f"Le répertoire {directory} n'existe pas ou n'est pas un répertoire.")
return []
files = []
if recursive:
# Parcours récursif
for root, _, filenames in os.walk(directory):
for filename in filenames:
file_path = Path(root) / filename
if file_path.suffix.lower() in extensions:
files.append(file_path)
else:
# Parcours non récursif
for file_path in directory_path.iterdir():
if file_path.is_file() and file_path.suffix.lower() in extensions:
files.append(file_path)
return files
def ingest_file(file_path: Path, pgpt_url: str, timeout: int,
retry_count: int, retry_delay: int) -> tuple[Path, bool, str]:
"""Injecte un fichier dans PrivateGPT.
Args:
file_path: Chemin du fichier à injecter
pgpt_url: URL de base de l'API PrivateGPT
timeout: Délai d'attente pour la requête
retry_count: Nombre de tentatives en cas d'échec
retry_delay: Délai entre les tentatives en secondes
Returns:
Tuple contenant (chemin_fichier, succès, message)
"""
ingest_url = f"{pgpt_url}/v1/ingest/file"
for attempt in range(retry_count):
try:
logger.info(f"Injection de {file_path} (tentative {attempt + 1}/{retry_count})")
with open(file_path, 'rb') as file:
files = {'file': (file_path.name, file, 'application/octet-stream')}
response = requests.post(ingest_url, files=files, timeout=timeout)
if response.status_code == 200:
result = response.json()
doc_ids = result.get('document_ids', [])
logger.info(f"Succès! {file_path} -> {len(doc_ids)} documents créés")
return file_path, True, f"{len(doc_ids)} documents créés"
error_msg = f"Erreur HTTP {response.status_code}: {response.text}"
logger.warning(error_msg)
if attempt < retry_count - 1:
logger.info(f"Nouvelle tentative dans {retry_delay} secondes...")
time.sleep(retry_delay)
else:
return file_path, False, error_msg
except Exception as e:
error_msg = f"Exception: {str(e)}"
logger.warning(error_msg)
if attempt < retry_count - 1:
logger.info(f"Nouvelle tentative dans {retry_delay} secondes...")
time.sleep(retry_delay)
else:
return file_path, False, error_msg
return file_path, False, "Nombre maximum de tentatives atteint"
def main():
"""Fonction principale."""
args = parse_arguments()
# Préparation des extensions si spécifiées
extensions = set(args.extensions) if args.extensions else SUPPORTED_EXTENSIONS
# Assurer que les extensions commencent par un point
extensions = {ext if ext.startswith('.') else f'.{ext}' for ext in extensions}
logger.info(f"Démarrage de l'injection automatique depuis {args.directory}")
logger.info(f"URL PrivateGPT: {args.url}")
logger.info(f"Mode récursif: {args.recursive}")
logger.info(f"Extensions: {', '.join(extensions)}")
# Trouver les fichiers
files = find_files(args.directory, args.recursive, extensions)
total_files = len(files)
if total_files == 0:
logger.warning(f"Aucun fichier trouvé avec les extensions {', '.join(extensions)} dans {args.directory}")
return
logger.info(f"Trouvé {total_files} fichiers à injecter")
# Statistiques
successful = 0
failed = 0
failed_files = []
# Injection des fichiers en parallèle
with ThreadPoolExecutor(max_workers=args.threads) as executor:
futures = {
executor.submit(
ingest_file,
file_path,
args.url,
args.timeout,
args.retry,
args.retry_delay
): file_path for file_path in files
}
for future in as_completed(futures):
file_path, success, message = future.result()
if success:
successful += 1
else:
failed += 1
failed_files.append((file_path, message))
# Afficher la progression
progress = (successful + failed) / total_files * 100
logger.info(f"Progression: {progress:.1f}% ({successful + failed}/{total_files})")
# Rapport final
logger.info("="*50)
logger.info("RAPPORT D'INJECTION")
logger.info("="*50)
logger.info(f"Total des fichiers: {total_files}")
logger.info(f"Succès: {successful}")
logger.info(f"Échecs: {failed}")
if failed > 0:
logger.info("\nDétails des échecs:")
for file_path, message in failed_files:
logger.info(f"- {file_path}: {message}")
logger.info("="*50)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
logger.info("\nInterruption par l'utilisateur. Arrêt du processus.")
sys.exit(1)
except Exception as e:
logger.error(f"Erreur non gérée: {str(e)}", exc_info=True)
sys.exit(1)