YueGuobin 46b262a02c feat(agent): add gns3-copilot AI assistant integration module
Integrate the gns3-copilot AI assistant module to provide intelligent
   automation and interaction capabilities for GNS3 network emulation.

   Key components:
   - AI agent framework with LLM integration (supports Qwen vision model)
   - GNS3 client library for project topology management
   - Extensive prompt templates for various network operation scenarios
   - Tool library for node creation, linking, configuration, and management
   - Support for English level assessment (A1-C2) and specialized personas
   - Network drawing and topology visualization tools
   - Linux device automation via Nornir/Telnetlib
   - Window controller for UI interaction

   Features:
   - Multi-modal AI agent with vision capabilities
   - Automated network topology deployment and configuration
   - Interactive node and drawing management
   - File-based project operations (read, write, list)
   - Specialized prompts for different scenarios and skill levels
   - Comprehensive tool set for network device management
2026-03-03 23:08:07 +08:00

438 lines
13 KiB
Python

"""
Drawing utility functions for GNS3 area annotations.
Calculates drawing parameters and generates SVG content for network area annotations.
Supports ellipse and rectangle shapes for two-node annotations.
"""
import math
from typing import Any, Literal
# Default parameters
DEFAULT_DEVICE_WIDTH = 50
DEFAULT_DEVICE_HEIGHT = 50
DEFAULT_FONT_SIZE = 14
DEFAULT_SHAPE_Z = 0
DEFAULT_TEXT_Z = 1
# Z-order thresholds (pixels)
Z_ORDER_LARGE_AREA_THRESHOLD = 500 # z=1 for large areas
Z_ORDER_MEDIUM_AREA_THRESHOLD = 300 # z=2 for medium areas
# Color schemes (business professional style)
# Text uses 'stroke', shape uses 'fill' with 'fill-opacity'
COLOR_SCHEMES = {
# Core/Backbone - biz_blue
"CORE_BACKBONE": {
"stroke": "#FFFFFF", # White (for text on dark background)
"fill": "#2980B9", # Blue background
"fill_opacity": 0.8, # Transparency level
},
# Normal Areas - biz_blue_light
"NORMAL_AREA": {
"stroke": "#000000", # Black (for text on light background)
"fill": "#5AA9DD", # Light blue background
"fill_opacity": 0.8, # Transparency level
},
# Logical Isolation - biz_purple
"ISOLATION": {
"stroke": "#FFFFFF", # White (for text on dark background)
"fill": "#9B59B6", # Purple background
"fill_opacity": 0.8, # Transparency level
},
# Management/Infrastructure - biz_orange
"MANAGEMENT_INFRA": {
"stroke": "#FFFFFF", # White (for text on medium-dark background)
"fill": "#E67E22", # Orange background
"fill_opacity": 0.8, # Transparency level
},
# Redundancy/High Availability - biz_orange_bright
"HIGH_AVAILABILITY": {
"stroke": "#000000", # Black (for text on bright background)
"fill": "#F39C12", # Bright orange background
"fill_opacity": 0.8, # Transparency level
},
# External/Boundary - biz_red_bright
"EXTERNAL": {
"stroke": "#FFFFFF", # White (for text on dark background)
"fill": "#E74C3C", # Bright red background
"fill_opacity": 0.8, # Transparency level
},
# Security/Trusted - biz_green_bright
"SECURITY_TRUSTED": {
"stroke": "#FFFFFF", # White (for text on medium-dark background)
"fill": "#2ECC71", # Bright green background
"fill_opacity": 0.8, # Transparency level
},
# Cloud/Tunnel - biz_cyan_dark
"CLOUD_TUNNEL": {
"stroke": "#FFFFFF", # White (for text on dark background)
"fill": "#00CED1", # Dark cyan background
"fill_opacity": 0.8, # Transparency level
},
# Default - biz_gray
"DEFAULT": {
"stroke": "#FFFFFF", # White (for text on medium-dark background)
"fill": "#A5CC78", # Gray background
"fill_opacity": 0.8, # Transparency level
},
}
def calculate_two_node_shape(
node1: dict,
node2: dict,
area_name: str,
shape_type: Literal["ellipse", "rectangle"] = "ellipse",
text_offset_ratio: float = 0.0,
) -> dict[str, Any]:
"""
Calculate shape annotation parameters for two nodes.
Args:
node1: First node with 'x', 'y', 'height', 'width' (top-left)
node2: Second node with 'x', 'y', 'height', 'width' (top-left)
area_name: Name of area (e.g., "Area 0", "AS 100")
shape_type: "ellipse" or "rectangle" (default: "ellipse")
text_offset_ratio: Ratio to offset text (0=center, positive=offset)
Returns:
Dict with shape, text SVG params, and metadata
"""
node1_width = node1.get("width", DEFAULT_DEVICE_WIDTH)
node1_height = node1.get("height", DEFAULT_DEVICE_HEIGHT)
node2_width = node2.get("width", DEFAULT_DEVICE_WIDTH)
node2_height = node2.get("height", DEFAULT_DEVICE_HEIGHT)
node1_center_x = node1["x"] + (node1_width / 2)
node1_center_y = node1["y"] + (node1_height / 2)
node2_center_x = node2["x"] + (node2_width / 2)
node2_center_y = node2["y"] + (node2_height / 2)
distance = math.sqrt(
(node2_center_x - node1_center_x) ** 2 + (node2_center_y - node1_center_y) ** 2
)
angle_rad = math.atan2(
node2_center_y - node1_center_y, node2_center_x - node1_center_x
)
angle_deg = round(math.degrees(angle_rad))
angle_rad = math.radians(angle_deg)
center_x = (node1_center_x + node2_center_x) / 2
center_y = (node1_center_y + node2_center_y) / 2
color_scheme = _get_color_scheme(area_name)
text_svg = generate_text_svg(area_name, color_scheme)
text_svg_width = len(area_name) * 8 + 20
text_svg_height = DEFAULT_FONT_SIZE + 16
if shape_type == "ellipse":
rx = distance / 2
ry = math.sqrt((node1_width / 2) ** 2 + (node1_height / 2) ** 2)
shape_width = rx * 2
shape_height = ry * 2
svg_x = center_x - (rx * math.cos(angle_rad) - ry * math.sin(angle_rad))
svg_y = center_y - (rx * math.sin(angle_rad) + ry * math.cos(angle_rad))
shape_svg = generate_ellipse_svg(
int(rx), int(ry), color_scheme, int(shape_width), int(shape_height)
)
offset_distance = ry * text_offset_ratio
metadata = {
"center_x": center_x,
"center_y": center_y,
"distance": distance,
"shape_width": shape_width,
"shape_height": shape_height,
"angle_deg": angle_deg,
"rx": rx,
"ry": ry,
}
else: # rectangle
shape_width = distance
shape_height = max(node1_width, node1_height, node2_width, node2_height)
svg_x = center_x - (
(shape_width / 2) * math.cos(angle_rad)
- (shape_height / 2) * math.sin(angle_rad)
)
svg_y = center_y - (
(shape_width / 2) * math.sin(angle_rad)
+ (shape_height / 2) * math.cos(angle_rad)
)
shape_svg = generate_rectangle_svg(
int(shape_width), int(shape_height), color_scheme
)
offset_distance = (shape_height / 2) * text_offset_ratio
metadata = {
"center_x": center_x,
"center_y": center_y,
"distance": distance,
"shape_width": shape_width,
"shape_height": shape_height,
"angle_deg": angle_deg,
"rx": None,
"ry": None,
}
if text_offset_ratio != 0:
perpendicular_x = -math.sin(angle_rad)
perpendicular_y = math.cos(angle_rad)
text_offset_x = perpendicular_x * offset_distance
text_offset_y = perpendicular_y * offset_distance
text_x = int(center_x + text_offset_x - text_svg_width / 2)
text_y = int(center_y + text_offset_y - text_svg_height / 2)
else:
text_x = int(center_x - text_svg_width / 2)
text_y = int(center_y - text_svg_height / 2)
return {
"shape": {
"svg": shape_svg,
"x": int(svg_x),
"y": int(svg_y),
"z": DEFAULT_SHAPE_Z,
"rotation": int(angle_deg),
"type": shape_type,
},
"text": {
"svg": text_svg,
"x": text_x,
"y": text_y,
"z": DEFAULT_TEXT_Z,
"rotation": 0,
},
"metadata": metadata,
}
def generate_ellipse_svg(
rx: int,
ry: int,
color_scheme: dict[str, Any],
svg_width: int,
svg_height: int,
) -> str:
"""Generate SVG for ellipse."""
return f'''<svg width="{svg_width}" height="{svg_height}"><ellipse cx="{rx}" cy="{ry}" rx="{rx}" ry="{ry}" fill="{color_scheme["fill"]}" fill-opacity="{color_scheme["fill_opacity"]}"/></svg>'''
def generate_rectangle_svg(
width: int,
height: int,
color_scheme: dict[str, Any],
) -> str:
"""Generate SVG for rectangle."""
return f'''<svg width="{width}" height="{height}"><rect x="0" y="0" width="{width}" height="{height}" fill="{color_scheme["fill"]}" fill-opacity="{color_scheme["fill_opacity"]}"/></svg>'''
def generate_text_svg(text: str, color_scheme: dict[str, Any]) -> str:
"""Generate SVG for text label."""
text_width = len(text) * 8 + 20
text_height = DEFAULT_FONT_SIZE + 16
return f'''<svg width="{text_width}" height="{text_height}"><text font-family="TypeWriter" font-size="{DEFAULT_FONT_SIZE}.0" font-weight="bold" fill="{color_scheme["stroke"]}" text-anchor="middle" x="{text_width / 2}" y="{text_height / 2 + 4}">{text}</text></svg>'''
def _hsv_to_hex(h: int, s: int, v: int) -> str:
"""Convert HSV to HEX color."""
h_norm = (h % 360) / 360
s_norm = s / 100
v_norm = v / 100
c: float = v_norm * s_norm
x: float = c * (1 - abs((h_norm / 60) % 2 - 1))
m: float = v_norm - c
r: float
g: float
b: float
if 0 <= h_norm < 60:
r, g, b = c, x, 0.0
elif 60 <= h_norm < 120:
r, g, b = x, c, 0.0
elif 120 <= h_norm < 180:
r, g, b = 0.0, c, x
elif 180 <= h_norm < 240:
r, g, b = 0.0, x, c
elif 240 <= h_norm < 300:
r, g, b = x, 0.0, c
else:
r, g, b = c, 0.0, x
r = int((r + m) * 255)
g = int((g + m) * 255)
b = int((b + m) * 255)
return f"#{r:02x}{g:02x}{b:02x}"
def calculate_z_order(area_size: float) -> int:
"""
Calculate z-order based on area size for proper layering.
Args:
area_size: Size of area (width for rectangles, 2*rx for ellipses)
Returns:
Z-order: 1=back (large), 2=middle, 3=front (small)
"""
if area_size >= Z_ORDER_LARGE_AREA_THRESHOLD:
return 1
elif area_size >= Z_ORDER_MEDIUM_AREA_THRESHOLD:
return 2
else:
return 3
def _get_color_scheme(area_name: str) -> dict[str, Any]:
"""
Get color scheme based on area name using keyword inference.
Maps labels to semantic groups for professional business styling.
"""
if area_name in COLOR_SCHEMES:
return COLOR_SCHEMES[area_name]
label = area_name.upper()
# 1. Routing Domain Core/Backbone
if "AREA 0" in label or "BACKBONE" in label or "CORE" in label:
return COLOR_SCHEMES["CORE_BACKBONE"]
if "BGP" in label or "AS " in label:
return COLOR_SCHEMES["CORE_BACKBONE"]
# 2. Routing Domain Normal Areas
if "AREA " in label or "LEVEL" in label:
return COLOR_SCHEMES["NORMAL_AREA"]
# 3. Logical Isolation
if (
"VRF" in label
or "VLAN" in label
or "MSTP" in label
or "VXLAN" in label
or "MPLS" in label
):
return COLOR_SCHEMES["ISOLATION"]
# 4. High Availability
if (
"VRRP" in label
or "HSRP" in label
or "HA" in label
or "STACK" in label
or "M-LAG" in label
):
return COLOR_SCHEMES["HIGH_AVAILABILITY"]
# 5. External/Internet
if (
"INET" in label
or "OUT" in label
or "EXTERNAL" in label
or "INTERNET" in label
or "DMZ" in label
):
return COLOR_SCHEMES["EXTERNAL"]
# 6. Management
if "MGMT" in label or "OOB" in label or "MANAGEMENT" in label:
return COLOR_SCHEMES["MANAGEMENT_INFRA"]
# 7. Security/Trusted
if "SECURITY" in label or "TRUSTED" in label or "VPN" in label:
return COLOR_SCHEMES["SECURITY_TRUSTED"]
# 8. Cloud/Tunnel
if "TUNNEL" in label or "CLOUD" in label or "GRE" in label or "IPSEC" in label:
return COLOR_SCHEMES["CLOUD_TUNNEL"]
# Legacy keyword matching for backward compatibility
label_lower = area_name.lower()
protocol_keywords = [
("ospf", "NORMAL_AREA"),
("is-is", "NORMAL_AREA"),
("rip", "NORMAL_AREA"),
("eigrp", "NORMAL_AREA"),
("bgp", "CORE_BACKBONE"),
("vxlan", "ISOLATION"),
("mpls", "ISOLATION"),
("vrf", "ISOLATION"),
("vlan", "ISOLATION"),
("vrrp", "HIGH_AVAILABILITY"),
("hsrp", "HIGH_AVAILABILITY"),
("gre", "CLOUD_TUNNEL"),
("ipsec", "SECURITY_TRUSTED"),
]
for keyword, scheme_key in protocol_keywords:
if keyword in label_lower:
return COLOR_SCHEMES[scheme_key]
return COLOR_SCHEMES["DEFAULT"]
def calculate_two_node_ellipse(
node1: dict,
node2: dict,
area_name: str,
text_offset_ratio: float = 0.0,
) -> dict[str, Any]:
"""
Calculate ellipse annotation parameters for two nodes.
Wrapper around calculate_two_node_shape with shape_type="ellipse".
"""
result = calculate_two_node_shape(
node1, node2, area_name, "ellipse", text_offset_ratio
)
return {
"ellipse": result["shape"],
"text": result["text"],
"metadata": result["metadata"],
}
def calculate_two_node_rectangle(
node1: dict,
node2: dict,
area_name: str,
text_offset_ratio: float = 0.0,
) -> dict[str, Any]:
"""
Calculate rectangle annotation parameters for two nodes.
Wrapper around calculate_two_node_shape with shape_type="rectangle".
"""
result = calculate_two_node_shape(
node1, node2, area_name, "rectangle", text_offset_ratio
)
metadata = result["metadata"]
return {
"rectangle": result["shape"],
"text": result["text"],
"metadata": {
"center_x": metadata["center_x"],
"center_y": metadata["center_y"],
"distance": metadata["distance"],
"rect_width": metadata["shape_width"],
"rect_height": metadata["shape_height"],
"angle_deg": metadata["angle_deg"],
},
}