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

1""" 

2Utilities for managing settings in the programmatic API. 

3 

4This module provides functions to create settings snapshots for the API 

5without requiring database access, reusing the same mechanisms as the 

6web interface. 

7""" 

8 

9import copy 

10from typing import Any 

11from loguru import logger 

12 

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 

17 

18 

19class InMemorySettingsManager(ISettingsManager): 

20 """ 

21 In-memory settings manager that doesn't require database access. 

22 

23 This is used for the programmatic API to provide settings without 

24 needing a database connection. 

25 """ 

26 

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() 

33 

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. 

37 

38 Args: 

39 setting_data: The setting metadata containing ui_element 

40 value: The value to convert 

41 

42 Returns: 

43 The typed value, or the original value if conversion fails 

44 """ 

45 if value is None: 

46 return None 

47 

48 ui_element = setting_data.get("ui_element", "text") 

49 setting_type = UI_ELEMENT_TO_SETTING_TYPE.get(ui_element) 

50 

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 

56 

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 

64 

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 

69 

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() 

73 

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 ) 

81 

82 # Load search engine configurations from individual JSON files 

83 from importlib import resources 

84 import json 

85 

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") 

91 

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}") 

106 

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 

117 

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 

126 

127 def get_all_settings(self) -> dict[str, Any]: 

128 """Get all settings with metadata.""" 

129 return copy.deepcopy(self._settings) 

130 

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() 

136 

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 

151 

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 

158 

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) 

165 

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 

176 

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() 

187 

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 

196 

197 

198def get_default_settings_snapshot() -> dict[str, Any]: 

199 """ 

200 Get a complete settings snapshot with default values. 

201 

202 This uses the same mechanism as the web interface but without 

203 requiring database access. Environment variables are checked 

204 for overrides. 

205 

206 Returns: 

207 Dict mapping setting keys to their values and metadata 

208 """ 

209 manager = InMemorySettingsManager() 

210 return manager.get_all_settings() 

211 

212 

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. 

220 

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 

232 

233 Returns: 

234 Complete settings snapshot for use with the API 

235 

236 Examples: 

237 # Most common - pass overrides as first argument 

238 settings = create_settings_snapshot({"search.tool": "wikipedia"}) 

239 

240 # Or use named parameter 

241 settings = create_settings_snapshot(overrides={"llm.provider": "openai"}) 

242 

243 # Use kwargs shortcuts 

244 settings = create_settings_snapshot(provider="openai", temperature=0.7) 

245 

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) 

257 

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" 

276 

277 settings[key] = {"value": value, "ui_element": ui_element} 

278 

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} 

286 

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} 

295 

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"]} 

301 

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 } 

311 

312 # Add any other common shortcuts here... 

313 

314 return settings 

315 

316 

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. 

322 

323 Args: 

324 settings_snapshot: Settings snapshot dict 

325 key: Setting key (e.g., "llm.provider") 

326 default: Default value if not found 

327 

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 

339 

340 

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. 

346 

347 This is a convenience wrapper around extract_setting_value that 

348 handles string-to-boolean conversion. 

349 

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 

354 

355 Returns: 

356 Boolean value of the setting 

357 """ 

358 value = extract_setting_value(settings_snapshot, key, default) 

359 return to_bool(value, default)