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 } 173 self._lock = threading.Lock() 174 self._settings = self.load_settings() 175 176 @classmethod 177 def shared(cls): 178 if cls._shared_instance is None: 179 cls._shared_instance = cls() 180 return cls._shared_instance 181 182 # Get a value, mockable for testing 183 def get_value(self, name: str) -> Any: 184 try: 185 return self.__getattr__(name) 186 except AttributeError: 187 return None 188 189 def __getattr__(self, name: str) -> Any: 190 if name == "_properties": 191 return super().__getattribute__("_properties") 192 if name not in self._properties: 193 return super().__getattribute__(name) 194 195 property_config = self._properties[name] 196 197 # Check if the value is in settings 198 if name in self._settings: 199 value = self._settings[name] 200 return value if value is None else property_config.type(value) 201 202 # Check environment variable 203 if property_config.env_var and property_config.env_var in os.environ: 204 value = os.environ[property_config.env_var] 205 return property_config.type(value) 206 207 # Use default value or default_lambda 208 if property_config.default_lambda: 209 value = property_config.default_lambda() 210 else: 211 value = property_config.default 212 213 return None if value is None else property_config.type(value) 214 215 def __setattr__(self, name, value): 216 if name in ("_properties", "_settings", "_lock"): 217 super().__setattr__(name, value) 218 elif name in self._properties: 219 self.update_settings({name: value}) 220 else: 221 raise AttributeError(f"Config has no attribute '{name}'") 222 223 @classmethod 224 def settings_dir(cls, create=True): 225 settings_dir = os.path.join(Path.home(), ".kiln_ai") 226 if create and not os.path.exists(settings_dir): 227 os.makedirs(settings_dir) 228 return settings_dir 229 230 @classmethod 231 def settings_path(cls, create=True): 232 settings_dir = cls.settings_dir(create) 233 return os.path.join(settings_dir, "settings.yaml") 234 235 @classmethod 236 def load_settings(cls): 237 if not os.path.isfile(cls.settings_path(create=False)): 238 return {} 239 with open(cls.settings_path(), "r") as f: 240 settings = yaml.safe_load(f.read()) or {} 241 return settings 242 243 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 244 if not hide_sensitive: 245 return self._settings 246 247 settings = { 248 k: "[hidden]" 249 if k in self._properties and self._properties[k].sensitive 250 else v 251 for k, v in self._settings.items() 252 } 253 # 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 254 for key, value in settings.items(): 255 if key in self._properties and self._properties[key].sensitive_keys: 256 sensitive_keys = self._properties[key].sensitive_keys or [] 257 for sensitive_key in sensitive_keys: 258 if isinstance(value, list): 259 for item in value: 260 if sensitive_key in item: 261 item[sensitive_key] = "[hidden]" 262 263 return settings 264 265 def save_setting(self, name: str, value: Any): 266 self.update_settings({name: value}) 267 268 def update_settings(self, new_settings: Dict[str, Any]): 269 # Lock to prevent race conditions in multi-threaded scenarios 270 with self._lock: 271 # Fresh load to avoid clobbering changes from other instances 272 current_settings = self.load_settings() 273 current_settings.update(new_settings) 274 # remove None values 275 current_settings = { 276 k: v for k, v in current_settings.items() if v is not None 277 } 278 with open(self.settings_path(), "w") as f: 279 yaml.dump(current_settings, f) 280 self._settings = current_settings 281 282 283def _get_user_id(): 284 try: 285 return getpass.getuser() or "unknown_user" 286 except Exception: 287 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 } 174 self._lock = threading.Lock() 175 self._settings = self.load_settings() 176 177 @classmethod 178 def shared(cls): 179 if cls._shared_instance is None: 180 cls._shared_instance = cls() 181 return cls._shared_instance 182 183 # Get a value, mockable for testing 184 def get_value(self, name: str) -> Any: 185 try: 186 return self.__getattr__(name) 187 except AttributeError: 188 return None 189 190 def __getattr__(self, name: str) -> Any: 191 if name == "_properties": 192 return super().__getattribute__("_properties") 193 if name not in self._properties: 194 return super().__getattribute__(name) 195 196 property_config = self._properties[name] 197 198 # Check if the value is in settings 199 if name in self._settings: 200 value = self._settings[name] 201 return value if value is None else property_config.type(value) 202 203 # Check environment variable 204 if property_config.env_var and property_config.env_var in os.environ: 205 value = os.environ[property_config.env_var] 206 return property_config.type(value) 207 208 # Use default value or default_lambda 209 if property_config.default_lambda: 210 value = property_config.default_lambda() 211 else: 212 value = property_config.default 213 214 return None if value is None else property_config.type(value) 215 216 def __setattr__(self, name, value): 217 if name in ("_properties", "_settings", "_lock"): 218 super().__setattr__(name, value) 219 elif name in self._properties: 220 self.update_settings({name: value}) 221 else: 222 raise AttributeError(f"Config has no attribute '{name}'") 223 224 @classmethod 225 def settings_dir(cls, create=True): 226 settings_dir = os.path.join(Path.home(), ".kiln_ai") 227 if create and not os.path.exists(settings_dir): 228 os.makedirs(settings_dir) 229 return settings_dir 230 231 @classmethod 232 def settings_path(cls, create=True): 233 settings_dir = cls.settings_dir(create) 234 return os.path.join(settings_dir, "settings.yaml") 235 236 @classmethod 237 def load_settings(cls): 238 if not os.path.isfile(cls.settings_path(create=False)): 239 return {} 240 with open(cls.settings_path(), "r") as f: 241 settings = yaml.safe_load(f.read()) or {} 242 return settings 243 244 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 245 if not hide_sensitive: 246 return self._settings 247 248 settings = { 249 k: "[hidden]" 250 if k in self._properties and self._properties[k].sensitive 251 else v 252 for k, v in self._settings.items() 253 } 254 # 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 255 for key, value in settings.items(): 256 if key in self._properties and self._properties[key].sensitive_keys: 257 sensitive_keys = self._properties[key].sensitive_keys or [] 258 for sensitive_key in sensitive_keys: 259 if isinstance(value, list): 260 for item in value: 261 if sensitive_key in item: 262 item[sensitive_key] = "[hidden]" 263 264 return settings 265 266 def save_setting(self, name: str, value: Any): 267 self.update_settings({name: value}) 268 269 def update_settings(self, new_settings: Dict[str, Any]): 270 # Lock to prevent race conditions in multi-threaded scenarios 271 with self._lock: 272 # Fresh load to avoid clobbering changes from other instances 273 current_settings = self.load_settings() 274 current_settings.update(new_settings) 275 # remove None values 276 current_settings = { 277 k: v for k, v in current_settings.items() if v is not None 278 } 279 with open(self.settings_path(), "w") as f: 280 yaml.dump(current_settings, f) 281 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 } 174 self._lock = threading.Lock() 175 self._settings = self.load_settings()
def
settings(self, hide_sensitive=False) -> Dict[str, Any]:
244 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 245 if not hide_sensitive: 246 return self._settings 247 248 settings = { 249 k: "[hidden]" 250 if k in self._properties and self._properties[k].sensitive 251 else v 252 for k, v in self._settings.items() 253 } 254 # 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 255 for key, value in settings.items(): 256 if key in self._properties and self._properties[key].sensitive_keys: 257 sensitive_keys = self._properties[key].sensitive_keys or [] 258 for sensitive_key in sensitive_keys: 259 if isinstance(value, list): 260 for item in value: 261 if sensitive_key in item: 262 item[sensitive_key] = "[hidden]" 263 264 return settings
def
update_settings(self, new_settings: Dict[str, Any]):
269 def update_settings(self, new_settings: Dict[str, Any]): 270 # Lock to prevent race conditions in multi-threaded scenarios 271 with self._lock: 272 # Fresh load to avoid clobbering changes from other instances 273 current_settings = self.load_settings() 274 current_settings.update(new_settings) 275 # remove None values 276 current_settings = { 277 k: v for k, v in current_settings.items() if v is not None 278 } 279 with open(self.settings_path(), "w") as f: 280 yaml.dump(current_settings, f) 281 self._settings = current_settings