local-deep-research 0.5.9__py3-none-any.whl → 0.6.1__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 (90) hide show
  1. local_deep_research/__version__.py +1 -1
  2. local_deep_research/advanced_search_system/candidate_exploration/progressive_explorer.py +11 -1
  3. local_deep_research/advanced_search_system/questions/browsecomp_question.py +32 -6
  4. local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py +32 -8
  5. local_deep_research/advanced_search_system/strategies/source_based_strategy.py +2 -0
  6. local_deep_research/api/__init__.py +2 -0
  7. local_deep_research/api/research_functions.py +177 -3
  8. local_deep_research/benchmarks/graders.py +150 -5
  9. local_deep_research/benchmarks/models/__init__.py +19 -0
  10. local_deep_research/benchmarks/models/benchmark_models.py +283 -0
  11. local_deep_research/benchmarks/ui/__init__.py +1 -0
  12. local_deep_research/benchmarks/web_api/__init__.py +6 -0
  13. local_deep_research/benchmarks/web_api/benchmark_routes.py +862 -0
  14. local_deep_research/benchmarks/web_api/benchmark_service.py +920 -0
  15. local_deep_research/config/llm_config.py +106 -21
  16. local_deep_research/defaults/default_settings.json +447 -2
  17. local_deep_research/error_handling/report_generator.py +10 -0
  18. local_deep_research/llm/__init__.py +19 -0
  19. local_deep_research/llm/llm_registry.py +155 -0
  20. local_deep_research/metrics/db_models.py +3 -7
  21. local_deep_research/metrics/search_tracker.py +25 -11
  22. local_deep_research/search_system.py +12 -9
  23. local_deep_research/utilities/log_utils.py +23 -10
  24. local_deep_research/utilities/thread_context.py +99 -0
  25. local_deep_research/web/app_factory.py +32 -8
  26. local_deep_research/web/database/benchmark_schema.py +230 -0
  27. local_deep_research/web/database/convert_research_id_to_string.py +161 -0
  28. local_deep_research/web/database/models.py +55 -1
  29. local_deep_research/web/database/schema_upgrade.py +397 -2
  30. local_deep_research/web/database/uuid_migration.py +265 -0
  31. local_deep_research/web/routes/api_routes.py +62 -31
  32. local_deep_research/web/routes/history_routes.py +13 -6
  33. local_deep_research/web/routes/metrics_routes.py +264 -4
  34. local_deep_research/web/routes/research_routes.py +45 -18
  35. local_deep_research/web/routes/route_registry.py +352 -0
  36. local_deep_research/web/routes/settings_routes.py +382 -22
  37. local_deep_research/web/services/research_service.py +22 -29
  38. local_deep_research/web/services/settings_manager.py +53 -0
  39. local_deep_research/web/services/settings_service.py +2 -0
  40. local_deep_research/web/static/css/styles.css +8 -0
  41. local_deep_research/web/static/js/components/detail.js +7 -14
  42. local_deep_research/web/static/js/components/details.js +8 -10
  43. local_deep_research/web/static/js/components/fallback/ui.js +4 -4
  44. local_deep_research/web/static/js/components/history.js +6 -6
  45. local_deep_research/web/static/js/components/logpanel.js +14 -11
  46. local_deep_research/web/static/js/components/progress.js +51 -46
  47. local_deep_research/web/static/js/components/research.js +250 -89
  48. local_deep_research/web/static/js/components/results.js +5 -7
  49. local_deep_research/web/static/js/components/settings.js +32 -26
  50. local_deep_research/web/static/js/components/settings_sync.js +24 -23
  51. local_deep_research/web/static/js/config/urls.js +285 -0
  52. local_deep_research/web/static/js/main.js +8 -8
  53. local_deep_research/web/static/js/research_form.js +267 -12
  54. local_deep_research/web/static/js/services/api.js +18 -18
  55. local_deep_research/web/static/js/services/keyboard.js +8 -8
  56. local_deep_research/web/static/js/services/socket.js +53 -35
  57. local_deep_research/web/static/js/services/ui.js +1 -1
  58. local_deep_research/web/templates/base.html +4 -1
  59. local_deep_research/web/templates/components/custom_dropdown.html +5 -3
  60. local_deep_research/web/templates/components/mobile_nav.html +3 -3
  61. local_deep_research/web/templates/components/sidebar.html +9 -3
  62. local_deep_research/web/templates/pages/benchmark.html +2697 -0
  63. local_deep_research/web/templates/pages/benchmark_results.html +1274 -0
  64. local_deep_research/web/templates/pages/benchmark_simple.html +453 -0
  65. local_deep_research/web/templates/pages/cost_analytics.html +1 -1
  66. local_deep_research/web/templates/pages/metrics.html +212 -39
  67. local_deep_research/web/templates/pages/research.html +8 -6
  68. local_deep_research/web/templates/pages/star_reviews.html +1 -1
  69. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +14 -1
  70. local_deep_research/web_search_engines/engines/search_engine_brave.py +15 -1
  71. local_deep_research/web_search_engines/engines/search_engine_ddg.py +20 -1
  72. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +26 -2
  73. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +15 -1
  74. local_deep_research/web_search_engines/engines/search_engine_retriever.py +192 -0
  75. local_deep_research/web_search_engines/engines/search_engine_tavily.py +307 -0
  76. local_deep_research/web_search_engines/rate_limiting/__init__.py +14 -0
  77. local_deep_research/web_search_engines/rate_limiting/__main__.py +9 -0
  78. local_deep_research/web_search_engines/rate_limiting/cli.py +209 -0
  79. local_deep_research/web_search_engines/rate_limiting/exceptions.py +21 -0
  80. local_deep_research/web_search_engines/rate_limiting/tracker.py +506 -0
  81. local_deep_research/web_search_engines/retriever_registry.py +108 -0
  82. local_deep_research/web_search_engines/search_engine_base.py +161 -43
  83. local_deep_research/web_search_engines/search_engine_factory.py +14 -0
  84. local_deep_research/web_search_engines/search_engines_config.py +20 -0
  85. local_deep_research-0.6.1.dist-info/METADATA +374 -0
  86. {local_deep_research-0.5.9.dist-info → local_deep_research-0.6.1.dist-info}/RECORD +89 -64
  87. local_deep_research-0.5.9.dist-info/METADATA +0 -420
  88. {local_deep_research-0.5.9.dist-info → local_deep_research-0.6.1.dist-info}/WHEEL +0 -0
  89. {local_deep_research-0.5.9.dist-info → local_deep_research-0.6.1.dist-info}/entry_points.txt +0 -0
  90. {local_deep_research-0.5.9.dist-info → local_deep_research-0.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -5,6 +5,7 @@ import requests
5
5
  from flask import Blueprint, current_app, jsonify, request
6
6
 
7
7
  from ...utilities.url_utils import normalize_url
8
+ from ...utilities.db_utils import get_db_session
8
9
  from ..models.database import get_db_connection
9
10
  from ..routes.research_routes import active_research, termination_flags
10
11
  from ..services.research_service import (
@@ -17,12 +18,50 @@ from ..services.resource_service import (
17
18
  delete_resource,
18
19
  get_resources_for_research,
19
20
  )
21
+ from ..services.settings_manager import SettingsManager
20
22
 
21
23
  # Create blueprint
22
24
  api_bp = Blueprint("api", __name__)
23
25
  logger = logging.getLogger(__name__)
24
26
 
25
27
 
28
+ @api_bp.route("/settings/current-config", methods=["GET"])
29
+ def get_current_config():
30
+ """Get the current configuration from database settings."""
31
+ try:
32
+ session = get_db_session()
33
+ settings_manager = SettingsManager(db_session=session)
34
+
35
+ config = {
36
+ "provider": settings_manager.get_setting(
37
+ "llm.provider", "Not configured"
38
+ ),
39
+ "model": settings_manager.get_setting(
40
+ "llm.model", "Not configured"
41
+ ),
42
+ "search_tool": settings_manager.get_setting(
43
+ "search.tool", "searxng"
44
+ ),
45
+ "iterations": settings_manager.get_setting("search.iterations", 8),
46
+ "questions_per_iteration": settings_manager.get_setting(
47
+ "search.questions_per_iteration", 5
48
+ ),
49
+ "search_strategy": settings_manager.get_setting(
50
+ "search.search_strategy", "focused_iteration"
51
+ ),
52
+ }
53
+
54
+ session.close()
55
+
56
+ return jsonify({"success": True, "config": config})
57
+
58
+ except Exception:
59
+ logger.exception("Error getting current config")
60
+ return jsonify(
61
+ {"success": False, "error": "An internal error occurred"}
62
+ ), 500
63
+
64
+
26
65
  # API Routes
27
66
  @api_bp.route("/start", methods=["POST"])
28
67
  def api_start_research():
@@ -53,7 +92,7 @@ def api_start_research():
53
92
  }
54
93
 
55
94
  cursor.execute(
56
- "INSERT INTO research_history (query, mode, status, created_at, progress_log, metadata) VALUES (?, ?, ?, ?, ?, ?)",
95
+ "INSERT INTO research_history (query, mode, status, created_at, progress_log, research_meta) VALUES (?, ?, ?, ?, ?, ?)",
57
96
  (
58
97
  query,
59
98
  mode,
@@ -100,39 +139,31 @@ def api_research_status(research_id):
100
139
  Get the status of a research process
101
140
  """
102
141
  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
142
+ from ..database.models import ResearchHistory
143
+
144
+ db_session = get_db_session()
145
+ with db_session:
146
+ research = (
147
+ db_session.query(ResearchHistory)
148
+ .filter_by(id=research_id)
149
+ .first()
150
+ )
114
151
 
115
- status, progress, completed_at, report_path, metadata_str = result
152
+ if research is None:
153
+ return jsonify({"error": "Research not found"}), 404
116
154
 
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(
124
- f"Invalid JSON in metadata for research {research_id}"
125
- )
155
+ # Get metadata
156
+ metadata = research.research_meta or {}
126
157
 
127
- return jsonify(
128
- {
129
- "status": status,
130
- "progress": progress,
131
- "completed_at": completed_at,
132
- "report_path": report_path,
133
- "metadata": metadata,
134
- }
135
- )
158
+ return jsonify(
159
+ {
160
+ "status": research.status,
161
+ "progress": research.progress,
162
+ "completed_at": research.completed_at,
163
+ "report_path": research.report_path,
164
+ "metadata": metadata,
165
+ }
166
+ )
136
167
  except Exception as e:
137
168
  logger.error(f"Error getting research status: {str(e)}")
138
169
  return jsonify({"status": "error", "message": str(e)}), 500
@@ -4,6 +4,7 @@ import traceback
4
4
  from pathlib import Path
5
5
 
6
6
  from flask import Blueprint, jsonify, make_response
7
+ from ..utils.templates import render_template_with_defaults
7
8
 
8
9
  from ..models.database import (
9
10
  get_db_connection,
@@ -17,7 +18,7 @@ from ..services.research_service import get_research_strategy
17
18
  logger = logging.getLogger(__name__)
18
19
 
19
20
  # Create a Blueprint for the history routes
20
- history_bp = Blueprint("history", __name__)
21
+ history_bp = Blueprint("history", __name__, url_prefix="/history")
21
22
 
22
23
 
23
24
  def resolve_report_path(report_path: str) -> Path:
@@ -34,9 +35,15 @@ def resolve_report_path(report_path: str) -> Path:
34
35
  return project_root / path
35
36
 
36
37
 
37
- @history_bp.route("/history", methods=["GET"])
38
+ @history_bp.route("/")
39
+ def history_page():
40
+ """Render the history page"""
41
+ return render_template_with_defaults("pages/history.html")
42
+
43
+
44
+ @history_bp.route("/api", methods=["GET"])
38
45
  def get_history():
39
- """Get the research history"""
46
+ """Get the research history JSON data"""
40
47
  try:
41
48
  conn = get_db_connection()
42
49
  conn.row_factory = lambda cursor, row: {
@@ -73,8 +80,8 @@ def get_history():
73
80
  item["duration_seconds"] = None
74
81
  if "report_path" not in item:
75
82
  item["report_path"] = None
76
- if "metadata" not in item:
77
- item["metadata"] = "{}"
83
+ if "research_meta" not in item:
84
+ item["research_meta"] = "{}"
78
85
  if "progress_log" not in item:
79
86
  item["progress_log"] = "[]"
80
87
 
@@ -287,7 +294,7 @@ def get_report(research_id):
287
294
  }
288
295
 
289
296
  # Also include any stored metadata
290
- stored_metadata = json.loads(result.get("metadata", "{}"))
297
+ stored_metadata = json.loads(result.get("research_meta", "{}"))
291
298
  if stored_metadata and isinstance(stored_metadata, dict):
292
299
  enhanced_metadata.update(stored_metadata)
293
300
 
@@ -11,7 +11,13 @@ from ...metrics.db_models import ResearchRating, TokenUsage
11
11
  from ...metrics.query_utils import get_time_filter_condition
12
12
  from ...metrics.search_tracker import get_search_tracker
13
13
  from ...utilities.db_utils import get_db_session
14
- from ..database.models import Research, ResearchStrategy
14
+ from ...web_search_engines.rate_limiting import get_tracker
15
+ from ..database.models import (
16
+ Research,
17
+ ResearchStrategy,
18
+ RateLimitAttempt,
19
+ RateLimitEstimate,
20
+ )
15
21
  from ..utils.templates import render_template_with_defaults
16
22
 
17
23
  # Create a Blueprint for metrics
@@ -296,6 +302,161 @@ def get_strategy_analytics(period="30d"):
296
302
  }
297
303
 
298
304
 
305
+ def get_rate_limiting_analytics(period="30d"):
306
+ """Get rate limiting analytics for the specified period."""
307
+ try:
308
+ session = get_db_session()
309
+
310
+ # Calculate date range
311
+ days_map = {"7d": 7, "30d": 30, "90d": 90, "365d": 365, "all": None}
312
+ days = days_map.get(period, 30)
313
+
314
+ try:
315
+ # Get current rate limit estimates
316
+ estimates = session.query(RateLimitEstimate).all()
317
+
318
+ # Get recent attempts for analytics
319
+ attempts_query = session.query(RateLimitAttempt)
320
+ if days:
321
+ cutoff_timestamp = (
322
+ datetime.now() - timedelta(days=days)
323
+ ).timestamp()
324
+ attempts_query = attempts_query.filter(
325
+ RateLimitAttempt.timestamp >= cutoff_timestamp
326
+ )
327
+
328
+ attempts = attempts_query.all()
329
+
330
+ # Calculate analytics
331
+ total_attempts = len(attempts)
332
+ successful_attempts = len([a for a in attempts if a.success])
333
+ failed_attempts = total_attempts - successful_attempts
334
+
335
+ # Engine-specific analytics
336
+ engine_stats = {}
337
+ for estimate in estimates:
338
+ engine_attempts = [
339
+ a for a in attempts if a.engine_type == estimate.engine_type
340
+ ]
341
+ engine_success = len([a for a in engine_attempts if a.success])
342
+ engine_total = len(engine_attempts)
343
+
344
+ engine_stats[estimate.engine_type] = {
345
+ "base_wait_seconds": round(estimate.base_wait_seconds, 2),
346
+ "min_wait_seconds": round(estimate.min_wait_seconds, 2),
347
+ "max_wait_seconds": round(estimate.max_wait_seconds, 2),
348
+ "success_rate": round(estimate.success_rate * 100, 1),
349
+ "total_attempts": estimate.total_attempts,
350
+ "recent_attempts": engine_total,
351
+ "recent_success_rate": round(
352
+ (engine_success / engine_total * 100)
353
+ if engine_total > 0
354
+ else 0,
355
+ 1,
356
+ ),
357
+ "last_updated": datetime.fromtimestamp(
358
+ estimate.last_updated
359
+ ).strftime("%Y-%m-%d %H:%M:%S"),
360
+ "status": "healthy"
361
+ if estimate.success_rate > 0.8
362
+ else "degraded"
363
+ if estimate.success_rate > 0.5
364
+ else "poor",
365
+ }
366
+
367
+ # Overall success rate trend
368
+ success_rate = round(
369
+ (successful_attempts / total_attempts * 100)
370
+ if total_attempts > 0
371
+ else 0,
372
+ 1,
373
+ )
374
+
375
+ # Rate limiting events (failures)
376
+ rate_limit_events = len(
377
+ [
378
+ a
379
+ for a in attempts
380
+ if not a.success and a.error_type == "RateLimitError"
381
+ ]
382
+ )
383
+
384
+ # Calculate average wait times
385
+ if attempts:
386
+ avg_wait_time = sum(a.wait_time for a in attempts) / len(
387
+ attempts
388
+ )
389
+ successful_wait_times = [
390
+ a.wait_time for a in attempts if a.success
391
+ ]
392
+ avg_successful_wait = (
393
+ sum(successful_wait_times) / len(successful_wait_times)
394
+ if successful_wait_times
395
+ else 0
396
+ )
397
+ else:
398
+ avg_wait_time = 0
399
+ avg_successful_wait = 0
400
+
401
+ return {
402
+ "rate_limiting_analytics": {
403
+ "total_attempts": total_attempts,
404
+ "successful_attempts": successful_attempts,
405
+ "failed_attempts": failed_attempts,
406
+ "success_rate": success_rate,
407
+ "rate_limit_events": rate_limit_events,
408
+ "avg_wait_time": round(avg_wait_time, 2),
409
+ "avg_successful_wait": round(avg_successful_wait, 2),
410
+ "engine_stats": engine_stats,
411
+ "total_engines_tracked": len(estimates),
412
+ "healthy_engines": len(
413
+ [
414
+ s
415
+ for s in engine_stats.values()
416
+ if s["status"] == "healthy"
417
+ ]
418
+ ),
419
+ "degraded_engines": len(
420
+ [
421
+ s
422
+ for s in engine_stats.values()
423
+ if s["status"] == "degraded"
424
+ ]
425
+ ),
426
+ "poor_engines": len(
427
+ [
428
+ s
429
+ for s in engine_stats.values()
430
+ if s["status"] == "poor"
431
+ ]
432
+ ),
433
+ }
434
+ }
435
+
436
+ finally:
437
+ session.close()
438
+
439
+ except Exception as e:
440
+ logger.exception(f"Error getting rate limiting analytics: {e}")
441
+ return {
442
+ "rate_limiting_analytics": {
443
+ "total_attempts": 0,
444
+ "successful_attempts": 0,
445
+ "failed_attempts": 0,
446
+ "success_rate": 0,
447
+ "rate_limit_events": 0,
448
+ "avg_wait_time": 0,
449
+ "avg_successful_wait": 0,
450
+ "engine_stats": {},
451
+ "total_engines_tracked": 0,
452
+ "healthy_engines": 0,
453
+ "degraded_engines": 0,
454
+ "poor_engines": 0,
455
+ "error": "An internal error occurred while processing the request.",
456
+ }
457
+ }
458
+
459
+
299
460
  @metrics_bp.route("/")
300
461
  def metrics_dashboard():
301
462
  """Render the metrics dashboard page."""
@@ -355,11 +516,15 @@ def api_metrics():
355
516
  # Get strategy analytics
356
517
  strategy_data = get_strategy_analytics(period)
357
518
 
519
+ # Get rate limiting analytics
520
+ rate_limiting_data = get_rate_limiting_analytics(period)
521
+
358
522
  # Combine metrics
359
523
  combined_metrics = {
360
524
  **token_metrics,
361
525
  **search_metrics,
362
526
  **strategy_data,
527
+ **rate_limiting_data,
363
528
  "user_satisfaction": user_satisfaction,
364
529
  }
365
530
 
@@ -384,6 +549,80 @@ def api_metrics():
384
549
  )
385
550
 
386
551
 
552
+ @metrics_bp.route("/api/rate-limiting")
553
+ def api_rate_limiting_metrics():
554
+ """Get detailed rate limiting metrics."""
555
+ try:
556
+ period = request.args.get("period", "30d")
557
+ rate_limiting_data = get_rate_limiting_analytics(period)
558
+
559
+ return jsonify(
560
+ {"status": "success", "data": rate_limiting_data, "period": period}
561
+ )
562
+ except Exception as e:
563
+ logger.exception(f"Error getting rate limiting metrics: {e}")
564
+ return jsonify(
565
+ {
566
+ "status": "error",
567
+ "message": "Failed to retrieve rate limiting metrics",
568
+ }
569
+ ), 500
570
+
571
+
572
+ @metrics_bp.route("/api/rate-limiting/current")
573
+ def api_current_rate_limits():
574
+ """Get current rate limit estimates for all engines."""
575
+ try:
576
+ tracker = get_tracker()
577
+ stats = tracker.get_stats()
578
+
579
+ current_limits = []
580
+ for stat in stats:
581
+ (
582
+ engine_type,
583
+ base_wait,
584
+ min_wait,
585
+ max_wait,
586
+ last_updated,
587
+ total_attempts,
588
+ success_rate,
589
+ ) = stat
590
+ current_limits.append(
591
+ {
592
+ "engine_type": engine_type,
593
+ "base_wait_seconds": round(base_wait, 2),
594
+ "min_wait_seconds": round(min_wait, 2),
595
+ "max_wait_seconds": round(max_wait, 2),
596
+ "success_rate": round(success_rate * 100, 1),
597
+ "total_attempts": total_attempts,
598
+ "last_updated": datetime.fromtimestamp(
599
+ last_updated
600
+ ).strftime("%Y-%m-%d %H:%M:%S"),
601
+ "status": "healthy"
602
+ if success_rate > 0.8
603
+ else "degraded"
604
+ if success_rate > 0.5
605
+ else "poor",
606
+ }
607
+ )
608
+
609
+ return jsonify(
610
+ {
611
+ "status": "success",
612
+ "current_limits": current_limits,
613
+ "timestamp": datetime.now().isoformat(),
614
+ }
615
+ )
616
+ except Exception as e:
617
+ logger.exception(f"Error getting current rate limits: {e}")
618
+ return jsonify(
619
+ {
620
+ "status": "error",
621
+ "message": "Failed to retrieve current rate limits",
622
+ }
623
+ ), 500
624
+
625
+
387
626
  @metrics_bp.route("/api/metrics/research/<int:research_id>")
388
627
  def api_research_metrics(research_id):
389
628
  """Get metrics for a specific research."""
@@ -1030,18 +1269,39 @@ def api_cost_analytics():
1030
1269
  if time_condition is not None:
1031
1270
  query = query.filter(time_condition)
1032
1271
 
1033
- usage_records = query.all()
1272
+ # First check if we have any records to avoid expensive queries
1273
+ record_count = query.count()
1034
1274
 
1035
- if not usage_records:
1275
+ if record_count == 0:
1036
1276
  return jsonify(
1037
1277
  {
1038
1278
  "status": "success",
1039
1279
  "period": period,
1040
- "total_cost": 0.0,
1280
+ "overview": {
1281
+ "total_cost": 0.0,
1282
+ "total_tokens": 0,
1283
+ "prompt_tokens": 0,
1284
+ "completion_tokens": 0,
1285
+ },
1286
+ "top_expensive_research": [],
1287
+ "research_count": 0,
1041
1288
  "message": "No token usage data found for this period",
1042
1289
  }
1043
1290
  )
1044
1291
 
1292
+ # If we have too many records, limit to recent ones to avoid timeout
1293
+ if record_count > 1000:
1294
+ logger.warning(
1295
+ f"Large dataset detected ({record_count} records), limiting to recent 1000 for performance"
1296
+ )
1297
+ usage_records = (
1298
+ query.order_by(TokenUsage.timestamp.desc())
1299
+ .limit(1000)
1300
+ .all()
1301
+ )
1302
+ else:
1303
+ usage_records = query.all()
1304
+
1045
1305
  # Convert to dict format
1046
1306
  usage_data = []
1047
1307
  for record in usage_records:
@@ -29,18 +29,12 @@ from ..database.models import ResearchHistory, ResearchLog
29
29
  from ...utilities.db_utils import get_db_session
30
30
 
31
31
  # Create a Blueprint for the research application
32
- research_bp = Blueprint("research", __name__, url_prefix="/research")
32
+ research_bp = Blueprint("research", __name__)
33
33
 
34
34
  # Output directory for research results
35
35
  OUTPUT_DIR = "research_outputs"
36
36
 
37
37
 
38
- # Route for index page - redirection
39
- @research_bp.route("/")
40
- def index():
41
- return render_template_with_defaults("pages/research.html")
42
-
43
-
44
38
  # Add the missing static file serving route
45
39
  @research_bp.route("/static/<path:path>")
46
40
  def serve_static(path):
@@ -325,7 +319,7 @@ def terminate_research(research_id):
325
319
  active_research[research_id]["log"].append(log_entry)
326
320
 
327
321
  # Add to database log
328
- logger.log("milestone", "Research ended: {}", termination_message)
322
+ logger.log("MILESTONE", f"Research ended: {termination_message}")
329
323
 
330
324
  # Update the log in the database (old way for backward compatibility)
331
325
  cursor.execute(
@@ -749,7 +743,7 @@ def get_research_status(research_id):
749
743
  conn = get_db_connection()
750
744
  cursor = conn.cursor()
751
745
  cursor.execute(
752
- "SELECT status, progress, completed_at, report_path, metadata FROM research_history WHERE id = ?",
746
+ "SELECT status, progress, completed_at, report_path, research_meta FROM research_history WHERE id = ?",
753
747
  (research_id,),
754
748
  )
755
749
  result = cursor.fetchone()
@@ -837,13 +831,46 @@ def get_research_status(research_id):
837
831
  if error_info:
838
832
  metadata["error_info"] = error_info
839
833
 
834
+ # Get the latest milestone log for this research
835
+ latest_milestone = None
836
+ try:
837
+ db_session = get_db_session()
838
+ with db_session:
839
+ milestone_log = (
840
+ db_session.query(ResearchLog)
841
+ .filter_by(research_id=research_id, level="MILESTONE")
842
+ .order_by(ResearchLog.timestamp.desc())
843
+ .first()
844
+ )
845
+ if milestone_log:
846
+ latest_milestone = {
847
+ "message": milestone_log.message,
848
+ "time": milestone_log.timestamp.isoformat()
849
+ if milestone_log.timestamp
850
+ else None,
851
+ "type": "MILESTONE",
852
+ }
853
+ logger.debug(
854
+ f"Found latest milestone for research {research_id}: {milestone_log.message}"
855
+ )
856
+ else:
857
+ logger.debug(
858
+ f"No milestone logs found for research {research_id}"
859
+ )
860
+ except Exception as e:
861
+ logger.warning(f"Error fetching latest milestone: {str(e)}")
862
+
840
863
  conn.close()
841
- return jsonify(
842
- {
843
- "status": status,
844
- "progress": progress,
845
- "completed_at": completed_at,
846
- "report_path": report_path,
847
- "metadata": metadata,
848
- }
849
- )
864
+ response_data = {
865
+ "status": status,
866
+ "progress": progress,
867
+ "completed_at": completed_at,
868
+ "report_path": report_path,
869
+ "metadata": metadata,
870
+ }
871
+
872
+ # Include latest milestone as a log_entry for frontend compatibility
873
+ if latest_milestone:
874
+ response_data["log_entry"] = latest_milestone
875
+
876
+ return jsonify(response_data)