""" DISASTER GRID — Autonomous AI Emergency Manager Streamlit dashboard — production-ready for Hugging Face Spaces """ import streamlit as st import random import time # ── Page config (must be the very first Streamlit call) ────────────────────── st.set_page_config( page_title="DISASTER GRID — Autonomous AI Emergency Manager", page_icon="⚡", layout="wide", initial_sidebar_state="collapsed", ) # ── Try real backend; graceful stub fallback ────────────────────────────────── try: from src.disaster_grid.environment import CityGrid from src.disaster_grid.models import ActionType REAL_ENV = True except ImportError: REAL_ENV = False if not REAL_ENV: class ActionType: MOVE_N = "MOVE_N" MOVE_S = "MOVE_S" MOVE_E = "MOVE_E" MOVE_W = "MOVE_W" REPAIR = "REPAIR" RECHARGE = "RECHARGE" WAIT = "WAIT" class CityGrid: def __init__(self): self.grid_health = [100] * 25 self.agent_pos = 0 self.agent_energy = 100 def reset(self, options=None): self.grid_health = [random.randint(60, 100) for _ in range(25)] self.grid_health[0] = 100 self.agent_pos = 0 self.agent_energy = 100 if options and "target_crises" in options: for idx in options["target_crises"]: if 0 <= idx < 25: self.grid_health[idx] = random.randint(5, 25) def step(self, action: dict): act = action.get("action", "WAIT") reward = -0.5 # entropy: one random sector degrades decay_idx = random.randint(1, 24) self.grid_health[decay_idx] = max( 0, self.grid_health[decay_idx] - random.randint(3, 8) ) self.agent_energy = max(0, self.agent_energy - 2) row, col = divmod(self.agent_pos, 5) if act == "MOVE_N" and row > 0: self.agent_pos -= 5 elif act == "MOVE_S" and row < 4: self.agent_pos += 5 elif act == "MOVE_W" and col > 0: self.agent_pos -= 1 elif act == "MOVE_E" and col < 4: self.agent_pos += 1 elif act == "REPAIR": if self.grid_health[self.agent_pos] < 30 and self.agent_energy >= 10: self.grid_health[self.agent_pos] = min( 100, self.grid_health[self.agent_pos] + 30 ) self.agent_energy -= 10 reward += 20 elif act == "RECHARGE": if self.agent_pos == 0: self.agent_energy = min(100, self.agent_energy + 40) reward += 5 avg_hp = sum(self.grid_health) / 25 done = avg_hp < 20 or self.agent_energy <= 0 info = {"avg_health": avg_hp, "reward": reward} return {}, reward, done, False, info # ── Heuristic agent policy ──────────────────────────────────────────────────── def pick_action(grid_health, agent_pos, agent_energy): """ Lightweight rule-based fallback policy. Returns (action_string, reasoning_string) — identical shape to the LLM agent. """ critical = [i for i, h in enumerate(grid_health) if h < 30] if agent_energy <= 20 and agent_pos == 0: return ActionType.RECHARGE, ( f"Energy critically low ({agent_energy}%) — recharging at base." ) if agent_energy <= 20: row, col = divmod(agent_pos, 5) act = ActionType.MOVE_N if row > 0 else ActionType.MOVE_W return act, f"Energy low ({agent_energy}%) — routing back to base for recharge." if agent_pos in critical: return ActionType.REPAIR, ( f"Sector {agent_pos} is critical (HP={grid_health[agent_pos]}) — " f"initiating emergency repair." ) if critical: target = critical[0] trow, tcol = divmod(target, 5) row, col = divmod(agent_pos, 5) if trow < row: act = ActionType.MOVE_N elif trow > row: act = ActionType.MOVE_S elif tcol < col: act = ActionType.MOVE_W else: act = ActionType.MOVE_E return act, ( f"Navigating toward critical sector {target} " f"(HP={grid_health[target]})." ) worst = min(range(25), key=lambda i: grid_health[i]) if worst == agent_pos: return ActionType.WAIT, "All sectors nominal — holding position, monitoring entropy." wrow, wcol = divmod(worst, 5) row, col = divmod(agent_pos, 5) if wrow < row: act = ActionType.MOVE_N elif wrow > row: act = ActionType.MOVE_S elif wcol < col: act = ActionType.MOVE_W else: act = ActionType.MOVE_E return act, ( f"Patrolling toward degraded sector {worst} " f"(HP={grid_health[worst]}) — entropy watch active." ) # ── Session-state initialisation ───────────────────────────────────────────── def _init_state(): if "env" not in st.session_state: env = CityGrid() env.reset() st.session_state.env = env e = st.session_state.env defaults = { "grid_health": list(e.grid_health), "agent_pos": e.agent_pos, "agent_energy": e.agent_energy, "step_count": 0, "total_reward": 0.0, "repair_count": 0, "recharge_count": 0, "auto_running": False, "game_over": False, "log_lines": [ "SYS › DISASTER GRID v1.0 — systems online.", "SYS › 25 sectors initialised — all readings nominal.", "SYS › Awaiting operator command...", ], } for k, v in defaults.items(): if k not in st.session_state: st.session_state[k] = v _init_state() # ── Helpers ─────────────────────────────────────────────────────────────────── def _sync_from_env(): e = st.session_state.env st.session_state.grid_health = list(e.grid_health) st.session_state.agent_pos = e.agent_pos st.session_state.agent_energy = e.agent_energy def _add_log(msg: str, kind: str = "sys"): prefix = { "sys": "SYS ›", "act": "ACT ›", "warn": "⚠ ›", "crit": "🔥 ›", "done": "✗ ›", } st.session_state.log_lines.append(f"{prefix.get(kind, ' ›')} {msg}") if len(st.session_state.log_lines) > 100: st.session_state.log_lines = st.session_state.log_lines[-100:] def _do_step(): if st.session_state.game_over: return action, reasoning = pick_action( st.session_state.grid_health, st.session_state.agent_pos, st.session_state.agent_energy, ) act_str = action.value if hasattr(action, "value") else str(action) _, reward, done, _, _ = st.session_state.env.step( {"action": act_str, "reasoning": reasoning} ) _sync_from_env() st.session_state.step_count += 1 st.session_state.total_reward += float(reward) if act_str == "REPAIR": st.session_state.repair_count += 1 if act_str == "RECHARGE": st.session_state.recharge_count += 1 _add_log(f'"{reasoning}"', "act") _add_log( f"Action: {act_str} | Reward: {reward:+.1f} " f"| Step #{st.session_state.step_count}", "sys", ) if done: st.session_state.game_over = True st.session_state.auto_running = False _add_log("SIMULATION ENDED — city collapsed or energy depleted.", "done") def _full_reset(): st.session_state.auto_running = False st.session_state.game_over = False st.session_state.step_count = 0 st.session_state.total_reward = 0.0 st.session_state.repair_count = 0 st.session_state.recharge_count = 0 st.session_state.log_lines = [ "SYS › City reset to baseline.", "SYS › All 25 sectors restored to nominal.", "SYS › Awaiting operator command...", ] st.session_state.env.reset() _sync_from_env() # ── CSS injection ───────────────────────────────────────────────────────────── THEME_CSS = """ """ st.markdown(THEME_CSS, unsafe_allow_html=True) # ── HTML builders ───────────────────────────────────────────────────────────── def _build_grid_html(grid_health: list, agent_pos: int) -> str: cells = [] for i in range(25): hp = grid_health[i] is_agent = i == agent_pos is_base = i == 0 is_crit = hp < 30 if is_agent: emoji = "🤖"; css_cls = "cell-agent" elif is_base: emoji = "🏢"; css_cls = "cell-base" elif is_crit: emoji = "🔥"; css_cls = "cell-crit" else: emoji = "🟩"; css_cls = "cell-ok" hp_color = ( "#ff4d4d" if hp < 30 else "#ffb800" if hp < 60 else "#00ff9d" ) cells.append(f"""