import streamlit as st import pandas as pd import json from typing import Any, Dict from agent import build_agent, chat, ml_predict # ton fichier agent.py # ========== CONFIG STREAMLIT ========== st.set_page_config( page_title="GENAI – Banking Lab", page_icon="🤖", layout="wide" ) # ========== SESSION STATE ========== if "agent" not in st.session_state: st.session_state.agent = build_agent() if "messages" not in st.session_state: st.session_state.messages = [] # [{"role": "user"/"assistant", "content": "..."}] if "uploaded_df" not in st.session_state: st.session_state.uploaded_df = None agent = st.session_state.agent # ========= PAGE HEADER GLOBAL ========= st.title("GENAI – Banking Lab") # ========= NAVIGATION PAR ONGLET EN HAUT ========= tab_eda, tab_ml, tab_chat = st.tabs(["📊 EDA", "🔮 Prédiction ML", "💬 Chatbot"]) # ==================== PAGE 1 : EDA ==================== with tab_eda: st.header("📊 Analyse Exploratoire – Risque Crédit") st.markdown( """ Explore les caractéristiques des clients et comprends les patterns associés au **risque de défaut**. """ ) # ================= CHARGEMENT CSV ================= uploaded_file = st.file_uploader("📂 Charger un fichier CSV (dataset crédit)", type=["csv"]) if uploaded_file: df = pd.read_csv(uploaded_file) st.session_state.uploaded_df = df else: df = st.session_state.uploaded_df if st.session_state.uploaded_df is not None else None if df is None: st.info("👉 Charge un fichier CSV pour commencer l'analyse.") st.stop() st.success(f"Dataset chargé : **{df.shape[0]} lignes**, **{df.shape[1]} colonnes**") # ================= APERCU ================= st.markdown("### 👀 Aperçu du dataset") st.dataframe(df.head(), use_container_width=True) # ================= INDICATEURS GLOBAUX ================= default_rate = df["default"].mean() * 100 colA, colB, colC = st.columns(3) colA.metric("Taux de défaut global", f"{default_rate:.1f} %") colB.metric("Clients sains", f"{(df['default']==0).sum()}") colC.metric("Clients en défaut", f"{(df['default']==1).sum()}") st.markdown("---") # ================= DISTRIBUTIONS PAR DEFAUT ================= st.markdown("## 📈 Variables clés vs défaut") numeric_cols = [ "fico_score", "debt_ratio", "income", "years_employed", "loan_amt_outstanding", "total_debt_outstanding" ] var = st.selectbox("Choisis une variable à explorer :", numeric_cols) import altair as alt chart = alt.Chart(df).mark_bar(opacity=0.7).encode( x=alt.X(var, bin=alt.Bin(maxbins=30)), y="count()", color=alt.Color("default:N", legend=alt.Legend(title="Default (0=OK, 1=Défaut)")) ).properties(width=650, height=350) st.altair_chart(chart) st.markdown("---") # ================= CORRÉLATION ================= st.markdown("## 🔗 Matrice de corrélation") corr = df.corr(numeric_only=True) st.dataframe(corr.style.background_gradient(cmap="Reds"), use_container_width=True) # Top variables explicatives st.markdown("### 🥇 Variables les plus corrélées avec le défaut") corr_default = corr["default"].drop("default").sort_values(ascending=False) st.bar_chart(corr_default) st.markdown("---") # ================= SCATTERPLOT ================= st.markdown("## 🧭 Scatterplot – localiser les zones à risque") x_var = st.selectbox("Axe X", numeric_cols, index=2) y_var = st.selectbox("Axe Y", numeric_cols, index=0) scatter = alt.Chart(df).mark_circle(size=60, opacity=0.6).encode( x=x_var, y=y_var, color=alt.Color("default:N", legend=alt.Legend(title="Défaut")), tooltip=["income", "fico_score", "debt_ratio", "default"] ).properties(width=750, height=450) st.altair_chart(scatter) st.success("Analyse EDA terminée ✔️") # ==================== PAGE 2 : FORMULAIRE PRÉDICTION ML ==================== with tab_ml: st.header("🔮 Prédiction de risque via le modèle ML (.pkl sur S3)") st.markdown( """ Remplis ce **questionnaire** : nous estimons ensuite le risque de défaut du client, et nous t’affichons une explication claire et visuelle. """ ) col_left, col_right = st.columns([1, 1]) # ========================= FORMULAIRE ========================= with col_left: st.markdown("### 🎯 Profil client / crédit") credit_lines = st.number_input( "Lignes de crédit ouvertes (credit_lines_outstanding)", min_value=0, max_value=50, value=5 ) loan_amt = st.number_input( "Montant du prêt en cours (€) – loan_amt_outstanding", min_value=0, max_value=1_000_000, value=15_000, step=1_000 ) total_debt = st.number_input( "Dette totale actuelle (€) – total_debt_outstanding", min_value=0, max_value=1_000_000, value=25_000, step=1_000 ) income = st.number_input( "Revenu annuel (€) – income", min_value=1, max_value=1_000_000, value=60_000, step=1_000 ) years = st.number_input( "Ancienneté dans l'emploi (années) – years_employed", min_value=0, max_value=50, value=10 ) fico = st.number_input( "Score FICO – fico_score", min_value=300, max_value=850, value=720 ) debt_ratio = total_debt / income if income > 0 else 0.0 st.metric("Debt ratio calculé", f"{debt_ratio:.2f}") default_payload = { "credit_lines_outstanding": credit_lines, "loan_amt_outstanding": loan_amt, "total_debt_outstanding": total_debt, "income": income, "years_employed": years, "fico_score": fico, "debt_ratio": debt_ratio } # ========================= JSON EDITABLE ========================= with col_right: st.markdown("### 🧾 Payload JSON (optionnel)") st.caption("Tu peux garder ce JSON tel quel ou l’ajuster manuellement avant la prédiction.") payload_str = st.text_area( "Payload envoyé à `ml_predict` :", value=json.dumps(default_payload, indent=2), height=260 ) lancer = st.button("🚀 Lancer la prédiction ML", type="primary") # ========================= PRÉDICTION & AFFICHAGE UX ========================= if lancer: try: payload = json.loads(payload_str) except json.JSONDecodeError as e: st.error(f"JSON invalide : {e}") payload = None if payload is not None: with st.spinner("Analyse du risque par le modèle…"): try: raw = ml_predict.invoke({"payload": payload}) except Exception as e: st.error(f"Erreur lors de l’appel de ml_predict : {e}") raw = None if raw is not None: # On essaye de parser le JSON retourné par le tool prediction = None try: parsed = json.loads(raw) prediction = parsed.get("prediction", {}) except Exception: prediction = None if prediction is None or not isinstance(prediction, dict): st.error("La réponse du modèle n’est pas dans le format attendu.") st.code(raw, language="json") else: label_name = prediction.get("label_name", "Résultat inconnu") risk_level = prediction.get("risk_level", "inconnu") proba_default = prediction.get("proba_default", None) explanation = prediction.get("explanation", "") features_used = prediction.get("features_used", []) # --------- Traduction du niveau de risque en jauge ---------- if isinstance(proba_default, (float, int)): proba_pct = max(0.0, min(float(proba_default), 1.0)) * 100 else: # fallback selon risk_level mapping = {"faible": 15.0, "modéré": 35.0, "élevé": 70.0} proba_pct = mapping.get(risk_level, 50.0) # Couleur / emoji selon le risque if risk_level == "faible": emoji = "🟢" texte_risque = "Risque faible" elif risk_level == "modéré": emoji = "🟠" texte_risque = "Risque modéré" elif risk_level == "élevé": emoji = "🔴" texte_risque = "Risque élevé" else: emoji = "⚪" texte_risque = "Risque non déterminé" st.markdown("---") st.subheader("🧠 Résultat de l’analyse du modèle") # Bloc résumé pour un client col_r1, col_r2 = st.columns([2, 1]) with col_r1: st.markdown( f""" **Verdict : {emoji} {label_name}** **Niveau de risque : {texte_risque}** """ ) if isinstance(proba_default, (float, int)): st.markdown( f"Le modèle estime une probabilité de défaut d’environ **{proba_pct:.1f}%**." ) if explanation: st.markdown(f"📝 *{explanation}*") with col_r2: st.markdown("### 📊 Jauge de risque") st.progress(int(proba_pct)) # Features utilisées – version simple if features_used: st.markdown("### 🔍 Variables prises en compte") st.write(", ".join(features_used)) # Détails techniques en expander with st.expander("🔧 Détails techniques / JSON brut"): st.markdown("**Réponse brute du tool `ml_predict` :**") st.code(raw, language="json") try: st.markdown("**Vue JSON parsée :**") st.json(parsed) except Exception: pass st.markdown("---") st.caption( "💡 Astuce : cette page sert pour les utilisateurs métier. " "Les développeurs peuvent récupérer le payload et la réponse brute dans l’expander." ) # ==================== PAGE 3 : CHATBOT ==================== with tab_chat: st.header("💬 Chat avec l’agent (web + RAG + ML)") st.markdown( """ Exemple de requêtes : - *“Résume-moi les frais de tenue de compte pour un non résident.”* - *“Utilise `rag_search` pour extraire les tarifs de découvert.”* - *“Appelle `ml_predict` avec {'credit_lines_outstanding': 5, ...} et explique le résultat.”* """ ) # Affichage de l'historique for msg in st.session_state.messages: with st.chat_message(msg["role"]): st.markdown(msg["content"]) # Champ d'entrée prompt = st.chat_input("Pose une question à l’agent…") if prompt: # 1. Ajout du message utilisateur st.session_state.messages.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.markdown(prompt) # 2. Appel agent AVEC L’HISTORIQUE COMPLET with st.chat_message("assistant"): with st.spinner("L’agent réfléchit…"): try: answer = chat(agent, st.session_state.messages) except Exception as e: answer = f"❌ ERREUR agent: {e}" st.markdown(answer) # 3. Ajout de la réponse assistant dans la mémoire st.session_state.messages.append({"role": "assistant", "content": answer})