diff --git a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py index f0aaf2fd2..ae4a3dc4d 100644 --- a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py +++ b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py @@ -63,23 +63,12 @@ from gns3_copilot.tools_v2 import ( LinuxTelnetBatchTool, VPCSMultiCommands, ) -from gns3_copilot.utils import get_config # Set up logger for FlowNet-Lab logger = logging.getLogger(__name__) -# Log loaded LLM model information -model_name = get_config("MODEL_NAME") -model_provider = get_config("MODE_PROVIDER") -base_url = get_config("BASE_URL", "") -temperature = get_config("TEMPERATURE", "0") -logger.info( - "LLM model configuration: name=%s, provider=%s, base_url=%s, temperature=%s", - model_name, - model_provider, - base_url, - temperature, -) +# Note: LLM model configuration is now managed by the new llm_model_configs system. +# The model_factory module handles model creation with configuration from the database. # Define the available tools for the agent tools = [ diff --git a/gns3server/agent/gns3_copilot/agent/model_factory.py b/gns3server/agent/gns3_copilot/agent/model_factory.py index 1034b78af..2b8c2e91f 100644 --- a/gns3server/agent/gns3_copilot/agent/model_factory.py +++ b/gns3server/agent/gns3_copilot/agent/model_factory.py @@ -1,35 +1,50 @@ """ Model Factory for FlowNet-Lab Agent -This module provides factory functions to create fresh LLM model instances -on-demand from SQLite configuration. This allows configuration changes -to take effect without restarting the application. +This module provides factory functions to create fresh LLM model instances. +Configuration can be loaded from: +1. Passed llm_config dictionary (from new llm_model_configs system) +2. Environment variables (fallback for backward compatibility) """ import logging -from typing import Any +import os +from typing import Any, Optional from langchain.chat_models import init_chat_model -from gns3_copilot.utils import get_config - logger = logging.getLogger(__name__) -def _load_env_variables() -> dict[str, str]: +def _load_llm_config(llm_config: Optional[dict[str, Any]] = None) -> dict[str, str]: """ - Load model configuration from SQLite database. + Load model configuration from llm_config dict or environment variables. + + Args: + llm_config: Optional configuration dictionary from llm_model_configs system. + If not provided, will load from environment variables. Returns: Dictionary containing model configuration. """ - return { - "model_name": get_config("MODEL_NAME", ""), - "model_provider": get_config("MODE_PROVIDER", ""), - "api_key": get_config("MODEL_API_KEY", ""), - "base_url": get_config("BASE_URL", ""), - "temperature": get_config("TEMPERATURE", "0"), - } + if llm_config: + # Use provided configuration (from llm_model_configs system) + return { + "model_name": llm_config.get("model", ""), + "model_provider": llm_config.get("provider", ""), + "api_key": llm_config.get("api_key", ""), + "base_url": llm_config.get("base_url", ""), + "temperature": str(llm_config.get("temperature", "0")), + } + else: + # Fallback to environment variables + return { + "model_name": os.getenv("MODEL_NAME", ""), + "model_provider": os.getenv("MODE_PROVIDER", ""), + "api_key": os.getenv("MODEL_API_KEY", ""), + "base_url": os.getenv("BASE_URL", ""), + "temperature": os.getenv("TEMPERATURE", "0"), + } def create_base_model() -> Any: @@ -46,7 +61,7 @@ def create_base_model() -> Any: Raises: ValueError: If required environment variables are missing or invalid. """ - env_vars = _load_env_variables() + env_vars = _load_llm_config() # Log the loaded configuration (mask sensitive data) logger.info( @@ -98,7 +113,7 @@ def create_title_model() -> Any: Raises: ValueError: If required environment variables are missing or invalid. """ - env_vars = _load_env_variables() + env_vars = _load_llm_config() logger.info( "Creating title model: name=%s, provider=%s, base_url=%s, temperature=1.0", @@ -174,7 +189,7 @@ def create_note_organizer_model() -> Any: Raises: ValueError: If required environment variables are missing or invalid. """ - env_vars = _load_env_variables() + env_vars = _load_llm_config() logger.info( "Creating note organizer model: name=%s, provider=%s, base_url=%s, temperature=0.3", @@ -245,7 +260,7 @@ def create_window_agent_base_model() -> Any: ValueError: If required environment variables are missing. RuntimeError: If model creation fails. """ - env_vars = _load_env_variables() + env_vars = _load_llm_config() logger.info( "Creating Window Agent base model: name=%s, provider=%s, base_url=%s, temperature=0", @@ -298,7 +313,7 @@ def create_window_agent_model_with_tools(tools: list[Any]) -> Any: ValueError: If required environment variables are missing. RuntimeError: If model creation or tool binding fails. """ - env_vars = _load_env_variables() + env_vars = _load_llm_config() logger.info( "Creating Window Agent model: name=%s, provider=%s, base_url=%s, temperature=0", @@ -351,7 +366,7 @@ def create_experiment_planner_model() -> Any: Raises: ValueError: If required environment variables are missing or invalid. """ - env_vars = _load_env_variables() + env_vars = _load_llm_config() logger.info( "Creating experiment planner model: name=%s, provider=%s, base_url=%s, temperature=0.7", @@ -428,7 +443,7 @@ def create_presentation_eval_model() -> Any: ValueError: If required environment variables are missing. RuntimeError: If model creation fails. """ - env_vars = _load_env_variables() + env_vars = _load_llm_config() logger.info( "Creating presentation evaluator model: name=%s, provider=%s, base_url=%s, temperature=0.5", diff --git a/gns3server/agent/gns3_copilot/agent/qwen_vision_model.py b/gns3server/agent/gns3_copilot/agent/qwen_vision_model.py index 5372964ad..dba216742 100644 --- a/gns3server/agent/gns3_copilot/agent/qwen_vision_model.py +++ b/gns3server/agent/gns3_copilot/agent/qwen_vision_model.py @@ -7,6 +7,7 @@ through DashScope SDK to identify network topology diagrams from images. import base64 import json +import os from typing import Any, Dict, Optional from pathlib import Path @@ -17,8 +18,6 @@ except ImportError: import logging -from gns3_copilot.utils import get_config - logger = logging.getLogger(__name__) @@ -161,13 +160,13 @@ class QwenVisionModel: "Please install it: pip install dashscope" ) - # Load API key from config if not provided + # Load API key from environment variable if not provided if api_key is None: - api_key = get_config("QWEN_API_KEY", "") + api_key = os.getenv("QWEN_API_KEY", "") if not api_key: raise ValueError( - "Qwen API key is required. Please set QWEN_API_KEY in configuration " + "Qwen API key is required. Please set QWEN_API_KEY environment variable " "or pass it to the constructor." ) @@ -389,19 +388,19 @@ def create_qwen_vision_model( """ Factory function to create a Qwen-VL vision model instance. - This function loads configuration from the database and creates + This function loads configuration from environment variables and creates a QwenVisionModel instance with appropriate settings. Args: - api_key: DashScope API key (if None, will load from config) - model_name: Model name to use (if None, will load from config, default: qwen3-vl-plus) + api_key: DashScope API key (if None, will load from QWEN_API_KEY env var) + model_name: Model name to use (if None, will load from QWEN_MODEL_NAME env var, default: qwen3-vl-plus) Returns: QwenVisionModel instance Raises: ImportError: If dashscope is not installed - ValueError: If API key is not provided or found in config + ValueError: If API key is not provided or found in environment Example: >>> model = create_qwen_vision_model() @@ -409,8 +408,8 @@ def create_qwen_vision_model( >>> print(topology["topology_name"]) >>> print(f"Found {len(topology['devices'])} devices") """ - # Load model name from config if not provided + # Load model name from environment variable if not provided if model_name is None: - model_name = get_config("QWEN_MODEL_NAME", "qwen3-vl-plus") + model_name = os.getenv("QWEN_MODEL_NAME", "qwen3-vl-plus") return QwenVisionModel(api_key=api_key, model_name=model_name) diff --git a/gns3server/agent/gns3_copilot/prompts/prompt_loader.py b/gns3server/agent/gns3_copilot/prompts/prompt_loader.py index 0879212cf..22063a2ef 100644 --- a/gns3server/agent/gns3_copilot/prompts/prompt_loader.py +++ b/gns3server/agent/gns3_copilot/prompts/prompt_loader.py @@ -125,11 +125,11 @@ def load_system_prompt(level: str | None = None) -> str: Load system prompt based on English proficiency level. This function loads the appropriate system prompt based on the ENGLISH_LEVEL - configuration from SQLite database or the provided level parameter. + environment variable or the provided level parameter. Args: level (str, optional): English proficiency level (A1, A2, B1, B2, C1, C2). - If not provided, will read from ENGLISH_LEVEL config in database. + If not provided, will read from ENGLISH_LEVEL environment variable. Returns: str: The system prompt content for the specified English level. @@ -140,15 +140,9 @@ def load_system_prompt(level: str | None = None) -> str: """ # Determine the English level to use if not level: - # Try to get ENGLISH_LEVEL from database config first - try: - from gns3_copilot.utils import get_config - level = get_config("ENGLISH_LEVEL", "") - logger.debug("Retrieved ENGLISH_LEVEL from database: '%s'", level) - except Exception as e: - # Fallback to environment variable if get_config fails - logger.debug("Failed to get ENGLISH_LEVEL from config: %s, using env var", e) - level = os.getenv("ENGLISH_LEVEL", "") + # Get ENGLISH_LEVEL from environment variable + level = os.getenv("ENGLISH_LEVEL", "") + logger.debug("Retrieved ENGLISH_LEVEL from environment: '%s'", level) # Return regular level prompt return _load_regular_level_prompt(level) 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 cfd272a02..52ae7b906 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py +++ b/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py @@ -5,6 +5,7 @@ This module provides a tool to execute configuration commands on multiple device import json import logging +import os from typing import Any from langchain.tools import BaseTool @@ -15,10 +16,7 @@ from nornir.core import Nornir from nornir.core.task import AggregatedResult, Result, Task from nornir_netmiko.tasks import netmiko_send_config -from gns3_copilot.utils import ( - get_config, - get_device_ports_from_topology, -) +from gns3_copilot.utils import get_device_ports_from_topology # config log logger = logging.getLogger(__name__) @@ -35,10 +33,10 @@ def _get_nornir_groups_config() -> dict[str, dict[str, Any]]: return { "cisco_IOSv_telnet": { "platform": "cisco_ios", - "hostname": get_config("GNS3_SERVER_HOST"), + "hostname": os.getenv("GNS3_SERVER_HOST", "127.0.0.1"), "timeout": 120, - "username": get_config("GNS3_SERVER_USERNAME"), - "password": get_config("GNS3_SERVER_PASSWORD"), + "username": os.getenv("GNS3_SERVER_USERNAME", ""), + "password": os.getenv("GNS3_SERVER_PASSWORD", ""), "connection_options": { "netmiko": {"extras": {"device_type": "cisco_ios_telnet"}} }, @@ -373,7 +371,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): defaults = _get_nornir_defaults() # Log nornir account information - gns3_host = get_config("GNS3_SERVER_HOST") + gns3_host = os.getenv("GNS3_SERVER_HOST", "127.0.0.1") logger.info( "Initializing Nornir with account: host=%s, platform=%s, timeout=%d", 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 b085ed57f..60a12078a 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py +++ b/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py @@ -5,6 +5,7 @@ This module provides a tool to execute display commands on multiple devices import json import logging +import os import re from typing import Any @@ -16,10 +17,7 @@ from nornir.core import Nornir from nornir.core.task import AggregatedResult, Result, Task from nornir_netmiko.tasks import netmiko_multiline -from gns3_copilot.utils import ( - get_config, - get_device_ports_from_topology, -) +from gns3_copilot.utils import get_device_ports_from_topology # config log logger = logging.getLogger(__name__) @@ -36,10 +34,10 @@ def _get_nornir_groups_config() -> dict[str, dict[str, Any]]: return { "cisco_IOSv_telnet": { "platform": "cisco_ios", - "hostname": get_config("GNS3_SERVER_HOST"), + "hostname": os.getenv("GNS3_SERVER_HOST", "127.0.0.1"), "timeout": 120, - "username": get_config("GNS3_SERVER_USERNAME"), - "password": get_config("GNS3_SERVER_PASSWORD"), + "username": os.getenv("GNS3_SERVER_USERNAME", ""), + "password": os.getenv("GNS3_SERVER_PASSWORD", ""), "connection_options": { "netmiko": {"extras": {"device_type": "cisco_ios_telnet"}} }, @@ -387,7 +385,7 @@ class ExecuteMultipleDeviceCommands(BaseTool): defaults = _get_nornir_defaults() # Log nornir account information - gns3_host = get_config("GNS3_SERVER_HOST") + gns3_host = os.getenv("GNS3_SERVER_HOST", "127.0.0.1") logger.info( "Initializing Nornir with account: host=%s, platform=%s, timeout=%d", diff --git a/gns3server/agent/gns3_copilot/tools_v2/linux_tools_nornir.py b/gns3server/agent/gns3_copilot/tools_v2/linux_tools_nornir.py index 5e6212781..3743cc048 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/linux_tools_nornir.py +++ b/gns3server/agent/gns3_copilot/tools_v2/linux_tools_nornir.py @@ -5,6 +5,7 @@ via Telnet console. import json import logging +import os import re import time from typing import Any @@ -16,10 +17,7 @@ from nornir.core import Nornir from nornir.core.task import AggregatedResult, Result, Task from nornir_netmiko.tasks import netmiko_send_command -from gns3_copilot.utils import ( - get_config, - get_device_ports_from_topology, -) +from gns3_copilot.utils import get_device_ports_from_topology # config log logger = logging.getLogger(__name__) @@ -36,10 +34,10 @@ def _get_nornir_groups_config() -> dict[str, dict[str, Any]]: return { "linux_telnet": { "platform": "linux", - "hostname": get_config("GNS3_SERVER_HOST"), + "hostname": os.getenv("GNS3_SERVER_HOST", "127.0.0.1"), "timeout": 120, - "username": get_config("LINUX_TELNET_USERNAME"), - "password": get_config("LINUX_TELNET_PASSWORD"), + "username": os.getenv("LINUX_TELNET_USERNAME", ""), + "password": os.getenv("LINUX_TELNET_PASSWORD", ""), "connection_options": { "netmiko": { "platform": "linux", @@ -172,8 +170,8 @@ class LinuxTelnetBatchTool(BaseTool): return device_configs_list # Check credentials only for valid inputs - linux_username = get_config("LINUX_TELNET_USERNAME") - linux_password = get_config("LINUX_TELNET_PASSWORD") + linux_username = os.getenv("LINUX_TELNET_USERNAME", "") + linux_password = os.getenv("LINUX_TELNET_PASSWORD", "") if not linux_username or not linux_password: user_message = ( diff --git a/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_telnetlib3.py b/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_telnetlib3.py index e2aed19a5..7002d5449 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_telnetlib3.py +++ b/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_telnetlib3.py @@ -5,6 +5,7 @@ Supports concurrent execution of multiple command groups across multiple VPCS de import json import logging +import os import re import threading from time import sleep @@ -14,7 +15,7 @@ from langchain.tools import BaseTool from langchain_core.callbacks import CallbackManagerForToolRun from telnetlib3 import Telnet -from gns3_copilot.utils import get_config, get_device_ports_from_topology +from gns3_copilot.utils import get_device_ports_from_topology logger = logging.getLogger(__name__) @@ -357,8 +358,8 @@ class VPCSMultiCommands(BaseTool): list(device_ports.keys()), ) - # Get host IP from database - gns3_host = get_config("GNS3_SERVER_HOST", "127.0.0.1") + # Get host IP from environment variable + gns3_host = os.getenv("GNS3_SERVER_HOST", "127.0.0.1") logger.info("Using GNS3 server host: %s", gns3_host) # Initialize results list (pre-allocate space for concurrent writes) diff --git a/gns3server/agent/gns3_copilot/utils/__init__.py b/gns3server/agent/gns3_copilot/utils/__init__.py index 84f6acbb9..6c4da8b89 100644 --- a/gns3server/agent/gns3_copilot/utils/__init__.py +++ b/gns3server/agent/gns3_copilot/utils/__init__.py @@ -12,14 +12,6 @@ Author: Guobin Yue """ # Import main utility functions -from .app_config import ( - DEFAULT_CONFIG, - get_all_config, - get_config, - init_config, - reset_config, - set_config, -) from .get_gns3_device_port import get_device_ports_from_topology from .parse_tool_content import format_tool_response, parse_tool_content @@ -38,12 +30,6 @@ __url__ = "https://github.com/yueguobin/gns3-copilot" # Export main utility functions __all__ = [ - "DEFAULT_CONFIG", - "get_config", - "set_config", - "get_all_config", - "init_config", - "reset_config", "get_device_ports_from_topology", "parse_tool_content", "format_tool_response", diff --git a/gns3server/agent/gns3_copilot/utils/app_config.py b/gns3server/agent/gns3_copilot/utils/app_config.py deleted file mode 100644 index 949fafda5..000000000 --- a/gns3server/agent/gns3_copilot/utils/app_config.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -Application Configuration Manager for FlowNet-Lab. - -This module provides a unified interface for managing application configuration -stored in SQLite database. It includes default values for all configuration -items and helper functions for reading and saving configuration. - -Functions: - get_config(key, default=None): Retrieve a configuration value with default - set_config(key, value): Save a configuration value to database - get_all_config(): Retrieve all configuration values - init_config(): Initialize database with default values - -Constants: - DEFAULT_CONFIG: Dictionary containing all configuration keys and their defaults -""" - -import logging -from typing import Any - -from gns3_copilot.utils.config_db import ( - clear_all, - get_all_values, - get_value, - init_db, - set_value, -) - -logger = logging.getLogger(__name__) - -# Default configuration values for all application settings -DEFAULT_CONFIG: dict[str, str] = { - # GNS3 Server Configuration - #"GNS3_SERVER_HOST": "", - #"GNS3_SERVER_URL": "http://127.0.0.1:3080/", - #"API_VERSION": "3", - #"GNS3_SERVER_USERNAME": "", - #"GNS3_SERVER_PASSWORD": "", - #"GNS3_SERVER_JWT_TOKEN": "", - # Model Configuration - "MODE_PROVIDER": "openai", - "MODEL_NAME": "gpt-4", - "MODEL_API_KEY": "", - "BASE_URL": "", - "TEMPERATURE": "0.0", - # Voice Configuration - #"VOICE": "False", - # Linux Telnet Configuration - "LINUX_TELNET_USERNAME": "", - "LINUX_TELNET_PASSWORD": "", - # Prompt Configuration - #"ENGLISH_LEVEL": "Normal Prompt", - # Reading Page Configuration - #"CALIBRE_SERVER_URL": "", - #"READING_NOTES_DIR": "notes", - # UI Configuration - #"CONTAINER_HEIGHT": "1200", - #"ZOOM_SCALE_TOPOLOGY": "0.8", - # Other Settings - #"LANGUAGE": "zh", - # DashScope API Configuration - #"DASHSCOPE_API_KEY": "", - # DashScope STT (Speech-to-Text) Configuration - #"DASHSCOPE_STT_MODEL": "fun-asr-realtime", - #"DASHSCOPE_STT_FORMAT": "wav", - #"DASHSCOPE_STT_SAMPLE_RATE": "16000", - #"DASHSCOPE_STT_LANGUAGE_HINTS": "zh,en", - # DashScope TTS (Text-to-Speech) Configuration - #"DASHSCOPE_TTS_MODEL": "cosyvoice-v3-flash", - #"DASHSCOPE_TTS_VOICE": "longanyang", - #"DASHSCOPE_TTS_FORMAT": "mp3", - #"DASHSCOPE_TTS_VOLUME": "50", - #"DASHSCOPE_TTS_SPEECH_RATE": "1.0", - #"DASHSCOPE_TTS_PITCH_RATE": "1.0", -} - - -def _get_default(key: str) -> str | None: - """Get the default value for a configuration key. - - Args: - key: Configuration key - - Returns: - Default value if key exists, None otherwise - """ - return DEFAULT_CONFIG.get(key) - - -def get_config(key: str, default: str | None = None) -> str: - """Retrieve a configuration value from the database. - - If the key doesn't exist in the database, it will use the default value - from DEFAULT_CONFIG. If the key is not in DEFAULT_CONFIG either, it will - use the provided default parameter. - - Args: - key: The configuration key to retrieve - default: Fallback default value if key not in DEFAULT_CONFIG - - Returns: - The configuration value as a string - - Example: - >>> get_config("GNS3_SERVER_URL") - 'http://127.0.0.1:3080/' - - >>> get_config("CUSTOM_KEY", "default_value") - 'default_value' - """ - # Get default value from DEFAULT_CONFIG or use provided default - default_value = default if default is not None else _get_default(key) - - # Retrieve value from database - value = get_value(key, default_value) - - if value is None: - return default_value if default_value is not None else "" - - # Ensure value is a string - return str(value) if value else default_value if default_value else "" - - -def set_config(key: str, value: str) -> bool: - """Save a configuration value to the database. - - Args: - key: The configuration key to save - value: The configuration value to store - - Returns: - True if save was successful, False otherwise - - Example: - >>> set_config("GNS3_SERVER_URL", "http://192.168.1.100:3080") - True - """ - return set_value(key, value) - - -def get_all_config() -> dict[str, str]: - """Retrieve all configuration values from the database. - - Returns: - Dictionary with all configuration key-value pairs - - Example: - >>> config = get_all_config() - >>> print(config["GNS3_SERVER_URL"]) - 'http://127.0.0.1:3080/' - """ - return get_all_values() - - -def init_config() -> None: - """Initialize the configuration database with default values. - - This function initializes the database and populates it with default - values for all configuration keys. If a key already exists in the - database, its value will not be overwritten. - - This should be called once at application startup. - - Example: - >>> init_config() - # Database is now initialized with default values - """ - try: - # Initialize database and create tables - init_db() - - # Set default values for all keys (only if not already set) - for key, default_value in DEFAULT_CONFIG.items(): - existing_value = get_value(key) - if existing_value is None: - set_value(key, default_value) - - logger.info( - "Configuration database initialized with %d keys", len(DEFAULT_CONFIG) - ) - except Exception as e: - logger.error("Failed to initialize configuration: %s", e) - raise - - -def reset_config() -> None: - """Reset all configuration to default values. - - This function clears all existing configuration values and - restores them to defaults defined in DEFAULT_CONFIG. - - Example: - >>> reset_config() - # All configuration values are now set to defaults - """ - try: - # Clear all existing values - clear_all() - - # Re-initialize with defaults - init_config() - - logger.info("Configuration reset to defaults") - except Exception as e: - logger.error("Failed to reset configuration: %s", e) - raise