ZakyF commited on
Commit
74a84ea
·
1 Parent(s): 404a444

refactor: update AI client initialization, refactor risk analysis logic, enhance styling, and improve report generation.

Browse files
Files changed (1) hide show
  1. app.py +135 -101
app.py CHANGED
@@ -5,170 +5,204 @@ 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()
 
5
  import plotly.graph_objects as go
6
  from google import genai
7
 
8
+ # --- CONFIG AI STUDIO 2026 ---
9
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
10
+ # Inisialisasi client dengan penanganan error jika API Key tidak valid
11
+ try:
12
+ client_gemini = genai.Client(api_key=GOOGLE_API_KEY)
13
+ except:
14
+ client_gemini = None
15
+
16
+ # --- PALETTE & STYLING (NAGARI LIGHT BLUE) ---
17
+ # Primary Blue: #0514DE, Light Blue: #82C3EB, Pale: #E0EDF4, Gold: #F7BD87, White: #FFFFFF
18
  custom_css = """
19
  @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap');
20
  body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background-color: #FFFFFF !important; }
21
+
22
+ .nagari-header {
23
+ background: linear-gradient(135deg, #0514DE 0%, #82C3EB 100%);
24
+ color: #FFFFFF !important;
25
+ padding: 30px;
26
+ border-radius: 15px;
27
+ border-bottom: 6px solid #F7BD87;
28
+ margin-bottom: 25px;
29
+ text-align: center;
30
+ }
31
+
32
+ .report-card {
33
+ background: #FFFFFF;
34
+ border-radius: 15px;
35
+ padding: 25px;
36
+ border: 1px solid #82C3EB;
37
+ box-shadow: 0 10px 15px -3px rgba(5, 20, 222, 0.1);
38
+ line-height: 1.7;
39
+ }
40
+
41
+ .status-box {
42
+ background: #E0EDF4;
43
+ padding: 20px;
44
+ border-radius: 12px;
45
+ border-left: 8px solid #0514DE;
46
+ margin-bottom: 20px;
47
+ }
48
+
49
+ .badge {
50
+ padding: 6px 15px;
51
+ border-radius: 20px;
52
+ color: white;
53
+ font-weight: bold;
54
+ font-size: 0.9em;
55
+ }
56
  """
57
 
58
+ class ArchonFinalNagari:
59
  def __init__(self):
60
  self.load_data()
61
 
62
  def load_data(self):
63
+ # Proteksi maksimal terhadap error ID (C0014, dll)
64
+ # Menggunakan 'df.copy()' untuk menghindari setting-with-copy warning
65
+ self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).fillna("Transaksi")
66
+ self.df_cust = pd.read_csv('customers.csv').fillna(0)
67
+ self.df_bal = pd.read_csv('balances_revised.csv', parse_dates=['month']).fillna(0)
68
+ self.df_rep = pd.read_csv('repayments_revised.csv', parse_dates=['due_date']).fillna("on_time")
69
+
70
+ def analyze_logic(self, customer_id):
 
 
 
71
  cid = str(customer_id).strip().upper()
 
72
  u_txn = self.df_txn[self.df_txn['customer_id'] == cid].copy()
73
  u_bal = self.df_bal[self.df_bal['customer_id'] == cid].sort_values('month')
74
  u_rep = self.df_rep[self.df_rep['customer_id'] == cid]
75
+ u_info_df = self.df_cust[self.df_cust['customer_id'] == cid]
76
 
77
+ if u_txn.empty or u_info_df.empty: return None
 
78
 
79
+ u_info = u_info_df.iloc[0]
80
 
81
+ # --- FASE 4: RISK SCORING (DETERMINISTIC) ---
82
  income_txn = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
83
+ # Perbaikan logic ratio: Gunakan gaji bulanan jika mutasi kosong
84
+ ref_income = max(income_txn, u_info['monthly_income'])
85
  expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
86
+ er = min(expense / ref_income, 1.0) if ref_income > 0 else 1.0
87
 
 
88
  er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
89
  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
90
  od_s = 1.0 if (u_bal['min_balance'] <= 0).any() else 0.0
91
  mp_s = 1.0 if (u_rep['status'] == 'late').any() else 0.0
92
 
93
+ score = (0.3 * er_s) + (0.2 * bt_s) + (0.2 * od_s) + (0.2 * mp_s) + 0.1
94
+ risk_lv = "HIGH" if score >= 0.7 else ("MEDIUM" if score >= 0.4 else "LOW")
95
 
96
+ return risk_lv, score, er, u_bal, u_txn, expense, ref_income
97
 
98
+ def get_deterministic_report(self, risk_lv, score, er, u_bal, expense, income):
99
+ # Backup Advisor: Penjelasan Adaptif Berbasis Data (Anti-Template)
100
+ msg = f"### 📑 HASIL AUDIT ARCHON ({risk_lv})\n"
101
+ msg += f"Berdasarkan analisis bobot Fase 4 (Risk Scoring), Bapak/Ibu memiliki skor resiliensi **{score:.2f}**.\n\n"
102
+
103
+ msg += f"**Analisis Arus Kas:**\n"
104
+ msg += f"- Pengeluaran aktual bulan ini sebesar Rp{expense:,.0f} ({er:.1%} dari total daya beli).\n"
105
+ if er > 0.8: msg += "- ⚠️ Pengeluaran melampaui batas aman perbankan. Segera tinjau pos pengeluaran lifestyle.\n"
106
+ else: msg += "- ✅ Struktur pengeluaran Anda masih dalam batas terkendali.\n\n"
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  latest_avg = u_bal.iloc[-1]['avg_balance'] if not u_bal.empty else 0
109
+ msg += f"**Analisis Saldo:**\n"
110
+ msg += f"- Saldo rata-rata saat ini Rp{latest_avg:,.0f}. "
111
  if len(u_bal) > 1 and latest_avg < u_bal.iloc[-2]['avg_balance']:
