Update app.py
Browse files
app.py
CHANGED
|
@@ -10,6 +10,7 @@ import numpy as np
|
|
| 10 |
import matplotlib
|
| 11 |
matplotlib.use('Agg')
|
| 12 |
import matplotlib.pyplot as plt
|
|
|
|
| 13 |
import shap, joblib, os, json, warnings, re, requests, io, hashlib
|
| 14 |
warnings.filterwarnings('ignore')
|
| 15 |
|
|
@@ -401,7 +402,8 @@ def call_groq_tools(messages, api_key, tools=None):
|
|
| 401 |
def _clean_response(text: str) -> tuple[str, dict]:
|
| 402 |
"""
|
| 403 |
Strip all LLM artifacts from response text and extract any embedded adjustments.
|
| 404 |
-
Handles: <think>β¦</think>
|
|
|
|
| 405 |
Returns (clean_text, adjustments_dict).
|
| 406 |
"""
|
| 407 |
if not text:
|
|
@@ -412,17 +414,16 @@ def _clean_response(text: str) -> tuple[str, dict]:
|
|
| 412 |
text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()
|
| 413 |
|
| 414 |
# ββ Parse & strip inline JSON objects (tool-call leak) ββββββββ
|
| 415 |
-
# Matches {"field": "...", "value": ..., "reason": "..."} anywhere in text
|
| 416 |
json_pattern = re.compile(
|
| 417 |
r'\{[^{}]*"field"\s*:\s*"([^"]+)"[^{}]*"value"\s*:\s*([\d.]+)[^{}]*\}',
|
| 418 |
re.DOTALL
|
| 419 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
for m in json_pattern.finditer(text):
|
| 421 |
field, val = m.group(1), float(m.group(2))
|
| 422 |
-
valid_fields = {
|
| 423 |
-
'digital_presence_score', 'business_age_years', 'num_employees',
|
| 424 |
-
'monthly_cash_flow', 'duration', 'loan_rp'
|
| 425 |
-
}
|
| 426 |
if field in valid_fields:
|
| 427 |
adjustments[field] = val
|
| 428 |
text = json_pattern.sub('', text).strip()
|
|
@@ -434,18 +435,37 @@ def _clean_response(text: str) -> tuple[str, dict]:
|
|
| 434 |
adjustments[field] = val
|
| 435 |
text = re.sub(r'\[ADJUST:[^\]]+\]', '', text).strip()
|
| 436 |
|
| 437 |
-
# ββ Strip trailing standalone
|
| 438 |
-
#
|
| 439 |
-
#
|
| 440 |
-
#
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
r'
|
| 445 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
)
|
| 447 |
-
|
| 448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
text = re.sub(r'\n{3,}', '\n\n', text).strip()
|
| 450 |
|
| 451 |
return text, adjustments
|
|
@@ -706,7 +726,6 @@ _defaults = dict(
|
|
| 706 |
chat_history=[], shap_png=None,
|
| 707 |
wi_dig=None, wi_biz=None, wi_emp=None,
|
| 708 |
wi_cash=None, wi_dur=None, wi_loan=None,
|
| 709 |
-
# β‘ Memory persistence
|
| 710 |
chat_summary='',
|
| 711 |
memory_loaded=False,
|
| 712 |
)
|
|
@@ -982,7 +1001,6 @@ def _get_top_issue(raw_input, pd_pct, lang):
|
|
| 982 |
|
| 983 |
# ============================================================
|
| 984 |
# AI CHAT β Maya persona
|
| 985 |
-
# β’ Few-shot + β£ Chain-of-Thought + β Tool Calling + β’ Memory
|
| 986 |
# ============================================================
|
| 987 |
def get_chat_response(user_msg, history, result, raw_input, shap_vals, feature_names, lang, rag_index=None):
|
| 988 |
pd_pct = result['pd'] * 100
|
|
@@ -1003,18 +1021,18 @@ def get_chat_response(user_msg, history, result, raw_input, shap_vals, feature_n
|
|
| 1003 |
except Exception:
|
| 1004 |
context = ""
|
| 1005 |
|
| 1006 |
-
# ββ
|
| 1007 |
fewshot = """
|
| 1008 |
CONTOH DIALOG MAYA (few-shot β ikuti gaya & kualitas ini):
|
| 1009 |
---
|
| 1010 |
User: "Gimana cara turunin skor PD saya?"
|
| 1011 |
-
Maya: "Berdasarkan data kamu, 3 prioritas utama: 1) **Naikkan digital score
|
| 1012 |
---
|
| 1013 |
User: "Kenapa skor saya tinggi banget?"
|
| 1014 |
Maya: "Dua driver terbesar menurut SHAP: **business_age_years** (SHAP=+0.234) β bisnis muda = risiko lebih tinggi di mata model, dan **digital_presence_score** (SHAP=+0.189) β bank pakai ini sebagai proxy kredibilitas online. Fokus perbaiki keduanya dulu, dampaknya paling signifikan!"
|
| 1015 |
---
|
| 1016 |
User: "Pinjaman berapa yang paling aman buat saya?"
|
| 1017 |
-
Maya: "Dengan cash flow Rp 15jt/bln dan tenor 24 bln, batas aman kamu adalah **Rp 126jt** (rasio cicilan 35%). Pinjaman kamu sekarang Rp 50jt β β
masih aman! Kalau mau tambah, pastikan cash flow naik dulu
|
| 1018 |
---
|
| 1019 |
"""
|
| 1020 |
|
|
@@ -1024,13 +1042,13 @@ Maya: "Dengan cash flow Rp 15jt/bln dan tenor 24 bln, batas aman kamu adalah **R
|
|
| 1024 |
lbl = {'id': 'π Ringkasan percakapan sebelumnya', 'en': 'π Earlier conversation summary', 'hi': 'π ΰ€ͺΰ₯ΰ€°ΰ₯ΰ€΅ ΰ€΅ΰ€Ύΰ€°ΰ₯ΰ€€ΰ€Ύΰ€²ΰ€Ύΰ€ͺ ΰ€Έΰ€Ύΰ€°ΰ€Ύΰ€ΰ€Ά'}
|
| 1025 |
summary_block = f"\n{lbl.get(lang, lbl['en'])}:\n{chat_summary}\n"
|
| 1026 |
|
| 1027 |
-
# ββ
|
| 1028 |
cot_block = """
|
| 1029 |
CARA BERPIKIR β CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
|
| 1030 |
β Apa yang sebenarnya ditanyakan user? (bukan hanya kata-katanya β cari intent sebenarnya)
|
| 1031 |
β‘ Data pemohon mana yang paling relevan? (PD score, cash flow, digital score, NPWP, dll)
|
| 1032 |
β’ Apa saran paling KONKRET & berdampak tinggi untuk UMKM spesifik ini?
|
| 1033 |
-
β£ Apakah perlu adjust slider What-If? Jika ya,
|
| 1034 |
β Tulis HANYA jawaban akhir β jangan tampilkan proses berpikir di output.
|
| 1035 |
"""
|
| 1036 |
|
|
@@ -1058,21 +1076,27 @@ CARA BERPIKIR β CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
|
|
| 1058 |
f"{summary_block}"
|
| 1059 |
+ cot_block
|
| 1060 |
+ fewshot +
|
| 1061 |
-
f"ATURAN MENJAWAB:\n"
|
| 1062 |
f"1. Jawab BEBAS β tidak harus soal kredit. Kalau ditanya soal diri sendiri, perkenalkan sebagai Maya.\n"
|
| 1063 |
f"2. Kalau relevan, selalu hubungkan ke data pemohon dengan menyebut angka aktualnya.\n"
|
| 1064 |
f"3. Berikan saran KONKRET dan SPESIFIK, bukan generik.\n"
|
| 1065 |
f"4. Maksimal 150 kata kecuali diminta lebih panjang.\n"
|
| 1066 |
-
f"5.
|
| 1067 |
-
f" BENAR:
|
| 1068 |
-
f"
|
| 1069 |
-
f" SALAH:
|
| 1070 |
-
f"
|
| 1071 |
-
f"
|
| 1072 |
-
f"
|
| 1073 |
-
f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1074 |
f"6. JANGAN pernah balik ke template. Jawab seperti manusia cerdas.\n"
|
| 1075 |
f"7. Baca context percakapan sebelumnya sebelum menjawab β jaga konsistensi topik.\n"
|
|
|
|
| 1076 |
)
|
| 1077 |
|
| 1078 |
messages = [{"role": "system", "content": system}]
|
|
@@ -1080,7 +1104,7 @@ CARA BERPIKIR β CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
|
|
| 1080 |
messages.append({"role": m['role'], "content": m['content']})
|
| 1081 |
messages.append({"role": "user", "content": user_msg})
|
| 1082 |
|
| 1083 |
-
# ββ
|
| 1084 |
response, tool_calls, _llm_src, _last_error = _call_chat_llm(messages)
|
| 1085 |
|
| 1086 |
# Parse tool_calls for adjustments (formal schema)
|
|
@@ -1120,14 +1144,14 @@ CARA BERPIKIR β CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
|
|
| 1120 |
adjustments['has_npwp'] = 1
|
| 1121 |
if dig < 75:
|
| 1122 |
n = '2' if not npwp else '1'
|
| 1123 |
-
tips.append(_t(f"{n}. **Naikkan Digital Score
|
| 1124 |
-
f"{n}. **Raise Digital Score
|
| 1125 |
f"{n}. **ΰ€‘ΰ€Ώΰ€ΰ€Ώΰ€ΰ€² ΰ€Έΰ₯ΰ€ΰ₯ΰ€°** {dig} ΰ€Έΰ₯ 75+ ΰ€ΰ€°ΰ₯ΰ€"))
|
| 1126 |
adjustments['digital_presence_score'] = 75
|
| 1127 |
if cf < 25e6:
|
| 1128 |
n = str(len(tips)+1)
|
| 1129 |
-
tips.append(_t(f"{n}. **Optimalkan cash flow
|
| 1130 |
-
f"{n}. **Grow cash flow
|
| 1131 |
f"{n}. **ΰ€¨ΰ€ΰ€¦ ΰ€ͺΰ₯ΰ€°ΰ€΅ΰ€Ύΰ€Ή** Rp {cf/1e6:.0f}M ΰ€Έΰ₯ 25M+ ΰ€ΰ€°ΰ₯ΰ€"))
|
| 1132 |
adjustments['monthly_cash_flow'] = 25000000
|
| 1133 |
if not tips:
|
|
@@ -1281,7 +1305,7 @@ CARA BERPIKIR β CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
|
|
| 1281 |
f"ΰ€ΰ€ͺΰ€ΰ€Ύ PD **{pd_pct:.1f}%** ΰ€Ήΰ₯ ΰ€ΰ€° {top_issue}"
|
| 1282 |
)
|
| 1283 |
|
| 1284 |
-
# Final cleanup: strip think-tags, leaked JSON, ADJUST tags
|
| 1285 |
if response:
|
| 1286 |
response, extra_adj = _clean_response(response)
|
| 1287 |
for field, val in extra_adj.items():
|
|
@@ -1291,6 +1315,155 @@ CARA BERPIKIR β CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
|
|
| 1291 |
return response or "...", adjustments, _last_error
|
| 1292 |
|
| 1293 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1294 |
# ============================================================
|
| 1295 |
# HEADER
|
| 1296 |
# ============================================================
|
|
@@ -1406,7 +1579,6 @@ if submitted:
|
|
| 1406 |
st.session_state.chat_history=[]
|
| 1407 |
st.session_state.chat_summary=''
|
| 1408 |
for k in ['wi_dig','wi_biz','wi_emp','wi_cash','wi_dur','wi_loan']: st.session_state[k]=None
|
| 1409 |
-
# Clear persisted memory on new submission
|
| 1410 |
_save_chat_memory([], '')
|
| 1411 |
except Exception as e:
|
| 1412 |
st.error(f"Error: {e}"); st.stop()
|
|
@@ -1451,6 +1623,11 @@ for col,(lbl_k,val,sub_k,color) in zip(st.columns(4),[
|
|
| 1451 |
f'<div class="kpi-sub">{T(sub_k,lang)}</div></div>', unsafe_allow_html=True)
|
| 1452 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 1453 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1454 |
# ============================================================
|
| 1455 |
# TABS
|
| 1456 |
# ============================================================
|
|
@@ -1482,7 +1659,6 @@ with t2:
|
|
| 1482 |
with t3:
|
| 1483 |
st.markdown(f'<div class="sec-title">{T("chat_title",lang)}</div>', unsafe_allow_html=True)
|
| 1484 |
|
| 1485 |
-
# Show memory summary badge if exists
|
| 1486 |
if st.session_state.get('chat_summary'):
|
| 1487 |
n_turns = len(st.session_state.chat_history)
|
| 1488 |
lbl = {'id': f'π§ Memory aktif Β· {n_turns} pesan tersimpan',
|
|
@@ -1520,7 +1696,6 @@ with t3:
|
|
| 1520 |
st.warning(f"β οΈ Semua LLM gagal, menggunakan Smart Fallback. Error: `{_debug_err[:100]}`")
|
| 1521 |
st.session_state.chat_history.append({'role':'assistant','content':ai_resp})
|
| 1522 |
|
| 1523 |
-
# β‘ Persist memory after each turn
|
| 1524 |
_save_chat_memory(
|
| 1525 |
st.session_state.chat_history,
|
| 1526 |
st.session_state.get('chat_summary', '')
|
|
|
|
| 10 |
import matplotlib
|
| 11 |
matplotlib.use('Agg')
|
| 12 |
import matplotlib.pyplot as plt
|
| 13 |
+
import streamlit.components.v1 as components
|
| 14 |
import shap, joblib, os, json, warnings, re, requests, io, hashlib
|
| 15 |
warnings.filterwarnings('ignore')
|
| 16 |
|
|
|
|
| 402 |
def _clean_response(text: str) -> tuple[str, dict]:
|
| 403 |
"""
|
| 404 |
Strip all LLM artifacts from response text and extract any embedded adjustments.
|
| 405 |
+
Handles: <think>β¦</think>, raw JSON objects, [ADJUST:β¦] tags, and trailing
|
| 406 |
+
standalone recommendation lines that duplicate inline suggestions.
|
| 407 |
Returns (clean_text, adjustments_dict).
|
| 408 |
"""
|
| 409 |
if not text:
|
|
|
|
| 414 |
text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()
|
| 415 |
|
| 416 |
# ββ Parse & strip inline JSON objects (tool-call leak) ββββββββ
|
|
|
|
| 417 |
json_pattern = re.compile(
|
| 418 |
r'\{[^{}]*"field"\s*:\s*"([^"]+)"[^{}]*"value"\s*:\s*([\d.]+)[^{}]*\}',
|
| 419 |
re.DOTALL
|
| 420 |
)
|
| 421 |
+
valid_fields = {
|
| 422 |
+
'digital_presence_score', 'business_age_years', 'num_employees',
|
| 423 |
+
'monthly_cash_flow', 'duration', 'loan_rp'
|
| 424 |
+
}
|
| 425 |
for m in json_pattern.finditer(text):
|
| 426 |
field, val = m.group(1), float(m.group(2))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
if field in valid_fields:
|
| 428 |
adjustments[field] = val
|
| 429 |
text = json_pattern.sub('', text).strip()
|
|
|
|
| 435 |
adjustments[field] = val
|
| 436 |
text = re.sub(r'\[ADJUST:[^\]]+\]', '', text).strip()
|
| 437 |
|
| 438 |
+
# ββ FIX: Strip ALL trailing standalone action-lines ββββββββββ
|
| 439 |
+
# Only strip when there's meaningful content before the trailing line,
|
| 440 |
+
# i.e. the response has more than 1 non-empty line. This prevents
|
| 441 |
+
# incorrectly stripping single-line responses that start with action words.
|
| 442 |
+
_action_line = re.compile(
|
| 443 |
+
r'\n[ \t]*(?:'
|
| 444 |
+
# Indonesian
|
| 445 |
+
r'(?:Naikkan|Tingkatkan|Pertahankan|Kurangi|Optimalkan|Perbaiki|Tambahkan|Pastikan|Coba|Daftarkan)'
|
| 446 |
+
r'|'
|
| 447 |
+
# English
|
| 448 |
+
r'(?:Raise|Increase|Maintain|Reduce|Lower|Improve|Try|Optimize|Register|Ensure)'
|
| 449 |
+
r'|'
|
| 450 |
+
# Hindi
|
| 451 |
+
r'(?:ΰ€¬ΰ€’ΰ€Όΰ€Ύΰ€ΰ€|ΰ€ΰ€ΰ€Ύΰ€ΰ€|ΰ€Έΰ₯ΰ€§ΰ€Ύΰ€°ΰ₯ΰ€|ΰ€ͺΰ€ΰ€ΰ₯ΰ€ΰ₯ΰ€€|ΰ€Έΰ₯ΰ€¨ΰ€Ώΰ€Άΰ₯ΰ€ΰ€Ώΰ€€)'
|
| 452 |
+
r')[^\n]{3,90}$',
|
| 453 |
+
re.IGNORECASE | re.MULTILINE
|
| 454 |
)
|
| 455 |
+
# Run up to 5 passes; guard: only strip if text has >1 non-empty line
|
| 456 |
+
for _ in range(5):
|
| 457 |
+
non_empty_lines = [l for l in text.split('\n') if l.strip()]
|
| 458 |
+
if len(non_empty_lines) <= 1:
|
| 459 |
+
break # Don't strip the only content line
|
| 460 |
+
new_text = _action_line.sub('', text).rstrip()
|
| 461 |
+
if new_text == text:
|
| 462 |
+
break
|
| 463 |
+
text = new_text
|
| 464 |
+
|
| 465 |
+
# ββ Strip markdown bold artifacts left on their own line βββββ
|
| 466 |
+
text = re.sub(r'\n\*\*\s*\*\*\s*$', '', text, flags=re.MULTILINE).strip()
|
| 467 |
+
|
| 468 |
+
# ββ Collapse excessive blank lines ββββββββββββββββββββββββββββ
|
| 469 |
text = re.sub(r'\n{3,}', '\n\n', text).strip()
|
| 470 |
|
| 471 |
return text, adjustments
|
|
|
|
| 726 |
chat_history=[], shap_png=None,
|
| 727 |
wi_dig=None, wi_biz=None, wi_emp=None,
|
| 728 |
wi_cash=None, wi_dur=None, wi_loan=None,
|
|
|
|
| 729 |
chat_summary='',
|
| 730 |
memory_loaded=False,
|
| 731 |
)
|
|
|
|
| 1001 |
|
| 1002 |
# ============================================================
|
| 1003 |
# AI CHAT β Maya persona
|
|
|
|
| 1004 |
# ============================================================
|
| 1005 |
def get_chat_response(user_msg, history, result, raw_input, shap_vals, feature_names, lang, rag_index=None):
|
| 1006 |
pd_pct = result['pd'] * 100
|
|
|
|
| 1021 |
except Exception:
|
| 1022 |
context = ""
|
| 1023 |
|
| 1024 |
+
# ββ Few-shot examples βββββββββββββββββββββββββββββββββββββ
|
| 1025 |
fewshot = """
|
| 1026 |
CONTOH DIALOG MAYA (few-shot β ikuti gaya & kualitas ini):
|
| 1027 |
---
|
| 1028 |
User: "Gimana cara turunin skor PD saya?"
|
| 1029 |
+
Maya: "Berdasarkan data kamu, 3 prioritas utama: 1) **Naikkan digital score ke 75+** via Google Business & marketplace aktif [ADJUST: digital_presence_score=75] β ini faktor SHAP terbesar. 2) **Optimalkan cash flow ke Rp 25jt/bln** [ADJUST: monthly_cash_flow=25000000] β dokumentasikan semua pemasukan, pisah rekening. 3) **Urus NPWP** kalau belum ada. Mau lihat simulasinya di tab What-If?"
|
| 1030 |
---
|
| 1031 |
User: "Kenapa skor saya tinggi banget?"
|
| 1032 |
Maya: "Dua driver terbesar menurut SHAP: **business_age_years** (SHAP=+0.234) β bisnis muda = risiko lebih tinggi di mata model, dan **digital_presence_score** (SHAP=+0.189) β bank pakai ini sebagai proxy kredibilitas online. Fokus perbaiki keduanya dulu, dampaknya paling signifikan!"
|
| 1033 |
---
|
| 1034 |
User: "Pinjaman berapa yang paling aman buat saya?"
|
| 1035 |
+
Maya: "Dengan cash flow Rp 15jt/bln dan tenor 24 bln, batas aman kamu adalah **Rp 126jt** (rasio cicilan 35%). Pinjaman kamu sekarang Rp 50jt β β
masih aman! Kalau mau tambah, pastikan cash flow naik dulu."
|
| 1036 |
---
|
| 1037 |
"""
|
| 1038 |
|
|
|
|
| 1042 |
lbl = {'id': 'π Ringkasan percakapan sebelumnya', 'en': 'π Earlier conversation summary', 'hi': 'π ΰ€ͺΰ₯ΰ€°ΰ₯ΰ€΅ ΰ€΅ΰ€Ύΰ€°ΰ₯ΰ€€ΰ€Ύΰ€²ΰ€Ύΰ€ͺ ΰ€Έΰ€Ύΰ€°ΰ€Ύΰ€ΰ€Ά'}
|
| 1043 |
summary_block = f"\n{lbl.get(lang, lbl['en'])}:\n{chat_summary}\n"
|
| 1044 |
|
| 1045 |
+
# ββ Chain-of-Thought instructions βββββββββββββββββββββββββ
|
| 1046 |
cot_block = """
|
| 1047 |
CARA BERPIKIR β CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
|
| 1048 |
β Apa yang sebenarnya ditanyakan user? (bukan hanya kata-katanya β cari intent sebenarnya)
|
| 1049 |
β‘ Data pemohon mana yang paling relevan? (PD score, cash flow, digital score, NPWP, dll)
|
| 1050 |
β’ Apa saran paling KONKRET & berdampak tinggi untuk UMKM spesifik ini?
|
| 1051 |
+
β£ Apakah perlu adjust slider What-If? Jika ya, embed tag [ADJUST: field=value] LANGSUNG di dalam kalimat rekomendasi.
|
| 1052 |
β Tulis HANYA jawaban akhir β jangan tampilkan proses berpikir di output.
|
| 1053 |
"""
|
| 1054 |
|
|
|
|
| 1076 |
f"{summary_block}"
|
| 1077 |
+ cot_block
|
| 1078 |
+ fewshot +
|
| 1079 |
+
f"ATURAN MENJAWAB β BACA SEMUA DENGAN SEKSAMA:\n"
|
| 1080 |
f"1. Jawab BEBAS β tidak harus soal kredit. Kalau ditanya soal diri sendiri, perkenalkan sebagai Maya.\n"
|
| 1081 |
f"2. Kalau relevan, selalu hubungkan ke data pemohon dengan menyebut angka aktualnya.\n"
|
| 1082 |
f"3. Berikan saran KONKRET dan SPESIFIK, bukan generik.\n"
|
| 1083 |
f"4. Maksimal 150 kata kecuali diminta lebih panjang.\n"
|
| 1084 |
+
f"5. FORMAT TAG [ADJUST:] β WAJIB IKUTI ATURAN INI:\n"
|
| 1085 |
+
f" β
BENAR: Embed tag langsung di akhir kalimat saran:\n"
|
| 1086 |
+
f" 'Coba naikkan digital score ke 75 [ADJUST: digital_presence_score=75] β dampaknya paling besar.'\n"
|
| 1087 |
+
f" β SALAH: Menulis kalimat saran TANPA tag, lalu menambah baris terpisah:\n"
|
| 1088 |
+
f" 'Coba naikkan digital score ke 75.\\nNaikkan digital score ke 75 [ADJUST: ...]'\n"
|
| 1089 |
+
f" β SALAH: Menulis tag di baris sendiri di akhir response:\n"
|
| 1090 |
+
f" '...skor kamu akan membaik.\\n[ADJUST: digital_presence_score=75]'\n"
|
| 1091 |
+
f" β SALAH BESAR: Menulis ulang rekomendasi di baris terpisah setelah selesai:\n"
|
| 1092 |
+
f" '...semua sudah dijelaskan di atas.\\nNaikkan digital score ke 75'\n"
|
| 1093 |
+
f" RULE: Setiap rekomendasi bernilai angka = SATU kalimat + SATU tag [ADJUST:] inline. TIDAK LEBIH.\n"
|
| 1094 |
+
f" RULE: JANGAN pernah tulis JSON object {{...}} di response.\n"
|
| 1095 |
+
f" RULE: Field valid: digital_presence_score(1-100), business_age_years(1-20),\n"
|
| 1096 |
+
f" num_employees(1-50), monthly_cash_flow(angka Rp), duration(4-72), loan_rp(angka Rp)\n"
|
| 1097 |
f"6. JANGAN pernah balik ke template. Jawab seperti manusia cerdas.\n"
|
| 1098 |
f"7. Baca context percakapan sebelumnya sebelum menjawab β jaga konsistensi topik.\n"
|
| 1099 |
+
f"8. AKHIRI response dengan kalimat lengkap. Jangan tambahkan apapun setelah kalimat terakhir.\n"
|
| 1100 |
)
|
| 1101 |
|
| 1102 |
messages = [{"role": "system", "content": system}]
|
|
|
|
| 1104 |
messages.append({"role": m['role'], "content": m['content']})
|
| 1105 |
messages.append({"role": "user", "content": user_msg})
|
| 1106 |
|
| 1107 |
+
# ββ Formal Tool Calling via _call_chat_llm ββββββββββββββββ
|
| 1108 |
response, tool_calls, _llm_src, _last_error = _call_chat_llm(messages)
|
| 1109 |
|
| 1110 |
# Parse tool_calls for adjustments (formal schema)
|
|
|
|
| 1144 |
adjustments['has_npwp'] = 1
|
| 1145 |
if dig < 75:
|
| 1146 |
n = '2' if not npwp else '1'
|
| 1147 |
+
tips.append(_t(f"{n}. **Naikkan Digital Score dari {dig} ke 75+** β aktif di Google Business, Tokopedia/Shopee",
|
| 1148 |
+
f"{n}. **Raise Digital Score from {dig} to 75+** β Google Business, marketplace",
|
| 1149 |
f"{n}. **ΰ€‘ΰ€Ώΰ€ΰ€Ώΰ€ΰ€² ΰ€Έΰ₯ΰ€ΰ₯ΰ€°** {dig} ΰ€Έΰ₯ 75+ ΰ€ΰ€°ΰ₯ΰ€"))
|
| 1150 |
adjustments['digital_presence_score'] = 75
|
| 1151 |
if cf < 25e6:
|
| 1152 |
n = str(len(tips)+1)
|
| 1153 |
+
tips.append(_t(f"{n}. **Optimalkan cash flow dari Rp {cf/1e6:.0f}jt ke 25jt+/bln** β diversifikasi produk",
|
| 1154 |
+
f"{n}. **Grow cash flow from Rp {cf/1e6:.0f}M to 25M+/month**",
|
| 1155 |
f"{n}. **ΰ€¨ΰ€ΰ€¦ ΰ€ͺΰ₯ΰ€°ΰ€΅ΰ€Ύΰ€Ή** Rp {cf/1e6:.0f}M ΰ€Έΰ₯ 25M+ ΰ€ΰ€°ΰ₯ΰ€"))
|
| 1156 |
adjustments['monthly_cash_flow'] = 25000000
|
| 1157 |
if not tips:
|
|
|
|
| 1305 |
f"ΰ€ΰ€ͺΰ€ΰ€Ύ PD **{pd_pct:.1f}%** ΰ€Ήΰ₯ ΰ€ΰ€° {top_issue}"
|
| 1306 |
)
|
| 1307 |
|
| 1308 |
+
# Final cleanup: strip think-tags, leaked JSON, ADJUST tags, trailing lines
|
| 1309 |
if response:
|
| 1310 |
response, extra_adj = _clean_response(response)
|
| 1311 |
for field, val in extra_adj.items():
|
|
|
|
| 1315 |
return response or "...", adjustments, _last_error
|
| 1316 |
|
| 1317 |
|
| 1318 |
+
# ============================================================
|
| 1319 |
+
# FLOATING DRAGGABLE GIF + SOUND PER TAB
|
| 1320 |
+
# ============================================================
|
| 1321 |
+
def render_media_manager():
|
| 1322 |
+
"""
|
| 1323 |
+
Inject floating draggable GIFs + audio per tab.
|
| 1324 |
+
Tab index mapping:
|
| 1325 |
+
0 = SHAP β 1 GIF (crying cat) + idk.mp3
|
| 1326 |
+
3 = What-If β 3 GIFs (dancing chibi) + shake.mp3
|
| 1327 |
+
others β semua tersembunyi, audio pause
|
| 1328 |
+
"""
|
| 1329 |
+
html = """
|
| 1330 |
+
<div id="media-container">
|
| 1331 |
+
<!-- ββ AUDIO βββββββββββββββββββββββββββββββββββββββ -->
|
| 1332 |
+
<audio id="audio-shap" loop preload="auto" src="/static/idk.mp3"></audio>
|
| 1333 |
+
<audio id="audio-whatif" loop preload="auto" src="/static/shake.mp3"></audio>
|
| 1334 |
+
|
| 1335 |
+
<!-- ββ GIFs: SHAP tab ββββββββββββββββββββββββββββββ -->
|
| 1336 |
+
<div id="gif-shap" class="drag-gif" style="bottom:20px;left:20px;display:none;">
|
| 1337 |
+
<img src="https://media1.tenor.com/m/8606581414273585609/crying-cat-no-msgs-kitty-meme-screaming.gif" width="140" draggable="false">
|
| 1338 |
+
<div class="drag-close" onclick="document.getElementById('gif-shap').style.display='none'">β</div>
|
| 1339 |
+
</div>
|
| 1340 |
+
|
| 1341 |
+
<!-- ββ GIFs: What-If tab βββββββββββββββββββββββββββ -->
|
| 1342 |
+
<div id="gif-wi-1" class="drag-gif" style="bottom:20px;left:20px;display:none;">
|
| 1343 |
+
<img src="https://media1.tenor.com/m/20429653/chibi-haachama-hololive-dancing-squareofthelightones-akai-haato.gif" width="110" draggable="false">
|
| 1344 |
+
<div class="drag-close" onclick="document.getElementById('gif-wi-1').style.display='none'">β</div>
|
| 1345 |
+
</div>
|
| 1346 |
+
<div id="gif-wi-2" class="drag-gif" style="bottom:20px;left:170px;display:none;">
|
| 1347 |
+
<img src="https://media1.tenor.com/m/13376873318217365775/evernight-hsr-lonely-lonely-i-guess-im-lonely-dance.gif" width="110" draggable="false">
|
| 1348 |
+
<div class="drag-close" onclick="document.getElementById('gif-wi-2').style.display='none'">β</div>
|
| 1349 |
+
</div>
|
| 1350 |
+
<div id="gif-wi-3" class="drag-gif" style="bottom:20px;left:320px;display:none;">
|
| 1351 |
+
<img src="https://media1.tenor.com/m/129497420709683859/anime-chibi-azazal-silly-jumping.gif" width="110" draggable="false">
|
| 1352 |
+
<div class="drag-close" onclick="document.getElementById('gif-wi-3').style.display='none'">β</div>
|
| 1353 |
+
</div>
|
| 1354 |
+
</div>
|
| 1355 |
+
|
| 1356 |
+
<style>
|
| 1357 |
+
.drag-gif {
|
| 1358 |
+
position: fixed;
|
| 1359 |
+
z-index: 9999;
|
| 1360 |
+
cursor: move;
|
| 1361 |
+
user-select: none;
|
| 1362 |
+
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.4));
|
| 1363 |
+
transition: opacity 0.2s;
|
| 1364 |
+
}
|
| 1365 |
+
.drag-gif:hover { opacity: 0.92; }
|
| 1366 |
+
.drag-gif img { display: block; border-radius: 8px; pointer-events: none; }
|
| 1367 |
+
.drag-close {
|
| 1368 |
+
position: absolute;
|
| 1369 |
+
top: -8px; right: -8px;
|
| 1370 |
+
background: #e74c3c;
|
| 1371 |
+
color: white;
|
| 1372 |
+
border-radius: 50%;
|
| 1373 |
+
width: 20px; height: 20px;
|
| 1374 |
+
font-size: 11px;
|
| 1375 |
+
line-height: 20px;
|
| 1376 |
+
text-align: center;
|
| 1377 |
+
cursor: pointer;
|
| 1378 |
+
font-weight: 700;
|
| 1379 |
+
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
| 1380 |
+
transition: transform 0.15s;
|
| 1381 |
+
}
|
| 1382 |
+
.drag-close:hover { transform: scale(1.2); }
|
| 1383 |
+
</style>
|
| 1384 |
+
|
| 1385 |
+
<script>
|
| 1386 |
+
(function () {
|
| 1387 |
+
'use strict';
|
| 1388 |
+
|
| 1389 |
+
// ββ Draggable logic βββββββββββββββββββββββββββββββββββββββ
|
| 1390 |
+
function makeDraggable(el) {
|
| 1391 |
+
let ox = 0, oy = 0, mx = 0, my = 0;
|
| 1392 |
+
el.addEventListener('mousedown', function (e) {
|
| 1393 |
+
if (e.target.classList.contains('drag-close')) return;
|
| 1394 |
+
e.preventDefault();
|
| 1395 |
+
mx = e.clientX; my = e.clientY;
|
| 1396 |
+
document.addEventListener('mousemove', drag);
|
| 1397 |
+
document.addEventListener('mouseup', stopDrag);
|
| 1398 |
+
});
|
| 1399 |
+
function drag(e) {
|
| 1400 |
+
ox = mx - e.clientX; oy = my - e.clientY;
|
| 1401 |
+
mx = e.clientX; my = e.clientY;
|
| 1402 |
+
el.style.top = (el.offsetTop - oy) + 'px';
|
| 1403 |
+
el.style.left = (el.offsetLeft - ox) + 'px';
|
| 1404 |
+
el.style.bottom = 'auto';
|
| 1405 |
+
}
|
| 1406 |
+
function stopDrag() {
|
| 1407 |
+
document.removeEventListener('mousemove', drag);
|
| 1408 |
+
document.removeEventListener('mouseup', stopDrag);
|
| 1409 |
+
}
|
| 1410 |
+
}
|
| 1411 |
+
document.querySelectorAll('.drag-gif').forEach(makeDraggable);
|
| 1412 |
+
|
| 1413 |
+
// ββ Audio + GIF per-tab controller βββββββββββββββββββββββ
|
| 1414 |
+
const audShap = document.getElementById('audio-shap');
|
| 1415 |
+
const audWhatif = document.getElementById('audio-whatif');
|
| 1416 |
+
const allGifs = document.querySelectorAll('.drag-gif');
|
| 1417 |
+
|
| 1418 |
+
function stopAll() {
|
| 1419 |
+
allGifs.forEach(g => g.style.display = 'none');
|
| 1420 |
+
[audShap, audWhatif].forEach(a => { a.pause(); a.currentTime = 0; });
|
| 1421 |
+
}
|
| 1422 |
+
|
| 1423 |
+
window.setActiveTab = function (idx) {
|
| 1424 |
+
stopAll();
|
| 1425 |
+
if (idx === 0) { // SHAP tab
|
| 1426 |
+
document.getElementById('gif-shap').style.display = 'block';
|
| 1427 |
+
audShap.play().catch(() => {});
|
| 1428 |
+
} else if (idx === 3) { // What-If tab
|
| 1429 |
+
['gif-wi-1','gif-wi-2','gif-wi-3'].forEach(id => {
|
| 1430 |
+
document.getElementById(id).style.display = 'block';
|
| 1431 |
+
});
|
| 1432 |
+
audWhatif.play().catch(() => {});
|
| 1433 |
+
}
|
| 1434 |
+
};
|
| 1435 |
+
|
| 1436 |
+
// ββ Auto-detect active Streamlit tab βββββββββββββββββββββ
|
| 1437 |
+
// Streamlit renders tabs as <li role="tab" aria-selected="true">
|
| 1438 |
+
function detectTab() {
|
| 1439 |
+
const tabs = document.querySelectorAll('[role="tab"]');
|
| 1440 |
+
for (let i = 0; i < tabs.length; i++) {
|
| 1441 |
+
if (tabs[i].getAttribute('aria-selected') === 'true') {
|
| 1442 |
+
window.setActiveTab(i);
|
| 1443 |
+
return;
|
| 1444 |
+
}
|
| 1445 |
+
}
|
| 1446 |
+
}
|
| 1447 |
+
|
| 1448 |
+
// Watch for Streamlit rerenders via MutationObserver
|
| 1449 |
+
const observer = new MutationObserver(function (mutations) {
|
| 1450 |
+
for (const m of mutations) {
|
| 1451 |
+
for (const n of m.addedNodes) {
|
| 1452 |
+
if (n.nodeType === 1) { detectTab(); return; }
|
| 1453 |
+
}
|
| 1454 |
+
}
|
| 1455 |
+
});
|
| 1456 |
+
observer.observe(document.body, { childList: true, subtree: true });
|
| 1457 |
+
|
| 1458 |
+
// Initial detection after short delay (Streamlit needs time to render tabs)
|
| 1459 |
+
setTimeout(detectTab, 800);
|
| 1460 |
+
setTimeout(detectTab, 2000);
|
| 1461 |
+
})();
|
| 1462 |
+
</script>
|
| 1463 |
+
"""
|
| 1464 |
+
components.html(html, height=0, scrolling=False)
|
| 1465 |
+
|
| 1466 |
+
|
| 1467 |
# ============================================================
|
| 1468 |
# HEADER
|
| 1469 |
# ============================================================
|
|
|
|
| 1579 |
st.session_state.chat_history=[]
|
| 1580 |
st.session_state.chat_summary=''
|
| 1581 |
for k in ['wi_dig','wi_biz','wi_emp','wi_cash','wi_dur','wi_loan']: st.session_state[k]=None
|
|
|
|
| 1582 |
_save_chat_memory([], '')
|
| 1583 |
except Exception as e:
|
| 1584 |
st.error(f"Error: {e}"); st.stop()
|
|
|
|
| 1623 |
f'<div class="kpi-sub">{T(sub_k,lang)}</div></div>', unsafe_allow_html=True)
|
| 1624 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 1625 |
|
| 1626 |
+
# ============================================================
|
| 1627 |
+
# FLOATING MEDIA (inject once, JS auto-detects active tab)
|
| 1628 |
+
# ============================================================
|
| 1629 |
+
render_media_manager()
|
| 1630 |
+
|
| 1631 |
# ============================================================
|
| 1632 |
# TABS
|
| 1633 |
# ============================================================
|
|
|
|
| 1659 |
with t3:
|
| 1660 |
st.markdown(f'<div class="sec-title">{T("chat_title",lang)}</div>', unsafe_allow_html=True)
|
| 1661 |
|
|
|
|
| 1662 |
if st.session_state.get('chat_summary'):
|
| 1663 |
n_turns = len(st.session_state.chat_history)
|
| 1664 |
lbl = {'id': f'π§ Memory aktif Β· {n_turns} pesan tersimpan',
|
|
|
|
| 1696 |
st.warning(f"β οΈ Semua LLM gagal, menggunakan Smart Fallback. Error: `{_debug_err[:100]}`")
|
| 1697 |
st.session_state.chat_history.append({'role':'assistant','content':ai_resp})
|
| 1698 |
|
|
|
|
| 1699 |
_save_chat_memory(
|
| 1700 |
st.session_state.chat_history,
|
| 1701 |
st.session_state.get('chat_summary', '')
|