SHELLAPANDIANGANHUNGING commited on
Commit
2d8c7f9
·
verified ·
1 Parent(s): 5de3362

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +279 -109
app.py CHANGED
@@ -1987,158 +1987,328 @@ else:
1987
  st.info("No data available for non-positive issue categories with 100% coverage and positive trend.")
1988
  # =================== OBJECTIVE 7 — Insight and Recommendation (Agentic AI LLM Style — Final) ===================
1989
  # =================== OBJECTIVE 7 — Insight and Recommendation (Final — Agentic AI, No markdown bold) ===================
1990
- # === RENDER INSIGHT CARD (unchanged logic, improved styling & center header) ===
1991
- st.markdown(
1992
- f"""
1993
- <div class="card" style="
1994
- background-color: #f8f9fa;
1995
- border-left: 4px solid #003DA5;
1996
- padding: 18px;
1997
- margin-bottom: 24px;
1998
- border-radius: 6px;
1999
- box-shadow: 0 3px 6px rgba(0,0,0,0.06);
2000
- ">
2001
- <h4 style="margin-top: 0; color: #003DA5; text-align: center;">🔍 Insight Summary</h4>
2002
- <p style="margin-bottom: 0; line-height: 1.6; font-size: 0.98em;">{insight_text}</p>
2003
- </div>
2004
- """,
2005
- unsafe_allow_html=True
2006
- )
2007
-
2008
- # === GENERATE RECOMMENDATION & MITIGATION VIA LLM (DeepSeek-7B) ===
2009
- @st.cache_resource
2010
- def get_pipe():
2011
- return pipeline(
2012
- "text-generation",
2013
- model="deepseek-ai/deepseek-llm-7b-chat",
2014
- device_map="auto",
2015
- torch_dtype="auto",
2016
- trust_remote_code=True
2017
- )
2018
 
2019
- pipe = get_pipe()
 
 
 
 
 
 
 
2020
 
