feat: rename FlowNet-Lab to GNS3-Copilot across codebase

Update all references from FlowNet-Lab to GNS3-Copilot in package names, documentation, and logging. This includes:
- Module and package __init__.py files
- License headers and file descriptions
- Log messages and internal comments
- Remove deprecated tools: GNS3CreateAreaDrawingTool and LinuxTelnetBatchTool

The renaming aligns with the project's new branding while maintaining all existing functionality.
This commit is contained in:
YueGuobin 2026-03-04 00:55:29 +08:00
parent 95b309c7af
commit d525e5cad0
24 changed files with 47 additions and 3487 deletions

View File

@ -1,7 +1,7 @@
"""
FlowNet-Lab - AI-powered network automation assistant for GNS3.
GNS3-Copilot - AI-powered network automation assistant for GNS3.
This package provides a command-line interface for launching the FlowNet-Lab
This package provides a command-line interface for launching the GNS3-Copilot
Streamlit application with support for Streamlit parameter passthrough.
"""

View File

@ -1,7 +1,7 @@
"""
FlowNet-Lab Agent Package
GNS3-Copilot Agent Package
This package contains the main FlowNet-Lab agent implementation for network automation tasks.
This package contains the main GNS3-Copilot agent implementation for network automation tasks.
"""
from .gns3_copilot import agent_builder

View File

@ -1,19 +1,19 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# This file is part of FlowNet-Lab.
# This file is part of GNS3-Copilot.
#
# FlowNet-Lab is free software: you can redistribute it and/or modify it
# GNS3-Copilot is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# FlowNet-Lab is distributed in the hope that it will be useful, but
# GNS3-Copilot is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
# for more details.
#
# You should have received a copy of the GNU Affero General Public License along
# with FlowNet-Lab. If not, see <https://www.gnu.org/licenses/>.
# with GNS3-Copilot. If not, see <https://www.gnu.org/licenses/>.
# mypy: ignore-errors
@ -55,16 +55,14 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "backend"))
from gns3_copilot.tools_v2 import (
ExecuteMultipleDeviceCommands,
GNS3CreateAreaDrawingTool,
GNS3CreateNodeTool,
GNS3LinkTool,
GNS3StartNodeTool,
GNS3TemplateTool,
LinuxTelnetBatchTool,
VPCSMultiCommands,
)
# Set up logger for FlowNet-Lab
# Set up logger for GNS3-Copilot
logger = logging.getLogger(__name__)
# Note: LLM model configuration is now managed by the new llm_model_configs system.
@ -79,15 +77,13 @@ tools = [
GNS3StartNodeTool(), # Start GNS3 nodes
ExecuteMultipleDeviceCommands(), # Execute show/display/debug commands on multiple devices (READ-ONLY)
VPCSMultiCommands(), # Execute VPCS commands on multiple devices
LinuxTelnetBatchTool(), # Execute Linux commands via Telnet on multiple devices
GNS3CreateAreaDrawingTool(), # Create area drawings in GNS3 topologies
]
# Augment the LLM with tools
tools_by_name = {tool.name: tool for tool in tools}
# Model with tools will be created dynamically by the factory when needed
# Log application startup
logger.info("FlowNet-Lab application starting up")
logger.info("GNS3-Copilot application starting up")
# Constants for conversation title management
DEFAULT_CONVERSATION_TITLE = "New Conversation"
@ -98,7 +94,7 @@ TITLE_MAX_LENGTH = 40
# Define state
class MessagesState(TypedDict):
"""
FlowNet-Lab conversation state management class.
GNS3-Copilot conversation state management class.
Maintains the conversation state for the LangGraph workflow, including message history,
call counters, and session titles for comprehensive dialogue management.

View File

@ -1,5 +1,5 @@
"""
Model Factory for FlowNet-Lab Agent
Model Factory for GNS3-Copilot Agent
This module provides factory functions to create fresh LLM model instances.
Configuration is loaded from the llm_model_configs system via connector_factory.

View File

