local-deep-research 0.1.26__py3-none-any.whl → 0.2.0__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.
- local_deep_research/__init__.py +23 -22
- local_deep_research/__main__.py +16 -0
- local_deep_research/advanced_search_system/__init__.py +7 -0
- local_deep_research/advanced_search_system/filters/__init__.py +8 -0
- local_deep_research/advanced_search_system/filters/base_filter.py +38 -0
- local_deep_research/advanced_search_system/filters/cross_engine_filter.py +200 -0
- local_deep_research/advanced_search_system/findings/base_findings.py +81 -0
- local_deep_research/advanced_search_system/findings/repository.py +452 -0
- local_deep_research/advanced_search_system/knowledge/__init__.py +1 -0
- local_deep_research/advanced_search_system/knowledge/base_knowledge.py +151 -0
- local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +159 -0
- local_deep_research/advanced_search_system/questions/__init__.py +1 -0
- local_deep_research/advanced_search_system/questions/base_question.py +64 -0
- local_deep_research/advanced_search_system/questions/decomposition_question.py +445 -0
- local_deep_research/advanced_search_system/questions/standard_question.py +119 -0
- local_deep_research/advanced_search_system/repositories/__init__.py +7 -0
- local_deep_research/advanced_search_system/strategies/__init__.py +1 -0
- local_deep_research/advanced_search_system/strategies/base_strategy.py +118 -0
- local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +450 -0
- local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +312 -0
- local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +270 -0
- local_deep_research/advanced_search_system/strategies/standard_strategy.py +300 -0
- local_deep_research/advanced_search_system/tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/base_tool.py +100 -0
- local_deep_research/advanced_search_system/tools/knowledge_tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/question_tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/search_tools/__init__.py +1 -0
- local_deep_research/api/__init__.py +5 -5
- local_deep_research/api/research_functions.py +96 -84
- local_deep_research/app.py +8 -0
- local_deep_research/citation_handler.py +25 -16
- local_deep_research/{config.py → config/config_files.py} +102 -110
- local_deep_research/config/llm_config.py +472 -0
- local_deep_research/config/search_config.py +77 -0
- local_deep_research/defaults/__init__.py +10 -5
- local_deep_research/defaults/main.toml +2 -2
- local_deep_research/defaults/search_engines.toml +60 -34
- local_deep_research/main.py +121 -19
- local_deep_research/migrate_db.py +147 -0
- local_deep_research/report_generator.py +72 -44
- local_deep_research/search_system.py +147 -283
- local_deep_research/setup_data_dir.py +35 -0
- local_deep_research/test_migration.py +178 -0
- local_deep_research/utilities/__init__.py +0 -0
- local_deep_research/utilities/db_utils.py +49 -0
- local_deep_research/{utilties → utilities}/enums.py +2 -2
- local_deep_research/{utilties → utilities}/llm_utils.py +63 -29
- local_deep_research/utilities/search_utilities.py +242 -0
- local_deep_research/{utilties → utilities}/setup_utils.py +4 -2
- local_deep_research/web/__init__.py +0 -1
- local_deep_research/web/app.py +86 -1709
- local_deep_research/web/app_factory.py +289 -0
- local_deep_research/web/database/README.md +70 -0
- local_deep_research/web/database/migrate_to_ldr_db.py +289 -0
- local_deep_research/web/database/migrations.py +447 -0
- local_deep_research/web/database/models.py +117 -0
- local_deep_research/web/database/schema_upgrade.py +107 -0
- local_deep_research/web/models/database.py +294 -0
- local_deep_research/web/models/settings.py +94 -0
- local_deep_research/web/routes/api_routes.py +559 -0
- local_deep_research/web/routes/history_routes.py +354 -0
- local_deep_research/web/routes/research_routes.py +715 -0
- local_deep_research/web/routes/settings_routes.py +1592 -0
- local_deep_research/web/services/research_service.py +947 -0
- local_deep_research/web/services/resource_service.py +149 -0
- local_deep_research/web/services/settings_manager.py +669 -0
- local_deep_research/web/services/settings_service.py +187 -0
- local_deep_research/web/services/socket_service.py +210 -0
- local_deep_research/web/static/css/custom_dropdown.css +277 -0
- local_deep_research/web/static/css/settings.css +1223 -0
- local_deep_research/web/static/css/styles.css +525 -48
- local_deep_research/web/static/js/components/custom_dropdown.js +428 -0
- local_deep_research/web/static/js/components/detail.js +348 -0
- local_deep_research/web/static/js/components/fallback/formatting.js +122 -0
- local_deep_research/web/static/js/components/fallback/ui.js +215 -0
- local_deep_research/web/static/js/components/history.js +487 -0
- local_deep_research/web/static/js/components/logpanel.js +949 -0
- local_deep_research/web/static/js/components/progress.js +1107 -0
- local_deep_research/web/static/js/components/research.js +1865 -0
- local_deep_research/web/static/js/components/results.js +766 -0
- local_deep_research/web/static/js/components/settings.js +3981 -0
- local_deep_research/web/static/js/components/settings_sync.js +106 -0
- local_deep_research/web/static/js/main.js +226 -0
- local_deep_research/web/static/js/services/api.js +253 -0
- local_deep_research/web/static/js/services/audio.js +31 -0
- local_deep_research/web/static/js/services/formatting.js +119 -0
- local_deep_research/web/static/js/services/pdf.js +622 -0
- local_deep_research/web/static/js/services/socket.js +882 -0
- local_deep_research/web/static/js/services/ui.js +546 -0
- local_deep_research/web/templates/base.html +72 -0
- local_deep_research/web/templates/components/custom_dropdown.html +47 -0
- local_deep_research/web/templates/components/log_panel.html +32 -0
- local_deep_research/web/templates/components/mobile_nav.html +22 -0
- local_deep_research/web/templates/components/settings_form.html +299 -0
- local_deep_research/web/templates/components/sidebar.html +21 -0
- local_deep_research/web/templates/pages/details.html +73 -0
- local_deep_research/web/templates/pages/history.html +51 -0
- local_deep_research/web/templates/pages/progress.html +57 -0
- local_deep_research/web/templates/pages/research.html +139 -0
- local_deep_research/web/templates/pages/results.html +59 -0
- local_deep_research/web/templates/settings_dashboard.html +78 -192
- local_deep_research/web/utils/__init__.py +0 -0
- local_deep_research/web/utils/formatters.py +76 -0
- local_deep_research/web_search_engines/engines/full_search.py +18 -16
- local_deep_research/web_search_engines/engines/meta_search_engine.py +182 -131
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +224 -139
- local_deep_research/web_search_engines/engines/search_engine_brave.py +88 -71
- local_deep_research/web_search_engines/engines/search_engine_ddg.py +48 -39
- local_deep_research/web_search_engines/engines/search_engine_github.py +415 -204
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +123 -90
- local_deep_research/web_search_engines/engines/search_engine_guardian.py +210 -157
- local_deep_research/web_search_engines/engines/search_engine_local.py +532 -369
- local_deep_research/web_search_engines/engines/search_engine_local_all.py +42 -36
- local_deep_research/web_search_engines/engines/search_engine_pubmed.py +358 -266
- local_deep_research/web_search_engines/engines/search_engine_searxng.py +211 -159
- local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +213 -170
- local_deep_research/web_search_engines/engines/search_engine_serpapi.py +84 -68
- local_deep_research/web_search_engines/engines/search_engine_wayback.py +186 -154
- local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +115 -77
- local_deep_research/web_search_engines/search_engine_base.py +174 -99
- local_deep_research/web_search_engines/search_engine_factory.py +192 -102
- local_deep_research/web_search_engines/search_engines_config.py +22 -15
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/METADATA +177 -97
- local_deep_research-0.2.0.dist-info/RECORD +135 -0
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/WHEEL +1 -2
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/entry_points.txt +3 -0
- local_deep_research/defaults/llm_config.py +0 -338
- local_deep_research/utilties/search_utilities.py +0 -114
- local_deep_research/web/static/js/app.js +0 -3763
- local_deep_research/web/templates/api_keys_config.html +0 -82
- local_deep_research/web/templates/collections_config.html +0 -90
- local_deep_research/web/templates/index.html +0 -348
- local_deep_research/web/templates/llm_config.html +0 -120
- local_deep_research/web/templates/main_config.html +0 -89
- local_deep_research/web/templates/search_engines_config.html +0 -154
- local_deep_research/web/templates/settings.html +0 -519
- local_deep_research-0.1.26.dist-info/RECORD +0 -61
- local_deep_research-0.1.26.dist-info/top_level.txt +0 -1
- /local_deep_research/{utilties → config}/__init__.py +0 -0
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.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
|