local-deep-research 0.1.26__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. local_deep_research/__init__.py +23 -22
  2. local_deep_research/__main__.py +16 -0
  3. local_deep_research/advanced_search_system/__init__.py +7 -0
  4. local_deep_research/advanced_search_system/filters/__init__.py +8 -0
  5. local_deep_research/advanced_search_system/filters/base_filter.py +38 -0
  6. local_deep_research/advanced_search_system/filters/cross_engine_filter.py +200 -0
  7. local_deep_research/advanced_search_system/findings/base_findings.py +81 -0
  8. local_deep_research/advanced_search_system/findings/repository.py +452 -0
  9. local_deep_research/advanced_search_system/knowledge/__init__.py +1 -0
  10. local_deep_research/advanced_search_system/knowledge/base_knowledge.py +151 -0
  11. local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +159 -0
  12. local_deep_research/advanced_search_system/questions/__init__.py +1 -0
  13. local_deep_research/advanced_search_system/questions/base_question.py +64 -0
  14. local_deep_research/advanced_search_system/questions/decomposition_question.py +445 -0
  15. local_deep_research/advanced_search_system/questions/standard_question.py +119 -0
  16. local_deep_research/advanced_search_system/repositories/__init__.py +7 -0
  17. local_deep_research/advanced_search_system/strategies/__init__.py +1 -0
  18. local_deep_research/advanced_search_system/strategies/base_strategy.py +118 -0
  19. local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +450 -0
  20. local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +312 -0
  21. local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +270 -0
  22. local_deep_research/advanced_search_system/strategies/standard_strategy.py +300 -0
  23. local_deep_research/advanced_search_system/tools/__init__.py +1 -0
  24. local_deep_research/advanced_search_system/tools/base_tool.py +100 -0
  25. local_deep_research/advanced_search_system/tools/knowledge_tools/__init__.py +1 -0
  26. local_deep_research/advanced_search_system/tools/question_tools/__init__.py +1 -0
  27. local_deep_research/advanced_search_system/tools/search_tools/__init__.py +1 -0
  28. local_deep_research/api/__init__.py +5 -5
  29. local_deep_research/api/research_functions.py +154 -160
  30. local_deep_research/app.py +8 -0
  31. local_deep_research/citation_handler.py +25 -16
  32. local_deep_research/{config.py → config/config_files.py} +102 -110
  33. local_deep_research/config/llm_config.py +472 -0
  34. local_deep_research/config/search_config.py +77 -0
  35. local_deep_research/defaults/__init__.py +10 -5
  36. local_deep_research/defaults/main.toml +2 -2
  37. local_deep_research/defaults/search_engines.toml +60 -34
  38. local_deep_research/main.py +121 -19
  39. local_deep_research/migrate_db.py +147 -0
  40. local_deep_research/report_generator.py +87 -45
  41. local_deep_research/search_system.py +153 -283
  42. local_deep_research/setup_data_dir.py +35 -0
  43. local_deep_research/test_migration.py +178 -0
  44. local_deep_research/utilities/__init__.py +0 -0
  45. local_deep_research/utilities/db_utils.py +49 -0
  46. local_deep_research/{utilties → utilities}/enums.py +2 -2
  47. local_deep_research/{utilties → utilities}/llm_utils.py +63 -29
  48. local_deep_research/utilities/search_utilities.py +242 -0
  49. local_deep_research/{utilties → utilities}/setup_utils.py +4 -2
  50. local_deep_research/web/__init__.py +0 -1
  51. local_deep_research/web/app.py +86 -1709
  52. local_deep_research/web/app_factory.py +289 -0
  53. local_deep_research/web/database/README.md +70 -0
  54. local_deep_research/web/database/migrate_to_ldr_db.py +289 -0
  55. local_deep_research/web/database/migrations.py +447 -0
  56. local_deep_research/web/database/models.py +117 -0
  57. local_deep_research/web/database/schema_upgrade.py +107 -0
  58. local_deep_research/web/models/database.py +294 -0
  59. local_deep_research/web/models/settings.py +94 -0
  60. local_deep_research/web/routes/api_routes.py +559 -0
  61. local_deep_research/web/routes/history_routes.py +354 -0
  62. local_deep_research/web/routes/research_routes.py +715 -0
  63. local_deep_research/web/routes/settings_routes.py +1583 -0
  64. local_deep_research/web/services/research_service.py +947 -0
  65. local_deep_research/web/services/resource_service.py +149 -0
  66. local_deep_research/web/services/settings_manager.py +669 -0
  67. local_deep_research/web/services/settings_service.py +187 -0
  68. local_deep_research/web/services/socket_service.py +210 -0
  69. local_deep_research/web/static/css/custom_dropdown.css +277 -0
  70. local_deep_research/web/static/css/settings.css +1223 -0
  71. local_deep_research/web/static/css/styles.css +525 -48
  72. local_deep_research/web/static/js/components/custom_dropdown.js +428 -0
  73. local_deep_research/web/static/js/components/detail.js +348 -0
  74. local_deep_research/web/static/js/components/fallback/formatting.js +122 -0
  75. local_deep_research/web/static/js/components/fallback/ui.js +215 -0
  76. local_deep_research/web/static/js/components/history.js +487 -0
  77. local_deep_research/web/static/js/components/logpanel.js +949 -0
  78. local_deep_research/web/static/js/components/progress.js +1107 -0
  79. local_deep_research/web/static/js/components/research.js +1865 -0
  80. local_deep_research/web/static/js/components/results.js +766 -0
  81. local_deep_research/web/static/js/components/settings.js +3981 -0
  82. local_deep_research/web/static/js/components/settings_sync.js +106 -0
  83. local_deep_research/web/static/js/main.js +226 -0
  84. local_deep_research/web/static/js/services/api.js +253 -0
  85. local_deep_research/web/static/js/services/audio.js +31 -0
  86. local_deep_research/web/static/js/services/formatting.js +119 -0
  87. local_deep_research/web/static/js/services/pdf.js +622 -0
  88. local_deep_research/web/static/js/services/socket.js +882 -0
  89. local_deep_research/web/static/js/services/ui.js +546 -0
  90. local_deep_research/web/templates/base.html +72 -0
  91. local_deep_research/web/templates/components/custom_dropdown.html +47 -0
  92. local_deep_research/web/templates/components/log_panel.html +32 -0
  93. local_deep_research/web/templates/components/mobile_nav.html +22 -0
  94. local_deep_research/web/templates/components/settings_form.html +299 -0
  95. local_deep_research/web/templates/components/sidebar.html +21 -0
  96. local_deep_research/web/templates/pages/details.html +73 -0
  97. local_deep_research/web/templates/pages/history.html +51 -0
  98. local_deep_research/web/templates/pages/progress.html +57 -0
  99. local_deep_research/web/templates/pages/research.html +139 -0
  100. local_deep_research/web/templates/pages/results.html +59 -0
  101. local_deep_research/web/templates/settings_dashboard.html +78 -192
  102. local_deep_research/web/utils/__init__.py +0 -0
  103. local_deep_research/web/utils/formatters.py +76 -0
  104. local_deep_research/web_search_engines/engines/full_search.py +18 -16
  105. local_deep_research/web_search_engines/engines/meta_search_engine.py +182 -131
  106. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +224 -139
  107. local_deep_research/web_search_engines/engines/search_engine_brave.py +88 -71
  108. local_deep_research/web_search_engines/engines/search_engine_ddg.py +48 -39
  109. local_deep_research/web_search_engines/engines/search_engine_github.py +415 -204
  110. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +123 -90
  111. local_deep_research/web_search_engines/engines/search_engine_guardian.py +210 -157
  112. local_deep_research/web_search_engines/engines/search_engine_local.py +532 -369
  113. local_deep_research/web_search_engines/engines/search_engine_local_all.py +42 -36
  114. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +358 -266
  115. local_deep_research/web_search_engines/engines/search_engine_searxng.py +212 -160
  116. local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +213 -170
  117. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +84 -68
  118. local_deep_research/web_search_engines/engines/search_engine_wayback.py +186 -154
  119. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +115 -77
  120. local_deep_research/web_search_engines/search_engine_base.py +174 -99
  121. local_deep_research/web_search_engines/search_engine_factory.py +192 -102
  122. local_deep_research/web_search_engines/search_engines_config.py +22 -15
  123. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/METADATA +177 -97
  124. local_deep_research-0.2.2.dist-info/RECORD +135 -0
  125. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/WHEEL +1 -2
  126. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/entry_points.txt +3 -0
  127. local_deep_research/defaults/llm_config.py +0 -338
  128. local_deep_research/utilties/search_utilities.py +0 -114
  129. local_deep_research/web/static/js/app.js +0 -3763
  130. local_deep_research/web/templates/api_keys_config.html +0 -82
  131. local_deep_research/web/templates/collections_config.html +0 -90
  132. local_deep_research/web/templates/index.html +0 -348
  133. local_deep_research/web/templates/llm_config.html +0 -120
  134. local_deep_research/web/templates/main_config.html +0 -89
  135. local_deep_research/web/templates/search_engines_config.html +0 -154
  136. local_deep_research/web/templates/settings.html +0 -519
  137. local_deep_research-0.1.26.dist-info/RECORD +0 -61
  138. local_deep_research-0.1.26.dist-info/top_level.txt +0 -1
  139. /local_deep_research/{utilties → config}/__init__.py +0 -0
  140. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,669 @@