2021
- def safe_llm_call(insight: str, mode: str = "rec"): # mode: 'rec' or 'mit'
2022
- sys_msg = (
2023
- "You are PLN's Lead Safety AI. Output ONLY a short, professional sentence. "
2024
- "Be directive, system-aware, and action-oriented. No markdown, no emoticons."
2025
- )
2026
- if mode == "rec":
2027
- sys_msg += " Focus on high-leverage intervention: 'Deploy...', 'Establish...', 'Launch...'"
2028
- else:
2029
- sys_msg += " Focus on automated/systemic control: 'Enforce...', 'Trigger...', 'Block...'"
2030
-
2031
- prompt = f"""<|begin▁of▁sentence|><|User|>Insight: {insight}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2032
 
2033
- {sys_msg}
2034
- <|Assistant|>"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2035
 
2036
  try:
2037
  out = pipe(
2038
- prompt,
2039
- max_new_tokens=128,
2040
  do_sample=False,
2041
  temperature=0.1,
2042
  return_full_text=False
2043
  )
2044
  text = out[0]["generated_text"].strip()
2045
- # Clean common artifacts
2046
- text = re.sub(r"^.*?:\s*", "", text) # remove "Recommendation: "
2047
- text = re.sub(r"[\n\"`]", " ", text).strip()
2048
- if len(text) < 10 or len(text) > 200:
2049
- raise ValueError("Invalid length")
2050
- return text
2051
  except Exception as e:
2052
- # Fallback still your preferred phrasing
 
2053
  fallbacks = {
2054
  ("1", "rec"): "Launch Agency Activation Sprint: ≥1 spot inspection/week per low-ratio location.",
2055
- ("1", "mit"): "Deploy QR-code checklists + WhatsApp reminders; target ratio ≥0.5 in 45 days.",
2056
- ("2", "rec"): "Activate Agentic Capacity Dashboard for real-time ratio & lead-time monitoring.",
2057
- ("2", "mit"): "Auto-trigger coaching alerts if deviation >20% from divisional baseline.",
2058
  ("3", "rec"): "Enforce photo-based validation for all Unsafe Condition/Action/Near Miss submissions.",
2059
  ("3", "mit"): "System blocks submission if photo evidence or justification is missing.",
2060
- ("4", "rec"): "Assign dedicated safety crews to Quadrant I; enforce ‘One Finding, One Day’ for Quadrant II.",
2061
- ("4", "mit"): "Auto-generate VP escalation reports if division remains in QI/QII ≥2 months.",
2062
- ("5", "rec"): "Form cross-functional RCA Task Force (Civil, Electrical, HSE, Contractors) for top categories.",
2063
  ("5", "mit"): "Update tender templates: all bids must include mitigations for these historical findings.",
2064
  }
2065
- return fallbacks.get((insight_lines.index(insight)+1 if insight in insight_lines else "1", mode),
2066
- "Review insight and implement targeted action.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2067
 
2068
- # Extract clean insight strings (for LLM input)
2069
- clean_insights = []
2070
  if dev["lowest_ratio_9_locs"]:
2071
- locs = ", ".join([f"{loc} ({ratio})" for loc, ratio in dev["lowest_ratio_9_locs"]])
2072
- clean_insights.append(f"Nine locations with the lowest finding-to-reporter ratio: {locs}.")
2073
 
2074
- parts_desc = []
2075
  if dev["obj3a_lowest_div"]:
2076
- parts_desc.append(f"division {dev['obj3a_lowest_div'][0]} (ratio: {dev['obj3a_lowest_div'][1]})")
 
2077
  if dev["obj3c_lowest_reporter"]:
2078
- parts_desc.append(f"reporter {dev['obj3c_lowest_reporter'][0]} ({dev['obj3c_lowest_reporter'][1]} findings/month)")
 
2079
  if dev["obj3d_slowest_div"]:
2080
- parts_desc.append(f"division {dev['obj3d_slowest_div'][0]} (avg. resolution: {dev['obj3d_slowest_div'][1]} days)")
 
2081
  if dev["obj3b_slowest_executor"]:
2082
- parts_desc.append(f"executor {dev['obj3b_slowest_executor'][0]} (avg. resolution: {dev['obj3b_slowest_executor'][1]} days)")
2083
- if parts_desc:
2084
- clean_insights.append(f"Uneven operational capacity: {'; '.join(parts_desc)}.")
 
 
 
 
 
2085
 
 
2086
  if uc + ua + nm > 0:
2087
- clean_insights.append(f"Non-Positive finding composition: Unsafe Condition ({uc}%), Unsafe Action ({ua}%), Near Miss ({nm}%).")
 
 
2088
 
2089
  if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
2090
- q1 = ", ".join([d for d in dev["obj5_q1_divs"][:3]]) or "—"
2091
- q2 = ", ".join([d for d in dev["obj5_q2_divs"][:3]]) or "—"
2092
- clean_insights.append(f"High-risk divisions (Q1): {q1}; Hidden-risk divisions (Q2): {q2}.")
2093
 
2094
  if dev["obj6_top2_categories"]:
2095
  c1, c2 = dev["obj6_top2_categories"]
2096
- clean_insights.append(f"Top non-Positive categories: {c1[0]} ({c1[1]}/month) and {c2[0]} ({c2[1]}/month).")
2097
-
2098
- # Generate paired rec & mit
2099
- rec_lines, mit_lines = [], []
2100
- with st.spinner("🧠 Generating Recommendation & Risk Mitigation with DeepSeek-7B..."):
2101
- for i, ins in enumerate(clean_insights, 1):
2102
- rec = safe_llm_call(ins, "rec")
2103
- mit = safe_llm_call(ins, "mit")
2104
- rec_lines.append(f"{i}. {rec}")
2105
- mit_lines.append(f"{i}. {mit}")
2106
 
2107
- rec_text = "<br>".join(rec_lines)
2108
- mit_text = "<br>".join(mit_lines)
2109
 
2110
- # === RENDER RECOMMENDATION CARD ===
 
2111
  st.markdown(
2112
  f"""
2113
  <div class="card" style="
