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._lock = threading.Lock()
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", "_lock"):
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 self._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
249
250
251def _get_user_id():
252    try:
253        return getpass.getuser() or "unknown_user"
254    except Exception:
255        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
type
default
env_var
default_lambda
sensitive
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._lock = threading.Lock()
143        self._settings = self.load_settings()
144
145    @classmethod
146    def shared(cls):
147        if cls._shared_instance is None:
148            cls._shared_instance = cls()
149        return cls._shared_instance
150
151    # Get a value, mockable for testing
152    def get_value(self, name: str) -> Any:
153        try:
154            return self.__getattr__(name)
155        except AttributeError:
156            return None
157
158    def __getattr__(self, name: str) -> Any:
159        if name == "_properties":
160            return super().__getattribute__("_properties")
161        if name not in self._properties:
162            return super().__getattribute__(name)
163
164        property_config = self._properties[name]
165
166        # Check if the value is in settings
167        if name in self._settings:
168            value = self._settings[name]
169            return value if value is None else property_config.type(value)
170
171        # Check environment variable
172        if property_config.env_var and property_config.env_var in os.environ:
173            value = os.environ[property_config.env_var]
174            return property_config.type(value)
175
176        # Use default value or default_lambda
177        if property_config.default_lambda:
178            value = property_config.default_lambda()
179        else:
180            value = property_config.default
181
182        return None if value is None else property_config.type(value)
183
184    def __setattr__(self, name, value):
185        if name in ("_properties", "_settings", "_lock"):
186            super().__setattr__(name, value)
187        elif name in self._properties:
188            self.update_settings({name: value})
189        else:
190            raise AttributeError(f"Config has no attribute '{name}'")
191
192    @classmethod
193    def settings_dir(cls, create=True):
194        settings_dir = os.path.join(Path.home(), ".kiln_ai")
195        if create and not os.path.exists(settings_dir):
196            os.makedirs(settings_dir)
197        return settings_dir
198
199    @classmethod
200    def settings_path(cls, create=True):
201        settings_dir = cls.settings_dir(create)
202        return os.path.join(settings_dir, "settings.yaml")
203
204    @classmethod
205    def load_settings(cls):
206        if not os.path.isfile(cls.settings_path(create=False)):
207            return {}
208        with open(cls.settings_path(), "r") as f:
209            settings = yaml.safe_load(f.read()) or {}
210        return settings
211
212    def settings(self, hide_sensitive=False) -> Dict[str, Any]:
213        if not hide_sensitive:
214            return self._settings
215
216        settings = {
217            k: "[hidden]"
218            if k in self._properties and self._properties[k].sensitive
219            else v
220            for k, v in self._settings.items()
221        }
222        # 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
223        for key, value in settings.items():
224            if key in self._properties and self._properties[key].sensitive_keys:
225                sensitive_keys = self._properties[key].sensitive_keys or []
226                for sensitive_key in sensitive_keys:
227                    if isinstance(value, list):
228                        for item in value:
229                            if sensitive_key in item:
230                                item[sensitive_key] = "[hidden]"
231
232        return settings
233
234    def save_setting(self, name: str, value: Any):
235        self.update_settings({name: value})
236
237    def update_settings(self, new_settings: Dict[str, Any]):
238        # Lock to prevent race conditions in multi-threaded scenarios
239        with self._lock:
240            # Fresh load to avoid clobbering changes from other instances
241            current_settings = self.load_settings()
242            current_settings.update(new_settings)
243            # remove None values
244            current_settings = {
245                k: v for k, v in current_settings.items() if v is not None
246            }
247            with open(self.settings_path(), "w") as f:
248                yaml.dump(current_settings, f)
249            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._lock = threading.Lock()
143        self._settings = self.load_settings()
@classmethod
def shared(cls):
145    @classmethod
146    def shared(cls):
147        if cls._shared_instance is None:
148            cls._shared_instance = cls()
149        return cls._shared_instance
def get_value(self, name: str) -> Any:
152    def get_value(self, name: str) -> Any:
153        try:
154            return self.__getattr__(name)
155        except AttributeError:
156            return None
@classmethod
def settings_dir(cls, create=True):
192    @classmethod
193    def settings_dir(cls, create=True):
194        settings_dir = os.path.join(Path.home(), ".kiln_ai")
195        if create and not os.path.exists(settings_dir):
196            os.makedirs(settings_dir)
197        return settings_dir
@classmethod
def settings_path(cls, create=True):
199    @classmethod
200    def settings_path(cls, create=True):
201        settings_dir = cls.settings_dir(create)
202        return os.path.join(settings_dir, "settings.yaml")
@classmethod
def load_settings(cls):
204    @classmethod
205    def load_settings(cls):
206        if not os.path.isfile(cls.settings_path(create=False)):
207            return {}
208        with open(cls.settings_path(), "r") as f:
209            settings = yaml.safe_load(f.read()) or {}
210        return settings
def settings(self, hide_sensitive=False) -> Dict[str, Any]:
212    def settings(self, hide_sensitive=False) -> Dict[str, Any]:
213        if not hide_sensitive:
214            return self._settings
215
216        settings = {
217            k: "[hidden]"
218            if k in self._properties and self._properties[k].sensitive
219            else v
220            for k, v in self._settings.items()
221        }
222        # 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
223        for key, value in settings.items():
224            if key in self._properties and self._properties[key].sensitive_keys:
225                sensitive_keys = self._properties[key].sensitive_keys or []
226                for sensitive_key in sensitive_keys:
227                    if isinstance(value, list):
228                        for item in value:
229                            if sensitive_key in item:
230                                item[sensitive_key] = "[hidden]"
231
232        return settings
def save_setting(self, name: str, value: Any):
234    def save_setting(self, name: str, value: Any):
235        self.update_settings({name: value})
def update_settings(self, new_settings: Dict[str, Any]):
237    def update_settings(self, new_settings: Dict[str, Any]):
238        # Lock to prevent race conditions in multi-threaded scenarios
239        with self._lock:
240            # Fresh load to avoid clobbering changes from other instances
241            current_settings = self.load_settings()
242            current_settings.update(new_settings)
243            # remove None values
244            current_settings = {
245                k: v for k, v in current_settings.items() if v is not None
246            }
247            with open(self.settings_path(), "w") as f:
248                yaml.dump(current_settings, f)
249            self._settings = current_settings