Coverage for src / local_deep_research / api / settings_utils.py: 92%
152 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +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 # noqa: F401
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) as e:
60 logger.warning(
61 f"Failed to convert value {value} to type {setting_type}: {e}"
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(json_file.read_text())
96 # Merge into main settings
97 for key, setting_data in engine_settings.items():
98 if key not in self._settings: 98 ↛ 99line 98 didn't jump to line 99 because the condition on line 98 was never true
99 self._settings[key] = setting_data.copy()
100 except Exception as e:
101 logger.warning(
102 f"Failed to load search engine config from {json_file.name}: {e}"
103 )
104 except Exception as e:
105 logger.warning(f"Failed to load search engine configs: {e}")
107 def get_setting(
108 self, key: str, default: Any = None, check_env: bool = True
109 ) -> Any:
110 """Get a setting value."""
111 if key in self._settings:
112 setting_data = self._settings[key]
113 value = setting_data.get("value", default)
114 # Ensure the value has the correct type
115 return self._get_typed_value(setting_data, value)
116 return default
118 def set_setting(self, key: str, value: Any, commit: bool = True) -> bool:
119 """Set a setting value (in memory only)."""
120 if key in self._settings:
121 # Validate and convert the value to the correct type
122 typed_value = self._get_typed_value(self._settings[key], value)
123 self._settings[key]["value"] = typed_value
124 return True
125 return False
127 def get_all_settings(self) -> dict[str, Any]:
128 """Get all settings with metadata."""
129 return copy.deepcopy(self._settings)
131 def load_from_defaults_file(
132 self, commit: bool = True, **kwargs: Any
133 ) -> None:
134 """Reload defaults (already done in __init__)."""
135 self._load_defaults()
137 def create_or_update_setting(
138 self, setting: dict[str, Any] | Any, commit: bool = True
139 ) -> Any | None:
140 """Create or update a setting (in memory only)."""
141 if isinstance(setting, dict) and "key" in setting:
142 key = setting["key"]
143 # If the setting has a value, ensure it has the correct type
144 if "value" in setting: 144 ↛ 148line 144 didn't jump to line 148 because the condition on line 144 was always true
145 typed_value = self._get_typed_value(setting, setting["value"])
146 setting = setting.copy() # Don't modify the original
147 setting["value"] = typed_value
148 self._settings[key] = setting
149 return setting
150 return None
152 def delete_setting(self, key: str, commit: bool = True) -> bool:
153 """Delete a setting (in memory only)."""
154 if key in self._settings:
155 del self._settings[key]
156 return True
157 return False
159 def get_bool_setting(
160 self, key: str, default: bool = False, check_env: bool = True
161 ) -> bool:
162 """Get a setting value as a boolean."""
163 value = self.get_setting(key, default, check_env)
164 return to_bool(value, default)
166 def get_settings_snapshot(self) -> dict[str, Any]:
167 """Get a simplified settings snapshot with just key-value pairs."""
168 all_settings = self.get_all_settings()
169 snapshot = {}
170 for key, setting in all_settings.items():
171 if isinstance(setting, dict) and "value" in setting:
172 snapshot[key] = setting["value"]
173 else:
174 snapshot[key] = setting
175 return snapshot
177 def import_settings(
178 self,
179 settings_data: dict[str, Any],
180 commit: bool = True,
181 overwrite: bool = True,
182 delete_extra: bool = False,
183 ) -> None:
184 """Import settings from a dictionary."""
185 if delete_extra:
186 self._settings.clear()
188 for key, value in settings_data.items():
189 if overwrite or key not in self._settings:
190 # Ensure proper type handling for imported settings
191 if isinstance(value, dict) and "value" in value:
192 typed_value = self._get_typed_value(value, value["value"])
193 value = value.copy()
194 value["value"] = typed_value
195 self._settings[key] = value
198def get_default_settings_snapshot() -> dict[str, Any]:
199 """
200 Get a complete settings snapshot with default values.
202 This uses the same mechanism as the web interface but without
203 requiring database access. Environment variables are checked
204 for overrides.
206 Returns:
207 Dict mapping setting keys to their values and metadata
208 """
209 manager = InMemorySettingsManager()
210 return manager.get_all_settings()
213def create_settings_snapshot(
214 overrides: dict[str, Any] | None = None,
215 base_settings: dict[str, Any] | None = None,
216 **kwargs,
217) -> dict[str, Any]:
218 """
219 Create a settings snapshot for the programmatic API.
221 Args:
222 overrides: Dict of setting overrides (e.g., {"llm.provider": "openai"})
223 This is the most common use case - pass a dict of settings to override.
224 base_settings: Base settings dict (defaults to get_default_settings_snapshot())
225 Rarely needed - only for advanced use cases.
226 **kwargs: Common setting shortcuts:
227 - provider: Maps to "llm.provider"
228 - api_key: Maps to "llm.{provider}.api_key"
229 - temperature: Maps to "llm.temperature"
230 - max_search_results: Maps to "search.max_results"
231 - search_engines: Maps to enabled search engines
233 Returns:
234 Complete settings snapshot for use with the API
236 Examples:
237 # Most common - pass overrides as first argument
238 settings = create_settings_snapshot({"search.tool": "wikipedia"})
240 # Or use named parameter
241 settings = create_settings_snapshot(overrides={"llm.provider": "openai"})
243 # Use kwargs shortcuts
244 settings = create_settings_snapshot(provider="openai", temperature=0.7)
246 # Advanced - provide custom base settings
247 settings = create_settings_snapshot(
248 overrides={"search.tool": "wikipedia"},
249 base_settings=my_custom_defaults
250 )
251 """
252 # Start with base settings or defaults
253 if base_settings is None:
254 settings = get_default_settings_snapshot()
255 else:
256 settings = copy.deepcopy(base_settings)
258 # Apply overrides if provided
259 if overrides:
260 for key, value in overrides.items():
261 if key in settings:
262 if isinstance(settings[key], dict) and "value" in settings[key]: 262 ↛ 265line 262 didn't jump to line 265 because the condition on line 262 was always true
263 settings[key]["value"] = value
264 else:
265 settings[key] = value
266 else:
267 # Create a simple setting entry for unknown keys
268 # Infer ui_element from value type
269 ui_element = "text" # default
270 if isinstance(value, bool):
271 ui_element = "checkbox"
272 elif isinstance(value, (int, float)):
273 ui_element = "number"
274 elif isinstance(value, dict):
275 ui_element = "json"
277 settings[key] = {"value": value, "ui_element": ui_element}
279 # Handle common kwargs shortcuts
280 if "provider" in kwargs:
281 provider = kwargs["provider"]
282 if "llm.provider" in settings: 282 ↛ 285line 282 didn't jump to line 285 because the condition on line 282 was always true
283 settings["llm.provider"]["value"] = provider
284 else:
285 settings["llm.provider"] = {"value": provider}
287 # Handle api_key if provided
288 if "api_key" in kwargs:
289 api_key = kwargs["api_key"]
290 api_key_setting = f"llm.{provider}.api_key"
291 if api_key_setting in settings:
292 settings[api_key_setting]["value"] = api_key
293 else:
294 settings[api_key_setting] = {"value": api_key}
296 if "temperature" in kwargs:
297 if "llm.temperature" in settings: 297 ↛ 300line 297 didn't jump to line 300 because the condition on line 297 was always true
298 settings["llm.temperature"]["value"] = kwargs["temperature"]
299 else:
300 settings["llm.temperature"] = {"value": kwargs["temperature"]}
302 if "max_search_results" in kwargs:
303 if "search.max_results" in settings: 303 ↛ 308line 303 didn't jump to line 308 because the condition on line 303 was always true
304 settings["search.max_results"]["value"] = kwargs[
305 "max_search_results"
306 ]
307 else:
308 settings["search.max_results"] = {
309 "value": kwargs["max_search_results"]
310 }
312 # Add any other common shortcuts here...
314 return settings
317def extract_setting_value(
318 settings_snapshot: dict[str, Any], key: str, default: Any = None
319) -> Any:
320 """
321 Extract a setting value from a settings snapshot.
323 Args:
324 settings_snapshot: Settings snapshot dict
325 key: Setting key (e.g., "llm.provider")
326 default: Default value if not found
328 Returns:
329 The setting value
330 """
331 if settings_snapshot is None:
332 return default
333 if key in settings_snapshot:
334 setting = settings_snapshot[key]
335 if isinstance(setting, dict) and "value" in setting:
336 return setting["value"]
337 return setting
338 return default
341def extract_bool_setting(
342 settings_snapshot: dict[str, Any], key: str, default: bool = False
343) -> bool:
344 """
345 Extract a boolean setting value from a settings snapshot.
347 This is a convenience wrapper around extract_setting_value that
348 handles string-to-boolean conversion.
350 Args:
351 settings_snapshot: Settings snapshot dict
352 key: Setting key (e.g., "local_search_normalize_vectors")
353 default: Default boolean value if not found
355 Returns:
356 Boolean value of the setting
357 """
358 value = extract_setting_value(settings_snapshot, key, default)
359 return to_bool(value, default)