CoolFace
Apppublic

1Jayanth/devops-autoheal

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
streamlit_app.py892 linesDownload Raw Back to root
1"""2DevOps AI Agent Simulator โ€” Streamlit UI (v4)3===============================================4Interactive web interface for the DevOps Incident Response Environment.5Communicates with the FastAPI backend via HTTP.6v4 fixes:7  - Passes task_id to /auto-run (fixes always-easy bug)8  - AI suggestion button in manual mode via /suggest9  - Reasoning display in manual step log10"""11 12import requests13import json14import time15import subprocess16import signal17try:18    import plotly.graph_objects as go19    HAS_PLOTLY = True20except ImportError:21    HAS_PLOTLY = False22from datetime import datetime23import streamlit as st24from dotenv import load_dotenv25import os26from typing import Any, Dict, List, Optional27 28 29# ---------------------------------------------------------------------------30# Configuration31# ---------------------------------------------------------------------------32 33 34load_dotenv()35 36API_URL = os.getenv("API_URL") or os.getenv("BACKEND_URL")37if not API_URL:38    try:39        API_URL = st.secrets.get("API_URL") or st.secrets.get("BACKEND_URL")40    except Exception: # Catch StreamlitSecretNotFoundError and others41        API_URL = None42 43if not API_URL:44    API_URL = "http://localhost:7860"45 46st.set_page_config(47    page_title="DevOps AI Agent Simulator",48    page_icon="๐Ÿค–",49    layout="wide",50    initial_sidebar_state="expanded",51)52try:53    import gymnasium as gym54    import numpy as np55    import stable_baselines356    HAS_TRAINING = True57except ImportError:58    HAS_TRAINING = False59 60# ---------------------------------------------------------------------------61# Custom CSS62# ---------------------------------------------------------------------------63 64st.markdown("""65<style>66    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');67 68    html, body, [class*="css"] { font-family: 'Inter', sans-serif; }69 70    .hero-section {71        background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);72        padding: 2rem 2.5rem;73        border-radius: 16px;74        margin-bottom: 1.5rem;75        color: white;76        border: 1px solid rgba(255,255,255,0.08);77    }78    .hero-section h1 { margin: 0 0 0.3rem 0; font-size: 2rem; }79    .hero-section p { margin: 0.2rem 0; opacity: 0.85; font-size: 0.95rem; }80 81    .metric-card {82        background: linear-gradient(145deg, #1a1a2e, #16213e);83        border: 1px solid rgba(255,255,255,0.08);84        border-radius: 12px;85        padding: 1.2rem;86        text-align: center;87        color: white;88    }89    .metric-card .label { font-size: 0.75rem; opacity: 0.6; text-transform: uppercase; letter-spacing: 1px; }90    .metric-card .value { font-size: 1.8rem; font-weight: 700; margin: 0.3rem 0; }91 92    .status-healthy { color: #00e676; }93    .status-degraded { color: #ffab00; }94    .status-down { color: #ff1744; }95    .status-critical { color: #d50000; font-weight: 800; animation: blink 1.5s linear infinite; }96    @keyframes blink { 50% { opacity: 0.3; } }97 98    .log-box {99        background: #0d1117;100        border: 1px solid #30363d;101        border-radius: 8px;102        padding: 1rem;103        font-family: 'JetBrains Mono', 'Fira Code', monospace;104        font-size: 0.82rem;105        color: #c9d1d9;106        white-space: pre-wrap;107        max-height: 200px;108        overflow-y: auto;109    }110 111    .chat-msg {112        background: #161b22;113        border: 1px solid #30363d;114        border-radius: 10px;115        padding: 1rem;116        margin: 0.5rem 0;117        color: #e6edf3;118    }119 120    .action-btn { margin: 0.2rem; }121 122    .step-log {123        background: #0d1117;124        border-left: 3px solid #58a6ff;125        padding: 0.6rem 1rem;126        margin: 0.3rem 0;127        border-radius: 0 6px 6px 0;128        font-size: 0.85rem;129        color: #c9d1d9;130    }131    .step-log.positive { border-left-color: #00e676; }132    .step-log.negative { border-left-color: #ff1744; }133 134    .auto-step {135        background: #0d1117;136        border-left: 3px solid #bb86fc;137        padding: 0.6rem 1rem;138        margin: 0.3rem 0;139        border-radius: 0 6px 6px 0;140        font-size: 0.85rem;141        color: #c9d1d9;142    }143    .auto-step.positive { border-left-color: #00e676; }144    .auto-step.negative { border-left-color: #ff1744; }145 146    .result-banner {147        background: linear-gradient(135deg, #1b5e20, #2e7d32);148        border: 1px solid #43a047;149        border-radius: 12px;150        padding: 1.2rem 1.5rem;151        color: white;152        text-align: center;153        margin: 1rem 0;154    }155    .result-banner.failed {156        background: linear-gradient(135deg, #b71c1c, #c62828);157        border-color: #e53935;158    }159    .result-banner h3 { margin: 0; font-size: 1.4rem; }160    .result-banner p { margin: 0.3rem 0 0 0; opacity: 0.9; }161 162    .mode-card {163        background: linear-gradient(145deg, #1a1a2e, #16213e);164        border: 1px solid rgba(255,255,255,0.08);165        border-radius: 12px;166        padding: 1.2rem;167        color: white;168    }169    .mode-card h4 { margin: 0 0 0.5rem 0; }170    .mode-card p { margin: 0; opacity: 0.8; font-size: 0.9rem; }171 172    .usecase-grid {173        display: grid;174        grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));175        gap: 0.8rem;176        margin: 1rem 0;177    }178    .usecase-item {179        background: linear-gradient(145deg, #1a1a2e, #16213e);180        border: 1px solid rgba(255,255,255,0.08);181        border-radius: 10px;182        padding: 1rem;183        color: white;184        text-align: center;185    }186    .usecase-item .icon { font-size: 1.8rem; margin-bottom: 0.4rem; }187    .usecase-item .title { font-weight: 600; font-size: 0.9rem; }188    .usecase-item .desc { font-size: 0.78rem; opacity: 0.7; margin-top: 0.3rem; }189 190    .reasoning-box {191        background: #161b22;192        border: 1px solid #bb86fc;193        border-radius: 8px;194        padding: 0.6rem 1rem;195        margin: 0.2rem 0 0.5rem 0;196        color: #e6edf3;197        font-size: 0.85rem;198    }199    .reasoning-box .label { color: #bb86fc; font-weight: 600; font-size: 0.78rem; }200</style>201""", unsafe_allow_html=True)202 203 204# ---------------------------------------------------------------------------205# Helper functions206# ---------------------------------------------------------------------------207 208def api_call(method: str, endpoint: str, data: dict = None) -> dict | list | None:209    """Make an API call to the FastAPI backend."""210    url = f"{API_URL}{endpoint}"211    try:212        if method == "GET":213            r = requests.get(url, timeout=10)214        else:215            # POST requests can involve long-running LLM loops (auto-run)216            r = requests.post(url, json=data or {}, timeout=300)217        r.raise_for_status()218        return r.json()219    except requests.exceptions.ConnectionError:220        st.error(f"โš ๏ธ Cannot connect to backend at {API_URL}. Please ensure it is running.")221        return None222    except Exception as e:223        st.error(f"API Error: {e}")224        return None225 226 227def get_status_class(status: str) -> str:228    return f"status-{status}" if status in ("healthy", "degraded", "down", "critical") else ""229 230 231def get_cpu_color(cpu: int) -> str:232    if cpu <= 40: return "#00e676"233    if cpu <= 70: return "#ffab00"234    return "#ff1744"235 236 237def get_mem_color(mem: int) -> str:238    if mem <= 50: return "#00e676"239    if mem <= 75: return "#ffab00"240    return "#ff1744"241 242 243def get_latency_color(lat: str) -> str:244    return {"low": "#00e676", "medium": "#ffab00", "high": "#ff1744"}.get(lat, "#999")245 246def get_delta_html(curr: Any, prev: Any, invert: bool = False) -> str:247    if prev is None or curr == prev: return ""248    try:249        diff = float(curr) - float(prev)250        if diff > 0:251            color = "#ff1744" if not invert else "#00e676"252            return f"<div style='font-size:0.75rem; color:{color}; margin-top:2px;'>โ–ฒ +{diff:.1f}</div>"253        else:254            color = "#00e676" if not invert else "#ff1744"255            return f"<div style='font-size:0.75rem; color:{color}; margin-top:2px;'>โ–ผ {diff:.1f}</div>"256    except (ValueError, TypeError):257        return f"<div style='font-size:0.75rem; color:#ffab00; margin-top:2px;'>โžค from {str(prev).upper()}</div>"258 259 260# ---------------------------------------------------------------------------261# Session state initialisation262# ---------------------------------------------------------------------------263 264if "state" not in st.session_state:265    st.session_state.state = None266if "prev_state" not in st.session_state:267    st.session_state.prev_state = None268if "step_history" not in st.session_state:269    st.session_state.step_history = []270if "chat_history" not in st.session_state:271    st.session_state.chat_history = []272if "actions_taken" not in st.session_state:273    st.session_state.actions_taken = []274if "total_reward" not in st.session_state:275    st.session_state.total_reward = 0.0276if "episode_done" not in st.session_state:277    st.session_state.episode_done = False278if "auto_run_result" not in st.session_state:279    st.session_state.auto_run_result = None280if "training_process" not in st.session_state:281    st.session_state.training_process = None282if "training_log" not in st.session_state:283    st.session_state.training_log = []284 285 286# ---------------------------------------------------------------------------287# Training Dashboard288# ---------------------------------------------------------------------------289 290def show_training_dashboard():291    st.markdown("## ๐Ÿค– Reinforcement Learning Training")292    293    if not HAS_TRAINING:294        st.warning("โš ๏ธ **Platform Not Supported for Local Training**")295        st.markdown(f"""296        RL Training requires **PyTorch** and **Stable Baselines 3**, which are currently not supported on your platform 297        (**{st.session_state.get('platform', 'Intel Mac')}** + **Python 3.13**).298 299        **How to Train?**300        1. **Push to Cloud**: Click the 'Push' button or run `openenv push`.301        2. **Run in Hugging Face**: All features will be fully functional on the Linux-based cloud environment.302        """)303        return304 305    st.info("Train a **PPO (Proximal Policy Optimization)** model using Stable Baselines 3 to resolve incidents automatically.")306    # ... rest of training logic ...307 308    col1, col2 = st.columns([1, 2])309 310    with col1:311        st.markdown("### โš™๏ธ Training Settings")312        timesteps = st.number_input("Total Timesteps", min_value=1000, max_value=100000, value=10000, step=1000)313        314        if st.session_state.training_process is None:315            if st.button("๐Ÿš€ Start Training", type="primary", use_container_width=True):316                try:317                    proc = subprocess.Popen(["python", "training.py", "--timesteps", str(timesteps)])318                    st.session_state.training_process = proc.pid319                    st.success(f"Training started (PID: {proc.pid})")320                    st.rerun()321                except Exception as e:322                    st.error(f"Failed to start training: {e}")323        else:324            if st.button("๐Ÿ›‘ Stop Training", type="secondary", use_container_width=True):325                try:326                    os.kill(st.session_state.training_process, signal.SIGTERM)327                    st.session_state.training_process = None328                    st.warning("Training stopped.")329                    st.rerun()330                except Exception as e:331                    st.error(f"Failed to stop process: {e}")332        333        st.markdown("---")334        st.markdown("**Status:** " + ("๐ŸŸข Running" if st.session_state.training_process else "โšช Idle"))335    336    with col2:337        st.markdown("### ๐Ÿ“ˆ Performance Monitor")338        339        if os.path.exists("training_log.json"):340            try:341                with open("training_log.json", "r") as f:342                    log_data = json.load(f)343                344                if log_data:345                    steps = [entry["step"] for entry in log_data]346                    rewards = [entry["reward"] for entry in log_data]347                    348                    fig = go.Figure()349                    fig.add_trace(go.Scatter(x=steps, y=rewards, mode='lines+markers', name='Mean Reward'))350                    fig.update_layout(351                        title="Mean Episode Reward vs. Timesteps",352                        xaxis_title="Timesteps",353                        yaxis_title="Mean Reward",354                        template="plotly_dark",355                        height=400356                    )357                    st.plotly_chart(fig, use_container_width=True)358                    359                    st.table(log_data[-5:][::-1])360                else:361                    st.info("Waiting for first training metrics...")362            except Exception as e:363                st.error(f"Error reading log: {e}")364        else:365            st.info("No training log found. Start training to see metrics.")366 367 368# ---------------------------------------------------------------------------369# Sidebar โ€” Controls370# ---------------------------------------------------------------------------371 372with st.sidebar:373    st.markdown("## โš™๏ธ Controls")374    view_mode = st.radio("View Mode", ["Incident Dashboard", "AI Training Hub"])375    st.divider()376 377    if view_mode == "Incident Dashboard":378        def load_selected_task():379            task_id = st.session_state.task_choice_sb380            st.session_state.active_task_id = task_id  # Save active task381            result = api_call("POST", "/reset", {"task_id": task_id})382            if result:383                st.session_state.state = result.get("observation", result)384                st.session_state.prev_state = None385                st.session_state.step_history = []386                st.session_state.actions_taken = []387                st.session_state.chat_history = []388                st.session_state.total_reward = 0.0389                st.session_state.episode_done = False390                st.session_state.auto_run_result = None391 392        available_tasks = ["easy", "medium", "hard", "expert"]393        st.markdown("### ๐Ÿ“‹ Select Task")394        task_choice = st.selectbox("Task", available_tasks, label_visibility="collapsed", key="task_choice_sb", on_change=load_selected_task)395 396        col_reset, col_gen = st.columns(2)397        with col_reset:398            if st.button("๐Ÿ”„ Reset", use_container_width=True):399                load_selected_task()400                st.rerun()401        with col_gen:402            if st.button("๐ŸŽฒ Random", use_container_width=True):403                tid = f"random_scenario_{int(time.time()) % 1000}"404                gen_result = api_call("POST", "/generate", {"task_id": tid, "difficulty": "random"})405                if gen_result:406                    result = api_call("POST", "/reset", {"task_id": tid})407                    if result:408                        st.session_state.active_task_id = tid  # Ensure Auto-Run uses this random task409                        st.session_state.state = result.get("observation", result)410                        st.session_state.prev_state = None411                        st.session_state.step_history = []412                        st.session_state.actions_taken = []413                        st.session_state.chat_history = []414                        st.session_state.total_reward = 0.0415                        st.session_state.episode_done = False416                        st.session_state.auto_run_result = None417                        st.toast(f"๐ŸŽฒ Generated new task: {tid}")418                        st.rerun()419 420        st.divider()421        st.markdown("### ๐Ÿ“œ Action History")422        if st.session_state.actions_taken:423            for i, a in enumerate(st.session_state.actions_taken, 1):424                st.markdown(f"`{i}.` {a}")425            st.metric("Total Reward", f"{st.session_state.total_reward:+.2f}")426        else:427            st.caption("No actions taken yet. Reset a task to begin.")428 429    st.divider()430    st.markdown("### ๐Ÿง  How AI Works")431    st.markdown("""432    1. **Analyzes** system metrics and logs433    2. **Infers** hidden root causes (Leaks/Locks)434    3. **Executes** multi-step corrective chains435    """)436 437    with st.expander("๐Ÿ› ๏ธ Technical Deep Dive"):438        st.markdown("""439        **Tech Stack:**440        - **Backend:** FastAPI + OpenEnv Core441        - **Logic:** Pydantic v2 + Dataclasses442        - **Agent:** OpenAI-compatible Reasoning Agent443        - **UI:** Streamlit + Custom CSS444 445        **Deep Logic:**446        - **Dual-State Engine:** Metrics are driven by *hidden* variables (Leaks, Locks).447        - **Failure Propagation:** High CPU increases DB Latency chance; High Memory triggers API crashes.448        - **Deterministic Grader:** Scientific 5-component scoring (0.0 - 1.0).449        """)450 451 452# ---------------------------------------------------------------------------453# Main content454# ---------------------------------------------------------------------------455 456if view_mode == "AI Training Hub":457    show_training_dashboard()458else:459    # โ”€โ”€ Hero Section โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€460    st.markdown("""461<div class="hero-section">462    <h1>๐Ÿค– DevOps AI Agent Simulator</h1>463    <p style="font-size: 1.1rem; margin-bottom: 0.8rem;">Interactive simulation of real-world DevOps system failures with AI-powered diagnosis</p>464</div>465""", unsafe_allow_html=True)466 467    if st.session_state.state is None:468        st.info("๐Ÿ‘ˆ Select a task and click **๐Ÿ”„ Reset** to start a simulation.")469        st.stop()470 471    state = st.session_state.state472    prev_state = st.session_state.prev_state473 474    # โ”€โ”€ System State Panel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€475    st.markdown("## ๐Ÿ“Š System State")476    cols = st.columns(5)477 478    with cols[0]:479        cpu = state["cpu_usage"]480        pcpu = prev_state.get("cpu_usage") if prev_state else None481        st.markdown(f"""482        <div class="metric-card">483            <div class="label">CPU Usage</div>484            <div class="value" style="color: {get_cpu_color(cpu)}">{cpu}%</div>485            {get_delta_html(cpu, pcpu)}486        </div>487        """, unsafe_allow_html=True)488 489    with cols[1]:490        mem = state["memory_usage"]491        pmem = prev_state.get("memory_usage") if prev_state else None492        st.markdown(f"""493        <div class="metric-card">494            <div class="label">Memory</div>495            <div class="value" style="color: {get_mem_color(mem)}">{mem}%</div>496            {get_delta_html(mem, pmem)}497        </div>498        """, unsafe_allow_html=True)499 500    with cols[2]:501        lat = state["db_latency"]502        plat = prev_state.get("db_latency") if prev_state else None503        st.markdown(f"""504        <div class="metric-card">505            <div class="label">DB Latency</div>506            <div class="value" style="color: {get_latency_color(lat)}">{lat.upper()}</div>507            {get_delta_html(lat, plat)}508        </div>509        """, unsafe_allow_html=True)510 511    with cols[3]:512        status = state["status"]513        if status == "down" and st.session_state.get("task_choice_sb") == "expert":514            status = "critical"515        516        st.markdown(f"""517        <div class="metric-card">518            <div class="label">Status</div>519            <div class="value {get_status_class(status)}">{status.upper()}</div>520        </div>521        """, unsafe_allow_html=True)522 523    with cols[4]:524        st.markdown(f"""525        <div class="metric-card">526            <div class="label">Step</div>527            <div class="value" style="color: #64b5f6">{state['step_count']}</div>528        </div>529        """, unsafe_allow_html=True)530 531    # Services532    svc_icons = {"api": "๐ŸŒ", "database": "๐Ÿ—„๏ธ", "cache": "โšก"}533    svc_text = "  ".join(534        f"{svc_icons.get(s, 'โ€ข')} **{s}** โœ…" if s in state["services"]535        else f"{svc_icons.get(s, 'โ€ข')} **{s}** โŒ"536        for s in ["api", "database", "cache"]537    )538    st.markdown(f"**Services:** {svc_text}")539 540    # Logs541    st.markdown("**๐Ÿ“ System Logs:**")542    st.markdown(f'<div class="log-box">{state["logs"]}</div>', unsafe_allow_html=True)543 544    st.markdown("---")545    546    # Reveal Reward Progression Chart if available547    if HAS_PLOTLY and getattr(st.session_state, "step_history", []):548        cumulative = []549        cur = 0550        for e in st.session_state.step_history:551            cur += e.get("reward", 0)552            cumulative.append(cur)553        if cumulative:554            st.markdown("### ๐Ÿ“ˆ Reward Progression")555            fig = go.Figure()556            fig.add_trace(go.Scatter(557                x=[e["step"] for e in st.session_state.step_history] , 558                y=cumulative, 559                mode='lines+markers', 560                name='Total Reward', 561                line=dict(color='#00e676', width=2),562                marker=dict(size=8)563            ))564            fig.update_layout(565                xaxis_title="Step", 566                yaxis_title="Total Reward", 567                template="plotly_dark", 568                height=250, 569                margin=dict(t=10, b=10, l=10, r=10),570                paper_bgcolor='rgba(0,0,0,0)',571                plot_bgcolor='rgba(0,0,0,0.2)'572            )573            st.plotly_chart(fig, use_container_width=True)574            st.markdown("---")575 576    # โ”€โ”€ Action Panel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€577 578    action_col, chat_col = st.columns([1, 1])579 580    with action_col:581        st.markdown("## ๐ŸŽฎ Actions")582 583        if st.session_state.episode_done:584            if state["status"] == "healthy":585                st.markdown(586                    '<div class="result-banner">'587                    '<h3>โœ… System Fully Resolved!</h3>'588                    '<p>All root causes have been addressed. The system is healthy.</p>'589                    '</div>',590                    unsafe_allow_html=True,591                )592            else:593                st.markdown(594                    '<div class="result-banner failed">'595                    f'<h3>โš ๏ธ Episode Ended โ€” Status: {state["status"].upper()}</h3>'596                    '<p>Max steps reached. The system was not fully restored.</p>'597                    '</div>',598                    unsafe_allow_html=True,599                )600            st.info("Click **๐Ÿ”„ Reset** in the sidebar to start a new episode.")601        else:602            actions = [603                ("๐Ÿ”„ restart_service:api", "restart_service:api"),604                ("๐Ÿ—„๏ธ restart_service:database", "restart_service:database"),605                ("๐Ÿ“ˆ scale_up:cpu", "scale_up:cpu"),606                ("โšก optimize_database", "optimize_database"),607                ("๐Ÿงน clear_cache", "clear_cache"),608                ("๐Ÿ” check_logs", "check_logs"),609                ("โธ๏ธ no_action", "no_action"),610            ]611 612            # AI Suggestion Button613            suggest_col1, suggest_col2 = st.columns([3, 1])614            with suggest_col1:615                if st.button("๐Ÿง  Ask AI for Next Action", use_container_width=True, type="secondary"):616                    suggestion = api_call("POST", "/suggest")617                    if suggestion:618                        st.session_state["ai_suggestion"] = suggestion619                        st.rerun()620            with suggest_col2:621                pass622            623            if st.session_state.get("ai_suggestion"):624                sug = st.session_state["ai_suggestion"]625                st.markdown(626                    f'<div class="reasoning-box">'627                    f'<span class="label">๐Ÿง  AI Suggests:</span> <strong>{sug.get("action", "?")}</strong><br>'628                    f'{sug.get("reasoning", "")}'629                    f'</div>',630                    unsafe_allow_html=True,631                )632 633            action_cols = st.columns(3)634            for i, (label, action_id) in enumerate(actions):635                with action_cols[i % 3]:636                    disabled = action_id in st.session_state.actions_taken and action_id not in ("check_logs", "no_action")637                    if st.button(label, key=f"act_{action_id}", use_container_width=True, disabled=disabled):638                        # Get AI reasoning for this action639                        diag = {}640                        try:641                            suggest_result = api_call("POST", "/suggest")642                            if suggest_result:643                                diag = suggest_result644                        except Exception:645                            pass646 647                        result = api_call("POST", "/step", {"action": {"action_str": action_id}})648                        if result:649                            obs = result.get("observation", result)650                            reward = result.get("reward", 0.0)651                            done = result.get("done", False)652                            step_diag = result.get("diagnosis", diag)653 654                            st.session_state.prev_state = dict(st.session_state.state)655                            st.session_state.state = obs656                            st.session_state.actions_taken.append(action_id)657                            st.session_state.total_reward += reward658                            st.session_state.episode_done = done659                            660                            metrics = obs.get("info", {})661                            entry = {662                                "step": len(st.session_state.actions_taken),663                                "action": action_id,664                                "reward": reward,665                                "status": obs["status"],666                                "message": obs.get("message", ""),667                                "info": metrics,668                                "reasoning": step_diag.get("reasoning", "") if isinstance(step_diag, dict) else ""669                            }670                            st.session_state.step_history.append(entry)671                            672                            # Update UI immediately for a "live" feel673                            st.toast(f"AI Step {entry['step']}: {action_id}", icon="๐Ÿค–")674                            675                            if action_id == "no_action" and obs["status"] == "healthy":676                                st.success("๐ŸŽฏ AI Agent successfully resolved the incident!")677                                break678                            679                            # Small pause for visual effect680                            import time681                            time.sleep(1.2)682                            st.rerun()683 684        # Step log685        if st.session_state.step_history:686            st.markdown("### ๐Ÿ“‹ Step Log & Timeline")687            688            # Basic stats summary if info exists689            latest_info = st.session_state.step_history[-1].get("info", {})690            if latest_info:691                ok_actions = latest_info.get("total_correct_actions", 0)692                bad_actions = latest_info.get("total_wrong_actions", 0)693                st.markdown(f"**Metrics:** โœ… Correct Actions: `{ok_actions}` | โŒ Wrong Actions: `{bad_actions}`")694                695            for entry in reversed(st.session_state.step_history):696                reward = entry["reward"]697                cls = "positive" if reward > 0 else "negative" if reward < 0 else ""698                699                with st.expander(f"Step {entry['step']}: **{entry['action']}**", expanded=(entry['step'] == len(st.session_state.step_history))):700                    st.markdown(f"**Reward:** <span class='reward-{cls}'>{reward:+.4f}</span>", unsafe_allow_html=True)701                    st.markdown(f"**Status:** `{entry['status'].upper()}`")702                    if entry.get("reasoning"):703                        st.info(f"**AI Reasoning:** {entry['reasoning']}")704                    st.write(entry["message"])705 706 707    # โ”€โ”€ Chat Panel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€708 709    with chat_col:710        st.markdown("## ๐Ÿ’ฌ AI Diagnosis Chat")711        st.caption("Ask the AI about the current incident for diagnosis and advice.")712 713        # Chat input714        user_msg = st.text_input("Ask about the incident...", key="chat_input",715                                 placeholder="What's wrong with the system?")716        if st.button("๐Ÿ” Diagnose", use_container_width=True) and user_msg:717            result = api_call("POST", "/chat", {"message": user_msg})718            if result:719                st.session_state.chat_history.append({720                    "role": "user",721                    "content": user_msg,722                })723                st.session_state.chat_history.append({724                    "role": "assistant",725                    "diagnosis": result.get("diagnosis", ""),726                    "reasoning": result.get("reasoning", ""),727                    "suggested_action": result.get("suggested_action", ""),728                })729                st.rerun()730 731        # Chat history732        if st.session_state.chat_history:733            for msg in reversed(st.session_state.chat_history):734                if msg["role"] == "user":735                    st.markdown(736                        f'<div class="chat-msg"><strong>๐Ÿง‘ You:</strong> {msg["content"]}</div>',737                        unsafe_allow_html=True,738                    )739                else:740                    st.markdown(741                        f'<div class="chat-msg">'742                        f'<strong>๐Ÿค– AI Diagnosis:</strong><br>'743                        f'<strong>Issue:</strong> {msg.get("diagnosis", "")}<br>'744                        f'<strong>Reasoning:</strong> {msg.get("reasoning", "")}<br>'745                        f'<strong>Suggested Action:</strong> <code>{msg.get("suggested_action", "")}</code>'746                        f'</div>',747                        unsafe_allow_html=True,748                    )749        else:750            st.caption("No chat messages yet. Ask a question above!")751 752 753    # ---------------------------------------------------------------------------754    # AI Agent Mode (add-on โ€” appended below existing UI)755    # ---------------------------------------------------------------------------756 757    st.markdown("---")758 759    st.markdown("## ๐Ÿค– AI Agent Mode")760    st.caption("Let the AI agent automatically diagnose and resolve the incident.")761 762    agent_col1, agent_col2 = st.columns([2, 1])763 764    with agent_col1:765        if st.session_state.episode_done:766            st.info("Episode already finished. **๐Ÿ”„ Reset** to run the agent on a new task.")767        elif st.session_state.state is None:768            st.info("Select a task and **๐Ÿ”„ Reset** first, then run the AI agent.")769        else:770            if st.button("โ–ถ๏ธ Run AI Agent Automatically", use_container_width=True, type="primary"):771                # Step-by-step animated execution772                progress_placeholder = st.empty()773                step_container = st.container()774 775                # Get the currently selected active task ID (works for both dropdown and random button)776                current_task = st.session_state.get("active_task_id", st.session_state.get("task_choice_sb", "easy"))777 778                with st.spinner(f"๐Ÿค– AI Agent is analyzing {current_task.upper()}..."):779                    # Clear UI history for a fresh auto-run visualization780                    st.session_state.actions_taken = []781                    st.session_state.step_history = []782                    result = api_call("POST", "/auto-run", {"task_id": current_task})783 784                if result:785                    # Animate step-by-step reveal786                    with step_container:787                        for i, step_entry in enumerate(result.get("steps", [])):788                            progress_placeholder.progress(789                                (i + 1) / max(len(result["steps"]), 1),790                                text=f"โณ AI is executing step {i+1}/{len(result['steps'])}..."791                            )792                            time.sleep(0.6)  # Realistic delay between steps793 794                        progress_placeholder.progress(1.0, text="โœ… AI Agent finished!")795                        time.sleep(0.5)796                        progress_placeholder.empty()797 798                    st.session_state.auto_run_result = result799                    # Final observation is derived from the last step in result800                    if result.get("steps"):801                        last = result["steps"][-1]802                        st.session_state.state = {803                            "status": last["status"],804                            "cpu_usage": last["cpu_usage"],805                            "memory_usage": last["memory_usage"],806                            "db_latency": last["db_latency"],807                            "services": last.get("services", []), 808                            "logs": last.get("logs", ""), 809                            "step_count": last["step"],810                        }811                    st.session_state.episode_done = True812                    for step_entry in result.get("steps", []):813                        st.session_state.actions_taken.append(step_entry["action"])814                        st.session_state.total_reward += step_entry["reward"]815                        st.session_state.step_history.append({816                            "step": step_entry["step"],817                            "action": step_entry["action"],818                            "reward": step_entry["reward"],819                            "status": step_entry["status"],820                            "message": step_entry.get("message", ""),821                        })822                    st.rerun()823 824    with agent_col2:825        if st.session_state.auto_run_result:826            r = st.session_state.auto_run_result827            st.metric("Final Status", r["final_status"].upper())828            st.metric("Steps Taken", r["total_steps"])829            st.metric("Total Reward", f"{r['total_reward']:+.2f}")830 831    # Display auto-run execution log832    if st.session_state.auto_run_result:833        ar = st.session_state.auto_run_result834        st.markdown("---")835        st.markdown("### ๐Ÿ“‹ AI Execution Log")836        for step_entry in ar.get("steps", []):837            reward = step_entry["reward"]838            reasoning = step_entry.get("reasoning", "")839            cls = "positive" if reward > 0 else "negative" if reward < 0 else ""840            st.markdown(841                f'<div class="auto-step {cls}">'842                f'<strong>Step {step_entry["step"]}</strong>: '843                f'{step_entry["action"]} โ†’ '844                f'reward={reward:+.4f} | status={step_entry["status"]} | '845                f'cpu={step_entry["cpu_usage"]}% mem={step_entry["memory_usage"]}% db={step_entry["db_latency"]}'846                f'<br><small>{step_entry["message"]}</small></div>',847                unsafe_allow_html=True,848            )849            if reasoning:850                st.markdown(851                    f'<div class="reasoning-box">'852                    f'<span class="label">๐Ÿง  Reasoning:</span> {reasoning}'853                    f'</div>',854                    unsafe_allow_html=True,855                )856 857 858 859# ---------------------------------------------------------------------------860# Mode Explanation (add-on โ€” appended at bottom)861# ---------------------------------------------------------------------------862 863st.markdown("---")864 865st.markdown("## ๐Ÿ“– Execution Modes")866 867mode_col1, mode_col2 = st.columns(2)868 869with mode_col1:870    st.markdown(871        '<div class="mode-card">'872        '<h4>๐ŸŽฎ Manual Mode</h4>'873        '<p>You control actions step-by-step. Choose which action to execute, '874        'observe the results, and decide the next move. Great for learning '875        'how the system works and understanding action dependencies.</p>'876        '</div>',877        unsafe_allow_html=True,878    )879 880with mode_col2:881    st.markdown(882        '<div class="mode-card">'883        '<h4>๐Ÿค– AI Agent Mode</h4>'884        '<p>The AI agent automatically diagnoses and resolves the system. '885        'It uses structured reasoning (Intent โ†’ Dependencies โ†’ Failure Avoidance '886        'โ†’ Orchestration) to find the optimal fix sequence. Watch the execution '887        'log to see how the agent reasons.</p>'888        '</div>',889        unsafe_allow_html=True,890    )891 892