|
""" |
|
Configuration loader for GAIA agent. |
|
|
|
This module provides a hierarchical configuration system for the GAIA agent, |
|
supporting configuration from multiple sources with proper precedence: |
|
1. Default values |
|
2. Configuration files |
|
3. Environment variables |
|
4. Runtime overrides |
|
|
|
It includes a Configuration class for typed access to configuration values |
|
and validation of configuration. |
|
""" |
|
|
|
import os |
|
import json |
|
import yaml |
|
import logging |
|
from typing import Dict, Any, Optional, List, Union, TypeVar, Generic, Type, cast |
|
from pathlib import Path |
|
from copy import deepcopy |
|
|
|
from src.gaia.config.default import DEFAULT_CONFIG |
|
from src.gaia.config.env import load_env_config |
|
|
|
|
|
T = TypeVar('T') |
|
|
|
logger = logging.getLogger("gaia.config") |
|
|
|
class ConfigurationError(Exception): |
|
"""Exception raised for configuration errors.""" |
|
pass |
|
|
|
class Configuration: |
|
""" |
|
Configuration class providing structured access to GAIA configuration. |
|
|
|
This class wraps a configuration dictionary and provides typed access to |
|
configuration values with dot notation, default values, and validation. |
|
""" |
|
|
|
def __init__(self, config_dict: Dict[str, Any]): |
|
""" |
|
Initialize the Configuration with the provided dictionary. |
|
|
|
Args: |
|
config_dict: The configuration dictionary |
|
""" |
|
self._config = deepcopy(config_dict) |
|
|
|
def get(self, path: str, default: Optional[T] = None) -> Union[T, Any]: |
|
""" |
|
Get a configuration value by path with optional default. |
|
|
|
Args: |
|
path: Dot-separated path to the configuration value |
|
default: Default value if the path doesn't exist |
|
|
|
Returns: |
|
The configuration value or default |
|
""" |
|
parts = path.split('.') |
|
current = self._config |
|
|
|
try: |
|
for part in parts: |
|
current = current[part] |
|
return current |
|
except (KeyError, TypeError): |
|
return default |
|
|
|
def get_typed(self, path: str, value_type: Type[T], default: Optional[T] = None) -> T: |
|
""" |
|
Get a configuration value with type checking. |
|
|
|
Args: |
|
path: Dot-separated path to the configuration value |
|
value_type: Expected type of the value |
|
default: Default value if the path doesn't exist |
|
|
|
Returns: |
|
The configuration value with the specified type |
|
|
|
Raises: |
|
ConfigurationError: If the value doesn't match the expected type |
|
""" |
|
value = self.get(path, default) |
|
|
|
if value is None: |
|
if default is None: |
|
raise ConfigurationError(f"Required configuration value '{path}' is missing") |
|
return default |
|
|
|
if not isinstance(value, value_type): |
|
|
|
try: |
|
if value_type is bool and isinstance(value, str): |
|
if value.lower() in ('true', 'yes', '1', 'on'): |
|
return cast(T, True) |
|
if value.lower() in ('false', 'no', '0', 'off'): |
|
return cast(T, False) |
|
|
|
if value_type in (int, float, str): |
|
return cast(T, value_type(value)) |
|
except (ValueError, TypeError): |
|
pass |
|
|
|
raise ConfigurationError( |
|
f"Configuration value '{path}' has incorrect type: " |
|
f"expected {value_type.__name__}, got {type(value).__name__}" |
|
) |
|
|
|
return cast(T, value) |
|
|
|
def set(self, path: str, value: Any) -> None: |
|
""" |
|
Set a configuration value by path. |
|
|
|
Args: |
|
path: Dot-separated path to the configuration value |
|
value: The value to set |
|
""" |
|
parts = path.split('.') |
|
current = self._config |
|
|
|
|
|
for part in parts[:-1]: |
|
if part not in current: |
|
current[part] = {} |
|
current = current[part] |
|
|
|
|
|
current[parts[-1]] = value |
|
|
|
def update(self, config_dict: Dict[str, Any]) -> None: |
|
""" |
|
Update configuration with values from another dictionary. |
|
|
|
This performs a deep update, preserving nested structure. |
|
|
|
Args: |
|
config_dict: Dictionary with configuration values to update |
|
""" |
|
self._deep_update(self._config, config_dict) |
|
|
|
def _deep_update(self, target: Dict[str, Any], source: Dict[str, Any]) -> None: |
|
""" |
|
Recursively update a dictionary with values from another dictionary. |
|
|
|
Args: |
|
target: Target dictionary to update |
|
source: Source dictionary with values to copy |
|
""" |
|
for key, value in source.items(): |
|
if isinstance(value, dict) and key in target and isinstance(target[key], dict): |
|
|
|
self._deep_update(target[key], value) |
|
else: |
|
|
|
target[key] = value |
|
|
|
def to_dict(self) -> Dict[str, Any]: |
|
""" |
|
Convert the configuration to a dictionary. |
|
|
|
Returns: |
|
A deep copy of the configuration dictionary |
|
""" |
|
return deepcopy(self._config) |
|
|
|
def validate(self, schema: Dict[str, Any]) -> List[str]: |
|
""" |
|
Validate the configuration against a schema. |
|
|
|
Args: |
|
schema: Dictionary defining the expected structure and types |
|
|
|
Returns: |
|
List of validation errors, empty if validation succeeds |
|
""" |
|
errors = [] |
|
self._validate_recursive(self._config, schema, "", errors) |
|
return errors |
|
|
|
def _validate_recursive( |
|
self, config: Dict[str, Any], schema: Dict[str, Any], |
|
path_prefix: str, errors: List[str] |
|
) -> None: |
|
""" |
|
Recursively validate configuration against schema. |
|
|
|
Args: |
|
config: Configuration dictionary to validate |
|
schema: Schema dictionary to validate against |
|
path_prefix: Current path prefix for error messages |
|
errors: List to append validation errors to |
|
""" |
|
|
|
for key, spec in schema.items(): |
|
if isinstance(spec, dict) and "required" in spec and spec["required"]: |
|
if key not in config: |
|
errors.append(f"Missing required field '{path_prefix}.{key}'") |
|
|
|
|
|
for key, value in config.items(): |
|
path = f"{path_prefix}.{key}" if path_prefix else key |
|
|
|
if key not in schema: |
|
|
|
continue |
|
|
|
spec = schema[key] |
|
|
|
if isinstance(spec, dict): |
|
if "type" in spec: |
|
|
|
expected_type = spec["type"] |
|
if not self._check_type(value, expected_type): |
|
errors.append( |
|
f"Invalid type for '{path}': expected {expected_type}, " |
|
f"got {type(value).__name__}" |
|
) |
|
|
|
if "values" in spec and isinstance(value, (list, tuple)): |
|
|
|
for i, item in enumerate(value): |
|
if not self._check_type(item, spec["values"]): |
|
errors.append( |
|
f"Invalid type for item {i} in '{path}': " |
|
f"expected {spec['values']}, got {type(item).__name__}" |
|
) |
|
|
|
if "properties" in spec and isinstance(value, dict): |
|
|
|
self._validate_recursive(value, spec["properties"], path, errors) |
|
|
|
def _check_type(self, value: Any, expected_type: str) -> bool: |
|
""" |
|
Check if a value matches the expected type. |
|
|
|
Args: |
|
value: Value to check |
|
expected_type: Expected type name as string |
|
|
|
Returns: |
|
True if the value matches the expected type, False otherwise |
|
""" |
|
if expected_type == "string": |
|
return isinstance(value, str) |
|
elif expected_type == "number": |
|
return isinstance(value, (int, float)) |
|
elif expected_type == "integer": |
|
return isinstance(value, int) |
|
elif expected_type == "boolean": |
|
return isinstance(value, bool) |
|
elif expected_type == "array": |
|
return isinstance(value, (list, tuple)) |
|
elif expected_type == "object": |
|
return isinstance(value, dict) |
|
elif expected_type == "null": |
|
return value is None |
|
elif expected_type == "any": |
|
return True |
|
else: |
|
return False |
|
|
|
|
|
class ConfigLoader: |
|
""" |
|
Configuration loader handling multiple configuration sources. |
|
|
|
This class loads configuration from multiple sources and combines them |
|
with proper precedence to create a final configuration. |
|
""" |
|
|
|
def __init__(self): |
|
"""Initialize the ConfigLoader.""" |
|
pass |
|
|
|
def load_config(self, config_file: Optional[str] = None) -> Configuration: |
|
""" |
|
Load configuration from all sources. |
|
|
|
Args: |
|
config_file: Optional path to a configuration file |
|
|
|
Returns: |
|
Configuration instance with combined configuration |
|
""" |
|
|
|
config_dict = deepcopy(DEFAULT_CONFIG) |
|
|
|
|
|
if config_file: |
|
file_config = self._load_config_file(config_file) |
|
self._deep_update(config_dict, file_config) |
|
|
|
|
|
env_config = load_env_config() |
|
self._deep_update(config_dict, env_config) |
|
|
|
|
|
return Configuration(config_dict) |
|
|
|
def _load_config_file(self, config_file: str) -> Dict[str, Any]: |
|
""" |
|
Load configuration from a file. |
|
|
|
Supports JSON and YAML files based on extension. |
|
|
|
Args: |
|
config_file: Path to the configuration file |
|
|
|
Returns: |
|
Dictionary with configuration from the file |
|
|
|
Raises: |
|
ConfigurationError: If the file cannot be loaded |
|
""" |
|
path = Path(config_file) |
|
|
|
if not path.exists(): |
|
raise ConfigurationError(f"Configuration file not found: {config_file}") |
|
|
|
try: |
|
with open(path, 'r') as f: |
|
if path.suffix.lower() in ('.yaml', '.yml'): |
|
return yaml.safe_load(f) or {} |
|
elif path.suffix.lower() == '.json': |
|
return json.load(f) |
|
else: |
|
raise ConfigurationError( |
|
f"Unsupported configuration file format: {path.suffix}" |
|
) |
|
except (yaml.YAMLError, json.JSONDecodeError) as e: |
|
raise ConfigurationError(f"Error parsing configuration file: {str(e)}") |
|
|
|
def _deep_update(self, target: Dict[str, Any], source: Dict[str, Any]) -> None: |
|
""" |
|
Recursively update a dictionary with values from another dictionary. |
|
|
|
Args: |
|
target: Target dictionary to update |
|
source: Source dictionary with values to copy |
|
""" |
|
for key, value in source.items(): |
|
if isinstance(value, dict) and key in target and isinstance(target[key], dict): |
|
|
|
self._deep_update(target[key], value) |
|
else: |
|
|
|
target[key] = value |