jaimalklyde commited on
Commit
d736fc2
·
verified ·
1 Parent(s): a4bff53

Update src/module/AIP.py

Browse files
Files changed (1) hide show
  1. src/module/AIP.py +207 -103
src/module/AIP.py CHANGED
@@ -1,9 +1,9 @@
1
  """
2
- MODULE JASMINE AGENT - VERSION ULTIME (FULL STACK + EMPATHIE)
3
- =============================================================
4
- Responsabilité : Agent IA centralisé.
5
- Capacités : ETL, Dashboard, Data Lab, DQ, Features, Forecasting.
6
- Ton : Professionnel, Proactif et Humain.
7
  """
8
 
9
  import google.generativeai as genai
@@ -16,18 +16,20 @@ import pandas as pd
16
  import numpy as np
17
  import traceback
18
 
19
- # --- IMPORT DES 6 MODULES CIBLES ---
 
20
  try:
21
  from module.Validation_Contract import ETL_Engine
22
- from module.Dashboard import HospitalDashboard
 
23
  from module.Data_Exploration import HospitalAdvancedAnalyzer
24
  from module.Indicateurs_DQ import HospitalDataQualityAnalyzer, load_from_google_sheets
25
  from module.Features import FeatureEngineer
26
  from module.Forecasting import HospitalForecaster
27
  except ImportError as e:
28
- st.error(f"❌ Erreur critique d'importation : {str(e)}")
29
 
30
- # --- STYLE GOTHAM ---
31
  def apply_custom_styles():
32
  st.markdown("""
33
  <style>
@@ -47,26 +49,15 @@ def apply_custom_styles():
47
  color: #8b949e; background: #0d1117; padding: 10px;
48
  border: 1px solid #30363d; border-radius: 0px; margin: 5px 0 10px 0;
49
  }
50
- .streamlit-expanderHeader { border-radius: 0px !important; background-color: #161b22 !important; border: 1px solid #30363d !important; }
51
- .streamlit-expanderContent { border-radius: 0px !important; border: 1px solid #30363d !important; border-top: none !important; background-color: #0d1117 !important; }
52
- h2 { font-family: 'Inter', sans-serif; font-weight: 600; border-bottom: 1px solid #333; padding-bottom: 10px; }
53
  </style>
54
  """, unsafe_allow_html=True)
55
 
56
  class JasmineAgent:
57
  def __init__(self, gspread_client):
58
  self.gspread_client = gspread_client
59
- self.google_key = os.environ.get("GOOGLE_API_KEY")
60
- self.groq_key = os.environ.get("GROQ_API_KEY")
61
 
62
- # Fallbacks
63
- if not self.google_key:
64
- try: self.google_key = st.secrets.get("GOOGLE_API_KEY")
65
- except: pass
66
- if not self.groq_key:
67
- try: self.groq_key = st.secrets.get("GROQ_API_KEY")
68
- except: pass
69
-
70
  self.groq_client = Groq(api_key=self.groq_key) if self.groq_key else None
71
  if self.google_key: genai.configure(api_key=self.google_key)
72
 
@@ -79,16 +70,16 @@ class JasmineAgent:
79
 
80
  # --- CARTE DES OUTILS ---
81
  self.tools_map = {
82
- "run_etl_pipeline": {"func": self._run_etl_wrapper, "desc": "1. ETL : Nettoyage et mise à jour des données."},
83
- "run_feature_engineering": {"func": self._run_features_wrapper, "desc": "2. ML Prep : Transformation des données pour l'IA."},
84
- "train_forecasting_models": {"func": self._train_models_wrapper, "desc": "3. Training : Entraînement des modèles prédictifs."},
85
- "get_future_forecast": {"func": self._get_forecast_wrapper, "desc": "4. Prédiction : Obtenir les prévisions futures."},
86
- "get_hospital_kpis": {"func": self._get_kpis_wrapper, "desc": "5. Dashboard : KPIs actuels (Lits, RH, Stocks)."},
87
- "analyze_patient_stats": {"func": self._get_data_exploration_wrapper, "desc": "6. Data Lab : Stats patients passées."},
88
- "get_data_quality_report": {"func": self._get_dq_stats_wrapper, "desc": "7. Audit : Qualité et fiabilité des données."}
89
  }
