feat(agent): refactor LLM configuration to support new llm_model_configs system

- Remove direct logging of LLM config from gns3_copilot.py
- Update model_factory to accept configuration from llm_model_configs dictionary
- Add fallback to environment variables for backward compatibility
- Centralize configuration loading in _load_llm_config function
This commit is contained in:
YueGuobin 2026-03-03 23:26:20 +08:00
parent 3c54a7d90a
commit 4857cff59c
10 changed files with 77 additions and 305 deletions

View File

@ -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 = [

View File

@ -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",

View File

@ -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)

View File

@ -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)

View File

@ -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",

View File

@ -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",

View File

@ -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 = (

View File

@ -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)

View File

@ -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",

View File

@ -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