@ -11,20 +11,13 @@ Main classes:
- Node: GNS3 Node management
- Link: GNS3 Link management
- GNS3TopologyTool: GNS3 topology reading tool
- GNS3ProjectReadFileTool: LangChain tool for reading project files
- GNS3ProjectWriteFileTool: LangChain tool for writing project files
- GNS3ProjectListFilesTool: LangChain tool for listing project files
- GNS3ProjectLock: LangChain tool for locking/unlocking GNS3 projects
File Manager Modules:
- gns3_project_read_file: GNS3ProjectReadFileTool implementation
- gns3_project_write_file: GNS3ProjectWriteFileTool implementation
- gns3_project_list_files: GNS3ProjectListFilesTool implementation
- gns3_file_index: File index management utilities
- GNS3UpdateDrawingTool: GNS3 drawing update tool
Main functions:
- get_gns3_connector: Factory function to create Gns3Connector from environment
- get_gns3_connector_with_llm_config: Factory function to create connector AND retrieve LLM config
- get_gns3_server_host: Get GNS3 server hostname from Controller or Config
- get_llm_config: Get LLM model configuration for a user
"""
from .connector_factory import (
@ -42,21 +35,6 @@ from .custom_gns3fy import (
Node,
Project,
)
from .gns3_create_drawing import GNS3CreateDrawingTool
from .gns3_delete_drawing import GNS3DeleteDrawingTool
from .gns3_file_index import add_file_to_index, get_file_list
from .gns3_get_drawings import GNS3GetDrawingsTool
from .gns3_get_nodes import GNS3GetNodesTool
from .gns3_project_create import GNS3ProjectCreate
from .gns3_project_delete import GNS3ProjectDelete
from .gns3_project_list_files import GNS3ProjectListFilesTool
from .gns3_project_lock import GNS3ProjectLock
from .gns3_project_open import GNS3ProjectOpen
from .gns3_project_path import GNS3ProjectPath
from .gns3_project_read_file import GNS3ProjectReadFileTool
from .gns3_project_update import GNS3ProjectUpdate
from .gns3_project_write_file import GNS3ProjectWriteFileTool
from .gns3_projects_list import GNS3ProjectList
from .gns3_topology_reader import GNS3TopologyTool
from .gns3_update_drawing import GNS3UpdateDrawingTool
@ -81,25 +59,9 @@ __all__ = [
"CONSOLE_TYPES",
"LINK_TYPES",
"GNS3TopologyTool",
"GNS3ProjectList",
"GNS3ProjectOpen",
"GNS3ProjectPath",
"GNS3ProjectCreate",
"GNS3ProjectDelete",
"GNS3ProjectLock",
"GNS3ProjectUpdate",
"GNS3ProjectReadFileTool",
"GNS3ProjectWriteFileTool",
"GNS3ProjectListFilesTool",
"GNS3CreateDrawingTool",
"GNS3DeleteDrawingTool",
"GNS3GetDrawingsTool",
"GNS3GetNodesTool",
"GNS3UpdateDrawingTool",
"get_gns3_connector",
"get_gns3_connector_with_llm_config",
"get_gns3_server_host",
"get_llm_config",
"add_file_to_index",
"get_file_list",
]

View File

@ -1,292 +0,0 @@
"""
GNS3 drawing creation tool for adding graphical elements.
Provides functionality to create multiple drawings in a GNS3 project
using specified SVG content and coordinates through the GNS3 API.
"""
import json
import logging
from pprint import pprint
from typing import Any
from langchain.tools import BaseTool
from langchain_core.callbacks import CallbackManagerForToolRun
from gns3_copilot.gns3_client import Project, get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
class GNS3CreateDrawingTool(BaseTool):
"""
A LangChain tool to create multiple drawings in a GNS3 project
using specified SVG content and coordinates.
**Input:**
A JSON object containing the project_id and an array of drawings with SVG content,
coordinates, and optional properties.
Example input:
{
"project_id": "uuid-of-project",
"drawings": [
{
"svg": "<svg>...</svg>",
"x": 100,
"y": -200,
"z": 0,
"locked": false,
"rotation": 0
},
{
"svg": "<svg>...</svg>",
"x": -200,
"y": 300,
"z": 1,
"locked": true,
"rotation": 90
}
]
}
**Output:**
A dictionary containing the creation results for all drawings.
Example output:
{
"project_id": "uuid-of-project",
"created_drawings": [
{
"drawing_id": "uuid-of-drawing1",
"status": "success"
},
{
"drawing_id": "uuid-of-drawing2",
"status": "success"
}
],
"total_drawings": 2,
"successful_drawings": 2,
"failed_drawings": 0
}
If an error occurs during input validation, returns a dictionary with an error message.
"""
name: str = "create_gns3_drawing"
description: str = """
Creates multiple drawings in a GNS3 project using specified SVG content and coordinates.
Input is a JSON object with project_id and an array of drawings, each containing svg, x, y,
and optional z, locked, and rotation parameters.
Example input:
{
"project_id": "uuid-of-project",
"drawings": [
{
"svg": "<svg>...</svg>",
"x": 100,
"y": -200,
"z": 0,
"locked": false,
"rotation": 0
}
]
}
Returns a dictionary with creation results for all drawings, including success/failure status.
If the operation fails during input validation, returns a dictionary with an error message.
"""
def _run(
self,
tool_input: str,
run_manager: CallbackManagerForToolRun | None = None,
**kwargs: Any,
) -> dict[str, Any]:
"""
Creates multiple drawings in a GNS3 project using the provided SVG content and coordinates.
Args:
tool_input (str): A JSON string containing project_id and an array of drawings.
run_manager: LangChain run manager (unused).
Returns:
dict: A dictionary with creation results for all drawings or an error message.
"""
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Parse input JSON
input_data = json.loads(tool_input)
project_id = input_data.get("project_id")
drawings = input_data.get("drawings", [])
# Validate input
if not project_id:
logger.error("Invalid input: Missing project_id.")
return {"error": "Missing project_id."}
if not isinstance(drawings, list) or len(drawings) == 0:
logger.error("Invalid input: drawings must be a non-empty array.")
return {"error": "drawings must be a non-empty array."}
# Validate each drawing in the array
for i, drawing_data in enumerate(drawings):
if not isinstance(drawing_data, dict):
logger.error(
"Invalid input: Drawing %d must be a dictionary.", i + 1
)
return {"error": f"Drawing {i + 1} must be a dictionary."}
svg = drawing_data.get("svg")
x = drawing_data.get("x")
y = drawing_data.get("y")
if not all(
[svg, isinstance(x, (int, float)), isinstance(y, (int, float))]
):
logger.error(
"Invalid input: Drawing %d missing or invalid svg, x, or y.",
i + 1,
)
return {
"error": f"Drawing {i + 1} missing or invalid svg, x, or y."
}
# Initialize Gns3Connector using factory function
logger.info("Connecting to GNS3 server...")
gns3_server = get_gns3_connector()
if gns3_server is None:
logger.error("Failed to create GNS3 connector")
return {
"error": "Failed to connect to GNS3 server. Please check your configuration."
}
# Create project instance
logger.info(
"Creating %d drawings in project %s...", len(drawings), project_id
)
project = Project(project_id=project_id, connector=gns3_server)
# Create drawings
results: list[dict[str, Any]] = []
for i, drawing_data in enumerate(drawings):
try:
svg = drawing_data.get("svg")
x = drawing_data.get("x")
y = drawing_data.get("y")
z = drawing_data.get("z", 0)
locked = drawing_data.get("locked", False)
rotation = drawing_data.get("rotation", 0)
logger.info(
"Creating drawing %d/%d at coordinates (%s, %s)...",
i + 1,
len(drawings),
x,
y,
)
# Create drawing using Project method
result = project.create_drawing(
svg=svg,
x=int(x),
y=int(y),
z=int(z),
locked=bool(locked),
rotation=int(rotation),
)
drawing_info = {
"drawing_id": result.get("drawing_id"),
"status": "success",
}
results.append(drawing_info)
except Exception as e:
error_info = {
"error": f"Drawing {i + 1} creation failed: {str(e)}",
"status": "failed",
}
results.append(error_info)
logger.error("Failed to create drawing %d: %s", i + 1, e)
# Continue with next drawing even if one fails
# Calculate summary statistics
successful_drawings = len(
[r for r in results if r.get("status") == "success"]
)
failed_drawings = len([r for r in results if r.get("status") == "failed"])
# Prepare final result
final_result = {
"project_id": project_id,
"created_drawings": results,
"total_drawings": len(drawings),
"successful_drawings": successful_drawings,
"failed_drawings": failed_drawings,
}
# Log the final result
logger.info(
"Drawing creation completed: %d successful, %d failed out of %d total drawings.",
successful_drawings,
failed_drawings,
len(drawings),
)
# Return JSON-formatted result
return final_result
except json.JSONDecodeError as e:
logger.error("Invalid JSON input: %s", e)
return {"error": f"Invalid JSON input: {e}"}
except Exception as e:
logger.error("Failed to process drawing creation request: %s", e)
return {"error": f"Failed to process drawing creation request: {str(e)}"}
if __name__ == "__main__":
# Test the tool locally with multiple drawings
test_input = json.dumps(
{
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", # Replace with actual project UUID
"drawings": [
{
"svg": '<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50"><text x="10" y="30" font-size="14">Label 1</text></svg>',
"x": 100,
"y": -200,
"z": 0,
"locked": False,
"rotation": 0,
},
{
"svg": '<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50"><text x="10" y="30" font-size="14">Label 2</text></svg>',
"x": 200,
"y": -300,
"z": 1,
"locked": True,
"rotation": 90,
},
],
}
)
tool = GNS3CreateDrawingTool()
result = tool._run(test_input)
pprint(result)
"""
example output:
{'created_drawings': [{'drawing_id': '52045be2-d6d9-46f8-85af-c33bf7074b6a',
'status': 'success'},
{'drawing_id': '8f1838a7-1aa4-4613-acaa-b300b23e60d5',
'status': 'success'}],
'failed_drawings': 0,
'project_id': '2245149a-71c8-4387-9d1f-441a683ef7e7',
'successful_drawings': 2,
'total_drawings': 2}
"""

View File

@ -1,160 +0,0 @@
"""
GNS3 drawing deletion tool for removing graphical elements.
Provides functionality to delete drawings from a GNS3 project.
"""
import json
import logging
from pprint import pprint
from typing import Any
from langchain.tools import BaseTool
from langchain_core.callbacks import CallbackManagerForToolRun
from gns3_copilot.gns3_client import Project, get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
class GNS3DeleteDrawingTool(BaseTool):
"""
A LangChain tool to delete a drawing from a GNS3 project.
**Input:**
A JSON object containing the project_id and drawing_id.
Example input:
{
"project_id": "uuid-of-project",
"drawing_id": "uuid-of-drawing"
}
**Output:**
A dictionary containing the deletion result.
Example output:
{
"project_id": "uuid-of-project",
"drawing_id": "uuid-of-drawing",
"status": "success"
}
If an error occurs during input validation, returns a dictionary with an error message.
"""
name: str = "delete_gns3_drawing"
description: str = """
Deletes a drawing from a GNS3 project.
Input is a JSON object with project_id and drawing_id.
Example input:
{
"project_id": "uuid-of-project",
"drawing_id": "uuid-of-drawing"
}
Returns a dictionary with deletion result and status.
If the operation fails, returns a dictionary with an error message.
"""
def _run(
self,
tool_input: str,
run_manager: CallbackManagerForToolRun | None = None,
**kwargs: Any,
) -> dict[str, Any]:
"""
Deletes a drawing from a GNS3 project.
Args:
tool_input (str): A JSON string containing project_id and drawing_id.
run_manager: LangChain run manager (unused).
Returns:
dict: A dictionary with the deletion result and status or an error message.
"""
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Parse input JSON
input_data = json.loads(tool_input)
project_id = input_data.get("project_id")
drawing_id = input_data.get("drawing_id")
# Validate input
if not project_id:
logger.error("Invalid input: Missing project_id.")
return {"error": "Missing project_id."}
if not drawing_id:
logger.error("Invalid input: Missing drawing_id.")
return {"error": "Missing drawing_id."}
# Initialize Gns3Connector using factory function
logger.info("Connecting to GNS3 server...")
gns3_server = get_gns3_connector()
if gns3_server is None:
logger.error("Failed to create GNS3 connector")
return {
"error": "Failed to connect to GNS3 server. Please check your configuration."
}
# Create project instance
logger.info(
"Deleting drawing %s from project %s...", drawing_id, project_id
)
project = Project(project_id=project_id, connector=gns3_server)
# Delete the drawing
project.delete_drawing(drawing_id=drawing_id)
# Prepare final result
final_result = {
"project_id": project_id,
"drawing_id": drawing_id,
"status": "success",
}
# Log the final result
logger.info("Drawing deletion completed successfully.")
# Return JSON-formatted result
return final_result
except json.JSONDecodeError as e:
logger.error("Invalid JSON input: %s", e)
return {"error": f"Invalid JSON input: {e}"}
except ValueError as e:
logger.error("Value error: %s", e)
return {"error": str(e)}
except Exception as e:
logger.error("Failed to delete drawing: %s", e)
return {"error": f"Failed to delete drawing: {str(e)}"}
if __name__ == "__main__":
# Test the tool locally
test_input = json.dumps(
{
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", # Replace with actual project UUID
"drawing_id": "0728feaf-defd-40e3-ae02-1f97734810e2", # Replace with actual drawing UUID
}
)
tool = GNS3DeleteDrawingTool()
result = tool._run(test_input)
pprint(result)
"""
example output:
error output when drawing is locked:
{'error': 'Failed to delete drawing: Unknown Status: Drawing ID '
'daf3385a-86f0-458d-8563-6e1fbd87af77 cannot be deleted because it '
'is locked (Original 409 Error)'}
successful output:
{'drawing_id': 'daf3385a-86f0-458d-8563-6e1fbd87af77',
'project_id': '2245149a-71c8-4387-9d1f-441a683ef7e7',
'status': 'success'}
"""

View File

@ -1,257 +0,0 @@
"""
GNS3 File Index Manager
This module provides functionality to manage an index of files in GNS3 projects.
Since GNS3 API doesn't provide a way to list files, we maintain an index
file to track files written to each project.
"""
import json
import logging
from datetime import datetime
from typing import Any
from gns3_copilot.gns3_client import Project, get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
# Load environment variables
# Index file name (stored in project directory)
INDEX_FILE_NAME = ".gns3_copilot_file_index.json"
def _get_index_path(project_id: str) -> str:
"""
Get the path to the index file for a project.
Args:
project_id: The project ID
Returns:
The path to the index file (relative to project directory)
"""
return INDEX_FILE_NAME
def load_file_index(project: Project) -> dict[str, Any]:
"""
Load the file index for a project.
Args:
project: GNS3 Project instance
Returns:
A dictionary containing the index data, or an empty index if not found
"""
if project.project_id is None:
raise ValueError("Project ID must be set")
index_path = _get_index_path(project.project_id)
try:
content = project.get_file(path=index_path)
index_data = json.loads(content)
# Validate index structure
if not isinstance(index_data, dict):
logger.warning("Invalid index structure, creating new index")
return _create_empty_index(project.project_id)
if "files" not in index_data:
index_data["files"] = []
logger.info(
"Loaded file index for project %s with %d files",
project.project_id,
len(index_data["files"]),
)
return index_data
except Exception as e:
logger.info(
"No existing file index found for project %s, creating new one: %s",
project.project_id,
e,
)
return _create_empty_index(project.project_id)
def save_file_index(project: Project, index_data: dict[str, Any]) -> None:
"""
Save the file index for a project.
Args:
project: GNS3 Project instance
index_data: The index data to save
Raises:
ValueError: If failed to save the index
"""
if project.project_id is None:
raise ValueError("Project ID must be set")
index_path = _get_index_path(project.project_id)
try:
# Ensure project_id is in the index
index_data["project_id"] = project.project_id
# Save as JSON with indentation for readability
content = json.dumps(index_data, indent=2, ensure_ascii=False)
project.write_file(path=index_path, data=content)
logger.info("Saved file index for project %s", project.project_id)
except Exception as e:
logger.error(
"Failed to save file index for project %s: %s", project.project_id, e
)
raise ValueError(f"Failed to save file index: {str(e)}") from e
def _create_empty_index(project_id: str) -> dict[str, Any]:
"""
Create an empty file index structure.
Args:
project_id: The project ID
Returns:
A dictionary with empty index structure
"""
return {
"project_id": project_id,
"files": [],
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
}
def add_file_to_index(
project: Project,
path: str,
size: int | None = None,
) -> dict[str, Any]:
"""
Add or update a file in the project index.
Args:
project: GNS3 Project instance
path: The file path
size: Optional file size in bytes
Returns:
The updated index data
Raises:
ValueError: If failed to update the index
"""
try:
# Load existing index
index_data = load_file_index(project)
# Check if file already exists
file_entry = next((f for f in index_data["files"] if f["path"] == path), None)
now = datetime.now().isoformat()
if file_entry:
# Update existing file
file_entry["updated_at"] = now
if size is not None:
file_entry["size"] = size
logger.info("Updated file in index: %s", path)
else:
# Add new file
new_entry: dict[str, Any] = {
"path": path,
"created_at": now,
"updated_at": None,
}
if size is not None:
new_entry["size"] = size
index_data["files"].append(new_entry)
logger.info("Added file to index: %s", path)
# Update index timestamp
index_data["updated_at"] = now
# Save index
save_file_index(project, index_data)
return index_data
except Exception as e:
logger.error("Failed to add file to index: %s", e)
raise ValueError(f"Failed to add file to index: {str(e)}") from e
def get_file_list(project: Project) -> list[dict[str, Any]]:
"""
Get the list of files from the project index.
Args:
project: GNS3 Project instance
Returns:
A list of file entries
"""
try:
index_data = load_file_index(project)
files = index_data.get("files", [])
return files if isinstance(files, list) else []
except Exception as e:
logger.error("Failed to get file list: %s", e)
return []
if __name__ == "__main__":
# Test the file index manager
print("=" * 80)
print("Testing GNS3 File Index Manager")
print("=" * 80)
try:
# Create connector and project
connector = get_gns3_connector()
if connector is None:
print("ERROR: Failed to create GNS3 connector")
exit(1)
# Replace with actual project ID for testing
test_project_id = "your-project-uuid"
print(f"\nTesting with project: {test_project_id}")
print("NOTE: Replace 'your-project-uuid' with actual project ID")
# Create project instance
project = Project(project_id=test_project_id, connector=connector)
# Load index
print("\n1. Loading file index...")
index_data = load_file_index(project)
print(f" Index loaded: {len(index_data.get('files', []))} files")
# Add file to index
print("\n2. Adding file to index...")
updated_index = add_file_to_index(project, "test_file.txt", size=1024)
print(f" File added. Total files: {len(updated_index['files'])}")
# Get file list
print("\n3. Getting file list...")
file_list = get_file_list(project)
print(" Files in index:")
for file_entry in file_list:
print(f" - {file_entry['path']}")
print("\n" + "=" * 80)
print("Test completed successfully!")
print("=" * 80)
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()

View File

@ -1,236 +0,0 @@
"""
GNS3 drawings retrieval tool for managing graphical elements.
Provides functionality to retrieve all drawings from a GNS3 project,
including their coordinates, SVG content, and other properties.
"""
import json
import logging
from pprint import pprint
from typing import Any
from langchain.tools import BaseTool
from langchain_core.callbacks import CallbackManagerForToolRun
from gns3_copilot.gns3_client import Project, get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
class GNS3GetDrawingsTool(BaseTool):
"""
A LangChain tool to retrieve all drawings from a GNS3 project.
**Input:**
A JSON object containing the project_id.
Example input:
{
"project_id": "uuid-of-project"
}
**Output:**
A dictionary containing all drawings in the project.
Example output:
{
"project_id": "uuid-of-project",
"drawings": [
{
"drawing_id": "uuid-of-drawing1",
"svg": "<svg>...</svg>",
"x": 100,
"y": 200,
"z": 0,
"locked": false,
"rotation": 0
},
{
"drawing_id": "uuid-of-drawing2",
"svg": "<svg>...</svg>",
"x": 300,
"y": 400,
"z": 1,
"locked": true,
"rotation": 90
}
],
"total_drawings": 2
}
If an error occurs during input validation, returns a dictionary with an error message.
"""
name: str = "get_gns3_drawings"
description: str = """
Retrieves all drawings from a GNS3 project.
Input is a JSON object with project_id.
Example input:
{
"project_id": "uuid-of-project"
}
Returns a dictionary with all drawings in the project, including their drawing_id,
svg content, coordinates (x, y), z-index, locked status, and rotation.
If the operation fails, returns a dictionary with an error message.
"""
def _run(
self,
tool_input: str,
run_manager: CallbackManagerForToolRun | None = None,
**kwargs: Any,
) -> dict[str, Any]:
"""
Retrieves all drawings from a GNS3 project.
Args:
tool_input (str): A JSON string containing project_id.
run_manager: LangChain run manager (unused).
Returns:
dict: A dictionary with all drawings in the project or an error message.
"""
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Parse input JSON
input_data = json.loads(tool_input)
project_id = input_data.get("project_id")
# Validate input
if not project_id:
logger.error("Invalid input: Missing project_id.")
return {"error": "Missing project_id."}
# Initialize Gns3Connector using factory function
logger.info("Connecting to GNS3 server...")
gns3_server = get_gns3_connector()
if gns3_server is None:
logger.error("Failed to create GNS3 connector")
return {
"error": "Failed to connect to GNS3 server. Please check your configuration."
}
# Create project instance
logger.info("Retrieving drawings from project %s...", project_id)
project = Project(project_id=project_id, connector=gns3_server)
# Get project drawings
project.get_drawings()
# Prepare drawing list
drawings_list = []
if project.drawings:
for drawing in project.drawings:
drawing_info = {
"drawing_id": drawing.get("drawing_id"),
"svg": drawing.get("svg"),
"x": drawing.get("x"),
"y": drawing.get("y"),
"z": drawing.get("z"),
"locked": drawing.get("locked"),
"rotation": drawing.get("rotation", 0),
}
drawings_list.append(drawing_info)
# Prepare final result
final_result = {
"project_id": project_id,
"drawings": drawings_list,
"total_drawings": len(drawings_list),
}
# Log the final result
logger.info(
"Retrieved %d drawings from project %s.",
len(drawings_list),
project_id,
)
# Return JSON-formatted result
return final_result
except json.JSONDecodeError as e:
logger.error("Invalid JSON input: %s", e)
return {"error": f"Invalid JSON input: {e}"}
except Exception as e:
logger.error("Failed to retrieve drawings: %s", e)
return {"error": f"Failed to retrieve drawings: {str(e)}"}
if __name__ == "__main__":
# Test the tool locally
test_input = json.dumps(
{
"project_id": "d7fc094c-685e-4db1-ac11-5e33a1b2e066"
} # Replace with actual project UUID
)
tool = GNS3GetDrawingsTool()
result = tool._run(test_input)
pprint(result)
"""
example output:
{'drawings': [{'drawing_id': 'e8202c5e-bd0a-447c-848c-15db2c9af2f0',
'locked': False,
'rotation': 0,
'svg': '<svg height="131" width="391"><rect fill="#ffffff" '
'fill-opacity="1" height="131" width="391" '
'stroke="#000000" stroke-width="2" '
'stroke-dasharray="undefined" rx="0" ry="0" /></svg>',
'x': -376,
'y': -381,
'z': 1},
{'drawing_id': '7d2f2411-efe8-4f3a-9882-cc0ad7798ee0',
'locked': False,
'rotation': 0,
'svg': '<svg height="100" width="100"><text fill="#000000" '
'fill-opacity="1.0" font-family="Noto Sans" '
'font-size="11" font-weight="bold"></text></svg>',
'x': -394,
'y': 4,
'z': 1},
{'drawing_id': '264be3ab-002c-4b1d-886d-b4c323de845a',
'locked': False,
'rotation': 0,
'svg': '<svg height="100" width="100"><text fill="#000000" '
'fill-opacity="1.0" font-family="Noto Sans" '
'font-size="11" font-weight="bold">哈哈哈\n'
'哈和</text></svg>',
'x': -596,
'y': -43,
'z': 1},
{'drawing_id': '44210a41-0100-46a0-8962-a73c72171c47',
'locked': False,
'rotation': 0,
'svg': '<svg height="100" width="100"><text fill="#000000" '
'fill-opacity="1.0" font-family="Noto Sans" '
'font-size="11" font-weight="bold">Area 0</text></svg>',
'x': -573,
'y': -272,
'z': 1},
{'drawing_id': 'efd1add9-a798-489d-af26-58b2d73c89dc',
'locked': False,
'rotation': 0,
'svg': '<svg height="0" width="100"><line stroke="#000000" '
'stroke-width="2" x1="0" x2="200" y1="0" y2="0" '
'stroke-dasharray="none" /></svg>',
'x': -636,
'y': -152,
'z': 1},
{'drawing_id': '89913ef3-1041-4b11-94e1-c5d65c3a52d4',
'locked': False,
'rotation': 0,
'svg': '<svg height="119" width="488"><ellipse fill="#ffffff" '
'fill-opacity="1" cx="244" cy="59.5" rx="244" ry="59.5" '
'stroke="#000000" stroke-width="2" '
'stroke-dasharray="undefined" /></svg>',
'x': -891,
'y': 66,
'z': 1}],
'project_id': '2245149a-71c8-4387-9d1f-441a683ef7e7',
'total_drawings': 8}
"""

View File

@ -1,182 +0,0 @@
"""
GNS3 node retrieval tool for device information.
Provides functionality to retrieve all node instances from a project,
including node dimensions, symbol, and other attributes.
"""
import json
import logging
from pprint import pprint
from typing import Any
from langchain.tools import BaseTool
from langchain_core.callbacks import CallbackManagerForToolRun
from gns3_copilot.gns3_client import get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
class GNS3GetNodesTool(BaseTool):
"""
A LangChain tool to retrieve all node instances from a GNS3 project.
The tool connects to the GNS3 server and extracts comprehensive node information
including dimensions, symbol, coordinates, and other attributes.
**Input:**
A JSON string with project_id parameter.
Example: '{"project_id": "uuid-of-project"}'
**Output:**
A dictionary containing a list of dictionaries, each with comprehensive node information.
Example output:
{
"nodes": [
{
"name": "R-1",
"node_id": "uuid1",
"node_type": "iou",
"height": 50,
"width": 50,
"symbol": ":/symbols/router.svg",
"x": -180,
"y": -30,
"z": 1,
"status": "started",
"ports": [
{"name": "Ethernet0/0", "short_name": "e0/0", "adapter_number": 0, "port_number": 0},
{"name": "Ethernet0/1", "short_name": "e0/1", "adapter_number": 0, "port_number": 1}
]
}
]
}
If an error occurs, returns a dictionary with an error message.
"""
name: str = "get_gns3_nodes"
description: str = """
Retrieves all node instances from a GNS3 project.
Requires a JSON string input with 'project_id' parameter.
Returns comprehensive node information including name, node_id, node_type,
height, width, symbol, coordinates (x, y, z), status, and simplified port information.
Example input: '{"project_id": "uuid-of-project"}'
Example output:
{
"nodes": [
{
"name": "R-1",
"node_id": "uuid1",
"node_type": "iou",
"height": 50,
"width": 50,
"symbol": ":/symbols/router.svg",
"x": -180,
"y": -30,
"z": 1,
"status": "started",
"ports": [
{"name": "Ethernet0/0", "short_name": "e0/0", "adapter_number": 0, "port_number": 0}
]
}
]
}
"""
def _run(
self,
tool_input: str,
run_manager: CallbackManagerForToolRun | None = None,
) -> dict[str, Any]:
"""
Connects to the GNS3 server and retrieves all nodes from the specified project.
Args:
tool_input (str): JSON string containing project_id parameter.
run_manager: LangChain run manager (unused).
Returns:
dict: A dictionary containing the list of nodes or an error message.
"""
try:
# Parse input JSON
input_data = json.loads(tool_input)
project_id = input_data.get("project_id")
if not project_id:
logger.error("Missing project_id in input")
return {"error": "Missing required parameter: project_id"}
# Initialize Gns3Connector using factory function
logger.info("Connecting to GNS3 server...")
gns3_server = get_gns3_connector()
if gns3_server is None:
logger.error("Failed to create GNS3 connector")
return {
"error": "Failed to connect to GNS3 server. Please check your configuration."
}
# Retrieve all nodes from the project
logger.info(f"Retrieving nodes from project: {project_id}")
nodes = gns3_server.get_nodes(project_id)
# Simplify and organize node information
node_info = []
for node in nodes:
# Simplify ports - keep essential information only
simplified_ports = []
if node.get("ports"):
for port in node["ports"]:
simplified_ports.append(
{
"name": port.get("name", "N/A"),
"short_name": port.get("short_name", "N/A"),
"adapter_number": port.get("adapter_number", 0),
"port_number": port.get("port_number", 0),
}
)
# Extract node information
node_data = {
"name": node.get("name", "N/A"),
"node_id": node.get("node_id", "N/A"),
"node_type": node.get("node_type", "N/A"),
"height": node.get("height"), # Node height (read-only)
"width": node.get("width"), # Node width (read-only)
"symbol": node.get("symbol", "N/A"),
"x": node.get("x"),
"y": node.get("y"),
"z": node.get("z"),
"status": node.get("status", "N/A"),
"console": node.get("console"),
"console_type": node.get("console_type", "N/A"),
"ports": simplified_ports,
}
node_info.append(node_data)
# Return JSON-formatted result
result = {"nodes": node_info}
logger.info(
"Node retrieval completed. Total nodes: %d.",
len(node_info),
)
return result
except json.JSONDecodeError as e:
logger.error("Failed to parse input JSON: %s", e)
return {"error": f"Invalid JSON input: {str(e)}"}
except Exception as e:
logger.error("Failed to connect to GNS3 server or retrieve nodes: %s", e)
return {"error": f"Failed to retrieve nodes: {str(e)}"}
if __name__ == "__main__":
# Test the tool locally
tool = GNS3GetNodesTool()
# Example: Replace with your actual project_id
test_project_id = "d7fc094c-685e-4db1-ac11-5e33a1b2e066"
result = tool._run(f'{{"project_id": "{test_project_id}"}}')
pprint(result)

View File

@ -1,167 +0,0 @@
import logging
from typing import Any
from langchain.tools import BaseTool
from gns3_copilot.gns3_client import Project, get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
class GNS3ProjectCreate(BaseTool):
"""
Tool to create a new GNS3 project.
This tool connects to GNS3 server and creates a new project with the specified
name and optional configuration parameters.
"""
name: str = "create_gns3_project"
description: str = """
Creates a new GNS3 project with the specified name and optional parameters.
Input: A dictionary with project parameters.
- name: The name of the project to create (required)
- auto_start: Automatically start the project when opened (optional, default: False)
- auto_close: Automatically close the project when client disconnects (optional, default: False)
- auto_open: Automatically open the project when GNS3 starts (optional, default: False)
- scene_width: Width of the drawing area in pixels (optional)
- scene_height: Height of the drawing area in pixels (optional)
Example input:
{
"name": "my_new_project",
"auto_start": false,
"auto_close": false,
"auto_open": false
}
Returns: A dictionary with project creation status and details:
- success: Whether the operation succeeded
- project: Project details (name, project_id, status, etc.)
- message: Status message
Example output:
{
"success": true,
"project": {
"project_id": "ff8e059c-c33d-47f4-bc11-c7dda8a1d500",
"name": "my_new_project",
"status": "opened"
},
"message": "Project 'my_new_project' created successfully"
}
"""
def _run(self, tool_input: Any = None, run_manager: Any = None) -> dict:
"""
Execute the project creation operation.
Args:
tool_input: Dictionary containing project parameters
run_manager: Run manager for tool execution (optional)
Returns:
Dictionary with operation result and project details
"""
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Validate input
if not tool_input or "name" not in tool_input:
return {
"success": False,
"error": "Missing required parameter: name",
}
name = tool_input["name"]
# Validate project name is not empty
if not name or not isinstance(name, str) or not name.strip():
return {
"success": False,
"error": "Project name must be a non-empty string",
}
# Get optional parameters
auto_start = tool_input.get("auto_start", False)
auto_close = tool_input.get("auto_close", False)
auto_open = tool_input.get("auto_open", False)
scene_width = tool_input.get("scene_width")
scene_height = tool_input.get("scene_height")
# Initialize Gns3Connector using factory function
logger.info("Connecting to GNS3 server...")
server = get_gns3_connector()
if server is None:
logger.error("Failed to create GNS3 connector")
return {
"success": False,
"error": "Failed to connect to GNS3 server. Please check your configuration.",
}
# Create project instance with specified parameters
project_params = {
"name": name,
"auto_start": auto_start,
"auto_close": auto_close,
"auto_open": auto_open,
}
# Add optional scene parameters if provided
if scene_width is not None:
project_params["scene_width"] = scene_width
if scene_height is not None:
project_params["scene_height"] = scene_height
project = Project(connector=server, **project_params)
# Create the project
project.create()
# Verify project was created successfully
if not project.project_id:
return {
"success": False,
"error": "Failed to create project: project_id not returned",
}
logger.info(
"Project created successfully: %s (ID: %s)",
project.name,
project.project_id,
)
# Prepare result
result = {
"success": True,
"project": {
"project_id": project.project_id,
"name": project.name,
"status": project.status,
"path": project.path,
},
"message": f"Project '{project.name}' created successfully",
}
# Log result
logger.info("Project creation result: %s", result)
# Return success with project details
return result
except ValueError as e:
logger.error("Validation error creating GNS3 project: %s", str(e))
return {
"success": False,
"error": f"Validation error: {str(e)}",
}
except Exception as e:
logger.error("Error creating GNS3 project: %s", str(e))
return {
"success": False,
"error": f"Failed to create GNS3 project: {str(e)}",
}

View File

@ -1,152 +0,0 @@
import logging
from typing import Any
from langchain.tools import BaseTool
from gns3_copilot.gns3_client import Project, get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
class GNS3ProjectDelete(BaseTool):
"""
Tool to delete a GNS3 project.
This tool connects to GNS3 server and deletes an existing project,
optionally retrieving project details before deletion.
"""
name: str = "delete_gns3_project"
description: str = """
Deletes an existing GNS3 project.
Input parameters:
Required:
- project_id: The UUID of the project to delete OR
- name: The name of the project to delete (one must be provided)
Returns: Project deletion status and detailed information including:
- success: Whether the operation succeeded
- project: Deleted project details (name, project_id, status, etc.)
- message: Status message
Example output:
{
"success": true,
"project": {
"project_id": "ff8e059c-c33d-47f4-bc11-c7dda8a1d500",
"name": "my_project",
"status": "closed"
},
"message": "Project 'my_project' deleted successfully"
}
"""
def _run(self, tool_input: Any = None, run_manager: Any = None) -> dict:
"""
Execute the project deletion operation.
Args:
tool_input: Dictionary containing project identifier
run_manager: Run manager for tool execution (optional)
Returns:
Dictionary with operation result and deleted project details
"""
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Validate input
if not tool_input:
return {
"success": False,
"error": "No input provided",
}
# Check for project identifier
project_id = tool_input.get("project_id")
project_name = tool_input.get("name")
if not project_id and not project_name:
return {
"success": False,
"error": "Missing required parameter: either 'project_id' or 'name' must be provided",
}
# Initialize Gns3Connector using factory function
logger.info("Connecting to GNS3 server...")
server = get_gns3_connector()
if server is None:
logger.error("Failed to create GNS3 connector")
return {
"success": False,
"error": "Failed to connect to GNS3 server. Please check your configuration.",
}
# Create project instance
if project_id:
project = Project(project_id=project_id, connector=server)
else:
project = Project(name=project_name, connector=server)
# Get project information before deletion
project.get(get_nodes=False, get_links=False, get_stats=False)
# Verify project was found
if not project.project_id:
if project_name:
return {
"success": False,
"error": f"Project with name '{project_name}' not found",
}
else:
return {
"success": False,
"error": f"Project with ID '{project_id}' not found",
}
# Store project details for response
project_details = {
"project_id": project.project_id,
"name": project.name,
"status": project.status,
"path": project.path,
}
# Delete the project
project.delete()
logger.info(
"Project deleted successfully: %s (ID: %s)",
project.name,
project.project_id,
)
# Prepare result
result = {
"success": True,
"project": project_details,
"message": f"Project '{project.name}' deleted successfully",
}
# Log result
logger.info("Project deletion result: %s", result)
# Return success with project details
return result
except ValueError as e:
logger.error("Validation error deleting GNS3 project: %s", str(e))
return {
"success": False,
"error": f"Validation error: {str(e)}",
}
except Exception as e:
logger.error("Error deleting GNS3 project: %s", str(e))
return {
"success": False,
"error": f"Failed to delete GNS3 project: {str(e)}",
}

View File

@ -1,239 +0,0 @@
"""
GNS3 Project List Files Tool
This module provides a LangChain tool to list files in a GNS3 project.
It uses a file index stored in the project directory to track files.
"""
import json
import logging
import re
import requests
from langchain.tools import BaseTool
from gns3_copilot.gns3_client import Project, get_gns3_connector
from gns3_copilot.gns3_client.gns3_file_index import load_file_index
# Configure logging
logger = logging.getLogger(__name__)
# Load environment variables
class GNS3ProjectListFilesTool(BaseTool):
"""
Tool to list files in a GNS3 project.
This tool reads a file index maintained in the project directory
and returns a list of all tracked files.
Input format (JSON string):
{
"project_id": "uuid-of-project",
"pattern": "*.cfg" # Optional: filter by file pattern
}
Returns:
JSON string with file list:
{
"project_id": "uuid-of-project",
"files": [
{
"path": "README.md",
"created_at": "2026-01-01T10:00:00",
"updated_at": "2026-01-01T10:05:00",
"size": 1024
}
],
"total_count": 1,
"status": "success"
}
"""
name: str = "gns3_project_list_files"
description: str = """List files in a GNS3 project using a file index.
This tool reads a file index (.gns3_copilot_file_index.json) that tracks
all files written to the project. It returns a list of file paths and metadata.
Args:
project_id (required): The UUID of the GNS3 project
pattern (optional): A glob pattern to filter files (e.g., "*.cfg", "configs/*")
Returns:
A list of files with their metadata
"""
def _run(self, tool_input: str) -> str:
"""
Execute the tool to list files in a project.
Args:
tool_input: JSON string containing project_id and optional pattern
Returns:
JSON string containing the file list
"""
try:
# Parse input JSON
input_data = json.loads(tool_input)
project_id = input_data.get("project_id")
pattern = input_data.get("pattern")
# Validate input
if not project_id:
logger.error("Invalid input: Missing project_id.")
return json.dumps({"error": "Missing project_id."})
# Validate UUID format (basic check)
uuid_pattern = r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
if not re.match(uuid_pattern, project_id):
logger.error("Invalid project_id format: %s", project_id)
return json.dumps({"error": f"Invalid project_id format: {project_id}"})
# Create connector
connector = get_gns3_connector()
if connector is None:
logger.error("Failed to create GNS3 connector")
return json.dumps(
{
"project_id": project_id,
"status": "failed",
"error": "Failed to create GNS3 connector. Check server configuration.",
}
)
# Create project instance
project = Project(project_id=project_id, connector=connector)
# Load file index
logger.info("Loading file index for project: %s", project_id)
index_data = load_file_index(project)
# Get file list
files = index_data.get("files", [])
# Apply pattern filter if provided
if pattern:
filtered_files = []
for file_entry in files:
if self._matches_pattern(file_entry["path"], pattern):
filtered_files.append(file_entry)
files = filtered_files
logger.info(
"Filtered %d files matching pattern: %s", len(files), pattern
)
# Prepare result
result = {
"project_id": project_id,
"files": files,
"total_count": len(files),
"status": "success",
}
logger.info(
"Successfully listed %d files for project: %s", len(files), project_id
)
return json.dumps(result, indent=2)
except requests.HTTPError as e:
logger.error("HTTP error listing files: %s", e)
return json.dumps(
{
"project_id": project_id if project_id else "unknown",
"status": "failed",
"error": f"HTTP error: {str(e)}",
}
)
except json.JSONDecodeError as e:
logger.error("Invalid JSON input: %s", e)
return json.dumps({"error": f"Invalid JSON input: {str(e)}"})
except Exception as e:
logger.error("Unexpected error listing files: %s", e)
return json.dumps(
{
"project_id": project_id if project_id else "unknown",
"status": "failed",
"error": f"Failed to list files: {str(e)}",
}
)
def _matches_pattern(self, path: str, pattern: str) -> bool:
"""
Check if a file path matches a glob pattern.
Args:
path: The file path to check
pattern: The glob pattern (e.g., "*.cfg", "configs/*")
Returns:
True if the path matches the pattern
"""
# Convert glob pattern to regex
regex_pattern = pattern.replace(".", r"\.").replace("*", ".*").replace("?", ".")
return bool(re.match(regex_pattern, path))
if __name__ == "__main__":
# Test the tool
print("=" * 80)
print("Testing GNS3ProjectListFilesTool")
print("=" * 80)
try:
tool = GNS3ProjectListFilesTool()
# Replace with actual project ID for testing
test_project_id = (
"1445a4ba-4635-430b-a332-bef438f65932" # Replace with actual project UUID
)
print(f"\nTesting with project: {test_project_id}")
print("NOTE: Replace 'your-project-uuid' with actual project ID")
# Test 1: List all files
print("\n1. Listing all files...")
result1 = tool._run(
json.dumps(
{
"project_id": test_project_id,
}
)
)
result_data1 = json.loads(result1)
print(f" Status: {result_data1.get('status')}")
print(f" Total files: {result_data1.get('total_count', 0)}")
if result_data1.get("files"):
print(" Files:")
for file_entry in result_data1["files"]:
print(f" - {file_entry['path']}")
# Test 2: List files with pattern
print("\n2. Listing files matching pattern '*.cfg'...")
result2 = tool._run(
json.dumps({"project_id": test_project_id, "pattern": "*.cfg"})
)
result_data2 = json.loads(result2)
print(f" Status: {result_data2.get('status')}")
print(f" Matching files: {result_data2.get('total_count', 0)}")
if result_data2.get("files"):
print(" Files:")
for file_entry in result_data2["files"]:
print(f" - {file_entry['path']}")
# Test 3: Missing project_id
print("\n3. Testing with missing project_id...")
result3 = tool._run(json.dumps({}))
print(f" Result: {result3}")
print("\n" + "=" * 80)
print("Test completed successfully!")
print("=" * 80)
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()

View File

@ -1,305 +0,0 @@
"""
GNS3 project lock/unlock tool for project management.
Provides functionality to lock, unlock, or check the lock status of GNS3 projects.
Locking a project prevents accidental modifications to drawings and nodes.
"""
import logging
from typing import Any
from langchain.tools import BaseTool
from gns3_copilot.gns3_client import Project, get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
class GNS3ProjectLock(BaseTool):
"""
Tool to lock, unlock, or check the lock status of a GNS3 project.
This tool connects to GNS3 server and performs lock/unlock operations
on the specified project, or retrieves the current lock status.
IMPORTANT: Project lock/unlock operations are only supported in GNS3 API v3.
Using this tool with GNS3 API v2 will result in an error.
Supported operations:
- "locked": Check if the project is currently locked
- "lock": Lock all drawings and nodes in the project
- "unlock": Unlock all drawings and nodes in the project
"""
name: str = "gns3_project_lock"
description: str = """
Lock, unlock, or check the lock status of a GNS3 project.
IMPORTANT: This tool only works with GNS3 API v3. It is not supported in API v2.
Input parameters:
- project_id: The unique UUID identifier of the project (required)
- operation: The operation to perform - "locked", "lock", or "unlock" (required)
Operations:
- "locked": Returns the current lock status of the project
- "lock": Locks all drawings and nodes in the project
- "unlock": Unlocks all drawings and nodes in the project
Returns: Dictionary with operation result including:
- success: Whether the operation succeeded
- operation: The operation performed
- project_id: The project ID
- locked_status: Current lock status (for "locked" operation)
- message: Status message
- error: Error message if operation failed
Example output (check locked status):
{
"success": true,
"operation": "locked",
"project_id": "ff8e059c-c33d-47f4-bc11-c7dda8a1d500",
"locked_status": false,
"message": "Project is currently unlocked"
}
Example output (lock project):
{
"success": true,
"operation": "lock",
"project_id": "ff8e059c-c33d-47f4-bc11-c7dda8a1d500",
"message": "Project locked successfully"
}
Example output (unlock project):
{
"success": true,
"operation": "unlock",
"project_id": "ff8e059c-c33d-47f4-bc11-c7dda8a1d500",
"message": "Project unlocked successfully"
}
Example output (API v2 not supported):
{
"success": false,
"operation": "lock",
"project_id": "ff8e059c-c33d-47f4-bc11-c7dda8a1d500",
"error": "Project lock/unlock operations are only supported in GNS3 API v3. Current API version: v2"
}
"""
def _run(self, tool_input: Any = None, run_manager: Any = None) -> dict:
"""
Execute the project lock/unlock operation.
Args:
tool_input: Dictionary containing project_id and operation
run_manager: Run manager for tool execution (optional)
Returns:
Dictionary with operation result
"""
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Validate input
if not tool_input or "project_id" not in tool_input:
return {
"success": False,
"error": "Missing required parameter: project_id",
}
if "operation" not in tool_input:
return {
"success": False,
"error": "Missing required parameter: operation",
}
project_id = tool_input["project_id"]
operation = tool_input["operation"]
# Validate operation type
valid_operations = ["locked", "lock", "unlock"]
if operation not in valid_operations:
return {
"success": False,
"error": f"Invalid operation '{operation}'. "
f"Must be one of: {', '.join(valid_operations)}",
}
# Initialize Gns3Connector using factory function
logger.info("Connecting to GNS3 server...")
server = get_gns3_connector()
if server is None:
logger.error("Failed to create GNS3 connector")
return {
"success": False,
"error": "Failed to connect to GNS3 server. "
"Please check your configuration.",
}
# Create project instance and retrieve project details
project = Project(project_id=project_id, connector=server)
project.get(get_nodes=False, get_links=False, get_stats=False)
# Check if project was found
if not project.name:
return {
"success": False,
"error": f"Project with ID '{project_id}' not found",
}
# Perform the requested operation
result: dict[str, Any] = {
"success": False,
"operation": operation,
"project_id": project_id,
}
if operation == "locked":
# Get current lock status
logger.info("Getting lock status for project %s", project_id)
try:
locked_status = project.get_locked()
result.update(
{
"success": True,
"locked_status": locked_status,
"message": f"Project is currently {'locked' if locked_status else 'unlocked'}",
}
)
logger.info("Project lock status: %s", locked_status)
except ValueError as e:
# Handle API version incompatibility
error_msg = str(e)
if "only supported in GNS3 API v3" in error_msg:
logger.error("API v2 not supported: %s", error_msg)
result.update(
{
"error": error_msg,
"message": "Project lock/unlock operations require GNS3 API v3. "
"Please upgrade your GNS3 server or use v3 API.",
}
)
else:
logger.error("Error getting lock status: %s", error_msg)
result.update(
{
"error": error_msg,
"message": f"Failed to get lock status: {error_msg}",
}
)
elif operation == "lock":
# Lock the project
logger.info("Locking project %s", project_id)
try:
project.lock_project()
result.update(
{
"success": True,
"message": f"Project '{project.name}' locked successfully",
}
)
logger.info("Project locked successfully")
except ValueError as e:
# Handle API version incompatibility
error_msg = str(e)
if "only supported in GNS3 API v3" in error_msg:
logger.error("API v2 not supported: %s", error_msg)
result.update(
{
"error": error_msg,
"message": "Project lock/unlock operations require GNS3 API v3. "
"Please upgrade your GNS3 server or use v3 API.",
}
)
else:
logger.error("Error locking project: %s", error_msg)
result.update(
{
"error": error_msg,
"message": f"Failed to lock project: {error_msg}",
}
)
elif operation == "unlock":
# Unlock the project
logger.info("Unlocking project %s", project_id)
try:
project.unlock_project()
result.update(
{
"success": True,
"message": f"Project '{project.name}' unlocked successfully",
}
)
logger.info("Project unlocked successfully")
except ValueError as e:
# Handle API version incompatibility
error_msg = str(e)
if "only supported in GNS3 API v3" in error_msg:
logger.error("API v2 not supported: %s", error_msg)
result.update(
{
"error": error_msg,
"message": "Project lock/unlock operations require GNS3 API v3. "
"Please upgrade your GNS3 server or use v3 API.",
}
)
else:
logger.error("Error unlocking project: %s", error_msg)
result.update(
{
"error": error_msg,
"message": f"Failed to unlock project: {error_msg}",
}
)
# Log result
logger.info("Project lock operation result: %s", result)
return result
except Exception as e:
logger.error("Error performing lock operation on GNS3 project: %s", str(e))
return {
"success": False,
"error": f"Failed to perform lock operation on GNS3 project: {str(e)}",
}
if __name__ == "__main__":
# Test the tool locally
print("=== Testing GNS3ProjectLock Tool ===")
# Test 1: Check lock status
print("\n--- Test 1: Check lock status ---")
test_input_locked = {
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", # Replace with actual project UUID
"operation": "locked",
}
tool = GNS3ProjectLock()
result_locked = tool._run(test_input_locked)
print(f"Result: {result_locked}")
# Test 2: Lock project
print("\n--- Test 2: Lock project ---")
test_input_lock = {
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", # Replace with actual project UUID
"operation": "lock",
}
result_lock = tool._run(test_input_lock)
print(f"Result: {result_lock}")
# Test 3: Unlock project
print("\n--- Test 3: Unlock project ---")
test_input_unlock = {
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", # Replace with actual project UUID
"operation": "unlock",
}
result_unlock = tool._run(test_input_unlock)
print(f"Result: {result_unlock}")

View File

@ -1,158 +0,0 @@
import logging
from typing import Any
from langchain.tools import BaseTool
from gns3_copilot.gns3_client import Project, get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
class GNS3ProjectOpen(BaseTool):
"""
Tool to open or close a GNS3 project by project_id.
This tool connects to GNS3 server and opens or closes the specified project.
It returns the project status and details after the operation.
"""
name: str = "open_gns3_project"
description: str = """
Opens or closes a GNS3 project by its project_id.
Input parameters:
- project_id: The unique UUID identifier of project (required)
- open: Set to True to open the project (default: False)
- close: Set to True to close the project (default: False)
Note: Exactly one of 'open' or 'close' must be set to True.
Returns: Project operation status and detailed information including:
- success: Whether the operation succeeded
- operation: "open" or "close"
- project: Project details (name, project_id, status, etc.)
- message: Status message
Example output (open):
{
"success": true,
"operation": "open",
"project": {
"project_id": "ff8e059c-c33d-47f4-bc11-c7dda8a1d500",
"name": "mylab",
"status": "opened"
},
"message": "Project 'mylab' opened successfully"
}
Example output (close):
{
"success": true,
"operation": "close",
"project": {
"project_id": "ff8e059c-c33d-47f4-bc11-c7dda8a1d500",
"name": "mylab",
"status": "closed"
},
"message": "Project 'mylab' closed successfully"
}
"""
def _run(self, tool_input: Any = None, run_manager: Any = None) -> dict:
"""
Execute project open or close operation.
Args:
tool_input: Dictionary containing project_id, open, or close
run_manager: Run manager for tool execution (optional)
Returns:
Dictionary with operation result and project details
"""
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Validate input
if not tool_input or "project_id" not in tool_input:
return {
"success": False,
"error": "Missing required parameter: project_id",
}
project_id = tool_input["project_id"]
should_open = tool_input.get("open", False)
should_close = tool_input.get("close", False)
# Validate that exactly one of open or close is True
if should_open and should_close:
return {
"success": False,
"error": "Cannot set both 'open' and 'close' to True. "
"Please specify only one operation.",
}
if not should_open and not should_close:
return {
"success": False,
"error": "Either 'open' or 'close' must be set to True.",
}
# Initialize Gns3Connector using factory function
logger.info("Connecting to GNS3 server...")
server = get_gns3_connector()
if server is None:
logger.error("Failed to create GNS3 connector")
return {
"success": False,
"error": "Failed to connect to GNS3 server. Please check your configuration.",
}
# Create project instance and retrieve project details
project = Project(project_id=project_id, connector=server)
project.get(get_nodes=False, get_links=False, get_stats=False)
# Check if project was found
if not project.name:
return {
"success": False,
"error": f"Project with ID '{project_id}' not found",
}
# Perform the requested operation
operation = None
if should_open:
project.open()
operation = "open"
action_message = "opened"
else: # should_close
project.close()
operation = "close"
action_message = "closed"
# Prepare result
result = {
"success": True,
"operation": operation,
"project": {
"project_id": project.project_id,
"name": project.name,
"status": project.status,
},
"message": f"Project '{project.name}' {action_message} successfully",
}
# Log result
logger.info("Project operation result: %s", result)
# Return success with project details
return result
except Exception as e:
logger.error("Error operating on GNS3 project: %s", str(e))
return {
"success": False,
"error": f"Failed to operate on GNS3 project: {str(e)}",
}

View File

@ -1,160 +0,0 @@
"""
GNS3 Project Path Tool.
This module provides a LangChain tool for retrieving GNS3 project paths.
These paths are needed for features like Notes to store markdown files
in the project directory.
Usage:
from gns3_copilot.gns3_client.gns3_project_path import GNS3ProjectPath
tool = GNS3ProjectPath()
result = tool._run({"project_name": "mylab", "project_id": "uuid"})
"""
import logging
from typing import Any
from langchain.tools import BaseTool
from gns3_copilot.gns3_client import Project, get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
class GNS3ProjectPath(BaseTool):
"""
Tool to retrieve the local filesystem path of a GNS3 project.
This tool connects to GNS3 server and retrieves the local path where
project files are stored on the server. Useful for features that need to
access project-specific files such as Notes.
Input parameters:
- project_name: Name of the GNS3 project (required)
- project_id: The unique UUID identifier of project (required)
Returns: Project path information including:
- success: Whether the operation succeeded
- project_path: Full path to the project directory on the server
- project_name: Name of the project
- project_id: UUID of the project
- message: Status message
Example output (success):
{
"success": true,
"project_path": "/home/user/GNS3/projects/mylab",
"project_name": "mylab",
"project_id": "ff8e059c-c33d-47f4-bc11-c7dda8a1d500",
"message": "Successfully retrieved project path"
}
Example output (error):
{
"success": false,
"error": "Project with ID 'xxx' not found"
}
"""
name: str = "get_gns3_project_path"
description: str = """
Retrieves the local filesystem path for a GNS3 project.
Input parameters:
- project_name: Name of the GNS3 project (required)
- project_id: The unique UUID identifier of project (required)
This tool is useful for accessing project-specific files and directories
on the GNS3 server, such as storing notes or configuration files.
Returns: Project path information including success status, path,
project details, and status message.
"""
def _run(self, tool_input: Any = None, run_manager: Any = None) -> dict:
"""
Execute the project path retrieval operation.
Args:
tool_input: Dictionary containing project_name and project_id
run_manager: Run manager for tool execution (optional)
Returns:
Dictionary with operation result and project path details
"""
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Validate input
if not tool_input:
return {
"success": False,
"error": "Missing required parameters: project_name and project_id",
}
project_name = tool_input.get("project_name")
project_id = tool_input.get("project_id")
if not project_name or not project_id:
return {
"success": False,
"error": "Both project_name and project_id are required parameters",
}
# Initialize Gns3Connector using factory function
logger.info("Connecting to GNS3 server...")
server = get_gns3_connector()
if server is None:
logger.error("Failed to create GNS3 connector")
return {
"success": False,
"error": "Failed to connect to GNS3 server. Please check your configuration.",
}
# Create project instance and retrieve project details
project = Project(project_id=project_id, connector=server)
project.get(get_nodes=False, get_links=False, get_stats=False)
# Check if project was found
if not project.name:
return {
"success": False,
"error": f"Project with ID '{project_id}' not found",
}
# Verify project name matches
if project.name != project_name:
logger.warning(
"Project name mismatch: expected '%s', got '%s'",
project_name,
project.name,
)
# Continue anyway as project_id is more authoritative
# Return project path if available
if project.path:
result = {
"success": True,
"project_path": project.path,
"project_name": project.name,
"project_id": project.project_id,
"message": f"Successfully retrieved project path for '{project.name}'",
}
# Log result
logger.info("Project path retrieval result: %s", result)
return result
else:
return {
"success": False,
"error": f"Project '{project_name}' exists but has no path attribute",
}
except Exception as e:
logger.error("Error retrieving project path: %s", str(e))
return {
"success": False,
"error": f"Failed to retrieve project path: {str(e)}",
}

View File

@ -1,191 +0,0 @@
"""
GNS3 Project Read File Tool
Provides a LangChain tool for reading files in GNS3 projects.
This tool directly uses the Project.get_file() method from custom_gns3fy.
"""
import json
import logging
import re
from typing import Any
from langchain.tools import BaseTool
from langchain_core.callbacks import CallbackManagerForToolRun
from gns3_copilot.gns3_client import Project, get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
class GNS3ProjectReadFileTool(BaseTool):
"""
A LangChain tool to read files from a GNS3 project.
This tool reads the content of a file located in a GNS3 project directory.
It uses the Project.get_file() method from custom_gns3fy.
**Input:**
A JSON object containing the project_id and the file path.
Example input:
{
"project_id": "uuid-of-project",
"path": "README.md"
}
**Output:**
A dictionary containing the project_id, file path, content, and status.
Example output:
{
"project_id": "uuid-of-project",
"path": "README.md",
"content": "This is the file content...",
"status": "success"
}
If an error occurs, returns a dictionary with an error message.
Example error output:
{
"project_id": "uuid-of-project",
"path": "README.md",
"status": "failed",
"error": "File not found"
}
"""
name: str = "gns3_project_read_file"
description: str = """
Reads a file from a GNS3 project directory.
Input is a JSON object with project_id and the file path.
Example input:
{
"project_id": "uuid-of-project",
"path": "README.md"
}
Returns a dictionary with the file content and status.
If the operation fails, returns a dictionary with an error message.
"""
def _run(
self,
tool_input: str,
run_manager: CallbackManagerForToolRun | None = None,
**kwargs: Any,
) -> dict[str, Any]:
"""
Reads a file from a GNS3 project.
Args:
tool_input (str): A JSON string containing project_id and file path.
run_manager: LangChain run manager (unused).
Returns:
dict: A dictionary with the file content and status, or an error message.
"""
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Parse input JSON
input_data = json.loads(tool_input)
project_id = input_data.get("project_id")
path = input_data.get("path")
# Validate input
if not project_id:
logger.error("Invalid input: Missing project_id.")
return {"error": "Missing project_id."}
if not path:
logger.error("Invalid input: Missing path.")
return {"error": "Missing path."}
# Validate project_id format (UUID)
if not self._validate_project_id(project_id):
error_msg = (
f"Invalid project_id format: {project_id}. Expected UUID format."
)
logger.error(error_msg)
return {"error": error_msg}
# Get connector using factory function
connector = get_gns3_connector()
if connector is None:
error_msg = "Failed to create GNS3 connector. Check configuration."
logger.error(error_msg)
return {"error": error_msg}
logger.info("Connecting to GNS3 server...")
# Create Project instance
project = Project(project_id=project_id, connector=connector)
# Read file content
logger.info("Reading file '%s' from project '%s'...", path, project_id)
content = project.get_file(path=path)
# Prepare successful result
result = {
"project_id": project_id,
"path": path,
"content": content,
"status": "success",
}
logger.info(
"Successfully read file '%s' from project '%s'", path, project_id
)
return result
except json.JSONDecodeError as e:
logger.error("Invalid JSON input: %s", e)
return {"error": f"Invalid JSON input: {e}"}
except ValueError as e:
logger.error("Value error: %s", e)
return {"error": f"Value error: {e}"}
except Exception as e:
logger.error("Failed to read file from project: %s", e)
return {
"project_id": project_id if "project_id" in locals() else None,
"path": path if "path" in locals() else None,
"status": "failed",
"error": f"Failed to read file: {str(e)}",
}
def _validate_project_id(self, project_id: str) -> bool:
"""
Validate project_id format (UUID).
Args:
project_id: The project ID to validate
Returns:
True if valid UUID format, False otherwise
"""
uuid_pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
return bool(re.match(uuid_pattern, project_id, re.IGNORECASE))
if __name__ == "__main__":
import pprint
print("=" * 80)
print("Testing GNS3ProjectReadFileTool")
print("=" * 80)
read_test_input = json.dumps(
{
"project_id": "1445a4ba-4635-430b-a332-bef438f65932", # Replace with actual project UUID
"path": "test_write.txt",
}
)
read_tool = GNS3ProjectReadFileTool()
read_result = read_tool._run(read_test_input)
print("\nRead Result:")
pprint.pprint(read_result)

View File

@ -1,224 +0,0 @@
import logging
from typing import Any
from langchain.tools import BaseTool
from gns3_copilot.gns3_client import Project, get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
class GNS3ProjectUpdate(BaseTool):
"""
Tool to update an existing GNS3 project configuration.
This tool connects to GNS3 server and updates project settings including
auto-start options, scene dimensions, display options, and other parameters.
"""
name: str = "update_gns3_project"
description: str = """
Updates an existing GNS3 project configuration.
Input parameters:
Required:
- project_id: The UUID of the project to update OR
- name: The name of the project to update (one must be provided)
Optional - Auto control options:
- auto_start: Automatically start the project when opened (default: keep current)
- auto_close: Automatically close the project when client disconnects (default: keep current)
- auto_open: Automatically open the project when GNS3 starts (default: keep current)
Optional - Scene settings:
- scene_width: Width of the drawing area in pixels (default: keep current)
- scene_height: Height of the drawing area in pixels (default: keep current)
- grid_size: Grid size for the drawing area for nodes (default: keep current)
- drawing_grid_size: Grid size for the drawing area for drawings (default: keep current)
Optional - Display options:
- show_grid: Show the grid on the drawing area (default: keep current)
- show_interface_labels: Show interface labels on the drawing area (default: keep current)
- show_layers: Show layers on the drawing area (default: keep current)
- snap_to_grid: Snap to grid on the drawing area (default: keep current)
- zoom: Zoom of the drawing area (default: keep current)
Returns: Project update status and detailed information including:
- success: Whether the operation succeeded
- project: Updated project details (name, project_id, status, settings, etc.)
- updated_fields: List of fields that were updated
- message: Status message
Example output:
{
"success": true,
"project": {
"project_id": "ff8e059c-c33d-47f4-bc11-c7dda8a1d500",
"name": "my_project",
"status": "opened",
"auto_start": true,
"auto_close": false,
"auto_open": false
},
"updated_fields": ["auto_start"],
"message": "Project 'my_project' updated successfully"
}
"""
def _run(self, tool_input: Any = None, run_manager: Any = None) -> dict:
"""
Execute project update operation.
Args:
tool_input: Dictionary containing project identifier and update parameters
run_manager: Run manager for tool execution (optional)
Returns:
Dictionary with operation result and updated project details
"""
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Validate input
if not tool_input:
return {
"success": False,
"error": "No input provided",
}
# Check for project identifier
project_id = tool_input.get("project_id")
project_name = tool_input.get("name")
if not project_id and not project_name:
return {
"success": False,
"error": "Missing required parameter: either 'project_id' or 'name' must be provided",
}
# Remove project_id and name from update parameters
update_params = {
k: v
for k, v in tool_input.items()
if k not in ("project_id", "name") and v is not None
}
# Check if there's anything to update
if not update_params:
return {
"success": False,
"error": "No update parameters provided",
}
# Initialize Gns3Connector using factory function
logger.info("Connecting to GNS3 server...")
server = get_gns3_connector()
if server is None:
logger.error("Failed to create GNS3 connector")
return {
"success": False,
"error": "Failed to connect to GNS3 server. Please check your configuration.",
}
# Create project instance
if project_id:
project = Project(project_id=project_id, connector=server)
else:
project = Project(name=project_name, connector=server)
# Get current project information
project.get(get_nodes=False, get_links=False, get_stats=False)
# Verify project was found
if not project.project_id:
if project_name:
return {
"success": False,
"error": f"Project with name '{project_name}' not found",
}
else:
return {
"success": False,
"error": f"Project with ID '{project_id}' not found",
}
# Store current values for comparison
old_values = {}
for field in update_params:
old_values[field] = getattr(project, field, None)
# Update the project
project.update(**update_params)
# Collect updated fields
updated_fields = []
for field in update_params:
new_value = getattr(project, field, None)
if old_values[field] != new_value:
updated_fields.append(field)
logger.info(
"Project updated successfully: %s (ID: %s), updated fields: %s",
project.name,
project.project_id,
", ".join(updated_fields),
)
# Prepare project details for response
project_details = {
"project_id": project.project_id,
"name": project.name,
"status": project.status,
"path": project.path,
}
# Add optional fields if they were updated or exist
optional_fields = [
"auto_start",
"auto_close",
"auto_open",
"scene_width",
"scene_height",
"grid_size",
"drawing_grid_size",
"show_grid",
"show_interface_labels",
"show_layers",
"snap_to_grid",
"zoom",
]
for field in optional_fields:
value = getattr(project, field, None)
if value is not None:
project_details[field] = value
# Prepare result
result = {
"success": True,
"project": project_details,
"updated_fields": updated_fields,
"message": f"Project '{project.name}' updated successfully",
}
# Log result
logger.info("Project update result: %s", result)
# Return success with project details
return result
except ValueError as e:
logger.error("Validation error updating GNS3 project: %s", str(e))
return {
"success": False,
"error": f"Validation error: {str(e)}",
}
except Exception as e:
logger.error("Error updating GNS3 project: %s", str(e))
return {
"success": False,
"error": f"Failed to update GNS3 project: {str(e)}",
}

View File

@ -1,207 +0,0 @@
"""
GNS3 Project Write File Tool
Provides a LangChain tool for writing files to GNS3 projects.
This tool directly uses the Project.write_file() method from custom_gns3fy.
"""
import json
import logging
import re
from typing import Any
from langchain.tools import BaseTool
from langchain_core.callbacks import CallbackManagerForToolRun
from gns3_copilot.gns3_client import Project, get_gns3_connector
from gns3_copilot.gns3_client.gns3_file_index import add_file_to_index
# Configure logging
logger = logging.getLogger(__name__)
class GNS3ProjectWriteFileTool(BaseTool):
"""
A LangChain tool to write files to a GNS3 project.
This tool writes content to a file in a GNS3 project directory.
It uses Project.write_file() method from custom_gns3fy.
**Input:**
A JSON object containing the project_id, file path, and data to write.
Example input:
{
"project_id": "uuid-of-project",
"path": "README.md",
"data": "This is the file content..."
}
**Output:**
A dictionary containing the project_id, file path, and status.
Example output:
{
"project_id": "uuid-of-project",
"path": "README.md",
"status": "success"
}
If an error occurs, returns a dictionary with an error message.
Example error output:
{
"project_id": "uuid-of-project",
"path": "README.md",
"status": "failed",
"error": "Failed to write file"
}
"""
name: str = "gns3_project_write_file"
description: str = """
Writes content to a file in a GNS3 project directory.
Input is a JSON object with project_id, file path, and data to write.
Example input:
{
"project_id": "uuid-of-project",
"path": "README.md",
"data": "This is the file content..."
}
Returns a dictionary with the write operation status.
If the operation fails, returns a dictionary with an error message.
"""
def _run(
self,
tool_input: str,
run_manager: CallbackManagerForToolRun | None = None,
**kwargs: Any,
) -> str:
"""
Writes a file to a GNS3 project.
Args:
tool_input (str): A JSON string containing project_id, file path, and data.
run_manager: LangChain run manager (unused).
Returns:
str: A JSON string with the write operation status, or an error message.
"""
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Parse input JSON
input_data = json.loads(tool_input)
project_id = input_data.get("project_id")
path = input_data.get("path")
data = input_data.get("data")
# Validate input
if not project_id:
logger.error("Invalid input: Missing project_id.")
return json.dumps({"error": "Missing project_id."})
if not path:
logger.error("Invalid input: Missing path.")
return json.dumps({"error": "Missing path."})
if data is None:
logger.error("Invalid input: Missing data.")
return json.dumps({"error": "Missing data."})
# Validate project_id format (UUID)
if not self._validate_project_id(project_id):
error_msg = (
f"Invalid project_id format: {project_id}. Expected UUID format."
)
logger.error(error_msg)
return json.dumps({"error": error_msg})
# Get connector using factory function
connector = get_gns3_connector()
if connector is None:
error_msg = "Failed to create GNS3 connector. Check configuration."
logger.error(error_msg)
return json.dumps({"error": error_msg})
logger.info("Connecting to GNS3 server...")
# Create Project instance
project = Project(project_id=project_id, connector=connector)
# Write file content
logger.info("Writing file '%s' to project '%s'...", path, project_id)
project.write_file(path=path, data=data)
# Update file index
logger.info("Updating file index for project '%s'...", project_id)
file_size = len(str(data))
add_file_to_index(project, path, size=file_size)
logger.info("Successfully updated file index")
# Prepare successful result
result = {
"project_id": project_id,
"path": path,
"status": "success",
}
logger.info(
"Successfully wrote file '%s' to project '%s'", path, project_id
)
return json.dumps(result)
except json.JSONDecodeError as e:
logger.error("Invalid JSON input: %s", e)
return json.dumps({"error": f"Invalid JSON input: {e}"})
except ValueError as e:
logger.error("Value error: %s", e)
return json.dumps({"error": f"Value error: {e}"})
except Exception as e:
logger.error("Failed to write file to project: %s", e)
return json.dumps(
{
"project_id": project_id if "project_id" in locals() else None,
"path": path if "path" in locals() else None,
"status": "failed",
"error": f"Failed to write file: {str(e)}",
}
)
def _validate_project_id(self, project_id: str) -> bool:
"""
Validate project_id format (UUID).
Args:
project_id: The project ID to validate
Returns:
True if valid UUID format, False otherwise
"""
uuid_pattern = r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
return bool(re.match(uuid_pattern, project_id))
if __name__ == "__main__":
import pprint
print("=" * 80)
print("Testing GNS3ProjectWriteFileTool")
print("=" * 80)
write_test_input = json.dumps(
{
"project_id": "1445a4ba-4635-430b-a332-bef438f65932", # Replace with actual project UUID
"path": "test_write.txt",
"data": "This is a test file created by GNS3ProjectWriteFileTool\n"
"Created on: 2026-01-01",
}
)
write_tool = GNS3ProjectWriteFileTool()
write_result = write_tool._run(write_test_input)
print("\nWrite Result:")
pprint.pprint(write_result)

View File

@ -1,77 +0,0 @@
import logging
from typing import Any
from langchain.tools import BaseTool
from gns3_copilot.gns3_client import get_gns3_connector
# Configure logging
logger = logging.getLogger(__name__)
"""
example output:
[('mylab', 'ff8e059c-c33d-47f4-bc11-c7dda8a1d500', 0, 0, 'closed'),
('q-learning-traffic-management', '69d49a6a-ff7f-45dd-af1e-dc14aff600cc', 0, 0, 'closed'),
('network_ai', 'f2f7ed27-7aa3-4b11-a64c-da947a2c7210', 6, 8, 'opened'),
('test', '365dd3ff-cda9-447a-94da-3a6cef75fe77', 0, 0, 'closed'),
('Soft-RoCE learning', 'd1e4509e-64bd-4109-b954-266223959ee9', 0, 0, 'closed')]
"""
class GNS3ProjectList(BaseTool):
name: str = "list_gns3_projects"
description: str = """
Retrieves a list of all GNS3 projects with their details.
Returns a dictionary containing a list of project information including name,
project_name, project_id, nodes count, links count, and status.
Example output:
{
"projects": [
("mylab", "ff8e059c-c33d-47f4-bc11-c7dda8a1d500", 0, 0, "closed"),
("network_ai", "f2f7ed27-7aa3-4b11-a64c-da947a2c7210", 6, 8, "opened")
]
}
"""
def _run(self, tool_input: Any = None, run_manager: Any = None) -> dict:
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Initialize Gns3Connector using factory function
logger.info("Connecting to GNS3 server...")
server = get_gns3_connector()
if server is None:
logger.error("Failed to create GNS3 connector")
return {
"error": "Failed to connect to GNS3 server. Please check your configuration."
}
# Return the projects data in a structured format
projects = server.projects_summary(is_print=False)
# Prepare result
result = {"projects": projects}
# Log result
logger.info("Projects list result: %s", result)
return result
except Exception as e:
logger.error("Error retrieving GNS3 project list: %s", str(e))
return {"error": f"Failed to retrieve GNS3 project list: {str(e)}"}
if __name__ == "__main__":
from pprint import pprint
# Test the tool
tool = GNS3ProjectList()
print("Testing GNS3ProjectList - retrieving all projects...")
result = tool._run()
pprint(result)

View File

@ -0,0 +1,28 @@
"""
Prompt loader for GNS3 Copilot.
This module provides utilities for loading system prompts.
"""
import logging
import os
from .base_prompt import SYSTEM_PROMPT
logger = logging.getLogger(__name__)
def load_system_prompt() -> str:
"""
Load the system prompt for GNS3 Copilot.
In the future, this can be extended to support multiple prompt variants
based on environment variables (e.g., ENGLISH_LEVEL).
Returns:
str: The system prompt string.
"""
# For now, just return the base system prompt
# Future enhancement: Load different prompts based on ENGLISH_LEVEL env var
# english_level = os.getenv("ENGLISH_LEVEL", "native")
return SYSTEM_PROMPT

View File

@ -1,5 +1,5 @@
"""
FlowNet-Lab Tools Package
GNS3-Copilot Tools Package
This package provides various tools for interacting with GNS3 network simulator, including:
- Device configuration command execution
@ -7,8 +7,6 @@ This package provides various tools for interacting with GNS3 network simulator,
- Multiple device command execution using Nornir
- VPCS device configuration using telnetlib3
- Node and link management
- Drawing management
- Notes management
Main modules:
- config_tools_nornir: Multiple device configuration command execution tool using Nornir
@ -18,14 +16,7 @@ Main modules:
- gns3_create_link: GNS3 link creation tool
- gns3_start_node: GNS3 node startup tool
- gns3_get_node_temp: GNS3 template retrieval tool
- gns3_get_drawings: GNS3 drawing retrieval tool
- gns3_create_drawing: GNS3 drawing creation tool
- gns3_update_drawing: GNS3 drawing update tool
- gns3_delete_drawing: GNS3 drawing deletion tool
- gns3_create_area_drawing: GNS3 area annotation creation tool (ellipse for 2 nodes)
- gns3_drawing_utils: Drawing utility functions for calculating SVG parameters
- linux_tools_nornir: Linux Telnet batch command execution tool using Nornir
- window_controller: Frontend window control and text input tools (async WebSocket-based)
- gns3_update_node_name: GNS3 node name update tool
Note: GNS3TopologyTool is now available from gns3_client package
@ -35,15 +26,12 @@ Author: Guobin Yue
# Import main tool classes
from .config_tools_nornir import ExecuteMultipleDeviceConfigCommands
from .display_tools_nornir import ExecuteMultipleDeviceCommands
from .gns3_create_area_drawing import GNS3CreateAreaDrawingTool
from .gns3_create_link import GNS3LinkTool
from .gns3_create_node import GNS3CreateNodeTool
from .gns3_get_node_temp import GNS3TemplateTool
from .gns3_start_node import GNS3StartNodeTool, GNS3StartNodeQuickTool
from .gns3_update_node_name import GNS3UpdateNodeNameTool
from .linux_tools_nornir import LinuxTelnetBatchTool
from .vpcs_tools_telnetlib3 import VPCSMultiCommands
from .window_controller import WindowControllerTool, TextInputTool
# Dynamic version management
try:
@ -68,11 +56,7 @@ __all__ = [
"GNS3StartNodeQuickTool",
"GNS3UpdateNodeNameTool",
"GNS3TemplateTool",
"GNS3CreateAreaDrawingTool",
"LinuxTelnetBatchTool",
"WindowControllerTool",
"TextInputTool",
]
# Package initialization message
# print(f"FlowNet-Lab Tools package loaded (version {__version__})")
# print(f"GNS3-Copilot Tools package loaded (version {__version__})")

