local-deep-research 0.5.9__py3-none-any.whl → 0.6.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.
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 +1136 -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.0.dist-info/METADATA +374 -0
  86. {local_deep_research-0.5.9.dist-info → local_deep_research-0.6.0.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.0.dist-info}/WHEEL +0 -0
  89. {local_deep_research-0.5.9.dist-info → local_deep_research-0.6.0.dist-info}/entry_points.txt +0 -0
  90. {local_deep_research-0.5.9.dist-info → local_deep_research-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -682,6 +682,77 @@
682
682
  </div>
683
683
  </div>
684
684
 
685
+ <!-- Rate Limiting Analytics -->
686
+ <div class="card" style="margin-top: 2rem;">
687
+ <div class="card-header">
688
+ <h2><i class="fas fa-clock"></i> Rate Limiting Analytics</h2>
689
+ </div>
690
+ <div class="card-content">
691
+ <!-- Rate Limiting Overview Cards -->
692
+ <div class="metrics-grid" style="margin-bottom: 2rem;">
693
+ <div class="metric-card">
694
+ <div class="metric-icon">
695
+ <i class="fas fa-stopwatch"></i>
696
+ </div>
697
+ <div class="metric-label tooltip" data-tooltip="Percentage of retry attempts that succeeded after rate limiting">
698
+ Rate Limit Success Rate
699
+ <i class="fas fa-info-circle info-icon"></i>
700
+ </div>
701
+ <div class="metric-value" id="rate-limit-success-rate">0%</div>
702
+ </div>
703
+
704
+ <div class="metric-card">
705
+ <div class="metric-icon">
706
+ <i class="fas fa-exclamation-triangle"></i>
707
+ </div>
708
+ <div class="metric-label tooltip" data-tooltip="Number of times search engines were rate limited">
709
+ Rate Limit Events
710
+ <i class="fas fa-info-circle info-icon"></i>
711
+ </div>
712
+ <div class="metric-value" id="rate-limit-events">0</div>
713
+ </div>
714
+
715
+ <div class="metric-card">
716
+ <div class="metric-icon">
717
+ <i class="fas fa-hourglass-half"></i>
718
+ </div>
719
+ <div class="metric-label tooltip" data-tooltip="Average time waited before retrying after rate limits">
720
+ Avg Wait Time
721
+ <i class="fas fa-info-circle info-icon"></i>
722
+ </div>
723
+ <div class="metric-value" id="avg-wait-time">0s</div>
724
+ </div>
725
+
726
+ <div class="metric-card">
727
+ <div class="metric-icon">
728
+ <i class="fas fa-server"></i>
729
+ </div>
730
+ <div class="metric-label tooltip" data-tooltip="Number of search engines being tracked for rate limiting">
731
+ Engines Tracked
732
+ <i class="fas fa-info-circle info-icon"></i>
733
+ </div>
734
+ <div class="metric-value" id="engines-tracked">0</div>
735
+ </div>
736
+ </div>
737
+
738
+ <!-- Engine Status Grid -->
739
+ <div style="margin-bottom: 2rem;">
740
+ <h3><i class="fas fa-tachometer-alt"></i> Search Engine Status</h3>
741
+ <div id="engine-status-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; margin-top: 1rem;">
742
+ <!-- Populated dynamically -->
743
+ </div>
744
+ </div>
745
+
746
+ <!-- Rate Limiting Chart -->
747
+ <div style="margin-top: 2rem;">
748
+ <h3><i class="fas fa-chart-line"></i> Rate Limiting Activity Over Time</h3>
749
+ <div class="chart-container" style="height: 300px;">
750
+ <canvas id="rate-limiting-chart"></canvas>
751
+ </div>
752
+ </div>
753
+ </div>
754
+ </div>
755
+
685
756
  <!-- Developer Insights -->
686
757
  <div class="card" style="margin-top: 2rem;">
687
758
  <div class="card-header">
@@ -817,7 +888,6 @@
817
888
  <script>
818
889
  // Metrics Dashboard JavaScript
819
890
  (function() {
820
- console.log("=== METRICS SCRIPT STARTED ===");
821
891
 
822
892
  let metricsData = null;
823
893
  let tokenChart = null;
@@ -871,40 +941,30 @@
871
941
 
872
942
  // Load metrics data for current time period
873
943
  async function loadMetrics(period = currentPeriod) {
874
- console.log('=== STARTING LOADMETRICS ===');
875
- console.log('Period:', period, 'Mode:', currentMode);
876
944
 
877
945
  try {
878
946
  // Show loading state
879
- console.log('Setting loading state...');
880
947
  document.getElementById('loading').style.display = 'block';
881
948
  document.getElementById('metrics-content').style.display = 'none';
882
949
  document.getElementById('error').style.display = 'none';
883
950
 
884
- console.log('Making basic API call...');
885
951
  const basicResponse = await fetch(`/metrics/api/metrics?period=${period}&mode=${currentMode}`);
886
- console.log('Basic response status:', basicResponse.status);
887
952
 
888
953
  if (!basicResponse.ok) {
889
954
  throw new Error(`Basic API failed: ${basicResponse.status}`);
890
955
  }
891
956
 
892
957
  const basicData = await basicResponse.json();
893
- console.log('Basic data received:', basicData);
894
958
 
895
- console.log('Making enhanced API call...');
896
959
  const enhancedResponse = await fetch(`/metrics/api/metrics/enhanced?period=${period}&mode=${currentMode}`);
897
- console.log('Enhanced response status:', enhancedResponse.status);
898
960
 
899
961
  if (!enhancedResponse.ok) {
900
962
  throw new Error(`Enhanced API failed: ${enhancedResponse.status}`);
901
963
  }
902
964
 
903
965
  const enhancedData = await enhancedResponse.json();
904
- console.log('Enhanced data received:', enhancedData);
905
966
 
906
967
  if (basicData.status === 'success') {
907
- console.log('Basic data success, setting metricsData');
908
968
  metricsData = basicData.metrics;
909
969
 
910
970
  // Check if we have any data at all
@@ -913,15 +973,12 @@
913
973
  (metricsData.by_model && metricsData.by_model.length > 0) ||
914
974
  (metricsData.recent_researches && metricsData.recent_researches.length > 0);
915
975
 
916
- console.log('Has data check:', hasData, 'tokens:', metricsData.total_tokens, 'researches:', metricsData.total_researches);
917
976
 
918
977
  if (hasData) {
919
- console.log('Displaying metrics...');
920
978
  // We have data, display normally
921
979
  displayMetrics();
922
980
 
923
981
  if (enhancedData.status === 'success') {
924
- console.log('Displaying enhanced metrics...');
925
982
  displayEnhancedMetrics(enhancedData.metrics);
926
983
  createTimeSeriesChart(enhancedData.metrics.time_series_data);
927
984
  createSearchActivityChart(enhancedData.metrics.search_time_series);
@@ -932,40 +989,35 @@
932
989
  // Load cost analytics
933
990
  loadCostAnalytics(period);
934
991
 
992
+ // Load rate limiting analytics
993
+ loadRateLimitingAnalytics(period);
994
+
935
995
  document.getElementById('loading').style.display = 'none';
936
996
  document.getElementById('metrics-content').style.display = 'block';
937
- console.log('Metrics display completed');
938
- } else {
939
- console.log('No data, showing empty state');
997
+ } else {
940
998
  // No data yet, show empty state with helpful messages
941
999
  showEmptyState();
942
1000
 
943
1001
  // Still try to display enhanced metrics in case there's some enhanced data
944
1002
  if (enhancedData.status === 'success') {
945
- console.log('Displaying enhanced metrics in empty state...');
946
1003
  displayEnhancedMetrics(enhancedData.metrics);
947
1004
  // Re-setup tooltips for any new content
948
1005
  setTimeout(setupTooltipPositioning, 100);
949
1006
  }
950
1007
  }
951
1008
  } else {
952
- console.log('Basic data failed, showing error');
953
1009
  showError();
954
1010
  }
955
1011
  } catch (error) {
956
- console.error('Error loading metrics:', error);
957
1012
  showError();
958
1013
  }
959
1014
  }
960
1015
 
961
1016
  // Display metrics on the page
962
1017
  function displayMetrics() {
963
- console.log('displayMetrics called with metricsData:', metricsData);
964
1018
 
965
1019
  // Update summary cards
966
- console.log('Updating total tokens:', metricsData.total_tokens);
967
1020
  document.getElementById('total-tokens').textContent = formatNumber(metricsData.total_tokens);
968
- console.log('Updating total researches:', metricsData.total_researches);
969
1021
  document.getElementById('total-researches').textContent = formatNumber(metricsData.total_researches);
970
1022
 
971
1023
  // Update token breakdown details
@@ -984,15 +1036,12 @@
984
1036
  document.getElementById('success-rate').textContent = '0%';
985
1037
 
986
1038
  // Update user satisfaction rating
987
- console.log('User satisfaction data:', metricsData.user_satisfaction);
988
1039
  if (metricsData.user_satisfaction && metricsData.user_satisfaction.avg_rating !== null) {
989
1040
  const rating = metricsData.user_satisfaction.avg_rating;
990
1041
  const stars = '⭐'.repeat(Math.floor(rating));
991
1042
  const displayText = `${rating} ${stars}`;
992
- console.log('Setting user rating to:', displayText);
993
1043
  document.getElementById('avg-user-rating').textContent = displayText;
994
1044
  } else {
995
- console.log('No user satisfaction data available');
996
1045
  document.getElementById('avg-user-rating').textContent = '-';
997
1046
  }
998
1047
 
@@ -1120,7 +1169,7 @@
1120
1169
  <div class="research-tokens">${formatNumber(research.tokens)} tokens</div>
1121
1170
  `;
1122
1171
  item.onclick = () => {
1123
- window.location.href = `/research/details/${research.id}`;
1172
+ window.location.href = `/details/${research.id}`;
1124
1173
  };
1125
1174
  container.appendChild(item);
1126
1175
  });
@@ -1194,8 +1243,6 @@
1194
1243
  // Get strategy data from metricsData (added by our API)
1195
1244
  const strategyData = metricsData.strategy_analytics;
1196
1245
 
1197
- // Log strategy data for debugging
1198
- console.log('Strategy data received:', strategyData);
1199
1246
 
1200
1247
  if (strategyData && strategyData.strategy_usage && strategyData.strategy_usage.length > 0) {
1201
1248
  strategyData.strategy_usage.forEach(strategy => {
@@ -1219,7 +1266,7 @@
1219
1266
  noDataMsg.style.color = 'var(--text-secondary)';
1220
1267
  noDataMsg.style.padding = '1rem';
1221
1268
 
1222
- let message = 'No strategy data yet. <a href="/research/" style="color: var(--accent-tertiary);">Start a research</a> to track strategies!';
1269
+ let message = 'No strategy data yet. <a href="/" style="color: var(--accent-tertiary);">Start a research</a> to track strategies!';
1223
1270
 
1224
1271
  // Show different message based on what data we have
1225
1272
  if (strategyData) {
@@ -1285,7 +1332,7 @@
1285
1332
  noDataMsg.style.textAlign = 'center';
1286
1333
  noDataMsg.style.color = 'var(--text-secondary)';
1287
1334
  noDataMsg.style.padding = '1rem';
1288
- noDataMsg.innerHTML = 'No ratings yet. <a href="/research/" style="color: var(--accent-tertiary);">Start a research</a> and rate it!';
1335
+ noDataMsg.innerHTML = 'No ratings yet. <a href="/" style="color: var(--accent-tertiary);">Start a research</a> and rate it!';
1289
1336
  ratingContainer.appendChild(noDataMsg);
1290
1337
  }
1291
1338
 
@@ -1322,7 +1369,7 @@
1322
1369
  // Add click handler to navigate to research details
1323
1370
  if (item.research_id) {
1324
1371
  row.onclick = () => {
1325
- window.location.href = `/research/details/${item.research_id}`;
1372
+ window.location.href = `/details/${item.research_id}`;
1326
1373
  };
1327
1374
  row.title = `Click to view research details`;
1328
1375
  }
@@ -1651,26 +1698,20 @@
1651
1698
  // Load cost analytics data
1652
1699
  async function loadCostAnalytics(period = currentPeriod) {
1653
1700
  try {
1654
- console.log('Loading cost analytics for period:', period);
1655
-
1656
1701
  const response = await fetch(`/metrics/api/cost-analytics?period=${period}`);
1657
1702
  if (!response.ok) {
1658
- console.warn('Cost analytics API failed:', response.status);
1659
1703
  displayCostData(null);
1660
1704
  return;
1661
1705
  }
1662
1706
 
1663
1707
  const data = await response.json();
1664
- console.log('Cost analytics data received:', data);
1665
1708
 
1666
1709
  if (data.status === 'success') {
1667
1710
  displayCostData(data);
1668
1711
  } else {
1669
- console.warn('Cost analytics returned error:', data.message);
1670
1712
  displayCostData(null);
1671
1713
  }
1672
1714
  } catch (error) {
1673
- console.error('Error loading cost analytics:', error);
1674
1715
  displayCostData(null);
1675
1716
  }
1676
1717
  }
@@ -1700,7 +1741,133 @@
1700
1741
  document.getElementById('total-output-cost').textContent = formatCurrency(overview.completion_cost);
1701
1742
  document.getElementById('total-cost-breakdown').textContent = formatCurrency(overview.total_cost);
1702
1743
 
1703
- console.log('Cost data displayed successfully');
1744
+ }
1745
+
1746
+ // Load rate limiting analytics
1747
+ async function loadRateLimitingAnalytics(period = currentPeriod) {
1748
+ try {
1749
+ const response = await fetch(`/metrics/api/rate-limiting?period=${period}`);
1750
+ if (!response.ok) {
1751
+ displayRateLimitingData(null);
1752
+ return;
1753
+ }
1754
+
1755
+ const data = await response.json();
1756
+
1757
+ if (data.status === 'success') {
1758
+ displayRateLimitingData(data.data);
1759
+ } else {
1760
+ displayRateLimitingData(null);
1761
+ }
1762
+ } catch (error) {
1763
+ displayRateLimitingData(null);
1764
+ }
1765
+ }
1766
+
1767
+ // Display rate limiting data in the dashboard
1768
+ function displayRateLimitingData(rateLimitData) {
1769
+ if (!rateLimitData || !rateLimitData.rate_limiting_analytics) {
1770
+ // No rate limiting data available, show zeros
1771
+ document.getElementById('rate-limit-success-rate').textContent = '0%';
1772
+ document.getElementById('rate-limit-events').textContent = '0';
1773
+ document.getElementById('avg-wait-time').textContent = '0s';
1774
+ document.getElementById('engines-tracked').textContent = '0';
1775
+
1776
+ // Clear engine status grid
1777
+ const statusGrid = document.getElementById('engine-status-grid');
1778
+ statusGrid.innerHTML = '<div style="grid-column: 1 / -1; text-align: center; padding: 2rem; color: var(--text-secondary);"><i class="fas fa-clock fa-2x" style="margin-bottom: 1rem;"></i><p>No rate limiting data yet</p><p style="font-size: 0.875rem;">Rate limiting will appear after search engines are used</p></div>';
1779
+ return;
1780
+ }
1781
+
1782
+ const analytics = rateLimitData.rate_limiting_analytics;
1783
+
1784
+ // Update overview cards
1785
+ document.getElementById('rate-limit-success-rate').textContent = `${analytics.success_rate}%`;
1786
+ document.getElementById('rate-limit-events').textContent = formatNumber(analytics.rate_limit_events);
1787
+ document.getElementById('avg-wait-time').textContent = `${analytics.avg_wait_time}s`;
1788
+ document.getElementById('engines-tracked').textContent = formatNumber(analytics.total_engines_tracked);
1789
+
1790
+ // Update engine status grid
1791
+ const statusGrid = document.getElementById('engine-status-grid');
1792
+ statusGrid.innerHTML = '';
1793
+
1794
+ if (Object.keys(analytics.engine_stats).length === 0) {
1795
+ statusGrid.innerHTML = '<div style="grid-column: 1 / -1; text-align: center; padding: 2rem; color: var(--text-secondary);"><i class="fas fa-clock fa-2x" style="margin-bottom: 1rem;"></i><p>No engine data yet</p><p style="font-size: 0.875rem;">Search engine metrics will appear after rate limiting occurs</p></div>';
1796
+ } else {
1797
+ Object.entries(analytics.engine_stats).forEach(([engineType, stats]) => {
1798
+ const statusCard = document.createElement('div');
1799
+ statusCard.className = 'metric-card';
1800
+ statusCard.style.borderLeft = `4px solid ${getStatusColor(stats.status)}`;
1801
+
1802
+ statusCard.innerHTML = `
1803
+ <div class="metric-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
1804
+ <h4 style="margin: 0; color: var(--text-primary);">${engineType.replace('SearchEngine', '')}</h4>
1805
+ <span class="status-badge" style="
1806
+ background: ${getStatusColor(stats.status)};
1807
+ color: white;
1808
+ padding: 2px 8px;
1809
+ border-radius: 12px;
1810
+ font-size: 0.75rem;
1811
+ font-weight: 600;
1812
+ text-transform: uppercase;
1813
+ ">${stats.status}</span>
1814
+ </div>
1815
+ <div class="engine-stats-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; font-size: 0.875rem;">
1816
+ <div>
1817
+ <div style="color: var(--text-secondary);">Base Wait:</div>
1818
+ <div style="font-weight: 600;">${stats.base_wait_seconds}s</div>
1819
+ </div>
1820
+ <div>
1821
+ <div style="color: var(--text-secondary);">Success Rate:</div>
1822
+ <div style="font-weight: 600; color: ${stats.success_rate > 80 ? '#4CAF50' : stats.success_rate > 50 ? '#FF9800' : '#F44336'};">${stats.success_rate}%</div>
1823
+ </div>
1824
+ <div>
1825
+ <div style="color: var(--text-secondary);">Total Attempts:</div>
1826
+ <div style="font-weight: 600;">${formatNumber(stats.total_attempts)}</div>
1827
+ </div>
1828
+ <div>
1829
+ <div style="color: var(--text-secondary);">Last Updated:</div>
1830
+ <div style="font-weight: 600;">${formatTime(stats.last_updated)}</div>
1831
+ </div>
1832
+ </div>
1833
+ <div style="margin-top: 0.75rem; font-size: 0.75rem; color: var(--text-secondary);">
1834
+ Range: ${stats.min_wait_seconds}s - ${stats.max_wait_seconds}s
1835
+ </div>
1836
+ `;
1837
+
1838
+ statusGrid.appendChild(statusCard);
1839
+ });
1840
+ }
1841
+
1842
+ }
1843
+
1844
+ // Get status color based on engine health
1845
+ function getStatusColor(status) {
1846
+ switch (status) {
1847
+ case 'healthy': return '#4CAF50';
1848
+ case 'degraded': return '#FF9800';
1849
+ case 'poor': return '#F44336';
1850
+ default: return '#9E9E9E';
1851
+ }
1852
+ }
1853
+
1854
+ // Format time for display
1855
+ function formatTime(timeString) {
1856
+ try {
1857
+ const date = new Date(timeString);
1858
+ const now = new Date();
1859
+ const diffHours = (now - date) / (1000 * 60 * 60);
1860
+
1861
+ if (diffHours < 1) {
1862
+ return 'Just now';
1863
+ } else if (diffHours < 24) {
1864
+ return `${Math.floor(diffHours)}h ago`;
1865
+ } else {
1866
+ return `${Math.floor(diffHours / 24)}d ago`;
1867
+ }
1868
+ } catch {
1869
+ return timeString;
1870
+ }
1704
1871
  }
1705
1872
 
1706
1873
  // Show error message
@@ -1736,6 +1903,12 @@
1736
1903
  document.getElementById('total-output-cost').textContent = '-';
1737
1904
  document.getElementById('total-cost-breakdown').textContent = '-';
1738
1905
 
1906
+ // Reset rate limiting metrics
1907
+ document.getElementById('rate-limit-success-rate').textContent = '0%';
1908
+ document.getElementById('rate-limit-events').textContent = '0';
1909
+ document.getElementById('avg-wait-time').textContent = '0s';
1910
+ document.getElementById('engines-tracked').textContent = '0';
1911
+
1739
1912
  // Show helpful message in charts
1740
1913
  showEmptyChartsAndTables();
1741
1914
  }
@@ -81,7 +81,7 @@
81
81
  <!-- Model Provider Selection -->
82
82
  <div class="form-group half">
83
83
  <label for="model_provider">Model Provider</label>
84
- <select id="model_provider" name="model_provider" class="form-control">
84
+ <select id="model_provider" name="model_provider" class="form-control" data-initial-value="{{ settings.llm_provider }}">
85
85
  <!-- Will be populated dynamically -->
86
86
  <option value="">Loading providers...</option>
87
87
  </select>
@@ -91,7 +91,7 @@
91
91
  <!-- Custom Endpoint (hidden by default) -->
92
92
  <div class="form-group half" id="endpoint_container" style="display: none;">
93
93
  <label for="custom_endpoint">Custom Endpoint</label>
94
- <input type="text" id="custom_endpoint" name="custom_endpoint" class="form-control" placeholder="https://your-endpoint-url/v1">
94
+ <input type="text" id="custom_endpoint" name="custom_endpoint" class="form-control" placeholder="https://your-endpoint-url/v1" value="{{ settings.llm_openai_endpoint_url }}">
95
95
  <span class="input-help">Enter the OpenAI-compatible API endpoint URL</span>
96
96
  </div>
97
97
  </div>
@@ -106,7 +106,8 @@
106
106
  label="Language Model",
107
107
  help_text="Select or enter a custom model name",
108
108
  show_refresh=True,
109
- refresh_aria_label="Refresh model list"
109
+ refresh_aria_label="Refresh model list",
110
+ data_initial_value=settings.llm_model
110
111
  ) }}
111
112
  </div>
112
113
 
@@ -119,7 +120,8 @@
119
120
  label="Search Engine",
120
121
  help_text="Select the search engine to use for research",
121
122
  show_refresh=True,
122
- refresh_aria_label="Refresh search engine list"
123
+ refresh_aria_label="Refresh search engine list",
124
+ data_initial_value=settings.search_tool
123
125
  ) }}
124
126
  </div>
125
127
  </div>
@@ -128,14 +130,14 @@
128
130
  <!-- Search Iterations -->
129
131
  <div class="form-group half">
130
132
  <label for="iterations">Search Iterations</label>
131
- <input type="number" id="iterations" name="iterations" class="form-control" min="1" max="5" value="2">
133
+ <input type="number" id="iterations" name="iterations" class="form-control" min="1" max="5" value="{{ settings.search_iterations }}">
132
134
  <span class="input-help">Number of research cycles to perform</span>
133
135
  </div>
134
136
 
135
137
  <!-- Questions Per Iteration -->
136
138
  <div class="form-group half">
137
139
  <label for="questions_per_iteration">Questions Per Iteration</label>
138
- <input type="number" id="questions_per_iteration" name="questions_per_iteration" class="form-control" min="1" max="10" value="3">
140
+ <input type="number" id="questions_per_iteration" name="questions_per_iteration" class="form-control" min="1" max="10" value="{{ settings.search_questions_per_iteration }}">
139
141
  <span class="input-help">Follow-up questions in each cycle</span>
140
142
  </div>
141
143
  </div>
@@ -782,7 +782,7 @@ function updateRecentRatings(recentRatings) {
782
782
  item.innerHTML = `
783
783
  <div class="rating-details">
784
784
  <div class="rating-query">
785
- <a href="/research/results/${rating.research_id}" class="rating-link">
785
+ <a href="/results/${rating.research_id}" class="rating-link">
786
786
  ${rating.query}
787
787
  </a>
788
788
  </div>
@@ -9,6 +9,7 @@ from ...advanced_search_system.filters.journal_reputation_filter import (
9
9
  )
10
10
  from ...config import search_config
11
11
  from ..search_engine_base import BaseSearchEngine
12
+ from ..rate_limiting import RateLimitError
12
13
 
13
14
 
14
15
  class ArXivSearchEngine(BaseSearchEngine):
@@ -155,8 +156,20 @@ class ArXivSearchEngine(BaseSearchEngine):
155
156
 
156
157
  return previews
157
158
 
158
- except Exception:
159
+ except Exception as e:
160
+ error_msg = str(e)
159
161
  logger.exception("Error getting arXiv previews")
162
+
163
+ # Check for rate limiting patterns
164
+ if (
165
+ "429" in error_msg
166
+ or "too many requests" in error_msg.lower()
167
+ or "rate limit" in error_msg.lower()
168
+ or "service unavailable" in error_msg.lower()
169
+ or "503" in error_msg
170
+ ):
171
+ raise RateLimitError(f"arXiv rate limit hit: {error_msg}")
172
+
160
173
  return []
161
174
 
162
175
  def _get_full_content(
@@ -7,6 +7,7 @@ from langchain_core.language_models import BaseLLM
7
7
 
8
8
  from ...config import search_config
9
9
  from ..search_engine_base import BaseSearchEngine
10
+ from ..rate_limiting import RateLimitError
10
11
 
11
12
  logger = logging.getLogger(__name__)
12
13
 
@@ -174,7 +175,20 @@ class BraveSearchEngine(BaseSearchEngine):
174
175
  return previews
175
176
 
176
177
  except Exception as e:
177
- logger.error(f"Error getting Brave Search results: {e}")
178
+ error_msg = str(e)
179
+ logger.error(f"Error getting Brave Search results: {error_msg}")
180
+
181
+ # Check for rate limit patterns
182
+ if (
183
+ "429" in error_msg
184
+ or "too many requests" in error_msg.lower()
185
+ or "rate limit" in error_msg.lower()
186
+ or "quota" in error_msg.lower()
187
+ ):
188
+ raise RateLimitError(
189
+ f"Brave Search rate limit hit: {error_msg}"
190
+ )
191
+
178
192
  return []
179
193
 
180
194
  def _get_full_content(
@@ -5,6 +5,7 @@ from langchain_community.utilities import DuckDuckGoSearchAPIWrapper
5
5
  from langchain_core.language_models import BaseLLM
6
6
 
7
7
  from ..search_engine_base import BaseSearchEngine
8
+ from ..rate_limiting import RateLimitError
8
9
  from .full_search import FullSearchResults # Import the FullSearchResults class
9
10
 
10
11
  logger = logging.getLogger(__name__)
@@ -114,7 +115,25 @@ class DuckDuckGoSearchEngine(BaseSearchEngine):
114
115
  return previews
115
116
 
116
117
  except Exception as e:
117
- logger.error(f"Error getting DuckDuckGo previews: {e}")
118
+ error_msg = str(e)
119
+ logger.error(f"Error getting DuckDuckGo previews: {error_msg}")
120
+
121
+ # Check for known rate limit patterns
122
+ if "202 Ratelimit" in error_msg or "ratelimit" in error_msg.lower():
123
+ raise RateLimitError(f"DuckDuckGo rate limit hit: {error_msg}")
124
+ elif "403" in error_msg or "forbidden" in error_msg.lower():
125
+ raise RateLimitError(
126
+ f"DuckDuckGo access forbidden (possible rate limit): {error_msg}"
127
+ )
128
+ elif (
129
+ "timeout" in error_msg.lower()
130
+ or "timed out" in error_msg.lower()
131
+ ):
132
+ # Timeouts can sometimes indicate rate limiting
133
+ raise RateLimitError(
134
+ f"DuckDuckGo timeout (possible rate limit): {error_msg}"
135
+ )
136
+
118
137
  return []
119
138
 
120
139
  def _get_full_content(
@@ -8,6 +8,7 @@ from langchain_core.language_models import BaseLLM
8
8
  from requests.exceptions import RequestException
9
9
 
10
10
  from ..search_engine_base import BaseSearchEngine
11
+ from ..rate_limiting import RateLimitError
11
12
 
12
13
  # Set up logging
13
14
  logging.basicConfig(level=logging.INFO)
@@ -220,20 +221,43 @@ class GooglePSESearchEngine(BaseSearchEngine):
220
221
  return response.json()
221
222
 
222
223
  except RequestException as e:
224
+ error_msg = str(e)
223
225
  logger.warning(
224
226
  "Request error on attempt %s / %s: %s",
225
227
  attempt + 1,
226
228
  self.max_retries,
227
- str(e),
229
+ error_msg,
228
230
  )
231
+
232
+ # Check for rate limiting patterns
233
+ if (
234
+ "quota" in error_msg.lower()
235
+ or "quotaExceeded" in error_msg
236
+ or "dailyLimitExceeded" in error_msg
237
+ or "rateLimitExceeded" in error_msg
238
+ or "429" in error_msg
239
+ or "403" in error_msg
240
+ ):
241
+ raise RateLimitError(
242
+ f"Google PSE rate limit/quota exceeded: {error_msg}"
243
+ )
244
+
229
245
  last_exception = e
230
246
  except Exception as e:
247
+ error_msg = str(e)
231
248
  logger.warning(
232
249
  "Error on attempt %s / %s: %s",
233
250
  attempt + 1,
234
251
  self.max_retries,
235
- str(e),
252
+ error_msg,
236
253
  )
254
+
255
+ # Check for rate limiting patterns in general errors
256
+ if "quota" in error_msg.lower() or "limit" in error_msg.lower():
257
+ raise RateLimitError(
258
+ f"Google PSE error (possible rate limit): {error_msg}"
259
+ )
260
+
237
261
  last_exception = e
238
262
 
239
263
  attempt += 1
@@ -9,6 +9,7 @@ from langchain_core.language_models import BaseLLM
9
9
 
10
10
  from ...config import search_config
11
11
  from ..search_engine_base import BaseSearchEngine
12
+ from ..rate_limiting import RateLimitError
12
13
 
13
14
  # Setup logging
14
15
  logging.basicConfig(level=logging.INFO)
@@ -639,7 +640,20 @@ The default assumption should be that medical and scientific queries want RECENT
639
640
  return summaries
640
641
 
641
642
  except Exception as e:
642
- logger.error(f"Error getting article summaries: {e}")
643
+ error_msg = str(e)
644
+ logger.error(f"Error getting article summaries: {error_msg}")
645
+
646
+ # Check for rate limiting patterns
647
+ if (
648
+ "429" in error_msg
649
+ or "too many requests" in error_msg.lower()
650
+ or "rate limit" in error_msg.lower()
651
+ or "service unavailable" in error_msg.lower()
652
+ or "503" in error_msg
653
+ or "403" in error_msg
654
+ ):
655
+ raise RateLimitError(f"PubMed rate limit hit: {error_msg}")
656
+
643
657
  return []
644
658
 
645
659
  def _get_article_abstracts(self, id_list: List[str]) -> Dict[str, str]: