ZakyF commited on
Commit
404a444
·
1 Parent(s): 26afa72

feat: Refactor Archon analysis engine, enhance risk reporting, and update UI/UX with Nagari branding.

Browse files
Files changed (1) hide show
  1. app.py +100 -90
app.py CHANGED
@@ -4,161 +4,171 @@ import numpy as np
4
  import gradio as gr
5
  import plotly.graph_objects as go
6
  from google import genai
7
- from transformers import pipeline
8
 
9
- # --- KONFIGURASI GENERATIVE AI ---
10
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
11
- # Inisialisasi Client (SDK Terbaru 2026)
12
- client_ai = genai.Client(api_key=GOOGLE_API_KEY) if GOOGLE_API_KEY else None
13
 
14
- # --- PALET WARNA BANK NAGARI ---
15
- # #0514DE (Deep Blue), #82C3EB (Light Blue), #E0EDF4 (Pale), #F7BD87 (Gold), #FFFFFF (White)
16
  custom_css = """
17
  @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap');
18
- .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background-color: #FFFFFF !important; }
19
- .nagari-header { background: linear-gradient(135deg, #0514DE 0%, #82C3EB 100%); color: white; padding: 30px; border-radius: 15px; border-bottom: 6px solid #F7BD87; margin-bottom: 25px; text-align: center; }
20
- .card-nagari { background: #E0EDF4; border-radius: 12px; padding: 20px; border: 1px solid #82C3EB; box-shadow: 0 4px 10px rgba(5, 20, 122, 0.05); }
21
- .status-badge { display: inline-block; padding: 5px 15px; border-radius: 20px; font-weight: bold; color: white; margin-top: 10px; }
22
- .report-box { background: white; border-radius: 10px; padding: 20px; border-left: 6px solid #0514DE; line-height: 1.6; }
23
- .advice-box { background: #fffdf0; border: 1px solid #F7BD87; padding: 20px; border-radius: 10px; }
24
  """
25
 
26
- class ArchonNagariEngine:
27
  def __init__(self):
28
  self.load_data()
29
- try: self.classifier = pipeline("text-classification", model="archon_v1")
30
- except: self.classifier = None
31
 
32
  def load_data(self):
33
- # Fase 1: Foundation (Fix C0014 error handling)
34
- self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).fillna({"raw_description": "Transaksi Umum"})
35
- self.df_cust = pd.read_csv('customers.csv')
36
- self.df_bal = pd.read_csv('balances_revised.csv', parse_dates=['month']).fillna(0)
37
- self.df_rep = pd.read_csv('repayments_revised.csv', parse_dates=['due_date']).fillna("on_time")
38
-
39
- def analyze(self, customer_id):
40
- # 1. Filter Data & Validation
41
- u_txn = self.df_txn[self.df_txn['customer_id'] == customer_id].copy()
42
- u_bal = self.df_bal[self.df_bal['customer_id'] == customer_id].sort_values('month')
43
- u_rep = self.df_rep[self.df_rep['customer_id'] == customer_id]
 
44
 
45
- if u_txn.empty or u_bal.empty: return None
 
 
 
 
 
 
 
 
46
 
47
- # --- FASE 4: RISK SCORING (DETERMINISTIC AI) ---
48
  income_txn = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
49
- cust_profile = self.df_cust[self.df_cust['customer_id'] == customer_id].iloc[0]
50
- base_inc = max(income_txn, cust_profile['monthly_income'])
51
  expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
52
- er = expense / base_inc
53
 
 
54
  er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
55
  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
56
  od_s = 1.0 if (u_bal['min_balance'] <= 0).any() else 0.0
57
  mp_s = 1.0 if (u_rep['status'] == 'late').any() else 0.0
58
 
59
- # Risk Score 0.0 - 1.0 (Bobot 30/20/20/20/10)
60
  final_score = (0.3 * er_s) + (0.2 * bt_s) + (0.2 * od_s) + (0.2 * mp_s) + 0.1
61
  risk_lv = "HIGH" if final_score >= 0.7 else ("MEDIUM" if final_score >= 0.4 else "LOW")
62
 
63
- return risk_lv, final_score, er, u_bal, u_txn, base_inc, expense
64
 
65
- def get_deterministic_explanation(self, risk_lv, score, er, u_bal):
66
- # Penjelasan adaptif berdasarkan output riil (Backup jika LLM gagal)
67
- exp = f"### 📖 Interpretasi Analisis Archon\n"
 
 
68
  if risk_lv == "HIGH":
