feat(copilot): add copilot_mode field and config command tool

- Add `copilot_mode` field to LLM model configs API with "teaching" (diagnostics only) and "lab_assistant" (full configuration access) modes
- Introduce new `ExecuteMultipleDeviceConfigCommands` tool for executing configuration commands on multiple devices
- Include `tags` field in node data structure for enhanced project management
- Update API documentation examples to reflect new `copilot_mode` field and context limit additions
This commit is contained in:
YueGuobin 2026-03-05 16:31:02 +08:00
parent 8dc8facb13
commit a777c36a29
10 changed files with 426 additions and 53 deletions

View File

@ -124,6 +124,7 @@ The `model_type` field accepts the following values:
| `api_key` | string | API key (auto-encrypted) | | `api_key` | string | API key (auto-encrypted) |
| `max_tokens` | integer | Max tokens for generation | | `max_tokens` | integer | Max tokens for generation |
| `context_strategy` | string | Context trimming strategy: "conservative" (60%), "balanced" (75%), "aggressive" (85%). Default: "balanced" | | `context_strategy` | string | Context trimming strategy: "conservative" (60%), "balanced" (75%), "aggressive" (85%). Default: "balanced" |
| `copilot_mode` | string | GNS3-Copilot mode: "teaching" (diagnostics only, default) or "lab_assistant" (full configuration access) |
| `is_default` | boolean | Set as default (default: false) | | `is_default` | boolean | Set as default (default: false) |
**Important Notes:** **Important Notes:**
@ -255,7 +256,8 @@ curl -X POST http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \
"temperature": 0.7, "temperature": 0.7,
"context_limit": 128, "context_limit": 128,
"context_strategy": "balanced", "context_strategy": "balanced",
"api_key": "sk-xxx" "api_key": "sk-xxx",
"copilot_mode": null
}, },
"user_id": "uuid-user", "user_id": "uuid-user",
"group_id": null, "group_id": null,
@ -308,7 +310,8 @@ curl -X GET http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \
"temperature": 0.7, "temperature": 0.7,
"context_limit": 128, "context_limit": 128,
"context_strategy": "balanced", "context_strategy": "balanced",
"api_key": "sk-xxx" "api_key": "sk-xxx",
"copilot_mode": "lab_assistant"
}, },
"user_id": "uuid-user", "user_id": "uuid-user",
"group_id": null, "group_id": null,
@ -330,7 +333,8 @@ curl -X GET http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \
"temperature": 0.7, "temperature": 0.7,
"context_limit": 200, "context_limit": 200,
"context_strategy": "balanced", "context_strategy": "balanced",
"api_key": null "api_key": null,
"copilot_mode": null
}, },
"user_id": null, "user_id": null,
"group_id": "uuid-group", "group_id": "uuid-group",
@ -353,7 +357,8 @@ curl -X GET http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \
"temperature": 0.7, "temperature": 0.7,
"context_limit": 128, "context_limit": 128,
"context_strategy": "balanced", "context_strategy": "balanced",
"api_key": "sk-xxx" "api_key": "sk-xxx",
"copilot_mode": "lab_assistant"
}, },
"user_id": "uuid-user", "user_id": "uuid-user",
"group_id": null, "group_id": null,
@ -393,7 +398,10 @@ curl -X GET http://localhost:3080/v3/access/groups/{group_id}/llm-model-configs
"base_url": "https://api.anthropic.com", "base_url": "https://api.anthropic.com",
"model": "claude-3-opus-20240229", "model": "claude-3-opus-20240229",
"temperature": 0.7, "temperature": 0.7,
"api_key": "sk-ant-xxx" "context_limit": 200,
"context_strategy": "balanced",
"api_key": "sk-ant-xxx",
"copilot_mode": null
}, },
"user_id": null, "user_id": null,
"group_id": "uuid-group", "group_id": "uuid-group",
@ -411,7 +419,10 @@ curl -X GET http://localhost:3080/v3/access/groups/{group_id}/llm-model-configs
"base_url": "https://api.openai.com/v1", "base_url": "https://api.openai.com/v1",
"model": "gpt-4", "model": "gpt-4",
"temperature": 0.7, "temperature": 0.7,
"api_key": "sk-xxx" "context_limit": 128,
"context_strategy": "balanced",
"api_key": "sk-xxx",
"copilot_mode": null
}, },
"user_id": null, "user_id": null,
"group_id": "uuid-group", "group_id": "uuid-group",

