ZakyF commited on
Commit
8e1ee0d
·
1 Parent(s): 74a84ea

feat: Enhance Gemini client initialization, refine deterministic explanation and NBO prompts, update UI styling, and improve data loading robustness

Browse files
Files changed (1) hide show
  1. app.py +86 -87
app.py CHANGED
@@ -5,53 +5,47 @@ import gradio as gr
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
 
@@ -60,12 +54,16 @@ class ArchonFinalNagari:
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()
@@ -78,9 +76,8 @@ class ArchonFinalNagari:
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
@@ -95,114 +92,116 @@ class ArchonFinalNagari:
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()
 
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
+ # Inisialisasi client dengan proteksi
11
+ client_ai = None
12
+ if GOOGLE_API_KEY:
13
+ try:
14
+ client_ai = genai.Client(api_key=GOOGLE_API_KEY)
15
+ except Exception as e:
16
+ print(f"Gemini Init Error: {e}")
17
+
18
+ # --- PALET WARNA BANK NAGARI & CSS ---
19
+ # Blue: #0514DE, Light Blue: #82C3EB, Gold: #F7BD87, White: #FFFFFF
20
  custom_css = """
21
  @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap');
22
  body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background-color: #FFFFFF !important; }
23
 
24
  .nagari-header {
25
  background: linear-gradient(135deg, #0514DE 0%, #82C3EB 100%);
26
+ padding: 35px;
 
27
  border-radius: 15px;
28
  border-bottom: 6px solid #F7BD87;
29
  margin-bottom: 25px;
30
+ text-align: center;
31
+ box-shadow: 0 10px 15px -3px rgba(5, 20, 222, 0.2);
32
  }
33
 
34
  .report-card {
35
  background: #FFFFFF;
36
  border-radius: 15px;
37
  padding: 25px;
38
+ border: 1px solid #E0EDF4;
39
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
40
+ line-height: 1.8;
41
  }
42
 
43
+ .status-card {
44
  background: #E0EDF4;
45
  padding: 20px;
46
  border-radius: 12px;
47
  border-left: 8px solid #0514DE;
48
+ margin-bottom: 20px;
 
 
 
 
 
 
 
 
49
  }
50
  """
51
 
 
54
  self.load_data()
55
 
56
  def load_data(self):
57
+ # Proteksi loading file agar tidak error di semua ID
58
+ try:
59
+ self.df_txn = pd.read_csv('transactions.csv').fillna("Umum")
60
+ self.df_txn['date'] = pd.to_datetime(self.df_txn['date'])
61
+ self.df_cust = pd.read_csv('customers.csv').fillna(0)
62
+ self.df_bal = pd.read_csv('balances_revised.csv').fillna(0)
63
+ self.df_bal['month'] = pd.to_datetime(self.df_bal['month'])
64
+ self.df_rep = pd.read_csv('repayments_revised.csv').fillna("on_time")
65
+ except Exception as e:
66
+ print(f"CRITICAL DATA ERROR: {e}")
67
 
68
  def analyze_logic(self, customer_id):
69
  cid = str(customer_id).strip().upper()
 
76
 
77
  u_info = u_info_df.iloc[0]
78
 
79
+ # --- FASE 4: DETERMINISTIC SCORE ---
80
  income_txn = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
 
81
  ref_income = max(income_txn, u_info['monthly_income'])
82
  expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
83
  er = min(expense / ref_income, 1.0) if ref_income > 0 else 1.0
 
92
 
93
  return risk_lv, score, er, u_bal, u_txn, expense, ref_income
94
 
95
+ def get_deterministic_explanation(self, risk_lv, score, er, u_bal, expense, income):
96
+ # Laporan Audit Adaptif (Fase 6) - Selalu muncul sebagai backup utama
97
+ msg = f"### 📑 LAPORAN AUDIT RESILIENSI\n"
98
+ msg += f"Berdasarkan parameter perbankan, tingkat resiliensi Anda berada pada kategori **{risk_lv}** (Skor: {score:.2f}).\n\n"
99
 
100
+ msg += f"**Analisis Rasio Pengeluaran:**\n"
101
+ msg += f"- Total belanja Anda tercatat Rp{expense:,.0f} dari basis pendapatan Rp{income:,.0f}.\n"
102
+ msg += f"- Rasio pengeluaran Anda adalah **{er:.1%}**. "
103
+ if er > 0.8: msg += "Ini merupakan sinyal merah (kritis), di mana pengeluaran hampir menghabiskan seluruh pendapatan harian."
104
+ else: msg += "Angka ini menunjukkan manajemen pengeluaran yang cukup terkendali."
105
 
106
+ latest_bal = u_bal.iloc[-1]['avg_balance'] if not u_bal.empty else 0
107
+ msg += f"\n\n**Analisis Kesehatan Saldo:**\n"
108
+ msg += f"- Saldo rata-rata terakhir Anda: Rp{latest_avg:,.0f}.\n"
109
  if len(u_bal) > 1 and latest_avg < u_bal.iloc[-2]['avg_balance']:
110
+ msg += "- Terdeteksi tren penurunan saldo bulanan. Disarankan untuk menjaga saldo minimum tetap di atas batas kritis."
111
+ else: msg += "- Pertumbuhan saldo Anda terpantau stabil dan positif."
 
112
 
113
  return msg
114
 
