379 lines
14 KiB
Python
379 lines
14 KiB
Python
"""
|
|
Settings management for SageUtils.
|
|
|
|
This module provides a centralized way to manage settings with:
|
|
- Default values and types
|
|
- Validation and type checking
|
|
- Automatic migration of missing settings
|
|
- Clear documentation of all available settings
|
|
"""
|
|
|
|
from typing import Any, Dict, Optional
|
|
from .config_manager import ConfigManager
|
|
from .settings_crypto import (
|
|
decrypt_sensitive_value,
|
|
encrypt_sensitive_value,
|
|
is_encrypted_value,
|
|
is_sensitive_setting_key,
|
|
)
|
|
|
|
from .logger import get_logger
|
|
logger = get_logger('settings')
|
|
|
|
# Define the schema for all SageUtils settings
|
|
SETTINGS_SCHEMA = {
|
|
# LLM Integration Settings
|
|
"enable_lmstudio_rest": {
|
|
"default": False,
|
|
"type": bool,
|
|
"description": "Enable LM Studio REST v1 LLM integration"
|
|
},
|
|
"enable_ollama_rest": {
|
|
"default": False,
|
|
"type": bool,
|
|
"description": "Enable Ollama REST LLM integration (HTTP API, no SDK required)"
|
|
},
|
|
"enable_openai": {
|
|
"default": False,
|
|
"type": bool,
|
|
"description": "Enable OpenAI (or compatible) LLM integration"
|
|
},
|
|
"openai_api_key": {
|
|
"default": "",
|
|
"type": str,
|
|
"description": "API key for OpenAI or compatible endpoint (can also be set via OPENAI_API_KEY env var)"
|
|
},
|
|
"openai_use_custom_url": {
|
|
"default": False,
|
|
"type": bool,
|
|
"description": "Use a custom base URL for the OpenAI-compatible endpoint"
|
|
},
|
|
"openai_base_url": {
|
|
"default": "",
|
|
"type": str,
|
|
"description": "Custom base URL for OpenAI-compatible endpoint (e.g., 'http://localhost:8080' for LocalAI)"
|
|
},
|
|
"ollama_use_custom_url": {
|
|
"default": False,
|
|
"type": bool,
|
|
"description": "Use a custom URL for Ollama instead of the default"
|
|
},
|
|
"ollama_custom_url": {
|
|
"default": "",
|
|
"type": str,
|
|
"description": "Custom URL for Ollama service (e.g., 'http://localhost:11434')"
|
|
},
|
|
"ollama_api_key": {
|
|
"default": "",
|
|
"type": str,
|
|
"description": "API key for Ollama endpoint authentication (can also be set via OLLAMA_API_KEY env var)"
|
|
},
|
|
"lmstudio_use_custom_url": {
|
|
"default": False,
|
|
"type": bool,
|
|
"description": "Use a custom URL for LM Studio instead of the default"
|
|
},
|
|
"lmstudio_custom_url": {
|
|
"default": "",
|
|
"type": str,
|
|
"description": "Custom URL for LM Studio service (e.g., 'http://localhost:1234')"
|
|
},
|
|
"lmstudio_api_token": {
|
|
"default": "",
|
|
"type": str,
|
|
"description": "API token for LM Studio REST endpoint (can also be set via LMSTUDIO_API_TOKEN env var)"
|
|
},
|
|
"default_llm_provider": {
|
|
"default": "lmstudio_rest",
|
|
"type": str,
|
|
"description": "Default LLM provider for the LLM sidebar and provider-switching LLM v3 nodes",
|
|
"valid_values": ["lmstudio", "ollama", "lmstudio_rest", "ollama_rest", "openai", "native"]
|
|
},
|
|
"llm_raise_node_exceptions": {
|
|
"default": False,
|
|
"type": bool,
|
|
"description": "When enabled, LLM nodes re-raise provider load/generation exceptions after logging"
|
|
},
|
|
# Sidebar Tab Visibility Settings
|
|
"show_models_tab": {
|
|
"default": True,
|
|
"type": bool,
|
|
"description": "Show Models tab in sidebar"
|
|
},
|
|
"show_files_tab": {
|
|
"default": True,
|
|
"type": bool,
|
|
"description": "Show Files tab in sidebar"
|
|
},
|
|
"show_search_tab": {
|
|
"default": True,
|
|
"type": bool,
|
|
"description": "Show Search (Civitai) tab in sidebar"
|
|
},
|
|
"show_gallery_tab": {
|
|
"default": True,
|
|
"type": bool,
|
|
"description": "Show Gallery tab in sidebar"
|
|
},
|
|
"show_prompts_tab": {
|
|
"default": True,
|
|
"type": bool,
|
|
"description": "Show Prompts (Prompt Builder) tab in sidebar"
|
|
},
|
|
"show_llm_tab": {
|
|
"default": True,
|
|
"type": bool,
|
|
"description": "Show LLM tab in sidebar"
|
|
}
|
|
}
|
|
|
|
|
|
def is_known_setting(key: str) -> bool:
|
|
"""Return True when the key is defined in SETTINGS_SCHEMA."""
|
|
return key in SETTINGS_SCHEMA
|
|
|
|
|
|
def get_setting_schema_default(key: str) -> Any:
|
|
"""Get default value for a known setting key."""
|
|
return SETTINGS_SCHEMA[key]["default"]
|
|
|
|
|
|
class SettingsValidator:
|
|
"""Validates settings values against the schema."""
|
|
|
|
@staticmethod
|
|
def _coerce_value(expected_type: type, value: Any) -> Any:
|
|
"""Coerce a value to the expected settings type."""
|
|
if expected_type == bool:
|
|
if isinstance(value, str):
|
|
return value.lower() in ('true', '1', 'yes', 'on')
|
|
return bool(value)
|
|
if expected_type == int:
|
|
return int(value)
|
|
if expected_type == float:
|
|
return float(value)
|
|
if expected_type == str:
|
|
return str(value)
|
|
|
|
raise TypeError(f"Cannot convert {type(value)} to {expected_type}")
|
|
|
|
@staticmethod
|
|
def validate_value(key: str, value: Any, schema_entry: Dict[str, Any]) -> Any:
|
|
"""Validate a single setting value against its schema."""
|
|
expected_type = schema_entry.get("type", str)
|
|
valid_values = schema_entry.get("valid_values")
|
|
|
|
# Type checking
|
|
if not isinstance(value, expected_type):
|
|
try:
|
|
value = SettingsValidator._coerce_value(expected_type, value)
|
|
except (ValueError, TypeError):
|
|
logger.warning(f"Setting '{key}': Invalid type. Expected {expected_type.__name__}, got {type(value).__name__}. Using default.")
|
|
return schema_entry["default"]
|
|
|
|
# Valid values checking
|
|
if valid_values and value not in valid_values:
|
|
logger.warning(f"Setting '{key}': Invalid value '{value}'. Must be one of {valid_values}. Using default.")
|
|
return schema_entry["default"]
|
|
|
|
return value
|
|
|
|
|
|
class SageSettings:
|
|
"""Enhanced settings manager for SageUtils with validation and defaults."""
|
|
|
|
def __init__(self):
|
|
self._config_manager = ConfigManager("config")
|
|
self._settings: Dict[str, Any] = {}
|
|
self._validator = SettingsValidator()
|
|
self.load_and_validate()
|
|
|
|
@staticmethod
|
|
def _is_known_setting(key: str) -> bool:
|
|
"""Return True when the key is defined in SETTINGS_SCHEMA."""
|
|
return is_known_setting(key)
|
|
|
|
@staticmethod
|
|
def _schema_default(key: str) -> Any:
|
|
"""Get default value for a known setting key."""
|
|
return get_setting_schema_default(key)
|
|
|
|
def load_and_validate(self) -> None:
|
|
"""Load settings from config manager and validate against schema."""
|
|
# Load current settings
|
|
raw_settings = self._config_manager.load() or {}
|
|
|
|
# Decrypt sensitive values for in-memory use.
|
|
current_settings: Dict[str, Any] = {}
|
|
settings_updated = False
|
|
for key, raw_value in raw_settings.items():
|
|
current_settings[key] = decrypt_sensitive_value(key, raw_value)
|
|
# Migrate existing plaintext secrets to encrypted-at-rest values on next save.
|
|
if (
|
|
is_sensitive_setting_key(key)
|
|
and isinstance(raw_value, str)
|
|
and raw_value.strip()
|
|
and not is_encrypted_value(raw_value)
|
|
):
|
|
settings_updated = True
|
|
logger.info(f"Setting '{key}' will be migrated to encrypted storage.")
|
|
|
|
# Start with defaults and update with current values
|
|
self._settings = {}
|
|
|
|
for key, schema_entry in SETTINGS_SCHEMA.items():
|
|
default_value = schema_entry["default"]
|
|
if key in current_settings:
|
|
# Validate existing setting
|
|
validated_value = self._validator.validate_value(
|
|
key, current_settings[key], schema_entry
|
|
)
|
|
self._settings[key] = validated_value
|
|
|
|
# Check if validation changed the value
|
|
if validated_value != current_settings[key]:
|
|
settings_updated = True
|
|
logger.info(f"Setting '{key}' corrected to: {validated_value}")
|
|
else:
|
|
# Use default for missing settings
|
|
self._settings[key] = default_value
|
|
settings_updated = True
|
|
logger.info(f"Setting '{key}' added with default value: {default_value}")
|
|
|
|
# Remove any settings not in schema (cleanup old/deprecated settings)
|
|
for key in raw_settings:
|
|
if key not in SETTINGS_SCHEMA:
|
|
logger.warning(f"Removing deprecated setting: '{key}'")
|
|
settings_updated = True
|
|
|
|
# Save if any changes were made
|
|
if settings_updated:
|
|
self.save()
|
|
logger.info("Settings updated and saved.")
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
"""Get a setting value with optional default fallback."""
|
|
if self._is_known_setting(key):
|
|
return self._settings.get(key, self._schema_default(key))
|
|
return self._settings.get(key, default)
|
|
|
|
def set(self, key: str, value: Any) -> bool:
|
|
"""Set a setting value with validation."""
|
|
if not self._is_known_setting(key):
|
|
logger.warning(f"Setting unknown key '{key}'. Consider adding it to SETTINGS_SCHEMA.")
|
|
self._settings[key] = value
|
|
return True
|
|
|
|
schema_entry = SETTINGS_SCHEMA[key]
|
|
validated_value = self._validator.validate_value(key, value, schema_entry)
|
|
|
|
if self._settings.get(key) != validated_value:
|
|
self._settings[key] = validated_value
|
|
logger.info(f"Setting '{key}' updated to: {validated_value}")
|
|
return True
|
|
|
|
return False
|
|
|
|
def save(self) -> bool:
|
|
"""Save current settings to file."""
|
|
try:
|
|
persisted_settings: Dict[str, Any] = {}
|
|
for key, value in self._settings.items():
|
|
persisted_settings[key] = encrypt_sensitive_value(key, value)
|
|
self._config_manager.data = persisted_settings
|
|
return self._config_manager.save()
|
|
except Exception as e:
|
|
logger.error(f"Failed to save settings: {e}")
|
|
return False
|
|
|
|
# Used in settings and server_routes.
|
|
def reset_to_defaults(self) -> None:
|
|
"""Reset all settings to their default values."""
|
|
self._settings = {key: self._schema_default(key) for key in SETTINGS_SCHEMA}
|
|
self.save()
|
|
logger.info("All settings reset to defaults.")
|
|
|
|
def get_setting_info(self, key: str) -> Optional[Dict[str, Any]]:
|
|
"""Get information about a setting including description and current value."""
|
|
if not self._is_known_setting(key):
|
|
return None
|
|
|
|
schema_entry = SETTINGS_SCHEMA[key].copy()
|
|
# Get current value, falling back to default if not set
|
|
schema_entry["current_value"] = self._settings.get(key, self._schema_default(key))
|
|
# Convert type to string representation for JSON serialization
|
|
if "type" in schema_entry:
|
|
schema_entry["type"] = schema_entry["type"].__name__
|
|
return schema_entry
|
|
|
|
# Used in settings and server_routes.
|
|
def list_all_settings(self) -> Dict[str, Dict[str, Any]]:
|
|
"""Get information about all settings."""
|
|
result = {}
|
|
for key in SETTINGS_SCHEMA:
|
|
setting_info = self.get_setting_info(key)
|
|
if setting_info is not None:
|
|
# The setting_info is already JSON-safe from get_setting_info()
|
|
result[key] = setting_info
|
|
return result
|
|
|
|
def is_feature_enabled(self, feature: str) -> bool:
|
|
"""Check if a feature is enabled. Convenience method for boolean settings."""
|
|
return bool(self.get(feature, False))
|
|
|
|
@property
|
|
def all_settings(self) -> Dict[str, Any]:
|
|
"""Get a copy of all current settings."""
|
|
return self._settings.copy()
|
|
|
|
|
|
# Global settings instance
|
|
_settings_instance: Optional[SageSettings] = None
|
|
|
|
|
|
def get_settings() -> SageSettings:
|
|
"""Get the global settings instance, creating it if necessary."""
|
|
global _settings_instance
|
|
if _settings_instance is None:
|
|
_settings_instance = SageSettings()
|
|
return _settings_instance
|
|
|
|
|
|
def get_setting(key: str, default: Any = None) -> Any:
|
|
"""Convenience function to get a setting value."""
|
|
return get_settings().get(key, default)
|
|
|
|
|
|
def set_setting(key: str, value: Any) -> bool:
|
|
"""Convenience function to set a setting value."""
|
|
return get_settings().set(key, value)
|
|
|
|
|
|
def is_feature_enabled(feature: str) -> bool:
|
|
"""Convenience function to check if a feature is enabled."""
|
|
return get_settings().is_feature_enabled(feature)
|
|
|
|
|
|
def save_settings() -> bool:
|
|
"""Convenience function to save settings."""
|
|
return get_settings().save()
|
|
|
|
|
|
# Backwards compatibility - provide the same interface as before
|
|
def get_sage_config() -> Dict[str, Any]:
|
|
"""Get settings in the same format as the old sage_config."""
|
|
return get_settings().all_settings
|
|
|
|
|
|
# For backwards compatibility, we'll also update the old config_manager module
|
|
# to use the new settings system
|
|
def _update_config_manager():
|
|
"""Update the config_manager module to use the new settings system."""
|
|
try:
|
|
from . import config_manager
|
|
# Replace the old settings_manager data with our new system
|
|
config_manager.settings_manager.data = get_settings().all_settings
|
|
config_manager.sage_config = get_settings().all_settings
|
|
except ImportError:
|
|
pass # Module might not be available during import
|