View File

@ -65,6 +65,7 @@ from pathlib import Path
# Add backend to path for prompt_manager # Add backend to path for prompt_manager
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "backend")) sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "backend"))
from gns3server.agent.gns3_copilot.tools_v2 import ( from gns3server.agent.gns3_copilot.tools_v2 import (
ExecuteMultipleDeviceConfigCommands,
ExecuteMultipleDeviceCommands, ExecuteMultipleDeviceCommands,
GNS3CreateNodeTool, GNS3CreateNodeTool,
GNS3LinkTool, GNS3LinkTool,
@ -88,6 +89,7 @@ tools = [
GNS3StartNodeTool(), # Start GNS3 nodes GNS3StartNodeTool(), # Start GNS3 nodes
GNS3UpdateNodeNameTool(), # Update node name GNS3UpdateNodeNameTool(), # Update node name
ExecuteMultipleDeviceCommands(), # Execute show/display/debug commands on multiple devices (READ-ONLY) ExecuteMultipleDeviceCommands(), # Execute show/display/debug commands on multiple devices (READ-ONLY)
ExecuteMultipleDeviceConfigCommands(), # Execute configuration commands on multiple devices
VPCSMultiCommands(), # Execute VPCS commands on multiple devices VPCSMultiCommands(), # Execute VPCS commands on multiple devices
] ]
# Augment the LLM with tools # Augment the LLM with tools
@ -197,7 +199,8 @@ def llm_call(state: dict, config: RunnableConfig | None = None):
state["topology_info"] = topology_info state["topology_info"] = topology_info
# Create pre_model_hook for automatic topology injection and trimming # Create pre_model_hook for automatic topology injection and trimming
system_prompt = load_system_prompt() # Load system prompt based on copilot_mode configuration
system_prompt = load_system_prompt(llm_config)
pre_hook = create_pre_model_hook( pre_hook = create_pre_model_hook(
system_prompt=system_prompt, system_prompt=system_prompt,
get_topology_func=lambda s: s.get("topology_info"), get_topology_func=lambda s: s.get("topology_info"),

View File

@ -477,6 +477,11 @@ class Gns3Connector:
**Required Attributes:** **Required Attributes:**
- `name` or `template_id` - `name` or `template_id`
**Optional Attributes (can be passed via kwargs):**
- `tags` (list): List of tags for the template (e.g., ["device_type:cisco_ios_telnet", "platform:cisco_ios"])
- Any other template attributes supported by GNS3 API
""" """
# Get existing template # Get existing template
_template = self.get_template(name=name, template_id=template_id) _template = self.get_template(name=name, template_id=template_id)
@ -505,6 +510,21 @@ class Gns3Connector:
- `name` - `name`
- `compute_id` by default is 'local' - `compute_id` by default is 'local'
- `template_type` - `template_type`
**Optional Attributes (can be passed via kwargs):**
- `tags` (list): List of tags for the template (e.g., ["device_type:cisco_ios_telnet", "platform:cisco_ios"])
- Any other template attributes supported by GNS3 API
**Example:**
```python
>>> connector.create_template(
... name="cisco_router",
... template_type="dynamips",
... tags=["device_type:cisco_ios_telnet", "platform:cisco_ios"]
... )
```
""" """
# kwargs["name"] might raise KeyError at runtime, for more robust code we can use get first # kwargs["name"] might raise KeyError at runtime, for more robust code we can use get first
template_name = kwargs.get("name") template_name = kwargs.get("name")
@ -1051,6 +1071,7 @@ class Node:
z: int | None = None z: int | None = None
template_id: str | None = None template_id: str | None = None
properties: Any | None = None properties: Any | None = None
tags: list[str] | None = None
template: str | None = None template: str | None = None
links: list[Link] = field(default_factory=list, repr=False) links: list[Link] = field(default_factory=list, repr=False)
@ -2084,6 +2105,7 @@ class Project:
# "template": _n.template, # "template": _n.template,
"x": _n.x, "x": _n.x,
"y": _n.y, "y": _n.y,
"tags": _n.tags if _n.tags else [],
} }
} }
) )