69
- exp += f"⚠️ **Level Risiko Tinggi ({score:.2f})**: Perilaku keuangan Anda memerlukan restrukturisasi segera. "
70
  elif risk_lv == "MEDIUM":
71
- exp += f"🔔 **Level Risiko Menengah ({score:.2f})**: Anda berada dalam zona peringatan dini (Early Warning). "
 
 
 
 
 
 
 
 
72
  else:
73
- exp += f" **Level Risiko Rendah ({score:.2f})**: Resiliensi finansial Anda terpantau sangat sehat. "
 
 
 
 
 
 
 
 
 
74
 
75
- exp += f"\n\n**Rincian Metrik:**\n"
76
- exp += f"- **Expense Ratio ({er:.1%})**: Anda menggunakan {er:.1%} pendapatan untuk belanja. "
77
- exp += f"{'Sangat boros (>80%), kurangi pengeluaran lifestyle.' if er > 0.8 else 'Cukup aman, namun tetap waspada.'}\n"
78
-
79
- latest_bal = u_bal.iloc[-1]['avg_balance']
80
- exp += f"- **Tren Saldo**: Saldo rata-rata Anda Rp{latest_bal:,.0f}. "
81
- exp += f"{'Tren menurun dibanding bulan lalu.' if len(u_bal) > 1 and latest_bal < u_bal.iloc[-2]['avg_balance'] else 'Saldo terpantau stabil/naik.'}"
82
-
83
- return exp
84
-
85
- def get_gemini_advice(self, risk_lv, er, cust_id, u_txn):
86
- # FASE 5: NBO GENERATIVE
87
- if not client_ai: return None
88
- merchants = u_txn.tail(3)['raw_description'].tolist()
89
- prompt = f"""
90
- Identitas: Senior Wealth Manager Bank Nagari.
91
- Analisis Nasabah {cust_id}: Risiko {risk_lv}, Rasio Pengeluaran {er:.1%}.
92
- Riwayat Belanja: {', '.join(merchants)}.
93
- Tugas: Berikan saran finansial spesifik & empati untuk Bapak/Ibu. Hubungkan dengan pengeluaran di {merchants[0]}. Maks 3 kalimat.
94
- """
95
  try:
96
- resp = client_ai.models.generate_content(model="gemini-1.5-flash", contents=prompt)
97
  return resp.text
98
  except: return None
99
 
100
  def create_viz(self, u_bal, u_txn):
101
- # Plot 1: Cashflow (Fase 6)
102
  u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
103
  cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
104
  fig1 = go.Figure()
105
  fig1.add_trace(go.Bar(x=cf.index, y=cf.get('credit', 0), name='Pemasukan', marker_color='#82C3EB'))
106
  fig1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran', marker_color='#0514DE'))
107
- fig1.update_layout(title="Inflow vs Outflow Bulanan", barmode='group', template='plotly_white')
108
 
109
- # Plot 2: Balance Trend
110
  fig2 = go.Figure()
111
  fig2.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Saldo Rata-rata', line=dict(color='#F7BD87', width=4)))
112
- fig2.update_layout(title="Kesehatan Saldo (Fase 6)", template='plotly_white')
113
  return fig1, fig2
114
 
115
- # --- UI LOGIC ---
116
- engine = ArchonNagariEngine()
117
 
118
  def run_archon(cust_id):
119
- res = engine.analyze(cust_id)
120
- if not res: return "## ❌ ID Tidak Valid", "Data tidak ditemukan.", None, None
 
121
 
122
  risk_lv, score, er, u_bal, u_txn, inc, exp = res
123
- det_exp = engine.get_deterministic_explanation(risk_lv, score, er, u_bal)
124
- gen_advice = engine.get_gemini_advice(risk_lv, er, cust_id, u_txn)
125
- f1, f2 = engine.create_plots(u_bal, u_txn)
126
 
127
- # Gabungkan penjelasan Deterministik dengan Gemini (Jika Gemini ok)
128
- final_report = det_exp
129
  if gen_advice:
