import streamlit as st
import sys
import os
import time
# ── Page Config ───────────────────────────────────────────────────
st.set_page_config(
page_title="SHADOW — Kenyan Fraud Intelligence",
page_icon="🛡️",
layout="wide",
initial_sidebar_state="collapsed"
)
# ── Styling ───────────────────────────────────────────────────────
st.markdown("""
""", unsafe_allow_html=True)
# ── Pipeline Import ───────────────────────────────────────────────
# Works whether run from project root (HF Spaces) or from app/ dir
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
from agents.pipeline import ShadowPipeline
PIPELINE_AVAILABLE = True
except ImportError:
PIPELINE_AVAILABLE = False
# ── Preset Messages ───────────────────────────────────────────────
PRESETS = {
"— Select a demo scenario —": "",
"🔴 Safaricom Impersonation": "Habari kutoka Safaricom. Laini yako inatumika na mtu mwingine (double registration). Piga *33*0000* kuzuia hii haraka au akaunti yako itafungwa ndani ya masaa 2.",
"🔴 KRA Penalty Threat": "KRA ALERT: Uko na tax arrears ya KES 23,450 kwa iTax system yako. Lipa ndani ya masaa 48 au utashtakiwa. Piga simu 0756XXXXXX sasa.",
"🟠 M-Pesa Reversal Scam": "Aki naomba urudishe ile pesa nimekutumia by mistake saa hii. Ni ya fees ya mtoto tafadhali. Tuma haraka 0712XXXXXX.",
"🟠 Fuliza Boost Scam": "KAMA ULIPATA FULIZA SEMA THANKS. Inbox nikuboostie fuliza from 0 to 100k in 2 minutes hii January hakuna stress.",
"🟡 Betting Jackpot Scam": "Hongera! Wewe ndio mshindi wa 500k SportPesa Weekly Jackpot. Tuma 2,500 ya registration fee kupokea pesa kwa MPESA yako leo.",
"🟡 WhatsApp OTP Theft": "Boss nisamehe, nilituma code ya WhatsApp kwa namba yako by mistake. Naomba unitumie hiyo code 6-digits haraka niingie kwa group ya kazi.",
"✅ Legitimate M-Pesa": "MPESA Confirmed. You have received Ksh 3,500.00 from JOHN KAMAU 0722XXXXXX on 8/5/26 at 10:23 AM. New M-PESA balance is Ksh 4,120.00.",
}
# ── Risk Color Helper ─────────────────────────────────────────────
def get_risk_color(level: str) -> str:
return {
"CRITICAL": "#ef4444",
"HIGH": "#f97316",
"MEDIUM": "#f59e0b",
"LOW": "#22c55e"
}.get(level, "#64748b")
def get_verdict_class(verdict: str) -> str:
if verdict == "SCAM":
return "verdict-scam"
elif verdict == "SUSPICIOUS":
return "verdict-suspicious"
return "verdict-safe"
def get_verdict_emoji(verdict: str) -> str:
return {"SCAM": "🚨", "SUSPICIOUS": "⚠️", "SAFE": "✅"}.get(verdict, "❓")
def get_trace_dot_color(agent: str, risk_hint: str) -> str:
if risk_hint in ["CRITICAL", "HIGH"]:
return "#ef4444"
elif risk_hint in ["MEDIUM"]:
return "#f59e0b"
elif agent == "OSINT PRECHECK":
return "#8b5cf6"
elif agent == "LANGUAGE AGENT":
return "#3b82f6"
elif agent == "THREAT AGENT":
return "#f97316"
elif agent == "RISK AGENT":
return "#ef4444"
elif agent == "ACTION AGENT":
return "#22c55e"
return "#64748b"
# ── Header ────────────────────────────────────────────────────────
st.markdown("""
◈ SHADOW
KENYAN FRAUD INTELLIGENCE SYSTEM
⚡ POWERED BY AMD INSTINCT MI300X + ROCm
""", unsafe_allow_html=True)
# ── Layout ────────────────────────────────────────────────────────
left_col, right_col = st.columns([1, 1.3], gap="large")
with left_col:
st.markdown("#### 📥 Analyze a Message")
# Preset selector
preset_choice = st.selectbox(
"Load a demo scenario",
options=list(PRESETS.keys()),
label_visibility="collapsed"
)
# Pre-fill text area from preset
default_text = PRESETS.get(preset_choice, "")
message = st.text_area(
"Message",
value=default_text,
height=160,
placeholder="Paste a suspicious SMS, WhatsApp message, or notification here...",
label_visibility="collapsed"
)
analyze_clicked = st.button("🔍 ANALYZE WITH SHADOW", use_container_width=True)
# Stats strip
st.markdown("
", unsafe_allow_html=True)
s1, s2, s3 = st.columns(3)
s1.metric("Scam Categories", "11")
s2.metric("Languages", "EN / SW / Sheng")
s3.metric("Pipeline Agents", "4")
st.markdown("---")
st.markdown("""
SHADOW uses a hybrid OSINT + 4-agent LLM pipeline to detect
Kenyan mobile fraud in real time. Qwen3 inference runs on
AMD Instinct MI300X via vLLM + ROCm.
""", unsafe_allow_html=True)
# ── Analysis Logic ─────────────────────────────────────────────────
with right_col:
if analyze_clicked:
if not message.strip():
st.warning("Please paste a message to analyze.")
else:
with st.spinner("Shadow is analyzing..."):
start = time.time()
if PIPELINE_AVAILABLE:
try:
pipeline = ShadowPipeline()
state = pipeline.run(message)
action = state.action_data or {}
risk = state.risk_data or {}
trace = state.execution_trace or []
elapsed = round(time.time() - start, 2)
except Exception as e:
st.error(f"Pipeline error: {str(e)}")
# Safe fallback
action = {
"verdict": "INCONCLUSIVE",
"risk_level": "UNKNOWN",
"scam_type": "Error",
"dashboard_summary": "An error occurred during analysis.",
"confidence": 0.0,
"explanation": {"red_flags_found": ["System error"]},
"recommended_actions": [],
"do_not_do": [],
"safety_tip": {},
"reporting": {}
}
risk = {"raw_score": 0}
trace = [{"agent": "SYSTEM", "step": 1, "summary": "Error running pipeline", "risk_hint": "UNKNOWN"}]
elapsed = round(time.time() - start, 2)
else:
# Fallback demo state if imports fail
action = {
"verdict": "SUSPICIOUS",
"risk_level": "MEDIUM",
"scam_type": "Pipeline Offline (Mock)",
"dashboard_summary": "This is a fallback response because the pipeline failed to load.",
"confidence": 0.50,
"explanation": {"red_flags_found": ["Mock execution"]},
"recommended_actions": [{"action": "Check system paths and imports"}],
"do_not_do": ["Trust this mock verdict"],
"safety_tip": {"english": "System is offline.", "swahili": "Mfumo haupatikani.", "sheng": "System iko chini."},
"reporting": {"should_report": False, "contacts": []}
}
risk = {"raw_score": 5}
trace = [{"agent": "MOCK AGENT", "step": 1, "summary": "Pipeline import failed", "risk_hint": "MEDIUM"}]
elapsed = 0.0
# Safe gets with empty defaults to prevent NoneType crashes
verdict = action.get("verdict") or "INCONCLUSIVE"
risk_level = action.get("risk_level") or "UNKNOWN"
scam_type = action.get("scam_type") or "Unknown"
summary = action.get("dashboard_summary") or ""
confidence = action.get("confidence")
if confidence is None:
confidence = 0.0
raw_score = risk.get("raw_score")
if raw_score is None:
raw_score = 0
explanation = action.get("explanation") or {}
red_flags = explanation.get("red_flags_found") or []
recommended = action.get("recommended_actions") or []
do_not = action.get("do_not_do") or []
safety_tip = action.get("safety_tip") or {}
reporting = action.get("reporting") or {}
# ── Verdict Card ──────────────────────────────────────
verdict_class = get_verdict_class(verdict)
verdict_emoji = get_verdict_emoji(verdict)
risk_color = get_risk_color(risk_level)
score_pct = min(int((raw_score / 10) * 100), 100)
st.markdown(f"""
{verdict_emoji} {verdict}
{summary}
{scam_type} | Confidence: {int(confidence*100)}% | {elapsed}s
""", unsafe_allow_html=True)
st.markdown("
", unsafe_allow_html=True)
# ── Risk Score Bar ────────────────────────────────────
st.markdown(f"""
⚡ Risk Score
Score: {raw_score}/10
{risk_level}
""", unsafe_allow_html=True)
# ── Two columns: Red Flags + Actions ──────────────────
c1, c2 = st.columns(2)
with c1:
flags_html = "".join([f'⚠ {f}
' for f in red_flags]) or 'None detected
'
st.markdown(f"""
🚩 Red Flags
{flags_html}
""", unsafe_allow_html=True)
with c2:
actions_html = ""
for a in recommended:
if isinstance(a, dict):
action_text = a.get("action", "")
if action_text:
actions_html += f'→ {action_text}
'
elif isinstance(a, str):
actions_html += f'→ {a}
'
donot_html = "".join([f'✗ {d}
' for d in do_not if isinstance(d, str)])
st.markdown(f"""
✅ What To Do
{actions_html}
{donot_html}
""", unsafe_allow_html=True)
# ── Execution Trace ───────────────────────────────────
trace_html = """
🧠 Agent Reasoning Timeline
"""
if not trace:
trace_html += '
No trace available.
'
for step in trace:
if not isinstance(step, dict):
continue
agent = step.get("agent") or "SYSTEM"
summary_text = step.get("summary") or ""
risk_hint = step.get("risk_hint") or ""
dot_color = get_trace_dot_color(agent, risk_hint)
trace_html += f"""
[{step.get('step', 0)}] {agent}
{summary_text}
"""
trace_html += "
"
st.markdown(trace_html, unsafe_allow_html=True)
# ── Safety Tip ────────────────────────────────────────
if safety_tip:
st.markdown(f"""
EN
{safety_tip.get('english', 'Not available')}
SW
{safety_tip.get('swahili', 'Haipatikani')}
SHENG
{safety_tip.get('sheng', 'Haiwezekani')}
""", unsafe_allow_html=True)
# ── Reporting ─────────────────────────────────────────
if reporting.get("should_report") and reporting.get("contacts"):
contacts = reporting.get("contacts", [])
contact_parts = []
for c in contacts:
if isinstance(c, dict) and 'name' in c and 'value' in c:
contact_parts.append(f"{c['name']}: {c['value']}")
if contact_parts:
contact_str = " | ".join(contact_parts)
st.markdown(f"""
📢 Report this: {contact_str}
""", unsafe_allow_html=True)
else:
# Empty state
st.markdown("""
◈
SHADOW IS WATCHING
Paste a message or select a demo scenario
to begin fraud analysis.
""", unsafe_allow_html=True)
# ── Footer ─────────────────────────────────────────────────────────
st.markdown("""
SHADOW — AMD Developer Hackathon 2026 |
Qwen3 on MI300X via vLLM + ROCm |
Built for Kenya's 54M mobile users |
GitHub
""", unsafe_allow_html=True)