View File

@ -29,9 +29,14 @@ Prompts Module for GNS3-Copilot
This package contains system prompts and prompt loading utilities for This package contains system prompts and prompt loading utilities for
the GNS3-Copilot AI agent. the GNS3-Copilot AI agent.
Available prompts:
- base_prompt: Teaching assistant mode (diagnostics only, no configuration)
- lab_assistant_prompt: Full lab assistant mode (diagnostics + configuration)
""" """
from .base_prompt import SYSTEM_PROMPT from .base_prompt import SYSTEM_PROMPT
from .lab_assistant_prompt import LAB_ASSISTANT_PROMPT
from .prompt_loader import load_system_prompt from .prompt_loader import load_system_prompt
from .title_prompt import TITLE_PROMPT from .title_prompt import TITLE_PROMPT
@ -49,6 +54,7 @@ __url__ = "https://github.com/yueguobin/gns3-copilot"
__all__ = [ __all__ = [
"SYSTEM_PROMPT", "SYSTEM_PROMPT",
"LAB_ASSISTANT_PROMPT",
"TITLE_PROMPT", "TITLE_PROMPT",
"load_system_prompt", "load_system_prompt",
] ]

View File

@ -0,0 +1,182 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# GNS3-Copilot - AI-powered Network Lab Assistant for GNS3
#
# This file is part of GNS3-Copilot project.
#
# GNS3-Copilot is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# 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 General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License
# along with GNS3-Copilot. If not, see <https://www.gnu.org/licenses/>.
#
# Copyright (C) 2025 Guobin Yue
# Author: Guobin Yue
#
# Project Home: https://github.com/yueguobin/gns3-copilot
#
"""
System Prompt for GNS3 Lab Assistant
This module contains the system prompt for the GNS3 Lab Assistant,
an AI-powered assistant that can perform both diagnostic AND configuration
operations in GNS3 network labs.
This is the full-featured assistant mode with configuration permissions enabled.
"""
# System prompt for GNS3 Lab Assistant
LAB_ASSISTANT_PROMPT = """
# ROLE & MISSION
You are a **GNS3 Network Lab Assistant** - an intelligent automation assistant
designed to help users manage, diagnose, and configure network devices in GNS3
virtual laboratories.
**Your Mission**: Understand user intent and use available tools to efficiently
complete network operations, including both diagnostics and configurations.
---
# AVAILABLE TOOLS
You have access to the following tools to help users:
| Tool | Purpose | Usage |
|------|---------|-------|
| `gns3_template_reader` | Get available node templates | List templates for creating nodes |
| `gns3_create_node` | Create new nodes in topology | Add routers, switches, VPCS, etc. |
| `gns3_link_tool` | Create links between nodes | Connect network topology |
| `gns3_start_node_tool` | Start/stop nodes | Control device power state |
| `gns3_update_node_name_tool` | Update node names | Rename devices |
| `execute_multiple_device_commands` | Execute display commands | Read-only diagnostics (show/display/debug) |
| `execute_multiple_device_config_commands` | Execute configuration commands | Make configuration changes |
| `vpcs_multi_commands` | Execute VPCS commands | Configure VPCS devices |
---
# TOOL USAGE RULES
1. **Sequential Execution**: Call ONE tool at a time, wait for results before calling next
2. **Topology Awareness**: If topology is already in context, DO NOT call topology reader again
3. **Efficient Operations**: Batch commands for multiple devices when possible
4. **Safety First**: Be cautious with destructive operations (reload, erase, format)
---
# WORKFLOW GUIDELINES
## Step 1: Understand Intent
Analyze what the user wants to accomplish:
- Diagnosis? → Use `execute_multiple_device_commands`
- Configuration? → Use `execute_multiple_device_config_commands`
- Topology changes? → Use node/link management tools
## Step 2: Plan & Execute
1. Check current topology context
2. Use appropriate tool(s) for the task
3. Verify results when necessary
4. Provide clear feedback to user
## Step 3: Report Results
Clearly communicate:
- What was done
- Results (success/failure)
- Any errors encountered
- Next steps or recommendations
---
# COMMAND EXAMPLES
## Diagnostic Commands (READ-ONLY)
```
# Cisco IOS
show version
show running-config
show ip interface brief
show ip route
show ip ospf neighbor
show ip bgp summary
debug ip routing
# Huawei VRP
display current-configuration
display ip routing-table
display ospf peer
display bgp peer
# Verification
ping 192.168.1.1
traceroute 10.0.0.1
```
## Configuration Commands
```
# Interface Configuration
interface GigabitEthernet0/0
ip address 192.168.1.1 255.255.255.0
no shutdown
# Routing Configuration
router ospf 1
network 192.168.1.0 0.0.0.255 area 0
# VLAN Configuration
vlan 10
name Sales
```
---
# RESPONSE GUIDELINES
1. **Language Matching**:
- User writes in Chinese → Respond in Chinese
- User writes in English → Respond in English
- Keep technical terms in English (OSPF, BGP, VLAN, CLI commands)
2. **Clear Structure**:
```markdown
## 操作总结 / Operation Summary
**执行的任务**: [What was done]
**结果**: [Success/Failure]
## 详细信息 / Details
[Device outputs, configurations, etc.]
```
3. **Error Handling**:
- Report errors clearly
- Suggest troubleshooting steps
- Offer to retry or investigate further
---
# SAFETY REMINDERS
While you have configuration permissions, exercise caution:
- ⚠️ Avoid destructive commands (reload, erase, format) unless explicitly requested
- ⚠️ Warn user before making major changes
- ⚠️ Recommend backup for critical configurations
- ⚠️ Verify connectivity before routing protocol changes
---
# CURRENT TOPOLOGY
{{topology_info}}
**Note**: Topology is already retrieved. DO NOT call topology reader again unless needed.
---
"""

View File

@ -27,29 +27,45 @@
Prompt Loader for GNS3-Copilot Prompt Loader for GNS3-Copilot
This module provides utilities for loading system prompts. This module provides utilities for loading system prompts.
Can be extended to support multiple prompt variants based on Supports multiple prompt variants based on LLM model configuration.
environment variables (e.g., ENGLISH_LEVEL).
Available Modes (controlled by config.copilot_mode in llm_model_configs):
- "teaching" (default): Teaching assistant mode - diagnostics only, no configuration
- "lab_assistant": Full lab assistant mode - diagnostics and configuration enabled
""" """
import logging import logging
import os
from .base_prompt import SYSTEM_PROMPT from .base_prompt import SYSTEM_PROMPT
from .lab_assistant_prompt import LAB_ASSISTANT_PROMPT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def load_system_prompt() -> str: def load_system_prompt(llm_config: dict | None = None) -> str:
""" """
Load the system prompt for GNS3-Copilot. Load the system prompt for GNS3-Copilot.
In the future, this can be extended to support multiple prompt variants The prompt mode is controlled by the `copilot_mode` field in the LLM model config:
based on environment variables (e.g., ENGLISH_LEVEL). - "teaching" (default): Teaching assistant mode - diagnostics only, no configuration
- "lab_assistant": Full lab assistant mode - diagnostics and configuration enabled
Args:
llm_config: LLM model configuration dictionary containing the config field
Returns: Returns:
str: The system prompt string. str: The system prompt string.
""" """
# For now, just return the base system prompt if not llm_config:
# Future enhancement: Load different prompts based on ENGLISH_LEVEL env var logger.info("No LLM config provided, using default TEACHING assistant prompt mode")
# english_level = os.getenv("ENGLISH_LEVEL", "native") return SYSTEM_PROMPT
return SYSTEM_PROMPT
config = llm_config.get("config", {})
mode = config.get("copilot_mode", "teaching").lower()
if mode == "lab_assistant":
logger.info("Using LAB_ASSISTANT prompt mode (diagnostics + configuration)")
return LAB_ASSISTANT_PROMPT
else:
logger.info("Using TEACHING assistant prompt mode (diagnostics only)")
return SYSTEM_PROMPT

View File

@ -53,25 +53,52 @@ def _get_nornir_defaults() -> dict[str, Any]:
"""Get Nornir default configuration for Cisco IOS.""" """Get Nornir default configuration for Cisco IOS."""
return {"data": {"location": "gns3"}} return {"data": {"location": "gns3"}}
def _get_nornir_groups_config() -> dict[str, dict[str, Any]]: def _get_nornir_groups_config(
"""Get Nornir groups configuration for Cisco IOS Telnet devices.""" device_type: str = "cisco_ios_telnet",
platform: str = "cisco_ios"
) -> dict[str, Any]:
"""
Get Nornir group configuration for Cisco IOS Telnet devices.
Args:
device_type: Device type for Netmiko (e.g., 'cisco_ios_telnet')
platform: Platform type for Nornir (e.g., 'cisco_ios')
Returns:
Dictionary containing Nornir group configuration
"""
return { return {
"cisco_IOSv_telnet": { "platform": platform,
"platform": "cisco_ios", "hostname": get_gns3_server_host(),
"hostname": get_gns3_server_host(), "timeout": 120,
"timeout": 120, "username": "",
"username": "", "password": "",
"password": "", "connection_options": {
"connection_options": { "netmiko": {"extras": {"device_type": device_type}}
"netmiko": {"extras": {"device_type": "cisco_ios_telnet"}}
},
}, },
} }
def _get_nornir_group(group_name: str = "cisco_IOSv_telnet") -> dict[str, Any]: def _get_nornir_group(
"""Get Nornir group configuration for a specific group.""" group_name: str = "cisco_IOSv_telnet",
all_groups = _get_nornir_groups_config() device_type: str = "cisco_ios_telnet",
return all_groups.get(group_name, {}) platform: str = "cisco_ios"
) -> dict[str, Any]:
"""
Get Nornir group configuration for a specific group.
Args:
group_name: Name of the group
device_type: Device type for Netmiko (e.g., 'cisco_ios_telnet')
platform: Platform type for Nornir (e.g., 'cisco_ios')
Returns:
Dictionary containing group configuration
"""
# _get_nornir_groups_config now returns the group config directly
return _get_nornir_groups_config(
device_type=device_type,
platform=platform
)
class ExecuteMultipleDeviceConfigCommands(BaseTool): class ExecuteMultipleDeviceConfigCommands(BaseTool):
""" """
@ -389,8 +416,27 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
def _initialize_nornir(self, hosts_data: dict[str, dict[str, Any]]) -> Nornir: def _initialize_nornir(self, hosts_data: dict[str, dict[str, Any]]) -> Nornir:
"""Initialize Nornir with the provided hosts data.""" """Initialize Nornir with the provided hosts data."""
try: try:
# Get latest environment configuration # Extract device_type and platform from hosts_data
groups_data = _get_nornir_group("cisco_IOSv_telnet") # Use the first device's configuration as default
device_type = None
platform = None
if hosts_data:
first_device_data = next(iter(hosts_data.values()), {})
device_type = first_device_data.get("device_type", "cisco_ios_telnet")
platform = first_device_data.get("platform", "cisco_ios")
logger.info(
"Extracted from tags: device_type=%s, platform=%s",
device_type,
platform
)
# Get latest environment configuration with dynamic device_type and platform
groups_data = _get_nornir_groups_config(
device_type=device_type or "cisco_ios_telnet",
platform=platform or "cisco_ios"
)
defaults = _get_nornir_defaults() defaults = _get_nornir_defaults()
# Log nornir account information # Log nornir account information

