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 = """ """ 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( """
Living OOB Mesh
MCP + Topology

๐ŸŒฟ Overgrowth

A living digital environment

""", ) 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'
{mermaid}
' else: ascii_diagram = diagrams.get('ascii', 'No diagram available') diagram = f"
{html.escape(ascii_diagram)}
" # 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)