1
+ import logging
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Optional, Union
5
+
6
+ import toml
7
+ from sqlalchemy import func
8
+ from sqlalchemy.exc import SQLAlchemyError
9
+ from sqlalchemy.orm import Session
10
+
11
+ from ...config.config_files import get_config_dir
12
+ from ...config.config_files import settings as dynaconf_settings
13
+ from ..database.models import Setting, SettingType
14
+ from ..models.settings import (
15
+ AppSetting,
16
+ BaseSetting,
17
+ LLMSetting,
18
+ ReportSetting,
19
+ SearchSetting,
20
+ )
21
+
22
+ # Setup logging
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class SettingsManager:
27
+ """
28
+ Manager for handling application settings with database storage and file fallback.
29
+ Provides methods to get and set settings, with the ability to override settings in memory.
30
+ """
31
+
32
+ def __init__(self, db_session: Session):
33
+ """
34
+ Initialize the settings manager
35
+
36
+ Args:
37
+ db_session: SQLAlchemy session for database operations
38
+ """
39
+ self.db_session = db_session
40
+ self.config_dir = get_config_dir() / "config"
41
+ self.settings_file = self.config_dir / "settings.toml"
42
+ self.search_engines_file = self.config_dir / "search_engines.toml"
43
+ self.collections_file = self.config_dir / "local_collections.toml"
44
+ self.secrets_file = self.config_dir / ".secrets.toml"
45
+ self.db_first = True # Always prioritize DB settings
46
+
47
+ # In-memory cache for settings
48
+ self._settings_cache: Dict[str, Any] = {}
49
+
50
+ def get_setting(self, key: str, default: Any = None) -> Any:
51
+ """
52
+ Get a setting value
53
+
54
+ Args:
55
+ key: Setting key
56
+ default: Default value if setting is not found
57
+
58
+ Returns:
59
+ Setting value or default if not found
60
+ """
61
+ # Check in-memory cache first (highest priority)
62
+ if key in self._settings_cache:
63
+ return self._settings_cache[key]
64
+
65
+ # If using database first approach and session available, check database
66
+ if self.db_first and self.db_session:
67
+ try:
68
+ settings = (
69
+ self.db_session.query(Setting)
70
+ # This will find exact matches and any subkeys.
71
+ .filter(Setting.key.startswith(key)).all()
72
+ )
73
+ if len(settings) == 1:
74
+ # This is a bottom-level key.
75
+ value = settings[0].value
76
+ self._settings_cache[key] = value
77
+ return value
78
+ elif len(settings) > 1:
79
+ # This is a higher-level key.
80
+ settings_map = {
81
+ s.key.removeprefix(f"{key}."): s.value for s in settings
82
+ }
83
+ # We deliberately don't update the cache here to avoid
84
+ # conflicts between low-level keys and their parent keys.
85
+ return settings_map
86
+ except SQLAlchemyError as e:
87
+ logger.error(f"Error retrieving setting {key} from database: {e}")
88
+
89
+ # Fall back to Dynaconf settings
90
+ try:
91
+ # Split the key into sections
92
+ parts = key.split(".")
93
+ if len(parts) == 2:
94
+ section, setting = parts
95
+ if hasattr(dynaconf_settings, section) and hasattr(
96
+ getattr(dynaconf_settings, section), setting
97
+ ):
98
+ value = getattr(getattr(dynaconf_settings, section), setting)
99
+ # Update cache and return
100
+ self._settings_cache[key] = value
101
+ return value
102
+ except Exception as e:
103
+ logger.debug(f"Error retrieving setting {key} from Dynaconf: {e}")
104
+
105
+ # Return default if not found
106
+ return default
107
+
108
+ def set_setting(self, key: str, value: Any, commit: bool = True) -> bool:
109
+ """
110
+ Set a setting value
111
+
112
+ Args:
113
+ key: Setting key
114
+ value: Setting value
115
+ commit: Whether to commit the change
116
+
117
+ Returns:
118
+ True if successful, False otherwise
119
+ """
120
+ # Always update cache
121
+ self._settings_cache[key] = value
122
+
123
+ # Always update database if available
124
+ if self.db_session:
125
+ try:
126
+ setting = (
127
+ self.db_session.query(Setting).filter(Setting.key == key).first()
128
+ )
129
+ if setting:
130
+ setting.value = value
131
+ setting.updated_at = (
132
+ func.now()
133
+ ) # Explicitly set the current timestamp
134
+ else:
135
+ # Determine setting type from key
136
+ setting_type = SettingType.APP
137
+ if key.startswith("llm."):
138
+ setting_type = SettingType.LLM
139
+ elif key.startswith("search."):
140
+ setting_type = SettingType.SEARCH
141
+ elif key.startswith("report."):
142
+ setting_type = SettingType.REPORT
143
+
144
+ # Create a new setting
145
+ new_setting = Setting(
146
+ key=key,
147
+ value=value,
148
+ type=setting_type,
149
+ name=key.split(".")[-1].replace("_", " ").title(),
150
+ description=f"Setting for {key}",
151
+ )
152
+ self.db_session.add(new_setting)
153
+
154
+ if commit:
155
+ self.db_session.commit()
156
+
157
+ return True
158
+ except SQLAlchemyError as e:
159
+ logger.error(f"Error setting value for {key}: {e}")
160
+ self.db_session.rollback()
161
+ return False
162
+
163
+ # No database session, only update cache
164
+ return True
165
+
166
+ def get_all_settings(self) -> Dict[str, Any]:
167
+ """
168
+ Get all settings
169
+
170
+ Returns:
171
+ Dictionary of all settings
172
+ """
173
+ result = {}
174
+
175
+ # Start with memory cache (highest priority)
176
+ result.update(self._settings_cache)
177
+
178
+ # Add database settings if available
179
+ if self.db_session:
180
+ try:
181
+ for setting in self.db_session.query(Setting).all():
182
+ result[setting.key] = setting.value
183
+ except SQLAlchemyError as e:
184
+ logger.error(f"Error retrieving all settings from database: {e}")
185
+
186
+ # Fill in missing values from Dynaconf (lowest priority)
187
+ for section in ["llm", "search", "report", "app", "web"]:
188
+ if hasattr(dynaconf_settings, section):
189
+ section_obj = getattr(dynaconf_settings, section)
190
+ for key in dir(section_obj):
191
+ if not key.startswith("_") and not callable(
192
+ getattr(section_obj, key)
193
+ ):
194
+ full_key = f"{section}.{key}"
195
+ if full_key not in result:
196
+ result[full_key] = getattr(section_obj, key)
197
+
198
+ return result
199
+
200
+ def create_or_update_setting(
201
+ self, setting: Union[BaseSetting, Dict[str, Any]], commit: bool = True
202
+ ) -> Optional[Setting]:
203
+ """
204
+ Create or update a setting
205
+
206
+ Args:
207
+ setting: Setting object or dictionary
208
+ commit: Whether to commit the change
209
+
210
+ Returns:
211
+ The created or updated Setting model, or None if failed
212
+ """
213
+ if not self.db_session:
214
+ logger.warning(
215
+ "No database session available, cannot create/update setting"
216
+ )
217
+ return None
218
+
219
+ # Convert dict to BaseSetting if needed
220
+ if isinstance(setting, dict):
221
+ # Determine type from key if not specified
222
+ if "type" not in setting and "key" in setting:
223
+ key = setting["key"]
224
+ if key.startswith("llm."):
225
+ setting_obj = LLMSetting(**setting)
226
+ elif key.startswith("search."):
227
+ setting_obj = SearchSetting(**setting)
228
+ elif key.startswith("report."):
229
+ setting_obj = ReportSetting(**setting)
230
+ else:
231
+ setting_obj = AppSetting(**setting)
232
+ else:
233
+ # Use generic BaseSetting
234
+ setting_obj = BaseSetting(**setting)
235
+ else:
236
+ setting_obj = setting
237
+
238
+ try:
239
+ # Check if setting exists
240
+ db_setting = (
241
+ self.db_session.query(Setting)
242
+ .filter(Setting.key == setting_obj.key)
243
+ .first()
244
+ )
245
+
246
+ if db_setting:
247
+ # Update existing setting
248
+ db_setting.value = setting_obj.value
249
+ db_setting.name = setting_obj.name
250
+ db_setting.description = setting_obj.description
251
+ db_setting.category = setting_obj.category
252
+ db_setting.ui_element = setting_obj.ui_element
253
+ db_setting.options = setting_obj.options
254
+ db_setting.min_value = setting_obj.min_value
255
+ db_setting.max_value = setting_obj.max_value
256
+ db_setting.step = setting_obj.step
257
+ db_setting.visible = setting_obj.visible
258
+ db_setting.editable = setting_obj.editable
259
+ db_setting.updated_at = (
260
+ func.now()
261
+ ) # Explicitly set the current timestamp
262
+ else:
263
+ # Create new setting
264
+ db_setting = Setting(
265
+ key=setting_obj.key,
266
+ value=setting_obj.value,
267
+ type=SettingType[setting_obj.type.upper()],
268
+ name=setting_obj.name,
269
+ description=setting_obj.description,
270
+ category=setting_obj.category,
271
+ ui_element=setting_obj.ui_element,
272
+ options=setting_obj.options,
273
+ min_value=setting_obj.min_value,
274
+ max_value=setting_obj.max_value,
275
+ step=setting_obj.step,
276
+ visible=setting_obj.visible,
277
+ editable=setting_obj.editable,
278
+ )
279
+ self.db_session.add(db_setting)
280
+
281
+ # Update cache
282
+ self._settings_cache[setting_obj.key] = setting_obj.value
283
+
284
+ if commit:
285
+ self.db_session.commit()
286
+
287
+ return db_setting
288
+
289
+ except SQLAlchemyError as e:
290
+ logger.error(f"Error creating/updating setting {setting_obj.key}: {e}")
291
+ self.db_session.rollback()
292
+ return None
293
+
294
+ def delete_setting(self, key: str, commit: bool = True) -> bool:
295
+ """
296
+ Delete a setting
297
+
298
+ Args:
299
+ key: Setting key
300
+ commit: Whether to commit the change
301
+
302
+ Returns:
303
+ True if successful, False otherwise
304
+ """
305
+ if not self.db_session:
306
+ logger.warning("No database session available, cannot delete setting")
307
+ return False
308
+
309
+ try:
310
+ # Remove from cache
311
+ if key in self._settings_cache:
312
+ del self._settings_cache[key]
313
+
314
+ # Remove from database
315
+ result = self.db_session.query(Setting).filter(Setting.key == key).delete()
316
+
317
+ if commit:
318
+ self.db_session.commit()
319
+
320
+ return result > 0
321
+ except SQLAlchemyError as e:
322
+ logger.error(f"Error deleting setting {key}: {e}")
323
+ self.db_session.rollback()
324
+ return False
325
+
326
+ def export_to_file(self, setting_type: Optional[SettingType] = None) -> bool:
327
+ """
328
+ Export settings to file
329
+
330
+ Args:
331
+ setting_type: Type of settings to export (or all if None)
332
+
333
+ Returns:
334
+ True if successful, False otherwise
335
+ """
336
+ try:
337
+ # Get settings
338
+ settings = self.get_all_settings()
339
+
340
+ # Group by section
341
+ sections = {}
342
+ for key, value in settings.items():
343
+ # Split key into section and name
344
+ parts = key.split(".", 1)
345
+ if len(parts) == 2:
346
+ section, name = parts
347
+ if section not in sections:
348
+ sections[section] = {}
349
+ sections[section][name] = value
350
+
351
+ # Write to appropriate file
352
+ if setting_type == SettingType.LLM:
353
+ file_path = self.settings_file
354
+ section_name = "llm"
355
+ elif setting_type == SettingType.SEARCH:
356
+ file_path = self.search_engines_file
357
+ section_name = "search"
358
+ elif setting_type == SettingType.REPORT:
359
+ file_path = self.settings_file
360
+ section_name = "report"
361
+ else:
362
+ # Write all sections to appropriate files
363
+ for section_name, section_data in sections.items():
364
+ if section_name == "search":
365
+ self._write_section_to_file(
366
+ self.search_engines_file, section_name, section_data
367
+ )
368
+ else:
369
+ self._write_section_to_file(
370
+ self.settings_file, section_name, section_data
371
+ )
372
+ return True
373
+
374
+ # Write specific section
375
+ if section_name in sections:
376
+ return self._write_section_to_file(
377
+ file_path, section_name, sections[section_name]
378
+ )
379
+
380
+ return False
381
+
382
+ except Exception as e:
383
+ logger.error(f"Error exporting settings to file: {e}")
384
+ return False
385
+
386
+ def import_from_file(
387
+ self, setting_type: Optional[SettingType] = None, commit: bool = True
388
+ ) -> bool:
389
+ """
390
+ Import settings from file
391
+
392
+ Args:
393
+ setting_type: Type of settings to import (or all if None)
394
+ commit: Whether to commit changes to database
395
+
396
+ Returns:
397
+ True if successful, False otherwise
398
+ """
399
+ try:
400
+ # Determine file path
401
+ if (
402
+ setting_type == SettingType.LLM
403
+ or setting_type == SettingType.APP
404
+ or setting_type == SettingType.REPORT
405
+ ):
406
+ file_path = self.settings_file
407
+ elif setting_type == SettingType.SEARCH:
408
+ file_path = self.search_engines_file
409
+ else:
410
+ # Import from all files
411
+ success = True
412
+ success &= self.import_from_file(SettingType.LLM, commit=False)
413
+ success &= self.import_from_file(SettingType.SEARCH, commit=False)
414
+ success &= self.import_from_file(SettingType.REPORT, commit=False)
415
+ success &= self.import_from_file(SettingType.APP, commit=False)
416
+
417
+ # Commit all changes at once
418
+ if commit and self.db_session:
419
+ self.db_session.commit()
420
+
421
+ return success
422
+
423
+ # Read from file
424
+ if not os.path.exists(file_path):
425
+ logger.warning(f"Settings file does not exist: {file_path}")
426
+ return False
427
+
428
+ # Parse TOML file
429
+ with open(file_path, "r") as f:
430
+ file_data = toml.load(f)
431
+
432
+ # Extract section based on setting type
433
+ section_name = setting_type.value.lower() if setting_type else None
434
+ if section_name and section_name in file_data:
435
+ section_data = file_data[section_name]
436
+ else:
437
+ section_data = file_data
438
+
439
+ # Import settings
440
+ for key, value in section_data.items():
441
+ if section_name:
442
+ full_key = f"{section_name}.{key}"
443
+ else:
444
+ # Try to determine section from key structure
445
+ if "." in key:
446
+ full_key = key
447
+ else:
448
+ # Assume it's an app setting
449
+ full_key = f"app.{key}"
450
+
451
+ self.set_setting(full_key, value, commit=False)
452
+
453
+ # Commit if requested
454
+ if commit and self.db_session:
455
+ self.db_session.commit()
456
+
457
+ return True
458
+
459
+ except Exception as e:
460
+ logger.error(f"Error importing settings from file: {e}")
461
+ if self.db_session:
462
+ self.db_session.rollback()
463
+ return False
464
+
465
+ def _write_section_to_file(
466
+ self, file_path: Path, section: str, data: Dict[str, Any]
467
+ ) -> bool:
468
+ """
469
+ Write a section of settings to a TOML file
470
+
471
+ Args:
472
+ file_path: Path to the file
473
+ section: Section name
474
+ data: Section data
475
+
476
+ Returns:
477
+ True if successful, False otherwise
478
+ """
479
+ try:
480
+ # Create file if it doesn't exist
481
+ if not os.path.exists(file_path):
482
+ file_path.parent.mkdir(parents=True, exist_ok=True)
483
+ with open(file_path, "w") as f:
484
+ f.write(f"[{section}]\n")
485
+
486
+ # Read existing file
487
+ with open(file_path, "r") as f:
488
+ file_data = toml.load(f)
489
+
490
+ # Update section
491
+ file_data[section] = data
492
+
493
+ # Write back to file
494
+ with open(file_path, "w") as f:
495
+ toml.dump(file_data, f)
496
+
497
+ return True
498
+ except Exception as e:
499
+ logger.error(f"Error writing section {section} to {file_path}: {e}")
500
+ return False
501
+
502
+ @classmethod
503
+ def get_instance(cls, db_session: Optional[Session] = None) -> "SettingsManager":
504
+ """
505
+ Get a singleton instance of the settings manager
506
+
507
+ Args:
508
+ db_session: Optional database session
509
+
510
+ Returns:
511
+ SettingsManager instance
512
+ """
513
+ if not hasattr(cls, "_instance"):
514
+ cls._instance = cls(db_session)
515
+ elif db_session and not cls._instance.db_session:
516
+ # Update existing instance with a session
517
+ cls._instance.db_session = db_session
518
+
519
+ return cls._instance
520
+
521
+ def import_default_settings(
522
+ self, main_settings_file, search_engines_file, collections_file
523
+ ):
524
+ """
525
+ Import settings directly from default files
526
+
527
+ Args:
528
+ main_settings_file: Path to the main settings.toml file
529
+ search_engines_file: Path to the search_engines.toml file
530
+ collections_file: Path to the local_collections.toml file
531
+
532
+ Returns:
533
+ True if successful, False otherwise
534
+ """
535
+ if not self.db_session:
536
+ logger.warning(
537
+ "No database session available, cannot import default settings"
538
+ )
539
+ return False
540
+
541
+ try:
542
+ # Import settings from main settings file
543
+ if os.path.exists(main_settings_file):
544
+ with open(main_settings_file, "r") as f:
545
+ main_data = toml.load(f)
546
+
547
+ # Process each section in the main settings file
548
+ for section, values in main_data.items():
549
+ if section in ["web", "llm", "general", "app"]:
550
+ setting_type = None
551
+ if section == "web" or section == "app":
552
+ setting_type = SettingType.APP
553
+ prefix = "app"
554
+ elif section == "llm":
555
+ setting_type = SettingType.LLM
556
+ prefix = "llm"
557
+ else: # general section
558
+ # Map general settings to appropriate types
559
+ prefix = None
560
+ for key, value in values.items():
561
+ if key in [
562
+ "enable_fact_checking",
563
+ "knowledge_accumulation",
564
+ "knowledge_accumulation_context_limit",
565
+ "output_dir",
566
+ ]:
567
+ self._create_setting(
568
+ f"report.{key}", value, SettingType.REPORT
569
+ )
570
+
571
+ # Add settings with correct prefix
572
+ if prefix:
573
+ for key, value in values.items():
574
+ self._create_setting(
575
+ f"{prefix}.{key}", value, setting_type
576
+ )
577
+
578
+ elif section == "search":
579
+ # Search settings go to search type
580
+ for key, value in values.items():
581
+ self._create_setting(
582
+ f"search.{key}", value, SettingType.SEARCH
583
+ )
584
+
585
+ elif section == "report":
586
+ # Report settings
587
+ for key, value in values.items():
588
+ self._create_setting(
589
+ f"report.{key}", value, SettingType.REPORT
590
+ )
591
+
592
+ # Import settings from search engines file
593
+ if os.path.exists(search_engines_file):
594
+ with open(search_engines_file, "r") as f:
595
+ search_data = toml.load(f)
596
+
597
+ # Find search section in search engines file
598
+ if "search" in search_data:
599
+ for key, value in search_data["search"].items():
600
+ # Skip complex sections that are nested
601
+ if not isinstance(value, dict):
602
+ self._create_setting(
603
+ f"search.{key}", value, SettingType.SEARCH
604
+ )
605
+
606
+ # Commit changes
607
+ self.db_session.commit()
608
+ return True
609
+
610
+ except Exception as e:
611
+ logger.error(f"Error importing default settings: {e}")
612
+ if self.db_session:
613
+ self.db_session.rollback()
614
+ return False
615
+
616
+ def _create_setting(self, key, value, setting_type):
617
+ """Create a setting with appropriate metadata"""
618
+
619
+ # Determine appropriate category
620
+ category = None
621
+ ui_element = "text"
622
+
623
+ # Determine category based on key pattern
624
+ if key.startswith("app."):
625
+ category = "app_interface"
626
+ elif key.startswith("llm."):
627
+ if any(
628
+ param in key
629
+ for param in ["temperature", "max_tokens", "n_batch", "n_gpu_layers"]
630
+ ):
631
+ category = "llm_parameters"
632
+ else:
633
+ category = "llm_general"
634
+ elif key.startswith("search."):
635
+ if any(
636
+ param in key
637
+ for param in ["iterations", "questions", "results", "region"]
638
+ ):
639
+ category = "search_parameters"
640
+ else:
641
+ category = "search_general"
642
+ elif key.startswith("report."):
643
+ category = "report_parameters"
644
+
645
+ # Determine UI element type based on value
646
+ if isinstance(value, bool):
647
+ ui_element = "checkbox"
648
+ elif isinstance(value, (int, float)) and not isinstance(value, bool):
649
+ ui_element = "number"
650
+ elif isinstance(value, (dict, list)):
651
+ ui_element = "textarea"
652
+
653
+ # Build setting object
654
+ setting_dict = {
655
+ "key": key,
656
+ "value": value,
657
+ "type": setting_type.value.lower(),
658
+ "name": key.split(".")[-1].replace("_", " ").title(),
659
+ "description": f"Setting for {key}",
660
+ "category": category,
661
+ "ui_element": ui_element,
662
+ }
663
+
664
+ # Create the setting in the database
665
+ db_setting = self.create_or_update_setting(setting_dict, commit=False)
666
+
667
+ # Also update cache
668
+ if db_setting:
669
+ self._settings_cache[key] = value