2114
- background-color: #e8f5e9;
2115
- border-left: 4px solid #4CAF50;
2116
- padding: 18px;
2117
  margin-bottom: 24px;
2118
  border-radius: 6px;
2119
  box-shadow: 0 3px 6px rgba(0,0,0,0.06);
2120
  ">
2121
- <h4 style="margin-top: 0; color: #2E7D32; text-align: center;">✅ Recommendation</h4>
2122
- <p style="margin-bottom: 0; line-height: 1.6; font-size: 0.98em;">{rec_text}</p>
2123
  </div>
2124
  """,
2125
  unsafe_allow_html=True
2126
  )
2127
 
2128
- # === RENDER RISK MITIGATION CARD ===
2129
- st.markdown(
2130
- f"""
2131
- <div class="card" style="
2132
- background-color: #e3f2fd;
2133
- border-left: 4px solid #1976D2;
2134
- padding: 18px;
2135
- margin-bottom: 24px;
2136
- border-radius: 6px;
2137
- box-shadow: 0 3px 6px rgba(0,0,0,0.06);
2138
- ">
2139
- <h4 style="margin-top: 0; color: #0D47A1; text-align: center;">🛡️ Risk Mitigation Strategy</h4>
2140
- <p style="margin-bottom: 0; line-height: 1.6; font-size: 0.98em;">{mit_text}</p>
2141
- </div>
2142
- """,
2143
- unsafe_allow_html=True
2144
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1987
  st.info("No data available for non-positive issue categories with 100% coverage and positive trend.")
1988
  # =================== OBJECTIVE 7 — Insight and Recommendation (Agentic AI LLM Style — Final) ===================
1989
  # =================== OBJECTIVE 7 — Insight and Recommendation (Final — Agentic AI, No markdown bold) ===================
1990
+ import streamlit as st
1991
+ import pandas as pd
1992
+ import re
1993
+ import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1994
 
1995
+ # ==============================
1996
+ # 1. IMPORT & INSTALL CHECK
1997
+ # ==============================
1998
+ try:
1999
+ from transformers import pipeline
2000
+ except ImportError:
2001
+ st.error("❌ `transformers` not installed. Run: `pip install transformers torch accelerate sentencepiece einops`")
2002
+ st.stop()
2003
 
2004
+ # ==============================
2005
+ # 2. LOAD LLM (Phi-3-mini — ringan & stabil)
2006
+ # ==============================
2007
+ @st.cache_resource
2008
+ def load_llm():
2009
+ try:
2010
+ st.info("🧠 Loading Phi-3-mini-4k-instruct (optimized for safety recommendations)...")
2011
+ pipe = pipeline(
2012
+ "text-generation",
2013
+ model="microsoft/Phi-3-mini-4k-instruct",
2014
+ device_map="auto",
2015
+ torch_dtype="auto",
2016
+ trust_remote_code=True,
2017
+ max_new_tokens=256
2018
+ )
2019
+ st.success("✅ Phi-3-mini loaded!")
2020
+ return pipe
2021
+ except Exception as e:
2022
+ st.error(f"❌ Failed to load model: {e}")
2023
+ st.stop()
2024
+
2025
+ pipe = load_llm()
2026
+
2027
+ # ==============================
2028
+ # 3. INSIGHT EXTRACTION (sama seperti kode Anda)
2029
+ # ==============================
2030
+ def extract_agentic_insights_v5(df: pd.DataFrame):
2031
+ dev = {
2032
+ "lowest_ratio_9_locs": [],
2033
+ "obj3a_lowest_div": None,
2034
+ "obj3b_slowest_executor": None,
2035
+ "obj3c_lowest_reporter": None,
2036
+ "obj3d_slowest_div": None,
2037
+ "obj4_unsafe_condition_pct": 0.0,
2038
+ "obj4_unsafe_action_pct": 0.0,
2039
+ "obj4_near_miss_pct": 0.0,
2040
+ "obj5_q1_divs": [],
2041
+ "obj5_q2_divs": [],
2042
+ "obj6_top2_categories": [],
2043
+ }
2044
 
2045
+ # === 1. 9 locations with lowest finding-to-reporter ratio ===
2046
+ if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2047
+ calc = df[['nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2048
+ calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
2049
+ calc = calc.dropna(subset=['created_at', 'nama_lokasi_full', 'creator_nid'])
2050
+ calc['bulan'] = calc['created_at'].dt.to_period('M')
2051
+ monthly = calc.groupby(['nama_lokasi_full', 'bulan']).agg(
2052
+ findings=('kode_temuan', 'size'),
2053
+ reporters=('creator_nid', 'nunique')
2054
+ ).reset_index()
2055
+ monthly = monthly[monthly['reporters'] > 0]
2056
+ monthly['ratio'] = monthly['findings'] / monthly['reporters']
2057
+ loc_avg = monthly.groupby('nama_lokasi_full')['ratio'].mean()
2058
+ lowest_9 = loc_avg.nsmallest(9)
2059
+ dev["lowest_ratio_9_locs"] = [(loc, round(ratio, 2)) for loc, ratio in lowest_9.items()]
2060
+
2061
+ # === 2a: Division — lowest ratio ===
2062
+ if {'nama', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2063
+ calc = df[['nama', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2064
+ calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
2065
+ agg = calc.groupby(['nama', 'bulan']).agg(
2066
+ findings=('kode_temuan', 'size'),
2067
+ reporters=('creator_nid', 'nunique')
2068
+ )
2069
+ agg = agg[agg['reporters'] > 0].reset_index()
2070
+ agg['ratio'] = agg['findings'] / agg['reporters']
2071
+ div_ratio = agg.groupby('nama')['ratio'].mean()
2072
+ if not div_ratio.empty:
2073
+ name = div_ratio.idxmin()
2074
+ val = round(div_ratio.min(), 2)
2075
+ dev["obj3a_lowest_div"] = (name, val)
2076
+
2077
+ # === 2b: Executor — slowest resolution ===
2078
+ if 'days_to_close' in df.columns:
2079
+ valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2080
+ exec_col = 'nama_pic' if 'nama_pic' in valid.columns else 'creator_name'
2081
+ if exec_col in valid.columns:
2082
+ lead = valid.groupby(exec_col)['days_to_close'].mean()
2083
+ if not lead.empty:
2084
+ name = lead.idxmax()
2085
+ val = round(lead.max(), 2)
2086
+ dev["obj3b_slowest_executor"] = (name, val)
2087
+
2088
+ # === 2c: Reporter — lowest frequency ===
2089
+ if {'creator_name', 'created_at'}.issubset(df.columns):
2090
+ calc = df[['creator_name', 'created_at']].copy()
2091
+ calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
2092
+ monthly = calc.groupby(['creator_name', 'bulan']).size().reset_index(name='count')
2093
+ avg = monthly.groupby('creator_name')['count'].mean()
2094
+ avg = avg[avg > 0]
2095
+ if not avg.empty:
2096
+ name = avg.idxmin()
2097
+ val = round(avg.min(), 2)
2098
+ dev["obj3c_lowest_reporter"] = (name, val)
2099
+
2100
+ # === 2d: Division — slowest resolution ===
2101
+ if 'days_to_close' in df.columns and 'nama' in df.columns:
2102
+ valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2103
+ if not valid.empty:
2104
+ lead = valid.groupby('nama')['days_to_close'].mean()
2105
+ if not lead.empty:
2106
+ name = lead.idxmax()
2107
+ val = round(lead.max(), 2)
2108
+ dev["obj3d_slowest_div"] = (name, val)
2109
+
2110
+ # === 3. Non-Positive composition ===
2111
+ if 'temuan_kategori' in df.columns:
2112
+ cnt = df['temuan_kategori'].value_counts(normalize=True) * 100
2113
+ dev["obj4_unsafe_condition_pct"] = round(cnt.get("Unsafe Condition", 0), 2)
2114
+ dev["obj4_unsafe_action_pct"] = round(cnt.get("Unsafe Action", 0), 2)
2115
+ dev["obj4_near_miss_pct"] = round(cnt.get("Near Miss", 0), 2)
2116
+
2117
+ # === 4. Risk Quadrants ===
2118
+ X_LIMIT, Y_LIMIT = 20, 3
2119
+ if {'nama', 'created_at', 'days_to_close', 'kode_temuan'}.issubset(df.columns):
2120
+ calc = df.copy()
2121
+ calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
2122
+ calc = calc.assign(month=calc['created_at'].dt.to_period('M').astype(str))
2123
+ monthly_counts = calc.groupby(['nama', 'month'])['kode_temuan'].nunique().reset_index()
2124
+ avg_count = monthly_counts.groupby('nama')['kode_temuan'].mean().reset_index(name='Finding Count')
2125
+ leadtime = calc.groupby('nama')['days_to_close'].mean().reset_index(name='Avg Lead Time')
2126
+ mat = avg_count.merge(leadtime, on='nama', how='left').fillna(0)
2127
+ for _, r in mat.iterrows():
2128
+ if r['Finding Count'] >= X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
2129
+ dev["obj5_q1_divs"].append(r['nama'])
2130
+ elif r['Finding Count'] < X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
2131
+ dev["obj5_q2_divs"].append(r['nama'])
2132
+
2133
+ # === 5. Top 2 non-Positive categories ===
2134
+ if {'kategori', 'temuan_kategori', 'created_at'}.issubset(df.columns):
2135
+ nonpos = df[df['temuan_kategori'] != 'Positive']
2136
+ if not nonpos.empty:
2137
+ start = nonpos['created_at'].min().to_period('M')
2138
+ end = nonpos['created_at'].max().to_period('M')
2139
+ n_months = len(pd.period_range(start=start, end=end, freq='M'))
2140
+ cat_avg = (nonpos.groupby('kategori').size() / n_months).sort_values(ascending=False).head(2)
2141
+ dev["obj6_top2_categories"] = [(cat, round(val, 2)) for cat, val in cat_avg.items()]
2142
+
2143
+ return dev
2144
+
2145
+ # ==============================
2146
+ # 4. LLM UTILS (aman & cepat)
2147
+ # ==============================
2148
+ def generate_llm_text(insight: str, mode: str = "rec") -> str:
2149
+ """Generate rec or mit text using Phi-3-mini."""
2150
+ suffix = "Recommend a single high-leverage action." if mode == "rec" else "Propose one automated/systemic risk control."
2151
+ messages = [
2152
+ {"role": "system", "content": "You are PLN's Lead Safety AI. Output ONLY a short, professional sentence. Be directive. No markdown, no emoticons."},
2153
+ {"role": "user", "content": f"Insight: {insight}\n\n{suffix}"}
2154
+ ]
2155
 
2156
  try:
2157
  out = pipe(
2158
+ messages,
 
2159
  do_sample=False,
2160
  temperature=0.1,
2161
  return_full_text=False
2162
  )
2163
  text = out[0]["generated_text"].strip()
2164
+ # Clean
2165
+ text = re.sub(r"^(Recommendation|Mitigation|Action|Control):\s*", "", text, flags=re.IGNORECASE)
2166
+ text = re.sub(r"[\n\"`*]", " ", text).strip(". ")
2167
+ return text[:250] # Batas panjang
 
 
2168
  except Exception as e:
2169
+ st.warning(f"LLM fallback for {mode}: {e}")
2170
+ # Fallback — tetap profesional & sesuai gaya Anda
2171
  fallbacks = {
2172
  ("1", "rec"): "Launch Agency Activation Sprint: ≥1 spot inspection/week per low-ratio location.",
2173
+ ("1", "mit"): "Deploy QR-code checklists + automated reminders; target ratio ≥0.5 in 45 days.",
2174
+ ("2", "rec"): "Activate Agentic Capacity Dashboard for real-time monitoring of reporter engagement and resolution efficiency.",
2175
+ ("2", "mit"): "Auto-trigger coaching alerts if performance deviates >20% from divisional baseline.",
2176
  ("3", "rec"): "Enforce photo-based validation for all Unsafe Condition/Action/Near Miss submissions.",
2177
  ("3", "mit"): "System blocks submission if photo evidence or justification is missing.",
2178
+ ("4", "rec"): "Assign dedicated safety crews to Quadrant I; enforce ‘One Finding, One Day’ closure for Quadrant II.",
2179
+ ("4", "mit"): "Auto-generate VP escalation reports if division remains in risk quadrant ≥2 months.",
2180
+ ("5", "rec"): "Form cross-functional RCA Task Force (Civil, Electrical, HSE, Contractors) for top recurring categories.",
2181
  ("5", "mit"): "Update tender templates: all bids must include mitigations for these historical findings.",
2182
  }
2183
+ return fallbacks.get((str(len(insight_list) + 1), mode), "Review insight and implement targeted action.")
2184
+
2185
+ # ==============================
2186
+ # 5. MAIN EXECUTION
2187
+ # ==============================
2188
+ st.markdown("<h3 class='section-title'>OBJECTIVE 7 — Insight and Recommendation</h3>", unsafe_allow_html=True)
2189
+
2190
+ # ✅ Pastikan df_filtered ada
2191
+ if 'df_filtered' not in st.session_state:
2192
+ st.error("⚠️ `df_filtered` not found in session state. Please load data first.")
2193
+ st.stop()
2194
+
2195
+ df_filtered = st.session_state.df_filtered
2196
+ dev = extract_agentic_insights_v5(df_filtered)
2197
+
2198
+ # === BUILD INSIGHT LINES (2 desimal, clean) ===
2199
+ insight_lines = []
2200
 
 
 
2201
  if dev["lowest_ratio_9_locs"]:
2202
+ loc_list = ", ".join([f"<strong>{loc}</strong> ({ratio:.2f})" for loc, ratio in dev["lowest_ratio_9_locs"]])
2203
+ insight_lines.append(f"1. Nine locations with the <em>lowest</em> finding-to-reporter ratio: {loc_list}.")
2204
 
2205
+ parts = []
2206
  if dev["obj3a_lowest_div"]:
2207
+ name, val = dev["obj3a_lowest_div"]
2208
+ parts.append(f"division <strong>{name}</strong> (ratio: {val:.2f})")
2209
  if dev["obj3c_lowest_reporter"]:
2210
+ name, val = dev["obj3c_lowest_reporter"]
2211
+ parts.append(f"reporter <strong>{name}</strong> ({val:.2f} findings/month)")
2212
  if dev["obj3d_slowest_div"]:
2213
+ name, val = dev["obj3d_slowest_div"]
2214
+ parts.append(f"division <strong>{name}</strong> (avg. resolution: {val:.2f} days)")
2215
  if dev["obj3b_slowest_executor"]:
2216
+ name, val = dev["obj3b_slowest_executor"]
2217
+ parts.append(f"executor <strong>{name}</strong> (avg. resolution: {val:.2f} days)")
2218
+
2219
+ if parts:
2220
+ insight_lines.append(
2221
+ f"2. Uneven operational capacity detected: {'; '.join(parts)}. "
2222
+ "This indicates systemic imbalance in reporting engagement and resolution efficiency."
2223
+ )
2224
 
2225
+ uc, ua, nm = dev["obj4_unsafe_condition_pct"], dev["obj4_unsafe_action_pct"], dev["obj4_near_miss_pct"]
2226
  if uc + ua + nm > 0:
2227
+ insight_lines.append(
2228
+ f"3. Non-Positive finding composition: Unsafe Condition ({uc:.2f}%), Unsafe Action ({ua:.2f}%), Near Miss ({nm:.2f}%)."
2229
+ )
2230
 
2231
  if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
2232
+ q1 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q1_divs"][:3]]) or "—"
2233
+ q2 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q2_divs"][:3]]) or "—"
2234
+ insight_lines.append(f"4. High-risk divisions (Q1): {q1}; Hidden-risk divisions (Q2): {q2}.")
2235
 
2236
  if dev["obj6_top2_categories"]:
2237
  c1, c2 = dev["obj6_top2_categories"]
2238
+ insight_lines.append(
2239
+ f"5. Top two recurring non-Positive categories: <strong>{c1[0]}</strong> ({c1[1]:.2f}/month) and <strong>{c2[0]}</strong> ({c2[1]:.2f}/month)."
2240
+ )
 
 
 
 
 
 
 
2241
 
2242
+ insight_text = "<br>".join(insight_lines)
 
2243
 
2244
+ # === RENDER 3 CARDS ===
2245
+ # Card 1: Insight
2246
  st.markdown(
2247
  f"""
