klydekushy commited on
Commit
bdb14b5
·
verified ·
1 Parent(s): 634a5fa

Update src/modules/jasmine_agent.py

Browse files
Files changed (1) hide show
  1. src/modules/jasmine_agent.py +160 -136
src/modules/jasmine_agent.py CHANGED
@@ -1,8 +1,12 @@
1
  """
2
- MODULE JASMINE AGENT - REASON & ACT (V30.2)
3
- ===========================================
4
- Agent autonome avec ta cascade de modèles PERSONNALISÉE.
5
- Gère les quotas et utilise le Sandbox Python.
 
 
 
 
6
  """
7
 
8
  import google.generativeai as genai
@@ -13,148 +17,168 @@ import os
13
  import re
14
 
15
  class JasmineAgent:
16
- def __init__(self):
17
- # --- 1. CONFIGURATION DES API (Fallback System) ---
18
- self.api_keys = {
19
- "google": os.environ.get("GOOGLE_API_KEY") or st.secrets.get("GOOGLE_API_KEY"),
20
- "groq": os.environ.get("GROQ_API_KEY") or st.secrets.get("GROQ_API_KEY")
21
- }
22
-
23
- if self.api_keys["google"]:
24
- genai.configure(api_key=self.api_keys["google"])
25
-
26
- if self.api_keys["groq"]:
27
- self.groq_client = Groq(api_key=self.api_keys["groq"])
28
- else:
29
- self.groq_client = None
30
 
31
- # --- TA CASCADE DE MODÈLES EXACTE ---
32
  self.MODEL_CASCADE = [
33
- "gemini-2.5-flash-lite",
34
- "llama-3.1-8b-instant",
35
- "gemini-2.5-flash",
36
- "openai/gpt-oss-20b",
37
- "qwen/qwen3-32b",
38
- "llama-3.3-70b-versatile"
39
  ]
40
 
 
 
 
41
  def build_system_prompt(self):
