a digital person for bluesky
at x 8.3 kB view raw
1""" 2Configuration loader for Void Bot. 3Loads configuration from config.yaml and environment variables. 4""" 5 6import os 7import yaml 8import logging 9from pathlib import Path 10from typing import Dict, Any, Optional, List 11 12logger = logging.getLogger(__name__) 13 14class ConfigLoader: 15 """Configuration loader that handles YAML config files and environment variables.""" 16 17 def __init__(self, config_path: str = "config.yaml"): 18 """ 19 Initialize the configuration loader. 20 21 Args: 22 config_path: Path to the YAML configuration file 23 """ 24 self.config_path = Path(config_path) 25 self._config = None 26 self._load_config() 27 28 def _load_config(self) -> None: 29 """Load configuration from YAML file.""" 30 if not self.config_path.exists(): 31 raise FileNotFoundError( 32 f"Configuration file not found: {self.config_path}\n" 33 f"Please copy config.yaml.example to config.yaml and configure it." 34 ) 35 36 try: 37 with open(self.config_path, 'r', encoding='utf-8') as f: 38 self._config = yaml.safe_load(f) or {} 39 except yaml.YAMLError as e: 40 raise ValueError(f"Invalid YAML in configuration file: {e}") 41 except Exception as e: 42 raise ValueError(f"Error loading configuration file: {e}") 43 44 def get(self, key: str, default: Any = None) -> Any: 45 """ 46 Get a configuration value using dot notation. 47 48 Args: 49 key: Configuration key in dot notation (e.g., 'letta.api_key') 50 default: Default value if key not found 51 52 Returns: 53 Configuration value or default 54 """ 55 keys = key.split('.') 56 value = self._config 57 58 for k in keys: 59 if isinstance(value, dict) and k in value: 60 value = value[k] 61 else: 62 return default 63 64 return value 65 66 def get_with_env(self, key: str, env_var: str, default: Any = None) -> Any: 67 """ 68 Get configuration value, preferring environment variable over config file. 69 70 Args: 71 key: Configuration key in dot notation 72 env_var: Environment variable name 73 default: Default value if neither found 74 75 Returns: 76 Value from environment variable, config file, or default 77 """ 78 # First try environment variable 79 env_value = os.getenv(env_var) 80 if env_value is not None: 81 return env_value 82 83 # Then try config file 84 config_value = self.get(key) 85 if config_value is not None: 86 return config_value 87 88 return default 89 90 def get_required(self, key: str, env_var: Optional[str] = None) -> Any: 91 """ 92 Get a required configuration value. 93 94 Args: 95 key: Configuration key in dot notation 96 env_var: Optional environment variable name to check first 97 98 Returns: 99 Configuration value 100 101 Raises: 102 ValueError: If required value is not found 103 """ 104 if env_var: 105 value = self.get_with_env(key, env_var) 106 else: 107 value = self.get(key) 108 109 if value is None: 110 source = f"config key '{key}'" 111 if env_var: 112 source += f" or environment variable '{env_var}'" 113 raise ValueError(f"Required configuration value not found: {source}") 114 115 return value 116 117 def get_section(self, section: str) -> Dict[str, Any]: 118 """ 119 Get an entire configuration section. 120 121 Args: 122 section: Section name 123 124 Returns: 125 Dictionary containing the section 126 """ 127 return self.get(section, {}) 128 129 def setup_logging(self) -> None: 130 """Setup logging based on configuration.""" 131 logging_config = self.get_section('logging') 132 133 # Set root logging level 134 level = logging_config.get('level', 'INFO') 135 logging.basicConfig( 136 level=getattr(logging, level), 137 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" 138 ) 139 140 # Set specific logger levels 141 loggers = logging_config.get('loggers', {}) 142 for logger_name, logger_level in loggers.items(): 143 logger_obj = logging.getLogger(logger_name) 144 logger_obj.setLevel(getattr(logging, logger_level)) 145 146 147# Global configuration instance 148_config_instance = None 149 150def get_config(config_path: str = "config.yaml") -> ConfigLoader: 151 """ 152 Get the global configuration instance. 153 154 Args: 155 config_path: Path to configuration file (only used on first call) 156 157 Returns: 158 ConfigLoader instance 159 """ 160 global _config_instance 161 if _config_instance is None: 162 _config_instance = ConfigLoader(config_path) 163 return _config_instance 164 165def reload_config() -> None: 166 """Reload the configuration from file.""" 167 global _config_instance 168 if _config_instance is not None: 169 _config_instance._load_config() 170 171def get_letta_config(config_path: str = "config.yaml") -> Dict[str, Any]: 172 """Get Letta configuration. 173 174 Args: 175 config_path: Path to configuration file 176 177 Returns: 178 Dictionary with Letta configuration 179 """ 180 config = get_config(config_path) 181 return { 182 'api_key': config.get_required('letta.api_key'), 183 'timeout': config.get('letta.timeout', 600), 184 'project_id': config.get_required('letta.project_id'), 185 'agent_id': config.get_required('letta.agent_id'), 186 } 187 188def get_bluesky_config(config_path: str = "config.yaml") -> Dict[str, Any]: 189 """Get Bluesky configuration. 190 191 Args: 192 config_path: Path to configuration file 193 194 Returns: 195 Dictionary with Bluesky configuration 196 """ 197 config = get_config(config_path) 198 return { 199 'username': config.get_required('bluesky.username', 'BSKY_USERNAME'), 200 'password': config.get_required('bluesky.password', 'BSKY_PASSWORD'), 201 'pds_uri': config.get_with_env('bluesky.pds_uri', 'PDS_URI', 'https://bsky.social'), 202 } 203 204def get_bot_config(config_path: str = "config.yaml") -> Dict[str, Any]: 205 """Get bot behavior configuration. 206 207 Args: 208 config_path: Path to configuration file 209 210 Returns: 211 Dictionary with bot configuration 212 """ 213 config = get_config(config_path) 214 return { 215 'fetch_notifications_delay': config.get('bot.fetch_notifications_delay', 30), 216 'max_processed_notifications': config.get('bot.max_processed_notifications', 10000), 217 'max_notification_pages': config.get('bot.max_notification_pages', 20), 218 } 219 220def get_agent_config() -> Dict[str, Any]: 221 """Get agent configuration.""" 222 config = get_config() 223 return { 224 'name': config.get('bot.agent.name', 'void'), 225 'model': config.get('bot.agent.model', 'openai/gpt-4o-mini'), 226 'embedding': config.get('bot.agent.embedding', 'openai/text-embedding-3-small'), 227 'description': config.get('bot.agent.description', 'A social media agent trapped in the void.'), 228 'max_steps': config.get('bot.agent.max_steps', 100), 229 'blocks': config.get('bot.agent.blocks', {}), 230 } 231 232def get_threading_config() -> Dict[str, Any]: 233 """Get threading configuration.""" 234 config = get_config() 235 return { 236 'parent_height': config.get('threading.parent_height', 40), 237 'depth': config.get('threading.depth', 10), 238 'max_post_characters': config.get('threading.max_post_characters', 300), 239 } 240 241def get_queue_config() -> Dict[str, Any]: 242 """Get queue configuration.""" 243 config = get_config() 244 return { 245 'priority_users': config.get('queue.priority_users', ['cameron.pfiffer.org']), 246 'base_dir': config.get('queue.base_dir', 'queue'), 247 'error_dir': config.get('queue.error_dir', 'queue/errors'), 248 'no_reply_dir': config.get('queue.no_reply_dir', 'queue/no_reply'), 249 'processed_file': config.get('queue.processed_file', 'queue/processed_notifications.json'), 250 }