2248
  <div class="card" style="
2249
+ background-color: #f8f9fa;
2250
+ border-left: 5px solid #003DA5;
2251
+ padding: 20px;
2252
  margin-bottom: 24px;
2253
  border-radius: 6px;
2254
  box-shadow: 0 3px 6px rgba(0,0,0,0.06);
2255
  ">
2256
+ <h4 style="margin-top: 0; color: #003DA5; text-align: center;">🔍 Insight Summary</h4>
2257
+ <p style="margin-bottom: 0; line-height: 1.6; font-size: 0.98em;">{insight_text}</p>
2258
  </div>
2259
  """,
2260
  unsafe_allow_html=True
2261
  )
2262
 
2263
+ if insight_lines:
2264
+ # Generate rec & mit
2265
+ rec_list, mit_list = [], []
2266
+ with st.spinner("🧠 Generating Recommendation & Risk Mitigation with Phi-3-mini..."):
2267
+ for i, ins in enumerate(insight_lines, 1):
2268
+ # Ekstrak teks bersih untuk LLM
2269
+ clean_ins = re.sub(r"<[^>]+>", "", ins).replace("1. ", "").replace("2. ", "").replace("3. ", "").replace("4. ", "").replace("5. ", "").strip()
2270
+ rec = generate_llm_text(clean_ins, "rec")
2271
+ mit = generate_llm_text(clean_ins, "mit")
2272
+ rec_list.append(f"{i}. {rec}")
2273
+ mit_list.append(f"{i}. {mit}")
2274
+
2275
+ rec_text = "<br>".join(rec_list)
2276
+ mit_text = "<br>".join(mit_list)
2277
+
2278
+ # Card 2: Recommendation
2279
+ st.markdown(
2280
+ f"""
2281
+ <div class="card" style="
2282
+ background-color: #e8f5e9;
2283
+ border-left: 5px solid #4CAF50;
2284
+ padding: 20px;
2285
+ margin-bottom: 24px;
2286
+ border-radius: 6px;
2287
+ box-shadow: 0 3px 6px rgba(0,0,0,0.06);
2288
+ ">
2289
+ <h4 style="margin-top: 0; color: #2E7D32; text-align: center;">✅ Recommendation</h4>
2290
+ <p style="margin-bottom: 0; line-height: 1.6; font-size: 0.98em;">{rec_text}</p>
2291
+ </div>
2292
+ """,
2293
+ unsafe_allow_html=True
2294
+ )
2295
+
2296
+ # Card 3: Risk Mitigation
2297
+ st.markdown(
2298
+ f"""
2299
+ <div class="card" style="
2300
+ background-color: #e3f2fd;
2301
+ border-left: 5px solid #1976D2;
2302
+ padding: 20px;
2303
+ margin-bottom: 24px;
2304
+ border-radius: 6px;
2305
+ box-shadow: 0 3px 6px rgba(0,0,0,0.06);
2306
+ ">
2307
+ <h4 style="margin-top: 0; color: #0D47A1; text-align: center;">🛡️ Risk Mitigation Strategy</h4>
2308
+ <p style="margin-bottom: 0; line-height: 1.6; font-size: 0.98em;">{mit_text}</p>
2309
+ </div>
2310
+ """,
2311
+ unsafe_allow_html=True
2312
+ )
2313
+ else:
2314
+ st.info("ℹ️ No insights generated. Ensure required columns are present in the dataset.")