130
- final_report += f"\n\n---\n**💡 Rekomendasi Virtual Advisor:**\n{gen_advice}"
131
- else:
132
- final_report += f"\n\n---\n*Saran Virtual Advisor sedang dalam sinkronisasi sistem.*"
133
-
134
  color = "#ef4444" if risk_lv == "HIGH" else ("#f59e0b" if risk_lv == "MEDIUM" else "#10b981")
135
  status_html = f"""
136
  <div class='card-nagari'>
137
- <h2 style='color: #0514DE; margin:0;'>Hasil Analisis AI</h2>
138
- <span class='status-badge' style='background:{color}'>{risk_lv} RISK</span>
139
- <p style='margin-top:15px;'><b>Risk Score:</b> {score:.2f} / 1.00</p>
140
- <p><b>Expense Ratio:</b> {er:.1%}</p>
141
  </div>
142
  """
143
- return status_html, final_report, f1, f2
144
 
145
  with gr.Blocks(css=custom_css) as demo:
146
- gr.HTML("<div class='nagari-header'><h1>🛡️ ARCHON-AI: FINANCIAL ADVISOR</h1><p>Sistem Intelijen Resiliensi Finansial & Manajemen Risiko</p></div>")
147
 
148
  with gr.Row():
149
  with gr.Column(scale=1):
150
- id_in = gr.Textbox(label="Customer ID", placeholder="Masukkan ID (C0001 - C0120)")
151
- btn = gr.Button("PROSES DATA", variant="primary")
152
  out_status = gr.HTML()
153
  gr.Markdown("---")
154
- gr.Markdown("ℹ️ **Info Metrik**: Skor risiko didasarkan pada perpaduan saldo, rasio belanja, dan kedisiplinan cicilan nasabah.")
155
 
156
  with gr.Column(scale=2):
157
  with gr.Tabs():
158
- with gr.TabItem("Analisis Cashflow"): plot_1 = gr.Plot()
159
- with gr.TabItem("Tren Pertumbuhan Saldo"): plot_2 = gr.Plot()
160
- out_report = gr.Markdown(elem_classes="report-box")
161
 
162
- btn.click(fn=run_archon, inputs=id_in, outputs=[out_status, out_report, plot_1, plot_2])
163
 
164
  demo.launch()
 
4
  import gradio as gr
5
  import plotly.graph_objects as go
6
  from google import genai
 
7
 
8
+ # --- KONFIGURASI GENERATIVE AI (GEMINI 2026) ---
9
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
10
+ # Pastikan GOOGLE_API_KEY sudah terisi di Secrets
11
+ ai_client = genai.Client(api_key=GOOGLE_API_KEY) if GOOGLE_API_KEY else None
12
 
13
+ # --- PALET WARNA NAGARI (BIRU MUDA - EMAS) ---
14
+ # Blue: #0514DE, Light Blue: #82C3EB, Pale: #E0EDF4, Gold: #F7BD87, White: #FFFFFF
15
  custom_css = """
16
  @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap');
17
+ body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background-color: #FFFFFF !important; }
18
+ .nagari-header { background: linear-gradient(135deg, #0514DE 0%, #82C3EB 100%); color: white; padding: 25px; border-radius: 12px; border-bottom: 5px solid #F7BD87; margin-bottom: 20px; text-align: center; }
19
+ .card-nagari { background: #E0EDF4; border-radius: 15px; padding: 25px; border: 1.5px solid #82C3EB; box-shadow: 0 4px 12px rgba(5, 20, 222, 0.08); }
20
+ .risk-box { display: inline-block; padding: 8px 18px; border-radius: 25px; font-weight: bold; color: white; margin-top: 10px; font-size: 1.1em; }
21
+ .report-panel { background: white; border-radius: 12px; padding: 25px; border-left: 8px solid #0514DE; line-height: 1.7; font-size: 1.05em; color: #1e293b; }
 
22
  """
23
 
24
+ class ArchonFinalMaster:
25
  def __init__(self):
26
  self.load_data()
 
 
27
 
28
  def load_data(self):
29
+ # Fase 1: Foundation (Cek presensi file)
30
+ try:
31
+ self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).fillna("Umum")
32
+ self.df_cust = pd.read_csv('customers.csv')
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
+ except Exception as e:
36
+ print(f"Error Loading CSV: {e}")
37
+
38
+ def analyze_engine(self, customer_id):
39
+ # 1. Bersihkan input ID (atasi spasi/case sensitive)
40
+ cid = str(customer_id).strip().upper()
41
 
