feat(agent): enhance LLM config loading with priority system

Refactor model factory to support three configuration sources in priority order:
1. Direct llm_config dictionary parameter
2. Fetch from llm_model_configs system via connector_factory (requires user_id and jwt_token)
3. Environment variables as fallback for backward compatibility

This improves flexibility by allowing runtime configuration while maintaining compatibility with existing environment-based setups.
This commit is contained in:
YueGuobin 2026-03-04 00:47:35 +08:00
parent 15dc1e7ea0
commit db9db56e7a
3 changed files with 162 additions and 313 deletions

View File

@ -2,33 +2,45 @@
Model Factory for FlowNet-Lab Agent Model Factory for FlowNet-Lab Agent
This module provides factory functions to create fresh LLM model instances. This module provides factory functions to create fresh LLM model instances.
Configuration can be loaded from: Configuration is loaded from the llm_model_configs system via connector_factory.
1. Passed llm_config dictionary (from new llm_model_configs system)
2. Environment variables (fallback for backward compatibility)
""" """
import logging import logging
import os import os
from typing import Any, Optional from typing import Any, Optional
from uuid import UUID
from langchain.chat_models import init_chat_model from langchain.chat_models import init_chat_model
from gns3_copilot.gns3_client import get_llm_config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _load_llm_config(llm_config: Optional[dict[str, Any]] = None) -> dict[str, str]: def _load_llm_config(
user_id: Optional[UUID] = None,
jwt_token: Optional[str] = None,
llm_config: Optional[dict[str, Any]] = None,
) -> dict[str, str]:
""" """
Load model configuration from llm_config dict or environment variables. Load model configuration from llm_config dict or fetch from llm_model_configs system.
Priority order:
1. Provided llm_config dictionary (highest priority)
2. Fetch from llm_model_configs system via connector_factory (requires user_id and jwt_token)
3. Environment variables (fallback for backward compatibility)
Args: Args:
llm_config: Optional configuration dictionary from llm_model_configs system. user_id: User UUID for fetching config from database
If not provided, will load from environment variables. jwt_token: JWT token for API authentication
llm_config: Optional configuration dictionary to use directly
Returns: Returns:
Dictionary containing model configuration. Dictionary containing model configuration.
""" """
# Priority 1: Use provided llm_config dictionary
if llm_config: if llm_config:
# Use provided configuration (from llm_model_configs system) logger.info("Using provided llm_config dictionary")
return { return {
"model_name": llm_config.get("model", ""), "model_name": llm_config.get("model", ""),
"model_provider": llm_config.get("provider", ""), "model_provider": llm_config.get("provider", ""),
@ -36,32 +48,66 @@ def _load_llm_config(llm_config: Optional[dict[str, Any]] = None) -> dict[str, s
"base_url": llm_config.get("base_url", ""), "base_url": llm_config.get("base_url", ""),
"temperature": str(llm_config.get("temperature", "0")), "temperature": str(llm_config.get("temperature", "0")),
} }
else:
# Fallback to environment variables # Priority 2: Fetch from llm_model_configs system via connector_factory
return { if user_id and jwt_token:
"model_name": os.getenv("MODEL_NAME", ""), logger.info(f"Fetching LLM config from database for user {user_id}")
"model_provider": os.getenv("MODE_PROVIDER", ""), config = get_llm_config(user_id=user_id, jwt_token=jwt_token)
"api_key": os.getenv("MODEL_API_KEY", ""),
"base_url": os.getenv("BASE_URL", ""), if config:
"temperature": os.getenv("TEMPERATURE", "0"), logger.info(
} f"Successfully loaded LLM config from database: "
f"provider={config.get('provider')}, model={config.get('model')}"
)
return {
"model_name": config.get("model", ""),
"model_provider": config.get("provider", ""),
"api_key": config.get("api_key", ""),
"base_url": config.get("base_url", ""),
"temperature": str(config.get("temperature", "0")),
}
else:
logger.warning(
f"No LLM config found in database for user {user_id}, falling back to environment variables"
)
# Priority 3: Fallback to environment variables
logger.info("Loading LLM config from environment variables (fallback)")
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: def create_base_model(
user_id: Optional[UUID] = None,
jwt_token: Optional[str] = None,
llm_config: Optional[dict[str, Any]] = None,
) -> Any:
""" """
Create a fresh base LLM model instance from current environment variables. Create a fresh base LLM model instance.
This function reads environment variables fresh every time it's called, Configuration priority:
allowing configuration changes to take effect immediately. 1. llm_config dictionary (if provided)
2. Fetch from llm_model_configs system via connector_factory (if user_id and jwt_token provided)
3. Environment variables (fallback)
Args:
user_id: User UUID for fetching config from database
jwt_token: JWT token for API authentication
llm_config: Optional configuration dictionary to use directly
Returns: Returns:
Any: A new LLM model instance configured with current env vars. Any: A new LLM model instance configured with current settings.
The actual type depends on the provider (e.g., ChatOpenAI, etc.). The actual type depends on the provider (e.g., ChatOpenAI, etc.).
Raises: Raises:
ValueError: If required environment variables are missing or invalid. ValueError: If required configuration fields are missing or invalid.
""" """
env_vars = _load_llm_config() env_vars = _load_llm_config(user_id, jwt_token, llm_config)
# Log the loaded configuration (mask sensitive data) # Log the loaded configuration (mask sensitive data)
logger.info( logger.info(
@ -98,7 +144,11 @@ def create_base_model() -> Any:
raise RuntimeError(f"Failed to create base model: {e}") from e raise RuntimeError(f"Failed to create base model: {e}") from e
def create_title_model() -> Any: def create_title_model(
user_id: Optional[UUID] = None,
jwt_token: Optional[str] = None,
llm_config: Optional[dict[str, Any]] = None,
) -> Any:
""" """
Create a fresh title generation model instance. Create a fresh title generation model instance.
@ -106,14 +156,24 @@ def create_title_model() -> Any:
It uses the same configuration as the base model but with a higher temperature It uses the same configuration as the base model but with a higher temperature
for more creative output. for more creative output.
Configuration priority:
1. llm_config dictionary (if provided)
2. Fetch from llm_model_configs system via connector_factory (if user_id and jwt_token provided)
3. Environment variables (fallback)
Args:
user_id: User UUID for fetching config from database
jwt_token: JWT token for API authentication
llm_config: Optional configuration dictionary to use directly
Returns: Returns:
Any: A new LLM model instance for title generation. Any: A new LLM model instance for title generation.
The actual type depends on the provider. The actual type depends on the provider.
Raises: Raises:
ValueError: If required environment variables are missing or invalid. ValueError: If required configuration fields are missing or invalid.
""" """
env_vars = _load_llm_config() env_vars = _load_llm_config(user_id, jwt_token, llm_config)
logger.info( logger.info(
"Creating title model: name=%s, provider=%s, base_url=%s, temperature=1.0", "Creating title model: name=%s, provider=%s, base_url=%s, temperature=1.0",
@ -174,305 +234,35 @@ def create_model_with_tools(
raise RuntimeError(f"Failed to bind tools to model: {e}") from e raise RuntimeError(f"Failed to bind tools to model: {e}") from e
def create_note_organizer_model() -> Any: def create_base_model_with_tools(
""" tools: list[Any],
Create a fresh model instance for note organization. user_id: Optional[UUID] = None,
jwt_token: Optional[str] = None,
This creates a model instance suitable for organizing and formatting notes. llm_config: Optional[dict[str, Any]] = None,
It uses the same configuration as the base model but with a lower temperature ) -> Any:
for more consistent and predictable output.
Returns:
Any: A new LLM model instance for note organization.
The actual type depends on the provider.
Raises:
ValueError: If required environment variables are missing or invalid.
"""
env_vars = _load_llm_config()
logger.info(
"Creating note organizer model: name=%s, provider=%s, base_url=%s, temperature=0.3",
env_vars["model_name"],
env_vars["model_provider"],
env_vars["base_url"] if env_vars["base_url"] else "default",
)
# Validate required fields
if not env_vars["model_name"]:
raise ValueError("MODEL_NAME environment variable is required")
if not env_vars["model_provider"]:
raise ValueError("MODE_PROVIDER environment variable is required")
try:
model = init_chat_model(
env_vars["model_name"],
model_provider=env_vars["model_provider"],
api_key=env_vars["api_key"],
base_url=env_vars["base_url"],
temperature="0.3", # Lower temperature for more consistent note organization
configurable_fields="any",
config_prefix="foo",
)
logger.info("Note organizer model created successfully")
return model
except Exception as e:
logger.error("Failed to create note organizer model: %s", e)
raise RuntimeError(f"Failed to create note organizer model: {e}") from e
def create_base_model_with_tools(tools: list[Any]) -> Any:
""" """
Create a fresh base model instance with tools bound. Create a fresh base model instance with tools bound.
This is a convenience function that combines creating the base model This is a convenience function that combines creating the base model
and binding tools to it. and binding tools to it.
Configuration priority:
1. llm_config dictionary (if provided)
2. Fetch from llm_model_configs system via connector_factory (if user_id and jwt_token provided)
3. Environment variables (fallback)
Args: Args:
tools: List of tools to bind to the model. tools: List of tools to bind to the model.
user_id: User UUID for fetching config from database
jwt_token: JWT token for API authentication
llm_config: Optional configuration dictionary to use directly
Returns: Returns:
Any: A new model instance with tools bound (type varies by provider). Any: A new model instance with tools bound (type varies by provider).
Raises: Raises:
ValueError: If required environment variables are missing. ValueError: If required configuration fields are missing.
RuntimeError: If model creation or tool binding fails. RuntimeError: If model creation or tool binding fails.
""" """
base_model = create_base_model() base_model = create_base_model(user_id, jwt_token, llm_config)
return create_model_with_tools(base_model, tools) return create_model_with_tools(base_model, tools)
def create_window_agent_base_model() -> Any:
"""
Create a base model instance for Window Agent (without tools).
This creates a model instance suitable for Window Agent (voice mode).
Uses temperature=0 for precise, deterministic responses.
Returns:
Any: A new base LLM model instance (without tools bound).
The actual type depends on the provider.
Raises:
ValueError: If required environment variables are missing.
RuntimeError: If model creation fails.
"""
env_vars = _load_llm_config()
logger.info(
"Creating Window Agent base model: name=%s, provider=%s, base_url=%s, temperature=0",
env_vars["model_name"],
env_vars["model_provider"],
env_vars["base_url"] if env_vars["base_url"] else "default",
)
# Validate required fields
if not env_vars["model_name"]:
raise ValueError("MODEL_NAME environment variable is required")
if not env_vars["model_provider"]:
raise ValueError("MODE_PROVIDER environment variable is required")
try:
# Create model with temperature=0 for voice mode (precise, concise responses)
model = init_chat_model(
env_vars["model_name"],
model_provider=env_vars["model_provider"],
api_key=env_vars["api_key"],
base_url=env_vars["base_url"],
temperature="0", # Force temperature=0 for voice mode
configurable_fields="any",
config_prefix="foo",
)
logger.info("Window Agent base model created successfully (no tools)")
return model
except Exception as e:
logger.error("Failed to create Window Agent base model: %s", e)
raise RuntimeError(f"Failed to create Window Agent base model: {e}") from e
def create_window_agent_model_with_tools(tools: list[Any]) -> Any:
"""
Create a model instance with tools for Window Agent.
Window Agent runs in VOICE mode and needs precise, deterministic responses.
Uses temperature=0 for consistency.
Args:
tools: List of tools to bind to the model.
Returns:
Any: A new model instance with tools bound (type varies by provider).
Raises:
ValueError: If required environment variables are missing.
RuntimeError: If model creation or tool binding fails.
"""
env_vars = _load_llm_config()
logger.info(
"Creating Window Agent model: name=%s, provider=%s, base_url=%s, temperature=0",
env_vars["model_name"],
env_vars["model_provider"],
env_vars["base_url"] if env_vars["base_url"] else "default",
)
# Validate required fields
if not env_vars["model_name"]:
raise ValueError("MODEL_NAME environment variable is required")
if not env_vars["model_provider"]:
raise ValueError("MODE_PROVIDER environment variable is required")
try:
# Create model with temperature=0 for voice mode (precise, concise responses)
model = init_chat_model(
env_vars["model_name"],
model_provider=env_vars["model_provider"],
api_key=env_vars["api_key"],
base_url=env_vars["base_url"],
temperature="0", # Force temperature=0 for voice mode
configurable_fields="any",
config_prefix="foo",
)
# Bind tools
model_with_tools = model.bind_tools(tools)
logger.info("Window Agent model created with %d tools", len(tools))
return model_with_tools
except Exception as e:
logger.error("Failed to create Window Agent model: %s", e)
raise RuntimeError(f"Failed to create Window Agent model: {e}") from e
def create_experiment_planner_model() -> Any:
"""
Create a fresh model instance for experiment planning.
This creates a model instance suitable for generating GNS3 lab experiment plans.
It uses the same configuration as the base model but with a moderate temperature
to balance creativity and accuracy in lab design.
Returns:
Any: A new LLM model instance for experiment planning.
The actual type depends on the provider.
Raises:
ValueError: If required environment variables are missing or invalid.
"""
env_vars = _load_llm_config()
logger.info(
"Creating experiment planner model: name=%s, provider=%s, base_url=%s, temperature=0.7",
env_vars["model_name"],
env_vars["model_provider"],
env_vars["base_url"] if env_vars["base_url"] else "default",
)
# Validate required fields
if not env_vars["model_name"]:
raise ValueError("MODEL_NAME environment variable is required")
if not env_vars["model_provider"]:
raise ValueError("MODE_PROVIDER environment variable is required")
try:
model = init_chat_model(
env_vars["model_name"],
model_provider=env_vars["model_provider"],
api_key=env_vars["api_key"],
base_url=env_vars["base_url"],
temperature="0.7", # Moderate temperature for balanced creativity and accuracy
configurable_fields="any",
config_prefix="foo",
)
logger.info("Experiment planner model created successfully")
return model
except Exception as e:
logger.error("Failed to create experiment planner model: %s", e)
raise RuntimeError(f"Failed to create experiment planner model: {e}") from e
def create_vision_model():
"""
Create a fresh Qwen-VL vision model instance for network topology recognition.
This creates a vision model instance using Qwen-VL through DashScope SDK
for recognizing network topology diagrams from images.
Returns:
QwenVisionModel: A new Qwen-VL vision model instance.
Raises:
ImportError: If dashscope package is not installed.
ValueError: If QWEN_API_KEY is not configured.
RuntimeError: If model creation fails.
"""
from gns3_copilot.agent.qwen_vision_model import create_qwen_vision_model
logger.info("Creating Qwen-VL vision model")
try:
model = create_qwen_vision_model()
logger.info("Qwen-VL vision model created successfully")
return model
except Exception as e:
logger.error("Failed to create Qwen-VL vision model: %s", e)
raise RuntimeError(f"Failed to create Qwen-VL vision model: {e}") from e
def create_presentation_eval_model() -> Any:
"""
Create a fresh model instance for presentation evaluation.
This creates a model instance suitable for evaluating network engineer
presentations. Uses moderate temperature for balanced objective assessment.
Returns:
Any: A new LLM model instance for presentation evaluation.
Raises:
ValueError: If required environment variables are missing.
RuntimeError: If model creation fails.
"""
env_vars = _load_llm_config()
logger.info(
"Creating presentation evaluator model: name=%s, provider=%s, base_url=%s, temperature=0.5",
env_vars["model_name"],
env_vars["model_provider"],
env_vars["base_url"] if env_vars["base_url"] else "default",
)
# Validate required fields
if not env_vars["model_name"]:
raise ValueError("MODEL_NAME environment variable is required")
if not env_vars["model_provider"]:
raise ValueError("MODE_PROVIDER environment variable is required")
try:
model = init_chat_model(
env_vars["model_name"],
model_provider=env_vars["model_provider"],
api_key=env_vars["api_key"],
base_url=env_vars["base_url"],
temperature="0.5", # Moderate temperature for balanced evaluation
configurable_fields="any",
config_prefix="foo",
)
logger.info("Presentation evaluator model created successfully")
return model
except Exception as e:
logger.error("Failed to create presentation evaluator model: %s", e)
raise RuntimeError(f"Failed to create presentation evaluator model: {e}") from e

View File

@ -31,6 +31,7 @@ from .connector_factory import (
get_gns3_connector, get_gns3_connector,
get_gns3_connector_with_llm_config, get_gns3_connector_with_llm_config,
get_gns3_server_host, get_gns3_server_host,
get_llm_config,
) )
from .custom_gns3fy import ( from .custom_gns3fy import (
CONSOLE_TYPES, CONSOLE_TYPES,
@ -98,6 +99,7 @@ __all__ = [
"get_gns3_connector", "get_gns3_connector",
"get_gns3_connector_with_llm_config", "get_gns3_connector_with_llm_config",
"get_gns3_server_host", "get_gns3_server_host",
"get_llm_config",
"add_file_to_index", "add_file_to_index",
"get_file_list", "get_file_list",
] ]

View File

@ -335,3 +335,60 @@ def get_gns3_server_host() -> str:
except Exception as e: except Exception as e:
logger.warning("Failed to extract host from URL %s: %s, using fallback", url, e) logger.warning("Failed to extract host from URL %s: %s, using fallback", url, e)
return DEFAULT_GNS3_URL.split("://")[1].split(":")[0] return DEFAULT_GNS3_URL.split("://")[1].split(":")[0]
def get_llm_config(user_id, jwt_token: str) -> Optional[dict]:
"""
Get LLM model configuration for a user.
This is a convenience function that retrieves the user's LLM configuration
from the llm_model_configs system.
Args:
user_id: User UUID (can be string or UUID object)
jwt_token: JWT token for authentication
Returns:
Dictionary with LLM configuration keys (provider, model, api_key, etc.),
or None if not found.
Example:
from gns3_copilot.gns3_client import get_llm_config
config = get_llm_config(user_id, jwt_token)
if config:
provider = config['provider']
model = config['model']
api_key = config['api_key']
"""
import asyncio
try:
# Convert user_id to UUID if it's a string
if isinstance(user_id, str):
user_id = UUID(user_id)
# Detect GNS3 URL
url = _detect_url_for_api()
# Import the async helper
from gns3_copilot.utils.llm_config_helper import get_user_llm_config
# Run async function in sync context
loop = asyncio.get_event_loop()
if loop.is_running():
# If loop is already running, run in a new thread
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(
asyncio.run,
get_user_llm_config(user_id, jwt_token, url)
)
return future.result(timeout=10)
else:
# No loop running, use run() directly
return asyncio.run(get_user_llm_config(user_id, jwt_token, url))
except Exception as e:
logger.error("Failed to get LLM config for user %s: %s", user_id, e)
return None