"""
app.py
Streamlit Dashboard for Roger Platform
Interactive interface with Infinite Auto-Refresh & Smart Updates
"""
import streamlit as st
import json
import hashlib
from datetime import datetime
import plotly.graph_objects as go
import time
# Import Roger components
# NOTE: Ensure these imports work in your local environment
from src.graphs.RogerGraph import graph
from src.states.combinedAgentState import CombinedAgentState
# ============================================
# PAGE CONFIGURATION
# ============================================
st.set_page_config(
page_title="Roger - Situational Awareness Platform",
page_icon="🇱🇰",
layout="wide",
initial_sidebar_state="expanded"
)
# ============================================
# CUSTOM CSS
# ============================================
st.markdown("""
""", unsafe_allow_html=True)
# ============================================
# HEADER
# ============================================
st.markdown('
🇱🇰 Roger
', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
# ============================================
# SIDEBAR
# ============================================
with st.sidebar:
st.header("⚙️ Configuration")
# Auto-refresh interval
refresh_rate = st.slider("Polling Interval (s)", 5, 60, 10)
st.divider()
# Control Buttons
col_start, col_stop = st.columns(2)
with col_start:
if st.button("▶ START", type="primary", use_container_width=True):
st.session_state.monitoring_active = True
st.rerun()
with col_stop:
if st.button("⏹ STOP", use_container_width=True):
st.session_state.monitoring_active = False
st.rerun()
st.divider()
st.info("""
**Team Adagard** Open Innovation Track
Roger transforms national-scale noise into actionable business intelligence using autonomous multi-agent architecture.
""")
st.code("START → Fan-Out → [Agents] → Fan-In → Dashboard → Loop", language="text")
# ============================================
# SESSION STATE INITIALIZATION
# ============================================
if "monitoring_active" not in st.session_state:
st.session_state.monitoring_active = False
if "latest_result" not in st.session_state:
st.session_state.latest_result = None
if "last_hash" not in st.session_state:
st.session_state.last_hash = ""
if "execution_count" not in st.session_state:
st.session_state.execution_count = 0
# ============================================
# HELPER FUNCTIONS
# ============================================
def calculate_hash(data_dict):
"""Creates a hash of the dashboard data to detect changes."""
# We focus on the snapshot and the feed length/content
snapshot = data_dict.get("risk_dashboard_snapshot", {})
feed = data_dict.get("final_ranked_feed", [])
# Create a simplified string representation to hash
content_str = f"{snapshot.get('last_updated')}-{len(feed)}-{snapshot.get('opportunity_index')}"
return hashlib.md5(content_str.encode()).hexdigest()
def render_dashboard(container, result):
"""Renders the entire dashboard into the provided container."""
snapshot = result.get("risk_dashboard_snapshot", {})
feed = result.get("final_ranked_feed", [])
# Clear the container to ensure clean re-render
container.empty()
with container.container():
st.divider()
# -------------------------------------------------------------------------
# 1. METRICS ROW
# -------------------------------------------------------------------------
st.subheader("📊 Operational Metrics")
m1, m2, m3, m4 = st.columns(4)
with m1:
st.metric("Logistics Friction", f"{snapshot.get('logistics_friction', 0):.3f}", help="Route risk score")
with m2:
st.metric("Compliance Volatility", f"{snapshot.get('compliance_volatility', 0):.3f}", help="Regulatory risk")
with m3:
st.metric("Market Instability", f"{snapshot.get('market_instability', 0):.3f}", help="Economic volatility")
with m4:
opp_val = snapshot.get("opportunity_index", 0.0)
st.metric("Opportunity Index", f"{opp_val:.3f}", delta="Growth Signal" if opp_val > 0.5 else "Neutral", delta_color="normal")
# -------------------------------------------------------------------------
# 2. RADAR CHART
# -------------------------------------------------------------------------
st.divider()
c1, c2 = st.columns([1, 1])
with c1:
st.subheader("📡 Risk vs. Opportunity Radar")
categories = ['Logistics', 'Compliance', 'Market', 'Social', 'Weather']
risk_vals = [
snapshot.get('logistics_friction', 0),
snapshot.get('compliance_volatility', 0),
snapshot.get('market_instability', 0),
0.4, 0.2
]
fig = go.Figure()
# Risk Layer
fig.add_trace(go.Scatterpolar(
r=risk_vals, theta=categories, fill='toself', name='Operational Risk',
line_color='#ff4444'
))
# Opportunity Layer
fig.add_trace(go.Scatterpolar(
r=[opp_val] * 5, theta=categories, name='Opportunity Threshold',
line_color='#00CC96', opacity=0.7, line=dict(dash='dot')
))
fig.update_layout(
polar=dict(radialaxis=dict(visible=True, range=[0, 1])),
showlegend=True,
height=350,
margin=dict(l=40, r=40, t=20, b=20),
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
font=dict(color="white")
)
st.plotly_chart(fig, use_container_width=True)
# -------------------------------------------------------------------------
# 3. INTELLIGENCE FEED
# -------------------------------------------------------------------------
with c2:
st.subheader("📰 Intelligence Feed")
tab_all, tab_risk, tab_opp = st.tabs(["All Events", "Risks ⚠️", "Opportunities 🚀"])
def render_feed(filter_type=None):
if not feed:
st.info("No events detected.")
return
count = 0
for event in feed[:15]:
imp = event.get("impact_type", "risk")
if filter_type and imp != filter_type: continue
border_color = "#ff4444" if imp == "risk" else "#00CC96"
icon = "⚠️" if imp == "risk" else "🚀"
summary = event.get("content_summary", "")
domain = event.get("target_agent", "unknown").upper()
score = event.get("confidence_score", 0.0)
st.markdown(
f"""
{domain}
SCORE: {score:.2f}
{icon} {summary}
""",
unsafe_allow_html=True
)
count += 1
if count == 0:
st.caption("No events in this category.")
with tab_all: render_feed()
with tab_risk: render_feed("risk")
with tab_opp: render_feed("opportunity")
st.divider()
st.caption(f"Last Updated: {datetime.utcnow().strftime('%H:%M:%S UTC')} | Run Count: {st.session_state.execution_count}")
# ============================================
# MAIN EXECUTION LOGIC
# ============================================
# We use a placeholder that we can overwrite dynamically
dashboard_placeholder = st.empty()
if st.session_state.monitoring_active:
# ---------------------------------------------------------
# PHASE 1: INITIAL LOAD (Runs only if we have NO data)
# ---------------------------------------------------------
if st.session_state.latest_result is None:
with dashboard_placeholder.container():
st.markdown("
", unsafe_allow_html=True)
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
st.markdown('INITIALIZING NEURAL AGENTS...
', unsafe_allow_html=True)
st.markdown('Connecting to Roger Graph Network
', unsafe_allow_html=True)
progress_bar = st.progress(0)
# Visual effect for initialization
steps = ["Loading Social Graph...", "Connecting to Market Data...", "Calibrating Risk Radar...", "Starting Fan-Out Sequence..."]
for i, step in enumerate(steps):
time.sleep(0.3)
progress_bar.progress((i + 1) * 25)
# --- PERFORM FIRST FETCH ---
try:
current_state = CombinedAgentState(max_runs=1, run_count=0)
result = graph.invoke(current_state)
# Save to session state
st.session_state.latest_result = result
st.session_state.last_hash = calculate_hash(result)
st.session_state.execution_count = 1
except Exception as e:
st.error(f"Initialization Error: {e}")
st.session_state.monitoring_active = False
st.stop()
# ---------------------------------------------------------
# PHASE 2: CONTINUOUS MONITORING LOOP
# ---------------------------------------------------------
# By this point, st.session_state.latest_result is GUARANTEED to have data.
while st.session_state.monitoring_active:
# 1. RENDER CURRENT DATA
# We render whatever is in the state immediately.
# This replaces the loading screen or the previous frame.
render_dashboard(dashboard_placeholder, st.session_state.latest_result)
# 2. WAIT (The "Background" part)
# The UI is now visible to the user while we sleep.
time.sleep(refresh_rate)
# 3. FETCH NEW DATA
try:
current_state = CombinedAgentState(max_runs=1, run_count=st.session_state.execution_count)
# Run the graph silently in background
new_result = graph.invoke(current_state)
# 4. CHECK FOR DIFFERENCES
new_hash = calculate_hash(new_result)
if new_hash != st.session_state.last_hash:
# DATA CHANGED: Update state
st.session_state.last_hash = new_hash
st.session_state.latest_result = new_result
st.session_state.execution_count += 1
# Optional: Pop a toast
st.toast(f"New Intel Detected ({len(new_result.get('final_ranked_feed', []))} events)", icon="⚡")
# The loop continues...
# The NEXT iteration (Step 1) will render this new data.
else:
# NO CHANGE:
# We do nothing. The loop continues.
# Step 1 will simply re-render the existing stable data.
pass
except Exception as e:
st.error(f"Monitoring Error: {e}")
time.sleep(5) # Wait before retrying on error
else:
# ---------------------------------------------------------
# IDLE STATE
# ---------------------------------------------------------
with dashboard_placeholder.container():
st.markdown("
", unsafe_allow_html=True)
col1, col2, col3 = st.columns([1, 4, 1])
with col2:
st.info("System Standby. Click '▶ START' in the sidebar to begin autonomous monitoring.")
if st.session_state.latest_result:
st.markdown("### Last Session Snapshot:")
# We use a temporary container here just for the snapshot
with st.container():
render_dashboard(st.empty(), st.session_state.latest_result)