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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. local_deep_research/__init__.py +23 -22
  2. local_deep_research/__main__.py +16 -0
  3. local_deep_research/advanced_search_system/__init__.py +7 -0
  4. local_deep_research/advanced_search_system/filters/__init__.py +8 -0
  5. local_deep_research/advanced_search_system/filters/base_filter.py +38 -0
  6. local_deep_research/advanced_search_system/filters/cross_engine_filter.py +200 -0
  7. local_deep_research/advanced_search_system/findings/base_findings.py +81 -0
  8. local_deep_research/advanced_search_system/findings/repository.py +452 -0
  9. local_deep_research/advanced_search_system/knowledge/__init__.py +1 -0
  10. local_deep_research/advanced_search_system/knowledge/base_knowledge.py +151 -0
  11. local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +159 -0
  12. local_deep_research/advanced_search_system/questions/__init__.py +1 -0
  13. local_deep_research/advanced_search_system/questions/base_question.py +64 -0
  14. local_deep_research/advanced_search_system/questions/decomposition_question.py +445 -0
  15. local_deep_research/advanced_search_system/questions/standard_question.py +119 -0
  16. local_deep_research/advanced_search_system/repositories/__init__.py +7 -0
  17. local_deep_research/advanced_search_system/strategies/__init__.py +1 -0
  18. local_deep_research/advanced_search_system/strategies/base_strategy.py +118 -0
  19. local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +450 -0
  20. local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +312 -0
  21. local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +270 -0
  22. local_deep_research/advanced_search_system/strategies/standard_strategy.py +300 -0
  23. local_deep_research/advanced_search_system/tools/__init__.py +1 -0
  24. local_deep_research/advanced_search_system/tools/base_tool.py +100 -0
  25. local_deep_research/advanced_search_system/tools/knowledge_tools/__init__.py +1 -0
  26. local_deep_research/advanced_search_system/tools/question_tools/__init__.py +1 -0
  27. local_deep_research/advanced_search_system/tools/search_tools/__init__.py +1 -0
  28. local_deep_research/api/__init__.py +5 -5
  29. local_deep_research/api/research_functions.py +154 -160
  30. local_deep_research/app.py +8 -0
  31. local_deep_research/citation_handler.py +25 -16
  32. local_deep_research/{config.py → config/config_files.py} +102 -110
  33. local_deep_research/config/llm_config.py +472 -0
  34. local_deep_research/config/search_config.py +77 -0
  35. local_deep_research/defaults/__init__.py +10 -5
  36. local_deep_research/defaults/main.toml +2 -2
  37. local_deep_research/defaults/search_engines.toml +60 -34
  38. local_deep_research/main.py +121 -19
  39. local_deep_research/migrate_db.py +147 -0
  40. local_deep_research/report_generator.py +87 -45
  41. local_deep_research/search_system.py +153 -283
  42. local_deep_research/setup_data_dir.py +35 -0
  43. local_deep_research/test_migration.py +178 -0
  44. local_deep_research/utilities/__init__.py +0 -0
  45. local_deep_research/utilities/db_utils.py +49 -0
  46. local_deep_research/{utilties → utilities}/enums.py +2 -2
  47. local_deep_research/{utilties → utilities}/llm_utils.py +63 -29
  48. local_deep_research/utilities/search_utilities.py +242 -0
  49. local_deep_research/{utilties → utilities}/setup_utils.py +4 -2
  50. local_deep_research/web/__init__.py +0 -1
  51. local_deep_research/web/app.py +86 -1709
  52. local_deep_research/web/app_factory.py +289 -0
  53. local_deep_research/web/database/README.md +70 -0
  54. local_deep_research/web/database/migrate_to_ldr_db.py +289 -0
  55. local_deep_research/web/database/migrations.py +447 -0
  56. local_deep_research/web/database/models.py +117 -0
  57. local_deep_research/web/database/schema_upgrade.py +107 -0
  58. local_deep_research/web/models/database.py +294 -0
  59. local_deep_research/web/models/settings.py +94 -0
  60. local_deep_research/web/routes/api_routes.py +559 -0
  61. local_deep_research/web/routes/history_routes.py +354 -0
  62. local_deep_research/web/routes/research_routes.py +715 -0
  63. local_deep_research/web/routes/settings_routes.py +1583 -0
  64. local_deep_research/web/services/research_service.py +947 -0
  65. local_deep_research/web/services/resource_service.py +149 -0
  66. local_deep_research/web/services/settings_manager.py +669 -0
  67. local_deep_research/web/services/settings_service.py +187 -0
  68. local_deep_research/web/services/socket_service.py +210 -0
  69. local_deep_research/web/static/css/custom_dropdown.css +277 -0
  70. local_deep_research/web/static/css/settings.css +1223 -0
  71. local_deep_research/web/static/css/styles.css +525 -48
  72. local_deep_research/web/static/js/components/custom_dropdown.js +428 -0
  73. local_deep_research/web/static/js/components/detail.js +348 -0
  74. local_deep_research/web/static/js/components/fallback/formatting.js +122 -0
  75. local_deep_research/web/static/js/components/fallback/ui.js +215 -0
  76. local_deep_research/web/static/js/components/history.js +487 -0
  77. local_deep_research/web/static/js/components/logpanel.js +949 -0
  78. local_deep_research/web/static/js/components/progress.js +1107 -0
  79. local_deep_research/web/static/js/components/research.js +1865 -0
  80. local_deep_research/web/static/js/components/results.js +766 -0
  81. local_deep_research/web/static/js/components/settings.js +3981 -0
  82. local_deep_research/web/static/js/components/settings_sync.js +106 -0
  83. local_deep_research/web/static/js/main.js +226 -0
  84. local_deep_research/web/static/js/services/api.js +253 -0
  85. local_deep_research/web/static/js/services/audio.js +31 -0
  86. local_deep_research/web/static/js/services/formatting.js +119 -0
  87. local_deep_research/web/static/js/services/pdf.js +622 -0
  88. local_deep_research/web/static/js/services/socket.js +882 -0
  89. local_deep_research/web/static/js/services/ui.js +546 -0
  90. local_deep_research/web/templates/base.html +72 -0
  91. local_deep_research/web/templates/components/custom_dropdown.html +47 -0
  92. local_deep_research/web/templates/components/log_panel.html +32 -0
  93. local_deep_research/web/templates/components/mobile_nav.html +22 -0
  94. local_deep_research/web/templates/components/settings_form.html +299 -0
  95. local_deep_research/web/templates/components/sidebar.html +21 -0
  96. local_deep_research/web/templates/pages/details.html +73 -0
  97. local_deep_research/web/templates/pages/history.html +51 -0
  98. local_deep_research/web/templates/pages/progress.html +57 -0
  99. local_deep_research/web/templates/pages/research.html +139 -0
  100. local_deep_research/web/templates/pages/results.html +59 -0
  101. local_deep_research/web/templates/settings_dashboard.html +78 -192
  102. local_deep_research/web/utils/__init__.py +0 -0
  103. local_deep_research/web/utils/formatters.py +76 -0
  104. local_deep_research/web_search_engines/engines/full_search.py +18 -16
  105. local_deep_research/web_search_engines/engines/meta_search_engine.py +182 -131
  106. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +224 -139
  107. local_deep_research/web_search_engines/engines/search_engine_brave.py +88 -71
  108. local_deep_research/web_search_engines/engines/search_engine_ddg.py +48 -39
  109. local_deep_research/web_search_engines/engines/search_engine_github.py +415 -204
  110. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +123 -90
  111. local_deep_research/web_search_engines/engines/search_engine_guardian.py +210 -157
  112. local_deep_research/web_search_engines/engines/search_engine_local.py +532 -369
  113. local_deep_research/web_search_engines/engines/search_engine_local_all.py +42 -36
  114. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +358 -266
  115. local_deep_research/web_search_engines/engines/search_engine_searxng.py +212 -160
  116. local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +213 -170
  117. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +84 -68
  118. local_deep_research/web_search_engines/engines/search_engine_wayback.py +186 -154
  119. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +115 -77
  120. local_deep_research/web_search_engines/search_engine_base.py +174 -99
  121. local_deep_research/web_search_engines/search_engine_factory.py +192 -102
  122. local_deep_research/web_search_engines/search_engines_config.py +22 -15
  123. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/METADATA +177 -97
  124. local_deep_research-0.2.2.dist-info/RECORD +135 -0
  125. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/WHEEL +1 -2
  126. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/entry_points.txt +3 -0
  127. local_deep_research/defaults/llm_config.py +0 -338
  128. local_deep_research/utilties/search_utilities.py +0 -114
  129. local_deep_research/web/static/js/app.js +0 -3763
  130. local_deep_research/web/templates/api_keys_config.html +0 -82
  131. local_deep_research/web/templates/collections_config.html +0 -90
  132. local_deep_research/web/templates/index.html +0 -348
  133. local_deep_research/web/templates/llm_config.html +0 -120
  134. local_deep_research/web/templates/main_config.html +0 -89
  135. local_deep_research/web/templates/search_engines_config.html +0 -154
  136. local_deep_research/web/templates/settings.html +0 -519
  137. local_deep_research-0.1.26.dist-info/RECORD +0 -61
  138. local_deep_research-0.1.26.dist-info/top_level.txt +0 -1
  139. /local_deep_research/{utilties → config}/__init__.py +0 -0
  140. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,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)