42
- return """
43
- Tu es JASMINE, une IA Opérationnelle de détection de fraude et d'analyse de graphes.
44
- Tu as un accès DIRECT aux données via un 'Python Sandbox'.
45
 
46
- TES OUTILS (Utilise-les impérativement) :
47
- -----------------------------------------
48
- 1. `python_interpreter` : Pour analyser les données.
49
- - Tu as accès aux variables globales : `G` (Graph NetworkX), `df_fraud` (Pandas), `nx` (NetworkX), `pd` (Pandas).
50
- - Les nœuds du graphe ont des attributs : 'Ville', 'Profession', 'community_id' (Louvain), 'pagerank_score', 'color' (Rouge=Fraude).
51
- - IDs des nœuds : Format 'TYPE:VALEUR' (ex: 'Client:CLI-2026-001').
52
 
53
- 2. `highlight_community` : Pour visualiser un secteur.
54
- - Arg: `target_id` (int) -> L'ID de la communauté (community_id).
55
-
56
- 3. `highlight_risk` : Pour visualiser les fraudes.
57
- - Arg: `show` (bool) -> True.
58
-
59
- RÈGLES D'OR (ANTI-HALLUCINATION) :
60
- ----------------------------------
61
- 1. NE DEVINE JAMAIS UNE DONNÉE. Si on te demande "Qui est le client X ?", écris un code Python pour le chercher dans `G.nodes`.
62
- 2. NE RECALCULE PAS LOUVAIN/PAGERANK. Ils sont DÉJÀ dans les attributs des nœuds (`G.nodes[n]['community_id']`).
63
- 3. RÉPONSES TEXTUELLES OU TABLEAUX. Ne montre pas le code Python à l'utilisateur, sauf s'il le demande explicitement. Donne juste le résultat.
64
- 4. INSPECTION : Si tu as un doute sur une colonne, fais `print(df_fraud.columns)` ou `print(G.nodes[list(G.nodes)[0]])` d'abord.
65
-
66
- FORMAT DE RÉPONSE (JSON STRICT) :
67
- ---------------------------------
68
- Tu dois répondre UNIQUEMENT au format JSON pour appeler un outil, ou en texte brut pour conclure.
69
 
70
- Exemple appel outil :
71
- ```json
72
- { "tool": "python_interpreter", "args": { "code": "print([n for n,d in G.nodes(data=True) if d.get('community_id') == 2])" } }
73
- ```
74
- """
75
-
76
- def _call_llm(self, model_name, messages):
77
- """Fonction générique pour appeler n'importe quel LLM de la cascade"""
78
- try:
79
- # --- GOOGLE GEMINI ---
80
- if "gemini" in model_name:
81
- if not self.api_keys["google"]: raise Exception("No Google Key")
82
- model = genai.GenerativeModel(model_name)
83
- # Conversion format OpenAI -> Gemini
84
- gemini_hist = []
85
- system_instruction = messages[0]["content"]
86
-
87
- # Injection du system prompt
88
- gemini_hist.append({"role": "user", "parts": [system_instruction]})
89
-
90
- for m in messages[1:]:
91
- role = "user" if m["role"] == "user" else "model"
92
- gemini_hist.append({"role": role, "parts": [str(m["content"])]})
93
-
94
- response = model.generate_content(
95
- gemini_hist,
96
- generation_config=genai.types.GenerationConfig(temperature=0.2)
97
- )
98
- return response.text
99
-
100
- # --- GROQ (LLAMA, QWEN, GPT-OSS via API compatible) ---
101
- else:
102
- if not self.groq_client: raise Exception("No Groq Key")
103
- completion = self.groq_client.chat.completions.create(
104
- model=model_name,
105
- messages=messages,
106
- temperature=0.1
107
- )
108
- return completion.choices[0].message.content
109
-
110
- except Exception as e:
111
- # print(f"⚠️ Échec modèle {model_name}: {e}")
112
- return None
113
-
114
- def ask(self, user_message, chat_history, context_result=None):
115
- """
116
- Boucle principale de décision.
117
- """
118
- system_prompt = self.build_system_prompt()
119
-
120
- # Construction de l'historique
121
- messages = [{"role": "system", "content": system_prompt}]
122
 
123
- # On garde les 10 derniers échanges pour le contexte
124
- for m in chat_history[-10:]:
125
- if m.get("type") == "tool_result" and m != chat_history[-1]:
126
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  role = "assistant" if m["role"] in ["model", "assistant"] else "user"
128
- messages.append({"role": role, "content": str(m["content"])})
129
-
130
- # Injection du résultat technique si disponible
131
- if context_result:
132
- messages.append({"role": "user", "content": f"RÉSULTAT DE L'OUTIL (Données Réelles) : {context_result}. Maintenant, synthétise une réponse pour l'utilisateur sans mentionner le code technique."})
133
- else:
134
- messages.append({"role": "user", "content": user_message})
135
-
136
- # Cascade des modèles
137
- for model_name in self.MODEL_CASCADE:
138
- response_text = self._call_llm(model_name, messages)
139
- if response_text:
140
- return self._parse_response(response_text)
141
-
142
- return "⚠️ Service indisponible (Quotas épuisés sur tous les modèles).", None
143
 
144
- def _parse_response(self, text):
145
- """Nettoie la réponse pour extraire le JSON ou le texte."""
146
- clean_text = re.sub(r"```json", "", text, flags=re.IGNORECASE)
147
- clean_text = re.sub(r"```", "", clean_text).strip()
 
 
 
 
 
 
 
 
148
 
149
- # Détection JSON pour action
150
- if "{" in clean_text and "tool" in clean_text:
151
  try:
152
- start = clean_text.find("{")
153
- end = clean_text.rfind("}") + 1
154
- json_str = clean_text[start:end]
155
- action = json.loads(json_str)
156
- return None, action
157
- except:
158
- pass
159
-
160
- return text, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
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