42
+ u_txn = self.df_txn[self.df_txn['customer_id'] == cid].copy()
43
+ u_bal = self.df_bal[self.df_bal['customer_id'] == cid].sort_values('month')
44
+ u_rep = self.df_rep[self.df_rep['customer_id'] == cid]
45
+ u_info_list = self.df_cust[self.df_cust['customer_id'] == cid]
46
+
47
+ if u_txn.empty or u_info_list.empty:
48
+ return None
49
+
50
+ u_info = u_info_list.iloc[0]
51
 
52
+ # --- FASE 4: RISK SCORING (ADAPTIVE LOGIC) ---
53
  income_txn = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
54
+ # Perbaikan: Gunakan income profil jika di mutasi tidak tercatat
55
+ base_income = max(income_txn, u_info['monthly_income'])
56
  expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
57
+ er = min(expense / base_income, 1.0) if base_income > 0 else 1.0 # Cap di 100% untuk kewajaran visual
58
 
59
+ # Scoring Parameters (Fase 4 - Bobot 30/20/20/20/10)
60
  er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
61
  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
62
  od_s = 1.0 if (u_bal['min_balance'] <= 0).any() else 0.0
63
  mp_s = 1.0 if (u_rep['status'] == 'late').any() else 0.0
64
 
 
65
  final_score = (0.3 * er_s) + (0.2 * bt_s) + (0.2 * od_s) + (0.2 * mp_s) + 0.1
66
  risk_lv = "HIGH" if final_score >= 0.7 else ("MEDIUM" if final_score >= 0.4 else "LOW")
67
 
68
+ return risk_lv, final_score, er, u_bal, u_txn, base_income, expense
69
 
70
+ def generate_expert_report(self, risk_lv, score, er, u_bal, expense, base_income):
71
+ # Penjelasan Adaptif (Non-Template)
72
+ msg = f"### 🛡️ Interpretasi Intelijen Archon\n"
73
+
74
+ # Penjelasan Risk Score secara Adaptif
75
  if risk_lv == "HIGH":
76
+ msg += f"⚠️ **Status Risiko Tinggi ({score:.2f})**: Berdasarkan analisis pola mutasi, Bapak/Ibu berada dalam zona risiko finansial kritis. "
77
  elif risk_lv == "MEDIUM":
78
+ msg += f"🔔 **Status Risiko Menengah ({score:.2f})**: Anda terpantau dalam pantauan dini (Early Warning). Kondisi ini memerlukan penyesuaian anggaran segera. "
79
+ else:
80
+ msg += f"✅ **Status Risiko Rendah ({score:.2f})**: Struktur keuangan Bapak/Ibu sangat sehat dan resilien terhadap fluktuasi ekonomi. "
81
+
82
+ # Analisis Rasio
83
+ msg += f"\n\n**Analisis Rasio Pengeluaran ({er:.1%}):**\n"
84
+ msg += f"Bapak/Ibu menghabiskan Rp{expense:,.0f} dari total daya serap pendapatan sebesar Rp{base_income:,.0f}. "
85
+ if er > 0.8:
86
+ msg += "Angka ini sudah melampaui batas aman perbankan (80%). Prioritaskan pemangkasan biaya gaya hidup."
87
  else:
88
+ msg += "Alokasi pengeluaran Anda masih dalam batas terkendali dan sehat."
89
+
90
+ # Analisis Saldo
91
+ latest_avg = u_bal.iloc[-1]['avg_balance'] if not u_bal.empty else 0
92
+ msg += f"\n\n**Analisis Kesehatan Saldo:**\n"
93
+ msg += f"Saldo rata-rata terakhir berada di angka Rp{latest_avg:,.0f}. "
94
+ if len(u_bal) > 1 and latest_avg < u_bal.iloc[-2]['avg_balance']:
95
+ msg += "Waspada: Terjadi tren penurunan saldo yang konsisten dalam dua periode terakhir."
96
+ else:
97
+ msg += "Stabilitas saldo terpantau positif dan memberikan cadangan kas yang cukup."
98
 
