OpenSpace / openspace /config /loader.py
darkfire514's picture
Upload 160 files
399b80c verified
import threading
from pathlib import Path
from typing import Union, Iterable, Dict, Any, Optional
from .grounding import GroundingConfig
from .constants import (
CONFIG_GROUNDING,
CONFIG_SECURITY,
CONFIG_DEV,
CONFIG_MCP,
CONFIG_AGENTS
)
from openspace.utils.logging import Logger
from .utils import load_json_file, save_json_file as save_json
logger = Logger.get_logger(__name__)
CONFIG_DIR = Path(__file__).parent
# Global configuration singleton
_config: GroundingConfig | None = None
_config_lock = threading.RLock() # Use RLock to support recursive locking
def _deep_merge_dict(base: dict, update: dict) -> dict:
"""Deep merge two dictionaries, update's values will override base's values"""
result = base.copy()
for key, value in update.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = _deep_merge_dict(result[key], value)
else:
result[key] = value
return result
def _load_json_file(path: Path) -> Dict[str, Any]:
"""Load single JSON configuration file.
This function wraps the generic load_json_file and adds global configuration specific error handling and logging.
"""
if not path.exists():
logger.debug(f"Configuration file does not exist, skipping: {path}")
return {}
try:
data = load_json_file(path)
logger.info(f"Loaded configuration file: {path}")
return data
except Exception as e:
logger.warning(f"Failed to load configuration file {path}: {e}")
return {}
def _load_multiple_files(paths: Iterable[Path]) -> Dict[str, Any]:
"""Load configuration from multiple files"""
merged = {}
for path in paths:
data = _load_json_file(path)
if data:
merged = _deep_merge_dict(merged, data)
return merged
def load_config(*config_paths: Union[str, Path]) -> GroundingConfig:
"""
Load configuration files
"""
global _config
with _config_lock:
if config_paths:
paths = [Path(p) for p in config_paths]
else:
paths = [
CONFIG_DIR / CONFIG_GROUNDING,
CONFIG_DIR / CONFIG_SECURITY,
CONFIG_DIR / CONFIG_DEV, # Optional: development environment configuration
]
# Load and merge configuration
raw_data = _load_multiple_files(paths)
# Load MCP configuration (separate processing)
# Check if mcpServers already provided in merged custom configs
has_custom_mcp_servers = "mcpServers" in raw_data
if has_custom_mcp_servers:
# Use mcpServers from custom config
if "mcp" not in raw_data:
raw_data["mcp"] = {}
raw_data["mcp"]["servers"] = raw_data.pop("mcpServers")
logger.debug(f"Using custom MCP servers from provided config ({len(raw_data['mcp']['servers'])} servers)")
else:
# Load default MCP servers from config_mcp.json
mcp_data = _load_json_file(CONFIG_DIR / CONFIG_MCP)
if mcp_data and "mcpServers" in mcp_data:
if "mcp" not in raw_data:
raw_data["mcp"] = {}
raw_data["mcp"]["servers"] = mcp_data["mcpServers"]
logger.debug(f"Loaded MCP servers from default config_mcp.json ({len(raw_data['mcp']['servers'])} servers)")
# Validate and create configuration object
try:
_config = GroundingConfig.model_validate(raw_data)
except Exception as e:
logger.error(f"Validation failed, using default configuration: {e}")
_config = GroundingConfig()
# Adjust log level according to configuration
if _config.debug:
Logger.set_debug(2)
elif _config.log_level:
try:
Logger.configure(level=_config.log_level)
except Exception as e:
logger.warning(f"Failed to set log level {_config.log_level}: {e}")
return _config
def get_config() -> GroundingConfig:
"""
Get global configuration instance.
Usage:
- Get configuration in Provider: get_config().get_backend_config('shell')
- Get security policy in Tool: get_config().get_security_policy('shell')
"""
global _config
if _config is None:
with _config_lock:
if _config is None:
load_config()
return _config
def reset_config() -> None:
"""Reset configuration (for testing)"""
global _config
with _config_lock:
_config = None
def save_config(config: GroundingConfig, path: Union[str, Path]) -> None:
save_json(config.model_dump(), path)
logger.info(f"Configuration saved to: {path}")
def load_agents_config() -> Dict[str, Any]:
agents_config_path = CONFIG_DIR / CONFIG_AGENTS
return _load_json_file(agents_config_path)
def get_agent_config(agent_name: str) -> Optional[Dict[str, Any]]:
"""
Get the configuration of the specified agent
"""
agents_config = load_agents_config()
if "agents" not in agents_config:
logger.warning(f"No 'agents' key found in {CONFIG_AGENTS}")
return None
for agent_cfg in agents_config.get("agents", []):
if agent_cfg.get("name") == agent_name:
return agent_cfg
logger.warning(f"Agent '{agent_name}' not found in {CONFIG_AGENTS}")
return None
__all__ = [
"CONFIG_DIR",
"load_config",
"get_config",
"reset_config",
"save_config",
"load_agents_config",
"get_agent_config"
]