diff --git a/scripts/nettoyer_pgpt.py b/IA/02 - injection_fiches/nettoyer_pgpt.py similarity index 100% rename from scripts/nettoyer_pgpt.py rename to IA/02 - injection_fiches/nettoyer_pgpt.py diff --git a/app/ia_nalyse/README.md b/app/ia_nalyse/README.md new file mode 100644 index 0000000..141f7b1 --- /dev/null +++ b/app/ia_nalyse/README.md @@ -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. \ No newline at end of file diff --git a/app/ia_nalyse/__init__.py b/app/ia_nalyse/__init__.py new file mode 100644 index 0000000..58cc0c9 --- /dev/null +++ b/app/ia_nalyse/__init__.py @@ -0,0 +1,2 @@ +# __init__.py – app/fiches +from .interface import interface_ia_nalyse diff --git a/app/ia_nalyse/interface.py b/app/ia_nalyse/interface.py new file mode 100644 index 0000000..b772c68 --- /dev/null +++ b/app/ia_nalyse/interface.py @@ -0,0 +1,191 @@ +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): + """Nettoie et prépare le graphe pour l'analyse.""" + 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): + """Interface pour sélectionner les minerais si nécessaire.""" + 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, niveaux_temp, niveau_depart): + """Interface pour sélectionner les nœuds spécifiques de départ et 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): + """Extrait les niveaux des nœuds du graphe""" + 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, 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: + 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, 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: + 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, niveaux, niveau_depart, niveau_arrivee, niveaux_speciaux): + """Extrait les liens des chemins en respectant les niveaux""" + 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): + 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("---") + + resultat = statut_utilisateur(st.session_state.username) + st.info(resultat["message"]) + + if 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"))): + soumettre_batch(st.session_state.username, G_final) + st.rerun() + else: + st.info(str(_("pages.ia_nalyse.empty_graph"))) + + elif resultat["statut"] == "terminé" and resultat["telechargement"]: + st.download_button(str(_("buttons.download")), resultat["telechargement"], file_name="analyse.zip") + if st.button(str(_("pages.ia_nalyse.confirm_download"))): + nettoyage_post_telechargement(st.session_state.username) + st.success("Résultat supprimé. Vous pouvez relancer une nouvelle analyse.") + if st.button(str(_("buttons.refresh"))): + st.rerun() + else: + if st.button(str(_("buttons.refresh"))): + st.rerun() diff --git a/assets/locales/en.json b/assets/locales/en.json index c32da17..1733906 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -1,280 +1,308 @@ { - "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" + "app": { + "title": "Fabnum – Chain Analysis", + "description": "Ecosystem exploration and vulnerability identification.", + "dev_mode": "You are in the development environment." }, - "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:" + "header": { + "title": "FabNum - Digital Manufacturing Chain", + "subtitle": "Ecosystem exploration and vulnerability identification." }, - "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" - } + "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" }, - "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" - } + "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..." }, - "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" + "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", + "ia_nalyse": "AI'nalysis", + "visualisations": "Visualizations", + "fiches": "Cards" + }, + "pages": { + "instructions": { + "title": "Instructions" }, - "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", - "continue": "Continuer", - "created_success": "Ticket created and placed in moderation", - "created_error": "Ticket creation failed. Please try later", - "see_ticket": "See ticket" - } + "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" + } + }, + "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", + "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." + }, + "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", + "continue": "Continuer", + "created_success": "Ticket created and placed in moderation", + "created_error": "Ticket creation failed. Please try later", + "see_ticket": "See ticket" + } + } + }, + "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", + "refresh": "Refresh", + "browse_files": "Browse files" + }, + "ui": { + "file_uploader": { + "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." } - }, - "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" - } - } } diff --git a/assets/locales/fr.json b/assets/locales/fr.json index 7346ebf..1b409c0 100644 --- a/assets/locales/fr.json +++ b/assets/locales/fr.json @@ -1,280 +1,308 @@ { - "app": { - "title": "Fabnum – Analyse de chaîne", - "description": "Parcours de l'écosystème et identification des vulnérabilités.", - "dev_mode": "Vous êtes dans l'environnement de développement." - }, - "header": { - "title": "FabNum - Chaîne de fabrication du numérique", - "subtitle": "Parcours de l'écosystème et identification des vulnérabilités." - }, - "footer": { - "copyright": "Fabnum © 2025", - "contact": "Contact", - "license": "Licence", - "license_text": "CC BY-NC-ND", - "eco_note": "🌱 Calculs CO₂ via", - "eco_provider": "The Green Web Foundation", - "powered_by": "🚀 Propulsé par", - "powered_by_name": "Streamlit" - }, - "sidebar": { - "menu": "Menu principal", - "navigation": "Navigation principale", - "theme": "Thème", - "theme_light": "Clair", - "theme_dark": "Sombre", - "theme_instructions_only": "Le changement de thème ne peut se faire que depuis l'onglet Instructions.", - "impact": "Impact environnemental", - "loading": "Chargement en cours…" - }, - "auth": { - "title": "Authentification", - "username": "Identifiant_token", - "token": "Token d'accès personnel Gitea", - "login": "Se connecter", - "logout": "Se déconnecter", - "logged_as": "Connecté en tant que", - "error": "❌ Accès refusé.", - "gitea_error": "❌ Impossible de vérifier l'utilisateur auprès de Gitea.", - "success": "Déconnecté avec succès." - }, - "navigation": { - "instructions": "Instructions", - "personnalisation": "Personnalisation", - "analyse": "Analyse", - "visualisations": "Visualisations", - "fiches": "Fiches" - }, - "pages": { - "instructions": { - "title": "Instructions" + "app": { + "title": "Fabnum – Analyse de chaîne", + "description": "Parcours de l'écosystème et identification des vulnérabilités.", + "dev_mode": "Vous êtes dans l'environnement de développement." }, - "personnalisation": { - "title": "Personnalisation des produits finaux", - "help": "Comment utiliser cet onglet ?", - "help_content": [ - "1. Cliquez sur « Ajouter un produit final » pour créer un nouveau produit", - "2. Donnez un nom à votre produit", - "3. Sélectionnez une opération d'assemblage appropriée (si pertinent)", - "4. Choisissez les composants qui constituent votre produit dans la liste proposée", - "5. Sauvegardez votre configuration pour une réutilisation future", - "6. Vous pourrez par la suite modifier ou supprimer vos produits personnalisés" - ], - "add_new_product": "Ajouter un nouveau produit final", - "new_product_name": "Nom du nouveau produit (unique)", - "assembly_operation": "Opération d'assemblage (optionnelle)", - "none": "-- Aucune --", - "components_to_link": "Composants à lier", - "create_product": "Créer le produit", - "added": "ajouté", - "modify_product": "Modifier un produit final ajouté", - "products_to_modify": "Produits à modifier", - "delete": "Supprimer", - "linked_assembly_operation": "Opération d'assemblage liée", - "components_linked_to": "Composants liés à", - "update": "Mettre à jour", - "updated": "mis à jour", - "deleted": "supprimé", - "save_restore_config": "Sauvegarder ou restaurer la configuration", - "export_config": "Exporter configuration", - "download_json": "Télécharger (JSON)", - "import_config": "Importer une configuration JSON (max 100 Ko)", - "file_too_large": "Fichier trop volumineux (max 100 Ko).", - "no_products_found": "Aucun produit trouvé dans le fichier.", - "select_products_to_restore": "Sélection des produits à restaurer", - "products_to_restore": "Produits à restaurer", - "restore_selected": "Restaurer les éléments sélectionnés", - "config_restored": "Configuration partielle restaurée avec succès.", - "import_error": "Erreur d'import :" + "header": { + "title": "FabNum - Chaîne de fabrication du numérique", + "subtitle": "Parcours de l'écosystème et identification des vulnérabilités." }, - "analyse": { - "title": "Analyse du graphe", - "help": "Comment utiliser cet onglet ?", - "help_content": [ - "1. Sélectionnez le niveau de départ (produit final, composant ou minerai)", - "2. Choisissez le niveau d'arrivée souhaité", - "3. Affinez votre sélection en spécifiant soit un ou des minerais à cibler spécifiquement ou des items précis à chaque niveau (optionnel)", - "4. Définissez les critères d'analyse en sélectionnant les indices de vulnérabilité pertinents", - "5. Choisissez le mode de combinaison des indices (ET/OU) selon votre besoin d'analyse", - "6. Explorez le graphique généré en utilisant les contrôles de zoom et de déplacement ; vous pouvez basculer en mode plein écran pour le graphe" - ], - "selection_nodes": "Sélection des nœuds de départ et d'arrivée", - "select_level": "-- Sélectionner un niveau --", - "start_level": "Niveau de départ", - "end_level": "Niveau d'arrivée", - "select_minerals": "Sélectionner un ou plusieurs minerais", - "filter_by_minerals": "Filtrer par minerais (optionnel)", - "fine_selection": "Sélection fine des items", - "filter_start_nodes": "Filtrer par noeuds de départ (optionnel)", - "filter_end_nodes": "Filtrer par noeuds d'arrivée (optionnel)", - "vulnerability_filters": "Sélection des filtres pour identifier les vulnérabilités", - "filter_ics": "Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)", - "filter_ivc": "Filtrer les chemins contenant au moins un minerai critique par rapport à la concurrence sectorielle (IVC > 30)", - "filter_ihh": "Filtrer les chemins contenant au moins une opération critique par rapport à la concentration géographique ou industrielle (IHH pays ou acteurs > 25)", - "apply_ihh_filter": "Appliquer le filtre IHH sur :", - "countries": "Pays", - "actors": "Acteurs", - "filter_isg": "Filtrer les chemins contenant un pays instable (ISG ≥ 60)", - "filter_logic": "Logique de filtrage", - "or": "OU", - "and": "ET", - "run_analysis": "Lancer l'analyse", - "sankey": { - "no_paths": "Aucun chemin trouvé pour les critères spécifiés.", - "no_matching_paths": "Aucun chemin ne correspond aux critères.", - "filtered_hierarchy": "Hiérarchie filtrée par niveaux et noeuds", - "download_dot": "Télécharger le fichier DOT filtré", - "relation": "Relation" - } + "footer": { + "copyright": "Fabnum © 2025", + "contact": "Contact", + "license": "Licence", + "license_text": "CC BY-NC-ND", + "eco_note": "🌱 Calculs CO₂ via", + "eco_provider": "The Green Web Foundation", + "powered_by": "🚀 Propulsé par", + "powered_by_name": "Streamlit" }, - "visualisations": { - "title": "Visualisations", - "help": "Comment utiliser cet onglet ?", - "help_content": [ - "1. Explorez les graphiques présentant l'Indice de Herfindahl-Hirschmann (IHH)", - "2. Analysez sa relation avec la criticité moyenne des minerais ou leur Indice de Vulnérabilité Concurrentielle (IVC)", - "3. Zoomer dans les graphes pour mieux découvrir les informations", - "", - "Il est important de se rappeler que l'IHH a deux seuils :", - "* en-dessous de 15, la concentration est considérée comme étant faible", - "* au-dessus de 25, elle est considérée comme étant forte", - "", - "Ainsi plus le positionnement d'un point est en haut à droite des graphiques, plus les risques sont élevés.", - "Les graphiques présentent 2 droites horizontales et vetrticales pour matérialiser ces seuils." - ], - "ihh_criticality": "Indice de Herfindahl-Hirschmann - IHH vs Criticité", - "ihh_criticality_desc": "La taille des points donne l'indication de la criticité de substituabilité du minerai.", - "ihh_ivc": "Indice de Herfindahl-Hirschmann - IHH vs IVC", - "ihh_ivc_desc": "La taille des points donne l'indication de la criticité concurrentielle du minerai.", - "launch": "Lancer", - "no_data": "Aucune donnée à visualiser.", - "categories": { - "assembly": "Assemblage", - "manufacturing": "Fabrication", - "processing": "Traitement", - "extraction": "Extraction" - }, - "axis_titles": { - "ihh_countries": "IHH Pays (%)", - "ihh_actors": "IHH Acteurs (%)", - "ihh_extraction": "IHH Extraction (%)", - "ihh_reserves": "IHH Réserves (%)" - }, - "chart_titles": { - "concentration_criticality": "Concentration et criticité – {0}", - "concentration_resources": "Concentration des ressources critiques vs vulnérabilité IVC" - } + "sidebar": { + "menu": "Menu principal", + "navigation": "Navigation principale", + "theme": "Thème", + "theme_light": "Clair", + "theme_dark": "Sombre", + "theme_instructions_only": "Le changement de thème ne peut se faire que depuis l'onglet Instructions.", + "impact": "Impact environnemental", + "loading": "Chargement en cours…" }, - "fiches": { - "title": "Découverte des fiches", - "help": "Comment utiliser cet onglet ?", - "help_content": [ - "1. Parcourez la liste des fiches disponibles par catégorie", - "2. Sélectionnez une fiche pour afficher son contenu complet", - "3. Consultez les données détaillées, graphiques et analyses supplémentaires", - "4. Utilisez ces informations pour approfondir votre compréhension des vulnérabilités identifiées", - "", - "Les catégories sont les suivantes :", - "* Assemblage : opération d'assemblage des produits finaux à partir des composants", - "* Connexe : opérations diverses nécessaires pour fabriquer le numérique, mais n'entrant pas directement dans sa composition", - "* Criticités : indices utilisés pour identifier et évaluer les vulnérabilités", - "* Fabrication : opération de fabrication des composants à partir de minerais", - "* Minerai : description et opérations d'extraction et de traitement des minerais" - ], - "no_files": "Aucune fiche disponible pour le moment.", - "choose_category": "Choisissez une catégorie de fiches", - "select_folder": "-- Sélectionner un dossier --", - "choose_file": "Choisissez une fiche", - "select_file": "-- Sélectionner une fiche --", - "loading_error": "Erreur lors du chargement de la fiche :", - "download_pdf": "Télécharger cette fiche en PDF", - "pdf_unavailable": "Le fichier PDF de cette fiche n'est pas disponible.", - "ticket_management": "Gestion des tickets pour cette fiche", - "tickets": { - "create_new": "Créer un nouveau ticket lié à cette fiche", - "model_load_error": "Impossible de charger le modèle de ticket.", - "contribution_type": "Type de contribution", - "specify": "Précisez", - "other": "Autre", - "concerned_card": "Fiche concernée", - "subject": "Sujet de la proposition", - "preview": "Prévisualiser le ticket", - "cancel": "Annuler", - "preview_title": "Prévisualisation du ticket", - "summary": "Résumé", - "title": "Titre", - "labels": "Labels", - "confirm": "Confirmer la création du ticket", - "created": "Ticket créé et formulaire vidé.", - "model_error": "Erreur chargement modèle :", - "no_linked_tickets": "Aucun ticket lié à cette fiche.", - "associated_tickets": "Tickets associés à cette fiche", - "moderation_notice": "ticket(s) en attente de modération ne sont pas affichés.", - "status": { - "awaiting": "En attente de traitement", - "in_progress": "En cours", - "completed": "Terminés", - "rejected": "Non retenus", - "others": "Autres" + "auth": { + "title": "Authentification", + "username": "Identifiant_token", + "token": "Token d'accès personnel Gitea", + "login": "Se connecter", + "logout": "Se déconnecter", + "logged_as": "Connecté en tant que", + "error": "❌ Accès refusé.", + "gitea_error": "❌ Impossible de vérifier l'utilisateur auprès de Gitea.", + "success": "Déconnecté avec succès." + }, + "navigation": { + "instructions": "Instructions", + "personnalisation": "Personnalisation", + "analyse": "Analyse", + "ia_nalyse": "IA'nalyse", + "visualisations": "Visualisations", + "fiches": "Fiches" + }, + "pages": { + "instructions": { + "title": "Instructions" }, - "no_title": "Sans titre", - "unknown": "inconnu", - "subject_label": "Sujet", - "no_labels": "aucun", - "comments": "Commentaire(s) :", - "no_comments": "Aucun commentaire.", - "comment_error": "Erreur lors de la récupération des commentaires :", - "opened_by": "Ouvert par", - "on_date": "le", - "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" - } + "personnalisation": { + "title": "Personnalisation des produits finaux", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "1. Cliquez sur « Ajouter un produit final » pour créer un nouveau produit", + "2. Donnez un nom à votre produit", + "3. Sélectionnez une opération d'assemblage appropriée (si pertinent)", + "4. Choisissez les composants qui constituent votre produit dans la liste proposée", + "5. Sauvegardez votre configuration pour une réutilisation future", + "6. Vous pourrez par la suite modifier ou supprimer vos produits personnalisés" + ], + "add_new_product": "Ajouter un nouveau produit final", + "new_product_name": "Nom du nouveau produit (unique)", + "assembly_operation": "Opération d'assemblage (optionnelle)", + "none": "-- Aucune --", + "components_to_link": "Composants à lier", + "create_product": "Créer le produit", + "added": "ajouté", + "modify_product": "Modifier un produit final ajouté", + "products_to_modify": "Produits à modifier", + "delete": "Supprimer", + "linked_assembly_operation": "Opération d'assemblage liée", + "components_linked_to": "Composants liés à", + "update": "Mettre à jour", + "updated": "mis à jour", + "deleted": "supprimé", + "save_restore_config": "Sauvegarder ou restaurer la configuration", + "export_config": "Exporter configuration", + "download_json": "Télécharger (JSON)", + "import_config": "Importer une configuration JSON (max 100 Ko)", + "file_too_large": "Fichier trop volumineux (max 100 Ko).", + "no_products_found": "Aucun produit trouvé dans le fichier.", + "select_products_to_restore": "Sélection des produits à restaurer", + "products_to_restore": "Produits à restaurer", + "restore_selected": "Restaurer les éléments sélectionnés", + "config_restored": "Configuration partielle restaurée avec succès.", + "import_error": "Erreur d'import :" + }, + "analyse": { + "title": "Analyse du graphe", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "1. Sélectionnez le niveau de départ (produit final, composant ou minerai)", + "2. Choisissez le niveau d'arrivée souhaité", + "3. Affinez votre sélection en spécifiant soit un ou des minerais à cibler spécifiquement ou des items précis à chaque niveau (optionnel)", + "4. Définissez les critères d'analyse en sélectionnant les indices de vulnérabilité pertinents", + "5. Choisissez le mode de combinaison des indices (ET/OU) selon votre besoin d'analyse", + "6. Explorez le graphique généré en utilisant les contrôles de zoom et de déplacement ; vous pouvez basculer en mode plein écran pour le graphe" + ], + "selection_nodes": "Sélection des nœuds de départ et d'arrivée", + "select_level": "-- Sélectionner un niveau --", + "start_level": "Niveau de départ", + "end_level": "Niveau d'arrivée", + "select_minerals": "Sélectionner un ou plusieurs minerais", + "filter_by_minerals": "Filtrer par minerais (optionnel)", + "fine_selection": "Sélection fine des items", + "filter_start_nodes": "Filtrer par noeuds de départ (optionnel)", + "filter_end_nodes": "Filtrer par noeuds d'arrivée (optionnel)", + "vulnerability_filters": "Sélection des filtres pour identifier les vulnérabilités", + "filter_ics": "Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)", + "filter_ivc": "Filtrer les chemins contenant au moins un minerai critique par rapport à la concurrence sectorielle (IVC > 30)", + "filter_ihh": "Filtrer les chemins contenant au moins une opération critique par rapport à la concentration géographique ou industrielle (IHH pays ou acteurs > 25)", + "apply_ihh_filter": "Appliquer le filtre IHH sur :", + "countries": "Pays", + "actors": "Acteurs", + "filter_isg": "Filtrer les chemins contenant un pays instable (ISG ≥ 60)", + "filter_logic": "Logique de filtrage", + "or": "OU", + "and": "ET", + "run_analysis": "Lancer l'analyse", + "sankey": { + "no_paths": "Aucun chemin trouvé pour les critères spécifiés.", + "no_matching_paths": "Aucun chemin ne correspond aux critères.", + "filtered_hierarchy": "Hiérarchie filtrée par niveaux et noeuds", + "download_dot": "Télécharger le fichier DOT filtré", + "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", + "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." + }, + "visualisations": { + "title": "Visualisations", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "1. Explorez les graphiques présentant l'Indice de Herfindahl-Hirschmann (IHH)", + "2. Analysez sa relation avec la criticité moyenne des minerais ou leur Indice de Vulnérabilité Concurrentielle (IVC)", + "3. Zoomer dans les graphes pour mieux découvrir les informations", + "", + "Il est important de se rappeler que l'IHH a deux seuils :", + "* en-dessous de 15, la concentration est considérée comme étant faible", + "* au-dessus de 25, elle est considérée comme étant forte", + "", + "Ainsi plus le positionnement d'un point est en haut à droite des graphiques, plus les risques sont élevés.", + "Les graphiques présentent 2 droites horizontales et vetrticales pour matérialiser ces seuils." + ], + "ihh_criticality": "Indice de Herfindahl-Hirschmann - IHH vs Criticité", + "ihh_criticality_desc": "La taille des points donne l'indication de la criticité de substituabilité du minerai.", + "ihh_ivc": "Indice de Herfindahl-Hirschmann - IHH vs IVC", + "ihh_ivc_desc": "La taille des points donne l'indication de la criticité concurrentielle du minerai.", + "launch": "Lancer", + "no_data": "Aucune donnée à visualiser.", + "categories": { + "assembly": "Assemblage", + "manufacturing": "Fabrication", + "processing": "Traitement", + "extraction": "Extraction" + }, + "axis_titles": { + "ihh_countries": "IHH Pays (%)", + "ihh_actors": "IHH Acteurs (%)", + "ihh_extraction": "IHH Extraction (%)", + "ihh_reserves": "IHH Réserves (%)" + }, + "chart_titles": { + "concentration_criticality": "Concentration et criticité – {0}", + "concentration_resources": "Concentration des ressources critiques vs vulnérabilité IVC" + } + }, + "fiches": { + "title": "Découverte des fiches", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "1. Parcourez la liste des fiches disponibles par catégorie", + "2. Sélectionnez une fiche pour afficher son contenu complet", + "3. Consultez les données détaillées, graphiques et analyses supplémentaires", + "4. Utilisez ces informations pour approfondir votre compréhension des vulnérabilités identifiées", + "", + "Les catégories sont les suivantes :", + "* Assemblage : opération d'assemblage des produits finaux à partir des composants", + "* Connexe : opérations diverses nécessaires pour fabriquer le numérique, mais n'entrant pas directement dans sa composition", + "* Criticités : indices utilisés pour identifier et évaluer les vulnérabilités", + "* Fabrication : opération de fabrication des composants à partir de minerais", + "* Minerai : description et opérations d'extraction et de traitement des minerais" + ], + "no_files": "Aucune fiche disponible pour le moment.", + "choose_category": "Choisissez une catégorie de fiches", + "select_folder": "-- Sélectionner un dossier --", + "choose_file": "Choisissez une fiche", + "select_file": "-- Sélectionner une fiche --", + "loading_error": "Erreur lors du chargement de la fiche :", + "download_pdf": "Télécharger cette fiche en PDF", + "pdf_unavailable": "Le fichier PDF de cette fiche n'est pas disponible.", + "ticket_management": "Gestion des tickets pour cette fiche", + "tickets": { + "create_new": "Créer un nouveau ticket lié à cette fiche", + "model_load_error": "Impossible de charger le modèle de ticket.", + "contribution_type": "Type de contribution", + "specify": "Précisez", + "other": "Autre", + "concerned_card": "Fiche concernée", + "subject": "Sujet de la proposition", + "preview": "Prévisualiser le ticket", + "cancel": "Annuler", + "preview_title": "Prévisualisation du ticket", + "summary": "Résumé", + "title": "Titre", + "labels": "Labels", + "confirm": "Confirmer la création du ticket", + "created": "Ticket créé et formulaire vidé.", + "model_error": "Erreur chargement modèle :", + "no_linked_tickets": "Aucun ticket lié à cette fiche.", + "associated_tickets": "Tickets associés à cette fiche", + "moderation_notice": "ticket(s) en attente de modération ne sont pas affichés.", + "status": { + "awaiting": "En attente de traitement", + "in_progress": "En cours", + "completed": "Terminés", + "rejected": "Non retenus", + "others": "Autres" + }, + "no_title": "Sans titre", + "unknown": "inconnu", + "subject_label": "Sujet", + "no_labels": "aucun", + "comments": "Commentaire(s) :", + "no_comments": "Aucun commentaire.", + "comment_error": "Erreur lors de la récupération des commentaires :", + "opened_by": "Ouvert par", + "on_date": "le", + "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" + } + } + }, + "node_levels": { + "0": "Produit final", + "1": "Composant", + "2": "Minerai", + "10": "Opération", + "11": "Pays d'opération", + "12": "Acteur d'opération", + "99": "Pays géographique" + }, + "errors": { + "log_read_error": "Erreur lecture log:", + "graph_preview_error": "Erreur de prévisualisation du graphe :", + "graph_creation_error": "Erreur lors de la création du graphique :", + "ihh_criticality_error": "Erreur dans la visualisation IHH vs Criticité :", + "ihh_ivc_error": "Erreur dans la visualisation IHH vs IVC :", + "comment_fetch_error": "Erreur lors de la récupération des commentaires :", + "template_load_error": "Erreur chargement modèle :", + "import_error": "Erreur d'import :" + }, + "buttons": { + "download": "Télécharger", + "run": "Lancer", + "save": "Enregistrer", + "cancel": "Annuler", + "confirm": "Confirmer", + "filter": "Filtrer", + "search": "Rechercher", + "create": "Créer", + "update": "Mettre à jour", + "delete": "Supprimer", + "preview": "Prévisualiser", + "export": "Exporter", + "import": "Importer", + "restore": "Restaurer", + "refresh": "Rafraîchir", + "browse_files": "Parcourir les fichiers" + }, + "ui": { + "file_uploader": { + "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." } - }, - "node_levels": { - "0": "Produit final", - "1": "Composant", - "2": "Minerai", - "10": "Opération", - "11": "Pays d'opération", - "12": "Acteur d'opération", - "99": "Pays géographique" - }, - "errors": { - "log_read_error": "Erreur lecture log:", - "graph_preview_error": "Erreur de prévisualisation du graphe :", - "graph_creation_error": "Erreur lors de la création du graphique :", - "ihh_criticality_error": "Erreur dans la visualisation IHH vs Criticité :", - "ihh_ivc_error": "Erreur dans la visualisation IHH vs IVC :", - "comment_fetch_error": "Erreur lors de la récupération des commentaires :", - "template_load_error": "Erreur chargement modèle :", - "import_error": "Erreur d'import :" - }, - "buttons": { - "download": "Télécharger", - "run": "Lancer", - "save": "Enregistrer", - "cancel": "Annuler", - "confirm": "Confirmer", - "filter": "Filtrer", - "search": "Rechercher", - "create": "Créer", - "update": "Mettre à jour", - "delete": "Supprimer", - "preview": "Prévisualiser", - "export": "Exporter", - "import": "Importer", - "restore": "Restaurer", - "browse_files": "Parcourir les fichiers" - }, - "ui": { - "file_uploader": { - "drag_drop_here": "Glissez-déposez votre fichier ici", - "size_limit": "Limite 100 Ko par fichier • JSON" - } - } } diff --git a/batch_ia/__init__.py b/batch_ia/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/batch_ia/analyse_ia.py b/batch_ia/analyse_ia.py index d509967..53293e1 100644 --- a/batch_ia/analyse_ia.py +++ b/batch_ia/analyse_ia.py @@ -1,12 +1,1921 @@ +#!/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 os import sys +import re +import yaml +from networkx.drawing.nx_agraph import read_dot from pathlib import Path +from collections import defaultdict +import uuid +import requests +import json +import time +import zipfile +from nettoyer_pgpt import delete_documents_by_criteria -dot_path = Path(sys.argv[1]) -output_path = Path(sys.argv[2]) +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}") -with dot_path.open() as f: - contenu = f.read() +BASE_DIR = Path(__file__).resolve().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" +TEMPLATE_PATH = TEMP_SECTIONS / f"rapport_final - {session_uuid}.md" -with output_path.open("w") as f: - f.write("Analyse IA du graphe suivant :\n\n") - f.write(contenu) \ No newline at end of file +if not TEMP_SECTIONS.exists(): + TEMP_SECTIONS.mkdir(parents=True) + +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) + +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 parse_graphs(graphe_path): + """ + Charge et analyse les graphes DOT (analyse et référence). + """ + # 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 + +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 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 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") + + # 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\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{rest.strip()}" + + # Ne pas ajouter de contenu vide + if content.strip(): + template.append(content.strip()) + + # 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 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 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_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) + suffixe = f" - chemins critiques {mot_cle}" + 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 + +def generate_text(input_file, full_prompt, system_message, temperature = "0.1"): + """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": True, # 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). + + 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} + """ + + reponse[produit_final] = f"\n**{produit_final}**\n\n" + generate_text(file, full_prompt, system_message).split("")[-1].strip() + # print(reponse[produit_final]) + + corps = "\n\n".join(reponse.values()) + print("Corps") + + 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) et quels sont les produits finaux 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. + """ + + + introduction = generate_text("", full_prompt, system_message).split("")[-1].strip() + print("Introduction") + + 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("")[-1].strip() + print("Préconisations") + + 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. + """ + + conclusion = generate_text("", full_prompt, system_message, "0.7").split("")[-1].strip() + print("Conclusion") + + 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 = f""" + Le fichier à réviser est {fichier_a_reviser}. 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 unique tâche est d'améliorer la qualité rédactionnelle du texte dans le fichier {fichier_a_reviser}, sans modifier ni sa structure, ni son sens initial, ni ajouter d’informations nouvelles. Cette révision doit : + + - É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 en plusieurs phrases courtes. + - Scinder les paragraphes en 2 ou 3 parties cohérentes et bien enchaînées avec des termes de coordinations, d'implication, … + - Remplacer systématiquement les acronymes par les expressions suivantes : + - 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 fluide, agréable à lire, parfaitement adapté à un COMEX, avec un ton professionnel et sobre. + + Répondez uniquement avec le texte révisé, sans autre commentaire. + """ + corps = generate_text(fichier_a_reviser, full_prompt, system_message, "0.6").split("")[-1].strip() + print("Relecture") + + return analyse + +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}") + +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 + +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 + +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) + # 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 compler + 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) + else: + print("") + +if __name__ == "__main__": + dot_path = Path(sys.argv[1]) + output_path = Path(sys.argv[2]) + main(dot_path, output_path) diff --git a/batch_ia/batch-fabnum-dev.service b/batch_ia/batch-fabnum-dev.service new file mode 100644 index 0000000..035ffae --- /dev/null +++ b/batch_ia/batch-fabnum-dev.service @@ -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(/.*)?" diff --git a/batch_ia/batch.service b/batch_ia/batch.service deleted file mode 100644 index c9b7d33..0000000 --- a/batch_ia/batch.service +++ /dev/null @@ -1,7 +0,0 @@ -[Unit] -Description=Batch IA Processing Service - -[Service] -ExecStart=/usr/bin/systemd-run --scope --property=CPUQuota=87.5% --property=MemoryMax=12G /usr/bin/python3 /chemin/complet/vers/batch_ia/batch_runner.py -WorkingDirectory=/chemin/complet/vers/batch_ia -Restart=always \ No newline at end of file diff --git a/batch_ia/batch_runner.py b/batch_ia/batch_runner.py index 69a6576..a89b21f 100644 --- a/batch_ia/batch_runner.py +++ b/batch_ia/batch_runner.py @@ -1,6 +1,7 @@ import time import subprocess -from batch_utils import * +from batch_utils import charger_status, sauvegarder_status, JOBS_DIR + while True: status = charger_status() @@ -13,7 +14,7 @@ while True: if jobs: login, _ = jobs[0] dot_file = JOBS_DIR / f"{login}.dot" - result_file = JOBS_DIR / f"{login}.result.txt" + result_file = JOBS_DIR / f"{login}.zip" status[login]["status"] = "en cours" sauvegarder_status(status) @@ -27,4 +28,4 @@ while True: sauvegarder_status(status) - time.sleep(10) \ No newline at end of file + time.sleep(60) diff --git a/batch_ia/batch_utils.py b/batch_ia/batch_utils.py index 73c6b6a..12d5dcc 100644 --- a/batch_ia/batch_utils.py +++ b/batch_ia/batch_utils.py @@ -2,10 +2,14 @@ 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(): @@ -20,27 +24,27 @@ def statut_utilisateur(login): entry = status.get(login) if not entry: return {"statut": None, "position": None, "telechargement": None, - "message": "Aucune tâche en cours."} + "message": f"{str(_('batch.no_task'))}."} if entry["status"] == "en attente": return {"statut": "en attente", "position": entry.get("position"), "telechargement": None, - "message": f"En attente (position {entry.get('position', '?')})."} + "message": f"{str(_('batch.in_queue'))} (position {entry.get('position', '?')})."} if entry["status"] == "en cours": return {"statut": "en cours", "position": 0, - "telechargement": None, "message": "Analyse en cours."} + "telechargement": None, "message": f"{str(_('batch.in_progress'))}."} if entry["status"] == "terminé": - result_file = JOBS_DIR / f"{login}.result.txt" + result_file = JOBS_DIR / f"{login}.zip" if result_file.exists(): return {"statut": "terminé", "position": None, - "telechargement": result_file.read_text(), - "message": "Analyse terminée. Télécharger le résultat."} + "telechargement": result_file.read_bytes(), + "message": f"{str(_('batch.complete'))}."} if entry["status"] == "échoué": return {"statut": "échoué", "position": None, "telechargement": None, - "message": f"Échec : {entry.get('error', 'erreur inconnue')}"} + "message": f"{str(_('batch.failure'))} : {entry.get('error', {str(_('batch.unknown_error'))})}"} def soumettre_batch(login, G): if statut_utilisateur(login)["statut"]: @@ -52,7 +56,7 @@ def soumettre_batch(login, G): def nettoyage_post_telechargement(login): (JOBS_DIR / f"{login}.dot").unlink(missing_ok=True) - (JOBS_DIR / f"{login}.result.txt").unlink(missing_ok=True) + (JOBS_DIR / f"{login}.zip").unlink(missing_ok=True) status = charger_status() status.pop(login, None) - sauvegarder_status(status) \ No newline at end of file + sauvegarder_status(status) diff --git a/batch_ia/nettoyer_pgpt.py b/batch_ia/nettoyer_pgpt.py new file mode 100644 index 0000000..3a7c4ba --- /dev/null +++ b/batch_ia/nettoyer_pgpt.py @@ -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 diff --git a/batch_ia/status.json b/batch_ia/status.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/batch_ia/status.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/components/sidebar.py b/components/sidebar.py index d18cdab..20ac21f 100644 --- a/components/sidebar.py +++ b/components/sidebar.py @@ -21,6 +21,7 @@ def afficher_menu(): str(_("navigation.instructions")), str(_("navigation.personnalisation")), str(_("navigation.analyse")), + *([str(_("navigation.ia_nalyse"))] if st.session_state.get("logged_in", False) else []), str(_("navigation.visualisations")), str(_("navigation.fiches")) ] diff --git a/fabnum.py b/fabnum.py index 1dddcfd..269991f 100644 --- a/fabnum.py +++ b/fabnum.py @@ -78,6 +78,7 @@ from app.fiches import interface_fiches from app.visualisations import interface_visualisations from app.personnalisation import interface_personnalisation from app.analyse import interface_analyse +from app.ia_nalyse import interface_ia_nalyse # Initialisation des traductions (langue française par défaut) init_translations() @@ -168,6 +169,7 @@ instructions_tab = _("navigation.instructions") fiches_tab = _("navigation.fiches") personnalisation_tab = _("navigation.personnalisation") analyse_tab = _("navigation.analyse") +ia_nalyse_tab = _("navigation.ia_nalyse") visualisations_tab = _("navigation.visualisations") if st.session_state.onglet == instructions_tab: @@ -187,6 +189,10 @@ if dot_file_path and st.session_state.onglet == analyse_tab: G_temp = st.session_state["G_temp"] interface_analyse(G_temp) +elif dot_file_path and st.session_state.onglet == ia_nalyse_tab: + G_temp = st.session_state["G_temp"] + interface_ia_nalyse(G_temp) + elif dot_file_path and st.session_state.onglet == visualisations_tab: G_temp = st.session_state["G_temp"] G_temp_ivc = st.session_state["G_temp_ivc"] diff --git a/scripts/Remarques.md b/scripts/Remarques.md deleted file mode 100644 index 04212c6..0000000 --- a/scripts/Remarques.md +++ /dev/null @@ -1,179 +0,0 @@ - -On ne peut pas proposer le rapport sous cette forme : plus de 6000 lignes pour une entrée simple. -Je propose plutôt de commencer par établir la liste de ce qui est couvert par le rapport : (enlever les """ qui sont là pour isoler ce que j'attends) - -Dans tout ce qui suit, les fichiers à incorporer se présente sous la forme suivante : -* Corpus/Assemblage/Fiche assemblage serveur/04-matrice-des-risques-liés-à-l-assemblage/00-indice-de-herfindahl-hirschmann.md -Les préfixes numériques peuvent varier, mais pas les slugs. Il faut donc chercher le bon fichier dans l'arborescence qui contient les slugs indépendamment des préfixes. - -""" -## Introduction - -Ce rapport analyse les vulnérabilités de la chaîne de fabrication du numérique pour : -* les produits finaux : (lister les produits) -* les composants : (lister les composants) -* les minerais : (lister les minerais) -""" - -Je verrai par la suite comment améliorer. - -""" -## Méthodologie d'analyse des risques - -### Indices et seuils - -La méthode d'évaluation intègre 4 indices et leurs combinaisons pour identifier les chemins critiques. - -#### IHH (Herfindahl-Hirschmann) : concentration géographiques ou industrielle d'une opération - -(Récupérer => Corpus/Criticités/Fiche technique IHH/00-contexte-et-objectif.md et enlever la première ligne de titre) - -(Récupérer => Corpus/Criticités/Fiche technique IHH/01-mode-de-calcul/_intro.md et enlever la première ligne de titre) - - * Seuils : <15 = Vert (Faible), 15-25 = Orange (Modérée), >25 = Rouge (Élevée) - -#### ISG (Stabilité Géopolitique) : stabilité des pays - -(Récupérer => Corpus/Criticités/Fiche technique ISG/00-contexte-et-objectif.md et enlever la première ligne de titre) - -(Récupérer => Corpus/Criticités/Fiche technique ISG/01-mode-de-calcul/_intro.md et enlever la première ligne de titre) - - * Seuils : <40 = Vert (Stable), 40-60 = Orange, >60 = Rouge (Instable) - -… on répète pour les autres -""" - -""" -### Combinaison des indices - -**IHH et ISG** - -Ces deux indices s'appliquent à toutes les opérations et se combinent dans l'évaluation du risque (niveau d'impact et probabilité de survenance) : -* l'IHH donne le niveau d'impact => une forte concentration implique un fort impact si le risque est avéré -* 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 - -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 : -ISG_combiné = (Somme des ISG des pays multipliée par leur part de marché) / Sommes de leur part de marché - -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 - -| ISG combiné / IHH | Vert | Orange | Rouge | -| :-- | :-- | :-- | :-- | -| Vert | 1 | 2 | 3 | -| Orange | 2 | 4 | 6 | -| Rouge | 3 | 6 | 9 | - -Les vulnérabilités se classent en trois niveaux pour chaque opération : - -* Vulnérabilité combinée élevée à critique : poids 6 et 9 -* Vulnérabilité combinée moyenne : poids 3 et 4 -* Vulnérabilité combinée faible : poids 1 et 2 - -**ICS et IVC** - -Ces deux indices se combinent dans l'évaluation du risque pour un minerai : -* 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 -* l'IVC donne la probabilité de l'impact => une forte concurrence intersectorielle (IVC élevé) implique une plus forte probabilité de survenance - -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. - -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. - -| ICS_moyen / IVC | Vert | Orange | Rouge | -| :-- | :-- | :-- | :-- | -| Vert | 1 | 2 | 3 | -| Orange | 2 | 4 | 6 | -| Rouge | 3 | 6 | 9 | - -Les vulnérabilités se classent en trois niveaux pour chaque minerai : - -* Vulnérabilité combinée élevée à critique : poids 6 et 9 -* Vulnérabilité combinée moyenne : poids 3 et 4 -* Vulnérabilité combinée faible : poids 1 et 2 -""" - -À partir de là on passe aux opérations ; on ne peut pas répéter les opérations du minerai Germanium sur tous les composants dans lesquels il intervient. On va factoriser. On présente donc chaque opération par rapport à l'item auquel elles sont associées. - -""" -## Détails des opérations -""" - -Assemblage et Fabrication se présente de la même manière. On remplace assemblage par fabrication, assembleurs par fabricants. - -Prenons un exemple pour illustre : Serveur ; on ne fait ce qui suit que pour les produits finaux (N0) ou les composants (N1) qui sont présents dans le graphe DOT. Même si les opérations en sont pas présentes dans le graphe. - -""" -### Serveur et Assemblage - -(Récupérer Corpus/Assemblage/Fiche assemblage serveur/00-présentation-synthétique.md et enlever la première ligne de titre) - -(Récupérer Corpus/Assemblage/Fiche assemblage serveur/02-principaux-assembleurs.md et décaler le titre initial de 2 niveaux) - -(En fonction des pays, récupérer leur ISG, en faire un tableau et faire ensuite le calcul de l'ISG combiné en récupérant les parts de marché <== On va devoir regarder le point de récupération attentivement ==>) - -(Récupérer Corpus/Assemblage/Fiche assemblage serveur/04-matrice-des-risques-liés-à-l-assemblage/00-indice-de-herfindahl-hirschmann.md et décaler tous les titres d'un niveau) - -(En fonction de l'ISG combiné et de l'IHH, donner le niveau de Vulnérabilité et son poids) -""" - -À refaire donc, pour tous les produits finaux et tous les composants présents dans le graphe DOT à analyser. - -Pour l'ISG, on va utiliser le graphe initial, schema.txt, base de données de l'application. Il se trouve à la racine du projet. -Dans ce graphe DOT de référence, les pays (N99) sont référencés comme suit : -Luxembourg_geographique [fillcolor="#e6f2ff", label="Luxembourg", isg="24", niveau="99"]; -On charge comme le graphe à analyser et on cherche le nœud de niveau=99 (et non niveau="99" du fichier) dont le label = le nom réel recherché. - -On passe ensuite aux minerais. Ils ont deux opérations, Extraction et Traitement qui se gèrent de la même manière. Je prends l'exemple du cuivre. - -""" -### Cuivre - -(Récupérer Corpus/Minerai/Fiche minerai cuivre/00-présentation-synthétique.md et enlever la première ligne de titre) - -**ICS** - -(Récupérer Corpus/Minerai/Fiche minerai cuivre/11-risque-de-substituabilité/_intro.md et enlever la première ligne de titre) - -(Récupérer dans le graphe à analyser toutes les arêtes où le Cuivre est le nœud target et faire la moyenne pour obtenir l'ICS moyen) - -**IVC** - -(Récupérer toutes les sections sous Corpus/Minerai/Fiche minerai cuivre/12-vulnérabilité-de-concurrence/ en mettant _intro.md en premier et les autres par ordre alphabérique (préfixes numériques) et remplacer la première ligne de titre de chaque section par le contenu du titre mis en italique) - -**ICS et IVC combinés** - -(En fonction de l'ICS moyen et de l'IVC, donner le niveau de Vulnérabilité et son poids) - -#### Extraction - -(Récupérer Corpus/Minerai/Fiche minerai cuivre/03-principaux-producteurs-extraction.md et enlever la première ligne de titre) - -(En fonction des pays, récupérer leur ISG, en faire un tableau et faire ensuite le calcul de l'ISG combiné en récupérant les parts de marché) - -(Récupérer Corpus/Minerai/Fiche minerai cuivre/10-matrices-des-risques/01-indice-de-herfindahl-hirschmann-extraction.md et décaler tous les titres d'un niveau) - -(En fonction de l'ISG combiné et de l'IHH, donner le niveau de Vulnérabilité et son poids) - -#### Traitement - -(Idem à extraction en remplaçant extration par traitement) -""" - -On en a fini avec les opérations. - -""" -## Chemins critiques - -( -Il faut maintenant faire une passe complète de tous les chemins entre produit final et minerai et les positionner dans : -* Chaîne avec risque critique, elle comprend : - * au moins une vulnérabilité combinée élevée à critique -* Chaîne avec risque majeur, elle comprend : - * au moins trois vulnérabilités combinée moyennes -* Chaîne avec risque moyen, elle comprend : - * au moins une vulnérabilité combinée moyenne -en donnant les éléments nécessaires pour identifier les Vulnérabilités combinées concernées -) -""" - -On en a fini. L'IA par la suite va examiner cette section et expliquer pourquoi c'est critique. À voir si cette partie ne devrait pas être le prompt ou tout au moins une partie. diff --git a/scripts/Risques.md b/scripts/Risques.md deleted file mode 100644 index 75bf032..0000000 --- a/scripts/Risques.md +++ /dev/null @@ -1,87 +0,0 @@ -# Quels sont les indices et leurs seuils - -* IHH (Herfindahl-Hirschmann) : concentration géographiques ou industrielle d'un opération - * Seuils : <15 = Vert (Faible), 15-25 = Orange (Modérée), >25 = Rouge (Élevée) -* ICS (Criticité de Substituabilité) : capacité à remplacer / substituer un élément dans le composant ou le procédé - * Seuils : <0.3 = Vert (Facile), 0.3-0.6 = Orange (Moyenne), >0.6 = Rouge (Difficile) -* ISG (Stabilité Géopolitique) : stabilité des pays - * Seuils : <40 = Vert (stable), 40-60 = Orange, >60 = Rouge (instable) -* IVC (Vulnérabilité de Concurrence) : pression concurrentielle avec d'autres secteurs que le numérique - * Seuils : <5 = Vert (Faible), 5-15 = Moyenne (Modérée), >15 = Rouge (Forte) - -Les seuils permettent de définir une échelle facile à comprendre. -Il est important de comprendre que se trouver en bas ou en haut d'une plage (verte, orange ou rouge) n'a pas le même niveau de risque. un niveau ICS à 0.65 (capacité de substitution faible) est moindre qu'un niveau d'ICS à 1 (aucune capacité à remplacer), tout en restant élevé. - -# Combinaison des risques - -**IHH et ISG** - -Ces deux indices se combinent dans l'évaluation du risque (niveau d'impact et probabilité de survenance) : -* l'IHH donne le niveau d'impact => une forte concentration implique un fort impact si le risque est avéré -* 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 - -Toutefois, l'ISG s'adresse à un pays et l'IHH à une opération (soit pour un pays, soit pour un acteur) ; il faut donc calculer l'ISG combiné des pays intervenant pour une opération. Voici le calcul que l'on va utiliser : -ISG_combiné = (Somme des ISG des pays multipliée par leur part de marché) / Sommes de leur part de marché - -Il faut donc produire ici le tableau des producteurs/fabricants/assembleurs (selon l'opération concernée) avec leur part de marché respective. Dans un premier temps, on se contentera de faire l'analyse pour les pays seulement. L'IHH des acteurs sera uniquement mentionné à titre informatif avec un commentaire léger. - -On établit alors une matrice en mettant des poids (Vert = 1, Orange = 2, Rouge = 3) et en faisant le produit des poids de l'ISG_combiné et de l'IHH - -| ISG_combiné / IHH | Vert | Orange | Rouge | -| :-- | :-- | :-- | :-- | -| Vert | 1 | 2 | 3 | -| Orange | 2 | 4 | 6 | -| Rouge | 3 | 6 | 9 | - -On peut alors dire que l'on classe en trois niveaux pour chaque opération : - -* Vulnérabilité combinée élevée à critique : poids 6 et 9 -* Vulnérabilité combinée moyenne : poids 3 et 4 -* Vulnérabilité combinée faible : poids 1 et 2 - -**ICS et IVC** - -Ces deux indices se combinent dans l'évaluation du risque : -* 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 -* l'IVC donne la probabilité de l'impact => une forte concurrence intersectorielle (IVC élevé) implique une plus forte probabilité de survenance - -L'ICS et l'IVC sont au niveau des minerais. -Par simplification, on va faire un calcul d'un ICS_moyen d'un minerai comme étant la moyenne des ICS pour chacun des composants dans lesquels il intervient. -Il faut donc récupérer pour le minerai son tableau des ICS pour faire cette moyenne. - -On établit alors une matrice en mettant des poids (Vert = 1, Orange = 2, Rouge = 3) et en faisant le produit des poids de l'ICS et de l'IVC. - -| ICS_moyen / IVC | Vert | Orange | Rouge | -| :-- | :-- | :-- | :-- | -| Vert | 1 | 2 | 3 | -| Orange | 2 | 4 | 6 | -| Rouge | 3 | 6 | 9 | - -On peut alors dire que l'on classe en trois niveaux pour chaaque minerai : - -* Vulnérabilité combinée élevée à critique : poids 6 et 9 -* Vulnérabilité combinée moyenne : poids 3 et 4 -* Vulnérabilité combinée faible : poids 1 et 2 - -# Interdépendances - -Il va falloir maintenant remonter toute la chaîne de fabrication pour faire la recherche des dépendances entre indices, indices combinés. - -Un produit final est assemblé à partir de composants. Il y a donc une opération d'assemblage avec IHH et ISG_combiné - -Un composant est fabriqué à partir de minerais. Il y a donc une opération de fabrication avec IHH et ISG_combiné. - -Un minerai est extrait et traité. Il y a donc associé au minerai un ICS_moyen et un IVC, une opération d'extraction avec un IHH et un ISG_combiné et une opération de traitement avec un IHH et un ISG_combiné. - -Il faut donc reprendre ici toutes les chaines du produit final au minerai en classant comme suit : - -* Chaîne avec risque critique, elle comprend : - * au moins une vulnérabilité combinée élevée à critique -* Chaîne avec risque majeur, elle comprend : - * au moins trois vulnérabilités combinée moyennes -* Chaîne avec risque moyen, elle comprend : - * au moins une vulnérabilité combinée moyenne - -# Compléments - -Pour chacun des indices (avant de les combiner), il récupérer leur détail dans le corpus (ce qui est mis en annexe du rapport_final actuel). cela doit faire partie de l'analyse détaillée à fournir. diff --git a/scripts/Réponses.md b/scripts/Réponses.md deleted file mode 100644 index b36bc8c..0000000 --- a/scripts/Réponses.md +++ /dev/null @@ -1,111 +0,0 @@ -Questions pour clarifier la mise en œuvre - -1. **Structure du graphe DOT**: Le graphe DOT contient-il déjà les relations hiérarchiques (produit → composant → minerai)? Comment sont-elles représentées? - -La structure du graphe est un digraph, donc avec une organisation hiérarchique : - -Dans la suite, quand j'écris N0, N1, … cela veut dire que le nœud portant l'item a un attribut niveau 0, 1, … -Exemple : - LynasAdvanced_Malaisie_Traitement_Erbium [fillcolor="#d1e0ff", - label="Lynas Advanced Materials", - niveau=12]; -LynasAdvanced_Malaisie_Traitement_Erbium est un N12. Important avec ce nom : on sait tout de suite que l'acteur est LynasAdvanced dont le nom réel est porté par le label (Lynas Advanced Materials), qu'il opère en Malaisie, pour faire le Traitement de l'Erbium. - -* un produit final (N0) est associé à : - * un ou plusieurs composants (N1) - * aucune ou une opération d'assemblage (N10) -* un composant (N1) est associé à : - * un ou plusieurs minerais (N2) - * aucune ou une opération de fabrication (N10) -* un minerai (N2) est associé à : - * une opération d'extraction (N10) - * une opération de traitement (N10) - -Exemple pour produit final (avec une seule chaîne) : - MaterielIA [fillcolor="#a0d6ff", - label="Matériel dédié IA", - niveau=0]; - MaterielIA -> CarteMere; - CarteMere [fillcolor="#b3ffe0", - label="Carte mère", - niveau=1]; - CarteMere -> Germanium [cout=0.6, - delai=0.6, - ics=0.64, - technique=0.7]; - Germanium [fillcolor="#ffd699", - ivc=1, - label="Germanium - Semi-conducteurs, détecteurs infrarouge, fibre optique", - niveau=2]; - Germanium -> Traitement_Germanium; - Traitement_Germanium [fillcolor="#ffd699", - ihh_acteurs=19, - ihh_pays=31, - label=Traitement, - niveau=10]; - Traitement_Germanium -> Chine_Traitement_Germanium [color=purple, - fontcolor=purple, - label="50%", - poids=2]; - Chine_Traitement_Germanium [fillcolor="#e6f2ff", - label=Chine, - niveau=11]; - Chine_Traitement_Germanium -> YunnanGermanium_Chine_Traitement_Germanium [color=purple, - fontcolor=purple, - label="30%", - poids=2]; - Chine_Traitement_Germanium -> Chine_geographique [color=darkgreen, - fontcolor=darkgreen]; - YunnanChihong_Chine_Traitement_Germanium [fillcolor="#d1e0ff", - label="Yunnan Chihong Zinc", - niveau=12]; - YunnanChihong_Chine_Traitement_Germanium -> Chine_geographique [color=darkgreen, - fontcolor=darkgreen]; - -Chaque opération (N10) (assemblage, fabrication, traitement, extraction) se décompose comme suit : -* l'opération elle-même (N10) - * un ou plusieurs pays (N11) où l'opération est réalisée - * pour chaque pays (N11, il est associé à : - * un ou plusieurs acteurs (N12) opérant dans le pays - * un pays géographique (N99) - -2. **Calcul d'ISG_combiné**: Les parts de marché par pays sont-elles disponibles dans le graphe ou doivent-elles être extraites d'ailleurs? - -Oui, le parts de marché sont disponble dans le graphe. - -Exemple pour les pays : - Traitement_Germanium -> Chine_Traitement_Germanium [color=purple, - fontcolor=purple, - label="50%", - poids=2]; - Chine_Traitement_Germanium [fillcolor="#e6f2ff", - label=Chine, - niveau=11]; -La part de marché de Chine_Traitement_Germanium (et donc Chine que l'on récupère dans le label de Chine_Traitement_Germanium) est de 50% à récupérer dans le label de l'arête Traitement_Germanium -> Chine_Traitement_Germanium - -Pour un acteur, on la récupère dans l'arête entre le pays et l'acteur : - Chine_Traitement_Germanium -> YunnanGermanium_Chine_Traitement_Germanium [color=purple, - fontcolor=purple, - label="30%", - poids=2]; -La part de marché de YunnanGermanium_Chine_Traitement_Germanium est de 30% (label de l'arête). - -3. **Traitement des matrices**: Souhaitez-vous que les matrices de vulnérabilité combinée soient: - - Calculées dynamiquement dans le script - - Présentées sous forme de tableaux dans le rapport - - Ou les deux? - -Les matrices sont à présenter dans le rapport et donc calculées dynamiquement. Toutes les informations nécessaire sont dans le graphe pour faire ces calculs. - -4. **Niveau de profondeur**: Pour chaque chaîne identifiée, quel niveau de détail souhaitez-vous dans le rapport? Uniquement les vulnérabilités combinées ou aussi le détail des indices individuels? - -Les deux sont à présenter. En premier une partie factuelle avec les éléments individuels et leur explication et ensuite la combinaison. - -5. **Approche structurelle**: Préférez-vous une organisation du rapport: - - Par niveau de risque (critique → majeur → moyen) - - Par produit/matière (en incluant tous les niveaux de risque pour chaque chaîne) - - Une combinaison des deux? - -Il faut se rappeler que ce rapport est le point d'entrée pour l'IA qui aura pour objectif de textualiser tout cela et de proposer des scénarios de veille et de prévention. - -Dans ce rapport factuel, il faut qu'il soit efficace pour l'IA et il viendra se joindre, de mon point de vue au rapport d'analyse que l'IA génèrera. diff --git a/scripts/generate_factorized_report.py b/scripts/generate_factorized_report.py deleted file mode 100644 index 26573c7..0000000 --- a/scripts/generate_factorized_report.py +++ /dev/null @@ -1,1931 +0,0 @@ -#!/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 os -import sys -import re -import yaml -from networkx.drawing.nx_agraph import read_dot -from pathlib import Path -from collections import defaultdict -import uuid -import requests -import json -import time - -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}") - -BASE_DIR = Path(__file__).resolve().parent -CORPUS_DIR = BASE_DIR.parent / "Corpus" -CONFIG_PATH = BASE_DIR / "config.yml" -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" -TEMPLATE_PATH = TEMP_SECTIONS / f"rapport_final - {session_uuid}.md" - -if not TEMP_SECTIONS.exists(): - TEMP_SECTIONS.mkdir(parents=True) - -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(config_path, thresholds_path=THRESHOLDS_PATH): - """Charge la configuration depuis les fichiers YAML.""" - # Charger la configuration principale - if not os.path.exists(config_path): - # print(f"Fichier de configuration introuvable: {config_path}") - sys.exit(1) - - with open(config_path, 'r', encoding='utf-8') as f: - config = yaml.safe_load(f) - - # Vérifier les chemins essentiels - required_paths = ['graphe_path', 'template_path', 'corpus_path'] - for path in required_paths: - if path not in config: - # print(f"Configuration incomplète: {path} manquant") - sys.exit(1) - - # Convertir les chemins relatifs en chemins absolus - for path in required_paths: - config[path] = os.path.join(os.path.dirname(config_path), config[path]) - - # 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) - -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 parse_graphs(config): - """ - Charge et analyse les graphes DOT (analyse et référence). - """ - # Charger le graphe à analyser - graphe_path = GRAPH_PATH - 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 - -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("### Synthèse de la méthodologie\n") - template.append(""" - 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. 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. - """) - 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 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 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") - - # 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\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{rest.strip()}" - - # Ne pas ajouter de contenu vide - if content.strip(): - template.append(content.strip()) - - # ICS et IVC combinés - if mineral_id in results["ics_ivc_combined"]: - combined = results["ics_ivc_combined"][mineral_id] - template.append(f"\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(f"##### 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 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 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_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) - suffixe = f" - chemins critiques {mot_cle}" - 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 - -def generate_text(input_file, full_prompt, system_message, temperature = "0.1"): - """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": True, # 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). - - 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} - """ - - reponse[produit_final] = f"\n**{produit_final}**\n\n" + generate_text(file, full_prompt, system_message).split("")[-1].strip() - # print(reponse[produit_final]) - - corps = "\n\n".join(reponse.values()) - print("Corps") - - 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). - - 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. - """ - - - introduction = generate_text("", full_prompt, system_message).split("")[-1].strip() - print("Introduction") - - 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("")[-1].strip() - print("Préconisations") - - 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. - """ - - conclusion = generate_text("", full_prompt, system_message, "0.7").split("")[-1].strip() - print("Conclusion") - - 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 - - fichier_a_reviser = TEMPLATE_PATH.name.replace(".md", " - analyse à relire.md") - write_report(analyse, fichier_a_reviser) - ingest_document(Path(fichier_a_reviser)) - - full_prompt = f""" - Le fichier à réviser est {fichier_a_reviser}. 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 unique tâche est d'améliorer la qualité rédactionnelle du texte dans le fichier {fichier_a_reviser}, sans modifier ni sa structure, ni son sens initial, ni ajouter d’informations nouvelles. Cette révision doit : - - - É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 en plusieurs phrases courtes. - - Remplacer systématiquement les acronymes par les expressions suivantes : - - 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 fluide, agréable à lire, parfaitement adapté à un COMEX, avec un ton professionnel et sobre. - - Répondez uniquement avec le texte révisé, sans autre commentaire. - """ - corps = generate_text(fichier_a_reviser, full_prompt, system_message, "0.6").split("")[-1].strip() - print("Relecture") - - return analyse - -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}") - -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 - -def main(): - """Fonction principale du script.""" - # Charger la configuration - config = load_config(CONFIG_PATH) - # Analyser les graphes - graph, ref_graph = parse_graphs(config) - # Extraire les données - data = extract_data_from_graph(graph, ref_graph) - # Calculer les vulnérabilités - results = calculate_vulnerabilities(data, config) - # Générer le rapport - report, file_names = generate_report(data, results, config) - # Écrire le rapport - write_report(report, TEMPLATE_PATH) - # Générer l'analyse par l'IA du rapport compler - analyse_finale = nettoyer_texte_fr(ia_analyse(file_names)) - write_report(analyse_finale, TEMP_SECTIONS / TEMPLATE_PATH.name.replace(".md", " - analyse.md")) - -if __name__ == "__main__": - main()