90
 
91
- # --- WRAPPERS TECHNIQUES ---
92
 
93
  def _run_etl_wrapper(self, args=None):
94
  try:
@@ -96,18 +87,22 @@ class JasmineAgent:
96
  success, result = engine.run_etl()
97
  if success:
98
  st.session_state.data_explorer = result
 
99
  return {"status": "succès", "details": result.get_summary()}
100
- return {"status": "erreur", "message": "Échec ETL."}
101
  except Exception as e: return {"status": "crash", "message": str(e)}
102
 
103
  def _run_features_wrapper(self, args=None):
104
  try:
105
- if 'data_explorer' not in st.session_state: return {"status": "erreur", "message": "Lancez l'ETL d'abord."}
106
  engineer = FeatureEngineer(st.session_state.data_explorer)
107
  df_final, msg = engineer.build_full_dataset()
108
  if df_final is not None:
109
  st.session_state['ml_features'] = df_final
110
- return {"status": "succès", "details": {"lignes": len(df_final), "features": len(df_final.columns)}}
 
 
 
111
  return {"status": "erreur", "message": msg}
112
  except Exception as e: return {"status": "crash", "message": str(e)}
113
 
@@ -119,42 +114,68 @@ class JasmineAgent:
119
  metrics = forecaster.train_all_models()
120
  st.session_state['forecaster'] = forecaster
121
  st.session_state['metrics'] = metrics
122
- best = forecaster.best_model_name
123
- return {"status": "succès", "details": {"best_model": best, "r2": metrics[best]['R²'], "mae": metrics[best]['MAE']}}
 
124
  except Exception as e: return {"status": "crash", "message": str(e)}
125
 
126
  def _get_forecast_wrapper(self, args=None):
127
  try:
128
- if 'forecaster' not in st.session_state: return {"status": "erreur", "message": "Modèles non entraînés."}
129
  forecaster = st.session_state['forecaster']
130
- dates, values, _ = forecaster.generate_future_forecast_recursive(months=3)
 
131
  alerts = forecaster.detect_future_anomalies(values)
