jaimalklyde commited on
Commit
8a7c2f9
·
verified ·
1 Parent(s): 803753e

Update src/module/AIP.py

Browse files
Files changed (1) hide show
  1. src/module/AIP.py +68 -138
src/module/AIP.py CHANGED
@@ -1,11 +1,7 @@
1
  """
2
- MODULE JASMINE AGENT - VERSION OMNISCIENTE (ETL + DASHBOARD + DATA LAB)
3
- =======================================================================
4
  Responsabilité : Agent IA centralisé.
5
- Capacités :
6
- 1. Pilotage de l'ETL (Mise à jour données)
7
- 2. Lecture du Dashboard (KPIs temps réel)
8
- 3. Exploration du Data Lab (Stats patients, pathologies, démographie)
9
  """
10
 
11
  import google.generativeai as genai
@@ -22,16 +18,16 @@ import traceback
22
  try:
23
  from module.Validation_Contract import ETL_Engine
24
  from module.Dashboard import HospitalDashboard
25
- from module.Data_Exploration import HospitalAdvancedAnalyzer # <--- NOUVELLE CAPACITÉ
26
- except ImportError:
27
- st.error("❌ Erreur critique : Impossible d'importer les modules métiers (ETL, Dashboard ou Data Lab).")
 
28
 
29
  # --- STYLE GOTHAM / AIP TERMINAL ---
30
  def apply_custom_styles():