View File

@ -54,25 +54,52 @@ def _get_nornir_defaults() -> dict[str, Any]:
"""Get Nornir default configuration for Cisco IOS.""" """Get Nornir default configuration for Cisco IOS."""
return {"data": {"location": "gns3"}} return {"data": {"location": "gns3"}}
def _get_nornir_groups_config() -> dict[str, dict[str, Any]]: def _get_nornir_groups_config(
"""Get Nornir groups configuration for Cisco IOS Telnet devices.""" device_type: str = "cisco_ios_telnet",
platform: str = "cisco_ios"
) -> dict[str, Any]:
"""
Get Nornir group configuration for Cisco IOS Telnet devices.
Args:
device_type: Device type for Netmiko (e.g., 'cisco_ios_telnet')
platform: Platform type for Nornir (e.g., 'cisco_ios')
Returns:
Dictionary containing Nornir group configuration
"""
return { return {
"cisco_IOSv_telnet": { "platform": platform,
"platform": "cisco_ios", "hostname": get_gns3_server_host(),
"hostname": get_gns3_server_host(), "timeout": 120,
"timeout": 120, "username": "",
"username": "", "password": "",
"password": "", "connection_options": {
"connection_options": { "netmiko": {"extras": {"device_type": device_type}}
"netmiko": {"extras": {"device_type": "cisco_ios_telnet"}}
},
}, },
} }
def _get_nornir_group(group_name: str = "cisco_IOSv_telnet") -> dict[str, Any]: def _get_nornir_group(
"""Get Nornir group configuration for a specific group.""" group_name: str = "cisco_IOSv_telnet",
all_groups = _get_nornir_groups_config() device_type: str = "cisco_ios_telnet",
return all_groups.get(group_name, {}) platform: str = "cisco_ios"
) -> dict[str, Any]:
"""
Get Nornir group configuration for a specific group.
Args:
group_name: Name of the group
device_type: Device type for Netmiko (e.g., 'cisco_ios_telnet')
platform: Platform type for Nornir (e.g., 'cisco_ios')
Returns:
Dictionary containing group configuration
"""
# _get_nornir_groups_config now returns the group config directly
return _get_nornir_groups_config(
device_type=device_type,
platform=platform
)
class ExecuteMultipleDeviceCommands(BaseTool): class ExecuteMultipleDeviceCommands(BaseTool):
""" """
@ -403,8 +430,27 @@ class ExecuteMultipleDeviceCommands(BaseTool):
def _initialize_nornir(self, hosts_data: dict[str, dict[str, Any]]) -> Nornir: def _initialize_nornir(self, hosts_data: dict[str, dict[str, Any]]) -> Nornir:
"""Initialize Nornir with the provided hosts data.""" """Initialize Nornir with the provided hosts data."""
try: try:
# Get latest environment configuration # Extract device_type and platform from hosts_data
groups_data = _get_nornir_group("cisco_IOSv_telnet") # Use the first device's configuration as default
device_type = None
platform = None
if hosts_data:
first_device_data = next(iter(hosts_data.values()), {})
device_type = first_device_data.get("device_type", "cisco_ios_telnet")
platform = first_device_data.get("platform", "cisco_ios")
logger.info(
"Extracted from tags: device_type=%s, platform=%s",
device_type,
platform
)
# Get latest environment configuration with dynamic device_type and platform
groups_data = _get_nornir_groups_config(
device_type=device_type or "cisco_ios_telnet",
platform=platform or "cisco_ios"
)
defaults = _get_nornir_defaults() defaults = _get_nornir_defaults()
# Log nornir account information # Log nornir account information

View File