View File

@ -1,403 +0,0 @@
"""
GNS3 area annotation drawing tool for creating visual area markers.
This tool creates visual annotations (ellipses) for network devices to represent
groupings such as OSPF areas, EIGRP AS numbers, or other protocol-defined regions.
Currently supports two-node ellipse annotations.
"""
import json
import logging
from typing import Any
from langchain.tools import BaseTool
from langchain_core.callbacks import CallbackManagerForToolRun
from gns3_copilot.gns3_client import (
GNS3GetNodesTool,
Project,
get_gns3_connector,
)
from gns3_copilot.utils.gns3_drawing_utils import (
calculate_two_node_ellipse,
calculate_two_node_rectangle,
)
# Configure logging
logger = logging.getLogger(__name__)
class GNS3CreateAreaDrawingTool(BaseTool):
"""
A LangChain tool to create visual area annotations for network devices.
Creates ellipse annotations that connect two network devices,
automatically calculating optimal position, size, and rotation based on node coordinates.
**Input:**
A JSON object containing the project_id, area_name, and node_names.
Example input:
{
"project_id": "uuid-of-project",
"area_name": "Area 0",
"node_names": ["R-1", "R-2"]
}
**Output:**
A dictionary containing the creation results:
{
"project_id": "uuid-of-project",
"area_name": "Area 0",
"node_count": 2,
"shape_type": "ellipse",
"created_drawings": [
{
"drawing_id": "uuid-of-drawing1",
"type": "ellipse",
"status": "success"
},
{
"drawing_id": "uuid-of-drawing2",
"type": "text",
"status": "success"
}
],
"total_drawings": 2,
"successful_drawings": 2,
"failed_drawings": 0
}
**Use Cases:**
- Protocol Domains: OSPF areas, BGP AS, IS-IS levels
- Logical Isolation: VRF, VLAN, MSTP
- High Availability: VRRP, HSRP, Stack, M-LAG
- External Boundaries: Internet, DMZ
- Management Networks: OOB, Management
**Semantic Color Coding:**
The tool automatically applies business-professional colors based on area_name keywords:
| Category | Colors | Keywords in area_name | Border Style |
|------------------------|---------------------|------------------------------|--------------|
| Primary Routing | Blue (#2196F3) | BGP, AS, Area 0, Backbone | Solid |
| Secondary Routing | Light Blue (#64B5F6)| Area, Level | Solid |
| Logical Isolation | Purple (#9C27B0) | VRF, VLAN, MSTP | Dashed |
| High Availability | Amber (#FFC107) | VRRP, HSRP, HA, Stack | Solid |
| External/Boundary | Red (#EF5350) | INET, OUT, External, DMZ | Solid |
| Management | Gray (#757575) | MGMT, OOB | Dashed |
**Implementation Details:**
- Retrieves node coordinates from the GNS3 project topology
- Calculates rotated ellipse parameters (center, radius, rotation angle)
- Generates professional SVG graphics with semantic colors
- Creates two drawings: ellipse shape and text label
- All coordinates are integers as required by GNS3 API
**Example Usage:**
User: "Configure OSPF area 0 on R-1 and R-2"
Call: create_gns3_area_drawing(project_id="xxx", area_name="Area 0", node_names=["R-1", "R-2"])
User: "Create VLAN 10 for SW-1 and SW-2"
Call: create_gns3_area_drawing(project_id="xxx", area_name="VLAN 10", node_names=["SW-1", "SW-2"])
User: "Configure VRRP group 1 on R-1 and R-2"
Call: create_gns3_area_drawing(project_id="xxx", area_name="VRRP Group 1", node_names=["R-1", "R-2"])
"""
name: str = "create_gns3_area_drawing"
description: str = """
Creates a visual annotation (ellipse or rectangle) to mark logical groupings between two network devices.
Use for protocol domains (OSPF areas, BGP AS, IS-IS levels), logical isolation (VRF, VLAN),
or high availability groups (VRRP, HSRP). Automatically calculates optimal position,
size, rotation, and applies semantic colors.
Parameters:
- project_id: GNS3 project UUID
- area_name: Logical group name (e.g., "Area 0", "VLAN 10", "VRRP Group 1")
- node_names: List of exactly 2 node names (e.g., ["R-1", "R-2"])
- shape_type: Shape type, either "ellipse" (default) or "rectangle"
"""
def _run(
self,
tool_input: str,
run_manager: CallbackManagerForToolRun | None = None,
**kwargs: Any,
) -> dict[str, Any]:
"""
Creates a visual area annotation for network devices.
Args:
tool_input: A JSON string containing project_id, area_name, and node_names.
run_manager: LangChain run manager (unused).
Returns:
dict: A dictionary with creation results or an error message.
"""
# Log received input
logger.info("Received input: %s", tool_input)
try:
# Parse input JSON
input_data = json.loads(tool_input)
project_id = input_data.get("project_id")
area_name = input_data.get("area_name")
node_names = input_data.get("node_names", [])
shape_type = input_data.get("shape_type", "ellipse")
# Validate input
if not project_id:
logger.error("Invalid input: Missing project_id.")
return {"error": "Missing project_id."}
if not area_name:
logger.error("Invalid input: Missing area_name.")
return {"error": "Missing area_name."}
if not isinstance(node_names, list) or len(node_names) == 0:
logger.error("Invalid input: node_names must be a non-empty array.")
return {"error": "node_names must be a non-empty array."}
# Validate: requires exactly 2 nodes
if len(node_names) != 2:
logger.error(
"Invalid input: Exactly 2 nodes are required, got %d.",
len(node_names),
)
return {
"error": f"Exactly 2 nodes are required, got {len(node_names)}. "
"Please provide exactly 2 node names."
}
# Initialize Gns3Connector using factory function
logger.info("Connecting to GNS3 server...")
gns3_server = get_gns3_connector()
if gns3_server is None:
logger.error("Failed to create GNS3 connector")
return {
"error": "Failed to connect to GNS3 server. Please check your configuration."
}
# Initialize Project object for drawing creation
logger.info("Initializing project for drawing creation...")
project = Project(project_id=project_id, connector=gns3_server)
project.get() # Load project details
# Get node information using GNS3GetNodesTool to retrieve complete node data
# including height, width, coordinates, etc.
logger.info(
"Retrieving node information for project %s...",
project_id,
)
get_nodes_tool = GNS3GetNodesTool()
nodes_result = get_nodes_tool._run(json.dumps({"project_id": project_id}))
# Check if node retrieval was successful
if "error" in nodes_result:
logger.error("Failed to retrieve nodes: %s", nodes_result["error"])
return {"error": f"Failed to retrieve nodes: {nodes_result['error']}"}
# Build a dictionary mapping node names to their complete information
nodes_dict = {node["name"]: node for node in nodes_result.get("nodes", [])}
# Validate all requested nodes exist
for node_name in node_names:
if node_name not in nodes_dict:
logger.error("Node %s not found in project", node_name)
return {
"error": f"Node '{node_name}' not found in project topology."
}
# Retrieve all requested nodes
nodes = [nodes_dict[node_name] for node_name in node_names]
# Log node information
node_info_list = [
f"{node_name} at ({node['x']}, {node['y']}, size: {node.get('width', 'N/A')}x{node.get('height', 'N/A')})"
for node_name, node in zip(node_names, nodes, strict=True)
]
logger.info("Found nodes: %s", ", ".join(node_info_list))
# Validate shape_type
if shape_type not in ["ellipse", "rectangle"]:
logger.error("Invalid shape_type: %s", shape_type)
return {
"error": f"Invalid shape_type '{shape_type}'. Must be 'ellipse' or 'rectangle'."
}
# Calculate shape parameters for 2 nodes
logger.info("Calculating %s parameters for 2 nodes...", shape_type)
# Use the appropriate calculation function based on shape_type
if shape_type == "ellipse":
shape_result = calculate_two_node_ellipse(nodes[0], nodes[1], area_name)
shape_key = "ellipse"
else: # rectangle
shape_result = calculate_two_node_rectangle(
nodes[0], nodes[1], area_name
)
shape_key = "rectangle"
metadata = shape_result["metadata"]
logger.info(
"Two-node %s: center=(%.2f, %.2f), distance=%.2f, angle=%.2f°",
shape_type,
metadata["center_x"],
metadata["center_y"],
metadata["distance"],
metadata["angle_deg"],
)
# Prepare drawings for creation
# Ensure all coordinates are integers as required by GNS3 API
drawings = [
{
"svg": shape_result[shape_key]["svg"],
"x": int(shape_result[shape_key]["x"]),
"y": int(shape_result[shape_key]["y"]),
"z": shape_result[shape_key]["z"],
"locked": False,
"rotation": int(shape_result[shape_key]["rotation"]),
},
{
"svg": shape_result["text"]["svg"],
"x": int(shape_result["text"]["x"]),
"y": int(shape_result["text"]["y"]),
"z": shape_result["text"]["z"],
"locked": False,
"rotation": int(shape_result["text"]["rotation"]),
},
]
# Create drawings using Project method
logger.info(
"Creating %d drawings in project %s...", len(drawings), project_id
)
results: list[dict[str, Any]] = []
for i, drawing_data in enumerate(drawings):
try:
drawing_type = shape_type if i == 0 else "text"
logger.info(
"Creating drawing %d/%d: %s at (%d, %d) with rotation %d°...",
i + 1,
len(drawings),
drawing_type,
drawing_data["x"],
drawing_data["y"],
drawing_data["rotation"],
)
result = project.create_drawing(
svg=drawing_data["svg"],
x=drawing_data["x"],
y=drawing_data["y"],
z=drawing_data["z"],
locked=drawing_data["locked"],
rotation=drawing_data["rotation"],
)
drawing_info = {
"drawing_id": result.get("drawing_id"),
"type": drawing_type,
"status": "success",
}
results.append(drawing_info)
except Exception as e:
error_info = {
"type": "ellipse" if i == 0 else "text",
"error": f"Drawing {i + 1} creation failed: {str(e)}",
"status": "failed",
}
results.append(error_info)
logger.error("Failed to create drawing %d: %s", i + 1, e)
# Calculate summary statistics
successful_drawings = len(
[r for r in results if r.get("status") == "success"]
)
failed_drawings = len([r for r in results if r.get("status") == "failed"])
# Prepare final result
final_result = {
"project_id": project_id,
"area_name": area_name,
"node_count": len(node_names),
"nodes": node_names,
"shape_type": shape_type,
"created_drawings": results,
"total_drawings": len(drawings),
"successful_drawings": successful_drawings,
"failed_drawings": failed_drawings,
}
# Log the final result
logger.info(
"Area annotation creation completed: %d successful, %d failed out of %d total drawings.",
successful_drawings,
failed_drawings,
len(drawings),
)
# Return JSON-formatted result
return final_result
except json.JSONDecodeError as e:
logger.error("Invalid JSON input: %s", e)
return {"error": f"Invalid JSON input: {e}"}
except Exception as e:
logger.error("Failed to process area annotation request: %s", e)
return {"error": f"Failed to process area annotation request: {str(e)}"}
if __name__ == "__main__":
# Test the tool locally
from pprint import pprint
# Test with ellipse (default)
test_input_ellipse = json.dumps(
{
"project_id": "d7fc094c-685e-4db1-ac11-5e33a1b2e066", # Replace with actual project UUID
"area_name": "Core Area",
"node_names": ["R-6", "R-4"], # Replace with actual node names
"shape_type": "ellipse",
}
)
# Test with rectangle
test_input_rectangle = json.dumps(
{
"project_id": "d7fc094c-685e-4db1-ac11-5e33a1b2e066", # Replace with actual project UUID
"area_name": "VLAN 10",
"node_names": ["SW-1", "SW-2"], # Replace with actual node names
"shape_type": "rectangle",
}
)
tool = GNS3CreateAreaDrawingTool()
result = tool._run(test_input_ellipse)
pprint(result)
"""
example output:
{
'area_name': 'Area 0',
'created_drawings': [
{'drawing_id': 'uuid-of-drawing1', 'status': 'success', 'type': 'ellipse'},
{'drawing_id': 'uuid-of-drawing2', 'status': 'success', 'type': 'text'}
],
'failed_drawings': 0,
'node_count': 2,
'nodes': ['R-1', 'R-2'],
'project_id': '2245149a-71c8-4387-9d1f-441a683ef7e7',
'shape_type': 'ellipse',
'successful_drawings': 2,
'total_drawings': 2
}
"""

View File

@ -1,5 +1,5 @@
"""
FlowNet-Lab Public Model Package
GNS3-Copilot Public Model Package
This package provides reusable public models and utilities for GNS3 network automation tasks.
It contains common functionality that can be shared across different tools and modules.