Update app.py
Browse files
app.py
CHANGED
|
@@ -1,15 +1,13 @@
|
|
| 1 |
# ============================================================
|
| 2 |
-
# ๐ฆ SME Credit Risk Assessment โ FINAL
|
| 3 |
# Final Project | AI Engineering Bootcamp Batch 10
|
| 4 |
# Author: 1na37
|
| 5 |
# ============================================================
|
| 6 |
-
#
|
| 7 |
-
#
|
| 8 |
-
#
|
| 9 |
-
#
|
| 10 |
-
#
|
| 11 |
-
# 5. Download button guarded (only shown after result exists)
|
| 12 |
-
# 6. Mute sync: inject_media_manager called every render after result
|
| 13 |
# ============================================================
|
| 14 |
|
| 15 |
import streamlit as st
|
|
@@ -32,7 +30,7 @@ st.set_page_config(
|
|
| 32 |
)
|
| 33 |
|
| 34 |
# ============================================================
|
| 35 |
-
# TRANSLATION SYSTEM
|
| 36 |
# ============================================================
|
| 37 |
TRANSLATIONS = {
|
| 38 |
'sidebar_api': {'id':'๐ API Keys (opsional)', 'en':'๐ API Keys (optional)', 'hi':'๐ API Keys (vaikalpik)'},
|
|
@@ -131,9 +129,7 @@ TRANSLATIONS = {
|
|
| 131 |
'wi_pd_from': {'id':'dari', 'en':'from', 'hi':'se'},
|
| 132 |
'wi_no_change': {'id':'Perubahan minimal pada skor risiko','en':'Minimal change in risk score','hi':'Jokhim score mein nyoonatam badlaav'},
|
| 133 |
'wi_tips_title': {'id':'๐ก Tips Optimasi Skor', 'en':'๐ก Score Optimization Tips', 'hi':'๐ก Score Sudhaar Tips'},
|
| 134 |
-
'wi_form_first': {'id':'
|
| 135 |
-
'en':'โฌ๏ธ Submit the form first to see the simulation.',
|
| 136 |
-
'hi':'โฌ๏ธ Pehle form submit karein simulation dekhne ke liye.'},
|
| 137 |
'wi_approved': {'id':'LAYAK', 'en':'APPROVED', 'hi':'SWIKAARY'},
|
| 138 |
'wi_review': {'id':'REVIEW', 'en':'REVIEW', 'hi':'SAMEEKSHA'},
|
| 139 |
'wi_highrisk': {'id':'RISIKO TINGGI', 'en':'HIGH RISK', 'hi':'UCHCH JOKHIM'},
|
|
@@ -157,6 +153,10 @@ TRANSLATIONS = {
|
|
| 157 |
'risk_approved': {'id':'๐ข LAYAK', 'en':'๐ข APPROVED', 'hi':'๐ข YOGYA'},
|
| 158 |
'risk_review': {'id':'๐ก PERLU REVIEW', 'en':'๐ก REVIEW', 'hi':'๐ก SAMEEKSHA'},
|
| 159 |
'risk_high': {'id':'๐ด BERISIKO TINGGI', 'en':'๐ด HIGH RISK', 'hi':'๐ด UCHCH JOKHIM'},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
}
|
| 161 |
|
| 162 |
def T(key: str, lang: str) -> str:
|
|
@@ -164,7 +164,7 @@ def T(key: str, lang: str) -> str:
|
|
| 164 |
return entry.get(lang, entry.get('en', key))
|
| 165 |
|
| 166 |
# ============================================================
|
| 167 |
-
# SELECTBOX LABELS
|
| 168 |
# ============================================================
|
| 169 |
CHECKING_OPTS = ['<0', '0<=X<200', '>=200', 'no checking']
|
| 170 |
CHECKING_LABELS = {
|
|
@@ -220,7 +220,7 @@ HOUSING_LABELS = {
|
|
| 220 |
LANG_LABELS = {'id':'๐ฎ๐ฉ Bahasa Indonesia','en':'๐ฌ๐ง English','hi':'๐ฎ๐ณ Hindi (Roman)'}
|
| 221 |
|
| 222 |
# ============================================================
|
| 223 |
-
# CSS
|
| 224 |
# ============================================================
|
| 225 |
st.markdown("""
|
| 226 |
<style>
|
|
@@ -295,7 +295,7 @@ def _log_api(step: str, src: str, ok: bool, ms: int):
|
|
| 295 |
st.session_state.api_log = st.session_state.api_log[-20:]
|
| 296 |
|
| 297 |
# ============================================================
|
| 298 |
-
# _set_status โ FIX: if/else instead of ternary
|
| 299 |
# ============================================================
|
| 300 |
def _set_status(msg: str):
|
| 301 |
st.session_state['llm_status'] = msg
|
|
@@ -313,7 +313,7 @@ def _set_status(msg: str):
|
|
| 313 |
pass
|
| 314 |
|
| 315 |
# ============================================================
|
| 316 |
-
# FORMAL TOOL CALLING SCHEMA
|
| 317 |
# ============================================================
|
| 318 |
MAYA_TOOLS = [
|
| 319 |
{
|
|
@@ -342,6 +342,7 @@ MAYA_TOOLS = [
|
|
| 342 |
}
|
| 343 |
]
|
| 344 |
|
|
|
|
| 345 |
_OR_TOOL_MODELS = [
|
| 346 |
"google/gemini-2.0-flash-exp:free",
|
| 347 |
"qwen/qwen3-32b:free",
|
|
@@ -356,7 +357,7 @@ _GROQ_TOOL_MODELS = [
|
|
| 356 |
]
|
| 357 |
|
| 358 |
# ============================================================
|
| 359 |
-
# LLM API CALLERS
|
| 360 |
# ============================================================
|
| 361 |
def call_openrouter_tools(messages, api_key, tools=None):
|
| 362 |
if not api_key:
|
|
@@ -452,6 +453,7 @@ def _clean_response(text: str) -> tuple:
|
|
| 452 |
return text, {}
|
| 453 |
adjustments = {}
|
| 454 |
text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()
|
|
|
|
| 455 |
json_pattern = re.compile(r'\{[^{}]*"field"\s*:\s*"([^"]+)"[^{}]*"value"\s*:\s*([\d.]+)[^{}]*\}', re.DOTALL)
|
| 456 |
valid_fields = {'digital_presence_score','business_age_years','num_employees','monthly_cash_flow','duration','loan_rp'}
|
| 457 |
for m in json_pattern.finditer(text):
|
|
@@ -459,23 +461,27 @@ def _clean_response(text: str) -> tuple:
|
|
| 459 |
if field in valid_fields:
|
| 460 |
adjustments[field] = val
|
| 461 |
text = json_pattern.sub('', text).strip()
|
|
|
|
| 462 |
for m in re.finditer(r'\[ADJUST:\s*(\w+)\s*=\s*([\d.]+)\]', text):
|
| 463 |
field, val = m.group(1), float(m.group(2))
|
| 464 |
if field not in adjustments:
|
| 465 |
adjustments[field] = val
|
| 466 |
text = re.sub(r'\s*\[ADJUST:[^\]]+\]\s*', ' ', text).strip()
|
|
|
|
| 467 |
_standalone_action = re.compile(
|
| 468 |
r',?\s*(?:coba\s+)?(?:naikkan|tingkatkan|kurangi|optimalkan|pertahankan|daftarkan|pastikan|perbaiki)'
|
| 469 |
r'\s+\S+\s+(?:ke|ke\s+)[\w\s./]+(?:jt|M|juta|bulan|tahun|/bln|/month)?\.?\s*$',
|
| 470 |
re.IGNORECASE
|
| 471 |
)
|
| 472 |
text = _standalone_action.sub('', text).strip()
|
|
|
|
| 473 |
_standalone_en = re.compile(
|
| 474 |
r',?\s*(?:try\s+)?(?:raise|increase|lower|reduce|optimize|improve|register)\s+'
|
| 475 |
r'\S+\s+(?:to|to\s+)[\w\s./]+(?:M|jt|juta|months?|years?)?\.?\s*$',
|
| 476 |
re.IGNORECASE
|
| 477 |
)
|
| 478 |
text = _standalone_en.sub('', text).strip()
|
|
|
|
| 479 |
text = re.sub(r' +', ' ', text)
|
| 480 |
text = re.sub(r' ([,.])', r'\1', text)
|
| 481 |
text = re.sub(r'\n\*\*\s*\*\*\s*$', '', text, flags=re.MULTILINE).strip()
|
|
@@ -483,6 +489,7 @@ def _clean_response(text: str) -> tuple:
|
|
| 483 |
return text, adjustments
|
| 484 |
|
| 485 |
def _call_chat_llm(messages):
|
|
|
|
| 486 |
_or = st.session_state.get("openrouter_key", "")
|
| 487 |
_grq = st.session_state.get("groq_key", "")
|
| 488 |
if _or:
|
|
@@ -584,6 +591,9 @@ def _load_chat_memory():
|
|
| 584 |
pass
|
| 585 |
return [], ""
|
| 586 |
|
|
|
|
|
|
|
|
|
|
| 587 |
def _summarize_history(history, lang):
|
| 588 |
if len(history) <= 8:
|
| 589 |
return history, st.session_state.get('chat_summary', '')
|
|
@@ -610,19 +620,24 @@ def _summarize_history(history, lang):
|
|
| 610 |
st.session_state.chat_summary = new_summary
|
| 611 |
return recent_turns, new_summary
|
| 612 |
|
| 613 |
-
# Early init
|
| 614 |
-
for _k, _v in [('
|
| 615 |
if _k not in st.session_state:
|
| 616 |
st.session_state[_k] = _v
|
| 617 |
|
| 618 |
# ============================================================
|
| 619 |
-
# SIDEBAR
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
# ============================================================
|
| 621 |
with st.sidebar:
|
| 622 |
st.markdown("## ๐ฆ SME Credit Risk AI")
|
| 623 |
st.markdown("---")
|
| 624 |
|
| 625 |
-
# Language selector
|
| 626 |
if 'lang_sel' not in st.session_state:
|
| 627 |
st.session_state.lang_sel = 'id'
|
| 628 |
lang = st.radio(
|
|
@@ -633,80 +648,14 @@ with st.sidebar:
|
|
| 633 |
)
|
| 634 |
st.markdown("---")
|
| 635 |
|
| 636 |
-
#
|
| 637 |
-
_musik_labels = {'id':'### ๐ต Musik','en':'### ๐ต Music','hi':'### ๐ต Sangeet'}
|
| 638 |
-
st.markdown(_musik_labels.get(lang, _musik_labels['id']))
|
| 639 |
-
|
| 640 |
-
_mute_labels = {
|
| 641 |
-
'id': ('๐ Matikan Suara', '๐ Nyalakan Suara'),
|
| 642 |
-
'en': ('๐ Mute Sound', '๐ Unmute Sound'),
|
| 643 |
-
'hi': ('๐ Awaaz Band', '๐ Awaaz Chalu'),
|
| 644 |
-
}
|
| 645 |
-
_ml = _mute_labels.get(lang, _mute_labels['id'])
|
| 646 |
-
mute_label = _ml[0] if not st.session_state.gif_muted else _ml[1]
|
| 647 |
-
if st.button(mute_label, key="btn_mute", use_container_width=True):
|
| 648 |
-
st.session_state.gif_muted = not st.session_state.gif_muted
|
| 649 |
-
st.rerun()
|
| 650 |
-
|
| 651 |
-
_musik_active = {
|
| 652 |
-
'id': 'Musik aktif di tab SHAP & What-If',
|
| 653 |
-
'en': 'Music active on SHAP & What-If tabs',
|
| 654 |
-
'hi': 'Sangeet SHAP & What-If tab mein active hai',
|
| 655 |
-
}
|
| 656 |
-
st.caption(_musik_active.get(lang, _musik_active['id']))
|
| 657 |
-
|
| 658 |
-
# Media file status
|
| 659 |
-
_media_files_map = {
|
| 660 |
-
'crying_cat.gif': {'id':'GIF Kucing Nangis (tab SHAP)','en':'Crying Cat GIF (SHAP tab)','hi':'Rota Billa GIF (SHAP tab)'},
|
| 661 |
-
'dance1.gif': {'id':'Dance GIF 1 (What-If)','en':'Dance GIF 1 (What-If)','hi':'Dance GIF 1 (What-If)'},
|
| 662 |
-
'dance2.gif': {'id':'Dance GIF 2 (What-If)','en':'Dance GIF 2 (What-If)','hi':'Dance GIF 2 (What-If)'},
|
| 663 |
-
'dance3.gif': {'id':'Dance GIF 3 (What-If)','en':'Dance GIF 3 (What-If)','hi':'Dance GIF 3 (What-If)'},
|
| 664 |
-
'dance4.gif': {'id':'Dance GIF 4 (What-If)','en':'Dance GIF 4 (What-If)','hi':'Dance GIF 4 (What-If)'},
|
| 665 |
-
'dance5.gif': {'id':'Dance GIF 5 (What-If)','en':'Dance GIF 5 (What-If)','hi':'Dance GIF 5 (What-If)'},
|
| 666 |
-
'idk.mp3': {'id':'Musik SHAP (idk.mp3)', 'en':'SHAP Music (idk.mp3)', 'hi':'SHAP Sangeet (idk.mp3)'},
|
| 667 |
-
'shake.mp3': {'id':'Musik What-If (shake.mp3)','en':'What-If Music (shake.mp3)','hi':'What-If Sangeet (shake.mp3)'},
|
| 668 |
-
}
|
| 669 |
-
_missing = [(f, l[lang]) for f, l in _media_files_map.items() if not os.path.exists('static/' + f)]
|
| 670 |
-
_found = [(f, l[lang]) for f, l in _media_files_map.items() if os.path.exists('static/' + f)]
|
| 671 |
-
_total = len(_media_files_map)
|
| 672 |
-
_upload_hint = {
|
| 673 |
-
'id': 'Upload file ke folder `static/` di HF Space.',
|
| 674 |
-
'en': 'Upload files to the `static/` folder in your HF Space.',
|
| 675 |
-
'hi': '`static/` folder mein files upload karein HF Space par.',
|
| 676 |
-
}
|
| 677 |
-
if _missing:
|
| 678 |
-
_warn_title = {
|
| 679 |
-
'id': 'โ ๏ธ Media belum diupload (' + str(len(_missing)) + '/' + str(_total) + ' hilang)',
|
| 680 |
-
'en': 'โ ๏ธ Media not uploaded (' + str(len(_missing)) + '/' + str(_total) + ' missing)',
|
| 681 |
-
'hi': 'โ ๏ธ Media upload nahi (' + str(len(_missing)) + '/' + str(_total) + ' missing)',
|
| 682 |
-
}
|
| 683 |
-
with st.expander(_warn_title.get(lang, _warn_title['id']), expanded=len(_missing) == _total):
|
| 684 |
-
for fname, name in _missing:
|
| 685 |
-
st.caption('โ ' + name)
|
| 686 |
-
st.caption(_upload_hint.get(lang, _upload_hint['id']))
|
| 687 |
-
if _found:
|
| 688 |
-
_partial = {
|
| 689 |
-
'id': 'โณ ' + str(len(_found)) + '/' + str(_total) + ' media siap',
|
| 690 |
-
'en': 'โณ ' + str(len(_found)) + '/' + str(_total) + ' media ready',
|
| 691 |
-
'hi': 'โณ ' + str(len(_found)) + '/' + str(_total) + ' media taiyaar',
|
| 692 |
-
}
|
| 693 |
-
st.caption(_partial.get(lang, _partial['id']))
|
| 694 |
-
else:
|
| 695 |
-
_all_ok = {
|
| 696 |
-
'id': 'โ
Semua ' + str(_total) + ' file media ditemukan โ GIF & musik siap!',
|
| 697 |
-
'en': 'โ
All ' + str(_total) + ' media files found โ GIFs & music ready!',
|
| 698 |
-
'hi': 'โ
Sabhi ' + str(_total) + ' media files mile โ GIF & sangeet taiyaar!',
|
| 699 |
-
}
|
| 700 |
-
st.caption(_all_ok.get(lang, _all_ok['id']))
|
| 701 |
-
st.markdown("---")
|
| 702 |
-
|
| 703 |
-
# API key status
|
| 704 |
openrouter_key = st.session_state.openrouter_key
|
| 705 |
groq_key = st.session_state.groq_key
|
| 706 |
st.caption("๐ **OpenRouter:** " + ('โ
' if openrouter_key else 'โ') + " **Groq:** " + ('โ
' if groq_key else 'โ'))
|
| 707 |
if not any([openrouter_key, groq_key]):
|
| 708 |
st.warning("โ ๏ธ Tidak ada API key. Tambahkan OPENROUTER_API_KEY / GROQ_API_KEY di HF Secrets.")
|
| 709 |
|
|
|
|
| 710 |
_llm_ph = st.empty()
|
| 711 |
st.session_state['_llm_ph'] = _llm_ph
|
| 712 |
if st.session_state.get('llm_status'):
|
|
@@ -716,7 +665,7 @@ with st.sidebar:
|
|
| 716 |
pass
|
| 717 |
st.markdown("---")
|
| 718 |
|
| 719 |
-
# API Tracker
|
| 720 |
st.markdown("### ๐ก API Tracker")
|
| 721 |
_active_status = st.session_state.get('llm_status', '')
|
| 722 |
if _active_status:
|
|
@@ -737,7 +686,7 @@ with st.sidebar:
|
|
| 737 |
st.rerun()
|
| 738 |
st.markdown("---")
|
| 739 |
|
| 740 |
-
# Stack + footer
|
| 741 |
st.markdown(T('sidebar_stack', lang))
|
| 742 |
stack_items = {
|
| 743 |
'id': "- ๐ค XGBoost + LightGBM + RF\n- ๐ SHAP Eksplainabilitas\n- ๐ฌ Narasi AI (OpenRouter + Groq)\n- ๐ค Maya AI Chat + Tool Calling\n- ๐ง Chain-of-Thought + Few-shot\n- ๐พ Memory Summarization\n- ๐ ID / EN / HI",
|
|
@@ -803,8 +752,7 @@ _defaults = dict(
|
|
| 803 |
wi_dig=None, wi_biz=None, wi_emp=None,
|
| 804 |
wi_cash=None, wi_dur=None, wi_loan=None,
|
| 805 |
chat_summary='', memory_loaded=False,
|
| 806 |
-
|
| 807 |
-
_last_muted_sent=None,
|
| 808 |
)
|
| 809 |
for k, v in _defaults.items():
|
| 810 |
if k not in st.session_state:
|
|
@@ -851,6 +799,7 @@ def shap_summary(vals, names, n=5):
|
|
| 851 |
])
|
| 852 |
|
| 853 |
def make_shap_png(shap_vals, feature_names):
|
|
|
|
| 854 |
sv = np.array(shap_vals)
|
| 855 |
names = list(feature_names)
|
| 856 |
n = min(12, len(sv))
|
|
@@ -872,7 +821,7 @@ def make_shap_png(shap_vals, feature_names):
|
|
| 872 |
return 'data:image/png;base64,' + b64
|
| 873 |
|
| 874 |
# ============================================================
|
| 875 |
-
# LLM HELPERS โ Narrative
|
| 876 |
# ============================================================
|
| 877 |
_OR_FREE_MODELS = [
|
| 878 |
"google/gemini-2.0-flash-exp:free",
|
|
@@ -920,11 +869,13 @@ def call_openrouter(messages, api_key, model=None):
|
|
| 920 |
except requests.exceptions.Timeout:
|
| 921 |
ms = int((_time.time() - t0) * 1000)
|
| 922 |
last_error = "Timeout (" + m + ")"
|
| 923 |
-
|
|
|
|
| 924 |
except Exception as e:
|
| 925 |
ms = int((_time.time() - t0) * 1000)
|
| 926 |
last_error = m + ": " + str(e)[:100]
|
| 927 |
-
|
|
|
|
| 928 |
return None, last_error
|
| 929 |
|
| 930 |
def call_groq(messages, api_key):
|
|
@@ -960,17 +911,20 @@ def call_groq(messages, api_key):
|
|
| 960 |
return None, last_error
|
| 961 |
|
| 962 |
def _call_llm(messages):
|
|
|
|
| 963 |
_or = st.session_state.get("openrouter_key", "")
|
| 964 |
_grq = st.session_state.get("groq_key", "")
|
| 965 |
last_err = None
|
| 966 |
if _or:
|
| 967 |
r, last_err = call_openrouter(messages, _or)
|
| 968 |
if r:
|
| 969 |
-
|
|
|
|
| 970 |
if _grq:
|
| 971 |
r, last_err = call_groq(messages, _grq)
|
| 972 |
if r:
|
| 973 |
-
|
|
|
|
| 974 |
return None, None, last_err or "No API key configured"
|
| 975 |
|
| 976 |
# ============================================================
|
|
@@ -1112,7 +1066,7 @@ def _get_top_issue(raw_input, pd_pct, lang):
|
|
| 1112 |
if not npwp:
|
| 1113 |
return _t("prioritas utama: **urus NPWP** dulu di pajak.go.id","top priority: **register NPWP** at pajak.go.id","mukhya prathamikta: **NPWP register karein** pajak.go.id par")
|
| 1114 |
elif dig < 40:
|
| 1115 |
-
return _t("**digital score " + str(dig) + "/100** adalah area paling kritis","**digital score " + str(dig) + "/100** is the most critical area","**digital score " + str(dig) + "/100** sabse mahatvapurn kshetra hai")
|
| 1116 |
elif cf < 10e6:
|
| 1117 |
return _t("**cash flow Rp " + str(int(cf/1e6)) + "jt/bln** perlu dioptimalkan","**cash flow Rp " + str(int(cf/1e6)) + "M/month** needs optimization","**naqad pravaah Rp " + str(int(cf/1e6)) + "M/maah** optimize zaroori hai")
|
| 1118 |
elif biz < 2:
|
|
@@ -1120,7 +1074,7 @@ def _get_top_issue(raw_input, pd_pct, lang):
|
|
| 1120 |
elif pd_pct < 20:
|
| 1121 |
return _t("profil kamu sudah sangat bagus!","your profile is already excellent!","aapka parichay pehle se behtareen hai!")
|
| 1122 |
else:
|
| 1123 |
-
return _t("beberapa area bisa diperbaiki!","several areas can be improved!","kai kshetra sudhaare jaa sakte hain!")
|
| 1124 |
|
| 1125 |
# ============================================================
|
| 1126 |
# AI CHAT โ Maya persona
|
|
@@ -1190,20 +1144,28 @@ CARA BERPIKIR โ CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
|
|
| 1190 |
"- Pinjaman: Rp " + str(int(raw_input.get('loan_rp',50e6)/1e6)) + "M | Tenor: " + str(raw_input.get('duration',24)) + " bulan\n"
|
| 1191 |
"- Riwayat Kredit: " + str(raw_input.get('credit_history')) + "\n\n"
|
| 1192 |
"FAKTOR RISIKO UTAMA (SHAP):\n" + factors + "\n"
|
| 1193 |
-
+ context
|
| 1194 |
-
+
|
| 1195 |
-
|
|
|
|
|
|
|
|
|
|
| 1196 |
"2. Kalau relevan, selalu hubungkan ke data pemohon dengan menyebut angka aktualnya.\n"
|
| 1197 |
"3. Berikan saran KONKRET dan SPESIFIK, bukan generik.\n"
|
| 1198 |
"4. Maksimal 150 kata kecuali diminta lebih panjang.\n"
|
| 1199 |
-
"5. FORMAT TAG [ADJUST:] โ
|
| 1200 |
-
" BENAR:
|
| 1201 |
-
"
|
| 1202 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1203 |
" num_employees(1-50), monthly_cash_flow(angka Rp), duration(4-72), loan_rp(angka Rp)\n"
|
| 1204 |
"6. JANGAN pernah balik ke template. Jawab seperti manusia cerdas.\n"
|
| 1205 |
"7. Baca context percakapan sebelumnya sebelum menjawab โ jaga konsistensi topik.\n"
|
| 1206 |
-
"8. AKHIRI response dengan kalimat lengkap.\n"
|
| 1207 |
)
|
| 1208 |
|
| 1209 |
messages = [{"role": "system", "content": system}]
|
|
@@ -1229,26 +1191,94 @@ CARA BERPIKIR โ CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
|
|
| 1229 |
|
| 1230 |
if any(w in low for w in ['siapa','kamu','km','who are you','nama','you are','maya','halo','hai','hei','hello','hi','perkenalan']):
|
| 1231 |
response = _t(
|
| 1232 |
-
"Hei! Aku **Maya**, AI Credit Advisor kamu
|
| 1233 |
-
"
|
| 1234 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1235 |
)
|
| 1236 |
-
|
|
|
|
|
|
|
|
|
|
| 1237 |
tips = []
|
| 1238 |
if not npwp:
|
| 1239 |
-
tips.append(_t("1. **Urus NPWP** โ
|
|
|
|
|
|
|
|
|
|
| 1240 |
if dig < 75:
|
| 1241 |
n = '2' if not npwp else '1'
|
| 1242 |
-
tips.append(_t(n + ". **Naikkan Digital Score dari " + str(dig) + " ke 75+**
|
|
|
|
|
|
|
| 1243 |
adjustments['digital_presence_score'] = 75
|
| 1244 |
if cf < 25e6:
|
| 1245 |
n = str(len(tips) + 1)
|
| 1246 |
-
tips.append(_t(n + ". **Optimalkan cash flow dari Rp " + str(int(cf/1e6)) + "jt ke 25jt+/bln**
|
|
|
|
|
|
|
| 1247 |
adjustments['monthly_cash_flow'] = 25000000
|
| 1248 |
if not tips:
|
| 1249 |
-
tips.append(_t("Profil kamu sudah solid dengan PD " + str(round(pd_pct,1)) + "%!
|
| 1250 |
-
|
| 1251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1252 |
if cf > 0:
|
| 1253 |
dur_val = raw_input.get('duration', 24)
|
| 1254 |
safe = cf * dur_val * 0.35
|
|
@@ -1256,28 +1286,113 @@ CARA BERPIKIR โ CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
|
|
| 1256 |
if cur > safe:
|
| 1257 |
adjustments['loan_rp'] = safe
|
| 1258 |
response = _t(
|
| 1259 |
-
"
|
| 1260 |
-
"
|
| 1261 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1262 |
)
|
| 1263 |
else:
|
| 1264 |
-
response = _t("Isi cash flow bulanan di form dulu ya
|
| 1265 |
-
|
|
|
|
|
|
|
|
|
|
| 1266 |
response = _t(
|
| 1267 |
-
"
|
| 1268 |
-
"
|
| 1269 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1270 |
)
|
| 1271 |
if dig < 75:
|
| 1272 |
adjustments['digital_presence_score'] = 75
|
|
|
|
| 1273 |
else:
|
| 1274 |
top_issue = _get_top_issue(raw_input, pd_pct, lang)
|
| 1275 |
has_key = any([st.session_state.get("openrouter_key"), st.session_state.get("groq_key")])
|
| 1276 |
err_note = "\n\nLLM error: " + str(_last_error)[:80] if (has_key and _last_error) else ""
|
| 1277 |
response = _t(
|
| 1278 |
-
"
|
| 1279 |
-
"
|
| 1280 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1281 |
)
|
| 1282 |
|
| 1283 |
if response:
|
|
@@ -1288,487 +1403,16 @@ CARA BERPIKIR โ CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
|
|
| 1288 |
|
| 1289 |
return response or "...", adjustments, _last_error
|
| 1290 |
|
| 1291 |
-
# ============================================================
|
| 1292 |
-
# FLOATING DRAGGABLE GIF + SOUND โ FIXED: now actually called
|
| 1293 |
-
# ============================================================
|
| 1294 |
-
@st.cache_resource(show_spinner=False)
|
| 1295 |
-
def _load_media_b64():
|
| 1296 |
-
def _b64(path, mime):
|
| 1297 |
-
try:
|
| 1298 |
-
with open(path, 'rb') as f:
|
| 1299 |
-
data = base64.b64encode(f.read()).decode()
|
| 1300 |
-
return "data:" + mime + ";base64," + data
|
| 1301 |
-
except Exception:
|
| 1302 |
-
return ""
|
| 1303 |
-
return {
|
| 1304 |
-
'crying_cat': _b64('static/crying_cat.gif','image/gif'),
|
| 1305 |
-
'dance1': _b64('static/dance1.gif','image/gif'),
|
| 1306 |
-
'dance2': _b64('static/dance2.gif','image/gif'),
|
| 1307 |
-
'dance3': _b64('static/dance3.gif','image/gif'),
|
| 1308 |
-
'dance4': _b64('static/dance4.gif','image/gif'),
|
| 1309 |
-
'dance5': _b64('static/dance5.gif','image/gif'),
|
| 1310 |
-
'idk': _b64('static/idk.mp3','audio/mpeg'),
|
| 1311 |
-
'shake': _b64('static/shake.mp3','audio/mpeg'),
|
| 1312 |
-
}
|
| 1313 |
-
|
| 1314 |
-
def inject_media_manager():
|
| 1315 |
-
"""
|
| 1316 |
-
Injects floating GIF + audio system into the parent Streamlit page.
|
| 1317 |
-
- Tab 0 (SHAP) โ crying_cat.gif + idk.mp3
|
| 1318 |
-
- Tab 3 (What-If) โ dance1-5.gif + shake.mp3
|
| 1319 |
-
- Other tabs โ hide all GIF, pause audio
|
| 1320 |
-
- GIFs are draggable by user
|
| 1321 |
-
- Roam every 6s
|
| 1322 |
-
- Audio unlocked on first user click (browser autoplay policy)
|
| 1323 |
-
- Mute state synced every render via VOL variable
|
| 1324 |
-
"""
|
| 1325 |
-
media = _load_media_b64()
|
| 1326 |
-
has_media = any(media.get(k, '') for k in ['crying_cat', 'dance1'])
|
| 1327 |
-
|
| 1328 |
-
muted = st.session_state.get('gif_muted', False)
|
| 1329 |
-
vol_frac = 0.0 if muted else 0.35
|
| 1330 |
-
vf_str = str(vol_frac)
|
| 1331 |
-
|
| 1332 |
-
def _gif(gid, b64):
|
| 1333 |
-
if not b64:
|
| 1334 |
-
return ""
|
| 1335 |
-
return (
|
| 1336 |
-
'<div id="' + gid + '" style="'
|
| 1337 |
-
'display:none;position:fixed;z-index:99999;'
|
| 1338 |
-
'cursor:grab;user-select:none;'
|
| 1339 |
-
'transition:left 1.1s cubic-bezier(.4,0,.2,1),'
|
| 1340 |
-
'top 1.1s cubic-bezier(.4,0,.2,1);'
|
| 1341 |
-
'filter:drop-shadow(0 4px 16px rgba(0,0,0,.6));'
|
| 1342 |
-
'left:80px;top:80px;">'
|
| 1343 |
-
'<img src="' + b64 + '" style="'
|
| 1344 |
-
'width:110px;height:auto;border-radius:14px;display:block;'
|
| 1345 |
-
'pointer-events:none;">'
|
| 1346 |
-
'</div>'
|
| 1347 |
-
)
|
| 1348 |
-
|
| 1349 |
-
g0 = _gif("sme-gif-shap", media.get("crying_cat", ""))
|
| 1350 |
-
g1 = _gif("sme-gif-wi1", media.get("dance1", ""))
|
| 1351 |
-
g2 = _gif("sme-gif-wi2", media.get("dance2", ""))
|
| 1352 |
-
g3 = _gif("sme-gif-wi3", media.get("dance3", ""))
|
| 1353 |
-
g4 = _gif("sme-gif-wi4", media.get("dance4", ""))
|
| 1354 |
-
g5 = _gif("sme-gif-wi5", media.get("dance5", ""))
|
| 1355 |
-
sa = media.get("idk", "")
|
| 1356 |
-
sw = media.get("shake", "")
|
| 1357 |
-
|
| 1358 |
-
dom_str = (g0 + g1 + g2 + g3 + g4 + g5 +
|
| 1359 |
-
'<audio id="sme-aud-shap" loop preload="auto"></audio>' +
|
| 1360 |
-
'<audio id="sme-aud-wi" loop preload="auto"></audio>')
|
| 1361 |
-
dom_js = dom_str.replace('\\', '\\\\').replace('`', '\\`').replace('${', '\\${')
|
| 1362 |
-
sa_safe = sa.replace('\\', '\\\\').replace("'", "\\'")
|
| 1363 |
-
sw_safe = sw.replace('\\', '\\\\').replace("'", "\\'")
|
| 1364 |
-
has_js = 'true' if has_media else 'false'
|
| 1365 |
-
|
| 1366 |
-
js = (
|
| 1367 |
-
"(function(){"
|
| 1368 |
-
"var par=window.parent, pd=par.document;"
|
| 1369 |
-
"var VOL=" + vf_str + ";"
|
| 1370 |
-
"var HAS=" + has_js + ";"
|
| 1371 |
-
|
| 1372 |
-
# ONE-TIME INIT
|
| 1373 |
-
"if(!par._sme){"
|
| 1374 |
-
"par._sme={ct:-1,att:new WeakSet(),rt:null,vol:VOL,dragging:false};"
|
| 1375 |
-
|
| 1376 |
-
# Inject DOM once
|
| 1377 |
-
"if(HAS&&!pd.getElementById('sme-media-root')){"
|
| 1378 |
-
"var r=pd.createElement('div');r.id='sme-media-root';"
|
| 1379 |
-
"r.innerHTML=`" + dom_js + "`;"
|
| 1380 |
-
"var as2=pd.getElementById('sme-aud-shap');"
|
| 1381 |
-
"var aw2=pd.getElementById('sme-aud-wi');"
|
| 1382 |
-
"if(as2)as2.setAttribute('src','" + sa_safe + "');"
|
| 1383 |
-
"if(aw2)aw2.setAttribute('src','" + sw_safe + "');"
|
| 1384 |
-
"pd.body.appendChild(r);"
|
| 1385 |
-
"}"
|
| 1386 |
-
|
| 1387 |
-
# GIF config
|
| 1388 |
-
"var GS=['sme-gif-shap'];"
|
| 1389 |
-
"var GW=['sme-gif-wi1','sme-gif-wi2','sme-gif-wi3','sme-gif-wi4','sme-gif-wi5'];"
|
| 1390 |
-
"var GA=GS.concat(GW);"
|
| 1391 |
-
"var ZN=["
|
| 1392 |
-
"[0.04,0.60],[0.80,0.60],[0.42,0.68],[0.04,0.08],[0.80,0.08],"
|
| 1393 |
-
"[0.04,0.35],[0.80,0.35],[0.24,0.65],[0.62,0.65],[0.24,0.08],"
|
| 1394 |
-
"[0.62,0.08],[0.52,0.60],[0.14,0.48],[0.74,0.48]"
|
| 1395 |
-
"];"
|
| 1396 |
-
"var LZ={};"
|
| 1397 |
-
|
| 1398 |
-
# Occupied positions
|
| 1399 |
-
"par._sme.occ=function(sk){"
|
| 1400 |
-
"var o=[];"
|
| 1401 |
-
"GA.forEach(function(id){"
|
| 1402 |
-
"if(id===sk)return;"
|
| 1403 |
-
"var el=pd.getElementById(id);"
|
| 1404 |
-
"if(!el||el.style.display==='none')return;"
|
| 1405 |
-
"o.push({x:el.offsetLeft,y:el.offsetTop});});"
|
| 1406 |
-
"return o;};"
|
| 1407 |
-
|
| 1408 |
-
# Pick random zone avoiding collisions
|
| 1409 |
-
"par._sme.rz=function(gid,oc){"
|
| 1410 |
-
"var ww=par.innerWidth,wh=par.innerHeight,sz=130;"
|
| 1411 |
-
"var sh=ZN.slice().sort(function(){return Math.random()-0.5;});"
|
| 1412 |
-
"var pv=LZ[gid]!=null?LZ[gid]:-1;"
|
| 1413 |
-
"var ca=sh.filter(function(z,i){return i!==pv;});"
|
| 1414 |
-
"if(!ca.length)ca=sh;"
|
| 1415 |
-
"for(var i=0;i<ca.length;i++){"
|
| 1416 |
-
"var z=ca[i],px=Math.min(z[0]*ww,ww-sz-10),py=Math.min(z[1]*wh,wh-sz-10);"
|
| 1417 |
-
"var ok=true;"
|
| 1418 |
-
"for(var j=0;j<oc.length;j++){"
|
| 1419 |
-
"var dx=px-oc[j].x,dy=py-oc[j].y;"
|
| 1420 |
-
"if(Math.sqrt(dx*dx+dy*dy)<160){ok=false;break;}}"
|
| 1421 |
-
"if(ok){LZ[gid]=ZN.indexOf(z);return{x:px,y:py};}}"
|
| 1422 |
-
"LZ[gid]=ZN.indexOf(ca[0]);"
|
| 1423 |
-
"return{x:Math.min(ca[0][0]*ww,ww-sz-10),"
|
| 1424 |
-
"y:Math.min(ca[0][1]*wh,wh-sz-10)};};"
|
| 1425 |
-
|
| 1426 |
-
# Move one GIF
|
| 1427 |
-
"par._sme.mv=function(gid){"
|
| 1428 |
-
"if(par._sme.dragging)return;"
|
| 1429 |
-
"var el=pd.getElementById(gid);"
|
| 1430 |
-
"if(!el||el.style.display==='none')return;"
|
| 1431 |
-
"var p=par._sme.rz(gid,par._sme.occ(gid));"
|
| 1432 |
-
"el.style.left=p.x+'px';el.style.top=p.y+'px';"
|
| 1433 |
-
"el.style.right='auto';el.style.bottom='auto';};"
|
| 1434 |
-
|
| 1435 |
-
# Play audio
|
| 1436 |
-
"par._sme.pa=function(id){"
|
| 1437 |
-
"var a=pd.getElementById(id);"
|
| 1438 |
-
"if(!a)return;"
|
| 1439 |
-
"a.volume=par._sme.vol;"
|
| 1440 |
-
"var p=a.play();"
|
| 1441 |
-
"if(p&&p.catch){p.catch(function(){"
|
| 1442 |
-
"par._sme.pendingAud=id;});}};"
|
| 1443 |
-
|
| 1444 |
-
# Stop all
|
| 1445 |
-
"par._sme.stop=function(){"
|
| 1446 |
-
"GA.forEach(function(id){"
|
| 1447 |
-
"var el=pd.getElementById(id);if(el)el.style.display='none';});"
|
| 1448 |
-
"['sme-aud-shap','sme-aud-wi'].forEach(function(id){"
|
| 1449 |
-
"var a=pd.getElementById(id);"
|
| 1450 |
-
"if(a){a.pause();a.currentTime=0;}});"
|
| 1451 |
-
"par._sme.pendingAud=null;};"
|
| 1452 |
-
|
| 1453 |
-
# Show SHAP (tab 0)
|
| 1454 |
-
"par._sme.showS=function(){"
|
| 1455 |
-
"par._sme.stop();"
|
| 1456 |
-
"var el=pd.getElementById('sme-gif-shap');if(!el)return;"
|
| 1457 |
-
"var p=par._sme.rz('sme-gif-shap',[]);"
|
| 1458 |
-
"el.style.left=p.x+'px';el.style.top=p.y+'px';"
|
| 1459 |
-
"el.style.right='auto';el.style.bottom='auto';"
|
| 1460 |
-
"el.style.display='block';"
|
| 1461 |
-
"par._sme.pa('sme-aud-shap');};"
|
| 1462 |
-
|
| 1463 |
-
# Show What-If dancers (tab 3)
|
| 1464 |
-
"par._sme.showW=function(){"
|
| 1465 |
-
"par._sme.stop();"
|
| 1466 |
-
"var ww=par.innerWidth,wh=par.innerHeight,sz=110;"
|
| 1467 |
-
"var sp=[0.03,0.22,0.41,0.60,0.79];"
|
| 1468 |
-
"GW.forEach(function(gid,i){"
|
| 1469 |
-
"var el=pd.getElementById(gid);if(!el)return;"
|
| 1470 |
-
"el.style.left=Math.min(sp[i]*ww,ww-sz-10)+'px';"
|
| 1471 |
-
"el.style.top=Math.min(0.68*wh,wh-sz-10)+'px';"
|
| 1472 |
-
"el.style.right='auto';el.style.bottom='auto';"
|
| 1473 |
-
"el.style.display='block';"
|
| 1474 |
-
"LZ[gid]=-1;});"
|
| 1475 |
-
"par._sme.pa('sme-aud-wi');};"
|
| 1476 |
-
|
| 1477 |
-
# Apply tab state
|
| 1478 |
-
"par._sme.apT=function(idx){"
|
| 1479 |
-
"if(idx===par._sme.ct)return;"
|
| 1480 |
-
"par._sme.ct=idx;"
|
| 1481 |
-
"if(idx===0)par._sme.showS();"
|
| 1482 |
-
"else if(idx===3)par._sme.showW();"
|
| 1483 |
-
"else par._sme.stop();};"
|
| 1484 |
-
|
| 1485 |
-
# Get current active tab index
|
| 1486 |
-
"par._sme.tidx=function(){"
|
| 1487 |
-
"var all=pd.querySelectorAll('[data-baseweb=\"tab\"]');"
|
| 1488 |
-
"if(!all||!all.length)all=pd.querySelectorAll('[role=\"tab\"]');"
|
| 1489 |
-
"if(!all||!all.length)return -1;"
|
| 1490 |
-
"for(var i=0;i<all.length;i++){"
|
| 1491 |
-
"if(all[i].getAttribute('aria-selected')==='true')return i;}"
|
| 1492 |
-
"return 0;};"
|
| 1493 |
-
|
| 1494 |
-
# DRAG support
|
| 1495 |
-
"par._sme.initDrag=function(el){"
|
| 1496 |
-
"if(el.dataset.drag)return;"
|
| 1497 |
-
"el.dataset.drag='1';"
|
| 1498 |
-
"var ox,oy,sx,sy;"
|
| 1499 |
-
"el.addEventListener('mousedown',function(e){"
|
| 1500 |
-
"e.preventDefault();"
|
| 1501 |
-
"par._sme.dragging=true;"
|
| 1502 |
-
"el.style.transition='none';"
|
| 1503 |
-
"el.style.cursor='grabbing';"
|
| 1504 |
-
"sx=e.clientX;sy=e.clientY;"
|
| 1505 |
-
"ox=el.offsetLeft;oy=el.offsetTop;"
|
| 1506 |
-
"pd.addEventListener('mousemove',mmv);"
|
| 1507 |
-
"pd.addEventListener('mouseup',mup);});"
|
| 1508 |
-
"function mmv(e){"
|
| 1509 |
-
"var dx=e.clientX-sx,dy=e.clientY-sy;"
|
| 1510 |
-
"el.style.left=(ox+dx)+'px';el.style.top=(oy+dy)+'px';"
|
| 1511 |
-
"el.style.right='auto';el.style.bottom='auto';}"
|
| 1512 |
-
"function mup(){"
|
| 1513 |
-
"par._sme.dragging=false;"
|
| 1514 |
-
"el.style.cursor='grab';"
|
| 1515 |
-
"el.style.transition='left 1.1s cubic-bezier(.4,0,.2,1),"
|
| 1516 |
-
"top 1.1s cubic-bezier(.4,0,.2,1)';"
|
| 1517 |
-
"pd.removeEventListener('mousemove',mmv);"
|
| 1518 |
-
"pd.removeEventListener('mouseup',mup);"
|
| 1519 |
-
"if(par._sme.pendingAud){par._sme.pa(par._sme.pendingAud);par._sme.pendingAud=null;}}"
|
| 1520 |
-
"};"
|
| 1521 |
-
|
| 1522 |
-
# Init drag on all GIFs
|
| 1523 |
-
"GA.forEach(function(id){"
|
| 1524 |
-
"var el=pd.getElementById(id);if(el)par._sme.initDrag(el);});"
|
| 1525 |
-
|
| 1526 |
-
# Audio unlock on first click
|
| 1527 |
-
"if(!par._sme.audioUnlocked){"
|
| 1528 |
-
"par._sme.audioUnlocked=false;"
|
| 1529 |
-
"pd.addEventListener('click',function _unlock(){"
|
| 1530 |
-
"par._sme.audioUnlocked=true;"
|
| 1531 |
-
"pd.removeEventListener('click',_unlock);"
|
| 1532 |
-
"if(par._sme.pendingAud){"
|
| 1533 |
-
"par._sme.pa(par._sme.pendingAud);"
|
| 1534 |
-
"par._sme.pendingAud=null;}},true);}"
|
| 1535 |
-
|
| 1536 |
-
# Roam every 6 seconds
|
| 1537 |
-
"if(par._sme.rt)par.clearInterval(par._sme.rt);"
|
| 1538 |
-
"par._sme.rt=par.setInterval(function(){"
|
| 1539 |
-
"GA.forEach(par._sme.mv);"
|
| 1540 |
-
"},6000);"
|
| 1541 |
-
|
| 1542 |
-
"}" # END first-time init
|
| 1543 |
-
|
| 1544 |
-
# EVERY RENDER: update volume + re-attach tab clicks
|
| 1545 |
-
"par._sme.vol=VOL;"
|
| 1546 |
-
"['sme-aud-shap','sme-aud-wi'].forEach(function(id){"
|
| 1547 |
-
"var a=pd.getElementById(id);if(a)a.volume=VOL;});"
|
| 1548 |
-
|
| 1549 |
-
# Re-attach drag to any new GIF elements
|
| 1550 |
-
"var GA2=['sme-gif-shap','sme-gif-wi1','sme-gif-wi2','sme-gif-wi3','sme-gif-wi4','sme-gif-wi5'];"
|
| 1551 |
-
"GA2.forEach(function(id){"
|
| 1552 |
-
"var el=pd.getElementById(id);"
|
| 1553 |
-
"if(el&&par._sme.initDrag)par._sme.initDrag(el);});"
|
| 1554 |
-
|
| 1555 |
-
# Re-attach tab click listeners
|
| 1556 |
-
"function addC(){"
|
| 1557 |
-
"var tabs=pd.querySelectorAll('[data-baseweb=\"tab\"]');"
|
| 1558 |
-
"if(!tabs||tabs.length<2)tabs=pd.querySelectorAll('[role=\"tab\"]');"
|
| 1559 |
-
"if(!tabs||tabs.length<2)return false;"
|
| 1560 |
-
"tabs.forEach(function(tab,i){"
|
| 1561 |
-
"if(par._sme.att.has(tab))return;"
|
| 1562 |
-
"par._sme.att.add(tab);"
|
| 1563 |
-
"tab.addEventListener('click',function(){"
|
| 1564 |
-
"par.setTimeout(function(){par._sme.apT(par._sme.tidx());},150);});});"
|
| 1565 |
-
"return true;}"
|
| 1566 |
-
|
| 1567 |
-
"var tr=0,pl=par.setInterval(function(){"
|
| 1568 |
-
"tr++;"
|
| 1569 |
-
"if(addC()){"
|
| 1570 |
-
"var ci=par._sme.tidx();"
|
| 1571 |
-
"if(ci!==par._sme.ct){par._sme.ct=-1;par._sme.apT(ci);}"
|
| 1572 |
-
"par.clearInterval(pl);"
|
| 1573 |
-
"par.setInterval(function(){addC();},2000);}"
|
| 1574 |
-
"if(tr>80)par.clearInterval(pl);"
|
| 1575 |
-
"},150);"
|
| 1576 |
-
|
| 1577 |
-
"})();"
|
| 1578 |
-
)
|
| 1579 |
-
|
| 1580 |
-
html = '<!DOCTYPE html><html><head></head><body><script>' + js + '</script></body></html>'
|
| 1581 |
-
components.html(html, height=1, scrolling=False)
|
| 1582 |
-
|
| 1583 |
-
|
| 1584 |
-
# ============================================================
|
| 1585 |
-
# HEADER
|
| 1586 |
-
# ============================================================
|
| 1587 |
-
st.markdown(
|
| 1588 |
-
'<div class="header-wrap">'
|
| 1589 |
-
'<h1>' + T("header_title", lang) + '</h1>'
|
| 1590 |
-
'<p>' + T("header_sub", lang) + '</p>'
|
| 1591 |
-
'<span class="badge b-blue">XGBoost + LightGBM + RF</span>'
|
| 1592 |
-
'<span class="badge b-green">SHAP XAI</span>'
|
| 1593 |
-
'<span class="badge b-yellow">Maya AI Chat</span>'
|
| 1594 |
-
'</div>',
|
| 1595 |
-
unsafe_allow_html=True
|
| 1596 |
-
)
|
| 1597 |
-
|
| 1598 |
-
# ============================================================
|
| 1599 |
-
# FORM
|
| 1600 |
-
# ============================================================
|
| 1601 |
-
with st.form("credit_form"):
|
| 1602 |
-
st.markdown(T('form_title', lang))
|
| 1603 |
-
col_a, col_b, col_c = st.columns(3)
|
| 1604 |
-
|
| 1605 |
-
with col_a:
|
| 1606 |
-
st.markdown(T('form_loan', lang))
|
| 1607 |
-
loan_rp = st.number_input(T('f_loan_amt',lang), min_value=1_000_000, max_value=5_000_000_000,
|
| 1608 |
-
value=50_000_000, step=1_000_000)
|
| 1609 |
-
lgd_input = st.slider(T('f_lgd',lang), 0.0, 1.0, 0.40, 0.05)
|
| 1610 |
-
duration = st.slider(T('f_duration',lang), 4, 72, 24)
|
| 1611 |
-
credit_amount = st.number_input(T('f_credit_amt',lang), min_value=100, max_value=20000,
|
| 1612 |
-
value=1500, step=100)
|
| 1613 |
-
purpose_sel = st.selectbox(
|
| 1614 |
-
T('f_purpose',lang),
|
| 1615 |
-
PURPOSE_OPTS,
|
| 1616 |
-
format_func=lambda x: PURPOSE_LABELS[x][lang]
|
| 1617 |
-
)
|
| 1618 |
-
age = st.slider(T('f_age',lang), 18, 75, 35)
|
| 1619 |
-
|
| 1620 |
-
with col_b:
|
| 1621 |
-
st.markdown(T('form_financial', lang))
|
| 1622 |
-
checking_sel = st.selectbox(
|
| 1623 |
-
T('f_checking',lang),
|
| 1624 |
-
CHECKING_OPTS,
|
| 1625 |
-
format_func=lambda x: CHECKING_LABELS[x][lang]
|
| 1626 |
-
)
|
| 1627 |
-
savings_sel = st.selectbox(
|
| 1628 |
-
T('f_savings',lang),
|
| 1629 |
-
SAVINGS_OPTS,
|
| 1630 |
-
format_func=lambda x: SAVINGS_LABELS[x][lang]
|
| 1631 |
-
)
|
| 1632 |
-
credit_hist_sel = st.selectbox(
|
| 1633 |
-
T('f_credit_hist',lang),
|
| 1634 |
-
CREDIT_H_OPTS,
|
| 1635 |
-
format_func=lambda x: CREDIT_H_LABELS[x][lang]
|
| 1636 |
-
)
|
| 1637 |
-
employment_sel = st.selectbox(
|
| 1638 |
-
T('f_employment',lang),
|
| 1639 |
-
EMPLOY_OPTS,
|
| 1640 |
-
format_func=lambda x: EMPLOY_LABELS[x][lang]
|
| 1641 |
-
)
|
| 1642 |
-
housing_sel = st.selectbox(
|
| 1643 |
-
T('f_housing',lang),
|
| 1644 |
-
HOUSING_OPTS,
|
| 1645 |
-
format_func=lambda x: HOUSING_LABELS[x][lang]
|
| 1646 |
-
)
|
| 1647 |
-
installment_rate = st.slider(T('f_installment',lang), 1, 4, 2)
|
| 1648 |
-
|
| 1649 |
-
with col_c:
|
| 1650 |
-
st.markdown(T('form_sme', lang))
|
| 1651 |
-
digital_score = st.slider(T('f_digital',lang), 1, 100, 50)
|
| 1652 |
-
has_social = st.checkbox(T('f_social',lang), value=True)
|
| 1653 |
-
ecomm_volume = st.number_input(T('f_ecomm',lang), min_value=0, value=5_000_000, step=500_000)
|
| 1654 |
-
has_npwp = st.checkbox(T('f_npwp',lang), value=True)
|
| 1655 |
-
has_siup = st.checkbox(T('f_siup',lang), value=True)
|
| 1656 |
-
biz_age = st.slider(T('f_biz_age',lang), 1, 20, 5)
|
| 1657 |
-
cash_flow = st.number_input(T('f_cashflow',lang), min_value=0, value=15_000_000, step=500_000)
|
| 1658 |
-
num_employees = st.slider(T('f_employees',lang), 1, 50, 5)
|
| 1659 |
-
|
| 1660 |
-
submitted = st.form_submit_button(T('f_submit', lang), use_container_width=True)
|
| 1661 |
-
|
| 1662 |
-
# ============================================================
|
| 1663 |
-
# PROCESS FORM SUBMISSION
|
| 1664 |
-
# ============================================================
|
| 1665 |
-
if submitted:
|
| 1666 |
-
raw = {
|
| 1667 |
-
'duration': duration,
|
| 1668 |
-
'credit_amount': credit_amount,
|
| 1669 |
-
'installment_commitment': installment_rate,
|
| 1670 |
-
'age': age,
|
| 1671 |
-
'checking_status': checking_sel,
|
| 1672 |
-
'credit_history': credit_hist_sel,
|
| 1673 |
-
'purpose': purpose_sel,
|
| 1674 |
-
'savings_status': savings_sel,
|
| 1675 |
-
'employment': employment_sel,
|
| 1676 |
-
'housing': housing_sel,
|
| 1677 |
-
'digital_presence_score': digital_score,
|
| 1678 |
-
'has_social_media': int(has_social),
|
| 1679 |
-
'ecommerce_volume': ecomm_volume,
|
| 1680 |
-
'has_npwp': int(has_npwp),
|
| 1681 |
-
'has_siup': int(has_siup),
|
| 1682 |
-
'business_age_years': float(biz_age),
|
| 1683 |
-
'monthly_cash_flow': float(cash_flow),
|
| 1684 |
-
'num_employees': num_employees,
|
| 1685 |
-
'loan_rp': float(loan_rp),
|
| 1686 |
-
}
|
| 1687 |
-
with st.spinner(T('spinner', lang)):
|
| 1688 |
-
X_proc = preprocess(raw, scaler, feature_names)
|
| 1689 |
-
pd_score = float(ensemble.predict_proba(X_proc)[0][1])
|
| 1690 |
-
result = risk_result(pd_score, loan_rp, lgd_input)
|
| 1691 |
-
|
| 1692 |
-
try:
|
| 1693 |
-
sv = explainer.shap_values(X_proc)
|
| 1694 |
-
sv = sv[1] if isinstance(sv, list) else sv
|
| 1695 |
-
sv = np.array(sv).flatten()
|
| 1696 |
-
except Exception:
|
| 1697 |
-
sv = np.zeros(len(feature_names))
|
| 1698 |
-
|
| 1699 |
-
narrative, llm_src = get_narrative(sv, feature_names, result, lang, raw)
|
| 1700 |
-
|
| 1701 |
-
st.session_state.result = result
|
| 1702 |
-
st.session_state.shap_vals = sv
|
| 1703 |
-
st.session_state.raw_input = raw
|
| 1704 |
-
st.session_state.narrative = narrative
|
| 1705 |
-
st.session_state.llm_src = llm_src
|
| 1706 |
-
st.session_state.narrative_lang = lang
|
| 1707 |
-
st.session_state.shap_png = make_shap_png(sv, feature_names)
|
| 1708 |
-
# Reset What-If sliders to new values
|
| 1709 |
-
st.session_state.wi_dig = digital_score
|
| 1710 |
-
st.session_state.wi_biz = biz_age
|
| 1711 |
-
st.session_state.wi_emp = num_employees
|
| 1712 |
-
st.session_state.wi_cash = int(cash_flow // 1e6)
|
| 1713 |
-
st.session_state.wi_dur = duration
|
| 1714 |
-
st.session_state.wi_loan = int(loan_rp // 1e6)
|
| 1715 |
-
|
| 1716 |
-
# ============================================================
|
| 1717 |
-
# Pull session state โ local vars (used by tabs below)
|
| 1718 |
-
# ============================================================
|
| 1719 |
-
result = st.session_state.get('result')
|
| 1720 |
-
shap_vals = st.session_state.get('shap_vals', np.zeros(len(feature_names) if feature_names else 1))
|
| 1721 |
-
raw_input = st.session_state.get('raw_input', {})
|
| 1722 |
-
narrative = st.session_state.get('narrative', '')
|
| 1723 |
-
llm_src = st.session_state.get('llm_src', '')
|
| 1724 |
-
|
| 1725 |
-
# ============================================================
|
| 1726 |
-
# KPI BANNER (only shown after submission)
|
| 1727 |
-
# ============================================================
|
| 1728 |
-
if result:
|
| 1729 |
-
st.markdown(
|
| 1730 |
-
'<div class="risk-banner ' + result['css'] + '">'
|
| 1731 |
-
+ result['cat'][lang] + '</div>',
|
| 1732 |
-
unsafe_allow_html=True
|
| 1733 |
-
)
|
| 1734 |
-
k1, k2, k3, k4 = st.columns(4)
|
| 1735 |
-
with k1:
|
| 1736 |
-
st.markdown(
|
| 1737 |
-
'<div class="kpi-box">'
|
| 1738 |
-
'<div class="kpi-lbl">' + T('kpi_pd', lang) + '</div>'
|
| 1739 |
-
'<div class="kpi-val">' + str(round(result['pd']*100,1)) + '%</div>'
|
| 1740 |
-
'<div class="kpi-sub">' + T('kpi_pd_sub', lang) + '</div>'
|
| 1741 |
-
'</div>', unsafe_allow_html=True
|
| 1742 |
-
)
|
| 1743 |
-
with k2:
|
| 1744 |
-
st.markdown(
|
| 1745 |
-
'<div class="kpi-box">'
|
| 1746 |
-
'<div class="kpi-lbl">' + T('kpi_el', lang) + '</div>'
|
| 1747 |
-
'<div class="kpi-val">Rp ' + str(round(result['el']/1e6,1)) + 'jt</div>'
|
| 1748 |
-
'<div class="kpi-sub">' + T('kpi_el_sub', lang) + '</div>'
|
| 1749 |
-
'</div>', unsafe_allow_html=True
|
| 1750 |
-
)
|
| 1751 |
-
with k3:
|
| 1752 |
-
st.markdown(
|
| 1753 |
-
'<div class="kpi-box">'
|
| 1754 |
-
'<div class="kpi-lbl">' + T('kpi_lgd', lang) + '</div>'
|
| 1755 |
-
'<div class="kpi-val">' + str(int(result['lgd']*100)) + '%</div>'
|
| 1756 |
-
'<div class="kpi-sub">' + T('kpi_lgd_sub', lang) + '</div>'
|
| 1757 |
-
'</div>', unsafe_allow_html=True
|
| 1758 |
-
)
|
| 1759 |
-
with k4:
|
| 1760 |
-
st.markdown(
|
| 1761 |
-
'<div class="kpi-box">'
|
| 1762 |
-
'<div class="kpi-lbl">' + T('kpi_ead', lang) + '</div>'
|
| 1763 |
-
'<div class="kpi-val">Rp ' + str(int(result['ead']/1e6)) + 'jt</div>'
|
| 1764 |
-
'<div class="kpi-sub">' + T('kpi_ead_sub', lang) + '</div>'
|
| 1765 |
-
'</div>', unsafe_allow_html=True
|
| 1766 |
-
)
|
| 1767 |
-
st.markdown("<br>", unsafe_allow_html=True)
|
| 1768 |
-
|
| 1769 |
# ============================================================
|
| 1770 |
# TAB LOADING PROGRESS
|
| 1771 |
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1772 |
_tab_load_steps = {
|
| 1773 |
'id': ['๐ Menyiapkan SHAP...', '๐ฌ Menyiapkan Narasi AI...', '๐ค Menyiapkan Chat...',
|
| 1774 |
'๐ฎ Menyiapkan What-If...', '๐ Menyiapkan Formula...', 'โ
Semua tab siap!'],
|
|
@@ -1780,13 +1424,6 @@ _tab_load_steps = {
|
|
| 1780 |
_tls = _tab_load_steps.get(lang, _tab_load_steps['id'])
|
| 1781 |
_tab_prog_ph = st.empty()
|
| 1782 |
|
| 1783 |
-
def _tab_step(n):
|
| 1784 |
-
try:
|
| 1785 |
-
pct = int(n / 5 * 100)
|
| 1786 |
-
_tab_prog_ph.progress(pct, text=_tls[n])
|
| 1787 |
-
except Exception:
|
| 1788 |
-
pass
|
| 1789 |
-
|
| 1790 |
_tab_step(0)
|
| 1791 |
|
| 1792 |
# ============================================================
|
|
@@ -1801,64 +1438,54 @@ t1, t2, t3, t4, t5 = st.tabs([
|
|
| 1801 |
with t1:
|
| 1802 |
_tab_step(0)
|
| 1803 |
st.markdown('<div class="sec-title">' + T("shap_title",lang) + '</div>', unsafe_allow_html=True)
|
| 1804 |
-
|
| 1805 |
-
if
|
| 1806 |
-
st.
|
| 1807 |
-
|
| 1808 |
-
|
|
|
|
|
|
|
|
|
|
| 1809 |
else:
|
| 1810 |
-
|
| 1811 |
-
|
| 1812 |
-
|
| 1813 |
-
"
|
| 1814 |
-
|
| 1815 |
-
|
| 1816 |
-
|
| 1817 |
-
|
| 1818 |
-
|
| 1819 |
-
|
| 1820 |
-
|
| 1821 |
-
|
| 1822 |
-
|
| 1823 |
-
|
| 1824 |
-
|
| 1825 |
-
|
| 1826 |
-
T('shap_col_feature',lang): [feature_names[i] for i in top_idx],
|
| 1827 |
-
'SHAP': [round(shap_vals[i],4) for i in top_idx],
|
| 1828 |
-
T('shap_col_impact',lang): [
|
| 1829 |
-
T('shap_risk_up',lang) if shap_vals[i] > 0 else T('shap_risk_down',lang)
|
| 1830 |
-
for i in top_idx
|
| 1831 |
-
],
|
| 1832 |
-
}), use_container_width=True, hide_index=True)
|
| 1833 |
|
| 1834 |
# โโ TAB 2: NARRATIVE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1835 |
with t2:
|
| 1836 |
_tab_step(1)
|
| 1837 |
flag = {'id':'๐ฎ๐ฉ','en':'๐ฌ๐ง','hi':'๐ฎ๐ณ'}[lang]
|
| 1838 |
st.markdown('<div class="sec-title">' + T("narr_title",lang) + ' ' + flag + '</div>', unsafe_allow_html=True)
|
| 1839 |
-
|
| 1840 |
-
if
|
| 1841 |
-
|
| 1842 |
-
|
| 1843 |
-
'hi':'AI vivarana dekhne ke liye upar form submit karein.'}[lang])
|
| 1844 |
else:
|
| 1845 |
-
|
| 1846 |
-
|
| 1847 |
-
|
| 1848 |
-
|
| 1849 |
-
|
| 1850 |
-
narrative_fb, src_fb = get_narrative(shap_vals, feature_names, result, lang, raw_input)
|
| 1851 |
-
st.session_state.narrative = narrative_fb
|
| 1852 |
-
st.session_state.llm_src = src_fb
|
| 1853 |
-
fmt = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', narrative_fb.replace('\n','<br>'))
|
| 1854 |
-
st.markdown('<div class="narrative-box">' + fmt + '</div>', unsafe_allow_html=True)
|
| 1855 |
_or_key = st.session_state.get("openrouter_key","")
|
| 1856 |
_grq_key = st.session_state.get("groq_key","")
|
| 1857 |
-
with st.expander("
|
| 1858 |
st.code(
|
| 1859 |
"OR key : " + ('OK ' + _or_key[:12] + '...' if _or_key else 'missing') + "\n"
|
| 1860 |
"Groq key : " + ('OK ' + _grq_key[:12] + '...' if _grq_key else 'missing') + "\n"
|
| 1861 |
-
"Source : " + str(
|
| 1862 |
"OR model : " + _OR_FREE_MODELS[0] + "\n"
|
| 1863 |
"Groq mdl : " + _GROQ_FREE_MODELS[0]
|
| 1864 |
)
|
|
@@ -1868,85 +1495,86 @@ with t3:
|
|
| 1868 |
_tab_step(2)
|
| 1869 |
st.markdown('<div class="sec-title">' + T("chat_title",lang) + '</div>', unsafe_allow_html=True)
|
| 1870 |
|
| 1871 |
-
if
|
| 1872 |
-
|
| 1873 |
-
|
| 1874 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1875 |
else:
|
| 1876 |
-
|
| 1877 |
-
|
| 1878 |
-
|
| 1879 |
-
|
| 1880 |
-
|
| 1881 |
-
|
| 1882 |
-
|
| 1883 |
-
|
| 1884 |
-
|
| 1885 |
-
|
| 1886 |
-
|
| 1887 |
-
|
| 1888 |
-
|
| 1889 |
-
|
| 1890 |
-
|
| 1891 |
-
|
| 1892 |
-
|
| 1893 |
-
|
| 1894 |
-
|
| 1895 |
-
|
| 1896 |
-
|
| 1897 |
-
|
| 1898 |
-
|
| 1899 |
-
|
| 1900 |
-
|
| 1901 |
-
|
| 1902 |
-
|
| 1903 |
-
|
| 1904 |
-
|
| 1905 |
-
|
| 1906 |
-
|
| 1907 |
-
|
| 1908 |
-
|
| 1909 |
-
|
| 1910 |
-
|
| 1911 |
-
|
| 1912 |
-
|
| 1913 |
-
|
| 1914 |
-
|
| 1915 |
-
|
| 1916 |
-
|
| 1917 |
-
ai_resp, adjustments, _debug_err = get_chat_response(
|
| 1918 |
-
to_process, st.session_state.chat_history,
|
| 1919 |
-
result, raw_input, shap_vals, feature_names, lang,
|
| 1920 |
-
rag_index=rag_index
|
| 1921 |
-
)
|
| 1922 |
-
_chat_status_ph.empty()
|
| 1923 |
-
if _debug_err and any([st.session_state.get("openrouter_key"), st.session_state.get("groq_key")]):
|
| 1924 |
-
st.warning("Semua LLM gagal, menggunakan Smart Fallback. Error: " + str(_debug_err)[:100])
|
| 1925 |
-
st.session_state.chat_history.append({'role':'assistant','content':ai_resp})
|
| 1926 |
-
_save_chat_memory(st.session_state.chat_history, st.session_state.get('chat_summary',''))
|
| 1927 |
-
adj_map = {
|
| 1928 |
-
'digital_presence_score': 'wi_dig', 'business_age_years': 'wi_biz',
|
| 1929 |
-
'num_employees': 'wi_emp', 'monthly_cash_flow': 'wi_cash',
|
| 1930 |
-
'duration': 'wi_dur', 'loan_rp': 'wi_loan',
|
| 1931 |
-
}
|
| 1932 |
-
if adjustments:
|
| 1933 |
-
for field, val in adjustments.items():
|
| 1934 |
-
if field in adj_map:
|
| 1935 |
-
st.session_state[adj_map[field]] = val
|
| 1936 |
-
st.success("๐ก " + T('chat_updated', lang))
|
| 1937 |
-
st.rerun()
|
| 1938 |
|
| 1939 |
-
|
| 1940 |
-
|
| 1941 |
-
|
| 1942 |
-
|
| 1943 |
-
|
| 1944 |
-
|
| 1945 |
|
| 1946 |
# โโ TAB 4: WHAT-IF โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1947 |
with t4:
|
| 1948 |
_tab_step(3)
|
| 1949 |
-
|
| 1950 |
@st.fragment
|
| 1951 |
def render_whatif():
|
| 1952 |
_lang = st.session_state.get("lang_sel","id")
|
|
@@ -1985,11 +1613,11 @@ with t4:
|
|
| 1985 |
'monthly_cash_flow': float(wi_cash * 1e6),
|
| 1986 |
'num_employees': wi_emp,
|
| 1987 |
}
|
| 1988 |
-
X_wi
|
| 1989 |
-
wi_pd
|
| 1990 |
wi_res = risk_result(wi_pd, wi_loan * 1e6, result_f['lgd'])
|
| 1991 |
-
d_pd
|
| 1992 |
-
d_el
|
| 1993 |
_log_api("What-If", "Local Model", True, 0)
|
| 1994 |
|
| 1995 |
r1, r2, r3 = st.columns(3)
|
|
@@ -2051,15 +1679,25 @@ with t4:
|
|
| 2051 |
with st.expander(T('wi_tips_title', _lang)):
|
| 2052 |
tips = []
|
| 2053 |
if wi_dig < 70:
|
| 2054 |
-
tips.append(_t("๐ฑ Naikkan Digital Score ke 70+ โ marketplace & Google Business",
|
|
|
|
|
|
|
| 2055 |
if wi_cash < 20:
|
| 2056 |
-
tips.append(_t("๐ต Target cash flow Rp 20jt+/bulan",
|
|
|
|
|
|
|
| 2057 |
if wi_biz < 3:
|
| 2058 |
-
tips.append(_t("๐ข Bisnis < 3 thn lebih berisiko โ bangun track record",
|
|
|
|
|
|
|
| 2059 |
if wi_emp < 5:
|
| 2060 |
-
tips.append(_t("๐ฅ Tambah karyawan = skala bisnis lebih sehat",
|
|
|
|
|
|
|
| 2061 |
if not tips:
|
| 2062 |
-
tips.append(_t("๐ Profil sudah optimal!",
|
|
|
|
|
|
|
| 2063 |
for tip in tips:
|
| 2064 |
st.markdown(tip)
|
| 2065 |
|
|
@@ -2072,70 +1710,53 @@ with t4:
|
|
| 2072 |
with t5:
|
| 2073 |
_tab_step(4)
|
| 2074 |
st.markdown('<div class="sec-title">' + T("form_formula_title",lang) + '</div>', unsafe_allow_html=True)
|
| 2075 |
-
|
| 2076 |
-
|
| 2077 |
-
|
| 2078 |
-
|
| 2079 |
-
|
| 2080 |
-
|
| 2081 |
-
|
| 2082 |
-
|
| 2083 |
-
|
| 2084 |
-
|
| 2085 |
-
|
|
|
|
|
|
|
|
|
|
| 2086 |
)
|
| 2087 |
-
|
| 2088 |
-
|
| 2089 |
-
|
| 2090 |
-
|
| 2091 |
-
|
| 2092 |
-
|
| 2093 |
-
|
| 2094 |
-
|
| 2095 |
-
|
| 2096 |
-
st.markdown(
|
| 2097 |
-
"| " + T('formula_komponen',lang) + " | " + T('formula_nilai',lang) + " |\n"
|
| 2098 |
-
"|--|--|\n"
|
| 2099 |
-
"| PD | " + str(round(result['pd']*100,1)) + "% (" + cat_lbl + ") |\n"
|
| 2100 |
-
"| LGD | " + str(int(result['lgd']*100)) + "% |\n"
|
| 2101 |
-
"| EAD | Rp " + str(int(result['ead']/1e6)) + "jt |\n"
|
| 2102 |
-
"| **EL** | **Rp " + str(round(result['el']/1e6,2)) + "jt** |"
|
| 2103 |
-
)
|
| 2104 |
-
_log_api("Formula", "Local", True, 0)
|
| 2105 |
|
| 2106 |
# ============================================================
|
| 2107 |
-
#
|
| 2108 |
-
# โ FIX: inject_media_manager() is NOW CALLED HERE
|
| 2109 |
-
# (was defined but never called in original code)
|
| 2110 |
-
# Called every render so mute state syncs correctly
|
| 2111 |
# ============================================================
|
| 2112 |
_tab_step(5)
|
| 2113 |
_tab_prog_ph.empty()
|
| 2114 |
-
|
| 2115 |
-
|
| 2116 |
-
|
| 2117 |
-
|
| 2118 |
-
|
| 2119 |
-
|
| 2120 |
-
|
| 2121 |
-
|
| 2122 |
-
|
| 2123 |
-
|
| 2124 |
-
|
| 2125 |
-
|
| 2126 |
-
|
| 2127 |
-
|
| 2128 |
-
|
| 2129 |
-
|
| 2130 |
-
|
| 2131 |
-
|
| 2132 |
-
|
| 2133 |
-
|
| 2134 |
-
"DISCLAIMER: Educational use only. Not financial advice."
|
| 2135 |
-
)
|
| 2136 |
-
st.download_button(
|
| 2137 |
-
T('download_btn', lang),
|
| 2138 |
-
data=report_txt,
|
| 2139 |
-
file_name=T('download_file', lang),
|
| 2140 |
-
mime="text/plain"
|
| 2141 |
-
)
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
+
# ๐ฆ SME Credit Risk Assessment โ FINAL
|
| 3 |
# Final Project | AI Engineering Bootcamp Batch 10
|
| 4 |
# Author: 1na37
|
| 5 |
# ============================================================
|
| 6 |
+
# Basis: OLDER untuk Maya AI logic, CoT, persona
|
| 7 |
+
# TRANSLATION untuk Hindi Latin transliteration
|
| 8 |
+
# NEWER untuk bug fixes: API Tracker
|
| 9 |
+
# FIX: _set_status ternary โ if/else (Python 3.13 compat)
|
| 10 |
+
# FIX: Sidebar reorder sesuai permintaan
|
|
|
|
|
|
|
| 11 |
# ============================================================
|
| 12 |
|
| 13 |
import streamlit as st
|
|
|
|
| 30 |
)
|
| 31 |
|
| 32 |
# ============================================================
|
| 33 |
+
# TRANSLATION SYSTEM โ Latin Hindi (Hinglish transliteration)
|
| 34 |
# ============================================================
|
| 35 |
TRANSLATIONS = {
|
| 36 |
'sidebar_api': {'id':'๐ API Keys (opsional)', 'en':'๐ API Keys (optional)', 'hi':'๐ API Keys (vaikalpik)'},
|
|
|
|
| 129 |
'wi_pd_from': {'id':'dari', 'en':'from', 'hi':'se'},
|
| 130 |
'wi_no_change': {'id':'Perubahan minimal pada skor risiko','en':'Minimal change in risk score','hi':'Jokhim score mein nyoonatam badlaav'},
|
| 131 |
'wi_tips_title': {'id':'๐ก Tips Optimasi Skor', 'en':'๐ก Score Optimization Tips', 'hi':'๐ก Score Sudhaar Tips'},
|
| 132 |
+
'wi_form_first': {'id':'Submit form terlebih dahulu', 'en':'Submit the form first', 'hi':'Pehle form submit karein'},
|
|
|
|
|
|
|
| 133 |
'wi_approved': {'id':'LAYAK', 'en':'APPROVED', 'hi':'SWIKAARY'},
|
| 134 |
'wi_review': {'id':'REVIEW', 'en':'REVIEW', 'hi':'SAMEEKSHA'},
|
| 135 |
'wi_highrisk': {'id':'RISIKO TINGGI', 'en':'HIGH RISK', 'hi':'UCHCH JOKHIM'},
|
|
|
|
| 153 |
'risk_approved': {'id':'๐ข LAYAK', 'en':'๐ข APPROVED', 'hi':'๐ข YOGYA'},
|
| 154 |
'risk_review': {'id':'๐ก PERLU REVIEW', 'en':'๐ก REVIEW', 'hi':'๐ก SAMEEKSHA'},
|
| 155 |
'risk_high': {'id':'๐ด BERISIKO TINGGI', 'en':'๐ด HIGH RISK', 'hi':'๐ด UCHCH JOKHIM'},
|
| 156 |
+
'empty_ensemble': {'id':'Ensemble', 'en':'Ensemble', 'hi':'Ensemble'},
|
| 157 |
+
'empty_xai': {'id':'XAI', 'en':'XAI', 'hi':'XAI'},
|
| 158 |
+
'empty_narrative': {'id':'Narasi', 'en':'Narrative', 'hi':'Vivarana'},
|
| 159 |
+
'empty_chat': {'id':'AI Chat', 'en':'AI Chat', 'hi':'AI Baatcheet'},
|
| 160 |
}
|
| 161 |
|
| 162 |
def T(key: str, lang: str) -> str:
|
|
|
|
| 164 |
return entry.get(lang, entry.get('en', key))
|
| 165 |
|
| 166 |
# ============================================================
|
| 167 |
+
# SELECTBOX LABELS โ Latin Hindi transliteration
|
| 168 |
# ============================================================
|
| 169 |
CHECKING_OPTS = ['<0', '0<=X<200', '>=200', 'no checking']
|
| 170 |
CHECKING_LABELS = {
|
|
|
|
| 220 |
LANG_LABELS = {'id':'๐ฎ๐ฉ Bahasa Indonesia','en':'๐ฌ๐ง English','hi':'๐ฎ๐ณ Hindi (Roman)'}
|
| 221 |
|
| 222 |
# ============================================================
|
| 223 |
+
# CSS STYLING
|
| 224 |
# ============================================================
|
| 225 |
st.markdown("""
|
| 226 |
<style>
|
|
|
|
| 295 |
st.session_state.api_log = st.session_state.api_log[-20:]
|
| 296 |
|
| 297 |
# ============================================================
|
| 298 |
+
# _set_status โ FIX: if/else instead of ternary
|
| 299 |
# ============================================================
|
| 300 |
def _set_status(msg: str):
|
| 301 |
st.session_state['llm_status'] = msg
|
|
|
|
| 313 |
pass
|
| 314 |
|
| 315 |
# ============================================================
|
| 316 |
+
# FORMAL TOOL CALLING SCHEMA (Agentic AI)
|
| 317 |
# ============================================================
|
| 318 |
MAYA_TOOLS = [
|
| 319 |
{
|
|
|
|
| 342 |
}
|
| 343 |
]
|
| 344 |
|
| 345 |
+
# Tool-capable model subsets
|
| 346 |
_OR_TOOL_MODELS = [
|
| 347 |
"google/gemini-2.0-flash-exp:free",
|
| 348 |
"qwen/qwen3-32b:free",
|
|
|
|
| 357 |
]
|
| 358 |
|
| 359 |
# ============================================================
|
| 360 |
+
# LLM API CALLERS (with status + timing)
|
| 361 |
# ============================================================
|
| 362 |
def call_openrouter_tools(messages, api_key, tools=None):
|
| 363 |
if not api_key:
|
|
|
|
| 453 |
return text, {}
|
| 454 |
adjustments = {}
|
| 455 |
text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()
|
| 456 |
+
|
| 457 |
json_pattern = re.compile(r'\{[^{}]*"field"\s*:\s*"([^"]+)"[^{}]*"value"\s*:\s*([\d.]+)[^{}]*\}', re.DOTALL)
|
| 458 |
valid_fields = {'digital_presence_score','business_age_years','num_employees','monthly_cash_flow','duration','loan_rp'}
|
| 459 |
for m in json_pattern.finditer(text):
|
|
|
|
| 461 |
if field in valid_fields:
|
| 462 |
adjustments[field] = val
|
| 463 |
text = json_pattern.sub('', text).strip()
|
| 464 |
+
|
| 465 |
for m in re.finditer(r'\[ADJUST:\s*(\w+)\s*=\s*([\d.]+)\]', text):
|
| 466 |
field, val = m.group(1), float(m.group(2))
|
| 467 |
if field not in adjustments:
|
| 468 |
adjustments[field] = val
|
| 469 |
text = re.sub(r'\s*\[ADJUST:[^\]]+\]\s*', ' ', text).strip()
|
| 470 |
+
|
| 471 |
_standalone_action = re.compile(
|
| 472 |
r',?\s*(?:coba\s+)?(?:naikkan|tingkatkan|kurangi|optimalkan|pertahankan|daftarkan|pastikan|perbaiki)'
|
| 473 |
r'\s+\S+\s+(?:ke|ke\s+)[\w\s./]+(?:jt|M|juta|bulan|tahun|/bln|/month)?\.?\s*$',
|
| 474 |
re.IGNORECASE
|
| 475 |
)
|
| 476 |
text = _standalone_action.sub('', text).strip()
|
| 477 |
+
|
| 478 |
_standalone_en = re.compile(
|
| 479 |
r',?\s*(?:try\s+)?(?:raise|increase|lower|reduce|optimize|improve|register)\s+'
|
| 480 |
r'\S+\s+(?:to|to\s+)[\w\s./]+(?:M|jt|juta|months?|years?)?\.?\s*$',
|
| 481 |
re.IGNORECASE
|
| 482 |
)
|
| 483 |
text = _standalone_en.sub('', text).strip()
|
| 484 |
+
|
| 485 |
text = re.sub(r' +', ' ', text)
|
| 486 |
text = re.sub(r' ([,.])', r'\1', text)
|
| 487 |
text = re.sub(r'\n\*\*\s*\*\*\s*$', '', text, flags=re.MULTILINE).strip()
|
|
|
|
| 489 |
return text, adjustments
|
| 490 |
|
| 491 |
def _call_chat_llm(messages):
|
| 492 |
+
"""Cascade: OR tools โ Groq tools โ OR no-tools โ Groq no-tools."""
|
| 493 |
_or = st.session_state.get("openrouter_key", "")
|
| 494 |
_grq = st.session_state.get("groq_key", "")
|
| 495 |
if _or:
|
|
|
|
| 591 |
pass
|
| 592 |
return [], ""
|
| 593 |
|
| 594 |
+
# ============================================================
|
| 595 |
+
# MEMORY SUMMARIZATION
|
| 596 |
+
# ============================================================
|
| 597 |
def _summarize_history(history, lang):
|
| 598 |
if len(history) <= 8:
|
| 599 |
return history, st.session_state.get('chat_summary', '')
|
|
|
|
| 620 |
st.session_state.chat_summary = new_summary
|
| 621 |
return recent_turns, new_summary
|
| 622 |
|
| 623 |
+
# Early init
|
| 624 |
+
for _k, _v in [('llm_status', ''), ('api_log', [])]:
|
| 625 |
if _k not in st.session_state:
|
| 626 |
st.session_state[_k] = _v
|
| 627 |
|
| 628 |
# ============================================================
|
| 629 |
+
# SIDEBAR โ reordered:
|
| 630 |
+
# 1. Title
|
| 631 |
+
# 2. Language
|
| 632 |
+
# 3. API key status + live LLM status
|
| 633 |
+
# 4. API Tracker
|
| 634 |
+
# 5. Stack / footer
|
| 635 |
# ============================================================
|
| 636 |
with st.sidebar:
|
| 637 |
st.markdown("## ๐ฆ SME Credit Risk AI")
|
| 638 |
st.markdown("---")
|
| 639 |
|
| 640 |
+
# 1. Language selector
|
| 641 |
if 'lang_sel' not in st.session_state:
|
| 642 |
st.session_state.lang_sel = 'id'
|
| 643 |
lang = st.radio(
|
|
|
|
| 648 |
)
|
| 649 |
st.markdown("---")
|
| 650 |
|
| 651 |
+
# 2. API key status + live LLM status indicator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
openrouter_key = st.session_state.openrouter_key
|
| 653 |
groq_key = st.session_state.groq_key
|
| 654 |
st.caption("๐ **OpenRouter:** " + ('โ
' if openrouter_key else 'โ') + " **Groq:** " + ('โ
' if groq_key else 'โ'))
|
| 655 |
if not any([openrouter_key, groq_key]):
|
| 656 |
st.warning("โ ๏ธ Tidak ada API key. Tambahkan OPENROUTER_API_KEY / GROQ_API_KEY di HF Secrets.")
|
| 657 |
|
| 658 |
+
# Live status placeholder โ updated by _set_status() during LLM calls
|
| 659 |
_llm_ph = st.empty()
|
| 660 |
st.session_state['_llm_ph'] = _llm_ph
|
| 661 |
if st.session_state.get('llm_status'):
|
|
|
|
| 665 |
pass
|
| 666 |
st.markdown("---")
|
| 667 |
|
| 668 |
+
# 3. API Tracker
|
| 669 |
st.markdown("### ๐ก API Tracker")
|
| 670 |
_active_status = st.session_state.get('llm_status', '')
|
| 671 |
if _active_status:
|
|
|
|
| 686 |
st.rerun()
|
| 687 |
st.markdown("---")
|
| 688 |
|
| 689 |
+
# 4. Stack + footer
|
| 690 |
st.markdown(T('sidebar_stack', lang))
|
| 691 |
stack_items = {
|
| 692 |
'id': "- ๐ค XGBoost + LightGBM + RF\n- ๐ SHAP Eksplainabilitas\n- ๐ฌ Narasi AI (OpenRouter + Groq)\n- ๐ค Maya AI Chat + Tool Calling\n- ๐ง Chain-of-Thought + Few-shot\n- ๐พ Memory Summarization\n- ๐ ID / EN / HI",
|
|
|
|
| 752 |
wi_dig=None, wi_biz=None, wi_emp=None,
|
| 753 |
wi_cash=None, wi_dur=None, wi_loan=None,
|
| 754 |
chat_summary='', memory_loaded=False,
|
| 755 |
+
llm_status='', api_log=[],
|
|
|
|
| 756 |
)
|
| 757 |
for k, v in _defaults.items():
|
| 758 |
if k not in st.session_state:
|
|
|
|
| 799 |
])
|
| 800 |
|
| 801 |
def make_shap_png(shap_vals, feature_names):
|
| 802 |
+
"""Returns base64 data URI string."""
|
| 803 |
sv = np.array(shap_vals)
|
| 804 |
names = list(feature_names)
|
| 805 |
n = min(12, len(sv))
|
|
|
|
| 821 |
return 'data:image/png;base64,' + b64
|
| 822 |
|
| 823 |
# ============================================================
|
| 824 |
+
# LLM HELPERS โ Narrative cascade
|
| 825 |
# ============================================================
|
| 826 |
_OR_FREE_MODELS = [
|
| 827 |
"google/gemini-2.0-flash-exp:free",
|
|
|
|
| 869 |
except requests.exceptions.Timeout:
|
| 870 |
ms = int((_time.time() - t0) * 1000)
|
| 871 |
last_error = "Timeout (" + m + ")"
|
| 872 |
+
short = m.split('/')[-1].replace(':free','')
|
| 873 |
+
_log_api("Narasi", "OR/" + short, False, ms)
|
| 874 |
except Exception as e:
|
| 875 |
ms = int((_time.time() - t0) * 1000)
|
| 876 |
last_error = m + ": " + str(e)[:100]
|
| 877 |
+
short = m.split('/')[-1].replace(':free','')
|
| 878 |
+
_log_api("Narasi", "OR/" + short, False, ms)
|
| 879 |
return None, last_error
|
| 880 |
|
| 881 |
def call_groq(messages, api_key):
|
|
|
|
| 911 |
return None, last_error
|
| 912 |
|
| 913 |
def _call_llm(messages):
|
| 914 |
+
"""Narrative LLM: OpenRouter free โ Groq free โ None."""
|
| 915 |
_or = st.session_state.get("openrouter_key", "")
|
| 916 |
_grq = st.session_state.get("groq_key", "")
|
| 917 |
last_err = None
|
| 918 |
if _or:
|
| 919 |
r, last_err = call_openrouter(messages, _or)
|
| 920 |
if r:
|
| 921 |
+
model_used = _OR_FREE_MODELS[0].split("/")[-1].replace(":free","")
|
| 922 |
+
return r, 'OpenRouter (' + model_used + ')', None
|
| 923 |
if _grq:
|
| 924 |
r, last_err = call_groq(messages, _grq)
|
| 925 |
if r:
|
| 926 |
+
model_used = _GROQ_FREE_MODELS[0]
|
| 927 |
+
return r, 'Groq (' + model_used + ')', None
|
| 928 |
return None, None, last_err or "No API key configured"
|
| 929 |
|
| 930 |
# ============================================================
|
|
|
|
| 1066 |
if not npwp:
|
| 1067 |
return _t("prioritas utama: **urus NPWP** dulu di pajak.go.id","top priority: **register NPWP** at pajak.go.id","mukhya prathamikta: **NPWP register karein** pajak.go.id par")
|
| 1068 |
elif dig < 40:
|
| 1069 |
+
return _t("**digital score " + str(dig) + "/100** adalah area paling kritis untuk ditingkatkan","**digital score " + str(dig) + "/100** is the most critical area to improve","**digital score " + str(dig) + "/100** sabse mahatvapurn sudhaar kshetra hai")
|
| 1070 |
elif cf < 10e6:
|
| 1071 |
return _t("**cash flow Rp " + str(int(cf/1e6)) + "jt/bln** perlu dioptimalkan","**cash flow Rp " + str(int(cf/1e6)) + "M/month** needs optimization","**naqad pravaah Rp " + str(int(cf/1e6)) + "M/maah** optimize zaroori hai")
|
| 1072 |
elif biz < 2:
|
|
|
|
| 1074 |
elif pd_pct < 20:
|
| 1075 |
return _t("profil kamu sudah sangat bagus!","your profile is already excellent!","aapka parichay pehle se behtareen hai!")
|
| 1076 |
else:
|
| 1077 |
+
return _t("beberapa area bisa diperbaiki โ tanya aku lebih spesifik!","several areas can be improved โ ask me specifically!","kai kshetra sudhaare jaa sakte hain โ vishesh roop se puchein!")
|
| 1078 |
|
| 1079 |
# ============================================================
|
| 1080 |
# AI CHAT โ Maya persona
|
|
|
|
| 1144 |
"- Pinjaman: Rp " + str(int(raw_input.get('loan_rp',50e6)/1e6)) + "M | Tenor: " + str(raw_input.get('duration',24)) + " bulan\n"
|
| 1145 |
"- Riwayat Kredit: " + str(raw_input.get('credit_history')) + "\n\n"
|
| 1146 |
"FAKTOR RISIKO UTAMA (SHAP):\n" + factors + "\n"
|
| 1147 |
+
+ context
|
| 1148 |
+
+ summary_block
|
| 1149 |
+
+ cot_block
|
| 1150 |
+
+ fewshot
|
| 1151 |
+
+ "ATURAN MENJAWAB โ BACA SEMUA DENGAN SEKSAMA:\n"
|
| 1152 |
+
"1. Jawab BEBAS โ tidak harus soal kredit. Kalau ditanya soal diri sendiri, perkenalkan sebagai Maya.\n"
|
| 1153 |
"2. Kalau relevan, selalu hubungkan ke data pemohon dengan menyebut angka aktualnya.\n"
|
| 1154 |
"3. Berikan saran KONKRET dan SPESIFIK, bukan generik.\n"
|
| 1155 |
"4. Maksimal 150 kata kecuali diminta lebih panjang.\n"
|
| 1156 |
+
"5. FORMAT TAG [ADJUST:] โ WAJIB IKUTI ATURAN INI:\n"
|
| 1157 |
+
" BENAR: Embed tag langsung di akhir kalimat saran:\n"
|
| 1158 |
+
" 'Coba naikkan digital score ke 75 [ADJUST: digital_presence_score=75] โ dampaknya paling besar.'\n"
|
| 1159 |
+
" SALAH: Menulis kalimat saran TANPA tag, lalu menambah baris terpisah\n"
|
| 1160 |
+
" SALAH: Menulis tag di baris sendiri di akhir response\n"
|
| 1161 |
+
" SALAH BESAR: Menulis ulang rekomendasi di baris terpisah setelah selesai\n"
|
| 1162 |
+
" RULE: Setiap rekomendasi bernilai angka = SATU kalimat + SATU tag [ADJUST:] inline. TIDAK LEBIH.\n"
|
| 1163 |
+
" RULE: JANGAN pernah tulis JSON object {...} di response.\n"
|
| 1164 |
+
" RULE: Field valid: digital_presence_score(1-100), business_age_years(1-20),\n"
|
| 1165 |
" num_employees(1-50), monthly_cash_flow(angka Rp), duration(4-72), loan_rp(angka Rp)\n"
|
| 1166 |
"6. JANGAN pernah balik ke template. Jawab seperti manusia cerdas.\n"
|
| 1167 |
"7. Baca context percakapan sebelumnya sebelum menjawab โ jaga konsistensi topik.\n"
|
| 1168 |
+
"8. AKHIRI response dengan kalimat lengkap. Jangan tambahkan apapun setelah kalimat terakhir.\n"
|
| 1169 |
)
|
| 1170 |
|
| 1171 |
messages = [{"role": "system", "content": system}]
|
|
|
|
| 1191 |
|
| 1192 |
if any(w in low for w in ['siapa','kamu','km','who are you','nama','you are','maya','halo','hai','hei','hello','hi','perkenalan']):
|
| 1193 |
response = _t(
|
| 1194 |
+
"Hei! Aku **Maya**, AI Credit Advisor kamu Aku dirancang untuk bantu kamu pahami skor kredit dan strategi bisnis UMKM. "
|
| 1195 |
+
"Skor PD kamu sekarang **" + str(round(pd_pct,1)) + "%** โ " + ('sudah bagus banget!' if pd_pct < 20 else 'masih ada ruang untuk diperbaiki.') + " "
|
| 1196 |
+
"Mau aku jelasin lebih detail atau ada yang mau ditanyain?",
|
| 1197 |
+
"Hey! I'm **Maya**, your AI Credit Advisor. I help you understand your credit score and SME business strategy. "
|
| 1198 |
+
"Your PD score is **" + str(round(pd_pct,1)) + "%** โ " + ('looking great!' if pd_pct < 20 else 'there is room to improve.') + " "
|
| 1199 |
+
"Anything you want to ask?",
|
| 1200 |
+
"Namaste! Main **Maya** hoon, aapki AI Credit Advisor. "
|
| 1201 |
+
"Aapka PD score **" + str(round(pd_pct,1)) + "%** hai โ " + ('bahut badhiya!' if pd_pct < 20 else 'sudhaar ki gunjaish hai.') + " "
|
| 1202 |
+
"Kuch poochna hai?"
|
| 1203 |
+
)
|
| 1204 |
+
|
| 1205 |
+
elif any(w in low for w in ['cuan','profit','untung','laba','cepet cuan','penghasilan cepat','cara cepet','duit cepet']):
|
| 1206 |
+
response = _t(
|
| 1207 |
+
"Cuan cepet? Sah-sah aja! Tapi di kredit, yang bikin bank percaya bukan seberapa cepat, tapi **konsistensi**. "
|
| 1208 |
+
"Dari profil kamu (cash flow Rp " + str(int(cf/1e6)) + "jt/bln, digital " + str(dig) + "/100):\n\n"
|
| 1209 |
+
"1. **Aktifin marketplace** โ cash flow naik [ADJUST: monthly_cash_flow=25000000]\n"
|
| 1210 |
+
"2. **Google Business aktif** โ digital score naik [ADJUST: digital_presence_score=75]\n"
|
| 1211 |
+
"3. **Dokumentasikan semua pemasukan** โ pisah rekening bisnis & pribadi",
|
| 1212 |
+
"Quick profit? Totally valid! But in credit, **consistency** matters more. "
|
| 1213 |
+
"From your profile (CF Rp " + str(int(cf/1e6)) + "M/mo, digital " + str(dig) + "/100):\n\n"
|
| 1214 |
+
"1. Active marketplace โ CF up [ADJUST: monthly_cash_flow=25000000]\n"
|
| 1215 |
+
"2. Google Business โ digital up [ADJUST: digital_presence_score=75]\n"
|
| 1216 |
+
"3. Document all income streams",
|
| 1217 |
+
"Jaldi munafa? **Niyamitata** zaroori hai.\n"
|
| 1218 |
+
"1) Marketplace active karein [ADJUST: monthly_cash_flow=25000000] "
|
| 1219 |
+
"2) Google Business [ADJUST: digital_presence_score=75] 3) Aay document karein"
|
| 1220 |
)
|
| 1221 |
+
adjustments['digital_presence_score'] = 75
|
| 1222 |
+
adjustments['monthly_cash_flow'] = 25000000
|
| 1223 |
+
|
| 1224 |
+
elif any(w in low for w in ['improve','better','lower','reduce','tingkatkan','kurangi','turunkan','cara','gimana','bagaimana','naik','turun','meningkat','naikkan','optimalkan']):
|
| 1225 |
tips = []
|
| 1226 |
if not npwp:
|
| 1227 |
+
tips.append(_t("1. **Urus NPWP** โ wajib untuk pinjaman formal, daftar di pajak.go.id",
|
| 1228 |
+
"1. **Register NPWP** โ required for formal loans, apply at pajak.go.id",
|
| 1229 |
+
"1. **NPWP register karein** โ aupchaarik rin ke liye zaroori"))
|
| 1230 |
+
adjustments['has_npwp'] = 1
|
| 1231 |
if dig < 75:
|
| 1232 |
n = '2' if not npwp else '1'
|
| 1233 |
+
tips.append(_t(n + ". **Naikkan Digital Score dari " + str(dig) + " ke 75+** โ aktif di Google Business, Tokopedia/Shopee",
|
| 1234 |
+
n + ". **Raise Digital Score from " + str(dig) + " to 75+** โ Google Business, marketplace",
|
| 1235 |
+
n + ". **Digital Score " + str(dig) + " se 75+ karein** โ Google Business, marketplace"))
|
| 1236 |
adjustments['digital_presence_score'] = 75
|
| 1237 |
if cf < 25e6:
|
| 1238 |
n = str(len(tips) + 1)
|
| 1239 |
+
tips.append(_t(n + ". **Optimalkan cash flow dari Rp " + str(int(cf/1e6)) + "jt ke 25jt+/bln** โ diversifikasi produk",
|
| 1240 |
+
n + ". **Grow cash flow from Rp " + str(int(cf/1e6)) + "M to 25M+/month**",
|
| 1241 |
+
n + ". **Naqad pravaah Rp " + str(int(cf/1e6)) + "M se 25M+ karein**"))
|
| 1242 |
adjustments['monthly_cash_flow'] = 25000000
|
| 1243 |
if not tips:
|
| 1244 |
+
tips.append(_t("Profil kamu sudah solid dengan PD " + str(round(pd_pct,1)) + "%! Coba simulasikan di tab What-If.",
|
| 1245 |
+
"Your profile is solid at PD " + str(round(pd_pct,1)) + "%! Try the What-If tab.",
|
| 1246 |
+
"Aapka parichay PD " + str(round(pd_pct,1)) + "% ke saath mazboot hai!"))
|
| 1247 |
+
response = (
|
| 1248 |
+
_t("Untuk turunkan PD dari **" + str(round(pd_pct,1)) + "%**, fokus ke:\n\n",
|
| 1249 |
+
"To lower PD from **" + str(round(pd_pct,1)) + "%**, focus on:\n\n",
|
| 1250 |
+
"PD **" + str(round(pd_pct,1)) + "%** kam karne ke liye:\n\n")
|
| 1251 |
+
+ '\n'.join(tips[:3])
|
| 1252 |
+
)
|
| 1253 |
+
|
| 1254 |
+
elif any(w in low for w in ['why','kenapa','mengapa','factor','faktor','shap','pengaruh','driver','apa yang','jelasin','jelaskan','explain','definisi']):
|
| 1255 |
+
ti = int(np.argsort(np.abs(shap_vals))[-1])
|
| 1256 |
+
ti2 = int(np.argsort(np.abs(shap_vals))[-2])
|
| 1257 |
+
fn = feature_names[ti]
|
| 1258 |
+
if 'business_age' in fn:
|
| 1259 |
+
analogi = _t(" โ ibarat pengalaman kerja, makin lama makin dipercaya"," โ like work experience, longer = more trustworthy"," โ kaam ke anubhav ki tarah, zyada = zyada bharosemand")
|
| 1260 |
+
elif 'cash_flow' in fn:
|
| 1261 |
+
analogi = _t(" โ ibarat gaji bulanan kamu, makin besar makin mudah bayar cicilan"," โ like your monthly income, higher = easier to repay"," โ maasik aay ki tarah, zyada = kist bhrna aasaan")
|
| 1262 |
+
elif 'digital' in fn:
|
| 1263 |
+
analogi = _t(" โ ibarat 'nilai reputasi online' bisnis kamu di mata bank"," โ your business's 'online reputation score' in the bank's eyes"," โ bank ki nazar mein vyapaar ka 'online pratishtha score'")
|
| 1264 |
+
else:
|
| 1265 |
+
analogi = ""
|
| 1266 |
+
response = _t(
|
| 1267 |
+
"Dua faktor terbesar yang drive skor PD **" + str(round(pd_pct,1)) + "%** kamu:\n\n"
|
| 1268 |
+
"1. **" + fn + "**" + analogi + "\n โ " + ('meningkatkan' if shap_vals[ti] > 0 else 'menurunkan') + " risiko (SHAP=" + str(round(shap_vals[ti],3)) + ")\n\n"
|
| 1269 |
+
"2. **" + feature_names[ti2] + "**\n โ " + ('meningkatkan' if shap_vals[ti2] > 0 else 'menurunkan') + " risiko (SHAP=" + str(round(shap_vals[ti2],3)) + ")\n\n"
|
| 1270 |
+
+ ('Skor bagus! Kedua faktor ini justru mendukung kelayakan kamu.' if pd_pct < 20 else 'Fokus perbaiki faktor pertama dulu โ dampaknya paling besar.'),
|
| 1271 |
+
"Two biggest drivers of your PD **" + str(round(pd_pct,1)) + "%**:\n\n"
|
| 1272 |
+
"1. **" + fn + "**" + analogi + "\n โ " + ('increases' if shap_vals[ti] > 0 else 'decreases') + " risk (SHAP=" + str(round(shap_vals[ti],3)) + ")\n\n"
|
| 1273 |
+
"2. **" + feature_names[ti2] + "**\n โ " + ('increases' if shap_vals[ti2] > 0 else 'decreases') + " risk (SHAP=" + str(round(shap_vals[ti2],3)) + ")\n\n"
|
| 1274 |
+
+ ('Great score! Both factors support your eligibility.' if pd_pct < 20 else 'Focus on the first factor โ it has the biggest impact.'),
|
| 1275 |
+
"Aapke PD **" + str(round(pd_pct,1)) + "%** ke do mukhya kaarak:\n\n"
|
| 1276 |
+
"1. **" + fn + "**" + analogi + "\n โ " + ('badhata' if shap_vals[ti] > 0 else 'ghataata') + " jokhim (SHAP=" + str(round(shap_vals[ti],3)) + ")\n\n"
|
| 1277 |
+
"2. **" + feature_names[ti2] + "**\n โ " + ('badhata' if shap_vals[ti2] > 0 else 'ghataata') + " jokhim (SHAP=" + str(round(shap_vals[ti2],3)) + ")\n\n"
|
| 1278 |
+
+ ('Badhiya score! Dono kaarak yogyata ka samarthan karte hain.' if pd_pct < 20 else 'Pehle pehle kaarak sudhaarein โ sabse bada prabhav.')
|
| 1279 |
+
)
|
| 1280 |
+
|
| 1281 |
+
elif any(w in low for w in ['pinjaman','loan','kredit','berapa','amount','besar','limit','ideal','rekomendasi pinjaman']):
|
| 1282 |
if cf > 0:
|
| 1283 |
dur_val = raw_input.get('duration', 24)
|
| 1284 |
safe = cf * dur_val * 0.35
|
|
|
|
| 1286 |
if cur > safe:
|
| 1287 |
adjustments['loan_rp'] = safe
|
| 1288 |
response = _t(
|
| 1289 |
+
"Berdasarkan cash flow kamu **Rp " + str(int(cf/1e6)) + "jt/bulan** dan tenor " + str(int(dur_val)) + " bulan, "
|
| 1290 |
+
"pinjaman ideal maksimal **Rp " + str(int(safe/1e6)) + "jt** (rasio cicilan 35%).\n\n"
|
| 1291 |
+
"Pinjaman kamu sekarang Rp " + str(int(cur/1e6)) + "jt โ "
|
| 1292 |
+
+ ('masih aman, good job!' if cur <= safe else 'melebihi batas aman. Pertimbangkan kurangi ke Rp ' + str(int(safe/1e6)) + 'jt atau perpanjang tenor.'),
|
| 1293 |
+
"Based on your cash flow **Rp " + str(int(cf/1e6)) + "M/month** and " + str(int(dur_val)) + "-month tenure, "
|
| 1294 |
+
"ideal loan is max **Rp " + str(int(safe/1e6)) + "M** (35% installment ratio).\n\n"
|
| 1295 |
+
"Current loan Rp " + str(int(cur/1e6)) + "M โ "
|
| 1296 |
+
+ ('within safe limits, good job!' if cur <= safe else 'exceeds safe limit. Consider reducing to Rp ' + str(int(safe/1e6)) + 'M.'),
|
| 1297 |
+
"Aapke naqad pravaah **Rp " + str(int(cf/1e6)) + "M/maah** aur " + str(int(dur_val)) + " maah ke aadhaar par "
|
| 1298 |
+
"adhiktam rin **Rp " + str(int(safe/1e6)) + "M** (35% kist anupaat).\n\n"
|
| 1299 |
+
"Vartamaan rin Rp " + str(int(cur/1e6)) + "M โ " + ('surakshit!' if cur <= safe else 'seema se adhik.')
|
| 1300 |
)
|
| 1301 |
else:
|
| 1302 |
+
response = _t("Isi cash flow bulanan di form dulu ya, biar aku bisa kasih rekomendasi akurat!",
|
| 1303 |
+
"Fill in your monthly cash flow first for an accurate recommendation!",
|
| 1304 |
+
"Sahi sujhaav ke liye pehle maasik naqad pravaah bharein!")
|
| 1305 |
+
|
| 1306 |
+
elif any(w in low for w in ['tabungan','saving','nabung','menabung','savings','uang','keuangan','finance','financial','simpan']):
|
| 1307 |
response = _t(
|
| 1308 |
+
"Soal tabungan dan cash flow kamu (saat ini **Rp " + str(int(cf/1e6)) + "jt/bln**):\n\n"
|
| 1309 |
+
"3 cara praktis tingkatkan tabungan UMKM:\n"
|
| 1310 |
+
"1. Pisahkan rekening bisnis & pribadi โ biar tidak tercampur\n"
|
| 1311 |
+
"2. Sisihkan minimal 10-15% dari omzet setiap bulan secara otomatis\n"
|
| 1312 |
+
"3. Dokumentasikan semua pemasukan โ ini juga naikkan digital score!\n\n"
|
| 1313 |
+
"Tabungan naik โ cash flow lebih sehat โ PD turun",
|
| 1314 |
+
"About savings and your cash flow (currently **Rp " + str(int(cf/1e6)) + "M/month**):\n\n"
|
| 1315 |
+
"3 practical SME savings tips:\n"
|
| 1316 |
+
"1. Separate business & personal bank accounts\n"
|
| 1317 |
+
"2. Auto-transfer 10-15% of monthly revenue to savings\n"
|
| 1318 |
+
"3. Document all income streams โ this also boosts digital score!\n\n"
|
| 1319 |
+
"More savings โ healthier cash flow โ lower PD",
|
| 1320 |
+
"Bachat aur naqad pravaah (**Rp " + str(int(cf/1e6)) + "M/maah**) ke baare mein:\n\n"
|
| 1321 |
+
"3 vyaavhaarik tarike:\n"
|
| 1322 |
+
"1. Vyapaar aur vyaktigat khaate alag rakhein\n"
|
| 1323 |
+
"2. Maasik aay ka 10-15% bachaen\n"
|
| 1324 |
+
"3. Sabhi aay document karein โ digital score bhi badhega!\n\n"
|
| 1325 |
+
"Zyada bachat โ swasth naqad pravaah โ PD kam"
|
| 1326 |
+
)
|
| 1327 |
+
adjustments['monthly_cash_flow'] = int(cf * 1.3)
|
| 1328 |
+
|
| 1329 |
+
elif any(w in low for w in ['riwayat kredit','credit history','kredit history','credit record','riwayat','history kredit']):
|
| 1330 |
+
ch_val = raw_input.get('credit_history', '-')
|
| 1331 |
+
response = _t(
|
| 1332 |
+
"**Riwayat kredit** = catatan sejarah bayar utangmu โ ibarat rapor keuangan.\n\n"
|
| 1333 |
+
"Status kamu: **" + ch_val + "**\n\n"
|
| 1334 |
+
"Dampak ke skor kredit:\n"
|
| 1335 |
+
"- All paid / Existing paid โ sinyal positif\n"
|
| 1336 |
+
"- Delayed previously โ bank waspada\n"
|
| 1337 |
+
"- Critical โ risiko naik signifikan\n\n"
|
| 1338 |
+
"Cara bangun riwayat bagus: bayar cicilan tepat waktu, jangan ambil pinjaman melebihi kemampuan bayar.",
|
| 1339 |
+
"**Credit history** = your track record of paying debts โ your financial report card.\n\n"
|
| 1340 |
+
"Your status: **" + ch_val + "**\n\n"
|
| 1341 |
+
"Score impact:\n"
|
| 1342 |
+
"- All paid / Existing paid โ positive signal\n"
|
| 1343 |
+
"- Delayed previously โ bank is cautious\n"
|
| 1344 |
+
"- Critical โ significant risk increase\n\n"
|
| 1345 |
+
"Build good history: pay on time, don't borrow beyond your repayment capacity.",
|
| 1346 |
+
"**Credit itihaas** = karz bhugtaan ka record โ vitteey report card.\n\n"
|
| 1347 |
+
"Aapki sthiti: **" + ch_val + "**\n\n"
|
| 1348 |
+
"Score prabhav:\n"
|
| 1349 |
+
"- All paid / Existing paid โ sakaraatmak sanket\n"
|
| 1350 |
+
"- Delayed previously โ bank saavdhan\n"
|
| 1351 |
+
"- Critical โ jokhim zyada\n\n"
|
| 1352 |
+
"Samay par bhugtaan karein, kshamta se adhik rin na lein."
|
| 1353 |
+
)
|
| 1354 |
+
|
| 1355 |
+
elif any(w in low for w in ['tips','bisnis','usaha','umkm','sme','digital','marketplace','online','ecommerce','strategi','business tips']):
|
| 1356 |
+
response = _t(
|
| 1357 |
+
"Tips bisnis UMKM buat kamu (PD " + str(round(pd_pct,1)) + "%):\n\n"
|
| 1358 |
+
"1. **Digital hadir** โ daftar Google Business Profile, aktif di Tokopedia/Shopee/TikTok Shop\n"
|
| 1359 |
+
" (Digital score kamu " + str(dig) + "/100 โ masih bisa naik!)\n"
|
| 1360 |
+
"2. **Pisah keuangan** โ rekening bisnis terpisah dari pribadi\n"
|
| 1361 |
+
"3. **Dokumentasi rutin** โ catat semua transaksi, ini bukti ke bank\n"
|
| 1362 |
+
"4. **Legalitas** โ NPWP & SIUP/NIB buka akses ke KUR & kredit formal\n\n"
|
| 1363 |
+
"Mau aku simulasikan dampaknya ke skor di tab What-If?",
|
| 1364 |
+
"SME business tips for you (PD " + str(round(pd_pct,1)) + "%):\n\n"
|
| 1365 |
+
"1. **Go digital** โ Google Business Profile, Tokopedia/Shopee/TikTok Shop\n"
|
| 1366 |
+
" (Your digital score " + str(dig) + "/100 โ room to improve!)\n"
|
| 1367 |
+
"2. **Separate finances** โ dedicated business bank account\n"
|
| 1368 |
+
"3. **Document everything** โ track all transactions as proof for banks\n"
|
| 1369 |
+
"4. **Get legal** โ NPWP & SIUP/NIB unlock KUR & formal credit\n\n"
|
| 1370 |
+
"Want me to simulate the impact in the What-If tab?",
|
| 1371 |
+
"SME vyapaar tips (PD " + str(round(pd_pct,1)) + "%):\n\n"
|
| 1372 |
+
"1. **Digital upasthiti** โ Google Business Profile, Tokopedia/Shopee\n"
|
| 1373 |
+
" (Aapka digital score " + str(dig) + "/100 โ sudhaar ki gunjaish!)\n"
|
| 1374 |
+
"2. **Alag vitteey khaata** โ vyapaar ka alag bank khaata\n"
|
| 1375 |
+
"3. **Sab document karein** โ sabhi laanden-denden ka record\n"
|
| 1376 |
+
"4. **Kanooni rahein** โ NPWP & SIUP/NIB KUR unlock karta hai\n\n"
|
| 1377 |
+
"Kya main What-If tab mein prabhav simulate karun?"
|
| 1378 |
)
|
| 1379 |
if dig < 75:
|
| 1380 |
adjustments['digital_presence_score'] = 75
|
| 1381 |
+
|
| 1382 |
else:
|
| 1383 |
top_issue = _get_top_issue(raw_input, pd_pct, lang)
|
| 1384 |
has_key = any([st.session_state.get("openrouter_key"), st.session_state.get("groq_key")])
|
| 1385 |
err_note = "\n\nLLM error: " + str(_last_error)[:80] if (has_key and _last_error) else ""
|
| 1386 |
response = _t(
|
| 1387 |
+
"Pertanyaan menarik! Untuk ini aku butuh koneksi AI.\n\n"
|
| 1388 |
+
"Yang bisa aku kasih tahu: PD kamu **" + str(round(pd_pct,1)) + "%** dan " + top_issue + "\n\n"
|
| 1389 |
+
"Coba tanya yang lebih spesifik: cara turunkan skor, faktor risiko, pinjaman ideal, tips tabungan, atau riwayat kredit." + err_note,
|
| 1390 |
+
"Great question! For this I need an AI connection.\n\n"
|
| 1391 |
+
"What I can share: your PD is **" + str(round(pd_pct,1)) + "%** and " + top_issue + "\n\n"
|
| 1392 |
+
"Try asking specifically: how to lower score, risk factors, ideal loan, savings tips, or credit history." + err_note,
|
| 1393 |
+
"Accha sawaal! Iske liye AI connection chahiye.\n\n"
|
| 1394 |
+
"Aapka PD **" + str(round(pd_pct,1)) + "%** hai aur " + top_issue + "\n\n"
|
| 1395 |
+
"Vishesh roop se puchein: score kaise kam karein, jokhim kaarak, ideal rin, bachat tips." + err_note
|
| 1396 |
)
|
| 1397 |
|
| 1398 |
if response:
|
|
|
|
| 1403 |
|
| 1404 |
return response or "...", adjustments, _last_error
|
| 1405 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1406 |
# ============================================================
|
| 1407 |
# TAB LOADING PROGRESS
|
| 1408 |
# ============================================================
|
| 1409 |
+
def _tab_step(n):
|
| 1410 |
+
try:
|
| 1411 |
+
pct = int(n / 5 * 100)
|
| 1412 |
+
_tab_prog_ph.progress(pct, text=_tls[n])
|
| 1413 |
+
except Exception:
|
| 1414 |
+
pass
|
| 1415 |
+
|
| 1416 |
_tab_load_steps = {
|
| 1417 |
'id': ['๐ Menyiapkan SHAP...', '๐ฌ Menyiapkan Narasi AI...', '๐ค Menyiapkan Chat...',
|
| 1418 |
'๐ฎ Menyiapkan What-If...', '๐ Menyiapkan Formula...', 'โ
Semua tab siap!'],
|
|
|
|
| 1424 |
_tls = _tab_load_steps.get(lang, _tab_load_steps['id'])
|
| 1425 |
_tab_prog_ph = st.empty()
|
| 1426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1427 |
_tab_step(0)
|
| 1428 |
|
| 1429 |
# ============================================================
|
|
|
|
| 1438 |
with t1:
|
| 1439 |
_tab_step(0)
|
| 1440 |
st.markdown('<div class="sec-title">' + T("shap_title",lang) + '</div>', unsafe_allow_html=True)
|
| 1441 |
+
_shap_all_zero = (shap_vals is not None and np.all(shap_vals == 0))
|
| 1442 |
+
if _shap_all_zero:
|
| 1443 |
+
st.warning(
|
| 1444 |
+
"โ ๏ธ **SHAP tidak tersedia** โ model XGBoost (`xgb_model.pkl`) mungkin belum di-upload atau "
|
| 1445 |
+
"tidak kompatibel dengan SHAP TreeExplainer. "
|
| 1446 |
+
"Pastikan `model/xgb_model.pkl` ada di HF Space dan di-train ulang jika perlu.\n\n"
|
| 1447 |
+
"Skor PD dan narasi tetap valid (menggunakan ensemble model)."
|
| 1448 |
+
)
|
| 1449 |
else:
|
| 1450 |
+
if st.session_state.get("shap_png"):
|
| 1451 |
+
_spng = st.session_state.shap_png
|
| 1452 |
+
if isinstance(_spng, str) and _spng.startswith('data:'):
|
| 1453 |
+
st.markdown('<img src="' + _spng + '" style="width:100%;border-radius:8px;">', unsafe_allow_html=True)
|
| 1454 |
+
elif _spng:
|
| 1455 |
+
st.image(_spng, use_container_width=True)
|
| 1456 |
+
st.caption(T('shap_caption', lang))
|
| 1457 |
+
top_idx = np.argsort(np.abs(shap_vals))[-10:][::-1]
|
| 1458 |
+
st.dataframe(pd.DataFrame({
|
| 1459 |
+
T('shap_col_feature',lang): [feature_names[i] for i in top_idx],
|
| 1460 |
+
'SHAP': [round(shap_vals[i],4) for i in top_idx],
|
| 1461 |
+
T('shap_col_impact',lang): [
|
| 1462 |
+
T('shap_risk_up',lang) if shap_vals[i] > 0 else T('shap_risk_down',lang)
|
| 1463 |
+
for i in top_idx
|
| 1464 |
+
],
|
| 1465 |
+
}), use_container_width=True, hide_index=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1466 |
|
| 1467 |
# โโ TAB 2: NARRATIVE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1468 |
with t2:
|
| 1469 |
_tab_step(1)
|
| 1470 |
flag = {'id':'๐ฎ๐ฉ','en':'๐ฌ๐ง','hi':'๐ฎ๐ณ'}[lang]
|
| 1471 |
st.markdown('<div class="sec-title">' + T("narr_title",lang) + ' ' + flag + '</div>', unsafe_allow_html=True)
|
| 1472 |
+
st.caption(T('narr_source',lang) + ": " + (llm_src if llm_src else "โ"))
|
| 1473 |
+
if narrative and narrative.strip():
|
| 1474 |
+
fmt = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', narrative.replace('\n','<br>'))
|
| 1475 |
+
st.markdown('<div class="narrative-box">' + fmt + '</div>', unsafe_allow_html=True)
|
|
|
|
| 1476 |
else:
|
| 1477 |
+
narrative_fb, src_fb = get_narrative(shap_vals, feature_names, result, lang, raw_input)
|
| 1478 |
+
st.session_state.narrative = narrative_fb
|
| 1479 |
+
st.session_state.llm_src = src_fb
|
| 1480 |
+
fmt = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', narrative_fb.replace('\n','<br>'))
|
| 1481 |
+
st.markdown('<div class="narrative-box">' + fmt + '</div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1482 |
_or_key = st.session_state.get("openrouter_key","")
|
| 1483 |
_grq_key = st.session_state.get("groq_key","")
|
| 1484 |
+
with st.expander("LLM Debug Info"):
|
| 1485 |
st.code(
|
| 1486 |
"OR key : " + ('OK ' + _or_key[:12] + '...' if _or_key else 'missing') + "\n"
|
| 1487 |
"Groq key : " + ('OK ' + _grq_key[:12] + '...' if _grq_key else 'missing') + "\n"
|
| 1488 |
+
"Source : " + str(src_fb) + "\n"
|
| 1489 |
"OR model : " + _OR_FREE_MODELS[0] + "\n"
|
| 1490 |
"Groq mdl : " + _GROQ_FREE_MODELS[0]
|
| 1491 |
)
|
|
|
|
| 1495 |
_tab_step(2)
|
| 1496 |
st.markdown('<div class="sec-title">' + T("chat_title",lang) + '</div>', unsafe_allow_html=True)
|
| 1497 |
|
| 1498 |
+
if st.session_state.get('chat_summary'):
|
| 1499 |
+
n_turns = len(st.session_state.chat_history)
|
| 1500 |
+
lbl = {
|
| 1501 |
+
'id': 'Memory aktif - ' + str(n_turns) + ' pesan tersimpan',
|
| 1502 |
+
'en': 'Memory active - ' + str(n_turns) + ' messages saved',
|
| 1503 |
+
'hi': 'Memory active - ' + str(n_turns) + ' sandesh save hue',
|
| 1504 |
+
}
|
| 1505 |
+
st.markdown('<div class="memory-badge">' + lbl.get(lang, lbl["en"]) + '</div>', unsafe_allow_html=True)
|
| 1506 |
+
|
| 1507 |
+
chips = [T('chat_chip1',lang), T('chat_chip2',lang), T('chat_chip3',lang), T('chat_chip4',lang)]
|
| 1508 |
+
chip_cols = st.columns(len(chips))
|
| 1509 |
+
chip_clicked = None
|
| 1510 |
+
for i, (col, chip) in enumerate(zip(chip_cols, chips)):
|
| 1511 |
+
with col:
|
| 1512 |
+
if st.button(chip, key="chip_" + str(i), use_container_width=True):
|
| 1513 |
+
chip_clicked = chip
|
| 1514 |
+
st.markdown("---")
|
| 1515 |
+
|
| 1516 |
+
if st.session_state.chat_history:
|
| 1517 |
+
html_chat = ""
|
| 1518 |
+
for msg in st.session_state.chat_history:
|
| 1519 |
+
content = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>',
|
| 1520 |
+
msg['content'].replace('\n','<br>'))
|
| 1521 |
+
cls = "chat-bubble-user" if msg['role'] == 'user' else "chat-bubble-ai"
|
| 1522 |
+
icon = "๐ค" if msg['role'] == 'user' else "๐ค"
|
| 1523 |
+
html_chat += '<div class="' + cls + '">' + icon + ' ' + content + '</div>'
|
| 1524 |
+
st.markdown(html_chat, unsafe_allow_html=True)
|
| 1525 |
else:
|
| 1526 |
+
st.info("๐ " + T('chat_welcome', lang))
|
| 1527 |
+
|
| 1528 |
+
user_input = st.chat_input(T('chat_input', lang))
|
| 1529 |
+
to_process = chip_clicked or user_input
|
| 1530 |
+
if to_process:
|
| 1531 |
+
st.session_state.chat_history.append({'role':'user','content':to_process})
|
| 1532 |
+
_chat_status_ph = st.empty()
|
| 1533 |
+
_spinner_label = {
|
| 1534 |
+
'id': '๐ค Maya sedang berpikir...',
|
| 1535 |
+
'en': '๐ค Maya is thinking...',
|
| 1536 |
+
'hi': '๐ค Maya soch rahi hai...',
|
| 1537 |
+
}.get(lang, '๐ค Maya sedang berpikir...')
|
| 1538 |
+
with st.spinner(_spinner_label):
|
| 1539 |
+
_chat_status_ph.info(
|
| 1540 |
+
{'id':'โณ Menghubungi LLM โ bisa 5โ15 detik tergantung model yang dipilih...',
|
| 1541 |
+
'en':'โณ Connecting to LLM โ may take 5โ15 seconds depending on the model...',
|
| 1542 |
+
'hi':'โณ LLM se connect ho raha hai โ model ke hisaab se 5โ15 second lag sakte hain...'}[lang],
|
| 1543 |
+
icon="โณ"
|
| 1544 |
+
)
|
| 1545 |
+
ai_resp, adjustments, _debug_err = get_chat_response(
|
| 1546 |
+
to_process, st.session_state.chat_history,
|
| 1547 |
+
result, raw_input, shap_vals, feature_names, lang,
|
| 1548 |
+
rag_index=rag_index
|
| 1549 |
+
)
|
| 1550 |
+
_chat_status_ph.empty()
|
| 1551 |
+
if _debug_err and any([st.session_state.get("openrouter_key"),
|
| 1552 |
+
st.session_state.get("groq_key")]):
|
| 1553 |
+
st.warning("Semua LLM gagal, menggunakan Smart Fallback. Error: " + str(_debug_err)[:100])
|
| 1554 |
+
st.session_state.chat_history.append({'role':'assistant','content':ai_resp})
|
| 1555 |
+
_save_chat_memory(st.session_state.chat_history, st.session_state.get('chat_summary',''))
|
| 1556 |
+
adj_map = {
|
| 1557 |
+
'digital_presence_score': 'wi_dig', 'business_age_years': 'wi_biz',
|
| 1558 |
+
'num_employees': 'wi_emp', 'monthly_cash_flow': 'wi_cash',
|
| 1559 |
+
'duration': 'wi_dur', 'loan_rp': 'wi_loan',
|
| 1560 |
+
}
|
| 1561 |
+
if adjustments:
|
| 1562 |
+
for field, val in adjustments.items():
|
| 1563 |
+
if field in adj_map:
|
| 1564 |
+
st.session_state[adj_map[field]] = val
|
| 1565 |
+
st.success("๐ก " + T('chat_updated', lang))
|
| 1566 |
+
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1567 |
|
| 1568 |
+
if st.session_state.chat_history:
|
| 1569 |
+
if st.button(T('chat_clear', lang)):
|
| 1570 |
+
st.session_state.chat_history = []
|
| 1571 |
+
st.session_state.chat_summary = ''
|
| 1572 |
+
_save_chat_memory([], '')
|
| 1573 |
+
st.rerun()
|
| 1574 |
|
| 1575 |
# โโ TAB 4: WHAT-IF โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1576 |
with t4:
|
| 1577 |
_tab_step(3)
|
|
|
|
| 1578 |
@st.fragment
|
| 1579 |
def render_whatif():
|
| 1580 |
_lang = st.session_state.get("lang_sel","id")
|
|
|
|
| 1613 |
'monthly_cash_flow': float(wi_cash * 1e6),
|
| 1614 |
'num_employees': wi_emp,
|
| 1615 |
}
|
| 1616 |
+
X_wi = preprocess(wi_raw, scaler, feature_names)
|
| 1617 |
+
wi_pd = float(ensemble.predict_proba(X_wi)[0][1])
|
| 1618 |
wi_res = risk_result(wi_pd, wi_loan * 1e6, result_f['lgd'])
|
| 1619 |
+
d_pd = wi_pd - result_f['pd']
|
| 1620 |
+
d_el = wi_res['el'] - result_f['el']
|
| 1621 |
_log_api("What-If", "Local Model", True, 0)
|
| 1622 |
|
| 1623 |
r1, r2, r3 = st.columns(3)
|
|
|
|
| 1679 |
with st.expander(T('wi_tips_title', _lang)):
|
| 1680 |
tips = []
|
| 1681 |
if wi_dig < 70:
|
| 1682 |
+
tips.append(_t("๐ฑ Naikkan Digital Score ke 70+ โ marketplace & Google Business",
|
| 1683 |
+
"๐ฑ Raise Digital Score to 70+ โ marketplace & Google Business",
|
| 1684 |
+
"๐ฑ Digital Score 70+ karein โ marketplace & Google Business"))
|
| 1685 |
if wi_cash < 20:
|
| 1686 |
+
tips.append(_t("๐ต Target cash flow Rp 20jt+/bulan",
|
| 1687 |
+
"๐ต Target Rp 20M+/month cash flow",
|
| 1688 |
+
"๐ต Naqad pravaah Rp 20M+/maah target"))
|
| 1689 |
if wi_biz < 3:
|
| 1690 |
+
tips.append(_t("๐ข Bisnis < 3 thn lebih berisiko โ bangun track record",
|
| 1691 |
+
"๐ข Business < 3 yr is riskier โ build track record",
|
| 1692 |
+
"๐ข 3 saal se kam vyapaar โ track record banaen"))
|
| 1693 |
if wi_emp < 5:
|
| 1694 |
+
tips.append(_t("๐ฅ Tambah karyawan = skala bisnis lebih sehat",
|
| 1695 |
+
"๐ฅ More employees signals healthy scale",
|
| 1696 |
+
"๐ฅ Zyada karmachaaree = swasth paimaana"))
|
| 1697 |
if not tips:
|
| 1698 |
+
tips.append(_t("๐ Profil sudah optimal!",
|
| 1699 |
+
"๐ Profile already well-optimized!",
|
| 1700 |
+
"๐ Parichay pehle se anukoolit!"))
|
| 1701 |
for tip in tips:
|
| 1702 |
st.markdown(tip)
|
| 1703 |
|
|
|
|
| 1710 |
with t5:
|
| 1711 |
_tab_step(4)
|
| 1712 |
st.markdown('<div class="sec-title">' + T("form_formula_title",lang) + '</div>', unsafe_allow_html=True)
|
| 1713 |
+
st.latex(r"EL = PD \times LGD \times EAD")
|
| 1714 |
+
st.latex(
|
| 1715 |
+
"EL = " + str(round(result['pd'],4)) + r" \times " + str(round(result['lgd'],2))
|
| 1716 |
+
+ r" \times Rp\," + "{:,}".format(int(result['ead']))
|
| 1717 |
+
+ r" = Rp\," + "{:,}".format(int(result['el']))
|
| 1718 |
+
)
|
| 1719 |
+
cf1, cf2 = st.columns(2)
|
| 1720 |
+
with cf1:
|
| 1721 |
+
st.markdown(T('formula_def', lang))
|
| 1722 |
+
with cf2:
|
| 1723 |
+
cat_lbl = (
|
| 1724 |
+
T('formula_low',lang) if result['pd'] < .2 else
|
| 1725 |
+
T('formula_medium',lang) if result['pd'] < .5 else
|
| 1726 |
+
T('formula_high',lang)
|
| 1727 |
)
|
| 1728 |
+
st.markdown(
|
| 1729 |
+
"| " + T('formula_komponen',lang) + " | " + T('formula_nilai',lang) + " |\n"
|
| 1730 |
+
"|--|--|\n"
|
| 1731 |
+
"| PD | " + str(round(result['pd']*100,1)) + "% (" + cat_lbl + ") |\n"
|
| 1732 |
+
"| LGD | " + str(int(result['lgd']*100)) + "% |\n"
|
| 1733 |
+
"| EAD | Rp " + str(int(result['ead']/1e6)) + "jt |\n"
|
| 1734 |
+
"| **EL** | **Rp " + str(round(result['el']/1e6,2)) + "jt** |"
|
| 1735 |
+
)
|
| 1736 |
+
_log_api("Formula", "Local", True, 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1737 |
|
| 1738 |
# ============================================================
|
| 1739 |
+
# DOWNLOAD REPORT
|
|
|
|
|
|
|
|
|
|
| 1740 |
# ============================================================
|
| 1741 |
_tab_step(5)
|
| 1742 |
_tab_prog_ph.empty()
|
| 1743 |
+
st.markdown("---")
|
| 1744 |
+
report_txt = (
|
| 1745 |
+
"SME CREDIT RISK REPORT โ 1na37 AI ยท Batch 10\n" + "="*50 + "\n"
|
| 1746 |
+
+ T('kpi_pd',lang) + ": " + str(round(result['pd']*100,1)) + "% โ " + result['cat'][lang] + "\n"
|
| 1747 |
+
+ T('kpi_el',lang) + ": Rp " + "{:,}".format(int(result['el'])) + "\n"
|
| 1748 |
+
"LGD: " + str(int(result['lgd']*100)) + "% (Basel II)\n"
|
| 1749 |
+
"EAD: Rp " + "{:,}".format(int(result['ead'])) + "\n"
|
| 1750 |
+
+ "="*50 + "\n"
|
| 1751 |
+
+ T('narr_title',lang) + "\n" + narrative + "\n"
|
| 1752 |
+
+ "="*50 + "\n"
|
| 1753 |
+
"TOP SHAP\n" + shap_summary(shap_vals, feature_names, 5) + "\n"
|
| 1754 |
+
+ "="*50 + "\n"
|
| 1755 |
+
"DISCLAIMER: Educational use only. Not financial advice."
|
| 1756 |
+
)
|
| 1757 |
+
st.download_button(
|
| 1758 |
+
T('download_btn', lang),
|
| 1759 |
+
data=report_txt,
|
| 1760 |
+
file_name=T('download_file', lang),
|
| 1761 |
+
mime="text/plain"
|
| 1762 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|