alexandre-cameron-borges commited on
Commit
c93ee2a
·
verified ·
1 Parent(s): ae50a33

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +337 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,339 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ import pandas as pd
3
+ import json
4
+ from typing import Any, Dict
5
+
6
+ from agent import build_agent, chat, ml_predict # ton fichier agent.py
7
+
8
+ # ========== CONFIG STREAMLIT ==========
9
+ st.set_page_config(
10
+ page_title="GENAI – Banking Lab",
11
+ page_icon="🤖",
12
+ layout="wide"
13
+ )
14
+
15
+ # ========== SESSION STATE ==========
16
+ if "agent" not in st.session_state:
17
+ st.session_state.agent = build_agent()
18
+
19
+ if "messages" not in st.session_state:
20
+ st.session_state.messages = [] # [{"role": "user"/"assistant", "content": "..."}]
21
+
22
+ if "uploaded_df" not in st.session_state:
23
+ st.session_state.uploaded_df = None
24
+
25
+ agent = st.session_state.agent
26
+
27
+ # ========= PAGE HEADER GLOBAL =========
28
+ st.title("GENAI – Banking Lab")
29
+
30
+ # ========= NAVIGATION PAR ONGLET EN HAUT =========
31
+ tab_eda, tab_ml, tab_chat = st.tabs(["📊 EDA", "🔮 Prédiction ML", "💬 Chatbot"])
32
+
33
+ # ==================== PAGE 1 : EDA ====================
34
+ with tab_eda:
35
+ st.header("📊 Analyse Exploratoire – Risque Crédit")
36
+
37
+ st.markdown(
38
+ """
39
+ Explore les caractéristiques des clients et comprends les patterns associés au **risque de défaut**.
40
+ """
41
+ )
42
+
43
+ # ================= CHARGEMENT CSV =================
44
+ uploaded_file = st.file_uploader("📂 Charger un fichier CSV (dataset crédit)", type=["csv"])
45
+
46
+ if uploaded_file:
47
+ df = pd.read_csv(uploaded_file)
48
+ st.session_state.uploaded_df = df
49
+ else:
50
+ df = st.session_state.uploaded_df if st.session_state.uploaded_df is not None else None
51
+
52
+ if df is None:
53
+ st.info("👉 Charge un fichier CSV pour commencer l'analyse.")
54
+ st.stop()
55
+
56
+ st.success(f"Dataset chargé : **{df.shape[0]} lignes**, **{df.shape[1]} colonnes**")
57
+
58
+ # ================= APERCU =================
59
+ st.markdown("### 👀 Aperçu du dataset")
60
+ st.dataframe(df.head(), use_container_width=True)
61
+
62
+ # ================= INDICATEURS GLOBAUX =================
63
+ default_rate = df["default"].mean() * 100
64
+ colA, colB, colC = st.columns(3)
65
+ colA.metric("Taux de défaut global", f"{default_rate:.1f} %")
66
+ colB.metric("Clients sains", f"{(df['default']==0).sum()}")
67
+ colC.metric("Clients en défaut", f"{(df['default']==1).sum()}")
68
+
69
+ st.markdown("---")
70
+
71
+ # ================= DISTRIBUTIONS PAR DEFAUT =================
72
+ st.markdown("## 📈 Variables clés vs défaut")
73
+
74
+ numeric_cols = [
75
+ "fico_score", "debt_ratio", "income", "years_employed",
76
+ "loan_amt_outstanding", "total_debt_outstanding"
77
+ ]
78
+
79
+ var = st.selectbox("Choisis une variable à explorer :", numeric_cols)
80
+
81
+ import altair as alt
82
+ chart = alt.Chart(df).mark_bar(opacity=0.7).encode(
83
+ x=alt.X(var, bin=alt.Bin(maxbins=30)),
84
+ y="count()",
85
+ color=alt.Color("default:N", legend=alt.Legend(title="Default (0=OK, 1=Défaut)"))
86
+ ).properties(width=650, height=350)
87
+
88
+ st.altair_chart(chart)
89
+
90
+ st.markdown("---")
91
+
92
+ # ================= CORRÉLATION =================
93
+ st.markdown("## 🔗 Matrice de corrélation")
94
+
95
+ corr = df.corr(numeric_only=True)
96
+ st.dataframe(corr.style.background_gradient(cmap="Reds"), use_container_width=True)
97
+
98
+ # Top variables explicatives
99
+ st.markdown("### 🥇 Variables les plus corrélées avec le défaut")
100
+
101
+ corr_default = corr["default"].drop("default").sort_values(ascending=False)
102
+ st.bar_chart(corr_default)
103
+
104
+ st.markdown("---")
105
+
106
+ # ================= SCATTERPLOT =================
107
+ st.markdown("## 🧭 Scatterplot – localiser les zones à risque")
108
+
109
+ x_var = st.selectbox("Axe X", numeric_cols, index=2)
110
+ y_var = st.selectbox("Axe Y", numeric_cols, index=0)
111
+
112
+ scatter = alt.Chart(df).mark_circle(size=60, opacity=0.6).encode(
113
+ x=x_var,
114
+ y=y_var,
115
+ color=alt.Color("default:N", legend=alt.Legend(title="Défaut")),
116
+ tooltip=["income", "fico_score", "debt_ratio", "default"]
117
+ ).properties(width=750, height=450)
118
+
119
+ st.altair_chart(scatter)
120
+
121
+ st.success("Analyse EDA terminée ✔️")
122
+
123
+
124
+ # ==================== PAGE 2 : FORMULAIRE PRÉDICTION ML ====================
125
+ with tab_ml:
126
+ st.header("🔮 Prédiction de risque via le modèle ML (.pkl sur S3)")
127
+
128
+ st.markdown(
129
+ """
130
+ Remplis ce **questionnaire** : nous estimons ensuite le risque de défaut du client,
131
+ et nous t’affichons une explication claire et visuelle.
132
+ """
133
+ )
134
+
135
+ col_left, col_right = st.columns([1, 1])
136
+
137
+ # ========================= FORMULAIRE =========================
138
+ with col_left:
139
+ st.markdown("### 🎯 Profil client / crédit")
140
+
141
+ credit_lines = st.number_input(
142
+ "Lignes de crédit ouvertes (credit_lines_outstanding)",
143
+ min_value=0, max_value=50, value=5
144
+ )
145
+
146
+ loan_amt = st.number_input(
147
+ "Montant du prêt en cours (€) – loan_amt_outstanding",
148
+ min_value=0, max_value=1_000_000, value=15_000, step=1_000
149
+ )
150
+
151
+ total_debt = st.number_input(
152
+ "Dette totale actuelle (€) – total_debt_outstanding",
153
+ min_value=0, max_value=1_000_000, value=25_000, step=1_000
154
+ )
155
+
156
+ income = st.number_input(
157
+ "Revenu annuel (€) – income",
158
+ min_value=1, max_value=1_000_000, value=60_000, step=1_000
159
+ )
160
+
161
+ years = st.number_input(
162
+ "Ancienneté dans l'emploi (années) – years_employed",
163
+ min_value=0, max_value=50, value=10
164
+ )
165
+
166
+ fico = st.number_input(
167
+ "Score FICO – fico_score",
168
+ min_value=300, max_value=850, value=720
169
+ )
170
+
171
+ debt_ratio = total_debt / income if income > 0 else 0.0
172
+ st.metric("Debt ratio calculé", f"{debt_ratio:.2f}")
173
+
174
+ default_payload = {
175
+ "credit_lines_outstanding": credit_lines,
176
+ "loan_amt_outstanding": loan_amt,
177
+ "total_debt_outstanding": total_debt,
178
+ "income": income,
179
+ "years_employed": years,
180
+ "fico_score": fico,
181
+ "debt_ratio": debt_ratio
182
+ }
183
+
184
+ # ========================= JSON EDITABLE =========================
185
+ with col_right:
186
+ st.markdown("### 🧾 Payload JSON (optionnel)")
187
+
188
+ st.caption("Tu peux garder ce JSON tel quel ou l’ajuster manuellement avant la prédiction.")
189
+
190
+ payload_str = st.text_area(
191
+ "Payload envoyé à `ml_predict` :",
192
+ value=json.dumps(default_payload, indent=2),
193
+ height=260
194
+ )
195
+
196
+ lancer = st.button("🚀 Lancer la prédiction ML", type="primary")
197
+
198
+ # ========================= PRÉDICTION & AFFICHAGE UX =========================
199
+ if lancer:
200
+ try:
201
+ payload = json.loads(payload_str)
202
+ except json.JSONDecodeError as e:
203
+ st.error(f"JSON invalide : {e}")
204
+ payload = None
205
+
206
+ if payload is not None:
207
+ with st.spinner("Analyse du risque par le modèle…"):
208
+ try:
209
+ raw = ml_predict.invoke({"payload": payload})
210
+ except Exception as e:
211
+ st.error(f"Erreur lors de l’appel de ml_predict : {e}")
212
+ raw = None
213
+
214
+ if raw is not None:
215
+ # On essaye de parser le JSON retourné par le tool
216
+ prediction = None
217
+ try:
218
+ parsed = json.loads(raw)
219
+ prediction = parsed.get("prediction", {})
220
+ except Exception:
221
+ prediction = None
222
+
223
+ if prediction is None or not isinstance(prediction, dict):
224
+ st.error("La réponse du modèle n’est pas dans le format attendu.")
225
+ st.code(raw, language="json")
226
+ else:
227
+ label_name = prediction.get("label_name", "Résultat inconnu")
228
+ risk_level = prediction.get("risk_level", "inconnu")
229
+ proba_default = prediction.get("proba_default", None)
230
+ explanation = prediction.get("explanation", "")
231
+ features_used = prediction.get("features_used", [])
232
+
233
+ # --------- Traduction du niveau de risque en jauge ----------
234
+ if isinstance(proba_default, (float, int)):
235
+ proba_pct = max(0.0, min(float(proba_default), 1.0)) * 100
236
+ else:
237
+ # fallback selon risk_level
238
+ mapping = {"faible": 15.0, "modéré": 35.0, "élevé": 70.0}
239
+ proba_pct = mapping.get(risk_level, 50.0)
240
+
241
+ # Couleur / emoji selon le risque
242
+ if risk_level == "faible":
243
+ emoji = "🟢"
244
+ texte_risque = "Risque faible"
245
+ elif risk_level == "modéré":
246
+ emoji = "🟠"
247
+ texte_risque = "Risque modéré"
248
+ elif risk_level == "élevé":
249
+ emoji = "🔴"
250
+ texte_risque = "Risque élevé"
251
+ else:
252
+ emoji = "⚪"
253
+ texte_risque = "Risque non déterminé"
254
+
255
+ st.markdown("---")
256
+ st.subheader("🧠 Résultat de l’analyse du modèle")
257
+
258
+ # Bloc résumé pour un client
259
+ col_r1, col_r2 = st.columns([2, 1])
260
+ with col_r1:
261
+ st.markdown(
262
+ f"""
263
+ **Verdict : {emoji} {label_name}**
264
+ **Niveau de risque : {texte_risque}**
265
+ """
266
+ )
267
+ if isinstance(proba_default, (float, int)):
268
+ st.markdown(
269
+ f"Le modèle estime une probabilité de défaut d’environ **{proba_pct:.1f}%**."
270
+ )
271
+ if explanation:
272
+ st.markdown(f"📝 *{explanation}*")
273
+
274
+ with col_r2:
275
+ st.markdown("### 📊 Jauge de risque")
276
+ st.progress(int(proba_pct))
277
+
278
+ # Features utilisées – version simple
279
+ if features_used:
280
+ st.markdown("### 🔍 Variables prises en compte")
281
+ st.write(", ".join(features_used))
282
+
283
+ # Détails techniques en expander
284
+ with st.expander("🔧 Détails techniques / JSON brut"):
285
+ st.markdown("**Réponse brute du tool `ml_predict` :**")
286
+ st.code(raw, language="json")
287
+ try:
288
+ st.markdown("**Vue JSON parsée :**")
289
+ st.json(parsed)
290
+ except Exception:
291
+ pass
292
+
293
+ st.markdown("---")
294
+ st.caption(
295
+ "💡 Astuce : cette page sert pour les utilisateurs métier. "
296
+ "Les développeurs peuvent récupérer le payload et la réponse brute dans l’expander."
297
+ )
298
+
299
+
300
+ # ==================== PAGE 3 : CHATBOT ====================
301
+ with tab_chat:
302
+ st.header("💬 Chat avec l’agent (web + RAG + ML)")
303
+
304
+ st.markdown(
305
+ """
306
+ Exemple de requêtes :
307
+ - *“Résume-moi les frais de tenue de compte pour un non résident.”*
308
+ - *“Utilise `rag_search` pour extraire les tarifs de découvert.”*
309
+ - *“Appelle `ml_predict` avec {'credit_lines_outstanding': 5, ...} et explique le résultat.”*
310
+ """
311
+ )
312
+
313
+ # Affichage de l'historique
314
+ for msg in st.session_state.messages:
315
+ with st.chat_message(msg["role"]):
316
+ st.markdown(msg["content"])
317
+
318
+ # Champ d'entrée
319
+ prompt = st.chat_input("Pose une question à l’agent…")
320
+
321
+ if prompt:
322
+ # 1. Ajout du message utilisateur
323
+ st.session_state.messages.append({"role": "user", "content": prompt})
324
+
325
+ with st.chat_message("user"):
326
+ st.markdown(prompt)
327
+
328
+ # 2. Appel agent AVEC L’HISTORIQUE COMPLET
329
+ with st.chat_message("assistant"):
330
+ with st.spinner("L’agent réfléchit…"):
331
+ try:
332
+ answer = chat(agent, st.session_state.messages)
333
+ except Exception as e:
334
+ answer = f"❌ ERREUR agent: {e}"
335
+
336
+ st.markdown(answer)
337
 
338
+ # 3. Ajout de la réponse assistant dans la mémoire
339
+ st.session_state.messages.append({"role": "assistant", "content": answer})