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

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 

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

60 logger.warning( 

61 f"Failed to convert value {value} to type {setting_type}" 

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( 

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

108 

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 

119 

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 

128 

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

130 """Get all settings with metadata.""" 

131 return copy.deepcopy(self._settings) 

132 

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

138 

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 

153 

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 

160 

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) 

167 

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 

178 

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

189 

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 

198 

199 

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

201 """ 

202 Get a complete settings snapshot with default values. 

203 

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

205 requiring database access. Environment variables are checked 

206 for overrides. 

207 

208 Returns: 

209 Dict mapping setting keys to their values and metadata 

210 """ 

211 manager = InMemorySettingsManager() 

212 return manager.get_all_settings() 

213 

214 

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. 

222 

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 

234 

235 Returns: 

236 Complete settings snapshot for use with the API 

237 

238 Examples: 

239 # Most common - pass overrides as first argument 

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

241 

242 # Or use named parameter 

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

244 

245 # Use kwargs shortcuts 

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

247 

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) 

259 

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" 

278 

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

280 

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} 

288 

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} 

297 

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

303 

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 } 

313 

314 # Add any other common shortcuts here... 

315 

316 return settings 

317 

318 

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. 

324 

325 Args: 

326 settings_snapshot: Settings snapshot dict 

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

328 default: Default value if not found 

329 

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 

341 

342 

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. 

348 

349 This is a convenience wrapper around extract_setting_value that 

350 handles string-to-boolean conversion. 

351 

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 

356 

357 Returns: 

358 Boolean value of the setting 

359 """ 

360 value = extract_setting_value(settings_snapshot, key, default) 

361 return to_bool(value, default)