Update app.py
Browse files
app.py
CHANGED
|
@@ -1,21 +1,26 @@
|
|
| 1 |
# ============================================================
|
| 2 |
-
# π¦ SME Credit Risk Assessment β FINAL (FIXED
|
| 3 |
# Final Project | AI Engineering Bootcamp Batch 10
|
| 4 |
# Author: 1na37
|
| 5 |
# ============================================================
|
| 6 |
-
# BASE:
|
| 7 |
-
# RESTORED
|
| 8 |
# - Full CoT block with β β‘ β’ β£ numbered steps
|
| 9 |
# - Full few-shot examples (3 dialogs)
|
| 10 |
-
# - _tab_step
|
| 11 |
-
#
|
| 12 |
-
#
|
| 13 |
-
#
|
| 14 |
-
# -
|
| 15 |
-
# -
|
| 16 |
-
#
|
| 17 |
-
#
|
| 18 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# ============================================================
|
| 20 |
|
| 21 |
import streamlit as st
|
|
@@ -504,6 +509,117 @@ def _clean_response(text: str) -> tuple:
|
|
| 504 |
text = re.sub(r'\n{3,}', '\n\n', text).strip()
|
| 505 |
return text, adjustments
|
| 506 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
def _call_chat_llm(messages):
|
| 508 |
"""Cascade: OR tools β Groq tools β OR no-tools β Groq no-tools."""
|
| 509 |
_or = st.session_state.get("openrouter_key", "")
|
|
@@ -1660,13 +1776,24 @@ CARA BERPIKIR β CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
|
|
| 1660 |
"Vishesh roop se puchein: score kaise kam karein, jokhim kaarak, ideal rin, bachat tips." + err_note
|
| 1661 |
)
|
| 1662 |
|
| 1663 |
-
#
|
| 1664 |
if response:
|
| 1665 |
response, extra_adj = _clean_response(response)
|
| 1666 |
for field, val in extra_adj.items():
|
| 1667 |
if field not in adjustments:
|
| 1668 |
adjustments[field] = val
|
| 1669 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1670 |
return response or "...", adjustments, _last_error
|
| 1671 |
|
| 1672 |
# ============================================================
|
|
@@ -2351,4 +2478,4 @@ st.download_button(
|
|
| 2351 |
data=report_txt,
|
| 2352 |
file_name=T('download_file', lang),
|
| 2353 |
mime="text/plain"
|
| 2354 |
-
)
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
+
# π¦ SME Credit Risk Assessment β FINAL (FIXED v4)
|
| 3 |
# Final Project | AI Engineering Bootcamp Batch 10
|
| 4 |
# Author: 1na37
|
| 5 |
# ============================================================
|
| 6 |
+
# BASE: Doc6 (live version β with GIF/music sidebar)
|
| 7 |
+
# RESTORED:
|
| 8 |
# - Full CoT block with β β‘ β’ β£ numbered steps
|
| 9 |
# - Full few-shot examples (3 dialogs)
|
| 10 |
+
# - _tab_step always defined before use
|
| 11 |
+
# ROOT CAUSE FIX (screenshot bug β sliders never triggered):
|
| 12 |
+
# - Free-tier LLMs (gemini-flash, llama, etc.) frequently IGNORE
|
| 13 |
+
# [ADJUST:] tag instructions and respond in natural language only.
|
| 14 |
+
# - _clean_response only extracts explicit [ADJUST:] tags β empty dict
|
| 15 |
+
# - SOLUTION: _extract_adjustments_semantic() parses numeric values
|
| 16 |
+
# directly from natural LLM text as a guaranteed fallback.
|
| 17 |
+
# e.g. "naikkan digital score ke 75" β digital_presence_score=75
|
| 18 |
+
# "Rp 25jt/bln" near "cash flow" β monthly_cash_flow=25000000
|
| 19 |
+
# "digital score jadi 80" β digital_presence_score=80
|
| 20 |
+
# OTHER FIXES:
|
| 21 |
+
# - _tab_prog_ph always defined before use (no NameError)
|
| 22 |
+
# - Memory badge shows π§ icon
|
| 23 |
+
# - 3 new fallback handlers: digital-score-target, humor, 6-month plan
|
| 24 |
# ============================================================
|
| 25 |
|
| 26 |
import streamlit as st
|
|
|
|
| 509 |
text = re.sub(r'\n{3,}', '\n\n', text).strip()
|
| 510 |
return text, adjustments
|
| 511 |
|
| 512 |
+
def _extract_adjustments_semantic(text: str, raw_input: dict) -> dict:
|
| 513 |
+
"""
|
| 514 |
+
FALLBACK: parse numeric slider values from natural LLM text.
|
| 515 |
+
|
| 516 |
+
Free-tier LLMs (gemini-flash, llama-3, etc.) frequently ignore [ADJUST:] tag
|
| 517 |
+
instructions and respond in plain natural language. This function detects
|
| 518 |
+
numeric recommendations in the response text and maps them to What-If fields.
|
| 519 |
+
|
| 520 |
+
Called AFTER _clean_response β only fills in fields not already captured
|
| 521 |
+
by explicit [ADJUST:] tags or formal tool calls.
|
| 522 |
+
|
| 523 |
+
Examples handled:
|
| 524 |
+
"naikkan digital score ke 75" β digital_presence_score=75
|
| 525 |
+
"digital score jadi 80" β digital_presence_score=80
|
| 526 |
+
"Rp 25jt/bln" near "cash flow" β monthly_cash_flow=25_000_000
|
| 527 |
+
"pinjaman ideal Rp 128jt" β loan_rp=128_000_000
|
| 528 |
+
"tenor 36 bulan" β duration=36
|
| 529 |
+
"""
|
| 530 |
+
if not text:
|
| 531 |
+
return {}
|
| 532 |
+
adjustments = {}
|
| 533 |
+
low = text.lower()
|
| 534 |
+
|
| 535 |
+
# ββ Digital presence score ββββββββββββββββββββββββββββββββ
|
| 536 |
+
for pat in [
|
| 537 |
+
r'digital\s+score\s+(?:ke|jadi|to|β|->|=)\s*(\d+)',
|
| 538 |
+
r'naikkan\s+digital\s+(?:score\s+)?(?:ke|jadi|to)\s*(\d+)',
|
| 539 |
+
r'raise\s+digital\s+(?:score\s+)?to\s*(\d+)',
|
| 540 |
+
r'digital\s+(?:ke|jadi)\s*(\d+)',
|
| 541 |
+
r'digital\s+presence\s+(?:score\s+)?(?:ke|jadi|to)\s*(\d+)',
|
| 542 |
+
]:
|
| 543 |
+
m = re.search(pat, low)
|
| 544 |
+
if m:
|
| 545 |
+
val = int(m.group(1))
|
| 546 |
+
if 1 <= val <= 100:
|
| 547 |
+
adjustments['digital_presence_score'] = float(val)
|
| 548 |
+
break
|
| 549 |
+
|
| 550 |
+
# ββ Monthly cash flow βββββββββββββββββββββββββββββββββββββ
|
| 551 |
+
# Match "Rp NNjt" or "NNjt" near cash flow context
|
| 552 |
+
for pat in [
|
| 553 |
+
r'cash\s+flow\s+(?:ke|jadi|to|β)\s*rp\s*(\d+)\s*(?:jt|juta|m\b)',
|
| 554 |
+
r'(?:optimal|target|naikkan)\s+cash\s+flow.*?rp\s*(\d+)\s*(?:jt|juta|m\b)',
|
| 555 |
+
r'rp\s*(\d+)\s*(?:jt|juta)\s*/\s*(?:bln|bulan|month)',
|
| 556 |
+
r'cash\s+flow\s+.*?(\d+)\s*(?:jt|juta)\s*/\s*(?:bln|bulan|month)',
|
| 557 |
+
r'cash\s+flow.*?rp\s*(\d+)\s*(?:jt|juta)',
|
| 558 |
+
]:
|
| 559 |
+
m = re.search(pat, low)
|
| 560 |
+
if m:
|
| 561 |
+
val_m = int(m.group(1)) * 1_000_000
|
| 562 |
+
cur_cf = raw_input.get('monthly_cash_flow', 0)
|
| 563 |
+
# Only update if it's a recommendation (different from current by >10%)
|
| 564 |
+
if val_m != cur_cf and 1_000_000 <= val_m <= 500_000_000:
|
| 565 |
+
adjustments['monthly_cash_flow'] = float(val_m)
|
| 566 |
+
break
|
| 567 |
+
|
| 568 |
+
# ββ Loan amount βββββββββββββββββββββββββββββββββββββββββββ
|
| 569 |
+
for pat in [
|
| 570 |
+
r'pinjaman\s+(?:ideal|aman|safe|maksimal|max)\s+.*?rp\s*(\d+(?:[,.]\d+)?)\s*(?:jt|juta|m\b)',
|
| 571 |
+
r'rp\s*(\d+(?:[,.]\d+)?)\s*(?:jt|juta)\s+(?:lebih aman|masih aman|safe|ideal)',
|
| 572 |
+
r'batas\s+aman\s+.*?rp\s*(\d+(?:[,.]\d+)?)\s*(?:jt|juta)',
|
| 573 |
+
r'ideal\s+loan\s+.*?rp\s*(\d+(?:[,.]\d+)?)\s*(?:jt|juta|m\b)',
|
| 574 |
+
r'max(?:imal)?\s+.*?rp\s*(\d+(?:[,.]\d+)?)\s*(?:jt|juta|m\b)',
|
| 575 |
+
]:
|
| 576 |
+
m = re.search(pat, low)
|
| 577 |
+
if m:
|
| 578 |
+
raw_val = m.group(1)
|
| 579 |
+
try:
|
| 580 |
+
# Handle "128,6" (ID decimal) β 128.6 β 128_600_000
|
| 581 |
+
# Handle "128" (integer) β 128 β 128_000_000
|
| 582 |
+
if ',' in raw_val or '.' in raw_val:
|
| 583 |
+
val_f = float(raw_val.replace(',', '.'))
|
| 584 |
+
else:
|
| 585 |
+
val_f = float(raw_val)
|
| 586 |
+
val_m = int(val_f * 1_000_000)
|
| 587 |
+
cur_loan = raw_input.get('loan_rp', 50e6)
|
| 588 |
+
if val_m != cur_loan and 5_000_000 <= val_m <= 500_000_000:
|
| 589 |
+
adjustments['loan_rp'] = float(val_m)
|
| 590 |
+
break
|
| 591 |
+
except ValueError:
|
| 592 |
+
pass
|
| 593 |
+
|
| 594 |
+
# ββ Duration / tenor βββββββββββββββββββββββββββββββββββββ
|
| 595 |
+
for pat in [
|
| 596 |
+
r'tenor\s+(\d+)\s*bulan',
|
| 597 |
+
r'duration\s+(\d+)\s*months?',
|
| 598 |
+
r'perpanjang\s+tenor\s+(?:ke|jadi|to)\s*(\d+)',
|
| 599 |
+
]:
|
| 600 |
+
m = re.search(pat, low)
|
| 601 |
+
if m:
|
| 602 |
+
val = int(m.group(1))
|
| 603 |
+
cur_dur = raw_input.get('duration', 24)
|
| 604 |
+
if val != cur_dur and 4 <= val <= 72:
|
| 605 |
+
adjustments['duration'] = float(val)
|
| 606 |
+
break
|
| 607 |
+
|
| 608 |
+
# ββ Business age (only if explicitly recommended, not just mentioned) ββ
|
| 609 |
+
for pat in [
|
| 610 |
+
r'bangun\s+bisnis\s+(?:selama\s+)?(\d+)\s*tahun',
|
| 611 |
+
r'business\s+age\s+(?:to|ke|jadi)\s*(\d+)',
|
| 612 |
+
]:
|
| 613 |
+
m = re.search(pat, low)
|
| 614 |
+
if m:
|
| 615 |
+
val = int(m.group(1))
|
| 616 |
+
if 1 <= val <= 20:
|
| 617 |
+
adjustments['business_age_years'] = float(val)
|
| 618 |
+
break
|
| 619 |
+
|
| 620 |
+
return adjustments
|
| 621 |
+
|
| 622 |
+
|
| 623 |
def _call_chat_llm(messages):
|
| 624 |
"""Cascade: OR tools β Groq tools β OR no-tools β Groq no-tools."""
|
| 625 |
_or = st.session_state.get("openrouter_key", "")
|
|
|
|
| 1776 |
"Vishesh roop se puchein: score kaise kam karein, jokhim kaarak, ideal rin, bachat tips." + err_note
|
| 1777 |
)
|
| 1778 |
|
| 1779 |
+
# ββ Final cleanup: clean response text + extract explicit [ADJUST:] tags ββ
|
| 1780 |
if response:
|
| 1781 |
response, extra_adj = _clean_response(response)
|
| 1782 |
for field, val in extra_adj.items():
|
| 1783 |
if field not in adjustments:
|
| 1784 |
adjustments[field] = val
|
| 1785 |
|
| 1786 |
+
# ββ Semantic fallback: parse numeric values from natural LLM text βββββββββ
|
| 1787 |
+
# Free-tier LLMs frequently ignore [ADJUST:] tag instructions even when
|
| 1788 |
+
# the system prompt says to use them. This guarantees slider updates work
|
| 1789 |
+
# even when the LLM gives a perfect natural-language recommendation but
|
| 1790 |
+
# forgets to embed the tags. Only fills fields not already captured above.
|
| 1791 |
+
if response and raw_input:
|
| 1792 |
+
sem_adj = _extract_adjustments_semantic(response, raw_input)
|
| 1793 |
+
for field, val in sem_adj.items():
|
| 1794 |
+
if field not in adjustments:
|
| 1795 |
+
adjustments[field] = val
|
| 1796 |
+
|
| 1797 |
return response or "...", adjustments, _last_error
|
| 1798 |
|
| 1799 |
# ============================================================
|
|
|
|
| 2478 |
data=report_txt,
|
| 2479 |
file_name=T('download_file', lang),
|
| 2480 |
mime="text/plain"
|
| 2481 |
+
)
|