SHELLAPANDIANGANHUNGING commited on
Commit
c944c31
·
verified ·
1 Parent(s): a6dca12

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -171
app.py CHANGED
@@ -1987,16 +1987,31 @@ else:
1987
  st.info("No data available for non-positive issue categories with 100% coverage and positive trend.")
1988
 
1989
  # =================== OBJECTIVE 7 — Insight and Recommendation ===================
1990
- st.markdown("<h3 class='section-title'>OBJECTIVE 7 — Insight and Recommendation</h3>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1991
 
1992
- # ✅ Pastikan df_filtered tersedia
1993
- if 'df_filtered' not in st.session_state:
1994
- st.error("⚠️ `df_filtered` not found in session state. Please ensure filters are applied.")
1995
- st.stop()
1996
- df_filtered = st.session_state.df_filtered
1997
 
1998
- # ✅ Definisi fungsi — dipastikan di global scope
1999
- def extract_agentic_insights_v5(df: pd.DataFrame):
 
 
2000
  dev = {
2001
  "lowest_ratio_9_locs": [],
2002
  "obj3a_lowest_div": None,
@@ -2011,218 +2026,182 @@ def extract_agentic_insights_v5(df: pd.DataFrame):
2011
  "obj6_top2_categories": [],
2012
  }
2013
 
2014
- # === 1. 9 locations with lowest finding-to-reporter ratio ===
2015
  if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2016
- calc = df[['nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2017
  calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
2018
- calc = calc.dropna(subset=['created_at', 'nama_lokasi_full', 'creator_nid'])
2019
  calc['bulan'] = calc['created_at'].dt.to_period('M')
 
2020
  monthly = calc.groupby(['nama_lokasi_full', 'bulan']).agg(
2021
  findings=('kode_temuan', 'size'),
2022
  reporters=('creator_nid', 'nunique')
2023
  ).reset_index()
 
2024
  monthly = monthly[monthly['reporters'] > 0]
2025
  monthly['ratio'] = monthly['findings'] / monthly['reporters']
 
2026
  loc_avg = monthly.groupby('nama_lokasi_full')['ratio'].mean()
2027
- lowest_9 = loc_avg.nsmallest(9)
2028
- dev["lowest_ratio_9_locs"] = [(loc, round(ratio, 2)) for loc, ratio in lowest_9.items()]
 
2029
 
2030
- # === 2a: Division lowest ratio ===
2031
  if {'nama', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2032
- calc = df[['nama', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2033
  calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
 
2034
  agg = calc.groupby(['nama', 'bulan']).agg(
2035
  findings=('kode_temuan', 'size'),
2036
  reporters=('creator_nid', 'nunique')
2037
- )
2038
- agg = agg[agg['reporters'] > 0].reset_index()
 
2039
  agg['ratio'] = agg['findings'] / agg['reporters']
 
2040
  div_ratio = agg.groupby('nama')['ratio'].mean()
2041
  if not div_ratio.empty:
2042
- name = div_ratio.idxmin()
2043
- val = round(div_ratio.min(), 2)
2044
- dev["obj3a_lowest_div"] = (name, val)
2045
 
2046
- # === 2b: Executor — slowest resolution ===
2047
  if 'days_to_close' in df.columns:
2048
- valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2049
- exec_col = 'nama_pic' if 'nama_pic' in valid.columns else 'creator_name'
 
2050
  if exec_col in valid.columns:
2051
  lead = valid.groupby(exec_col)['days_to_close'].mean()
2052
- if not lead.empty:
2053
- name = lead.idxmax()
2054
- val = round(lead.max(), 2)
2055
- dev["obj3b_slowest_executor"] = (name, val)
2056
 
2057
- # === 2c: Reporter — lowest frequency ===
2058
  if {'creator_name', 'created_at'}.issubset(df.columns):
2059
- calc = df[['creator_name', 'created_at']].copy()
2060
  calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
2061
- monthly = calc.groupby(['creator_name', 'bulan']).size().reset_index(name='count')
2062
- avg = monthly.groupby('creator_name')['count'].mean()
2063
- avg = avg[avg > 0]
2064
- if not avg.empty:
2065
- name = avg.idxmin()
2066
- val = round(avg.min(), 2)
2067
- dev["obj3c_lowest_reporter"] = (name, val)
2068
-
2069
- # === 2d: Division — slowest resolution ===
2070
- if 'days_to_close' in df.columns and 'nama' in df.columns:
2071
- valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2072
- if not valid.empty:
2073
- lead = valid.groupby('nama')['days_to_close'].mean()
2074
- if not lead.empty:
2075
- name = lead.idxmax()
2076
- val = round(lead.max(), 2)
2077
- dev["obj3d_slowest_div"] = (name, val)
2078
-
2079
- # === 3. Non-Positive composition ===
2080
  if 'temuan_kategori' in df.columns:
2081
  cnt = df['temuan_kategori'].value_counts(normalize=True) * 100
2082
- dev["obj4_unsafe_condition_pct"] = round(cnt.get("Unsafe Condition", 0), 2)
2083
- dev["obj4_unsafe_action_pct"] = round(cnt.get("Unsafe Action", 0), 2)
2084
- dev["obj4_near_miss_pct"] = round(cnt.get("Near Miss", 0), 2)
2085
 
2086
- # === 4. Risk Quadrants ===
2087
- X_LIMIT, Y_LIMIT = 20, 3
2088
  if {'nama', 'created_at', 'days_to_close', 'kode_temuan'}.issubset(df.columns):
2089
  calc = df.copy()
2090
- calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
2091
- calc = calc.assign(month=calc['created_at'].dt.to_period('M').astype(str))
2092
- monthly_counts = calc.groupby(['nama', 'month'])['kode_temuan'].nunique().reset_index()
2093
- avg_count = monthly_counts.groupby('nama')['kode_temuan'].mean().reset_index(name='Finding Count')
2094
- leadtime = calc.groupby('nama')['days_to_close'].mean().reset_index(name='Avg Lead Time')
2095
- mat = avg_count.merge(leadtime, on='nama', how='left').fillna(0)
2096
- for _, r in mat.iterrows():
2097
- if r['Finding Count'] >= X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
 
 
 
 
2098
  dev["obj5_q1_divs"].append(r['nama'])
2099
- elif r['Finding Count'] < X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
2100
  dev["obj5_q2_divs"].append(r['nama'])
2101
 
2102
- # === 5. Top 2 non-Positive categories ===
2103
  if {'kategori', 'temuan_kategori', 'created_at'}.issubset(df.columns):
2104
- nonpos = df[df['temuan_kategori'] != 'Positive']
2105
- if not nonpos.empty:
2106
- start = nonpos['created_at'].min().to_period('M')
2107
- end = nonpos['created_at'].max().to_period('M')
2108
- n_months = len(pd.period_range(start=start, end=end, freq='M'))
2109
- cat_avg = (nonpos.groupby('kategori').size() / n_months).sort_values(ascending=False).head(2)
2110
- dev["obj6_top2_categories"] = [(cat, round(val, 2)) for cat, val in cat_avg.items()]
2111
 
2112
  return dev
2113
 
2114
- # === Jalankan ekstraksi ===
2115
- dev = extract_agentic_insights_v5(df_filtered)
2116
 
2117
- # === Siapkan entri tabel ===
2118
- entries = []
 
2119
 
2120
- # 1. Low-ratio locations
2121
- if dev["lowest_ratio_9_locs"]:
2122
- loc_list = ", ".join([f"{loc} ({ratio:.2f})" for loc, ratio in dev["lowest_ratio_9_locs"]])
2123
- entries.append({
2124
- "Risk Category": "Reporting Coverage Risk",
2125
- "Insight": f"Nine locations with the lowest finding-to-reporter ratio: {loc_list}.",
2126
- "Recommendation": "Launch <em>Agency Activation Sprint</em>: assign Safety Champions to conduct ≥1 spot inspection/week per site.",
2127
- "Mitigation": "Deploy QR-code checklists + automated WhatsApp reminders. Target: ratio ≥0.5 within 45 days."
2128
- })
2129
 
2130
- # 2. Capacity imbalance
2131
- parts = []
2132
- if dev["obj3a_lowest_div"]:
2133
- name, val = dev["obj3a_lowest_div"]
2134
- parts.append(f"division <strong>{name}</strong> (ratio: {val:.2f})")
2135
- if dev["obj3c_lowest_reporter"]:
2136
- name, val = dev["obj3c_lowest_reporter"]
2137
- parts.append(f"reporter <strong>{name}</strong> ({val:.2f} findings/month)")
2138
- if dev["obj3d_slowest_div"]:
2139
- name, val = dev["obj3d_slowest_div"]
2140
- parts.append(f"division <strong>{name}</strong> (avg. resolution: {val:.2f} days)")
2141
- if dev["obj3b_slowest_executor"]:
2142
- name, val = dev["obj3b_slowest_executor"]
2143
- parts.append(f"executor <strong>{name}</strong> (avg. resolution: {val:.2f} days)")
2144
-
2145
- if parts:
2146
- insight = f"Uneven operational capacity detected: {'; '.join(parts)}."
2147
- entries.append({
2148
- "Risk Category": "Capacity Imbalance Risk",
2149
- "Insight": insight,
2150
- "Recommendation": "Activate <em>Agentic Capacity Dashboard</em> for real-time monitoring of reporting & resolution KPIs.",
2151
- "Mitigation": "Auto-trigger coaching alerts to Area PICs if deviation >20% from baseline, with peer benchmarking."
2152
- })
2153
 
2154
- # 3. Non-Positive composition
2155
- uc, ua, nm = dev["obj4_unsafe_condition_pct"], dev["obj4_unsafe_action_pct"], dev["obj4_near_miss_pct"]
2156
- if uc + ua + nm > 0:
2157
- insight = f"Non-Positive finding composition: Unsafe Condition ({uc:.2f}%), Unsafe Action ({ua:.2f}%), Near Miss ({nm:.2f}%)."
2158
- entries.append({
2159
- "Risk Category": "Data Quality & Categorization Risk",
2160
- "Insight": insight,
2161
- "Recommendation": "Enforce photo-based validation for all Unsafe Condition/Action/Near Miss submissions.",
2162
- "Mitigation": "System blocks submission if photo evidence or justification is missing."
2163
- })
2164
 
2165
- # 4. Risk Quadrants
2166
- if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
2167
- q1 = ", ".join([f"{d}" for d in dev["obj5_q1_divs"][:3]]) or "—"
2168
- q2 = ", ".join([f"{d}" for d in dev["obj5_q2_divs"][:3]]) or "—"
2169
- insight = f"High-risk divisions (Q1): {q1}; Hidden-risk divisions (Q2): {q2}."
2170
- entries.append({
2171
- "Risk Category": "SLA & Backlog Risk",
2172
- "Insight": insight,
2173
- "Recommendation": "Assign dedicated safety crews to QI divisions; enforce <em>One Finding, One Day</em> closure for QII.",
2174
- "Mitigation": "Auto-generate executive escalation reports to VP Ops if any division remains in QI/QII ≥2 months."
2175
- })
2176
 
2177
- # 5. Top categories
2178
- if dev["obj6_top2_categories"]:
2179
- c1, c2 = dev["obj6_top2_categories"]
2180
- insight = f"Top recurring non-Positive categories: <strong>{c1[0]}</strong> ({c1[1]:.2f}/month) and <strong>{c2[0]}</strong> ({c2[1]:.2f}/month)."
2181
- entries.append({
2182
- "Risk Category": "Recurring Hazard Risk",
2183
- "Insight": insight,
2184
- "Recommendation": f"Form cross-functional <em>RCA Task Force</em> (Civil, Electrical, HSE, Contractors) for <strong>{c1[0]}</strong> and <strong>{c2[0]}</strong>.",
2185
- "Mitigation": "Update tender templates: all bids must include historical mitigations for these categories."
2186
- })
2187
 
2188
- # === RENDER TABEL TERPADU ===
2189
- if entries:
2190
- rows = []
2191
- for e in entries:
2192
- rows.append(f"""
2193
- <tr>
2194
- <td style="padding:12px; font-weight:bold; vertical-align:top; background-color:#f8f9fa;">{e['Risk Category']}</td>
2195
- <td style="padding:12px; vertical-align:top; line-height:1.5;">{e['Insight']}</td>
2196
- <td style="padding:12px; vertical-align:top; line-height:1.5; color:#2E7D32;"><strong>▶</strong> {e['Recommendation']}</td>
2197
- <td style="padding:12px; vertical-align:top; line-height:1.5; color:#1976D2;"><strong>✓</strong> {e['Mitigation']}</td>
2198
- </tr>
2199
- """)
2200
-
2201
- table_html = f"""
2202
- <div class="card" style="
2203
- background-color: white;
2204
- border-left: 5px solid #003DA5;
2205
- padding: 20px;
2206
- margin-bottom: 24px;
2207
- border-radius: 6px;
2208
- box-shadow: 0 3px 8px rgba(0,0,0,0.08);
2209
- ">
2210
- <h4 style="margin-top: 0; color: #003DA5; text-align: center;">🔍 Objective 7 — Risk-Based Insight, Recommendation & Mitigation</h4>
2211
- <table style="width:100%; border-collapse:collapse; font-size:0.95em; margin-top:16px;">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2212
  <thead>
2213
- <tr style="background-color:#e3f2fd;">
2214
- <th style="padding:12px; text-align:center; border:1px solid #ccc; font-weight:600; color:#0D47A1;">Risk Category</th>
2215
- <th style="padding:12px; text-align:left; border:1px solid #ccc; font-weight:600; color:#0D47A1;">Insight</th>
2216
- <th style="padding:12px; text-align:left; border:1px solid #ccc; font-weight:600; color:#2E7D32;">Recommendation</th>
2217
- <th style="padding:12px; text-align:left; border:1px solid #ccc; font-weight:600; color:#0D47A1;">Risk Mitigation Strategy</th>
2218
  </tr>
2219
  </thead>
2220
  <tbody>
2221
- {"".join(rows)}
2222
  </tbody>
2223
  </table>
2224
  </div>
2225
- """
2226
- st.markdown(table_html, unsafe_allow_html=True)
2227
- else:
2228
- st.info("ℹ️ No actionable insights generated. Ensure required columns exist.")
 
1987
  st.info("No data available for non-positive issue categories with 100% coverage and positive trend.")
1988
 
1989
  # =================== OBJECTIVE 7 — Insight and Recommendation ===================
1990
+ import streamlit as st
1991
+ import pandas as pd
1992
+ import requests
1993
+
1994
+ # =========================
1995
+ # UNIVERSAL LLM CALL
1996
+ # =========================
1997
+ def llm_call(prompt: str):
1998
+ """Universal call untuk LLM (HF Docker / LM Studio / OpenAI)."""
1999
+ url = "http://localhost:1234/v1/chat/completions" # UBAH jika perlu
2000
+ payload = {
2001
+ "model": "Qwen2.5-7B-Instruct", # UBAH sesuai model
2002
+ "messages": [{"role": "user", "content": prompt}],
2003
+ "temperature": 0.3,
2004
+ "max_tokens": 700
2005
+ }
2006
+ r = requests.post(url, json=payload)
2007
+ r.raise_for_status()
2008
+ return r.json()["choices"][0]["message"]["content"]
2009
 
 
 
 
 
 
2010
 
2011
+ # =========================
2012
+ # EXTRACT INSIGHTS
2013
+ # =========================
2014
+ def extract_agentic_insights(df: pd.DataFrame):
2015
  dev = {
2016
  "lowest_ratio_9_locs": [],
2017
  "obj3a_lowest_div": None,
 
2026
  "obj6_top2_categories": [],
2027
  }
2028
 
2029
+ # === 1. 9 lowest location ratios ===
2030
  if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2031
+ calc = df.copy()
2032
  calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
2033
+ calc = calc.dropna(subset=['created_at', 'creator_nid'])
2034
  calc['bulan'] = calc['created_at'].dt.to_period('M')
2035
+
2036
  monthly = calc.groupby(['nama_lokasi_full', 'bulan']).agg(
2037
  findings=('kode_temuan', 'size'),
2038
  reporters=('creator_nid', 'nunique')
2039
  ).reset_index()
2040
+
2041
  monthly = monthly[monthly['reporters'] > 0]
2042
  monthly['ratio'] = monthly['findings'] / monthly['reporters']
2043
+
2044
  loc_avg = monthly.groupby('nama_lokasi_full')['ratio'].mean()
2045
+ lowest9 = loc_avg.nsmallest(9)
2046
+
2047
+ dev["lowest_ratio_9_locs"] = [(k, round(v, 3)) for k, v in lowest9.items()]
2048
 
2049
+ # === 2. Divisions & reporters ===
2050
  if {'nama', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2051
+ calc = df.copy()
2052
  calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
2053
+
2054
  agg = calc.groupby(['nama', 'bulan']).agg(
2055
  findings=('kode_temuan', 'size'),
2056
  reporters=('creator_nid', 'nunique')
2057
+ ).reset_index()
2058
+
2059
+ agg = agg[agg['reporters'] > 0]
2060
  agg['ratio'] = agg['findings'] / agg['reporters']
2061
+
2062
  div_ratio = agg.groupby('nama')['ratio'].mean()
2063
  if not div_ratio.empty:
2064
+ dev["obj3a_lowest_div"] = (div_ratio.idxmin(), round(div_ratio.min(), 2))
 
 
2065
 
2066
+ # Slowest executor
2067
  if 'days_to_close' in df.columns:
2068
+ valid = df[df['days_to_close'] >= 0]
2069
+ exec_col = 'nama_pic' if 'nama_pic' in df.columns else 'creator_name'
2070
+
2071
  if exec_col in valid.columns:
2072
  lead = valid.groupby(exec_col)['days_to_close'].mean()
2073
+ dev["obj3b_slowest_executor"] = (lead.idxmax(), round(lead.max(), 1))
 
 
 
2074
 
2075
+ # Lowest reporter
2076
  if {'creator_name', 'created_at'}.issubset(df.columns):
2077
+ calc = df.copy()
2078
  calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
2079
+ monthly = calc.groupby(['creator_name', 'bulan']).size()
2080
+ avg = monthly.groupby('creator_name').mean()
2081
+ dev["obj3c_lowest_reporter"] = (avg.idxmin(), round(avg.min(), 2))
2082
+
2083
+ # Slowest division (lead time)
2084
+ if {'nama', 'days_to_close'}.issubset(df.columns):
2085
+ lead = df.groupby('nama')['days_to_close'].mean()
2086
+ dev["obj3d_slowest_div"] = (lead.idxmax(), round(lead.max(), 1))
2087
+
2088
+ # === 3. Non positive %
 
 
 
 
 
 
 
 
 
2089
  if 'temuan_kategori' in df.columns:
2090
  cnt = df['temuan_kategori'].value_counts(normalize=True) * 100
2091
+ dev["obj4_unsafe_condition_pct"] = round(cnt.get("Unsafe Condition", 0), 1)
2092
+ dev["obj4_unsafe_action_pct"] = round(cnt.get("Unsafe Action", 0), 1)
2093
+ dev["obj4_near_miss_pct"] = round(cnt.get("Near Miss", 0), 1)
2094
 
2095
+ # === 4. Quadrants ===
 
2096
  if {'nama', 'created_at', 'days_to_close', 'kode_temuan'}.issubset(df.columns):
2097
  calc = df.copy()
2098
+ calc['created_at'] = pd.to_datetime(calc['created_at'])
2099
+ calc['month'] = calc['created_at'].dt.to_period('M').astype(str)
2100
+
2101
+ monthly_count = calc.groupby(['nama', 'month'])['kode_temuan'].size().reset_index(name='count')
2102
+ avg_count = monthly_count.groupby('nama')['count'].mean().reset_index(name='Finding Count')
2103
+ avg_lead = calc.groupby('nama')['days_to_close'].mean().reset_index(name='Avg Lead Time')
2104
+
2105
+ m = avg_count.merge(avg_lead, on='nama')
2106
+ X, Y = 20, 3
2107
+
2108
+ for _, r in m.iterrows():
2109
+ if r['Finding Count'] >= X and r['Avg Lead Time'] >= Y:
2110
  dev["obj5_q1_divs"].append(r['nama'])
2111
+ elif r['Finding Count'] < X and r['Avg Lead Time'] >= Y:
2112
  dev["obj5_q2_divs"].append(r['nama'])
2113
 
2114
+ # === 5. top 2 category ===
2115
  if {'kategori', 'temuan_kategori', 'created_at'}.issubset(df.columns):
2116
+ nonpos = df[df['temuan_kategori'] != "Positive"]
2117
+ start = nonpos['created_at'].min().to_period('M')
2118
+ end = nonpos['created_at'].max().to_period('M')
2119
+ n_months = len(pd.period_range(start, end, freq='M'))
2120
+ cat_avg = nonpos.groupby('kategori').size() / n_months
2121
+ cat_avg = cat_avg.sort_values(ascending=False).head(2)
2122
+ dev["obj6_top2_categories"] = [(k, round(v, 1)) for k, v in cat_avg.items()]
2123
 
2124
  return dev
2125
 
 
 
2126
 
2127
+ # =========================
2128
+ # RENDER + LLM GENERATION
2129
+ # =========================
2130
 
2131
+ dev = extract_agentic_insights(df_filtered)
 
 
 
 
 
 
 
 
2132
 
2133
+ # ======== BUILD TEXT FOR LLM ========
2134
+ prompt = f"""
2135
+ You are an advanced Safety Analytics LLM.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2136
 
2137
+ Given the following structured insights from real safety operational data:
 
 
 
 
 
 
 
 
 
2138
 
2139
+ {dev}
 
 
 
 
 
 
 
 
 
 
2140
 
2141
+ Your tasks:
 
 
 
 
 
 
 
 
 
2142
 
2143
+ 1. Write a concise **Insight Summary** (max 6 bullets). Use corporate tone and highlight anomalies.
2144
+ 2. Generate **5 Recommended Actions**, each 1–2 sentences.
2145
+ 3. Generate **5 Risk Mitigation Strategies**, each paired to each recommendation.
2146
+
2147
+ Return output in this JSON structure ONLY:
2148
+
2149
+ {{
2150
+ "insight_summary": "...",
2151
+ "recommendations": ["...", "...", "...", "...", "..."],
2152
+ "mitigations": ["...", "...", "...", "...", "..."]
2153
+ }}
2154
+ """
2155
+
2156
+ llm_output = llm_call(prompt)
2157
+
2158
+ import json
2159
+ out = json.loads(llm_output)
2160
+
2161
+ # ----------------------------
2162
+ # STREAMLIT RENDERING
2163
+ # ----------------------------
2164
+ st.markdown("<h3 class='section-title'>OBJECTIVE 7 — Insight and Recommendation</h3>", unsafe_allow_html=True)
2165
+
2166
+ # Insight card
2167
+ st.markdown(
2168
+ f"""
2169
+ <div style="background:#f8f9fa; border-left:4px solid #003DA5; padding:16px; border-radius:4px;">
2170
+ <h4 style="margin:0;color:#003DA5;">Insight Summary (LLM Generated)</h4>
2171
+ <p style="line-height:1.6;">{out['insight_summary'].replace("\n", "<br>")}</p>
2172
+ </div>
2173
+ """,
2174
+ unsafe_allow_html=True
2175
+ )
2176
+
2177
+ # Recommendations + Mitigations table
2178
+ rows = ""
2179
+ for i in range(5):
2180
+ rows += f"""
2181
+ <tr>
2182
+ <td style='text-align:center; font-weight:bold;'>{i+1}</td>
2183
+ <td style='padding:8px;'>{out['recommendations'][i]}</td>
2184
+ <td style='padding:8px;'>{out['mitigations'][i]}</td>
2185
+ </tr>
2186
+ """
2187
+
2188
+ st.markdown(
2189
+ f"""
2190
+ <div style="background:#e8f5e9; border-left:4px solid #4CAF50; padding:16px; border-radius:4px;">
2191
+ <h4 style="margin:0;color:#2E7D32;">Recommended Actions & Agentic Risk Mitigation (LLM)</h4>
2192
+ <table style="width:100%; border-collapse:collapse; margin-top:12px;">
2193
  <thead>
2194
+ <tr style="background:#d4efdf;">
2195
+ <th>#</th>
2196
+ <th>Recommended Action</th>
2197
+ <th>Risk Mitigation</th>
 
2198
  </tr>
2199
  </thead>
2200
  <tbody>
2201
+ {rows}
2202
  </tbody>
2203
  </table>
2204
  </div>
2205
+ """,
2206
+ unsafe_allow_html=True
2207
+ )