112
+ msg += "Terdeteksi tren penurunan saldo dibanding periode sebelumnya."
113
  else:
114
+ msg += "Pertumbuhan saldo terpantau stabil."
115
 
116
  return msg
117
 
118
+ def get_llm_advice(self, risk_lv, er, cust_id, u_txn):
119
+ # FASE 5: NBO GENERATIVE (GEMINI)
120
+ if not client_gemini: return None
121
+ merchants = u_txn.tail(3)['raw_description'].tolist()
122
+ prompt = f"""
123
+ Identitas: Senior Banker Bank Nagari.
124
+ Analisis Nasabah {cust_id}: Risiko {risk_lv}, Pengeluaran {er:.1%}.
125
+ Transaksi Terakhir: {merchants}.
126
+ Berikan saran finansial yang sangat hangat, empatik, dan SANGAT PERSONAL dalam Bahasa Indonesia.
127
+ Hubungkan saran Anda dengan transaksi terakhir mereka di {merchants[0] if merchants else 'merchant'}.
128
+ Gunakan sapaan Bapak/Ibu. Maksimal 3 kalimat pendek.
129
+ """
130
  try:
131
+ resp = client_gemini.models.generate_content(model="gemini-1.5-flash", contents=prompt)
132
  return resp.text
133
+ except:
134
+ return None
135
 
136
  def create_viz(self, u_bal, u_txn):
137
+ # Plot 1: Cashflow (Fase 6)
138
  u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
139
  cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
140
  fig1 = go.Figure()
141
+ fig1.add_trace(go.Bar(x=cf.index, y=cf.get('credit', 0), name='Inflow', marker_color='#82C3EB'))
142
+ fig1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Outflow', marker_color='#0514DE'))
143
+ fig1.update_layout(title="Arus Kas Masuk & Keluar", barmode='group', template='plotly_white')
144
 
145
+ # Plot 2: Balance History
146
  fig2 = go.Figure()
147
+ fig2.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Rata-rata Saldo', line=dict(color='#F7BD87', width=4)))
148
+ fig2.update_layout(title="Grafik Pertumbuhan Saldo", template='plotly_white')
149
  return fig1, fig2
150
 
151
+ # --- UI LOGIC ---
152
+ engine = ArchonFinalNagari()
153
 
154
+ def execute(cust_id):
155
+ res = engine.analyze_logic(cust_id)
156
+ if not res: return "## ⚠️ Nasabah Tidak Ditemukan", "Cek kembali Customer ID Anda.", None, None
 
157
 
158
+ risk_lv, score, er, u_bal, u_txn, exp, inc = res
159
+ audit_report = engine.get_deterministic_report(risk_lv, score, er, u_bal, exp, inc)
160
+ ai_advice = engine.get_llm_advice(risk_lv, er, cust_id, u_txn)
161
  p1, p2 = engine.create_viz(u_bal, u_txn)
162
 
163
+ # GABUNGKAN DALAM SATU PANEL (Audit + LLM)
164
+ final_output = audit_report
165
+ if ai_advice:
166
+ final_output += f"\n\n---\n**💡 SARAN VIRTUAL ADVISOR (AI):**\n{ai_advice}"
167
+ else:
168
+ final_report += f"\n\n---\n*Koneksi AI Advisor sedang tidak stabil. Menggunakan laporan audit internal.*"
169
 
170
+ # Status Badge
171
  color = "#ef4444" if risk_lv == "HIGH" else ("#f59e0b" if risk_lv == "MEDIUM" else "#10b981")
172
  status_html = f"""
173
+ <div class='status-box'>
174
+ <h2 style='color: #0514DE; margin:0;'>Archon Analysis Result</h2>
175
+ <span class='badge' style='background:{color}'>{risk_lv} RISK LEVEL</span>
176
+ <p style='margin-top:15px;'><b>Risk Score:</b> {score:.2f} | <b>Expense Ratio:</b> {er:.1%}</p>
 
177
  </div>
178
  """
179
+ return status_html, final_output, p1, p2
180
 
181
  with gr.Blocks(css=custom_css) as demo:
182
+ gr.HTML("""
183
+ <div class='nagari-header'>
184
+ <h1 style='margin:0; font-weight:700;'>🛡️ **ARCHON-AI**: BANK NAGARI</h1>
185
+ <p style='margin:0; opacity:0.9;'>Sistem Intelijen Resiliensi Finansial & Manajemen Risiko</p>
186
+ </div>
187
+ """)
188
 
189
  with gr.Row():
190
  with gr.Column(scale=1):
191
+ id_in = gr.Textbox(label="Input Customer ID", placeholder="C0001 - C0120")
192
+ btn = gr.Button("ANALYZE CUSTOMER", variant="primary")
193
  out_status = gr.HTML()
194
  gr.Markdown("---")
195
+ gr.Markdown("ℹ️ **Audit Info**: Analisis dilakukan secara hibrida menggunakan data mutasi rekening dan profil pendapatan bulanan nasabah.")
196
 
197
  with gr.Column(scale=2):
198
  with gr.Tabs():
199
+ with gr.TabItem("Analysis Summary"):
200
+ out_report = gr.Markdown(elem_classes="report-card")
201
+ with gr.TabItem("Cashflow Charts"):
202
+ plot_cf = gr.Plot()
203
+ with gr.TabItem("Balance History"):
204
+ plot_bal = gr.Plot()
205
+
206
+ btn.click(fn=execute, inputs=id_in, outputs=[out_status, out_report, plot_cf, plot_bal])
207
 
208
  demo.launch()