Compare commits
84 Commits
00d5a538b0
...
a9bf92a4bc
| Author | SHA1 | Date | |
|---|---|---|---|
| a9bf92a4bc | |||
| 2846403860 | |||
| f259d6b3e3 | |||
| c24898bf02 | |||
| 359d17f628 | |||
| c0ab1f1591 | |||
| 67182d8b53 | |||
| 8efc016014 | |||
|
|
4d511cbe23 | ||
|
|
35aa7d12fa | ||
|
|
4bb06a4801 | ||
|
|
c55d478660 | ||
|
|
9ca623aef1 | ||
|
|
16b1ad37d7 | ||
|
|
255361e9aa | ||
|
|
69272a44d6 | ||
|
|
d47e8608cc | ||
|
|
8a601aa24a | ||
|
|
c2cf505b48 | ||
|
|
959e2be867 | ||
|
|
c5d854b165 | ||
|
|
95ede9c6f1 | ||
|
|
c4fffb829c | ||
|
|
981c473204 | ||
|
|
5839098db6 | ||
|
|
81f5bb3b66 | ||
|
|
ec00ec3a9b | ||
|
|
c5482c3033 | ||
|
|
4809661b0f | ||
| 813fb5684e | |||
| b2c47048c7 | |||
| 54c6a309e6 | |||
| 952f0dd92d | |||
| d5fffdce14 | |||
| d8bc030a52 | |||
| f8baf851ae | |||
| 747c56f252 | |||
| 9c6c857f28 | |||
| 928a39fd96 | |||
| 32591990fe | |||
| f12bee11b7 | |||
| eaeae5f1f5 | |||
| 4b16c2210e | |||
| 86a902de9d | |||
| 4f61b37db1 | |||
| e26fc3e20d | |||
| 2c4931bdfe | |||
| 282a7ad739 | |||
| 66ce80fe51 | |||
| 70d856c58b | |||
| a3608353a2 | |||
| f8c630cdfe | |||
| 8c99fd2da3 | |||
| be9c3709db | |||
| c56a46545f | |||
| 852b81ba93 | |||
| c1a0a8e072 | |||
| a569c71ad4 | |||
| b206375c6c | |||
| 7d9dccda22 | |||
| 3dc85bd99b | |||
| 50042f6655 | |||
| 89d167a2f8 | |||
| 3f2f13b65f | |||
| f0f87b64f4 | |||
| 03cc42c22d | |||
| 8f8e041c6b | |||
| 96a083fd72 | |||
| 58f7cbf669 | |||
| cc54af71e6 | |||
| 33695092af | |||
| 427c7d26f5 | |||
| b06c6857ba | |||
| 965f4b31cf | |||
| 5b215e5e5f | |||
| 92bfd442c2 | |||
| fc08a00a6c | |||
| cf604957e3 | |||
| 4ae0fbfdb3 | |||
| 9491dd076f | |||
| c9d3c8422a | |||
| 45fbd0f277 | |||
| 39919ca596 | |||
| a79106569f |
12
.gitignore
vendored
12
.gitignore
vendored
@ -6,25 +6,31 @@
|
||||
__pycache__/
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.dot
|
||||
prompt.md
|
||||
.gitignore
|
||||
|
||||
# Ignorer cache et temporaire
|
||||
.cache/
|
||||
*.log
|
||||
*.tmp
|
||||
*.old
|
||||
tempo/
|
||||
tmp/
|
||||
jobs/
|
||||
|
||||
# Ignorer config locale
|
||||
.ropeproject/
|
||||
.streamlit/
|
||||
venv/
|
||||
.venv/
|
||||
Local/
|
||||
HTML/
|
||||
static/
|
||||
Corpus/
|
||||
|
||||
# Ignorer données Fiches (adapté à ton projet)
|
||||
Instructions.md
|
||||
Fiches/
|
||||
HTML/
|
||||
static/Fiches/
|
||||
|
||||
# Autres spécifiques si besoin
|
||||
.DS_Store
|
||||
|
||||
11
.streamlit/config.toml
Normal file
11
.streamlit/config.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[server]
|
||||
enableXsrfProtection = false
|
||||
enableCORS = false
|
||||
enableStaticServing = true
|
||||
|
||||
[client]
|
||||
showErrorDetails = true
|
||||
toolbarMode = "minimal"
|
||||
|
||||
[theme]
|
||||
base = "light"
|
||||
200
IA/00 - fiches_corpus/batch_generate_fiches.py
Normal file
200
IA/00 - fiches_corpus/batch_generate_fiches.py
Normal file
@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
import requests
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
# Import des fonctions de génération
|
||||
from app.fiches.generer import (
|
||||
generer_fiche
|
||||
)
|
||||
from app.fiches.utils.fiche_utils import load_seuils
|
||||
from utils.gitea import charger_arborescence_fiches
|
||||
from config import GITEA_TOKEN, FICHES_CRITICITE
|
||||
|
||||
# Configuration du logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler("batch_generation.log"),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_fiche_type(md_source):
|
||||
"""Extrait le type de fiche depuis le frontmatter YAML."""
|
||||
front_match = re.match(r"(?s)^---\n(.*?)\n---\n", md_source)
|
||||
if not front_match:
|
||||
return "autre"
|
||||
|
||||
context = yaml.safe_load(front_match.group(1))
|
||||
return context.get("type_fiche", "autre")
|
||||
|
||||
def get_indice_court(md_source):
|
||||
"""Extrait l'indice court depuis le frontmatter YAML."""
|
||||
front_match = re.match(r"(?s)^---\n(.*?)\n---\n", md_source)
|
||||
if not front_match:
|
||||
return None
|
||||
|
||||
context = yaml.safe_load(front_match.group(1))
|
||||
return context.get("indice_court")
|
||||
|
||||
def batch_generate_fiches(output_dir="", force_regenerate=False):
|
||||
"""Génère toutes les fiches en batch en suivant un ordre de priorité."""
|
||||
# Assurer que les répertoires de sortie existent
|
||||
if output_dir:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Toujours créer les répertoires nécessaires
|
||||
os.makedirs(os.path.join("Fiches"), exist_ok=True)
|
||||
os.makedirs(os.path.join("HTML"), exist_ok=True)
|
||||
os.makedirs(os.path.join("static", "Fiches"), exist_ok=True)
|
||||
|
||||
# Charger les seuils
|
||||
try:
|
||||
seuils = load_seuils("assets/config.yaml")
|
||||
logger.info("Seuils chargés avec succès")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement des seuils: {e}")
|
||||
return
|
||||
|
||||
# Charger l'arborescence des fiches
|
||||
try:
|
||||
arborescence = charger_arborescence_fiches()
|
||||
logger.info(f"Arborescence chargée: {len(arborescence)} dossiers trouvés")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement de l'arborescence: {e}")
|
||||
return
|
||||
|
||||
# Créer une liste de toutes les fiches avec leurs informations
|
||||
toutes_fiches = []
|
||||
for dossier, fiches in arborescence.items():
|
||||
for fiche in fiches:
|
||||
toutes_fiches.append({
|
||||
"dossier": dossier,
|
||||
"nom": fiche["nom"],
|
||||
"download_url": fiche["download_url"],
|
||||
"type": fiche.get("type", "autre")
|
||||
})
|
||||
|
||||
logger.info(f"Total de {len(toutes_fiches)} fiches à traiter")
|
||||
|
||||
# Organisation des fiches par type pour respecter l'ordre de priorité
|
||||
fiches_par_type = {
|
||||
"criticite": [],
|
||||
"assemblage": [],
|
||||
"fabrication": [],
|
||||
"minerai": [],
|
||||
"autre": []
|
||||
}
|
||||
|
||||
# Télécharger et catégoriser les fiches
|
||||
headers = {"Authorization": f"token {GITEA_TOKEN}"}
|
||||
|
||||
# Création des listes spécifiques pour faciliter le traitement ordonné
|
||||
fiches_criticite = []
|
||||
autres_fiches = []
|
||||
|
||||
# Première étape : identifier les fiches de criticité
|
||||
logger.info("Identification des fiches de criticité...")
|
||||
for fiche in toutes_fiches:
|
||||
# Vérifier si c'est une fiche de criticité par deux méthodes
|
||||
est_criticite = False
|
||||
|
||||
# Méthode 1: vérifier par le nom du fichier
|
||||
if fiche["nom"] in FICHES_CRITICITE:
|
||||
est_criticite = True
|
||||
logger.info(f"Fiche de criticité identifiée par nom: {fiche['nom']}")
|
||||
|
||||
# Méthode 2: vérifier par le dossier
|
||||
elif fiche["dossier"] == "Criticités":
|
||||
est_criticite = True
|
||||
logger.info(f"Fiche de criticité identifiée par dossier: {fiche['nom']}")
|
||||
|
||||
if est_criticite:
|
||||
fiches_criticite.append(fiche)
|
||||
else:
|
||||
autres_fiches.append(fiche)
|
||||
|
||||
# Traiter d'abord les fiches de criticité
|
||||
logger.info(f"Traitement prioritaire des fiches de criticité ({len(fiches_criticite)} fiches)")
|
||||
criticite_count = 0
|
||||
|
||||
for fiche in fiches_criticite:
|
||||
try:
|
||||
# Télécharger la fiche de criticité
|
||||
reponse = requests.get(fiche["download_url"], headers=headers)
|
||||
reponse.raise_for_status()
|
||||
md_source = reponse.text
|
||||
|
||||
# Générer immédiatement la fiche de criticité
|
||||
logger.info(f"Génération prioritaire de la fiche de criticité: {fiche['dossier']}/{fiche['nom']}")
|
||||
generer_fiche(md_source, fiche["dossier"], fiche["nom"], seuils)
|
||||
|
||||
# Ajouter à la liste pour le décompte
|
||||
fiches_par_type["criticite"].append(fiche)
|
||||
criticite_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la génération de la fiche de criticité {fiche['nom']}: {e}")
|
||||
|
||||
logger.info(f"{criticite_count} fiches de criticité générées")
|
||||
|
||||
# Maintenant catégoriser et traiter les fiches restantes (non-criticité)
|
||||
for fiche in autres_fiches:
|
||||
try:
|
||||
# Télécharger le contenu pour déterminer le type
|
||||
reponse = requests.get(fiche["download_url"], headers=headers)
|
||||
reponse.raise_for_status()
|
||||
md_source = reponse.text
|
||||
|
||||
# Déterminer le type
|
||||
type_fiche = get_fiche_type(md_source)
|
||||
if type_fiche in fiches_par_type:
|
||||
fiches_par_type[type_fiche].append({**fiche, "md_source": md_source})
|
||||
else:
|
||||
fiches_par_type["autre"].append({**fiche, "md_source": md_source})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du traitement de {fiche['nom']}: {e}")
|
||||
|
||||
# Ordre de traitement pour les fiches restantes
|
||||
ordre_types = ["assemblage", "fabrication", "minerai", "autre"]
|
||||
|
||||
# Générer les fiches restantes dans l'ordre spécifié
|
||||
for type_fiche in ordre_types:
|
||||
logger.info(f"Traitement des fiches de type '{type_fiche}' ({len(fiches_par_type[type_fiche])} fiches)")
|
||||
|
||||
for fiche in fiches_par_type[type_fiche]:
|
||||
try:
|
||||
md_source = fiche["md_source"]
|
||||
dossier = fiche["dossier"]
|
||||
nom_fichier = fiche["nom"]
|
||||
|
||||
# Vérifier si la génération est nécessaire
|
||||
html_path = os.path.join("HTML", dossier, os.path.splitext(nom_fichier)[0] + ".html")
|
||||
if force_regenerate or not os.path.exists(html_path):
|
||||
logger.info(f"Génération de {dossier}/{nom_fichier}")
|
||||
generer_fiche(md_source, dossier, nom_fichier, seuils)
|
||||
else:
|
||||
logger.info(f"Ignoré (déjà généré): {dossier}/{nom_fichier}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la génération de {fiche['nom']}: {e}")
|
||||
|
||||
logger.info("Génération batch terminée")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Générateur batch de fiches")
|
||||
parser.add_argument("--output", "-o", default="", help="Répertoire de sortie (laissez vide pour utiliser les dossiers de l'application)")
|
||||
parser.add_argument("--force", "-f", action="store_true", help="Forcer la régénération de toutes les fiches")
|
||||
args = parser.parse_args()
|
||||
|
||||
logger.info(f"Démarrage de la génération batch (output: {args.output if args.output else 'dossiers par défaut'}, force: {args.force})")
|
||||
batch_generate_fiches(output_dir=args.output, force_regenerate=args.force)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
96
IA/00 - fiches_corpus/generate_corpus.py
Normal file
96
IA/00 - fiches_corpus/generate_corpus.py
Normal file
@ -0,0 +1,96 @@
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
EXCLUDE_DIRS = {"Local"}
|
||||
MAX_SECTION_LENGTH = 1200 # non utilisé ici car découpe selon présence de ###
|
||||
|
||||
def slugify(text):
|
||||
return re.sub(r'\W+', '-', text.strip()).strip('-').lower()
|
||||
|
||||
def split_markdown_sections_refined(content):
|
||||
lines = content.splitlines()
|
||||
sections = []
|
||||
header_level_2 = None
|
||||
section_lines = []
|
||||
subsections = []
|
||||
current_subsection = None
|
||||
inside_section = False
|
||||
|
||||
for line in lines:
|
||||
if line.startswith("## "):
|
||||
if header_level_2:
|
||||
if current_subsection:
|
||||
subsections.append(current_subsection)
|
||||
sections.append((header_level_2, section_lines, subsections))
|
||||
section_lines, subsections = [], []
|
||||
current_subsection = None
|
||||
header_level_2 = line[3:].strip()
|
||||
inside_section = True
|
||||
elif line.startswith("### ") and inside_section:
|
||||
if current_subsection:
|
||||
subsections.append(current_subsection)
|
||||
current_subsection = (line[4:].strip(), [])
|
||||
elif inside_section:
|
||||
if current_subsection:
|
||||
current_subsection[1].append(line)
|
||||
else:
|
||||
section_lines.append(line)
|
||||
|
||||
if header_level_2:
|
||||
if current_subsection:
|
||||
subsections.append(current_subsection)
|
||||
sections.append((header_level_2, section_lines, subsections))
|
||||
return sections
|
||||
|
||||
def process_markdown_file(md_path, rel_output_dir):
|
||||
with open(md_path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
sections = split_markdown_sections_refined(content)
|
||||
|
||||
for idx, (sec_title, sec_lines, subsections) in enumerate(sections):
|
||||
base_name = f"{idx:02d}-{slugify(sec_title)}"
|
||||
if subsections:
|
||||
sec_dir = rel_output_dir / base_name
|
||||
sec_dir.mkdir(parents=True, exist_ok=True)
|
||||
with open(sec_dir / "_intro.md", "w", encoding="utf-8") as f_out:
|
||||
f_out.write(f"## {sec_title}\n")
|
||||
f_out.write("\n".join(sec_lines).strip())
|
||||
for sub_idx, (sub_title, sub_lines) in enumerate(subsections):
|
||||
sub_name = f"{sub_idx:02d}-{slugify(sub_title)}.md"
|
||||
with open(sec_dir / sub_name, "w", encoding="utf-8") as f_out:
|
||||
f_out.write(f"### {sub_title}\n")
|
||||
f_out.write("\n".join(sub_lines).strip())
|
||||
else:
|
||||
with open(rel_output_dir / f"{base_name}.md", "w", encoding="utf-8") as f_out:
|
||||
f_out.write(f"## {sec_title}\n")
|
||||
f_out.write("\n".join(sec_lines).strip())
|
||||
|
||||
def build_corpus_structure():
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
print(BASE_DIR)
|
||||
SOURCE_DIR = BASE_DIR / "Fiches"
|
||||
DEST_DIR = BASE_DIR / "Corpus"
|
||||
|
||||
if DEST_DIR.exists():
|
||||
shutil.rmtree(DEST_DIR)
|
||||
DEST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for root, _, files in os.walk(SOURCE_DIR):
|
||||
rel_path = Path(root).relative_to(SOURCE_DIR)
|
||||
if any(part in EXCLUDE_DIRS for part in rel_path.parts):
|
||||
continue
|
||||
for file in files:
|
||||
if not file.endswith(".md") or ".md." in file:
|
||||
continue
|
||||
input_file = Path(root) / file
|
||||
subdir = rel_path
|
||||
filename_no_ext = Path(file).stem
|
||||
output_dir = DEST_DIR / subdir / filename_no_ext
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
process_markdown_file(input_file, output_dir)
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_corpus_structure()
|
||||
print("✅ Corpus généré avec succès dans le dossier 'Corpus/'")
|
||||
209
IA/01 - corpus_rapport_factuel/analyze_graph.py
Normal file
209
IA/01 - corpus_rapport_factuel/analyze_graph.py
Normal file
@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script d'analyse de la structure du graphe DOT pour comprendre
|
||||
comment intégrer l'ISG dans le générateur de template.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from networkx.drawing.nx_agraph import read_dot
|
||||
|
||||
# Chemins
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
GRAPH_PATH = BASE_DIR / "graphe.dot"
|
||||
|
||||
def analyze_graph_structure(dot_path):
|
||||
"""Analyse la structure du graphe et affiche ses caractéristiques."""
|
||||
print(f"Analyse du fichier: {dot_path}")
|
||||
|
||||
# Lire le graphe
|
||||
G = read_dot(dot_path)
|
||||
|
||||
# Informations de base
|
||||
print(f"Nombre total de nœuds: {len(G.nodes())}")
|
||||
print(f"Nombre total d'arêtes: {len(G.edges())}")
|
||||
|
||||
# Analyse des attributs des nœuds
|
||||
node_attrs = {}
|
||||
for node, attrs in G.nodes(data=True):
|
||||
for key in attrs:
|
||||
if key not in node_attrs:
|
||||
node_attrs[key] = set()
|
||||
node_attrs[key].add(attrs[key])
|
||||
|
||||
print("\nAttributs des nœuds:")
|
||||
for attr, values in node_attrs.items():
|
||||
print(f"- {attr}: {len(values)} valeurs différentes")
|
||||
if len(values) < 20: # Afficher seulement si le nombre de valeurs est raisonnable
|
||||
print(f" Valeurs: {', '.join(sorted(values))}")
|
||||
|
||||
# Analyse des niveaux (si l'attribut existe)
|
||||
if 'level' in node_attrs:
|
||||
print("\nAnalyse par niveau:")
|
||||
levels = {}
|
||||
for node, attrs in G.nodes(data=True):
|
||||
if 'level' in attrs:
|
||||
level = attrs['level']
|
||||
if level not in levels:
|
||||
levels[level] = []
|
||||
levels[level].append(node)
|
||||
|
||||
for level, nodes in sorted(levels.items()):
|
||||
print(f"- Niveau {level}: {len(nodes)} nœuds")
|
||||
# Afficher quelques exemples
|
||||
if len(nodes) < 5:
|
||||
print(f" Exemples: {', '.join(nodes)}")
|
||||
else:
|
||||
print(f" Exemples: {', '.join(nodes[:3])}... (et {len(nodes)-3} autres)")
|
||||
|
||||
# Analyse des attributs ISG
|
||||
print("\nRecherche des attributs ISG:")
|
||||
isg_nodes = []
|
||||
for node, attrs in G.nodes(data=True):
|
||||
if 'isg' in attrs:
|
||||
isg_nodes.append((node, attrs['isg']))
|
||||
|
||||
if isg_nodes:
|
||||
print(f"- {len(isg_nodes)} nœuds avec attribut ISG")
|
||||
print(" Exemples:")
|
||||
for node, isg in isg_nodes[:5]:
|
||||
print(f" - {node}: ISG = {isg}")
|
||||
else:
|
||||
print("- Aucun nœud avec attribut ISG trouvé")
|
||||
|
||||
# Analyse des connexions pour les nœuds critiques (IHH)
|
||||
print("\nAnalyse des nœuds avec IHH:")
|
||||
ihh_nodes = []
|
||||
for node, attrs in G.nodes(data=True):
|
||||
if 'ihh_pays' in attrs or 'ihh_acteurs' in attrs:
|
||||
ihh_value_pays = attrs.get('ihh_pays', 'N/A')
|
||||
ihh_value_acteurs = attrs.get('ihh_acteurs', 'N/A')
|
||||
ihh_nodes.append((node, ihh_value_pays, ihh_value_acteurs))
|
||||
|
||||
if ihh_nodes:
|
||||
print(f"- {len(ihh_nodes)} nœuds avec attributs IHH")
|
||||
print(" Exemples:")
|
||||
for node, ihh_pays, ihh_acteurs in ihh_nodes[:5]:
|
||||
print(f" - {node}: IHH pays = {ihh_pays}, IHH acteurs = {ihh_acteurs}")
|
||||
|
||||
# Analyser les connexions de ce nœud
|
||||
print(f" Connexions sortantes:")
|
||||
out_edges = list(G.out_edges(node))
|
||||
if out_edges:
|
||||
for i, (_, target) in enumerate(out_edges[:3]):
|
||||
print(f" - Vers {target}")
|
||||
if len(out_edges) > 3:
|
||||
print(f" - ... et {len(out_edges)-3} autres")
|
||||
else:
|
||||
print(" - Aucune connexion sortante")
|
||||
|
||||
print(f" Connexions entrantes:")
|
||||
in_edges = list(G.in_edges(node))
|
||||
if in_edges:
|
||||
for i, (source, _) in enumerate(in_edges[:3]):
|
||||
print(f" - Depuis {source}")
|
||||
if len(in_edges) > 3:
|
||||
print(f" - ... et {len(in_edges)-3} autres")
|
||||
else:
|
||||
print(" - Aucune connexion entrante")
|
||||
else:
|
||||
print("- Aucun nœud avec attributs IHH trouvé")
|
||||
|
||||
# Vérifier si un nœud a un attribut de niveau 99 (ISG supposé)
|
||||
print("\nRecherche des nœuds de niveau 99 (ISG):")
|
||||
level_99_nodes = []
|
||||
for node, attrs in G.nodes(data=True):
|
||||
if attrs.get('level') == '99':
|
||||
level_99_nodes.append(node)
|
||||
|
||||
if level_99_nodes:
|
||||
print(f"- {len(level_99_nodes)} nœuds de niveau 99")
|
||||
print(" Exemples:")
|
||||
for node in level_99_nodes[:5]:
|
||||
print(f" - {node}")
|
||||
# Analyser les connexions de ce nœud
|
||||
print(f" Connexions entrantes:")
|
||||
in_edges = list(G.in_edges(node))
|
||||
if in_edges:
|
||||
for i, (source, _) in enumerate(in_edges[:3]):
|
||||
print(f" - Depuis {source}")
|
||||
if len(in_edges) > 3:
|
||||
print(f" - ... et {len(in_edges)-3} autres")
|
||||
else:
|
||||
print(" - Aucune connexion entrante")
|
||||
else:
|
||||
print("- Aucun nœud de niveau 99 trouvé")
|
||||
|
||||
def check_isg_paths(dot_path):
|
||||
"""Vérifie les chemins entre les nœuds critiques (IHH) et les nœuds ISG."""
|
||||
print("\nAnalyse des chemins entre nœuds IHH et nœuds ISG:")
|
||||
|
||||
# Lire le graphe
|
||||
G = read_dot(dot_path)
|
||||
|
||||
# Identifier les nœuds avec IHH
|
||||
ihh_nodes = []
|
||||
for node, attrs in G.nodes(data=True):
|
||||
ihh_pays = attrs.get('ihh_pays', 0)
|
||||
ihh_acteurs = attrs.get('ihh_acteurs', 0)
|
||||
try:
|
||||
ihh_pays = float(ihh_pays)
|
||||
ihh_acteurs = float(ihh_acteurs)
|
||||
if ihh_pays > 25 or ihh_acteurs > 25: # Seuil critique
|
||||
ihh_nodes.append(node)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if not ihh_nodes:
|
||||
print("- Aucun nœud IHH critique trouvé")
|
||||
return
|
||||
|
||||
print(f"- {len(ihh_nodes)} nœuds IHH critiques identifiés")
|
||||
|
||||
# Pour chaque nœud IHH critique, chercher des chemins vers des nœuds ISG
|
||||
for node in ihh_nodes[:5]: # Limiter à 5 exemples
|
||||
print(f"\n Analyse des chemins pour {node}:")
|
||||
|
||||
# Analyser les voisins directs
|
||||
successors = list(G.successors(node))
|
||||
print(f" - {len(successors)} successeurs directs")
|
||||
|
||||
if successors:
|
||||
for succ in successors[:3]:
|
||||
print(f" - Vers {succ}")
|
||||
|
||||
# Vérifier les attributs de ce successeur
|
||||
succ_attrs = G.nodes[succ]
|
||||
print(f" Attributs: {', '.join(f'{k}={v}' for k, v in succ_attrs.items() if k in ['level', 'isg'])}")
|
||||
|
||||
# Chercher les successeurs de niveau 2
|
||||
succ2 = list(G.successors(succ))
|
||||
print(f" {len(succ2)} successeurs de niveau 2")
|
||||
|
||||
if succ2:
|
||||
for s2 in succ2[:2]:
|
||||
print(f" - Vers {s2}")
|
||||
s2_attrs = G.nodes[s2]
|
||||
print(f" Attributs: {', '.join(f'{k}={v}' for k, v in s2_attrs.items() if k in ['level', 'isg'])}")
|
||||
|
||||
# Chercher encore plus loin si nécessaire
|
||||
succ3 = list(G.successors(s2))
|
||||
print(f" {len(succ3)} successeurs de niveau 3")
|
||||
|
||||
if succ3:
|
||||
for s3 in succ3[:2]:
|
||||
print(f" - Vers {s3}")
|
||||
s3_attrs = G.nodes[s3]
|
||||
print(f" Attributs: {', '.join(f'{k}={v}' for k, v in s3_attrs.items() if k in ['level', 'isg'])}")
|
||||
|
||||
def main():
|
||||
"""Fonction principale."""
|
||||
print("=== Analyse de la structure du graphe ===")
|
||||
analyze_graph_structure(GRAPH_PATH)
|
||||
|
||||
print("\n=== Analyse des chemins entre nœuds critiques et ISG ===")
|
||||
check_isg_paths(GRAPH_PATH)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
146
IA/01 - corpus_rapport_factuel/check_paths.py
Normal file
146
IA/01 - corpus_rapport_factuel/check_paths.py
Normal file
@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
def extract_paths(file_path):
|
||||
"""Extrait tous les chemins du fichier rapport_template.md"""
|
||||
paths = []
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
# Extraire les lignes qui commencent par "Corpus/"
|
||||
if line.strip().startswith("Corpus/"):
|
||||
paths.append(line.strip())
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la lecture du fichier {file_path}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
return paths
|
||||
|
||||
def check_paths(paths, base_dir):
|
||||
"""Vérifie si les chemins existent dans le système de fichiers"""
|
||||
results = {
|
||||
"existing": [],
|
||||
"missing": [],
|
||||
"problematic": [] # Chemins qui pourraient nécessiter des corrections
|
||||
}
|
||||
|
||||
for path in paths:
|
||||
# Vérifier si le chemin est absolu ou relatif
|
||||
abs_path = os.path.join(base_dir, path)
|
||||
|
||||
if os.path.exists(abs_path):
|
||||
results["existing"].append(path)
|
||||
else:
|
||||
# Essayer de détecter des problèmes potentiels
|
||||
problem_detected = False
|
||||
|
||||
# Vérifier les chemins avec "Fiche minerai" ou "Fiche fabrication"
|
||||
if "Fiche minerai" in path or "Fiche fabrication" in path:
|
||||
# Problème courant: mauvaise casse ou absence du mot "minerai"
|
||||
path_lower = path.lower()
|
||||
if "minerai" not in path_lower and "/minerai/" in path_lower:
|
||||
corrected_path = path.replace("/Fiche ", "/Fiche minerai ")
|
||||
if os.path.exists(os.path.join(base_dir, corrected_path)):
|
||||
results["problematic"].append((path, corrected_path, "Mot 'minerai' manquant"))
|
||||
problem_detected = True
|
||||
|
||||
# Vérifier les chemins SSD
|
||||
if "SSD25" in path:
|
||||
corrected_path = path.replace("SSD25", "SSD 2.5")
|
||||
if os.path.exists(os.path.join(base_dir, corrected_path)):
|
||||
results["problematic"].append((path, corrected_path, "Format 'SSD25' au lieu de 'SSD 2.5'"))
|
||||
problem_detected = True
|
||||
|
||||
# Si aucun problème spécifique n'a été détecté, marquer comme manquant
|
||||
if not problem_detected:
|
||||
results["missing"].append(path)
|
||||
|
||||
return results
|
||||
|
||||
def find_similar_paths(missing_path, base_dir):
|
||||
"""Essaie de trouver des chemins similaires pour aider à diagnostiquer le problème"""
|
||||
missing_parts = missing_path.split('/')
|
||||
similar_paths = []
|
||||
|
||||
# Rechercher dans les sous-répertoires correspondants
|
||||
search_dir = os.path.join(base_dir, *missing_parts[:-1])
|
||||
if os.path.exists(search_dir):
|
||||
for file in os.listdir(search_dir):
|
||||
if file.endswith('.md'):
|
||||
similar_path = os.path.join(search_dir, file).replace(base_dir + '/', '')
|
||||
similar_paths.append(similar_path)
|
||||
|
||||
# Si aucun chemin similaire n'est trouvé, remonter d'un niveau
|
||||
if not similar_paths and len(missing_parts) > 2:
|
||||
parent_dir = os.path.join(base_dir, *missing_parts[:-2])
|
||||
if os.path.exists(parent_dir):
|
||||
for dir_name in os.listdir(parent_dir):
|
||||
if dir_name.lower() in missing_parts[-2].lower():
|
||||
dir_path = os.path.join(parent_dir, dir_name)
|
||||
if os.path.isdir(dir_path):
|
||||
for file in os.listdir(dir_path):
|
||||
if file.endswith('.md'):
|
||||
similar_path = os.path.join(dir_path, file).replace(base_dir + '/', '')
|
||||
similar_paths.append(similar_path)
|
||||
|
||||
return similar_paths
|
||||
|
||||
def main():
|
||||
# Vérifier que nous sommes dans le bon répertoire
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
base_dir = script_dir
|
||||
|
||||
# Chemin vers le rapport_template.md
|
||||
template_path = os.path.join(base_dir, "Corpus", "rapport_template.md")
|
||||
|
||||
if not os.path.exists(template_path):
|
||||
print(f"Erreur: Le fichier {template_path} n'existe pas.")
|
||||
sys.exit(1)
|
||||
|
||||
print("=== Vérification des chemins dans rapport_template.md ===")
|
||||
|
||||
# Extraire les chemins
|
||||
paths = extract_paths(template_path)
|
||||
print(f"Nombre total de chemins trouvés: {len(paths)}")
|
||||
|
||||
# Vérifier les chemins
|
||||
results = check_paths(paths, base_dir)
|
||||
|
||||
# Afficher les résultats
|
||||
print("\n=== Résultats ===")
|
||||
print(f"Chemins existants: {len(results['existing'])}")
|
||||
print(f"Chemins manquants: {len(results['missing'])}")
|
||||
print(f"Chemins problématiques: {len(results['problematic'])}")
|
||||
|
||||
# Afficher les chemins manquants
|
||||
if results["missing"]:
|
||||
print("\n=== Chemins manquants ===")
|
||||
for path in results["missing"]:
|
||||
print(f"- {path}")
|
||||
similar = find_similar_paths(path, base_dir)
|
||||
if similar:
|
||||
print(" Chemins similaires trouvés:")
|
||||
for sim_path in similar[:3]: # Limiter à 3 suggestions
|
||||
print(f" * {sim_path}")
|
||||
|
||||
# Afficher les chemins problématiques avec suggestions
|
||||
if results["problematic"]:
|
||||
print("\n=== Chemins problématiques ===")
|
||||
for orig, corrected, reason in results["problematic"]:
|
||||
print(f"- {orig}")
|
||||
print(f" Suggestion: {corrected}")
|
||||
print(f" Raison: {reason}")
|
||||
|
||||
# Résumé
|
||||
if not results["missing"] and not results["problematic"]:
|
||||
print("\nTous les chemins dans le rapport sont valides !")
|
||||
else:
|
||||
print("\nDes chemins problématiques ont été détectés. Veuillez corriger les erreurs.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1267
IA/01 - corpus_rapport_factuel/generate_template.py
Normal file
1267
IA/01 - corpus_rapport_factuel/generate_template.py
Normal file
File diff suppressed because it is too large
Load Diff
147
IA/01 - corpus_rapport_factuel/replace_paths.py
Normal file
147
IA/01 - corpus_rapport_factuel/replace_paths.py
Normal file
@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script pour remplacer les références de chemins dans le rapport par le contenu des fichiers.
|
||||
Ajuste automatiquement les niveaux de titres pour maintenir la hiérarchie.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# Chemins de base
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
BASE_DIR = BASE_DIR / ".."
|
||||
CORPUS_DIR = BASE_DIR / "Corpus"
|
||||
INPUT_PATH = CORPUS_DIR / "rapport_template.md"
|
||||
OUTPUT_PATH = CORPUS_DIR / "rapport_final.md"
|
||||
|
||||
def determine_heading_level(line):
|
||||
"""Détermine le niveau de titre d'une ligne."""
|
||||
match = re.match(r'^(#+)\s+', line)
|
||||
if match:
|
||||
return len(match.group(1))
|
||||
return 0
|
||||
|
||||
def determine_parent_level(lines, current_index):
|
||||
"""Détermine le niveau de titre parent pour une ligne donnée."""
|
||||
# Remonter dans les lignes précédentes pour trouver le titre parent
|
||||
for i in range(current_index - 1, -1, -1):
|
||||
level = determine_heading_level(lines[i])
|
||||
if level > 0:
|
||||
return level
|
||||
return 0
|
||||
|
||||
def adjust_heading_levels(content, parent_level, is_intro_file=False, is_ivc_section=False):
|
||||
"""Ajuste les niveaux de titres dans le contenu pour s'adapter à la hiérarchie."""
|
||||
lines = content.split('\n')
|
||||
|
||||
# Si le contenu est vide, retourner une chaîne vide
|
||||
if not lines:
|
||||
return ""
|
||||
|
||||
# Déterminer le niveau minimum de titre dans le contenu original
|
||||
min_level = 10
|
||||
for line in lines:
|
||||
level = determine_heading_level(line)
|
||||
if level > 0 and level < min_level:
|
||||
min_level = level
|
||||
|
||||
# Si aucun titre trouvé, simplement supprimer la première ligne si nécessaire
|
||||
if min_level == 10:
|
||||
if not is_intro_file and not is_ivc_section and lines:
|
||||
return '\n'.join(lines[1:])
|
||||
return content
|
||||
|
||||
# Traitement spécial pour les fichiers IVC et intro
|
||||
if is_ivc_section or is_intro_file:
|
||||
adjusted_lines = []
|
||||
# Pour les fichiers IVC ou intro, on garde toutes les lignes mais on ajuste les niveaux des titres
|
||||
for line in lines:
|
||||
level = determine_heading_level(line)
|
||||
if level > 0:
|
||||
# Nouveau niveau = niveau parent + 1 + (niveau actuel - min_level)
|
||||
new_level = parent_level + 1 + (level - min_level)
|
||||
# S'assurer que le niveau ne dépasse pas 6 (limite en markdown)
|
||||
new_level = min(new_level, 6)
|
||||
line = re.sub(r'^#+\s+', '#' * new_level + ' ', line)
|
||||
adjusted_lines.append(line)
|
||||
else:
|
||||
# Pour les fichiers standards, on supprime la première ligne
|
||||
lines = lines[1:]
|
||||
adjusted_lines = []
|
||||
# Ajuster les niveaux de titres pour les lignes restantes
|
||||
for line in lines:
|
||||
level = determine_heading_level(line)
|
||||
if level > 0:
|
||||
# Nouveau niveau = niveau parent + 1 + (niveau actuel - min_level)
|
||||
new_level = parent_level + 1 + (level - min_level)
|
||||
# S'assurer que le niveau ne dépasse pas 6 (limite en markdown)
|
||||
new_level = min(new_level, 6)
|
||||
line = re.sub(r'^#+\s+', '#' * new_level + ' ', line)
|
||||
adjusted_lines.append(line)
|
||||
|
||||
return '\n'.join(adjusted_lines)
|
||||
|
||||
def process_report():
|
||||
"""Traite le rapport pour remplacer les chemins par le contenu."""
|
||||
if not os.path.exists(INPUT_PATH):
|
||||
print(f"Fichier d'entrée introuvable: {INPUT_PATH}")
|
||||
return
|
||||
|
||||
# Lire le rapport
|
||||
with open(INPUT_PATH, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
output_lines = []
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i].strip()
|
||||
|
||||
# Vérifier si la ligne est un chemin
|
||||
if line.startswith('Corpus/'):
|
||||
path = line
|
||||
full_path = BASE_DIR / path
|
||||
|
||||
# Déterminer le niveau de titre parent
|
||||
parent_level = determine_parent_level(lines, i)
|
||||
|
||||
try:
|
||||
# Lire le contenu du fichier
|
||||
if os.path.exists(full_path):
|
||||
# Vérifier si c'est un fichier _intro.md
|
||||
is_intro_file = os.path.basename(full_path) == "_intro.md"
|
||||
|
||||
# Vérifier si c'est une section d'Indice de Vulnérabilité de Concurrence
|
||||
is_ivc_section = "Vulnérabilité de Concurrence" in line or "/ivc-" in path.lower() or "/fiche technique ivc/" in path.lower()
|
||||
|
||||
with open(full_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Ajuster les niveaux de titres
|
||||
adjusted_content = adjust_heading_levels(content, parent_level, is_intro_file, is_ivc_section)
|
||||
|
||||
# Ajouter le contenu ajusté
|
||||
output_lines.append(f"<!-- Contenu du fichier {path} -->")
|
||||
output_lines.append(adjusted_content)
|
||||
output_lines.append(f"<!-- Fin du contenu de {path} -->")
|
||||
else:
|
||||
output_lines.append(f"<!-- Fichier non trouvé: {path} -->")
|
||||
output_lines.append(line)
|
||||
except Exception as e:
|
||||
output_lines.append(f"<!-- Erreur lors de la lecture du fichier {path}: {str(e)} -->")
|
||||
output_lines.append(line)
|
||||
else:
|
||||
# Conserver la ligne telle quelle
|
||||
output_lines.append(line)
|
||||
|
||||
i += 1
|
||||
|
||||
# Écrire le rapport final
|
||||
with open(OUTPUT_PATH, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(output_lines))
|
||||
|
||||
print(f"Rapport final généré: {OUTPUT_PATH}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
process_report()
|
||||
90
IA/02 - injection_fiches/README.md
Normal file
90
IA/02 - injection_fiches/README.md
Normal file
@ -0,0 +1,90 @@
|
||||
# Script d'Injection Automatique pour PrivateGPT
|
||||
|
||||
Ce script permet d'automatiser l'injection de documents dans PrivateGPT à partir d'un répertoire local. Au lieu d'utiliser l'interface utilisateur pour télécharger les fichiers un par un, vous pouvez injecter un dossier entier en une seule commande.
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Python 3.7 ou supérieur
|
||||
- PrivateGPT installé et fonctionnel sous Docker
|
||||
- Accès à l'API REST de PrivateGPT (port 8001 par défaut)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clonez ce dépôt ou téléchargez les fichiers dans un dossier
|
||||
|
||||
2. Installez les dépendances nécessaires :
|
||||
```bash
|
||||
pip install requests
|
||||
```
|
||||
|
||||
## Utilisation
|
||||
|
||||
Le script s'utilise en ligne de commande avec différentes options :
|
||||
|
||||
```bash
|
||||
python auto_ingest.py -d REPERTOIRE [-u URL] [-r] [-t THREADS] [--retry RETRY] [--retry-delay RETRY_DELAY] [--timeout TIMEOUT] [--extensions EXT1 EXT2 ...]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
- `-d, --directory` : Chemin du répertoire contenant les fichiers à injecter (obligatoire)
|
||||
- `-u, --url` : URL de l'API PrivateGPT (défaut: http://localhost:8001)
|
||||
- `-r, --recursive` : Parcourir récursivement les sous-répertoires
|
||||
- `-t, --threads` : Nombre de threads pour les injections parallèles (défaut: 5)
|
||||
- `--retry` : Nombre de tentatives en cas d'échec (défaut: 3)
|
||||
- `--retry-delay` : Délai entre les tentatives en secondes (défaut: 5)
|
||||
- `--timeout` : Délai d'attente pour chaque requête en secondes (défaut: 300)
|
||||
- `--extensions` : Liste d'extensions spécifiques à injecter (ex: pdf txt)
|
||||
|
||||
## Exemples d'utilisation
|
||||
|
||||
### Injection simple d'un répertoire
|
||||
|
||||
```bash
|
||||
python auto_ingest.py -d /chemin/vers/documents
|
||||
```
|
||||
|
||||
### Injection récursive avec extensions spécifiques
|
||||
|
||||
```bash
|
||||
python auto_ingest.py -d /chemin/vers/documents -r --extensions pdf docx txt
|
||||
```
|
||||
|
||||
### Injection avec paramètres avancés
|
||||
|
||||
```bash
|
||||
python auto_ingest.py -d /chemin/vers/documents -r -t 10 --timeout 600 --retry 5
|
||||
```
|
||||
|
||||
## Formats de fichiers supportés
|
||||
|
||||
Par défaut, le script reconnaît et traite les formats suivants :
|
||||
- PDF (.pdf)
|
||||
- Documents texte (.txt, .md)
|
||||
- Documents Microsoft Office (.doc, .docx, .ppt, .pptx, .xls, .xlsx)
|
||||
- CSV (.csv)
|
||||
- EPUB (.epub)
|
||||
- HTML (.html, .htm)
|
||||
|
||||
## Résolution des problèmes
|
||||
|
||||
### Erreur de connexion
|
||||
|
||||
Si vous obtenez des erreurs de connexion, vérifiez que :
|
||||
1. PrivateGPT est bien en cours d'exécution
|
||||
2. L'URL est correcte (par défaut: http://localhost:8001)
|
||||
3. Le port 8001 est accessible et n'est pas bloqué par un pare-feu
|
||||
|
||||
### Erreurs d'injection
|
||||
|
||||
- Si un fichier spécifique ne peut pas être injecté, vérifiez qu'il est d'un format supporté par PrivateGPT
|
||||
- Pour les fichiers volumineux, vous pouvez augmenter la valeur de `--timeout`
|
||||
- En cas d'erreurs répétées, augmentez les valeurs de `--retry` et `--retry-delay`
|
||||
|
||||
## Logs
|
||||
|
||||
Le script génère des logs dans :
|
||||
- La console (stdout)
|
||||
- Un fichier pgpt_auto_ingest.log dans le répertoire courant
|
||||
|
||||
Ces logs contiennent des informations détaillées sur le processus d'injection.
|
||||
254
IA/02 - injection_fiches/auto_ingest.py
Executable file
254
IA/02 - injection_fiches/auto_ingest.py
Executable file
@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
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 os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
import logging
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Tuple, Set
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
# 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"
|
||||
else:
|
||||
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)
|
||||
90
IA/02 - injection_fiches/auto_ingest.sh
Executable file
90
IA/02 - injection_fiches/auto_ingest.sh
Executable file
@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
# Script d'exécution pour auto_ingest.py
|
||||
|
||||
# Vérification des dépendances
|
||||
check_dependencies() {
|
||||
# Vérifier Python
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "Erreur: Python 3 n'est pas installé ou n'est pas dans le PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Vérifier pip et requests
|
||||
if ! python3 -c "import requests" &> /dev/null; then
|
||||
echo "Installation de la bibliothèque requests..."
|
||||
pip3 install requests
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Erreur: Impossible d'installer la bibliothèque requests"
|
||||
echo "Exécutez manuellement: pip3 install requests"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Fonction d'aide
|
||||
show_help() {
|
||||
echo "Script d'injection automatique pour PrivateGPT"
|
||||
echo ""
|
||||
echo "Usage: $0 [options] -d RÉPERTOIRE"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -d, --directory RÉPERTOIRE Répertoire contenant les fichiers à injecter (obligatoire)"
|
||||
echo " -u, --url URL URL de l'API PrivateGPT (défaut: http://localhost:8001)"
|
||||
echo " -r, --recursive Parcourir récursivement les sous-répertoires"
|
||||
echo " -t, --threads N Nombre de threads pour les injections parallèles (défaut: 5)"
|
||||
echo " --retry N Nombre de tentatives en cas d'échec (défaut: 3)"
|
||||
echo " --retry-delay N Délai entre les tentatives en secondes (défaut: 5)"
|
||||
echo " --timeout N Délai d'attente pour chaque requête en secondes (défaut: 300)"
|
||||
echo " --extensions EXT1 EXT2 ... Liste d'extensions spécifiques à injecter"
|
||||
echo " -h, --help Afficher cette aide"
|
||||
echo ""
|
||||
echo "Exemple: $0 -d /documents -r --extensions pdf docx"
|
||||
}
|
||||
|
||||
# Vérifier si aucun argument n'est fourni
|
||||
if [ $# -eq 0 ]; then
|
||||
show_help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Vérifier l'argument d'aide
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Vérifier la présence de l'argument obligatoire (-d ou --directory)
|
||||
directory_specified=false
|
||||
for ((i=1; i<=$#; i++)); do
|
||||
if [ "${!i}" = "-d" ] || [ "${!i}" = "--directory" ]; then
|
||||
directory_specified=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$directory_specified" = false ]; then
|
||||
echo "Erreur: L'option -d/--directory est obligatoire"
|
||||
echo "Utilisez -h ou --help pour afficher l'aide"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Vérifier les dépendances
|
||||
check_dependencies
|
||||
|
||||
# Chemin au script Python
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PYTHON_SCRIPT="${SCRIPT_DIR}/auto_ingest.py"
|
||||
|
||||
# Vérifier que le script Python existe
|
||||
if [ ! -f "$PYTHON_SCRIPT" ]; then
|
||||
echo "Erreur: Le script auto_ingest.py n'existe pas dans $SCRIPT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Rendre le script Python exécutable
|
||||
chmod +x "$PYTHON_SCRIPT"
|
||||
|
||||
# Exécuter le script Python avec tous les arguments
|
||||
python3 "$PYTHON_SCRIPT" "$@"
|
||||
40
IA/02 - injection_fiches/docker-compose.yml.example
Normal file
40
IA/02 - injection_fiches/docker-compose.yml.example
Normal file
@ -0,0 +1,40 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
privategpt:
|
||||
image: ghcr.io/zylon-ai/private-gpt:latest
|
||||
container_name: privategpt
|
||||
ports:
|
||||
- "8001:8001"
|
||||
environment:
|
||||
- PGPT_PROFILES=local
|
||||
# Décommentez et modifiez ces variables si vous voulez utiliser un modèle différent
|
||||
# - PGPT_SETTINGS_LLMS_DEFAULT__MODEL=/models/custom-model.gguf
|
||||
# - PGPT_SETTINGS_EMBEDDING_DEFAULT__MODEL=/models/custom-embedding-model
|
||||
volumes:
|
||||
# Volume persistant pour les données
|
||||
- privategpt-data:/app/local_data
|
||||
# Montage du répertoire d'auto-injection
|
||||
- ./documents_to_ingest:/app/documents_to_ingest
|
||||
# Montage des modèles personnalisés (décommentez si nécessaire)
|
||||
# - ./custom_models:/app/models
|
||||
restart: unless-stopped
|
||||
# Décommentez ces lignes si vous avez un GPU NVIDIA
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: 1
|
||||
# capabilities: [gpu]
|
||||
|
||||
volumes:
|
||||
privategpt-data:
|
||||
name: privategpt-data
|
||||
|
||||
# Instructions d'utilisation:
|
||||
# 1. Copiez ce fichier sous le nom "docker-compose.yml"
|
||||
# 2. Créez un répertoire "documents_to_ingest" à côté du fichier docker-compose.yml
|
||||
# 3. Placez vos documents à injecter dans ce répertoire
|
||||
# 4. Lancez avec la commande: docker-compose up -d
|
||||
# 5. Utilisez le script d'injection: ./auto_ingest.sh -d documents_to_ingest -u http://localhost:8001
|
||||
224
IA/02 - injection_fiches/nettoyer_pgpt.py
Normal file
224
IA/02 - injection_fiches/nettoyer_pgpt.py
Normal file
@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script de nettoyage pour PrivateGPT
|
||||
|
||||
Ce script permet de lister et supprimer les documents ingérés dans PrivateGPT.
|
||||
Options:
|
||||
- Lister tous les documents
|
||||
- Supprimer des documents par préfixe (ex: "temp_section_")
|
||||
- Supprimer des documents par motif
|
||||
- Supprimer tous les documents
|
||||
|
||||
Usage:
|
||||
python nettoyer_pgpt.py --list
|
||||
python nettoyer_pgpt.py --delete-prefix "temp_section_"
|
||||
python nettoyer_pgpt.py --delete-pattern "rapport_.*\.md"
|
||||
python nettoyer_pgpt.py --delete-all
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import requests
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
# Configuration de l'API PrivateGPT
|
||||
PGPT_URL = "http://127.0.0.1:8001"
|
||||
API_URL = f"{PGPT_URL}/v1"
|
||||
|
||||
|
||||
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 list_documents() -> List[Dict[str, Any]]:
|
||||
"""Liste tous les documents ingérés et renvoie la liste des métadonnées"""
|
||||
try:
|
||||
# Récupérer la liste des documents
|
||||
response = requests.get(f"{API_URL}/ingest/list")
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Format de réponse OpenAI
|
||||
if "data" in data:
|
||||
documents = data.get("data", [])
|
||||
# Format alternatif
|
||||
else:
|
||||
documents = data.get("documents", [])
|
||||
|
||||
# Construire une liste normalisée des documents
|
||||
normalized_docs = []
|
||||
for doc in documents:
|
||||
doc_id = doc.get("doc_id") or doc.get("id")
|
||||
metadata = doc.get("doc_metadata", {})
|
||||
filename = metadata.get("file_name") or metadata.get("filename", "Inconnu")
|
||||
|
||||
normalized_docs.append({
|
||||
"id": doc_id,
|
||||
"filename": filename,
|
||||
"metadata": metadata
|
||||
})
|
||||
|
||||
return normalized_docs
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors de la récupération des documents: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def print_documents(documents: List[Dict[str, Any]]) -> None:
|
||||
"""Affiche la liste des documents de façon lisible"""
|
||||
if not documents:
|
||||
print("📋 Aucun document trouvé dans PrivateGPT")
|
||||
return
|
||||
|
||||
print(f"📋 {len(documents)} documents trouvés dans PrivateGPT:")
|
||||
|
||||
# Regrouper par nom de fichier pour un affichage plus compact
|
||||
files_grouped = {}
|
||||
for doc in documents:
|
||||
filename = doc["filename"]
|
||||
if filename not in files_grouped:
|
||||
files_grouped[filename] = []
|
||||
files_grouped[filename].append(doc["id"])
|
||||
|
||||
# Afficher les résultats groupés
|
||||
for i, (filename, ids) in enumerate(files_grouped.items(), 1):
|
||||
print(f"{i}. {filename} ({len(ids)} chunks)")
|
||||
if args.verbose:
|
||||
for j, doc_id in enumerate(ids, 1):
|
||||
print(f" {j}. ID: {doc_id}")
|
||||
|
||||
|
||||
def delete_document(doc_id: str) -> bool:
|
||||
"""Supprime un document par son ID"""
|
||||
try:
|
||||
response = requests.delete(f"{API_URL}/ingest/{doc_id}")
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ Échec de la suppression de l'ID {doc_id}: Code {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors de la suppression de l'ID {doc_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def delete_documents_by_criteria(documents: List[Dict[str, Any]],
|
||||
prefix: Optional[str] = None,
|
||||
pattern: Optional[str] = None,
|
||||
delete_all: bool = False) -> int:
|
||||
"""
|
||||
Supprime des documents selon différents critères
|
||||
Retourne le nombre de documents supprimés
|
||||
"""
|
||||
if not documents:
|
||||
print("❌ Aucun document à supprimer")
|
||||
return 0
|
||||
|
||||
if not (prefix or pattern or delete_all):
|
||||
print("❌ Aucun critère de suppression spécifié")
|
||||
return 0
|
||||
|
||||
# Comptage des suppressions réussies
|
||||
success_count = 0
|
||||
|
||||
# Filtrer les documents à supprimer
|
||||
docs_to_delete = []
|
||||
|
||||
if delete_all:
|
||||
docs_to_delete = documents
|
||||
print(f"🗑️ Suppression de tous les documents ({len(documents)} chunks)...")
|
||||
elif prefix:
|
||||
docs_to_delete = [doc for doc in documents if doc["filename"].startswith(prefix)]
|
||||
print(f"🗑️ Suppression des documents dont le nom commence par '{prefix}' ({len(docs_to_delete)} chunks)...")
|
||||
elif pattern:
|
||||
try:
|
||||
regex = re.compile(pattern)
|
||||
docs_to_delete = [doc for doc in documents if regex.search(doc["filename"])]
|
||||
print(f"🗑️ Suppression des documents correspondant au motif '{pattern}' ({len(docs_to_delete)} chunks)...")
|
||||
except re.error as e:
|
||||
print(f"❌ Expression régulière invalide: {e}")
|
||||
return 0
|
||||
|
||||
# Demander confirmation si beaucoup de documents
|
||||
if len(docs_to_delete) > 5 and not args.force:
|
||||
confirm = input(f"⚠️ Vous êtes sur le point de supprimer {len(docs_to_delete)} chunks. Confirmer ? (o/N) ")
|
||||
if confirm.lower() != 'o':
|
||||
print("❌ Opération annulée")
|
||||
return 0
|
||||
|
||||
# Supprimer les documents
|
||||
for doc in docs_to_delete:
|
||||
if delete_document(doc["id"]):
|
||||
success_count += 1
|
||||
if args.verbose:
|
||||
print(f"✅ Document supprimé: {doc['filename']} (ID: {doc['id']})")
|
||||
|
||||
# Petite pause pour éviter de surcharger l'API
|
||||
time.sleep(0.1)
|
||||
|
||||
print(f"✅ {success_count}/{len(docs_to_delete)} documents supprimés avec succès")
|
||||
return success_count
|
||||
|
||||
|
||||
def generate_unique_prefix() -> str:
|
||||
"""Génère un préfixe unique basé sur un UUID pour différencier les fichiers temporaires"""
|
||||
unique_id = str(uuid.uuid4())[:8] # Prendre les 8 premiers caractères de l'UUID
|
||||
return f"temp_{unique_id}_"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Utilitaire de nettoyage pour PrivateGPT")
|
||||
|
||||
# Options principales
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("--list", action="store_true", help="Lister tous les documents ingérés")
|
||||
group.add_argument("--delete-prefix", type=str, help="Supprimer les documents dont le nom commence par PREFIX")
|
||||
group.add_argument("--delete-pattern", type=str, help="Supprimer les documents dont le nom correspond au motif PATTERN (regex)")
|
||||
group.add_argument("--delete-all", action="store_true", help="Supprimer tous les documents (⚠️ DANGER)")
|
||||
group.add_argument("--generate-prefix", action="store_true", help="Générer un préfixe unique pour les fichiers temporaires")
|
||||
|
||||
# Options additionnelles
|
||||
parser.add_argument("--force", action="store_true", help="Ne pas demander de confirmation")
|
||||
parser.add_argument("--verbose", action="store_true", help="Afficher plus de détails")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Vérifier la disponibilité de l'API
|
||||
if not check_api_availability():
|
||||
sys.exit(1)
|
||||
|
||||
# Générer un préfixe unique
|
||||
if args.generate_prefix:
|
||||
unique_prefix = generate_unique_prefix()
|
||||
print(f"🔑 Préfixe unique généré: {unique_prefix}")
|
||||
print(f"Utilisez ce préfixe pour les fichiers temporaires de votre script.")
|
||||
sys.exit(0)
|
||||
|
||||
# Récupérer la liste des documents
|
||||
documents = list_documents()
|
||||
|
||||
# Traiter selon l'option choisie
|
||||
if args.list:
|
||||
print_documents(documents)
|
||||
elif args.delete_prefix:
|
||||
delete_documents_by_criteria(documents, prefix=args.delete_prefix)
|
||||
elif args.delete_pattern:
|
||||
delete_documents_by_criteria(documents, pattern=args.delete_pattern)
|
||||
elif args.delete_all:
|
||||
delete_documents_by_criteria(documents, delete_all=True)
|
||||
263
IA/02 - injection_fiches/watch_directory.py
Executable file
263
IA/02 - injection_fiches/watch_directory.py
Executable file
@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script de surveillance de répertoire pour l'injection automatique dans PrivateGPT
|
||||
|
||||
Ce script surveille un répertoire et injecte automatiquement les nouveaux fichiers
|
||||
dans PrivateGPT dès qu'ils sont ajoutés ou modifiés.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Set, Dict, List
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler, FileCreatedEvent, FileModifiedEvent
|
||||
except ImportError:
|
||||
print("Bibliothèque 'watchdog' non installée. Installation en cours...")
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "watchdog"])
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler, FileCreatedEvent, FileModifiedEvent
|
||||
|
||||
# Configuration du logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler("pgpt_watch_directory.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'
|
||||
}
|
||||
|
||||
class DocumentHandler(FileSystemEventHandler):
|
||||
"""Gestionnaire d'événements pour les fichiers de documents."""
|
||||
|
||||
def __init__(self, watch_dir: str, ingest_script: str, pgpt_url: str,
|
||||
extensions: Set[str], delay: int = 5):
|
||||
"""
|
||||
Initialise le gestionnaire d'événements.
|
||||
|
||||
Args:
|
||||
watch_dir: Répertoire à surveiller
|
||||
ingest_script: Chemin vers le script d'injection
|
||||
pgpt_url: URL de l'API PrivateGPT
|
||||
extensions: Extensions de fichiers à traiter
|
||||
delay: Délai en secondes à attendre avant le traitement (évite de traiter des fichiers partiellement écrits)
|
||||
"""
|
||||
self.watch_dir = os.path.abspath(watch_dir)
|
||||
self.ingest_script = os.path.abspath(ingest_script)
|
||||
self.pgpt_url = pgpt_url
|
||||
self.extensions = extensions
|
||||
self.delay = delay
|
||||
|
||||
# Queue pour les fichiers en attente de traitement
|
||||
self.pending_files: Dict[str, float] = {}
|
||||
|
||||
# Vérifier que le script d'injection existe
|
||||
if not os.path.exists(self.ingest_script):
|
||||
logger.error(f"Le script d'injection {self.ingest_script} n'existe pas!")
|
||||
raise FileNotFoundError(f"Script d'injection introuvable: {self.ingest_script}")
|
||||
|
||||
def on_created(self, event):
|
||||
"""Appelé lorsqu'un fichier est créé."""
|
||||
if not event.is_directory:
|
||||
self._handle_file_event(event)
|
||||
|
||||
def on_modified(self, event):
|
||||
"""Appelé lorsqu'un fichier est modifié."""
|
||||
if not event.is_directory:
|
||||
self._handle_file_event(event)
|
||||
|
||||
def _handle_file_event(self, event):
|
||||
"""Traite un événement de fichier (création ou modification)."""
|
||||
file_path = event.src_path
|
||||
file_ext = os.path.splitext(file_path)[1].lower()
|
||||
|
||||
# Ignorer les fichiers non supportés
|
||||
if file_ext not in self.extensions:
|
||||
return
|
||||
|
||||
# Ignorer les fichiers temporaires et cachés
|
||||
file_name = os.path.basename(file_path)
|
||||
if file_name.startswith('.') or file_name.startswith('~') or file_name.endswith('.tmp'):
|
||||
return
|
||||
|
||||
# Ajouter à la queue avec l'horodatage actuel
|
||||
self.pending_files[file_path] = time.time()
|
||||
logger.info(f"Fichier détecté: {file_path} (en attente pendant {self.delay} secondes)")
|
||||
|
||||
def process_pending_files(self):
|
||||
"""Traite les fichiers en attente qui ont dépassé le délai d'attente."""
|
||||
current_time = time.time()
|
||||
files_to_process: List[str] = []
|
||||
|
||||
# Identifier les fichiers prêts à être traités
|
||||
for file_path, timestamp in list(self.pending_files.items()):
|
||||
if current_time - timestamp >= self.delay:
|
||||
if os.path.exists(file_path): # Vérifier que le fichier existe toujours
|
||||
files_to_process.append(file_path)
|
||||
self.pending_files.pop(file_path)
|
||||
|
||||
# Traiter les fichiers par lot
|
||||
if files_to_process:
|
||||
self._ingest_files(files_to_process)
|
||||
|
||||
def _ingest_files(self, files: List[str]):
|
||||
"""
|
||||
Injecte une liste de fichiers en utilisant le script d'injection.
|
||||
|
||||
Args:
|
||||
files: Liste des chemins de fichiers à injecter
|
||||
"""
|
||||
try:
|
||||
# Créer un répertoire temporaire pour stocker la liste des fichiers
|
||||
temp_dir = os.path.join(os.path.dirname(self.ingest_script), "temp")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
# Créer un fichier de liste
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
list_file = os.path.join(temp_dir, f"files_to_ingest_{timestamp}.txt")
|
||||
|
||||
with open(list_file, "w") as f:
|
||||
for file_path in files:
|
||||
f.write(f"{file_path}\n")
|
||||
|
||||
# Construire la commande pour le script d'injection
|
||||
for file_path in files:
|
||||
file_dir = os.path.dirname(file_path)
|
||||
logger.info(f"Injection de {file_path}...")
|
||||
|
||||
cmd = [
|
||||
sys.executable,
|
||||
self.ingest_script,
|
||||
"-d", file_dir,
|
||||
"-u", self.pgpt_url,
|
||||
"--extensions", os.path.splitext(file_path)[1][1:] # Extension sans le point
|
||||
]
|
||||
|
||||
# Exécuter la commande
|
||||
process = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if process.returncode == 0:
|
||||
logger.info(f"Injection réussie de {file_path}")
|
||||
else:
|
||||
logger.error(f"Échec de l'injection de {file_path}: {process.stderr}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'injection des fichiers: {str(e)}")
|
||||
|
||||
def parse_arguments():
|
||||
"""Parse les arguments de ligne de commande."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Surveille un répertoire et injecte automatiquement les nouveaux fichiers dans PrivateGPT"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d", "--directory",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Chemin du répertoire à surveiller"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s", "--script",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Chemin vers le script auto_ingest.py (par défaut: détection automatique)"
|
||||
)
|
||||
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(
|
||||
"--delay",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Délai en secondes avant de traiter un nouveau fichier (défaut: 5)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--extensions",
|
||||
nargs="+",
|
||||
help="Liste d'extensions spécifiques à surveiller (ex: pdf txt)"
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
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}
|
||||
|
||||
# Déterminer le chemin du script d'injection
|
||||
if args.script:
|
||||
ingest_script = args.script
|
||||
else:
|
||||
# Utiliser le script auto_ingest.py dans le même répertoire que ce script
|
||||
ingest_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "auto_ingest.py")
|
||||
|
||||
# Créer le répertoire de surveillance s'il n'existe pas
|
||||
watch_dir = os.path.abspath(args.directory)
|
||||
if not os.path.exists(watch_dir):
|
||||
logger.info(f"Création du répertoire de surveillance: {watch_dir}")
|
||||
os.makedirs(watch_dir, exist_ok=True)
|
||||
|
||||
logger.info(f"Démarrage de la surveillance de {watch_dir}")
|
||||
logger.info(f"URL PrivateGPT: {args.url}")
|
||||
logger.info(f"Extensions surveillées: {', '.join(extensions)}")
|
||||
logger.info(f"Délai de traitement: {args.delay} secondes")
|
||||
|
||||
# Initialiser le gestionnaire et l'observateur
|
||||
event_handler = DocumentHandler(
|
||||
watch_dir=watch_dir,
|
||||
ingest_script=ingest_script,
|
||||
pgpt_url=args.url,
|
||||
extensions=extensions,
|
||||
delay=args.delay
|
||||
)
|
||||
|
||||
observer = Observer()
|
||||
observer.schedule(event_handler, path=watch_dir, recursive=True)
|
||||
observer.start()
|
||||
|
||||
try:
|
||||
logger.info("Surveillance en cours... (Ctrl+C pour quitter)")
|
||||
|
||||
while True:
|
||||
# Traiter les fichiers en attente
|
||||
event_handler.process_pending_files()
|
||||
time.sleep(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("\nInterruption par l'utilisateur. Arrêt de la surveillance.")
|
||||
observer.stop()
|
||||
|
||||
observer.join()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur non gérée: {str(e)}", exc_info=True)
|
||||
sys.exit(1)
|
||||
94
IA/02 - injection_fiches/watch_directory.sh
Executable file
94
IA/02 - injection_fiches/watch_directory.sh
Executable file
@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
# Script d'exécution pour watch_directory.py
|
||||
|
||||
# Vérification des dépendances
|
||||
check_dependencies() {
|
||||
# Vérifier Python
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "Erreur: Python 3 n'est pas installé ou n'est pas dans le PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Vérifier les bibliothèques Python requises
|
||||
python3 -c "
|
||||
try:
|
||||
import watchdog
|
||||
except ImportError:
|
||||
print('Installation de la bibliothèque watchdog...')
|
||||
import subprocess, sys
|
||||
subprocess.run([sys.executable, '-m', 'pip', 'install', 'watchdog'])
|
||||
" || {
|
||||
echo "Erreur: Impossible d'installer les dépendances"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Fonction d'aide
|
||||
show_help() {
|
||||
echo "Script de surveillance de répertoire pour PrivateGPT"
|
||||
echo ""
|
||||
echo "Usage: $0 [options] -d RÉPERTOIRE"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -d, --directory RÉPERTOIRE Répertoire à surveiller (obligatoire)"
|
||||
echo " -s, --script CHEMIN Chemin vers le script auto_ingest.py (facultatif)"
|
||||
echo " -u, --url URL URL de l'API PrivateGPT (défaut: http://localhost:8001)"
|
||||
echo " --delay N Délai en secondes avant de traiter un nouveau fichier (défaut: 5)"
|
||||
echo " --extensions EXT1 EXT2 ... Liste d'extensions spécifiques à surveiller"
|
||||
echo " -h, --help Afficher cette aide"
|
||||
echo ""
|
||||
echo "Exemple: $0 -d /documents/à/surveiller --extensions pdf docx"
|
||||
}
|
||||
|
||||
# Vérifier si aucun argument n'est fourni
|
||||
if [ $# -eq 0 ]; then
|
||||
show_help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Vérifier l'argument d'aide
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Vérifier la présence de l'argument obligatoire (-d ou --directory)
|
||||
directory_specified=false
|
||||
for ((i=1; i<=$#; i++)); do
|
||||
if [ "${!i}" = "-d" ] || [ "${!i}" = "--directory" ]; then
|
||||
directory_specified=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$directory_specified" = false ]; then
|
||||
echo "Erreur: L'option -d/--directory est obligatoire"
|
||||
echo "Utilisez -h ou --help pour afficher l'aide"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Vérifier les dépendances
|
||||
check_dependencies
|
||||
|
||||
# Chemin au script Python
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PYTHON_SCRIPT="${SCRIPT_DIR}/watch_directory.py"
|
||||
|
||||
# Vérifier que le script Python existe
|
||||
if [ ! -f "$PYTHON_SCRIPT" ]; then
|
||||
echo "Erreur: Le script watch_directory.py n'existe pas dans $SCRIPT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Rendre le script Python exécutable
|
||||
chmod +x "$PYTHON_SCRIPT"
|
||||
|
||||
# Message d'information sur l'arrêt
|
||||
echo "Démarrage de la surveillance..."
|
||||
echo "Appuyez sur Ctrl+C pour arrêter la surveillance"
|
||||
echo ""
|
||||
|
||||
# Exécuter le script Python avec tous les arguments
|
||||
python3 "$PYTHON_SCRIPT" "$@"
|
||||
171
IA/get_regeneration_plan.py
Normal file
171
IA/get_regeneration_plan.py
Normal file
@ -0,0 +1,171 @@
|
||||
from datetime import datetime
|
||||
from collections import defaultdict, deque
|
||||
import os
|
||||
import sys
|
||||
from datetime import timezone
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
# À adapter dans ton environnement
|
||||
from config import GITEA_URL, ORGANISATION, DEPOT_FICHES, ENV
|
||||
from utils.gitea import recuperer_date_dernier_commit
|
||||
from IA.make_config import MAKE # MAKE doit être importé depuis un fichier de config
|
||||
|
||||
def get_mtime(path):
|
||||
try:
|
||||
return datetime.fromtimestamp(os.path.getmtime(path), tz=timezone.utc)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
def get_commit_time(path_relative):
|
||||
commits_url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/commits?path={path_relative.replace("Fiches", "Documents")}&sha={ENV}"
|
||||
return recuperer_date_dernier_commit(commits_url)
|
||||
|
||||
def resolve_path_from_where(where_str):
|
||||
parts = where_str.split(".")
|
||||
current = MAKE
|
||||
path_stack = []
|
||||
|
||||
for part in parts:
|
||||
if isinstance(current, dict) and part in current:
|
||||
path_stack.append((part, current))
|
||||
current = current[part]
|
||||
else:
|
||||
return None
|
||||
|
||||
if not isinstance(current, str):
|
||||
return None
|
||||
|
||||
for i in range(len(path_stack) - 1, -1, -1):
|
||||
key, context = path_stack[i]
|
||||
if "directory" in context:
|
||||
directory = context["directory"]
|
||||
if "fiches" in where_str:
|
||||
return os.path.join("Fiches", directory, current)
|
||||
else:
|
||||
return os.path.join(directory, current)
|
||||
|
||||
return None
|
||||
|
||||
def identifier_type_fiche(path):
|
||||
for type_fiche, data in MAKE["fiches"].items():
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
directory = data.get("directory", "")
|
||||
prefix = data.get("prefix", "")
|
||||
base = os.path.join("Fiches", directory, prefix)
|
||||
if path.startswith(base):
|
||||
return type_fiche, data
|
||||
raise ValueError("Type de fiche non reconnu")
|
||||
|
||||
def doit_regenerer(fichier, doc_deps, fiche_data=None):
|
||||
mtime_fichier = get_mtime(fichier)
|
||||
|
||||
if fiche_data:
|
||||
gitea_dep = fiche_data.get("depends_on", {}).get("gitea", {})
|
||||
if gitea_dep.get("compare") == "file2commit":
|
||||
commit_time = get_commit_time(fichier)
|
||||
if commit_time and mtime_fichier and commit_time > mtime_fichier:
|
||||
return True
|
||||
|
||||
for _, dep in doc_deps.items():
|
||||
if isinstance(dep, dict) and "where" in dep:
|
||||
source_path = resolve_path_from_where(dep["where"])
|
||||
if source_path:
|
||||
if dep["compare"] == "file2file":
|
||||
mtime_source = get_mtime(source_path)
|
||||
elif dep["compare"] == "file2commit":
|
||||
mtime_source = get_commit_time(source_path)
|
||||
else:
|
||||
continue
|
||||
if mtime_source and mtime_fichier and mtime_source > mtime_fichier:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_regeneration_plan(fiche_path):
|
||||
def build_dependency_graph_complete(path, graph=None, visited=None):
|
||||
if graph is None:
|
||||
graph = defaultdict(set)
|
||||
if visited is None:
|
||||
visited = set()
|
||||
if path in visited:
|
||||
return graph
|
||||
visited.add(path)
|
||||
|
||||
try:
|
||||
_, fiche_data = identifier_type_fiche(path)
|
||||
except ValueError:
|
||||
return graph
|
||||
|
||||
depends = fiche_data.get("depends_on", {})
|
||||
doc_deps = depends.get("document", {})
|
||||
|
||||
for _, dep_info in doc_deps.items():
|
||||
if isinstance(dep_info, dict) and "where" in dep_info:
|
||||
dep_path = resolve_path_from_where(dep_info["where"])
|
||||
if dep_path:
|
||||
graph[path].add(dep_path)
|
||||
build_dependency_graph_complete(dep_path, graph, visited)
|
||||
|
||||
if "file" in fiche_data:
|
||||
for fichier in fiche_data["file"].values():
|
||||
dir_fiche = fiche_data.get("directory", "")
|
||||
fichier_path = os.path.join("Fiches", dir_fiche, fichier)
|
||||
if fichier_path not in graph:
|
||||
graph[fichier_path] = set()
|
||||
|
||||
return graph
|
||||
|
||||
def topological_sort(graph):
|
||||
in_degree = defaultdict(int)
|
||||
for node in graph:
|
||||
for dep in graph[node]:
|
||||
in_degree[dep] += 1
|
||||
queue = deque([node for node in graph if in_degree[node] == 0])
|
||||
result = []
|
||||
|
||||
while queue:
|
||||
node = queue.popleft()
|
||||
result.append(node)
|
||||
for dep in graph[node]:
|
||||
in_degree[dep] -= 1
|
||||
if in_degree[dep] == 0:
|
||||
queue.append(dep)
|
||||
|
||||
all_nodes = set(graph.keys()).union(*graph.values())
|
||||
for node in all_nodes:
|
||||
if node not in result:
|
||||
result.append(node)
|
||||
|
||||
return result[::-1]
|
||||
|
||||
graph = build_dependency_graph_complete(fiche_path)
|
||||
sorted_fiches = topological_sort(graph)
|
||||
if fiche_path not in sorted_fiches:
|
||||
sorted_fiches.append(fiche_path)
|
||||
|
||||
|
||||
to_regen = []
|
||||
regen_flags = {}
|
||||
|
||||
for fiche in sorted_fiches:
|
||||
print(f"=> {fiche}")
|
||||
try:
|
||||
_, fiche_data = identifier_type_fiche(fiche)
|
||||
except ValueError:
|
||||
fiche_data = None
|
||||
depends = fiche_data.get("depends_on", {}) if fiche_data else {}
|
||||
doc_deps = depends.get("document", {}) if depends else {}
|
||||
|
||||
doit = doit_regenerer(fiche, doc_deps, fiche_data)
|
||||
if any(regen_flags.get(dep, False) for dep in graph.get(fiche, [])):
|
||||
doit = True
|
||||
|
||||
regen_flags[fiche] = doit
|
||||
if doit:
|
||||
to_regen.append(fiche)
|
||||
|
||||
return to_regen
|
||||
|
||||
plan = get_regeneration_plan("Fiches/Minerai/Fiche minerai antimoine.md")
|
||||
print(plan)
|
||||
141
IA/make_config.py
Normal file
141
IA/make_config.py
Normal file
@ -0,0 +1,141 @@
|
||||
from utils.gitea import recuperer_date_dernier_commit
|
||||
#
|
||||
# from config import GITEA_URL, GITEA_TOKEN, ORGANISATION, DEPOT_FICHES, DEPOT_CODE, ENV, ENV_CODE, DOT_FILE
|
||||
#
|
||||
#def recuperer_date_dernier_commit(url):
|
||||
# headers = {"Authorization": f"token {GITEA_TOKEN}"}
|
||||
# try:
|
||||
# response = requests.get(url, headers=headers, timeout=10)
|
||||
# response.raise_for_status()
|
||||
# commits = response.json()
|
||||
# if commits:
|
||||
# return parser.isoparse(commits[0]["commit"]["author"]["date"])
|
||||
# except Exception as e:
|
||||
# logging.error(f"Erreur récupération commit schema : {e}")
|
||||
# return None
|
||||
#
|
||||
# path_relative = f"Documents/{dossier_choisi}/{fiche_choisie}"
|
||||
# commits_url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/commits?path={path_relative}&sha={ENV}"
|
||||
#
|
||||
# local_mtime = datetime.fromtimestamp(os.path.getmtime(path_relative), tz=timezone.utc)
|
||||
# remote_mtime = recuperer_date_dernier_commit(commit_url)
|
||||
|
||||
MAKE = {
|
||||
"assets": {
|
||||
"directory": "assets",
|
||||
"seuils": {
|
||||
"depends_on": "None",
|
||||
},
|
||||
"file": {
|
||||
"seuils": "config.yaml"
|
||||
}
|
||||
},
|
||||
"fiches": {
|
||||
"directory": "Fiches",
|
||||
"criticites": {
|
||||
"directory": "Criticités",
|
||||
"préfix": "Fiche technique ",
|
||||
"depends_on": {
|
||||
"gitea": {
|
||||
"compare": "file2commit"
|
||||
},
|
||||
"document": {
|
||||
"seuils": {
|
||||
"where": "assets.file.seuils",
|
||||
"compare": "file2file"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file": {
|
||||
"ihh": "Fiche technique IHH.md",
|
||||
"isg": "Fiche technique ISG.md",
|
||||
"ivc": "Fiche technique IVC.md",
|
||||
"ics": "Fiche technique ICS.md"
|
||||
}
|
||||
},
|
||||
"assemblage": {
|
||||
"directory": "Assemblage",
|
||||
"prefix": "Fiche assemblage ",
|
||||
"depends_on": {
|
||||
"gitea": {
|
||||
"compare": "file2commit"
|
||||
},
|
||||
"document": {
|
||||
"ihh": {
|
||||
"where": "fiches.criticites.file.ihh",
|
||||
"compare": "file2file"
|
||||
},
|
||||
"isg": {
|
||||
"where": "fiches.criticites.file.isg",
|
||||
"compare": "file2file"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fabrication": {
|
||||
"directory": "Fabrication",
|
||||
"prefix": "Fiche fabrication ",
|
||||
"depends_on": {
|
||||
"gitea": {
|
||||
"compare": "file2commit"
|
||||
},
|
||||
"document": {
|
||||
"ihh": {
|
||||
"where": "fiches.criticites.file.ihh",
|
||||
"compare": "file2file"
|
||||
},
|
||||
"isg": {
|
||||
"where": "fiches.criticites.file.isg",
|
||||
"compare": "file2file"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"connexe": {
|
||||
"directory": "Connexe",
|
||||
"prefix": "Fiche assemblage ",
|
||||
"depends_on": {
|
||||
"gitea": {
|
||||
"compare": "file2commit"
|
||||
},
|
||||
"document": {
|
||||
"ihh": {
|
||||
"where": "fiches.criticites.file.ihh",
|
||||
"compare": "file2file"
|
||||
},
|
||||
"isg": {
|
||||
"where": "fiches.criticites.file.isg",
|
||||
"compare": "file2file"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"minerai": {
|
||||
"directory": "Minerai",
|
||||
"prefix": "Fiche minerai ",
|
||||
"depends_on": {
|
||||
"gitea": {
|
||||
"compare": "file2commit"
|
||||
},
|
||||
"document": {
|
||||
"ihh": {
|
||||
"where": "fiches.criticites.file.ihh",
|
||||
"compare": "file2file"
|
||||
},
|
||||
"isg": {
|
||||
"where": "fiches.criticites.file.isg",
|
||||
"compare": "file2file"
|
||||
},
|
||||
"ics": {
|
||||
"where": "fiches.criticites.file.ics",
|
||||
"compare": "file2file"
|
||||
},
|
||||
"ivc": {
|
||||
"where": "fiches.criticites.file.ivc",
|
||||
"compare": "file2file"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Instructions.md
Normal file
93
Instructions.md
Normal file
@ -0,0 +1,93 @@
|
||||
# Bienvenue dans l'application Fabnum
|
||||
|
||||
## Présentation de l'application
|
||||
|
||||
Cet outil interactif vous permet d'explorer et d'analyser les vulnérabilités de la chaîne de fabrication du numérique. Grâce à une interface, espérons-le, intuitive, vous pourrez visualiser les différentes étapes de production, identifier les points critiques et comprendre les enjeux géopolitiques et plus liés à la fabrication des technologies numériques.
|
||||
|
||||
L'application vous offre diverses fonctionnalités :
|
||||
|
||||
* Visualisation des données et des graphiques
|
||||
* Personnalisation des produits à analyser
|
||||
* Exploration détaillée des indices de vulnérabilité et des opérations
|
||||
* Possibilité d'exporter les résultats selon différents formats selon autorisation
|
||||
|
||||
## Comprendre la chaîne de fabrication numérique
|
||||
|
||||
### Structure de la chaîne
|
||||
La chaîne de fabrication numérique se décompose en trois niveaux hiérarchiques :
|
||||
|
||||
* Produits finaux : appareils complets comme les smartphones, ordinateurs, serveurs, etc.
|
||||
* Composants : éléments constitutifs comme les processeurs, écrans, capteurs, batteries, etc.
|
||||
* Minerais et matériaux : ressources de base nécessaires à la fabrication des composants
|
||||
|
||||
### Opérations à chaque niveau
|
||||
Chaque niveau de la chaîne implique des opérations spécifiques :
|
||||
|
||||
* Niveau Produit final : assemblage des composants pour créer le produit fini
|
||||
* Niveau Composant : fabrication des pièces à partir des minerais et matériaux
|
||||
* Niveau Minerai : extraction et traitement des ressources premières
|
||||
|
||||
### Dimension géopolitique
|
||||
Pour chaque opération, l'application détaille :
|
||||
|
||||
* Les pays où l'opération est réalisée, avec leur part respective du marché mondial
|
||||
* Les acteurs économiques impliqués dans chaque pays, avec leur part de marché
|
||||
* Les liens entre les pays d'opération, les acteurs et leur contexte géopolitique
|
||||
|
||||
[Voir la vision détaillée du projet](https://fabnum-dev.peccini.fr/app/static/Description/Objectif%20final.pdf)
|
||||
|
||||
## Comprendre les indices de vulnérabilité
|
||||
|
||||
L'application utilise quatre indices clés pour évaluer les vulnérabilités dans la chaîne de fabrication numérique :
|
||||
|
||||
### Indice de Herfindahl-Hirschmann (IHH)
|
||||
|
||||
* Que mesure-t-il ? La concentration géographique ou industrielle d'une opération.
|
||||
* Comment l'interpréter ? Plus l'indice est élevé, plus l'opération est concentrée dans un nombre limité de pays, ce qui augmente la vulnérabilité.
|
||||
* Où le trouver ? Associé à chaque opération dans les graphiques d'analyse.
|
||||
|
||||
[Voir la fiche détaillée](https://fabnum-dev.peccini.fr/app/static/Fiches/Criticit%C3%A9s/Fiche%20technique%20IHH.pdf)
|
||||
|
||||
### Indice de Vulnérabilité Concurrentielle (IVC)
|
||||
|
||||
* Que mesure-t-il ? La pression exercée par d'autres secteurs industriels sur une même ressource minérale.
|
||||
* Comment l'interpréter ? Un IVC élevé indique une forte compétition pour accéder à ce minerai, augmentant le risque de pénurie.
|
||||
* Où le trouver ? Associé à chaque minerai dans les visualisations et fiches.
|
||||
|
||||
[Voir la fiche détaillée](https://fabnum-dev.peccini.fr/app/static/Fiches/Criticit%C3%A9s/Fiche%20technique%20IVC.pdf)
|
||||
|
||||
### Indice de Criticité de Substituabilité (ICS)
|
||||
|
||||
* Que mesure-t-il ? La possibilité de remplacer un minerai par un autre dans la fabrication d'un composant. Plus rarement, il matérialise la capacité de remplacer un procédé.
|
||||
* Comment l'interpréter ? Un ICS élevé signifie qu'il est difficile de trouver une alternative à ce minerai.
|
||||
* Où le trouver ? Associé à la relation entre composants et minerais.
|
||||
|
||||
[Voir la fiche détaillée](https://fabnum-dev.peccini.fr/app/static/Fiches/Criticit%C3%A9s/Fiche%20technique%20ICS.pdf)
|
||||
|
||||
### Indice de Stabilité Géopolitique (ISG)
|
||||
|
||||
* Que mesure-t-il ? La stabilité politique, économique et sociale d'un pays, basée sur trois sous-indicateurs.
|
||||
* Comment l'interpréter ? Un ISG élevé indique un pays instable, ce qui augmente les risques d'approvisionnement.
|
||||
* Où le trouver ? Utilisé pour pondérer les risques identifiés tout au long de la chaîne.
|
||||
|
||||
[Voir la fiche détaillée](https://fabnum-dev.peccini.fr/app/static/Fiches/Criticit%C3%A9s/Fiche%20technique%20ISG.pdf)
|
||||
|
||||
## Guide d'utilisation de l'application
|
||||
|
||||
L'application est organisée en quatre onglets principaux, chacun offrant une perspective différente sur la chaîne de fabrication numérique :
|
||||
|
||||
* Onglet Personnalisation : Créer et gérer des produits finaux personnalisés pour des analyses spécifiques.
|
||||
* À noter : Les produits personnalisés sont temporaires par défaut, mais peuvent être sauvegardés pour une utilisation ultérieure.
|
||||
* Onglet Analyse : Explorer visuellement les relations entre les différents niveaux de la chaîne de fabrication.
|
||||
* Exemple d'utilisation : Pour comprendre les vulnérabilités liées aux composants d'un smartphone, sélectionnez « Produit final » comme niveau de départ, « Composant » comme niveau d'arrivée, puis spécifiez « Smartphone » comme item de produit final.
|
||||
* Onglet IA'nalyse : Obtenez un rapport synthétique circonstancié d'une partie du schéma global.
|
||||
* Cet onglet n'est accessible qu'aux personnes diposant d'un compte.
|
||||
* Onglet Plan d'action : Visualiser toutes les criticités d'une chaîne Produit final <-> Composant <-> Minerai.
|
||||
* À partir d'une sélection, toutes les chaînes critiques sont examinées à la loupe.
|
||||
* Des propositions d'actions et d'indicateurs sont fournies selon les opérations et leur niveau de criticité.
|
||||
* Onglet Visualisations : Observer les corrélations entre les différents indices et comprendre les tendances globales.
|
||||
* Indicateurs clés : Portez attention aux points situés dans les zones de haute valeur pour les deux indices, car ils représentent les vulnérabilités les plus critiques.
|
||||
* Onglet Fiches : Accéder à des informations détaillées sur chaque opération et minerai.
|
||||
* À explorer : Les fiches contiennent souvent des informations qui ne sont pas visibles directement dans les graphiques, comme des tendances historiques ou des prévisions futures ; n'hésitez pas à les consulter
|
||||
|
||||
En début de chacun des onglets, vous trouverez un mini-guide spécifique sur leur utilisation.
|
||||
128
README.md
128
README.md
@ -14,7 +14,7 @@ Le code proposé répond à la partie outillage, avec une architecture modulaire
|
||||
|
||||
Le projet est bâti sur un backeng Gitea pour la gestion des fiches et des tickets d'évolution. (Accéder au backend)[https://fabnum-git.peccini.fr/FabNum/Fiches]
|
||||
|
||||
Le serveur qui héberge l'application héberge aussi le service Gitea, ce qui permet d'éliminer les temps de latence dus au réseau.
|
||||
Le serveur qui héberge l'application héberge aussi le service Gitea, ce qui permet d'éliminer les temps de latence dus au réseau. Les fiches sont toutefois mise en cache localement en générant les fichiers markdown (référence), html (affichage) et pdf (download).
|
||||
|
||||
L'application est écrite en python et utilise majoritairement streamlit.
|
||||
|
||||
@ -26,25 +26,44 @@ Le fichier **requirements.txt** permet d'installer tout ce qui est nécessaire p
|
||||
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
# Installation depuis le fichier
|
||||
pip install -r requirements.txt
|
||||
# Génération du fichier
|
||||
pipreqs requirements.txt
|
||||
|
||||
### Environnement
|
||||
|
||||
Le fichier **.env.local** qui contient GITEA_TOKEN n'est pas dans le dépôt car il contient la clé pour accéder au backend.
|
||||
Le fichier **.env.local** qui contient GITEA_TOKEN n'est pas dans le dépôt car il contient la clé pour accéder au backend. Il doit donc être créé ainsi que le Token. Le TOken doit permetre l'accès en lecture au dépôt et en lecture/écriture au gestionnaire des tickets.
|
||||
|
||||
Pour l'environnement de pré-production, (https://fabnum-dev.peccini.fr)[https://fabnum-dev.peccini.fr] :
|
||||
|
||||
ENV=dev
|
||||
ENV_CODE = "dev"
|
||||
PORT=8502
|
||||
DOT_FILE = "schema.txt"
|
||||
GITEA_URL = "https://fabnum-git.peccini.fr/api/v1"
|
||||
ORGANISATION = "fabnum"
|
||||
DEPOT_FICHES = "fiches"
|
||||
DEPOT_CODE = "code"
|
||||
ID_PROJET = "3"
|
||||
INSTRUCTIONS = "Instructions.md"
|
||||
FICHE_IHH = "Fiches/Criticités/Fiche technique IHH.md"
|
||||
FICHE_ICS = "Fiches/Criticités/Fiche technique ICS.md"
|
||||
FICHE_ISG = "Fiches/Criticités/Fiche technique ISG.md"
|
||||
FICHE_IVC = "Fiches/Criticités/Fiche technique IVC.md"
|
||||
|
||||
Pour l'environnement de production, (https://fabnum.peccini.fr)[https://fabnum.peccini.fr], le fichier est identique sauf pour :
|
||||
|
||||
ENV=public
|
||||
PORT=8501
|
||||
|
||||
La différence entre les deux environnements se fait au travers de la configuration du proxy Nginx (ci-après celle de dev ; il suffit de changer dev en public pour l'environnement de production) :
|
||||
|
||||
# Ajout d'un en-tête personnalisé pour indiquer l'environnement
|
||||
add_header X-Environment "dev" always;
|
||||
# Transmettre l'en-tête d'environnement au backend
|
||||
proxy_set_header X-Environment "dev";
|
||||
|
||||
Cette configuration est utilisée par config.py.
|
||||
|
||||
dev et public sont les deux branches officielles du dépôt.
|
||||
|
||||
L'application se lance simplement sous la forme :
|
||||
@ -66,12 +85,10 @@ Pour automatiser le lancement, il est intégré dans systemd :
|
||||
[Service]
|
||||
User=fabnum
|
||||
WorkingDirectory=/home/fabnum/fabnum-dev
|
||||
ExecStart=/home/fabnum/fabnum-dev/venv/bin/streamlit run /home/fabnum/fabnum-dev/fa
|
||||
bnum.py
|
||||
ExecStart=/home/fabnum/fabnum-dev/venv/bin/streamlit run /home/fabnum/fabnum-dev/fabnum.py --server.port 8502
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
SELinuxContext=system_u:system_r:httpd_t:s0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -92,11 +109,19 @@ Le cœur de l'application. Ce script sert de point d'entrée et d'orchestrateur
|
||||
|
||||
Ce fichier est conçu de manière modulaire, déléguant les fonctionnalités spécifiques aux modules spécialisés, ce qui facilite la maintenance et les évolutions futures.
|
||||
|
||||
Dans la sidebar, l'application indique une estimation des émissions de gaz à effet de serre. Pour cela, un mécanisme est mis en place dans la configuration Nginx pour enregistrer dans le fichier :
|
||||
/var/log/nginx/fabnum-dev.access.log
|
||||
les informations d'octets transférés.
|
||||
|
||||
Ce système est basé sur la création d'un cookie de session, utilisé ensuite pour distinguer les utilisations.
|
||||
|
||||
<<<<<<<<<<<à compléter avec la configuration Nginx>>>>>>>>>>>
|
||||
|
||||
## Architecture et principes de conception
|
||||
|
||||
### Modularité et simplification
|
||||
|
||||
L'application a été restructurée selon les principes suivants :
|
||||
L'application a été structurée selon les principes suivants :
|
||||
|
||||
1. **Séparation des responsabilités** : Chaque module a une fonction bien définie
|
||||
2. **Modularité** : Les fonctionnalités sont décomposées en composants réutilisables
|
||||
@ -135,43 +160,56 @@ L'application est organisée de façon modulaire, avec une structure simplifiée
|
||||
|
||||
```
|
||||
fabnum-dev/
|
||||
├── fabnum.py # Point d'entrée principal
|
||||
├── config.py # Configuration et variables d'environnement
|
||||
├── app/ # Modules fonctionnels principaux
|
||||
│ ├── analyse/ # Module d'analyse des chaînes de dépendance
|
||||
│ │ ├── interface.py # Interface utilisateur pour l'analyse
|
||||
│ │ ├── sankey.py # Génération des diagrammes Sankey
|
||||
│ │ └── README.md # Documentation du module
|
||||
│ ├── fiches/ # Gestion et affichage des fiches
|
||||
│ │ ├── interface.py # Interface utilisateur pour les fiches
|
||||
│ │ ├── generer.py # Génération des fiches
|
||||
│ │ ├── utils/ # Utilitaires spécifiques aux fiches
|
||||
│ │ └── README.md # Documentation du module
|
||||
│ ├── personnalisation/ # Personnalisation de la chaîne
|
||||
│ │ ├── interface.py # Interface principale
|
||||
│ │ ├── ajout.py # Ajout de produits
|
||||
│ │ ├── modification.py # Modification de produits
|
||||
│ │ ├── import_export.py # Import/export de configurations
|
||||
│ │ └── README.md # Documentation du module
|
||||
│ └── visualisations/ # Visualisations graphiques
|
||||
│ ├── interface.py # Interface des visualisations
|
||||
│ ├── graphes.py # Gestion des graphes à visualiser
|
||||
│ └── README.md # Documentation du module
|
||||
├── components/ # Composants d'interface réutilisables
|
||||
│ ├── sidebar.py # Barre latérale de navigation
|
||||
│ ├── header.py # En-tête de l'application
|
||||
│ ├── footer.py # Pied de page
|
||||
│ └── README.md # Documentation des composants
|
||||
├── utils/ # Utilitaires partagés
|
||||
│ ├── gitea.py # Connexion API Gitea
|
||||
│ ├── graph_utils.py # Manipulation des graphes
|
||||
│ └── README.md # Documentation des utilitaires
|
||||
├── assets/ # Ressources statiques
|
||||
│ ├── styles/ # Feuilles de style CSS
|
||||
│ └── impact_co2.js # Calcul d'impact environnemental
|
||||
├── .env # Configuration versionnée
|
||||
├── .env.local # Configuration locale (non versionnée)
|
||||
└── requirements.txt # Dépendances Python
|
||||
├── fabnum.py # Point d'entrée principal
|
||||
├── config.py # Configuration et variables d'environnement
|
||||
├── app/ # Modules fonctionnels principaux
|
||||
│ ├── analyse/ # Module d'analyse des chaînes de dépendance
|
||||
│ │ ├── interface.py # Interface utilisateur pour l'analyse
|
||||
│ │ ├── sankey.py # Génération des diagrammes Sankey
|
||||
│ │ └── README.md # Documentation du module
|
||||
│ ├── fiches/ # Gestion et affichage des fiches
|
||||
│ │ ├── interface.py # Interface utilisateur pour les fiches
|
||||
│ │ ├── generer.py # Génération des fiches
|
||||
│ │ ├── utils/ # Utilitaires spécifiques aux fiches
|
||||
│ │ ├── utils/dynamic # Gestion de la génération et affichage des fiches par type d'opération
|
||||
│ │ ├── utils/tickets # Gestion de l'affichage et de la création des tickets
|
||||
│ │ ├── utils/fiches_utils.py # Outils de gestion et rendu des fiches
|
||||
│ │ └── README.md # Documentation du module
|
||||
│ ├── personnalisation/ # Personnalisation de la chaîne
|
||||
│ │ ├── interface.py # Interface principale
|
||||
│ │ ├── ajout.py # Ajout de produits
|
||||
│ │ ├── modification.py # Modification de produits
|
||||
│ │ ├── import_export.py # Import/export de configurations
|
||||
│ │ └── README.md # Documentation du module
|
||||
│ └── visualisations/ # Visualisations graphiques
|
||||
│ ├── interface.py # Interface des visualisations
|
||||
│ ├── graphes.py # Gestion des graphes à visualiser
|
||||
│ └── README.md # Documentation du module
|
||||
├── components/ # Composants d'interface réutilisables
|
||||
│ ├── sidebar.py # Barre latérale de navigation
|
||||
│ ├── header.py # En-tête de l'application
|
||||
│ ├── footer.py # Pied de page
|
||||
│ ├── connexion.py # Module de connexion à partir d'un token Gitea
|
||||
│ └── README.md # Documentation des composants
|
||||
├── utils/ # Utilitaires partagés
|
||||
│ ├── gitea.py # Connexion API Gitea
|
||||
│ ├── graph_utils.py # Manipulation des graphes
|
||||
│ ├── translations.py # Module de gestion de l'internationalisation
|
||||
│ ├── visualisations.py # Manipulation des graphes de l'onglet Visualisations
|
||||
│ └── README.md # Documentation des utilitaires
|
||||
├── assets/ # Ressources statiques
|
||||
│ ├── locales/ # Gestion de l'internationalisation
|
||||
│ ├── styles/ # Feuilles de style CSS
|
||||
│ ├── confir.yaml # Définition des seuils pour les indices
|
||||
│ ├── fiches_labels.csv # Dictionnaire d'association entre les fiches et leurs labels (tickets)
|
||||
│ ├── impact_co2.js # Calcul d'impact environnemental
|
||||
│ ├── licence.md # Licence ajoutée à toutes les fiches
|
||||
│ └── weakness.png # Icône de l'onglet dans le navigateur
|
||||
├── .env # Configuration versionnée
|
||||
├── .env.local # Configuration locale (non versionnée)
|
||||
└── requirements.txt # Dépendances Python
|
||||
├── .streamlit/ # Configuration streamlit côté serveur
|
||||
│ └── config.toml # Fichier de configuration : important theme.base = light
|
||||
```
|
||||
|
||||
Chaque module dispose de sa propre documentation détaillée dans un fichier README.md.
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
# __init__.py – app/fiches
|
||||
from .interface import interface_analyse
|
||||
|
||||
__all_ = [interface_analyse]
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
from typing import List, Tuple, Dict, Optional
|
||||
import networkx as nx
|
||||
import streamlit as st
|
||||
from utils.translations import _
|
||||
from utils.widgets import html_expander
|
||||
from utils.persistance import maj_champ_statut, get_champ_statut, supprime_champ_statut
|
||||
|
||||
from .sankey import afficher_sankey
|
||||
|
||||
@ -16,8 +20,20 @@ niveau_labels = {
|
||||
inverse_niveau_labels = {v: k for k, v in niveau_labels.items()}
|
||||
|
||||
|
||||
def preparer_graphe(G):
|
||||
"""Nettoie et prépare le graphe pour l'analyse."""
|
||||
def preparer_graphe(
|
||||
G: nx.DiGraph,
|
||||
) -> Tuple[nx.DiGraph, Dict[str, int]]:
|
||||
"""
|
||||
Nettoie et prépare le graphe pour l'analyse.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
|
||||
Returns:
|
||||
Tuple[nx.DiGraph, Dict[str, int]]: Un tuple contenant :
|
||||
- Le graphe NetworkX proprement configuré
|
||||
- Un dictionnaire des niveaux associés aux nœuds
|
||||
"""
|
||||
niveaux_temp = {
|
||||
node: int(str(attrs.get("niveau")).strip('"'))
|
||||
for node, attrs in G.nodes(data=True)
|
||||
@ -28,103 +44,246 @@ def preparer_graphe(G):
|
||||
[n for n in G.nodes() if niveaux_temp.get(n) == 10 and 'Reserves' in n])
|
||||
return G, niveaux_temp
|
||||
|
||||
def selectionner_niveaux(
|
||||
) -> Tuple[int|None, int|None]:
|
||||
"""
|
||||
Interface pour sélectionner les niveaux de départ et d'arrivée.
|
||||
|
||||
def selectionner_niveaux():
|
||||
"""Interface pour sélectionner les niveaux de départ et d'arrivée."""
|
||||
Returns:
|
||||
Tuple[int, int]: Un tuple contenant deux nombres si des nœuds ont été sélectionnés,
|
||||
- None sinon
|
||||
"""
|
||||
st.markdown(f"## {str(_('pages.analyse.selection_nodes'))}")
|
||||
valeur_defaut = str(_("pages.analyse.select_level"))
|
||||
niveau_choix = [valeur_defaut] + list(niveau_labels.values())
|
||||
default_index = next((i for i, opt in enumerate(niveau_choix) if get_champ_statut("pages.analyse.select_level.niveau_depart") in opt), 0)
|
||||
|
||||
niveau_depart = st.selectbox(str(_("pages.analyse.start_level")), niveau_choix, key="analyse_niveau_depart")
|
||||
niveau_depart = st.selectbox(str(_("pages.analyse.start_level")), niveau_choix, index=default_index, key="analyse_niveau_depart")
|
||||
if niveau_depart == valeur_defaut:
|
||||
return None, None
|
||||
else:
|
||||
maj_champ_statut("pages.analyse.select_level.niveau_depart", niveau_depart)
|
||||
|
||||
niveau_depart_int = inverse_niveau_labels[niveau_depart]
|
||||
niveaux_arrivee_possibles = [v for k, v in niveau_labels.items() if k > niveau_depart_int]
|
||||
niveaux_arrivee_choix = [valeur_defaut] + niveaux_arrivee_possibles
|
||||
niveau_choix = [valeur_defaut] + niveaux_arrivee_possibles
|
||||
default_index = next((i for i, opt in enumerate(niveau_choix) if get_champ_statut("pages.analyse.select_level.niveau_arrivee") in opt), 0)
|
||||
|
||||
analyse_niveau_arrivee = st.selectbox(str(_("pages.analyse.end_level")), niveaux_arrivee_choix, key="analyse_niveau_arrivee")
|
||||
if analyse_niveau_arrivee == valeur_defaut:
|
||||
niveau_arrivee = st.selectbox(str(_("pages.analyse.end_level")), niveau_choix, index=default_index, key="analyse_niveau_arrivee")
|
||||
if niveau_arrivee == valeur_defaut:
|
||||
return niveau_depart_int, None
|
||||
else:
|
||||
maj_champ_statut("pages.analyse.select_level.niveau_arrivee", niveau_arrivee)
|
||||
|
||||
niveau_arrivee_int = inverse_niveau_labels[analyse_niveau_arrivee]
|
||||
niveau_arrivee_int = inverse_niveau_labels[niveau_arrivee]
|
||||
return niveau_depart_int, niveau_arrivee_int
|
||||
|
||||
def selectionner_minerais(G: nx.DiGraph, niveau_depart: int, niveau_arrivee: int) -> Optional[List[str]]:
|
||||
if not (niveau_depart < 2 < niveau_arrivee):
|
||||
return None
|
||||
|
||||
def selectionner_minerais(G, niveau_depart, niveau_arrivee):
|
||||
"""Interface pour sélectionner les minerais si nécessaire."""
|
||||
minerais_selection = None
|
||||
if niveau_depart < 2 < niveau_arrivee:
|
||||
st.markdown(f"### {str(_('pages.analyse.select_minerals'))}")
|
||||
# Tous les nœuds de niveau 2 (minerai)
|
||||
minerais_nodes = sorted([
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("niveau") and int(str(d.get("niveau")).strip('"')) == 2
|
||||
])
|
||||
st.markdown(f"### {str(_('pages.analyse.select_minerals'))}")
|
||||
|
||||
minerais_selection = st.multiselect(
|
||||
str(_("pages.analyse.filter_by_minerals")),
|
||||
minerais_nodes,
|
||||
key="analyse_minerais"
|
||||
)
|
||||
minerais_nodes = sorted([
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("niveau") and int(str(d.get("niveau")).strip('\"')) == 2
|
||||
])
|
||||
|
||||
return minerais_selection
|
||||
# Initialiser depuis champ_statut si besoin
|
||||
if "analyse_minerais" not in st.session_state:
|
||||
anciens = []
|
||||
i = 0
|
||||
while True:
|
||||
m = get_champ_statut(f"pages.analyse.filter_by_minerals.{i}")
|
||||
if not m:
|
||||
break
|
||||
anciens.append(m)
|
||||
i += 1
|
||||
st.session_state["analyse_minerais"] = anciens
|
||||
|
||||
# Widget multiselect sans default, seulement key
|
||||
st.multiselect(
|
||||
str(_("pages.analyse.filter_by_minerals")),
|
||||
minerais_nodes,
|
||||
key="analyse_minerais"
|
||||
)
|
||||
|
||||
def selectionner_noeuds(G, niveaux_temp, niveau_depart, niveau_arrivee):
|
||||
"""Interface pour sélectionner les nœuds spécifiques de départ et d'arrivée."""
|
||||
selection = st.session_state["analyse_minerais"]
|
||||
|
||||
# Toujours purger, puis recréer si nécessaire
|
||||
supprime_champ_statut("pages.analyse.filter_by_minerals")
|
||||
if selection:
|
||||
for i, m in enumerate(selection):
|
||||
maj_champ_statut(f"pages.analyse.filter_by_minerals.{i}", m)
|
||||
|
||||
return selection if selection else None
|
||||
|
||||
def selectionner_noeuds(
|
||||
G: nx.DiGraph,
|
||||
niveaux_temp: Dict[str, int],
|
||||
niveau_depart: int,
|
||||
niveau_arrivee: int
|
||||
) -> Tuple[List[str]|None, List[str]|None]:
|
||||
"""
|
||||
Interface pour sélectionner les nœuds spécifiques de départ et d'arrivée.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
niveaux_temp (Dict[str, int]): Dictionnaire contenant les niveaux associés aux nœuds.
|
||||
niveau_depart (int): Le niveau de départ sélectionné.
|
||||
niveau_arrivee (int): Le niveau d'arrivée sélectionné.
|
||||
|
||||
Returns:
|
||||
Optional[Tuple[List[str], List[str]]]: Un tuple contenant les listes des nœuds
|
||||
- None sinon
|
||||
"""
|
||||
st.markdown("---")
|
||||
st.markdown(f"## {str(_('pages.analyse.fine_selection'))}")
|
||||
|
||||
depart_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_depart]
|
||||
arrivee_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_arrivee]
|
||||
|
||||
noeuds_depart = st.multiselect(str(_("pages.analyse.filter_start_nodes")),
|
||||
sorted(depart_nodes),
|
||||
key="analyse_noeuds_depart")
|
||||
noeuds_arrivee = st.multiselect(str(_("pages.analyse.filter_end_nodes")),
|
||||
sorted(arrivee_nodes),
|
||||
key="analyse_noeuds_arrivee")
|
||||
# DEPARTS -------------------------------------
|
||||
if "analyse_noeuds_depart" not in st.session_state:
|
||||
anciens_departs = []
|
||||
i = 0
|
||||
while True:
|
||||
val = get_champ_statut(f"pages.analyse.filter_start_nodes.{i}")
|
||||
if not val:
|
||||
break
|
||||
anciens_departs.append(val)
|
||||
i += 1
|
||||
st.session_state["analyse_noeuds_depart"] = anciens_departs
|
||||
|
||||
noeuds_depart = noeuds_depart if noeuds_depart else None
|
||||
noeuds_arrivee = noeuds_arrivee if noeuds_arrivee else None
|
||||
st.multiselect(
|
||||
str(_("pages.analyse.filter_start_nodes")),
|
||||
sorted(depart_nodes),
|
||||
key="analyse_noeuds_depart"
|
||||
)
|
||||
|
||||
return noeuds_depart, noeuds_arrivee
|
||||
departs_selection = st.session_state["analyse_noeuds_depart"]
|
||||
|
||||
supprime_champ_statut("pages.analyse.filter_start_nodes")
|
||||
if departs_selection:
|
||||
for i, val in enumerate(departs_selection):
|
||||
maj_champ_statut(f"pages.analyse.filter_start_nodes.{i}", val)
|
||||
|
||||
def configurer_filtres_vulnerabilite():
|
||||
"""Interface pour configurer les filtres de vulnérabilité."""
|
||||
# ARRIVEES -------------------------------------
|
||||
if "analyse_noeuds_arrivee" not in st.session_state:
|
||||
anciens_arrivees = []
|
||||
i = 0
|
||||
while True:
|
||||
val = get_champ_statut(f"pages.analyse.filter_end_nodes.{i}")
|
||||
if not val:
|
||||
break
|
||||
anciens_arrivees.append(val)
|
||||
i += 1
|
||||
st.session_state["analyse_noeuds_arrivee"] = anciens_arrivees
|
||||
|
||||
st.multiselect(
|
||||
str(_("pages.analyse.filter_end_nodes")),
|
||||
sorted(arrivee_nodes),
|
||||
key="analyse_noeuds_arrivee"
|
||||
)
|
||||
|
||||
arrivees_selection = st.session_state["analyse_noeuds_arrivee"]
|
||||
|
||||
supprime_champ_statut("pages.analyse.filter_end_nodes")
|
||||
if arrivees_selection:
|
||||
for i, val in enumerate(arrivees_selection):
|
||||
maj_champ_statut(f"pages.analyse.filter_end_nodes.{i}", val)
|
||||
|
||||
departs_selection = departs_selection if departs_selection else None
|
||||
arrivees_selection = arrivees_selection if arrivees_selection else None
|
||||
|
||||
return departs_selection, arrivees_selection
|
||||
|
||||
def configurer_filtres_vulnerabilite() -> Tuple[bool, bool, bool, str, bool, str]:
|
||||
"""
|
||||
Interface pour configurer les filtres de vulnérabilité.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, bool, bool, str, bool, str]: Un tuple contenant :
|
||||
- La possibilité de filtrer les ICS
|
||||
- La possibilité de filtrer les IV C
|
||||
- La possibilité de filtrer les IH H
|
||||
- Le type d'application pour les IH H (pays ou acteur)
|
||||
- La possibilité de filtrer les IS G
|
||||
- La logique de filtrage (ou ou and)
|
||||
"""
|
||||
st.markdown("---")
|
||||
st.markdown(f"## {str(_('pages.analyse.vulnerability_filters'))}")
|
||||
|
||||
filtrer_ics = st.checkbox(str(_("pages.analyse.filter_ics")),
|
||||
key="analyse_filtrer_ics")
|
||||
filtrer_ivc = st.checkbox(str(_("pages.analyse.filter_ivc")),
|
||||
key="analyse_filtrer_ivc")
|
||||
filtrer_ihh = st.checkbox(str(_("pages.analyse.filter_ihh")),
|
||||
key="analyse_filtrer_ihh")
|
||||
def init_checkbox(key, champ):
|
||||
if key not in st.session_state:
|
||||
val = get_champ_statut(champ)
|
||||
st.session_state[key] = val.lower() == "true"
|
||||
|
||||
def init_radio(key, champ, options, default):
|
||||
if key not in st.session_state:
|
||||
val = get_champ_statut(champ)
|
||||
st.session_state[key] = val if val in options else default
|
||||
|
||||
# Initialiser les valeurs si F5
|
||||
init_checkbox("analyse_filtrer_ics", "pages.analyse.filter_ics")
|
||||
init_checkbox("analyse_filtrer_ivc", "pages.analyse.filter_ivc")
|
||||
init_checkbox("analyse_filtrer_ihh", "pages.analyse.filter_ihh")
|
||||
init_checkbox("analyse_filtrer_isg", "pages.analyse.filter_isg")
|
||||
init_radio("analyse_ihh_type", "pages.analyse.apply_ihh_filter", ["Pays", "Acteurs"], "Pays")
|
||||
init_radio("analyse_logique_filtrage", "pages.analyse.filter_logic", ["OU", "ET"], "OU")
|
||||
|
||||
filtrer_ics = st.checkbox(str(_("pages.analyse.filter_ics")), key="analyse_filtrer_ics")
|
||||
filtrer_ivc = st.checkbox(str(_("pages.analyse.filter_ivc")), key="analyse_filtrer_ivc")
|
||||
filtrer_ihh = st.checkbox(str(_("pages.analyse.filter_ihh")), key="analyse_filtrer_ihh")
|
||||
|
||||
ihh_type = "Pays"
|
||||
if filtrer_ihh:
|
||||
ihh_type = st.radio(str(_("pages.analyse.apply_ihh_filter")),
|
||||
[str(_("pages.analyse.countries")), str(_("pages.analyse.actors"))],
|
||||
horizontal=True,
|
||||
key="analyse_ihh_type")
|
||||
ihh_type = st.radio(
|
||||
str(_("pages.analyse.apply_ihh_filter")),
|
||||
[str(_("pages.analyse.countries")), str(_("pages.analyse.actors"))],
|
||||
horizontal=True,
|
||||
key="analyse_ihh_type"
|
||||
)
|
||||
|
||||
filtrer_isg = st.checkbox(str(_("pages.analyse.filter_isg")),
|
||||
key="analyse_filtrer_isg")
|
||||
logique_filtrage = st.radio(str(_("pages.analyse.filter_logic")),
|
||||
[str(_("pages.analyse.or")), str(_("pages.analyse.and"))],
|
||||
horizontal=True,
|
||||
key="analyse_logique_filtrage")
|
||||
filtrer_isg = st.checkbox(str(_("pages.analyse.filter_isg")), key="analyse_filtrer_isg")
|
||||
|
||||
logique_options = ["OU", "ET"]
|
||||
logique_labels = {
|
||||
"OU": str(_("pages.analyse.or")),
|
||||
"ET": str(_("pages.analyse.and"))
|
||||
}
|
||||
|
||||
logique_filtrage = st.radio(
|
||||
str(_("pages.analyse.filter_logic")),
|
||||
options=logique_options,
|
||||
format_func=lambda x: logique_labels.get(x, x),
|
||||
horizontal=True,
|
||||
key="analyse_logique_filtrage"
|
||||
)
|
||||
|
||||
# Sauvegarde de l'état
|
||||
maj_champ_statut("pages.analyse.filter_ics", str(filtrer_ics))
|
||||
maj_champ_statut("pages.analyse.filter_ivc", str(filtrer_ivc))
|
||||
maj_champ_statut("pages.analyse.filter_ihh", str(filtrer_ihh))
|
||||
maj_champ_statut("pages.analyse.apply_ihh_filter", ihh_type)
|
||||
maj_champ_statut("pages.analyse.filter_isg", str(filtrer_isg))
|
||||
maj_champ_statut("pages.analyse.filter_logic", logique_filtrage)
|
||||
|
||||
return filtrer_ics, filtrer_ivc, filtrer_ihh, ihh_type, filtrer_isg, logique_filtrage
|
||||
|
||||
def interface_analyse(
|
||||
G_temp: nx.DiGraph,
|
||||
) -> None:
|
||||
"""
|
||||
Interface utilisateur pour l'analyse des graphes.
|
||||
|
||||
def interface_analyse(G_temp):
|
||||
st.markdown(f"# {str(_('pages.analyse.title'))}")
|
||||
with st.expander(str(_("pages.analyse.help")), expanded=False):
|
||||
st.markdown("\n".join(_("pages.analyse.help_content")))
|
||||
Args:
|
||||
G_temp (nx.DiGraph): Le graphe NetworkX à analyser.
|
||||
"""
|
||||
titre = f"# {str(_('pages.analyse.title'))}"
|
||||
maj_champ_statut("pages.analyse.title", titre)
|
||||
st.markdown(titre)
|
||||
html_expander(f"{str(_('pages.analyse.help'))}", content="\n".join(_("pages.analyse.help_content")), open_by_default=False, details_class="details_introduction")
|
||||
st.markdown("---")
|
||||
|
||||
try:
|
||||
@ -148,7 +307,7 @@ def interface_analyse(G_temp):
|
||||
|
||||
# Lancement de l'analyse
|
||||
st.markdown("---")
|
||||
if st.button(str(_("pages.analyse.run_analysis")), type="primary", key="analyse_lancer"):
|
||||
if st.button(str(_("pages.analyse.run_analysis")), type="primary", key="analyse_lancer", icon=":material/graph_4:"):
|
||||
afficher_sankey(
|
||||
G_temp,
|
||||
niveau_depart=niveau_depart,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from typing import Dict, List, Tuple, Optional, Set
|
||||
import streamlit as st
|
||||
from networkx.drawing.nx_agraph import write_dot
|
||||
import pandas as pd
|
||||
@ -25,8 +26,18 @@ niveau_labels = {
|
||||
|
||||
inverse_niveau_labels = {v: k for k, v in niveau_labels.items()}
|
||||
|
||||
def extraire_niveaux(G):
|
||||
"""Extrait les niveaux des nœuds du graphe"""
|
||||
def extraire_niveaux(
|
||||
G: nx.DiGraph,
|
||||
) -> Dict[str, int]:
|
||||
"""
|
||||
Extrait les niveaux des nœuds du graphe.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: Un dictionnaire associant chaque nœud à son niveau.
|
||||
"""
|
||||
niveaux = {}
|
||||
for node, attrs in G.nodes(data=True):
|
||||
niveau_str = attrs.get("niveau")
|
||||
@ -37,17 +48,52 @@ def extraire_niveaux(G):
|
||||
logging.warning(f"Niveau non entier pour le noeud {node}: {niveau_str}")
|
||||
return niveaux
|
||||
|
||||
def extraire_criticite(G, u, v):
|
||||
"""Extrait la criticité d'un lien entre deux nœuds"""
|
||||
def extraire_ics(
|
||||
G: nx.DiGraph,
|
||||
u: str,
|
||||
v: str,
|
||||
) -> float:
|
||||
"""
|
||||
Extrait la criticité d'un lien entre deux nœuds.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
u (str): L'ID du premier nœud.
|
||||
v (str): L'ID du second nœud.
|
||||
|
||||
Returns:
|
||||
float: La valeur de criticité entre les deux nœuds, ou 0 si impossible à extraire.
|
||||
"""
|
||||
data = G.get_edge_data(u, v)
|
||||
if not data:
|
||||
return 0
|
||||
if isinstance(data, dict) and all(isinstance(k, int) for k in data):
|
||||
return float(data[0].get("criticite", 0))
|
||||
return float(data.get("criticite", 0))
|
||||
return float(data[0].get("ics", 0))
|
||||
return float(data.get("ics", 0))
|
||||
|
||||
def extraire_chemins_selon_criteres(
|
||||
G: nx.DiGraph,
|
||||
niveaux: Dict[str, int],
|
||||
niveau_depart: int,
|
||||
noeuds_depart: Optional[List[str]],
|
||||
noeuds_arrivee: Optional[List[str]],
|
||||
minerais: Optional[List[str]],
|
||||
) -> List[Tuple[str, ...]]:
|
||||
"""
|
||||
Extrait les chemins selon les critères spécifiés.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
niveaux (Dict[str, int]): Dictionnaire associant chaque nœud à son niveau.
|
||||
niveau_depart (int): Le niveau de départ sélectionné.
|
||||
noeuds_depart (Optional[List[str]]): Les nœuds de départ si sélectionnés.
|
||||
noeuds_arrivee (Optional[List[str]]): Les nœuds d'arrivée si sélectionnés.
|
||||
minerais (Optional[List[str]]): Les minerais si sélectionnés.
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, ...]]: Liste des chemins valides selon les critères spécifiés.
|
||||
"""
|
||||
|
||||
def extraire_chemins_selon_criteres(G, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais):
|
||||
"""Extrait les chemins selon les critères spécifiés"""
|
||||
chemins = []
|
||||
if noeuds_depart and noeuds_arrivee:
|
||||
for nd in noeuds_depart:
|
||||
@ -70,8 +116,24 @@ def extraire_chemins_selon_criteres(G, niveaux, niveau_depart, noeuds_depart, no
|
||||
|
||||
return chemins
|
||||
|
||||
def verifier_critere_ihh(G, chemin, niveaux, ihh_type):
|
||||
"""Vérifie si un chemin respecte le critère IHH (concentration géographique ou industrielle)"""
|
||||
def verifier_critere_ihh(
|
||||
G: nx.DiGraph,
|
||||
chemin: Tuple[str, ...],
|
||||
niveaux: Dict[str, int],
|
||||
ihh_type: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Vérifie si un chemin respecte le critère IHH (concentration géographique ou industrielle).
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
chemin (Tuple[str, ...]): Un chemin validé selon les critères antérieurs.
|
||||
niveaux (Dict[str, int]): Dictionnaire associant chaque nœud à son niveau.
|
||||
ihh_type (str): Le type d'application pour les IHH (pays ou acteur).
|
||||
|
||||
Returns:
|
||||
bool: True si le chemin respecte le critère IHH, False sinon.
|
||||
"""
|
||||
ihh_field = "ihh_pays" if ihh_type == "Pays" else "ihh_acteurs"
|
||||
for i in range(len(chemin) - 1):
|
||||
u, v = chemin[i], chemin[i + 1]
|
||||
@ -84,8 +146,22 @@ def verifier_critere_ihh(G, chemin, niveaux, ihh_type):
|
||||
return True
|
||||
return False
|
||||
|
||||
def verifier_critere_ivc(G, chemin, niveaux):
|
||||
"""Vérifie si un chemin respecte le critère IVC (criticité par rapport à la concurrence sectorielle)"""
|
||||
def verifier_critere_ivc(
|
||||
G: nx.DiGraph,
|
||||
chemin: Tuple[str, ...],
|
||||
niveaux: Dict[str, int],
|
||||
) -> bool:
|
||||
"""
|
||||
Vérifie si un chemin respecte le critère IVC (criticité par rapport à la concurrence sectorielle).
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
chemin (Tuple[str, ...]): Un chemin validé selon les critères antérieurs.
|
||||
niveaux (Dict[str, int]): Dictionnaire associant chaque nœud à son niveau.
|
||||
|
||||
Returns:
|
||||
bool: True si le chemin respecte le critère IVC, False sinon.
|
||||
"""
|
||||
for i in range(len(chemin) - 1):
|
||||
u = chemin[i]
|
||||
niveau_u = niveaux.get(u)
|
||||
@ -93,8 +169,22 @@ def verifier_critere_ivc(G, chemin, niveaux):
|
||||
return True
|
||||
return False
|
||||
|
||||
def verifier_critere_ics(G, chemin, niveaux):
|
||||
"""Vérifie si un chemin respecte le critère ICS (criticité d'un minerai pour un composant)"""
|
||||
def verifier_critere_ics(
|
||||
G: nx.DiGraph,
|
||||
chemin: Tuple[str, ...],
|
||||
niveaux: Dict[str, int],
|
||||
) -> bool:
|
||||
"""
|
||||
Vérifie si un chemin respecte le critère ICS (criticité d'un minerai pour un composant).
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
chemin (Tuple[str, ...]): Un chemin validé selon les critères antérieurs.
|
||||
niveaux (Dict[str, int]): Dictionnaire associant chaque nœud à son niveau.
|
||||
|
||||
Returns:
|
||||
bool: True si le chemin respecte le critère ICS, False sinon.
|
||||
"""
|
||||
for i in range(len(chemin) - 1):
|
||||
u, v = chemin[i], chemin[i + 1]
|
||||
niveau_u = niveaux.get(u)
|
||||
@ -102,12 +192,26 @@ def verifier_critere_ics(G, chemin, niveaux):
|
||||
|
||||
if ((niveau_u == 1 and niveau_v == 2) or
|
||||
(niveau_u == 1001 and niveau_v == 1002) or
|
||||
(niveau_u == 10 and niveau_v in (1000, 1001))) and extraire_criticite(G, u, v) > 0.66:
|
||||
(niveau_u == 10 and niveau_v in (1000, 1001))) and extraire_ics(G, u, v) > 0.66:
|
||||
return True
|
||||
return False
|
||||
|
||||
def verifier_critere_isg(G, chemin, niveaux):
|
||||
"""Vérifie si un chemin contient un pays instable (ISG ≥ 60)"""
|
||||
def verifier_critere_isg(
|
||||
G: nx.DiGraph,
|
||||
chemin: Tuple[str, ...],
|
||||
niveaux: Dict[str, int],
|
||||
) -> bool:
|
||||
"""
|
||||
Vérifie si un chemin contient un pays instable (ISG ≥ 60).
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
chemin (Tuple[str, ...]): Un chemin validé selon les critères antérieurs.
|
||||
niveaux (Dict[str, int]): Dictionnaire associant chaque nœud à son niveau.
|
||||
|
||||
Returns:
|
||||
bool: True si le chemin contient un pays instable, False sinon.
|
||||
"""
|
||||
for i in range(len(chemin) - 1):
|
||||
u, v = chemin[i], chemin[i + 1]
|
||||
|
||||
@ -120,8 +224,26 @@ def verifier_critere_isg(G, chemin, niveaux):
|
||||
return True
|
||||
return False
|
||||
|
||||
def extraire_liens_filtres(chemins, niveaux, niveau_depart, niveau_arrivee, niveaux_speciaux):
|
||||
"""Extrait les liens des chemins en respectant les niveaux"""
|
||||
def extraire_liens_filtres(
|
||||
chemins: List[Tuple[str, ...]],
|
||||
niveaux: Dict[str, int],
|
||||
niveau_depart: int,
|
||||
niveau_arrivee: int,
|
||||
niveaux_speciaux: List[int]
|
||||
) -> Set[Tuple[str, str]]:
|
||||
"""
|
||||
Extrait les liens des chemins en respectant les niveaux.
|
||||
|
||||
Args:
|
||||
chemins (List[Tuple[str, ...]]): Liste initiale des chemins validés.
|
||||
niveaux (Dict[str, int]): Dictionnaire associant chaque nœud à son niveau.
|
||||
niveau_depart (int): Le niveau de départ sélectionné.
|
||||
niveau_arrivee (int): Le niveau d'arrivée sélectionné.
|
||||
niveaux_speciaux (List[int]): Les niveaux spéciaux à inclure dans l'extraction.
|
||||
|
||||
Returns:
|
||||
Set[Tuple[str, str]]: Ensemble des paires de nœuds liés après filtrage.
|
||||
"""
|
||||
liens = set()
|
||||
for chemin in chemins:
|
||||
for i in range(len(chemin) - 1):
|
||||
@ -135,9 +257,40 @@ def extraire_liens_filtres(chemins, niveaux, niveau_depart, niveau_arrivee, nive
|
||||
liens.add((u, v))
|
||||
return liens
|
||||
|
||||
def filtrer_chemins_par_criteres(G, chemins, niveaux, niveau_depart, niveau_arrivee,
|
||||
filtrer_ics, filtrer_ivc, filtrer_ihh, ihh_type, filtrer_isg, logique_filtrage):
|
||||
"""Filtre les chemins selon les critères de vulnérabilité"""
|
||||
def filtrer_chemins_par_criteres(
|
||||
G: nx.DiGraph,
|
||||
chemins: List[Tuple[str, ...]],
|
||||
niveaux: Dict[str, int],
|
||||
niveau_depart: int,
|
||||
niveau_arrivee: int,
|
||||
filtrer_ics: bool,
|
||||
filtrer_ivc: bool,
|
||||
filtrer_ihh: bool,
|
||||
ihh_type: str,
|
||||
filtrer_isg: bool,
|
||||
logique_filtrage: str,
|
||||
) -> Tuple[Set[Tuple[str, str]], Set[Tuple[str, ...]]]:
|
||||
"""
|
||||
Filtre les chemins selon les critères de vulnérabilité.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
chemins (List[Tuple[str, ...]]): Liste initiale des chemins validés.
|
||||
niveaux (Dict[str, int]): Dictionnaire associant chaque nœud à son niveau.
|
||||
niveau_depart (int): Le niveau de départ sélectionné.
|
||||
niveau_arrivee (int): Le niveau d'arrivée sélectionné.
|
||||
filtrer_ics (bool): Si le filtre ICS doit être appliqué.
|
||||
filtrer_ivc (bool): Si le filtre IVC doit être appliqué.
|
||||
filtrer_ihh (bool): Si le filtre IHH doit être appliqué.
|
||||
ihh_type (str): Le type d'application pour les IHH (pays ou acteur).
|
||||
filtrer_isg (bool): Si le filtre ISG doit être appliqué.
|
||||
logique_filtrage (str): La logique de filtrage (ET OU).
|
||||
|
||||
Returns:
|
||||
Tuple[Set[Tuple[str, str]], Set[Tuple[str, ...]]]: Un tuple contenant :
|
||||
- Les liens validés
|
||||
- Les chemins filtrés finaux
|
||||
"""
|
||||
niveaux_speciaux = [1000, 1001, 1002, 1010, 1011, 1012]
|
||||
|
||||
# Extraction des liens sans filtrage
|
||||
@ -153,7 +306,7 @@ def filtrer_chemins_par_criteres(G, chemins, niveaux, niveau_depart, niveau_arri
|
||||
# Vérification des critères pour ce chemin
|
||||
has_ihh = filtrer_ihh and verifier_critere_ihh(G, chemin, niveaux, ihh_type)
|
||||
has_ivc = filtrer_ivc and verifier_critere_ivc(G, chemin, niveaux)
|
||||
has_criticite = filtrer_ics and verifier_critere_ics(G, chemin, niveaux)
|
||||
has_ics = filtrer_ics and verifier_critere_ics(G, chemin, niveaux)
|
||||
has_isg_critique = filtrer_isg and verifier_critere_isg(G, chemin, niveaux)
|
||||
|
||||
# Appliquer la logique de filtrage
|
||||
@ -161,12 +314,12 @@ def filtrer_chemins_par_criteres(G, chemins, niveaux, niveau_depart, niveau_arri
|
||||
keep = True
|
||||
if filtrer_ihh: keep = keep and has_ihh
|
||||
if filtrer_ivc: keep = keep and has_ivc
|
||||
if filtrer_ics: keep = keep and has_criticite
|
||||
if filtrer_ics: keep = keep and has_ics
|
||||
if filtrer_isg: keep = keep and has_isg_critique
|
||||
if keep:
|
||||
chemins_filtres.add(tuple(chemin))
|
||||
elif logique_filtrage == "OU":
|
||||
if has_ihh or has_ivc or has_criticite or has_isg_critique:
|
||||
if has_ihh or has_ivc or has_ics or has_isg_critique:
|
||||
chemins_filtres.add(tuple(chemin))
|
||||
|
||||
# Extraction des liens après filtrage
|
||||
@ -176,8 +329,18 @@ def filtrer_chemins_par_criteres(G, chemins, niveaux, niveau_depart, niveau_arri
|
||||
|
||||
return liens_filtres, chemins_filtres
|
||||
|
||||
def couleur_criticite(p):
|
||||
"""Retourne la couleur en fonction du niveau de criticité"""
|
||||
def couleur_ics(
|
||||
p: float
|
||||
) -> str:
|
||||
"""
|
||||
Retourne la couleur en fonction du niveau de criticité.
|
||||
|
||||
Args:
|
||||
p (float): Valeur de criticité (entre 0 et 1).
|
||||
|
||||
Returns:
|
||||
str: Couleur représentative du niveau de criticité.
|
||||
"""
|
||||
if p <= 0.33:
|
||||
return "darkgreen"
|
||||
elif p <= 0.66:
|
||||
@ -185,8 +348,22 @@ def couleur_criticite(p):
|
||||
else:
|
||||
return "darkred"
|
||||
|
||||
def edge_info(G, u, v):
|
||||
"""Génère l'info-bulle pour un lien"""
|
||||
def edge_info(
|
||||
G: nx.DiGraph,
|
||||
u: str,
|
||||
v: str
|
||||
) -> str:
|
||||
"""
|
||||
Génère l'info-bulle pour un lien.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
u (str): L'ID du premier nœud.
|
||||
v (str): L'ID du second nœud.
|
||||
|
||||
Returns:
|
||||
str: Texte à afficher dans l'info-bulle pour le lien spécifié.
|
||||
"""
|
||||
# Liste d'attributs à exclure des infobulles des liens
|
||||
attributs_exclus = ["poids", "color", "fontcolor"]
|
||||
|
||||
@ -198,16 +375,38 @@ def edge_info(G, u, v):
|
||||
base = [f"{k}: {v}" for k, v in data.items() if k not in attributs_exclus]
|
||||
return f"{str(_('pages.analyse.sankey.relation'))} : {u} → {v}<br>" + "<br>".join(base)
|
||||
|
||||
def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins):
|
||||
"""Prépare les données pour le graphique Sankey"""
|
||||
def preparer_donnees_sankey(
|
||||
G: nx.DiGraph,
|
||||
liens_chemins: Set[Tuple[str, str]],
|
||||
niveaux: Dict[str, int],
|
||||
chemins: List[Tuple[str, ...]]
|
||||
) -> Tuple[pd.DataFrame, List[str], List[List[str]], List[str], Dict[str, int]]:
|
||||
"""
|
||||
Prépare les données pour le graphique Sankey.
|
||||
|
||||
Args:
|
||||
G (Any): Le graphe NetworkX contenant les données des produits.
|
||||
liens_chemins (Set[Tuple[str, str]]): Ensemble des paires de nœuds liés.
|
||||
niveaux (Dict[str, int]): Dictionnaire associant chaque nœud à son niveau.
|
||||
chemins (List[Tuple[str, ...]]): Liste initiale des chemins validés.
|
||||
|
||||
Returns:
|
||||
Tuple[pd.DataFrame, List[str], List[List[str]], List[str], Dict[str, int]]:
|
||||
Un tuple contenant :
|
||||
- Le DataFrame lié aux chemins filtrés
|
||||
- La liste triée et ordonnée des nœuds
|
||||
- Les listes de données personnalisées pour les nœuds
|
||||
- Les donnnées personnalisées pour les liens
|
||||
- Le dictionnaire associant chaque nœud à son index
|
||||
"""
|
||||
# Liste d'attributs à exclure des infobulles des nœuds
|
||||
node_attributs_exclus = ["fillcolor", "niveau"]
|
||||
|
||||
df_liens = pd.DataFrame(list(liens_chemins), columns=["source", "target"])
|
||||
df_liens = df_liens.groupby(["source", "target"]).size().reset_index(name="value")
|
||||
|
||||
df_liens["criticite"] = df_liens.apply(
|
||||
lambda row: extraire_criticite(G, row["source"], row["target"]), axis=1)
|
||||
df_liens["ics"] = df_liens.apply(
|
||||
lambda row: extraire_ics(G, row["source"], row["target"]), axis=1)
|
||||
df_liens["value"] = 0.1
|
||||
|
||||
# Ne garder que les nœuds effectivement connectés
|
||||
@ -221,7 +420,7 @@ def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins):
|
||||
noeuds_utilises.add(n)
|
||||
|
||||
df_liens["color"] = df_liens.apply(
|
||||
lambda row: couleur_criticite(row["criticite"]) if row["criticite"] > 0 else "white",
|
||||
lambda row: couleur_ics(row["ics"]) if row["ics"] > 0 else "white",
|
||||
axis=1
|
||||
)
|
||||
|
||||
@ -251,8 +450,30 @@ def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins):
|
||||
|
||||
return df_liens, sorted_nodes, customdata, link_customdata, node_indices
|
||||
|
||||
def creer_graphique_sankey(G, niveaux, df_liens, sorted_nodes, customdata, link_customdata, node_indices):
|
||||
"""Crée et retourne le graphique Sankey"""
|
||||
def creer_graphique_sankey(
|
||||
G: nx.DiGraph,
|
||||
niveaux: Dict[str, int],
|
||||
df_liens: pd.DataFrame,
|
||||
sorted_nodes: List[str],
|
||||
customdata: List[str],
|
||||
link_customdata: List[str],
|
||||
node_indices: Dict[str, int],
|
||||
) -> go.Figure:
|
||||
"""
|
||||
Crée et retourne le graphique Sankey.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
niveaux (Dict[str, int]): Dictionnaire associant chaque nœud à son niveau.
|
||||
df_liens (pd.DataFrame): Données du DataFrame lié aux chemins filtrés.
|
||||
sorted_nodes (List[str]): Liste triée et ordonnée des nœuds.
|
||||
customdata (List[str]): Listes de données personnalisées pour les nœuds.
|
||||
link_customdata (List[str]): Données personnalisées pour les liens.
|
||||
node_indices (Dict[str, int]): Dictionnaire associant chaque nœud à son index.
|
||||
|
||||
Returns:
|
||||
go.Figure: Le graphique Sankey final.
|
||||
"""
|
||||
sources = df_liens["source"].map(node_indices).tolist()
|
||||
targets = df_liens["target"].map(node_indices).tolist()
|
||||
values = df_liens["value"].tolist()
|
||||
@ -291,9 +512,22 @@ def creer_graphique_sankey(G, niveaux, df_liens, sorted_nodes, customdata, link_
|
||||
|
||||
return fig
|
||||
|
||||
def exporter_graphe_filtre(G, liens_chemins):
|
||||
"""Gère l'export du graphe filtré au format DOT"""
|
||||
if not st.session_state.get("logged_in", False) or not liens_chemins:
|
||||
def exporter_graphe_filtre(
|
||||
G: nx.DiGraph,
|
||||
liens_chemins: Set[Tuple[str, str]]
|
||||
) -> None:
|
||||
"""
|
||||
Gère l'export du graphe filtré au format DOT.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
liens_chemins (Set[Tuple[str, str]]): Ensemble des paires de nœuds liés.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
from utils.persistance import get_champ_statut
|
||||
if get_champ_statut("login") == "" or not liens_chemins:
|
||||
return
|
||||
|
||||
G_export = nx.DiGraph()
|
||||
@ -321,13 +555,30 @@ def exporter_graphe_filtre(G, liens_chemins):
|
||||
)
|
||||
|
||||
def afficher_sankey(
|
||||
G,
|
||||
niveau_depart, niveau_arrivee,
|
||||
noeuds_depart=None, noeuds_arrivee=None,
|
||||
G: nx.DiGraph,
|
||||
niveau_depart: int, niveau_arrivee: int,
|
||||
noeuds_depart: Optional[List[str]] = None, noeuds_arrivee: Optional[List[str]] = None,
|
||||
minerais=None,
|
||||
filtrer_ics=False, filtrer_ivc=False,
|
||||
filtrer_ihh=False, ihh_type="Pays", filtrer_isg=False,
|
||||
logique_filtrage="OU"):
|
||||
filtrer_ics: bool = False, filtrer_ivc: bool = False,
|
||||
filtrer_ihh: bool = False, ihh_type: str = "Pays", filtrer_isg: bool = False,
|
||||
logique_filtrage: str = "OU") -> None:
|
||||
"""
|
||||
Fonction principale qui s'occupe de la création et de l'affichage du graphique Sankey.
|
||||
|
||||
Args:
|
||||
G: Le graphe NetworkX contenant les données des produits.
|
||||
niveau_depart, niveau_arrivee: Les niveaux initiaux pour le filtrage.
|
||||
noeuds_depart, noeuds_arrivee: Les nœuds initiaux pour le filtrage.
|
||||
minerais: La liste des minerais à inclure dans le filtrage.
|
||||
filtrer_ics, filtrer_ivc: Les booléens pour le filtrage ICS et IVC.
|
||||
filtrer_ihh: Le booléen pour le filtrage IHH.
|
||||
ihh_type: Le type d'application pour les IHH (Pays ou Acteur).
|
||||
filtrer_isg: Le booléen pour le filtrage ISG.
|
||||
logique_filtrage: La logique de filtrage à appliquer (ET OU).
|
||||
|
||||
Returns:
|
||||
go.Figure
|
||||
"""
|
||||
|
||||
# Étape 1 : Extraction des niveaux des nœuds
|
||||
niveaux = extraire_niveaux(G)
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
# __init__.py – app/fiches
|
||||
from .interface import interface_fiches
|
||||
|
||||
__all__ = ["interface_fiches"]
|
||||
|
||||
@ -1,24 +1,47 @@
|
||||
"""
|
||||
Module de génération des fiches pour l'application.
|
||||
|
||||
Fonctions principales :
|
||||
1. `remplacer_latex_par_mathml`
|
||||
2. `markdown_to_html_rgaa`
|
||||
3. `rendu_html`
|
||||
4. `generer_fiche`
|
||||
|
||||
Toutes ces fonctions gèrent la conversion et le rendu de contenu Markdown
|
||||
vers du HTML structuré avec des mathématiques, respectant les règles RGAA.
|
||||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
import yaml
|
||||
import markdown
|
||||
from bs4 import BeautifulSoup
|
||||
from latex2mathml.converter import convert as latex_to_mathml
|
||||
from .utils.fiche_utils import render_fiche_markdown
|
||||
import pypandoc
|
||||
import streamlit as st
|
||||
|
||||
from .utils.dynamic import (
|
||||
from app.fiches.utils import (
|
||||
build_dynamic_sections,
|
||||
build_ivc_sections,
|
||||
build_ihh_sections,
|
||||
build_isg_sections,
|
||||
build_production_sections,
|
||||
build_minerai_sections
|
||||
build_minerai_sections,
|
||||
render_fiche_markdown
|
||||
)
|
||||
|
||||
# === Fonctions de transformation ===
|
||||
def remplacer_latex_par_mathml(markdown_text):
|
||||
def remplacer_latex_par_mathml(markdown_text: str) -> str:
|
||||
"""
|
||||
Remplace les formules LaTeX par des blocs MathML.
|
||||
|
||||
Args:
|
||||
markdown_text (str): Texte Markdown contenant du LaTeX.
|
||||
|
||||
Returns:
|
||||
str: Le même texte avec les formules LaTeX converties en MathML.
|
||||
"""
|
||||
|
||||
def remplacer_bloc_display(match):
|
||||
formule_latex = match.group(1).strip()
|
||||
try:
|
||||
@ -39,7 +62,17 @@ def remplacer_latex_par_mathml(markdown_text):
|
||||
markdown_text = re.sub(r"(?<!\$)\$(.+?)\$(?!\$)", remplacer_bloc_inline, markdown_text, flags=re.DOTALL)
|
||||
return markdown_text
|
||||
|
||||
def markdown_to_html_rgaa(markdown_text, caption_text=None):
|
||||
def markdown_to_html_rgaa(markdown_text: str, caption_text: str|None) -> str:
|
||||
"""
|
||||
Convertit un texte Markdown en HTML structuré accessible.
|
||||
|
||||
Args:
|
||||
markdown_text (str): Texte Markdown à convertir.
|
||||
caption_text (str, optional): Titre du tableau si applicable.
|
||||
|
||||
Returns:
|
||||
str: Le HTML structuré avec des attributs de contraintes ARIA.
|
||||
"""
|
||||
html = markdown.markdown(markdown_text, extensions=['tables'])
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
for i, table in enumerate(soup.find_all("table"), start=1):
|
||||
@ -53,7 +86,16 @@ def markdown_to_html_rgaa(markdown_text, caption_text=None):
|
||||
th["scope"] = "col"
|
||||
return str(soup)
|
||||
|
||||
def rendu_html(contenu_md):
|
||||
def rendu_html(contenu_md: str) -> list[str]:
|
||||
"""
|
||||
Rend le contenu Markdown en HTML avec une structure spécifique.
|
||||
|
||||
Args:
|
||||
contenu_md (str): Texte Markdown à formater.
|
||||
|
||||
Returns:
|
||||
list[str]: Liste d'étapes de construction du HTML final.
|
||||
"""
|
||||
lignes = contenu_md.split('\n')
|
||||
sections_n1 = []
|
||||
section_n1_actuelle = {"titre": None, "intro": [], "sections_n2": {}}
|
||||
@ -84,7 +126,7 @@ def rendu_html(contenu_md):
|
||||
html_output.append(f"<h2>{bloc['titre']}</h2>")
|
||||
if bloc["intro"]:
|
||||
intro_md = remplacer_latex_par_mathml("\n".join(bloc["intro"]))
|
||||
html_intro = markdown_to_html_rgaa(intro_md)
|
||||
html_intro = markdown_to_html_rgaa(intro_md, None)
|
||||
html_output.append(html_intro)
|
||||
for sous_titre, contenu in bloc["sections_n2"].items():
|
||||
contenu_md = remplacer_latex_par_mathml("\n".join(contenu))
|
||||
@ -95,7 +137,25 @@ def rendu_html(contenu_md):
|
||||
|
||||
return html_output
|
||||
|
||||
def generer_fiche(md_source, dossier, nom_fichier, seuils):
|
||||
def generer_fiche(md_source: str, dossier: str, nom_fichier: str, seuils: dict) -> str:
|
||||
"""
|
||||
Génère un document PDF et son HTML correspondant pour une fiche.
|
||||
|
||||
Args:
|
||||
md_source (str): Texte Markdown source contenant la fiche.
|
||||
dossier (str): Dossier/rubrique de destination.
|
||||
nom_fichier (str): Nom du fichier (sans extension).
|
||||
seuils (dict): Valeurs de seuils pour l'analyse.
|
||||
|
||||
Returns:
|
||||
str: Chemin absolu vers le fichier HTML généré.
|
||||
|
||||
Notes:
|
||||
Cette fonction :
|
||||
- Convertit et formate les données Markdown.
|
||||
- Génère un document PDF sous format XeLaTeX.
|
||||
- Crée un document HTML accessible avec des mathématiques.
|
||||
"""
|
||||
front_match = re.match(r"(?s)^---\n(.*?)\n---\n", md_source)
|
||||
context = yaml.safe_load(front_match.group(1)) if front_match else {}
|
||||
|
||||
|
||||
@ -2,25 +2,24 @@
|
||||
import streamlit as st
|
||||
import requests
|
||||
import os
|
||||
import pathlib
|
||||
from utils.translations import _
|
||||
|
||||
from .utils.tickets.display import afficher_tickets_par_fiche
|
||||
from .utils.tickets.creation import formulaire_creation_ticket_dynamique
|
||||
from .utils.tickets.core import rechercher_tickets_gitea
|
||||
|
||||
from config import GITEA_TOKEN, GITEA_URL, ORGANISATION, DEPOT_FICHES, ENV, FICHES_CRITICITE
|
||||
|
||||
from utils.gitea import charger_arborescence_fiches
|
||||
from utils.widgets import html_expander
|
||||
|
||||
from .utils.fiche_utils import load_seuils, doit_regenerer_fiche
|
||||
from app.fiches.utils import (
|
||||
afficher_tickets_par_fiche,
|
||||
formulaire_creation_ticket_dynamique,
|
||||
rechercher_tickets_gitea,
|
||||
load_seuils,
|
||||
doit_regenerer_fiche
|
||||
)
|
||||
from app.fiches.generer import generer_fiche
|
||||
|
||||
from .generer import generer_fiche
|
||||
|
||||
def interface_fiches():
|
||||
def interface_fiches() -> None:
|
||||
st.markdown(f"# {str(_('pages.fiches.title'))}")
|
||||
with st.expander(str(_("pages.fiches.help")), expanded=False):
|
||||
st.markdown("\n".join(_("pages.fiches.help_content")))
|
||||
html_expander(f"{str(_('pages.fiches.help'))}", content="\n".join(_("pages.fiches.help_content")), open_by_default=False, details_class="details_introduction")
|
||||
st.markdown("---")
|
||||
|
||||
if "fiches_arbo" not in st.session_state:
|
||||
@ -32,19 +31,45 @@ def interface_fiches():
|
||||
return
|
||||
|
||||
dossiers = sorted(arbo.keys(), key=lambda x: x.lower())
|
||||
dossier_choisi = st.selectbox(
|
||||
str(_("pages.fiches.choose_category",)),
|
||||
[str(_("pages.fiches.select_folder"))] + dossiers
|
||||
|
||||
if "dossier_choisi" not in st.session_state or st.session_state["dossier_choisi"] not in dossiers:
|
||||
st.session_state["dossier_choisi"] = str(_("pages.fiches.select_folder"))
|
||||
|
||||
try:
|
||||
index_dossier = [str(_("pages.fiches.select_folder"))] + dossiers
|
||||
idx = index_dossier.index(st.session_state["dossier_choisi"])
|
||||
except ValueError:
|
||||
idx = 0
|
||||
|
||||
st.session_state["dossier_choisi"] = st.selectbox(
|
||||
str(_("pages.fiches.choose_category")),
|
||||
[str(_("pages.fiches.select_folder"))] + dossiers,
|
||||
index=idx
|
||||
)
|
||||
|
||||
dossier_choisi = st.session_state["dossier_choisi"]
|
||||
|
||||
if dossier_choisi and dossier_choisi != str(_("pages.fiches.select_folder")):
|
||||
fiches = arbo.get(dossier_choisi, [])
|
||||
noms_fiches = [f['nom'] for f in fiches]
|
||||
fiche_choisie = st.selectbox(
|
||||
|
||||
if "fiche_choisie" not in st.session_state or st.session_state["fiche_choisie"] not in noms_fiches:
|
||||
st.session_state["fiche_choisie"] = str(_("pages.fiches.select_file"))
|
||||
|
||||
try:
|
||||
index_fiche = [str(_("pages.fiches.select_file"))] + noms_fiches
|
||||
idx_fiche = index_fiche.index(st.session_state["fiche_choisie"])
|
||||
except ValueError:
|
||||
idx_fiche = 0
|
||||
|
||||
st.session_state["fiche_choisie"] = st.selectbox(
|
||||
str(_("pages.fiches.choose_file")),
|
||||
[str(_("pages.fiches.select_file"))] + noms_fiches
|
||||
[str(_("pages.fiches.select_file"))] + noms_fiches,
|
||||
index=idx_fiche
|
||||
)
|
||||
|
||||
fiche_choisie = st.session_state["fiche_choisie"]
|
||||
|
||||
if fiche_choisie and fiche_choisie != str(_("pages.fiches.select_file")):
|
||||
fiche_info = next((f for f in fiches if f["nom"] == fiche_choisie), None)
|
||||
if fiche_info:
|
||||
@ -79,11 +104,11 @@ def interface_fiches():
|
||||
with open(html_path, "r", encoding="utf-8") as f:
|
||||
st.markdown(f.read(), unsafe_allow_html=True)
|
||||
|
||||
if st.session_state.get("logged_in", False):
|
||||
from utils.persistance import get_champ_statut
|
||||
if not get_champ_statut("login") == "":
|
||||
pdf_name = nom_fiche + ".pdf"
|
||||
pdf_path = os.path.join("static", "Fiches", dossier_choisi, pdf_name)
|
||||
|
||||
# Bouton de téléchargement du PDF
|
||||
if os.path.exists(pdf_path):
|
||||
with open(pdf_path, "rb") as pdf_file:
|
||||
st.download_button(
|
||||
@ -102,4 +127,4 @@ def interface_fiches():
|
||||
formulaire_creation_ticket_dynamique(fiche_choisie)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"{str(_('pages.fiches.loading_error', 'Erreur lors du chargement de la fiche :'))} {e}")
|
||||
st.error(f"{str(_('pages.fiches.loading_error'))} {e}")
|
||||
|
||||
31
app/fiches/utils/__init__.py
Normal file
31
app/fiches/utils/__init__.py
Normal file
@ -0,0 +1,31 @@
|
||||
from .tickets.display import afficher_tickets_par_fiche
|
||||
from .tickets.creation import formulaire_creation_ticket_dynamique
|
||||
from .tickets.core import rechercher_tickets_gitea
|
||||
from .fiche_utils import (
|
||||
load_seuils,
|
||||
doit_regenerer_fiche
|
||||
)
|
||||
from .dynamic import (
|
||||
build_dynamic_sections,
|
||||
build_ivc_sections,
|
||||
build_ihh_sections,
|
||||
build_isg_sections,
|
||||
build_production_sections,
|
||||
build_minerai_sections
|
||||
)
|
||||
from .fiche_utils import render_fiche_markdown
|
||||
|
||||
__all__ = [
|
||||
"afficher_tickets_par_fiche",
|
||||
"formulaire_creation_ticket_dynamique",
|
||||
"rechercher_tickets_gitea",
|
||||
"load_seuils",
|
||||
"doit_regenerer_fiche",
|
||||
"build_dynamic_sections",
|
||||
"build_ivc_sections",
|
||||
"build_ihh_sections",
|
||||
"build_isg_sections",
|
||||
"build_production_sections",
|
||||
"build_minerai_sections",
|
||||
"render_fiche_markdown"
|
||||
]
|
||||
@ -7,7 +7,22 @@ import streamlit as st
|
||||
from config import FICHES_CRITICITE
|
||||
|
||||
def build_production_sections(md: str) -> str:
|
||||
"""
|
||||
Procédure pour construire et remplacer les sections des fiches de production.
|
||||
|
||||
Cette fonction permet d'extraire les données du markdown, organiser
|
||||
les informations sur les pays d'implantation et acteurs, puis générer
|
||||
un tableau structuré dans l'intervention. Le code prend en charge
|
||||
deux types de fiches : fabrication et assemblage.
|
||||
|
||||
Args:
|
||||
md (str): Fichier Markdown à traiter contenant la structure YAML des sections.
|
||||
|
||||
Returns:
|
||||
str: Markdown modifié avec les tableaux construits selon le type de fiche.
|
||||
"""
|
||||
schema = None
|
||||
type_fiche = None
|
||||
front_match = re.match(r"(?s)^---\n(.*?)\n---\n", md)
|
||||
if front_match:
|
||||
try:
|
||||
@ -23,10 +38,10 @@ def build_production_sections(md: str) -> str:
|
||||
yaml_block = re.search(r"```yaml\n(.+?)\n```", md, re.DOTALL)
|
||||
if not yaml_block:
|
||||
return md
|
||||
|
||||
|
||||
# Capture le bloc YAML complet pour le supprimer plus tard
|
||||
yaml_block_full = yaml_block.group(0)
|
||||
|
||||
|
||||
try:
|
||||
yaml_data = yaml.safe_load(yaml_block.group(1))
|
||||
except Exception as e:
|
||||
@ -133,7 +148,7 @@ def build_production_sections(md: str) -> str:
|
||||
st.warning(f"Aucune section IHH trouvée pour le schéma {schema} dans la fiche technique IHH.")
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors de la lecture/traitement de la fiche IHH: {e}")
|
||||
|
||||
|
||||
# Supprimer le bloc YAML du markdown final
|
||||
md_modifie = md_modifie.replace(yaml_block_full, "")
|
||||
|
||||
|
||||
@ -65,6 +65,21 @@ def _synth(df: pd.DataFrame) -> str:
|
||||
return "\n".join(lignes)
|
||||
|
||||
def build_dynamic_sections(md_raw: str) -> str:
|
||||
"""
|
||||
Procédure pour construire et remplacer les sections dynamiques dans les fiches d'analyse produit (ICS).
|
||||
|
||||
Cette fonction permet de :
|
||||
|
||||
1. Extraire les données structurées en YAML des blocs du markdown.
|
||||
2. Générer un tableau pivotant les données sur la criticité et faisabilité technique.
|
||||
3. Produire une synthèse finale avec l'analyse critique par composant.
|
||||
|
||||
Args:
|
||||
md (str): Contenu brut du fichier Markdown contenant les structures YAML à analyser.
|
||||
|
||||
Returns:
|
||||
str: Le markdown enrichi des tableaux de donnée analysés, ou le contenu original inchangé si aucun bloc structuré n'est trouvé.
|
||||
"""
|
||||
md_raw = _normalize_unicode(md_raw)
|
||||
df = _pairs_dataframe(md_raw)
|
||||
if df.empty:
|
||||
|
||||
@ -51,20 +51,20 @@ def _generer_tableau_produits(produits: dict) -> str:
|
||||
"""Génère un tableau markdown pour les produits."""
|
||||
if not produits:
|
||||
return ""
|
||||
|
||||
|
||||
resultat = ["\n\n## Assemblage des produits\n"]
|
||||
lignes = [
|
||||
"| Produit | Assemblage IHH Pays | Assemblage IHH Acteurs |",
|
||||
"| :-- | :--: | :--: |"
|
||||
]
|
||||
|
||||
|
||||
for produit, data in sorted(produits.items()):
|
||||
pastille_1 = pastille("IHH", data['assemblage_ihh_pays'])
|
||||
pastille_2 = pastille("IHH", data['assemblage_ihh_acteurs'])
|
||||
lignes.append(
|
||||
f"| {produit} | {pastille_1} {data['assemblage_ihh_pays']} | {pastille_2} {data['assemblage_ihh_acteurs']} |"
|
||||
)
|
||||
|
||||
|
||||
resultat.append("\n".join(lignes))
|
||||
return "\n".join(resultat)
|
||||
|
||||
@ -73,20 +73,20 @@ def _generer_tableau_composants(composants: dict) -> str:
|
||||
"""Génère un tableau markdown pour les composants."""
|
||||
if not composants:
|
||||
return ""
|
||||
|
||||
|
||||
resultat = ["\n\n## Fabrication des composants\n"]
|
||||
lignes = [
|
||||
"| Composant | Fabrication IHH Pays | Fabrication IHH Acteurs |",
|
||||
"| :-- | :--: | :--: |"
|
||||
]
|
||||
|
||||
|
||||
for composant, data in sorted(composants.items()):
|
||||
pastille_1 = pastille("IHH", data['fabrication_ihh_pays'])
|
||||
pastille_2 = pastille("IHH", data['fabrication_ihh_acteurs'])
|
||||
lignes.append(
|
||||
f"| {composant} | {pastille_1} {data['fabrication_ihh_pays']} | {pastille_2} {data['fabrication_ihh_acteurs']} |"
|
||||
)
|
||||
|
||||
|
||||
resultat.append("\n".join(lignes))
|
||||
return "\n".join(resultat)
|
||||
|
||||
@ -95,13 +95,13 @@ def _generer_tableau_minerais(minerais: dict) -> str:
|
||||
"""Génère un tableau markdown pour les minerais."""
|
||||
if not minerais:
|
||||
return ""
|
||||
|
||||
|
||||
resultat = ["\n\n## Opérations sur les minerais\n"]
|
||||
lignes = [
|
||||
"| Minerai | Extraction IHH Pays | Extraction IHH Acteurs | Réserves IHH Pays | Traitement IHH Pays | Traitement IHH Acteurs |",
|
||||
"| :-- | :--: | :--: | :--: | :--: | :--: |"
|
||||
]
|
||||
|
||||
|
||||
for minerai, data in sorted(minerais.items()):
|
||||
pastille_1 = pastille("IHH", data['extraction_ihh_pays'])
|
||||
pastille_2 = pastille("IHH", data['extraction_ihh_acteurs'])
|
||||
@ -112,7 +112,7 @@ def _generer_tableau_minerais(minerais: dict) -> str:
|
||||
f"| {minerai} | {pastille_1} {data['extraction_ihh_pays']} | {pastille_2} {data['extraction_ihh_acteurs']} | "
|
||||
f"{pastille_3} {data['reserves_ihh_pays']} | {pastille_4} {data['traitement_ihh_pays']} | {pastille_5} {data['traitement_ihh_acteurs']} |"
|
||||
)
|
||||
|
||||
|
||||
resultat.append("\n".join(lignes))
|
||||
return "\n".join(resultat)
|
||||
|
||||
@ -121,23 +121,37 @@ def _synth_ihh(operations: list[dict]) -> str:
|
||||
"""Génère des tableaux de synthèse pour les indices HHI à partir des opérations."""
|
||||
# Extraction et organisation des données
|
||||
data_by_item = _extraire_donnees_operations(operations)
|
||||
|
||||
|
||||
# Catégorisation des items
|
||||
produits = {k: v for k, v in data_by_item.items() if v['type'] == 'produit'}
|
||||
composants = {k: v for k, v in data_by_item.items() if v['type'] == 'composant'}
|
||||
minerais = {k: v for k, v in data_by_item.items() if v['type'] == 'minerai'}
|
||||
|
||||
|
||||
# Génération des tableaux pour chaque catégorie
|
||||
tableaux = [
|
||||
_generer_tableau_produits(produits),
|
||||
_generer_tableau_composants(composants),
|
||||
_generer_tableau_minerais(minerais)
|
||||
]
|
||||
|
||||
|
||||
# Assemblage du résultat final
|
||||
return "\n".join([t for t in tableaux if t])
|
||||
|
||||
def build_ihh_sections(md: str) -> str:
|
||||
"""
|
||||
Fonction principale pour générer les sections dynamiques dans le markdown, spécifiquement dédiée à l'analyse des indices IHH.
|
||||
|
||||
La fonction gère les différents types de données présents dans les fiches, notamment :
|
||||
- Les opérations d'extraction et de traitement du minerai
|
||||
- L'assemblage des produits finaux
|
||||
- La fabrication des composants intermédiaires
|
||||
|
||||
Args:
|
||||
md (str): Contenu brut du fichier Markdown contenant les structures YAML à analyser.
|
||||
|
||||
Returns:
|
||||
str: Le markdown enrichi avec les tableaux de donnée analysés, ou le contenu original inchangé si aucun bloc structuré n'est trouvé.
|
||||
"""
|
||||
segments = []
|
||||
operations = []
|
||||
intro = None
|
||||
|
||||
@ -25,6 +25,20 @@ def _synth_isg(md: str) -> str:
|
||||
return "\n".join(lignes)
|
||||
|
||||
def build_isg_sections(md: str) -> str:
|
||||
"""
|
||||
Fonction principale pour générer les sections dynamiques dans le markdown, spécifiquement dédiée à l'analyse des indices ISG.
|
||||
|
||||
La fonction gère :
|
||||
- La structure YAML front-matter pour vérifier si c'est bien un tableau ISG
|
||||
- L'extraction et tri du pays selon la valeur ISG
|
||||
- Le formatage des données de WGI, FSI, NDGain et ISG dans le tableau final
|
||||
|
||||
Args:
|
||||
md (str): Contenu brut du fichier Markdown contenant les structures YAML à analyser.
|
||||
|
||||
Returns:
|
||||
str: Le markdown enrichi avec le tableau de donnée analysé pour l'indice ISG, ou le contenu original inchangé si aucun bloc structuré n'est trouvé.
|
||||
"""
|
||||
front_match = re.match(r"(?s)^---\n(.*?)\n---\n", md)
|
||||
if front_match:
|
||||
front_matter = yaml.safe_load(front_match.group(1))
|
||||
|
||||
@ -31,7 +31,20 @@ def _ivc_segments(md: str):
|
||||
yield None, md[pos:] # reste éventuel
|
||||
|
||||
def build_ivc_sections(md: str) -> str:
|
||||
"""Remplace les blocs YAML minerai + segment avec rendu Jinja2, conserve l'intro."""
|
||||
"""
|
||||
Fonction principale pour générer les sections dynamiques dans le markdown, spécifiquement dédiée à l'analyse des Indices de Vulnérabilité Complète (IVC).
|
||||
|
||||
La fonction gère :
|
||||
- L'extraction et tri des données IVC pour chaque minerai
|
||||
- Le formatage des données d'IVC, Vulnérabilité, etc. dans le tableau final
|
||||
- La génération du tableau synthétique pour l'analyse globale
|
||||
|
||||
Args:
|
||||
md (str): Contenu brut du fichier Markdown contenant les structures YAML à analyser.
|
||||
|
||||
Returns:
|
||||
str: Le markdown enrichi avec le tableau de donnée analysé pour l'indice IVC, ou le contenu original inchangé si aucun bloc structuré n'est trouvé.
|
||||
"""
|
||||
segments = []
|
||||
minerais = [] # Pour collecter les données de chaque minerai
|
||||
intro = None
|
||||
|
||||
@ -458,7 +458,6 @@ def build_minerai_ics_composant_section(md: str) -> str:
|
||||
|
||||
return md
|
||||
|
||||
|
||||
def build_minerai_sections(md: str) -> str:
|
||||
"""Traite les fiches de minerai et génère les tableaux des producteurs."""
|
||||
# Extraire le type de fiche depuis l'en-tête YAML
|
||||
@ -567,6 +566,8 @@ def build_minerai_sections(md: str) -> str:
|
||||
md,
|
||||
flags=re.DOTALL
|
||||
)
|
||||
# suppression pour le dernier minerai dans la fiche IHH
|
||||
md = re.sub(r"# Tableaux de synthèse.*<!---- AUTO-END:SECTION-IHH-TRAITEMENT -->", "", md, flags=re.DOTALL)
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors de la génération des sections IHH: {e}")
|
||||
|
||||
|
||||
@ -1,18 +1,34 @@
|
||||
# pastille.py
|
||||
|
||||
from typing import Any
|
||||
|
||||
PASTILLE_ICONS = {
|
||||
"vert": "✅",
|
||||
"orange": "🔶",
|
||||
"rouge": "🔴"
|
||||
}
|
||||
|
||||
def pastille(indice: str, valeur: Any, seuils: dict = None) -> str:
|
||||
def pastille(indice: str, valeur: str) -> str:
|
||||
"""Renvoie l'icône Unicode correspondante à la pastille en fonction de sa valeur et des seuils.
|
||||
|
||||
La pastille prend une couleur (vert, orange ou rouge) selon la valeur
|
||||
de l'indicateur par rapport aux seuils définis. Les icônes sont définies
|
||||
comme des emojis Unicode pour faciliter leur affichage dans les interfaces
|
||||
interactives comme Streamlit.
|
||||
|
||||
Args:
|
||||
indice (str): Clé permettant d'accéder au seuil spécifique.
|
||||
Exemples de valeurs possibles : "criticite", "confort".
|
||||
valeur (Any): Valeur numérique à comparer aux seuils.
|
||||
Généralement une float, mais peut être convertie automatiquement
|
||||
en nombre si possible.
|
||||
|
||||
Returns:
|
||||
str: Une icône Unicode correspondante à la pastille. Si aucune icône n'est définie,
|
||||
une chaîne vide est renvoyée.
|
||||
"""
|
||||
try:
|
||||
import streamlit as st
|
||||
seuils = seuils or st.session_state.get("seuils", {})
|
||||
if indice not in seuils:
|
||||
seuils = st.session_state.get("seuils", {})
|
||||
if seuils and indice not in seuils:
|
||||
return ""
|
||||
|
||||
seuil = seuils[indice]
|
||||
|
||||
@ -12,22 +12,35 @@ Usage :
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import frontmatter, yaml, jinja2, re, pathlib
|
||||
from typing import Dict
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
from utils.gitea import recuperer_date_dernier_commit
|
||||
|
||||
|
||||
def load_seuils(path: str | pathlib.Path = "config/indices_seuils.yaml") -> Dict:
|
||||
"""Charge le fichier YAML des seuils et renvoie le dict 'seuils'."""
|
||||
"""Charge le fichier YAML des seuils et renvoie le dict 'seuils'.
|
||||
|
||||
Args:
|
||||
path (str | pathlib.Path, optional): Chemin vers le fichier des seuils. Defaults to "config/indices_seuils.yaml".
|
||||
|
||||
Returns:
|
||||
Dict: Dictionnaire contenant les seuils.
|
||||
"""
|
||||
data = yaml.safe_load(pathlib.Path(path).read_text(encoding="utf-8"))
|
||||
return data.get("seuils", {})
|
||||
|
||||
|
||||
def _migrate_metadata(meta: Dict) -> Dict:
|
||||
"""Normalise les clés YAML (ex : sheet_type → type_fiche)."""
|
||||
"""Normalise les clés YAML (ex : sheet_type → type_fiche).
|
||||
|
||||
Args:
|
||||
meta (Dict): Métadonnées à normaliser.
|
||||
|
||||
Returns:
|
||||
Dict: Métadonnées normalisées.
|
||||
"""
|
||||
keymap = {
|
||||
"sheet_type": "type_fiche",
|
||||
"indice_code": "indice_court", # si besoin
|
||||
@ -37,12 +50,24 @@ def _migrate_metadata(meta: Dict) -> Dict:
|
||||
meta[new] = meta.pop(old)
|
||||
return meta
|
||||
|
||||
def render_fiche_markdown(
|
||||
md_text: str,
|
||||
seuils: Dict,
|
||||
license_path: str = "assets/licence.md"
|
||||
) -> str:
|
||||
"""Renvoie la fiche rendue (Markdown) avec les placeholders remplacés et le tableau de version.
|
||||
|
||||
def render_fiche_markdown(md_text: str, seuils: Dict, license_path: str = "assets/licence.md") -> str:
|
||||
"""Renvoie la fiche rendue (Markdown) :
|
||||
– placeholders Jinja2 remplacés ({{ … }})
|
||||
– table seuils injectée via dict 'seuils'.
|
||||
- licence ajoutée après le tableau de version et avant le premier titre de niveau 2
|
||||
Args:
|
||||
md_text (str): Contenu Markdown brut.
|
||||
seuils (Dict): Tableau des versions.
|
||||
license_path (str, optional): Chemin vers le fichier de licence. Defaults to "assets/licence.md".
|
||||
|
||||
Returns:
|
||||
str: Fiche Markdown rendue avec les placeholders remplacés et la table de version.
|
||||
|
||||
Note:
|
||||
- Les licences sont ajoutées après le tableau de version.
|
||||
- Les titres de niveau 2 doivent être présents pour l'insertion automatique de licence.
|
||||
"""
|
||||
post = frontmatter.loads(md_text)
|
||||
meta = _migrate_metadata(dict(post.metadata))
|
||||
@ -68,11 +93,11 @@ def render_fiche_markdown(md_text: str, seuils: Dict, license_path: str = "asset
|
||||
# Charger le contenu de la licence
|
||||
try:
|
||||
license_content = pathlib.Path(license_path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# Insérer la licence après le tableau de version et avant le premier titre h2
|
||||
# Trouver la position du premier titre h2
|
||||
h2_match = re.search(r"^## ", rendered_body, flags=re.M)
|
||||
|
||||
|
||||
if h2_match:
|
||||
h2_position = h2_match.start()
|
||||
rendered_body = f"{rendered_body[:h2_position]}\n\n{license_content}\n\n{rendered_body[h2_position:]}"
|
||||
@ -81,19 +106,51 @@ def render_fiche_markdown(md_text: str, seuils: Dict, license_path: str = "asset
|
||||
rendered_body = f"{rendered_body}\n\n{license_content}"
|
||||
except Exception as e:
|
||||
# En cas d'erreur lors de la lecture du fichier de licence, continuer sans l'ajouter
|
||||
import streamlit as st
|
||||
st.error(e)
|
||||
pass
|
||||
|
||||
return rendered_body
|
||||
|
||||
|
||||
def fichier_plus_recent(chemin_fichier, reference):
|
||||
def fichier_plus_recent(
|
||||
chemin_fichier: str|None,
|
||||
reference: datetime
|
||||
) -> bool:
|
||||
"""Vérifie si un fichier est plus récent que la référence donnée.
|
||||
|
||||
Args:
|
||||
chemin_fichier (str): Chemin vers le fichier à vérifier.
|
||||
reference (datetime): Référence temporelle de comparaison.
|
||||
|
||||
Returns:
|
||||
bool: True si le fichier est plus récent, False sinon.
|
||||
"""
|
||||
try:
|
||||
modif = datetime.fromtimestamp(os.path.getmtime(chemin_fichier), tz=timezone.utc)
|
||||
return modif > reference
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def doit_regenerer_fiche(html_path, fiche_type, fiche_choisie, commit_url, fichiers_criticite):
|
||||
def doit_regenerer_fiche(
|
||||
html_path: str,
|
||||
fiche_type: str,
|
||||
fiche_choisie: str,
|
||||
commit_url: str,
|
||||
fichiers_criticite: Dict[str, str]
|
||||
) -> bool:
|
||||
"""Détermine si une fiche doit être regénérée.
|
||||
|
||||
Args:
|
||||
html_path (str): Chemin vers le fichier HTML.
|
||||
fiche_type (str): Type de la fiche.
|
||||
fiche_choisie (str): Nom choisi pour la fiche.
|
||||
commit_url (str): URL du dernier commit.
|
||||
fichiers_criticite (Dict[str, str]): Dictionnaire des fichiers critiques.
|
||||
|
||||
Returns:
|
||||
bool: True si la fiche doit être regénérée, False sinon.
|
||||
"""
|
||||
if not os.path.exists(html_path):
|
||||
return True
|
||||
|
||||
|
||||
@ -110,10 +110,6 @@ def creer_ticket_gitea(titre, corps, labels):
|
||||
|
||||
reponse = gitea_request("post", url, headers={"Content-Type": "application/json"}, data=json.dumps(data))
|
||||
if not reponse:
|
||||
return
|
||||
|
||||
issue_url = reponse.json().get("html_url", "")
|
||||
if issue_url:
|
||||
st.success(f"{str(_('pages.fiches.tickets.created_success'))} [Voir le ticket]({issue_url})")
|
||||
return False
|
||||
else:
|
||||
st.success(str(_('pages.fiches.tickets.created')))
|
||||
return True
|
||||
|
||||
@ -74,6 +74,8 @@ def afficher_controles_formulaire():
|
||||
col1, col2 = st.columns(2)
|
||||
if col1.button(str(_("pages.fiches.tickets.preview"))):
|
||||
st.session_state.previsualiser = True
|
||||
# S'assurer que l'expander reste ouvert en mode prévisualisation
|
||||
st.session_state.expander_state = True
|
||||
if col2.button(str(_("pages.fiches.tickets.cancel"))):
|
||||
st.session_state.previsualiser = False
|
||||
st.rerun()
|
||||
@ -81,6 +83,23 @@ def afficher_controles_formulaire():
|
||||
|
||||
def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible):
|
||||
"""Gère la prévisualisation et la soumission du ticket."""
|
||||
# Si nous avons tenté de créer un ticket (succès ou erreur)
|
||||
if st.session_state.get("ticket_cree", False) or st.session_state.get("ticket_erreur", False):
|
||||
if st.session_state.get("ticket_cree", False):
|
||||
st.success(str(_("pages.fiches.tickets.created_success")))
|
||||
else:
|
||||
st.error(str(_("pages.fiches.tickets.creation_error")))
|
||||
|
||||
if st.button(str(_("pages.fiches.tickets.continue"))):
|
||||
# Réinitialiser le formulaire et cacher l'expander
|
||||
st.session_state.ticket_cree = False
|
||||
st.session_state.ticket_erreur = False
|
||||
st.session_state.previsualiser = False
|
||||
st.session_state.expander_state = False
|
||||
st.rerun()
|
||||
return
|
||||
|
||||
# Si nous ne sommes pas en mode prévisualisation, ne rien afficher
|
||||
if not st.session_state.get("previsualiser", False):
|
||||
return
|
||||
|
||||
@ -101,15 +120,31 @@ def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible):
|
||||
labels_ids.append(labels_existants["Backlog"])
|
||||
|
||||
corps = construire_corps_ticket_markdown(reponses)
|
||||
creer_ticket_gitea(titre_ticket, corps, labels_ids)
|
||||
resultat = creer_ticket_gitea(titre_ticket, corps, labels_ids)
|
||||
|
||||
# Marquer le résultat et ouvrir l'expander pour afficher le résultat
|
||||
st.session_state.ticket_cree = resultat
|
||||
st.session_state.ticket_erreur = not resultat
|
||||
st.session_state.previsualiser = False
|
||||
st.success(str(_("pages.fiches.tickets.created")))
|
||||
st.session_state.expander_state = True
|
||||
st.rerun()
|
||||
|
||||
|
||||
def formulaire_creation_ticket_dynamique(fiche_selectionnee):
|
||||
"""Fonction principale pour le formulaire de création de ticket."""
|
||||
with st.expander(str(_("pages.fiches.tickets.create_new")), expanded=False):
|
||||
# Initialiser l'état de l'expander si ce n'est pas déjà fait
|
||||
if "expander_state" not in st.session_state:
|
||||
st.session_state.expander_state = False
|
||||
|
||||
with st.expander(str(_("pages.fiches.tickets.create_new")), expanded=st.session_state.expander_state):
|
||||
# Initialiser les états si ce n'est pas déjà fait
|
||||
if "ticket_cree" not in st.session_state:
|
||||
st.session_state.ticket_cree = False
|
||||
if "ticket_erreur" not in st.session_state:
|
||||
st.session_state.ticket_erreur = False
|
||||
if "previsualiser" not in st.session_state:
|
||||
st.session_state.previsualiser = False
|
||||
|
||||
# Chargement et vérification du modèle
|
||||
contenu_modele = charger_modele_ticket()
|
||||
if not contenu_modele:
|
||||
@ -119,11 +154,21 @@ def formulaire_creation_ticket_dynamique(fiche_selectionnee):
|
||||
# Traitement du modèle et génération du formulaire
|
||||
sections = parser_modele_ticket(contenu_modele)
|
||||
labels, selected_ops, cible = generer_labels(fiche_selectionnee)
|
||||
reponses = creer_champs_formulaire(sections, fiche_selectionnee)
|
||||
|
||||
# Gestion des contrôles et de la prévisualisation
|
||||
afficher_controles_formulaire()
|
||||
gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible)
|
||||
|
||||
# Créer le formulaire et gérer ses états
|
||||
if st.session_state.ticket_cree or st.session_state.ticket_erreur:
|
||||
# Si le ticket a été créé ou a échoué, afficher le message approprié et le bouton continuer
|
||||
gerer_previsualisation_et_soumission({}, labels, selected_ops, cible)
|
||||
else:
|
||||
# Sinon afficher le formulaire normal
|
||||
reponses = creer_champs_formulaire(sections, fiche_selectionnee)
|
||||
|
||||
# Afficher les contrôles uniquement si nous ne sommes pas en mode prévisualisation
|
||||
if not st.session_state.previsualiser:
|
||||
afficher_controles_formulaire()
|
||||
|
||||
# Gérer la prévisualisation et soumission
|
||||
gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible)
|
||||
|
||||
|
||||
def charger_modele_ticket():
|
||||
|
||||
@ -6,7 +6,6 @@ import re
|
||||
from collections import defaultdict
|
||||
from dateutil import parser
|
||||
from utils.translations import _
|
||||
from .core import rechercher_tickets_gitea
|
||||
|
||||
|
||||
def extraire_statut_par_label(ticket):
|
||||
|
||||
42
app/ia_nalyse/README.md
Normal file
42
app/ia_nalyse/README.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Module d'Analyse
|
||||
|
||||
Ce module permet d'analyser les relations entre les différentes parties de la chaîne de fabrication du numérique. Il offre des outils pour visualiser les flux et identifier les vulnérabilités potentielles dans la chaîne d'approvisionnement.
|
||||
|
||||
## Structure du module
|
||||
|
||||
Le module d'analyse comprend deux composants principaux :
|
||||
|
||||
- **interface.py** : Gère l'interface utilisateur pour paramétrer les analyses
|
||||
- **sankey.py** : Génère les diagrammes de flux (Sankey) pour visualiser les relations entre les éléments
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### Interface d'analyse
|
||||
L'interface permet de :
|
||||
- Sélectionner les niveaux de départ et d'arrivée pour l'analyse (produits, composants, minerais, opérations, etc.)
|
||||
- Filtrer les données par minerais spécifiques
|
||||
- Effectuer une sélection fine des nœuds de départ et d'arrivée
|
||||
- Appliquer des filtres pour identifier les vulnérabilités :
|
||||
- Filtres ICS (criticité pour un composant)
|
||||
- Filtres IVC (criticité par rapport à la concurrence sectorielle)
|
||||
- Filtres IHH (concentration géographique ou industrielle)
|
||||
- Filtres ISG (instabilité des pays)
|
||||
- Choisir la logique de filtrage (OU, ET)
|
||||
|
||||
### Visualisation Sankey
|
||||
Le module génère des diagrammes Sankey qui :
|
||||
- Affichent les flux entre les différents éléments de la chaîne
|
||||
- Mettent en évidence les relations de dépendance
|
||||
- Permettent d'identifier visuellement les goulots d'étranglement potentiels
|
||||
- Sont interactifs et permettent d'explorer la chaîne de valeur
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. Sélectionnez un niveau de départ (ex : Produit final, Composant)
|
||||
2. Choisissez un niveau d'arrivée (ex : Pays géographique, Acteur d'opération)
|
||||
3. Si nécessaire, filtrez par minerais spécifiques
|
||||
4. Affinez votre sélection avec des nœuds de départ et d'arrivée spécifiques
|
||||
5. Appliquez les filtres de vulnérabilité souhaités
|
||||
6. Lancez l'analyse pour générer le diagramme Sankey
|
||||
|
||||
Le diagramme résultant permet d'identifier visuellement les relations et points de vulnérabilité dans la chaîne d'approvisionnement du numérique.
|
||||
2
app/ia_nalyse/__init__.py
Normal file
2
app/ia_nalyse/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# __init__.py – app/fiches
|
||||
from .interface import interface_ia_nalyse
|
||||
311
app/ia_nalyse/interface.py
Normal file
311
app/ia_nalyse/interface.py
Normal file
@ -0,0 +1,311 @@
|
||||
from typing import List, Optional, Tuple, Dict, Set
|
||||
import streamlit as st
|
||||
import networkx as nx
|
||||
from utils.translations import _
|
||||
from utils.widgets import html_expander
|
||||
|
||||
from utils.graph_utils import (
|
||||
extraire_chemins_depuis,
|
||||
extraire_chemins_vers
|
||||
)
|
||||
|
||||
from batch_ia.batch_utils import soumettre_batch, statut_utilisateur, nettoyage_post_telechargement
|
||||
|
||||
niveau_labels = {
|
||||
0: "Produit final",
|
||||
1: "Composant",
|
||||
2: "Minerai",
|
||||
10: "Opération",
|
||||
11: "Pays d'opération",
|
||||
12: "Acteur d'opération",
|
||||
99: "Pays géographique"
|
||||
}
|
||||
|
||||
inverse_niveau_labels = {v: k for k, v in niveau_labels.items()}
|
||||
|
||||
|
||||
def preparer_graphe(
|
||||
G: nx.DiGraph,
|
||||
) -> Tuple[nx.DiGraph, Dict[str, int]]:
|
||||
"""
|
||||
Nettoie et prépare le graphe pour l'analyse.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
|
||||
Returns:
|
||||
Tuple[nx.DiGraph, Dict[str, int]]: Un tuple contenant :
|
||||
- Le graphe NetworkX proprement configuré
|
||||
- Un dictionnaire des niveaux associés aux nœuds
|
||||
"""
|
||||
niveaux_temp = {
|
||||
node: int(str(attrs.get("niveau")).strip('"'))
|
||||
for node, attrs in G.nodes(data=True)
|
||||
if attrs.get("niveau") and str(attrs.get("niveau")).strip('"').isdigit()
|
||||
}
|
||||
G.remove_nodes_from([n for n in G.nodes() if n not in niveaux_temp])
|
||||
G.remove_nodes_from(
|
||||
[n for n in G.nodes() if niveaux_temp.get(n) == 10 and 'Reserves' in n])
|
||||
return G, niveaux_temp
|
||||
|
||||
|
||||
def selectionner_minerais(
|
||||
G: nx.DiGraph,
|
||||
) -> Optional[List[str]]:
|
||||
"""
|
||||
Interface pour sélectionner les minerais si nécessaire.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
|
||||
Returns:
|
||||
Optional[List[str]]: La liste des minerais si une sélection a été effectuée,
|
||||
- None sinon
|
||||
"""
|
||||
minerais_selection = None
|
||||
|
||||
st.markdown(f"## {str(_('pages.ia_nalyse.select_minerals'))}")
|
||||
# Tous les nœuds de niveau 2 (minerai)
|
||||
minerais_nodes = sorted([
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("niveau") and int(str(d.get("niveau")).strip('"')) == 2
|
||||
])
|
||||
|
||||
minerais_selection = st.multiselect(
|
||||
str(_("pages.ia_nalyse.filter_by_minerals")),
|
||||
minerais_nodes,
|
||||
key="analyse_minerais"
|
||||
)
|
||||
|
||||
return minerais_selection
|
||||
|
||||
|
||||
def selectionner_noeuds(
|
||||
G: nx.DiGraph,
|
||||
niveaux_temp: Dict[str, int],
|
||||
niveau_depart: int,
|
||||
) -> Tuple[Optional[List[str]], List[str]]:
|
||||
"""
|
||||
Interface pour sélectionner les nœuds spécifiques de départ et d'arrivée.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
niveaux_temp (Dict[str, int]): Dictionnaire associant chaque nœud à son niveau.
|
||||
niveau_depart (int): Le niveau de départ sélectionné.
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[List[str]], List[str]]: Un tuple contenant :
|
||||
- La liste des nœuds de départ si une sélection a été effectuée,
|
||||
- None sinon
|
||||
- La liste des nœuds d'arrivée
|
||||
"""
|
||||
st.markdown("---")
|
||||
st.markdown(f"## {str(_('pages.ia_nalyse.fine_selection'))}")
|
||||
|
||||
depart_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_depart]
|
||||
noeuds_arrivee = [n for n in G.nodes() if niveaux_temp.get(n) == 99]
|
||||
|
||||
noeuds_depart = st.multiselect(str(_("pages.ia_nalyse.filter_start_nodes")),
|
||||
sorted(depart_nodes),
|
||||
key="analyse_noeuds_depart")
|
||||
|
||||
noeuds_depart = noeuds_depart if noeuds_depart else None
|
||||
|
||||
return noeuds_depart, noeuds_arrivee
|
||||
|
||||
def extraire_niveaux(
|
||||
G: nx.DiGraph,
|
||||
) -> Dict[str, int]:
|
||||
"""
|
||||
Extrait les niveaux des nœuds du graphe.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: Un dictionnaire associant chaque nœud à son niveau.
|
||||
"""
|
||||
niveaux = {}
|
||||
for node, attrs in G.nodes(data=True):
|
||||
niveau_str = attrs.get("niveau")
|
||||
if niveau_str:
|
||||
niveaux[node] = int(str(niveau_str).strip('"'))
|
||||
return niveaux
|
||||
|
||||
def extraire_chemins_selon_criteres(
|
||||
G: nx.DiGraph,
|
||||
niveaux: Dict[str, int],
|
||||
niveau_depart: int,
|
||||
noeuds_depart: Optional[List[str]],
|
||||
noeuds_arrivee: Optional[List[str]],
|
||||
minerais: Optional[List[str]],
|
||||
) -> List[Tuple[str, ...]]:
|
||||
"""
|
||||
Extrait les chemins selon les critères spécifiés.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
niveaux (Dict[str, int]): Dictionnaire associant chaque nœud à son niveau.
|
||||
niveau_depart (int): Le niveau de départ sélectionné.
|
||||
noeuds_depart (Optional[List[str]]): Les nœuds de départ si sélectionnés.
|
||||
noeuds_arrivee (Optional[List[str]]): Les nœuds d'arrivée si sélectionnés.
|
||||
minerais (Optional[List[str]]): Les minerais si sélectionnés.
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, ...]]: Liste des chemins valides selon les critères spécifiés.
|
||||
"""
|
||||
chemins = []
|
||||
if noeuds_depart and noeuds_arrivee:
|
||||
for nd in noeuds_depart:
|
||||
for na in noeuds_arrivee:
|
||||
tous_chemins = extraire_chemins_depuis(G, nd)
|
||||
chemins.extend([chemin for chemin in tous_chemins if na in chemin])
|
||||
elif noeuds_depart:
|
||||
for nd in noeuds_depart:
|
||||
chemins.extend(extraire_chemins_depuis(G, nd))
|
||||
elif noeuds_arrivee:
|
||||
for na in noeuds_arrivee:
|
||||
chemins.extend(extraire_chemins_vers(G, na, niveau_depart))
|
||||
else:
|
||||
sources_depart = [n for n in G.nodes() if niveaux.get(n) == niveau_depart]
|
||||
for nd in sources_depart:
|
||||
chemins.extend(extraire_chemins_depuis(G, nd))
|
||||
|
||||
if minerais:
|
||||
chemins = [chemin for chemin in chemins if any(n in minerais for n in chemin)]
|
||||
|
||||
return chemins
|
||||
|
||||
def exporter_graphe_filtre(
|
||||
G: nx.DiGraph,
|
||||
liens_chemins: Set[Tuple[str, str]],
|
||||
) -> nx.DiGraph|None:
|
||||
"""
|
||||
Gère l'export du graphe filtré au format DOT.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
liens_chemins (Set[Tuple[str, str]]): Ensemble des paires de nœuds liés.
|
||||
|
||||
Returns:
|
||||
nx.DiGraph: le graphe exporté
|
||||
- Sinon aucun résultat (None)
|
||||
"""
|
||||
from utils.persistance import get_champ_statut
|
||||
if get_champ_statut("login") == "" or not liens_chemins:
|
||||
return
|
||||
|
||||
G_export = nx.DiGraph()
|
||||
for u, v in liens_chemins:
|
||||
G_export.add_node(u, **G.nodes[u])
|
||||
G_export.add_node(v, **G.nodes[v])
|
||||
data = G.get_edge_data(u, v)
|
||||
if isinstance(data, dict) and all(isinstance(k, int) for k in data):
|
||||
G_export.add_edge(u, v, **data[0])
|
||||
elif isinstance(data, dict):
|
||||
G_export.add_edge(u, v, **data)
|
||||
else:
|
||||
G_export.add_edge(u, v)
|
||||
|
||||
return(G_export)
|
||||
|
||||
def extraire_liens_filtres(
|
||||
chemins: List[Tuple[str, ...]],
|
||||
niveaux: Dict[str, int],
|
||||
niveau_depart: int,
|
||||
niveau_arrivee: int,
|
||||
niveaux_speciaux: List[int]
|
||||
) -> Set[Tuple[str, str]]:
|
||||
"""
|
||||
Extrait les liens des chemins en respectant les niveaux.
|
||||
|
||||
Args:
|
||||
chemins (List[Tuple[str, ...]]): Liste initiale des chemins validés.
|
||||
niveaux (Dict[str, int]): Dictionnaire associant chaque nœud à son niveau.
|
||||
niveau_depart (int): Le niveau de départ sélectionné.
|
||||
niveau_arrivee (int): Le niveau d'arrivée sélectionné.
|
||||
niveaux_speciaux (List[int]): Les niveaux spéciaux à inclure dans l'extraction.
|
||||
|
||||
Returns:
|
||||
Set[Tuple[str, str]]: Ensemble des paires de nœuds liés après filtrage.
|
||||
"""
|
||||
liens = set()
|
||||
for chemin in chemins:
|
||||
for i in range(len(chemin) - 1):
|
||||
u, v = chemin[i], chemin[i + 1]
|
||||
niveau_u = niveaux.get(u, 999)
|
||||
niveau_v = niveaux.get(v, 999)
|
||||
if (
|
||||
(niveau_depart <= niveau_u <= niveau_arrivee or niveau_u in niveaux_speciaux)
|
||||
and (niveau_depart <= niveau_v <= niveau_arrivee or niveau_v in niveaux_speciaux)
|
||||
):
|
||||
liens.add((u, v))
|
||||
return liens
|
||||
|
||||
def interface_ia_nalyse(
|
||||
G_temp: nx.DiGraph,
|
||||
) -> None:
|
||||
"""
|
||||
Fonction principale qui s'occupe de la création du graphe pour analyse.
|
||||
|
||||
Args:
|
||||
G_temp (nx.DiGraph): Le graphe NetworkX contenant les données des produits.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
st.markdown(f"# {str(_('pages.ia_nalyse.title'))}")
|
||||
html_expander(f"{str(_('pages.ia_nalyse.help'))}", content="\n".join(_("pages.ia_nalyse.help_content")), open_by_default=False, details_class="details_introduction")
|
||||
st.markdown("---")
|
||||
|
||||
from utils.persistance import get_champ_statut
|
||||
resultat = statut_utilisateur(get_champ_statut("login"))
|
||||
if resultat:
|
||||
st.info(resultat["message"])
|
||||
|
||||
if resultat and resultat["statut"] is None:
|
||||
# Préparation du graphe
|
||||
G_temp, niveaux_temp = preparer_graphe(G_temp)
|
||||
|
||||
# Sélection des niveaux
|
||||
niveau_depart = 0
|
||||
niveau_arrivee = 99
|
||||
|
||||
# Sélection fine des noeuds
|
||||
noeuds_depart, noeuds_arrivee = selectionner_noeuds(G_temp, niveaux_temp, niveau_depart)
|
||||
|
||||
# Sélection des minerais si nécessaire
|
||||
minerais = selectionner_minerais(G_temp)
|
||||
|
||||
# Étape 1 : Extraction des niveaux des nœuds
|
||||
niveaux = extraire_niveaux(G_temp)
|
||||
|
||||
# Étape 2 : Extraction des chemins selon les critères
|
||||
chemins = extraire_chemins_selon_criteres(G_temp, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais)
|
||||
|
||||
niveaux_speciaux = [1000, 1001, 1002, 1010, 1011, 1012]
|
||||
# Extraction des liens sans filtrage
|
||||
liens_chemins = extraire_liens_filtres(chemins, niveaux, niveau_depart, niveau_arrivee, niveaux_speciaux)
|
||||
|
||||
if liens_chemins:
|
||||
G_final = exporter_graphe_filtre(G_temp, liens_chemins)
|
||||
if st.button(str(_("pages.ia_nalyse.submit_request")), icon=":material/send:"):
|
||||
soumettre_batch(get_champ_statut("login"), G_final)
|
||||
st.rerun()
|
||||
else:
|
||||
st.info(str(_("pages.ia_nalyse.empty_graph")))
|
||||
|
||||
elif resultat and resultat["statut"] == "terminé" and resultat["telechargement"]:
|
||||
if not st.session_state.get("telechargement_confirme"):
|
||||
st.download_button(str(_("buttons.download")), resultat["telechargement"], file_name="analyse.zip", icon=":material/download:")
|
||||
if st.button(str(_("pages.ia_nalyse.confirm_download")), icon=":material/task_alt:"):
|
||||
nettoyage_post_telechargement(get_champ_statut("login"))
|
||||
st.session_state["telechargement_confirme"] = True
|
||||
st.rerun()
|
||||
else:
|
||||
st.success("Résultat supprimé. Vous pouvez relancer une nouvelle analyse.")
|
||||
if st.button(str(_("buttons.refresh")), icon=":material/refresh:"):
|
||||
st.rerun()
|
||||
else:
|
||||
if st.button(str(_("buttons.refresh")), icon=":material/refresh:"):
|
||||
st.rerun()
|
||||
@ -1,6 +1,7 @@
|
||||
# __init__.py – app/personnalisation
|
||||
|
||||
from .interface import interface_personnalisation
|
||||
from .ajout import ajouter_produit
|
||||
from .modification import modifier_produit
|
||||
from .import_export import importer_exporter_graph
|
||||
|
||||
__all__ = [
|
||||
"interface_personnalisation"
|
||||
]
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
import streamlit as st
|
||||
from utils.translations import _
|
||||
|
||||
def ajouter_produit(G):
|
||||
st.markdown(f"## {str(_('pages.personnalisation.add_new_product'))}")
|
||||
new_prod = st.text_input(str(_("pages.personnalisation.new_product_name")), key="new_prod")
|
||||
if new_prod:
|
||||
ops_dispo = sorted([
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("niveau") == "10"
|
||||
and any(G.has_edge(p, n) and G.nodes[p].get("niveau") == "0" for p in G.predecessors(n))
|
||||
])
|
||||
sel_new_op = st.selectbox(str(_("pages.personnalisation.assembly_operation")), [str(_("pages.personnalisation.none"))] + ops_dispo, index=0)
|
||||
niveau1 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
|
||||
sel_comps = st.multiselect(str(_("pages.personnalisation.components_to_link")), options=niveau1)
|
||||
if st.button(str(_("pages.personnalisation.create_product"))):
|
||||
G.add_node(new_prod, niveau="0", personnalisation="oui", label=new_prod)
|
||||
if sel_new_op != str(_("pages.personnalisation.none")):
|
||||
G.add_edge(new_prod, sel_new_op)
|
||||
for comp in sel_comps:
|
||||
G.add_edge(new_prod, comp)
|
||||
st.success(f"{new_prod} {str(_('pages.personnalisation.added'))}")
|
||||
return G
|
||||
@ -1,15 +1,19 @@
|
||||
# interface.py – app/personnalisation
|
||||
|
||||
import streamlit as st
|
||||
from utils.persistance import maj_champ_statut
|
||||
from utils.translations import _
|
||||
from .ajout import ajouter_produit
|
||||
from .modification import modifier_produit
|
||||
from .import_export import importer_exporter_graph
|
||||
from utils.widgets import html_expander
|
||||
|
||||
from app.personnalisation.utils import ajouter_produit
|
||||
from app.personnalisation.utils import modifier_produit
|
||||
from app.personnalisation.utils import importer_exporter_graph
|
||||
|
||||
def interface_personnalisation(G):
|
||||
st.markdown(f"# {str(_('pages.personnalisation.title'))}")
|
||||
with st.expander(str(_("pages.personnalisation.help")), expanded=False):
|
||||
st.markdown("\n".join(_("pages.personnalisation.help_content")))
|
||||
titre = f"# {str(_('pages.personnalisation.title'))}"
|
||||
maj_champ_statut("pages.personnalisation.title", titre)
|
||||
st.markdown(titre)
|
||||
html_expander(f"{str(_('pages.personnalisation.help'))}", content="\n".join(_("pages.personnalisation.help_content")), open_by_default=False, details_class="details_introduction")
|
||||
st.markdown("---")
|
||||
|
||||
G = ajouter_produit(G)
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
import streamlit as st
|
||||
from utils.translations import _
|
||||
|
||||
def get_produits_personnalises(G):
|
||||
"""Récupère la liste des produits personnalisés du niveau 0."""
|
||||
return sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "0" and d.get("personnalisation") == "oui"])
|
||||
|
||||
def supprimer_produit(G, prod):
|
||||
"""Supprime un produit du graphe."""
|
||||
G.remove_node(prod)
|
||||
st.success(f"{prod} {str(_('pages.personnalisation.deleted'))}")
|
||||
st.session_state.pop("prod_sel", None)
|
||||
return G
|
||||
|
||||
def get_operations_disponibles(G):
|
||||
"""Récupère la liste des opérations d'assemblage disponibles."""
|
||||
return sorted([
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("niveau") == "10"
|
||||
and any(G.has_edge(p, n) and G.nodes[p].get("niveau") == "0" for p in G.predecessors(n))
|
||||
])
|
||||
|
||||
def get_operations_actuelles(G, prod):
|
||||
"""Récupère les opérations actuellement liées au produit."""
|
||||
return [succ for succ in G.successors(prod) if G.nodes[succ].get("niveau") == "10"]
|
||||
|
||||
def get_composants_niveau1(G):
|
||||
"""Récupère la liste des composants de niveau 1."""
|
||||
return sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
|
||||
|
||||
def get_composants_lies(G, prod):
|
||||
"""Récupère les composants actuellement liés au produit."""
|
||||
return [succ for succ in G.successors(prod) if G.nodes[succ].get("niveau") == "1"]
|
||||
|
||||
def mettre_a_jour_operations(G, prod, curr_ops, sel_op):
|
||||
"""Met à jour les opérations liées au produit."""
|
||||
none_option = str(_("pages.personnalisation.none", "-- Aucune --"))
|
||||
for op in curr_ops:
|
||||
if sel_op == none_option or op != sel_op:
|
||||
G.remove_edge(prod, op)
|
||||
if sel_op != none_option and (not curr_ops or sel_op not in curr_ops):
|
||||
G.add_edge(prod, sel_op)
|
||||
return G
|
||||
|
||||
def mettre_a_jour_composants(G, prod, linked, nouveaux):
|
||||
"""Met à jour les composants liés au produit."""
|
||||
for comp in set(linked) - set(nouveaux):
|
||||
G.remove_edge(prod, comp)
|
||||
for comp in set(nouveaux) - set(linked):
|
||||
G.add_edge(prod, comp)
|
||||
return G
|
||||
|
||||
def modifier_produit(G):
|
||||
st.markdown(f"## {str(_('pages.personnalisation.modify_product'))}")
|
||||
|
||||
# Sélection du produit à modifier
|
||||
produits0 = get_produits_personnalises(G)
|
||||
sel_display = st.multiselect(str(_("pages.personnalisation.products_to_modify")), options=produits0)
|
||||
|
||||
if not sel_display:
|
||||
return G
|
||||
|
||||
# Obtention du produit sélectionné
|
||||
prod = sel_display[0]
|
||||
|
||||
# Suppression du produit si demandé
|
||||
if st.button(f"{str(_('pages.personnalisation.delete'))} {prod}"):
|
||||
return supprimer_produit(G, prod)
|
||||
|
||||
# Gestion des opérations d'assemblage
|
||||
ops_dispo = get_operations_disponibles(G)
|
||||
curr_ops = get_operations_actuelles(G, prod)
|
||||
default_idx = ops_dispo.index(curr_ops[0]) + 1 if curr_ops and curr_ops[0] in ops_dispo else 0
|
||||
sel_op = st.selectbox(str(_("pages.personnalisation.linked_assembly_operation")), [str(_("pages.personnalisation.none"))] + ops_dispo, index=default_idx)
|
||||
|
||||
# Gestion des composants
|
||||
niveau1 = get_composants_niveau1(G)
|
||||
linked = get_composants_lies(G, prod)
|
||||
nouveaux = st.multiselect(f"{str(_('pages.personnalisation.components_linked_to'))} {prod}", options=niveau1, default=linked)
|
||||
|
||||
# Mise à jour des liens si demandé
|
||||
if st.button(f"{str(_('pages.personnalisation.update'))} {prod}"):
|
||||
G = mettre_a_jour_operations(G, prod, curr_ops, sel_op)
|
||||
G = mettre_a_jour_composants(G, prod, linked, nouveaux)
|
||||
st.success(f"{prod} {str(_('pages.personnalisation.updated'))}")
|
||||
|
||||
return G
|
||||
11
app/personnalisation/utils/__init__.py
Normal file
11
app/personnalisation/utils/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
# __init__.py – app/personnalisation
|
||||
|
||||
from .ajout import ajouter_produit
|
||||
from .modification import modifier_produit
|
||||
from .import_export import importer_exporter_graph
|
||||
|
||||
__all__ = [
|
||||
"ajouter_produit",
|
||||
"modifier_produit",
|
||||
"importer_exporter_graph"
|
||||
]
|
||||
107
app/personnalisation/utils/ajout.py
Normal file
107
app/personnalisation/utils/ajout.py
Normal file
@ -0,0 +1,107 @@
|
||||
# === Ajout de produit personnalisé ===
|
||||
import streamlit as st
|
||||
import networkx as nx
|
||||
from utils.translations import _
|
||||
from utils.persistance import get_champ_statut, maj_champ_statut, supprime_champ_statut
|
||||
|
||||
def ajouter_produit(G: nx.DiGraph) -> nx.DiGraph:
|
||||
st.markdown(f"## {str(_('pages.personnalisation.add_new_product'))}")
|
||||
|
||||
# Restauration des produits personnalisés sauvegardés
|
||||
if "pages.personnalisation.create_product.fait" not in st.session_state and get_champ_statut("pages.personnalisation.create_product.fait") == "oui":
|
||||
index = 0
|
||||
while True:
|
||||
new_prod = get_champ_statut(f"pages.personnalisation.create_product.{index}.nom")
|
||||
if new_prod == "":
|
||||
break
|
||||
|
||||
G.add_node(new_prod, niveau="0", personnalisation="oui", label=new_prod)
|
||||
|
||||
sel_new_op = get_champ_statut(f"pages.personnalisation.create_product.{index}.edge")
|
||||
if sel_new_op:
|
||||
G.add_edge(new_prod, sel_new_op)
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
comp = get_champ_statut(f"pages.personnalisation.create_product.{index}.composants.{i}")
|
||||
if comp == "":
|
||||
break
|
||||
G.add_edge(new_prod, comp)
|
||||
i += 1
|
||||
|
||||
index += 1
|
||||
|
||||
niveau1 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
|
||||
ops_dispo = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "assemblage"])
|
||||
|
||||
if "pages.personnalisation.add.new_product_name" not in st.session_state:
|
||||
st.session_state["pages.personnalisation.add.new_product_name"] = get_champ_statut("pages.personnalisation.add.new_product_name")
|
||||
|
||||
new_prod = st.text_input(
|
||||
str(_("pages.personnalisation.new_product_name")),
|
||||
key="pages.personnalisation.add.new_product_name"
|
||||
)
|
||||
|
||||
if new_prod:
|
||||
if new_prod in G.nodes:
|
||||
st.warning(str(_("pages.personnalisation.product_exists")))
|
||||
return G
|
||||
|
||||
if "pages.personnalisation.add.assembly_operation" not in st.session_state:
|
||||
st.session_state["pages.personnalisation.add.assembly_operation"] = get_champ_statut("pages.personnalisation.add.assembly_operation")
|
||||
|
||||
if "pages.personnalisation.add.components_to_link" not in st.session_state:
|
||||
composants = []
|
||||
i = 0
|
||||
while True:
|
||||
val = get_champ_statut(f"pages.personnalisation.add.components_to_link.{i}")
|
||||
if val == "":
|
||||
break
|
||||
composants.append(val)
|
||||
i += 1
|
||||
st.session_state["pages.personnalisation.add.components_to_link"] = composants
|
||||
|
||||
ops_dispo = sorted([
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("niveau") == "10"
|
||||
and any(G.has_edge(p, n) and G.nodes[p].get("niveau") == "0" for p in G.predecessors(n))
|
||||
])
|
||||
sel_new_op = st.selectbox(str(_("pages.personnalisation.assembly_operation")), ops_dispo,
|
||||
key="pages.personnalisation.add.assembly_operation")
|
||||
|
||||
sel_comps = st.multiselect(
|
||||
str(_("pages.personnalisation.components_to_link")),
|
||||
options=niveau1,
|
||||
key="pages.personnalisation.add.components_to_link"
|
||||
)
|
||||
|
||||
if st.button(str(_("pages.personnalisation.create_product"))):
|
||||
G.add_node(new_prod, niveau="0", personnalisation="oui", label=new_prod)
|
||||
|
||||
# Trouver le prochain index disponible
|
||||
index = 0
|
||||
while get_champ_statut(f"pages.personnalisation.create_product.{index}.nom") != "":
|
||||
index += 1
|
||||
|
||||
maj_champ_statut(f"pages.personnalisation.create_product.{index}.nom", new_prod)
|
||||
|
||||
if sel_new_op != str(_("pages.personnalisation.none")):
|
||||
G.add_edge(new_prod, sel_new_op)
|
||||
maj_champ_statut(f"pages.personnalisation.create_product.{index}.edge", sel_new_op)
|
||||
|
||||
for j, comp in enumerate(sel_comps):
|
||||
G.add_edge(new_prod, comp)
|
||||
maj_champ_statut(f"pages.personnalisation.create_product.{index}.composants.{j}", comp)
|
||||
|
||||
maj_champ_statut("pages.personnalisation.create_product.fait", "oui")
|
||||
|
||||
# Nettoyage de session et des champs persistants
|
||||
del st.session_state["pages.personnalisation.add.new_product_name"]
|
||||
del st.session_state["pages.personnalisation.add.assembly_operation"]
|
||||
del st.session_state["pages.personnalisation.add.components_to_link"]
|
||||
supprime_champ_statut("pages.personnalisation.add")
|
||||
|
||||
st.success(f"{new_prod} {str(_('pages.personnalisation.added'))}")
|
||||
st.rerun()
|
||||
|
||||
return G
|
||||
@ -1,10 +1,11 @@
|
||||
import streamlit as st
|
||||
import json
|
||||
from utils.translations import get_translation as _
|
||||
import networkx as nx
|
||||
|
||||
def importer_exporter_graph(G):
|
||||
def importer_exporter_graph(G: nx.DiGraph) -> nx.DiGraph:
|
||||
st.markdown(f"## {_('pages.personnalisation.save_restore_config')}")
|
||||
if st.button(str(_("pages.personnalisation.export_config"))):
|
||||
if st.button(str(_("pages.personnalisation.export_config")), icon=":material/save:"):
|
||||
nodes = [n for n, d in G.nodes(data=True) if d.get("personnalisation") == "oui"]
|
||||
edges = [(u, v) for u, v in G.edges() if u in nodes]
|
||||
conf = {"nodes": nodes, "edges": edges}
|
||||
@ -13,7 +14,8 @@ def importer_exporter_graph(G):
|
||||
label=str(_("pages.personnalisation.download_json")),
|
||||
data=json_str,
|
||||
file_name="config_personnalisation.json",
|
||||
mime="application/json"
|
||||
mime="application/json",
|
||||
icon=":material/save:"
|
||||
)
|
||||
|
||||
uploaded = st.file_uploader(str(_("pages.personnalisation.import_config")), type=["json"])
|
||||
@ -37,7 +39,7 @@ def importer_exporter_graph(G):
|
||||
key="restaurer_selection"
|
||||
)
|
||||
|
||||
if st.button(str(_("pages.personnalisation.restore_selected")), type="primary"):
|
||||
if st.button(str(_("pages.personnalisation.restore_selected")), type="primary", icon=":material/history:"):
|
||||
for node in sel_nodes:
|
||||
if not G.has_node(node):
|
||||
G.add_node(node, niveau="0", personnalisation="oui", label=node)
|
||||
349
app/personnalisation/utils/modification.py
Normal file
349
app/personnalisation/utils/modification.py
Normal file
@ -0,0 +1,349 @@
|
||||
from typing import List
|
||||
import streamlit as st
|
||||
from utils.translations import _
|
||||
import networkx as nx
|
||||
from utils.persistance import supprime_champ_statut, get_champ_statut, maj_champ_statut
|
||||
|
||||
def get_produits_personnalises(
|
||||
G: nx.DiGraph
|
||||
) -> List[str]:
|
||||
"""
|
||||
Récupère la liste des produits personnalisés du niveau 0.
|
||||
|
||||
Args:
|
||||
G (Any): Le graphe NetworkX contenant les données des produits.
|
||||
|
||||
Returns:
|
||||
List[str]: Liste triée des noms de produits.
|
||||
"""
|
||||
return sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "0" and d.get("personnalisation") == "oui"])
|
||||
|
||||
def supprimer_produit(
|
||||
G: nx.DiGraph,
|
||||
prod: str
|
||||
) -> nx.DiGraph:
|
||||
"""
|
||||
Supprime un produit du graphe et affiche le message de succès.
|
||||
|
||||
Args:
|
||||
G (Any): Le graphe NetworkX sur lequel supprimer le produit.
|
||||
prod (str): Le nom du produit à supprimer.
|
||||
"""
|
||||
G.remove_node(prod)
|
||||
st.success(f"{prod} {str(_('pages.personnalisation.deleted'))}")
|
||||
st.session_state.pop("prod_sel", None)
|
||||
st.rerun()
|
||||
return G
|
||||
|
||||
def get_operations_disponibles(
|
||||
G: nx.DiGraph
|
||||
) -> List[str]:
|
||||
"""
|
||||
Récupère la liste des opérations d'assemblage disponibles.
|
||||
|
||||
Args:
|
||||
G (Any): Le graphe NetworkX contenant les données des produits et des opérations.
|
||||
|
||||
Returns:
|
||||
List[str]: Liste triée des noms des opérations.
|
||||
"""
|
||||
return sorted([
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("niveau") == "10"
|
||||
and any(G.has_edge(p, n) and G.nodes[p].get("niveau") == "0" for p in G.predecessors(n))
|
||||
])
|
||||
|
||||
def get_operations_actuelles(
|
||||
G: nx.DiGraph,
|
||||
prod: str
|
||||
) -> List[str]:
|
||||
"""
|
||||
Récupère les opérations actuellement liées au produit.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits et des opérations.
|
||||
prod (str): Le nom du produit dont récupérer les opérations.
|
||||
|
||||
Returns:
|
||||
List[str]: Liste des noms des opérations actuelles.
|
||||
"""
|
||||
return [succ for succ in G.successors(prod) if G.nodes[succ].get("niveau") == "10"]
|
||||
|
||||
def get_composants_niveau1(
|
||||
G: nx.DiGraph
|
||||
) -> List[str]:
|
||||
"""
|
||||
Récupère la liste des composants de niveau 1.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits et des composants.
|
||||
|
||||
Returns:
|
||||
List[str]: Liste triée des noms des composants.
|
||||
"""
|
||||
return sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
|
||||
|
||||
def get_composants_lies(
|
||||
G: nx.DiGraph,
|
||||
prod: str
|
||||
) -> List[str]:
|
||||
"""
|
||||
Récupère les composants actuellement liés au produit.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits et des composants.
|
||||
prod (str): Le nom du produit dont récupérer les composants.
|
||||
|
||||
Returns:
|
||||
List[str]: Liste des noms des composants liés.
|
||||
"""
|
||||
return [succ for succ in G.successors(prod) if G.nodes[succ].get("niveau") == "1"]
|
||||
|
||||
def mettre_a_jour_operations(
|
||||
G: nx.DiGraph,
|
||||
prod: str,
|
||||
curr_ops: List[str],
|
||||
sel_op: str
|
||||
) -> nx.DiGraph:
|
||||
"""
|
||||
Met à jour les opérations liées au produit.
|
||||
|
||||
Args:
|
||||
G (Any): Le graphe NetworkX contenant les données des produits et des opérations.
|
||||
prod (str): Le nom du produit dont mettre à jour les opérations.
|
||||
curr_ops (List[str]): Liste actuelle des opérations liées.
|
||||
sel_op (str): L'opération sélectionnée pour mise à jour.
|
||||
|
||||
Notes:
|
||||
Cette fonction crée ou supprime les liens entre le produit et les opérations
|
||||
selon la sélection effectuée par l'utilisateur.
|
||||
"""
|
||||
|
||||
none_option = str(_("pages.personnalisation.none"))
|
||||
for op in curr_ops:
|
||||
if sel_op == none_option or op != sel_op:
|
||||
G.remove_edge(prod, op)
|
||||
if sel_op != none_option and (not curr_ops or sel_op not in curr_ops):
|
||||
G.add_edge(prod, sel_op)
|
||||
return G
|
||||
|
||||
def mettre_a_jour_composants(
|
||||
G: nx.DiGraph,
|
||||
prod: str,
|
||||
linked: List[str],
|
||||
nouveaux: List[str]
|
||||
) -> nx.DiGraph:
|
||||
"""
|
||||
Met à jour les composants liés au produit.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe NetworkX contenant les données des produits et des composants.
|
||||
prod (str): Le nom du produit dont mettre à jour les composants.
|
||||
linked (List[str]): Liste actuelle des composants liés.
|
||||
nouveaux (List[str]): Nouvelle liste de composants à lier.
|
||||
|
||||
Notes:
|
||||
Cette fonction crée ou supprime les liens entre le produit et les composants
|
||||
selon la sélection effectuée par l'utilisateur.
|
||||
"""
|
||||
"""Met à jour les composants liés au produit."""
|
||||
for comp in set(linked) - set(nouveaux):
|
||||
G.remove_edge(prod, comp)
|
||||
for comp in set(nouveaux) - set(linked):
|
||||
G.add_edge(prod, comp)
|
||||
return G
|
||||
|
||||
def modifier_produit(
|
||||
G: nx.DiGraph
|
||||
) -> nx.DiGraph:
|
||||
"""
|
||||
Méthode de personnalisation qui permet à l'utilisateur d'ajuster un produit.
|
||||
|
||||
Args:
|
||||
G (Any): Le graphe NetworkX sur lequel modifier les produits et leurs composants.
|
||||
Contient des données concernant la personalisation des produits,
|
||||
leur niveau, et les opérations liées.
|
||||
|
||||
Notes:
|
||||
Cette fonction fournit une interface utilisateur pour sélectionner
|
||||
un produit à personnaliser, gérer ses composants, et définir ses opérations
|
||||
d'assemblage. Elle implémente la logique de mise à jour des connexions entre
|
||||
les différents éléments du graphe.
|
||||
"""
|
||||
st.markdown(f"## {str(_('pages.personnalisation.modify_product'))}")
|
||||
|
||||
# Restauration des produits personnalisés sauvegardés
|
||||
if "pages.personnalisation.create_product.fait" not in st.session_state and get_champ_statut("pages.personnalisation.create_product.fait") == "oui":
|
||||
index = 0
|
||||
while True:
|
||||
new_prod = get_champ_statut(f"pages.personnalisation.create_product.{index}.nom")
|
||||
if new_prod == "":
|
||||
break
|
||||
|
||||
G.add_node(new_prod, niveau="0", personnalisation="oui", label=new_prod)
|
||||
|
||||
sel_new_op = get_champ_statut(f"pages.personnalisation.create_product.{index}.edge")
|
||||
if sel_new_op:
|
||||
G.add_edge(new_prod, sel_new_op)
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
comp = get_champ_statut(f"pages.personnalisation.create_product.{index}.composants.{i}")
|
||||
if comp == "":
|
||||
break
|
||||
G.add_edge(new_prod, comp)
|
||||
i += 1
|
||||
|
||||
index += 1
|
||||
|
||||
st.session_state["pages.personnalisation.create_product.fait"] = "oui"
|
||||
|
||||
# Sélection du produit à modifier
|
||||
produits0 = get_produits_personnalises(G)
|
||||
|
||||
# Restaurer la sélection du produit depuis la persistance
|
||||
if "pages.personnalisation.modify.selected_product" not in st.session_state:
|
||||
st.session_state["pages.personnalisation.modify.selected_product"] = get_champ_statut("pages.personnalisation.modify.selected_product")
|
||||
|
||||
sel_display = st.selectbox(
|
||||
label=str(_("pages.personnalisation.products_to_modify")),
|
||||
options=produits0,
|
||||
key="pages.personnalisation.modify.selected_product"
|
||||
)
|
||||
|
||||
if sel_display:
|
||||
maj_champ_statut("pages.personnalisation.modify.selected_product", sel_display)
|
||||
|
||||
if not sel_display:
|
||||
return G
|
||||
|
||||
# Obtention du produit sélectionné
|
||||
prod = sel_display
|
||||
|
||||
# Trouver l'index du produit dans la sauvegarde
|
||||
index_key = None
|
||||
index = 0
|
||||
while True:
|
||||
saved_name = get_champ_statut(f"pages.personnalisation.create_product.{index}.nom")
|
||||
if saved_name == "":
|
||||
break
|
||||
if saved_name == prod:
|
||||
index_key = index
|
||||
break
|
||||
index += 1
|
||||
|
||||
# Suppression du produit si demandé
|
||||
if st.button(f"{str(_('pages.personnalisation.delete'))} {prod}"):
|
||||
if index_key is not None:
|
||||
# Supprimer les données du produit
|
||||
supprime_champ_statut(f"pages.personnalisation.create_product.{index_key}.nom")
|
||||
supprime_champ_statut(f"pages.personnalisation.create_product.{index_key}.edge")
|
||||
supprime_champ_statut(f"pages.personnalisation.create_product.{index_key}.composants")
|
||||
# Réorganiser les indices pour combler le trou
|
||||
next_index = index_key + 1
|
||||
while get_champ_statut(f"pages.personnalisation.create_product.{next_index}.nom") != "":
|
||||
# Déplacer le produit suivant vers l'index actuel
|
||||
nom = get_champ_statut(f"pages.personnalisation.create_product.{next_index}.nom")
|
||||
edge = get_champ_statut(f"pages.personnalisation.create_product.{next_index}.edge")
|
||||
|
||||
maj_champ_statut(f"pages.personnalisation.create_product.{next_index - 1}.nom", nom)
|
||||
if edge:
|
||||
maj_champ_statut(f"pages.personnalisation.create_product.{next_index - 1}.edge", edge)
|
||||
else:
|
||||
supprime_champ_statut(f"pages.personnalisation.create_product.{next_index - 1}.edge")
|
||||
|
||||
# Copier les composants
|
||||
i = 0
|
||||
while True:
|
||||
comp = get_champ_statut(f"pages.personnalisation.create_product.{next_index}.composants.{i}")
|
||||
if comp == "":
|
||||
break
|
||||
maj_champ_statut(f"pages.personnalisation.create_product.{next_index - 1}.composants.{i}", comp)
|
||||
i += 1
|
||||
|
||||
# Supprimer l'ancien emplacement
|
||||
supprime_champ_statut(f"pages.personnalisation.create_product.{next_index}")
|
||||
next_index += 1
|
||||
# Nettoyer aussi les champs de modification
|
||||
supprime_champ_statut("pages.personnalisation.modify")
|
||||
if "pages.personnalisation.modify.selected_product" in st.session_state:
|
||||
del st.session_state["pages.personnalisation.modify.selected_product"]
|
||||
if "pages.personnalisation.modify.selected_operation" in st.session_state:
|
||||
del st.session_state["pages.personnalisation.modify.selected_operation"]
|
||||
if "pages.personnalisation.modify.selected_components" in st.session_state:
|
||||
del st.session_state["pages.personnalisation.modify.selected_components"]
|
||||
return supprimer_produit(G, prod)
|
||||
|
||||
# Gestion des opérations d'assemblage
|
||||
ops_dispo = get_operations_disponibles(G)
|
||||
curr_ops = get_operations_actuelles(G, prod)
|
||||
|
||||
# Restaurer la sélection de l'opération depuis la persistance
|
||||
if "pages.personnalisation.modify.selected_operation" not in st.session_state:
|
||||
saved_op = get_champ_statut("pages.personnalisation.modify.selected_operation")
|
||||
if saved_op and saved_op in [str(_("pages.personnalisation.none"))] + ops_dispo:
|
||||
st.session_state["pages.personnalisation.modify.selected_operation"] = saved_op
|
||||
else:
|
||||
default_op = curr_ops[0] if curr_ops else str(_("pages.personnalisation.none"))
|
||||
st.session_state["pages.personnalisation.modify.selected_operation"] = default_op
|
||||
|
||||
sel_op = st.selectbox(
|
||||
str(_("pages.personnalisation.linked_assembly_operation")),
|
||||
[str(_("pages.personnalisation.none"))] + ops_dispo,
|
||||
key="pages.personnalisation.modify.selected_operation"
|
||||
)
|
||||
|
||||
if sel_op:
|
||||
maj_champ_statut("pages.personnalisation.modify.selected_operation", sel_op)
|
||||
|
||||
# Gestion des composants
|
||||
niveau1 = get_composants_niveau1(G)
|
||||
linked = get_composants_lies(G, prod)
|
||||
|
||||
# Restaurer la sélection des composants depuis la persistance
|
||||
if "pages.personnalisation.modify.selected_components" not in st.session_state:
|
||||
saved_comps = []
|
||||
i = 0
|
||||
while True:
|
||||
comp = get_champ_statut(f"pages.personnalisation.modify.selected_components.{i}")
|
||||
if comp == "":
|
||||
break
|
||||
if comp in niveau1:
|
||||
saved_comps.append(comp)
|
||||
i += 1
|
||||
st.session_state["pages.personnalisation.modify.selected_components"] = saved_comps if saved_comps else linked
|
||||
|
||||
nouveaux = st.multiselect(
|
||||
f"{str(_('pages.personnalisation.components_linked_to'))} {prod}",
|
||||
options=niveau1,
|
||||
key="pages.personnalisation.modify.selected_components"
|
||||
)
|
||||
|
||||
if nouveaux:
|
||||
supprime_champ_statut("pages.personnalisation.modify.selected_components")
|
||||
for i, comp in enumerate(nouveaux):
|
||||
maj_champ_statut(f"pages.personnalisation.modify.selected_components.{i}", comp)
|
||||
|
||||
# Mise à jour des liens si demandé
|
||||
if st.button(f"{str(_('pages.personnalisation.update'))} {prod}"):
|
||||
G = mettre_a_jour_operations(G, prod, curr_ops, sel_op)
|
||||
if index_key is not None:
|
||||
if sel_op != str(_("pages.personnalisation.none")):
|
||||
maj_champ_statut(f"pages.personnalisation.create_product.{index_key}.edge", sel_op)
|
||||
else:
|
||||
supprime_champ_statut(f"pages.personnalisation.create_product.{index_key}.edge")
|
||||
|
||||
G = mettre_a_jour_composants(G, prod, linked, nouveaux)
|
||||
if index_key is not None:
|
||||
if nouveaux:
|
||||
supprime_champ_statut(f"pages.personnalisation.create_product.{index_key}.composants")
|
||||
for j, comp in enumerate(nouveaux):
|
||||
maj_champ_statut(f"pages.personnalisation.create_product.{index_key}.composants.{j}", comp)
|
||||
else:
|
||||
supprime_champ_statut(f"pages.personnalisation.create_product.{index_key}.composants")
|
||||
|
||||
st.success(f"{prod} {str(_('pages.personnalisation.updated'))}")
|
||||
# Forcer la sauvegarde de l'état
|
||||
maj_champ_statut("pages.personnalisation.create_product.fait", "oui")
|
||||
|
||||
return G
|
||||
34
app/plan_d_action/__init__.py
Normal file
34
app/plan_d_action/__init__.py
Normal file
@ -0,0 +1,34 @@
|
||||
# app/plan_d_action/__init__.py
|
||||
|
||||
from .utils.data.plan_d_action import initialiser_interface
|
||||
|
||||
from .utils.interface.parser import preparer_graphe
|
||||
from .utils.interface.niveau_utils import extraire_niveaux
|
||||
from .utils.interface.selection import (
|
||||
selectionner_minerais,
|
||||
selectionner_noeuds,
|
||||
extraire_chemins_selon_criteres
|
||||
)
|
||||
from .utils.interface.export import (
|
||||
exporter_graphe_filtre,
|
||||
extraire_liens_filtres
|
||||
)
|
||||
from .utils.interface.visualization import remplacer_par_badge
|
||||
from .utils.interface.config import (
|
||||
niveau_labels,
|
||||
JOBS
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"initialiser_interface",
|
||||
"preparer_graphe",
|
||||
"extraire_niveaux",
|
||||
"selectionner_minerais",
|
||||
"selectionner_noeuds",
|
||||
"extraire_chemins_selon_criteres",
|
||||
"exporter_graphe_filtre",
|
||||
"extraire_liens_filtres",
|
||||
"remplacer_par_badge",
|
||||
"niveau_labels",
|
||||
"JOBS"
|
||||
]
|
||||
127
app/plan_d_action/interface.py
Normal file
127
app/plan_d_action/interface.py
Normal file
@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import networkx as nx
|
||||
|
||||
"""
|
||||
Script pour générer un rapport factorisé des vulnérabilités critiques
|
||||
suivant la structure définie dans Remarques.md.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import uuid
|
||||
from utils.translations import _
|
||||
from utils.widgets import html_expander
|
||||
from networkx.drawing.nx_agraph import write_dot
|
||||
|
||||
from batch_ia import (
|
||||
load_config,
|
||||
write_report,
|
||||
parse_graphs,
|
||||
extract_data_from_graph,
|
||||
calculate_vulnerabilities,
|
||||
generate_report,
|
||||
)
|
||||
|
||||
from app.plan_d_action import (
|
||||
initialiser_interface,
|
||||
preparer_graphe,
|
||||
extraire_niveaux,
|
||||
selectionner_minerais,
|
||||
selectionner_noeuds,
|
||||
extraire_chemins_selon_criteres,
|
||||
exporter_graphe_filtre,
|
||||
extraire_liens_filtres,
|
||||
niveau_labels,
|
||||
remplacer_par_badge,
|
||||
JOBS
|
||||
)
|
||||
|
||||
inverse_niveau_labels = {v: k for k, v in niveau_labels.items()}
|
||||
|
||||
|
||||
def interface_plan_d_action(G_temp: nx.DiGraph) -> None:
|
||||
"""Interface pour planifier l'action de sélection des minerais.
|
||||
|
||||
Args:
|
||||
G_temp (nx.DiGraph): Le graphe temporaire à analyser.
|
||||
|
||||
Returns:
|
||||
None: Modifie le state du Streamlit avec les données nécessaires
|
||||
pour la génération du rapport factorisé des vulnérabilités critiques.
|
||||
"""
|
||||
|
||||
if "sel_prod" not in st.session_state:
|
||||
st.session_state.sel_prod = None
|
||||
if "sel_comp" not in st.session_state:
|
||||
st.session_state.sel_comp = None
|
||||
if "sel_miner" not in st.session_state:
|
||||
st.session_state.sel_miner = None
|
||||
|
||||
if "plan_d_action" not in st.session_state:
|
||||
st.session_state["plan_d_action"] = 0
|
||||
if "g_md_done" not in st.session_state:
|
||||
st.session_state["g_md_done"] = False
|
||||
if "uuid" not in st.session_state:
|
||||
st.session_state["uuid"] = str(uuid.uuid4())[:8]
|
||||
st.session_state["G_dot"] = f"{JOBS}/{st.session_state["uuid"]}.dot"
|
||||
st.session_state["G_md"] = f"{JOBS}/{st.session_state["uuid"]}.md"
|
||||
|
||||
if st.session_state["plan_d_action"] == 0:
|
||||
st.markdown(f"# {str(_('pages.plan_d_action.title'))}")
|
||||
html_expander(f"{str(_('pages.plan_d_action.help'))}", content="\n".join(_("pages.plan_d_action.help_content")), open_by_default=False, details_class="details_introduction")
|
||||
# Préparation du graphe
|
||||
G_temp, niveaux_temp = preparer_graphe(G_temp)
|
||||
|
||||
# Sélection des niveaux
|
||||
niveau_depart = 0
|
||||
niveau_arrivee = 99
|
||||
# Sélection fine des noeuds
|
||||
noeuds_depart, noeuds_arrivee = selectionner_noeuds(G_temp, niveaux_temp, niveau_depart)
|
||||
# Sélection des minerais si nécessaire
|
||||
if noeuds_depart:
|
||||
minerais = selectionner_minerais(G_temp, noeuds_depart)
|
||||
# Étape 1 : Extraction des niveaux des nœuds
|
||||
niveaux = extraire_niveaux(G_temp)
|
||||
# Étape 2 : Extraction des chemins selon les critères
|
||||
chemins = extraire_chemins_selon_criteres(G_temp, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais)
|
||||
niveaux_speciaux = [1000, 1001, 1002, 1010, 1011, 1012]
|
||||
# Extraction des liens sans filtrage
|
||||
liens_chemins = extraire_liens_filtres(chemins, niveaux, niveau_depart, niveau_arrivee, niveaux_speciaux)
|
||||
else:
|
||||
liens_chemins = None
|
||||
|
||||
if liens_chemins:
|
||||
G_final = exporter_graphe_filtre(G_temp, liens_chemins)
|
||||
st.session_state["G_final"] = G_final
|
||||
# formulaire ou sélection
|
||||
if st.button(str(_("pages.plan_d_action.submit_request")), icon=":material/send:"):
|
||||
# On déclenche la suite — mais on NE traite rien maintenant
|
||||
st.session_state["plan_d_action"] = 1
|
||||
st.rerun() # force la réexécution immédiatement avec état mis à jour
|
||||
|
||||
elif st.session_state["plan_d_action"] == 1:
|
||||
st.markdown("")
|
||||
# Traitement lourd une seule fois
|
||||
if not st.session_state["g_md_done"]:
|
||||
write_dot(st.session_state["G_final"], st.session_state["G_dot"])
|
||||
config = load_config()
|
||||
graph, ref_graph = parse_graphs(st.session_state["G_dot"])
|
||||
data = extract_data_from_graph(graph, ref_graph)
|
||||
results = calculate_vulnerabilities(data, config)
|
||||
report, file_names = generate_report(data, results, config)
|
||||
write_report(remplacer_par_badge(report), st.session_state["G_md"])
|
||||
st.session_state["g_md_done"] = True # pour ne pas re-traiter à chaque affichage
|
||||
|
||||
# Affichage de l’interface Streamlit
|
||||
initialiser_interface(st.session_state["G_md"])
|
||||
if (st.button("Réinitialiser", icon=":material/refresh:")):
|
||||
st.session_state["plan_d_action"] = 0
|
||||
st.session_state["g_md_done"] = False
|
||||
st.session_state.sel_prod = None
|
||||
st.session_state.sel_comp = None
|
||||
st.session_state.sel_miner = None
|
||||
for f in JOBS.glob(f"*{st.session_state["uuid"]}*"):
|
||||
if f.is_file():
|
||||
f.unlink()
|
||||
st.rerun()
|
||||
29
app/plan_d_action/utils/data/__init__.py
Normal file
29
app/plan_d_action/utils/data/__init__.py
Normal file
@ -0,0 +1,29 @@
|
||||
from .config import (
|
||||
PRECONISATIONS,
|
||||
INDICATEURS,
|
||||
poids_operation
|
||||
)
|
||||
from .data_processing import parse_chains_md
|
||||
from .data_utils import (
|
||||
set_vulnerability,
|
||||
colorer_couleurs,
|
||||
initialiser_seuils
|
||||
)
|
||||
from .pda_interface import (
|
||||
afficher_bloc_ihh_isg,
|
||||
afficher_description,
|
||||
afficher_caracteristiques_minerai
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"PRECONISATIONS",
|
||||
"INDICATEURS",
|
||||
"poids_operation",
|
||||
"parse_chains_md",
|
||||
"set_vulnerability",
|
||||
"colorer_couleurs",
|
||||
"initialiser_seuils",
|
||||
"afficher_bloc_ihh_isg",
|
||||
"afficher_description",
|
||||
"afficher_caracteristiques_minerai"
|
||||
]
|
||||
165
app/plan_d_action/utils/data/config.py
Normal file
165
app/plan_d_action/utils/data/config.py
Normal file
@ -0,0 +1,165 @@
|
||||
PRECONISATIONS = {
|
||||
'Facile': [
|
||||
"Constituer des stocks stratégiques.",
|
||||
"Surveiller activement les signaux géopolitiques.",
|
||||
"Renforcer la surveillance des régions critiques."
|
||||
],
|
||||
'Modérée': [
|
||||
"Diversifier progressivement les fournisseurs.",
|
||||
"Favoriser la modularité des produits.",
|
||||
"Augmenter progressivement les taux de recyclage."
|
||||
],
|
||||
'Difficile': [
|
||||
"Investir fortement en R&D pour la substitution.",
|
||||
"Développer des technologies alternatives robustes.",
|
||||
"Établir des partenariats stratégiques locaux solides."
|
||||
],
|
||||
'Extraction': {
|
||||
'Facile': [
|
||||
"Constituer des stocks « in-country » (site minier / port) pour 30 jours.",
|
||||
"Activer un moniteur de prix spot quotidien.",
|
||||
"Lancer une veille ESG locale (manifestations, météo extrême)."
|
||||
],
|
||||
'Modérée': [
|
||||
"Négocier des contrats « take-or-pay » avec au moins 2 exploitants distincts.",
|
||||
"Mettre en place un audit semestriel des pratiques de sécurité/logistique des mines.",
|
||||
"Financer en co-investissement un entrepôt portuaire multi-produits."
|
||||
],
|
||||
'Difficile': [
|
||||
"Participer au capital d’un producteur émergent hors zone de concentration.",
|
||||
"Obtenir des droits d’« off-take » de 5 ans sur 20 % de la production d’une mine alternative.",
|
||||
"Soutenir (CAPEX) l’ouverture d’une nouvelle voie ferroviaire ou portuaire sécurisée."
|
||||
]
|
||||
},
|
||||
'Traitement': {
|
||||
'Facile': [
|
||||
"Sécuriser un stock tampon sur site (90 jours).",
|
||||
"Faire certifier la traçabilité chimique du concentré."
|
||||
|
||||
],
|
||||
'Modérée': [
|
||||
"Valider un second affineur dans une région politiquement stable.",
|
||||
"Imposer des clauses « force-majeure » limitant l’arrêt total à 48 h.",
|
||||
"Explorer les possibilités de recyclage et d'économie circulaire"
|
||||
],
|
||||
'Difficile': [
|
||||
"Co-développer un site de raffinage dans une zone « friend-shore ».",
|
||||
"Financer un procédé de purification à rendement plus élevé (réduit la dépendance au minerai primaire).",
|
||||
"Constituer des réserves stratégiques pour les périodes de tension"
|
||||
]
|
||||
},
|
||||
'Fabrication': {
|
||||
'Facile': [
|
||||
"Mettre un seuil minimal de sécurité (45 jours) sur le composant critique en usine SMT.",
|
||||
"Suivre hebdomadairement la capacité libre des fondeurs/EMS.",
|
||||
"Maintenir une veille technologique sur les évolutions du marché"
|
||||
],
|
||||
'Modérée': [
|
||||
"Dual-sourcer le composant critique intégrant un minerai critique (au moins 30 % chez un second fondeur).",
|
||||
"Déployer le « design-for-substitution » : même PCB compatible avec le composant concerné.",
|
||||
"Optimiser les processus d'approvisionnement existants"
|
||||
],
|
||||
'Difficile': [
|
||||
"Lancer un programme R&D de substitution ou d'alternative budgeté sur 3 ans.",
|
||||
"Contractualiser un accord exclusif avec un fondeur hors zone rouge pour 25 % des volumes."
|
||||
]
|
||||
},
|
||||
'Assemblage': {
|
||||
'Facile': [
|
||||
"Allonger la rotation des stocks de produits finis (en aval) pour amortir un retard de 2 semaines.",
|
||||
"Mettre en place un plan de re-déploiement du personnel sur d’autres lignes en cas de rupture composant."
|
||||
],
|
||||
'Modérée': [
|
||||
"Avoir un site d’assemblage secondaire (low-volume) dans une région verte, testé tous les 6 mois.",
|
||||
"Segmenter les nomenclatures : version « premium » avec composant haut de gamme, version « fallback » avec composant moins critique."
|
||||
],
|
||||
'Difficile': [
|
||||
"Investir dans une plateforme d’assemblage flexible (robots modulaires) capable de basculer vers un composant de substitution en < 72 h.",
|
||||
"Signer un accord gouvernemental pour un soutien logistique prioritaire (corridor aérien dédié) en cas de crise géopolitique.",
|
||||
"Mettre en place des contrats à long terme avec des clauses de garantie d'approvisionnement"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
INDICATEURS = {
|
||||
'Facile': [
|
||||
"Suivi régulier de la stabilité géopolitique (ISG).",
|
||||
"Durée réelle d'utilisation du matériel.",
|
||||
"Niveau des stocks stratégiques disponibles."
|
||||
],
|
||||
'Modérée': [
|
||||
"Taux de diversification des fournisseurs par région.",
|
||||
"Évolution trimestrielle de la concurrence intersectorielle (IVC).",
|
||||
"Taux annuel de recyclage des composants critiques."
|
||||
],
|
||||
'Difficile': [
|
||||
"Budget annuel investi dans la recherche technologique.",
|
||||
"Nombre de brevets déposés pour des substituts.",
|
||||
"Progrès réel en matière de substitution technologique (ICS)."
|
||||
],
|
||||
'Extraction': {
|
||||
'Facile': [
|
||||
"Jours de stock portuaire (objectif ≥ 30).",
|
||||
"Indice ISG moyen pondéré des pays extracteurs (alerte ≥ 60).",
|
||||
"Volatilité hebdo du prix spot (écart-type %)."
|
||||
],
|
||||
'Modérée': [
|
||||
"Part du 2ᵉ fournisseur dans le volume total (objectif ≥ 20 %).",
|
||||
"Délai moyen d’obtention des permis d’export."
|
||||
],
|
||||
'Difficile': [
|
||||
"Capacité annuelle d’une mine alternative financée (% du besoin interne).",
|
||||
"Progrès physique de l’infrastructure logistique (Km de voie, % achevé)."
|
||||
]
|
||||
},
|
||||
'Traitement': {
|
||||
'Facile': [
|
||||
"Couverture stock tampon (jours).",
|
||||
"Certificats de traçabilité obtenus (% lots)."
|
||||
],
|
||||
'Modérée': [
|
||||
"Nombre d’affineurs validés (objectif ≥ 2).",
|
||||
"Taux de rendement global du procédé (%)."
|
||||
],
|
||||
'Difficile': [
|
||||
"Part de production refinée hors zone rouge (%).",
|
||||
"Capex cumulé investi dans de nouveaux procédés (M€)."
|
||||
]
|
||||
},
|
||||
'Fabrication': {
|
||||
'Facile': [
|
||||
"Stock de composants critiques (jours).",
|
||||
"Capacité libre des EMS (%) rapportée chaque vendredi."
|
||||
],
|
||||
'Modérée': [
|
||||
"Part du second fondeur dans la production du composant (%).",
|
||||
"Nombre de PCB « design-for-substitution » validés."
|
||||
],
|
||||
'Difficile': [
|
||||
"Dépenses R&D substituts (€) vs budget.",
|
||||
"TRI attendu sur les investisseurs fondeurs alternatifs."
|
||||
]
|
||||
},
|
||||
'Assemblage': {
|
||||
'Facile': [
|
||||
"Jours de produits finis en entrepôt.",
|
||||
"Temps de retouche ligne en cas de rupture (heures)."
|
||||
],
|
||||
'Modérée': [
|
||||
"Volume annuel produit sur le site de secours (%).",
|
||||
"Temps de requalification d’une ligne vers la version « fallback »."
|
||||
],
|
||||
'Difficile': [
|
||||
"Taux d’automatisation reconfigurable (% machines modulaires).",
|
||||
"Nb d’heures du corridor aérien prioritaire utilisé vs capacité."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
poids_operation = {
|
||||
'Extraction': 1,
|
||||
'Traitement': 1.5,
|
||||
'Assemblage': 1.5,
|
||||
'Fabrication': 2,
|
||||
'Substitution': 2
|
||||
}
|
||||
201
app/plan_d_action/utils/data/data_processing.py
Normal file
201
app/plan_d_action/utils/data/data_processing.py
Normal file
@ -0,0 +1,201 @@
|
||||
import re
|
||||
|
||||
def parse_chains_md(filepath: str) -> tuple[dict, dict, dict, list, dict, dict]:
|
||||
"""Lit et analyse un fichier Markdown contenant des informations sur les chaînes minérales.
|
||||
|
||||
Args:
|
||||
filepath (str): Chemin vers le fichier Markdown à analyser.
|
||||
|
||||
Returns:
|
||||
tuple: Un ensemble de dictionnaires et listes contenant les données extraites du fichier,
|
||||
incluant les produits, composants, mineraux, chaînes et leurs descriptions détaillées.
|
||||
"""
|
||||
re_start_section = re.compile(r"^##\s*Chaînes\s+avec\s+risque\s+critique", re.IGNORECASE)
|
||||
re_other_h2 = re.compile(r"^##\s+(?!(Chaînes\s+avec\s+risque\s+critique))")
|
||||
re_chain_heading = re.compile(r"^###\s*(.+)\s*→\s*(.+)\s*→\s*(.+)$")
|
||||
re_phase = re.compile(r"^\*\s*(Assemblage|Fabrication|Minerai|Extraction|Traitement)", re.IGNORECASE)
|
||||
re_IHH = re.compile(r"IHH\s*[:]\s*([0-9]+(?:\.[0-9]+)?)")
|
||||
re_ISG = re.compile(r"ISG\s*combiné\s*[:]\s*([0-9]+(?:\.[0-9]+)?)|ISG\s*[:]\s*([0-9]+(?:\.[0-9]+)?)", re.IGNORECASE)
|
||||
re_ICS = re.compile(r"ICS\s*moyen\s*[:]\s*([0-9]+(?:\.[0-9]+)?)", re.IGNORECASE)
|
||||
re_IVC = re.compile(r"IVC\s*[:]\s*([0-9]+(?:\.[0-9]+)?)", re.IGNORECASE)
|
||||
|
||||
produits, composants, mineraux, chains = {}, {}, {}, []
|
||||
descriptions = {}
|
||||
details_sections = {}
|
||||
current_chain = None
|
||||
current_phase = None
|
||||
current_section = None
|
||||
in_section = False
|
||||
|
||||
with open(filepath, encoding="utf-8") as f:
|
||||
for raw_line in f:
|
||||
line = raw_line.strip()
|
||||
if not in_section:
|
||||
if re_start_section.match(line):
|
||||
in_section = True
|
||||
continue
|
||||
if re_other_h2.match(line):
|
||||
break
|
||||
m_chain = re_chain_heading.match(line)
|
||||
if m_chain:
|
||||
prod, comp, miner = map(str.strip, m_chain.groups())
|
||||
produits.setdefault(prod, {"IHH_Assemblage": None, "ISG_Assemblage": None})
|
||||
composants.setdefault(comp, {"IHH_Fabrication": None, "ISG_Fabrication": None})
|
||||
mineraux.setdefault(miner, {
|
||||
"ICS": None, "IVC": None,
|
||||
"IHH_Extraction": None, "ISG_Extraction": None,
|
||||
"IHH_Traitement": None, "ISG_Traitement": None
|
||||
})
|
||||
chains.append({"produit": prod, "composant": comp, "minerai": miner})
|
||||
current_chain = {"prod": prod, "comp": comp, "miner": miner}
|
||||
current_phase = None
|
||||
current_section = f"{prod} → {comp} → {miner}"
|
||||
descriptions[current_section] = ""
|
||||
continue
|
||||
if current_chain is None:
|
||||
continue
|
||||
m_phase = re_phase.match(line)
|
||||
if m_phase:
|
||||
current_phase = m_phase.group(1).capitalize()
|
||||
continue
|
||||
if current_phase:
|
||||
p = current_chain
|
||||
if current_phase == "Assemblage":
|
||||
if (m := re_IHH.search(line)):
|
||||
produits[p["prod"]]["IHH_Assemblage"] = float(m.group(1))
|
||||
continue
|
||||
if (m := re_ISG.search(line)):
|
||||
raw = m.group(1) or m.group(2)
|
||||
produits[p["prod"]]["ISG_Assemblage"] = float(raw)
|
||||
continue
|
||||
if current_phase == "Fabrication":
|
||||
if (m := re_IHH.search(line)):
|
||||
composants[p["comp"]]["IHH_Fabrication"] = float(m.group(1))
|
||||
continue
|
||||
if (m := re_ISG.search(line)):
|
||||
raw = m.group(1) or m.group(2)
|
||||
composants[p["comp"]]["ISG_Fabrication"] = float(raw)
|
||||
continue
|
||||
if current_phase == "Minerai":
|
||||
if (m := re_ICS.search(line)):
|
||||
mineraux[p["miner"]]["ICS"] = float(m.group(1))
|
||||
continue
|
||||
if (m := re_IVC.search(line)):
|
||||
mineraux[p["miner"]]["IVC"] = float(m.group(1))
|
||||
continue
|
||||
if current_phase == "Extraction":
|
||||
if (m := re_IHH.search(line)):
|
||||
mineraux[p["miner"]]["IHH_Extraction"] = float(m.group(1))
|
||||
continue
|
||||
if (m := re_ISG.search(line)):
|
||||
raw = m.group(1) or m.group(2)
|
||||
mineraux[p["miner"]]["ISG_Extraction"] = float(raw)
|
||||
continue
|
||||
if current_phase == "Traitement":
|
||||
if (m := re_IHH.search(line)):
|
||||
mineraux[p["miner"]]["IHH_Traitement"] = float(m.group(1))
|
||||
continue
|
||||
if (m := re_ISG.search(line)):
|
||||
raw = m.group(1) or m.group(2)
|
||||
mineraux[p["miner"]]["ISG_Traitement"] = float(raw)
|
||||
continue
|
||||
else:
|
||||
if current_section:
|
||||
descriptions[current_section] += raw_line
|
||||
|
||||
# Parse detailed sections from the complete file
|
||||
with open(filepath, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Extract sections using regex patterns
|
||||
lines = content.split('\n')
|
||||
|
||||
# Find section boundaries
|
||||
operations_start = None
|
||||
minerais_start = None
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip() == "## Détails des opérations":
|
||||
operations_start = i
|
||||
elif line.strip() == "## Détails des minerais":
|
||||
minerais_start = i
|
||||
|
||||
if operations_start is not None:
|
||||
# Parse operations section (assemblage and fabrication)
|
||||
operations_end = minerais_start if minerais_start else len(lines)
|
||||
operations_lines = lines[operations_start:operations_end]
|
||||
|
||||
current_section_name = None
|
||||
current_content = []
|
||||
|
||||
for line in operations_lines:
|
||||
if line.startswith("### ") and " et " in line:
|
||||
# Save previous section
|
||||
if current_section_name and current_content:
|
||||
details_sections[current_section_name] = '\n'.join(current_content)
|
||||
|
||||
# Start new section
|
||||
section_title = line.replace("### ", "").strip()
|
||||
if " et Assemblage" in section_title:
|
||||
product_name = section_title.replace(" et Assemblage", "").strip()
|
||||
current_section_name = f"{product_name}_assemblage"
|
||||
elif " et Fabrication" in section_title:
|
||||
component_name = section_title.replace(" et Fabrication", "").strip()
|
||||
current_section_name = f"{component_name}_fabrication"
|
||||
current_content = []
|
||||
elif current_section_name:
|
||||
current_content.append(line)
|
||||
|
||||
# Save last section
|
||||
if current_section_name and current_content:
|
||||
details_sections[current_section_name] = '\n'.join(current_content)
|
||||
|
||||
if minerais_start is not None:
|
||||
# Parse minerais section
|
||||
minerais_lines = lines[minerais_start:]
|
||||
|
||||
current_minerai = None
|
||||
current_section_type = "general"
|
||||
current_content = []
|
||||
|
||||
for line in minerais_lines:
|
||||
if line.startswith("### ") and "→" not in line and " et " not in line:
|
||||
# Save previous section
|
||||
if current_minerai and current_content:
|
||||
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
|
||||
|
||||
# Start new minerai
|
||||
current_minerai = line.replace("### ", "").strip()
|
||||
current_section_type = "general"
|
||||
current_content = []
|
||||
|
||||
elif line.startswith("#### Extraction"):
|
||||
# Save previous section
|
||||
if current_minerai and current_content:
|
||||
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
|
||||
|
||||
current_section_type = "extraction"
|
||||
current_content = []
|
||||
|
||||
elif line.startswith("#### Traitement"):
|
||||
# Save previous section
|
||||
if current_minerai and current_content:
|
||||
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
|
||||
|
||||
current_section_type = "traitement"
|
||||
current_content = []
|
||||
|
||||
elif line.startswith("## ") and current_minerai:
|
||||
# End of minerais section
|
||||
if current_content:
|
||||
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
|
||||
break
|
||||
|
||||
elif current_minerai:
|
||||
current_content.append(line)
|
||||
|
||||
# Save last section
|
||||
if current_minerai and current_content:
|
||||
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
|
||||
|
||||
return produits, composants, mineraux, chains, descriptions, details_sections
|
||||
104
app/plan_d_action/utils/data/data_utils.py
Normal file
104
app/plan_d_action/utils/data/data_utils.py
Normal file
@ -0,0 +1,104 @@
|
||||
import yaml
|
||||
import streamlit as st
|
||||
|
||||
def get_seuil(seuils_dict: dict, key: str) -> float|None:
|
||||
"""Récupère un seuil pour une clé donnée dans le dictionnaire.
|
||||
|
||||
Args:
|
||||
seuils_dict (dict): Dictionnaire contenant les seuils.
|
||||
key (str): Clé du seuil à récupérer.
|
||||
|
||||
Returns:
|
||||
float|None: Le seuil si existant, sinon None.
|
||||
"""
|
||||
try:
|
||||
if key in seuils_dict:
|
||||
data = seuils_dict[key]
|
||||
for niveau in ["rouge", "orange", "vert"]:
|
||||
if niveau in data:
|
||||
seuil = data[niveau]
|
||||
if "min" in seuil and seuil["min"] is not None:
|
||||
return seuil["min"]
|
||||
if "max" in seuil and seuil["max"] is not None:
|
||||
return seuil["max"]
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def set_vulnerability(v1: int, v2: int, t1: str, t2: str, seuils: dict) -> tuple[int,str,str,str]:
|
||||
"""Calcule la vulnérabilité en fonction des seuils.
|
||||
|
||||
Args:
|
||||
v1 (int): Valeur de vulnérabilité 1.
|
||||
v2 (int): Valeur de vulnérabilité 2.
|
||||
t1 (str): Type 1.
|
||||
t2 (str): Type 2.
|
||||
seuils (dict): Dictionnaire des seuils.
|
||||
|
||||
Returns:
|
||||
tuple[int, str, str, str]: Poids et couleurs pour les deux types.
|
||||
"""
|
||||
v1_poids = 1
|
||||
v1_couleur = "Vert"
|
||||
if v1 > seuils[t1]["rouge"]["min"]:
|
||||
v1_poids = 3
|
||||
v1_couleur = "Rouge"
|
||||
elif v1 > seuils[t1]["vert"]["max"]:
|
||||
v1_poids = 2
|
||||
v1_couleur = "Orange"
|
||||
|
||||
v2_poids = 1
|
||||
v2_couleur = "Vert"
|
||||
if v2 > seuils[t2]["rouge"]["min"]:
|
||||
v2_poids = 3
|
||||
v2_couleur = "Rouge"
|
||||
elif v2 > seuils[t2]["vert"]["max"]:
|
||||
v2_poids = 2
|
||||
v2_couleur = "Orange"
|
||||
|
||||
poids = v1_poids * v2_poids
|
||||
couleur = "Rouge"
|
||||
if poids <= 2:
|
||||
couleur = "Vert"
|
||||
elif poids <= 4:
|
||||
couleur = "Orange"
|
||||
|
||||
return poids, couleur, v1_couleur, v2_couleur
|
||||
|
||||
def colorer_couleurs(la_couleur: str) -> str:
|
||||
"""Convertit une couleur en badge Markdown.
|
||||
|
||||
Args:
|
||||
la_couleur (str): Couleur à convertir (rouge/difficile, orange/modérée, vert/facile).
|
||||
|
||||
Returns:
|
||||
str: Badge Markdown correspondant.
|
||||
"""
|
||||
t = la_couleur.lower()
|
||||
if t == "rouge" or t == "difficile":
|
||||
return f":red-badge[{la_couleur}]"
|
||||
if t == "orange" or t == "modérée":
|
||||
return f":orange-badge[{la_couleur}]"
|
||||
if t == "vert" or t == "facile":
|
||||
return f":green-badge[{la_couleur}]"
|
||||
return la_couleur
|
||||
|
||||
def initialiser_seuils(config_path: str) -> dict:
|
||||
"""Charge les seuils depuis un fichier YAML de configuration.
|
||||
|
||||
Args:
|
||||
config_path (str): Chemin vers le fichier de configuration YAML.
|
||||
|
||||
Returns:
|
||||
dict: Dictionnaire des seuils chargés.
|
||||
"""
|
||||
seuils = {}
|
||||
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f)
|
||||
seuils = config.get("seuils", seuils)
|
||||
except FileNotFoundError:
|
||||
st.warning(f"Fichier de configuration {config_path} non trouvé.")
|
||||
|
||||
return seuils
|
||||
243
app/plan_d_action/utils/data/pda_interface.py
Normal file
243
app/plan_d_action/utils/data/pda_interface.py
Normal file
@ -0,0 +1,243 @@
|
||||
import streamlit as st
|
||||
|
||||
def afficher_bloc_ihh_isg(titre, ihh, isg, details_content="", ui = True) -> str|None:
|
||||
contenu_bloc = ""
|
||||
if ui:
|
||||
st.markdown(f"### {titre}")
|
||||
else:
|
||||
contenu_bloc = f"### {titre}\n"
|
||||
|
||||
if not details_content:
|
||||
st.markdown("Données non disponibles")
|
||||
return
|
||||
|
||||
lines = details_content.split('\n')
|
||||
|
||||
# 1. Afficher vulnérabilité combinée en premier
|
||||
if "#### Vulnérabilité combinée IHH-ISG" in details_content:
|
||||
contenu_md = "#### Vulnérabilité combinée IHH-ISG\n"
|
||||
contenu_md += afficher_section_texte(lines, "#### Vulnérabilité combinée IHH-ISG", "###")
|
||||
contenu_md += "\n"
|
||||
if ui:
|
||||
conteneur, = st.columns([1], gap="small", border=True)
|
||||
with conteneur:
|
||||
st.markdown(contenu_md)
|
||||
else:
|
||||
contenu_bloc += contenu_md
|
||||
|
||||
# 2. Afficher ISG des pays impliqués
|
||||
if "##### ISG des pays impliqués" in details_content:
|
||||
# Afficher le résumé ISG combiné
|
||||
for line in lines:
|
||||
if "**ISG combiné:" in line:
|
||||
st.markdown(line)
|
||||
break
|
||||
|
||||
contenu_md = "#### ISG des pays impliqués\n"
|
||||
contenu_md += afficher_section_avec_tableau(lines, "##### ISG des pays impliqués")
|
||||
contenu_md += "\n"
|
||||
if ui:
|
||||
st.markdown(contenu_md)
|
||||
else:
|
||||
contenu_bloc += contenu_md
|
||||
|
||||
# 3. Afficher la section IHH complète
|
||||
if "#### Indice de Herfindahl-Hirschmann" in details_content:
|
||||
contenu_md = "#### Indice de Herfindahl-Hirschmann\n"
|
||||
|
||||
# Tableau de résumé IHH
|
||||
contenu_md += afficher_section_avec_tableau(lines, "#### Indice de Herfindahl-Hirschmann")
|
||||
contenu_md += "\n"
|
||||
if ui:
|
||||
st.markdown(contenu_md)
|
||||
else:
|
||||
contenu_bloc += contenu_md
|
||||
|
||||
# IHH par entreprise
|
||||
if "##### IHH par entreprise (acteurs)" in details_content:
|
||||
contenu_md = "##### IHH par entreprise (acteurs)\n"
|
||||
contenu_md += afficher_section_texte(lines, "##### IHH par entreprise (acteurs)", "##### IHH par pays")
|
||||
st.markdown(contenu_md)
|
||||
|
||||
# IHH par pays
|
||||
if "##### IHH par pays" in details_content:
|
||||
contenu_md = "##### IHH par pays\n"
|
||||
contenu_md += afficher_section_texte(lines, "##### IHH par pays", "##### En résumé")
|
||||
contenu_md += "\n"
|
||||
if ui:
|
||||
st.markdown(contenu_md)
|
||||
else:
|
||||
contenu_bloc += contenu_md
|
||||
|
||||
# En résumé
|
||||
if "##### En résumé" in details_content:
|
||||
contenu_md = "##### En résumé\n"
|
||||
contenu_md += afficher_section_texte(lines, "##### En résumé", "####")
|
||||
contenu_md += "\n"
|
||||
if ui:
|
||||
st.markdown(contenu_md)
|
||||
else:
|
||||
contenu_bloc += contenu_md
|
||||
|
||||
if not ui:
|
||||
return contenu_bloc
|
||||
else:
|
||||
return None
|
||||
|
||||
def afficher_section_avec_tableau(lines, section_start, section_end=None):
|
||||
"""Affiche une section contenant un tableau"""
|
||||
in_section = False
|
||||
table_lines = []
|
||||
|
||||
for line in lines:
|
||||
if section_start in line:
|
||||
in_section = True
|
||||
continue
|
||||
elif in_section and section_end and section_end in line:
|
||||
break
|
||||
elif in_section and line.startswith('#') and section_start not in line:
|
||||
break
|
||||
elif in_section:
|
||||
if line.strip().startswith('|'):
|
||||
table_lines.append(line)
|
||||
elif table_lines and not line.strip().startswith('|'):
|
||||
# Fin du tableau
|
||||
break
|
||||
|
||||
if table_lines:
|
||||
contenu = '\n'.join(table_lines)
|
||||
return contenu
|
||||
|
||||
def afficher_section_texte(lines, section_start, section_end_marker=None):
|
||||
"""Affiche le texte d'une section sans les tableaux"""
|
||||
in_section = False
|
||||
contenu_md = []
|
||||
|
||||
for line in lines:
|
||||
if section_start in line:
|
||||
in_section = True
|
||||
continue
|
||||
elif in_section and section_end_marker and line.startswith(section_end_marker):
|
||||
break
|
||||
elif in_section and line.startswith('#') and section_start not in line:
|
||||
break
|
||||
elif in_section and line.strip() and not line.strip().startswith('|'):
|
||||
contenu_md.append(line + '\n')
|
||||
|
||||
contenu = '\n'.join(contenu_md)
|
||||
return contenu
|
||||
|
||||
def afficher_description(titre, description, ui = True) -> str|None:
|
||||
contenu_bloc = ""
|
||||
if ui:
|
||||
st.markdown(f"### {titre}")
|
||||
else:
|
||||
contenu_bloc = f"### {titre}\n"
|
||||
|
||||
if description:
|
||||
lines = description.split('\n')
|
||||
description_lines = []
|
||||
|
||||
# Extraire le premier paragraphe descriptif
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
if description_lines: # Si on a déjà du contenu, une ligne vide termine le paragraphe
|
||||
break
|
||||
continue
|
||||
# Arrêter aux titres de sections ou tableaux
|
||||
if (line.startswith('####') or
|
||||
line.startswith('|') or
|
||||
line.startswith('**Unité')):
|
||||
break
|
||||
description_lines.append(line)
|
||||
|
||||
if description_lines:
|
||||
# Rejoindre les lignes en un seul paragraphe
|
||||
contenu_md = ' '.join(description_lines)
|
||||
else:
|
||||
contenu_md = "Description non disponible"
|
||||
else:
|
||||
contenu_md = "Description non disponible"
|
||||
|
||||
if ui:
|
||||
conteneur, = st.columns([1], gap="small", border=True)
|
||||
with conteneur:
|
||||
st.markdown(contenu_md)
|
||||
return None
|
||||
else:
|
||||
contenu_bloc += contenu_md
|
||||
return contenu_bloc
|
||||
|
||||
def afficher_caracteristiques_minerai(minerai, mineraux_data, details_content="", ui = True) -> str|None:
|
||||
contenu_bloc = ""
|
||||
if ui:
|
||||
st.markdown("### Caractéristiques générales")
|
||||
else:
|
||||
contenu_bloc = "### Caractéristiques générales\n"
|
||||
|
||||
if not details_content:
|
||||
if ui:
|
||||
st.markdown("Données non disponibles")
|
||||
else:
|
||||
contenu_bloc += "Données non disponibles\n"
|
||||
return
|
||||
|
||||
lines = details_content.split('\n')
|
||||
|
||||
# 3. Afficher la vulnérabilité combinée ICS-IVC en dernier
|
||||
if "#### Vulnérabilité combinée ICS-IVC" in details_content:
|
||||
contenu_md = "#### Vulnérabilité combinée ICS-IVC\n"
|
||||
contenu_md += afficher_section_texte(lines, "#### Vulnérabilité combinée ICS-IVC", "####")
|
||||
contenu_md += "\n"
|
||||
if ui:
|
||||
conteneur, = st.columns([1], gap="small", border=True)
|
||||
with conteneur:
|
||||
st.markdown(contenu_md)
|
||||
else:
|
||||
contenu_bloc += contenu_md
|
||||
|
||||
# 1. Afficher la section ICS complète
|
||||
if "#### ICS" in details_content:
|
||||
contenu_md = "#### ICS\n"
|
||||
|
||||
# Afficher le premier tableau ICS (avec toutes les colonnes)
|
||||
contenu_md += afficher_section_avec_tableau(lines, "#### ICS", "##### Valeurs d'ICS par composant concerné")
|
||||
contenu_md += "\n"
|
||||
if ui:
|
||||
st.markdown(contenu_md)
|
||||
else:
|
||||
contenu_bloc += contenu_md
|
||||
|
||||
# Afficher la sous-section "Valeurs d'ICS par composant"
|
||||
if "##### Valeurs d'ICS par composant concerné" in details_content:
|
||||
contenu_md = "##### Valeurs d'ICS par composant concerné\n"
|
||||
|
||||
# Afficher le résumé ICS moyen
|
||||
for line in lines:
|
||||
if "**ICS moyen" in line:
|
||||
contenu_md += line
|
||||
break
|
||||
|
||||
contenu_md += "\n"
|
||||
contenu_md += afficher_section_avec_tableau(lines, "##### Valeurs d'ICS par composant concerné", "**ICS moyen")
|
||||
contenu_md += "\n"
|
||||
|
||||
if ui:
|
||||
st.markdown(contenu_md)
|
||||
else:
|
||||
contenu_bloc += contenu_md
|
||||
|
||||
# 2. Afficher la section IVC complète
|
||||
if "#### IVC" in details_content:
|
||||
contenu_md = "#### IVC\n"
|
||||
|
||||
# Afficher tous les détails de la section IVC
|
||||
contenu_md += afficher_section_texte(lines, "#### IVC", "#### Vulnérabilité combinée ICS-IVC")
|
||||
contenu_md += "\n"
|
||||
if ui:
|
||||
st.markdown(contenu_md)
|
||||
return None
|
||||
else:
|
||||
contenu_bloc += contenu_md
|
||||
return contenu_bloc
|
||||
379
app/plan_d_action/utils/data/plan_d_action.py
Normal file
379
app/plan_d_action/utils/data/plan_d_action.py
Normal file
@ -0,0 +1,379 @@
|
||||
import streamlit as st
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from app.plan_d_action.utils.data import (
|
||||
PRECONISATIONS,
|
||||
INDICATEURS,
|
||||
poids_operation,
|
||||
parse_chains_md,
|
||||
set_vulnerability,
|
||||
colorer_couleurs,
|
||||
afficher_bloc_ihh_isg,
|
||||
afficher_description,
|
||||
afficher_caracteristiques_minerai,
|
||||
initialiser_seuils
|
||||
)
|
||||
|
||||
def calcul_poids_chaine(poids_A: int, poids_F: int, poids_T: int, poids_E: int, poids_M: int) -> tuple[str,dict,int]:
|
||||
poids_total = (\
|
||||
poids_A * poids_operation["Assemblage"] + \
|
||||
poids_F * poids_operation["Fabrication"] + \
|
||||
poids_T * poids_operation["Traitement"] + \
|
||||
poids_E * poids_operation["Extraction"] + \
|
||||
poids_M * poids_operation["Substitution"] \
|
||||
) / sum(poids_operation.values())
|
||||
|
||||
if poids_total < 3:
|
||||
criticite_chaine = "Modérée"
|
||||
niveau_criticite = {"Facile"}
|
||||
elif poids_total < 6:
|
||||
criticite_chaine = "Élevée"
|
||||
niveau_criticite = {"Facile", "Modérée"}
|
||||
else:
|
||||
criticite_chaine = "Critique"
|
||||
niveau_criticite = {"Facile", "Modérée", "Difficile"}
|
||||
|
||||
return criticite_chaine, niveau_criticite, poids_total
|
||||
|
||||
def analyser_chaines(chaines: list[dict], produits: dict, composants: dict, mineraux: dict, seuils: dict, top_n: int = 0) -> list[tuple[str, str, int]]:
|
||||
resultats = []
|
||||
|
||||
for chaine in chaines:
|
||||
sel_prod = chaine["produit"]
|
||||
sel_comp = chaine["composant"]
|
||||
sel_miner = chaine["minerai"]
|
||||
|
||||
poids_A, *_ = set_vulnerability(produits[sel_prod]["IHH_Assemblage"], produits[sel_prod]["ISG_Assemblage"], "IHH", "ISG", seuils)
|
||||
poids_F, *_ = set_vulnerability(composants[sel_comp]["IHH_Fabrication"], composants[sel_comp]["ISG_Fabrication"], "IHH", "ISG", seuils)
|
||||
poids_T, *_ = set_vulnerability(mineraux[sel_miner]["IHH_Traitement"], mineraux[sel_miner]["ISG_Traitement"], "IHH", "ISG", seuils)
|
||||
poids_E, *_ = set_vulnerability(mineraux[sel_miner]["IHH_Extraction"], mineraux[sel_miner]["ISG_Extraction"], "IHH", "ISG", seuils)
|
||||
poids_M, *_ = set_vulnerability(mineraux[sel_miner]["ICS"], mineraux[sel_miner]["IVC"], "ICS", "IVC", seuils)
|
||||
|
||||
criticite_chaine, niveau_criticite, poids_total = calcul_poids_chaine(
|
||||
poids_A, poids_F, poids_T, poids_E, poids_M
|
||||
)
|
||||
|
||||
resultats.append({
|
||||
"chaine": chaine,
|
||||
"criticite_chaine": criticite_chaine,
|
||||
"niveau_criticite": niveau_criticite,
|
||||
"poids_total": poids_total
|
||||
})
|
||||
|
||||
# Tri décroissant
|
||||
resultats.sort(key=lambda x: x["poids_total"], reverse=True)
|
||||
|
||||
# Si top_n n'est pas spécifié, tout est retourné
|
||||
if top_n == 0 or top_n >= len(resultats):
|
||||
return resultats
|
||||
|
||||
# Déterminer le seuil de coupure
|
||||
seuil_poids = resultats[top_n - 1]["poids_total"]
|
||||
|
||||
# Inclure tous ceux dont le poids est égal au seuil
|
||||
top_resultats = [r for r in resultats if r["poids_total"] >= seuil_poids]
|
||||
|
||||
return top_resultats
|
||||
|
||||
def tableau_de_bord(chains: list[dict], produits: dict, composants: dict, mineraux: dict, seuils: dict):
|
||||
col_left, col_right = st.columns([2, 3], gap="small", border=True)
|
||||
with col_left:
|
||||
st.markdown("**<u>Panneau de sélection</u>**", unsafe_allow_html=True)
|
||||
|
||||
produits_disponibles = sorted({c["produit"] for c in chains})
|
||||
sel_prod = st.selectbox("Produit", produits_disponibles, index=produits_disponibles.index(st.session_state.sel_prod) if st.session_state.sel_prod else 0)
|
||||
if sel_prod != st.session_state.sel_prod:
|
||||
st.session_state.sel_prod = sel_prod
|
||||
st.session_state.sel_comp = None
|
||||
st.session_state.sel_miner = None
|
||||
st.rerun()
|
||||
|
||||
composants_dispo = sorted({c["composant"] for c in chains if c["produit"] == sel_prod})
|
||||
sel_comp = st.selectbox("Composant", composants_dispo, index=composants_dispo.index(st.session_state.sel_comp) if st.session_state.sel_comp else 0)
|
||||
if sel_comp != st.session_state.sel_comp:
|
||||
st.session_state.sel_comp = sel_comp
|
||||
st.session_state.sel_miner = None
|
||||
st.rerun()
|
||||
|
||||
mineraux_dispo = sorted({c["minerai"] for c in chains if c["produit"] == sel_prod and c["composant"] == sel_comp})
|
||||
sel_miner = st.selectbox("Minerai", mineraux_dispo, index=mineraux_dispo.index(st.session_state.sel_miner) if st.session_state.sel_miner else 0)
|
||||
if sel_miner != st.session_state.sel_miner:
|
||||
st.session_state.sel_miner = sel_miner
|
||||
st.rerun()
|
||||
|
||||
with col_right:
|
||||
top_chains = analyser_chaines(chains, produits, composants, mineraux, seuils, top_n=5)
|
||||
st.markdown("**<u>Top chaînes critiques pour sélection rapide</u>**", unsafe_allow_html=True)
|
||||
for i, entry in enumerate(top_chains):
|
||||
ch = entry["chaine"]
|
||||
poids = entry["poids_total"]
|
||||
criticite = entry["criticite_chaine"]
|
||||
if st.button(f"**{ch['produit']} <-> {ch['composant']} <-> {ch['minerai']}** : {poids:.2f} → {criticite}", key=f"select_{i}"):
|
||||
st.session_state.sel_prod = ch["produit"]
|
||||
st.session_state.sel_comp = ch["composant"]
|
||||
st.session_state.sel_miner = ch["minerai"]
|
||||
st.rerun()
|
||||
|
||||
c1, c2 = st.columns([3, 2], gap="small", border=True, vertical_alignment='center')
|
||||
with c1:
|
||||
st.markdown("**<u>Synthèse des criticités</u>**", unsafe_allow_html=True)
|
||||
poids_A, couleur_A, couleur_A_ihh, couleur_A_isg = set_vulnerability(produits[sel_prod]["IHH_Assemblage"], produits[sel_prod]["ISG_Assemblage"], "IHH", "ISG", seuils)
|
||||
poids_F, couleur_F, couleur_F_ihh, couleur_F_isg = set_vulnerability(composants[sel_comp]["IHH_Fabrication"], composants[sel_comp]["ISG_Fabrication"], "IHH", "ISG", seuils)
|
||||
poids_T, couleur_T, couleur_T_ihh, couleur_T_isg = set_vulnerability(mineraux[sel_miner]["IHH_Traitement"], mineraux[sel_miner]["ISG_Traitement"], "IHH", "ISG", seuils)
|
||||
poids_E, couleur_E, couleur_E_ihh, couleur_E_isg = set_vulnerability(mineraux[sel_miner]["IHH_Extraction"], mineraux[sel_miner]["ISG_Extraction"], "IHH", "ISG", seuils)
|
||||
poids_M, couleur_M, couleur_M_ics, couleur_M_ivc = set_vulnerability(mineraux[sel_miner]["ICS"], mineraux[sel_miner]["IVC"], "ICS", "IVC", seuils)
|
||||
|
||||
st.markdown(f"* **{sel_prod} - Assemblage** : {colorer_couleurs(couleur_A)} ({poids_A})")
|
||||
st.markdown(f"* **{sel_comp} - Fabrication** : {colorer_couleurs(couleur_F)} ({poids_F})")
|
||||
st.markdown(f"* **{sel_miner} - Traitement** : {colorer_couleurs(couleur_T)} ({poids_T})")
|
||||
st.markdown(f"* **{sel_miner} - Extraction** : {colorer_couleurs(couleur_E)} ({poids_E})")
|
||||
st.markdown(f"* **{sel_miner} - Minerai** : {colorer_couleurs(couleur_M)} ({poids_M})")
|
||||
|
||||
criticite_chaine, niveau_criticite, poids_total = calcul_poids_chaine(poids_A, poids_F, poids_T, poids_E, poids_M)
|
||||
with c2:
|
||||
st.error(f"**Criticité globale : {criticite_chaine} ({poids_total})**")
|
||||
|
||||
return (
|
||||
sel_prod, sel_comp, sel_miner, niveau_criticite,
|
||||
couleur_A, poids_A, couleur_F, poids_F, couleur_T, poids_T, couleur_E, poids_E, couleur_M, poids_M,
|
||||
couleur_A_ihh, couleur_A_isg, couleur_F_ihh, couleur_F_isg, couleur_T_ihh, couleur_T_isg,couleur_E_ihh, couleur_E_isg, couleur_M_ics, couleur_M_ivc
|
||||
)
|
||||
|
||||
def afficher_criticites(produits: dict, composants: dict, mineraux: dict, sel_prod: str, sel_comp: str, sel_miner: str, seuils: dict) -> None:
|
||||
with st.expander("Vue d’ensemble des criticités", expanded=True):
|
||||
st.markdown("## Vue d’ensemble des criticités", unsafe_allow_html=True)
|
||||
|
||||
col_left, col_right = st.columns([1, 1], gap="small", border=True)
|
||||
|
||||
with col_left:
|
||||
fig1, ax1 = plt.subplots(figsize=(2, 2.4))
|
||||
ax1.scatter([produits[sel_prod]["ISG_Assemblage"]], [produits[sel_prod]["IHH_Assemblage"]], label="Assemblage", s=5)
|
||||
ax1.scatter([composants[sel_comp]["ISG_Fabrication"]], [composants[sel_comp]["IHH_Fabrication"]], label="Fabrication", s=5)
|
||||
ax1.scatter([mineraux[sel_miner]["ISG_Extraction"]], [mineraux[sel_miner]["IHH_Extraction"]], label="Extraction", s=5)
|
||||
ax1.scatter([mineraux[sel_miner]["ISG_Traitement"]], [mineraux[sel_miner]["IHH_Traitement"]], label="Traitement", s=5)
|
||||
|
||||
# Seuils ISG (vertical)
|
||||
ax1.axvline(seuils["ISG"]["vert"]["max"], linestyle='--', color='green', alpha=0.7, linewidth=0.5) # Seuil vert-orange
|
||||
ax1.axvline(seuils["ISG"]["rouge"]["min"], linestyle='--', color='red', alpha=0.7, linewidth=0.5) # Seuil orange-rouge
|
||||
|
||||
# Seuils IHH (horizontal)
|
||||
ax1.axhline(seuils["IHH"]["vert"]["max"], linestyle='--', color='green', alpha=0.7, linewidth=0.5) # Seuil vert-orange
|
||||
ax1.axhline(seuils["IHH"]["rouge"]["min"], linestyle='--', color='red', alpha=0.7, linewidth=0.5) # Seuil orange-rouge
|
||||
|
||||
ax1.set_xlim(0, 100)
|
||||
ax1.set_ylim(0, 100)
|
||||
ax1.set_xlabel("ISG", fontsize=4)
|
||||
ax1.set_ylabel("IHH", fontsize=4)
|
||||
ax1.tick_params(axis='both', which='major', labelsize=4)
|
||||
ax1.legend(bbox_to_anchor=(0.5, -0.25), loc='upper center', fontsize=4)
|
||||
plt.tight_layout()
|
||||
st.pyplot(fig1)
|
||||
|
||||
with col_right:
|
||||
fig2, ax2 = plt.subplots(figsize=(2, 2.1))
|
||||
ax2.scatter([mineraux[sel_miner]["IVC"]], [mineraux[sel_miner]["ICS"]], color='green', s=5, label=sel_miner)
|
||||
|
||||
# Seuils IVC (vertical)
|
||||
ax2.axvline(seuils["IVC"]["vert"]["max"], linestyle='--', color='green', alpha=0.7, linewidth=0.5) # Seuil vert-orange
|
||||
ax2.axvline(seuils["IVC"]["rouge"]["min"], linestyle='--', color='red', alpha=0.7, linewidth=0.5) # Seuil orange-rouge
|
||||
|
||||
# Seuils ICS (horizontal)
|
||||
ax2.axhline(seuils["ICS"]["vert"]["max"], linestyle='--', color='green', alpha=0.7, linewidth=0.5) # Seuil vert-orange
|
||||
ax2.axhline(seuils["ICS"]["rouge"]["min"], linestyle='--', color='red', alpha=0.7, linewidth=0.5) # Seuil orange-rouge
|
||||
|
||||
ax2.set_xlim(0, max(100, mineraux[sel_miner]["IVC"]))
|
||||
ax2.set_ylim(0, 1)
|
||||
ax2.set_xlabel("IVC", fontsize=4)
|
||||
ax2.set_ylabel("ICS", fontsize=4)
|
||||
ax2.tick_params(axis='both', which='major', labelsize=4)
|
||||
ax2.legend(bbox_to_anchor=(0.5, -0.25), loc='upper center', fontsize=4)
|
||||
plt.tight_layout()
|
||||
st.pyplot(fig2)
|
||||
|
||||
st.markdown(f"""
|
||||
Les lignes pointillées en {colorer_couleurs("vert")} ou {colorer_couleurs("rouge")} représentent les seuils des indices concernés.\n
|
||||
Les indices ISG (stabilité géopolitique) et IVC (concurrence intersectorielle) influent sur la probabilité de survenance d'un risque.\n
|
||||
Les indices IHH (concentration géographique) et ICS (capacité de substitution) influent sur le niveau d'impact d'un risque.\n
|
||||
Une opération se trouvant au-dessus des deux seuils a donc une forte probabilité d'être impactée avec un niveau élevé sur l'incapacité à continuer la production.
|
||||
""")
|
||||
|
||||
def afficher_explications_et_details(
|
||||
couleur_A, poids_A, couleur_F, poids_F, couleur_T, poids_T, couleur_E, poids_E, couleur_M, poids_M,
|
||||
produits, composants, mineraux, sel_prod, sel_comp, sel_miner,
|
||||
couleur_A_ihh, couleur_A_isg, couleur_F_ihh, couleur_F_isg, couleur_T_ihh, couleur_T_isg,couleur_E_ihh, couleur_E_isg, couleur_M_ics, couleur_M_ivc, ui = True) -> str|None:
|
||||
with st.expander("Explications et détails", expanded = True):
|
||||
from collections import Counter
|
||||
couleurs = [couleur_A, couleur_F, couleur_T, couleur_E, couleur_M]
|
||||
compte = Counter(couleurs)
|
||||
nb_rouge = compte["Rouge"]
|
||||
nb_orange = compte["Orange"]
|
||||
nb_vert = compte["Vert"]
|
||||
|
||||
contenu_md = f"""
|
||||
Pour cette chaîne :blue-background[**{sel_prod} <-> {sel_comp} <-> {sel_miner}**], avec {nb_rouge} criticité(s) de niveau {colorer_couleurs("Rouge")}, {nb_orange} {colorer_couleurs("Orange")} et {nb_vert} {colorer_couleurs("Vert")}, les indices individuels par opération sont :
|
||||
|
||||
* **{sel_prod} - Assemblage** : {colorer_couleurs(couleur_A)} ({poids_A})
|
||||
* IHH = {produits[sel_prod]["IHH_Assemblage"]} ({colorer_couleurs(couleur_A_ihh)}) <-> ISG = {produits[sel_prod]["ISG_Assemblage"]} ({colorer_couleurs(couleur_A_isg)})
|
||||
* pondération de l'Assemblage dans le calcul de la criticité globale : 1,5
|
||||
* se référer à **{sel_prod} et Assemblage** plus bas pour le détail complet
|
||||
* **{sel_comp} - Fabrication** : {colorer_couleurs(couleur_F)} ({poids_F})
|
||||
* IHH = {composants[sel_comp]["IHH_Fabrication"]} ({colorer_couleurs(couleur_F_ihh)}) <-> ISG = {composants[sel_comp]["ISG_Fabrication"]} ({colorer_couleurs(couleur_F_isg)})
|
||||
* pondération de la Fabrication dans le calcul de la criticité globale : 2
|
||||
* se référer à **{sel_comp} et Fabrication** plus bas pour le détail complet
|
||||
* **{sel_miner} - Traitement** : {colorer_couleurs(couleur_A)} ({poids_A})
|
||||
* IHH = {mineraux[sel_miner]["IHH_Traitement"]} ({colorer_couleurs(couleur_T_ihh)}) <-> ISG = {mineraux[sel_miner]["ISG_Traitement"]} ({colorer_couleurs(couleur_T_isg)})
|
||||
* pondération du Traitement dans le calcul de la criticité globale : 1,5
|
||||
* se référer à **{sel_miner} — Vue globale** plus bas pour le détail complet de l'ensemble du minerai
|
||||
* **{sel_miner} - Extraction** : {colorer_couleurs(couleur_E)} ({poids_E})
|
||||
* IHH = {mineraux[sel_miner]["IHH_Extraction"]} ({colorer_couleurs(couleur_E_ihh)}) <-> ISG = {mineraux[sel_miner]["ISG_Extraction"]} ({colorer_couleurs(couleur_E_isg)})
|
||||
* pondération de l'Extraction dans le calcul de la criticité globale : 1
|
||||
* **{sel_miner} - Minerai** : {colorer_couleurs(couleur_M)} ({poids_M})
|
||||
* ICS = {mineraux[sel_miner]["ICS"]} ({colorer_couleurs(couleur_M_ics)}) <-> IVC = {mineraux[sel_miner]["IVC"]} ({colorer_couleurs(couleur_M_ivc)})
|
||||
* pondération de la Substitution dans le calcul de la criticité globale : 2
|
||||
"""
|
||||
contenu_md += "\n"
|
||||
if ui:
|
||||
st.markdown(contenu_md)
|
||||
return None
|
||||
else:
|
||||
return contenu_md
|
||||
|
||||
def afficher_preconisations_et_indicateurs_generiques(niveau_criticite: dict, poids_A: int, poids_F: int, poids_T: int, poids_E: int, poids_M: int, ui: bool = True) -> tuple[str|None,str|None]:
|
||||
contenu_md_left = "### Préconisations :\n\n"
|
||||
contenu_md_left += "Mise en œuvre : \n"
|
||||
|
||||
for niveau, contenu in PRECONISATIONS.items():
|
||||
if niveau in niveau_criticite:
|
||||
contenu_md_left += f"* {colorer_couleurs(niveau)}\n"
|
||||
for p in PRECONISATIONS[niveau]:
|
||||
contenu_md_left += f" - {p}\n"
|
||||
|
||||
contenu_md_right = "### Indicateurs :\n\n"
|
||||
contenu_md_right += "Mise en œuvre : \n"
|
||||
|
||||
for niveau, contenu in INDICATEURS.items():
|
||||
if niveau in niveau_criticite:
|
||||
contenu_md_right += f"* {colorer_couleurs(niveau)}\n"
|
||||
for p in INDICATEURS[niveau]:
|
||||
contenu_md_right += f" - {p}\n"
|
||||
|
||||
if ui:
|
||||
with st.expander("Préconisations et indicateurs génériques"):
|
||||
col_left, col_right = st.columns([1, 1], gap="small", border=True)
|
||||
with col_left:
|
||||
st.markdown(contenu_md_left)
|
||||
with col_right:
|
||||
st.markdown(contenu_md_right)
|
||||
return None, None
|
||||
else:
|
||||
return contenu_md_left, contenu_md_right
|
||||
|
||||
def afficher_preconisations_specifiques(operation: str, niveau_criticite_operation: dict) -> str:
|
||||
contenu_md = "#### Préconisations :\n\n"
|
||||
contenu_md += "Mise en œuvre : \n"
|
||||
for niveau, contenu in PRECONISATIONS[operation].items():
|
||||
if niveau in niveau_criticite_operation[operation]:
|
||||
contenu_md += f"* {colorer_couleurs(niveau)}\n"
|
||||
for p in PRECONISATIONS[operation][niveau]:
|
||||
contenu_md += f" - {p}\n"
|
||||
return(contenu_md)
|
||||
|
||||
def afficher_indicateurs_specifiques(operation: str, niveau_criticite_operation: dict) -> str:
|
||||
contenu_md = "#### Indicateurs :\n\n"
|
||||
contenu_md += "Mise en œuvre : \n"
|
||||
for niveau, contenu in INDICATEURS[operation].items():
|
||||
if niveau in niveau_criticite_operation[operation]:
|
||||
contenu_md += f"* {colorer_couleurs(niveau)}\n"
|
||||
for p in INDICATEURS[operation][niveau]:
|
||||
contenu_md += f" - {p}\n"
|
||||
return(contenu_md)
|
||||
|
||||
def afficher_preconisations_et_indicateurs_specifiques(sel_prod: str, sel_comp: str, sel_miner: str, niveau_criticite_operation: dict) -> None:
|
||||
for operation in ["Assemblage", "Fabrication", "Traitement", "Extraction"]:
|
||||
if operation == "Assemblage":
|
||||
item = sel_prod
|
||||
elif operation == "Fabrication":
|
||||
item = sel_comp
|
||||
else:
|
||||
item = sel_miner
|
||||
with st.expander(f"Préconisations et indicateurs spécifiques - {operation}"):
|
||||
st.markdown(f"### {operation} -> :blue-background[{item}]")
|
||||
col_left, col_right = st.columns([1, 1], gap="small", border=True)
|
||||
with col_left:
|
||||
st.markdown(afficher_preconisations_specifiques(operation, niveau_criticite_operation))
|
||||
with col_right:
|
||||
st.markdown(afficher_indicateurs_specifiques(operation, niveau_criticite_operation))
|
||||
|
||||
def afficher_preconisations_et_indicateurs(niveau_criticite: dict, sel_prod: str, sel_comp: str, sel_miner: str, poids_A: int, poids_F: int, poids_T: int, poids_E: int, poids_M: int) -> None:
|
||||
st.markdown("## Préconisations et indicateurs")
|
||||
|
||||
afficher_preconisations_et_indicateurs_generiques(niveau_criticite, poids_A, poids_F, poids_T, poids_E, poids_M)
|
||||
|
||||
def affectation_poids(poids_operation):
|
||||
if poids_operation < 3:
|
||||
niveau_criticite = {"Facile"}
|
||||
elif poids_operation < 6:
|
||||
niveau_criticite = {"Facile", "Modérée"}
|
||||
else:
|
||||
niveau_criticite = {"Facile", "Modérée", "Difficile"}
|
||||
return niveau_criticite
|
||||
|
||||
niveau_criticite_operation = {}
|
||||
niveau_criticite_operation["Assemblage"] = affectation_poids(poids_A)
|
||||
niveau_criticite_operation["Fabrication"] = affectation_poids(poids_F)
|
||||
niveau_criticite_operation["Traitement"] = affectation_poids(poids_T)
|
||||
niveau_criticite_operation["Extraction"] = affectation_poids(poids_E)
|
||||
|
||||
afficher_preconisations_et_indicateurs_specifiques(sel_prod, sel_comp, sel_miner, niveau_criticite_operation)
|
||||
|
||||
def afficher_details_operations(produits, composants, mineraux, sel_prod, sel_comp, sel_miner, details_sections) -> None:
|
||||
st.markdown("## Détails des opérations")
|
||||
|
||||
with st.expander(f"{sel_prod} et Assemblage"):
|
||||
assemblage_details = details_sections.get(f"{sel_prod}_assemblage", "")
|
||||
|
||||
afficher_description(f"{sel_prod} et Assemblage", assemblage_details)
|
||||
afficher_bloc_ihh_isg("Assemblage", produits[sel_prod]["IHH_Assemblage"], produits[sel_prod]["ISG_Assemblage"], assemblage_details)
|
||||
|
||||
with st.expander(f"{sel_comp} et Fabrication"):
|
||||
fabrication_details = details_sections.get(f"{sel_comp}_fabrication", "")
|
||||
afficher_description(f"{sel_comp} et Fabrication", fabrication_details)
|
||||
afficher_bloc_ihh_isg("Fabrication", composants[sel_comp]["IHH_Fabrication"], composants[sel_comp]["ISG_Fabrication"], fabrication_details)
|
||||
|
||||
with st.expander(f"{sel_miner} — Vue globale"):
|
||||
minerai_general = details_sections.get(f"{sel_miner}_general", "")
|
||||
afficher_description(f"{sel_miner} — Vue globale", minerai_general)
|
||||
|
||||
extraction_details = details_sections.get(f"{sel_miner}_extraction", "")
|
||||
afficher_bloc_ihh_isg("Extraction", mineraux[sel_miner]["IHH_Extraction"], mineraux[sel_miner]["ISG_Extraction"], extraction_details)
|
||||
|
||||
traitement_details = details_sections.get(f"{sel_miner}_traitement", "").removesuffix("\n---\n")
|
||||
afficher_bloc_ihh_isg("Traitement", mineraux[sel_miner]["IHH_Traitement"], mineraux[sel_miner]["ISG_Traitement"], traitement_details)
|
||||
|
||||
afficher_caracteristiques_minerai(sel_miner, mineraux[sel_miner], minerai_general)
|
||||
|
||||
def initialiser_interface(filepath: str, config_path: str = "assets/config.yaml") -> None:
|
||||
|
||||
produits, composants, mineraux, chains, descriptions, details_sections = parse_chains_md(filepath)
|
||||
|
||||
if not chains:
|
||||
st.warning("Aucune chaîne critique trouvée dans le fichier.")
|
||||
return
|
||||
|
||||
seuils = initialiser_seuils(config_path)
|
||||
|
||||
sel_prod, sel_comp, sel_miner, niveau_criticite, \
|
||||
couleur_A, poids_A, couleur_F, poids_F, couleur_T, poids_T, couleur_E, poids_E, couleur_M, poids_M, \
|
||||
couleur_A_ihh, couleur_A_isg, couleur_F_ihh, couleur_F_isg, couleur_T_ihh, couleur_T_isg,couleur_E_ihh, couleur_E_isg, couleur_M_ics, couleur_M_ivc \
|
||||
= tableau_de_bord(chains, produits, composants, mineraux, seuils)
|
||||
|
||||
afficher_criticites(produits, composants, mineraux, sel_prod, sel_comp, sel_miner, seuils)
|
||||
|
||||
afficher_explications_et_details(
|
||||
couleur_A, poids_A, couleur_F, poids_F, couleur_T, poids_T, couleur_E, poids_E, couleur_M, poids_M,
|
||||
produits, composants, mineraux, sel_prod, sel_comp, sel_miner,
|
||||
couleur_A_ihh, couleur_A_isg, couleur_F_ihh, couleur_F_isg, couleur_T_ihh, couleur_T_isg,couleur_E_ihh, couleur_E_isg, couleur_M_ics, couleur_M_ivc)
|
||||
|
||||
afficher_preconisations_et_indicateurs(niveau_criticite, sel_prod, sel_comp, sel_miner, poids_A, poids_F, poids_T, poids_E, poids_M)
|
||||
|
||||
afficher_details_operations(produits, composants, mineraux, sel_prod, sel_comp, sel_miner, details_sections)
|
||||
5
app/plan_d_action/utils/interface/__init__.py
Normal file
5
app/plan_d_action/utils/interface/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .config import CORRESPONDANCE_COULEURS
|
||||
|
||||
__all__ = [
|
||||
"CORRESPONDANCE_COULEURS"
|
||||
]
|
||||
27
app/plan_d_action/utils/interface/config.py
Normal file
27
app/plan_d_action/utils/interface/config.py
Normal file
@ -0,0 +1,27 @@
|
||||
from pathlib import Path
|
||||
|
||||
CORRESPONDANCE_COULEURS = {
|
||||
"Rouge": "red",
|
||||
"Orange": "orange",
|
||||
"Vert": "green",
|
||||
"FAIBLE": "green",
|
||||
"MODÉRÉE": "orange",
|
||||
"ÉLEVÉE à CRITIQUE": "red"
|
||||
}
|
||||
|
||||
niveau_labels = {
|
||||
0: "Produit final",
|
||||
1: "Composant",
|
||||
2: "Minerai",
|
||||
10: "Opération",
|
||||
11: "Pays d'opération",
|
||||
12: "Acteur d'opération",
|
||||
99: "Pays géographique"
|
||||
}
|
||||
|
||||
# Répertoire courant du script
|
||||
CURRENT_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Répertoire "jobs" dans app/plan_d_action
|
||||
JOBS = CURRENT_DIR / "jobs"
|
||||
JOBS.mkdir(exist_ok=True)
|
||||
65
app/plan_d_action/utils/interface/export.py
Normal file
65
app/plan_d_action/utils/interface/export.py
Normal file
@ -0,0 +1,65 @@
|
||||
from typing import Dict, Tuple, Union, List
|
||||
import networkx as nx
|
||||
|
||||
def exporter_graphe_filtre(
|
||||
G: nx.DiGraph,
|
||||
liens_chemins: List[Tuple[Union[str, int], Union[str, int]]]
|
||||
) -> nx.DiGraph:
|
||||
"""Gère l'export du graphe filtré au format DOT.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe d'origine à exporter.
|
||||
liens_chemins (list): Liste des tuples contenant les paires de nœuds reliées
|
||||
par un chemin dans le graphe, avec leurs attributs associés.
|
||||
|
||||
Returns:
|
||||
tuple: Un tuple contenant le graphe exporté sous forme de DiGraph
|
||||
et le dictionnaire des attributs du graphe exporté.
|
||||
"""
|
||||
|
||||
G_export = nx.DiGraph()
|
||||
for u, v in liens_chemins:
|
||||
G_export.add_node(u, **G.nodes[u])
|
||||
G_export.add_node(v, **G.nodes[v])
|
||||
data = G.get_edge_data(u, v)
|
||||
if isinstance(data, dict) and all(isinstance(k, int) for k in data):
|
||||
G_export.add_edge(u, v, **data[0])
|
||||
elif isinstance(data, dict):
|
||||
G_export.add_edge(u, v, **data)
|
||||
else:
|
||||
G_export.add_edge(u, v)
|
||||
|
||||
return(G_export)
|
||||
|
||||
def extraire_liens_filtres(
|
||||
chemins: List[List[Union[str, int]]],
|
||||
niveaux: Dict[str | int, int],
|
||||
niveau_depart: int,
|
||||
niveau_arrivee: int,
|
||||
niveaux_speciaux: list[int]
|
||||
) -> List[Tuple[Union[str, int], Union[str, int]]]:
|
||||
"""Extrait les liens des chemins en respectant les niveaux.
|
||||
|
||||
Args:
|
||||
chemins (list): Liste des chemins dans le graphe.
|
||||
niveaux (dict): Dictionnaire associant chaque nœud au niveau correspondant.
|
||||
niveau_depart (int): Niveau de départ pour la sélection des liens.
|
||||
niveau_arrivee (int): Niveau d'arrivée pour la sélection des liens.
|
||||
niveaux_speciaux (set): Ensemble des niveaux spéciaux à considérer dans le filtrage.
|
||||
|
||||
Returns:
|
||||
set: Ensemble des paires de nœuds constituant les liens du graphe filtré
|
||||
en respectant les niveaux et les niveaux spéciaux demandés.
|
||||
"""
|
||||
liens = set()
|
||||
for chemin in chemins:
|
||||
for i in range(len(chemin) - 1):
|
||||
u, v = chemin[i], chemin[i + 1]
|
||||
niveau_u = niveaux.get(u, 999)
|
||||
niveau_v = niveaux.get(v, 999)
|
||||
if (
|
||||
(niveau_depart <= niveau_u <= niveau_arrivee or niveau_u in niveaux_speciaux)
|
||||
and (niveau_depart <= niveau_v <= niveau_arrivee or niveau_v in niveaux_speciaux)
|
||||
):
|
||||
liens.add((u, v))
|
||||
return liens
|
||||
19
app/plan_d_action/utils/interface/niveau_utils.py
Normal file
19
app/plan_d_action/utils/interface/niveau_utils.py
Normal file
@ -0,0 +1,19 @@
|
||||
from typing import Dict
|
||||
import networkx as nx
|
||||
|
||||
def extraire_niveaux(G: nx.DiGraph) -> Dict[str | int, int]:
|
||||
"""Extrait les niveaux des nœuds du graphe.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe d'origine à analyser.
|
||||
|
||||
Returns:
|
||||
dict: Un dictionnaire associant chaque nœud au niveau correspondant.
|
||||
Les valeurs sont des entiers représentant les niveaux.
|
||||
"""
|
||||
niveaux = {}
|
||||
for node, attrs in G.nodes(data=True):
|
||||
niveau_str = attrs.get("niveau")
|
||||
if niveau_str:
|
||||
niveaux[node] = int(str(niveau_str).strip('"'))
|
||||
return niveaux
|
||||
23
app/plan_d_action/utils/interface/parser.py
Normal file
23
app/plan_d_action/utils/interface/parser.py
Normal file
@ -0,0 +1,23 @@
|
||||
from typing import Dict, Tuple, Union
|
||||
import networkx as nx
|
||||
|
||||
def preparer_graphe(G: nx.DiGraph) -> Tuple[nx.DiGraph, Dict[Union[str, int], int]]:
|
||||
"""Nettoie et prépare le graphe pour l'analyse.
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): Le graphe d'origine à nettoyer.
|
||||
|
||||
Returns:
|
||||
tuple: Un tuple contenant le graphe nettoyé et les niveaux temporels associés
|
||||
aux nœuds du graphe. Le graphe nettoyé a eu ses nœuds qui ne respectaient
|
||||
pas les critères d'admissibilité enlevés.
|
||||
"""
|
||||
niveaux_temp = {
|
||||
node: int(str(attrs.get("niveau")).strip('"'))
|
||||
for node, attrs in G.nodes(data=True)
|
||||
if attrs.get("niveau") and str(attrs.get("niveau")).strip('"').isdigit()
|
||||
}
|
||||
G.remove_nodes_from([n for n in G.nodes() if n not in niveaux_temp])
|
||||
G.remove_nodes_from(
|
||||
[n for n in G.nodes() if niveaux_temp.get(n) == 10 and 'Reserves' in n])
|
||||
return G, niveaux_temp
|
||||
156
app/plan_d_action/utils/interface/selection.py
Normal file
156
app/plan_d_action/utils/interface/selection.py
Normal file
@ -0,0 +1,156 @@
|
||||
from typing import Any, Dict, Tuple, List, Union, Optional
|
||||
import streamlit as st
|
||||
import networkx as nx
|
||||
from utils.translations import _
|
||||
from utils.persistance import maj_champ_statut, get_champ_statut, supprime_champ_statut
|
||||
|
||||
from utils.graph_utils import (
|
||||
extraire_chemins_depuis,
|
||||
extraire_chemins_vers
|
||||
)
|
||||
|
||||
def selectionner_minerais(G: nx.Graph, noeuds_depart: List[Any]) -> List[Union[str, int]]:
|
||||
"""Interface pour sélectionner les minerais si nécessaire.
|
||||
|
||||
Args:
|
||||
G (nx.Graph): Le graphe des relations entre les nœuds.
|
||||
noeuds_depart (list): Les nœuds de départ qui doivent être considérés.
|
||||
|
||||
Returns:
|
||||
list: La liste des nœuds sélectionnés comme minerais.
|
||||
"""
|
||||
minerais_selection = None
|
||||
|
||||
st.markdown(f"## {str(_('pages.plan_d_action.select_minerals'))}")
|
||||
|
||||
# Étape 1 : récupérer tous les nœuds descendants depuis les produits finaux
|
||||
descendants = set()
|
||||
for start in noeuds_depart:
|
||||
descendants.update(nx.descendants(G, start)) # tous les successeurs (récursifs)
|
||||
|
||||
# Étape 2 : ne garder que les nœuds de niveau 2 parmi les descendants
|
||||
minerais_nodes = sorted([
|
||||
n for n in descendants
|
||||
if G.nodes[n].get("niveau") and int(str(G.nodes[n].get("niveau")).strip('"')) == 2
|
||||
])
|
||||
|
||||
# Initialiser depuis champ_statut si besoin
|
||||
if "analyse_minerais" not in st.session_state:
|
||||
anciens = []
|
||||
i = 0
|
||||
while True:
|
||||
val = get_champ_statut(f"pages.plan_d_action.filter_by_minerals.{i}")
|
||||
if not val:
|
||||
break
|
||||
anciens.append(val)
|
||||
i += 1
|
||||
st.session_state["analyse_minerais"] = anciens
|
||||
|
||||
# Afficher le multiselect
|
||||
st.multiselect(
|
||||
str(_("pages.plan_d_action.filter_by_minerals")),
|
||||
minerais_nodes,
|
||||
key="analyse_minerais"
|
||||
)
|
||||
|
||||
minerais_selection = st.session_state["analyse_minerais"]
|
||||
|
||||
# Toujours purger, puis réécrire si nécessaire
|
||||
supprime_champ_statut("pages.plan_d_action.filter_by_minerals")
|
||||
if minerais_selection:
|
||||
for i, val in enumerate(minerais_selection):
|
||||
maj_champ_statut(f"pages.plan_d_action.filter_by_minerals.{i}", val)
|
||||
|
||||
return minerais_selection
|
||||
|
||||
def selectionner_noeuds(
|
||||
G: nx.Graph,
|
||||
niveaux_temp: Dict[Union[str, int], int],
|
||||
niveau_depart: int
|
||||
) -> Tuple[Optional[List[Union[str, int]]], List[Union[str, int]]]:
|
||||
"""Interface pour sélectionner les nœuds spécifiques de départ et d'arrivée.
|
||||
|
||||
Args:
|
||||
G (nx.Graph): Le graphe des relations entre les nœuds.
|
||||
niveaux_temp (dict): Dictionnaire contenant les niveaux des nœuds.
|
||||
niveau_depart (int): Niveau à partir duquel commencer la sélection.
|
||||
|
||||
Returns:
|
||||
tuple: La paire de départ et d'arrivée des nœuds sélectionnés.
|
||||
"""
|
||||
st.markdown("---")
|
||||
st.markdown(f"## {str(_('pages.plan_d_action.fine_selection'))}")
|
||||
|
||||
depart_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_depart]
|
||||
noeuds_arrivee = [n for n in G.nodes() if niveaux_temp.get(n) == 99]
|
||||
|
||||
if "analyse_noeuds_depart" not in st.session_state:
|
||||
anciens = []
|
||||
i = 0
|
||||
while True:
|
||||
val = get_champ_statut(f"pages.plan_d_action.filter_start_nodes.{i}")
|
||||
if not val:
|
||||
break
|
||||
anciens.append(val)
|
||||
i += 1
|
||||
st.session_state["analyse_noeuds_depart"] = anciens
|
||||
|
||||
st.multiselect(
|
||||
str(_("pages.plan_d_action.filter_start_nodes")),
|
||||
sorted(depart_nodes),
|
||||
key="analyse_noeuds_depart"
|
||||
)
|
||||
|
||||
noeuds_depart = st.session_state["analyse_noeuds_depart"]
|
||||
|
||||
supprime_champ_statut("pages.plan_d_action.filter_start_nodes")
|
||||
if noeuds_depart:
|
||||
for i, val in enumerate(noeuds_depart):
|
||||
maj_champ_statut(f"pages.plan_d_action.filter_start_nodes.{i}", val)
|
||||
|
||||
noeuds_depart = noeuds_depart if noeuds_depart else None
|
||||
|
||||
return noeuds_depart, noeuds_arrivee
|
||||
|
||||
def extraire_chemins_selon_criteres(
|
||||
G: nx.Graph,
|
||||
niveaux: Dict[str | int, int],
|
||||
niveau_depart: int,
|
||||
noeuds_depart: Optional[List[Union[str, int]]],
|
||||
noeuds_arrivee: List[Union[str, int]],
|
||||
minerais: Optional[List[Union[str, int]]]
|
||||
) -> List[List[str | int]]:
|
||||
"""Extrait les chemins selon les critères spécifiés.
|
||||
|
||||
Args:
|
||||
G (nx.Graph): Le graphe des relations entre les nœuds.
|
||||
niveaux (dict): Dictionnaire contenant les niveaux des nœuds.
|
||||
niveau_depart (int): Niveau à partir duquel commencer la sélection.
|
||||
noeuds_depart (list, optional): Les nœuds de départ qui doivent être considérés.
|
||||
noeuds_arrivee (list): Les nœuds d'arrivée qui doivent être inclus dans les chemins.
|
||||
minerais (list, optional): La liste des nœuds sélectionnés comme minerais.
|
||||
|
||||
Returns:
|
||||
list: Liste des chemins trouvés selon les critères spécifiés.
|
||||
"""
|
||||
chemins = []
|
||||
if noeuds_depart and noeuds_arrivee:
|
||||
for nd in noeuds_depart:
|
||||
for na in noeuds_arrivee:
|
||||
tous_chemins = extraire_chemins_depuis(G, nd)
|
||||
chemins.extend([chemin for chemin in tous_chemins if na in chemin])
|
||||
elif noeuds_depart:
|
||||
for nd in noeuds_depart:
|
||||
chemins.extend(extraire_chemins_depuis(G, nd))
|
||||
elif noeuds_arrivee:
|
||||
for na in noeuds_arrivee:
|
||||
chemins.extend(extraire_chemins_vers(G, na, niveau_depart))
|
||||
else:
|
||||
sources_depart = [n for n in G.nodes() if niveaux.get(n) == niveau_depart]
|
||||
for nd in sources_depart:
|
||||
chemins.extend(extraire_chemins_depuis(G, nd))
|
||||
|
||||
if minerais:
|
||||
chemins = [chemin for chemin in chemins if any(n in minerais for n in chemin)]
|
||||
|
||||
return chemins
|
||||
26
app/plan_d_action/utils/interface/visualization.py
Normal file
26
app/plan_d_action/utils/interface/visualization.py
Normal file
@ -0,0 +1,26 @@
|
||||
from typing import Dict, Optional
|
||||
import re
|
||||
from app.plan_d_action.utils.interface import CORRESPONDANCE_COULEURS
|
||||
|
||||
def remplacer_par_badge(
|
||||
markdown_text: str,
|
||||
correspondance: Optional[Dict[str, str]] = CORRESPONDANCE_COULEURS
|
||||
) -> str:
|
||||
"""Remplace certains mots par des badges colorés dans un texte Markdown.
|
||||
|
||||
Args:
|
||||
markdown_text (str): Le texte Markdown à modifier.
|
||||
correspondance (dict, optional): Dictionnaire qui mappe les mots
|
||||
à remplacer vers les couleurs de leurs badges. Si None, utilise
|
||||
CORRESPONDANCE_COULEURS par défaut.
|
||||
|
||||
Returns:
|
||||
str: Le texte Markdown modifié avec des badges ajoutés.
|
||||
"""
|
||||
# Échappe les mots à remplacer s'ils contiennent des accents ou espaces
|
||||
for mot, couleur in correspondance.items():
|
||||
# Utilise des bords de mots (\b) pour éviter les remplacements partiels
|
||||
pattern = r'\b' + re.escape(mot) + r'\b'
|
||||
remplacement = f":{couleur}-badge[{mot}]"
|
||||
markdown_text = re.sub(pattern, remplacement, markdown_text)
|
||||
return markdown_text
|
||||
@ -1,3 +1,5 @@
|
||||
from typing import List, Dict, Optional, Any
|
||||
import networkx as nx
|
||||
import streamlit as st
|
||||
import altair as alt
|
||||
import numpy as np
|
||||
@ -6,7 +8,17 @@ import pandas as pd
|
||||
from utils.translations import _
|
||||
|
||||
|
||||
def afficher_graphique_altair(df):
|
||||
def afficher_graphique_altair(df: pd.DataFrame) -> None:
|
||||
"""
|
||||
Affiche un graphique Altair pour les données d'IHH.
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): DataFrame contenant les données de IHH.
|
||||
|
||||
Notes:
|
||||
Cette fonction crée un graphique interactif pour visualiser les
|
||||
données d'IHH selon différentes catégories et niveaux de gravité.
|
||||
"""
|
||||
# Définir les catégories originales (en français) et leur ordre
|
||||
categories_fr = ["Assemblage", "Fabrication", "Traitement", "Extraction"]
|
||||
|
||||
@ -55,8 +67,8 @@ def afficher_graphique_altair(df):
|
||||
base = alt.Chart(df_cat).encode(
|
||||
x=alt.X('ihh_pays:Q', title=str(_("pages.visualisations.axis_titles.ihh_countries"))),
|
||||
y=alt.Y('ihh_acteurs:Q', title=str(_("pages.visualisations.axis_titles.ihh_actors"))),
|
||||
size=alt.Size('criticite_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None),
|
||||
color=alt.Color('criticite_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred']))
|
||||
size=alt.Size('ics_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None),
|
||||
color=alt.Color('ics_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred']))
|
||||
)
|
||||
|
||||
points = base.mark_circle(opacity=0.6)
|
||||
@ -89,7 +101,20 @@ def afficher_graphique_altair(df):
|
||||
st.altair_chart(chart, use_container_width=True)
|
||||
|
||||
|
||||
def creer_graphes(donnees):
|
||||
def creer_graphes(donnees: Optional[List[Dict[str, Any]]]) -> None:
|
||||
"""
|
||||
Crée un graphique Altair pour les données d'IVC.
|
||||
|
||||
Args:
|
||||
donnees (Optional[List[Dict[str, Any]]]): Liste des données d'IVC.
|
||||
|
||||
Returns:
|
||||
None.
|
||||
|
||||
Notes:
|
||||
Cette fonction traite les données d'IVC et crée un graphique
|
||||
interactif pour visualiser la concentration des ressources.
|
||||
"""
|
||||
if not donnees:
|
||||
st.warning(str(_("pages.visualisations.no_data")))
|
||||
return
|
||||
@ -162,7 +187,17 @@ def creer_graphes(donnees):
|
||||
st.error(f"{str(_('errors.graph_creation_error'))} {e}")
|
||||
|
||||
|
||||
def lancer_visualisation_ihh_criticite(graph):
|
||||
def lancer_visualisation_ihh_ics(graph: nx.DiGraph) -> None:
|
||||
"""
|
||||
Lance une visualisation Altair pour les données d'IHH critique.
|
||||
|
||||
Args:
|
||||
graph (nx.DiGraph): Le graphe NetworkX contenant les données de IHH.
|
||||
|
||||
Notes:
|
||||
Cette fonction traite le graphe et crée un graphique Altair
|
||||
pour visualiser les données d'IHH critique.
|
||||
"""
|
||||
try:
|
||||
import networkx as nx
|
||||
from utils.graph_utils import recuperer_donnees
|
||||
@ -180,7 +215,17 @@ def lancer_visualisation_ihh_criticite(graph):
|
||||
st.error(f"{str(_('errors.ihh_criticality_error'))} {e}")
|
||||
|
||||
|
||||
def lancer_visualisation_ihh_ivc(graph):
|
||||
def lancer_visualisation_ihh_ivc(graph: nx.DiGraph) -> None:
|
||||
"""
|
||||
Lance une visualisation Altair pour les données d'IVC.
|
||||
|
||||
Args:
|
||||
graph (Annx.Graphy): Le graphe NetworkX contenant les données de IV C.
|
||||
|
||||
Notes:
|
||||
Cette fonction traite le graphe et crée un graphique Altair
|
||||
pour visualiser les données d'IV C.
|
||||
"""
|
||||
try:
|
||||
from utils.graph_utils import recuperer_donnees_2
|
||||
noeuds_niveau_2 = [
|
||||
|
||||
@ -1,25 +1,42 @@
|
||||
import streamlit as st
|
||||
from utils.widgets import html_expander
|
||||
from utils.translations import _
|
||||
import networkx as nx
|
||||
|
||||
from .graphes import (
|
||||
lancer_visualisation_ihh_criticite,
|
||||
lancer_visualisation_ihh_ics,
|
||||
lancer_visualisation_ihh_ivc
|
||||
)
|
||||
|
||||
|
||||
def interface_visualisations(G_temp, G_temp_ivc):
|
||||
def interface_visualisations(G_temp: nx.DiGraph, G_temp_ivc: nx.DiGraph) -> None:
|
||||
"""
|
||||
Affiche l'interface utilisateur des visualisations.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G_temp : object
|
||||
Graphique temporel contenant les données de IHH.
|
||||
G_temp_ivc : object
|
||||
Graphique temporel contenant les données d'IVC.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Cette fonction initialise l'interface utilisateur qui permet aux utilisateurs de visualiser
|
||||
différentes données relatives à la gravité et au risque d'infections.
|
||||
Elle gère également le traitement des erreurs liées aux graphiques temporels IHH et IV C.
|
||||
"""
|
||||
st.markdown(f"# {str(_('pages.visualisations.title'))}")
|
||||
with st.expander(str(_("pages.visualisations.help")), expanded=False):
|
||||
st.markdown("\n".join(_("pages.visualisations.help_content")))
|
||||
html_expander(f"{str(_('pages.visualisations.help'))}", content="\n".join(_("pages.visualisations.help_content")), open_by_default=False, details_class="details_introduction")
|
||||
st.markdown("---")
|
||||
|
||||
st.markdown(f"""## {str(_("pages.visualisations.ihh_criticality"))}
|
||||
|
||||
{str(_("pages.visualisations.ihh_criticality_desc"))}
|
||||
""")
|
||||
if st.button(str(_("buttons.run")), key="btn_ihh_criticite"):
|
||||
if st.button(str(_("buttons.run")), key="btn_ihh_ics", icon=":material/bubble_chart:"):
|
||||
try:
|
||||
lancer_visualisation_ihh_criticite(G_temp)
|
||||
lancer_visualisation_ihh_ics(G_temp)
|
||||
except Exception as e:
|
||||
st.error(f"{str(_('errors.ihh_criticality_error'))} {e}")
|
||||
|
||||
@ -28,7 +45,7 @@ def interface_visualisations(G_temp, G_temp_ivc):
|
||||
{str(_("pages.visualisations.ihh_ivc_desc"))}
|
||||
""")
|
||||
|
||||
if st.button(str(_("buttons.run")), key="btn_ihh_ivc"):
|
||||
if st.button(str(_("buttons.run")), key="btn_ihh_ivc", icon=":material/bubble_chart:"):
|
||||
try:
|
||||
lancer_visualisation_ihh_ivc(G_temp_ivc)
|
||||
except Exception as e:
|
||||
|
||||
@ -2,9 +2,12 @@
|
||||
|
||||
## Styles
|
||||
|
||||
Le fichier **styles.css** a été construit pour agir sur le styme produit par Streamlit ou pour décorer des éléments construits par fabnum.py
|
||||
Le fichier **base.css** a été construit pour agir sur le style produit par Streamlit ou pour décorer des éléments construits par fabnum.py, indépendamment du thème choisi
|
||||
Les deux fichiers **theme-light.css** et **theme-dark.css** contiennent les variables utilisées par base.css pour afficher les couleurs.
|
||||
|
||||
Il sera important de regarder s'il est possible d'interagir avec le css de Streamlit sans passer par des déclarations !important
|
||||
Streamlit utilise le theme par défaut du système du poste de travail de l'internaute. Afin de maîtriser complètement le thème avec base.css, il est important que la configuration côté serveur de Streamlit soit forcée au thème light dans le fichier .streamlit/config.toml :
|
||||
[theme]
|
||||
base = "light"
|
||||
|
||||
## Icone
|
||||
|
||||
@ -37,4 +40,4 @@ La dernière colonne donne le label de l'item associé à la fiche.
|
||||
|
||||
Les labels associés aux fiches dans ce fichier doivent, bien évidemment, être **totalement égaux** aux labels de la gestion des tickets.
|
||||
|
||||
Ces labels avec en plus la branche permettent de faire le lien exact entre une fiche présentée dans l'application et le système de gestion des tickets.
|
||||
Ces labels avec en plus la branche permettent de faire le lien exact entre une fiche présentée dans l'application et le système de gestion des tickets.
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
version: 1.1
|
||||
date: 2025-05-06
|
||||
date: 2025-05-27
|
||||
|
||||
seuils:
|
||||
IVC: # Indice de vulnérabilité concurrentielle
|
||||
vert: { max: 5 }
|
||||
orange: { min: 5, max: 15 }
|
||||
rouge: { min: 15 }
|
||||
vert: { max: 15 }
|
||||
orange: { min: 15, max: 60 }
|
||||
rouge: { min: 60 }
|
||||
|
||||
IHH: # Index Herfindahl-Hirschman
|
||||
vert: { max: 15 }
|
||||
|
||||
@ -1,276 +0,0 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Fabnum – Chain Analysis",
|
||||
"description": "Ecosystem exploration and vulnerability identification.",
|
||||
"dev_mode": "You are in the development environment."
|
||||
},
|
||||
"header": {
|
||||
"title": "FabNum - Digital Manufacturing Chain",
|
||||
"subtitle": "Ecosystem exploration and vulnerability identification."
|
||||
},
|
||||
"footer": {
|
||||
"copyright": "Fabnum © 2025",
|
||||
"contact": "Contact",
|
||||
"license": "License",
|
||||
"license_text": "CC BY-NC-ND",
|
||||
"eco_note": "🌱 CO₂ calculations via",
|
||||
"eco_provider": "The Green Web Foundation",
|
||||
"powered_by": "🚀 Powered by",
|
||||
"powered_by_name": "Streamlit"
|
||||
},
|
||||
"sidebar": {
|
||||
"menu": "Main Menu",
|
||||
"navigation": "Main Navigation",
|
||||
"theme": "Theme",
|
||||
"theme_light": "Light",
|
||||
"theme_dark": "Dark",
|
||||
"theme_instructions_only": "Theme changes can only be made from the Instructions tab.",
|
||||
"impact": "Environmental Impact",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"auth": {
|
||||
"title": "Authentication",
|
||||
"username": "Username_token",
|
||||
"token": "Gitea Personal Access Token",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"logged_as": "Logged in as",
|
||||
"error": "❌ Access denied.",
|
||||
"gitea_error": "❌ Unable to verify user with Gitea.",
|
||||
"success": "Successfully logged out."
|
||||
},
|
||||
"navigation": {
|
||||
"instructions": "Instructions",
|
||||
"personnalisation": "Customization",
|
||||
"analyse": "Analysis",
|
||||
"visualisations": "Visualizations",
|
||||
"fiches": "Cards"
|
||||
},
|
||||
"pages": {
|
||||
"instructions": {
|
||||
"title": "Instructions"
|
||||
},
|
||||
"personnalisation": {
|
||||
"title": "Final Product Customization",
|
||||
"help": "How to use this tab?",
|
||||
"help_content": [
|
||||
"1. Click on \"Add a final product\" to create a new product",
|
||||
"2. Give your product a name",
|
||||
"3. Select an appropriate assembly operation (if relevant)",
|
||||
"4. Choose the components that make up your product from the list provided",
|
||||
"5. Save your configuration for future reuse",
|
||||
"6. You will be able to modify or delete your custom products later"
|
||||
],
|
||||
"add_new_product": "Add a new final product",
|
||||
"new_product_name": "New product name (unique)",
|
||||
"assembly_operation": "Assembly operation (optional)",
|
||||
"none": "-- None --",
|
||||
"components_to_link": "Components to link",
|
||||
"create_product": "Create product",
|
||||
"added": "added",
|
||||
"modify_product": "Modify an added final product",
|
||||
"products_to_modify": "Products to modify",
|
||||
"delete": "Delete",
|
||||
"linked_assembly_operation": "Linked assembly operation",
|
||||
"components_linked_to": "Components linked to",
|
||||
"update": "Update",
|
||||
"updated": "updated",
|
||||
"deleted": "deleted",
|
||||
"save_restore_config": "Save or restore configuration",
|
||||
"export_config": "Export configuration",
|
||||
"download_json": "Download (JSON)",
|
||||
"import_config": "Import a JSON configuration (max 100 KB)",
|
||||
"file_too_large": "File too large (max 100 KB).",
|
||||
"no_products_found": "No products found in the file.",
|
||||
"select_products_to_restore": "Select products to restore",
|
||||
"products_to_restore": "Products to restore",
|
||||
"restore_selected": "Restore selected items",
|
||||
"config_restored": "Partial configuration successfully restored.",
|
||||
"import_error": "Import error:"
|
||||
},
|
||||
"analyse": {
|
||||
"title": "Graph Analysis",
|
||||
"help": "How to use this tab?",
|
||||
"help_content": [
|
||||
"1. Select the starting level (final product, component, or mineral)",
|
||||
"2. Choose the desired destination level",
|
||||
"3. Refine your selection by specifying either one or more specific minerals to target or specific items at each level (optional)",
|
||||
"4. Define the analysis criteria by selecting the relevant vulnerability indices",
|
||||
"5. Choose the index combination mode (AND/OR) according to your analysis needs",
|
||||
"6. Explore the generated graph using zoom and panning controls; you can switch to full screen mode for the graph"
|
||||
],
|
||||
"selection_nodes": "Selection of start and end nodes",
|
||||
"select_level": "-- Select a level --",
|
||||
"start_level": "Start level",
|
||||
"end_level": "End level",
|
||||
"select_minerals": "Select one or more minerals",
|
||||
"filter_by_minerals": "Filter by minerals (optional)",
|
||||
"fine_selection": "Fine selection of items",
|
||||
"filter_start_nodes": "Filter by start nodes (optional)",
|
||||
"filter_end_nodes": "Filter by end nodes (optional)",
|
||||
"vulnerability_filters": "Selection of filters to identify vulnerabilities",
|
||||
"filter_ics": "Filter paths containing at least one critical mineral for a component (ICS > 66%)",
|
||||
"filter_ivc": "Filter paths containing at least one critical mineral in relation to sectoral competition (IVC > 30)",
|
||||
"filter_ihh": "Filter paths containing at least one critical operation in relation to geographical or industrial concentration (IHH countries or actors > 25)",
|
||||
"apply_ihh_filter": "Apply IHH filter on:",
|
||||
"countries": "Countries",
|
||||
"actors": "Actors",
|
||||
"filter_isg": "Filter paths containing an unstable country (ISG ≥ 60)",
|
||||
"filter_logic": "Filter logic",
|
||||
"or": "OR",
|
||||
"and": "AND",
|
||||
"run_analysis": "Run analysis",
|
||||
"sankey": {
|
||||
"no_paths": "No paths found for the specified criteria.",
|
||||
"no_matching_paths": "No paths match the criteria.",
|
||||
"filtered_hierarchy": "Hierarchy filtered by levels and nodes",
|
||||
"download_dot": "Download filtered DOT file",
|
||||
"relation": "Relation"
|
||||
}
|
||||
},
|
||||
"visualisations": {
|
||||
"title": "Visualizations",
|
||||
"help": "How to use this tab?",
|
||||
"help_content": [
|
||||
"1. Explore the graphs presenting the Herfindahl-Hirschmann Index (IHH)",
|
||||
"2. Analyze its relationship with the average criticality of minerals or their Competitive Vulnerability Index (IVC)",
|
||||
"3. Zoom in on the graphs to better discover the information",
|
||||
"",
|
||||
"It is important to remember that the IHH has two thresholds:",
|
||||
"* below 15, concentration is considered to be low",
|
||||
"* above 25, it is considered to be high",
|
||||
"",
|
||||
"Thus, the higher a point is positioned in the top right of the graphs, the higher the risks.",
|
||||
"The graphs present 2 horizontal and vertical lines to mark these thresholds."
|
||||
],
|
||||
"ihh_criticality": "Herfindahl-Hirschmann Index - IHH vs Criticality",
|
||||
"ihh_criticality_desc": "The size of the points indicates the substitutability criticality of the mineral.",
|
||||
"ihh_ivc": "Herfindahl-Hirschmann Index - IHH vs IVC",
|
||||
"ihh_ivc_desc": "The size of the points indicates the competitive criticality of the mineral.",
|
||||
"launch": "Launch",
|
||||
"no_data": "No data to display.",
|
||||
"categories": {
|
||||
"assembly": "Assembly",
|
||||
"manufacturing": "Manufacturing",
|
||||
"processing": "Processing",
|
||||
"extraction": "Extraction"
|
||||
},
|
||||
"axis_titles": {
|
||||
"ihh_countries": "IHH Countries (%)",
|
||||
"ihh_actors": "IHH Actors (%)",
|
||||
"ihh_extraction": "IHH Extraction (%)",
|
||||
"ihh_reserves": "IHH Reserves (%)"
|
||||
},
|
||||
"chart_titles": {
|
||||
"concentration_criticality": "Concentration and Criticality – {0}",
|
||||
"concentration_resources": "Concentration of Critical Resources vs IVC Vulnerability"
|
||||
}
|
||||
},
|
||||
"fiches": {
|
||||
"title": "Card Discovery",
|
||||
"help": "How to use this tab?",
|
||||
"help_content": [
|
||||
"1. Browse the list of available cards by category",
|
||||
"2. Select a card to display its full content",
|
||||
"3. Consult detailed data, graphs, and additional analyses",
|
||||
"4. Use this information to deepen your understanding of the identified vulnerabilities",
|
||||
"",
|
||||
"The categories are as follows:",
|
||||
"* Assembly: operation of assembling final products from components",
|
||||
"* Related: various operations necessary to manufacture digital technology, but not directly entering its composition",
|
||||
"* Criticalities: indices used to identify and evaluate vulnerabilities",
|
||||
"* Manufacturing: operation of manufacturing components from minerals",
|
||||
"* Mineral: description and operations of extraction and processing of minerals"
|
||||
],
|
||||
"no_files": "No cards available at the moment.",
|
||||
"choose_category": "Choose a card category",
|
||||
"select_folder": "-- Select a folder --",
|
||||
"choose_file": "Choose a card",
|
||||
"select_file": "-- Select a card --",
|
||||
"loading_error": "Error loading the card:",
|
||||
"download_pdf": "Download this card as PDF",
|
||||
"pdf_unavailable": "The PDF file for this card is not available.",
|
||||
"ticket_management": "Ticket management for this card",
|
||||
"tickets": {
|
||||
"create_new": "Create a new ticket linked to this card",
|
||||
"model_load_error": "Unable to load the ticket template.",
|
||||
"contribution_type": "Contribution type",
|
||||
"specify": "Specify",
|
||||
"other": "Other",
|
||||
"concerned_card": "Concerned card",
|
||||
"subject": "Subject of the proposal",
|
||||
"preview": "Preview ticket",
|
||||
"cancel": "Cancel",
|
||||
"preview_title": "Ticket preview",
|
||||
"summary": "Summary",
|
||||
"title": "Title",
|
||||
"labels": "Labels",
|
||||
"confirm": "Confirm ticket creation",
|
||||
"created": "Ticket created and form cleared.",
|
||||
"model_error": "Template loading error:",
|
||||
"no_linked_tickets": "No tickets linked to this card.",
|
||||
"associated_tickets": "Tickets associated with this card",
|
||||
"moderation_notice": "ticket(s) awaiting moderation are not displayed.",
|
||||
"status": {
|
||||
"awaiting": "Awaiting processing",
|
||||
"in_progress": "In progress",
|
||||
"completed": "Completed",
|
||||
"rejected": "Rejected",
|
||||
"others": "Others"
|
||||
},
|
||||
"no_title": "No title",
|
||||
"unknown": "unknown",
|
||||
"subject_label": "Subject",
|
||||
"no_labels": "none",
|
||||
"comments": "Comment(s):",
|
||||
"no_comments": "No comments.",
|
||||
"comment_error": "Error retrieving comments:",
|
||||
"opened_by": "Opened by",
|
||||
"on_date": "on",
|
||||
"updated": "UPDATED"
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_levels": {
|
||||
"0": "Final product",
|
||||
"1": "Component",
|
||||
"2": "Mineral",
|
||||
"10": "Operation",
|
||||
"11": "Operation country",
|
||||
"12": "Operation actor",
|
||||
"99": "Geographic country"
|
||||
},
|
||||
"errors": {
|
||||
"log_read_error": "Log reading error:",
|
||||
"graph_preview_error": "Graph preview error:",
|
||||
"graph_creation_error": "Error creating the graph:",
|
||||
"ihh_criticality_error": "Error in IHH vs Criticality visualization:",
|
||||
"ihh_ivc_error": "Error in IHH vs IVC visualization:",
|
||||
"comment_fetch_error": "Error retrieving comments:",
|
||||
"template_load_error": "Template loading error:",
|
||||
"import_error": "Import error:"
|
||||
},
|
||||
"buttons": {
|
||||
"download": "Download",
|
||||
"run": "Run",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"filter": "Filter",
|
||||
"search": "Search",
|
||||
"create": "Create",
|
||||
"update": "Update",
|
||||
"delete": "Delete",
|
||||
"preview": "Preview",
|
||||
"export": "Export",
|
||||
"import": "Import",
|
||||
"restore": "Restore",
|
||||
"browse_files": "Browse files"
|
||||
},
|
||||
"ui": {
|
||||
"file_uploader": {
|
||||
"drag_drop_here": "Drag and drop file here",
|
||||
"size_limit": "100 KB limit per file • JSON"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,10 +6,10 @@
|
||||
},
|
||||
"header": {
|
||||
"title": "FabNum - Digital Manufacturing Chain",
|
||||
"subtitle": "Ecosystem exploration and vulnerability identification."
|
||||
"subtitle": "Ecosystem exploration and vulnerability identification.\n -> This site is still under development <-"
|
||||
},
|
||||
"footer": {
|
||||
"copyright": "Fabnum © 2025",
|
||||
"copyright": "FabNum@Polycrisis Observatory © 2025",
|
||||
"contact": "Contact",
|
||||
"license": "License",
|
||||
"license_text": "CC BY-NC-ND",
|
||||
@ -43,6 +43,8 @@
|
||||
"instructions": "Instructions",
|
||||
"personnalisation": "Customization",
|
||||
"analyse": "Analysis",
|
||||
"ia_nalyse": "AI'nalysis",
|
||||
"plan_d_action": "Actions plan",
|
||||
"visualisations": "Visualizations",
|
||||
"fiches": "Cards"
|
||||
},
|
||||
@ -64,6 +66,7 @@
|
||||
"add_new_product": "Add a new final product",
|
||||
"new_product_name": "New product name (unique)",
|
||||
"assembly_operation": "Assembly operation (optional)",
|
||||
"product_exists": "This product already exists",
|
||||
"none": "-- None --",
|
||||
"components_to_link": "Components to link",
|
||||
"create_product": "Create product",
|
||||
@ -128,6 +131,45 @@
|
||||
"relation": "Relation"
|
||||
}
|
||||
},
|
||||
"ia_nalyse": {
|
||||
"title": "Graph Analysis by AI",
|
||||
"help": "How to use this tab?",
|
||||
"help_content": [
|
||||
"The graph covers all levels, from end products to geographic countries.\n",
|
||||
"1. You can select minerals through which the paths go.",
|
||||
"2. You can choose end products to perform an analysis tailored to your context.\n",
|
||||
"These two selections are optional, but strongly recommended for a more relevant analysis.",
|
||||
"The analysis is carried out using a private AI on a minimalist server. The result is therefore not immediate (approximately 30 minutes) and you will be notified of the progress."
|
||||
],
|
||||
"select_minerals": "Select one or more minerals",
|
||||
"filter_by_minerals": "Filter by minerals (optional, but highly recommended)",
|
||||
"fine_selection": "End product selection",
|
||||
"filter_start_nodes": "Filter by start nodes (optional, but recommended)",
|
||||
"run_analysis": "Run analysis",
|
||||
"confirm_download": "Confirm download",
|
||||
"submit_request": "Submit your request",
|
||||
"empty_graph": "The graph is empty. Please make another selection."
|
||||
},
|
||||
"plan_d_action": {
|
||||
"title": "Graph analysis for action",
|
||||
"help": "How to use this tab?",
|
||||
"help_content": [
|
||||
"The graph covers all levels, from end products to geographic countries.\n",
|
||||
"1. You can select minerals through which the paths go.",
|
||||
"2. You can choose end products to perform an analysis tailored to your context.\n",
|
||||
"These two selections are optional, but strongly recommended for a more relevant analysis.",
|
||||
"The recommendations for actions and indicator monitoring are generic. They must therefore be adapted to the context.",
|
||||
"The proposed actions or indicators depend on the type of organization concerned and can be applied directly or required of digital providers."
|
||||
],
|
||||
"select_minerals": "Select one or more minerals",
|
||||
"filter_by_minerals": "Filter by minerals (optional, but highly recommended)",
|
||||
"fine_selection": "End product selection",
|
||||
"filter_start_nodes": "Filter by start nodes",
|
||||
"run_analysis": "Run analysis",
|
||||
"confirm_download": "Confirm download",
|
||||
"submit_request": "Submit your request",
|
||||
"empty_graph": "The graph is empty. Please make another selection."
|
||||
},
|
||||
"visualisations": {
|
||||
"title": "Visualizations",
|
||||
"help": "How to use this tab?",
|
||||
@ -227,7 +269,11 @@
|
||||
"comment_error": "Error retrieving comments:",
|
||||
"opened_by": "Opened by",
|
||||
"on_date": "on",
|
||||
"updated": "UPDATED"
|
||||
"updated": "UPDATED",
|
||||
"continue": "Continuer",
|
||||
"created_success": "Ticket created and placed in moderation",
|
||||
"created_error": "Ticket creation failed. Please try later",
|
||||
"see_ticket": "See ticket"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -265,6 +311,7 @@
|
||||
"export": "Export",
|
||||
"import": "Import",
|
||||
"restore": "Restore",
|
||||
"refresh": "Refresh",
|
||||
"browse_files": "Browse files"
|
||||
},
|
||||
"ui": {
|
||||
@ -272,5 +319,14 @@
|
||||
"drag_drop_here": "Drag and drop file here",
|
||||
"size_limit": "100 KB limit per file • JSON"
|
||||
}
|
||||
},
|
||||
"batch": {
|
||||
"in_queue": "In queue",
|
||||
"in_progress": "Analysis in progress",
|
||||
"failure": "Error",
|
||||
"unknwon_error": "unknown error",
|
||||
"no_task": "No task wainting or in progress",
|
||||
"complete": "Analysis complete. Download the result in zip format, which contains the detailed report and analysis.",
|
||||
"step": "Step"
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,10 +6,10 @@
|
||||
},
|
||||
"header": {
|
||||
"title": "FabNum - Chaîne de fabrication du numérique",
|
||||
"subtitle": "Parcours de l'écosystème et identification des vulnérabilités."
|
||||
"subtitle": "Parcours de l'écosystème et identification des vulnérabilités.\n-> Ce site est encore en cours de développement <-"
|
||||
},
|
||||
"footer": {
|
||||
"copyright": "Fabnum © 2025",
|
||||
"copyright": "FabNum@Polycrisis Observatory © 2025",
|
||||
"contact": "Contact",
|
||||
"license": "Licence",
|
||||
"license_text": "CC BY-NC-ND",
|
||||
@ -43,6 +43,8 @@
|
||||
"instructions": "Instructions",
|
||||
"personnalisation": "Personnalisation",
|
||||
"analyse": "Analyse",
|
||||
"ia_nalyse": "IA'nalyse",
|
||||
"plan_d_action": "Plan d'action",
|
||||
"visualisations": "Visualisations",
|
||||
"fiches": "Fiches"
|
||||
},
|
||||
@ -64,6 +66,7 @@
|
||||
"add_new_product": "Ajouter un nouveau produit final",
|
||||
"new_product_name": "Nom du nouveau produit (unique)",
|
||||
"assembly_operation": "Opération d'assemblage (optionnelle)",
|
||||
"product_exists": "Ce produit existe déjà.",
|
||||
"none": "-- Aucune --",
|
||||
"components_to_link": "Composants à lier",
|
||||
"create_product": "Créer le produit",
|
||||
@ -128,6 +131,45 @@
|
||||
"relation": "Relation"
|
||||
}
|
||||
},
|
||||
"ia_nalyse": {
|
||||
"title": "Analyse du graphe par IA",
|
||||
"help": "Comment utiliser cet onglet ?",
|
||||
"help_content": [
|
||||
"Le graphe intègre l'ensemble des niveaux, des produits finaux aux pays géographiques.\n",
|
||||
"1. Vous pouvez sélectionner des minerais par lesquels passent les chemins.",
|
||||
"2. Vous pouvez choisir des produits finaux pour faire une analyse adaptée à votre contexte.\n",
|
||||
" Ces deux sélections sont optionnelles, mais fortement recommandées pour avoir une meilleure pertinence de l'analyse.",
|
||||
"L'analyse se réalise à l'aide d'une IA privée, sur un serveur minimaliste. Le résultat n'est donc pas immédiat (ordre de grandeur : 30 minutes) et vous serez informé de l'avancement."
|
||||
],
|
||||
"select_minerals": "Sélectionner un ou plusieurs minerais",
|
||||
"filter_by_minerals": "Filtrer par minerais (optionnel, mais recommandé)",
|
||||
"fine_selection": "Sélection des produits finaux",
|
||||
"filter_start_nodes": "Filtrer par noeuds de départ (optionnel, mais recommandé)",
|
||||
"run_analysis": "Lancer l'analyse",
|
||||
"confirm_download": "Confirmer le téléchargement",
|
||||
"submit_request": "Soumettre votre demande",
|
||||
"empty_graph": "Le graphe est vide. Veuillez faire une autre sélection."
|
||||
},
|
||||
"plan_d_action": {
|
||||
"title": "Analyse du graphe pour action",
|
||||
"help": "Comment utiliser cet onglet ?",
|
||||
"help_content": [
|
||||
"Le graphe intègre l'ensemble des niveaux, des produits finaux aux pays géographiques.\n",
|
||||
"1. Vous pouvez sélectionner des minerais par lesquels passent les chemins.",
|
||||
"2. Vous pouvez choisir des produits finaux pour faire une analyse adaptée à votre contexte.\n",
|
||||
" Ces deux sélections sont optionnelles, mais fortement recommandées pour avoir une meilleure pertinence de l'analyse.",
|
||||
"Les préconisations d'actions et de suivi d'indicateurs sont génériques. Elles doivent donc être adaptées au contexte.",
|
||||
"Les actions ou les indicateurs proposés dépendent du type d'organisation concernée et peuvent être appliquées directement ou exigées des fournisseurs de numérique."
|
||||
],
|
||||
"select_minerals": "Sélectionner un ou plusieurs minerais",
|
||||
"filter_by_minerals": "Filtrer par minerais (optionnel, mais recommandé)",
|
||||
"fine_selection": "Sélection des produits finaux",
|
||||
"filter_start_nodes": "Filtrer par noeuds de départ",
|
||||
"run_analysis": "Lancer l'analyse",
|
||||
"confirm_download": "Confirmer le téléchargement",
|
||||
"submit_request": "Soumettre votre demande",
|
||||
"empty_graph": "Le graphe est vide. Veuillez faire une autre sélection."
|
||||
},
|
||||
"visualisations": {
|
||||
"title": "Visualisations",
|
||||
"help": "Comment utiliser cet onglet ?",
|
||||
@ -227,7 +269,11 @@
|
||||
"comment_error": "Erreur lors de la récupération des commentaires :",
|
||||
"opened_by": "Ouvert par",
|
||||
"on_date": "le",
|
||||
"updated": "MAJ"
|
||||
"updated": "MAJ",
|
||||
"continue": "Continuer",
|
||||
"created_success": "Ticket créé et placé en modération",
|
||||
"created_error": "Échec de la création du ticket. Veuillez réessayer plus tard",
|
||||
"see_ticket": "Voir le ticket"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -265,6 +311,7 @@
|
||||
"export": "Exporter",
|
||||
"import": "Importer",
|
||||
"restore": "Restaurer",
|
||||
"refresh": "Rafraîchir",
|
||||
"browse_files": "Parcourir les fichiers"
|
||||
},
|
||||
"ui": {
|
||||
@ -272,5 +319,14 @@
|
||||
"drag_drop_here": "Glissez-déposez votre fichier ici",
|
||||
"size_limit": "Limite 100 Ko par fichier • JSON"
|
||||
}
|
||||
},
|
||||
"batch": {
|
||||
"in_queue": "En attente",
|
||||
"in_progress": "Analyse en cours",
|
||||
"failure": "Échec",
|
||||
"unknwon_error": "erreur inconnue",
|
||||
"no_task": "Aucune tâche en attente ou en cours",
|
||||
"complete": "Analyse terminée. Télécharger le résultat au format zip, qui contient le rapport détaillé et l'analyse.",
|
||||
"step": "Étape"
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,33 +4,33 @@
|
||||
1. Reset et base
|
||||
========================================== */
|
||||
.stAppHeader {
|
||||
visibility: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
|
||||
sans-serif;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
body,
|
||||
.stApp,
|
||||
.block-container {
|
||||
background-color: var(--bg-color) !important;
|
||||
color: var(--text-color) !important;
|
||||
background-color: var(--bg-color) !important;
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
2. Layout et containers
|
||||
========================================== */
|
||||
.block-container {
|
||||
max-width: 1024px !important;
|
||||
padding: 0 1rem 10rem;
|
||||
max-width: 1024px !important;
|
||||
padding: 0 1rem 10rem;
|
||||
}
|
||||
|
||||
.stVerticalBlock {
|
||||
gap: 0.5rem !important;
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
@ -42,97 +42,97 @@ body,
|
||||
.stDownloadButton > button,
|
||||
.stFormSubmitButton > button,
|
||||
.stSlider > div > div {
|
||||
background-color: darkgreen !important;
|
||||
color: white !important;
|
||||
border: 1px solid grey;
|
||||
background-color: #072c6e !important;
|
||||
color: white !important;
|
||||
border: 1px solid grey;
|
||||
}
|
||||
|
||||
.st-key-FormSubmitter-auth_form-Se-connecter {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
section:not([data-testid="stSidebar"])
|
||||
button[data-testid="stBaseButton-primary"],
|
||||
button[data-testid="stBaseButton-primary"],
|
||||
section:not([data-testid="stSidebar"])
|
||||
button[data-testid="stBaseButton-secondary"] {
|
||||
color: white !important;
|
||||
background: darkgreen !important;
|
||||
button[data-testid="stBaseButton-secondary"] {
|
||||
color: white !important;
|
||||
background: #072c6e !important;
|
||||
}
|
||||
|
||||
section:not([data-testid="stSidebar"])
|
||||
button[data-testid="stBaseButton-primary"]
|
||||
p,
|
||||
button[data-testid="stBaseButton-primary"]
|
||||
p,
|
||||
section:not([data-testid="stSidebar"])
|
||||
button[data-testid="stBaseButton-secondary"]
|
||||
p {
|
||||
color: white !important;
|
||||
button[data-testid="stBaseButton-secondary"]
|
||||
p {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.bouton-fictif {
|
||||
display: inline-flex;
|
||||
-moz-box-align: center;
|
||||
align-items: center;
|
||||
-moz-box-pack: center;
|
||||
justify-content: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
min-height: 2.5rem;
|
||||
margin-bottom: 20px;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
font-size: x-large;
|
||||
font-family: inherit;
|
||||
user-select: none;
|
||||
border: 1px solid rgba(49, 51, 63, 0.2);
|
||||
background-color: darkgrey !important;
|
||||
color: darkgreen !important;
|
||||
font-weight: bold !important;
|
||||
width: 100%;
|
||||
letter-spacing: 0.2em;
|
||||
display: inline-flex;
|
||||
-moz-box-align: center;
|
||||
align-items: center;
|
||||
-moz-box-pack: center;
|
||||
justify-content: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
min-height: 2.5rem;
|
||||
margin-bottom: 20px;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
font-size: x-large;
|
||||
font-family: inherit;
|
||||
user-select: none;
|
||||
border: 1px solid rgba(49, 51, 63, 0.2);
|
||||
background-color: darkgrey !important;
|
||||
color: #072c6e !important;
|
||||
font-weight: bold !important;
|
||||
width: 100%;
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
|
||||
button[data-testid="stBaseButton-headerNoPadding"] svg {
|
||||
fill: var(--text-color) !important;
|
||||
fill: var(--text-color) !important;
|
||||
}
|
||||
|
||||
/* --- 3.2 Onglets et radiogroup --- */
|
||||
div[role="radiogroup"] > label {
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 0.4em;
|
||||
margin-right: 0.5em;
|
||||
cursor: pointer;
|
||||
border: 1px solid #fff;
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 0.4em;
|
||||
margin-right: 0.5em;
|
||||
cursor: pointer;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
div[role="radiogroup"] > label[data-selected="true"] {
|
||||
font-weight: bold;
|
||||
border: 2px solid #145a1a;
|
||||
font-weight: bold;
|
||||
border: 2px solid #145a1a;
|
||||
}
|
||||
|
||||
section:not([data-testid="stSidebar"]) div[role="radiogroup"] > label p {
|
||||
background-color: var(--radio-bg) !important;
|
||||
color: var(--radio-text) !important;
|
||||
background-color: var(--radio-bg) !important;
|
||||
color: var(--radio-text) !important;
|
||||
}
|
||||
|
||||
section:not([data-testid="stSidebar"])
|
||||
div[role="radiogroup"]
|
||||
> label[data-selected="true"] {
|
||||
background-color: var(--radio-selected-bg) !important;
|
||||
color: var(--radio-selected-text) !important;
|
||||
div[role="radiogroup"]
|
||||
> label[data-selected="true"] {
|
||||
background-color: var(--radio-selected-bg) !important;
|
||||
color: var(--radio-selected-text) !important;
|
||||
}
|
||||
|
||||
/* --- 3.3 Champs de formulaire --- */
|
||||
div[data-baseweb="select"],
|
||||
section:not([data-testid="stSidebar"]) div[data-baseweb="base-input"],
|
||||
section[data-testid="stFileUploaderDropzone"] {
|
||||
border: 1px solid var(--input-border) !important;
|
||||
border-radius: 5px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--input-border) !important;
|
||||
border-radius: 5px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
small {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
section:not([data-testid="stSidebar"]) div[data-testid="stSelectbox"] p,
|
||||
@ -142,7 +142,7 @@ section:not([data-testid="stSidebar"]) div[data-testid="stCheckbox"] p,
|
||||
section:not([data-testid="stSidebar"]) div[data-testid="stTextInput"] p,
|
||||
section:not([data-testid="stSidebar"]) div[data-testid="stTextArea"] p,
|
||||
section:not([data-testid="stSidebar"]) div[data-testid="stAlertContentInfo"] p {
|
||||
color: var(--text-color) !important;
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
@ -151,98 +151,98 @@ section:not([data-testid="stSidebar"]) div[data-testid="stAlertContentInfo"] p {
|
||||
|
||||
/* --- 4.1 Header --- */
|
||||
.wide-header {
|
||||
width: 100vw;
|
||||
margin-left: calc(-50vw + 50%);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid #ddd;
|
||||
text-align: center;
|
||||
padding-top: 1rem;
|
||||
margin-top: -1.25em;
|
||||
background-color: var(--header-bg);
|
||||
width: 100vw;
|
||||
margin-left: calc(-50vw + 50%);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid #ddd;
|
||||
text-align: center;
|
||||
padding-top: 1rem;
|
||||
margin-top: -1.25em;
|
||||
background-color: var(--header-bg);
|
||||
}
|
||||
|
||||
.titre-header {
|
||||
font-size: 2rem !important;
|
||||
font-weight: bolder !important;
|
||||
color: var(--header-title);
|
||||
font-size: 2rem !important;
|
||||
font-weight: bolder !important;
|
||||
color: var(--header-title);
|
||||
}
|
||||
|
||||
/* --- 4.2 Footer --- */
|
||||
.wide-footer {
|
||||
width: 100vw;
|
||||
margin-left: calc(-50vw + 50%);
|
||||
margin-top: 3rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-top: 1px solid #ddd;
|
||||
text-align: center;
|
||||
padding-top: 1rem;
|
||||
background-color: var(--footer-bg);
|
||||
width: 100vw;
|
||||
margin-left: calc(-50vw + 50%);
|
||||
margin-top: 3rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-top: 1px solid #ddd;
|
||||
text-align: center;
|
||||
padding-top: 1rem;
|
||||
background-color: var(--footer-bg);
|
||||
}
|
||||
|
||||
.info-footer {
|
||||
font-size: 1rem !important;
|
||||
font-weight: 800;
|
||||
color: var(--footer-text);
|
||||
font-size: 1rem !important;
|
||||
font-weight: 800;
|
||||
color: var(--footer-text);
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
5. Sidebar
|
||||
========================================== */
|
||||
section[data-testid="stSidebar"] {
|
||||
background-color: #ccc !important;
|
||||
color: #111 !important;
|
||||
background-color: #ccc !important;
|
||||
color: #111 !important;
|
||||
}
|
||||
|
||||
section[data-testid="stSidebar"] .stButton > button {
|
||||
background-color: darkgreen !important;
|
||||
color: white !important;
|
||||
font-weight: bold !important;
|
||||
border: 1px solid #ccc !important;
|
||||
width: 100%;
|
||||
background-color: #072c6e !important;
|
||||
color: white !important;
|
||||
font-weight: bold !important;
|
||||
border: 1px solid #ccc !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
section[data-testid="stSidebar"] .decorative-heading {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
color: #145a1a;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
color: #072c6e;
|
||||
}
|
||||
|
||||
section[data-testid="stSidebar"] div[role="radiogroup"] {
|
||||
justify-content: center !important;
|
||||
display: flex !important;
|
||||
gap: 1rem;
|
||||
justify-content: center !important;
|
||||
display: flex !important;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
6. Tables
|
||||
========================================== */
|
||||
table {
|
||||
border: 1px solid var(--table-border) !important;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 1.5em;
|
||||
border: 1px solid var(--table-border) !important;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid var(--table-border) !important;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border: 1px solid var(--table-border) !important;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
caption {
|
||||
caption-side: top;
|
||||
font-weight: bold;
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
caption-side: bottom;
|
||||
text-align: center;
|
||||
caption-side: top;
|
||||
font-weight: bold;
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
caption-side: bottom;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table[role="table"] th[scope="col"] {
|
||||
background-color: var(--background-color);
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
@ -254,94 +254,99 @@ table[role="table"] th[scope="col"] {
|
||||
|
||||
/* --- 7.2 Graphiques --- */
|
||||
.stPlotlyChart text {
|
||||
fill: var(--plot-text) !important;
|
||||
fill: var(--plot-text) !important;
|
||||
}
|
||||
|
||||
.stPlotlyChart text {
|
||||
fill: black !important;
|
||||
text-shadow: none !important;
|
||||
font-weight: bold !important;
|
||||
font-size: 14px !important;
|
||||
font-family: Verdana, sans-serif !important;
|
||||
fill: black !important;
|
||||
text-shadow: none !important;
|
||||
font-weight: bold !important;
|
||||
font-size: 14px !important;
|
||||
font-family: Verdana, sans-serif !important;
|
||||
}
|
||||
|
||||
/* Cache complètement la section d'actions Vega */
|
||||
.vega-actions {
|
||||
display: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Et aussi le <details> parent, s'il faut tout masquer */
|
||||
details[title="Click to view actions"] {
|
||||
display: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* --- 7.3 Détails et paragraphes --- */
|
||||
details {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
padding: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
background-color: var(--background-color);
|
||||
border-color: var(--details-border) !important;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
padding: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
background-color: var(--background-color);
|
||||
border-color: var(--details-border) !important;
|
||||
}
|
||||
|
||||
section:not([data-testid="stSidebar"])
|
||||
div:not[data-testid="stElementContainer"]
|
||||
p:not(#Authentification):not(#Theme) {
|
||||
color: var(--paragraph-color) !important;
|
||||
div:not[data-testid="stElementContainer"]
|
||||
p:not(#Authentification):not(#Theme) {
|
||||
color: var(--paragraph-color) !important;
|
||||
}
|
||||
|
||||
section:not([data-testid="stSidebar"]) hr {
|
||||
background-color: var(--hr-color) !important;
|
||||
background-color: var(--hr-color) !important;
|
||||
}
|
||||
|
||||
/* --- 7.4 Conteneurs de commentaires et tickets --- */
|
||||
.conteneur_commentaire,
|
||||
.conteneur_ticket {
|
||||
background: var(--background-color);
|
||||
padding: 1em;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1em;
|
||||
border: 1px solid #ccc;
|
||||
background: var(--background-color);
|
||||
padding: 1em;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1em;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.commentaire_auteur,
|
||||
.ticket_auteur {
|
||||
color: var(--text-color) !important;
|
||||
margin: 0;
|
||||
color: var(--text-color) !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.commentaire_contenu,
|
||||
.ticket_contenu {
|
||||
color: var(--text-color) !important;
|
||||
margin: 0.5rem 0 0;
|
||||
color: var(--text-color) !important;
|
||||
margin: 0.5rem 0 0;
|
||||
}
|
||||
|
||||
/* --- 7.5 Blocs mathématiques --- */
|
||||
.math-block {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: 1em 0;
|
||||
border: 1px solid var(--math-block-border);
|
||||
border-radius: 10px;
|
||||
background: var(--math-block-bg);
|
||||
font-size: x-large;
|
||||
color: var(--text-color);
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: 1em 0;
|
||||
border: 1px solid var(--math-block-border);
|
||||
border-radius: 10px;
|
||||
background: var(--math-block-bg);
|
||||
font-size: x-large;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.math-block math {
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
8. Éléments spécifiques
|
||||
========================================== */
|
||||
div.stElementContainer.element-container.st-key-nom_utilisateur {
|
||||
display: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.st-key-telecharger_fiche_pdf {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 1rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.details_introduction {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
29
batch_ia/__init__.py
Normal file
29
batch_ia/__init__.py
Normal file
@ -0,0 +1,29 @@
|
||||
# batch_ia/__init__.py
|
||||
|
||||
# config.py
|
||||
from .utils.config import TEMPLATE_PATH, load_config, session_uuid, TEMP_SECTIONS
|
||||
|
||||
# files.py
|
||||
from .utils.files import write_report
|
||||
|
||||
# graphs.py
|
||||
from .utils.graphs import (
|
||||
parse_graphs,
|
||||
extract_data_from_graph,
|
||||
calculate_vulnerabilities
|
||||
)
|
||||
|
||||
# sections.py
|
||||
from .utils.sections import generate_report
|
||||
|
||||
__all__ = [
|
||||
"TEMPLATE_PATH",
|
||||
"session_uuid",
|
||||
"TEMP_SECTIONS",
|
||||
"load_config",
|
||||
"write_report",
|
||||
"parse_graphs",
|
||||
"extract_data_from_graph",
|
||||
"calculate_vulnerabilities",
|
||||
"generate_report",
|
||||
]
|
||||
73
batch_ia/analyse_ia.py
Normal file
73
batch_ia/analyse_ia.py
Normal file
@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script pour générer un rapport factorisé des vulnérabilités critiques
|
||||
suivant la structure définie dans Remarques.md.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import streamlit as st
|
||||
|
||||
from utils.config import (
|
||||
TEMP_SECTIONS,
|
||||
TEMPLATE_PATH, session_uuid,
|
||||
load_config
|
||||
)
|
||||
|
||||
from utils.files import (
|
||||
write_report
|
||||
)
|
||||
|
||||
from utils.graphs import (
|
||||
parse_graphs,
|
||||
extract_data_from_graph,
|
||||
calculate_vulnerabilities
|
||||
)
|
||||
|
||||
from utils.sections import (
|
||||
generate_report
|
||||
)
|
||||
|
||||
from utils.sections_utils import (
|
||||
nettoyer_texte_fr
|
||||
)
|
||||
|
||||
from utils.ia import (
|
||||
ingest_document,
|
||||
ia_analyse,
|
||||
supprimer_fichiers,
|
||||
generer_rapport_final
|
||||
)
|
||||
|
||||
|
||||
def main(dot_path, output_path):
|
||||
"""Fonction principale du script."""
|
||||
# Charger la configuration
|
||||
config = load_config()
|
||||
|
||||
# Analyser les graphes
|
||||
graph, ref_graph = parse_graphs(dot_path)
|
||||
# Extraire les données
|
||||
data = extract_data_from_graph(graph, ref_graph)
|
||||
# Calculer les vulnérabilités
|
||||
results = calculate_vulnerabilities(data, config)
|
||||
if "step" not in st.session_state:
|
||||
st.session_state["step"] = 1
|
||||
# Générer le rapport
|
||||
report, file_names = generate_report(data, results, config)
|
||||
# Écrire le rapport
|
||||
write_report(report, TEMPLATE_PATH)
|
||||
ingest_document(TEMPLATE_PATH)
|
||||
# Générer l'analyse par l'IA du rapport complet
|
||||
analyse_finale = nettoyer_texte_fr(ia_analyse(file_names))
|
||||
analyse_fichier = TEMP_SECTIONS / TEMPLATE_PATH.name.replace(".md", " - analyse.md")
|
||||
write_report(analyse_finale, analyse_fichier)
|
||||
|
||||
if generer_rapport_final(TEMPLATE_PATH, analyse_fichier, output_path):
|
||||
supprimer_fichiers(session_uuid)
|
||||
|
||||
if __name__ == "__main__":
|
||||
dot_path = Path(sys.argv[1])
|
||||
output_path = Path(sys.argv[2])
|
||||
main(dot_path, output_path)
|
||||
31
batch_ia/batch-fabnum-dev.service
Normal file
31
batch_ia/batch-fabnum-dev.service
Normal file
@ -0,0 +1,31 @@
|
||||
[Unit]
|
||||
Description=Service batch IA pour utilisateur fabnum
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=fabnum
|
||||
WorkingDirectory=/home/fabnum/fabnum-dev/batch_ia
|
||||
Environment=PYTHONPATH=/home/fabnum/fabnum-dev
|
||||
ExecStart=/home/fabnum/fabnum-dev/venv/bin/python /home/fabnum/fabnum-dev/batch_ia/batch_runner.py
|
||||
Restart=always
|
||||
Nice=10
|
||||
CPUSchedulingPolicy=batch
|
||||
|
||||
# Limites de ressources
|
||||
CPUQuota=87.5% # ~14 cores sur 16
|
||||
MemoryMax=12G # RAM maximale autorisée
|
||||
TasksMax=1 # maximum 1 subprocess/thread simultané
|
||||
|
||||
# Sécurité renforcée
|
||||
ProtectSystem=full
|
||||
ReadWritePaths=/home/fabnum/fabnum-dev/batch_ia
|
||||
|
||||
# Journal propre
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
# semanage fcontext -a -t svirt_sandbox_file_t "/home/fabnum/fabnum-dev/batch_ia(/.*)?"
|
||||
32
batch_ia/batch_runner.py
Normal file
32
batch_ia/batch_runner.py
Normal file
@ -0,0 +1,32 @@
|
||||
import time
|
||||
import subprocess
|
||||
from batch_utils import charger_status, sauvegarder_status, JOBS_DIR
|
||||
|
||||
|
||||
while True:
|
||||
status = charger_status()
|
||||
jobs = [(login, data) for login, data in status.items() if data["status"] == "en attente"]
|
||||
jobs = sorted(jobs, key=lambda x: x[1].get("timestamp", 0))
|
||||
|
||||
for i, (login, data) in enumerate(jobs):
|
||||
status[login]["position"] = i
|
||||
sauvegarder_status(status)
|
||||
|
||||
if jobs:
|
||||
login, _ = jobs[0]
|
||||
dot_file = JOBS_DIR / f"{login}.dot"
|
||||
result_file = JOBS_DIR / f"{login}.zip"
|
||||
|
||||
status[login]["status"] = "en cours"
|
||||
sauvegarder_status(status)
|
||||
|
||||
try:
|
||||
subprocess.run(["python3", "analyse_ia.py", str(dot_file), str(result_file)], check=True)
|
||||
status[login]["status"] = "terminé"
|
||||
except Exception as e:
|
||||
status[login]["status"] = "échoué"
|
||||
status[login]["error"] = str(e)
|
||||
|
||||
sauvegarder_status(status)
|
||||
|
||||
time.sleep(60)
|
||||
64
batch_ia/batch_utils.py
Normal file
64
batch_ia/batch_utils.py
Normal file
@ -0,0 +1,64 @@
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from networkx.drawing.nx_agraph import write_dot
|
||||
import streamlit as st
|
||||
from utils.translations import _
|
||||
|
||||
BATCH_DIR = Path(__file__).resolve().parent
|
||||
JOBS_DIR = BATCH_DIR / "jobs"
|
||||
STATUS_FILE = BATCH_DIR / "status.json"
|
||||
ANALYSE = " - analyse.md"
|
||||
RAPPORT = " - rapport.md"
|
||||
|
||||
def charger_status():
|
||||
if STATUS_FILE.exists():
|
||||
return json.loads(STATUS_FILE.read_text())
|
||||
return {}
|
||||
|
||||
def sauvegarder_status(data):
|
||||
STATUS_FILE.write_text(json.dumps(data, indent=2))
|
||||
|
||||
def statut_utilisateur(login):
|
||||
status = charger_status()
|
||||
entry = status.get(login)
|
||||
if not entry:
|
||||
return {"statut": None, "position": None, "telechargement": None,
|
||||
"message": f"{str(_('batch.no_task'))}."}
|
||||
|
||||
if entry["status"] == "en attente":
|
||||
return {"statut": "en attente", "position": entry.get("position"),
|
||||
"telechargement": None,
|
||||
"message": f"{str(_('batch.in_queue'))} (position {entry.get('position', '?')})."}
|
||||
|
||||
if entry["status"] == "en cours":
|
||||
if "step" not in st.session_state:
|
||||
st.session_state["step"] = 1
|
||||
return {"statut": "en cours", "position": 0,
|
||||
"telechargement": None, "message": f"{str(_('batch.in_progress'))} ({str(_('batch.step'))} {st.session_state["step"]}/5)."}
|
||||
|
||||
if entry["status"] == "terminé":
|
||||
result_file = JOBS_DIR / f"{login}.zip"
|
||||
if result_file.exists():
|
||||
return {"statut": "terminé", "position": None,
|
||||
"telechargement": result_file.read_bytes(),
|
||||
"message": f"{str(_('batch.complete'))}."}
|
||||
|
||||
if entry["status"] == "échoué":
|
||||
return {"statut": "échoué", "position": None, "telechargement": None,
|
||||
"message": f"{str(_('batch.failure'))} : {entry.get('error', {str(_('batch.unknown_error'))})}"}
|
||||
|
||||
def soumettre_batch(login, G):
|
||||
if statut_utilisateur(login)["statut"]:
|
||||
raise RuntimeError("Un batch est déjà en cours.")
|
||||
write_dot(G, JOBS_DIR / f"{login}.dot")
|
||||
status = charger_status()
|
||||
status[login] = {"status": "en attente", "timestamp": time.time()}
|
||||
sauvegarder_status(status)
|
||||
|
||||
def nettoyage_post_telechargement(login):
|
||||
(JOBS_DIR / f"{login}.dot").unlink(missing_ok=True)
|
||||
(JOBS_DIR / f"{login}.zip").unlink(missing_ok=True)
|
||||
status = charger_status()
|
||||
status.pop(login, None)
|
||||
sauvegarder_status(status)
|
||||
100
batch_ia/nettoyer_pgpt.py
Normal file
100
batch_ia/nettoyer_pgpt.py
Normal file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script de nettoyage pour PrivateGPT
|
||||
|
||||
Ce script permet de lister et supprimer les documents ingérés dans PrivateGPT.
|
||||
Options:
|
||||
- Lister tous les documents
|
||||
- Supprimer des documents par préfixe (ex: "temp_section_")
|
||||
- Supprimer des documents par motif
|
||||
- Supprimer tous les documents
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import requests
|
||||
import time
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
# Configuration de l'API PrivateGPT
|
||||
PGPT_URL = "http://127.0.0.1:8001"
|
||||
API_URL = f"{PGPT_URL}/v1"
|
||||
|
||||
def list_documents() -> List[Dict[str, Any]]:
|
||||
"""Liste tous les documents ingérés et renvoie la liste des métadonnées"""
|
||||
try:
|
||||
# Récupérer la liste des documents
|
||||
response = requests.get(f"{API_URL}/ingest/list")
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Format de réponse OpenAI
|
||||
if "data" in data:
|
||||
documents = data.get("data", [])
|
||||
# Format alternatif
|
||||
else:
|
||||
documents = data.get("documents", [])
|
||||
|
||||
# Construire une liste normalisée des documents
|
||||
normalized_docs = []
|
||||
for doc in documents:
|
||||
doc_id = doc.get("doc_id") or doc.get("id")
|
||||
metadata = doc.get("doc_metadata", {})
|
||||
filename = metadata.get("file_name") or metadata.get("filename", "Inconnu")
|
||||
|
||||
normalized_docs.append({
|
||||
"id": doc_id,
|
||||
"filename": filename,
|
||||
"metadata": metadata
|
||||
})
|
||||
|
||||
return normalized_docs
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors de la récupération des documents: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def delete_document(doc_id: str) -> bool:
|
||||
"""Supprime un document par son ID"""
|
||||
try:
|
||||
response = requests.delete(f"{API_URL}/ingest/{doc_id}")
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ Échec de la suppression de l'ID {doc_id}: Code {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors de la suppression de l'ID {doc_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def delete_documents_by_criteria(pattern) -> int:
|
||||
"""
|
||||
Supprime des documents selon différents critères
|
||||
Retourne le nombre de documents supprimés
|
||||
"""
|
||||
|
||||
documents = list_documents()
|
||||
|
||||
if not documents or not pattern:
|
||||
return 0
|
||||
|
||||
# Comptage des suppressions réussies
|
||||
success_count = 0
|
||||
|
||||
# Filtrer les documents à supprimer
|
||||
docs_to_delete = []
|
||||
try:
|
||||
regex = re.compile(pattern)
|
||||
docs_to_delete = [doc for doc in documents if regex.search(doc["filename"])]
|
||||
except re.error as e:
|
||||
return 0
|
||||
|
||||
# Supprimer les documents
|
||||
for doc in docs_to_delete:
|
||||
delete_document(doc["id"])
|
||||
# Petite pause pour éviter de surcharger l'API
|
||||
time.sleep(0.1)
|
||||
|
||||
return success_count
|
||||
1
batch_ia/status.json
Normal file
1
batch_ia/status.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
batch_ia/utils/__init__.py
Normal file
1
batch_ia/utils/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
93
batch_ia/utils/config.py
Normal file
93
batch_ia/utils/config.py
Normal file
@ -0,0 +1,93 @@
|
||||
import os
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
|
||||
def init_uuid():
|
||||
if not TEMP_SECTIONS.exists():
|
||||
TEMP_SECTIONS.mkdir(parents=True)
|
||||
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}")
|
||||
return session_uuid
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
CORPUS_DIR = BASE_DIR.parent / "Corpus"
|
||||
THRESHOLDS_PATH = BASE_DIR.parent / "assets" / "config.yaml"
|
||||
REFERENCE_GRAPH_PATH = BASE_DIR.parent / "schema.txt"
|
||||
GRAPH_PATH = BASE_DIR.parent / "graphe.dot"
|
||||
TEMP_SECTIONS = BASE_DIR / "temp_sections"
|
||||
session_uuid = init_uuid()
|
||||
TEMPLATE_PATH = TEMP_SECTIONS / f"rapport_final - {session_uuid}.md"
|
||||
|
||||
PGPT_URL = "http://127.0.0.1:8001"
|
||||
API_URL = f"{PGPT_URL}/v1"
|
||||
PROMPT_METHODOLOGIE = """
|
||||
Le rapport à examiner a été établi à partir de la méthodologie suivante.
|
||||
|
||||
Le dispositif d’évaluation des risques proposé repose sur quatre indices clairement définis, chacun analysant un aspect spécifique des risques dans la chaîne d’approvisionnement numérique. L’indice IHH mesure la concentration géographique ou industrielle, permettant d’évaluer la dépendance vis-à-vis de certains acteurs ou régions. L’indice ISG indique la stabilité géopolitique des pays impliqués dans la chaîne de production, en intégrant des critères politiques, sociaux et climatiques. L’indice ICS quantifie la facilité ou la difficulté à remplacer ou substituer un élément spécifique dans la chaîne, évaluant ainsi les risques liés à la dépendance technologique et économique. Enfin, l’indice IVC examine la pression concurrentielle sur les ressources utilisées par le numérique, révélant ainsi le risque potentiel que ces ressources soient détournées vers d’autres secteurs industriels.
|
||||
|
||||
Ces indices se combinent judicieusement par paires pour une évaluation approfondie et pertinente des risques. La combinaison IHH-ISG permet d’associer la gravité d'un impact potentiel (IHH) à la probabilité de survenance d’un événement perturbateur (ISG), créant ainsi une matrice de vulnérabilité combinée utile pour identifier rapidement les points critiques dans la chaîne de production. La combinaison ICS-IVC fonctionne selon la même logique, mais se concentre spécifiquement sur les ressources minérales : l’ICS indique la gravité potentielle d'une rupture d'approvisionnement due à une faible substituabilité, tandis que l’IVC évalue la probabilité que les ressources soient captées par d'autres secteurs industriels concurrents. Ces combinaisons permettent d’obtenir une analyse précise et opérationnelle du niveau de risque global.
|
||||
|
||||
Les avantages de cette méthodologie résident dans son approche à la fois systématique et granulaire, adaptée à l'échelle décisionnelle d'un COMEX. Elle permet d’identifier avec précision les vulnérabilités majeures et leurs origines spécifiques, facilitant ainsi la prise de décision stratégique éclairée et proactive. En combinant des facteurs géopolitiques, industriels, technologiques et concurrentiels, ces indices offrent un suivi efficace de la chaîne de fabrication numérique, garantissant ainsi une gestion optimale des risques et la continuité opérationnelle à long terme.
|
||||
"""
|
||||
|
||||
DICTIONNAIRE_CRITICITES = {
|
||||
"IHH": {"vert": "Faible", "orange": "Modérée", "rouge": "Élevée"},
|
||||
"ISG": {"vert": "Stable", "orange": "Intermédiaire", "rouge": "Instable"},
|
||||
"ICS": {"vert": "Facile", "orange": "Moyenne", "rouge": "Difficile"},
|
||||
"IVC": {"vert": "Faible", "orange": "Modérée", "rouge": "Forte"}
|
||||
}
|
||||
POIDS_COULEURS = {
|
||||
"Vert": 1,
|
||||
"Orange": 2,
|
||||
"Rouge": 3
|
||||
}
|
||||
|
||||
def load_config(thresholds_path=THRESHOLDS_PATH):
|
||||
"""Charge la configuration depuis les fichiers YAML."""
|
||||
config = {}
|
||||
# Charger les seuils
|
||||
if os.path.exists(thresholds_path):
|
||||
with open(thresholds_path, 'r', encoding='utf-8') as f:
|
||||
thresholds = yaml.safe_load(f)
|
||||
config['thresholds'] = thresholds.get('seuils', {})
|
||||
return config
|
||||
|
||||
def determine_threshold_color(value, index_type, thresholds):
|
||||
"""
|
||||
Détermine la couleur du seuil en fonction du type d'indice et de sa valeur.
|
||||
Utilise les seuils de config.yaml si disponibles.
|
||||
"""
|
||||
|
||||
# Récupérer les seuils pour cet indice
|
||||
if index_type in thresholds:
|
||||
index_thresholds = thresholds[index_type]
|
||||
# Déterminer la couleur
|
||||
if "vert" in index_thresholds and "max" in index_thresholds["vert"] and \
|
||||
index_thresholds["vert"]["max"] is not None and value < index_thresholds["vert"]["max"]:
|
||||
suffix = get_suffix_for_index(index_type, "vert")
|
||||
return "Vert", suffix
|
||||
elif "orange" in index_thresholds and "min" in index_thresholds["orange"] and "max" in index_thresholds["orange"] and \
|
||||
index_thresholds["orange"]["min"] is not None and index_thresholds["orange"]["max"] is not None and \
|
||||
index_thresholds["orange"]["min"] <= value < index_thresholds["orange"]["max"]:
|
||||
suffix = get_suffix_for_index(index_type, "orange")
|
||||
return "Orange", suffix
|
||||
elif "rouge" in index_thresholds and "min" in index_thresholds["rouge"] and \
|
||||
index_thresholds["rouge"]["min"] is not None and value >= index_thresholds["rouge"]["min"]:
|
||||
suffix = get_suffix_for_index(index_type, "rouge")
|
||||
return "Rouge", suffix
|
||||
|
||||
return "Non déterminé", ""
|
||||
|
||||
def get_suffix_for_index(index_type, color):
|
||||
"""Retourne le suffixe approprié pour chaque indice et couleur."""
|
||||
suffixes = DICTIONNAIRE_CRITICITES
|
||||
|
||||
if index_type in suffixes and color in suffixes[index_type]:
|
||||
return suffixes[index_type][color]
|
||||
return ""
|
||||
|
||||
def get_weight_for_color(color):
|
||||
"""Retourne le poids correspondant à une couleur."""
|
||||
weights = POIDS_COULEURS
|
||||
return weights.get(color, 0)
|
||||
133
batch_ia/utils/files.py
Normal file
133
batch_ia/utils/files.py
Normal file
@ -0,0 +1,133 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from .config import (
|
||||
CORPUS_DIR
|
||||
)
|
||||
|
||||
def strip_prefix(name):
|
||||
"""Supprime le préfixe numérique éventuel d'un nom de fichier ou de dossier."""
|
||||
return re.sub(r'^\d+[-_ ]*', '', name).lower()
|
||||
|
||||
def find_prefixed_directory(pattern, base_path=None):
|
||||
"""
|
||||
Recherche un sous-répertoire dont le nom (sans préfixe) correspond au pattern.
|
||||
|
||||
Args:
|
||||
pattern: Nom du répertoire sans préfixe
|
||||
base_path: Répertoire de base où chercher
|
||||
|
||||
Returns:
|
||||
Le chemin relatif du répertoire trouvé (avec préfixe) ou None
|
||||
"""
|
||||
if base_path:
|
||||
search_path = os.path.join(CORPUS_DIR, base_path)
|
||||
else:
|
||||
search_path = CORPUS_DIR
|
||||
|
||||
if not os.path.exists(search_path):
|
||||
# print(f"Chemin inexistant: {search_path}")
|
||||
return None
|
||||
|
||||
for d in os.listdir(search_path):
|
||||
dir_path = os.path.join(search_path, d)
|
||||
if os.path.isdir(dir_path) and strip_prefix(d) == pattern.lower():
|
||||
return os.path.relpath(dir_path, CORPUS_DIR)
|
||||
|
||||
# print(f"Aucun répertoire correspondant à: '{pattern}' trouvé dans {search_path}")
|
||||
return None
|
||||
|
||||
def find_corpus_file(pattern, base_path=None):
|
||||
"""
|
||||
Recherche récursive dans le corpus d'un fichier en ignorant les préfixes numériques dans les dossiers et fichiers.
|
||||
|
||||
Args:
|
||||
pattern: Chemin relatif type "sous-dossier/nom-fichier"
|
||||
base_path: Dossier de base à partir duquel chercher
|
||||
|
||||
Returns:
|
||||
Chemin relatif du fichier trouvé ou None
|
||||
"""
|
||||
|
||||
if base_path:
|
||||
search_path = os.path.join(CORPUS_DIR, base_path)
|
||||
else:
|
||||
search_path = CORPUS_DIR
|
||||
|
||||
# # print(f"Recherche de: '{pattern}' dans {search_path}")
|
||||
|
||||
if not os.path.exists(search_path):
|
||||
# print(pattern)
|
||||
# print(base_path)
|
||||
# print(f"Chemin inexistant: {search_path}")
|
||||
return None
|
||||
|
||||
if '/' not in pattern:
|
||||
# Recherche directe d'un fichier
|
||||
for file in os.listdir(search_path):
|
||||
if not file.endswith('.md'):
|
||||
continue
|
||||
if strip_prefix(os.path.splitext(file)[0]) == pattern.lower():
|
||||
rel_path = os.path.relpath(os.path.join(search_path, file), CORPUS_DIR)
|
||||
# # print(f"Fichier trouvé: {rel_path}")
|
||||
return rel_path
|
||||
else:
|
||||
# Séparation du chemin en dossier/fichier
|
||||
first, rest = pattern.split('/', 1)
|
||||
matched_dir = find_prefixed_directory(first, base_path)
|
||||
if matched_dir:
|
||||
return find_corpus_file(rest, matched_dir)
|
||||
|
||||
# print(f"Aucun fichier correspondant à: '{pattern}' trouvé dans {base_path}.")
|
||||
return None
|
||||
|
||||
|
||||
def read_corpus_file(file_path, remove_first_title=False, shift_titles=0):
|
||||
"""
|
||||
Lit un fichier du corpus et applique les transformations demandées.
|
||||
|
||||
Args:
|
||||
file_path: Chemin relatif du fichier dans le corpus
|
||||
remove_first_title: Si True, supprime la première ligne de titre
|
||||
shift_titles: Nombre de niveaux à ajouter aux titres
|
||||
|
||||
Returns:
|
||||
Le contenu du fichier avec les transformations appliquées
|
||||
"""
|
||||
full_path = os.path.join(CORPUS_DIR, file_path)
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
# print(f"Fichier non trouvé: {full_path}")
|
||||
return f"Fichier non trouvé: {file_path}"
|
||||
|
||||
# # print(f"Lecture du fichier: {full_path}")
|
||||
with open(full_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Supprimer la première ligne si c'est un titre et si demandé
|
||||
if remove_first_title and lines and lines[0].startswith('#'):
|
||||
# # print(f"Suppression du titre: {lines[0].strip()}")
|
||||
lines = lines[1:]
|
||||
|
||||
# Décaler les niveaux de titre si demandé
|
||||
if shift_titles > 0:
|
||||
for i in range(len(lines)):
|
||||
if lines[i].startswith('#'):
|
||||
lines[i] = '#' * shift_titles + lines[i]
|
||||
|
||||
# Nettoyer les retours à la ligne superflus
|
||||
content = ''.join(lines)
|
||||
# Supprimer les retours à la ligne en fin de contenu
|
||||
content = content.rstrip('\n') + '\n'
|
||||
|
||||
return content
|
||||
|
||||
def write_report(report, fichier):
|
||||
"""Écrit le rapport généré dans le fichier spécifié."""
|
||||
|
||||
report = re.sub(r'<!----.*?-->', '', report)
|
||||
report = re.sub(r'\n\n\n+', '\n\n', report)
|
||||
|
||||
with open(fichier, 'w', encoding='utf-8') as f:
|
||||
f.write(report)
|
||||
# print(f"Rapport généré avec succès: {TEMPLATE_PATH}")
|
||||
591
batch_ia/utils/graphs.py
Normal file
591
batch_ia/utils/graphs.py
Normal file
@ -0,0 +1,591 @@
|
||||
import os
|
||||
import sys
|
||||
from networkx.drawing.nx_agraph import read_dot
|
||||
|
||||
from .config import (
|
||||
REFERENCE_GRAPH_PATH,
|
||||
determine_threshold_color, get_weight_for_color
|
||||
)
|
||||
|
||||
def parse_graphs(graphe_path):
|
||||
"""
|
||||
Charge et analyse les graphes DOT (analyse et référence).
|
||||
"""
|
||||
print(graphe_path)
|
||||
# Charger le graphe à analyser
|
||||
if not os.path.exists(graphe_path):
|
||||
print(f"Fichier de graphe à analyser introuvable: {graphe_path}")
|
||||
sys.exit(1)
|
||||
|
||||
# Charger le graphe de référence
|
||||
reference_path = REFERENCE_GRAPH_PATH
|
||||
if not os.path.exists(reference_path):
|
||||
print(f"Fichier de graphe de référence introuvable: {reference_path}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
# Charger les graphes avec NetworkX
|
||||
graph = read_dot(graphe_path)
|
||||
ref_graph = read_dot(reference_path)
|
||||
|
||||
# Convertir les attributs en types appropriés pour les deux graphes
|
||||
for g in [graph, ref_graph]:
|
||||
for node, attrs in g.nodes(data=True):
|
||||
for key, value in list(attrs.items()):
|
||||
# Convertir les valeurs numériques
|
||||
if key in ['niveau', 'ihh_acteurs', 'ihh_pays', 'isg', 'ivc']:
|
||||
try:
|
||||
if key in ['isg', 'ivc', 'ihh_acteurs', 'ihh_pays', 'niveau']:
|
||||
attrs[key] = int(value.strip('"'))
|
||||
else:
|
||||
attrs[key] = float(value.strip('"'))
|
||||
except (ValueError, TypeError):
|
||||
# Garder la valeur originale si la conversion échoue
|
||||
pass
|
||||
elif key == 'label':
|
||||
# Nettoyer les guillemets des étiquettes
|
||||
attrs[key] = value.strip('"')
|
||||
|
||||
# Convertir les attributs des arêtes
|
||||
for u, v, attrs in g.edges(data=True):
|
||||
for key, value in list(attrs.items()):
|
||||
if key in ['ics', 'cout', 'delai', 'technique']:
|
||||
try:
|
||||
attrs[key] = float(value.strip('"'))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
elif key == 'label' and '%' in value:
|
||||
# Extraire le pourcentage
|
||||
try:
|
||||
percentage = value.strip('"').replace('%', '')
|
||||
attrs['percentage'] = float(percentage)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return graph, ref_graph
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'analyse des graphes: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
def extract_data_from_graph(graph, ref_graph):
|
||||
"""
|
||||
Extrait toutes les données pertinentes des graphes DOT.
|
||||
"""
|
||||
data = {
|
||||
"products": {}, # Produits finaux (N0)
|
||||
"components": {}, # Composants (N1)
|
||||
"minerals": {}, # Minerais (N2)
|
||||
"operations": {}, # Opérations (N10)
|
||||
"countries": {}, # Pays (N11)
|
||||
"geo_countries": {}, # Pays géographiques (N99)
|
||||
"actors": {} # Acteurs (N12)
|
||||
}
|
||||
|
||||
# Extraire tous les pays géographiques du graphe de référence
|
||||
for node, attrs in ref_graph.nodes(data=True):
|
||||
if attrs.get('niveau') == 99:
|
||||
country_name = attrs.get('label', node)
|
||||
isg_value = attrs.get('isg', 0)
|
||||
|
||||
data["geo_countries"][country_name] = {
|
||||
"id": node,
|
||||
"isg": isg_value
|
||||
}
|
||||
|
||||
# Extraire les nœuds du graphe à analyser
|
||||
for node, attrs in graph.nodes(data=True):
|
||||
level = attrs.get('niveau', -1)
|
||||
label = attrs.get('label', node)
|
||||
|
||||
if level == 0 or level == 1000: # Produit final
|
||||
data["products"][node] = {
|
||||
"label": label,
|
||||
"components": [],
|
||||
"assembly": None,
|
||||
"level": level
|
||||
}
|
||||
elif level == 1 or level == 1001: # Composant
|
||||
data["components"][node] = {
|
||||
"label": label,
|
||||
"minerals": [],
|
||||
"manufacturing": None
|
||||
}
|
||||
elif level == 2: # Minerai
|
||||
data["minerals"][node] = {
|
||||
"label": label,
|
||||
"ivc": attrs.get('ivc', 0),
|
||||
"extraction": None,
|
||||
"treatment": None,
|
||||
"ics_values": {}
|
||||
}
|
||||
elif level == 10 or level == 1010: # Opération
|
||||
op_type = label.lower()
|
||||
data["operations"][node] = {
|
||||
"label": label,
|
||||
"type": op_type,
|
||||
"ihh_acteurs": attrs.get('ihh_acteurs', 0),
|
||||
"ihh_pays": attrs.get('ihh_pays', 0),
|
||||
"countries": {}
|
||||
}
|
||||
elif level == 11 or level == 1011: # Pays
|
||||
data["countries"][node] = {
|
||||
"label": label,
|
||||
"actors": {},
|
||||
"geo_country": None,
|
||||
"market_share": 0
|
||||
}
|
||||
elif level == 12 or level == 1012: # Acteur
|
||||
data["actors"][node] = {
|
||||
"label": label,
|
||||
"country": None,
|
||||
"market_share": 0
|
||||
}
|
||||
|
||||
# Extraire les relations et attributs des arêtes
|
||||
for source, target, edge_attrs in graph.edges(data=True):
|
||||
if source not in graph.nodes or target not in graph.nodes:
|
||||
continue
|
||||
|
||||
source_level = graph.nodes[source].get('niveau', -1)
|
||||
target_level = graph.nodes[target].get('niveau', -1)
|
||||
|
||||
# Extraire part de marché
|
||||
market_share = 0
|
||||
if 'percentage' in edge_attrs:
|
||||
market_share = edge_attrs['percentage']
|
||||
elif 'label' in edge_attrs and '%' in edge_attrs['label']:
|
||||
try:
|
||||
market_share = float(edge_attrs['label'].strip('"').replace('%', ''))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Relations produit → composant
|
||||
if (source_level == 0 and target_level == 1) or (source_level == 1000 and target_level == 1001):
|
||||
if target not in data["products"][source]["components"]:
|
||||
data["products"][source]["components"].append(target)
|
||||
|
||||
# Relations produit → opération (assemblage)
|
||||
elif (source_level == 0 and target_level == 10) or (source_level == 1000 and target_level == 1010):
|
||||
if graph.nodes[target].get('label', '').lower() == 'assemblage':
|
||||
data["products"][source]["assembly"] = target
|
||||
|
||||
# Relations composant → minerai avec ICS
|
||||
elif (source_level == 1 or source_level == 1001) and target_level == 2:
|
||||
if target not in data["components"][source]["minerals"]:
|
||||
data["components"][source]["minerals"].append(target)
|
||||
|
||||
# Stocker l'ICS s'il est présent
|
||||
if 'ics' in edge_attrs:
|
||||
ics_value = edge_attrs['ics']
|
||||
data["minerals"][target]["ics_values"][source] = ics_value
|
||||
|
||||
# Relations composant → opération (fabrication)
|
||||
elif (source_level == 1 or source_level == 1001) and target_level == 10:
|
||||
if graph.nodes[target].get('label', '').lower() == 'fabrication':
|
||||
data["components"][source]["manufacturing"] = target
|
||||
|
||||
# Relations minerai → opération (extraction/traitement)
|
||||
elif source_level == 2 and target_level == 10:
|
||||
op_label = graph.nodes[target].get('label', '').lower()
|
||||
if 'extraction' in op_label:
|
||||
data["minerals"][source]["extraction"] = target
|
||||
elif 'traitement' in op_label:
|
||||
data["minerals"][source]["treatment"] = target
|
||||
|
||||
# Relations opération → pays avec part de marché
|
||||
elif (source_level == 10 and target_level == 11) or (source_level == 1010 and target_level == 1011):
|
||||
data["operations"][source]["countries"][target] = market_share
|
||||
data["countries"][target]["market_share"] = market_share
|
||||
|
||||
# Relations pays → acteur avec part de marché
|
||||
elif (source_level == 11 and target_level == 12) or (source_level == 1011 and target_level == 1012):
|
||||
data["countries"][source]["actors"][target] = market_share
|
||||
data["actors"][target]["market_share"] = market_share
|
||||
data["actors"][target]["country"] = source
|
||||
|
||||
# Relations pays → pays géographique
|
||||
elif (source_level == 11 or source_level == 1011) and target_level == 99:
|
||||
country_name = graph.nodes[target].get('label', '')
|
||||
data["countries"][source]["geo_country"] = country_name
|
||||
|
||||
# Compléter les opérations manquantes pour les produits et composants
|
||||
# en les récupérant du graphe de référence si elles existent
|
||||
|
||||
# Pour les produits finaux (N0)
|
||||
for product_id, product_data in data["products"].items():
|
||||
if product_data["assembly"] is None:
|
||||
# Chercher l'opération d'assemblage dans le graphe de référence
|
||||
for source, target, edge_attrs in ref_graph.edges(data=True):
|
||||
if (source == product_id and
|
||||
((ref_graph.nodes[source].get('niveau') == 0 and
|
||||
ref_graph.nodes[target].get('niveau') == 10) or
|
||||
(ref_graph.nodes[source].get('niveau') == 1000 and
|
||||
ref_graph.nodes[target].get('niveau') == 1010)) and
|
||||
ref_graph.nodes[target].get('label', '').lower() == 'assemblage'):
|
||||
|
||||
# L'opération existe dans le graphe de référence
|
||||
assembly_id = target
|
||||
product_data["assembly"] = assembly_id
|
||||
|
||||
# Ajouter l'opération si elle n'existe pas déjà
|
||||
if assembly_id not in data["operations"]:
|
||||
data["operations"][assembly_id] = {
|
||||
"label": ref_graph.nodes[assembly_id].get('label', assembly_id),
|
||||
"type": "assemblage",
|
||||
"ihh_acteurs": ref_graph.nodes[assembly_id].get('ihh_acteurs', 0),
|
||||
"ihh_pays": ref_graph.nodes[assembly_id].get('ihh_pays', 0),
|
||||
"countries": {}
|
||||
}
|
||||
|
||||
# Extraire les relations de l'opération vers les pays
|
||||
for op_source, op_target, op_edge_attrs in ref_graph.edges(data=True):
|
||||
if (op_source == assembly_id and
|
||||
(ref_graph.nodes[op_target].get('niveau') == 11 or ref_graph.nodes[op_target].get('niveau') == 1011)):
|
||||
|
||||
country_id = op_target
|
||||
|
||||
# Extraire part de marché
|
||||
market_share = 0
|
||||
if 'percentage' in op_edge_attrs:
|
||||
market_share = op_edge_attrs['percentage']
|
||||
elif 'label' in op_edge_attrs and '%' in op_edge_attrs['label']:
|
||||
try:
|
||||
market_share = float(op_edge_attrs['label'].strip('"').replace('%', ''))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Ajouter le pays à l'opération
|
||||
data["operations"][assembly_id]["countries"][country_id] = market_share
|
||||
|
||||
# Ajouter le pays s'il n'existe pas déjà
|
||||
if country_id not in data["countries"]:
|
||||
data["countries"][country_id] = {
|
||||
"label": ref_graph.nodes[country_id].get('label', country_id),
|
||||
"actors": {},
|
||||
"geo_country": None,
|
||||
"market_share": market_share
|
||||
}
|
||||
else:
|
||||
data["countries"][country_id]["market_share"] = market_share
|
||||
|
||||
# Extraire les relations du pays vers les acteurs
|
||||
for country_source, country_target, country_edge_attrs in ref_graph.edges(data=True):
|
||||
if (country_source == country_id and
|
||||
(ref_graph.nodes[country_target].get('niveau') == 12 or ref_graph.nodes[country_target].get('niveau') == 1012)):
|
||||
|
||||
actor_id = country_target
|
||||
|
||||
# Extraire part de marché
|
||||
actor_market_share = 0
|
||||
if 'percentage' in country_edge_attrs:
|
||||
actor_market_share = country_edge_attrs['percentage']
|
||||
elif 'label' in country_edge_attrs and '%' in country_edge_attrs['label']:
|
||||
try:
|
||||
actor_market_share = float(country_edge_attrs['label'].strip('"').replace('%', ''))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Ajouter l'acteur au pays
|
||||
data["countries"][country_id]["actors"][actor_id] = actor_market_share
|
||||
|
||||
# Ajouter l'acteur s'il n'existe pas déjà
|
||||
if actor_id not in data["actors"]:
|
||||
data["actors"][actor_id] = {
|
||||
"label": ref_graph.nodes[actor_id].get('label', actor_id),
|
||||
"country": country_id,
|
||||
"market_share": actor_market_share
|
||||
}
|
||||
else:
|
||||
data["actors"][actor_id]["market_share"] = actor_market_share
|
||||
data["actors"][actor_id]["country"] = country_id
|
||||
|
||||
# Extraire la relation du pays vers le pays géographique
|
||||
for geo_source, geo_target, geo_edge_attrs in ref_graph.edges(data=True):
|
||||
if (geo_source == country_id and
|
||||
ref_graph.nodes[geo_target].get('niveau') == 99):
|
||||
|
||||
geo_country_name = ref_graph.nodes[geo_target].get('label', '')
|
||||
data["countries"][country_id]["geo_country"] = geo_country_name
|
||||
|
||||
break # Une seule opération d'assemblage par produit
|
||||
|
||||
# Pour les composants (N1)
|
||||
for component_id, component_data in data["components"].items():
|
||||
if component_data["manufacturing"] is None:
|
||||
# Chercher l'opération de fabrication dans le graphe de référence
|
||||
for source, target, edge_attrs in ref_graph.edges(data=True):
|
||||
if (source == component_id and
|
||||
((ref_graph.nodes[source].get('niveau') == 1 and
|
||||
ref_graph.nodes[target].get('niveau') == 10) or
|
||||
(ref_graph.nodes[source].get('niveau') == 1001 and
|
||||
ref_graph.nodes[target].get('niveau') == 1010)) and
|
||||
ref_graph.nodes[target].get('label', '').lower() == 'fabrication'):
|
||||
|
||||
# L'opération existe dans le graphe de référence
|
||||
manufacturing_id = target
|
||||
component_data["manufacturing"] = manufacturing_id
|
||||
|
||||
# Ajouter l'opération si elle n'existe pas déjà
|
||||
if manufacturing_id not in data["operations"]:
|
||||
data["operations"][manufacturing_id] = {
|
||||
"label": ref_graph.nodes[manufacturing_id].get('label', manufacturing_id),
|
||||
"type": "fabrication",
|
||||
"ihh_acteurs": ref_graph.nodes[manufacturing_id].get('ihh_acteurs', 0),
|
||||
"ihh_pays": ref_graph.nodes[manufacturing_id].get('ihh_pays', 0),
|
||||
"countries": {}
|
||||
}
|
||||
|
||||
# Extraire les relations de l'opération vers les pays
|
||||
for op_source, op_target, op_edge_attrs in ref_graph.edges(data=True):
|
||||
if (op_source == manufacturing_id and
|
||||
(ref_graph.nodes[op_target].get('niveau') == 11 or ref_graph.nodes[op_target].get('niveau') == 1011)):
|
||||
|
||||
country_id = op_target
|
||||
|
||||
# Extraire part de marché
|
||||
market_share = 0
|
||||
if 'percentage' in op_edge_attrs:
|
||||
market_share = op_edge_attrs['percentage']
|
||||
elif 'label' in op_edge_attrs and '%' in op_edge_attrs['label']:
|
||||
try:
|
||||
market_share = float(op_edge_attrs['label'].strip('"').replace('%', ''))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Ajouter le pays à l'opération
|
||||
data["operations"][manufacturing_id]["countries"][country_id] = market_share
|
||||
|
||||
# Ajouter le pays s'il n'existe pas déjà
|
||||
if country_id not in data["countries"]:
|
||||
data["countries"][country_id] = {
|
||||
"label": ref_graph.nodes[country_id].get('label', country_id),
|
||||
"actors": {},
|
||||
"geo_country": None,
|
||||
"market_share": market_share
|
||||
}
|
||||
else:
|
||||
data["countries"][country_id]["market_share"] = market_share
|
||||
|
||||
# Extraire les relations du pays vers les acteurs
|
||||
for country_source, country_target, country_edge_attrs in ref_graph.edges(data=True):
|
||||
if (country_source == country_id and
|
||||
(ref_graph.nodes[country_target].get('niveau') == 12 or ref_graph.nodes[country_target].get('niveau') == 1012)):
|
||||
|
||||
actor_id = country_target
|
||||
|
||||
# Extraire part de marché
|
||||
actor_market_share = 0
|
||||
if 'percentage' in country_edge_attrs:
|
||||
actor_market_share = country_edge_attrs['percentage']
|
||||
elif 'label' in country_edge_attrs and '%' in country_edge_attrs['label']:
|
||||
try:
|
||||
actor_market_share = float(country_edge_attrs['label'].strip('"').replace('%', ''))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Ajouter l'acteur au pays
|
||||
data["countries"][country_id]["actors"][actor_id] = actor_market_share
|
||||
|
||||
# Ajouter l'acteur s'il n'existe pas déjà
|
||||
if actor_id not in data["actors"]:
|
||||
data["actors"][actor_id] = {
|
||||
"label": ref_graph.nodes[actor_id].get('label', actor_id),
|
||||
"country": country_id,
|
||||
"market_share": actor_market_share
|
||||
}
|
||||
else:
|
||||
data["actors"][actor_id]["market_share"] = actor_market_share
|
||||
data["actors"][actor_id]["country"] = country_id
|
||||
|
||||
# Extraire la relation du pays vers le pays géographique
|
||||
for geo_source, geo_target, geo_edge_attrs in ref_graph.edges(data=True):
|
||||
if (geo_source == country_id and
|
||||
ref_graph.nodes[geo_target].get('niveau') == 99):
|
||||
|
||||
geo_country_name = ref_graph.nodes[geo_target].get('label', '')
|
||||
data["countries"][country_id]["geo_country"] = geo_country_name
|
||||
|
||||
break # Une seule opération de fabrication par composant
|
||||
|
||||
return data
|
||||
|
||||
def calculate_vulnerabilities(data, config):
|
||||
"""
|
||||
Calcule les vulnérabilités combinées pour toutes les opérations et minerais.
|
||||
"""
|
||||
thresholds = config.get('thresholds', {})
|
||||
results = {
|
||||
"ihh_isg_combined": {}, # Pour chaque opération
|
||||
"ics_ivc_combined": {}, # Pour chaque minerai
|
||||
"chains": [] # Pour stocker tous les chemins possibles
|
||||
}
|
||||
|
||||
# 1. Calculer ISG_combiné pour chaque opération
|
||||
for op_id, operation in data["operations"].items():
|
||||
isg_weighted_sum = 0
|
||||
total_share = 0
|
||||
|
||||
# Parcourir chaque pays impliqué dans l'opération
|
||||
for country_id, share in operation["countries"].items():
|
||||
country = data["countries"][country_id]
|
||||
geo_country = country.get("geo_country")
|
||||
|
||||
if geo_country and geo_country in data["geo_countries"]:
|
||||
isg_value = data["geo_countries"][geo_country]["isg"]
|
||||
isg_weighted_sum += isg_value * share
|
||||
total_share += share
|
||||
|
||||
# Calculer la moyenne pondérée
|
||||
isg_combined = 0
|
||||
if total_share > 0:
|
||||
isg_combined = isg_weighted_sum / total_share
|
||||
|
||||
# Déterminer couleurs et poids
|
||||
ihh_value = operation["ihh_pays"]
|
||||
ihh_color, ihh_suffix = determine_threshold_color(ihh_value, "IHH", thresholds)
|
||||
isg_color, isg_suffix = determine_threshold_color(isg_combined, "ISG", thresholds)
|
||||
|
||||
# Calculer poids combiné
|
||||
ihh_weight = get_weight_for_color(ihh_color)
|
||||
isg_weight = get_weight_for_color(isg_color)
|
||||
combined_weight = ihh_weight * isg_weight
|
||||
|
||||
# Déterminer vulnérabilité combinée
|
||||
if combined_weight in [6, 9]:
|
||||
vulnerability = "ÉLEVÉE à CRITIQUE"
|
||||
elif combined_weight in [3, 4]:
|
||||
vulnerability = "MOYENNE"
|
||||
else: # 1, 2
|
||||
vulnerability = "FAIBLE"
|
||||
|
||||
# Stocker résultats
|
||||
results["ihh_isg_combined"][op_id] = {
|
||||
"ihh_value": ihh_value,
|
||||
"ihh_color": ihh_color,
|
||||
"ihh_suffix": ihh_suffix,
|
||||
"isg_combined": isg_combined,
|
||||
"isg_color": isg_color,
|
||||
"isg_suffix": isg_suffix,
|
||||
"combined_weight": combined_weight,
|
||||
"vulnerability": vulnerability
|
||||
}
|
||||
|
||||
# 2. Calculer ICS_moyen pour chaque minerai
|
||||
for mineral_id, mineral in data["minerals"].items():
|
||||
ics_values = list(mineral["ics_values"].values())
|
||||
ics_average = 0
|
||||
|
||||
if ics_values:
|
||||
ics_average = sum(ics_values) / len(ics_values)
|
||||
|
||||
ivc_value = mineral.get("ivc", 0)
|
||||
|
||||
# Déterminer couleurs et poids
|
||||
ics_color, ics_suffix = determine_threshold_color(ics_average, "ICS", thresholds)
|
||||
ivc_color, ivc_suffix = determine_threshold_color(ivc_value, "IVC", thresholds)
|
||||
|
||||
# Calculer poids combiné
|
||||
ics_weight = get_weight_for_color(ics_color)
|
||||
ivc_weight = get_weight_for_color(ivc_color)
|
||||
combined_weight = ics_weight * ivc_weight
|
||||
|
||||
# Déterminer vulnérabilité combinée
|
||||
if combined_weight in [6, 9]:
|
||||
vulnerability = "ÉLEVÉE à CRITIQUE"
|
||||
elif combined_weight in [3, 4]:
|
||||
vulnerability = "MOYENNE"
|
||||
else: # 1, 2
|
||||
vulnerability = "FAIBLE"
|
||||
|
||||
# Stocker résultats
|
||||
results["ics_ivc_combined"][mineral_id] = {
|
||||
"ics_average": ics_average,
|
||||
"ics_color": ics_color,
|
||||
"ics_suffix": ics_suffix,
|
||||
"ivc_value": ivc_value,
|
||||
"ivc_color": ivc_color,
|
||||
"ivc_suffix": ivc_suffix,
|
||||
"combined_weight": combined_weight,
|
||||
"vulnerability": vulnerability
|
||||
}
|
||||
|
||||
# 3. Identifier tous les chemins et leurs vulnérabilités
|
||||
for product_id, product in data["products"].items():
|
||||
for component_id in product["components"]:
|
||||
component = data["components"][component_id]
|
||||
|
||||
for mineral_id in component["minerals"]:
|
||||
mineral = data["minerals"][mineral_id]
|
||||
|
||||
# Collecter toutes les vulnérabilités dans ce chemin
|
||||
path_vulnerabilities = []
|
||||
|
||||
# Assemblage (si présent)
|
||||
assembly_id = product["assembly"]
|
||||
if assembly_id and assembly_id in results["ihh_isg_combined"]:
|
||||
path_vulnerabilities.append({
|
||||
"type": "assemblage",
|
||||
"vulnerability": results["ihh_isg_combined"][assembly_id]["vulnerability"],
|
||||
"operation_id": assembly_id
|
||||
})
|
||||
|
||||
# Fabrication (si présent)
|
||||
manufacturing_id = component["manufacturing"]
|
||||
if manufacturing_id and manufacturing_id in results["ihh_isg_combined"]:
|
||||
path_vulnerabilities.append({
|
||||
"type": "fabrication",
|
||||
"vulnerability": results["ihh_isg_combined"][manufacturing_id]["vulnerability"],
|
||||
"operation_id": manufacturing_id
|
||||
})
|
||||
|
||||
# Minerai (ICS+IVC)
|
||||
if mineral_id in results["ics_ivc_combined"]:
|
||||
path_vulnerabilities.append({
|
||||
"type": "minerai",
|
||||
"vulnerability": results["ics_ivc_combined"][mineral_id]["vulnerability"],
|
||||
"mineral_id": mineral_id
|
||||
})
|
||||
|
||||
# Extraction (si présent)
|
||||
extraction_id = mineral["extraction"]
|
||||
if extraction_id and extraction_id in results["ihh_isg_combined"]:
|
||||
path_vulnerabilities.append({
|
||||
"type": "extraction",
|
||||
"vulnerability": results["ihh_isg_combined"][extraction_id]["vulnerability"],
|
||||
"operation_id": extraction_id
|
||||
})
|
||||
|
||||
# Traitement (si présent)
|
||||
treatment_id = mineral["treatment"]
|
||||
if treatment_id and treatment_id in results["ihh_isg_combined"]:
|
||||
path_vulnerabilities.append({
|
||||
"type": "traitement",
|
||||
"vulnerability": results["ihh_isg_combined"][treatment_id]["vulnerability"],
|
||||
"operation_id": treatment_id
|
||||
})
|
||||
|
||||
# Classifier le chemin
|
||||
path_info = {
|
||||
"product": product_id,
|
||||
"component": component_id,
|
||||
"mineral": mineral_id,
|
||||
"vulnerabilities": path_vulnerabilities
|
||||
}
|
||||
|
||||
# Déterminer le niveau de risque du chemin
|
||||
critical_count = path_vulnerabilities.count({"vulnerability": "ÉLEVÉE à CRITIQUE"})
|
||||
medium_count = path_vulnerabilities.count({"vulnerability": "MOYENNE"})
|
||||
|
||||
if any(v["vulnerability"] == "ÉLEVÉE à CRITIQUE" for v in path_vulnerabilities):
|
||||
path_info["risk_level"] = "critique"
|
||||
elif medium_count >= 3:
|
||||
path_info["risk_level"] = "majeur"
|
||||
elif any(v["vulnerability"] == "MOYENNE" for v in path_vulnerabilities):
|
||||
path_info["risk_level"] = "moyen"
|
||||
else:
|
||||
path_info["risk_level"] = "faible"
|
||||
|
||||
results["chains"].append(path_info)
|
||||
|
||||
return results
|
||||
286
batch_ia/utils/ia.py
Normal file
286
batch_ia/utils/ia.py
Normal file
@ -0,0 +1,286 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import zipfile
|
||||
import streamlit as st
|
||||
|
||||
from nettoyer_pgpt import (
|
||||
delete_documents_by_criteria
|
||||
)
|
||||
|
||||
from utils.config import (
|
||||
TEMP_SECTIONS,
|
||||
session_uuid,
|
||||
API_URL, PROMPT_METHODOLOGIE
|
||||
)
|
||||
|
||||
def ingest_document(file_path: Path) -> bool:
|
||||
"""Ingère un document dans PrivateGPT"""
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
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_uuid,
|
||||
"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 generate_text(input_file, full_prompt, system_message, temperature = "0.3", use_context = True):
|
||||
"""Génère du texte avec l'API PrivateGPT"""
|
||||
try:
|
||||
|
||||
# Définir les paramètres de la requête
|
||||
payload = {
|
||||
"messages": [
|
||||
{"role": "system", "content": system_message},
|
||||
{"role": "user", "content": full_prompt}
|
||||
],
|
||||
"use_context": use_context, # Active la recherche RAG dans les documents ingérés
|
||||
"temperature": temperature, # 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)
|
||||
if input_file:
|
||||
try:
|
||||
# Vérifier si le filtre de contexte est supporté sans faire de requête supplémentaire
|
||||
liste_des_fichiers = list(TEMP_SECTIONS.glob(f"*{session_uuid}*.md"))
|
||||
filter_metadata = {
|
||||
"document_name": [input_file.name] + [f.name for f in liste_des_fichiers]
|
||||
}
|
||||
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 ia_analyse(file_names):
|
||||
for file in file_names:
|
||||
ingest_document(file)
|
||||
time.sleep(5)
|
||||
|
||||
reponse = {}
|
||||
for file in file_names:
|
||||
produit_final = re.search(r"chemins critiques (.+)\.md$", file.name).group(1)
|
||||
|
||||
# Préparer le prompt avec le contexte précédent si disponible et demandé
|
||||
full_prompt = f"""
|
||||
Rédigez une synthèse du fichier {file.name} dédiée au produit final '{produit_final}'.
|
||||
Cette synthèse, destinée spécifiquement au Directeur des Risques, membre du COMEX d'une grande entreprise utilisant ce produit, doit être claire et concise (environ 10 lignes).
|
||||
|
||||
En utilisant impérativement la méthodologie fournie, expliquez en termes simples mais précis, pourquoi et comment les vulnérabilités identifiées constituent un risque concret pour l'entreprise. Mentionnez clairement :
|
||||
|
||||
- Les composants spécifiques du produit '{produit_final}' concernés par ces vulnérabilités.
|
||||
- Les minerais précis responsables de ces vulnérabilités et leur rôle dans l’impact sur les composants.
|
||||
- Les points critiques exacts identifiés dans la chaîne d'approvisionnement (par exemple : faible substituabilité, forte concentration géographique, instabilité géopolitique, concurrence élevée entre secteurs industriels).
|
||||
- Identifier autant que faire se peut, les pays générant la forte concentration géographiques ou qui sont sujet à instabilité géopolitique, les secteurs en concurrence avec le numérique pour les minerais.
|
||||
|
||||
Respectez strictement les consignes suivantes :
|
||||
|
||||
- N'utilisez aucun acronyme ni valeur numérique ; uniquement leur équivalent textuel (ex : criticité de substituabilité, vulnérabilité élevée ou critique, etc.).
|
||||
- N'incluez à ce stade aucune préconisation ni recommandation.
|
||||
|
||||
Votre texte doit être parfaitement adapté à une compréhension rapide par des dirigeants d’entreprise.
|
||||
"""
|
||||
|
||||
|
||||
# Définir les paramètres de la requête
|
||||
system_message = f"""
|
||||
Vous êtes un assistant stratégique expert chargé de rédiger des synthèses destinées à des décideurs de très haut niveau (Directeurs des Risques, membres du COMEX, stratèges industriels). Vous analysez exclusivement les vulnérabilités systémiques affectant les produits numériques, à partir des données précises fournies dans le fichier {file.name}.
|
||||
|
||||
Votre analyse doit être rigoureuse, accessible, pertinente pour la prise de décision stratégique, et conforme à la méthodologie définie ci-dessous :
|
||||
|
||||
{PROMPT_METHODOLOGIE}
|
||||
|
||||
/no_think
|
||||
"""
|
||||
|
||||
reponse[produit_final] = f"\n**{produit_final}**\n\n" + generate_text(file, full_prompt, system_message).split("</think>")[-1].strip()
|
||||
# print(reponse[produit_final])
|
||||
|
||||
corps = "\n\n".join(reponse.values())
|
||||
print("Corps")
|
||||
|
||||
st.session_state["step"] = 2
|
||||
|
||||
full_prompt = corps + "\n\n" + PROMPT_METHODOLOGIE
|
||||
|
||||
system_message = """
|
||||
Vous êtes un expert en rédaction de rapports stratégiques destinés à un COMEX ou une Direction des Risques.
|
||||
|
||||
Votre mission est d'écrire une introduction professionnelle, claire et synthétique (maximum 7 lignes) à partir des éléments suivants :
|
||||
1. Un corps d’analyse décrivant les vulnérabilités identifiées pour un produit numérique.
|
||||
2. La méthodologie détaillée utilisée pour cette analyse (fourni en deuxième partie).
|
||||
|
||||
Votre introduction doit :
|
||||
- Présenter brièvement le sujet traité (vulnérabilités du produit final), quels sont les produits finaux et les minerais concernés.
|
||||
- Annoncer clairement le contenu et l'objectif de l'analyse présentée dans le corps.
|
||||
- Résumer succinctement les axes méthodologiques principaux (concentration géographique ou industrielle, stabilité géopolitique, criticité de substituabilité, concurrence intersectorielle des minerais).
|
||||
- Être facilement compréhensible par des décideurs de haut niveau (pas d'acronymes, ni chiffres ; uniquement des formulations textuelles).
|
||||
- Être fluide, agréable à lire, avec un ton sobre et professionnel.
|
||||
|
||||
Répondez uniquement avec l'introduction rédigée. Ne fournissez aucune autre explication complémentaire.
|
||||
|
||||
/no_think
|
||||
"""
|
||||
|
||||
|
||||
introduction = generate_text("", full_prompt, system_message).split("</think>")[-1].strip()
|
||||
print("Introduction")
|
||||
|
||||
st.session_state["step"] = 3
|
||||
|
||||
full_prompt = corps + "\n\n" + PROMPT_METHODOLOGIE
|
||||
|
||||
system_message = """
|
||||
Vous êtes un expert stratégique en gestion des risques liés à la chaîne de valeur numérique. Vous conseillez directement le COMEX et la Direction des Risques de grandes entreprises utilisatrices de produits numériques. Ces entreprises n'ont pour levier d’action que le choix de leurs fournisseurs ou l'allongement de la durée de vie de leur matériel.
|
||||
|
||||
À partir des vulnérabilités identifiées dans la première partie du prompt (corps d'analyse) et en tenant compte du contexte et de la méthodologie décrite en deuxième partie, rédigez un texte clair, structuré en deux parties distinctes :
|
||||
|
||||
1. **Préconisations stratégiques :**
|
||||
Proposez clairement des axes concrets pour limiter les risques identifiés dans l’analyse. Ces préconisations doivent impérativement être réalistes et directement actionnables par les dirigeants compte tenu de leurs leviers limités.
|
||||
|
||||
2. **Indicateurs de suivi :**
|
||||
Identifiez précisément les indicateurs pertinents à suivre pour évaluer régulièrement l’évolution de ces risques. Ces indicateurs doivent être inspirés directement des axes méthodologiques fournis (concentration géographique, stabilité géopolitique, substituabilité, concurrence intersectorielle) ou s’appuyer sur des bonnes pratiques reconnues.
|
||||
|
||||
Votre rédaction doit être fluide, concise, très professionnelle, et directement accessible à un COMEX. Évitez strictement toute explication complémentaire ou ajout superflu. Ne proposez que le texte demandé.
|
||||
"""
|
||||
|
||||
preconisations = generate_text("", full_prompt, system_message, "0.5").split("</think>")[-1].strip()
|
||||
print("Préconisations")
|
||||
|
||||
st.session_state["step"] = 4
|
||||
|
||||
full_prompt = corps + "\n\n" + preconisations
|
||||
system_message = """
|
||||
Vous êtes un expert stratégique spécialisé dans les risques liés à la chaîne de valeur du numérique. Vous conseillez directement le COMEX et la Direction des Risques de grandes entreprises dépendantes du numérique, dont les leviers d’action se limitent au choix des fournisseurs et à l’allongement de la durée d’utilisation du matériel.
|
||||
|
||||
À partir du résultat de l'analyse des vulnérabilités présenté en première partie du prompt (corps) et des préconisations stratégiques formulées en deuxième partie, rédigez une conclusion synthétique et percutante (environ 6 à 8 lignes maximum) afin de :
|
||||
|
||||
- Résumer clairement les principaux risques identifiés.
|
||||
- Souligner brièvement les axes prioritaires proposés pour agir concrètement.
|
||||
- Inviter de manière dynamique le COMEX à passer immédiatement à l'action.
|
||||
|
||||
Votre rédaction doit être fluide, professionnelle, claire et immédiatement exploitable par des dirigeants. Ne fournissez aucune explication supplémentaire. Ne répondez que par la conclusion demandée.
|
||||
|
||||
/no_think
|
||||
"""
|
||||
|
||||
conclusion = generate_text("", full_prompt, system_message, "0.7").split("</think>")[-1].strip()
|
||||
print("Conclusion")
|
||||
|
||||
st.session_state["step"] = 5
|
||||
|
||||
analyse = "# Rapport d'analyse\n\n" + \
|
||||
"\n\n## Introduction\n\n" + \
|
||||
introduction + \
|
||||
"\n\n## Analyse des produits finaux\n\n" + \
|
||||
corps + \
|
||||
"\n\n## Préconisations\n\n" + \
|
||||
preconisations + \
|
||||
"\n\n## Conclusion\n\n" + \
|
||||
conclusion + \
|
||||
"\n\n## Méthodologie\n\n" + \
|
||||
PROMPT_METHODOLOGIE
|
||||
|
||||
# fichier_a_reviser = Path(TEMPLATE_PATH.name.replace(".md", " - analyse à relire.md"))
|
||||
# write_report(analyse, TEMP_SECTIONS / fichier_a_reviser)
|
||||
# ingest_document(TEMP_SECTIONS / fichier_a_reviser)
|
||||
|
||||
full_prompt = """
|
||||
Suivre scrupuleusement les consignes.
|
||||
"""
|
||||
|
||||
system_message = f"""
|
||||
Vous êtes un réviseur professionnel expert en écriture stratégique, maîtrisant parfaitement la langue française et habitué à réviser des textes destinés à des dirigeants de haut niveau (COMEX).
|
||||
|
||||
Votre tâche unique est d'améliorer strictement la qualité rédactionnelle du texte suivant, sans modifier en aucune manière :
|
||||
- la structure existante (sections, titres, sous-titres),
|
||||
- l'ordre des paragraphes et des idées,
|
||||
- le sens précis du contenu original,
|
||||
- sans ajouter aucune information nouvelle.
|
||||
|
||||
Votre révision doit impérativement respecter les points suivants :
|
||||
- Éliminer toutes répétitions ou redondances et varier systématiquement les tournures entre les paragraphes.
|
||||
- Rendre chaque phrase claire, directe et concise. Si une phrase est trop longue, scindez-la clairement en plusieurs phrases courtes.
|
||||
- Structurer chaque paragraphe en 2 à 3 parties cohérentes, reliées entre elles par des termes logiques (coordination, implication, opposition, etc.) et séparées par des retours à la ligne.
|
||||
- Remplacer systématiquement les acronymes par ces expressions précises :
|
||||
- ICS → « capacité à substituer un minerai »
|
||||
- IHH → « concentration géographique ou industrielle »
|
||||
- ISG → « stabilité géopolitique »
|
||||
- IVC → « concurrence intersectorielle pour les minerais »
|
||||
|
||||
Votre texte final doit être parfaitement fluide, agréable à lire, adapté à un COMEX, avec un ton professionnel et sobre.
|
||||
|
||||
**Important : Ne répondez strictement que par le texte révisé ci-dessous, sans aucun commentaire ou explication supplémentaire.**
|
||||
|
||||
Voici le texte à réviser précisément :
|
||||
|
||||
{analyse}
|
||||
|
||||
/no_think
|
||||
"""
|
||||
revision = generate_text("", full_prompt, system_message, "0.1", False).split("</think>")[-1].strip()
|
||||
print("Relecture")
|
||||
|
||||
return revision
|
||||
|
||||
def supprimer_fichiers(session_uuid):
|
||||
try:
|
||||
delete_documents_by_criteria(session_uuid)
|
||||
for temp_file in TEMP_SECTIONS.glob(f"*{session_uuid}*.md"):
|
||||
temp_file.unlink()
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def generer_rapport_final(rapport, analyse, resultat):
|
||||
try:
|
||||
rapport = Path(rapport)
|
||||
analyse = Path(analyse)
|
||||
with zipfile.ZipFile(resultat, "w") as zipf:
|
||||
zipf.write(rapport, arcname=rapport.name)
|
||||
zipf.write(analyse, arcname=analyse.name)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du zip : {e}")
|
||||
return False
|
||||
768
batch_ia/utils/sections.py
Normal file
768
batch_ia/utils/sections.py
Normal file
@ -0,0 +1,768 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from .config import (
|
||||
CORPUS_DIR,
|
||||
TEMPLATE_PATH,
|
||||
determine_threshold_color
|
||||
)
|
||||
|
||||
from .files import (
|
||||
find_prefixed_directory,
|
||||
find_corpus_file,
|
||||
write_report,
|
||||
read_corpus_file
|
||||
)
|
||||
|
||||
from .sections_utils import (
|
||||
trouver_dossier_composant,
|
||||
extraire_sections_par_mot_cle
|
||||
)
|
||||
|
||||
def generate_introduction_section(data):
|
||||
"""
|
||||
Génère la section d'introduction du rapport.
|
||||
"""
|
||||
products = [p["label"] for p in data["products"].values()]
|
||||
components = [c["label"] for c in data["components"].values()]
|
||||
minerals = [m["label"] for m in data["minerals"].values()]
|
||||
|
||||
template = []
|
||||
template.append("## Introduction\n")
|
||||
template.append("Ce rapport analyse les vulnérabilités de la chaîne de fabrication du numérique pour :\n")
|
||||
|
||||
template.append("* les produits finaux : " + ", ".join(products))
|
||||
template.append("* les composants : " + ", ".join(components))
|
||||
template.append("* les minerais : " + ", ".join(minerals) + "\n")
|
||||
|
||||
return "\n".join(template)
|
||||
|
||||
def generate_methodology_section():
|
||||
"""
|
||||
Génère la section méthodologie du rapport.
|
||||
"""
|
||||
template = []
|
||||
template.append("## Méthodologie d'analyse des risques\n")
|
||||
template.append("### Indices et seuils\n")
|
||||
template.append("La méthode d'évaluation intègre 4 indices et leurs combinaisons pour identifier les chemins critiques.\n")
|
||||
|
||||
# IHH
|
||||
template.append("#### IHH (Herfindahl-Hirschmann) : concentration géographiques ou industrielle d'une opération\n")
|
||||
|
||||
# Essayer d'abord avec le chemin exact
|
||||
ihh_context_file = "Criticités/Fiche technique IHH/00-contexte-et-objectif.md"
|
||||
if os.path.exists(os.path.join(CORPUS_DIR, ihh_context_file)):
|
||||
template.append(read_corpus_file(ihh_context_file, remove_first_title=True))
|
||||
else:
|
||||
# Fallback à la recherche par motif
|
||||
ihh_context_file = find_corpus_file("contexte-et-objectif", "Criticités/Fiche technique IHH")
|
||||
if ihh_context_file:
|
||||
template.append(read_corpus_file(ihh_context_file, remove_first_title=True))
|
||||
|
||||
# Essayer d'abord avec le chemin exact
|
||||
ihh_calc_file = "Criticités/Fiche technique IHH/01-mode-de-calcul/_intro.md"
|
||||
if os.path.exists(os.path.join(CORPUS_DIR, ihh_calc_file)):
|
||||
template.append(read_corpus_file(ihh_calc_file, remove_first_title=True))
|
||||
else:
|
||||
# Fallback à la recherche par motif
|
||||
ihh_calc_file = find_corpus_file("mode-de-calcul/_intro", "Criticités/Fiche technique IHH")
|
||||
if ihh_calc_file:
|
||||
template.append(read_corpus_file(ihh_calc_file, remove_first_title=True))
|
||||
|
||||
template.append(" * Seuils : <15 = Vert (Faible), 15-25 = Orange (Modérée), >25 = Rouge (Élevée)\n")
|
||||
|
||||
# ISG
|
||||
template.append("#### ISG (Stabilité Géopolitique) : stabilité des pays\n")
|
||||
|
||||
# Essayer d'abord avec le chemin exact
|
||||
isg_context_file = "Criticités/Fiche technique ISG/00-contexte-et-objectif.md"
|
||||
if os.path.exists(os.path.join(CORPUS_DIR, isg_context_file)):
|
||||
template.append(read_corpus_file(isg_context_file, remove_first_title=True))
|
||||
else:
|
||||
# Fallback à la recherche par motif
|
||||
isg_context_file = find_corpus_file("contexte-et-objectif", "Criticités/Fiche technique ISG")
|
||||
if isg_context_file:
|
||||
template.append(read_corpus_file(isg_context_file, remove_first_title=True))
|
||||
|
||||
# Essayer d'abord avec le chemin exact
|
||||
isg_calc_file = "Criticités/Fiche technique ISG/01-mode-de-calcul/_intro.md"
|
||||
if os.path.exists(os.path.join(CORPUS_DIR, isg_calc_file)):
|
||||
template.append(read_corpus_file(isg_calc_file, remove_first_title=True))
|
||||
else:
|
||||
# Fallback à la recherche par motif
|
||||
isg_calc_file = find_corpus_file("mode-de-calcul/_intro", "Criticités/Fiche technique ISG")
|
||||
if isg_calc_file:
|
||||
template.append(read_corpus_file(isg_calc_file, remove_first_title=True))
|
||||
|
||||
template.append(" * Seuils : <40 = Vert (Stable), 40-60 = Orange, >60 = Rouge (Instable)\n")
|
||||
|
||||
# ICS
|
||||
template.append("#### ICS (Criticité de Substituabilité) : capacité à remplacer / substituer un élément\n")
|
||||
|
||||
# Essayer d'abord avec le chemin exact
|
||||
ics_context_file = "Criticités/Fiche technique ICS/00-contexte-et-objectif.md"
|
||||
if os.path.exists(os.path.join(CORPUS_DIR, ics_context_file)):
|
||||
template.append(read_corpus_file(ics_context_file, remove_first_title=True))
|
||||
else:
|
||||
# Fallback à la recherche par motif
|
||||
ics_context_file = find_corpus_file("contexte-et-objectif", "Criticités/Fiche technique ICS")
|
||||
if ics_context_file:
|
||||
template.append(read_corpus_file(ics_context_file, remove_first_title=True))
|
||||
|
||||
# Essayer d'abord avec le chemin exact
|
||||
ics_calc_file = "Criticités/Fiche technique ICS/01-mode-de-calcul/_intro.md"
|
||||
if os.path.exists(os.path.join(CORPUS_DIR, ics_calc_file)):
|
||||
template.append(read_corpus_file(ics_calc_file, remove_first_title=True))
|
||||
else:
|
||||
# Fallback à la recherche par motif
|
||||
ics_calc_file = find_corpus_file("mode-de-calcul/_intro", "Criticités/Fiche technique ICS")
|
||||
if ics_calc_file:
|
||||
template.append(read_corpus_file(ics_calc_file, remove_first_title=True))
|
||||
|
||||
template.append(" * Seuils : <0.3 = Vert (Facile), 0.3-0.6 = Orange (Moyenne), >0.6 = Rouge (Difficile)\n")
|
||||
|
||||
# IVC
|
||||
template.append("#### IVC (Vulnérabilité de Concurrence) : pression concurrentielle avec d'autres secteurs\n")
|
||||
|
||||
# Essayer d'abord avec le chemin exact
|
||||
ivc_context_file = "Criticités/Fiche technique IVC/00-contexte-et-objectif.md"
|
||||
if os.path.exists(os.path.join(CORPUS_DIR, ivc_context_file)):
|
||||
template.append(read_corpus_file(ivc_context_file, remove_first_title=True))
|
||||
else:
|
||||
# Fallback à la recherche par motif
|
||||
ivc_context_file = find_corpus_file("contexte-et-objectif", "Criticités/Fiche technique IVC")
|
||||
if ivc_context_file:
|
||||
template.append(read_corpus_file(ivc_context_file, remove_first_title=True))
|
||||
|
||||
# Essayer d'abord avec le chemin exact
|
||||
ivc_calc_file = "Criticités/Fiche technique IVC/01-mode-de-calcul/_intro.md"
|
||||
if os.path.exists(os.path.join(CORPUS_DIR, ivc_calc_file)):
|
||||
template.append(read_corpus_file(ivc_calc_file, remove_first_title=True))
|
||||
else:
|
||||
# Fallback à la recherche par motif
|
||||
ivc_calc_file = find_corpus_file("mode-de-calcul/_intro", "Criticités/Fiche technique IVC")
|
||||
if ivc_calc_file:
|
||||
template.append(read_corpus_file(ivc_calc_file, remove_first_title=True))
|
||||
|
||||
template.append(" * Seuils : <5 = Vert (Faible), 5-15 = Orange (Modérée), >15 = Rouge (Forte)\n")
|
||||
|
||||
# Combinaison des indices
|
||||
template.append("### Combinaison des indices\n")
|
||||
|
||||
# IHH et ISG
|
||||
template.append("**IHH et ISG**\n")
|
||||
template.append("Ces deux indices s'appliquent à toutes les opérations et se combinent dans l'évaluation du risque (niveau d'impact et probabilité de survenance) :\n")
|
||||
template.append("* l'IHH donne le niveau d'impact => une forte concentration implique un fort impact si le risque est avéré")
|
||||
template.append("* l'ISG donne la probabilité de survenance => plus les pays sont instables (et donc plus l'ISG est élevé) et plus la survenance du risque est élevée\n")
|
||||
|
||||
template.append("Pour évaluer le risque pour une opération, les ISG des pays sont pondérés par les parts de marché respectives pour donner un ISG combiné dont le calcul est :")
|
||||
template.append("ISG_combiné = (Somme des ISG des pays multipliée par leur part de marché) / Sommes de leur part de marché\n")
|
||||
|
||||
template.append("On établit alors une matrice (Vert = 1, Orange = 2, Rouge = 3) et en faisant le produit des poids de l'ISG combiné et de l'IHH\n")
|
||||
|
||||
template.append("| ISG combiné / IHH | Vert | Orange | Rouge |")
|
||||
template.append("| :-- | :-- | :-- | :-- |")
|
||||
template.append("| Vert | 1 | 2 | 3 |")
|
||||
template.append("| Orange | 2 | 4 | 6 |")
|
||||
template.append("| Rouge | 3 | 6 | 9 |\n")
|
||||
|
||||
template.append("Les vulnérabilités se classent en trois niveaux pour chaque opération :\n")
|
||||
template.append("* Vulnérabilité combinée élevée à critique : poids 6 et 9")
|
||||
template.append("* Vulnérabilité combinée moyenne : poids 3 et 4")
|
||||
template.append("* Vulnérabilité combinée faible : poids 1 et 2\n")
|
||||
|
||||
# ICS et IVC
|
||||
template.append("**ICS et IVC**\n")
|
||||
template.append("Ces deux indices se combinent dans l'évaluation du risque pour un minerai :\n")
|
||||
template.append("* l'ICS donne le niveau d'impact => une faible substituabilité (et donc un ICS élevé) implique un fort impact si le risque est avéré ; l'ICS est associé à la relation entre un composant et un minerai")
|
||||
template.append("* l'IVC donne la probabilité de l'impact => une forte concurrence intersectorielle (IVC élevé) implique une plus forte probabilité de survenance\n")
|
||||
|
||||
template.append("Par simplification, on intègre un ICS moyen d'un minerai comme étant la moyenne des ICS pour chacun des composants dans lesquels il intervient.\n")
|
||||
|
||||
template.append("On établit alors une matrice (Vert = 1, Orange = 2, Rouge = 3) et en faisant le produit des poids de l'ICS moyen et de l'IVC.\n")
|
||||
|
||||
template.append("| ICS_moyen / IVC | Vert | Orange | Rouge |")
|
||||
template.append("| :-- | :-- | :-- | :-- |")
|
||||
template.append("| Vert | 1 | 2 | 3 |")
|
||||
template.append("| Orange | 2 | 4 | 6 |")
|
||||
template.append("| Rouge | 3 | 6 | 9 |\n")
|
||||
|
||||
template.append("Les vulnérabilités se classent en trois niveaux pour chaque minerai :\n")
|
||||
template.append("* Vulnérabilité combinée élevée à critique : poids 6 et 9")
|
||||
template.append("* Vulnérabilité combinée moyenne : poids 3 et 4")
|
||||
template.append("* Vulnérabilité combinée faible : poids 1 et 2\n")
|
||||
|
||||
return "\n".join(template)
|
||||
|
||||
def generate_operations_section(data, results, config):
|
||||
"""
|
||||
Génère la section détaillant les opérations (assemblage, fabrication, extraction, traitement).
|
||||
"""
|
||||
# # print("DEBUG: Génération de la section des opérations")
|
||||
# # print(f"DEBUG: Nombre de produits: {len(data['products'])}")
|
||||
# # print(f"DEBUG: Nombre de composants: {len(data['components'])}")
|
||||
# # print(f"DEBUG: Nombre d'opérations: {len(data['operations'])}")
|
||||
|
||||
template = []
|
||||
template.append("## Détails des opérations\n")
|
||||
|
||||
# 1. Traiter les produits finaux (assemblage)
|
||||
for product_id, product in data["products"].items():
|
||||
# # print(f"DEBUG: Produit {product_id} ({product['label']}), assembly = {product['assembly']}")
|
||||
if product["assembly"]:
|
||||
template.append(f"### {product['label']} et Assemblage\n")
|
||||
|
||||
# Récupérer la présentation synthétique
|
||||
# product_slug = product['label'].lower().replace(' ', '-')
|
||||
sous_repertoire = f"{product['label']}"
|
||||
if product["level"] == 0:
|
||||
type = "Assemblage"
|
||||
else:
|
||||
type = "Connexe"
|
||||
sous_repertoire = trouver_dossier_composant(sous_repertoire, type, "Fiche assemblage ")
|
||||
product_slug = sous_repertoire.split(' ', 2)[2]
|
||||
presentation_file = find_corpus_file("présentation-synthétique", f"{type}/Fiche assemblage {product_slug}")
|
||||
if presentation_file:
|
||||
template.append(read_corpus_file(presentation_file, remove_first_title=True))
|
||||
template.append("")
|
||||
|
||||
# Récupérer les principaux assembleurs
|
||||
assembleurs_file = find_corpus_file("principaux-assembleurs", f"{type}/Fiche assemblage {product_slug}")
|
||||
if assembleurs_file:
|
||||
template.append(read_corpus_file(assembleurs_file, shift_titles=2))
|
||||
template.append("")
|
||||
|
||||
# ISG des pays impliqués
|
||||
assembly_id = product["assembly"]
|
||||
operation = data["operations"][assembly_id]
|
||||
|
||||
template.append("##### ISG des pays impliqués\n")
|
||||
template.append("| Pays | Part de marché | ISG | Criticité |")
|
||||
template.append("| :-- | :-- | :-- | :-- |")
|
||||
|
||||
isg_weighted_sum = 0
|
||||
total_share = 0
|
||||
|
||||
for country_id, share in operation["countries"].items():
|
||||
country = data["countries"][country_id]
|
||||
geo_country = country.get("geo_country")
|
||||
|
||||
if geo_country and geo_country in data["geo_countries"]:
|
||||
isg_value = data["geo_countries"][geo_country]["isg"]
|
||||
color, suffix = determine_threshold_color(isg_value, "ISG", config.get('thresholds'))
|
||||
template.append(f"| {country['label']} | {share}% | {isg_value} | {color} ({suffix}) |")
|
||||
|
||||
isg_weighted_sum += isg_value * share
|
||||
total_share += share
|
||||
|
||||
# Calculer ISG combiné
|
||||
if total_share > 0:
|
||||
isg_combined = isg_weighted_sum / total_share
|
||||
color, suffix = determine_threshold_color(isg_combined, "ISG", config.get('thresholds'))
|
||||
template.append(f"\n**ISG combiné: {isg_combined:.0f} - {color} ({suffix})**")
|
||||
|
||||
# IHH
|
||||
ihh_file = find_corpus_file("matrice-des-risques-liés-à-l-assemblage/indice-de-herfindahl-hirschmann", f"{type}/Fiche assemblage {product_slug}")
|
||||
if ihh_file:
|
||||
template.append(read_corpus_file(ihh_file, shift_titles=1))
|
||||
template.append("\n")
|
||||
|
||||
# Vulnérabilité combinée
|
||||
if assembly_id in results["ihh_isg_combined"]:
|
||||
combined = results["ihh_isg_combined"][assembly_id]
|
||||
template.append("#### Vulnérabilité combinée IHH-ISG\n")
|
||||
template.append(f"* IHH: {combined['ihh_value']} - {combined['ihh_color']} ({combined['ihh_suffix']})")
|
||||
template.append(f"* ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']} ({combined['isg_suffix']})")
|
||||
template.append(f"* Poids combiné: {combined['combined_weight']}")
|
||||
template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n")
|
||||
|
||||
# 2. Traiter les composants (fabrication)
|
||||
for component_id, component in data["components"].items():
|
||||
# # print(f"DEBUG: Composant {component_id} ({component['label']}), manufacturing = {component['manufacturing']}")
|
||||
if component["manufacturing"]:
|
||||
template.append(f"### {component['label']} et Fabrication\n")
|
||||
|
||||
# Récupérer la présentation synthétique
|
||||
# component_slug = component['label'].lower().replace(' ', '-')
|
||||
sous_repertoire = f"{component['label']}"
|
||||
sous_repertoire = trouver_dossier_composant(sous_repertoire, "Fabrication", "Fiche fabrication ")
|
||||
component_slug = sous_repertoire.split(' ', 2)[2]
|
||||
presentation_file = find_corpus_file("présentation-synthétique", f"Fabrication/Fiche fabrication {component_slug}")
|
||||
if presentation_file:
|
||||
template.append(read_corpus_file(presentation_file, remove_first_title=True))
|
||||
template.append("\n")
|
||||
|
||||
# Récupérer les principaux fabricants
|
||||
fabricants_file = find_corpus_file("principaux-fabricants", f"Fabrication/Fiche fabrication {component_slug}")
|
||||
if fabricants_file:
|
||||
template.append(read_corpus_file(fabricants_file, shift_titles=2))
|
||||
template.append("\n")
|
||||
|
||||
# ISG des pays impliqués
|
||||
manufacturing_id = component["manufacturing"]
|
||||
operation = data["operations"][manufacturing_id]
|
||||
|
||||
template.append("##### ISG des pays impliqués\n")
|
||||
template.append("| Pays | Part de marché | ISG | Criticité |")
|
||||
template.append("| :-- | :-- | :-- | :-- |")
|
||||
|
||||
isg_weighted_sum = 0
|
||||
total_share = 0
|
||||
|
||||
for country_id, share in operation["countries"].items():
|
||||
country = data["countries"][country_id]
|
||||
geo_country = country.get("geo_country")
|
||||
|
||||
if geo_country and geo_country in data["geo_countries"]:
|
||||
isg_value = data["geo_countries"][geo_country]["isg"]
|
||||
color, suffix = determine_threshold_color(isg_value, "ISG", config.get('thresholds'))
|
||||
template.append(f"| {country['label']} | {share}% | {isg_value} | {color} ({suffix}) |")
|
||||
|
||||
isg_weighted_sum += isg_value * share
|
||||
total_share += share
|
||||
|
||||
# Calculer ISG combiné
|
||||
if total_share > 0:
|
||||
isg_combined = isg_weighted_sum / total_share
|
||||
color, suffix = determine_threshold_color(isg_combined, "ISG", config.get('thresholds'))
|
||||
template.append(f"\n**ISG combiné: {isg_combined:.0f} - {color} ({suffix})**\n\n")
|
||||
|
||||
# IHH
|
||||
ihh_file = find_corpus_file("matrice-des-risques-liés-à-la-fabrication/indice-de-herfindahl-hirschmann", f"Fabrication/Fiche fabrication {component_slug}")
|
||||
if ihh_file:
|
||||
template.append(read_corpus_file(ihh_file, shift_titles=1))
|
||||
template.append("\n")
|
||||
|
||||
# Vulnérabilité combinée
|
||||
if manufacturing_id in results["ihh_isg_combined"]:
|
||||
combined = results["ihh_isg_combined"][manufacturing_id]
|
||||
template.append("#### Vulnérabilité combinée IHH-ISG\n")
|
||||
template.append(f"* IHH: {combined['ihh_value']} - {combined['ihh_color']} ({combined['ihh_suffix']})")
|
||||
template.append(f"* ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']} ({combined['isg_suffix']})")
|
||||
template.append(f"* Poids combiné: {combined['combined_weight']}")
|
||||
template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n")
|
||||
|
||||
# 3. Traiter les minerais (détaillés dans une section séparée)
|
||||
|
||||
result = "\n".join(template)
|
||||
# # print(f"DEBUG: Fin de génération de la section des opérations. Taille: {len(result)} caractères")
|
||||
if len(result) <= 30: # Juste le titre de section
|
||||
# # print("DEBUG: ALERTE - La section des opérations est vide ou presque vide!")
|
||||
# Ajout d'une section de débogage dans le rapport
|
||||
template.append("### DÉBOGAGE - Opérations manquantes\n")
|
||||
template.append("Aucune opération d'assemblage ou de fabrication n'a été trouvée dans les données.\n")
|
||||
template.append("Informations disponibles:\n")
|
||||
template.append(f"* Nombre de produits: {len(data['products'])}\n")
|
||||
template.append(f"* Nombre de composants: {len(data['components'])}\n")
|
||||
template.append(f"* Nombre d'opérations: {len(data['operations'])}\n")
|
||||
template.append("\nDétail des produits et de leurs opérations d'assemblage:\n")
|
||||
for pid, p in data["products"].items():
|
||||
template.append(f"* {p['label']}: {'Assemblage: ' + str(p['assembly']) if p['assembly'] else 'Pas d\'assemblage'}\n")
|
||||
template.append("\nDétail des composants et de leurs opérations de fabrication:\n")
|
||||
for cid, c in data["components"].items():
|
||||
template.append(f"* {c['label']}: {'Fabrication: ' + str(c['manufacturing']) if c['manufacturing'] else 'Pas de fabrication'}\n")
|
||||
result = "\n".join(template)
|
||||
|
||||
return result
|
||||
|
||||
def generate_minerals_section(data, results, config):
|
||||
"""
|
||||
Génère la section détaillant les minerais et leurs opérations d'extraction et traitement.
|
||||
"""
|
||||
template = []
|
||||
template.append("## Détails des minerais\n")
|
||||
|
||||
for mineral_id, mineral in data["minerals"].items():
|
||||
mineral_slug = mineral['label'].lower().replace(' ', '-')
|
||||
fiche_dir = f"{CORPUS_DIR}/Minerai/Fiche minerai {mineral_slug}"
|
||||
if not os.path.exists(fiche_dir):
|
||||
continue
|
||||
|
||||
template.append(f"---\n\n### {mineral['label']}\n")
|
||||
|
||||
# Récupérer la présentation synthétique
|
||||
presentation_file = find_corpus_file("présentation-synthétique", f"Minerai/Fiche minerai {mineral_slug}")
|
||||
if presentation_file:
|
||||
template.append(read_corpus_file(presentation_file, remove_first_title=True))
|
||||
template.append("\n")
|
||||
|
||||
# ICS
|
||||
template.append("#### ICS\n")
|
||||
|
||||
ics_intro_file = find_corpus_file("risque-de-substituabilité/_intro", f"Minerai/Fiche minerai {mineral_slug}")
|
||||
if ics_intro_file:
|
||||
template.append(read_corpus_file(ics_intro_file, remove_first_title=True))
|
||||
template.append("\n")
|
||||
|
||||
# Calcul de l'ICS moyen
|
||||
ics_values = list(mineral["ics_values"].values())
|
||||
if ics_values:
|
||||
ics_average = sum(ics_values) / len(ics_values)
|
||||
color, suffix = determine_threshold_color(ics_average, "ICS", config.get('thresholds'))
|
||||
|
||||
template.append("##### Valeurs d'ICS par composant concerné\n")
|
||||
template.append("| Composant | ICS | Criticité |")
|
||||
template.append("| :-- | :-- | :-- |")
|
||||
|
||||
for comp_id, ics_value in mineral["ics_values"].items():
|
||||
comp_name = data["components"][comp_id]["label"]
|
||||
comp_color, comp_suffix = determine_threshold_color(ics_value, "ICS", config.get('thresholds'))
|
||||
template.append(f"| {comp_name} | {ics_value:.2f} | {comp_color} ({comp_suffix}) |")
|
||||
|
||||
template.append(f"\n**ICS moyen : {ics_average:.2f} - {color} ({suffix})**\n")
|
||||
|
||||
# IVC
|
||||
template.append("#### IVC\n\n")
|
||||
|
||||
# Valeur IVC
|
||||
ivc_value = mineral.get("ivc", 0)
|
||||
color, suffix = determine_threshold_color(ivc_value, "IVC", config.get('thresholds'))
|
||||
template.append(f"**IVC: {ivc_value} - {color} ({suffix})**\n")
|
||||
|
||||
# Récupérer toutes les sections de vulnérabilité de concurrence
|
||||
ivc_sections = []
|
||||
ivc_dir = find_prefixed_directory("vulnérabilité-de-concurrence", f"Minerai/Fiche minerai {mineral_slug}")
|
||||
corpus_path = os.path.join(CORPUS_DIR, ivc_dir) if os.path.exists(os.path.join(CORPUS_DIR, ivc_dir)) else None
|
||||
if corpus_path:
|
||||
for file in sorted(os.listdir(corpus_path)):
|
||||
if file.endswith('.md') and "_intro.md" not in file and "sources" not in file:
|
||||
ivc_sections.append(os.path.join(ivc_dir, file))
|
||||
|
||||
# Inclure chaque section IVC
|
||||
for section_file in ivc_sections:
|
||||
content = read_corpus_file(section_file, remove_first_title=False)
|
||||
# Nettoyer les balises des fichiers IVC
|
||||
content = re.sub(r'```.*?```', '', content, flags=re.DOTALL)
|
||||
|
||||
# Mettre le titre en italique s'il commence par un # (format Markdown pour titre)
|
||||
if content and '\n' in content:
|
||||
first_line, rest = content.split('\n', 1)
|
||||
if first_line.strip().startswith('#'):
|
||||
# Extraire le texte du titre sans les # et les espaces
|
||||
title_text = first_line.strip().lstrip('#').strip()
|
||||
content = f"\n*{title_text}*\n\n{rest.strip()}\n"
|
||||
|
||||
template.append(content)
|
||||
|
||||
# ICS et IVC combinés
|
||||
if mineral_id in results["ics_ivc_combined"]:
|
||||
combined = results["ics_ivc_combined"][mineral_id]
|
||||
template.append("\n#### Vulnérabilité combinée ICS-IVC\n")
|
||||
template.append(f"* ICS moyen: {combined['ics_average']:.2f} - {combined['ics_color']} ({combined['ics_suffix']})")
|
||||
template.append(f"* IVC: {combined['ivc_value']} - {combined['ivc_color']} ({combined['ivc_suffix']})")
|
||||
template.append(f"* Poids combiné: {combined['combined_weight']}")
|
||||
template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n")
|
||||
|
||||
# Extraction
|
||||
if mineral["extraction"]:
|
||||
template.append("#### Extraction\n")
|
||||
|
||||
# Récupérer les principaux producteurs
|
||||
producers_file = find_corpus_file("principaux-producteurs-extraction", f"Minerai/Fiche minerai {mineral_slug}")
|
||||
if producers_file:
|
||||
template.append(read_corpus_file(producers_file, remove_first_title=True))
|
||||
template.append("\n")
|
||||
|
||||
# ISG des pays impliqués
|
||||
extraction_id = mineral["extraction"]
|
||||
operation = data["operations"][extraction_id]
|
||||
|
||||
template.append("##### ISG des pays impliqués\n")
|
||||
template.append("| Pays | Part de marché | ISG | Criticité |")
|
||||
template.append("| :-- | :-- | :-- | :-- |")
|
||||
|
||||
isg_weighted_sum = 0
|
||||
total_share = 0
|
||||
|
||||
for country_id, share in operation["countries"].items():
|
||||
country = data["countries"][country_id]
|
||||
geo_country = country.get("geo_country")
|
||||
|
||||
if geo_country and geo_country in data["geo_countries"]:
|
||||
isg_value = data["geo_countries"][geo_country]["isg"]
|
||||
color, suffix = determine_threshold_color(isg_value, "ISG", config.get('thresholds'))
|
||||
template.append(f"| {country['label']} | {share}% | {isg_value} | {color} ({suffix}) |")
|
||||
|
||||
isg_weighted_sum += isg_value * share
|
||||
total_share += share
|
||||
|
||||
# Calculer ISG combiné
|
||||
if total_share > 0:
|
||||
isg_combined = isg_weighted_sum / total_share
|
||||
color, suffix = determine_threshold_color(isg_combined, "ISG", config.get('thresholds'))
|
||||
template.append(f"\n**ISG combiné: {isg_combined:.0f} - {color} ({suffix})**\n")
|
||||
|
||||
# IHH extraction
|
||||
ihh_file = find_corpus_file("matrice-des-risques/indice-de-herfindahl-hirschmann-extraction", f"Minerai/Fiche minerai {mineral_slug}")
|
||||
if ihh_file:
|
||||
template.append(read_corpus_file(ihh_file, shift_titles=1))
|
||||
template.append("\n")
|
||||
|
||||
# Vulnérabilité combinée
|
||||
if extraction_id in results["ihh_isg_combined"]:
|
||||
combined = results["ihh_isg_combined"][extraction_id]
|
||||
template.append("##### Vulnérabilité combinée IHH-ISG pour l'extraction\n")
|
||||
template.append(f"* IHH: {combined['ihh_value']} - {combined['ihh_color']} ({combined['ihh_suffix']})")
|
||||
template.append(f"* ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']} ({combined['isg_suffix']})")
|
||||
template.append(f"* Poids combiné: {combined['combined_weight']}")
|
||||
template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n")
|
||||
|
||||
# Traitement
|
||||
if mineral["treatment"]:
|
||||
template.append("#### Traitement\n")
|
||||
|
||||
# Récupérer les principaux producteurs
|
||||
producers_file = find_corpus_file("principaux-producteurs-traitement", f"Minerai/Fiche minerai {mineral_slug}")
|
||||
if producers_file:
|
||||
template.append(read_corpus_file(producers_file, remove_first_title=True))
|
||||
template.append("\n")
|
||||
|
||||
# ISG des pays impliqués
|
||||
treatment_id = mineral["treatment"]
|
||||
operation = data["operations"][treatment_id]
|
||||
|
||||
template.append("##### ISG des pays impliqués\n")
|
||||
template.append("| Pays | Part de marché | ISG | Criticité |")
|
||||
template.append("| :-- | :-- | :-- | :-- |")
|
||||
|
||||
isg_weighted_sum = 0
|
||||
total_share = 0
|
||||
|
||||
for country_id, share in operation["countries"].items():
|
||||
country = data["countries"][country_id]
|
||||
geo_country = country.get("geo_country")
|
||||
|
||||
if geo_country and geo_country in data["geo_countries"]:
|
||||
isg_value = data["geo_countries"][geo_country]["isg"]
|
||||
color, suffix = determine_threshold_color(isg_value, "ISG", config.get('thresholds'))
|
||||
template.append(f"| {country['label']} | {share}% | {isg_value} | {color} ({suffix}) |")
|
||||
|
||||
isg_weighted_sum += isg_value * share
|
||||
total_share += share
|
||||
|
||||
# Calculer ISG combiné
|
||||
if total_share > 0:
|
||||
isg_combined = isg_weighted_sum / total_share
|
||||
color, suffix = determine_threshold_color(isg_combined, "ISG", config.get('thresholds'))
|
||||
template.append(f"\n**ISG combiné: {isg_combined:.0f} - {color} ({suffix})**\n")
|
||||
|
||||
# IHH traitement
|
||||
ihh_file = find_corpus_file("matrice-des-risques/indice-de-herfindahl-hirschmann-traitement", f"Minerai/Fiche minerai {mineral_slug}")
|
||||
if ihh_file:
|
||||
template.append(read_corpus_file(ihh_file, shift_titles=1))
|
||||
template.append("\n")
|
||||
|
||||
# Vulnérabilité combinée
|
||||
if treatment_id in results["ihh_isg_combined"]:
|
||||
combined = results["ihh_isg_combined"][treatment_id]
|
||||
template.append("##### Vulnérabilité combinée IHH-ISG pour le traitement\n")
|
||||
template.append(f"* IHH: {combined['ihh_value']} - {combined['ihh_color']} ({combined['ihh_suffix']})")
|
||||
template.append(f"* ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']} ({combined['isg_suffix']})")
|
||||
template.append(f"* Poids combiné: {combined['combined_weight']}")
|
||||
template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n")
|
||||
|
||||
return "\n".join(template)
|
||||
|
||||
def generate_critical_paths_section(data, results):
|
||||
"""
|
||||
Génère la section des chemins critiques.
|
||||
"""
|
||||
template = []
|
||||
template.append("## Chemins critiques\n")
|
||||
|
||||
# Récupérer les chaînes par niveau de risque
|
||||
critical_chains = []
|
||||
major_chains = []
|
||||
medium_chains = []
|
||||
|
||||
for chain in results["chains"]:
|
||||
if chain["risk_level"] == "critique":
|
||||
critical_chains.append(chain)
|
||||
elif chain["risk_level"] == "majeur":
|
||||
major_chains.append(chain)
|
||||
elif chain["risk_level"] == "moyen":
|
||||
medium_chains.append(chain)
|
||||
|
||||
# 1. Chaînes critiques
|
||||
template.append("### Chaînes avec risque critique\n")
|
||||
template.append("*Ces chaînes comprennent au moins une vulnérabilité combinée élevée à critique*\n")
|
||||
|
||||
if critical_chains:
|
||||
for chain in critical_chains:
|
||||
product_name = data["products"][chain["product"]]["label"]
|
||||
component_name = data["components"][chain["component"]]["label"]
|
||||
mineral_name = data["minerals"][chain["mineral"]]["label"]
|
||||
|
||||
template.append(f"#### {product_name} → {component_name} → {mineral_name}\n")
|
||||
|
||||
# Vulnérabilités
|
||||
template.append("**Vulnérabilités identifiées:**\n")
|
||||
for vuln in chain["vulnerabilities"]:
|
||||
vuln_type = vuln["type"].capitalize()
|
||||
vuln_level = vuln["vulnerability"]
|
||||
|
||||
if vuln_type == "Minerai":
|
||||
mineral_id = vuln["mineral_id"]
|
||||
template.append(f"* {vuln_type} ({mineral_name}): {vuln_level}")
|
||||
if mineral_id in results["ics_ivc_combined"]:
|
||||
combined = results["ics_ivc_combined"][mineral_id]
|
||||
template.append(f" * ICS moyen: {combined['ics_average']:.2f} - {combined['ics_color']}")
|
||||
template.append(f" * IVC: {combined['ivc_value']} - {combined['ivc_color']}")
|
||||
else:
|
||||
op_id = vuln["operation_id"]
|
||||
op_label = data["operations"][op_id]["label"]
|
||||
template.append(f"* {vuln_type} ({op_label}): {vuln_level}")
|
||||
if op_id in results["ihh_isg_combined"]:
|
||||
combined = results["ihh_isg_combined"][op_id]
|
||||
template.append(f" * IHH: {combined['ihh_value']} - {combined['ihh_color']}")
|
||||
template.append(f" * ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']}")
|
||||
|
||||
template.append("\n")
|
||||
else:
|
||||
template.append("Aucune chaîne à risque critique identifiée.\n")
|
||||
|
||||
# 2. Chaînes majeures
|
||||
template.append("### Chaînes avec risque majeur\n")
|
||||
template.append("*Ces chaînes comprennent au moins trois vulnérabilités combinées moyennes*\n")
|
||||
|
||||
if major_chains:
|
||||
for chain in major_chains:
|
||||
product_name = data["products"][chain["product"]]["label"]
|
||||
component_name = data["components"][chain["component"]]["label"]
|
||||
mineral_name = data["minerals"][chain["mineral"]]["label"]
|
||||
|
||||
template.append(f"#### {product_name} → {component_name} → {mineral_name}\n")
|
||||
|
||||
# Vulnérabilités
|
||||
template.append("**Vulnérabilités identifiées:**\n")
|
||||
for vuln in chain["vulnerabilities"]:
|
||||
vuln_type = vuln["type"].capitalize()
|
||||
vuln_level = vuln["vulnerability"]
|
||||
|
||||
if vuln_type == "Minerai":
|
||||
mineral_id = vuln["mineral_id"]
|
||||
template.append(f"* {vuln_type} ({mineral_name}): {vuln_level}\n")
|
||||
if mineral_id in results["ics_ivc_combined"]:
|
||||
combined = results["ics_ivc_combined"][mineral_id]
|
||||
template.append(f" * ICS moyen: {combined['ics_average']:.2f} - {combined['ics_color']}\n")
|
||||
template.append(f" * IVC: {combined['ivc_value']} - {combined['ivc_color']}\n")
|
||||
else:
|
||||
op_id = vuln["operation_id"]
|
||||
op_label = data["operations"][op_id]["label"]
|
||||
template.append(f"* {vuln_type} ({op_label}): {vuln_level}\n")
|
||||
if op_id in results["ihh_isg_combined"]:
|
||||
combined = results["ihh_isg_combined"][op_id]
|
||||
template.append(f" * IHH: {combined['ihh_value']} - {combined['ihh_color']}\n")
|
||||
template.append(f" * ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']}\n")
|
||||
|
||||
template.append("\n")
|
||||
else:
|
||||
template.append("Aucune chaîne à risque majeur identifiée.\n")
|
||||
|
||||
# 3. Chaînes moyennes
|
||||
template.append("### Chaînes avec risque moyen\n")
|
||||
template.append("*Ces chaînes comprennent au moins une vulnérabilité combinée moyenne*\n")
|
||||
|
||||
if medium_chains:
|
||||
for chain in medium_chains:
|
||||
product_name = data["products"][chain["product"]]["label"]
|
||||
component_name = data["components"][chain["component"]]["label"]
|
||||
mineral_name = data["minerals"][chain["mineral"]]["label"]
|
||||
|
||||
template.append(f"#### {product_name} → {component_name} → {mineral_name}\n")
|
||||
|
||||
# Vulnérabilités
|
||||
template.append("**Vulnérabilités identifiées:**\n")
|
||||
for vuln in chain["vulnerabilities"]:
|
||||
vuln_type = vuln["type"].capitalize()
|
||||
vuln_level = vuln["vulnerability"]
|
||||
|
||||
if vuln_type == "Minerai":
|
||||
mineral_id = vuln["mineral_id"]
|
||||
template.append(f"* {vuln_type} ({mineral_name}): {vuln_level}\n")
|
||||
if mineral_id in results["ics_ivc_combined"]:
|
||||
combined = results["ics_ivc_combined"][mineral_id]
|
||||
template.append(f" * ICS moyen: {combined['ics_average']:.2f} - {combined['ics_color']}\n")
|
||||
template.append(f" * IVC: {combined['ivc_value']} - {combined['ivc_color']}\n")
|
||||
else:
|
||||
op_id = vuln["operation_id"]
|
||||
op_label = data["operations"][op_id]["label"]
|
||||
template.append(f"* {vuln_type} ({op_label}): {vuln_level}\n")
|
||||
if op_id in results["ihh_isg_combined"]:
|
||||
combined = results["ihh_isg_combined"][op_id]
|
||||
template.append(f" * IHH: {combined['ihh_value']} - {combined['ihh_color']}\n")
|
||||
template.append(f" * ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']}\n")
|
||||
|
||||
template.append("\n")
|
||||
else:
|
||||
template.append("Aucune chaîne à risque moyen identifiée.\n")
|
||||
|
||||
return "\n".join(template)
|
||||
|
||||
def slugify(text):
|
||||
return re.sub(r'\W+', '-', text.strip()).strip('-').lower()
|
||||
|
||||
def generate_report(data, results, config):
|
||||
"""
|
||||
Génère le rapport complet structuré selon les spécifications.
|
||||
"""
|
||||
# Titre principal
|
||||
report_titre = ["# Évaluation des vulnérabilités critiques\n"]
|
||||
|
||||
# Section d'introduction
|
||||
report_introduction = generate_introduction_section(data)
|
||||
# report.append(generate_introduction_section(data))
|
||||
|
||||
# Section méthodologie
|
||||
report_methodologie = generate_methodology_section()
|
||||
# report.append(generate_methodology_section())
|
||||
|
||||
# Section détails des opérations
|
||||
report_operations = generate_operations_section(data, results, config)
|
||||
# report.append(generate_operations_section(data, results, config))
|
||||
|
||||
# Section détails des minerais
|
||||
report_minerals = generate_minerals_section(data, results, config)
|
||||
# report.append(generate_minerals_section(data, results, config))
|
||||
|
||||
# Section chemins critiques
|
||||
report_critical_paths = generate_critical_paths_section(data, results)
|
||||
|
||||
suffixe = " - chemins critiques"
|
||||
fichier = TEMPLATE_PATH.name.replace(".md", f"{suffixe}.md")
|
||||
fichier_path = TEMPLATE_PATH.parent / fichier
|
||||
# Élever les titres Markdown dans report_critical_paths
|
||||
report_critical_paths = re.sub(r'^(#{2,})', lambda m: '#' * (len(m.group(1)) - 1), report_critical_paths, flags=re.MULTILINE)
|
||||
write_report(report_critical_paths, fichier_path)
|
||||
|
||||
# Récupérer les sections critiques décomposées par mot-clé
|
||||
chemins_critiques_sections = extraire_sections_par_mot_cle(fichier_path)
|
||||
|
||||
file_names = []
|
||||
|
||||
# Pour chaque mot-clé, écrire un fichier individuel
|
||||
for mot_cle, contenu in chemins_critiques_sections.items():
|
||||
print(mot_cle)
|
||||
mot_cle_slug = slugify(mot_cle)
|
||||
suffixe = f" - chemins critiques {mot_cle_slug}"
|
||||
fichier_personnalise = TEMPLATE_PATH.with_name(
|
||||
TEMPLATE_PATH.name.replace(".md", f"{suffixe}.md")
|
||||
)
|
||||
# Ajouter du texte au début du contenu
|
||||
introduction = f"# Détail des chemins critiques pour : {mot_cle}\n\n"
|
||||
contenu = introduction + contenu
|
||||
write_report(contenu, fichier_personnalise)
|
||||
file_names.append(fichier_personnalise)
|
||||
# report.append(generate_critical_paths_section(data, results))
|
||||
|
||||
# Ordre de composition final
|
||||
report = (
|
||||
report_titre +
|
||||
[report_introduction] +
|
||||
[report_critical_paths] +
|
||||
[report_operations] +
|
||||
[report_minerals] +
|
||||
[report_methodologie]
|
||||
)
|
||||
|
||||
return "\n".join(report), file_names
|
||||
92
batch_ia/utils/sections_utils.py
Normal file
92
batch_ia/utils/sections_utils.py
Normal file
@ -0,0 +1,92 @@
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
|
||||
from .config import (
|
||||
CORPUS_DIR
|
||||
)
|
||||
|
||||
def composant_match(nom_composant, nom_dossier):
|
||||
"""
|
||||
Vérifie si le nom du composant correspond approximativement à un nom de dossier (lettres et chiffres dans le même ordre).
|
||||
"""
|
||||
def clean(s):
|
||||
return ''.join(c.lower() for c in s if c.isalnum())
|
||||
|
||||
cleaned_comp = clean(nom_composant)
|
||||
cleaned_dir = clean(nom_dossier)
|
||||
|
||||
# Vérifie que chaque caractère de cleaned_comp est présent dans cleaned_dir dans le bon ordre
|
||||
it = iter(cleaned_dir)
|
||||
return all(c in it for c in cleaned_comp)
|
||||
|
||||
def trouver_dossier_composant(nom_composant, base_path, prefixe):
|
||||
"""
|
||||
Parcourt les sous-répertoires de base_path et retourne celui qui correspond au composant.
|
||||
"""
|
||||
search_path = os.path.join(CORPUS_DIR, base_path)
|
||||
if not os.path.exists(search_path):
|
||||
return None
|
||||
|
||||
for d in os.listdir(search_path):
|
||||
if os.path.isdir(os.path.join(search_path, d)):
|
||||
if composant_match(f"{prefixe}{nom_composant}", d):
|
||||
return os.path.join(base_path, d)
|
||||
return None
|
||||
|
||||
def extraire_sections_par_mot_cle(fichier_markdown: Path) -> dict:
|
||||
"""
|
||||
Extrait les sections de niveau 3 uniquement dans la section
|
||||
'## Chaînes avec risque critique' du fichier Markdown,
|
||||
et les regroupe par mot-clé (ce qui se trouve entre '### ' et ' →').
|
||||
Réduit chaque titre d’un niveau (#).
|
||||
"""
|
||||
with fichier_markdown.open(encoding="utf-8") as f:
|
||||
contenu = f.read()
|
||||
|
||||
# Extraire uniquement la section '## Chaînes avec risque critique'
|
||||
match_section = re.search(
|
||||
r"## Chaînes avec risque critique(.*?)(?=\n## |\Z)", contenu, re.DOTALL
|
||||
)
|
||||
if not match_section:
|
||||
return {}
|
||||
|
||||
section_critique = match_section.group(1)
|
||||
|
||||
# Extraire les mots-clés entre '### ' et ' →'
|
||||
mots_cles = set(re.findall(r"^### (.+?) →", section_critique, re.MULTILINE))
|
||||
|
||||
# Extraire tous les blocs de niveau 3 dans cette section uniquement
|
||||
blocs_sections = re.findall(r"(### .+?)(?=\n### |\n## |\Z)", section_critique, re.DOTALL)
|
||||
|
||||
# Regrouper les blocs par mot-clé
|
||||
regroupement = defaultdict(list)
|
||||
for bloc in blocs_sections:
|
||||
match = re.match(r"### (.+?) →", bloc)
|
||||
if match:
|
||||
mot = match.group(1)
|
||||
if mot in mots_cles:
|
||||
# Réduction du niveau des titres
|
||||
bloc_modifie = re.sub(r"^###", "##", bloc, flags=re.MULTILINE)
|
||||
bloc_modifie = re.sub(r"^###", "##", bloc_modifie, flags=re.MULTILINE)
|
||||
regroupement[mot].append(bloc_modifie)
|
||||
|
||||
return {mot: "\n\n".join(blocs) for mot, blocs in regroupement.items()}
|
||||
|
||||
def nettoyer_texte_fr(texte: str) -> str:
|
||||
# Apostrophes droites -> typographiques
|
||||
texte = texte.replace("'", "’")
|
||||
# Guillemets droits -> guillemets français (avec espace fine insécable)
|
||||
texte = re.sub(r'"(.*?)"', r'« \1 »', texte)
|
||||
# Espaces fines insécables avant : ; ! ?
|
||||
texte = re.sub(r' (?=[:;!?])', '\u202F', texte)
|
||||
# Unités : espace insécable entre chiffre et unité
|
||||
texte = re.sub(r'(\d) (?=\w+)', lambda m: f"{m.group(1)}\u202F", texte)
|
||||
# Suppression des doubles espaces
|
||||
texte = re.sub(r' {2,}', ' ', texte)
|
||||
# Remplacement optionnel des tirets simples (optionnel)
|
||||
texte = texte.replace(" - ", " – ")
|
||||
# Nettoyage ponctuation multiple accidentelle
|
||||
texte = re.sub(r'\s+([.,;!?])', r'\1', texte)
|
||||
return texte
|
||||
@ -10,11 +10,12 @@ Le module components comprend plusieurs fichiers clés :
|
||||
- **header.py** : Composant d'en-tête unifié pour toutes les pages
|
||||
- **footer.py** : Pied de page standardisé incluant les mentions légales et informations de contact
|
||||
- **fiches.py** : Composants spécifiques à l'affichage et à la manipulation des fiches
|
||||
- **connexion.py** : Composants spécifiques à la gestion de la connexion / déconnexion sur la base d'un token Gitea
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### Barre latérale (sidebar.py)
|
||||
- Menu de navigation principal entre les différentes sections
|
||||
- Menu de navigation prin>>>>>>>>>>>>> cipal entre les différentes sections
|
||||
- Options de configuration et de personnalisation
|
||||
- Affichage des informations sur l'impact environnemental
|
||||
- Gestion du thème (clair/sombre)
|
||||
@ -45,4 +46,4 @@ afficher_menu()
|
||||
afficher_pied_de_page()
|
||||
```
|
||||
|
||||
Cette approche modulaire permet de maintenir une interface cohérente tout en facilitant les mises à jour de l'interface utilisateur.
|
||||
Cette approche modulaire permet de maintenir une interface cohérente tout en facilitant les mises à jour de l'interface utilisateur.
|
||||
|
||||
@ -3,6 +3,7 @@ import requests
|
||||
import logging
|
||||
import os
|
||||
from utils.translations import _
|
||||
from utils.persistance import get_champ_statut, maj_champ_statut
|
||||
|
||||
def initialiser_logger():
|
||||
LOG_FILE_PATH = "/var/log/fabnum-auth.log"
|
||||
@ -19,7 +20,8 @@ def initialiser_logger():
|
||||
return logger
|
||||
|
||||
def connexion():
|
||||
if "logged_in" not in st.session_state or not st.session_state.logged_in:
|
||||
login = get_champ_statut("login")
|
||||
if login == "":
|
||||
auth_title = str(_("auth.title"))
|
||||
st.html(f"""
|
||||
<section role="region" aria-label="region-authentification">
|
||||
@ -33,18 +35,14 @@ def connexion():
|
||||
|
||||
logger = initialiser_logger()
|
||||
|
||||
if "logged_in" not in st.session_state:
|
||||
st.session_state.logged_in = False
|
||||
st.session_state.username = ""
|
||||
st.session_state.token = ""
|
||||
|
||||
if not st.session_state.logged_in:
|
||||
if get_champ_statut("login") == "":
|
||||
with st.form("auth_form"):
|
||||
# Ajout d'un champ identifiant fictif pour activer l'autocomplétion navigateur
|
||||
# et permettre de stocker le token comme un mot de passe par le navigateur
|
||||
# L'identifiant n'est donc pas utilisé par la suite ; il est caché en CSS
|
||||
identifiant = st.text_input(str(_("auth.username")), value="fabnum-connexion", key="nom_utilisateur")
|
||||
token = st.text_input(str(_("auth.token")), type="password")
|
||||
submitted = st.form_submit_button(str(_("auth.login")))
|
||||
submitted = st.form_submit_button(str(_("auth.login")), icon=":material/login:")
|
||||
|
||||
if submitted and token:
|
||||
erreur = True
|
||||
@ -70,10 +68,8 @@ def connexion():
|
||||
check_url = f"{GITEA_URL}/teams/{team_id}/members/{username}"
|
||||
check_response = requests.get(check_url, headers=headers, timeout=5)
|
||||
if check_response.status_code == 200:
|
||||
st.session_state.logged_in = True
|
||||
st.session_state.username = username
|
||||
st.session_state.token = token
|
||||
erreur = False
|
||||
maj_champ_statut("login", username)
|
||||
logger.info(f"Connexion réussie pour {username} depuis IP {ip}")
|
||||
st.rerun()
|
||||
|
||||
@ -91,7 +87,8 @@ def connexion():
|
||||
|
||||
|
||||
def bouton_deconnexion():
|
||||
if st.session_state.get("logged_in", False):
|
||||
login = get_champ_statut("login")
|
||||
if not login == "":
|
||||
auth_title = str(_("auth.title"))
|
||||
st.html(f"""
|
||||
<section role="region" aria-label="region-authentification">
|
||||
@ -99,11 +96,9 @@ def bouton_deconnexion():
|
||||
<p id="Authentification" class="decorative-heading">{auth_title}</p>
|
||||
""")
|
||||
|
||||
st.sidebar.markdown(f"{str(_('auth.logged_as'))} `{st.session_state.username}`")
|
||||
if st.sidebar.button(str(_("auth.logout"))):
|
||||
st.session_state.logged_in = False
|
||||
st.session_state.username = ""
|
||||
st.session_state.token = ""
|
||||
st.sidebar.markdown(f"{str(_('auth.logged_as'))} `{login}`")
|
||||
if st.sidebar.button(str(_("auth.logout")), icon=":material/logout:"):
|
||||
maj_champ_statut("login", "")
|
||||
st.success(str(_("auth.success")))
|
||||
st.rerun()
|
||||
|
||||
|
||||
@ -11,7 +11,9 @@ def afficher_pied_de_page():
|
||||
<div role='contentinfo' aria-labelledby='footer-appli' class='wide-footer'>
|
||||
<div class='info-footer'>
|
||||
<p id='footer-appli' class='info-footer'>
|
||||
{_("footer.copyright")} – <a href='mailto:stephan-pro@peccini.fr'>{_("footer.contact")}</a> – {_("footer.license")} <a href='https://creativecommons.org/licenses/by-nc-nd/4.0/deed.fr' target='_blank'>{_("footer.license_text")}</a>
|
||||
{_("footer.copyright")} – <a href='mailto:stephan.peccini@polycrisis-observatory.org'>{_("footer.contact")}</a> – {_("footer.license")} <a href='https://creativecommons.org/licenses/by-nc-nd/4.0/deed.fr' target='_blank'>{_("footer.license_text")}</a>
|
||||
</p>
|
||||
<a href="https://www.polycrisis-observatory.org/" target="_blank"><img src="/app/static/images/polycrisis_observatory.webp" alt="Logo de l'Observatoire des Polycrises"></a>
|
||||
</p>
|
||||
<p class='footer-note'>
|
||||
{_("footer.eco_note")} <a href='https://www.thegreenwebfoundation.org/' target='_blank'>{_("footer.eco_provider")}</a><br>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import streamlit as st
|
||||
from config import ENV
|
||||
from utils.translations import _
|
||||
from utils.persistance import get_session_id
|
||||
|
||||
|
||||
def afficher_entete():
|
||||
@ -11,7 +12,7 @@ def afficher_entete():
|
||||
"""
|
||||
|
||||
if ENV == "dev":
|
||||
header += f"<p>🔧 {_("app.dev_mode")}</p>"
|
||||
header += f"<p>🔧 {_("app.dev_mode")} Session : {get_session_id()}</p>"
|
||||
else:
|
||||
header += f"<p>{_("header.subtitle")}</p>"
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import streamlit as st
|
||||
from components.connexion import connexion, bouton_deconnexion
|
||||
import streamlit.components.v1 as components
|
||||
from utils.translations import _
|
||||
|
||||
from utils.persistance import get_champ_statut, maj_champ_statut
|
||||
|
||||
def afficher_menu():
|
||||
with st.sidebar:
|
||||
@ -13,20 +13,24 @@ def afficher_menu():
|
||||
|
||||
# Définir la variable instructions_text une seule fois en haut de la fonction
|
||||
instructions_text = str(_("navigation.instructions"))
|
||||
if "onglet" not in st.session_state:
|
||||
st.session_state.onglet = instructions_text
|
||||
navigation_onglet = get_champ_statut("navigation_onglet")
|
||||
if navigation_onglet == "":
|
||||
navigation_onglet = instructions_text
|
||||
maj_champ_statut("navigation_onglet", navigation_onglet)
|
||||
|
||||
onglet_choisi = None
|
||||
onglets = [
|
||||
str(_("navigation.instructions")),
|
||||
str(_("navigation.personnalisation")),
|
||||
str(_("navigation.analyse")),
|
||||
*([str(_("navigation.ia_nalyse"))] if not get_champ_statut("login") == "" else []),
|
||||
*([str(_("navigation.plan_d_action"))]),
|
||||
str(_("navigation.visualisations")),
|
||||
str(_("navigation.fiches"))
|
||||
]
|
||||
|
||||
for nom in onglets:
|
||||
if st.session_state.onglet == nom:
|
||||
if navigation_onglet == nom:
|
||||
st.markdown(f'<div class="bouton-fictif">{nom}</div>', unsafe_allow_html=True)
|
||||
else:
|
||||
if st.button(str(nom)):
|
||||
@ -43,63 +47,55 @@ def afficher_menu():
|
||||
# Pour éviter de perdre les informations dans les formulaires,
|
||||
# le changement de thème n'est proposé que si l'utilisateur est sur l'onglet "Instructions"
|
||||
#
|
||||
if st.session_state.onglet == instructions_text:
|
||||
if "theme_mode" not in st.session_state:
|
||||
st.session_state.theme_mode = str(_("sidebar.theme_light"))
|
||||
#
|
||||
theme_mode = get_champ_statut("theme_mode")
|
||||
if theme_mode == "":
|
||||
theme_mode = str(_("sidebar.theme_light"))
|
||||
maj_champ_statut("theme_mode", theme_mode)
|
||||
|
||||
theme_title = str(_("sidebar.theme"))
|
||||
st.markdown(f"""
|
||||
<section role="region" aria-label="region-theme">
|
||||
<div role="region" aria-labelledby="Theme">
|
||||
<p id="Theme" class="decorative-heading">{theme_title}</p>
|
||||
""", unsafe_allow_html=True)
|
||||
theme_title = str(_("sidebar.theme"))
|
||||
st.markdown(f"""
|
||||
<section role="region" aria-label="region-theme">
|
||||
<div role="region" aria-labelledby="Theme">
|
||||
<p id="Theme" class="decorative-heading">{theme_title}</p>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
theme_options = [
|
||||
str(_("sidebar.theme_light")),
|
||||
str(_("sidebar.theme_dark"))
|
||||
]
|
||||
theme = st.radio(
|
||||
str(_("sidebar.theme")),
|
||||
theme_options,
|
||||
index=theme_options.index(st.session_state.theme_mode),
|
||||
horizontal=True,
|
||||
label_visibility="hidden"
|
||||
)
|
||||
theme_options = [
|
||||
str(_("sidebar.theme_light")),
|
||||
str(_("sidebar.theme_dark"))
|
||||
]
|
||||
theme = st.radio(
|
||||
str(_("sidebar.theme")),
|
||||
theme_options,
|
||||
index=theme_options.index(theme_mode),
|
||||
horizontal=True,
|
||||
label_visibility="hidden"
|
||||
)
|
||||
maj_champ_statut("theme_mode", theme)
|
||||
|
||||
st.markdown("""
|
||||
<hr />
|
||||
</div>
|
||||
</nav>""", unsafe_allow_html=True)
|
||||
else :
|
||||
theme_title = str(_("sidebar.theme"))
|
||||
st.markdown(f"""
|
||||
<section role="region" aria-label="region-theme">
|
||||
<div role="region" aria-labelledby="Theme">
|
||||
<p id="Theme" class="decorative-heading">{theme_title}</p>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.info(str(_("sidebar.theme_instructions_only")))
|
||||
|
||||
st.markdown("""
|
||||
<hr />
|
||||
</div>
|
||||
</nav>""", unsafe_allow_html=True)
|
||||
|
||||
theme = st.session_state.theme_mode
|
||||
st.markdown("""
|
||||
<hr />
|
||||
</div>
|
||||
</nav>""", unsafe_allow_html=True)
|
||||
|
||||
connexion()
|
||||
|
||||
if st.session_state.get("logged_in", False):
|
||||
if not get_champ_statut("login") == "":
|
||||
bouton_deconnexion()
|
||||
|
||||
# === RERUN SI BESOIN ===
|
||||
if (onglet_choisi and onglet_choisi != st.session_state.onglet) or (theme != st.session_state.theme_mode):
|
||||
if (onglet_choisi and onglet_choisi != navigation_onglet) or (theme != theme_mode):
|
||||
if onglet_choisi: # Ne met à jour que si on a cliqué
|
||||
st.session_state.onglet = onglet_choisi
|
||||
st.session_state.theme_mode = theme
|
||||
maj_champ_statut("navigation_onglet", onglet_choisi)
|
||||
maj_champ_statut("theme_mode", theme)
|
||||
st.rerun()
|
||||
|
||||
|
||||
#
|
||||
# Important :
|
||||
# Avec Selinux, il faut donner les bons droits
|
||||
#
|
||||
# sudo chcon -Rt httpd_sys_content_t /chemin/d/acces/assets/
|
||||
#
|
||||
def afficher_impact(total_bytes):
|
||||
impact_label = str(_("sidebar.impact"))
|
||||
loading_text = str(_("sidebar.loading"))
|
||||
@ -129,7 +125,7 @@ def afficher_impact(total_bytes):
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #145a1a;
|
||||
color: #072c6e;
|
||||
text-align: center;
|
||||
}}
|
||||
span {{
|
||||
|
||||
22
config.py
22
config.py
@ -1,15 +1,35 @@
|
||||
import os
|
||||
import streamlit as st
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(".env")
|
||||
load_dotenv(".env.local", override=True)
|
||||
|
||||
# Fonction pour déterminer l'environnement à partir de l'en-tête X-Environment
|
||||
def determine_environment():
|
||||
# Valeur par défaut (si aucun en-tête n'est détecté)
|
||||
environment = "dev"
|
||||
|
||||
# Si nous sommes dans une session Streamlit
|
||||
if hasattr(st, 'context') and hasattr(st.context, 'headers'):
|
||||
try:
|
||||
# Lire directement l'en-tête X-Environment défini par Nginx : dev/public
|
||||
nginx_env = st.context.headers.get("x-environment")
|
||||
|
||||
if nginx_env:
|
||||
environment = nginx_env.lower()
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors de la lecture de l'en-tête X-Environment: {e}\nEnvironnement dev par défaut")
|
||||
|
||||
return environment
|
||||
|
||||
ENV = determine_environment()
|
||||
|
||||
GITEA_URL = os.getenv("GITEA_URL", "https://fabnum-git.peccini.fr/api/v1")
|
||||
GITEA_TOKEN = os.getenv("GITEA_TOKEN", "")
|
||||
ORGANISATION = os.getenv("ORGANISATION", "fabnum")
|
||||
DEPOT_FICHES = os.getenv("DEPOT_FICHES", "fiches")
|
||||
DEPOT_CODE = os.getenv("DEPOT_CODE", "code")
|
||||
ENV = os.getenv("ENV")
|
||||
ENV_CODE = os.getenv("ENV_CODE")
|
||||
DOT_FILE = os.getenv("DOT_FILE")
|
||||
INSTRUCTIONS = os.getenv("INSTRUCTIONS", "Instructions.md")
|
||||
|
||||
15
fabnum-dev.service
Normal file
15
fabnum-dev.service
Normal file
@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Fabnum Dev - Streamlit App
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=fabnum
|
||||
WorkingDirectory=/home/fabnum/fabnum-dev
|
||||
ExecStart=/home/fabnum/fabnum-dev/venv/bin/streamlit run /home/fabnum/fabnum-dev
|
||||
/fabnum.py --server.port 8502
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
74
fabnum.py
74
fabnum.py
@ -1,8 +1,20 @@
|
||||
import utils.persistance
|
||||
utils.persistance.update_session_paths()
|
||||
|
||||
import streamlit as st
|
||||
from utils.persistance import get_champ_statut, get_session_id
|
||||
|
||||
st.set_page_config(
|
||||
page_title="Fabnum – Analyse de chaîne",
|
||||
page_icon="assets/weakness.png", # ajout
|
||||
layout="centered",
|
||||
initial_sidebar_state="expanded"
|
||||
)
|
||||
|
||||
import re
|
||||
|
||||
# Configuration Gitea
|
||||
from config import INSTRUCTIONS
|
||||
from config import INSTRUCTIONS, ENV
|
||||
|
||||
from utils.gitea import (
|
||||
charger_instructions_depuis_gitea
|
||||
@ -11,6 +23,8 @@ from utils.gitea import (
|
||||
# Import du module de traductions
|
||||
from utils.translations import init_translations, _, set_language
|
||||
|
||||
from utils.widgets import html_expander
|
||||
|
||||
def afficher_instructions_avec_expanders(markdown_content):
|
||||
"""
|
||||
Affiche le contenu markdown avec les sections de niveau 2 (## Titre) dans des expanders
|
||||
@ -49,8 +63,8 @@ def afficher_instructions_avec_expanders(markdown_content):
|
||||
|
||||
# Affichage dans un expander
|
||||
status = True if i == 1 else False
|
||||
with st.expander(f"## {titre_section}", expanded=status):
|
||||
st.markdown(contenu_section, unsafe_allow_html=True)
|
||||
# with st.expander(f"## {titre_section}", expanded=status):
|
||||
html_expander(f"{titre_section}", content=contenu_section, open_by_default=status, details_class="details_introduction")
|
||||
|
||||
from utils.graph_utils import (
|
||||
charger_graphe
|
||||
@ -68,13 +82,8 @@ from app.fiches import interface_fiches
|
||||
from app.visualisations import interface_visualisations
|
||||
from app.personnalisation import interface_personnalisation
|
||||
from app.analyse import interface_analyse
|
||||
|
||||
st.set_page_config(
|
||||
page_title="Fabnum – Analyse de chaîne",
|
||||
page_icon="assets/weakness.png", # ajout
|
||||
layout="centered",
|
||||
initial_sidebar_state="expanded"
|
||||
)
|
||||
from app.ia_nalyse import interface_ia_nalyse
|
||||
from app.plan_d_action.interface import interface_plan_d_action
|
||||
|
||||
# Initialisation des traductions (langue française par défaut)
|
||||
init_translations()
|
||||
@ -82,12 +91,18 @@ init_translations()
|
||||
# Pour tester d'autres langues, décommenter cette ligne :
|
||||
set_language("fr")
|
||||
|
||||
session_id = st.context.headers.get("x-session-id")
|
||||
|
||||
#
|
||||
# Important
|
||||
# Avec Selinux, il faut mettre les bons droits :
|
||||
#
|
||||
# sudo semanage fcontext -a -t var_log_t '/var/log/nginx/fabnum-public\.access\.log'
|
||||
# sudo restorecon -v /var/log/nginx/fabnum-public.access.log
|
||||
#
|
||||
session_id = get_session_id()
|
||||
def get_total_bytes_for_session(session_id):
|
||||
total_bytes = 0
|
||||
try:
|
||||
with open("/var/log/nginx/fabnum-dev.access.log", "r") as f:
|
||||
with open(f"/var/log/nginx/fabnum-{ENV}.access.log", "r") as f:
|
||||
for line in f:
|
||||
if session_id in line:
|
||||
match = re.search(r'"GET.*?" \d+ (\d+)', line)
|
||||
@ -121,7 +136,8 @@ def charger_theme():
|
||||
}
|
||||
|
||||
# Thème en cours (conversion du nom traduit vers l'identifiant interne)
|
||||
current_theme_display = st.session_state.get("theme_mode", "Clair").lower()
|
||||
st.session_state["theme_mode"] = get_champ_statut("theme_mode")
|
||||
current_theme_display = st.session_state.get("theme_mode").lower()
|
||||
current_theme = theme_mapping.get(current_theme_display, "light") # Par défaut light si non trouvé
|
||||
theme_css = st.session_state[f"theme_css_content_{current_theme}"]
|
||||
|
||||
@ -144,7 +160,7 @@ def fermer_page():
|
||||
st.markdown("""</section>""", unsafe_allow_html=True)
|
||||
st.markdown("</main>", unsafe_allow_html=True)
|
||||
|
||||
total_bytes = get_total_bytes_for_session(session_id)
|
||||
total_bytes = get_total_bytes_for_session(get_session_id())
|
||||
|
||||
afficher_pied_de_page()
|
||||
afficher_impact(total_bytes)
|
||||
@ -158,28 +174,44 @@ instructions_tab = _("navigation.instructions")
|
||||
fiches_tab = _("navigation.fiches")
|
||||
personnalisation_tab = _("navigation.personnalisation")
|
||||
analyse_tab = _("navigation.analyse")
|
||||
ia_nalyse_tab = _("navigation.ia_nalyse")
|
||||
plan_d_action_tab = _("navigation.plan_d_action")
|
||||
visualisations_tab = _("navigation.visualisations")
|
||||
|
||||
if st.session_state.onglet == instructions_tab:
|
||||
navigation_onglet = get_champ_statut("navigation_onglet")
|
||||
|
||||
if navigation_onglet == instructions_tab:
|
||||
markdown_content = charger_instructions_depuis_gitea(INSTRUCTIONS)
|
||||
if markdown_content:
|
||||
afficher_instructions_avec_expanders(markdown_content)
|
||||
|
||||
elif st.session_state.onglet == fiches_tab:
|
||||
elif navigation_onglet == fiches_tab:
|
||||
interface_fiches()
|
||||
|
||||
else:
|
||||
# Charger le graphe une seule fois
|
||||
# Le graphe n'est pas nécessaire pour Instructions ou Fiches
|
||||
G_temp, G_temp_ivc, dot_file_path = charger_graphe()
|
||||
dot_file_path = charger_graphe()
|
||||
|
||||
if dot_file_path and st.session_state.onglet == analyse_tab:
|
||||
if dot_file_path and navigation_onglet == analyse_tab:
|
||||
G_temp = st.session_state["G_temp"]
|
||||
interface_analyse(G_temp)
|
||||
|
||||
elif dot_file_path and st.session_state.onglet == visualisations_tab:
|
||||
elif dot_file_path and navigation_onglet == ia_nalyse_tab:
|
||||
G_temp = st.session_state["G_temp"]
|
||||
interface_ia_nalyse(G_temp)
|
||||
|
||||
elif dot_file_path and navigation_onglet == plan_d_action_tab:
|
||||
G_temp = st.session_state["G_temp"]
|
||||
interface_plan_d_action(G_temp)
|
||||
|
||||
elif dot_file_path and navigation_onglet == visualisations_tab:
|
||||
G_temp = st.session_state["G_temp"]
|
||||
G_temp_ivc = st.session_state["G_temp_ivc"]
|
||||
interface_visualisations(G_temp, G_temp_ivc)
|
||||
|
||||
elif dot_file_path and st.session_state.onglet == personnalisation_tab:
|
||||
elif dot_file_path and navigation_onglet == personnalisation_tab:
|
||||
G_temp = st.session_state["G_temp"]
|
||||
G_temp = interface_personnalisation(G_temp)
|
||||
|
||||
fermer_page()
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Lancer fabnum avec Streamlit, selon l'environnement défini dans .env
|
||||
|
||||
# Aller dans le dossier du script
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Charger l'environnement Python
|
||||
source venv/bin/activate
|
||||
|
||||
# Charger les variables d'environnement définies dans .env
|
||||
if [ -f .env ]; then
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
else
|
||||
echo "⚠️ Fichier .env manquant !"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Valeur par défaut si PORT non défini
|
||||
PORT=${PORT:-8501}
|
||||
|
||||
echo "🔄 Lancement de Fabnum ($ENV) sur le port $PORT..."
|
||||
|
||||
# Exécuter streamlit via l'interpréteur du venv
|
||||
exec venv/bin/streamlit run fabnum.py --server.address=127.0.0.1 --server.port=$PORT
|
||||
16
pgpt/.docker/router.yml
Normal file
16
pgpt/.docker/router.yml
Normal file
@ -0,0 +1,16 @@
|
||||
http:
|
||||
services:
|
||||
ollama:
|
||||
loadBalancer:
|
||||
healthCheck:
|
||||
interval: 5s
|
||||
path: /
|
||||
servers:
|
||||
- url: http://ollama-cpu:11434
|
||||
- url: http://ollama-cuda:11434
|
||||
- url: http://host.docker.internal:11434
|
||||
|
||||
routers:
|
||||
ollama-router:
|
||||
rule: "PathPrefix(`/`)"
|
||||
service: ollama
|
||||
51
pgpt/Dockerfile.ollama
Normal file
51
pgpt/Dockerfile.ollama
Normal file
@ -0,0 +1,51 @@
|
||||
FROM python:3.11.6-slim-bookworm AS base
|
||||
|
||||
# Install poetry
|
||||
RUN pip install pipx
|
||||
RUN python3 -m pipx ensurepath
|
||||
RUN pipx install poetry==1.8.3
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
ENV PATH=".venv/bin/:$PATH"
|
||||
|
||||
# https://python-poetry.org/docs/configuration/#virtualenvsin-project
|
||||
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||
|
||||
FROM base AS dependencies
|
||||
WORKDIR /home/worker/app
|
||||
COPY pyproject.toml poetry.lock ./
|
||||
|
||||
ARG POETRY_EXTRAS="ui vector-stores-qdrant llms-ollama embeddings-ollama"
|
||||
RUN poetry install --no-root --extras "${POETRY_EXTRAS}"
|
||||
|
||||
FROM base AS app
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PORT=8080
|
||||
ENV APP_ENV=prod
|
||||
ENV PYTHONPATH="$PYTHONPATH:/home/worker/app/private_gpt/"
|
||||
EXPOSE 8080
|
||||
|
||||
# Prepare a non-root user
|
||||
# More info about how to configure UIDs and GIDs in Docker:
|
||||
# https://github.com/systemd/systemd/blob/main/docs/UIDS-GIDS.md
|
||||
|
||||
# Define the User ID (UID) for the non-root user
|
||||
# UID 100 is chosen to avoid conflicts with existing system users
|
||||
ARG UID=100
|
||||
|
||||
# Define the Group ID (GID) for the non-root user
|
||||
# GID 65534 is often used for the 'nogroup' or 'nobody' group
|
||||
ARG GID=65534
|
||||
|
||||
RUN adduser --system --gid ${GID} --uid ${UID} --home /home/worker worker
|
||||
WORKDIR /home/worker/app
|
||||
|
||||
RUN chown worker /home/worker/app
|
||||
RUN mkdir local_data && chown worker local_data
|
||||
RUN mkdir models && chown worker models
|
||||
COPY --chown=worker --from=dependencies /home/worker/app/.venv/ .venv
|
||||
COPY --chown=worker private_gpt/ private_gpt
|
||||
COPY --chown=worker *.yaml .
|
||||
COPY --chown=worker scripts/ scripts
|
||||
|
||||
USER worker
|
||||
ENTRYPOINT python -m private_gpt
|
||||
105
pgpt/docker-compose.yaml
Normal file
105
pgpt/docker-compose.yaml
Normal file
@ -0,0 +1,105 @@
|
||||
services:
|
||||
#-----------------------------------
|
||||
#---- Private-GPT services ---------
|
||||
#-----------------------------------
|
||||
|
||||
private-gpt-ollama:
|
||||
image: ${PGPT_IMAGE:-zylonai/private-gpt}:${PGPT_TAG:-0.6.2}-ollama
|
||||
user: root
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.ollama
|
||||
volumes:
|
||||
- /home/fabnum/fabnum-dev/Fiches:/home/worker/app/local_data/Fiches:Z
|
||||
ports:
|
||||
- "127.0.0.1:8001:8001"
|
||||
environment:
|
||||
PORT: 8001
|
||||
PGPT_PROFILES: docker
|
||||
PGPT_MODE: ollama
|
||||
PGPT_EMBED_MODE: ollama
|
||||
PGPT_OLLAMA_API_BASE: http://ollama:11434
|
||||
HF_TOKEN: ${HF_TOKEN:-}
|
||||
profiles:
|
||||
- ""
|
||||
- ollama-cpu
|
||||
- ollama-cuda
|
||||
- ollama-api
|
||||
depends_on:
|
||||
ollama:
|
||||
condition: service_started
|
||||
|
||||
private-gpt-llamacpp-cpu:
|
||||
image: ${PGPT_IMAGE:-zylonai/private-gpt}:${PGPT_TAG:-0.6.2}-llamacpp-cpu
|
||||
user: root
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.llamacpp-cpu
|
||||
volumes:
|
||||
- ./local_data/:/home/worker/app/local_data
|
||||
- ./models/:/home/worker/app/models
|
||||
entrypoint: sh -c ".venv/bin/python scripts/setup && .venv/bin/python -m private_gpt"
|
||||
ports:
|
||||
- "127.0.0.1:8001:8001"
|
||||
environment:
|
||||
PORT: 8001
|
||||
PGPT_PROFILES: local
|
||||
HF_TOKEN: ${HF_TOKEN:-}
|
||||
profiles:
|
||||
- llamacpp-cpu
|
||||
|
||||
#-----------------------------------
|
||||
#---- Ollama services --------------
|
||||
#-----------------------------------
|
||||
|
||||
ollama:
|
||||
image: traefik:v2.10
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
command:
|
||||
- "--providers.file.filename=/etc/router.yml"
|
||||
- "--log.level=ERROR"
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:11434"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./.docker/router.yml:/etc/router.yml:ro
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
security_opt:
|
||||
- label:disable
|
||||
profiles:
|
||||
- ""
|
||||
- ollama-cpu
|
||||
- ollama-cuda
|
||||
- ollama-api
|
||||
|
||||
ollama-cpu:
|
||||
image: ollama/ollama:latest
|
||||
ports:
|
||||
- "127.0.0.1:11434:11434"
|
||||
volumes:
|
||||
- ./models:/root/.ollama:Z
|
||||
healthcheck:
|
||||
disable: true
|
||||
profiles:
|
||||
- ""
|
||||
- ollama-cpu
|
||||
|
||||
ollama-cuda:
|
||||
image: ollama/ollama:latest
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ./models:/root/.ollama
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
profiles:
|
||||
- ollama-cuda
|
||||
27
pgpt/private_gpt/__init__.py
Normal file
27
pgpt/private_gpt/__init__.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""private-gpt."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
# Set to 'DEBUG' to have extensive logging turned on, even for libraries
|
||||
ROOT_LOG_LEVEL = "INFO"
|
||||
|
||||
PRETTY_LOG_FORMAT = (
|
||||
"%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)+25s - %(message)s"
|
||||
)
|
||||
logging.basicConfig(level=ROOT_LOG_LEVEL, format=PRETTY_LOG_FORMAT, datefmt="%H:%M:%S")
|
||||
logging.captureWarnings(True)
|
||||
|
||||
# Disable gradio analytics
|
||||
# This is done this way because gradio does not solely rely on what values are
|
||||
# passed to gr.Blocks(enable_analytics=...) but also on the environment
|
||||
# variable GRADIO_ANALYTICS_ENABLED. `gradio.strings` actually reads this env
|
||||
# directly, so to fully disable gradio analytics we need to set this env var.
|
||||
os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
|
||||
|
||||
# Disable chromaDB telemetry
|
||||
# It is already disabled, see PR#1144
|
||||
# os.environ["ANONYMIZED_TELEMETRY"] = "False"
|
||||
|
||||
# adding tiktoken cache path within repo to be able to run in offline environment.
|
||||
os.environ["TIKTOKEN_CACHE_DIR"] = "tiktoken_cache"
|
||||
11
pgpt/private_gpt/__main__.py
Normal file
11
pgpt/private_gpt/__main__.py
Normal file
@ -0,0 +1,11 @@
|
||||
# start a fastapi server with uvicorn
|
||||
|
||||
import uvicorn
|
||||
|
||||
from private_gpt.main import app
|
||||
from private_gpt.settings.settings import settings
|
||||
|
||||
# Set log_config=None to do not use the uvicorn logging configuration, and
|
||||
# use ours instead. For reference, see below:
|
||||
# https://github.com/tiangolo/fastapi/discussions/7457#discussioncomment-5141108
|
||||
uvicorn.run(app, host="0.0.0.0", port=settings().server.port, log_config=None)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user