import gradio as gr import os import logging import json import time import urllib.parse import subprocess from pathlib import Path # Avoid outbound connectivity checks during startup unless explicitly requested. from debug_network import debug_connectivity if os.getenv("DEBUG_CONNECTIVITY", "0") == "1": debug_connectivity() from agent.pipeline import ( analyze_change, oob_troubleshoot, perform_recovery, load_synapse_log, reset_state, ) from agent import topology from agent.network_ops import ( get_lab_topology, get_lab_projects, manage_device, get_device_configuration, configure_device, backup_device_config, build_network_from_description, ) from agent.api_monitor import monitor from agent import topology # Configure logging to both file and stdout so runtime logs are visible and persisted logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[ logging.FileHandler("app.log"), logging.StreamHandler() ], force=True ) def get_commit_hash() -> str: """Return current git commit hash (short).""" try: result = subprocess.run( ["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True, timeout=2, check=True, ) return result.stdout.strip() or "unknown" except Exception: return "unknown" import html from pathlib import Path VINE_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Inter:wght@400;500;600&display=swap'); :root { --og-green: #2d5f4f; --og-mint: #5fc9a0; --og-fern: #3d8169; --og-cream: #fafaf8; --og-shadow: 0 8px 24px rgba(45,95,79,0.12); } html, body { overflow-x: hidden; } body, .gradio-container { font-family: 'Inter', system-ui, -apple-system, sans-serif; background: repeating-linear-gradient(45deg, transparent, transparent 35px, rgba(95,201,160,0.03) 35px, rgba(95,201,160,0.03) 70px), repeating-linear-gradient(-45deg, transparent, transparent 35px, rgba(61,129,105,0.03) 35px, rgba(61,129,105,0.03) 70px), linear-gradient(to bottom, #fafaf8, #f5f7f5); color: #1a2f26; position: relative; min-height: 100vh; } .gradio-container > * { position: relative; z-index: 1; } .og-hero { background: linear-gradient(135deg, rgba(95,201,160,0.08), rgba(61,129,105,0.12)), repeating-linear-gradient(45deg, transparent, transparent 20px, rgba(95,201,160,0.04) 20px, rgba(95,201,160,0.04) 40px), repeating-linear-gradient(-45deg, transparent, transparent 20px, rgba(61,129,105,0.04) 20px, rgba(61,129,105,0.04) 40px), linear-gradient(to bottom right, #f8faf9, #f0f5f2); border: 1px solid rgba(95,201,160,0.2); box-shadow: var(--og-shadow); border-radius: 16px; padding: 28px; position: relative; overflow: hidden; } .og-hero::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, transparent, var(--og-mint), transparent); opacity: 0.6; } .og-hero::after { content: ""; position: absolute; inset: 0; background: radial-gradient(circle at 20% 30%, rgba(95,201,160,0.08), transparent 40%), radial-gradient(circle at 80% 70%, rgba(61,129,105,0.08), transparent 40%); pointer-events: none; } .og-hero::after { inset: auto auto -120px -20px; transform: rotate(8deg); } .og-hero h2 { font-family: 'Playfair Display', serif; font-size: 28px; color: var(--og-green); letter-spacing: 0.02em; } .og-hero p { margin: 0; font-size: 15px; color: #0f4d3f; max-width: 720px; } .og-pill { display: inline-block; padding: 7px 12px; border-radius: 999px; background: rgba(60,191,154,0.22); color: var(--og-green); font-weight: 700; margin: 0 10px 8px 0; letter-spacing: 0.01em; box-shadow: 0 8px 18px rgba(0,0,0,0.06); } .og-panel { background: rgba(255,255,255,0.92); backdrop-filter: blur(16px); border: 1px solid rgba(95,201,160,0.15); border-radius: 12px; padding: 22px; box-shadow: 0 4px 16px rgba(45,95,79,0.08); position: relative; overflow: hidden; } /* Hide transparent boxes on empty rows/columns */ .og-main-row > div:empty, .og-main-row > div > div:empty { display: none !important; } /* Ensure main row doesn't create unwanted backgrounds */ .og-main-row { background: transparent !important; gap: 16px; } .og-main-row > div { background: transparent !important; } .og-panel::before { .og-panel::after { content: ""; position: absolute; inset: auto -20px -40px auto; width: 150px; height: 150px; background: radial-gradient(circle at 30% 30%, rgba(15,77,63,0.14), transparent 65%); opacity: 0.55; pointer-events: none; } .og-tab-title { font-family: 'Playfair Display', serif; color: var(--og-green); } .og-tabs .tab-nav button { font-family: 'Playfair Display', serif; color: var(--og-fern); } .og-tabs .tab-nav button.selected { background: rgba(60,191,154,0.16); border-color: rgba(60,191,154,0.4); color: var(--og-green); } .og-divider { border-bottom: 1px solid rgba(15,77,63,0.12); margin: 10px 0 14px 0; } .og-label { color: var(--og-green); font-weight: 700; letter-spacing: 0.02em; } textarea, input, select { border-radius: 12px !important; border: 1px solid rgba(15,77,63,0.16) !important; background: rgba(255,255,255,0.95) !important; } .gr-button { border-radius: 12px !important; box-shadow: 0 12px 22px rgba(15,77,63,0.18) !important; background: linear-gradient(120deg, var(--og-green), #128162) !important; color: #f5f3ec !important; border: none !important; } .gr-button.secondary { background: rgba(15,77,63,0.08) !important; color: var(--og-green) !important; box-shadow: none !important; } """ MERMAID_BOOTSTRAP = """ """ SMOKE_CACHE_PATH = Path("examples/last_good_pipeline.json") def load_smoke_cache(): try: if SMOKE_CACHE_PATH.exists(): with open(SMOKE_CACHE_PATH, "r") as f: return json.load(f) except Exception: return None return None def save_smoke_cache(data: dict): try: SMOKE_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True) with open(SMOKE_CACHE_PATH, "w") as f: json.dump(data, f, indent=2) except Exception: # Cache write failures are non-fatal pass def fallback_mermaid_diagram() -> str: """Always return a basic mermaid graph from local topology.""" try: topo = topology._load_topology() devices = topo.get("devices", []) links = topo.get("links", []) if not devices: return "graph LR\n A[No topology] --> B[No links]" lines = ["graph LR"] for d in devices: dev_id = d.get("id", "node").replace("-", "_") label = d.get("id", "node") role = d.get("role") if role: label += f"\\n{role}" lines.append(f' {dev_id}["{label}"]') for l in links: a = (l.get("a") or "").replace("-", "_") b = (l.get("b") or "").replace("-", "_") if a and b: lines.append(f" {a} --- {b}") return "\n".join(lines) except Exception: return "graph LR\n A[Diagram unavailable]" def build_ui(): # Preload available GNS3 projects for brownfield imports gns3_server = os.getenv("GNS3_SERVER") gns3_enabled = bool(gns3_server) try: project_choices = [ p.get("name") for p in get_lab_projects() if isinstance(p, dict) and p.get("name") ] except Exception: project_choices = [] def dev_info(): commit = get_commit_hash() deploy_mode = os.getenv("OG_DEPLOY_MODE", "hybrid") enable_seed = os.getenv("OG_ENABLE_SEED", "1") ssh_on_seed_fail = os.getenv("OG_SSH_ON_SEED_FAIL", "0") gns3 = os.getenv("GNS3_SERVER", "unset") return ( f"**Build:** `{commit}` \n" f"**Deploy mode:** `{deploy_mode}` | **Seed:** `{enable_seed}` | **SSH on seed fail:** `{ssh_on_seed_fail}` \n" f"**GNS3:** `{gns3}` \n" f"**WG secret set:** `{'yes' if os.getenv('WG_CONFIG_B64') else 'no'}`" ) with gr.Blocks( title="Overgrowth – a living digital environment", css=VINE_CSS, theme=gr.themes.Soft(primary_hue="green", neutral_hue="slate"), ) as demo: gr.HTML(MERMAID_BOOTSTRAP) gr.HTML( """
Living OOB Mesh
MCP + Topology

