import requests
import time
import argparse
import json
import sys
import uuid
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple
from enum import Enum
# Configuration de l'API PrivateGPT
PGPT_URL = "http://127.0.0.1:8001"
API_URL = f"{PGPT_URL}/v1"
DEFAULT_INPUT_PATH = "rapport_final.md"
DEFAULT_OUTPUT_PATH = "rapport_analyse_genere.md"
MAX_CONTEXT_LENGTH = 3000 # Taille maximale du contexte en caractères
TEMP_DIR = Path("temp_sections") # Répertoire pour les fichiers intermédiaires
# Stratégies de gestion du contexte
class ContextStrategy(str, Enum):
NONE = "none" # Pas de contexte
TRUNCATE = "truncate" # Tronquer le contexte
SUMMARIZE = "summarize" # Résumer le contexte
LATEST = "latest" # Uniquement la dernière section
FILE = "file" # Utiliser des fichiers ingérés (meilleure option)
# Les 5 étapes de génération du rapport
steps = [
{
"title": "Analyse Principale",
"prompt": """Vous êtes un assistant stratégique expert chargé de produire des rapports destinés à des décideurs de haut niveau (COMEX, directions risques, stratèges politiques ou industriels).
Objectif : Analyser les vulnérabilités systémiques dans une chaîne de valeur numérique décrite dans le fichier rapport_final.md en vous appuyant exclusivement sur ce rapport structuré fourni.
Votre tâche est de développer les sections suivantes en respectant toutes les contraintes décrites :
1. Analyse détaillée (vulnérabilités critiques → modérées)
2. Interdépendances (effets en cascade)
3. Points de vigilance (indicateurs à surveiller)
Interprétation des indices :
• IHH (Herfindahl-Hirschmann) :
- Échelle : 0-100
- Seuils : <25 = Vert (faible), 25-50 = Orange (modéré), >50 = Rouge (élevée)
• ICS (Substituabilité) :
- Échelle : 0-1
- Interprétation : 0 = facilement substituable, 1 = impossible
• ISG (Stabilité Géopolitique) :
- Échelle : 0-100
- Seuils : <40 = Vert, 40-60 = Orange, >60 = Rouge
• IVC (Concurrence) :
- Échelle : 0-150
- Seuils : <10 = Faible, 10-50 = Moyenne, >50 = Forte
Contraintes strictes :
1. N'utiliser aucune connaissance externe. Seul le contenu fourni compte.
2. Ne pas recommander de diversification minière, industrielle ou politique.
3. Concentrez-vous sur les impacts sur les produits numériques.
Votre mission :
- Interpréter les indices (IHH, ICS, ISG, IVC)
- Croiser et hiérarchiser les vulnérabilités
- Distinguer les horizons temporels
- Identifier les chaînes de vulnérabilités interdépendantes
- Identifier des indicateurs d'alerte pertinents
Style attendu :
- Narratif, fluide, impactant, structuré
- Paragraphes complets + encadrés synthétiques avec données chiffrées
Commencez par lire attentivement les indices et les fiches dans la section "Éléments factuels", puis produisez vos sections d'analyse."""
},
{
"title": "Synthèse et Conclusion",
"prompt": """Vous êtes un assistant stratégique expert chargé de produire des rapports destinés à des décideurs de haut niveau (COMEX, directions risques, stratèges politiques ou industriels).
Sur la base de l'analyse précédente, rédigez les deux sections finales du rapport :
1. Synthèse (narration + hiérarchie vulnérabilités)
2. Conclusion (scénarios d'impact)
Pour la Synthèse :
- Résumez de façon percutante les vulnérabilités identifiées dans la chaîne de valeur numérique
- Hiérarchisez les risques en fonction du croisement des indices (IHH, ISG, ICS, IVC)
- Orientez cette synthèse vers la prise de décision pour différents acteurs économiques
- Utilisez un ton direct, factuel et orienté décision
Pour la Conclusion :
- Proposez 3-4 scénarios d'impact précis avec déclencheur, horizon temporel, gravité et conséquences
- Basez chaque scénario sur les données factuelles de l'analyse
- Montrez comment les différents indices interagissent dans chaque scénario
- Concentrez-vous particulièrement sur les applications numériques identifiées
Interprétation des indices (rappel) :
• IHH (Herfindahl-Hirschmann) : <25 = Vert, 25-50 = Orange, >50 = Rouge
• ICS (Substituabilité) : 0 = facilement substituable, 1 = impossible
• ISG (Stabilité Géopolitique) : <40 = Vert, 40-60 = Orange, >60 = Rouge
• IVC (Concurrence) : <10 = Faible, 10-50 = Moyenne, >50 = Forte
Style attendu :
- Narratif, percutant, orienté décideur
- Encadrés synthétiques avec données chiffrées clés
- Focus sur les implications concrètes pour les entreprises"""
}
]
def check_api_availability() -> bool:
"""Vérifie si l'API PrivateGPT est disponible"""
try:
response = requests.get(f"{PGPT_URL}/health")
if response.status_code == 200:
print("✅ API PrivateGPT disponible")
return True
else:
print(f"❌ L'API PrivateGPT a retourné le code d'état {response.status_code}")
return False
except requests.RequestException as e:
print(f"❌ Erreur de connexion à l'API PrivateGPT: {e}")
return False
def ingest_document(file_path: Path, session_id: str = "") -> bool:
"""Ingère un document dans PrivateGPT"""
try:
with open(file_path, "rb") as f:
# Si un session_id est fourni, l'ajouter au nom du fichier pour le tracking
if session_id:
file_name = f"input_{session_id}_{file_path.name}"
else:
file_name = file_path.name
files = {"file": (file_name, f, "text/markdown")}
# Ajouter des métadonnées pour identifier facilement ce fichier d'entrée
metadata = {
"type": "input_file",
"session_id": session_id,
"document_type": "rapport_analyse_input"
}
response = requests.post(
f"{API_URL}/ingest/file",
files=files,
data={"metadata": json.dumps(metadata)} if "metadata" in requests.get(f"{API_URL}/ingest/file").text else None
)
response.raise_for_status()
print(f"✅ Document '{file_path}' ingéré avec succès sous le nom '{file_name}'")
return True
except FileNotFoundError:
print(f"❌ Fichier '{file_path}' introuvable")
return False
except requests.RequestException as e:
print(f"❌ Erreur lors de l'ingestion du document: {e}")
return False
def setup_temp_directory() -> None:
"""Crée le répertoire temporaire pour les fichiers intermédiaires"""
if not TEMP_DIR.exists():
TEMP_DIR.mkdir(parents=True)
print(f"📁 Répertoire temporaire '{TEMP_DIR}' créé")
def save_section_to_file(section: Dict[str, str], index: int, session_uuid: str) -> Path:
"""Sauvegarde une section dans un fichier temporaire et retourne le chemin"""
setup_temp_directory()
section_file = TEMP_DIR / f"temp_section_{session_uuid}_{index+1}_{section['title'].lower().replace(' ', '_')}.md"
# Contenu du fichier avec métadonnées et commentaire explicite
content = (
f"# SECTION TEMPORAIRE GÉNÉRÉE - {section['title']}\n\n"
f"Note: Ce document est une section temporaire du rapport d'analyse en cours de génération.\n"
f"UUID de session: {session_uuid}\n\n"
f"{section['output']}"
)
# Écrire dans le fichier
section_file.write_text(content, encoding="utf-8")
return section_file
def ingest_section_files(section_files: List[Path]) -> List[str]:
"""Ingère les fichiers de section et retourne leurs noms de fichiers"""
ingested_file_names = []
for file_path in section_files:
try:
with open(file_path, "rb") as f:
files = {"file": (file_path.name, f, "text/markdown")}
# Ajouter des métadonnées pour identifier facilement nos fichiers temporaires
metadata = {
"type": "temp_section",
"document_type": "rapport_analyse_section"
}
response = requests.post(
f"{API_URL}/ingest/file",
files=files,
data={"metadata": json.dumps(metadata)} if "metadata" in requests.get(f"{API_URL}/ingest/file").text else None
)
response.raise_for_status()
# Ajouter le nom du fichier à la liste pour pouvoir le retrouver et le supprimer plus tard
ingested_file_names.append(file_path.name)
print(f"✅ Section '{file_path.name}' ingérée")
except Exception as e:
print(f"⚠️ Erreur lors de l'ingestion de '{file_path.name}': {e}")
return ingested_file_names
def get_context(sections: List[Dict[str, str]], strategy: ContextStrategy, max_length: int) -> str:
"""Génère le contexte selon la stratégie choisie"""
if not sections or strategy == ContextStrategy.NONE:
return ""
# Stratégie: utiliser des fichiers ingérés
if strategy == ContextStrategy.FILE:
# Cette stratégie n'utilise pas de contexte dans le prompt
# Elle repose sur les fichiers ingérés et le paramètre use_context=True
section_names = [s["title"] for s in sections]
context_note = f"NOTE IMPORTANTE: Les sections précédentes ({', '.join(section_names)}) " + \
f"ont été ingérées sous forme de fichiers temporaires avec l'identifiant unique '{session_uuid}'. " + \
f"Utilisez UNIQUEMENT le document '{input_file.name}' et ces sections temporaires pour votre analyse. " + \
f"IGNOREZ tous les autres documents qui pourraient être présents dans la base de connaissances. " + \
f"Assurez la cohérence avec les sections déjà générées pour maintenir la continuité du rapport."
print(f"📄 Utilisation de {len(sections)} sections ingérées comme contexte")
return context_note
# Stratégie: uniquement la dernière section
if strategy == ContextStrategy.LATEST:
latest = sections[-1]
context = f"# {latest['title']}\n{latest['output']}"
if len(context) > max_length:
context = context[:max_length] + "..."
print(f"📄 Contexte basé sur la dernière section: {latest['title']} ({len(context)} caractères)")
return context
# Stratégie: tronquer toutes les sections
if strategy == ContextStrategy.TRUNCATE:
full_context = "\n\n".join([f"# {s['title']}\n{s['output']}" for s in sections])
if len(full_context) > max_length:
truncated = full_context[:max_length] + "..."
print(f"✂️ Contexte tronqué à {max_length} caractères")
return truncated
return full_context
# Stratégie: résumer intelligemment
if strategy == ContextStrategy.SUMMARIZE:
try:
# Construire un prompt pour demander un résumé des sections précédentes
sections_text = "\n\n".join([f"# {s['title']}\n{s['output']}" for s in sections])
# Limiter la taille du texte à résumer si nécessaire
if len(sections_text) > max_length * 2:
sections_text = sections_text[:max_length * 2] + "..."
summary_prompt = {
"messages": [
{"role": "system", "content": "Vous êtes un assistant spécialisé dans le résumé concis. Créez un résumé très court mais informatif qui conserve les points clés."},
{"role": "user", "content": f"Résumez les sections suivantes en {max_length // 10} mots maximum, en conservant les idées principales et les points essentiels:\n\n{sections_text}"}
],
"temperature": 0.1,
"stream": False
}
# Envoyer la requête pour obtenir un résumé
response = requests.post(
f"{API_URL}/chat/completions",
json=summary_prompt,
headers={"accept": "application/json"}
)
response.raise_for_status()
# Extraire et retourner le résumé
result = response.json()
summary = result["choices"][0]["message"]["content"]
print(f"✅ Résumé de contexte généré ({len(summary)} caractères)")
return summary
except Exception as e:
print(f"⚠️ Impossible de générer un résumé du contexte: {e}")
# En cas d'échec, revenir à la stratégie de troncature
return get_context(sections, ContextStrategy.TRUNCATE, max_length)
def generate_text(prompt: str, previous_context: str = "", use_context: bool = True, retry_on_error: bool = True) -> Optional[str]:
"""Génère du texte avec l'API PrivateGPT"""
try:
# Préparer le prompt avec le contexte précédent si disponible et demandé
full_prompt = prompt
if previous_context and use_context:
full_prompt += "\n\nContexte précédent (informations des sections déjà générées) :\n" + previous_context
# Définir les paramètres de la requête
system_message = "Vous êtes un assistant stratégique expert chargé de produire des rapports destinés à des décideurs de haut niveau (COMEX, directions risques, stratèges politiques ou industriels). " + \
f"Objectif : Analyser les vulnérabilités systémiques dans une chaîne de valeur numérique selon les données du fichier {input_file.name}. " + \
"Interprétation des indices : " + \
"• IHH (Herfindahl-Hirschmann) : Échelle 0-100, Seuils : <25 = Vert (faible), 25-50 = Orange (modéré), >50 = Rouge (élevée) " + \
"• ISG (Stabilité Géopolitique) : Échelle 0-100, Seuils : <40 = Vert (stable), 40-60 = Orange, >60 = Rouge (instable) " + \
"• ICS (Substituabilité) : Échelle 0-1, 0 = facilement substituable, 1 = impossible " + \
"• IVC (Concurrence) : Échelle 0-150, Seuils : <10 = Faible, 10-50 = Moyenne, >50 = Forte " + \
"Style attendu : Narratif, fluide, impactant, structuré avec paragraphes complets et données chiffrées. " + \
f"Concentrez-vous UNIQUEMENT sur le document principal '{input_file.name}' et sur les sections temporaires préfixées par 'temp_section_{session_uuid}_'. " + \
"IGNOREZ absolument tous les autres documents qui pourraient être présents dans la base de connaissances. " + \
"Les sections précédentes ont été générées et ingérées comme documents temporaires - " + \
"assurez la cohérence avec leur contenu pour maintenir la continuité du rapport."
# Définir les paramètres de la requête
payload = {
"messages": [
{"role": "system", "content": system_message},
{"role": "user", "content": full_prompt}
],
"use_context": True, # Active la recherche RAG dans les documents ingérés
"temperature": 0.2, # Température réduite pour plus de cohérence
"stream": False
}
# Tenter d'ajouter un filtre de contexte (fonctionnalité expérimentale qui peut ne pas être supportée)
try:
# Vérifier si le filtre de contexte est supporté sans faire de requête supplémentaire
filter_metadata = {
"document_name": [input_file.name] + [f.name for f in section_files]
}
payload["filter_metadata"] = filter_metadata
except Exception as e:
print(f"ℹ️ Remarque: Impossible d'appliquer le filtre de contexte: {e}")
# Envoyer la requête
response = requests.post(
f"{API_URL}/chat/completions",
json=payload,
headers={"accept": "application/json"}
)
response.raise_for_status()
# Extraire la réponse générée
result = response.json()
if "choices" in result and len(result["choices"]) > 0:
return result["choices"][0]["message"]["content"]
else:
print("⚠️ Format de réponse inattendu:", json.dumps(result, indent=2))
return None
except requests.RequestException as e:
print(f"❌ Erreur lors de la génération de texte: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Détails: {e.response.text}")
return None
def cleanup_temp_files(temp_file_names: List[str] = None, remove_directory: bool = False, session_id: str = "") -> None:
"""Nettoie les fichiers temporaires et les documents ingérés"""
try:
# Supprimer les fichiers du répertoire temporaire
if TEMP_DIR.exists():
for temp_file in TEMP_DIR.glob("*.md"):
temp_file.unlink()
print(f"🗑️ Fichier temporaire supprimé : {temp_file.name}")
# Supprimer le répertoire s'il est vide et si demandé
if remove_directory and not any(TEMP_DIR.iterdir()):
TEMP_DIR.rmdir()
print(f"🗑️ Répertoire temporaire '{TEMP_DIR}' supprimé")
# Supprimer les documents ingérés via l'API de liste et suppression
try:
# Lister tous les documents ingérés
list_response = requests.get(f"{API_URL}/ingest/list")
if list_response.status_code == 200:
documents_data = list_response.json()
# Format de réponse OpenAI
if "data" in documents_data:
documents = documents_data.get("data", [])
# Format alternatif
else:
documents = documents_data.get("documents", [])
deleted_count = 0
# Parcourir les documents et supprimer ceux qui correspondent à nos fichiers temporaires
for doc in documents:
doc_metadata = doc.get("doc_metadata", {})
file_name = doc_metadata.get("file_name", "") or doc_metadata.get("filename", "")
# Vérifier si c'est un de nos fichiers temporaires ou le fichier d'entrée
is_our_file = False
if temp_file_names and file_name in temp_file_names:
is_our_file = True
elif f"temp_section_{session_uuid}_" in file_name:
is_our_file = True
elif session_id and f"input_{session_id}_" in file_name:
is_our_file = True
if is_our_file:
doc_id = doc.get("doc_id") or doc.get("id")
if doc_id:
delete_response = requests.delete(f"{API_URL}/ingest/{doc_id}")
if delete_response.status_code == 200:
deleted_count += 1
if deleted_count > 0:
print(f"🗑️ {deleted_count} documents supprimés de PrivateGPT")
except Exception as e:
print(f"⚠️ Erreur lors de la suppression des documents ingérés: {e}")
except Exception as e:
print(f"⚠️ Erreur lors du nettoyage des fichiers temporaires: {e}")
def clean_ai_thoughts(text: str) -> str:
"""Nettoie le texte généré en supprimant les 'pensées' de l'IA entre balises """
import re
# Supprimer tout le contenu entre les balises et , y compris les balises
text = re.sub(r'.*?', '', text, flags=re.DOTALL)
# Supprimer les lignes vides multiples
text = re.sub(r'\n{3,}', '\n\n', text)
return text
def main(input_path: str, output_path: str, context_strategy: ContextStrategy = ContextStrategy.FILE, context_length: int = MAX_CONTEXT_LENGTH):
"""Fonction principale qui exécute le processus complet"""
global input_file, section_files, session_uuid # Variables globales pour le filtre de contexte et l'UUID
# Générer un UUID unique pour cette session
session_uuid = str(uuid.uuid4())[:8] # Utiliser les 8 premiers caractères pour plus de concision
print(f"🔑 UUID de session généré: {session_uuid}")
# Vérifier la disponibilité de l'API
if not check_api_availability():
sys.exit(1)
# Convertir les chemins en objets Path (accessibles globalement)
input_file = Path(input_path)
output_file = Path(output_path)
# Ingérer le document principal avec l'UUID de session
if not ingest_document(input_file, session_uuid):
sys.exit(1)
# Récupérer la valeur du délai depuis args
delay = args.delay if 'args' in globals() else 5
# Attendre que l'ingestion soit complètement traitée
print(f"⏳ Attente du traitement de l'ingestion pendant {delay} secondes...")
time.sleep(delay)
print(f"🔧 Stratégie de contexte initiale: {context_strategy.value}, taille max: {context_length} caractères")
# Préparer le répertoire pour les fichiers temporaires
setup_temp_directory()
# Générer chaque section du rapport
step_outputs = []
section_files = [] # Chemins des fichiers temporaires
ingested_file_names = [] # Noms des fichiers ingérés
# Liste des stratégies de fallback dans l'ordre
fallback_strategies = [
ContextStrategy.FILE,
ContextStrategy.TRUNCATE,
ContextStrategy.SUMMARIZE,
ContextStrategy.LATEST,
ContextStrategy.NONE
]
for i, step in enumerate(steps):
print(f"\n🚧 Étape {i+1}/{len(steps)}: {step['title']}")
# Si nous avons des sections précédentes et utilisons la stratégie FILE
if step_outputs and context_strategy == ContextStrategy.FILE:
# Sauvegarder et ingérer toutes les sections précédentes qui ne l'ont pas encore été
for j, section in enumerate(step_outputs):
if j >= len(section_files): # Cette section n'a pas encore été sauvegardée
section_file = save_section_to_file(section, j, session_uuid)
section_files.append(section_file)
# Ingérer le fichier si nous utilisons la stratégie FILE
new_ids = ingest_section_files([section_file])
ingested_section_ids.extend(new_ids)
# Attendre que l'ingestion soit traitée
print(f"⏳ Attente du traitement de l'ingestion des sections précédentes...")
time.sleep(2)
# Essayer chaque stratégie jusqu'à ce qu'une réussisse
output = None
current_strategy_index = fallback_strategies.index(context_strategy)
while output is None and current_strategy_index < len(fallback_strategies):
current_strategy = fallback_strategies[current_strategy_index]
print(f"🔄 Tentative avec la stratégie: {current_strategy.value}")
# Préparer le contexte selon la stratégie actuelle
previous = get_context(step_outputs, current_strategy, context_length) if step_outputs else ""
# Générer le texte
try:
output = generate_text(step["prompt"], previous, current_strategy != ContextStrategy.NONE)
if output is None:
# Passer à la stratégie suivante
current_strategy_index += 1
print(f"⚠️ Échec avec la stratégie {current_strategy.value}, passage à la suivante")
time.sleep(1) # Pause avant nouvelle tentative
except Exception as e:
print(f"⚠️ Erreur lors de la génération avec {current_strategy.value}: {e}")
current_strategy_index += 1
time.sleep(1) # Pause avant nouvelle tentative
if output:
step_outputs.append({"title": step["title"], "output": output})
print(f"✅ Section '{step['title']}' générée ({len(output)} caractères)")
# Si nous sommes en mode FILE, sauvegarder immédiatement cette section
if context_strategy == ContextStrategy.FILE:
section_file = save_section_to_file(step_outputs[-1], len(step_outputs)-1, session_uuid)
section_files.append(section_file)
# Ingérer le fichier
new_file_names = ingest_section_files([section_file])
ingested_file_names.extend(new_file_names)
# Petite pause pour permettre l'indexation
time.sleep(1)
else:
print(f"❌ Échec de la génération pour '{step['title']}' après toutes les stratégies")
# Vérifier si nous avons généré toutes les sections
if len(step_outputs) != len(steps):
print(f"⚠️ Attention: Seules {len(step_outputs)}/{len(steps)} sections ont été générées")
# Écrire le rapport final dans l'ordre souhaité (Synthèse en premier)
# Réordonner les sections pour que la synthèse soit en premier
ordered_outputs = sorted(step_outputs, key=lambda x: 0 if x["title"] == "Synthèse" else
1 if x["title"] == "Analyse détaillée" else
2 if x["title"] == "Interdépendances" else
3 if x["title"] == "Points de vigilance" else 4)
# Assembler le rapport
report_sections = []
for s in ordered_outputs:
# Nettoyer les "pensées" de l'IA dans chaque section
cleaned_output = clean_ai_thoughts(s['output'])
report_sections.append(f"## {s['title']}\n\n{cleaned_output}")
report_text = "# Analyse des vulnérabilités de la chaine de fabrication du numériquee\n\n".join(report_sections)
# Écrire le rapport dans un fichier
try:
output_file.write_text(report_text, encoding="utf-8")
print(f"\n📄 Rapport final généré dans '{output_file}'")
except IOError as e:
print(f"❌ Erreur lors de l'écriture du fichier de sortie: {e}")
# Nettoyer les fichiers temporaires si demandé
if args.clean_temp:
print("\n🧹 Nettoyage des fichiers temporaires et documents ingérés...")
cleanup_temp_files(ingested_file_names, args.remove_temp_dir, session_uuid)
if __name__ == "__main__":
# Définir les arguments en ligne de commande
parser = argparse.ArgumentParser(description="Générer un rapport d'analyse structuré avec PrivateGPT")
parser.add_argument("-i", "--input", type=str, default=DEFAULT_INPUT_PATH,
help=f"Chemin du fichier d'entrée (défaut: {DEFAULT_INPUT_PATH})")
parser.add_argument("-o", "--output", type=str, default=DEFAULT_OUTPUT_PATH,
help=f"Chemin du fichier de sortie (défaut: {DEFAULT_OUTPUT_PATH})")
parser.add_argument("--context", type=str, choices=[s.value for s in ContextStrategy], default=ContextStrategy.FILE.value,
help=f"Stratégie de gestion du contexte (défaut: {ContextStrategy.FILE.value})")
parser.add_argument("--context-length", type=int, default=MAX_CONTEXT_LENGTH,
help=f"Taille maximale du contexte en caractères (défaut: {MAX_CONTEXT_LENGTH})")
parser.add_argument("--delay", type=int, default=5,
help="Délai d'attente en secondes après l'ingestion (défaut: 5)")
parser.add_argument("--clean-temp", action="store_true", default=True,
help="Nettoyer les fichiers temporaires et documents ingérés par le script (défaut: True)")
parser.add_argument("--keep-temp", action="store_true",
help="Conserver les fichiers temporaires (remplace --clean-temp)")
parser.add_argument("--remove-temp-dir", action="store_true",
help="Supprimer le répertoire temporaire après exécution (défaut: False)")
args = parser.parse_args()
# Gérer les options contradictoires
if args.keep_temp:
args.clean_temp = False
# Exécuter le programme principal avec les options configurées
context_strategy = ContextStrategy(args.context)
main(args.input, args.output, context_strategy, args.context_length)