import gradio as gr import os from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() 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, ) import json 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; } """ def build_ui(): 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( """
Living OOB Mesh
MCP + Topology

đŸŒŋ Overgrowth

A living digital environment

""", ) device_choices = topology.list_devices() with gr.Row(elem_classes=["og-main-row"]): with gr.Column(scale=1, elem_classes=["og-panel"]): change_request = gr.Textbox( label="Describe your network change", placeholder=( "Example: Add VLAN 120 for tenant Blue on leaf-01 and " "uplink it to tor-03 (staging only)." ), lines=6, ) run_btn = gr.Button("Run Analysis", variant="primary") with gr.Column(scale=2, elem_classes=["og-panel", "og-tabs"]): with gr.Tab("Plan"): plan_md = gr.Markdown(label="Planned Steps (Hybrid NL + JSON)") with gr.Tab("Risk & Topology"): risk_md = gr.Markdown(label="Risk & Topology Summary") with gr.Tab("Diffs"): diffs_md = gr.Markdown(label="Config Diff Summary") with gr.Tab("Rollback"): rollback_md = gr.Markdown(label="Rollback Plan") with gr.Tab("Tool Calls"): tool_log_md = gr.Markdown(label="MCP Tool Call Log") with gr.Tab("OOB Troubleshooting"): troubleshoot_device = gr.Dropdown( label="Device", choices=device_choices or ["leaf-01", "leaf-02"], value=device_choices[0] if device_choices else "leaf-01", ) troubleshoot_text = gr.Textbox( label="Describe the issue", placeholder="Example: Link flaps on leaf-01, seeing packet loss.", lines=3, ) troubleshoot_btn = gr.Button("Run Troubleshooting") troubleshoot_md = gr.Markdown(label="Troubleshooting Summary") troubleshoot_log = gr.Markdown(label="Troubleshooting Log") troubleshoot_actions = gr.Markdown(label="Recommended Actions") troubleshoot_conf = gr.Markdown(label="Root Cause Confidence") troubleshoot_state = gr.Markdown(label="Device State Snapshot") execute_btn = gr.Button("Execute Recovery Plan") execute_summary = gr.Markdown(label="Recovery Execution Summary") execute_log = gr.Markdown(label="Recovery Execution Log") reset_btn = gr.Button("Reset Overgrowth State (dangerous)", variant="secondary") reset_md = gr.Markdown(label="State Reset Log") with gr.Tab("Synapse Log"): synapse_btn = gr.Button("Refresh Log") synapse_md = gr.Markdown(label="Overgrowth Synapse Log") with gr.Tab("Build Network"): gr.Markdown("### 🌱 Build Network from Description\nDescribe your network needs in plain English - Overgrowth builds and configures it automatically.") network_description = gr.Textbox( label="Network Description", placeholder=( "Example: I'm running a big music festival with multiple stages. " "I need networks for: main stage with audio/video systems, " "food truck area with 8 POS terminals, VIP lounge with WiFi, " "ticketing booth with 4 terminals, and backstage with artist WiFi. " "All need secure management access and internet connectivity." ), lines=8, ) with gr.Row(): build_project_name = gr.Textbox( label="Project Name", value="overgrowth", scale=2 ) auto_config_checkbox = gr.Checkbox( label="Auto-Configure Devices", value=True, scale=1 ) build_btn = gr.Button("🚀 Build Network", variant="primary", size="lg") build_result = gr.Markdown(label="Build Results") build_topology = gr.Code(label="Topology Details", language="json") with gr.Tab("Lab Management"): gr.Markdown("### 🌱 GNS3 Lab Control\nManage your actual lab infrastructure at lab.grahampaasch.com") with gr.Row(): with gr.Column(): gr.Markdown("#### Projects") refresh_projects_btn = gr.Button("Refresh Projects", variant="secondary") projects_list = gr.Markdown(label="Available Projects") gr.Markdown("#### Topology") project_name = gr.Textbox( label="Project Name", value="overgrowth", placeholder="Enter GNS3 project name" ) get_topology_btn = gr.Button("Get Topology") topology_summary = gr.Markdown(label="Topology Summary") topology_diagram = gr.Code(label="Network Diagram (ASCII)", language="plaintext") topology_details = gr.Code(label="Topology JSON", language="json") with gr.Column(): gr.Markdown("#### Device Control") device_project = gr.Textbox( label="Project", value="overgrowth" ) device_name = gr.Textbox( label="Device Name", placeholder="e.g., Switch-1" ) with gr.Row(): start_device_btn = gr.Button("â–ļī¸ Start", variant="primary") stop_device_btn = gr.Button("âšī¸ Stop", variant="stop") device_status = gr.Markdown(label="Status") gr.Markdown("#### Device Configuration") device_host = gr.Textbox( label="Device IP/Host", placeholder="e.g., 192.168.1.10" ) device_user = gr.Textbox( label="SSH Username", value="admin" ) get_config_btn = gr.Button("Get Config") config_display = gr.Code(label="Configuration", language="python") # text not supported, use python config_commands = gr.Textbox( label="Config Commands (one per line)", placeholder="interface GigabitEthernet0/0\nip address 10.0.0.1 255.255.255.0", lines=4 ) apply_config_btn = gr.Button("Apply Configuration", variant="primary") config_result = gr.Markdown(label="Result") backup_name = gr.Textbox( label="Backup Device Name", placeholder="e.g., router-1" ) backup_btn = gr.Button("Backup Config", variant="secondary") backup_result = gr.Markdown(label="Backup Result") def on_run(change_text: str): """ Wraps the agent pipeline for Gradio. Returns: plan_markdown, risk_markdown, diffs_markdown, rollback_markdown, tool_log_markdown """ return analyze_change(change_text) def run_troubleshoot(dev, text): return oob_troubleshoot(dev, text) def execute_recovery_plan_wrapper(dev, text): return perform_recovery(dev, text) # Lab Management handlers def refresh_projects(): projects = get_lab_projects() if projects and not projects[0].get('error'): lines = ["### GNS3 Projects\n"] for p in projects[:20]: # Show first 20 status = p.get('status', 'unknown') lines.append(f"- **{p['name']}** ({status})") return "\n".join(lines) elif projects and projects[0].get('error'): return f"❌ Error: {projects[0]['error']}" else: return "No projects found" def get_topology(proj_name): """Fetch topology and return summary, diagram, and JSON""" topo = get_lab_topology(proj_name) if topo.get('error'): error_msg = f"❌ Error: {topo['error']}" return error_msg, error_msg, json.dumps({"error": topo['error']}, indent=2) # Return summary, ASCII diagram, and JSON summary = topo.get('summary', '') diagram = topo.get('ascii_diagram', 'No diagram available') json_str = json.dumps({ 'project': topo.get('project'), 'status': topo.get('status'), 'nodes': topo.get('nodes', []), 'links': topo.get('links', []) }, indent=2) return summary, diagram, json_str def start_device(proj, dev): result = manage_device("start", proj, dev) if result.get('success'): return f"✅ Started **{dev}** in project **{proj}**" else: return f"❌ Failed to start {dev}: {result.get('error', 'unknown error')}" def stop_device(proj, dev): result = manage_device("stop", proj, dev) if result.get('success'): return f"✅ Stopped **{dev}** in project **{proj}**" else: return f"❌ Failed to stop {dev}: {result.get('error', 'unknown error')}" def get_config(host, user): result = get_device_configuration(host, user) if result.get('success'): return result.get('config', '') else: return f"Error: {result.get('error', 'unknown error')}" def apply_config(host, user, commands_text): commands = [cmd.strip() for cmd in commands_text.split('\n') if cmd.strip()] if not commands: return "❌ No commands provided" result = configure_device(host, commands, user) if result.get('success'): return f"✅ Configuration applied to **{host}**\n\n```\n{result.get('output', '')}\n```" else: return f"❌ Failed: {result.get('error', 'unknown error')}" def backup_config(host, user, dev_name): result = backup_device_config(host, dev_name, user) if result.get('success'): return f"✅ Backup completed for **{dev_name}**\n\n{result.get('backup_info', '')}" else: return f"❌ Backup failed: {result.get('error', 'unknown error')}" def build_network(description, project, auto_config): if not description or not description.strip(): return "âš ī¸ Please provide a network description", "" result_md = "### đŸ—ī¸ Building Network...\n\n" result_md += f"**Description:** {description}\n\n" result_md += f"**Project:** {project}\n\n" result_md += "---\n\n" result = build_network_from_description(description, project, auto_config) if result.get('success'): result_md += "## ✅ Network Built Successfully!\n\n" # Parse the result topology_info = result.get('topology', {}) config_info = result.get('configuration', {}) result_md += f"### 📊 Topology Created\n" result_md += f"- **Nodes:** {topology_info.get('node_count', 'N/A')}\n" result_md += f"- **Links:** {topology_info.get('link_count', 'N/A')}\n\n" if auto_config and config_info: result_md += f"### 🔧 Auto-Configuration\n" result_md += f"- **Status:** {config_info.get('status', 'Completed')}\n" result_md += f"- **Devices Configured:** {config_info.get('configured_count', 'N/A')}\n\n" if config_info.get('details'): result_md += "**Configuration Details:**\n" result_md += f"```\n{config_info['details']}\n```\n\n" result_md += "### đŸŽ¯ Next Steps\n" result_md += "1. Go to **Lab Management** tab to view topology\n" result_md += "2. Verify device configurations\n" result_md += "3. Start testing your network\n\n" result_md += f"### 🌐 Access Your Network\n" result_md += f"View in GNS3: `http://lab.grahampaasch.com:3080`\n\n" # Return topology as formatted JSON import json topology_json = json.dumps(result, indent=2) return result_md, topology_json else: error_msg = "## ❌ Failed to build network:\n\n" # Extract error details if available error_text = result.get('text', result.get('error', 'Unknown error')) error_msg += f"```\n{error_text}\n```\n\n" error_msg += "**Possible causes:**\n" error_msg += "- GNS3 server may be unreachable\n" error_msg += "- Network timeout (check connection to lab.grahampaasch.com)\n" error_msg += "- Template or resource not available\n\n" error_msg += "Please check the logs and try again." import json error_json = json.dumps(result, indent=2) return error_msg, error_json run_btn.click( fn=on_run, inputs=[change_request], outputs=[plan_md, risk_md, diffs_md, rollback_md, tool_log_md], ) troubleshoot_btn.click( fn=run_troubleshoot, inputs=[troubleshoot_device, troubleshoot_text], outputs=[ troubleshoot_md, troubleshoot_log, troubleshoot_actions, troubleshoot_conf, troubleshoot_state, ], ) execute_btn.click( fn=execute_recovery_plan_wrapper, inputs=[troubleshoot_device, troubleshoot_text], outputs=[execute_summary, execute_log], ) reset_btn.click( fn=reset_state, inputs=[], outputs=[reset_md], ) synapse_btn.click(fn=load_synapse_log, inputs=None, outputs=synapse_md) # Lab Management click handlers refresh_projects_btn.click( fn=refresh_projects, inputs=[], outputs=[projects_list] ) get_topology_btn.click( fn=get_topology, inputs=[project_name], outputs=[topology_summary, topology_diagram, topology_details] ) start_device_btn.click( fn=start_device, inputs=[device_project, device_name], outputs=[device_status] ) stop_device_btn.click( fn=stop_device, inputs=[device_project, device_name], outputs=[device_status] ) get_config_btn.click( fn=get_config, inputs=[device_host, device_user], outputs=[config_display] ) apply_config_btn.click( fn=apply_config, inputs=[device_host, device_user, config_commands], outputs=[config_result] ) backup_btn.click( fn=backup_config, inputs=[device_host, device_user, backup_name], outputs=[backup_result] ) build_btn.click( fn=build_network, inputs=[network_description, build_project_name, auto_config_checkbox], outputs=[build_result, build_topology] ) 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)