update ui and use google ai
Browse files
app.py
CHANGED
|
@@ -3,135 +3,141 @@ import pandas as pd
|
|
| 3 |
import numpy as np
|
| 4 |
import gradio as gr
|
| 5 |
import plotly.graph_objects as go
|
| 6 |
-
|
| 7 |
-
from huggingface_hub import InferenceClient
|
| 8 |
|
| 9 |
-
# ---
|
| 10 |
-
|
| 11 |
-
|
|
|
|
| 12 |
|
| 13 |
-
# CSS
|
| 14 |
custom_css = """
|
| 15 |
-
@import url('https://fonts.googleapis.com/css2?family=
|
| 16 |
-
body { font-family: '
|
| 17 |
-
.gradio-container { max-width:
|
| 18 |
-
.
|
| 19 |
-
.
|
| 20 |
-
.
|
| 21 |
-
.
|
|
|
|
| 22 |
"""
|
| 23 |
|
| 24 |
-
class
|
| 25 |
def __init__(self):
|
| 26 |
self.load_data()
|
| 27 |
-
try:
|
| 28 |
-
self.classifier = pipeline("text-classification", model="archon_v1")
|
| 29 |
-
except:
|
| 30 |
-
self.classifier = None
|
| 31 |
|
| 32 |
def load_data(self):
|
| 33 |
-
|
| 34 |
-
self.
|
| 35 |
-
self.
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
def
|
| 38 |
-
#
|
| 39 |
u_txn = self.df_txn[self.df_txn['customer_id'] == customer_id].copy()
|
| 40 |
u_bal = self.df_bal[self.df_bal['customer_id'] == customer_id].sort_values('month')
|
| 41 |
u_rep = self.df_rep[self.df_rep['customer_id'] == customer_id]
|
| 42 |
|
| 43 |
-
if u_txn.empty or u_bal.empty:
|
|
|
|
| 44 |
|
| 45 |
-
# --- FASE 4:
|
| 46 |
income = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
|
| 47 |
expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
|
| 48 |
er = expense / income if income > 0 else 1.0
|
| 49 |
|
| 50 |
-
# Scoring
|
| 51 |
er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
|
| 52 |
bt_s = 1.0 if len(u_bal) >= 2 and u_bal.iloc[-1]['avg_balance'] < u_bal.iloc[-2]['avg_balance'] else 0.0
|
| 53 |
od_s = 1.0 if (u_bal['min_balance'] <= 0).any() else 0.0
|
| 54 |
mp_s = 1.0 if (u_rep['status'] == 'late').any() else 0.0
|
|
|
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
risk_lv = "HIGH" if score >= 0.7 else ("MEDIUM" if score >= 0.4 else "LOW")
|
| 59 |
|
| 60 |
-
return risk_lv,
|
| 61 |
|
| 62 |
-
def
|
| 63 |
-
# FASE 5: NBO
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
prompt = f"<s>[INST] Anda adalah AI Financial Advisor Bank. Nasabah {cust_id} memiliki risiko {risk_lv} dengan pengeluaran {er:.1%}. Transaksi terakhirnya adalah '{last_txn}'. Berikan 1 saran finansial singkat yang SANGAT NATURAL (tidak kaku), empati, dan solutif dalam Bahasa Indonesia. Jangan gunakan kata 'Nasabah'. [/INST]</s>"
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
try:
|
| 69 |
-
response =
|
| 70 |
-
return response.
|
| 71 |
except:
|
| 72 |
-
return "
|
| 73 |
|
| 74 |
def create_viz(self, u_bal, u_txn):
|
| 75 |
-
#
|
|
|
|
| 76 |
fig_bal = go.Figure()
|
| 77 |
-
fig_bal.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='
|
| 78 |
-
fig_bal.add_trace(go.Bar(x=u_bal['month'], y=u_bal['min_balance'], name='
|
| 79 |
-
fig_bal.update_layout(title="
|
| 80 |
|
| 81 |
-
#
|
| 82 |
-
u_txn['
|
| 83 |
-
|
| 84 |
-
|
| 85 |
fig_cf = go.Figure()
|
| 86 |
-
fig_cf.add_trace(go.Bar(x=
|
| 87 |
-
fig_cf.add_trace(go.Bar(x=
|
| 88 |
-
fig_cf.update_layout(title="
|
| 89 |
|
| 90 |
return fig_bal, fig_cf
|
| 91 |
|
| 92 |
# --- INTERFACE ---
|
| 93 |
-
engine =
|
| 94 |
|
| 95 |
-
def
|
| 96 |
-
|
| 97 |
-
if not
|
|
|
|
| 98 |
|
| 99 |
-
risk_lv, score, er, u_bal, u_txn =
|
| 100 |
-
advice = engine.
|
| 101 |
-
|
| 102 |
|
| 103 |
-
|
| 104 |
|
| 105 |
-
|
| 106 |
-
<div class="risk-card {
|
| 107 |
-
<
|
| 108 |
<p><b>Risk Score:</b> {score:.2f} | <b>Expense Ratio:</b> {er:.1%}</p>
|
| 109 |
-
<
|
| 110 |
-
<p><i>Sistem mendeteksi aktivitas keuangan nasabah memerlukan perhatian khusus pada manajemen saldo harian.</i></p>
|
| 111 |
</div>
|
| 112 |
"""
|
| 113 |
-
return
|
| 114 |
|
| 115 |
-
with gr.Blocks(css=custom_css
|
| 116 |
-
gr.
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
| 120 |
with gr.Column(scale=1):
|
| 121 |
-
|
| 122 |
-
btn = gr.Button("
|
| 123 |
-
|
| 124 |
|
| 125 |
with gr.Column(scale=2):
|
| 126 |
with gr.Tabs():
|
| 127 |
-
with gr.TabItem("
|
| 128 |
-
plot_bal = gr.Plot()
|
| 129 |
-
with gr.TabItem("Cashflow Insight"):
|
| 130 |
plot_cf = gr.Plot()
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
| 134 |
|
| 135 |
-
btn.click(fn=
|
| 136 |
|
| 137 |
demo.launch()
|
|
|
|
| 3 |
import numpy as np
|
| 4 |
import gradio as gr
|
| 5 |
import plotly.graph_objects as go
|
| 6 |
+
import google.generativeai as genai
|
|
|
|
| 7 |
|
| 8 |
+
# --- KONFIGURASI GEMINI (Ganti dari Mistral ke Gemini) ---
|
| 9 |
+
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
| 10 |
+
genai.configure(api_key=GOOGLE_API_KEY)
|
| 11 |
+
gemini_model = genai.GenerativeModel('gemini-1.5-flash')
|
| 12 |
|
| 13 |
+
# --- CSS CUSTOM: STYLE BANK NAGARI ---
|
| 14 |
custom_css = """
|
| 15 |
+
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
|
| 16 |
+
body { font-family: 'Roboto', sans-serif; background-color: #f4f4f4; }
|
| 17 |
+
.gradio-container { max-width: 1250px !important; }
|
| 18 |
+
.nagari-header { background-color: #800000; color: white; padding: 20px; border-radius: 10px 10px 0 0; border-bottom: 4px solid #FFD700; }
|
| 19 |
+
.risk-card { padding: 25px; border-radius: 12px; background: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1); border-top: 5px solid #800000; }
|
| 20 |
+
.risk-high { border-left: 10px solid #d32f2f; }
|
| 21 |
+
.risk-medium { border-left: 10px solid #f9a825; }
|
| 22 |
+
.risk-low { border-left: 10px solid #2e7d32; }
|
| 23 |
"""
|
| 24 |
|
| 25 |
+
class ArchonNagariEngine:
|
| 26 |
def __init__(self):
|
| 27 |
self.load_data()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
def load_data(self):
|
| 30 |
+
# Fase 1: Foundation (Cek error pada data mentah)
|
| 31 |
+
self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).fillna("")
|
| 32 |
+
self.df_cust = pd.read_csv('customers.csv').fillna(0)
|
| 33 |
+
self.df_bal = pd.read_csv('balances_revised.csv', parse_dates=['month']).fillna(0)
|
| 34 |
+
self.df_rep = pd.read_csv('repayments_revised.csv', parse_dates=['due_date']).fillna("on_time")
|
| 35 |
|
| 36 |
+
def run_analysis(self, customer_id):
|
| 37 |
+
# Validasi Keberadaan Data
|
| 38 |
u_txn = self.df_txn[self.df_txn['customer_id'] == customer_id].copy()
|
| 39 |
u_bal = self.df_bal[self.df_bal['customer_id'] == customer_id].sort_values('month')
|
| 40 |
u_rep = self.df_rep[self.df_rep['customer_id'] == customer_id]
|
| 41 |
|
| 42 |
+
if u_txn.empty or u_bal.empty:
|
| 43 |
+
return None
|
| 44 |
|
| 45 |
+
# --- FASE 4: RISK SCORING (WEIGHTED 30/20/20/20/10) ---
|
| 46 |
income = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
|
| 47 |
expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
|
| 48 |
er = expense / income if income > 0 else 1.0
|
| 49 |
|
| 50 |
+
# Scoring Rules (Fase 4)
|
| 51 |
er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
|
| 52 |
bt_s = 1.0 if len(u_bal) >= 2 and u_bal.iloc[-1]['avg_balance'] < u_bal.iloc[-2]['avg_balance'] else 0.0
|
| 53 |
od_s = 1.0 if (u_bal['min_balance'] <= 0).any() else 0.0
|
| 54 |
mp_s = 1.0 if (u_rep['status'] == 'late').any() else 0.0
|
| 55 |
+
vol_s = 0.5 # Placeholder Volatility
|
| 56 |
|
| 57 |
+
final_score = (0.3 * er_s) + (0.2 * bt_s) + (0.2 * od_s) + (0.2 * mp_s) + (0.1 * vol_s)
|
| 58 |
+
risk_lv = "HIGH" if final_score >= 0.7 else ("MEDIUM" if final_score >= 0.4 else "LOW")
|
|
|
|
| 59 |
|
| 60 |
+
return risk_lv, final_score, er, u_bal, u_txn
|
| 61 |
|
| 62 |
+
def get_gemini_advice(self, risk_lv, er, cust_id, u_txn):
|
| 63 |
+
# FASE 5: NBO DENGAN GEMINI (Sangat Natural & Tidak Template)
|
| 64 |
+
tx_history = u_txn.tail(5)['raw_description'].tolist()
|
| 65 |
+
context = f"Nasabah {cust_id} memiliki risiko {risk_lv}, rasio belanja {er:.1%}. Transaksi terakhir: {', '.join(tx_history)}."
|
|
|
|
| 66 |
|
| 67 |
+
prompt = f"""
|
| 68 |
+
Tugas: Sebagai Virtual Advisor Bank Nagari yang bijak.
|
| 69 |
+
Data: {context}
|
| 70 |
+
Instruksi: Berikan saran finansial yang personal, hangat, dan solutif dalam Bahasa Indonesia.
|
| 71 |
+
Gunakan sapaan 'Bapak/Ibu'. Jangan terlihat seperti bot. Fokus pada peningkatan ketahanan finansial (Resilience).
|
| 72 |
+
Maksimal 3 kalimat.
|
| 73 |
+
"""
|
| 74 |
try:
|
| 75 |
+
response = gemini_model.generate_content(prompt)
|
| 76 |
+
return response.text
|
| 77 |
except:
|
| 78 |
+
return "Kami menyarankan Bapak/Ibu untuk meninjau kembali pos pengeluaran bulan ini agar saldo tetap terjaga sehat."
|
| 79 |
|
| 80 |
def create_viz(self, u_bal, u_txn):
|
| 81 |
+
# FASE 6: VISUALIZATION (BANK NAGARI STYLE)
|
| 82 |
+
# 1. Trend Saldo
|
| 83 |
fig_bal = go.Figure()
|
| 84 |
+
fig_bal.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Rata-rata Saldo', line=dict(color='#800000', width=4)))
|
| 85 |
+
fig_bal.add_trace(go.Bar(x=u_bal['month'], y=u_bal['min_balance'], name='Saldo Minimum', marker_color='#FFD700', opacity=0.5))
|
| 86 |
+
fig_bal.update_layout(title="Laporan Tren Saldo Bulanan", template="plotly_white", legend=dict(orientation="h"))
|
| 87 |
|
| 88 |
+
# 2. Income vs Expense
|
| 89 |
+
u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
|
| 90 |
+
cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
|
|
|
|
| 91 |
fig_cf = go.Figure()
|
| 92 |
+
fig_cf.add_trace(go.Bar(x=cf.index, y=cf.get('credit', 0), name='Pemasukan', marker_color='#2e7d32'))
|
| 93 |
+
fig_cf.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran', marker_color='#800000'))
|
| 94 |
+
fig_cf.update_layout(title="Arus Kas Pemasukan vs Pengeluaran", barmode='group', template="plotly_white")
|
| 95 |
|
| 96 |
return fig_bal, fig_cf
|
| 97 |
|
| 98 |
# --- INTERFACE ---
|
| 99 |
+
engine = ArchonNagariEngine()
|
| 100 |
|
| 101 |
+
def run_archon(cust_id):
|
| 102 |
+
res = engine.run_analysis(cust_id)
|
| 103 |
+
if not res:
|
| 104 |
+
return "### ⚠️ Data Tidak Ditemukan", "ID tidak terdaftar di sistem.", None, None
|
| 105 |
|
| 106 |
+
risk_lv, score, er, u_bal, u_txn = res
|
| 107 |
+
advice = engine.get_gemini_advice(risk_lv, er, cust_id, u_txn)
|
| 108 |
+
p_bal, p_cf = engine.create_viz(u_bal, u_txn)
|
| 109 |
|
| 110 |
+
status_cls = "risk-high" if risk_lv == "HIGH" else ("risk-medium" if risk_lv == "MEDIUM" else "risk-low")
|
| 111 |
|
| 112 |
+
report_html = f"""
|
| 113 |
+
<div class="risk-card {status_cls}">
|
| 114 |
+
<h2 style="color: #800000; margin-top:0;">📋 Ringkasan Risiko: {risk_lv}</h2>
|
| 115 |
<p><b>Risk Score:</b> {score:.2f} | <b>Expense Ratio:</b> {er:.1%}</p>
|
| 116 |
+
<p style="font-size: 0.9em; color: #666;">Berdasarkan bobot parameter Fase 4: Pengeluaran, Saldo, dan Riwayat Cicilan.</p>
|
|
|
|
| 117 |
</div>
|
| 118 |
"""
|
| 119 |
+
return report_html, advice, p_bal, p_cf
|
| 120 |
|
| 121 |
+
with gr.Blocks(css=custom_css) as demo:
|
| 122 |
+
with gr.Div(elem_classes="nagari-header"):
|
| 123 |
+
gr.Markdown("# 🛡️ ARCHON-AI: FINANCIAL RESILIENCE ENGINE")
|
| 124 |
+
gr.Markdown("Pusat Intelijen Manajemen Risiko & Perilaku Nasabah")
|
| 125 |
+
|
| 126 |
+
with gr.Row(variant="panel"):
|
| 127 |
with gr.Column(scale=1):
|
| 128 |
+
id_input = gr.Textbox(label="Customer ID", placeholder="e.g., C0014")
|
| 129 |
+
btn = gr.Button("PROSES ANALISIS", variant="primary")
|
| 130 |
+
out_report = gr.HTML()
|
| 131 |
|
| 132 |
with gr.Column(scale=2):
|
| 133 |
with gr.Tabs():
|
| 134 |
+
with gr.TabItem("Analisis Arus Kas"):
|
|
|
|
|
|
|
| 135 |
plot_cf = gr.Plot()
|
| 136 |
+
with gr.TabItem("Tren Saldo"):
|
| 137 |
+
plot_bal = gr.Plot()
|
| 138 |
+
|
| 139 |
+
out_advice = gr.Textbox(label="Saran AI Virtual Advisor (Powered by Gemini)", lines=4)
|
| 140 |
|
| 141 |
+
btn.click(fn=run_archon, inputs=id_input, outputs=[out_report, out_advice, plot_bal, plot_cf])
|
| 142 |
|
| 143 |
demo.launch()
|