132
- return {"status": "succès", "details": {"tendance": "Hausse" if values[-1] > values[0] else "Baisse", "max_prevu": float(np.max(values)), "alerte_count": len(alerts), "prochaine_alerte": alerts[0] if alerts else None}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  except Exception as e: return {"status": "crash", "message": str(e)}
134
 
135
  def _get_kpis_wrapper(self, args=None):
136
  try:
137
- dash = HospitalDashboard(self.gspread_client, "bdd_mvp_faker")
138
- if not dash.load_data(): return {"status": "erreur", "message": "Données Dashboard indisponibles."}
139
- return {"status": "succès", "details": dash._get_kpis()}
 
 
 
 
 
 
 
 
 
 
 
 
140
  except Exception as e: return {"status": "crash", "message": str(e)}
141
 
142
  def _get_data_exploration_wrapper(self, args=None):
143
  try:
144
  analyzer = HospitalAdvancedAnalyzer(self.gspread_client, "bdd_mvp_faker")
145
- if not analyzer.load_data(): return {"status": "erreur", "message": "Data Lab inaccessible."}
 
146
  df_pat = analyzer.get_df('patients')
147
- df_sej = analyzer.get_df('sejours')
148
  stats = {}
149
  if df_pat is not None:
150
  df_pat['age'] = pd.to_numeric(df_pat['age'], errors='coerce')
151
- stats['age_moyen'] = round(df_pat['age'].mean(), 1)
152
- stats['total_patients'] = len(df_pat)
153
- counts = df_pat['sexe'].value_counts(normalize=True)
154
- stats['ratio_hommes'] = round(counts.get('M', 0) * 100, 1)
155
- stats['ratio_femmes'] = round(counts.get('F', 0) * 100, 1)
156
- if df_sej is not None:
157
- stats['pathologie_top'] = df_sej['pathologie_code'].mode()[0]
158
  return {"status": "succès", "details": stats}
159
  except Exception as e: return {"status": "crash", "message": str(e)}
160
 
@@ -163,71 +184,154 @@ class JasmineAgent:
163
  raw_data = load_from_google_sheets(self.gspread_client, "bdd_mvp_faker")
164
  dq = HospitalDataQualityAnalyzer(raw_data)
165
  scores = dq.compute_global_score()
166
- return {"status": "succès", "details": {"score": scores['global'], "anomalies": dq.metrics['patients']['anomalies']}}
 
 
 
 
 
 
 
 
 
 
167
  except Exception as e: return {"status": "crash", "message": str(e)}
168
 
169
- # --- HUMANISATION (Le Moteur de Personnalité) ---
170
  def _generate_human_response(self, tool_name, d):
171
- """Traduit les données JSON en langage naturel professionnel"""
 
 
172
 
 
173
  if tool_name == "get_hospital_kpis":
174
- taux = float(d.get('taux_global', 0))
175
- rh = max(0, d.get('tension_rh', 0))
176
- ruptures = int(d.get('nb_ruptures', 0))
 
 
177
 
178
- msg = "Voici la situation actuelle de l'établissement :\n\n"
179
- if taux > 95: msg += f"🔴 **URGENCE :** Saturation critique (**{taux:.1f}%**). Plan Blanc recommandé.\n"
180
- elif taux > 85: msg += f"🟠 **Tension :** L'occupation est élevée (**{taux:.1f}%**).\n"
181
- else: msg += f"🟢 **Lits :** Situation calme (**{taux:.1f}%** d'occupation).\n"
182
 
183
- if rh > 20: msg += f"⚠️ **RH :** Tension forte sur le personnel (**{rh:.1f}%** manquant).\n"
184
- else: msg += f"✅ **RH :** Effectifs stables.\n"
 
 
185
 
186
- if ruptures > 0: msg += f"📦 **Logistique :** {ruptures} rupture(s) de stock détectée(s)."
187
- else: msg += "📦 **Logistique :** Stocks au vert."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  return msg
189
 
 
190
  elif tool_name == "get_future_forecast":
191
- tendance = d['tendance']
192
- max_p = d['max_prevu']
193
- alerts = d.get('alerte_count', 0)
194
- msg = f"Selon mes modèles, la tendance à 3 mois est à la **{tendance}**.\n"
195
- msg += f"Le pic d'admissions prévu est de **{max_p:.0f} patients/jour**.\n\n"
196
- if alerts > 0:
197
- next_alert = d['prochaine_alerte']
198
- msg += f"⚠️ **Vigilance :** Un pic anormal est détecté autour de **J+{next_alert['day']}**."
199
- else: msg += "✅ Aucune anomalie majeure prévue."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  return msg
201
 
202
- elif tool_name == "train_forecasting_models":
203
- return f"Entraînement terminé. Le modèle **{d['best_model']}** est le plus performant (Fiabilité : **{d['r2']*100:.1f}%**)."
204
-
205
- elif tool_name == "run_etl_pipeline":
206
- return f"Mise à jour effectuée. La base contient maintenant **{d['total_rows_clean']:,}** lignes consolidées."
207
-
208
- elif tool_name == "run_feature_engineering":
209
- return f"Préparation ML terminée : **{d['lignes']:,} vecteurs** générés avec **{d['features']} variables** d'analyse."
210
-
211
- elif tool_name == "analyze_patient_stats":
212
- return f"Analyse sur **{d['total_patients']} patients**. Âge moyen : **{d['age_moyen']} ans**. Pathologie dominante : **{d['pathologie_top']}**."
213
-
214
  elif tool_name == "get_data_quality_report":
215
- score = d['score']
216
- if score > 85: return f"🛡️ Données fiables (**{score}/100**)."
217
- return f"⚠️ **Attention Data :** Score de qualité faible (**{score}/100**) avec **{d['anomalies']} anomalies**."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
- return "Opération réussie."
 
220
 
221
- # --- INTELLIGENCE PRINCIPALE ---
222
  def ask(self, user_message, chat_history=[]):
223
  system_prompt = f"""
224
- ROLE: Tu es Jasmine, Directrice adjointe virtuelle.
225
- TON: Professionnel, concis, proactif.
226
- OUTILS:
 
227
  {chr(10).join([f"- {k}: {v['desc']}" for k, v in self.tools_map.items()])}
228
- RÈGLES:
229
- 1. Utilise les outils pour répondre aux faits.
230
- 2. Réponds TOUJOURS en JSON : {{ "thought": "...", "tool": "...", "args": {{}}, "answer": "..." }}
 
 
 
 
 
 
 
 
231
  """
232
 
233
  response_payload = None
@@ -248,7 +352,7 @@ class JasmineAgent:
248
  break
249
  except: continue
250
 
251
- if not response_payload: return "❌ Erreur API.", None, None
252
 
253
  try:
254
  decision = json.loads(re.sub(r'```json\s*|\s*```', '', response_payload).strip())
@@ -258,14 +362,14 @@ class JasmineAgent:
258
  if tool_name in self.tools_map:
259
  res = self.tools_map[tool_name]["func"]({})
260
  if res["status"] == "succès":
261
- # UTILISATION DE LA NOUVELLE MÉTHODE HUMAINE
262
  final_response = self._generate_human_response(tool_name, res["details"])
263
  else:
264
- final_response = f"⚠️ Problème technique : {res.get('message')}"
265
  return final_response, thought, res
266
 
267
- return decision.get("answer", "Je vous écoute."), thought, None
268
- except Exception as e: return f"Erreur système : {str(e)}", None, None
269
 
270
  def show_terminal(client, sheet_name="bdd_mvp_faker"):
271
  apply_custom_styles()
@@ -281,13 +385,13 @@ def show_terminal(client, sheet_name="bdd_mvp_faker"):
281
  if msg.get("data"):
282
  with st.expander("💾 SYSTEM PAYLOAD"): st.json(msg["data"])
283
 
284
- user_input = st.chat_input("Ex: 'Analyse les stocks' ou 'Lance la prédiction'...")
285
  if user_input:
286
  st.session_state.chat_history.append({"role": "user", "content": user_input})
287
  st.rerun()
288
 
289
  if st.session_state.chat_history and st.session_state.chat_history[-1]["role"] == "user":
290
- with st.spinner("Jasmine analyse..."):
291
  response, thought, data = st.session_state.agent_instance.ask(st.session_state.chat_history[-1]["content"])
292
  st.session_state.chat_history.append({"role": "assistant", "content": response, "thought": thought, "data": data})
293
  st.rerun()
 
1
  """
2
+ MODULE JASMINE AGENT - EXECUTIVE EDITION
3
+ ========================================
4
+ Responsabilité : Cerveau central. Analyse stratégique et recommandations de direction.
5
+ Niveau : C-Level (Directeur Général).
6
+ Capacités : Connexion profonde aux moteurs de calcul (Forecasting, Dashboard, DQ).
7
  """
8
 
9
  import google.generativeai as genai
 
16
  import numpy as np
17
  import traceback
18
 
19
+ # --- IMPORTATION ROBUSTE DES MODULES ---
20
+ # Jasmine s'adapte aux modules disponibles pour éviter les crashs
21
  try:
22
  from module.Validation_Contract import ETL_Engine
23
+ # MISE A JOUR: Import des nouveaux moteurs du Dashboard
24
+ from module.Dashboard import RealtimeMonitor, CrisisSimulator
25
  from module.Data_Exploration import HospitalAdvancedAnalyzer
26
  from module.Indicateurs_DQ import HospitalDataQualityAnalyzer, load_from_google_sheets
27
  from module.Features import FeatureEngineer
28
  from module.Forecasting import HospitalForecaster
29
  except ImportError as e:
30
+ st.error(f"Erreur critique d'importation des modules : {str(e)}")
31
 
32
+ # --- STYLE ---
33
  def apply_custom_styles():
34
  st.markdown("""
35
  <style>
 
49
  color: #8b949e; background: #0d1117; padding: 10px;
50
  border: 1px solid #30363d; border-radius: 0px; margin: 5px 0 10px 0;
51
  }
 
 
 
52
  </style>
53
  """, unsafe_allow_html=True)
54
 
55
  class JasmineAgent:
56
  def __init__(self, gspread_client):
57
  self.gspread_client = gspread_client
58
+ self.google_key = os.environ.get("GOOGLE_API_KEY") or st.secrets.get("GOOGLE_API_KEY")
59
+ self.groq_key = os.environ.get("GROQ_API_KEY") or st.secrets.get("GROQ_API_KEY")
60
 
 
 
 
 
 
 
 
 
61
  self.groq_client = Groq(api_key=self.groq_key) if self.groq_key else None
62
  if self.google_key: genai.configure(api_key=self.google_key)
63
 
 
70
 
71
  # --- CARTE DES OUTILS ---
72
  self.tools_map = {
73
+ "run_etl_pipeline": {"func": self._run_etl_wrapper, "desc": "ETL : Mise à jour des données brutes."},
74
+ "run_feature_engineering": {"func": self._run_features_wrapper, "desc": "Features : Transformation pour l'IA."},
75
+ "train_forecasting_models": {"func": self._train_models_wrapper, "desc": "Training : Entraînement des modèles IA."},
76
+ "get_future_forecast": {"func": self._get_forecast_wrapper, "desc": "Prédiction : Analyse du futur (3 mois)."},
77
+ "get_hospital_kpis": {"func": self._get_kpis_wrapper, "desc": "Cockpit : État actuel temps réel (Monitor)."},
78
+ "analyze_patient_stats": {"func": self._get_data_exploration_wrapper, "desc": "Data Lab : Analyse historique profonde."},
79
+ "get_data_quality_report": {"func": self._get_dq_stats_wrapper, "desc": "Audit : Fiabilité des données."}
80
  }
81
 
82
+ # --- WRAPPERS INTELLIGENTS (Extraction de contexte riche) ---
83
 
84
  def _run_etl_wrapper(self, args=None):
85
  try:
 
87
  success, result = engine.run_etl()
88
  if success:
89
  st.session_state.data_explorer = result
90
+ # On retourne le résumé complet pour analyse
91
  return {"status": "succès", "details": result.get_summary()}
92
+ return {"status": "erreur", "message": "Échec critique du pipeline ETL."}
93
  except Exception as e: return {"status": "crash", "message": str(e)}
94
 
95
  def _run_features_wrapper(self, args=None):
96
  try:
97
+ if 'data_explorer' not in st.session_state: return {"status": "erreur", "message": "Données brutes manquantes. Lancez l'ETL."}
98
  engineer = FeatureEngineer(st.session_state.data_explorer)
99
  df_final, msg = engineer.build_full_dataset()
100
  if df_final is not None:
101
  st.session_state['ml_features'] = df_final
102
+ # Analyse rapide des features générées pour le contexte
103
+ cols = list(df_final.columns)
104
+ lags = [c for c in cols if 'lag' in c]
105
+ return {"status": "succès", "details": {"lignes": len(df_final), "total_features": len(cols), "lags_detectes": len(lags)}}
106
  return {"status": "erreur", "message": msg}
107
  except Exception as e: return {"status": "crash", "message": str(e)}
108
 
 
114
  metrics = forecaster.train_all_models()
115
  st.session_state['forecaster'] = forecaster
116
  st.session_state['metrics'] = metrics
117
+
118
+ # On renvoie toutes les métriques pour comparaison
119
+ return {"status": "succès", "details": metrics}
120
  except Exception as e: return {"status": "crash", "message": str(e)}
121
 
122
  def _get_forecast_wrapper(self, args=None):
123
  try:
124
+ if 'forecaster' not in st.session_state: return {"status": "erreur", "message": "IA non entraînée."}
125
  forecaster = st.session_state['forecaster']
126
+ # On demande 3 mois pour voir la tendance long terme
127
+ dates, values, conf = forecaster.generate_future_forecast_recursive(months=3)
128
  alerts = forecaster.detect_future_anomalies(values)
129
+
130
+ # Analyse statistique de la prédiction
131
+ trend_pct = ((values[-1] - values[0]) / values[0]) * 100
132
+
133
+ return {
134
+ "status": "succès",
135
+ "details": {
136
+ "tendance_pct": trend_pct,
137
+ "min_val": float(np.min(values)),
138
+ "max_val": float(np.max(values)),
139
+ "mean_val": float(np.mean(values)),
140
+ "alertes": alerts, # Liste complète des alertes
141
+ "confiance_spread": float(np.mean(conf['upper'] - conf['lower'])) # Indicateur d'incertitude
142
+ }
143
+ }
144
  except Exception as e: return {"status": "crash", "message": str(e)}
145
 
146
  def _get_kpis_wrapper(self, args=None):
147
  try:
148
+ # CORRECTION MAJEURE : Utilisation du RealtimeMonitor
149
+ if 'ml_features' not in st.session_state or 'forecaster' not in st.session_state:
150
+ return {"status": "erreur", "message": "Le Cockpit nécessite les données ML et le modèle entraîné."}
151
+
152
+ monitor = RealtimeMonitor(st.session_state['ml_features'], st.session_state['forecaster'])
153
+ state = monitor.current_state # Récupère tout l'état (KPIs + tendances + alertes)
154
+
155
+ # On ajoute l'analyse par service
156
+ services_breakdown = monitor.get_service_breakdown()
157
+ top_tension_service = services_breakdown.iloc[0].name if services_breakdown is not None else "Inconnu"
158
+
159
+ # On fusionne tout
160
+ state['top_tension_service'] = top_tension_service
161
+
162
+ return {"status": "succès", "details": state}
163
  except Exception as e: return {"status": "crash", "message": str(e)}
164
 
165
  def _get_data_exploration_wrapper(self, args=None):
166
  try:
167
  analyzer = HospitalAdvancedAnalyzer(self.gspread_client, "bdd_mvp_faker")
168
+ if not analyzer.load_data(): return {"status": "erreur", "message": "Données historiques inaccessibles."}
169
+
170
  df_pat = analyzer.get_df('patients')
 
171
  stats = {}
172
  if df_pat is not None:
173
  df_pat['age'] = pd.to_numeric(df_pat['age'], errors='coerce')
174
+ stats['demographie'] = {
175
+ 'total': len(df_pat),
176
+ 'age_moyen': round(df_pat['age'].mean(), 1),
177
+ 'senior_ratio': round((df_pat['age'] > 65).mean() * 100, 1)
178
+ }
 
 
179
  return {"status": "succès", "details": stats}
180
  except Exception as e: return {"status": "crash", "message": str(e)}
181
 
 
184
  raw_data = load_from_google_sheets(self.gspread_client, "bdd_mvp_faker")
185
  dq = HospitalDataQualityAnalyzer(raw_data)
186
  scores = dq.compute_global_score()
187
+
188
+ # On veut savoir où sont les problèmes exactement
189
+ details_anomalies = {k: v['anomalies'] for k, v in dq.metrics.items() if 'anomalies' in v}
190
+
191
+ return {
192
+ "status": "succès",
193
+ "details": {
194
+ "scores": scores,
195
+ "anomalies_par_table": details_anomalies
196
+ }
197
+ }
198
  except Exception as e: return {"status": "crash", "message": str(e)}
199
 
200
+ # --- CERVEAU ANALYTIQUE (Human Response Engine) ---
201
  def _generate_human_response(self, tool_name, d):
202
+ """
203
+ Génère une analyse structurée : Constat -> Analyse -> Stratégie.
204
+ """
205
 
206
+ # 1. ANALYSE DU COCKPIT (Présent)
207
  if tool_name == "get_hospital_kpis":
208
+ taux = d.get('taux_occupation', 0) * 100
209
+ trend = d.get('trend_occupation', 0)
210
+ score = d.get('health_score', 100)
211
+ tension_rh = d.get('tension_personnel', 0)
212
+ service_top = d.get('top_tension_service', 'N/A')
213
 
214
+ msg = "### 🚁 Rapport de Situation Temps Réel\n\n"
 
 
 
215
 
216
+ # Constat
217
+ msg += f"**État Global :** Le score de santé de l'hôpital est de **{score:.0f}/100**.\n"
218
+ msg += f"- Occupation : **{taux:.1f}%** ({trend:+.1f}% sur 72h)\n"
219
+ msg += f"- Tension RH : **{tension_rh:.2f}** (Normal = 1.0)\n\n"
220
 
221
+ # Analyse
222
+ msg += "**🧠 Analyse Profonde :**\n"
223
+ if taux > 90:
224
+ msg += "Nous sommes en zone de saturation critique. La tendance montre une inertie qui risque de bloquer les urgences d'ici 24h. "
225
+ elif taux > 80:
226
+ msg += "L'activité est dense mais fluide. Les capacités tampons sont entamées. "
227
+ else:
228
+ msg += "L'hôpital est en sous-régime, ce qui permet d'absorber un éventuel choc externe. "
229
+
230
+ if tension_rh > 1.1:
231
+ msg += f"Le point de friction majeur est le service **{service_top}**, où le manque de personnel crée un goulot d'étranglement.\n\n"
232
+ else:
233
+ msg += "L'adéquation charge/ressource est optimale.\n\n"
234
+
235
+ # Stratégie
236
+ msg += "**🎯 Recommandations Stratégiques :**\n"
237
+ if score < 60:
238
+ msg += "1. **PRIORITAIRE :** Déclencher le niveau 1 du Plan Blanc pour libérer des lits.\n"
239
+ msg += f"2. Redéployer immédiatement du personnel vers le service {service_top}.\n"
240
+ msg += "3. Suspendre les admissions programmées non-urgentes pour 48h."
241
+ elif score < 80:
242
+ msg += f"1. Surveiller spécifiquement le service {service_top}.\n"
243
+ msg += "2. Préparer l'ouverture de lits 'buffer' si la tendance hausse se confirme."
244
+ else:
245
+ msg += "1. Maintenir le dispositif actuel.\n2. Profiter de l'accalmie pour effectuer les maintenances logistiques."
246
+
247
  return msg
248
 
249
+ # 2. ANALYSE PRÉDICTIVE (Futur)
250
  elif tool_name == "get_future_forecast":
251
+ tendance = d['tendance_pct']
252
+ max_val = d['max_val']
253
+ alertes = d['alertes']
254
+
255
+ msg = "### 🔮 Note de Prospective (3 Mois)\n\n"
256
+
257
+ # Constat
258
+ direction = "HAUSSE" if tendance > 0 else "BAISSE"
259
+ msg += f"**Trajectoire :** Mes modèles anticipent une **{direction} globale de {abs(tendance):.1f}%** des admissions.\n"
260
+ msg += f"**Point Haut :** Nous attendons un pic à **{max_val:.0f} patients/jour**.\n\n"
261
+
262
+ # Analyse
263
+ msg += "**🧠 Analyse des Risques :**\n"
264
+ if len(alertes) > 0:
265
+ nb_high = len([a for a in alertes if a['severity'] == 'HIGH'])
266
+ first_alert_day = alertes[0]['day']
267
+ msg += f"Je détecte **{len(alertes)} anomalies statistiques** dans la période à venir, dont **{nb_high} critiques**.\n"
268
+ msg += f"La première tension majeure apparaîtra dans **{first_alert_day} jours**. Cela correspond probablement à une saisonnalité forte (ex: épidémie) ou à un événement cyclique mal anticipé.\n\n"
269
+ else:
270
+ msg += "La courbe prévisionnelle reste dans le tunnel de normalité. La variance est faible, ce qui indique une période de stabilité opérationnelle.\n\n"
271
+
272
+ # Stratégie
273
+ msg += "**🎯 Actions Préventives :**\n"
274
+ if tendance > 15 or len(alertes) > 2:
275
+ msg += "1. **Anticipation RH :** Valider les plannings de garde pour J+30 dès maintenant.\n"
276
+ msg += "2. **Logistique :** Augmenter les stocks de consommables de +20% en prévision du pic.\n"
277
+ msg += "3. **Flux :** Prévenir les services d'aval (SSR) qu'une vague d'entrée arrive."
278
+ else:
279
+ msg += "1. Optimiser le taux d'occupation des lits programmés.\n"
280
+ msg += "2. Aucune mesure d'urgence requise."
281
+
282
  return msg
283
 
284
+ # 3. QUALITÉ DES DONNÉES (Fondation)
 
 
 
 
 
 
 
 
 
 
 
285
  elif tool_name == "get_data_quality_report":
286
+ scores = d['scores']
287
+ anomalies = d['anomalies_par_table']
288
+ total_anom = sum(anomalies.values())
289
+
290
+ msg = "### 🛡️ Audit de Fiabilité des Données\n\n"
291
+ msg += f"**Score de Confiance : {scores['global']}/100**\n"
292
+ msg += f"(Complétude : {scores['completeness']:.1f}% | Exactitude : {scores['accuracy']:.1f}%)\n\n"
293
+
294
+ msg += "**🧠 Diagnostic Technique :**\n"
295
+ if total_anomalies > 0:
296
+ msg += f"J'ai identifié un total de **{total_anom} lignes corrompues** qui 'bruitent' mes analyses.\n"
297
+ worst_table = max(anomalies, key=anomalies.get)
298
+ msg += f"Le point noir se situe dans la table **'{worst_table}'** ({anomalies[worst_table]} erreurs). "
299
+ msg += "Ces erreurs (dates inversées, âges négatifs, doublons) réduisent la précision de mes prévisions d'environ 5 à 10%.\n\n"
300
+ else:
301
+ msg += "Le Data Lake est propre. Les signaux faibles sont clairs.\n\n"
302
+
303
+ msg += "**🎯 Plan de Remédiation :**\n"
304
+ if scores['global'] < 80:
305
+ msg += "1. **URGENT :** Demander au DSI de purger la table 'Patients' (doublons).\n"
306
+ msg += "2. Mettre en place des contraintes de saisie dans le logiciel métier (dates).\n"
307
+ msg += "3. Relancer l'ETL après correction manuelle."
308
+ else:
309
+ msg += "Maintenir la surveillance automatisée hebdomadaire."
310
+
311
+ return msg
312
 
313
+ # Par défaut (autres outils)
314
+ return f"✅ **Exécution Validée :** L'outil {tool_name} a fonctionné correctement. Les données ont été mises à jour dans le système central."
315
 
316
+ # --- CERVEAU PRINCIPAL ---
317
  def ask(self, user_message, chat_history=[]):
318
  system_prompt = f"""
319
+ ROLE: Tu es Jasmine, la Directrice des Opérations (COO) Virtuelle de l'hôpital.
320
+ TON: Autoritaire mais bienveillant, précis, orienté solution. Tu ne dis pas "je pense", tu dis "l'analyse montre".
321
+
322
+ OUTILS DISPONIBLES:
323
  {chr(10).join([f"- {k}: {v['desc']}" for k, v in self.tools_map.items()])}
324
+
325
+ DIRECTIVE SUPRÊME:
326
+ Ne te contente jamais de donner un chiffre. Donne le CONTEXTE, la CAUSE PROBABLE, et l'ACTION REQUISE.
327
+
328
+ FORMAT DE RÉPONSE JSON ATTENDU:
329
+ {{
330
+ "thought": "Analyse interne de la demande...",
331
+ "tool": "nom_de_l_outil_a_lancer",
332
+ "args": {{}},
333
+ "answer": "Texte si pas d'outil nécessaire"
334
+ }}
335
  """
336
 
337
  response_payload = None
 
352
  break
353
  except: continue
354
 
355
+ if not response_payload: return "❌ **Erreur Critique :** Mes services cognitifs ne répondent pas. Vérifiez les clés API.", None, None
356
 
357
  try:
358
  decision = json.loads(re.sub(r'```json\s*|\s*```', '', response_payload).strip())
 
362
  if tool_name in self.tools_map:
363
  res = self.tools_map[tool_name]["func"]({})
364
  if res["status"] == "succès":
365
+ # C'est ici que la magie opère
366
  final_response = self._generate_human_response(tool_name, res["details"])
367
  else:
368
+ final_response = f"⚠️ **Alerte Technique :** {res.get('message')}"
369
  return final_response, thought, res
370
 
371
+ return decision.get("answer", "Je reste en veille."), thought, None
372
+ except Exception as e: return f"Erreur système interne : {str(e)}", None, None
373
 
374
  def show_terminal(client, sheet_name="bdd_mvp_faker"):
375
  apply_custom_styles()
 
385
  if msg.get("data"):
386
  with st.expander("💾 SYSTEM PAYLOAD"): st.json(msg["data"])
387
 
388
+ user_input = st.chat_input("Ordre à l'IA (ex: 'Audit complet', 'Situation RH', 'Prévisions')...")
389
  if user_input:
390
  st.session_state.chat_history.append({"role": "user", "content": user_input})
391
  st.rerun()
392
 
393
  if st.session_state.chat_history and st.session_state.chat_history[-1]["role"] == "user":
394
+ with st.spinner("Analyse stratégique en cours..."):
395
  response, thought, data = st.session_state.agent_instance.ask(st.session_state.chat_history[-1]["content"])
396
  st.session_state.chat_history.append({"role": "assistant", "content": response, "thought": thought, "data": data})
397
  st.rerun()