Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import os | |
| # 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 | |
| import json | |
| import time | |
| import html | |
| 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 = """ | |
| <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script> | |
| <script> | |
| window.renderMermaid = function() { | |
| if (!window.mermaid) { return; } | |
| mermaid.initialize({ startOnLoad: false, theme: "neutral" }); | |
| document.querySelectorAll(".og-mermaid").forEach(el => el.removeAttribute("data-processed")); | |
| mermaid.run({ querySelector: ".og-mermaid" }); | |
| }; | |
| </script> | |
| """ | |
| def build_ui(): | |
| # Preload available GNS3 projects for brownfield imports | |
| try: | |
| project_choices = [ | |
| p.get("name") for p in get_lab_projects() if isinstance(p, dict) and p.get("name") | |
| ] | |
| except Exception: | |
| project_choices = [] | |
| 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( | |
| """ | |
| <div class="og-hero"> | |
| <div class="og-pill">Living OOB Mesh</div> | |
| <div class="og-pill">MCP + Topology</div> | |
| <h2 style="font-family:'Playfair Display',serif; margin:6px 0 4px 0;">🌿 Overgrowth</h2> | |
| <p style="margin:0; font-size:15px; color:#0f4d3f;"> | |
| A living digital environment | |
| </p> | |
| </div> | |
| """, | |
| ) | |
| 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") | |
| # 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]) | |
| 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)" | |
| ) | |
| gns3_project_input = gr.Dropdown( | |
| label="GNS3 project (brownfield)", | |
| choices=project_choices or ["overgrowth"], | |
| value=project_choices[0] if project_choices else "overgrowth" | |
| ) | |
| 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") | |
| with gr.Tabs(): | |
| with gr.Tab("📋 Source of Truth"): | |
| sot_output = gr.Code(label="Network Model (YAML)", language="yaml", lines=20) | |
| with gr.Tab("🛒 Bill of Materials"): | |
| bom_output = gr.Markdown(label="Shopping List") | |
| with gr.Tab("🔧 Setup Guide"): | |
| setup_output = gr.Markdown(label="Deployment Guide") | |
| with gr.Tab("📊 Diagram"): | |
| diagram_output = gr.HTML(label="Network Diagram") | |
| # Pipeline handler | |
| def run_pipeline(user_input, mode_choice, gns3_project, sot_path, simulation_only_flag, read_only_flag): | |
| """Execute the full automation pipeline""" | |
| from agent.pipeline_engine import OvergrowthPipeline | |
| from agent.llm_client import LLMClient | |
| pipeline = OvergrowthPipeline() | |
| try: | |
| # Report LLM env status up-front so users see if keys are missing | |
| llm_env = LLMClient.env_status() | |
| # 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) | |
| 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" | |
| # 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" | |
| 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" | |
| 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 - Visualizations created\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') | |
| diagrams = results.get('diagrams', {}) or {} | |
| mermaid = diagrams.get('mermaid') | |
| if mermaid: | |
| diagram = f'<div class="mermaid og-mermaid">{mermaid}</div><script>window.renderMermaid && window.renderMermaid();</script>' | |
| else: | |
| ascii_diagram = diagrams.get('ascii', 'No diagram available') | |
| diagram = f"<pre>{html.escape(ascii_diagram)}</pre>" | |
| # Also return updated API stats and activity | |
| updated_stats = monitor.get_stats().format_dashboard(include_heading=False) | |
| updated_activity = monitor.format_activity_feed() | |
| return status, sot_str, bom_md, setup_md, diagram, updated_stats, updated_activity | |
| except Exception as e: | |
| error = f"## ❌ Pipeline Error\n\n{str(e)}" | |
| import traceback | |
| error += f"\n\n```\n{traceback.format_exc()}\n```" | |
| # Still update API stats even on error | |
| 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], | |
| outputs=[pipeline_status, sot_output, bom_output, setup_output, diagram_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) | |