local-deep-research 0.1.26__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +154 -160
- 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 +87 -45
- local_deep_research/search_system.py +153 -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 +1583 -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 +212 -160
- 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.2.dist-info}/METADATA +177 -97
- local_deep_research-0.2.2.dist-info/RECORD +135 -0
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/WHEEL +1 -2
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.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.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,559 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
|
5
|
+
import requests
|
6
|
+
from flask import Blueprint, current_app, jsonify, request
|
7
|
+
|
8
|
+
from ..models.database import get_db_connection
|
9
|
+
from ..routes.research_routes import active_research, termination_flags
|
10
|
+
from ..services.research_service import (
|
11
|
+
cancel_research,
|
12
|
+
run_research_process,
|
13
|
+
start_research_process,
|
14
|
+
)
|
15
|
+
from ..services.resource_service import (
|
16
|
+
add_resource,
|
17
|
+
delete_resource,
|
18
|
+
get_resources_for_research,
|
19
|
+
)
|
20
|
+
|
21
|
+
# Create blueprint
|
22
|
+
api_bp = Blueprint("api", __name__)
|
23
|
+
logger = logging.getLogger(__name__)
|
24
|
+
|
25
|
+
|
26
|
+
# API Routes
|
27
|
+
@api_bp.route("/start", methods=["POST"])
|
28
|
+
def api_start_research():
|
29
|
+
"""
|
30
|
+
Start a new research process
|
31
|
+
"""
|
32
|
+
data = request.json
|
33
|
+
query = data.get("query", "")
|
34
|
+
mode = data.get("mode", "quick")
|
35
|
+
|
36
|
+
if not query:
|
37
|
+
return jsonify({"status": "error", "message": "Query is required"}), 400
|
38
|
+
|
39
|
+
try:
|
40
|
+
# Create a record in the database with explicit UTC timestamp
|
41
|
+
from datetime import datetime
|
42
|
+
|
43
|
+
created_at = datetime.utcnow().isoformat()
|
44
|
+
|
45
|
+
conn = get_db_connection()
|
46
|
+
cursor = conn.cursor()
|
47
|
+
|
48
|
+
# Save basic research settings for API route
|
49
|
+
research_settings = {
|
50
|
+
"model_provider": "OLLAMA", # Default
|
51
|
+
"model": "llama2", # Default
|
52
|
+
"search_engine": "auto", # Default
|
53
|
+
}
|
54
|
+
|
55
|
+
cursor.execute(
|
56
|
+
"INSERT INTO research_history (query, mode, status, created_at, progress_log, metadata) VALUES (?, ?, ?, ?, ?, ?)",
|
57
|
+
(
|
58
|
+
query,
|
59
|
+
mode,
|
60
|
+
"in_progress",
|
61
|
+
created_at,
|
62
|
+
json.dumps(
|
63
|
+
[{"time": created_at, "message": "Research started", "progress": 0}]
|
64
|
+
),
|
65
|
+
json.dumps(research_settings),
|
66
|
+
),
|
67
|
+
)
|
68
|
+
research_id = cursor.lastrowid
|
69
|
+
conn.commit()
|
70
|
+
conn.close()
|
71
|
+
|
72
|
+
# Start the research process
|
73
|
+
research_thread = start_research_process(
|
74
|
+
research_id,
|
75
|
+
query,
|
76
|
+
mode,
|
77
|
+
active_research,
|
78
|
+
termination_flags,
|
79
|
+
run_research_process,
|
80
|
+
)
|
81
|
+
|
82
|
+
# Store the thread reference
|
83
|
+
active_research[research_id]["thread"] = research_thread
|
84
|
+
|
85
|
+
return jsonify(
|
86
|
+
{
|
87
|
+
"status": "success",
|
88
|
+
"message": "Research started successfully",
|
89
|
+
"research_id": research_id,
|
90
|
+
}
|
91
|
+
)
|
92
|
+
except Exception as e:
|
93
|
+
logger.error(f"Error starting research: {str(e)}")
|
94
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
95
|
+
|
96
|
+
|
97
|
+
@api_bp.route("/status/<int:research_id>", methods=["GET"])
|
98
|
+
def api_research_status(research_id):
|
99
|
+
"""
|
100
|
+
Get the status of a research process
|
101
|
+
"""
|
102
|
+
try:
|
103
|
+
conn = get_db_connection()
|
104
|
+
cursor = conn.cursor()
|
105
|
+
cursor.execute(
|
106
|
+
"SELECT status, progress, completed_at, report_path, metadata FROM research_history WHERE id = ?",
|
107
|
+
(research_id,),
|
108
|
+
)
|
109
|
+
result = cursor.fetchone()
|
110
|
+
conn.close()
|
111
|
+
|
112
|
+
if result is None:
|
113
|
+
return jsonify({"error": "Research not found"}), 404
|
114
|
+
|
115
|
+
status, progress, completed_at, report_path, metadata_str = result
|
116
|
+
|
117
|
+
# Parse metadata if it exists
|
118
|
+
metadata = {}
|
119
|
+
if metadata_str:
|
120
|
+
try:
|
121
|
+
metadata = json.loads(metadata_str)
|
122
|
+
except json.JSONDecodeError:
|
123
|
+
logger.warning(f"Invalid JSON in metadata for research {research_id}")
|
124
|
+
|
125
|
+
return jsonify(
|
126
|
+
{
|
127
|
+
"status": status,
|
128
|
+
"progress": progress,
|
129
|
+
"completed_at": completed_at,
|
130
|
+
"report_path": report_path,
|
131
|
+
"metadata": metadata,
|
132
|
+
}
|
133
|
+
)
|
134
|
+
except Exception as e:
|
135
|
+
logger.error(f"Error getting research status: {str(e)}")
|
136
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
137
|
+
|
138
|
+
|
139
|
+
@api_bp.route("/terminate/<int:research_id>", methods=["POST"])
|
140
|
+
def api_terminate_research(research_id):
|
141
|
+
"""
|
142
|
+
Terminate a research process
|
143
|
+
"""
|
144
|
+
try:
|
145
|
+
result = cancel_research(research_id)
|
146
|
+
return jsonify(
|
147
|
+
{"status": "success", "message": "Research terminated", "result": result}
|
148
|
+
)
|
149
|
+
except Exception as e:
|
150
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
151
|
+
|
152
|
+
|
153
|
+
@api_bp.route("/resources/<int:research_id>", methods=["GET"])
|
154
|
+
def api_get_resources(research_id):
|
155
|
+
"""
|
156
|
+
Get resources for a specific research
|
157
|
+
"""
|
158
|
+
try:
|
159
|
+
resources = get_resources_for_research(research_id)
|
160
|
+
return jsonify({"status": "success", "resources": resources})
|
161
|
+
except Exception as e:
|
162
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
163
|
+
|
164
|
+
|
165
|
+
@api_bp.route("/resources/<int:research_id>", methods=["POST"])
|
166
|
+
def api_add_resource(research_id):
|
167
|
+
"""
|
168
|
+
Add a new resource to a research project
|
169
|
+
"""
|
170
|
+
try:
|
171
|
+
data = request.json
|
172
|
+
|
173
|
+
# Required fields
|
174
|
+
title = data.get("title")
|
175
|
+
url = data.get("url")
|
176
|
+
|
177
|
+
# Optional fields
|
178
|
+
content_preview = data.get("content_preview")
|
179
|
+
source_type = data.get("source_type", "web")
|
180
|
+
metadata = data.get("metadata", {})
|
181
|
+
|
182
|
+
# Validate required fields
|
183
|
+
if not title or not url:
|
184
|
+
return (
|
185
|
+
jsonify({"status": "error", "message": "Title and URL are required"}),
|
186
|
+
400,
|
187
|
+
)
|
188
|
+
|
189
|
+
# Check if the research exists
|
190
|
+
conn = get_db_connection()
|
191
|
+
cursor = conn.cursor()
|
192
|
+
cursor.execute("SELECT id FROM research_history WHERE id = ?", (research_id,))
|
193
|
+
result = cursor.fetchone()
|
194
|
+
conn.close()
|
195
|
+
|
196
|
+
if not result:
|
197
|
+
return jsonify({"status": "error", "message": "Research not found"}), 404
|
198
|
+
|
199
|
+
# Add the resource
|
200
|
+
resource_id = add_resource(
|
201
|
+
research_id=research_id,
|
202
|
+
title=title,
|
203
|
+
url=url,
|
204
|
+
content_preview=content_preview,
|
205
|
+
source_type=source_type,
|
206
|
+
metadata=metadata,
|
207
|
+
)
|
208
|
+
|
209
|
+
return jsonify(
|
210
|
+
{
|
211
|
+
"status": "success",
|
212
|
+
"message": "Resource added successfully",
|
213
|
+
"resource_id": resource_id,
|
214
|
+
}
|
215
|
+
)
|
216
|
+
except Exception as e:
|
217
|
+
logger.error(f"Error adding resource: {str(e)}")
|
218
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
219
|
+
|
220
|
+
|
221
|
+
@api_bp.route(
|
222
|
+
"/resources/<int:research_id>/delete/<int:resource_id>", methods=["DELETE"]
|
223
|
+
)
|
224
|
+
def api_delete_resource(research_id, resource_id):
|
225
|
+
"""
|
226
|
+
Delete a resource from a research project
|
227
|
+
"""
|
228
|
+
try:
|
229
|
+
# Delete the resource
|
230
|
+
success = delete_resource(resource_id)
|
231
|
+
|
232
|
+
if success:
|
233
|
+
return jsonify(
|
234
|
+
{"status": "success", "message": "Resource deleted successfully"}
|
235
|
+
)
|
236
|
+
else:
|
237
|
+
return jsonify({"status": "error", "message": "Resource not found"}), 404
|
238
|
+
except Exception as e:
|
239
|
+
logger.error(f"Error deleting resource: {str(e)}")
|
240
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
241
|
+
|
242
|
+
|
243
|
+
@api_bp.route("/check/ollama_status", methods=["GET"])
|
244
|
+
def check_ollama_status():
|
245
|
+
"""
|
246
|
+
Check if Ollama API is running
|
247
|
+
"""
|
248
|
+
try:
|
249
|
+
# Get Ollama URL from config
|
250
|
+
llm_config = current_app.config.get("LLM_CONFIG", {})
|
251
|
+
provider = llm_config.get("provider", "ollama")
|
252
|
+
|
253
|
+
if provider.lower() != "ollama":
|
254
|
+
return jsonify(
|
255
|
+
{"running": True, "message": f"Using provider: {provider}, not Ollama"}
|
256
|
+
)
|
257
|
+
|
258
|
+
# Get Ollama API URL
|
259
|
+
ollama_base_url = os.getenv(
|
260
|
+
"OLLAMA_BASE_URL",
|
261
|
+
llm_config.get("ollama_base_url", "http://localhost:11434"),
|
262
|
+
)
|
263
|
+
|
264
|
+
logger.info(f"Checking Ollama status at: {ollama_base_url}")
|
265
|
+
|
266
|
+
# Check if Ollama is running
|
267
|
+
try:
|
268
|
+
response = requests.get(f"{ollama_base_url}/api/tags", timeout=5)
|
269
|
+
|
270
|
+
# Add response details for debugging
|
271
|
+
logger.debug(f"Ollama status check response code: {response.status_code}")
|
272
|
+
|
273
|
+
if response.status_code == 200:
|
274
|
+
# Try to validate the response content
|
275
|
+
try:
|
276
|
+
data = response.json()
|
277
|
+
|
278
|
+
# Check the format
|
279
|
+
if "models" in data:
|
280
|
+
model_count = len(data.get("models", []))
|
281
|
+
logger.info(
|
282
|
+
f"Ollama service is running with {model_count} models (new API format)"
|
283
|
+
)
|
284
|
+
else:
|
285
|
+
# Older API format
|
286
|
+
model_count = len(data)
|
287
|
+
logger.info(
|
288
|
+
f"Ollama service is running with {model_count} models (old API format)"
|
289
|
+
)
|
290
|
+
|
291
|
+
return jsonify(
|
292
|
+
{
|
293
|
+
"running": True,
|
294
|
+
"message": f"Ollama service is running with {model_count} models",
|
295
|
+
"model_count": model_count,
|
296
|
+
}
|
297
|
+
)
|
298
|
+
except ValueError as json_err:
|
299
|
+
logger.warning(f"Ollama returned invalid JSON: {json_err}")
|
300
|
+
# It's running but returned invalid JSON
|
301
|
+
return jsonify(
|
302
|
+
{
|
303
|
+
"running": True,
|
304
|
+
"message": "Ollama service is running but returned invalid data format",
|
305
|
+
"error_details": str(json_err),
|
306
|
+
}
|
307
|
+
)
|
308
|
+
else:
|
309
|
+
logger.warning(
|
310
|
+
f"Ollama returned non-200 status code: {response.status_code}"
|
311
|
+
)
|
312
|
+
return jsonify(
|
313
|
+
{
|
314
|
+
"running": False,
|
315
|
+
"message": f"Ollama service returned status code: {response.status_code}",
|
316
|
+
"status_code": response.status_code,
|
317
|
+
}
|
318
|
+
)
|
319
|
+
|
320
|
+
except requests.exceptions.ConnectionError as conn_err:
|
321
|
+
logger.warning(f"Ollama connection error: {conn_err}")
|
322
|
+
return jsonify(
|
323
|
+
{
|
324
|
+
"running": False,
|
325
|
+
"message": "Ollama service is not running or not accessible",
|
326
|
+
"error_type": "connection_error",
|
327
|
+
"error_details": str(conn_err),
|
328
|
+
}
|
329
|
+
)
|
330
|
+
except requests.exceptions.Timeout as timeout_err:
|
331
|
+
logger.warning(f"Ollama request timed out: {timeout_err}")
|
332
|
+
return jsonify(
|
333
|
+
{
|
334
|
+
"running": False,
|
335
|
+
"message": "Ollama service request timed out after 5 seconds",
|
336
|
+
"error_type": "timeout",
|
337
|
+
"error_details": str(timeout_err),
|
338
|
+
}
|
339
|
+
)
|
340
|
+
|
341
|
+
except Exception as e:
|
342
|
+
logger.error(f"Error checking Ollama status: {str(e)}")
|
343
|
+
import traceback
|
344
|
+
|
345
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
346
|
+
return jsonify(
|
347
|
+
{
|
348
|
+
"running": False,
|
349
|
+
"message": f"Error checking Ollama: {str(e)}",
|
350
|
+
"error_type": "exception",
|
351
|
+
"error_details": str(e),
|
352
|
+
}
|
353
|
+
)
|
354
|
+
|
355
|
+
|
356
|
+
@api_bp.route("/check/ollama_model", methods=["GET"])
|
357
|
+
def check_ollama_model():
|
358
|
+
"""
|
359
|
+
Check if the configured Ollama model is available
|
360
|
+
"""
|
361
|
+
try:
|
362
|
+
# Get Ollama configuration
|
363
|
+
llm_config = current_app.config.get("LLM_CONFIG", {})
|
364
|
+
provider = llm_config.get("provider", "ollama")
|
365
|
+
|
366
|
+
if provider.lower() != "ollama":
|
367
|
+
return jsonify(
|
368
|
+
{
|
369
|
+
"available": True,
|
370
|
+
"message": f"Using provider: {provider}, not Ollama",
|
371
|
+
"provider": provider,
|
372
|
+
}
|
373
|
+
)
|
374
|
+
|
375
|
+
# Get model name from request or use config default
|
376
|
+
model_name = request.args.get("model")
|
377
|
+
if not model_name:
|
378
|
+
model_name = llm_config.get("model", "gemma3:12b")
|
379
|
+
|
380
|
+
# Log which model we're checking for debugging
|
381
|
+
logger.info(f"Checking availability of Ollama model: {model_name}")
|
382
|
+
|
383
|
+
ollama_base_url = os.getenv(
|
384
|
+
"OLLAMA_BASE_URL",
|
385
|
+
llm_config.get("ollama_base_url", "http://localhost:11434"),
|
386
|
+
)
|
387
|
+
|
388
|
+
# Check if the model is available
|
389
|
+
try:
|
390
|
+
response = requests.get(f"{ollama_base_url}/api/tags", timeout=5)
|
391
|
+
|
392
|
+
# Log response details for debugging
|
393
|
+
logger.debug(f"Ollama API response status: {response.status_code}")
|
394
|
+
|
395
|
+
if response.status_code != 200:
|
396
|
+
logger.warning(
|
397
|
+
f"Ollama API returned non-200 status: {response.status_code}"
|
398
|
+
)
|
399
|
+
return jsonify(
|
400
|
+
{
|
401
|
+
"available": False,
|
402
|
+
"model": model_name,
|
403
|
+
"message": f"Could not access Ollama service - status code: {response.status_code}",
|
404
|
+
"status_code": response.status_code,
|
405
|
+
}
|
406
|
+
)
|
407
|
+
|
408
|
+
# Try to parse the response
|
409
|
+
try:
|
410
|
+
data = response.json()
|
411
|
+
|
412
|
+
# Debug log the first bit of the response
|
413
|
+
response_preview = (
|
414
|
+
str(data)[:500] + "..." if len(str(data)) > 500 else str(data)
|
415
|
+
)
|
416
|
+
logger.debug(f"Ollama API response data: {response_preview}")
|
417
|
+
|
418
|
+
# Get models based on API format
|
419
|
+
models = []
|
420
|
+
if "models" in data:
|
421
|
+
# Newer Ollama API
|
422
|
+
logger.debug("Using new Ollama API format (models key)")
|
423
|
+
models = data.get("models", [])
|
424
|
+
else:
|
425
|
+
# Older Ollama API format
|
426
|
+
logger.debug("Using old Ollama API format (array)")
|
427
|
+
models = data
|
428
|
+
|
429
|
+
# Log available models for debugging
|
430
|
+
model_names = [m.get("name", "") for m in models]
|
431
|
+
logger.debug(
|
432
|
+
f"Available Ollama models: {', '.join(model_names[:10])}"
|
433
|
+
+ (
|
434
|
+
f" and {len(model_names) - 10} more"
|
435
|
+
if len(model_names) > 10
|
436
|
+
else ""
|
437
|
+
)
|
438
|
+
)
|
439
|
+
|
440
|
+
# Case-insensitive model name comparison
|
441
|
+
model_exists = any(
|
442
|
+
m.get("name", "").lower() == model_name.lower() for m in models
|
443
|
+
)
|
444
|
+
|
445
|
+
if model_exists:
|
446
|
+
logger.info(f"Ollama model {model_name} is available")
|
447
|
+
return jsonify(
|
448
|
+
{
|
449
|
+
"available": True,
|
450
|
+
"model": model_name,
|
451
|
+
"message": f"Model {model_name} is available",
|
452
|
+
"all_models": model_names,
|
453
|
+
}
|
454
|
+
)
|
455
|
+
else:
|
456
|
+
# Check if models were found at all
|
457
|
+
if not models:
|
458
|
+
logger.warning("No models found in Ollama")
|
459
|
+
message = "No models found in Ollama. Please pull models first."
|
460
|
+
else:
|
461
|
+
logger.warning(
|
462
|
+
f"Model {model_name} not found among {len(models)} available models"
|
463
|
+
)
|
464
|
+
message = (
|
465
|
+
f"Model {model_name} is not available. Available models: "
|
466
|
+
+ ", ".join(model_names[:5])
|
467
|
+
) + (
|
468
|
+
f" and {len(model_names) - 5} more"
|
469
|
+
if len(model_names) > 5
|
470
|
+
else ""
|
471
|
+
)
|
472
|
+
|
473
|
+
return jsonify(
|
474
|
+
{
|
475
|
+
"available": False,
|
476
|
+
"model": model_name,
|
477
|
+
"message": message,
|
478
|
+
"all_models": model_names,
|
479
|
+
}
|
480
|
+
)
|
481
|
+
except ValueError as json_err:
|
482
|
+
# JSON parsing error
|
483
|
+
logger.error(f"Failed to parse Ollama API response: {json_err}")
|
484
|
+
return jsonify(
|
485
|
+
{
|
486
|
+
"available": False,
|
487
|
+
"model": model_name,
|
488
|
+
"message": f"Invalid response from Ollama API: {json_err}",
|
489
|
+
"error_type": "json_parse_error",
|
490
|
+
}
|
491
|
+
)
|
492
|
+
|
493
|
+
except requests.exceptions.ConnectionError as conn_err:
|
494
|
+
# Connection error
|
495
|
+
logger.warning(f"Connection error to Ollama API: {conn_err}")
|
496
|
+
return jsonify(
|
497
|
+
{
|
498
|
+
"available": False,
|
499
|
+
"model": model_name,
|
500
|
+
"message": "Could not connect to Ollama service",
|
501
|
+
"error_type": "connection_error",
|
502
|
+
"error_details": str(conn_err),
|
503
|
+
}
|
504
|
+
)
|
505
|
+
except requests.exceptions.Timeout:
|
506
|
+
# Timeout error
|
507
|
+
logger.warning("Timeout connecting to Ollama API")
|
508
|
+
return jsonify(
|
509
|
+
{
|
510
|
+
"available": False,
|
511
|
+
"model": model_name,
|
512
|
+
"message": "Connection to Ollama service timed out",
|
513
|
+
"error_type": "timeout",
|
514
|
+
}
|
515
|
+
)
|
516
|
+
|
517
|
+
except Exception as e:
|
518
|
+
# General exception
|
519
|
+
logger.error(f"Error checking Ollama model: {e}")
|
520
|
+
import traceback
|
521
|
+
|
522
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
523
|
+
|
524
|
+
return jsonify(
|
525
|
+
{
|
526
|
+
"available": False,
|
527
|
+
"model": (
|
528
|
+
model_name
|
529
|
+
if "model_name" in locals()
|
530
|
+
else llm_config.get("model", "gemma3:12b")
|
531
|
+
),
|
532
|
+
"message": f"Error checking model: {str(e)}",
|
533
|
+
"error_type": "exception",
|
534
|
+
"error_details": str(e),
|
535
|
+
}
|
536
|
+
)
|
537
|
+
|
538
|
+
|
539
|
+
# Helper route to get system configuration
|
540
|
+
@api_bp.route("/config", methods=["GET"])
|
541
|
+
def api_get_config():
|
542
|
+
"""
|
543
|
+
Get public system configuration
|
544
|
+
"""
|
545
|
+
# Only return public configuration
|
546
|
+
public_config = {
|
547
|
+
"version": current_app.config.get("VERSION", "0.1.0"),
|
548
|
+
"llm_provider": current_app.config.get("LLM_CONFIG", {}).get(
|
549
|
+
"provider", "ollama"
|
550
|
+
),
|
551
|
+
"search_tool": current_app.config.get("SEARCH_CONFIG", {}).get(
|
552
|
+
"search_tool", "auto"
|
553
|
+
),
|
554
|
+
"features": {
|
555
|
+
"notifications": current_app.config.get("ENABLE_NOTIFICATIONS", False)
|
556
|
+
},
|
557
|
+
}
|
558
|
+
|
559
|
+
return jsonify(public_config)
|