diff --git a/docs/gns3-copilot/llm-model-configs-api.md b/docs/gns3-copilot/llm-model-configs-api.md index a94b513f4..30adb03fc 100644 --- a/docs/gns3-copilot/llm-model-configs-api.md +++ b/docs/gns3-copilot/llm-model-configs-api.md @@ -124,6 +124,7 @@ The `model_type` field accepts the following values: | `api_key` | string | API key (auto-encrypted) | | `max_tokens` | integer | Max tokens for generation | | `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) | **Important Notes:** @@ -255,7 +256,8 @@ curl -X POST http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \ "temperature": 0.7, "context_limit": 128, "context_strategy": "balanced", - "api_key": "sk-xxx" + "api_key": "sk-xxx", + "copilot_mode": null }, "user_id": "uuid-user", "group_id": null, @@ -308,7 +310,8 @@ curl -X GET http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \ "temperature": 0.7, "context_limit": 128, "context_strategy": "balanced", - "api_key": "sk-xxx" + "api_key": "sk-xxx", + "copilot_mode": "lab_assistant" }, "user_id": "uuid-user", "group_id": null, @@ -330,7 +333,8 @@ curl -X GET http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \ "temperature": 0.7, "context_limit": 200, "context_strategy": "balanced", - "api_key": null + "api_key": null, + "copilot_mode": null }, "user_id": null, "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, "context_limit": 128, "context_strategy": "balanced", - "api_key": "sk-xxx" + "api_key": "sk-xxx", + "copilot_mode": "lab_assistant" }, "user_id": "uuid-user", "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", "model": "claude-3-opus-20240229", "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, "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", "model": "gpt-4", "temperature": 0.7, - "api_key": "sk-xxx" + "context_limit": 128, + "context_strategy": "balanced", + "api_key": "sk-xxx", + "copilot_mode": null }, "user_id": null, "group_id": "uuid-group", diff --git a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py index f8b2d4b83..b9e66408e 100644 --- a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py +++ b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py @@ -65,6 +65,7 @@ from pathlib import Path # Add backend to path for prompt_manager sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "backend")) from gns3server.agent.gns3_copilot.tools_v2 import ( + ExecuteMultipleDeviceConfigCommands, ExecuteMultipleDeviceCommands, GNS3CreateNodeTool, GNS3LinkTool, @@ -88,6 +89,7 @@ tools = [ GNS3StartNodeTool(), # Start GNS3 nodes GNS3UpdateNodeNameTool(), # Update node name 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 ] # Augment the LLM with tools @@ -197,7 +199,8 @@ def llm_call(state: dict, config: RunnableConfig | None = None): state["topology_info"] = topology_info # 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( system_prompt=system_prompt, get_topology_func=lambda s: s.get("topology_info"), diff --git a/gns3server/agent/gns3_copilot/gns3_client/custom_gns3fy.py b/gns3server/agent/gns3_copilot/gns3_client/custom_gns3fy.py index 0dd347a69..46fb50498 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/custom_gns3fy.py +++ b/gns3server/agent/gns3_copilot/gns3_client/custom_gns3fy.py @@ -477,6 +477,11 @@ class Gns3Connector: **Required Attributes:** - `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 _template = self.get_template(name=name, template_id=template_id) @@ -505,6 +510,21 @@ class Gns3Connector: - `name` - `compute_id` by default is 'local' - `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 template_name = kwargs.get("name") @@ -1051,6 +1071,7 @@ class Node: z: int | None = None template_id: str | None = None properties: Any | None = None + tags: list[str] | None = None template: str | None = None links: list[Link] = field(default_factory=list, repr=False) @@ -2084,6 +2105,7 @@ class Project: # "template": _n.template, "x": _n.x, "y": _n.y, + "tags": _n.tags if _n.tags else [], } } ) diff --git a/gns3server/agent/gns3_copilot/prompts/__init__.py b/gns3server/agent/gns3_copilot/prompts/__init__.py index 6aa0e2a23..d11aee333 100644 --- a/gns3server/agent/gns3_copilot/prompts/__init__.py +++ b/gns3server/agent/gns3_copilot/prompts/__init__.py @@ -29,9 +29,14 @@ Prompts Module for GNS3-Copilot This package contains system prompts and prompt loading utilities for 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 .lab_assistant_prompt import LAB_ASSISTANT_PROMPT from .prompt_loader import load_system_prompt from .title_prompt import TITLE_PROMPT @@ -49,6 +54,7 @@ __url__ = "https://github.com/yueguobin/gns3-copilot" __all__ = [ "SYSTEM_PROMPT", + "LAB_ASSISTANT_PROMPT", "TITLE_PROMPT", "load_system_prompt", ] diff --git a/gns3server/agent/gns3_copilot/prompts/lab_assistant_prompt.py b/gns3server/agent/gns3_copilot/prompts/lab_assistant_prompt.py new file mode 100644 index 000000000..2218e9ec6 --- /dev/null +++ b/gns3server/agent/gns3_copilot/prompts/lab_assistant_prompt.py @@ -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 . +# +# 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. + +--- +""" diff --git a/gns3server/agent/gns3_copilot/prompts/prompt_loader.py b/gns3server/agent/gns3_copilot/prompts/prompt_loader.py index ed9397987..955eb3323 100644 --- a/gns3server/agent/gns3_copilot/prompts/prompt_loader.py +++ b/gns3server/agent/gns3_copilot/prompts/prompt_loader.py @@ -27,29 +27,45 @@ Prompt Loader for GNS3-Copilot This module provides utilities for loading system prompts. -Can be extended to support multiple prompt variants based on -environment variables (e.g., ENGLISH_LEVEL). +Supports multiple prompt variants based on LLM model configuration. + +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 os from .base_prompt import SYSTEM_PROMPT +from .lab_assistant_prompt import LAB_ASSISTANT_PROMPT 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. - In the future, this can be extended to support multiple prompt variants - based on environment variables (e.g., ENGLISH_LEVEL). + The prompt mode is controlled by the `copilot_mode` field in the LLM model config: + - "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: 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 + if not llm_config: + logger.info("No LLM config provided, using default TEACHING assistant prompt mode") + 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 diff --git a/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py b/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py index 11f410c85..aa0772a1c 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py +++ b/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py @@ -53,25 +53,52 @@ def _get_nornir_defaults() -> dict[str, Any]: """Get Nornir default configuration for Cisco IOS.""" return {"data": {"location": "gns3"}} -def _get_nornir_groups_config() -> dict[str, dict[str, Any]]: - """Get Nornir groups configuration for Cisco IOS Telnet devices.""" +def _get_nornir_groups_config( + 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 { - "cisco_IOSv_telnet": { - "platform": "cisco_ios", - "hostname": get_gns3_server_host(), - "timeout": 120, - "username": "", - "password": "", - "connection_options": { - "netmiko": {"extras": {"device_type": "cisco_ios_telnet"}} - }, + "platform": platform, + "hostname": get_gns3_server_host(), + "timeout": 120, + "username": "", + "password": "", + "connection_options": { + "netmiko": {"extras": {"device_type": device_type}} }, } -def _get_nornir_group(group_name: str = "cisco_IOSv_telnet") -> dict[str, Any]: - """Get Nornir group configuration for a specific group.""" - all_groups = _get_nornir_groups_config() - return all_groups.get(group_name, {}) +def _get_nornir_group( + group_name: str = "cisco_IOSv_telnet", + device_type: str = "cisco_ios_telnet", + 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): """ @@ -389,8 +416,27 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): def _initialize_nornir(self, hosts_data: dict[str, dict[str, Any]]) -> Nornir: """Initialize Nornir with the provided hosts data.""" try: - # Get latest environment configuration - groups_data = _get_nornir_group("cisco_IOSv_telnet") + # Extract device_type and platform from hosts_data + # 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() # Log nornir account information diff --git a/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py b/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py index baa4dbd83..6d36d6dc7 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py +++ b/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py @@ -54,25 +54,52 @@ def _get_nornir_defaults() -> dict[str, Any]: """Get Nornir default configuration for Cisco IOS.""" return {"data": {"location": "gns3"}} -def _get_nornir_groups_config() -> dict[str, dict[str, Any]]: - """Get Nornir groups configuration for Cisco IOS Telnet devices.""" +def _get_nornir_groups_config( + 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 { - "cisco_IOSv_telnet": { - "platform": "cisco_ios", - "hostname": get_gns3_server_host(), - "timeout": 120, - "username": "", - "password": "", - "connection_options": { - "netmiko": {"extras": {"device_type": "cisco_ios_telnet"}} - }, + "platform": platform, + "hostname": get_gns3_server_host(), + "timeout": 120, + "username": "", + "password": "", + "connection_options": { + "netmiko": {"extras": {"device_type": device_type}} }, } -def _get_nornir_group(group_name: str = "cisco_IOSv_telnet") -> dict[str, Any]: - """Get Nornir group configuration for a specific group.""" - all_groups = _get_nornir_groups_config() - return all_groups.get(group_name, {}) +def _get_nornir_group( + group_name: str = "cisco_IOSv_telnet", + device_type: str = "cisco_ios_telnet", + 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): """ @@ -403,8 +430,27 @@ class ExecuteMultipleDeviceCommands(BaseTool): def _initialize_nornir(self, hosts_data: dict[str, dict[str, Any]]) -> Nornir: """Initialize Nornir with the provided hosts data.""" try: - # Get latest environment configuration - groups_data = _get_nornir_group("cisco_IOSv_telnet") + # Extract device_type and platform from hosts_data + # 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() # Log nornir account information diff --git a/gns3server/agent/gns3_copilot/utils/get_gns3_device_port.py b/gns3server/agent/gns3_copilot/utils/get_gns3_device_port.py index 713d14625..ec56908e4 100644 --- a/gns3server/agent/gns3_copilot/utils/get_gns3_device_port.py +++ b/gns3server/agent/gns3_copilot/utils/get_gns3_device_port.py @@ -48,7 +48,9 @@ def get_device_ports_from_topology( { "device_name": { "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 @@ -82,10 +84,36 @@ def get_device_ports_from_topology( logger.warning("Device '%s' missing console_port", device_name) 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 hosts_data[device_name] = { "port": node_info["console_port"], "groups": ["cisco_IOSv_telnet"], + "device_type": device_type, + "platform": platform, } logger.info("Returning %d device port mappings", len(hosts_data)) diff --git a/gns3server/schemas/controller/llm_model_configs.py b/gns3server/schemas/controller/llm_model_configs.py index 7e4961e8b..7fe992928 100644 --- a/gns3server/schemas/controller/llm_model_configs.py +++ b/gns3server/schemas/controller/llm_model_configs.py @@ -48,9 +48,14 @@ class LLMModelConfigData(BaseModel): context_strategy: Literal["conservative", "balanced", "aggressive"] = Field( "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 - 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 @@ -71,6 +76,10 @@ class LLMModelConfigCreate(BaseModel): context_strategy: Literal["conservative", "balanced", "aggressive"] = Field( "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 model_config = ConfigDict(extra="allow") @@ -96,6 +105,10 @@ class LLMModelConfigUpdate(BaseModel): context_strategy: Optional[Literal["conservative", "balanced", "aggressive"]] = Field( 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 model_config = ConfigDict(extra="allow")