115
+ def get_gemini_nbo(self, risk_lv, er, cust_id, u_txn):
116
  # FASE 5: NBO GENERATIVE (GEMINI)
117
+ if not client_ai: return None
118
  merchants = u_txn.tail(3)['raw_description'].tolist()
119
+
120
  prompt = f"""
121
+ Identitas: Penasihat Keuangan Virtual Bank Nagari.
122
+ Nasabah {cust_id}: Risiko {risk_lv}, Pengeluaran {er:.1%}.
123
+ Transaksi terakhir di: {merchants}.
124
+ Tugas: Beri saran finansial yang personal, hangat, dan solutif untuk Bapak/Ibu.
125
+ Jangan gunakan kata 'Nasabah'. Hubungkan saran Anda dengan transaksi terakhir mereka.
126
+ Gunakan gaya bahasa profesional namun empatik. Maksimal 3 kalimat.
127
  """
128
  try:
129
+ response = client_ai.models.generate_content(model="gemini-1.5-flash", contents=prompt)
130
+ return response.text
131
+ except Exception as e:
132
+ print(f"Gemini Call Error: {e}")
133
  return None
134
 
135
  def create_viz(self, u_bal, u_txn):
136
+ # Arus Kas
137
  u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
138
  cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
139
  fig1 = go.Figure()
140
+ fig1.add_trace(go.Bar(x=cf.index, y=cf.get('credit', 0), name='Pemasukan', marker_color='#82C3EB'))
141
+ fig1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran', marker_color='#0514DE'))
142
+ fig1.update_layout(title="Analisis Arus Kas", barmode='group', template='plotly_white')
143
 
144
+ # Saldo
145
  fig2 = go.Figure()
146
  fig2.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Rata-rata Saldo', line=dict(color='#F7BD87', width=4)))
147
+ fig2.update_layout(title="Tren Pertumbuhan Tabungan", template='plotly_white')
148
  return fig1, fig2
149
 
150
+ # --- UI EXECUTION ---
151
  engine = ArchonFinalNagari()
152
 
153
+ def run_archon(cust_id):
154
  res = engine.analyze_logic(cust_id)
155
+ if not res: return "## Data ID Tidak Ditemukan", "Mohon gunakan ID yang valid (C0001 - C0120).", None, None
156
 
157
  risk_lv, score, er, u_bal, u_txn, exp, inc = res
158
+ audit_report = engine.get_deterministic_explanation(risk_lv, score, er, u_bal, exp, inc)
159
+ ai_advice = engine.get_gemini_nbo(risk_lv, er, cust_id, u_txn)
160
  p1, p2 = engine.create_viz(u_bal, u_txn)
161
 
162
+ # INTEGRASI LAPORAN (Audit + Gemini)
163
+ final_report = audit_report
164
  if ai_advice:
165
+ final_report += f"\n\n---\n**💡 SARAN VIRTUAL ADVISOR (AI):**\n{ai_advice}"
166
  else:
167
+ final_report += f"\n\n---\n*Saran Virtual Advisor saat ini sedang dalam sinkronisasi sistem perbankan.*"
168
 
 
169
  color = "#ef4444" if risk_lv == "HIGH" else ("#f59e0b" if risk_lv == "MEDIUM" else "#10b981")
170
  status_html = f"""
171
+ <div class='status-card'>
172
+ <h2 style='color: #0514DE; margin:0;'>Hasil Analisis Perilaku</h2>
173
+ <span style='background:{color}; padding:5px 15px; border-radius:20px; color:white; font-weight:bold; font-size:0.9em;'>{risk_lv} RISK</span>
174
  <p style='margin-top:15px;'><b>Risk Score:</b> {score:.2f} | <b>Expense Ratio:</b> {er:.1%}</p>
175
  </div>
176
  """
177
+ return status_html, final_report, p1, p2
178
 
179
  with gr.Blocks(css=custom_css) as demo:
180
+ # Header dengan teks putih dan tebal
181
+ gr.HTML(f"""
182
  <div class='nagari-header'>
183
+ <h1 style='color: white !important; font-weight: 800; margin: 0;'>🛡️ **ARCHON-AI**: BANK NAGARI</h1>
184
+ <p style='color: white !important; margin: 5px 0 0 0; opacity: 0.9;'>Platform Intelijen Risiko & Resiliensi Finansial</p>
185
  </div>
186
  """)
187
 
188
  with gr.Row():
189
  with gr.Column(scale=1):
190
+ id_in = gr.Textbox(label="Customer ID", placeholder="C0001")
191
+ btn = gr.Button("MULAI ANALISIS", variant="primary")
192
  out_status = gr.HTML()
193
  gr.Markdown("---")
194
+ gr.Markdown("ℹ️ **Info Auditor**: Laporan ini menggabungkan data mutasi real-time dengan profil historis nasabah.")
195
 
196
  with gr.Column(scale=2):
197
  with gr.Tabs():
198
+ with gr.TabItem("Audit Report"):
199
  out_report = gr.Markdown(elem_classes="report-card")
200
  with gr.TabItem("Cashflow Charts"):
201
  plot_cf = gr.Plot()
202
+ with gr.TabItem("Saving History"):
203
  plot_bal = gr.Plot()
204
 
205
+ btn.click(fn=run_archon, inputs=id_in, outputs=[out_status, out_report, plot_cf, plot_bal])
206
 
207
  demo.launch()