🌿 Overgrowth

A living digital environment

🏁 Judge Quickstart (90 seconds)

  1. Paste this or use your own: Design a 3-site retail network with guest WiFi, POS VLANs, cameras, and secure VPN to HQ.
  2. Click Run Full Pipeline (keep simulation/read-only on).
  3. Review Status + tabs: Source of Truth YAML β†’ BOM β†’ Setup Guide.
  4. Peek at Live API Activity Monitor to see LLM/tool calls and costs.

Secrets: set BLAXEL_MCP_1ST_BDAY (or Anthropic/OpenAI/etc.) and optional GNS3 env vars for brownfield. Build info below shows deploy mode.

""", ) gr.Markdown(dev_info()) device_choices = topology.list_devices() # ===== API MONITORING DASHBOARD ===== # This is the key feature judges will love to see! gr.Markdown(""" ## πŸ” Live API Activity Monitor **Real-time visibility into all API calls - LLM, GNS3, and more** """) with gr.Row(elem_classes=["og-main-row"]): with gr.Column(scale=1, elem_classes=["og-panel"]): gr.Markdown("### πŸ“Š Session Statistics") api_stats = gr.Markdown(value=monitor.get_stats().format_dashboard(include_heading=False)) refresh_stats_btn = gr.Button("πŸ”„ Refresh Stats", size="sm", variant="secondary") with gr.Column(scale=2, elem_classes=["og-panel"]): gr.Markdown("### πŸ“‘ Recent API Calls") api_activity = gr.Markdown(value=monitor.format_activity_feed()) refresh_activity_btn = gr.Button("πŸ”„ Refresh Activity", size="sm", variant="secondary") with gr.Column(scale=2, elem_classes=["og-panel"]): gr.Markdown("### πŸ“œ Runtime Log (tail)") log_output = gr.Textbox(value="", lines=12, label="Latest Logs", interactive=False) refresh_log_btn = gr.Button("πŸ”„ Refresh Logs", size="sm", variant="secondary") # Auto-refresh handlers def refresh_api_stats(): return monitor.get_stats().format_dashboard(include_heading=False) def refresh_api_activity(): return monitor.format_activity_feed() refresh_stats_btn.click(fn=refresh_api_stats, outputs=[api_stats]) refresh_activity_btn.click(fn=refresh_api_activity, outputs=[api_activity]) def refresh_logs(): try: log_path = Path("app.log") if not log_path.exists(): return "No logs yet." import subprocess tail = subprocess.run(["tail", "-n", "60", str(log_path)], capture_output=True, text=True) return tail.stdout or "No logs yet." except Exception as e: return f"Failed to read logs: {e}" refresh_log_btn.click(fn=refresh_logs, outputs=[log_output]) # Auto-refresh runtime log every 5s log_timer = gr.Timer(5.0) log_timer.tick(fn=refresh_logs, outputs=[log_output]) gr.Markdown("---") # ===== INTERACTIVE CONSULTATION TAB ===== # Temporarily disabled due to Gradio 4.44.0 schema bug # TODO: Re-enable when upgrading to Gradio 4.44.1+ # gr.Markdown("""### πŸ’¬ Interactive Consultation # **Have a conversation before running the full pipeline** # # Not sure what you need? Start here! The AI will ask clarifying questions to understand your network requirements. # """) gr.Markdown("---") # Main Pipeline UI gr.Markdown("""### 🌿 Network Automation Pipeline **From Consultation β†’ Production** Describe your network in plain English. Overgrowth handles the rest: 1. πŸ’¬ **Consultation** - Capture requirements 2. πŸ“‹ **Source of Truth** - Generate network data model 3. πŸ“Š **Diagram** - Visualize topology 4. πŸ›’ **Bill of Materials** - Hardware shopping list 5. πŸ”§ **Setup Guide** - Human deployment steps + OOB network 6. πŸ€– **Autonomous Deploy** - AI configures devices (Ansible/Netmiko) 7. πŸ‘οΈ **Observability** - Monitoring, SNMP, topology discovery 8. βœ… **Validation** - pytest-based network testing & compliance _Brownfield ready: import existing NetBox/GNS3/SoT data, run simulation-only, and keep validation read-only._ """) with gr.Row(elem_classes=["og-main-row"]): with gr.Column(scale=1, elem_classes=["og-panel"]): pipeline_input = gr.Textbox( label="Describe Your Network", placeholder=( "Example: We're a coffee shop chain with 3 locations. " "We need WiFi for customers, POS systems with payment processing, " "security cameras, and secure VPN to HQ for centralized management. " "Each location has ~50 customers at peak time." ), lines=10, ) mode_choice = gr.Dropdown( label="Pipeline Mode", choices=[ "Design new (greenfield)", "Import from NetBox", "Import from GNS3", "Load existing SoT file" ], value="Design new (greenfield)" ) gr.Markdown("**Optional: provide an API key to override defaults for this run**") anthropic_key_box = gr.Textbox( label="Anthropic API Key (optional)", placeholder="sk-ant-...", type="password", ) openai_key_box = gr.Textbox( label="OpenAI API Key (optional)", placeholder="sk-...", type="password", ) gns3_project_input = gr.Dropdown( label="GNS3 project (brownfield)", choices=project_choices or ["overgrowth"], value=project_choices[0] if project_choices else "overgrowth", visible=gns3_enabled, interactive=gns3_enabled, ) if not gns3_enabled: gr.Markdown( "> GNS3 not configured in this Space; pipeline will run simulation-only and hide brownfield controls.", elem_classes=["og-label"] ) sot_path_input = gr.Textbox( label="Existing SoT path", value="infra/network_model.yaml" ) simulation_only = gr.Checkbox( label="Simulation-only (skip deployment)", value=True ) read_only_validation = gr.Checkbox( label="Read-only validation (no remediation)", value=True ) run_pipeline_btn = gr.Button("πŸš€ Run Full Pipeline", variant="primary", size="lg") with gr.Column(scale=2, elem_classes=["og-panel", "og-tabs"]): gr.Markdown("#### πŸ“¦ Pipeline Outputs") pipeline_status = gr.Markdown(label="Status") sot_output = gr.Code(label="Network Model (YAML)", language="yaml", lines=20) bom_output = gr.Markdown(label="Shopping List") setup_output = gr.Markdown(label="Deployment Guide") # Pipeline handler def run_pipeline(user_input, mode_choice, gns3_project, sot_path, simulation_only_flag, read_only_flag, anthropic_key, openai_key): """Execute the full automation pipeline""" from agent.pipeline_engine import OvergrowthPipeline from agent.llm_client import LLMClient # Apply per-run API keys (not persisted) if anthropic_key: os.environ["ANTHROPIC_API_KEY"] = anthropic_key.strip() os.environ["ANTHROPIC_MCP_1ST_BDAY"] = anthropic_key.strip() os.environ["OG_LLM_PROVIDER"] = "anthropic" elif openai_key: os.environ["OPENAI_API_KEY"] = openai_key.strip() os.environ["OPENAI_MCP_1ST_BDAY"] = openai_key.strip() os.environ["OG_LLM_PROVIDER"] = "openai" pipeline = OvergrowthPipeline() try: # Report LLM env status up-front so users see if keys are missing llm_env = LLMClient.env_status() # If user explicitly wants deployment (simulation_only_flag=False), ensure we drop read-only guard. if simulation_only_flag is False: read_only_flag = False # Run the pipeline (this will generate API calls that get tracked) mode_map = { "Design new (greenfield)": None, "Import from NetBox": "netbox", "Import from GNS3": "gns3", "Load existing SoT file": "file", } chosen_source = mode_map.get(mode_choice) source_note = "" if not gns3_enabled and chosen_source == "gns3": chosen_source = None source_note = "\n> GNS3 not configured; running greenfield simulation instead.\n" simulation_only_flag = True if chosen_source: results = pipeline.run_brownfield_pipeline( consultation_input=user_input, source=chosen_source, project_name=gns3_project, sot_path=sot_path, simulation_only=simulation_only_flag, read_only_validation=read_only_flag, ) else: results = pipeline.run_full_pipeline(user_input) # If the pipeline stopped early for clarifications, show questions and exit if results.get("needs_more_input"): status = "## 🀝 Consultation Needed\n\n" status += "Your request is too brief. Please answer these questions to continue:\n\n" for q in results.get('questions', []): status += f"- {q}\n" return status, "", "", "", "", monitor.get_stats().format_dashboard(include_heading=False), monitor.format_activity_feed() # Check pre-flight validation preflight = results.get('preflight', {}) ready_to_deploy = preflight.get('ready_to_deploy', False) # Format status with API usage summary status = "## πŸš€ Pipeline Execution Complete!\n\n" if source_note: status += source_note + "\n" status += "> Diagrams disabled here; open the GNS3 project link for topology view.\n\n" # Add API usage summary at the top stats = monitor.get_stats() status += f"### πŸ’° API Usage This Session\n" status += f"- **Total Cost:** ${stats.total_cost:.4f}\n" status += f"- **LLM Calls:** {stats.llm_calls} | **GNS3 Calls:** {stats.gns3_calls}\n" status += f"- **Tokens:** {stats.total_tokens:,} ({stats.total_input_tokens:,} in / {stats.total_output_tokens:,} out)\n\n" # LLM connectivity status += "### πŸ€– LLM Connectivity\n" status += f"- Provider selected: **{llm_env.get('provider', 'unknown')}**\n" status += f"- Anthropic key: {llm_env.get('anthropic_key')}\n" status += f"- OpenAI key: {llm_env.get('openai_key')}\n" status += f"- Blaxel key: {llm_env.get('blaxel_key', 'missing')}\n" status += f"- SambaNova key: {llm_env.get('sambanova_key', 'missing')}\n" status += f"- Nebius key: {llm_env.get('nebius_key', 'missing')}\n" status += f"- HuggingFace key: {llm_env.get('huggingface_key', 'missing')}\n" status += f"- Modal key: {llm_env.get('modal_key', 'missing')}\n\n" wg_status = results.get("wg_status") if wg_status: status += "### πŸ•ΈοΈ WireGuard\n" status += f"- Status: {wg_status}\n\n" commit = get_commit_hash() status += "### 🧾 Build\n" status += f"- Commit: `{commit}`\n\n" mode_info = results.get("mode") if mode_info: status += "### 🌳 Pipeline Mode\n" status += f"- Brownfield source: **{mode_info.get('source', 'greenfield')}**\n" status += f"- Simulation-only: {'Yes' if mode_info.get('simulation_only') else 'No'}\n" status += f"- Read-only validation: {'Yes' if mode_info.get('read_only_validation') else 'No'}\n" import_note = results.get("import_summary", {}).get("note") if isinstance(results.get("import_summary"), dict) else None if import_note: status += f"- Import notes: {import_note}\n" status += "\n" if stats.errors: from agent.api_monitor import monitor as api_monitor_instance recent_errors = api_monitor_instance.get_recent_errors(limit=3) if recent_errors: status += "### ⚠️ Recent API Errors\n" for err_call in reversed(recent_errors): msg = err_call.error_message or "Unknown error" status += f"- {err_call.provider.upper()} {err_call.endpoint}: {msg}\n" status += "\n" # Budget visibility if stats.budget_limit not in (None, 0): remaining = stats.budget_remaining() status += f"- **Budget:** ${stats.budget_limit:.2f} (remaining ${remaining:.2f})\n" budget_note = stats.budget_status() if stats.budget_alert_triggered or (stats.budget_usage_ratio() or 0) >= stats.budget_alert_fraction: status += f"> 🚨 {budget_note}\n\n" else: status += f" - {budget_note}\n\n" # Lab simulation link (GNS3) lab_info = results.get("lab") or {} gns3_base = lab_info.get("api") or os.getenv("GNS3_SERVER") gns3_project = lab_info.get("project_name") or os.getenv("GNS3_PROJECT_NAME", "overgrowth") gns3_project_id = lab_info.get("project_id") or os.getenv("GNS3_PROJECT_ID") gns3_web = lab_info.get("web_url") if not gns3_web: gns3_web_override = os.getenv("GNS3_WEB_URL") if gns3_web_override: gns3_web = gns3_web_override elif gns3_base and gns3_project_id: gns3_web = f"{gns3_base.rstrip('/')}/static/web-ui/server/1/project/{gns3_project_id}" elif gns3_base: gns3_web = f"{gns3_base.rstrip('/')}/static/webUi" if gns3_base: status += "### πŸ§ͺ Lab Simulation\n" status += f"- GNS3 API: {gns3_base}\n" status += f"- Project: `{gns3_project}`\n" if gns3_project_id: status += f"- Project ID: `{gns3_project_id}`\n" if gns3_web: status += f"- Web UI: {gns3_web}\n" missing_sites = [] if isinstance(lab_info, dict): missing_sites = lab_info.get("missing_sites") or [] requested_sites = lab_info.get("requested_site_count") or results.get("requested_site_count") builder_sites = lab_info.get("builder_site_count") or results.get("builder_site_count") topology_sites = lab_info.get("topology_site_count") or results.get("topology_site_count") actual_sites = lab_info.get("actual_site_count") or results.get("actual_site_count") mismatch = lab_info.get("site_count_mismatch") or results.get("site_count_mismatch") if requested_sites is not None: status += f"- Requested site count (parsed): {requested_sites}\n" if builder_sites is not None: status += f"- Builder site count (GNS3): {builder_sites}\n" if topology_sites is not None: status += f"- Topology site count (telemetry): {topology_sites}\n" if actual_sites is not None: status += f"- Actual site count: {actual_sites}\n" if mismatch: status += ( f"- Site count mismatch: requested {mismatch.get('requested')}, " f"builder {mismatch.get('builder')}, " f"topology {mismatch.get('topology')}\n" ) build_err = lab_info.get("build_error") or (lab_info.get("build_result") or {}).get("error") if build_err: status += f"- Build error: {build_err}\n" if missing_sites: status += f"- Missing sites in GNS3 build: {', '.join(missing_sites)}\n" status += "\n" # SSH preflight summary (if available) preflight_ssh = results.get("deployment", {}).get("ssh_preflight") if isinstance(results.get("deployment"), dict) else None if preflight_ssh: reachable = len([r for r in preflight_ssh if r.get("status") == "reachable"]) unreachable = len([r for r in preflight_ssh if r.get("status") == "unreachable"]) status += "### πŸ”Œ SSH Reachability\n" status += f"- Reachable: {reachable}\n" status += f"- Unreachable: {unreachable}\n\n" # Pre-flight validation section if ready_to_deploy: status += "### βœ… Pre-flight Validation PASSED\n" status += f"- Schema validation: βœ“ Passed\n" status += f"- Policy checks: βœ“ Passed\n" if preflight.get('warnings'): status += f"- Warnings: {len(preflight['warnings'])}\n" if preflight.get('info'): status += f"- Info: {len(preflight['info'])}\n" status += "\n" else: status += "### ❌ Pre-flight Validation FAILED\n" status += "**Deployment blocked until errors are fixed:**\n\n" for error in preflight.get('errors', []): status += f"- ❌ {error}\n" status += "\n" if preflight.get('warnings'): status += "**Warnings:**\n" for warning in preflight.get('warnings', []): status += f"- ⚠️ {warning}\n" status += "\n" status += "### Validation Findings\n" status += f"- Errors: {len(preflight.get('errors', []))}\n" status += f"- Warnings: {len(preflight.get('warnings', []))}\n" status += f"- Info: {len(preflight.get('info', []))}\n" if preflight.get('batfish_analysis'): bf = preflight['batfish_analysis'] status += f"- Batfish: {'mock' if bf.get('mock_mode') else 'analysis ran'} | Loops: {len(bf.get('routing_loops', []))} | Undefined refs: {len(bf.get('undefined_references', []))}\n" status += "\n" # Completed stages status += "### Completed Stages:\n" status += "0. " + ("βœ…" if ready_to_deploy else "❌") + " Pre-flight Validation\n" status += "1. βœ… Consultation - Intent captured\n" status += "2. βœ… Source of Truth - Network model generated\n" status += "3. 🚫 Diagrams - Disabled (use GNS3 UI)\n" status += "4. βœ… Bill of Materials - Shopping list ready\n" status += "5. βœ… Setup Guide - Deployment instructions generated\n" sim_only = results.get("mode", {}).get("simulation_only") ro_mode = results.get("mode", {}).get("read_only_validation") deploy_disabled = results.get("deployment_reason") == "deployment_disabled" if ready_to_deploy: if deploy_disabled: status += "6. 🚫 Autonomous Deploy - Disabled in this environment\n" status += "7. πŸ‘οΈ Observability - Snapshot only\n" status += "8. βœ… Validation - Read-only checks complete\n\n" elif sim_only or ro_mode: status += "6. πŸ§ͺ Autonomous Deploy - Skipped (simulation/read-only)\n" status += "7. πŸ‘οΈ Observability - Snapshot only\n" status += "8. βœ… Validation - Read-only checks complete\n\n" else: status += "6. ⏳ Autonomous Deploy - Ready for execution\n" status += "7. ⏳ Observability - Ready for setup\n" status += "8. ⏳ Validation - Ready for verification\n\n" else: status += "6. 🚫 Autonomous Deploy - BLOCKED (fix validation errors)\n" status += "7. 🚫 Observability - BLOCKED\n" status += "8. 🚫 Validation - BLOCKED\n\n" status += "### πŸ“ Files Created:\n" status += "- `infra/network_model.yaml` - Source of Truth\n" status += "- `infra/bill_of_materials.json` - BOM data\n" status += "- `infra/setup_guide.md` - Deployment guide\n" questions = results.get('questions', []) if questions: status += "\n### ❓ Clarifying Questions\n" for q in questions: status += f"- {q}\n" # Extract outputs sot_yaml = results.get('model', {}) import yaml sot_str = yaml.dump(sot_yaml, default_flow_style=False) bom_md = results.get('shopping_list', 'No BOM generated') setup_md = results.get('setup_guide', 'No setup guide generated') updated_stats = monitor.get_stats().format_dashboard(include_heading=False) updated_activity = monitor.format_activity_feed() # Cache last known good outputs for offline fallback try: save_smoke_cache({ "status": status, "sot": sot_str, "bom": bom_md, "setup": setup_md, "stats": updated_stats, "activity": updated_activity, }) except Exception: pass return status, sot_str, bom_md, setup_md, updated_stats, updated_activity except Exception as e: cached = load_smoke_cache() import traceback error_block = f"\n\n```\n{traceback.format_exc()}\n```" if cached: status = f"## ⚠️ Live pipeline failed\n\n{str(e)}\n\nShowing last known good snapshot instead." status += error_block return ( status, cached.get("sot", ""), cached.get("bom", ""), cached.get("setup", ""), cached.get("stats", monitor.get_stats().format_dashboard(include_heading=False)), cached.get("activity", monitor.format_activity_feed()), ) else: error = f"## ❌ Pipeline Error\n\n{str(e)}" error += error_block updated_stats = monitor.get_stats().format_dashboard(include_heading=False) updated_activity = monitor.format_activity_feed() return error, "", "", "", updated_stats, updated_activity # Event Handlers run_pipeline_btn.click( fn=run_pipeline, inputs=[ pipeline_input, mode_choice, gns3_project_input, sot_path_input, simulation_only, read_only_validation, anthropic_key_box, openai_key_box, ], outputs=[pipeline_status, sot_output, bom_output, setup_output, api_stats, api_activity], ) return demo demo = build_ui().queue() # enable queue by default for Spaces app = demo # Hugging Face picks this up automatically if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860, share=False, show_api=False)