Coverage for src/local_deep_research/api/settings_utils.py: 93%
152 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-03 23:15 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-03 23:15 +0000
1"""
2Utilities for managing settings in the programmatic API.
4This module provides functions to create settings snapshots for the API
5without requiring database access, reusing the same mechanisms as the
6web interface.
7"""
9import copy
10from typing import Any
11from loguru import logger
13from ..settings import SettingsManager
14from ..settings.base import ISettingsManager
15from ..settings.manager import UI_ELEMENT_TO_SETTING_TYPE, check_env_setting
16from ..utilities.type_utils import to_bool
19class InMemorySettingsManager(ISettingsManager):
20 """
21 In-memory settings manager that doesn't require database access.
23 This is used for the programmatic API to provide settings without
24 needing a database connection.
25 """
27 def __init__(self):
28 """Initialize with default settings from JSON file."""
29 # Create a base manager to get default settings
30 self._base_manager = SettingsManager(db_session=None)
31 self._settings = {}
32 self._load_defaults()
34 def _get_typed_value(self, setting_data: dict[str, Any], value: Any) -> Any:
35 """
36 Convert a value to the appropriate type based on the setting's ui_element.
38 Args:
39 setting_data: The setting metadata containing ui_element
40 value: The value to convert
42 Returns:
43 The typed value, or the original value if conversion fails
44 """
45 if value is None:
46 return None
48 ui_element = setting_data.get("ui_element", "text")
49 setting_type = UI_ELEMENT_TO_SETTING_TYPE.get(ui_element)
51 if setting_type is None:
52 logger.warning(
53 f"Unknown ui_element type: {ui_element}, returning value as-is"
54 )
55 return value
57 try:
58 return setting_type(value)
59 except (ValueError, TypeError):
60 logger.warning(
61 f"Failed to convert value {value} to type {setting_type}"
62 )
63 return value
65 def _load_defaults(self):
66 """Load default settings from the JSON file."""
67 # Get default settings from the base manager
68 defaults = self._base_manager.default_settings
70 # Convert to the format expected by get_all_settings
71 for key, setting_data in defaults.items():
72 self._settings[key] = setting_data.copy()
74 # Check environment variable override
75 env_value = check_env_setting(key)
76 if env_value is not None:
77 # Use the typed value conversion
78 self._settings[key]["value"] = self._get_typed_value(
79 setting_data, env_value
80 )
82 # Load search engine configurations from individual JSON files
83 from importlib import resources
84 import json
86 try:
87 # Load search engines from defaults/settings/search_engines/
88 search_engines_dir = resources.files(
89 "local_deep_research.defaults.settings"
90 ).joinpath("search_engines")
92 if search_engines_dir.exists() and search_engines_dir.is_dir(): 92 ↛ exitline 92 didn't return from function '_load_defaults' because the condition on line 92 was always true
93 for json_file in search_engines_dir.glob("*.json"):
94 try:
95 engine_settings = json.loads(
96 json_file.read_text(encoding="utf-8-sig")
97 )
98 # Merge into main settings
99 for key, setting_data in engine_settings.items():
100 if key not in self._settings: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 self._settings[key] = setting_data.copy()
102 except Exception:
103 logger.warning(
104 f"Failed to load search engine config from {json_file.name}"
105 )
106 except Exception:
107 logger.warning("Failed to load search engine configs")
109 def get_setting(
110 self, key: str, default: Any = None, check_env: bool = True
111 ) -> Any:
112 """Get a setting value."""
113 if key in self._settings:
114 setting_data = self._settings[key]
115 value = setting_data.get("value", default)
116 # Ensure the value has the correct type
117 return self._get_typed_value(setting_data, value)
118 return default
120 def set_setting(self, key: str, value: Any, commit: bool = True) -> bool:
121 """Set a setting value (in memory only)."""
122 if key in self._settings:
123 # Validate and convert the value to the correct type
124 typed_value = self._get_typed_value(self._settings[key], value)
125 self._settings[key]["value"] = typed_value
126 return True
127 return False
129 def get_all_settings(self) -> dict[str, Any]:
130 """Get all settings with metadata."""
131 return copy.deepcopy(self._settings)
133 def load_from_defaults_file(
134 self, commit: bool = True, **kwargs: Any
135 ) -> None:
136 """Reload defaults (already done in __init__)."""
137 self._load_defaults()
139 def create_or_update_setting(
140 self, setting: dict[str, Any] | Any, commit: bool = True
141 ) -> Any | None:
142 """Create or update a setting (in memory only)."""
143 if isinstance(setting, dict) and "key" in setting:
144 key = setting["key"]
145 # If the setting has a value, ensure it has the correct type
146 if "value" in setting:
147 typed_value = self._get_typed_value(setting, setting["value"])
148 setting = setting.copy() # Don't modify the original
149 setting["value"] = typed_value
150 self._settings[key] = setting
151 return setting
152 return None
154 def delete_setting(self, key: str, commit: bool = True) -> bool:
155 """Delete a setting (in memory only)."""
156 if key in self._settings:
157 del self._settings[key]
158 return True
159 return False
161 def get_bool_setting(
162 self, key: str, default: bool = False, check_env: bool = True
163 ) -> bool:
164 """Get a setting value as a boolean."""
165 value = self.get_setting(key, default, check_env)
166 return to_bool(value, default)
168 def get_settings_snapshot(self) -> dict[str, Any]:
169 """Get a simplified settings snapshot with just key-value pairs."""
170 all_settings = self.get_all_settings()
171 snapshot = {}
172 for key, setting in all_settings.items():
173 if isinstance(setting, dict) and "value" in setting:
174 snapshot[key] = setting["value"]
175 else:
176 snapshot[key] = setting
177 return snapshot
179 def import_settings(
180 self,
181 settings_data: dict[str, Any],
182 commit: bool = True,
183 overwrite: bool = True,
184 delete_extra: bool = False,
185 ) -> None:
186 """Import settings from a dictionary."""
187 if delete_extra:
188 self._settings.clear()
190 for key, value in settings_data.items():
191 if overwrite or key not in self._settings:
192 # Ensure proper type handling for imported settings
193 if isinstance(value, dict) and "value" in value:
194 typed_value = self._get_typed_value(value, value["value"])
195 value = value.copy()
196 value["value"] = typed_value
197 self._settings[key] = value
200def get_default_settings_snapshot() -> dict[str, Any]:
201 """
202 Get a complete settings snapshot with default values.
204 This uses the same mechanism as the web interface but without
205 requiring database access. Environment variables are checked
206 for overrides.
208 Returns:
209 Dict mapping setting keys to their values and metadata
210 """
211 manager = InMemorySettingsManager()
212 return manager.get_all_settings()
215def create_settings_snapshot(
216 overrides: dict[str, Any] | None = None,
217 base_settings: dict[str, Any] | None = None,
218 **kwargs,
219) -> dict[str, Any]:
220 """
221 Create a settings snapshot for the programmatic API.
223 Args:
224 overrides: Dict of setting overrides (e.g., {"llm.provider": "openai"})
225 This is the most common use case - pass a dict of settings to override.
226 base_settings: Base settings dict (defaults to get_default_settings_snapshot())
227 Rarely needed - only for advanced use cases.
228 **kwargs: Common setting shortcuts:
229 - provider: Maps to "llm.provider"
230 - api_key: Maps to "llm.{provider}.api_key"
231 - temperature: Maps to "llm.temperature"
232 - max_search_results: Maps to "search.max_results"
233 - search_engines: Maps to enabled search engines
235 Returns:
236 Complete settings snapshot for use with the API
238 Examples:
239 # Most common - pass overrides as first argument
240 settings = create_settings_snapshot({"search.tool": "wikipedia"})
242 # Or use named parameter
243 settings = create_settings_snapshot(overrides={"llm.provider": "openai"})
245 # Use kwargs shortcuts
246 settings = create_settings_snapshot(provider="openai", temperature=0.7)
248 # Advanced - provide custom base settings
249 settings = create_settings_snapshot(
250 overrides={"search.tool": "wikipedia"},
251 base_settings=my_custom_defaults
252 )
253 """
254 # Start with base settings or defaults
255 if base_settings is None:
256 settings = get_default_settings_snapshot()
257 else:
258 settings = copy.deepcopy(base_settings)
260 # Apply overrides if provided
261 if overrides:
262 for key, value in overrides.items():
263 if key in settings:
264 if isinstance(settings[key], dict) and "value" in settings[key]: 264 ↛ 267line 264 didn't jump to line 267 because the condition on line 264 was always true
265 settings[key]["value"] = value
266 else:
267 settings[key] = value
268 else:
269 # Create a simple setting entry for unknown keys
270 # Infer ui_element from value type
271 ui_element = "text" # default
272 if isinstance(value, bool):
273 ui_element = "checkbox"
274 elif isinstance(value, (int, float)):
275 ui_element = "number"
276 elif isinstance(value, dict):
277 ui_element = "json"
279 settings[key] = {"value": value, "ui_element": ui_element}
281 # Handle common kwargs shortcuts
282 if "provider" in kwargs:
283 provider = kwargs["provider"]
284 if "llm.provider" in settings: 284 ↛ 287line 284 didn't jump to line 287 because the condition on line 284 was always true
285 settings["llm.provider"]["value"] = provider
286 else:
287 settings["llm.provider"] = {"value": provider}
289 # Handle api_key if provided
290 if "api_key" in kwargs:
291 api_key = kwargs["api_key"]
292 api_key_setting = f"llm.{provider}.api_key"
293 if api_key_setting in settings:
294 settings[api_key_setting]["value"] = api_key
295 else:
296 settings[api_key_setting] = {"value": api_key}
298 if "temperature" in kwargs:
299 if "llm.temperature" in settings: 299 ↛ 302line 299 didn't jump to line 302 because the condition on line 299 was always true
300 settings["llm.temperature"]["value"] = kwargs["temperature"]
301 else:
302 settings["llm.temperature"] = {"value": kwargs["temperature"]}
304 if "max_search_results" in kwargs:
305 if "search.max_results" in settings: 305 ↛ 310line 305 didn't jump to line 310 because the condition on line 305 was always true
306 settings["search.max_results"]["value"] = kwargs[
307 "max_search_results"
308 ]
309 else:
310 settings["search.max_results"] = {
311 "value": kwargs["max_search_results"]
312 }
314 # Add any other common shortcuts here...
316 return settings
319def extract_setting_value(
320 settings_snapshot: dict[str, Any], key: str, default: Any = None
321) -> Any:
322 """
323 Extract a setting value from a settings snapshot.
325 Args:
326 settings_snapshot: Settings snapshot dict
327 key: Setting key (e.g., "llm.provider")
328 default: Default value if not found
330 Returns:
331 The setting value
332 """
333 if settings_snapshot is None:
334 return default
335 if key in settings_snapshot:
336 setting = settings_snapshot[key]
337 if isinstance(setting, dict) and "value" in setting:
338 return setting["value"]
339 return setting
340 return default
343def extract_bool_setting(
344 settings_snapshot: dict[str, Any], key: str, default: bool = False
345) -> bool:
346 """
347 Extract a boolean setting value from a settings snapshot.
349 This is a convenience wrapper around extract_setting_value that
350 handles string-to-boolean conversion.
352 Args:
353 settings_snapshot: Settings snapshot dict
354 key: Setting key (e.g., "local_search_normalize_vectors")
355 default: Default boolean value if not found
357 Returns:
358 Boolean value of the setting
359 """
360 value = extract_setting_value(settings_snapshot, key, default)
361 return to_bool(value, default)