Spaces:
Running
Running
Update src/modules/jasmine_agent.py
Browse files- src/modules/jasmine_agent.py +614 -159
src/modules/jasmine_agent.py
CHANGED
|
@@ -1,184 +1,639 @@
|
|
| 1 |
"""
|
| 2 |
-
MODULE
|
| 3 |
-
========================================
|
| 4 |
Améliorations :
|
| 5 |
-
✅
|
| 6 |
-
✅ Outils pré-codés
|
| 7 |
-
✅
|
| 8 |
-
✅
|
| 9 |
-
✅
|
| 10 |
"""
|
| 11 |
|
| 12 |
-
import google.generativeai as genai
|
| 13 |
-
from groq import Groq
|
| 14 |
-
import json
|
| 15 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
import os
|
| 17 |
-
import
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def __init__(self, data_context=None):
|
| 21 |
-
# Config APIs
|
| 22 |
-
self.google_key = os.environ.get("GOOGLE_API_KEY")
|
| 23 |
-
if not self.google_key and "GOOGLE_API_KEY" in st.secrets:
|
| 24 |
-
self.google_key = st.secrets["GOOGLE_API_KEY"]
|
| 25 |
-
if self.google_key: genai.configure(api_key=self.google_key)
|
| 26 |
-
|
| 27 |
-
self.groq_key = os.environ.get("GROQ_API_KEY")
|
| 28 |
-
if not self.groq_key and "GROQ_API_KEY" in st.secrets:
|
| 29 |
-
self.groq_key = st.secrets["GROQ_API_KEY"]
|
| 30 |
-
self.groq_client = Groq(api_key=self.groq_key) if self.groq_key else None
|
| 31 |
-
|
| 32 |
-
# Cascade de modèles
|
| 33 |
-
self.MODEL_CASCADE = [
|
| 34 |
-
"gemini-2.0-flash-exp",
|
| 35 |
-
"llama-3.3-70b-versatile",
|
| 36 |
-
"gemini-2.0-flash-lite",
|
| 37 |
-
"llama-3.1-8b-instant"
|
| 38 |
-
]
|
| 39 |
-
|
| 40 |
-
# Contexte data (RAG)
|
| 41 |
-
self.data_context = data_context or {}
|
| 42 |
-
|
| 43 |
-
def build_system_prompt(self):
|
| 44 |
-
"""Système prompt avec RAG complet"""
|
| 45 |
-
|
| 46 |
-
# Extraction du contexte
|
| 47 |
-
sheets_info = self.data_context.get('sheets_columns', {})
|
| 48 |
-
graph_stats = self.data_context.get('graph_stats', {})
|
| 49 |
-
communities = self.data_context.get('communities_cache', {})
|
| 50 |
-
|
| 51 |
-
# Construction dynamique du contexte Excel
|
| 52 |
-
excel_context = "\n STRUCTURE DES DONNÉES EXCEL :\n"
|
| 53 |
-
for sheet, cols in sheets_info.items():
|
| 54 |
-
excel_context += f"\nFeuille '{sheet}' : {', '.join(cols)}\n"
|
| 55 |
-
|
| 56 |
-
# Stats du graphe
|
| 57 |
-
graph_context = f"""
|
| 58 |
-
STATISTIQUES DU GRAPHE :
|
| 59 |
-
- Nœuds totaux : {graph_stats.get('total_nodes', 0)}
|
| 60 |
-
- Relations : {graph_stats.get('total_edges', 0)}
|
| 61 |
-
- Communautés détectées : {graph_stats.get('num_communities', 0)}
|
| 62 |
-
- Nœuds à risque : {graph_stats.get('fraud_nodes', 0)}
|
| 63 |
-
"""
|
| 64 |
-
|
| 65 |
-
return f"""
|
| 66 |
-
Tu es JASMINE, Analyste de Données Expert spécialisée en Graphes RDF.
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
-
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
- CORRECT : `attrs = G.nodes['Client:CLI-2026-0001']`
|
| 105 |
-
- ❌ FAUX : `noeud[0].get('Ville')` → noeud est une liste d'IDs, pas de dicts !
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
-
for
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
MODULE ONTOLOGY GRAPH - V20 PROFESSIONAL
|
| 3 |
+
=========================================
|
| 4 |
Améliorations :
|
| 5 |
+
✅ Cache déterministe des communautés
|
| 6 |
+
✅ Outils pré-codés performants
|
| 7 |
+
✅ Contexte RAG complet pour l'IA
|
| 8 |
+
✅ Formatage automatique des résultats
|
| 9 |
+
✅ Graphe optimisé pour lisibilité
|
| 10 |
"""
|
| 11 |
|
|
|
|
|
|
|
|
|
|
| 12 |
import streamlit as st
|
| 13 |
+
import pandas as pd
|
| 14 |
+
import networkx as nx
|
| 15 |
+
from pyvis.network import Network
|
| 16 |
+
import tempfile
|
| 17 |
+
import streamlit.components.v1 as components
|
| 18 |
+
import json
|
| 19 |
+
import sys
|
| 20 |
import os
|
| 21 |
+
import time
|
| 22 |
+
import io
|
| 23 |
+
import contextlib
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
# --- IMPORTS ---
|
| 26 |
+
try:
|
| 27 |
+
from src.Algorithms.graph_louvain_pagerank import apply_ai_algorithms
|
| 28 |
+
from src.Algorithms.Fraud_Detection import detect_fraud_logic
|
| 29 |
+
from src.modules.jasmine_agent import JasmineAgent
|
| 30 |
+
except ImportError:
|
| 31 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
|
| 32 |
+
from src.Algorithms.graph_louvain_pagerank import apply_ai_algorithms
|
| 33 |
+
from src.Algorithms.Fraud_Detection import detect_fraud_logic
|
| 34 |
+
from src.modules.jasmine_agent import JasmineAgent
|
| 35 |
|
| 36 |
+
# ==============================================================================
|
| 37 |
+
# 1. OUTILS PRÉ-CODÉS (Plus de recalculs aléatoires)
|
| 38 |
+
# ==============================================================================
|
| 39 |
|
| 40 |
+
def search_by_id(entity_id, G):
|
| 41 |
+
"""Recherche optimisée par ID avec formatage automatique"""
|
| 42 |
+
# Normalisation de l'ID
|
| 43 |
+
if ':' not in entity_id:
|
| 44 |
+
# Recherche partielle
|
| 45 |
+
matches = [n for n in G.nodes if entity_id.upper() in n.upper()]
|
| 46 |
+
if not matches:
|
| 47 |
+
return f"❌ Aucune entité trouvée pour '{entity_id}'"
|
| 48 |
+
entity_id = matches[0]
|
| 49 |
+
|
| 50 |
+
if entity_id not in G.nodes:
|
| 51 |
+
return f"❌ L'entité '{entity_id}' n'existe pas dans le graphe"
|
| 52 |
+
|
| 53 |
+
# Récupération des attributs
|
| 54 |
+
attrs = dict(G.nodes[entity_id])
|
| 55 |
+
|
| 56 |
+
# Formatage en tableau
|
| 57 |
+
result = f"### 📋 Profil de {attrs.get('label', entity_id)}\n\n"
|
| 58 |
+
result += "| Propriété | Valeur |\n|-----------|--------|\n"
|
| 59 |
+
|
| 60 |
+
priority_keys = ['label', 'Nom', 'Ville', 'Profession', 'Date_Naissance', 'Montant', 'Statut']
|
| 61 |
+
displayed = set()
|
| 62 |
+
|
| 63 |
+
for key in priority_keys:
|
| 64 |
+
if key in attrs and key not in ['group', 'color', 'shape', 'title', 'size']:
|
| 65 |
+
result += f"| {key} | {attrs[key]} |\n"
|
| 66 |
+
displayed.add(key)
|
| 67 |
+
|
| 68 |
+
for key, val in attrs.items():
|
| 69 |
+
if key not in displayed and key not in ['group', 'color', 'shape', 'title', 'size', 'pagerank_score', 'community_id']:
|
| 70 |
+
result += f"| {key} | {val} |\n"
|
| 71 |
+
|
| 72 |
+
# Relations
|
| 73 |
+
neighbors = list(G.successors(entity_id))
|
| 74 |
+
if neighbors:
|
| 75 |
+
result += f"\n### 🔗 Relations ({len(neighbors)})\n\n"
|
| 76 |
+
result += "| Type | Entité | Relation |\n|------|--------|----------|\n"
|
| 77 |
+
for n in neighbors[:10]:
|
| 78 |
+
edge_data = G[entity_id][n]
|
| 79 |
+
n_label = G.nodes[n].get('label', n)
|
| 80 |
+
n_type = G.nodes[n].get('group', 'Inconnu')
|
| 81 |
+
rel = edge_data.get('label', 'lié_à')
|
| 82 |
+
result += f"| {n_type} | {n_label} | {rel} |\n"
|
| 83 |
+
|
| 84 |
+
return result
|
| 85 |
|
| 86 |
+
def search_by_attribute(attr_name, value, G):
|
| 87 |
+
"""Recherche par attribut (Ville, Profession, etc.)"""
|
| 88 |
+
matches = []
|
| 89 |
+
for node, data in G.nodes(data=True):
|
| 90 |
+
if data.get(attr_name) == value:
|
| 91 |
+
matches.append((node, data.get('label', node), data.get('group', 'N/A')))
|
| 92 |
+
|
| 93 |
+
if not matches:
|
| 94 |
+
return f"❌ Aucune entité avec {attr_name}='{value}'"
|
| 95 |
+
|
| 96 |
+
result = f"### 🔍 Entités avec {attr_name} = '{value}' ({len(matches)} résultats)\n\n"
|
| 97 |
+
result += "| ID | Nom | Type |\n|----|-----|------|\n"
|
| 98 |
+
for node_id, label, group in matches[:50]:
|
| 99 |
+
result += f"| {node_id} | {label} | {group} |\n"
|
| 100 |
+
|
| 101 |
+
return result
|
| 102 |
|
| 103 |
+
def get_community_stats(communities_cache):
|
| 104 |
+
"""Statistiques des communautés (Pré-calculées)"""
|
| 105 |
+
if not communities_cache:
|
| 106 |
+
return "❌ Aucune communauté détectée"
|
| 107 |
+
|
| 108 |
+
result = "### 🏘️ Secteurs Détectés (Algorithme Louvain)\n\n"
|
| 109 |
+
result += "| ID | Nom du Secteur | Membres | Leader (PageRank) |\n"
|
| 110 |
+
result += "|----|----------------|---------|-------------------|\n"
|
| 111 |
+
|
| 112 |
+
for cid, info in sorted(communities_cache.items(), key=lambda x: x[1]['size'], reverse=True):
|
| 113 |
+
result += f"| {cid} | {info['alias']} | {info['size']} | {info['leader_name']} |\n"
|
| 114 |
+
|
| 115 |
+
return result
|
| 116 |
|
| 117 |
+
def get_community_details(community_id, G):
|
| 118 |
+
"""Liste tous les membres d'une communauté"""
|
| 119 |
+
members = [(n, G.nodes[n]) for n in G.nodes if G.nodes[n].get('community_id') == community_id]
|
| 120 |
+
|
| 121 |
+
if not members:
|
| 122 |
+
return f"❌ Aucun membre dans la communauté {community_id}"
|
| 123 |
+
|
| 124 |
+
result = f"### 👥 Membres du Secteur #{community_id} ({len(members)} personnes)\n\n"
|
| 125 |
+
result += "| ID | Nom | Type | Influence |\n|----|-----|------|------------|\n"
|
| 126 |
+
|
| 127 |
+
for node_id, data in sorted(members, key=lambda x: x[1].get('pagerank_score', 0), reverse=True)[:30]:
|
| 128 |
+
label = data.get('label', node_id)
|
| 129 |
+
group = data.get('group', 'N/A')
|
| 130 |
+
pr = data.get('pagerank_score', 0)
|
| 131 |
+
result += f"| {node_id} | {label} | {group} | {pr:.4f} |\n"
|
| 132 |
+
|
| 133 |
+
return result
|
| 134 |
|
| 135 |
+
def get_fraud_report(df_fraud):
|
| 136 |
+
"""Affiche le rapport de fraude formaté"""
|
| 137 |
+
if df_fraud.empty:
|
| 138 |
+
return "✅ Aucune anomalie détectée"
|
| 139 |
+
|
| 140 |
+
result = f"### 🚨 Rapport de Fraude ({len(df_fraud)} anomalies)\n\n"
|
| 141 |
+
result += df_fraud.to_markdown(index=False)
|
| 142 |
+
return result
|
| 143 |
|
| 144 |
+
# ==============================================================================
|
| 145 |
+
# 2. MOTEUR D'EXÉCUTION + VALIDATION
|
| 146 |
+
# ==============================================================================
|
| 147 |
|
| 148 |
+
def validate_code_security(code_str):
|
| 149 |
+
"""Validation de sécurité du code Python"""
|
| 150 |
+
dangerous_patterns = [
|
| 151 |
+
'import os', 'import sys', 'import subprocess',
|
| 152 |
+
'__import__', 'eval(', 'exec(', 'open(',
|
| 153 |
+
'st.session_state', 'st.secrets'
|
| 154 |
+
]
|
| 155 |
+
|
| 156 |
+
for pattern in dangerous_patterns:
|
| 157 |
+
if pattern in code_str:
|
| 158 |
+
return False, f"⚠️ Code interdit : '{pattern}' n'est pas autorisé"
|
| 159 |
+
|
| 160 |
+
return True, "OK"
|
| 161 |
|
| 162 |
+
def execute_generated_code(code_str, G, df_fraud):
|
| 163 |
+
"""Exécute le code avec validation de sécurité"""
|
| 164 |
+
# Validation
|
| 165 |
+
is_safe, msg = validate_code_security(code_str)
|
| 166 |
+
if not is_safe:
|
| 167 |
+
return msg
|
| 168 |
+
|
| 169 |
+
output_capture = io.StringIO()
|
| 170 |
+
local_env = {
|
| 171 |
+
"G": G,
|
| 172 |
+
"df_fraud": df_fraud,
|
| 173 |
+
"nx": nx,
|
| 174 |
+
"pd": pd,
|
| 175 |
+
"result": None
|
| 176 |
+
}
|
| 177 |
|
| 178 |
+
error_msg = None
|
| 179 |
+
output = ""
|
|
|
|
|
|
|
| 180 |
|
| 181 |
+
try:
|
| 182 |
+
with contextlib.redirect_stdout(output_capture):
|
| 183 |
+
exec(code_str, {}, local_env)
|
| 184 |
+
output = output_capture.getvalue()
|
| 185 |
+
except Exception as e:
|
| 186 |
+
error_msg = f"ERREUR PYTHON : {type(e).__name__}: {e}"
|
| 187 |
|
| 188 |
+
if error_msg:
|
| 189 |
+
return f"⚠️ {error_msg}"
|
| 190 |
+
elif output.strip():
|
| 191 |
+
return f"{output}"
|
| 192 |
+
else:
|
| 193 |
+
return "[INFO] Code exécuté sans sortie"
|
| 194 |
|
| 195 |
+
def execute_agent_tool(tool_name, args, G, df_fraud, communities_cache):
|
| 196 |
+
"""Dispatcher des outils avec formatage automatique"""
|
| 197 |
+
result_data = ""
|
| 198 |
+
visual_update = None
|
| 199 |
+
|
| 200 |
+
# === OUTILS PRÉ-CODÉS ===
|
| 201 |
+
if tool_name == "search_by_id":
|
| 202 |
+
entity_id = args.get("entity_id", "")
|
| 203 |
+
result_data = search_by_id(entity_id, G)
|
| 204 |
+
|
| 205 |
+
elif tool_name == "search_by_attribute":
|
| 206 |
+
attr_name = args.get("attr_name", "")
|
| 207 |
+
value = args.get("value", "")
|
| 208 |
+
result_data = search_by_attribute(attr_name, value, G)
|
| 209 |
+
|
| 210 |
+
elif tool_name == "get_community_stats":
|
| 211 |
+
result_data = get_community_stats(communities_cache)
|
| 212 |
+
|
| 213 |
+
elif tool_name == "get_community_details":
|
| 214 |
+
community_id = args.get("community_id")
|
| 215 |
+
result_data = get_community_details(community_id, G)
|
| 216 |
+
|
| 217 |
+
elif tool_name == "get_fraud_report":
|
| 218 |
+
result_data = get_fraud_report(df_fraud)
|
| 219 |
+
|
| 220 |
+
# === ACTIONS VISUELLES ===
|
| 221 |
+
elif tool_name == "highlight_community":
|
| 222 |
+
tid = args.get("target_id")
|
| 223 |
+
if tid is not None:
|
| 224 |
+
visual_update = {"action": "highlight_community", "target_id": int(tid)}
|
| 225 |
+
result_data = f"✅ Zoom visuel activé sur le secteur #{tid}"
|
| 226 |
+
|
| 227 |
+
elif tool_name == "highlight_node":
|
| 228 |
+
node_id = args.get("node_id", "")
|
| 229 |
+
if node_id in G.nodes:
|
| 230 |
+
visual_update = {"action": "highlight_node", "target_id": node_id}
|
| 231 |
+
result_data = f"✅ Zoom sur {node_id}"
|
| 232 |
+
else:
|
| 233 |
+
result_data = f"❌ Nœud '{node_id}' introuvable"
|
| 234 |
+
|
| 235 |
+
elif tool_name == "reset_view":
|
| 236 |
+
visual_update = {"action": "reset_view"}
|
| 237 |
+
result_data = "✅ Vue réinitialisée"
|
| 238 |
+
|
| 239 |
+
# === INTERPRÉTEUR PYTHON (Dernier recours) ===
|
| 240 |
+
elif tool_name == "python_interpreter":
|
| 241 |
+
code = args.get("code", "")
|
| 242 |
+
if not code:
|
| 243 |
+
result_data = "❌ Code vide"
|
| 244 |
+
else:
|
| 245 |
+
result_data = execute_generated_code(code, G, df_fraud)
|
| 246 |
+
|
| 247 |
+
else:
|
| 248 |
+
result_data = f"❌ Outil inconnu : {tool_name}"
|
| 249 |
+
|
| 250 |
+
return result_data, visual_update
|
| 251 |
|
| 252 |
+
# ==============================================================================
|
| 253 |
+
# 3. STYLE (Inchangé)
|
| 254 |
+
# ==============================================================================
|
| 255 |
+
def apply_gotham_theme():
|
| 256 |
+
st.markdown("""
|
| 257 |
+
<style>
|
| 258 |
+
.stApp { background-color: #0d1117 !important; color: #c9d1d9; }
|
| 259 |
+
.ontology-title { color: #ffffff !important; font-family: 'Space Grotesk', sans-serif !important; text-transform: uppercase; font-weight: 700; font-size: 1.4rem; margin: 0; padding: 0; letter-spacing: 1.5px; }
|
| 260 |
+
.aip-card-header { background-color: #21262d; border: 1px solid #30363d; border-bottom: none; border-radius: 6px 6px 0 0; padding: 12px 20px; font-family: 'Space Grotesk', sans-serif; font-size: 0.8rem; font-weight: 600; color: #8b949e; display: flex; justify-content: space-between; align-items: center; letter-spacing: 1px; }
|
| 261 |
+
[data-testid="stVerticalBlock"] [data-testid="stVerticalBlockBorderWrapper"] > div { background-color: #161b22; border-radius: 0 0 6px 6px !important; border: 1px solid #30363d !important; border-top: none !important; }
|
| 262 |
+
.user-msg { text-align: right; color: #58a6ff; background: rgba(88, 166, 255, 0.05); border: 1px solid rgba(88, 166, 255, 0.2); padding: 12px; border-radius: 6px; margin-bottom: 10px; font-size: 0.9rem; font-family: 'Inter', sans-serif; }
|
| 263 |
+
.bot-msg { text-align: left; color: #c9d1d9; background: rgba(48, 54, 61, 0.5); border: 1px solid #30363d; padding: 12px; border-radius: 6px; margin-bottom: 10px; font-size: 0.9rem; font-family: 'Inter', sans-serif; white-space: pre-wrap; }
|
| 264 |
+
code { color: #e83e8c; background: rgba(255,255,255,0.1); padding: 2px 4px; border-radius: 4px; }
|
| 265 |
+
pre { background: #0d1117 !important; border: 1px solid #30363d; border-radius: 6px; padding: 10px; }
|
| 266 |
+
iframe { border: 1px solid #30363d !important; border-radius: 6px; }
|
| 267 |
+
</style>
|
| 268 |
+
""", unsafe_allow_html=True)
|
| 269 |
+
|
| 270 |
+
# ==============================================================================
|
| 271 |
+
# 4. DATA INGESTION (Optimisée avec cache)
|
| 272 |
+
# ==============================================================================
|
| 273 |
+
def get_fixed_colors():
|
| 274 |
+
return {
|
| 275 |
+
"Client": "#1E88E5",
|
| 276 |
+
"Garant": "#8E44AD",
|
| 277 |
+
"Pret": "#F39C12",
|
| 278 |
+
"Telephone": "#E74C3C",
|
| 279 |
+
"Email": "#D35400",
|
| 280 |
+
"Adresse": "#27AE60",
|
| 281 |
+
"Transaction": "#00BCD4",
|
| 282 |
+
"Prop": "#6C757D"
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
def get_node_style(entity_type):
|
| 286 |
+
clean = str(entity_type).strip()
|
| 287 |
+
fixed = get_fixed_colors()
|
| 288 |
+
return fixed.get(clean, "#34495E"), "dot"
|
| 289 |
+
|
| 290 |
+
def safe_open_sheet(client, name):
|
| 291 |
+
for i in range(5):
|
| 292 |
+
try: return client.open(name)
|
| 293 |
+
except: time.sleep(1)
|
| 294 |
+
return None
|
| 295 |
+
|
| 296 |
+
def safe_get_records(sh, w_name):
|
| 297 |
+
for i in range(3):
|
| 298 |
+
try: return pd.DataFrame(sh.worksheet(w_name).get_all_records())
|
| 299 |
+
except: time.sleep(1)
|
| 300 |
+
return pd.DataFrame()
|
| 301 |
+
|
| 302 |
+
def extract_triplets(ontology_df, client, sheet_name):
|
| 303 |
+
"""Extraction avec cache pour le RAG"""
|
| 304 |
+
triplets = []
|
| 305 |
+
sh = safe_open_sheet(client, sheet_name)
|
| 306 |
+
if not sh: return pd.DataFrame(), {}
|
| 307 |
+
|
| 308 |
+
# 1. Chargement de toutes les feuilles
|
| 309 |
+
cache = {}
|
| 310 |
+
sheets_columns = {} # Pour le RAG
|
| 311 |
+
|
| 312 |
+
for s in ontology_df['Sheet'].unique():
|
| 313 |
+
df = safe_get_records(sh, s)
|
| 314 |
+
if not df.empty:
|
| 315 |
+
df.columns = df.columns.str.strip()
|
| 316 |
+
cache[s] = df
|
| 317 |
+
sheets_columns[s] = df.columns.tolist()
|
| 318 |
+
|
| 319 |
+
# 2. Fusion des données par ID
|
| 320 |
+
master_data = {}
|
| 321 |
+
for sheet_name, df in cache.items():
|
| 322 |
+
id_col = next((c for c in df.columns if any(x in c for x in ['ID_Client', 'ID_Garant', 'ClientID', 'GarantID'])), None)
|
| 323 |
+
if id_col:
|
| 324 |
+
for _, row in df.iterrows():
|
| 325 |
+
uid = str(row[id_col]).strip().upper()
|
| 326 |
+
if uid not in master_data: master_data[uid] = {}
|
| 327 |
+
master_data[uid].update({k: v for k, v in row.to_dict().items() if v and str(v).strip()})
|
| 328 |
+
|
| 329 |
+
# 3. Création des triplets
|
| 330 |
+
for _, rule in ontology_df.iterrows():
|
| 331 |
+
df = cache.get(rule['Sheet'])
|
| 332 |
+
if df is None or rule['SubjectCol'] not in df.columns: continue
|
| 333 |
|
| 334 |
+
for _, row in df.iterrows():
|
| 335 |
+
s_val = str(row.get(rule['SubjectCol'], '')).strip().upper()
|
| 336 |
+
if not s_val: continue
|
| 337 |
+
|
| 338 |
+
s_id = f"{rule['SubjectClass']}:{s_val}"
|
| 339 |
+
full_props = master_data.get(s_val, row.to_dict())
|
| 340 |
+
|
| 341 |
+
if rule['ObjectType'] == 'relation':
|
| 342 |
+
ocol = str(rule['ObjectColOrConcept'])
|
| 343 |
+
oval = str(row.get(ocol, '')).strip()
|
| 344 |
+
if oval:
|
| 345 |
+
o_cls = ocol.replace("ID_", "") if "ID_" in ocol and ocol != "ID_Officiel" else "ID_Officiel" if ocol == "ID_Officiel" else ocol
|
| 346 |
+
triplets.append({
|
| 347 |
+
"subject": s_id,
|
| 348 |
+
"predicate": rule['Predicate'],
|
| 349 |
+
"object": f"{o_cls}:{oval.upper()}",
|
| 350 |
+
"object_type": "entity",
|
| 351 |
+
"subject_props": full_props
|
| 352 |
+
})
|
| 353 |
+
elif rule['ObjectType'] == 'data_property':
|
| 354 |
+
oval = str(row.get(rule['ObjectColOrConcept'], '')).strip()
|
| 355 |
+
if oval:
|
| 356 |
+
triplets.append({
|
| 357 |
+
"subject": s_id,
|
| 358 |
+
"predicate": rule['Predicate'],
|
| 359 |
+
"object": oval,
|
| 360 |
+
"object_type": "literal",
|
| 361 |
+
"subject_props": full_props
|
| 362 |
+
})
|
| 363 |
+
|
| 364 |
+
return pd.DataFrame(triplets), sheets_columns
|
| 365 |
+
|
| 366 |
+
def build_communities_cache(G):
|
| 367 |
+
"""Construit le cache déterministe des communautés"""
|
| 368 |
+
communities = {}
|
| 369 |
+
comm_members = {}
|
| 370 |
+
|
| 371 |
+
# Groupement par communauté
|
| 372 |
+
for n, d in G.nodes(data=True):
|
| 373 |
+
cid = d.get('community_id')
|
| 374 |
+
if cid is not None:
|
| 375 |
+
if cid not in comm_members:
|
| 376 |
+
comm_members[cid] = []
|
| 377 |
+
comm_members[cid].append(n)
|
| 378 |
+
|
| 379 |
+
# Calcul des infos pour chaque communauté
|
| 380 |
+
for cid, members in comm_members.items():
|
| 381 |
+
# Leader = plus haut PageRank
|
| 382 |
+
leader = max(members, key=lambda n: G.nodes[n].get('pagerank_score', 0))
|
| 383 |
+
leader_name = G.nodes[leader].get('Nom', G.nodes[leader].get('label', leader))
|
| 384 |
+
|
| 385 |
+
# Alias
|
| 386 |
+
alias = f"Secteur {leader_name}"
|
| 387 |
+
|
| 388 |
+
communities[cid] = {
|
| 389 |
+
'size': len(members),
|
| 390 |
+
'leader': leader,
|
| 391 |
+
'leader_name': leader_name,
|
| 392 |
+
'alias': alias,
|
| 393 |
+
'members': members
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
return communities
|
| 397 |
+
|
| 398 |
+
def apply_visual_actions(G, action_data):
|
| 399 |
+
"""Application des actions visuelles"""
|
| 400 |
+
if not action_data: return G
|
| 401 |
+
action_type = action_data.get("action")
|
| 402 |
+
|
| 403 |
+
if action_type == "highlight_community":
|
| 404 |
+
target_id = action_data.get("target_id")
|
| 405 |
+
for node in G.nodes:
|
| 406 |
+
if G.nodes[node].get('community_id') != target_id:
|
| 407 |
+
G.nodes[node]['color'] = 'rgba(50, 50, 50, 0.1)'
|
| 408 |
+
G.nodes[node]['opacity'] = 0.1
|
| 409 |
+
G.nodes[node]['borderWidth'] = 0
|
| 410 |
+
else:
|
| 411 |
+
G.nodes[node]['borderWidth'] = 4
|
| 412 |
+
|
| 413 |
+
elif action_type == "highlight_node":
|
| 414 |
+
target_id = action_data.get("target_id")
|
| 415 |
+
for node in G.nodes:
|
| 416 |
+
if node != target_id:
|
| 417 |
+
G.nodes[node]['color'] = 'rgba(50, 50, 50, 0.1)'
|
| 418 |
+
G.nodes[node]['opacity'] = 0.1
|
| 419 |
+
else:
|
| 420 |
+
G.nodes[node]['borderWidth'] = 5
|
| 421 |
+
G.nodes[node]['size'] = 50
|
| 422 |
+
|
| 423 |
+
elif action_type == "highlight_risk":
|
| 424 |
+
for node in G.nodes:
|
| 425 |
+
if G.nodes[node].get('color') != "#FF0000":
|
| 426 |
+
G.nodes[node]['color'] = 'rgba(50, 50, 50, 0.1)'
|
| 427 |
+
G.nodes[node]['opacity'] = 0.1
|
| 428 |
+
|
| 429 |
+
return G
|
| 430 |
+
|
| 431 |
+
# ==============================================================================
|
| 432 |
+
# 5. MAIN ORCHESTRATOR
|
| 433 |
+
# ==============================================================================
|
| 434 |
+
def show_ontology_graph(client, sheet_name):
|
| 435 |
+
apply_gotham_theme()
|
| 436 |
+
|
| 437 |
+
# API KEYS
|
| 438 |
+
try:
|
| 439 |
+
if "GOOGLE_API_KEY" in st.secrets and not os.environ.get("GOOGLE_API_KEY"):
|
| 440 |
+
os.environ["GOOGLE_API_KEY"] = st.secrets["GOOGLE_API_KEY"]
|
| 441 |
+
if "GROQ_API_KEY" in st.secrets and not os.environ.get("GROQ_API_KEY"):
|
| 442 |
+
os.environ["GROQ_API_KEY"] = st.secrets["GROQ_API_KEY"]
|
| 443 |
+
except: pass
|
| 444 |
+
|
| 445 |
+
# Session state
|
| 446 |
+
if "chat_history" not in st.session_state:
|
| 447 |
+
st.session_state["chat_history"] = []
|
| 448 |
+
if "current_visual_action" not in st.session_state:
|
| 449 |
+
st.session_state["current_visual_action"] = None
|
| 450 |
+
if "jasmine_active" not in st.session_state:
|
| 451 |
+
st.session_state["jasmine_active"] = False
|
| 452 |
+
if "communities_cache" not in st.session_state:
|
| 453 |
+
st.session_state["communities_cache"] = {}
|
| 454 |
+
|
| 455 |
+
# UI
|
| 456 |
+
c1, c2 = st.columns([5, 1])
|
| 457 |
+
with c1:
|
| 458 |
+
st.markdown('<h1 class="ontology-title">JUMEAU NUMÉRIQUE - VUE OPÉRATIONNELLE</h1>', unsafe_allow_html=True)
|
| 459 |
+
with c2:
|
| 460 |
+
if st.toggle("TERMINAL", value=st.session_state["jasmine_active"]):
|
| 461 |
+
st.session_state["jasmine_active"] = True
|
| 462 |
+
else:
|
| 463 |
+
st.session_state["jasmine_active"] = False
|
| 464 |
+
|
| 465 |
+
# DATA LOADING
|
| 466 |
+
ontology_df = pd.DataFrame(safe_get_records(safe_open_sheet(client, sheet_name), "Ontology"))
|
| 467 |
+
if ontology_df.empty:
|
| 468 |
+
st.caption("Chargement...");
|
| 469 |
+
return
|
| 470 |
+
|
| 471 |
+
triplets_df, sheets_columns = extract_triplets(ontology_df, client, sheet_name)
|
| 472 |
+
if triplets_df.empty:
|
| 473 |
+
st.error("Aucune donnée.");
|
| 474 |
+
return
|
| 475 |
+
|
| 476 |
+
# GRAPH BUILD
|
| 477 |
+
G = nx.DiGraph()
|
| 478 |
+
for _, r in triplets_df.iterrows():
|
| 479 |
+
s, p, o = r['subject'], r['predicate'], r['object']
|
| 480 |
+
if s not in G.nodes:
|
| 481 |
+
c, sh = get_node_style(s.split(':')[0])
|
| 482 |
+
props = r.get('subject_props', {})
|
| 483 |
+
G.add_node(s, label=s.split(':')[1] if ':' in s else s, group=s.split(':')[0],
|
| 484 |
+
color=c, shape=sh, title=str(props), **props)
|
| 485 |
+
|
| 486 |
+
if r['object_type'] == 'entity':
|
| 487 |
+
if o not in G.nodes:
|
| 488 |
+
c, sh = get_node_style(o.split(':')[0])
|
| 489 |
+
G.add_node(o, label=o.split(':')[1] if ':' in o else o, group=o.split(':')[0],
|
| 490 |
+
color=c, shape=sh, title=o)
|
| 491 |
+
G.add_edge(s, o, label=p)
|
| 492 |
+
else:
|
| 493 |
+
pid = f"Prop:{hash(o)%10000}"
|
| 494 |
+
G.add_node(pid, label=o[:20], group="Prop", color="#6C757D", shape="text", size=12, font={'size': 10})
|
| 495 |
+
G.add_edge(s, pid, label=p, dashes=True)
|
| 496 |
+
|
| 497 |
+
# Algorithmes
|
| 498 |
+
G, risk_nodes, df_fraud = detect_fraud_logic(G)
|
| 499 |
+
G, ai_metadata = apply_ai_algorithms(G)
|
| 500 |
+
|
| 501 |
+
# Cache des communautés (une seule fois)
|
| 502 |
+
if not st.session_state["communities_cache"]:
|
| 503 |
+
st.session_state["communities_cache"] = build_communities_cache(G)
|
| 504 |
+
|
| 505 |
+
# Contexte pour le RAG
|
| 506 |
+
data_context = {
|
| 507 |
+
'sheets_columns': sheets_columns,
|
| 508 |
+
'graph_stats': {
|
| 509 |
+
'total_nodes': G.number_of_nodes(),
|
| 510 |
+
'total_edges': G.number_of_edges(),
|
| 511 |
+
'num_communities': ai_metadata.get('louvain_communities_count', 0),
|
| 512 |
+
'fraud_nodes': len(risk_nodes)
|
| 513 |
+
},
|
| 514 |
+
'communities_cache': st.session_state["communities_cache"]
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
# === CHAT & EXECUTION ===
|
| 518 |
+
if st.session_state["jasmine_active"]:
|
| 519 |
+
col_chat, col_graph = st.columns([2, 3], gap="medium")
|
| 520 |
+
|
| 521 |
+
with col_chat:
|
| 522 |
+
st.markdown("""<div class="aip-card-header"><span>AIP TERMINAL</span><span style="color:#00E676;">● ONLINE</span></div>""", unsafe_allow_html=True)
|
| 523 |
+
|
| 524 |
+
with st.container(height=700):
|
| 525 |
+
if not st.session_state["chat_history"]:
|
| 526 |
+
st.markdown('<div class="bot-msg">🟢 Jasmine Core Initialized<br>Type votre requête...</div>', unsafe_allow_html=True)
|
| 527 |
|
| 528 |
+
for msg in st.session_state["chat_history"]:
|
| 529 |
+
if msg.get("type") == "tool_result":
|
| 530 |
+
continue
|
| 531 |
+
css = "user-msg" if msg["role"] == "user" else "bot-msg"
|
| 532 |
+
content = msg["content"].replace("\n", "<br>")
|
| 533 |
+
st.markdown(f'<div class="{css}">{content}</div>', unsafe_allow_html=True)
|
| 534 |
|
| 535 |
+
if prompt := st.chat_input("Ordre..."):
|
| 536 |
+
st.session_state["chat_history"].append({"role": "user", "content": prompt})
|
| 537 |
+
|
| 538 |
+
# Initialisation de l'agent avec contexte
|
| 539 |
+
agent = JasmineAgent(data_context=data_context)
|
| 540 |
+
resp_text, tool_action = agent.ask(prompt, st.session_state["chat_history"])
|
| 541 |
+
|
| 542 |
+
# Réponse immédiate de l'IA
|
| 543 |
+
if resp_text:
|
| 544 |
+
st.session_state["chat_history"].append({"role": "assistant", "content": resp_text})
|
| 545 |
+
|
| 546 |
+
# Exécution de l'outil si demandé
|
| 547 |
+
if tool_action:
|
| 548 |
+
with st.spinner("⚙️ Exécution..."):
|
| 549 |
+
exec_result, visual_update = execute_agent_tool(
|
| 550 |
+
tool_action.get("tool"),
|
| 551 |
+
tool_action.get("args", {}),
|
| 552 |
+
G,
|
| 553 |
+
df_fraud,
|
| 554 |
+
st.session_state["communities_cache"]
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
# Stockage caché du résultat technique
|
| 558 |
+
st.session_state["chat_history"].append({
|
| 559 |
+
"role": "user",
|
| 560 |
+
"content": f"[RÉSULTAT OUTIL]\n{exec_result}",
|
| 561 |
+
"type": "tool_result"
|
| 562 |
+
})
|
| 563 |
+
|
| 564 |
+
if visual_update:
|
| 565 |
+
st.session_state["current_visual_action"] = visual_update
|
| 566 |
+
|
| 567 |
+
# L'IA synthétise le résultat
|
| 568 |
+
final_resp, _ = agent.ask(
|
| 569 |
+
"Synthétise ce résultat en langage naturel pour l'utilisateur.",
|
| 570 |
+
st.session_state["chat_history"]
|
| 571 |
+
)
|
| 572 |
+
|
| 573 |
+
if final_resp:
|
| 574 |
+
st.session_state["chat_history"].append({"role": "assistant", "content": final_resp})
|
| 575 |
+
|
| 576 |
+
st.rerun()
|
| 577 |
+
else:
|
| 578 |
+
col_graph = st.container()
|
| 579 |
|
| 580 |
+
# === GRAPH RENDER (Optimisé) ===
|
| 581 |
+
with col_graph:
|
| 582 |
+
if st.session_state["current_visual_action"]:
|
| 583 |
+
G = apply_visual_actions(G, st.session_state["current_visual_action"])
|
| 584 |
+
|
| 585 |
+
nt = Network(height="740px", width="100%", bgcolor="#0d1117", font_color="#c9d1d9")
|
| 586 |
+
nt.from_nx(G)
|
| 587 |
+
|
| 588 |
+
# Options optimisées pour lisibilité
|
| 589 |
+
options = {
|
| 590 |
+
"physics": {
|
| 591 |
+
"forceAtlas2Based": {
|
| 592 |
+
"gravitationalConstant": -120,
|
| 593 |
+
"centralGravity": 0.015,
|
| 594 |
+
"springLength": 250,
|
| 595 |
+
"springConstant": 0.05,
|
| 596 |
+
"damping": 0.4,
|
| 597 |
+
"avoidOverlap": 0.8
|
| 598 |
+
},
|
| 599 |
+
"solver": "forceAtlas2Based",
|
| 600 |
+
"stabilization": {"iterations": 200},
|
| 601 |
+
"minVelocity": 0.5
|
| 602 |
+
},
|
| 603 |
+
"nodes": {
|
| 604 |
+
"font": {"size": 14, "face": "Inter", "color": "#ffffff"},
|
| 605 |
+
"borderWidth": 2,
|
| 606 |
+
"borderWidthSelected": 4
|
| 607 |
+
},
|
| 608 |
+
"edges": {
|
| 609 |
+
"smooth": {"type": "continuous"},
|
| 610 |
+
"font": {"size": 11, "align": "middle", "color": "#8b949e"},
|
| 611 |
+
"arrows": {"to": {"enabled": True, "scaleFactor": 0.5}}
|
| 612 |
+
},
|
| 613 |
+
"interaction": {
|
| 614 |
+
"hover": True,
|
| 615 |
+
"navigationButtons": False,
|
| 616 |
+
"keyboard": True,
|
| 617 |
+
"zoomView": True
|
| 618 |
+
}
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
nt.set_options(json.dumps(options))
|
| 622 |
+
path = tempfile.gettempdir() + "/ontology_viz.html"
|
| 623 |
+
nt.save_graph(path)
|
| 624 |
+
|
| 625 |
+
with open(path, 'r', encoding='utf-8') as f:
|
| 626 |
+
html_content = f.read()
|
| 627 |
+
|
| 628 |
+
custom_css = """<style>
|
| 629 |
+
body { background-color: #0d1117 !important; margin: 0; padding: 0; }
|
| 630 |
+
#mynetwork { background-color: #0d1117 !important; width: 100%; height: 740px; }
|
| 631 |
+
</style>"""
|
| 632 |
+
html_content = html_content.replace('</head>', f'{custom_css}</head>')
|
| 633 |
+
|
| 634 |
+
components.html(html_content, height=740, scrolling=False)
|
| 635 |
+
|
| 636 |
+
if st.session_state["current_visual_action"]:
|
| 637 |
+
if st.button("RÉINITIALISER VUE", key="rst_btn"):
|
| 638 |
+
st.session_state["current_visual_action"] = None
|
| 639 |
+
st.rerun()
|