99
+ return msg
100
+
101
+ def get_generative_advice(self, risk_lv, er, cust_id, u_txn):
102
+ # FASE 5: NBO (GENERATIVE AI - BACKUP)
103
+ if not ai_client: return None
104
+ merchants = u_txn.tail(2)['raw_description'].tolist()
105
+ prompt = f"Advisor Bank Nagari: Nasabah {cust_id} risiko {risk_lv}, pengeluaran {er:.1%}. Transaksi terakhir di {merchants}. Beri 1 saran finansial hangat & solutif untuk Bapak/Ibu (Maks 3 kalimat)."
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  try:
107
+ resp = ai_client.models.generate_content(model="gemini-1.5-flash", contents=prompt)
108
  return resp.text
109
  except: return None
110
 
111
  def create_viz(self, u_bal, u_txn):
112
+ # Plot 1: Arus Kas (Nagari Light Blue Palette)
113
  u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
114
  cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
115
  fig1 = go.Figure()
116
  fig1.add_trace(go.Bar(x=cf.index, y=cf.get('credit', 0), name='Pemasukan', marker_color='#82C3EB'))
117
  fig1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran', marker_color='#0514DE'))
118
+ fig1.update_layout(title="Laporan Arus Kas", barmode='group', template='plotly_white')
119
 
120
+ # Plot 2: Saldo
121
  fig2 = go.Figure()
122
  fig2.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Saldo Rata-rata', line=dict(color='#F7BD87', width=4)))
123
+ fig2.update_layout(title="Kesehatan Pertumbuhan Saldo", template='plotly_white')
124
  return fig1, fig2
125
 
126
+ # --- UI EXECUTION ---
127
+ engine = ArchonFinalMaster()
128
 
129
  def run_archon(cust_id):
130
+ res = engine.analyze_engine(cust_id)
131
+ if not res:
132
+ return "## ❌ ID Tidak Valid atau Tidak Ditemukan", "Pastikan ID menggunakan format C0001 - C0120.", None, None
133
 
134
  risk_lv, score, er, u_bal, u_txn, inc, exp = res
135
+ report_md = engine.generate_expert_report(risk_lv, score, er, u_bal, exp, inc)
136
+ gen_advice = engine.get_generative_advice(risk_lv, er, cust_id, u_txn)
137
+ p1, p2 = engine.create_viz(u_bal, u_txn)
138
 
139
+ # Gabungkan laporan deterministik dengan Gemini
140
+ final_analysis = report_md
141
  if gen_advice:
142
+ final_analysis += f"\n\n---\n**💡 Saran Tambahan Virtual Advisor:**\n{gen_advice}"
143
+
 
 
144
  color = "#ef4444" if risk_lv == "HIGH" else ("#f59e0b" if risk_lv == "MEDIUM" else "#10b981")
145
  status_html = f"""
146
  <div class='card-nagari'>
147
+ <h2 style='color: #0514DE; margin:0;'>Hasil Analisis Archon</h2>
148
+ <div class='risk-box' style='background:{color}'>{risk_lv} RISK</div>
149
+ <p style='margin-top:15px; font-size:1.1em;'><b>Risk Score:</b> {score:.2f} / 1.00</p>
150
+ <p style='font-size:1.1em;'><b>Expense Ratio:</b> {er:.1%}</p>
151
  </div>
152
  """
153
+ return status_html, final_analysis, p1, p2
154
 
155
  with gr.Blocks(css=custom_css) as demo:
156
+ gr.HTML("<div class='nagari-header'><h1>🏦 ARCHON-AI: BANK NAGARI</h1><p>Advisor Resiliensi Finansial & Pengelolaan Risiko Cerdas</p></div>")
157
 
158
  with gr.Row():
159
  with gr.Column(scale=1):
160
+ id_in = gr.Textbox(label="Input Customer ID", placeholder="C0001")
161
+ btn = gr.Button("PROSES ANALISIS", variant="primary")
162
  out_status = gr.HTML()
163
  gr.Markdown("---")
164
+ gr.Markdown("ℹ️ **Interpretasi**: Skor risiko menggabungkan analisis saldo, pengeluaran, dan riwayat kredit nasabah secara otomatis.")
165
 
166
  with gr.Column(scale=2):
167
  with gr.Tabs():
168
+ with gr.TabItem("Arus Kas Bulanan"): plot_cf = gr.Plot()
169
+ with gr.TabItem("Tren Saldo"): plot_bal = gr.Plot()
170
+ out_report = gr.Markdown(elem_classes="report-panel")
171
 
172
+ btn.click(fn=run_archon, inputs=id_in, outputs=[out_status, out_report, plot_cf, plot_bal])
173
 
174
  demo.launch()