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)