@ -48,7 +48,9 @@ def get_device_ports_from_topology(
{ {
"device_name": { "device_name": {
"port": console_port, "port": console_port,
"groups": ["cisco_IOSv_telnet"] "groups": ["cisco_IOSv_telnet"],
"device_type": "cisco_ios_telnet", # Extracted from tags
"platform": "cisco_ios" # Extracted from tags
} }
} }
Devices that don't exist or missing console_port will not be included Devices that don't exist or missing console_port will not be included
@ -82,10 +84,36 @@ def get_device_ports_from_topology(
logger.warning("Device '%s' missing console_port", device_name) logger.warning("Device '%s' missing console_port", device_name)
continue continue
# Extract device_type and platform from tags
device_type = None
platform = None
tags = node_info.get("tags", [])
for tag in tags:
if tag.startswith("device_type:"):
device_type = tag.split(":", 1)[1].strip()
elif tag.startswith("platform:"):
platform = tag.split(":", 1)[1].strip()
# Use defaults if not found in tags
if device_type is None:
device_type = "cisco_ios_telnet"
logger.debug("Device '%s': device_type not found in tags, using default: cisco_ios_telnet", device_name)
else:
logger.debug("Device '%s': extracted device_type=%s from tags", device_name, device_type)
if platform is None:
platform = "cisco_ios"
logger.debug("Device '%s': platform not found in tags, using default: cisco_ios", device_name)
else:
logger.debug("Device '%s': extracted platform=%s from tags", device_name, platform)
# Add device to hosts_data # Add device to hosts_data
hosts_data[device_name] = { hosts_data[device_name] = {
"port": node_info["console_port"], "port": node_info["console_port"],
"groups": ["cisco_IOSv_telnet"], "groups": ["cisco_IOSv_telnet"],
"device_type": device_type,
"platform": platform,
} }
logger.info("Returning %d device port mappings", len(hosts_data)) logger.info("Returning %d device port mappings", len(hosts_data))

View File

@ -48,9 +48,14 @@ class LLMModelConfigData(BaseModel):
context_strategy: Literal["conservative", "balanced", "aggressive"] = Field( context_strategy: Literal["conservative", "balanced", "aggressive"] = Field(
"balanced", description="Context trimming strategy: conservative (60%), balanced (75%), aggressive (85%)" "balanced", description="Context trimming strategy: conservative (60%), balanced (75%), aggressive (85%)"
) )
copilot_mode: Optional[str] = Field(
None,
description="GNS3-Copilot mode: 'teaching' (diagnostics only) or 'lab_assistant' (full configuration access)"
)
# Allow extra fields for extensibility # Allow extra fields for extensibility
model_config = ConfigDict(extra="allow") # Ensure all fields are included in serialization, even if None
model_config = ConfigDict(extra="allow", populate_by_name=True)
# Request schemas # Request schemas
@ -71,6 +76,10 @@ class LLMModelConfigCreate(BaseModel):
context_strategy: Literal["conservative", "balanced", "aggressive"] = Field( context_strategy: Literal["conservative", "balanced", "aggressive"] = Field(
"balanced", description="Context trimming strategy" "balanced", description="Context trimming strategy"
) )
copilot_mode: Optional[str] = Field(
None,
description="GNS3-Copilot mode: 'teaching' (diagnostics only) or 'lab_assistant' (full configuration access)"
)
# Allow extra config fields # Allow extra config fields
model_config = ConfigDict(extra="allow") model_config = ConfigDict(extra="allow")
@ -96,6 +105,10 @@ class LLMModelConfigUpdate(BaseModel):
context_strategy: Optional[Literal["conservative", "balanced", "aggressive"]] = Field( context_strategy: Optional[Literal["conservative", "balanced", "aggressive"]] = Field(
None, description="Context trimming strategy" None, description="Context trimming strategy"
) )
copilot_mode: Optional[str] = Field(
None,
description="GNS3-Copilot mode: 'teaching' (diagnostics only) or 'lab_assistant' (full configuration access)"
)
# Allow extra config fields # Allow extra config fields
model_config = ConfigDict(extra="allow") model_config = ConfigDict(extra="allow")