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,1592 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
import platform
|
5
|
+
import subprocess
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Any, Optional, Tuple
|
8
|
+
|
9
|
+
import requests
|
10
|
+
import toml
|
11
|
+
from flask import (
|
12
|
+
Blueprint,
|
13
|
+
current_app,
|
14
|
+
flash,
|
15
|
+
jsonify,
|
16
|
+
redirect,
|
17
|
+
render_template,
|
18
|
+
request,
|
19
|
+
url_for,
|
20
|
+
)
|
21
|
+
from flask_wtf.csrf import generate_csrf
|
22
|
+
from sqlalchemy.orm import Session
|
23
|
+
|
24
|
+
from ...config.config_files import get_config_dir
|
25
|
+
from ...web_search_engines.search_engine_factory import get_available_engines
|
26
|
+
from ..database.models import Setting, SettingType
|
27
|
+
from ..services.settings_service import (
|
28
|
+
create_or_update_setting,
|
29
|
+
get_setting,
|
30
|
+
get_settings_manager,
|
31
|
+
set_setting,
|
32
|
+
)
|
33
|
+
|
34
|
+
# Initialize logger
|
35
|
+
logger = logging.getLogger(__name__)
|
36
|
+
|
37
|
+
# Create a Blueprint for settings
|
38
|
+
settings_bp = Blueprint("settings", __name__, url_prefix="/research/settings")
|
39
|
+
|
40
|
+
# Legacy config for backwards compatibility
|
41
|
+
SEARCH_ENGINES_FILE = None
|
42
|
+
CONFIG_DIR = None
|
43
|
+
MAIN_CONFIG_FILE = None
|
44
|
+
LOCAL_COLLECTIONS_FILE = None
|
45
|
+
|
46
|
+
|
47
|
+
def set_config_paths(
|
48
|
+
config_dir, search_engines_file, main_config_file, local_collections_file
|
49
|
+
):
|
50
|
+
"""Set the config paths for the settings routes (legacy support)"""
|
51
|
+
global CONFIG_DIR, SEARCH_ENGINES_FILE, MAIN_CONFIG_FILE, LOCAL_COLLECTIONS_FILE
|
52
|
+
CONFIG_DIR = config_dir
|
53
|
+
SEARCH_ENGINES_FILE = search_engines_file
|
54
|
+
MAIN_CONFIG_FILE = main_config_file
|
55
|
+
LOCAL_COLLECTIONS_FILE = local_collections_file
|
56
|
+
|
57
|
+
|
58
|
+
def get_db_session() -> Session:
|
59
|
+
"""Get the database session from the app context"""
|
60
|
+
if hasattr(current_app, "db_session"):
|
61
|
+
return current_app.db_session
|
62
|
+
else:
|
63
|
+
return current_app.extensions["sqlalchemy"].session()
|
64
|
+
|
65
|
+
|
66
|
+
def validate_setting(setting: Setting, value: Any) -> Tuple[bool, Optional[str]]:
|
67
|
+
"""
|
68
|
+
Validate a setting value based on its type and constraints.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
setting: The Setting object to validate against
|
72
|
+
value: The value to validate
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
Tuple of (is_valid, error_message)
|
76
|
+
"""
|
77
|
+
# Convert value based on UI element type
|
78
|
+
if setting.ui_element == "checkbox":
|
79
|
+
# Convert string representations of boolean to actual boolean
|
80
|
+
if isinstance(value, str):
|
81
|
+
value = value.lower() in ("true", "on", "yes", "1")
|
82
|
+
elif setting.ui_element == "number" or setting.ui_element == "slider":
|
83
|
+
try:
|
84
|
+
value = float(value)
|
85
|
+
except (ValueError, TypeError):
|
86
|
+
return False, "Value must be a number"
|
87
|
+
|
88
|
+
# Check min/max constraints if defined
|
89
|
+
if setting.min_value is not None and value < setting.min_value:
|
90
|
+
return False, f"Value must be at least {setting.min_value}"
|
91
|
+
if setting.max_value is not None and value > setting.max_value:
|
92
|
+
return False, f"Value must be at most {setting.max_value}"
|
93
|
+
elif setting.ui_element == "select":
|
94
|
+
# Check if value is in the allowed options
|
95
|
+
if setting.options:
|
96
|
+
# Skip options validation for dynamically populated dropdowns
|
97
|
+
if setting.key not in ["llm.provider", "llm.model"]:
|
98
|
+
allowed_values = [opt.get("value") for opt in setting.options]
|
99
|
+
if value not in allowed_values:
|
100
|
+
return (
|
101
|
+
False,
|
102
|
+
f"Value must be one of: {', '.join(str(v) for v in allowed_values)}",
|
103
|
+
)
|
104
|
+
# All checks passed
|
105
|
+
return True, None
|
106
|
+
|
107
|
+
|
108
|
+
def get_all_settings_json():
|
109
|
+
"""Get all settings as a JSON-serializable dictionary
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
List of setting dictionaries
|
113
|
+
"""
|
114
|
+
db_session = get_db_session()
|
115
|
+
settings_list = []
|
116
|
+
|
117
|
+
# Get all settings
|
118
|
+
settings = (
|
119
|
+
db_session.query(Setting)
|
120
|
+
.order_by(Setting.type, Setting.category, Setting.name)
|
121
|
+
.all()
|
122
|
+
)
|
123
|
+
|
124
|
+
# Convert to dictionaries
|
125
|
+
for setting in settings:
|
126
|
+
# Ensure objects are properly serialized
|
127
|
+
value = setting.value
|
128
|
+
|
129
|
+
# Convert objects to properly formatted JSON strings for display
|
130
|
+
if isinstance(value, (dict, list)) and value:
|
131
|
+
try:
|
132
|
+
# For frontend display, we'll keep objects as they are
|
133
|
+
# The javascript will handle formatting them
|
134
|
+
pass
|
135
|
+
except Exception as e:
|
136
|
+
logger.error(f"Error serializing setting {setting.key}: {e}")
|
137
|
+
|
138
|
+
setting_dict = {
|
139
|
+
"key": setting.key,
|
140
|
+
"value": value,
|
141
|
+
"type": setting.type.value if setting.type else None,
|
142
|
+
"name": setting.name,
|
143
|
+
"description": setting.description,
|
144
|
+
"category": setting.category,
|
145
|
+
"ui_element": setting.ui_element,
|
146
|
+
"options": setting.options,
|
147
|
+
"min_value": setting.min_value,
|
148
|
+
"max_value": setting.max_value,
|
149
|
+
"step": setting.step,
|
150
|
+
"visible": setting.visible,
|
151
|
+
"editable": setting.editable,
|
152
|
+
}
|
153
|
+
settings_list.append(setting_dict)
|
154
|
+
|
155
|
+
return settings_list
|
156
|
+
|
157
|
+
|
158
|
+
@settings_bp.route("/", methods=["GET"])
|
159
|
+
def settings_page():
|
160
|
+
"""Main settings dashboard with links to specialized config pages"""
|
161
|
+
return render_template("settings_dashboard.html")
|
162
|
+
|
163
|
+
|
164
|
+
@settings_bp.route("/save_all_settings", methods=["POST"])
|
165
|
+
def save_all_settings():
|
166
|
+
"""Handle saving all settings at once from the unified settings page"""
|
167
|
+
db_session = get_db_session()
|
168
|
+
# Get the settings manager but we don't need to assign it to a variable right now
|
169
|
+
# get_settings_manager(db_session)
|
170
|
+
|
171
|
+
try:
|
172
|
+
# Process JSON data
|
173
|
+
form_data = request.get_json()
|
174
|
+
if not form_data:
|
175
|
+
return (
|
176
|
+
jsonify({"status": "error", "message": "No settings data provided"}),
|
177
|
+
400,
|
178
|
+
)
|
179
|
+
|
180
|
+
# Track validation errors
|
181
|
+
validation_errors = []
|
182
|
+
settings_by_type = {}
|
183
|
+
|
184
|
+
# Track changes for logging
|
185
|
+
updated_settings = []
|
186
|
+
created_settings = []
|
187
|
+
|
188
|
+
# Store original values for better messaging
|
189
|
+
original_values = {}
|
190
|
+
|
191
|
+
# Update each setting
|
192
|
+
for key, value in form_data.items():
|
193
|
+
# Skip corrupted keys or empty strings as keys
|
194
|
+
if not key or not isinstance(key, str) or key.strip() == "":
|
195
|
+
continue
|
196
|
+
|
197
|
+
# Get the original value
|
198
|
+
current_setting = (
|
199
|
+
db_session.query(Setting).filter(Setting.key == key).first()
|
200
|
+
)
|
201
|
+
if current_setting:
|
202
|
+
original_values[key] = current_setting.value
|
203
|
+
|
204
|
+
# Determine setting type and category
|
205
|
+
setting_type = None
|
206
|
+
if key.startswith("llm."):
|
207
|
+
setting_type = SettingType.LLM
|
208
|
+
category = "llm_general"
|
209
|
+
if (
|
210
|
+
"temperature" in key
|
211
|
+
or "max_tokens" in key
|
212
|
+
or "batch" in key
|
213
|
+
or "layers" in key
|
214
|
+
):
|
215
|
+
category = "llm_parameters"
|
216
|
+
elif key.startswith("search."):
|
217
|
+
setting_type = SettingType.SEARCH
|
218
|
+
category = "search_general"
|
219
|
+
if (
|
220
|
+
"iterations" in key
|
221
|
+
or "results" in key
|
222
|
+
or "region" in key
|
223
|
+
or "questions" in key
|
224
|
+
or "section" in key
|
225
|
+
):
|
226
|
+
category = "search_parameters"
|
227
|
+
elif key.startswith("report."):
|
228
|
+
setting_type = SettingType.REPORT
|
229
|
+
category = "report_parameters"
|
230
|
+
elif key.startswith("app."):
|
231
|
+
setting_type = SettingType.APP
|
232
|
+
category = "app_interface"
|
233
|
+
else:
|
234
|
+
# Skip keys without a known prefix
|
235
|
+
logger.warning(f"Skipping setting with unknown type: {key}")
|
236
|
+
continue
|
237
|
+
|
238
|
+
# Special handling for corrupted or empty values
|
239
|
+
if value == "[object Object]" or (
|
240
|
+
isinstance(value, str) and value.strip() in ["{}", "[]", "{", "["]
|
241
|
+
):
|
242
|
+
if key.startswith("report."):
|
243
|
+
value = {}
|
244
|
+
else:
|
245
|
+
# Use default or null for other types
|
246
|
+
if key == "llm.model":
|
247
|
+
value = "gpt-3.5-turbo"
|
248
|
+
elif key == "llm.provider":
|
249
|
+
value = "openai"
|
250
|
+
elif key == "search.tool":
|
251
|
+
value = "auto"
|
252
|
+
elif key in ["app.theme", "app.default_theme"]:
|
253
|
+
value = "dark"
|
254
|
+
else:
|
255
|
+
value = None
|
256
|
+
|
257
|
+
logger.warning(f"Corrected corrupted value for {key}: {value}")
|
258
|
+
|
259
|
+
# Handle JSON string values (already parsed by JavaScript)
|
260
|
+
if isinstance(value, (dict, list)):
|
261
|
+
# Keep as is, already parsed
|
262
|
+
pass
|
263
|
+
# Handle string values that might be JSON
|
264
|
+
elif isinstance(value, str) and (
|
265
|
+
value.startswith("{") or value.startswith("[")
|
266
|
+
):
|
267
|
+
try:
|
268
|
+
# Try to parse the string as JSON
|
269
|
+
value = json.loads(value)
|
270
|
+
except json.JSONDecodeError:
|
271
|
+
# If it fails to parse, keep as string
|
272
|
+
pass
|
273
|
+
|
274
|
+
if current_setting:
|
275
|
+
# Validate the setting
|
276
|
+
is_valid, error_message = validate_setting(current_setting, value)
|
277
|
+
|
278
|
+
if is_valid:
|
279
|
+
# Update category if different from our determination
|
280
|
+
if category and current_setting.category != category:
|
281
|
+
current_setting.category = category
|
282
|
+
|
283
|
+
# Save the setting
|
284
|
+
success = set_setting(key, value, db_session=db_session)
|
285
|
+
if success:
|
286
|
+
updated_settings.append(key)
|
287
|
+
|
288
|
+
# Track settings by type for exporting
|
289
|
+
if current_setting.type not in settings_by_type:
|
290
|
+
settings_by_type[current_setting.type] = []
|
291
|
+
settings_by_type[current_setting.type].append(current_setting)
|
292
|
+
else:
|
293
|
+
# Add to validation errors
|
294
|
+
validation_errors.append(
|
295
|
+
{
|
296
|
+
"key": key,
|
297
|
+
"name": current_setting.name,
|
298
|
+
"error": error_message,
|
299
|
+
}
|
300
|
+
)
|
301
|
+
else:
|
302
|
+
# Create a new setting
|
303
|
+
new_setting = {
|
304
|
+
"key": key,
|
305
|
+
"value": value,
|
306
|
+
"type": setting_type.value.lower(),
|
307
|
+
"name": key.split(".")[-1].replace("_", " ").title(),
|
308
|
+
"description": f"Setting for {key}",
|
309
|
+
"category": category,
|
310
|
+
"ui_element": "text", # Default UI element
|
311
|
+
}
|
312
|
+
|
313
|
+
# Determine better UI element based on value type
|
314
|
+
if isinstance(value, bool):
|
315
|
+
new_setting["ui_element"] = "checkbox"
|
316
|
+
elif isinstance(value, (int, float)) and not isinstance(value, bool):
|
317
|
+
new_setting["ui_element"] = "number"
|
318
|
+
elif isinstance(value, (dict, list)):
|
319
|
+
new_setting["ui_element"] = "textarea"
|
320
|
+
|
321
|
+
# Create the setting
|
322
|
+
db_setting = create_or_update_setting(new_setting)
|
323
|
+
|
324
|
+
if db_setting:
|
325
|
+
created_settings.append(key)
|
326
|
+
# Track settings by type for exporting
|
327
|
+
if db_setting.type not in settings_by_type:
|
328
|
+
settings_by_type[db_setting.type] = []
|
329
|
+
settings_by_type[db_setting.type].append(db_setting)
|
330
|
+
else:
|
331
|
+
validation_errors.append(
|
332
|
+
{
|
333
|
+
"key": key,
|
334
|
+
"name": new_setting["name"],
|
335
|
+
"error": "Failed to create setting",
|
336
|
+
}
|
337
|
+
)
|
338
|
+
|
339
|
+
# Report validation errors if any
|
340
|
+
if validation_errors:
|
341
|
+
return (
|
342
|
+
jsonify(
|
343
|
+
{
|
344
|
+
"status": "error",
|
345
|
+
"message": "Validation errors",
|
346
|
+
"errors": validation_errors,
|
347
|
+
}
|
348
|
+
),
|
349
|
+
400,
|
350
|
+
)
|
351
|
+
|
352
|
+
# Export settings to file for each type
|
353
|
+
for setting_type in settings_by_type:
|
354
|
+
get_settings_manager(db_session).export_to_file(setting_type)
|
355
|
+
|
356
|
+
# Get all settings to return to the client for proper state update
|
357
|
+
all_settings = []
|
358
|
+
for setting in db_session.query(Setting).all():
|
359
|
+
# Convert enum to string if present
|
360
|
+
setting_type = setting.type
|
361
|
+
if hasattr(setting_type, "value"):
|
362
|
+
setting_type = setting_type.value
|
363
|
+
|
364
|
+
all_settings.append(
|
365
|
+
{
|
366
|
+
"key": setting.key,
|
367
|
+
"value": setting.value,
|
368
|
+
"name": setting.name,
|
369
|
+
"description": setting.description,
|
370
|
+
"type": setting_type,
|
371
|
+
"category": setting.category,
|
372
|
+
"ui_element": setting.ui_element,
|
373
|
+
"editable": setting.editable,
|
374
|
+
"options": setting.options,
|
375
|
+
}
|
376
|
+
)
|
377
|
+
|
378
|
+
# Customize the success message based on what changed
|
379
|
+
success_message = ""
|
380
|
+
if len(updated_settings) == 1:
|
381
|
+
# For a single update, provide more specific info about what changed
|
382
|
+
key = updated_settings[0]
|
383
|
+
updated_setting = (
|
384
|
+
db_session.query(Setting).filter(Setting.key == key).first()
|
385
|
+
)
|
386
|
+
name = (
|
387
|
+
updated_setting.name
|
388
|
+
if updated_setting
|
389
|
+
else key.split(".")[-1].replace("_", " ").title()
|
390
|
+
)
|
391
|
+
|
392
|
+
# Format the message
|
393
|
+
if key in original_values:
|
394
|
+
# Get original value but comment out if not used
|
395
|
+
# old_value = original_values[key]
|
396
|
+
new_value = updated_setting.value if updated_setting else None
|
397
|
+
|
398
|
+
# If it's a boolean, use "enabled/disabled" language
|
399
|
+
if isinstance(new_value, bool):
|
400
|
+
state = "enabled" if new_value else "disabled"
|
401
|
+
success_message = f"{name} {state}"
|
402
|
+
else:
|
403
|
+
# For non-boolean values
|
404
|
+
if isinstance(new_value, (dict, list)):
|
405
|
+
success_message = f"{name} updated"
|
406
|
+
else:
|
407
|
+
success_message = f"{name} updated"
|
408
|
+
else:
|
409
|
+
success_message = f"{name} updated"
|
410
|
+
else:
|
411
|
+
# Multiple settings or generic message
|
412
|
+
success_message = f"Settings saved successfully ({len(updated_settings)} updated, {len(created_settings)} created)"
|
413
|
+
|
414
|
+
return jsonify(
|
415
|
+
{
|
416
|
+
"status": "success",
|
417
|
+
"message": success_message,
|
418
|
+
"updated": updated_settings,
|
419
|
+
"created": created_settings,
|
420
|
+
"settings": all_settings,
|
421
|
+
}
|
422
|
+
)
|
423
|
+
|
424
|
+
except Exception as e:
|
425
|
+
logger.error(f"Error saving settings: {e}")
|
426
|
+
return (
|
427
|
+
jsonify({"status": "error", "message": f"Error saving settings: {str(e)}"}),
|
428
|
+
500,
|
429
|
+
)
|
430
|
+
|
431
|
+
|
432
|
+
@settings_bp.route("/reset_to_defaults", methods=["POST"])
|
433
|
+
def reset_to_defaults():
|
434
|
+
"""Reset all settings to their default values"""
|
435
|
+
db_session = get_db_session()
|
436
|
+
|
437
|
+
try:
|
438
|
+
# First, delete all existing settings to ensure clean state
|
439
|
+
try:
|
440
|
+
# Get count before deletion
|
441
|
+
settings_count = db_session.query(Setting).count()
|
442
|
+
logger.info(f"Deleting {settings_count} existing settings before reset")
|
443
|
+
|
444
|
+
# Delete all settings
|
445
|
+
db_session.query(Setting).delete()
|
446
|
+
db_session.commit()
|
447
|
+
logger.info("Successfully deleted all existing settings")
|
448
|
+
except Exception as e:
|
449
|
+
logger.error(f"Error deleting existing settings: {e}")
|
450
|
+
db_session.rollback()
|
451
|
+
return (
|
452
|
+
jsonify(
|
453
|
+
{
|
454
|
+
"status": "error",
|
455
|
+
"message": f"Error cleaning existing settings: {str(e)}",
|
456
|
+
}
|
457
|
+
),
|
458
|
+
500,
|
459
|
+
)
|
460
|
+
|
461
|
+
# Import default settings from files
|
462
|
+
try:
|
463
|
+
# Import default config files from the defaults directory
|
464
|
+
from importlib.resources import files
|
465
|
+
|
466
|
+
try:
|
467
|
+
defaults_dir = files("local_deep_research.defaults")
|
468
|
+
except ImportError:
|
469
|
+
# Fallback for older Python versions
|
470
|
+
from pkg_resources import resource_filename
|
471
|
+
|
472
|
+
defaults_dir = Path(
|
473
|
+
resource_filename("local_deep_research", "defaults")
|
474
|
+
)
|
475
|
+
|
476
|
+
logger.info(f"Loading defaults from: {defaults_dir}")
|
477
|
+
|
478
|
+
# Get temporary path to default files
|
479
|
+
import tempfile
|
480
|
+
|
481
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
482
|
+
# Copy default files to temp directory
|
483
|
+
temp_main = Path(temp_dir) / "settings.toml"
|
484
|
+
temp_search = Path(temp_dir) / "search_engines.toml"
|
485
|
+
temp_collections = Path(temp_dir) / "local_collections.toml"
|
486
|
+
|
487
|
+
# Copy default files (platform independent)
|
488
|
+
import importlib.resources as pkg_resources
|
489
|
+
|
490
|
+
from ... import defaults
|
491
|
+
|
492
|
+
with open(temp_main, "wb") as f:
|
493
|
+
f.write(pkg_resources.read_binary(defaults, "main.toml"))
|
494
|
+
|
495
|
+
with open(temp_search, "wb") as f:
|
496
|
+
f.write(pkg_resources.read_binary(defaults, "search_engines.toml"))
|
497
|
+
|
498
|
+
with open(temp_collections, "wb") as f:
|
499
|
+
f.write(
|
500
|
+
pkg_resources.read_binary(defaults, "local_collections.toml")
|
501
|
+
)
|
502
|
+
|
503
|
+
# Create settings manager with temp files
|
504
|
+
# Get configuration directory (not used currently but might be needed in future)
|
505
|
+
# config_dir = get_config_dir() / "config"
|
506
|
+
|
507
|
+
# Create settings manager for the temporary config
|
508
|
+
settings_mgr = get_settings_manager(db_session)
|
509
|
+
|
510
|
+
# Import settings from default files
|
511
|
+
settings_mgr.import_default_settings(
|
512
|
+
temp_main, temp_search, temp_collections
|
513
|
+
)
|
514
|
+
|
515
|
+
logger.info("Successfully imported settings from default files")
|
516
|
+
except Exception as e:
|
517
|
+
logger.error(f"Error importing default settings: {e}")
|
518
|
+
|
519
|
+
# Fallback to predefined settings if file import fails
|
520
|
+
logger.info("Falling back to predefined settings")
|
521
|
+
# Import here to avoid circular imports
|
522
|
+
from ..database.migrations import (
|
523
|
+
setup_predefined_settings as setup_settings,
|
524
|
+
)
|
525
|
+
|
526
|
+
setup_settings(db_session)
|
527
|
+
|
528
|
+
# Also export the settings to file for consistency
|
529
|
+
settings_mgr = get_settings_manager(db_session)
|
530
|
+
for setting_type in SettingType:
|
531
|
+
settings_mgr.export_to_file(setting_type)
|
532
|
+
|
533
|
+
# Return success
|
534
|
+
return jsonify(
|
535
|
+
{
|
536
|
+
"status": "success",
|
537
|
+
"message": "All settings have been reset to default values",
|
538
|
+
}
|
539
|
+
)
|
540
|
+
|
541
|
+
except Exception as e:
|
542
|
+
logger.error(f"Error resetting settings to defaults: {e}")
|
543
|
+
return (
|
544
|
+
jsonify(
|
545
|
+
{
|
546
|
+
"status": "error",
|
547
|
+
"message": f"Error resetting settings to defaults: {str(e)}",
|
548
|
+
}
|
549
|
+
),
|
550
|
+
500,
|
551
|
+
)
|
552
|
+
|
553
|
+
|
554
|
+
@settings_bp.route("/all_settings", methods=["GET"])
|
555
|
+
def get_all_settings_route():
|
556
|
+
"""Get all settings for the unified dashboard"""
|
557
|
+
settings_list = get_all_settings_json()
|
558
|
+
return jsonify({"status": "success", "settings": settings_list})
|
559
|
+
|
560
|
+
|
561
|
+
# API Routes
|
562
|
+
@settings_bp.route("/api", methods=["GET"])
|
563
|
+
def api_get_all_settings():
|
564
|
+
"""Get all settings"""
|
565
|
+
try:
|
566
|
+
# Get query parameters
|
567
|
+
setting_type = request.args.get("type")
|
568
|
+
category = request.args.get("category")
|
569
|
+
|
570
|
+
# Create settings manager
|
571
|
+
db_session = get_db_session()
|
572
|
+
settings_manager = get_settings_manager(db_session)
|
573
|
+
|
574
|
+
# Get settings
|
575
|
+
if setting_type:
|
576
|
+
try:
|
577
|
+
setting_type_enum = SettingType[setting_type.upper()]
|
578
|
+
settings = settings_manager.get_all_settings(setting_type_enum)
|
579
|
+
except KeyError:
|
580
|
+
return jsonify({"error": f"Invalid setting type: {setting_type}"}), 400
|
581
|
+
else:
|
582
|
+
settings = settings_manager.get_all_settings()
|
583
|
+
|
584
|
+
# Filter by category if requested
|
585
|
+
if category:
|
586
|
+
filtered_settings = {}
|
587
|
+
# Need to get all setting details to check category
|
588
|
+
db_settings = db_session.query(Setting).all()
|
589
|
+
category_keys = [s.key for s in db_settings if s.category == category]
|
590
|
+
|
591
|
+
# Filter settings by keys
|
592
|
+
for key, value in settings.items():
|
593
|
+
if key in category_keys:
|
594
|
+
filtered_settings[key] = value
|
595
|
+
|
596
|
+
settings = filtered_settings
|
597
|
+
|
598
|
+
return jsonify({"settings": settings})
|
599
|
+
except Exception as e:
|
600
|
+
logger.error(f"Error getting settings: {e}")
|
601
|
+
return jsonify({"error": str(e)}), 500
|
602
|
+
|
603
|
+
|
604
|
+
@settings_bp.route("/api/<path:key>", methods=["GET"])
|
605
|
+
def api_get_setting(key):
|
606
|
+
"""Get a specific setting by key"""
|
607
|
+
try:
|
608
|
+
db_session = get_db_session()
|
609
|
+
# No need to assign if not used
|
610
|
+
# get_settings_manager(db_session)
|
611
|
+
|
612
|
+
# Get setting
|
613
|
+
value = get_setting(key, db_session=db_session)
|
614
|
+
if value is None:
|
615
|
+
return jsonify({"error": f"Setting not found: {key}"}), 404
|
616
|
+
|
617
|
+
# Get additional metadata from database.
|
618
|
+
db_setting = db_session.query(Setting).filter(Setting.key == key).first()
|
619
|
+
|
620
|
+
if db_setting:
|
621
|
+
# Return full setting details
|
622
|
+
setting_data = {
|
623
|
+
"key": db_setting.key,
|
624
|
+
"value": db_setting.value,
|
625
|
+
"type": db_setting.type.value,
|
626
|
+
"name": db_setting.name,
|
627
|
+
"description": db_setting.description,
|
628
|
+
"category": db_setting.category,
|
629
|
+
"ui_element": db_setting.ui_element,
|
630
|
+
"options": db_setting.options,
|
631
|
+
"min_value": db_setting.min_value,
|
632
|
+
"max_value": db_setting.max_value,
|
633
|
+
"step": db_setting.step,
|
634
|
+
"visible": db_setting.visible,
|
635
|
+
"editable": db_setting.editable,
|
636
|
+
}
|
637
|
+
else:
|
638
|
+
# Return minimal info
|
639
|
+
setting_data = {"key": key, "value": value}
|
640
|
+
|
641
|
+
return jsonify({"settings": setting_data})
|
642
|
+
except Exception as e:
|
643
|
+
logger.error(f"Error getting setting {key}: {e}")
|
644
|
+
return jsonify({"error": str(e)}), 500
|
645
|
+
|
646
|
+
|
647
|
+
@settings_bp.route("/api/<path:key>", methods=["PUT"])
|
648
|
+
def api_update_setting(key):
|
649
|
+
"""Update a setting"""
|
650
|
+
try:
|
651
|
+
# Get request data
|
652
|
+
data = request.get_json()
|
653
|
+
if not data:
|
654
|
+
return jsonify({"error": "No data provided"}), 400
|
655
|
+
|
656
|
+
value = data.get("value")
|
657
|
+
if value is None:
|
658
|
+
return jsonify({"error": "No value provided"}), 400
|
659
|
+
|
660
|
+
# Get DB session and settings manager
|
661
|
+
db_session = get_db_session()
|
662
|
+
# Only use settings_manager if needed - we don't need to assign if not used
|
663
|
+
# get_settings_manager(db_session)
|
664
|
+
|
665
|
+
# Check if setting exists
|
666
|
+
db_setting = db_session.query(Setting).filter(Setting.key == key).first()
|
667
|
+
|
668
|
+
if db_setting:
|
669
|
+
# Check if setting is editable
|
670
|
+
if not db_setting.editable:
|
671
|
+
return jsonify({"error": f"Setting {key} is not editable"}), 403
|
672
|
+
|
673
|
+
# Update setting
|
674
|
+
success = set_setting(key, value)
|
675
|
+
if success:
|
676
|
+
return jsonify({"message": f"Setting {key} updated successfully"})
|
677
|
+
else:
|
678
|
+
return jsonify({"error": f"Failed to update setting {key}"}), 500
|
679
|
+
else:
|
680
|
+
# Create new setting with default metadata
|
681
|
+
setting_dict = {
|
682
|
+
"key": key,
|
683
|
+
"value": value,
|
684
|
+
"name": key.split(".")[-1].replace("_", " ").title(),
|
685
|
+
"description": f"Setting for {key}",
|
686
|
+
}
|
687
|
+
|
688
|
+
# Add additional metadata if provided
|
689
|
+
for field in [
|
690
|
+
"type",
|
691
|
+
"name",
|
692
|
+
"description",
|
693
|
+
"category",
|
694
|
+
"ui_element",
|
695
|
+
"options",
|
696
|
+
"min_value",
|
697
|
+
"max_value",
|
698
|
+
"step",
|
699
|
+
"visible",
|
700
|
+
"editable",
|
701
|
+
]:
|
702
|
+
if field in data:
|
703
|
+
setting_dict[field] = data[field]
|
704
|
+
|
705
|
+
# Create setting
|
706
|
+
db_setting = create_or_update_setting(setting_dict)
|
707
|
+
|
708
|
+
if db_setting:
|
709
|
+
return (
|
710
|
+
jsonify(
|
711
|
+
{
|
712
|
+
"message": f"Setting {key} created successfully",
|
713
|
+
"setting": {
|
714
|
+
"key": db_setting.key,
|
715
|
+
"value": db_setting.value,
|
716
|
+
"type": db_setting.type.value,
|
717
|
+
"name": db_setting.name,
|
718
|
+
},
|
719
|
+
}
|
720
|
+
),
|
721
|
+
201,
|
722
|
+
)
|
723
|
+
else:
|
724
|
+
return jsonify({"error": f"Failed to create setting {key}"}), 500
|
725
|
+
except Exception as e:
|
726
|
+
logger.error(f"Error updating setting {key}: {e}")
|
727
|
+
return jsonify({"error": str(e)}), 500
|
728
|
+
|
729
|
+
|
730
|
+
@settings_bp.route("/api/<path:key>", methods=["DELETE"])
|
731
|
+
def api_delete_setting(key):
|
732
|
+
"""Delete a setting"""
|
733
|
+
try:
|
734
|
+
db_session = get_db_session()
|
735
|
+
settings_manager = get_settings_manager(db_session)
|
736
|
+
|
737
|
+
# Check if setting exists
|
738
|
+
db_setting = db_session.query(Setting).filter(Setting.key == key).first()
|
739
|
+
if not db_setting:
|
740
|
+
return jsonify({"error": f"Setting not found: {key}"}), 404
|
741
|
+
|
742
|
+
# Delete setting
|
743
|
+
success = settings_manager.delete_setting(key)
|
744
|
+
if success:
|
745
|
+
return jsonify({"message": f"Setting {key} deleted successfully"})
|
746
|
+
else:
|
747
|
+
return jsonify({"error": f"Failed to delete setting {key}"}), 500
|
748
|
+
except Exception as e:
|
749
|
+
logger.error(f"Error deleting setting {key}: {e}")
|
750
|
+
return jsonify({"error": str(e)}), 500
|
751
|
+
|
752
|
+
|
753
|
+
@settings_bp.route("/api/export", methods=["POST"])
|
754
|
+
def api_export_settings():
|
755
|
+
"""Export settings to file"""
|
756
|
+
try:
|
757
|
+
data = request.get_json() or {}
|
758
|
+
setting_type_str = data.get("type")
|
759
|
+
|
760
|
+
db_session = get_db_session()
|
761
|
+
settings_manager = get_settings_manager(db_session)
|
762
|
+
|
763
|
+
# Export settings
|
764
|
+
if setting_type_str:
|
765
|
+
try:
|
766
|
+
setting_type = SettingType[setting_type_str.upper()]
|
767
|
+
success = settings_manager.export_to_file(setting_type)
|
768
|
+
except KeyError:
|
769
|
+
return (
|
770
|
+
jsonify({"error": f"Invalid setting type: {setting_type_str}"}),
|
771
|
+
400,
|
772
|
+
)
|
773
|
+
else:
|
774
|
+
success = settings_manager.export_to_file()
|
775
|
+
|
776
|
+
if success:
|
777
|
+
return jsonify({"message": "Settings exported successfully"})
|
778
|
+
else:
|
779
|
+
return jsonify({"error": "Failed to export settings"}), 500
|
780
|
+
except Exception as e:
|
781
|
+
logger.error(f"Error exporting settings: {e}")
|
782
|
+
return jsonify({"error": str(e)}), 500
|
783
|
+
|
784
|
+
|
785
|
+
@settings_bp.route("/api/import", methods=["POST"])
|
786
|
+
def api_import_settings():
|
787
|
+
"""Import settings from file"""
|
788
|
+
try:
|
789
|
+
data = request.get_json() or {}
|
790
|
+
setting_type_str = data.get("type")
|
791
|
+
|
792
|
+
db_session = get_db_session()
|
793
|
+
settings_manager = get_settings_manager(db_session)
|
794
|
+
|
795
|
+
# Import settings
|
796
|
+
if setting_type_str:
|
797
|
+
try:
|
798
|
+
setting_type = SettingType[setting_type_str.upper()]
|
799
|
+
success = settings_manager.import_from_file(setting_type)
|
800
|
+
except KeyError:
|
801
|
+
return (
|
802
|
+
jsonify({"error": f"Invalid setting type: {setting_type_str}"}),
|
803
|
+
400,
|
804
|
+
)
|
805
|
+
else:
|
806
|
+
success = settings_manager.import_from_file()
|
807
|
+
|
808
|
+
if success:
|
809
|
+
return jsonify({"message": "Settings imported successfully"})
|
810
|
+
else:
|
811
|
+
return jsonify({"error": "Failed to import settings"}), 500
|
812
|
+
except Exception as e:
|
813
|
+
logger.error(f"Error importing settings: {e}")
|
814
|
+
return jsonify({"error": str(e)}), 500
|
815
|
+
|
816
|
+
|
817
|
+
@settings_bp.route("/api/categories", methods=["GET"])
|
818
|
+
def api_get_categories():
|
819
|
+
"""Get all setting categories"""
|
820
|
+
try:
|
821
|
+
db_session = get_db_session()
|
822
|
+
|
823
|
+
# Get all distinct categories
|
824
|
+
categories = db_session.query(Setting.category).distinct().all()
|
825
|
+
category_list = [c[0] for c in categories if c[0] is not None]
|
826
|
+
|
827
|
+
return jsonify({"categories": category_list})
|
828
|
+
except Exception as e:
|
829
|
+
logger.error(f"Error getting categories: {e}")
|
830
|
+
return jsonify({"error": str(e)}), 500
|
831
|
+
|
832
|
+
|
833
|
+
@settings_bp.route("/api/types", methods=["GET"])
|
834
|
+
def api_get_types():
|
835
|
+
"""Get all setting types"""
|
836
|
+
try:
|
837
|
+
# Get all setting types
|
838
|
+
types = [t.value for t in SettingType]
|
839
|
+
return jsonify({"types": types})
|
840
|
+
except Exception as e:
|
841
|
+
logger.error(f"Error getting types: {e}")
|
842
|
+
return jsonify({"error": str(e)}), 500
|
843
|
+
|
844
|
+
|
845
|
+
@settings_bp.route("/api/ui_elements", methods=["GET"])
|
846
|
+
def api_get_ui_elements():
|
847
|
+
"""Get all UI element types"""
|
848
|
+
try:
|
849
|
+
# Define supported UI element types
|
850
|
+
ui_elements = [
|
851
|
+
"text",
|
852
|
+
"select",
|
853
|
+
"checkbox",
|
854
|
+
"slider",
|
855
|
+
"number",
|
856
|
+
"textarea",
|
857
|
+
"color",
|
858
|
+
"date",
|
859
|
+
"file",
|
860
|
+
"password",
|
861
|
+
]
|
862
|
+
|
863
|
+
return jsonify({"ui_elements": ui_elements})
|
864
|
+
except Exception as e:
|
865
|
+
logger.error(f"Error getting UI elements: {e}")
|
866
|
+
return jsonify({"error": str(e)}), 500
|
867
|
+
|
868
|
+
|
869
|
+
@settings_bp.route("/api/available-models", methods=["GET"])
|
870
|
+
def api_get_available_models():
|
871
|
+
"""Get available LLM models from various providers"""
|
872
|
+
try:
|
873
|
+
# Define provider options with generic provider names
|
874
|
+
provider_options = [
|
875
|
+
{"value": "OLLAMA", "label": "Ollama (Local)"},
|
876
|
+
{"value": "OPENAI", "label": "OpenAI (Cloud)"},
|
877
|
+
{"value": "ANTHROPIC", "label": "Anthropic (Cloud)"},
|
878
|
+
{"value": "OPENAI_ENDPOINT", "label": "Custom OpenAI Endpoint"},
|
879
|
+
{"value": "VLLM", "label": "vLLM (Local)"},
|
880
|
+
{"value": "LMSTUDIO", "label": "LM Studio (Local)"},
|
881
|
+
{"value": "LLAMACPP", "label": "Llama.cpp (Local)"},
|
882
|
+
]
|
883
|
+
|
884
|
+
# Available models by provider
|
885
|
+
providers = {}
|
886
|
+
|
887
|
+
# Try to get Ollama models
|
888
|
+
ollama_models = []
|
889
|
+
try:
|
890
|
+
import json
|
891
|
+
import re
|
892
|
+
|
893
|
+
import requests
|
894
|
+
from flask import current_app
|
895
|
+
|
896
|
+
# Try to query the Ollama API directly
|
897
|
+
try:
|
898
|
+
current_app.logger.info("Attempting to connect to Ollama API")
|
899
|
+
|
900
|
+
base_url = os.getenv(
|
901
|
+
"OLLAMA_BASE_URL",
|
902
|
+
"http://localhost:11434",
|
903
|
+
)
|
904
|
+
ollama_response = requests.get(f"{base_url}/api/tags", timeout=5)
|
905
|
+
|
906
|
+
current_app.logger.debug(
|
907
|
+
f"Ollama API response: Status {ollama_response.status_code}"
|
908
|
+
)
|
909
|
+
|
910
|
+
# Try to parse the response even if status code is not 200 to help with debugging
|
911
|
+
response_text = ollama_response.text
|
912
|
+
current_app.logger.debug(
|
913
|
+
f"Ollama API raw response: {response_text[:500]}..."
|
914
|
+
)
|
915
|
+
|
916
|
+
if ollama_response.status_code == 200:
|
917
|
+
try:
|
918
|
+
ollama_data = ollama_response.json()
|
919
|
+
current_app.logger.debug(
|
920
|
+
f"Ollama API JSON data: {json.dumps(ollama_data)[:500]}..."
|
921
|
+
)
|
922
|
+
|
923
|
+
if "models" in ollama_data:
|
924
|
+
# Format for newer Ollama API
|
925
|
+
current_app.logger.info(
|
926
|
+
f"Found {len(ollama_data.get('models', []))} models in newer Ollama API format"
|
927
|
+
)
|
928
|
+
for model in ollama_data.get("models", []):
|
929
|
+
# Extract name correctly from the model object
|
930
|
+
name = model.get("name", "")
|
931
|
+
if name:
|
932
|
+
# Improved display name formatting
|
933
|
+
display_name = re.sub(r"[:/]", " ", name).strip()
|
934
|
+
display_name = " ".join(
|
935
|
+
word.capitalize()
|
936
|
+
for word in display_name.split()
|
937
|
+
)
|
938
|
+
# Create the model entry with value and label
|
939
|
+
ollama_models.append(
|
940
|
+
{
|
941
|
+
"value": name, # Original model name as value (for API calls)
|
942
|
+
"label": f"{display_name} (Ollama)", # Pretty name as label
|
943
|
+
"provider": "OLLAMA", # Add provider field for consistency
|
944
|
+
}
|
945
|
+
)
|
946
|
+
current_app.logger.debug(
|
947
|
+
f"Added Ollama model: {name} -> {display_name}"
|
948
|
+
)
|
949
|
+
else:
|
950
|
+
# Format for older Ollama API
|
951
|
+
current_app.logger.info(
|
952
|
+
f"Found {len(ollama_data)} models in older Ollama API format"
|
953
|
+
)
|
954
|
+
for model in ollama_data:
|
955
|
+
name = model.get("name", "")
|
956
|
+
if name:
|
957
|
+
# Improved display name formatting
|
958
|
+
display_name = re.sub(r"[:/]", " ", name).strip()
|
959
|
+
display_name = " ".join(
|
960
|
+
word.capitalize()
|
961
|
+
for word in display_name.split()
|
962
|
+
)
|
963
|
+
ollama_models.append(
|
964
|
+
{
|
965
|
+
"value": name,
|
966
|
+
"label": f"{display_name} (Ollama)",
|
967
|
+
"provider": "OLLAMA", # Add provider field for consistency
|
968
|
+
}
|
969
|
+
)
|
970
|
+
current_app.logger.debug(
|
971
|
+
f"Added Ollama model: {name} -> {display_name}"
|
972
|
+
)
|
973
|
+
|
974
|
+
# Sort models alphabetically
|
975
|
+
ollama_models.sort(key=lambda x: x["label"])
|
976
|
+
|
977
|
+
except json.JSONDecodeError as json_err:
|
978
|
+
current_app.logger.error(
|
979
|
+
f"Failed to parse Ollama API response as JSON: {json_err}"
|
980
|
+
)
|
981
|
+
raise Exception(f"Ollama API returned invalid JSON: {json_err}")
|
982
|
+
else:
|
983
|
+
current_app.logger.warning(
|
984
|
+
f"Ollama API returned non-200 status code: {ollama_response.status_code}"
|
985
|
+
)
|
986
|
+
raise Exception(
|
987
|
+
f"Ollama API returned status code {ollama_response.status_code}"
|
988
|
+
)
|
989
|
+
|
990
|
+
except requests.exceptions.RequestException as e:
|
991
|
+
current_app.logger.warning(f"Could not connect to Ollama API: {str(e)}")
|
992
|
+
# Fallback to default models if Ollama is not running
|
993
|
+
current_app.logger.info(
|
994
|
+
"Using fallback Ollama models due to connection error"
|
995
|
+
)
|
996
|
+
ollama_models = [
|
997
|
+
{
|
998
|
+
"value": "llama3",
|
999
|
+
"label": "Llama 3 (Ollama)",
|
1000
|
+
"provider": "OLLAMA",
|
1001
|
+
},
|
1002
|
+
{
|
1003
|
+
"value": "mistral",
|
1004
|
+
"label": "Mistral (Ollama)",
|
1005
|
+
"provider": "OLLAMA",
|
1006
|
+
},
|
1007
|
+
{
|
1008
|
+
"value": "gemma:latest",
|
1009
|
+
"label": "Gemma (Ollama)",
|
1010
|
+
"provider": "OLLAMA",
|
1011
|
+
},
|
1012
|
+
]
|
1013
|
+
|
1014
|
+
# Always set the ollama_models in providers, whether we got real or fallback models
|
1015
|
+
providers["ollama_models"] = ollama_models
|
1016
|
+
current_app.logger.info(f"Final Ollama models count: {len(ollama_models)}")
|
1017
|
+
|
1018
|
+
# Log some model names for debugging
|
1019
|
+
if ollama_models:
|
1020
|
+
model_names = [m["value"] for m in ollama_models[:5]]
|
1021
|
+
current_app.logger.info(
|
1022
|
+
f"Sample Ollama models: {', '.join(model_names)}"
|
1023
|
+
)
|
1024
|
+
|
1025
|
+
except Exception as e:
|
1026
|
+
current_app.logger.error(f"Error getting Ollama models: {str(e)}")
|
1027
|
+
# Use fallback models
|
1028
|
+
current_app.logger.info("Using fallback Ollama models due to error")
|
1029
|
+
providers["ollama_models"] = [
|
1030
|
+
{"value": "llama3", "label": "Llama 3 (Ollama)", "provider": "OLLAMA"},
|
1031
|
+
{"value": "mistral", "label": "Mistral (Ollama)", "provider": "OLLAMA"},
|
1032
|
+
{
|
1033
|
+
"value": "gemma:latest",
|
1034
|
+
"label": "Gemma (Ollama)",
|
1035
|
+
"provider": "OLLAMA",
|
1036
|
+
},
|
1037
|
+
]
|
1038
|
+
|
1039
|
+
# Add OpenAI models
|
1040
|
+
providers["openai_models"] = [
|
1041
|
+
{"value": "gpt-4o", "label": "GPT-4o (OpenAI)"},
|
1042
|
+
{"value": "gpt-4", "label": "GPT-4 (OpenAI)"},
|
1043
|
+
{"value": "gpt-3.5-turbo", "label": "GPT-3.5 Turbo (OpenAI)"},
|
1044
|
+
]
|
1045
|
+
|
1046
|
+
# Add Anthropic models
|
1047
|
+
providers["anthropic_models"] = [
|
1048
|
+
{
|
1049
|
+
"value": "claude-3-5-sonnet-latest",
|
1050
|
+
"label": "Claude 3.5 Sonnet (Anthropic)",
|
1051
|
+
},
|
1052
|
+
{"value": "claude-3-opus-20240229", "label": "Claude 3 Opus (Anthropic)"},
|
1053
|
+
{
|
1054
|
+
"value": "claude-3-sonnet-20240229",
|
1055
|
+
"label": "Claude 3 Sonnet (Anthropic)",
|
1056
|
+
},
|
1057
|
+
{"value": "claude-3-haiku-20240307", "label": "Claude 3 Haiku (Anthropic)"},
|
1058
|
+
]
|
1059
|
+
|
1060
|
+
# Return all options
|
1061
|
+
return jsonify({"provider_options": provider_options, "providers": providers})
|
1062
|
+
|
1063
|
+
except Exception as e:
|
1064
|
+
import traceback
|
1065
|
+
|
1066
|
+
error_trace = traceback.format_exc()
|
1067
|
+
current_app.logger.error(
|
1068
|
+
f"Error getting available models: {str(e)}\n{error_trace}"
|
1069
|
+
)
|
1070
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
1071
|
+
|
1072
|
+
|
1073
|
+
@settings_bp.route("/api/available-search-engines", methods=["GET"])
|
1074
|
+
def api_get_available_search_engines():
|
1075
|
+
"""Get available search engines"""
|
1076
|
+
try:
|
1077
|
+
# First try to get engines from search_engines.toml file
|
1078
|
+
engines_dict = get_engines_from_file()
|
1079
|
+
|
1080
|
+
# If we got engines from file, use those
|
1081
|
+
if engines_dict:
|
1082
|
+
# Make sure searxng is included if it should be
|
1083
|
+
if "searxng" not in engines_dict:
|
1084
|
+
engines_dict["searxng"] = {
|
1085
|
+
"display_name": "SearXNG (Self-hosted)",
|
1086
|
+
"description": "Self-hosted metasearch engine",
|
1087
|
+
"strengths": ["privacy", "customization", "no API key needed"],
|
1088
|
+
}
|
1089
|
+
|
1090
|
+
# Format as options for dropdown
|
1091
|
+
engine_options = [
|
1092
|
+
{
|
1093
|
+
"value": key,
|
1094
|
+
"label": engines_dict.get(key, {}).get("display_name", key),
|
1095
|
+
}
|
1096
|
+
for key in engines_dict.keys()
|
1097
|
+
]
|
1098
|
+
|
1099
|
+
return jsonify({"engines": engines_dict, "engine_options": engine_options})
|
1100
|
+
|
1101
|
+
# Fallback to factory function if file method failed
|
1102
|
+
try:
|
1103
|
+
# Get available engines
|
1104
|
+
search_engines = get_available_engines(include_api_key_services=True)
|
1105
|
+
|
1106
|
+
# Handle if search_engines is a list (not a dict)
|
1107
|
+
if isinstance(search_engines, list):
|
1108
|
+
# Convert to dict with engine name as key and display name as value
|
1109
|
+
engines_dict = {
|
1110
|
+
engine: engine.replace("_", " ").title()
|
1111
|
+
for engine in search_engines
|
1112
|
+
}
|
1113
|
+
else:
|
1114
|
+
engines_dict = search_engines
|
1115
|
+
|
1116
|
+
# Make sure searxng is included
|
1117
|
+
if "searxng" not in engines_dict:
|
1118
|
+
engines_dict["searxng"] = "SearXNG (Self-hosted)"
|
1119
|
+
|
1120
|
+
# Format as options for dropdown
|
1121
|
+
engine_options = [
|
1122
|
+
{
|
1123
|
+
"value": key,
|
1124
|
+
"label": (
|
1125
|
+
value
|
1126
|
+
if isinstance(value, str)
|
1127
|
+
else key.replace("_", " ").title()
|
1128
|
+
),
|
1129
|
+
}
|
1130
|
+
for key, value in engines_dict.items()
|
1131
|
+
]
|
1132
|
+
|
1133
|
+
return jsonify({"engines": engines_dict, "engine_options": engine_options})
|
1134
|
+
except Exception as e:
|
1135
|
+
# If both methods fail, return default engines with searxng
|
1136
|
+
logger.error(f"Error getting available search engines from factory: {e}")
|
1137
|
+
|
1138
|
+
# Use hardcoded defaults from search_engines.toml
|
1139
|
+
defaults = {
|
1140
|
+
"wikipedia": "Wikipedia",
|
1141
|
+
"arxiv": "ArXiv Papers",
|
1142
|
+
"pubmed": "PubMed Medical",
|
1143
|
+
"github": "GitHub Code",
|
1144
|
+
"searxng": "SearXNG (Self-hosted)",
|
1145
|
+
"serpapi": "SerpAPI (Google)",
|
1146
|
+
"google_pse": "Google PSE",
|
1147
|
+
"auto": "Auto-select",
|
1148
|
+
}
|
1149
|
+
|
1150
|
+
engine_options = [
|
1151
|
+
{"value": key, "label": value} for key, value in defaults.items()
|
1152
|
+
]
|
1153
|
+
|
1154
|
+
return jsonify({"engines": defaults, "engine_options": engine_options})
|
1155
|
+
except Exception as e:
|
1156
|
+
logger.error(f"Error getting available search engines: {e}")
|
1157
|
+
return jsonify({"error": str(e)}), 500
|
1158
|
+
|
1159
|
+
|
1160
|
+
def get_engines_from_file():
|
1161
|
+
"""Get available search engines directly from the toml file"""
|
1162
|
+
try:
|
1163
|
+
# Try to load from the actual config directory
|
1164
|
+
config_dir = get_config_dir()
|
1165
|
+
search_engines_file = config_dir / "config" / "search_engines.toml"
|
1166
|
+
|
1167
|
+
# If file doesn't exist in user config, try the defaults
|
1168
|
+
if not search_engines_file.exists():
|
1169
|
+
# Look in the defaults folder instead
|
1170
|
+
import inspect
|
1171
|
+
|
1172
|
+
from ...defaults import search_engines
|
1173
|
+
|
1174
|
+
# Get the path to the search_engines.toml file
|
1175
|
+
module_path = inspect.getfile(search_engines)
|
1176
|
+
default_file = Path(module_path)
|
1177
|
+
|
1178
|
+
if default_file.exists() and default_file.suffix == ".toml":
|
1179
|
+
search_engines_file = default_file
|
1180
|
+
|
1181
|
+
# If we found a file, load it
|
1182
|
+
if search_engines_file.exists():
|
1183
|
+
data = toml.load(search_engines_file)
|
1184
|
+
|
1185
|
+
# Filter out the metadata entries (like DEFAULT_SEARCH_ENGINE)
|
1186
|
+
engines = {k: v for k, v in data.items() if isinstance(v, dict)}
|
1187
|
+
|
1188
|
+
# Add display names for each engine
|
1189
|
+
for key, engine in engines.items():
|
1190
|
+
if "display_name" not in engine:
|
1191
|
+
# Create a display name from the key
|
1192
|
+
engine["display_name"] = key.replace("_", " ").title()
|
1193
|
+
|
1194
|
+
return engines
|
1195
|
+
|
1196
|
+
return None
|
1197
|
+
except Exception as e:
|
1198
|
+
logger.error(f"Error loading search engines from file: {e}")
|
1199
|
+
return None
|
1200
|
+
|
1201
|
+
|
1202
|
+
# Legacy routes for backward compatibility - these will redirect to the new routes
|
1203
|
+
@settings_bp.route("/main", methods=["GET"])
|
1204
|
+
def main_config_page():
|
1205
|
+
"""Redirect to app settings page"""
|
1206
|
+
return redirect(url_for("settings.settings_page"))
|
1207
|
+
|
1208
|
+
|
1209
|
+
@settings_bp.route("/collections", methods=["GET"])
|
1210
|
+
def collections_config_page():
|
1211
|
+
"""Redirect to app settings page"""
|
1212
|
+
return redirect(url_for("settings.settings_page"))
|
1213
|
+
|
1214
|
+
|
1215
|
+
@settings_bp.route("/api_keys", methods=["GET"])
|
1216
|
+
def api_keys_config_page():
|
1217
|
+
"""Redirect to LLM settings page"""
|
1218
|
+
return redirect(url_for("settings.settings_page"))
|
1219
|
+
|
1220
|
+
|
1221
|
+
@settings_bp.route("/search_engines", methods=["GET"])
|
1222
|
+
def search_engines_config_page():
|
1223
|
+
"""Redirect to search settings page"""
|
1224
|
+
return redirect(url_for("settings.settings_page"))
|
1225
|
+
|
1226
|
+
|
1227
|
+
@settings_bp.route("/open_file_location", methods=["POST"])
|
1228
|
+
def open_file_location():
|
1229
|
+
"""Open the location of a configuration file"""
|
1230
|
+
file_path = request.form.get("file_path")
|
1231
|
+
|
1232
|
+
if not file_path:
|
1233
|
+
flash("No file path provided", "error")
|
1234
|
+
return redirect(url_for("settings.settings_page"))
|
1235
|
+
|
1236
|
+
# Get the directory containing the file
|
1237
|
+
dir_path = os.path.dirname(os.path.abspath(file_path))
|
1238
|
+
|
1239
|
+
# Open the directory in the file explorer
|
1240
|
+
try:
|
1241
|
+
if platform.system() == "Windows":
|
1242
|
+
subprocess.Popen(f'explorer "{dir_path}"')
|
1243
|
+
elif platform.system() == "Darwin": # macOS
|
1244
|
+
subprocess.Popen(["open", dir_path])
|
1245
|
+
else: # Linux
|
1246
|
+
subprocess.Popen(["xdg-open", dir_path])
|
1247
|
+
|
1248
|
+
flash(f"Opening folder: {dir_path}", "success")
|
1249
|
+
except Exception as e:
|
1250
|
+
flash(f"Error opening folder: {str(e)}", "error")
|
1251
|
+
|
1252
|
+
# Redirect back to the settings page
|
1253
|
+
return redirect(url_for("settings.settings_page"))
|
1254
|
+
|
1255
|
+
|
1256
|
+
@settings_bp.context_processor
|
1257
|
+
def inject_csrf_token():
|
1258
|
+
"""Inject CSRF token into the template context for all settings routes."""
|
1259
|
+
return dict(csrf_token=generate_csrf)
|
1260
|
+
|
1261
|
+
|
1262
|
+
@settings_bp.route("/fix_corrupted_settings", methods=["POST"])
|
1263
|
+
def fix_corrupted_settings():
|
1264
|
+
"""Fix corrupted settings in the database"""
|
1265
|
+
db_session = get_db_session()
|
1266
|
+
|
1267
|
+
try:
|
1268
|
+
# Track fixed and removed settings
|
1269
|
+
fixed_settings = []
|
1270
|
+
removed_duplicate_settings = []
|
1271
|
+
fixed_scoping_issues = []
|
1272
|
+
|
1273
|
+
# First, find and remove duplicate settings with the same key
|
1274
|
+
# This happens because of errors in settings import/export
|
1275
|
+
from sqlalchemy import func as sql_func
|
1276
|
+
|
1277
|
+
# Find keys with duplicates
|
1278
|
+
duplicate_keys = (
|
1279
|
+
db_session.query(Setting.key)
|
1280
|
+
.group_by(Setting.key)
|
1281
|
+
.having(sql_func.count(Setting.key) > 1)
|
1282
|
+
.all()
|
1283
|
+
)
|
1284
|
+
duplicate_keys = [key[0] for key in duplicate_keys]
|
1285
|
+
|
1286
|
+
# For each duplicate key, keep the latest updated one and remove others
|
1287
|
+
for key in duplicate_keys:
|
1288
|
+
dupe_settings = (
|
1289
|
+
db_session.query(Setting)
|
1290
|
+
.filter(Setting.key == key)
|
1291
|
+
.order_by(Setting.updated_at.desc())
|
1292
|
+
.all()
|
1293
|
+
)
|
1294
|
+
|
1295
|
+
# Keep the first one (most recently updated) and delete the rest
|
1296
|
+
for i, setting in enumerate(dupe_settings):
|
1297
|
+
if i > 0: # Skip the first one (keep it)
|
1298
|
+
db_session.delete(setting)
|
1299
|
+
removed_duplicate_settings.append(key)
|
1300
|
+
|
1301
|
+
# Fix scoping issues - remove app.* settings that should be in other categories
|
1302
|
+
# Report settings
|
1303
|
+
for key in [
|
1304
|
+
"app.enable_fact_checking",
|
1305
|
+
"app.knowledge_accumulation",
|
1306
|
+
"app.knowledge_accumulation_context_limit",
|
1307
|
+
"app.output_dir",
|
1308
|
+
]:
|
1309
|
+
setting = db_session.query(Setting).filter(Setting.key == key).first()
|
1310
|
+
if setting:
|
1311
|
+
# Move to proper category if not already there
|
1312
|
+
proper_key = key.replace("app.", "report.")
|
1313
|
+
existing_proper = (
|
1314
|
+
db_session.query(Setting).filter(Setting.key == proper_key).first()
|
1315
|
+
)
|
1316
|
+
|
1317
|
+
if not existing_proper:
|
1318
|
+
# Create proper setting
|
1319
|
+
new_setting = Setting(
|
1320
|
+
key=proper_key,
|
1321
|
+
value=setting.value,
|
1322
|
+
type=SettingType.REPORT,
|
1323
|
+
name=setting.name,
|
1324
|
+
description=setting.description,
|
1325
|
+
category=(
|
1326
|
+
setting.category.replace("app", "report")
|
1327
|
+
if setting.category
|
1328
|
+
else "report_parameters"
|
1329
|
+
),
|
1330
|
+
ui_element=setting.ui_element,
|
1331
|
+
options=setting.options,
|
1332
|
+
min_value=setting.min_value,
|
1333
|
+
max_value=setting.max_value,
|
1334
|
+
step=setting.step,
|
1335
|
+
visible=setting.visible,
|
1336
|
+
editable=setting.editable,
|
1337
|
+
)
|
1338
|
+
db_session.add(new_setting)
|
1339
|
+
|
1340
|
+
# Delete the app one
|
1341
|
+
db_session.delete(setting)
|
1342
|
+
fixed_scoping_issues.append(key)
|
1343
|
+
|
1344
|
+
# Search settings
|
1345
|
+
for key in [
|
1346
|
+
"app.research_iterations",
|
1347
|
+
"app.questions_per_iteration",
|
1348
|
+
"app.search_engine",
|
1349
|
+
"app.iterations",
|
1350
|
+
"app.max_results",
|
1351
|
+
"app.region",
|
1352
|
+
"app.safe_search",
|
1353
|
+
"app.search_language",
|
1354
|
+
"app.snippets_only",
|
1355
|
+
]:
|
1356
|
+
setting = db_session.query(Setting).filter(Setting.key == key).first()
|
1357
|
+
if setting:
|
1358
|
+
# Move to proper category if not already there
|
1359
|
+
proper_key = key.replace("app.", "search.")
|
1360
|
+
existing_proper = (
|
1361
|
+
db_session.query(Setting).filter(Setting.key == proper_key).first()
|
1362
|
+
)
|
1363
|
+
|
1364
|
+
if not existing_proper:
|
1365
|
+
# Create proper setting
|
1366
|
+
new_setting = Setting(
|
1367
|
+
key=proper_key,
|
1368
|
+
value=setting.value,
|
1369
|
+
type=SettingType.SEARCH,
|
1370
|
+
name=setting.name,
|
1371
|
+
description=setting.description,
|
1372
|
+
category=(
|
1373
|
+
setting.category.replace("app", "search")
|
1374
|
+
if setting.category
|
1375
|
+
else "search_parameters"
|
1376
|
+
),
|
1377
|
+
ui_element=setting.ui_element,
|
1378
|
+
options=setting.options,
|
1379
|
+
min_value=setting.min_value,
|
1380
|
+
max_value=setting.max_value,
|
1381
|
+
step=setting.step,
|
1382
|
+
visible=setting.visible,
|
1383
|
+
editable=setting.editable,
|
1384
|
+
)
|
1385
|
+
db_session.add(new_setting)
|
1386
|
+
|
1387
|
+
# Delete the app one
|
1388
|
+
db_session.delete(setting)
|
1389
|
+
fixed_scoping_issues.append(key)
|
1390
|
+
|
1391
|
+
# LLM settings
|
1392
|
+
for key in [
|
1393
|
+
"app.model",
|
1394
|
+
"app.provider",
|
1395
|
+
"app.temperature",
|
1396
|
+
"app.max_tokens",
|
1397
|
+
"app.openai_endpoint_url",
|
1398
|
+
"app.lmstudio_url",
|
1399
|
+
"app.llamacpp_model_path",
|
1400
|
+
]:
|
1401
|
+
setting = db_session.query(Setting).filter(Setting.key == key).first()
|
1402
|
+
if setting:
|
1403
|
+
# Move to proper category if not already there
|
1404
|
+
proper_key = key.replace("app.", "llm.")
|
1405
|
+
existing_proper = (
|
1406
|
+
db_session.query(Setting).filter(Setting.key == proper_key).first()
|
1407
|
+
)
|
1408
|
+
|
1409
|
+
if not existing_proper:
|
1410
|
+
# Create proper setting
|
1411
|
+
new_setting = Setting(
|
1412
|
+
key=proper_key,
|
1413
|
+
value=setting.value,
|
1414
|
+
type=SettingType.LLM,
|
1415
|
+
name=setting.name,
|
1416
|
+
description=setting.description,
|
1417
|
+
category=(
|
1418
|
+
setting.category.replace("app", "llm")
|
1419
|
+
if setting.category
|
1420
|
+
else "llm_parameters"
|
1421
|
+
),
|
1422
|
+
ui_element=setting.ui_element,
|
1423
|
+
options=setting.options,
|
1424
|
+
min_value=setting.min_value,
|
1425
|
+
max_value=setting.max_value,
|
1426
|
+
step=setting.step,
|
1427
|
+
visible=setting.visible,
|
1428
|
+
editable=setting.editable,
|
1429
|
+
)
|
1430
|
+
db_session.add(new_setting)
|
1431
|
+
|
1432
|
+
# Delete the app one
|
1433
|
+
db_session.delete(setting)
|
1434
|
+
fixed_scoping_issues.append(key)
|
1435
|
+
|
1436
|
+
# Check for settings with corrupted values
|
1437
|
+
all_settings = db_session.query(Setting).all()
|
1438
|
+
for setting in all_settings:
|
1439
|
+
# Check different types of corruption
|
1440
|
+
is_corrupted = False
|
1441
|
+
|
1442
|
+
if setting.value is None:
|
1443
|
+
is_corrupted = True
|
1444
|
+
elif isinstance(setting.value, str) and setting.value in [
|
1445
|
+
"{",
|
1446
|
+
"[",
|
1447
|
+
"{}",
|
1448
|
+
"[]",
|
1449
|
+
"[object Object]",
|
1450
|
+
"null",
|
1451
|
+
"undefined",
|
1452
|
+
]:
|
1453
|
+
is_corrupted = True
|
1454
|
+
elif isinstance(setting.value, dict) and len(setting.value) == 0:
|
1455
|
+
is_corrupted = True
|
1456
|
+
|
1457
|
+
# Skip if not corrupted
|
1458
|
+
if not is_corrupted:
|
1459
|
+
continue
|
1460
|
+
|
1461
|
+
# Get default value from migrations
|
1462
|
+
# Import commented out as it's not directly used
|
1463
|
+
# from ..database.migrations import setup_predefined_settings
|
1464
|
+
|
1465
|
+
default_value = None
|
1466
|
+
|
1467
|
+
# Try to find a matching default setting based on key
|
1468
|
+
if setting.key.startswith("llm."):
|
1469
|
+
if setting.key == "llm.model":
|
1470
|
+
default_value = "gpt-3.5-turbo"
|
1471
|
+
elif setting.key == "llm.provider":
|
1472
|
+
default_value = "openai"
|
1473
|
+
elif setting.key == "llm.temperature":
|
1474
|
+
default_value = 0.7
|
1475
|
+
elif setting.key == "llm.max_tokens":
|
1476
|
+
default_value = 1024
|
1477
|
+
elif setting.key.startswith("search."):
|
1478
|
+
if setting.key == "search.tool":
|
1479
|
+
default_value = "auto"
|
1480
|
+
elif setting.key == "search.max_results":
|
1481
|
+
default_value = 10
|
1482
|
+
elif setting.key == "search.region":
|
1483
|
+
default_value = "us"
|
1484
|
+
elif setting.key == "search.research_iterations":
|
1485
|
+
default_value = 2
|
1486
|
+
elif setting.key == "search.questions_per_iteration":
|
1487
|
+
default_value = 3
|
1488
|
+
elif setting.key == "search.searches_per_section":
|
1489
|
+
default_value = 2
|
1490
|
+
elif setting.key == "search.skip_relevance_filter":
|
1491
|
+
default_value = False
|
1492
|
+
elif setting.key == "search.safe_search":
|
1493
|
+
default_value = True
|
1494
|
+
elif setting.key == "search.search_language":
|
1495
|
+
default_value = "English"
|
1496
|
+
elif setting.key.startswith("report."):
|
1497
|
+
if setting.key == "report.searches_per_section":
|
1498
|
+
default_value = 2
|
1499
|
+
elif setting.key == "report.enable_fact_checking":
|
1500
|
+
default_value = True
|
1501
|
+
elif setting.key == "report.detailed_citations":
|
1502
|
+
default_value = True
|
1503
|
+
elif setting.key.startswith("app."):
|
1504
|
+
if setting.key == "app.theme" or setting.key == "app.default_theme":
|
1505
|
+
default_value = "dark"
|
1506
|
+
elif setting.key == "app.enable_notifications":
|
1507
|
+
default_value = True
|
1508
|
+
elif (
|
1509
|
+
setting.key == "app.enable_web"
|
1510
|
+
or setting.key == "app.web_interface"
|
1511
|
+
):
|
1512
|
+
default_value = True
|
1513
|
+
elif setting.key == "app.host":
|
1514
|
+
default_value = "0.0.0.0"
|
1515
|
+
elif setting.key == "app.port":
|
1516
|
+
default_value = 5000
|
1517
|
+
elif setting.key == "app.debug":
|
1518
|
+
default_value = True
|
1519
|
+
|
1520
|
+
# Update the setting with the default value if found
|
1521
|
+
if default_value is not None:
|
1522
|
+
setting.value = default_value
|
1523
|
+
fixed_settings.append(setting.key)
|
1524
|
+
else:
|
1525
|
+
# If no default found but it's a corrupted JSON, set to empty object
|
1526
|
+
if setting.key.startswith("report."):
|
1527
|
+
setting.value = {}
|
1528
|
+
fixed_settings.append(setting.key)
|
1529
|
+
|
1530
|
+
# Commit changes
|
1531
|
+
if fixed_settings or removed_duplicate_settings or fixed_scoping_issues:
|
1532
|
+
db_session.commit()
|
1533
|
+
logger.info(
|
1534
|
+
f"Fixed {len(fixed_settings)} corrupted settings: {', '.join(fixed_settings)}"
|
1535
|
+
)
|
1536
|
+
if removed_duplicate_settings:
|
1537
|
+
logger.info(
|
1538
|
+
f"Removed {len(removed_duplicate_settings)} duplicate settings"
|
1539
|
+
)
|
1540
|
+
if fixed_scoping_issues:
|
1541
|
+
logger.info(f"Fixed {len(fixed_scoping_issues)} scoping issues")
|
1542
|
+
|
1543
|
+
# Return success
|
1544
|
+
return jsonify(
|
1545
|
+
{
|
1546
|
+
"status": "success",
|
1547
|
+
"message": f"Fixed {len(fixed_settings)} corrupted settings, removed {len(removed_duplicate_settings)} duplicates, and fixed {len(fixed_scoping_issues)} scoping issues",
|
1548
|
+
"fixed_settings": fixed_settings,
|
1549
|
+
"removed_duplicates": removed_duplicate_settings,
|
1550
|
+
"fixed_scoping": fixed_scoping_issues,
|
1551
|
+
}
|
1552
|
+
)
|
1553
|
+
|
1554
|
+
except Exception as e:
|
1555
|
+
logger.error(f"Error fixing corrupted settings: {e}")
|
1556
|
+
db_session.rollback()
|
1557
|
+
return (
|
1558
|
+
jsonify(
|
1559
|
+
{
|
1560
|
+
"status": "error",
|
1561
|
+
"message": f"Error fixing corrupted settings: {str(e)}",
|
1562
|
+
}
|
1563
|
+
),
|
1564
|
+
500,
|
1565
|
+
)
|
1566
|
+
|
1567
|
+
|
1568
|
+
@settings_bp.route("/api/ollama-status", methods=["GET"])
|
1569
|
+
def check_ollama_status():
|
1570
|
+
"""Check if Ollama is running and available"""
|
1571
|
+
try:
|
1572
|
+
# Set a shorter timeout for the request
|
1573
|
+
base_url = os.getenv(
|
1574
|
+
"OLLAMA_BASE_URL",
|
1575
|
+
"http://localhost:11434",
|
1576
|
+
)
|
1577
|
+
response = requests.get(f"{base_url}/api/version", timeout=2.0)
|
1578
|
+
|
1579
|
+
if response.status_code == 200:
|
1580
|
+
return jsonify(
|
1581
|
+
{"running": True, "version": response.json().get("version", "unknown")}
|
1582
|
+
)
|
1583
|
+
else:
|
1584
|
+
return jsonify(
|
1585
|
+
{
|
1586
|
+
"running": False,
|
1587
|
+
"error": f"Ollama returned status code {response.status_code}",
|
1588
|
+
}
|
1589
|
+
)
|
1590
|
+
except requests.exceptions.RequestException as e:
|
1591
|
+
logger.info(f"Ollama check failed: {str(e)}")
|
1592
|
+
return jsonify({"running": False, "error": str(e)})
|