kiln_ai.utils.config
1import getpass 2import os 3import threading 4from pathlib import Path 5from typing import Any, Callable, Dict, List, Optional 6 7import yaml 8 9 10class ConfigProperty: 11 def __init__( 12 self, 13 type_: type, 14 default: Any = None, 15 env_var: Optional[str] = None, 16 default_lambda: Optional[Callable[[], Any]] = None, 17 sensitive: bool = False, 18 sensitive_keys: Optional[List[str]] = None, 19 ): 20 self.type = type_ 21 self.default = default 22 self.env_var = env_var 23 self.default_lambda = default_lambda 24 self.sensitive = sensitive 25 self.sensitive_keys = sensitive_keys 26 27 28class Config: 29 _shared_instance = None 30 31 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 32 self._properties: Dict[str, ConfigProperty] = properties or { 33 "user_id": ConfigProperty( 34 str, 35 env_var="KILN_USER_ID", 36 default_lambda=_get_user_id, 37 ), 38 "autosave_runs": ConfigProperty( 39 bool, 40 env_var="KILN_AUTOSAVE_RUNS", 41 default=True, 42 ), 43 "open_ai_api_key": ConfigProperty( 44 str, 45 env_var="OPENAI_API_KEY", 46 sensitive=True, 47 ), 48 "groq_api_key": ConfigProperty( 49 str, 50 env_var="GROQ_API_KEY", 51 sensitive=True, 52 ), 53 "ollama_base_url": ConfigProperty( 54 str, 55 env_var="OLLAMA_BASE_URL", 56 ), 57 "bedrock_access_key": ConfigProperty( 58 str, 59 env_var="AWS_ACCESS_KEY_ID", 60 sensitive=True, 61 ), 62 "bedrock_secret_key": ConfigProperty( 63 str, 64 env_var="AWS_SECRET_ACCESS_KEY", 65 sensitive=True, 66 ), 67 "open_router_api_key": ConfigProperty( 68 str, 69 env_var="OPENROUTER_API_KEY", 70 sensitive=True, 71 ), 72 "fireworks_api_key": ConfigProperty( 73 str, 74 env_var="FIREWORKS_API_KEY", 75 sensitive=True, 76 ), 77 "fireworks_account_id": ConfigProperty( 78 str, 79 env_var="FIREWORKS_ACCOUNT_ID", 80 ), 81 "anthropic_api_key": ConfigProperty( 82 str, 83 env_var="ANTHROPIC_API_KEY", 84 sensitive=True, 85 ), 86 "gemini_api_key": ConfigProperty( 87 str, 88 env_var="GEMINI_API_KEY", 89 sensitive=True, 90 ), 91 "projects": ConfigProperty( 92 list, 93 default_lambda=lambda: [], 94 ), 95 "azure_openai_api_key": ConfigProperty( 96 str, 97 env_var="AZURE_OPENAI_API_KEY", 98 sensitive=True, 99 ), 100 "azure_openai_endpoint": ConfigProperty( 101 str, 102 env_var="AZURE_OPENAI_ENDPOINT", 103 ), 104 "huggingface_api_key": ConfigProperty( 105 str, 106 env_var="HUGGINGFACE_API_KEY", 107 sensitive=True, 108 ), 109 "vertex_project_id": ConfigProperty( 110 str, 111 env_var="VERTEX_PROJECT_ID", 112 ), 113 "vertex_location": ConfigProperty( 114 str, 115 env_var="VERTEX_LOCATION", 116 ), 117 "together_api_key": ConfigProperty( 118 str, 119 env_var="TOGETHERAI_API_KEY", 120 sensitive=True, 121 ), 122 "wandb_api_key": ConfigProperty( 123 str, 124 env_var="WANDB_API_KEY", 125 sensitive=True, 126 ), 127 "wandb_base_url": ConfigProperty( 128 str, 129 env_var="WANDB_BASE_URL", 130 ), 131 "custom_models": ConfigProperty( 132 list, 133 default_lambda=lambda: [], 134 ), 135 "openai_compatible_providers": ConfigProperty( 136 list, 137 default_lambda=lambda: [], 138 sensitive_keys=["api_key"], 139 ), 140 } 141 self._settings = self.load_settings() 142 143 @classmethod 144 def shared(cls): 145 if cls._shared_instance is None: 146 cls._shared_instance = cls() 147 return cls._shared_instance 148 149 # Get a value, mockable for testing 150 def get_value(self, name: str) -> Any: 151 try: 152 return self.__getattr__(name) 153 except AttributeError: 154 return None 155 156 def __getattr__(self, name: str) -> Any: 157 if name == "_properties": 158 return super().__getattribute__("_properties") 159 if name not in self._properties: 160 return super().__getattribute__(name) 161 162 property_config = self._properties[name] 163 164 # Check if the value is in settings 165 if name in self._settings: 166 value = self._settings[name] 167 return value if value is None else property_config.type(value) 168 169 # Check environment variable 170 if property_config.env_var and property_config.env_var in os.environ: 171 value = os.environ[property_config.env_var] 172 return property_config.type(value) 173 174 # Use default value or default_lambda 175 if property_config.default_lambda: 176 value = property_config.default_lambda() 177 else: 178 value = property_config.default 179 180 return None if value is None else property_config.type(value) 181 182 def __setattr__(self, name, value): 183 if name in ("_properties", "_settings"): 184 super().__setattr__(name, value) 185 elif name in self._properties: 186 self.update_settings({name: value}) 187 else: 188 raise AttributeError(f"Config has no attribute '{name}'") 189 190 @classmethod 191 def settings_dir(cls, create=True): 192 settings_dir = os.path.join(Path.home(), ".kiln_ai") 193 if create and not os.path.exists(settings_dir): 194 os.makedirs(settings_dir) 195 return settings_dir 196 197 @classmethod 198 def settings_path(cls, create=True): 199 settings_dir = cls.settings_dir(create) 200 return os.path.join(settings_dir, "settings.yaml") 201 202 @classmethod 203 def load_settings(cls): 204 if not os.path.isfile(cls.settings_path(create=False)): 205 return {} 206 with open(cls.settings_path(), "r") as f: 207 settings = yaml.safe_load(f.read()) or {} 208 return settings 209 210 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 211 if not hide_sensitive: 212 return self._settings 213 214 settings = { 215 k: "[hidden]" 216 if k in self._properties and self._properties[k].sensitive 217 else v 218 for k, v in self._settings.items() 219 } 220 # Hide sensitive keys in lists. Could generalize this if we every have more types, but right not it's only needed for root elements of lists 221 for key, value in settings.items(): 222 if key in self._properties and self._properties[key].sensitive_keys: 223 sensitive_keys = self._properties[key].sensitive_keys or [] 224 for sensitive_key in sensitive_keys: 225 if isinstance(value, list): 226 for item in value: 227 if sensitive_key in item: 228 item[sensitive_key] = "[hidden]" 229 230 return settings 231 232 def save_setting(self, name: str, value: Any): 233 self.update_settings({name: value}) 234 235 def update_settings(self, new_settings: Dict[str, Any]): 236 # Lock to prevent race conditions in multi-threaded scenarios 237 with threading.Lock(): 238 # Fresh load to avoid clobbering changes from other instances 239 current_settings = self.load_settings() 240 current_settings.update(new_settings) 241 # remove None values 242 current_settings = { 243 k: v for k, v in current_settings.items() if v is not None 244 } 245 with open(self.settings_path(), "w") as f: 246 yaml.dump(current_settings, f) 247 self._settings = current_settings 248 249 250def _get_user_id(): 251 try: 252 return getpass.getuser() or "unknown_user" 253 except Exception: 254 return "unknown_user"
class
ConfigProperty:
11class ConfigProperty: 12 def __init__( 13 self, 14 type_: type, 15 default: Any = None, 16 env_var: Optional[str] = None, 17 default_lambda: Optional[Callable[[], Any]] = None, 18 sensitive: bool = False, 19 sensitive_keys: Optional[List[str]] = None, 20 ): 21 self.type = type_ 22 self.default = default 23 self.env_var = env_var 24 self.default_lambda = default_lambda 25 self.sensitive = sensitive 26 self.sensitive_keys = sensitive_keys
ConfigProperty( type_: type, default: Any = None, env_var: Optional[str] = None, default_lambda: Optional[Callable[[], Any]] = None, sensitive: bool = False, sensitive_keys: Optional[List[str]] = None)
12 def __init__( 13 self, 14 type_: type, 15 default: Any = None, 16 env_var: Optional[str] = None, 17 default_lambda: Optional[Callable[[], Any]] = None, 18 sensitive: bool = False, 19 sensitive_keys: Optional[List[str]] = None, 20 ): 21 self.type = type_ 22 self.default = default 23 self.env_var = env_var 24 self.default_lambda = default_lambda 25 self.sensitive = sensitive 26 self.sensitive_keys = sensitive_keys
class
Config:
29class Config: 30 _shared_instance = None 31 32 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 33 self._properties: Dict[str, ConfigProperty] = properties or { 34 "user_id": ConfigProperty( 35 str, 36 env_var="KILN_USER_ID", 37 default_lambda=_get_user_id, 38 ), 39 "autosave_runs": ConfigProperty( 40 bool, 41 env_var="KILN_AUTOSAVE_RUNS", 42 default=True, 43 ), 44 "open_ai_api_key": ConfigProperty( 45 str, 46 env_var="OPENAI_API_KEY", 47 sensitive=True, 48 ), 49 "groq_api_key": ConfigProperty( 50 str, 51 env_var="GROQ_API_KEY", 52 sensitive=True, 53 ), 54 "ollama_base_url": ConfigProperty( 55 str, 56 env_var="OLLAMA_BASE_URL", 57 ), 58 "bedrock_access_key": ConfigProperty( 59 str, 60 env_var="AWS_ACCESS_KEY_ID", 61 sensitive=True, 62 ), 63 "bedrock_secret_key": ConfigProperty( 64 str, 65 env_var="AWS_SECRET_ACCESS_KEY", 66 sensitive=True, 67 ), 68 "open_router_api_key": ConfigProperty( 69 str, 70 env_var="OPENROUTER_API_KEY", 71 sensitive=True, 72 ), 73 "fireworks_api_key": ConfigProperty( 74 str, 75 env_var="FIREWORKS_API_KEY", 76 sensitive=True, 77 ), 78 "fireworks_account_id": ConfigProperty( 79 str, 80 env_var="FIREWORKS_ACCOUNT_ID", 81 ), 82 "anthropic_api_key": ConfigProperty( 83 str, 84 env_var="ANTHROPIC_API_KEY", 85 sensitive=True, 86 ), 87 "gemini_api_key": ConfigProperty( 88 str, 89 env_var="GEMINI_API_KEY", 90 sensitive=True, 91 ), 92 "projects": ConfigProperty( 93 list, 94 default_lambda=lambda: [], 95 ), 96 "azure_openai_api_key": ConfigProperty( 97 str, 98 env_var="AZURE_OPENAI_API_KEY", 99 sensitive=True, 100 ), 101 "azure_openai_endpoint": ConfigProperty( 102 str, 103 env_var="AZURE_OPENAI_ENDPOINT", 104 ), 105 "huggingface_api_key": ConfigProperty( 106 str, 107 env_var="HUGGINGFACE_API_KEY", 108 sensitive=True, 109 ), 110 "vertex_project_id": ConfigProperty( 111 str, 112 env_var="VERTEX_PROJECT_ID", 113 ), 114 "vertex_location": ConfigProperty( 115 str, 116 env_var="VERTEX_LOCATION", 117 ), 118 "together_api_key": ConfigProperty( 119 str, 120 env_var="TOGETHERAI_API_KEY", 121 sensitive=True, 122 ), 123 "wandb_api_key": ConfigProperty( 124 str, 125 env_var="WANDB_API_KEY", 126 sensitive=True, 127 ), 128 "wandb_base_url": ConfigProperty( 129 str, 130 env_var="WANDB_BASE_URL", 131 ), 132 "custom_models": ConfigProperty( 133 list, 134 default_lambda=lambda: [], 135 ), 136 "openai_compatible_providers": ConfigProperty( 137 list, 138 default_lambda=lambda: [], 139 sensitive_keys=["api_key"], 140 ), 141 } 142 self._settings = self.load_settings() 143 144 @classmethod 145 def shared(cls): 146 if cls._shared_instance is None: 147 cls._shared_instance = cls() 148 return cls._shared_instance 149 150 # Get a value, mockable for testing 151 def get_value(self, name: str) -> Any: 152 try: 153 return self.__getattr__(name) 154 except AttributeError: 155 return None 156 157 def __getattr__(self, name: str) -> Any: 158 if name == "_properties": 159 return super().__getattribute__("_properties") 160 if name not in self._properties: 161 return super().__getattribute__(name) 162 163 property_config = self._properties[name] 164 165 # Check if the value is in settings 166 if name in self._settings: 167 value = self._settings[name] 168 return value if value is None else property_config.type(value) 169 170 # Check environment variable 171 if property_config.env_var and property_config.env_var in os.environ: 172 value = os.environ[property_config.env_var] 173 return property_config.type(value) 174 175 # Use default value or default_lambda 176 if property_config.default_lambda: 177 value = property_config.default_lambda() 178 else: 179 value = property_config.default 180 181 return None if value is None else property_config.type(value) 182 183 def __setattr__(self, name, value): 184 if name in ("_properties", "_settings"): 185 super().__setattr__(name, value) 186 elif name in self._properties: 187 self.update_settings({name: value}) 188 else: 189 raise AttributeError(f"Config has no attribute '{name}'") 190 191 @classmethod 192 def settings_dir(cls, create=True): 193 settings_dir = os.path.join(Path.home(), ".kiln_ai") 194 if create and not os.path.exists(settings_dir): 195 os.makedirs(settings_dir) 196 return settings_dir 197 198 @classmethod 199 def settings_path(cls, create=True): 200 settings_dir = cls.settings_dir(create) 201 return os.path.join(settings_dir, "settings.yaml") 202 203 @classmethod 204 def load_settings(cls): 205 if not os.path.isfile(cls.settings_path(create=False)): 206 return {} 207 with open(cls.settings_path(), "r") as f: 208 settings = yaml.safe_load(f.read()) or {} 209 return settings 210 211 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 212 if not hide_sensitive: 213 return self._settings 214 215 settings = { 216 k: "[hidden]" 217 if k in self._properties and self._properties[k].sensitive 218 else v 219 for k, v in self._settings.items() 220 } 221 # Hide sensitive keys in lists. Could generalize this if we every have more types, but right not it's only needed for root elements of lists 222 for key, value in settings.items(): 223 if key in self._properties and self._properties[key].sensitive_keys: 224 sensitive_keys = self._properties[key].sensitive_keys or [] 225 for sensitive_key in sensitive_keys: 226 if isinstance(value, list): 227 for item in value: 228 if sensitive_key in item: 229 item[sensitive_key] = "[hidden]" 230 231 return settings 232 233 def save_setting(self, name: str, value: Any): 234 self.update_settings({name: value}) 235 236 def update_settings(self, new_settings: Dict[str, Any]): 237 # Lock to prevent race conditions in multi-threaded scenarios 238 with threading.Lock(): 239 # Fresh load to avoid clobbering changes from other instances 240 current_settings = self.load_settings() 241 current_settings.update(new_settings) 242 # remove None values 243 current_settings = { 244 k: v for k, v in current_settings.items() if v is not None 245 } 246 with open(self.settings_path(), "w") as f: 247 yaml.dump(current_settings, f) 248 self._settings = current_settings
Config( properties: Optional[Dict[str, ConfigProperty]] = None)
32 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 33 self._properties: Dict[str, ConfigProperty] = properties or { 34 "user_id": ConfigProperty( 35 str, 36 env_var="KILN_USER_ID", 37 default_lambda=_get_user_id, 38 ), 39 "autosave_runs": ConfigProperty( 40 bool, 41 env_var="KILN_AUTOSAVE_RUNS", 42 default=True, 43 ), 44 "open_ai_api_key": ConfigProperty( 45 str, 46 env_var="OPENAI_API_KEY", 47 sensitive=True, 48 ), 49 "groq_api_key": ConfigProperty( 50 str, 51 env_var="GROQ_API_KEY", 52 sensitive=True, 53 ), 54 "ollama_base_url": ConfigProperty( 55 str, 56 env_var="OLLAMA_BASE_URL", 57 ), 58 "bedrock_access_key": ConfigProperty( 59 str, 60 env_var="AWS_ACCESS_KEY_ID", 61 sensitive=True, 62 ), 63 "bedrock_secret_key": ConfigProperty( 64 str, 65 env_var="AWS_SECRET_ACCESS_KEY", 66 sensitive=True, 67 ), 68 "open_router_api_key": ConfigProperty( 69 str, 70 env_var="OPENROUTER_API_KEY", 71 sensitive=True, 72 ), 73 "fireworks_api_key": ConfigProperty( 74 str, 75 env_var="FIREWORKS_API_KEY", 76 sensitive=True, 77 ), 78 "fireworks_account_id": ConfigProperty( 79 str, 80 env_var="FIREWORKS_ACCOUNT_ID", 81 ), 82 "anthropic_api_key": ConfigProperty( 83 str, 84 env_var="ANTHROPIC_API_KEY", 85 sensitive=True, 86 ), 87 "gemini_api_key": ConfigProperty( 88 str, 89 env_var="GEMINI_API_KEY", 90 sensitive=True, 91 ), 92 "projects": ConfigProperty( 93 list, 94 default_lambda=lambda: [], 95 ), 96 "azure_openai_api_key": ConfigProperty( 97 str, 98 env_var="AZURE_OPENAI_API_KEY", 99 sensitive=True, 100 ), 101 "azure_openai_endpoint": ConfigProperty( 102 str, 103 env_var="AZURE_OPENAI_ENDPOINT", 104 ), 105 "huggingface_api_key": ConfigProperty( 106 str, 107 env_var="HUGGINGFACE_API_KEY", 108 sensitive=True, 109 ), 110 "vertex_project_id": ConfigProperty( 111 str, 112 env_var="VERTEX_PROJECT_ID", 113 ), 114 "vertex_location": ConfigProperty( 115 str, 116 env_var="VERTEX_LOCATION", 117 ), 118 "together_api_key": ConfigProperty( 119 str, 120 env_var="TOGETHERAI_API_KEY", 121 sensitive=True, 122 ), 123 "wandb_api_key": ConfigProperty( 124 str, 125 env_var="WANDB_API_KEY", 126 sensitive=True, 127 ), 128 "wandb_base_url": ConfigProperty( 129 str, 130 env_var="WANDB_BASE_URL", 131 ), 132 "custom_models": ConfigProperty( 133 list, 134 default_lambda=lambda: [], 135 ), 136 "openai_compatible_providers": ConfigProperty( 137 list, 138 default_lambda=lambda: [], 139 sensitive_keys=["api_key"], 140 ), 141 } 142 self._settings = self.load_settings()
def
settings(self, hide_sensitive=False) -> Dict[str, Any]:
211 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 212 if not hide_sensitive: 213 return self._settings 214 215 settings = { 216 k: "[hidden]" 217 if k in self._properties and self._properties[k].sensitive 218 else v 219 for k, v in self._settings.items() 220 } 221 # Hide sensitive keys in lists. Could generalize this if we every have more types, but right not it's only needed for root elements of lists 222 for key, value in settings.items(): 223 if key in self._properties and self._properties[key].sensitive_keys: 224 sensitive_keys = self._properties[key].sensitive_keys or [] 225 for sensitive_key in sensitive_keys: 226 if isinstance(value, list): 227 for item in value: 228 if sensitive_key in item: 229 item[sensitive_key] = "[hidden]" 230 231 return settings
def
update_settings(self, new_settings: Dict[str, Any]):
236 def update_settings(self, new_settings: Dict[str, Any]): 237 # Lock to prevent race conditions in multi-threaded scenarios 238 with threading.Lock(): 239 # Fresh load to avoid clobbering changes from other instances 240 current_settings = self.load_settings() 241 current_settings.update(new_settings) 242 # remove None values 243 current_settings = { 244 k: v for k, v in current_settings.items() if v is not None 245 } 246 with open(self.settings_path(), "w") as f: 247 yaml.dump(current_settings, f) 248 self._settings = current_settings