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# Configuration keys 10MCP_SECRETS_KEY = "mcp_secrets" 11 12 13class ConfigProperty: 14 def __init__( 15 self, 16 type_: type, 17 default: Any = None, 18 env_var: Optional[str] = None, 19 default_lambda: Optional[Callable[[], Any]] = None, 20 sensitive: bool = False, 21 sensitive_keys: Optional[List[str]] = None, 22 ): 23 self.type = type_ 24 self.default = default 25 self.env_var = env_var 26 self.default_lambda = default_lambda 27 self.sensitive = sensitive 28 self.sensitive_keys = sensitive_keys 29 30 31class Config: 32 _shared_instance = None 33 34 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 35 self._properties: Dict[str, ConfigProperty] = properties or { 36 "user_id": ConfigProperty( 37 str, 38 env_var="KILN_USER_ID", 39 default_lambda=_get_user_id, 40 ), 41 "autosave_runs": ConfigProperty( 42 bool, 43 env_var="KILN_AUTOSAVE_RUNS", 44 default=True, 45 ), 46 "open_ai_api_key": ConfigProperty( 47 str, 48 env_var="OPENAI_API_KEY", 49 sensitive=True, 50 ), 51 "groq_api_key": ConfigProperty( 52 str, 53 env_var="GROQ_API_KEY", 54 sensitive=True, 55 ), 56 "ollama_base_url": ConfigProperty( 57 str, 58 env_var="OLLAMA_BASE_URL", 59 ), 60 "docker_model_runner_base_url": ConfigProperty( 61 str, 62 env_var="DOCKER_MODEL_RUNNER_BASE_URL", 63 ), 64 "bedrock_access_key": ConfigProperty( 65 str, 66 env_var="AWS_ACCESS_KEY_ID", 67 sensitive=True, 68 ), 69 "bedrock_secret_key": ConfigProperty( 70 str, 71 env_var="AWS_SECRET_ACCESS_KEY", 72 sensitive=True, 73 ), 74 "open_router_api_key": ConfigProperty( 75 str, 76 env_var="OPENROUTER_API_KEY", 77 sensitive=True, 78 ), 79 "fireworks_api_key": ConfigProperty( 80 str, 81 env_var="FIREWORKS_API_KEY", 82 sensitive=True, 83 ), 84 "fireworks_account_id": ConfigProperty( 85 str, 86 env_var="FIREWORKS_ACCOUNT_ID", 87 ), 88 "anthropic_api_key": ConfigProperty( 89 str, 90 env_var="ANTHROPIC_API_KEY", 91 sensitive=True, 92 ), 93 "gemini_api_key": ConfigProperty( 94 str, 95 env_var="GEMINI_API_KEY", 96 sensitive=True, 97 ), 98 "projects": ConfigProperty( 99 list, 100 default_lambda=lambda: [], 101 ), 102 "azure_openai_api_key": ConfigProperty( 103 str, 104 env_var="AZURE_OPENAI_API_KEY", 105 sensitive=True, 106 ), 107 "azure_openai_endpoint": ConfigProperty( 108 str, 109 env_var="AZURE_OPENAI_ENDPOINT", 110 ), 111 "huggingface_api_key": ConfigProperty( 112 str, 113 env_var="HUGGINGFACE_API_KEY", 114 sensitive=True, 115 ), 116 "vertex_project_id": ConfigProperty( 117 str, 118 env_var="VERTEX_PROJECT_ID", 119 ), 120 "vertex_location": ConfigProperty( 121 str, 122 env_var="VERTEX_LOCATION", 123 ), 124 "together_api_key": ConfigProperty( 125 str, 126 env_var="TOGETHERAI_API_KEY", 127 sensitive=True, 128 ), 129 "wandb_api_key": ConfigProperty( 130 str, 131 env_var="WANDB_API_KEY", 132 sensitive=True, 133 ), 134 "siliconflow_cn_api_key": ConfigProperty( 135 str, 136 env_var="SILICONFLOW_CN_API_KEY", 137 sensitive=True, 138 ), 139 "wandb_base_url": ConfigProperty( 140 str, 141 env_var="WANDB_BASE_URL", 142 ), 143 "custom_models": ConfigProperty( 144 list, 145 default_lambda=lambda: [], 146 ), 147 "openai_compatible_providers": ConfigProperty( 148 list, 149 default_lambda=lambda: [], 150 sensitive_keys=["api_key"], 151 ), 152 "cerebras_api_key": ConfigProperty( 153 str, 154 env_var="CEREBRAS_API_KEY", 155 sensitive=True, 156 ), 157 "enable_demo_tools": ConfigProperty( 158 bool, 159 env_var="ENABLE_DEMO_TOOLS", 160 default=False, 161 ), 162 # Allow the user to set the path to lookup MCP server commands, like npx. 163 "custom_mcp_path": ConfigProperty( 164 str, 165 env_var="CUSTOM_MCP_PATH", 166 ), 167 # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name 168 MCP_SECRETS_KEY: ConfigProperty( 169 dict[str, str], 170 sensitive=True, 171 ), 172 # has the user indicated it's for personal or work use? 173 "user_type": ConfigProperty( 174 str, # "personal" or "work" 175 ), 176 # if the user has provided their work contact 177 "work_use_contact": ConfigProperty( 178 str, 179 ), 180 } 181 self._lock = threading.Lock() 182 self._settings = self.load_settings() 183 184 @classmethod 185 def shared(cls): 186 if cls._shared_instance is None: 187 cls._shared_instance = cls() 188 return cls._shared_instance 189 190 # Get a value, mockable for testing 191 def get_value(self, name: str) -> Any: 192 try: 193 return self.__getattr__(name) 194 except AttributeError: 195 return None 196 197 def __getattr__(self, name: str) -> Any: 198 if name == "_properties": 199 return super().__getattribute__("_properties") 200 if name not in self._properties: 201 return super().__getattribute__(name) 202 203 property_config = self._properties[name] 204 205 # Check if the value is in settings 206 if name in self._settings: 207 value = self._settings[name] 208 return value if value is None else property_config.type(value) 209 210 # Check environment variable 211 if property_config.env_var and property_config.env_var in os.environ: 212 value = os.environ[property_config.env_var] 213 return property_config.type(value) 214 215 # Use default value or default_lambda 216 if property_config.default_lambda: 217 value = property_config.default_lambda() 218 else: 219 value = property_config.default 220 221 return None if value is None else property_config.type(value) 222 223 def __setattr__(self, name, value): 224 if name in ("_properties", "_settings", "_lock"): 225 super().__setattr__(name, value) 226 elif name in self._properties: 227 self.update_settings({name: value}) 228 else: 229 raise AttributeError(f"Config has no attribute '{name}'") 230 231 @classmethod 232 def settings_dir(cls, create=True) -> str: 233 settings_dir = os.path.join(Path.home(), ".kiln_ai") 234 if create and not os.path.exists(settings_dir): 235 os.makedirs(settings_dir) 236 return settings_dir 237 238 @classmethod 239 def settings_path(cls, create=True) -> str: 240 settings_dir = cls.settings_dir(create) 241 return os.path.join(settings_dir, "settings.yaml") 242 243 @classmethod 244 def load_settings(cls): 245 if not os.path.isfile(cls.settings_path(create=False)): 246 return {} 247 with open(cls.settings_path(), "r") as f: 248 settings = yaml.safe_load(f.read()) or {} 249 return settings 250 251 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 252 if not hide_sensitive: 253 return self._settings 254 255 settings = { 256 k: "[hidden]" 257 if k in self._properties and self._properties[k].sensitive 258 else v 259 for k, v in self._settings.items() 260 } 261 # 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 262 for key, value in settings.items(): 263 if key in self._properties and self._properties[key].sensitive_keys: 264 sensitive_keys = self._properties[key].sensitive_keys or [] 265 for sensitive_key in sensitive_keys: 266 if isinstance(value, list): 267 for item in value: 268 if sensitive_key in item: 269 item[sensitive_key] = "[hidden]" 270 271 return settings 272 273 def save_setting(self, name: str, value: Any): 274 self.update_settings({name: value}) 275 276 def update_settings(self, new_settings: Dict[str, Any]): 277 # Lock to prevent race conditions in multi-threaded scenarios 278 with self._lock: 279 # Fresh load to avoid clobbering changes from other instances 280 current_settings = self.load_settings() 281 current_settings.update(new_settings) 282 # remove None values 283 current_settings = { 284 k: v for k, v in current_settings.items() if v is not None 285 } 286 with open(self.settings_path(), "w") as f: 287 yaml.dump(current_settings, f) 288 self._settings = current_settings 289 290 291def _get_user_id(): 292 try: 293 return getpass.getuser() or "unknown_user" 294 except Exception: 295 return "unknown_user"
MCP_SECRETS_KEY =
'mcp_secrets'
class
ConfigProperty:
14class ConfigProperty: 15 def __init__( 16 self, 17 type_: type, 18 default: Any = None, 19 env_var: Optional[str] = None, 20 default_lambda: Optional[Callable[[], Any]] = None, 21 sensitive: bool = False, 22 sensitive_keys: Optional[List[str]] = None, 23 ): 24 self.type = type_ 25 self.default = default 26 self.env_var = env_var 27 self.default_lambda = default_lambda 28 self.sensitive = sensitive 29 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)
15 def __init__( 16 self, 17 type_: type, 18 default: Any = None, 19 env_var: Optional[str] = None, 20 default_lambda: Optional[Callable[[], Any]] = None, 21 sensitive: bool = False, 22 sensitive_keys: Optional[List[str]] = None, 23 ): 24 self.type = type_ 25 self.default = default 26 self.env_var = env_var 27 self.default_lambda = default_lambda 28 self.sensitive = sensitive 29 self.sensitive_keys = sensitive_keys
class
Config:
32class Config: 33 _shared_instance = None 34 35 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 36 self._properties: Dict[str, ConfigProperty] = properties or { 37 "user_id": ConfigProperty( 38 str, 39 env_var="KILN_USER_ID", 40 default_lambda=_get_user_id, 41 ), 42 "autosave_runs": ConfigProperty( 43 bool, 44 env_var="KILN_AUTOSAVE_RUNS", 45 default=True, 46 ), 47 "open_ai_api_key": ConfigProperty( 48 str, 49 env_var="OPENAI_API_KEY", 50 sensitive=True, 51 ), 52 "groq_api_key": ConfigProperty( 53 str, 54 env_var="GROQ_API_KEY", 55 sensitive=True, 56 ), 57 "ollama_base_url": ConfigProperty( 58 str, 59 env_var="OLLAMA_BASE_URL", 60 ), 61 "docker_model_runner_base_url": ConfigProperty( 62 str, 63 env_var="DOCKER_MODEL_RUNNER_BASE_URL", 64 ), 65 "bedrock_access_key": ConfigProperty( 66 str, 67 env_var="AWS_ACCESS_KEY_ID", 68 sensitive=True, 69 ), 70 "bedrock_secret_key": ConfigProperty( 71 str, 72 env_var="AWS_SECRET_ACCESS_KEY", 73 sensitive=True, 74 ), 75 "open_router_api_key": ConfigProperty( 76 str, 77 env_var="OPENROUTER_API_KEY", 78 sensitive=True, 79 ), 80 "fireworks_api_key": ConfigProperty( 81 str, 82 env_var="FIREWORKS_API_KEY", 83 sensitive=True, 84 ), 85 "fireworks_account_id": ConfigProperty( 86 str, 87 env_var="FIREWORKS_ACCOUNT_ID", 88 ), 89 "anthropic_api_key": ConfigProperty( 90 str, 91 env_var="ANTHROPIC_API_KEY", 92 sensitive=True, 93 ), 94 "gemini_api_key": ConfigProperty( 95 str, 96 env_var="GEMINI_API_KEY", 97 sensitive=True, 98 ), 99 "projects": ConfigProperty( 100 list, 101 default_lambda=lambda: [], 102 ), 103 "azure_openai_api_key": ConfigProperty( 104 str, 105 env_var="AZURE_OPENAI_API_KEY", 106 sensitive=True, 107 ), 108 "azure_openai_endpoint": ConfigProperty( 109 str, 110 env_var="AZURE_OPENAI_ENDPOINT", 111 ), 112 "huggingface_api_key": ConfigProperty( 113 str, 114 env_var="HUGGINGFACE_API_KEY", 115 sensitive=True, 116 ), 117 "vertex_project_id": ConfigProperty( 118 str, 119 env_var="VERTEX_PROJECT_ID", 120 ), 121 "vertex_location": ConfigProperty( 122 str, 123 env_var="VERTEX_LOCATION", 124 ), 125 "together_api_key": ConfigProperty( 126 str, 127 env_var="TOGETHERAI_API_KEY", 128 sensitive=True, 129 ), 130 "wandb_api_key": ConfigProperty( 131 str, 132 env_var="WANDB_API_KEY", 133 sensitive=True, 134 ), 135 "siliconflow_cn_api_key": ConfigProperty( 136 str, 137 env_var="SILICONFLOW_CN_API_KEY", 138 sensitive=True, 139 ), 140 "wandb_base_url": ConfigProperty( 141 str, 142 env_var="WANDB_BASE_URL", 143 ), 144 "custom_models": ConfigProperty( 145 list, 146 default_lambda=lambda: [], 147 ), 148 "openai_compatible_providers": ConfigProperty( 149 list, 150 default_lambda=lambda: [], 151 sensitive_keys=["api_key"], 152 ), 153 "cerebras_api_key": ConfigProperty( 154 str, 155 env_var="CEREBRAS_API_KEY", 156 sensitive=True, 157 ), 158 "enable_demo_tools": ConfigProperty( 159 bool, 160 env_var="ENABLE_DEMO_TOOLS", 161 default=False, 162 ), 163 # Allow the user to set the path to lookup MCP server commands, like npx. 164 "custom_mcp_path": ConfigProperty( 165 str, 166 env_var="CUSTOM_MCP_PATH", 167 ), 168 # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name 169 MCP_SECRETS_KEY: ConfigProperty( 170 dict[str, str], 171 sensitive=True, 172 ), 173 # has the user indicated it's for personal or work use? 174 "user_type": ConfigProperty( 175 str, # "personal" or "work" 176 ), 177 # if the user has provided their work contact 178 "work_use_contact": ConfigProperty( 179 str, 180 ), 181 } 182 self._lock = threading.Lock() 183 self._settings = self.load_settings() 184 185 @classmethod 186 def shared(cls): 187 if cls._shared_instance is None: 188 cls._shared_instance = cls() 189 return cls._shared_instance 190 191 # Get a value, mockable for testing 192 def get_value(self, name: str) -> Any: 193 try: 194 return self.__getattr__(name) 195 except AttributeError: 196 return None 197 198 def __getattr__(self, name: str) -> Any: 199 if name == "_properties": 200 return super().__getattribute__("_properties") 201 if name not in self._properties: 202 return super().__getattribute__(name) 203 204 property_config = self._properties[name] 205 206 # Check if the value is in settings 207 if name in self._settings: 208 value = self._settings[name] 209 return value if value is None else property_config.type(value) 210 211 # Check environment variable 212 if property_config.env_var and property_config.env_var in os.environ: 213 value = os.environ[property_config.env_var] 214 return property_config.type(value) 215 216 # Use default value or default_lambda 217 if property_config.default_lambda: 218 value = property_config.default_lambda() 219 else: 220 value = property_config.default 221 222 return None if value is None else property_config.type(value) 223 224 def __setattr__(self, name, value): 225 if name in ("_properties", "_settings", "_lock"): 226 super().__setattr__(name, value) 227 elif name in self._properties: 228 self.update_settings({name: value}) 229 else: 230 raise AttributeError(f"Config has no attribute '{name}'") 231 232 @classmethod 233 def settings_dir(cls, create=True) -> str: 234 settings_dir = os.path.join(Path.home(), ".kiln_ai") 235 if create and not os.path.exists(settings_dir): 236 os.makedirs(settings_dir) 237 return settings_dir 238 239 @classmethod 240 def settings_path(cls, create=True) -> str: 241 settings_dir = cls.settings_dir(create) 242 return os.path.join(settings_dir, "settings.yaml") 243 244 @classmethod 245 def load_settings(cls): 246 if not os.path.isfile(cls.settings_path(create=False)): 247 return {} 248 with open(cls.settings_path(), "r") as f: 249 settings = yaml.safe_load(f.read()) or {} 250 return settings 251 252 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 253 if not hide_sensitive: 254 return self._settings 255 256 settings = { 257 k: "[hidden]" 258 if k in self._properties and self._properties[k].sensitive 259 else v 260 for k, v in self._settings.items() 261 } 262 # 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 263 for key, value in settings.items(): 264 if key in self._properties and self._properties[key].sensitive_keys: 265 sensitive_keys = self._properties[key].sensitive_keys or [] 266 for sensitive_key in sensitive_keys: 267 if isinstance(value, list): 268 for item in value: 269 if sensitive_key in item: 270 item[sensitive_key] = "[hidden]" 271 272 return settings 273 274 def save_setting(self, name: str, value: Any): 275 self.update_settings({name: value}) 276 277 def update_settings(self, new_settings: Dict[str, Any]): 278 # Lock to prevent race conditions in multi-threaded scenarios 279 with self._lock: 280 # Fresh load to avoid clobbering changes from other instances 281 current_settings = self.load_settings() 282 current_settings.update(new_settings) 283 # remove None values 284 current_settings = { 285 k: v for k, v in current_settings.items() if v is not None 286 } 287 with open(self.settings_path(), "w") as f: 288 yaml.dump(current_settings, f) 289 self._settings = current_settings
Config( properties: Optional[Dict[str, ConfigProperty]] = None)
35 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 36 self._properties: Dict[str, ConfigProperty] = properties or { 37 "user_id": ConfigProperty( 38 str, 39 env_var="KILN_USER_ID", 40 default_lambda=_get_user_id, 41 ), 42 "autosave_runs": ConfigProperty( 43 bool, 44 env_var="KILN_AUTOSAVE_RUNS", 45 default=True, 46 ), 47 "open_ai_api_key": ConfigProperty( 48 str, 49 env_var="OPENAI_API_KEY", 50 sensitive=True, 51 ), 52 "groq_api_key": ConfigProperty( 53 str, 54 env_var="GROQ_API_KEY", 55 sensitive=True, 56 ), 57 "ollama_base_url": ConfigProperty( 58 str, 59 env_var="OLLAMA_BASE_URL", 60 ), 61 "docker_model_runner_base_url": ConfigProperty( 62 str, 63 env_var="DOCKER_MODEL_RUNNER_BASE_URL", 64 ), 65 "bedrock_access_key": ConfigProperty( 66 str, 67 env_var="AWS_ACCESS_KEY_ID", 68 sensitive=True, 69 ), 70 "bedrock_secret_key": ConfigProperty( 71 str, 72 env_var="AWS_SECRET_ACCESS_KEY", 73 sensitive=True, 74 ), 75 "open_router_api_key": ConfigProperty( 76 str, 77 env_var="OPENROUTER_API_KEY", 78 sensitive=True, 79 ), 80 "fireworks_api_key": ConfigProperty( 81 str, 82 env_var="FIREWORKS_API_KEY", 83 sensitive=True, 84 ), 85 "fireworks_account_id": ConfigProperty( 86 str, 87 env_var="FIREWORKS_ACCOUNT_ID", 88 ), 89 "anthropic_api_key": ConfigProperty( 90 str, 91 env_var="ANTHROPIC_API_KEY", 92 sensitive=True, 93 ), 94 "gemini_api_key": ConfigProperty( 95 str, 96 env_var="GEMINI_API_KEY", 97 sensitive=True, 98 ), 99 "projects": ConfigProperty( 100 list, 101 default_lambda=lambda: [], 102 ), 103 "azure_openai_api_key": ConfigProperty( 104 str, 105 env_var="AZURE_OPENAI_API_KEY", 106 sensitive=True, 107 ), 108 "azure_openai_endpoint": ConfigProperty( 109 str, 110 env_var="AZURE_OPENAI_ENDPOINT", 111 ), 112 "huggingface_api_key": ConfigProperty( 113 str, 114 env_var="HUGGINGFACE_API_KEY", 115 sensitive=True, 116 ), 117 "vertex_project_id": ConfigProperty( 118 str, 119 env_var="VERTEX_PROJECT_ID", 120 ), 121 "vertex_location": ConfigProperty( 122 str, 123 env_var="VERTEX_LOCATION", 124 ), 125 "together_api_key": ConfigProperty( 126 str, 127 env_var="TOGETHERAI_API_KEY", 128 sensitive=True, 129 ), 130 "wandb_api_key": ConfigProperty( 131 str, 132 env_var="WANDB_API_KEY", 133 sensitive=True, 134 ), 135 "siliconflow_cn_api_key": ConfigProperty( 136 str, 137 env_var="SILICONFLOW_CN_API_KEY", 138 sensitive=True, 139 ), 140 "wandb_base_url": ConfigProperty( 141 str, 142 env_var="WANDB_BASE_URL", 143 ), 144 "custom_models": ConfigProperty( 145 list, 146 default_lambda=lambda: [], 147 ), 148 "openai_compatible_providers": ConfigProperty( 149 list, 150 default_lambda=lambda: [], 151 sensitive_keys=["api_key"], 152 ), 153 "cerebras_api_key": ConfigProperty( 154 str, 155 env_var="CEREBRAS_API_KEY", 156 sensitive=True, 157 ), 158 "enable_demo_tools": ConfigProperty( 159 bool, 160 env_var="ENABLE_DEMO_TOOLS", 161 default=False, 162 ), 163 # Allow the user to set the path to lookup MCP server commands, like npx. 164 "custom_mcp_path": ConfigProperty( 165 str, 166 env_var="CUSTOM_MCP_PATH", 167 ), 168 # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name 169 MCP_SECRETS_KEY: ConfigProperty( 170 dict[str, str], 171 sensitive=True, 172 ), 173 # has the user indicated it's for personal or work use? 174 "user_type": ConfigProperty( 175 str, # "personal" or "work" 176 ), 177 # if the user has provided their work contact 178 "work_use_contact": ConfigProperty( 179 str, 180 ), 181 } 182 self._lock = threading.Lock() 183 self._settings = self.load_settings()
def
settings(self, hide_sensitive=False) -> Dict[str, Any]:
252 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 253 if not hide_sensitive: 254 return self._settings 255 256 settings = { 257 k: "[hidden]" 258 if k in self._properties and self._properties[k].sensitive 259 else v 260 for k, v in self._settings.items() 261 } 262 # 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 263 for key, value in settings.items(): 264 if key in self._properties and self._properties[key].sensitive_keys: 265 sensitive_keys = self._properties[key].sensitive_keys or [] 266 for sensitive_key in sensitive_keys: 267 if isinstance(value, list): 268 for item in value: 269 if sensitive_key in item: 270 item[sensitive_key] = "[hidden]" 271 272 return settings
def
update_settings(self, new_settings: Dict[str, Any]):
277 def update_settings(self, new_settings: Dict[str, Any]): 278 # Lock to prevent race conditions in multi-threaded scenarios 279 with self._lock: 280 # Fresh load to avoid clobbering changes from other instances 281 current_settings = self.load_settings() 282 current_settings.update(new_settings) 283 # remove None values 284 current_settings = { 285 k: v for k, v in current_settings.items() if v is not None 286 } 287 with open(self.settings_path(), "w") as f: 288 yaml.dump(current_settings, f) 289 self._settings = current_settings