31
  st.markdown("""
32
  <style>
33
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=JetBrains+Mono:wght@400&display=swap');
34
-
35
  .user-msg {
36
  background-color: rgba(20, 30, 45, 0.9); border: 1px solid #2b3b55;
37
  border-left: 3px solid #64B5F6; padding: 15px; margin: 10px 0;
@@ -56,11 +52,10 @@ def apply_custom_styles():
56
  class JasmineAgent:
57
  def __init__(self, gspread_client):
58
  self.gspread_client = gspread_client
59
-
60
- # --- SECRETS & CLIENTS ---
61
  self.google_key = os.environ.get("GOOGLE_API_KEY")
62
  self.groq_key = os.environ.get("GROQ_API_KEY")
63
-
 
64
  if not self.google_key:
65
  try: self.google_key = st.secrets.get("GOOGLE_API_KEY")
66
  except: pass
@@ -71,7 +66,6 @@ class JasmineAgent:
71
  self.groq_client = Groq(api_key=self.groq_key) if self.groq_key else None
72
  if self.google_key: genai.configure(api_key=self.google_key)
73
 
74
- # --- CASCADE DE MODÈLES ---
75
  self.MODEL_CASCADE = [
76
  "gemini-2.0-flash-exp",
77
  "llama-3.3-70b-versatile",
@@ -79,24 +73,26 @@ class JasmineAgent:
79
  "llama-3.1-8b-instant"
80
  ]
81
 
82
- # --- OUTILS DISPONIBLES ---
83
  self.tools_map = {
84
  "run_etl_pipeline": {
85
- "description": "Lance l'ETL pour mettre à jour les données.",
86
  "func": self._run_etl_wrapper
87
  },
88
  "get_hospital_kpis": {
89
- "description": "Récupère les KPIs temps réel (Occupation, Tension RH, Stocks, Admissions du jour).",
90
  "func": self._get_kpis_wrapper
91
  },
92
- # --- NOUVEL OUTIL : DATA LAB ---
93
  "analyze_patient_stats": {
94
- "description": "Analyse approfondie des patients : âge moyen, ratio H/F, pathologie la plus fréquente, durée moyenne de séjour.",
95
  "func": self._get_data_exploration_wrapper
 
 
 
 
96
  }
97
  }
98
 
99
- # --- WRAPPERS TECHNIQUES ---
100
 
101
  def _run_etl_wrapper(self, args=None):
102
  try:
@@ -104,72 +100,64 @@ class JasmineAgent:
104
  success, result = engine.run_etl()
105
  if success:
106
  st.session_state.data_explorer = result
107
- summary = result.get_summary()
108
- return {"status": "succès", "details": summary}
109
- return {"status": "erreur", "message": f"Echec ETL: {result}"}
110
  except Exception as e: return {"status": "crash", "message": str(e)}
111
 
112
  def _get_kpis_wrapper(self, args=None):
113
  try:
114
  dash = HospitalDashboard(self.gspread_client, "bdd_mvp_faker")
115
- if not dash.load_data():
116
- return {"status": "erreur", "message": "Données non chargées."}
117
  return {"status": "succès", "details": dash._get_kpis()}
118
  except Exception as e: return {"status": "crash", "message": str(e)}
119
 
120
  def _get_data_exploration_wrapper(self, args=None):
121
- """Connecteur vers le module Data Exploration pour les stats avancées"""
122
  try:
123
  analyzer = HospitalAdvancedAnalyzer(self.gspread_client, "bdd_mvp_faker")
124
- if not analyzer.load_data():
125
- return {"status": "erreur", "message": "Impossible de charger le Data Lab."}
126
-
127
- # Extraction des stats clés depuis les dataframes du module
128
  df_pat = analyzer.get_df('patients')
129
  df_sej = analyzer.get_df('sejours')
130
-
131
  stats = {}
132
-
133
- # 1. Démographie
134
  if df_pat is not None and not df_pat.empty:
135
  df_pat['age'] = pd.to_numeric(df_pat['age'], errors='coerce')
136
  stats['age_moyen'] = round(df_pat['age'].mean(), 1)
137
  stats['total_patients'] = len(df_pat)
138
- counts = df_pat['sexe'].value_counts(normalize=True)
139
- stats['ratio_hommes'] = round(counts.get('M', 0) * 100, 1)
140
- stats['ratio_femmes'] = round(counts.get('F', 0) * 100, 1)
141
- stats['top_ville'] = df_pat['ville'].mode()[0] if 'ville' in df_pat.columns else "N/A"
142
-
143
- # 2. Séjours & Pathos
144
  if df_sej is not None and not df_sej.empty:
145
- df_sej['duree'] = pd.to_numeric(df_sej['duree_sejour_jours'], errors='coerce')
146
- stats['duree_moyenne_sejour'] = round(df_sej['duree'].mean(), 1)
147
  stats['pathologie_top'] = df_sej['pathologie_code'].mode()[0]
148
- stats['gravite_moyenne'] = round(pd.to_numeric(df_sej['gravite_1_5'], errors='coerce').mean(), 2)
149
-
150
  return {"status": "succès", "details": stats}
151
-
152
- except Exception as e:
153
- return {"status": "crash", "message": f"Erreur Data Lab: {str(e)}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- # --- INTELLIGENCE CENTRALE ---
156
 
157
  def build_system_prompt(self):
158
  tools_desc = "\n".join([f"- {k}: {v['description']}" for k, v in self.tools_map.items()])
159
  return f"""
160
- ROLE: Tu es Jasmine, l'IA de supervision de l'hôpital.
161
- OBJECTIF: Analyser les données, piloter les outils et conseiller la direction.
162
-
163
- OUTILS :
164
  {tools_desc}
165
-
166
  RÈGLES :
167
- 1. Utilise 'get_hospital_kpis' pour l'état actuel (Lits, Stocks, RH).
168
- 2. Utilise 'analyze_patient_stats' pour les questions démographiques ou médicales (Âge, Pathos).
169
- 3. Si l'utilisateur demande ton avis ou une analyse SANS outil, réponds dans le champ "answer".
170
- 4. Réponds TOUJOURS au format JSON strict :
171
  {{
172
- "thought": "Raisonnement...",
173
  "tool": "nom_outil" (ou "none"),
174
  "answer": "Ta réponse directe si tool=none",
175
  "args": {{}}
@@ -178,11 +166,9 @@ class JasmineAgent:
178
 
179
  def ask(self, user_message, chat_history=[]):
180
  system_prompt = self.build_system_prompt()
181
- last_error = None
182
  response_payload = None
183
  used_model = None
184
 
185
- # Fallback Loop
186
  for model_name in self.MODEL_CASCADE:
187
  try:
188
  if "gemini" in model_name:
@@ -195,112 +181,56 @@ class JasmineAgent:
195
  if not self.groq_client: continue
196
  completion = self.groq_client.chat.completions.create(
197
  model=model_name,
198
- messages=[{"role": "system", "content": system_prompt},
199
- {"role": "user", "content": user_message}],
200
  response_format={"type": "json_object"})
201
  response_payload = completion.choices[0].message.content
202
  used_model = model_name
203
  break
204
- except Exception as e:
205
- last_error = str(e)
206
- continue
207
 
208
- if not response_payload:
209
- return f"❌ Erreur système : {last_error}", None, None
210
 
211
  try:
212
  clean_json = re.sub(r'```json\s*|\s*```', '', response_payload).strip()
213
  decision = json.loads(clean_json)
214
-
215
  tool_name = decision.get("tool")
216
  thought = f"Modèle: {used_model}\n\n{decision.get('thought', '')}"
217
 
218
- # Exécution de l'outil
219
  if tool_name in self.tools_map:
220
- tool_result = self.tools_map[tool_name]["func"]({})
221
-
222
- if tool_result["status"] == "succès":
223
- d = tool_result["details"]
224
-
225
- # --- FORMATAGE DE LA RÉPONSE SELON L'OUTIL ---
226
- if tool_name == "run_etl_pipeline":
227
- final_response = (f"✅ **ETL Terminé.**\n"
228
- f"- Tables : {d['tables_loaded']}\n"
229
- f"- Lignes : {d['total_rows_clean']:,}")
230
-
231
  elif tool_name == "get_hospital_kpis":
232
- taux = float(d.get('taux_global', 0)) # Sécurité Float
233
- final_response = (f"📊 **État Hôpital :**\n\n"
234
- f"🔹 **Occupation :** {taux:.1f}%\n"
235
- f"🔹 **Admissions J :** {d.get('nb_adm')}\n"
236
- f"🔹 **Alertes Stocks :** {d.get('nb_ruptures')}\n"
237
- f"🔹 **Services tendus :** {d.get('nb_tension')}")
238
-
239
- elif tool_name == "analyze_patient_stats":
240
- final_response = (f"🔬 **Analyse Patients :**\n\n"
241
- f"🔹 **Population :** {d.get('total_patients')} patients\n"
242
- f"🔹 **Âge moyen :** {d.get('age_moyen')} ans\n"
243
- f"🔹 **Répartition :** 👨 {d.get('ratio_hommes')}% | 👩 {d.get('ratio_femmes')}%\n"
244
- f"🔹 **Pathologie #1 :** {d.get('pathologie_top')}\n"
245
- f"🔹 **Durée moy. séjour :** {d.get('duree_moyenne_sejour')} jours")
246
- else:
247
- final_response = "Action effectuée."
248
- else:
249
- final_response = f"⚠️ Erreur technique : {tool_result['message']}"
250
-
251
- return final_response, thought, tool_result
252
-
253
- else:
254
- # Mode Conversation / Conseil direct (via JSON 'answer')
255
- final_response = decision.get("answer", "Je suis prête à analyser les données.")
256
- return final_response, thought, None
257
-
258
- except Exception as e:
259
- return f"Erreur décodage IA ({used_model}): {str(e)}", None, None
260
 
261
-
262
- # --- INTERFACE ---
263
  def show_terminal(client, sheet_name="bdd_mvp_faker"):
264
  apply_custom_styles()
265
  st.markdown("## 🤖 AIP COMMAND TERMINAL")
266
- st.caption("Agent intelligent connecté : ETL • Dashboard • Data Lab")
267
-
268
- if 'agent_instance' not in st.session_state:
269
- st.session_state.agent_instance = JasmineAgent(client)
270
 
271
- if "chat_history" not in st.session_state:
272
- st.session_state.chat_history = []
273
-
274
- # Affichage Historique
275
  for msg in st.session_state.chat_history:
276
  role_class = "user-msg" if msg["role"] == "user" else "bot-msg"
277
  st.markdown(f'<div class="{role_class}">{msg["content"]}</div>', unsafe_allow_html=True)
278
-
279
  if msg.get("thought"):
280
- with st.expander("👁️ AIP LOGIC TRACE"):
281
- st.markdown(f'<div class="debug-block">{msg["thought"]}</div>', unsafe_allow_html=True)
282
-
283
- if msg.get("data"):
284
- with st.expander("💾 SYSTEM PAYLOAD"):
285
- st.json(msg["data"])
286
-
287
- # Input
288
- user_input = st.chat_input("Ex: 'Quelle est la pathologie la plus fréquente ?' ou 'Mets à jour les données'...")
289
-
290
  if user_input:
291
  st.session_state.chat_history.append({"role": "user", "content": user_input})
292
  st.rerun()
293
 
294
  if st.session_state.chat_history and st.session_state.chat_history[-1]["role"] == "user":
295
- last_prompt = st.session_state.chat_history[-1]["content"]
296
-
297
  with st.spinner("Jasmine analyse..."):
298
- response, thought, data = st.session_state.agent_instance.ask(last_prompt)
299
-
300
- st.session_state.chat_history.append({
301
- "role": "assistant",
302
- "content": response,
303
- "thought": thought,
304
- "data": data
305
- })
306
  st.rerun()
 
1
  """
2
+ MODULE JASMINE AGENT - VERSION OMNISCIENTE V2 (ETL + DASHBOARD + DATA LAB + DATA QUALITY)
3
+ =========================================================================================
4
  Responsabilité : Agent IA centralisé.
 
 
 
 
5
  """
6
 
7
  import google.generativeai as genai
 
18
  try:
19
  from module.Validation_Contract import ETL_Engine
20
  from module.Dashboard import HospitalDashboard
21
+ from module.Data_Exploration import HospitalAdvancedAnalyzer
22
+ from module.Indicateurs_DQ import HospitalDataQualityAnalyzer, load_from_google_sheets
23
+ except ImportError as e:
24
+ st.error(f"❌ Erreur d'importation des modules : {str(e)}")
25
 
26
  # --- STYLE GOTHAM / AIP TERMINAL ---
27
  def apply_custom_styles():
28
  st.markdown("""
29
  <style>
30
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=JetBrains+Mono:wght@400&display=swap');
 
31
  .user-msg {
32
  background-color: rgba(20, 30, 45, 0.9); border: 1px solid #2b3b55;
33
  border-left: 3px solid #64B5F6; padding: 15px; margin: 10px 0;
 
52
  class JasmineAgent:
53
  def __init__(self, gspread_client):
54
  self.gspread_client = gspread_client
 
 
55
  self.google_key = os.environ.get("GOOGLE_API_KEY")
56
  self.groq_key = os.environ.get("GROQ_API_KEY")
57
+
58
+ # Fallbacks secrets
59
  if not self.google_key:
60
  try: self.google_key = st.secrets.get("GOOGLE_API_KEY")
61
  except: pass
 
66
  self.groq_client = Groq(api_key=self.groq_key) if self.groq_key else None
67
  if self.google_key: genai.configure(api_key=self.google_key)
68
 
 
69
  self.MODEL_CASCADE = [
70
  "gemini-2.0-flash-exp",
71
  "llama-3.3-70b-versatile",
 
73
  "llama-3.1-8b-instant"
74
  ]
75
 
 
76
  self.tools_map = {
77
  "run_etl_pipeline": {
78
+ "description": "Met à jour et nettoie les données.",
79
  "func": self._run_etl_wrapper
80
  },
81
  "get_hospital_kpis": {
82
+ "description": "Récupère les chiffres clés (Lits, RH, Stocks).",
83
  "func": self._get_kpis_wrapper
84
  },
 
85
  "analyze_patient_stats": {
86
+ "description": "Stats patients et pathologies (Data Lab).",
87
  "func": self._get_data_exploration_wrapper
88
+ },
89
+ "get_data_quality_report": {
90
+ "description": "Audit complet de la qualité et des erreurs dans les données.",
91
+ "func": self._get_dq_stats_wrapper
92
  }
93
  }
94
 
95
+ # --- WRAPPERS ---
96
 
97
  def _run_etl_wrapper(self, args=None):
98
  try:
 
100
  success, result = engine.run_etl()
101
  if success:
102
  st.session_state.data_explorer = result
103
+ return {"status": "succès", "details": result.get_summary()}
104
+ return {"status": "erreur", "message": "Échec de l'exécution ETL."}
 
105
  except Exception as e: return {"status": "crash", "message": str(e)}
106
 
107
  def _get_kpis_wrapper(self, args=None):
108
  try:
109
  dash = HospitalDashboard(self.gspread_client, "bdd_mvp_faker")
110
+ if not dash.load_data(): return {"status": "erreur", "message": "Données Dashboard indisponibles."}
 
111
  return {"status": "succès", "details": dash._get_kpis()}
112
  except Exception as e: return {"status": "crash", "message": str(e)}
113
 
114
  def _get_data_exploration_wrapper(self, args=None):
 
115
  try:
116
  analyzer = HospitalAdvancedAnalyzer(self.gspread_client, "bdd_mvp_faker")
117
+ if not analyzer.load_data(): return {"status": "erreur", "message": "Data Lab inaccessible."}
 
 
 
118
  df_pat = analyzer.get_df('patients')
119
  df_sej = analyzer.get_df('sejours')
 
120
  stats = {}
 
 
121
  if df_pat is not None and not df_pat.empty:
122
  df_pat['age'] = pd.to_numeric(df_pat['age'], errors='coerce')
123
  stats['age_moyen'] = round(df_pat['age'].mean(), 1)
124
  stats['total_patients'] = len(df_pat)
 
 
 
 
 
 
125
  if df_sej is not None and not df_sej.empty:
 
 
126
  stats['pathologie_top'] = df_sej['pathologie_code'].mode()[0]
 
 
127
  return {"status": "succès", "details": stats}
128
+ except Exception as e: return {"status": "crash", "message": str(e)}
129
+
130
+ def _get_dq_stats_wrapper(self, args=None):
131
+ try:
132
+ raw_data = load_from_google_sheets(self.gspread_client, "bdd_mvp_faker")
133
+ if not raw_data: return {"status": "erreur", "message": "Impossible de lire Google Sheets."}
134
+ dq_analyzer = HospitalDataQualityAnalyzer(raw_data)
135
+ scores = dq_analyzer.compute_global_score()
136
+ metrics = dq_analyzer.metrics
137
+ return {
138
+ "status": "succès",
139
+ "details": {
140
+ "score_global": scores['global'],
141
+ "completude": scores['completeness'],
142
+ "anomalies_pat": metrics['patients']['anomalies'],
143
+ "doublons": metrics['patients']['doublons']
144
+ }
145
+ }
146
+ except Exception as e: return {"status": "crash", "message": str(e)}
147
 
148
+ # --- CORE LOGIC ---
149
 
150
  def build_system_prompt(self):
151
  tools_desc = "\n".join([f"- {k}: {v['description']}" for k, v in self.tools_map.items()])
152
  return f"""
153
+ ROLE: Tu es Jasmine, l'IA de supervision.
154
+ OUTILS DISPONIBLES :
 
 
155
  {tools_desc}
 
156
  RÈGLES :
157
+ 1. 'get_data_quality_report' pour toute question sur la fiabilité ou les erreurs.
158
+ 2. Réponds TOUJOURS au format JSON :
 
 
159
  {{
160
+ "thought": "Ton analyse...",
161
  "tool": "nom_outil" (ou "none"),
162
  "answer": "Ta réponse directe si tool=none",
163
  "args": {{}}
 
166
 
167
  def ask(self, user_message, chat_history=[]):
168
  system_prompt = self.build_system_prompt()
 
169
  response_payload = None
170
  used_model = None
171
 
 
172
  for model_name in self.MODEL_CASCADE:
173
  try:
174
  if "gemini" in model_name:
 
181
  if not self.groq_client: continue
182
  completion = self.groq_client.chat.completions.create(
183
  model=model_name,
184
+ messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}],
 
185
  response_format={"type": "json_object"})
186
  response_payload = completion.choices[0].message.content
187
  used_model = model_name
188
  break
189
+ except: continue
 
 
190
 
191
+ if not response_payload: return "❌ Erreur de connexion API.", None, None
 
192
 
193
  try:
194
  clean_json = re.sub(r'```json\s*|\s*```', '', response_payload).strip()
195
  decision = json.loads(clean_json)
 
196
  tool_name = decision.get("tool")
197
  thought = f"Modèle: {used_model}\n\n{decision.get('thought', '')}"
198
 
 
199
  if tool_name in self.tools_map:
200
+ res = self.tools_map[tool_name]["func"]({})
201
+ if res["status"] == "succès":
202
+ d = res["details"]
203
+ if tool_name == "get_data_quality_report":
204
+ score = d['score_global']
205
+ icon = "🟢" if score > 90 else "🟠" if score > 75 else "🔴"
206
+ final_response = f"🛡️ **Qualité :** {icon} {score}/100\n🔹 Complétude : {d['completude']}%\n🔹 Anomalies : {d['anomalies_pat']}"
 
 
 
 
207
  elif tool_name == "get_hospital_kpis":
208
+ final_response = f"📊 **KPIs :** Lits à {d['taux_global']:.1f}% | Tension RH: {d['tension_rh']:.1f}%"
209
+ else: final_response = f"✅ Action {tool_name} terminée."
210
+ else: final_response = f"⚠️ {res.get('message')}"
211
+ return final_response, thought, res
212
+ return decision.get("answer", "Analyse terminée."), thought, None
213
+ except Exception as e: return f"Erreur IA : {str(e)}", None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
 
 
215
  def show_terminal(client, sheet_name="bdd_mvp_faker"):
216
  apply_custom_styles()
217
  st.markdown("## 🤖 AIP COMMAND TERMINAL")
218
+ if 'agent_instance' not in st.session_state: st.session_state.agent_instance = JasmineAgent(client)
219
+ if "chat_history" not in st.session_state: st.session_state.chat_history = []
 
 
220
 
 
 
 
 
221
  for msg in st.session_state.chat_history:
222
  role_class = "user-msg" if msg["role"] == "user" else "bot-msg"
223
  st.markdown(f'<div class="{role_class}">{msg["content"]}</div>', unsafe_allow_html=True)
 
224
  if msg.get("thought"):
225
+ with st.expander("👁️ AIP LOGIC TRACE"): st.markdown(f'<div class="debug-block">{msg["thought"]}</div>', unsafe_allow_html=True)
226
+
227
+ user_input = st.chat_input("Dites 'Quel est le score de qualité ?'...")
 
 
 
 
 
 
 
228
  if user_input:
229
  st.session_state.chat_history.append({"role": "user", "content": user_input})
230
  st.rerun()
231
 
232
  if st.session_state.chat_history and st.session_state.chat_history[-1]["role"] == "user":
 
 
233
  with st.spinner("Jasmine analyse..."):
234
+ response, thought, data = st.session_state.agent_instance.ask(st.session_state.chat_history[-1]["content"])
235
+ st.session_state.chat_history.append({"role": "assistant", "content": response, "thought": thought, "data": data})
 
 
 
 
 
 
236
  st.rerun()