1na37 commited on
Commit
d02dffc
Β·
verified Β·
1 Parent(s): 1abaf3d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +141 -14
app.py CHANGED
@@ -1,21 +1,26 @@
1
  # ============================================================
2
- # 🏦 SME Credit Risk Assessment β€” FINAL (FIXED v3)
3
  # Final Project | AI Engineering Bootcamp Batch 10
4
  # Author: 1na37
5
  # ============================================================
6
- # BASE: Doc4 (no GIF/music) as foundation
7
- # RESTORED from Doc3:
8
  # - Full CoT block with β‘  β‘‘ β‘’ β‘£ numbered steps
9
  # - Full few-shot examples (3 dialogs)
10
- # - _tab_step(0) called at correct place before tabs
11
- # - inject_media_manager() REMOVED intentionally (no GIF/music)
12
- # FIXED:
13
- # - _tab_label_ph always defined before use
14
- # - _tab_step works even before result exists
15
- # - Tool calling β†’ slider trigger fully wired
16
- # - _clean_response correctly strips [ADJUST:] without orphan sentences
17
- # - Memory badge shows correctly
18
- # - SHAP fallback message clear
 
 
 
 
 
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
- # Clean response and extract any remaining [ADJUST:] tags
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
+ )