Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 1999 |
-
|
|
|
|
|
|
|
| 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
|
| 2015 |
if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
|
| 2016 |
-
calc = df
|
| 2017 |
calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
|
| 2018 |
-
calc = calc.dropna(subset=['created_at', '
|
| 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 |
-
|
| 2028 |
-
|
|
|
|
| 2029 |
|
| 2030 |
-
# ===
|
| 2031 |
if {'nama', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
|
| 2032 |
-
calc = df
|
| 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 |
-
|
|
|
|
| 2039 |
agg['ratio'] = agg['findings'] / agg['reporters']
|
|
|
|
| 2040 |
div_ratio = agg.groupby('nama')['ratio'].mean()
|
| 2041 |
if not div_ratio.empty:
|
| 2042 |
-
|
| 2043 |
-
val = round(div_ratio.min(), 2)
|
| 2044 |
-
dev["obj3a_lowest_div"] = (name, val)
|
| 2045 |
|
| 2046 |
-
#
|
| 2047 |
if 'days_to_close' in df.columns:
|
| 2048 |
-
valid = df[df['days_to_close']
|
| 2049 |
-
exec_col = 'nama_pic' if 'nama_pic' in
|
|
|
|
| 2050 |
if exec_col in valid.columns:
|
| 2051 |
lead = valid.groupby(exec_col)['days_to_close'].mean()
|
| 2052 |
-
|
| 2053 |
-
name = lead.idxmax()
|
| 2054 |
-
val = round(lead.max(), 2)
|
| 2055 |
-
dev["obj3b_slowest_executor"] = (name, val)
|
| 2056 |
|
| 2057 |
-
#
|
| 2058 |
if {'creator_name', 'created_at'}.issubset(df.columns):
|
| 2059 |
-
calc = df
|
| 2060 |
calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
|
| 2061 |
-
monthly = calc.groupby(['creator_name', 'bulan']).size()
|
| 2062 |
-
avg = monthly.groupby('creator_name')
|
| 2063 |
-
|
| 2064 |
-
|
| 2065 |
-
|
| 2066 |
-
|
| 2067 |
-
|
| 2068 |
-
|
| 2069 |
-
|
| 2070 |
-
|
| 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),
|
| 2083 |
-
dev["obj4_unsafe_action_pct"] = round(cnt.get("Unsafe Action", 0),
|
| 2084 |
-
dev["obj4_near_miss_pct"] = round(cnt.get("Near Miss", 0),
|
| 2085 |
|
| 2086 |
-
# === 4.
|
| 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']
|
| 2091 |
-
calc = calc
|
| 2092 |
-
|
| 2093 |
-
|
| 2094 |
-
|
| 2095 |
-
|
| 2096 |
-
|
| 2097 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2098 |
dev["obj5_q1_divs"].append(r['nama'])
|
| 2099 |
-
elif r['Finding Count'] <
|
| 2100 |
dev["obj5_q2_divs"].append(r['nama'])
|
| 2101 |
|
| 2102 |
-
# === 5.
|
| 2103 |
if {'kategori', 'temuan_kategori', 'created_at'}.issubset(df.columns):
|
| 2104 |
-
nonpos = df[df['temuan_kategori'] !=
|
| 2105 |
-
|
| 2106 |
-
|
| 2107 |
-
|
| 2108 |
-
|
| 2109 |
-
|
| 2110 |
-
|
| 2111 |
|
| 2112 |
return dev
|
| 2113 |
|
| 2114 |
-
# === Jalankan ekstraksi ===
|
| 2115 |
-
dev = extract_agentic_insights_v5(df_filtered)
|
| 2116 |
|
| 2117 |
-
#
|
| 2118 |
-
|
|
|
|
| 2119 |
|
| 2120 |
-
|
| 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 |
-
#
|
| 2131 |
-
|
| 2132 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 2189 |
-
|
| 2190 |
-
|
| 2191 |
-
|
| 2192 |
-
|
| 2193 |
-
|
| 2194 |
-
|
| 2195 |
-
|
| 2196 |
-
|
| 2197 |
-
|
| 2198 |
-
|
| 2199 |
-
|
| 2200 |
-
|
| 2201 |
-
|
| 2202 |
-
|
| 2203 |
-
|
| 2204 |
-
|
| 2205 |
-
|
| 2206 |
-
|
| 2207 |
-
|
| 2208 |
-
|
| 2209 |
-
|
| 2210 |
-
|
| 2211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2212 |
<thead>
|
| 2213 |
-
<tr style="background
|
| 2214 |
-
<th
|
| 2215 |
-
<th
|
| 2216 |
-
<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 |
-
{
|
| 2222 |
</tbody>
|
| 2223 |
</table>
|
| 2224 |
</div>
|
| 2225 |
-
"""
|
| 2226 |
-
|
| 2227 |
-
|
| 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 |
+
)
|
|
|