klydekushy commited on
Commit
26568c8
·
verified ·
1 Parent(s): bdb14b5

Update src/modules/jasmine_agent.py

Browse files
Files changed (1) hide show
  1. src/modules/jasmine_agent.py +614 -159
src/modules/jasmine_agent.py CHANGED
@@ -1,184 +1,639 @@
1
  """
2
- MODULE JASMINE AGENT - PROFESSIONAL V25
3
- ========================================
4
  Améliorations :
5
- RAG avec contexte Excel complet
6
- ✅ Outils pré-codés (pas de recalculs)
7
- Accès correct à NetworkX
8
- Validation de sécurité du code
9
- Formatage automatique Markdown
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 re
18
-
19
- class JasmineAgent:
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
- {excel_context}
69
- {graph_context}
 
 
 
 
 
 
 
 
70
 
71
- 🔧 TES OUTILS DISPONIBLES :
 
 
72
 
73
- 1️⃣ RECHERCHE PAR ID (Optimisé - Utilise-le systématiquement)
74
- {{"tool": "search_by_id", "args": {{"entity_id": "CLI-2026-0001"}}}}
75
- Retourne TOUTES les propriétés (Ville, Profession, etc.) + Relations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- 2️⃣ RECHERCHE PAR ATTRIBUT
78
- {{"tool": "search_by_attribute", "args": {{"attr_name": "Ville", "value": "Paris"}}}}
79
- Trouve toutes les entités avec cet attribut
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- 3️⃣ STATISTIQUES COMMUNAUTÉS (Pré-calculées)
82
- {{"tool": "get_community_stats", "args": {{}}}}
83
- Liste des secteurs avec tailles et leaders (DÉJÀ CALCULÉ, pas de variation)
 
 
 
 
 
 
 
 
 
 
84
 
85
- 4️⃣ DÉTAILS D'UNE COMMUNAUTÉ
86
- {{"tool": "get_community_details", "args": {{"community_id": 3}}}}
87
- Liste tous les membres d'un secteur spécifique
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- 5️⃣ RAPPORT DE FRAUDE
90
- {{"tool": "get_fraud_report", "args": {{}}}}
91
- Affiche le tableau complet des anomalies détectées
 
 
 
 
 
92
 
93
- 6️⃣ ZOOM VISUEL
94
- {{"tool": "highlight_community", "args": {{"target_id": 3}}}}
95
- {{"tool": "highlight_node", "args": {{"node_id": "Client:CLI-2026-0001"}}}}
96
 
97
- 7️⃣ CODE PYTHON (Dernier recours uniquement)
98
- {{"tool": "python_interpreter", "args": {{"code": "..."}}}}
 
 
 
 
 
 
 
 
 
 
 
99
 
100
- ⚠️ RÈGLES STRICTES :
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- 🔒 ACCÈS NetworkX :
103
- - CORRECT : `G.nodes['Client:CLI-2026-0001']['Ville']`
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
- 📋 COLONNES EXCEL (Sensible à la casse) :
108
- - ✅ 'Ville', 'Profession', 'Nom', 'Date_Naissance'
109
- - ❌ 'ville', 'profession' n'existent pas
 
 
 
110
 
111
- 🎯 PRIORITÉ DES OUTILS :
112
- 1. Pour chercher un ID → search_by_id
113
- 2. Pour les communautés → get_community_stats (pas de code)
114
- 3. Pour les fraudes → get_fraud_report
115
- 4. Python → SEULEMENT si aucun outil ne convient
 
116
 
117
- 💡 FORMAT DE RÉPONSE :
118
- - Toujours répondre en langage naturel
119
- - Utiliser des tableaux Markdown si pertinent
120
- - Ne jamais demander à l'utilisateur d'exécuter du code
121
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
- def _format_messages_for_groq(self, system_prompt, chat_history, user_message):
124
- msgs = [{"role": "system", "content": system_prompt}]
125
- for m in chat_history[-8:]:
126
- role = "assistant" if m["role"] in ["model", "assistant"] else "user"
127
- content = str(m.get("content", ""))
128
- if content.strip(): msgs.append({"role": role, "content": content})
129
- msgs.append({"role": "user", "content": user_message})
130
- return msgs
131
-
132
- def _format_messages_for_gemini(self, system_prompt, chat_history, user_message):
133
- msgs = [{"role": "user", "parts": [system_prompt]}]
134
- for m in chat_history[-8:]:
135
- role = "user" if m["role"] == "user" else "model"
136
- content = str(m.get("content", ""))
137
- if content.strip(): msgs.append({"role": role, "parts": [content]})
138
- msgs.append({"role": "user", "parts": [user_message]})
139
- return msgs
140
-
141
- def ask(self, user_message, chat_history):
142
- system_prompt = self.build_system_prompt()
143
- last_error = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
- for model_name in self.MODEL_CASCADE:
146
- try:
147
- response_text = ""
148
- if "gemini" in model_name:
149
- if not self.google_key: raise Exception("No Google Key")
150
- formatted_msgs = self._format_messages_for_gemini(system_prompt, chat_history, user_message)
151
- model = genai.GenerativeModel(model_name)
152
- res = model.generate_content(formatted_msgs)
153
- response_text = res.text
154
- else:
155
- if not self.groq_client: raise Exception("No Groq Key")
156
- formatted_msgs = self._format_messages_for_groq(system_prompt, chat_history, user_message)
157
- completion = self.groq_client.chat.completions.create(
158
- model=model_name, messages=formatted_msgs, temperature=0.2, stop=None
159
- )
160
- response_text = completion.choices[0].message.content
161
-
162
- # Extraction de l'action JSON
163
- clean_text = re.sub(r"```json", "", response_text, flags=re.IGNORECASE)
164
- clean_text = re.sub(r"```", "", clean_text).strip()
165
-
166
- action = None
167
- if "{" in clean_text and "}" in clean_text:
168
- try:
169
- json_start = clean_text.find("{")
170
- json_end = clean_text.rfind("}") + 1
171
- json_str = clean_text[json_start:json_end]
172
- action = json.loads(json_str)
173
- # On retire le JSON du texte si présent
174
- clean_text = clean_text[:json_start] + clean_text[json_end:]
175
- clean_text = clean_text.strip()
176
- except: pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
- return clean_text, action
 
 
 
 
 
179
 
180
- except Exception as e:
181
- last_error = str(e)
182
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
- return f"⚠️ ERREUR CASCADE : {last_error}", None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()