Spaces:
Sleeping
Sleeping
| import json | |
| from typing import List, Dict | |
| def to_pretty_json(data) -> str: | |
| return json.dumps(data, indent=2, sort_keys=True) | |
| # Known presets from the Track 1 MCP server: | |
| PRESET_KEYWORDS = [ | |
| { | |
| "change_type": "vlan", | |
| "preset_id": "leaf_tor_vlan_stage", | |
| "keywords": ["stage", "staging", "test"], | |
| "default": False, | |
| }, | |
| { | |
| "change_type": "vlan", | |
| "preset_id": "leaf_tor_vlan_commit", | |
| "keywords": ["commit", "production", "prod", "rollout"], | |
| "default": False, | |
| }, | |
| { | |
| "change_type": "interface", | |
| "preset_id": "tor_uplink_enable", | |
| "keywords": ["enable uplink", "bring up uplink", "turn on uplink"], | |
| "default": False, | |
| }, | |
| { | |
| "change_type": "interface", | |
| "preset_id": "tor_uplink_shutdown", | |
| "keywords": ["shutdown uplink", "shut down uplink", "disable uplink"], | |
| "default": False, | |
| }, | |
| { | |
| "change_type": "bgp_neighbor", | |
| "preset_id": "leaf_bgp_fabric_neighbor_add", | |
| "keywords": ["add bgp", "new neighbor", "add neighbor"], | |
| "default": False, | |
| }, | |
| { | |
| "change_type": "bgp_neighbor", | |
| "preset_id": "leaf_bgp_fabric_neighbor_remove", | |
| "keywords": ["remove bgp", "delete neighbor", "remove neighbor"], | |
| "default": False, | |
| }, | |
| ] | |
| def _guess_preset(change_text: str) -> Dict[str, str]: | |
| text = change_text.lower() | |
| # First pass: explicit keywords. | |
| for preset in PRESET_KEYWORDS: | |
| for kw in preset["keywords"]: | |
| if kw in text: | |
| return { | |
| "change_type": preset["change_type"], | |
| "preset_id": preset["preset_id"], | |
| "mode": "lightning", | |
| } | |
| # Second pass: vague cues (VLAN vs interface vs bgp) | |
| if "vlan" in text: | |
| return { | |
| "change_type": "vlan", | |
| "preset_id": "leaf_tor_vlan_stage", | |
| "mode": "lightning", | |
| } | |
| if "uplink" in text or "interface" in text: | |
| return { | |
| "change_type": "interface", | |
| "preset_id": "tor_uplink_enable", | |
| "mode": "lightning", | |
| } | |
| if "bgp" in text or "neighbor" in text: | |
| return { | |
| "change_type": "bgp_neighbor", | |
| "preset_id": "leaf_bgp_fabric_neighbor_add", | |
| "mode": "lightning", | |
| } | |
| # Fallback: safest-ish default. | |
| return { | |
| "change_type": "vlan", | |
| "preset_id": "leaf_tor_vlan_stage", | |
| "mode": "lightning", | |
| } | |
| def _guess_device(change_text: str) -> str: | |
| """ | |
| Ultra-naive device extraction: look for leaf-XX / tor-XX / core-XX tokens, | |
| otherwise default to leaf-01. | |
| """ | |
| text = change_text.lower() | |
| for prefix in ["leaf-", "tor-", "core-"]: | |
| idx = text.find(prefix) | |
| if idx != -1: | |
| # grab token like leaf-01 | |
| token = text[idx:].split()[0].strip(",.;:") | |
| return token | |
| return "leaf-01" | |
| def parse_change_request(change_text: str) -> List[Dict]: | |
| """ | |
| For now, treat the entire request as one step. | |
| Later you can split into multiple steps by sentence/semicolon/etc. | |
| """ | |
| base = _guess_preset(change_text) | |
| device = _guess_device(change_text) | |
| step_json = { | |
| "device": device, | |
| "action": f"{base['change_type']}_change", | |
| "change_type": base["change_type"], | |
| "preset_id": base["preset_id"], | |
| "mode": base["mode"], | |
| "raw_text": change_text.strip(), | |
| } | |
| step = { | |
| "description": change_text.strip(), | |
| "json": step_json, | |
| } | |
| return [step] | |
| def build_diff_summaries(steps: List[Dict]) -> List[Dict]: | |
| """ | |
| Produces simple 'diff-ish' summaries per step. | |
| """ | |
| summaries = [] | |
| for step in steps: | |
| j = step["json"] | |
| device = j.get("device", "device") | |
| change_type = j.get("change_type", "change") | |
| preset_id = j.get("preset_id", "preset") | |
| title = f"{change_type} change on {device} ({preset_id})" | |
| body = ( | |
| f"# BEFORE (conceptual)\n" | |
| f"# - configuration in stable state for {change_type} preset '{preset_id}'\n\n" | |
| f"# AFTER (conceptual)\n" | |
| f"# - configuration reflecting applied preset '{preset_id}' on {device}\n" | |
| ) | |
| summaries.append({"title": title, "body": body}) | |
| return summaries | |
| def build_rollback_plan(steps: List[Dict]) -> List[Dict]: | |
| """ | |
| Invert each step in reverse order. | |
| """ | |
| rollback_steps = [] | |
| for step in reversed(steps): | |
| j = step["json"] | |
| device = j.get("device", "device") | |
| change_type = j.get("change_type", "change") | |
| preset_id = j.get("preset_id", "preset") | |
| description = f"Rollback {change_type} change on {device} (preset {preset_id})" | |
| commands = [ | |
| f"# rollback placeholder for {change_type} change on {device}", | |
| f"# original preset: {preset_id}", | |
| ] | |
| rollback_steps.append( | |
| { | |
| "description": description, | |
| "commands": commands, | |
| } | |
| ) | |
| return rollback_steps | |