local-deep-research 0.4.4__py3-none-any.whl → 0.5.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 (220) hide show
  1. local_deep_research/__init__.py +7 -0
  2. local_deep_research/__version__.py +1 -1
  3. local_deep_research/advanced_search_system/answer_decoding/__init__.py +5 -0
  4. local_deep_research/advanced_search_system/answer_decoding/browsecomp_answer_decoder.py +421 -0
  5. local_deep_research/advanced_search_system/candidate_exploration/README.md +219 -0
  6. local_deep_research/advanced_search_system/candidate_exploration/__init__.py +25 -0
  7. local_deep_research/advanced_search_system/candidate_exploration/adaptive_explorer.py +329 -0
  8. local_deep_research/advanced_search_system/candidate_exploration/base_explorer.py +341 -0
  9. local_deep_research/advanced_search_system/candidate_exploration/constraint_guided_explorer.py +436 -0
  10. local_deep_research/advanced_search_system/candidate_exploration/diversity_explorer.py +457 -0
  11. local_deep_research/advanced_search_system/candidate_exploration/parallel_explorer.py +250 -0
  12. local_deep_research/advanced_search_system/candidate_exploration/progressive_explorer.py +255 -0
  13. local_deep_research/advanced_search_system/candidates/__init__.py +5 -0
  14. local_deep_research/advanced_search_system/candidates/base_candidate.py +59 -0
  15. local_deep_research/advanced_search_system/constraint_checking/README.md +150 -0
  16. local_deep_research/advanced_search_system/constraint_checking/__init__.py +35 -0
  17. local_deep_research/advanced_search_system/constraint_checking/base_constraint_checker.py +122 -0
  18. local_deep_research/advanced_search_system/constraint_checking/constraint_checker.py +223 -0
  19. local_deep_research/advanced_search_system/constraint_checking/constraint_satisfaction_tracker.py +387 -0
  20. local_deep_research/advanced_search_system/constraint_checking/dual_confidence_checker.py +424 -0
  21. local_deep_research/advanced_search_system/constraint_checking/evidence_analyzer.py +174 -0
  22. local_deep_research/advanced_search_system/constraint_checking/intelligent_constraint_relaxer.py +503 -0
  23. local_deep_research/advanced_search_system/constraint_checking/rejection_engine.py +143 -0
  24. local_deep_research/advanced_search_system/constraint_checking/strict_checker.py +259 -0
  25. local_deep_research/advanced_search_system/constraint_checking/threshold_checker.py +213 -0
  26. local_deep_research/advanced_search_system/constraints/__init__.py +6 -0
  27. local_deep_research/advanced_search_system/constraints/base_constraint.py +58 -0
  28. local_deep_research/advanced_search_system/constraints/constraint_analyzer.py +143 -0
  29. local_deep_research/advanced_search_system/evidence/__init__.py +12 -0
  30. local_deep_research/advanced_search_system/evidence/base_evidence.py +57 -0
  31. local_deep_research/advanced_search_system/evidence/evaluator.py +159 -0
  32. local_deep_research/advanced_search_system/evidence/requirements.py +122 -0
  33. local_deep_research/advanced_search_system/filters/base_filter.py +3 -1
  34. local_deep_research/advanced_search_system/filters/cross_engine_filter.py +8 -2
  35. local_deep_research/advanced_search_system/filters/journal_reputation_filter.py +43 -29
  36. local_deep_research/advanced_search_system/findings/repository.py +54 -17
  37. local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +3 -1
  38. local_deep_research/advanced_search_system/query_generation/adaptive_query_generator.py +405 -0
  39. local_deep_research/advanced_search_system/questions/__init__.py +16 -0
  40. local_deep_research/advanced_search_system/questions/atomic_fact_question.py +171 -0
  41. local_deep_research/advanced_search_system/questions/browsecomp_question.py +287 -0
  42. local_deep_research/advanced_search_system/questions/decomposition_question.py +13 -4
  43. local_deep_research/advanced_search_system/questions/entity_aware_question.py +184 -0
  44. local_deep_research/advanced_search_system/questions/standard_question.py +9 -3
  45. local_deep_research/advanced_search_system/search_optimization/cross_constraint_manager.py +624 -0
  46. local_deep_research/advanced_search_system/source_management/diversity_manager.py +613 -0
  47. local_deep_research/advanced_search_system/strategies/__init__.py +42 -0
  48. local_deep_research/advanced_search_system/strategies/adaptive_decomposition_strategy.py +564 -0
  49. local_deep_research/advanced_search_system/strategies/base_strategy.py +4 -4
  50. local_deep_research/advanced_search_system/strategies/browsecomp_entity_strategy.py +1031 -0
  51. local_deep_research/advanced_search_system/strategies/browsecomp_optimized_strategy.py +778 -0
  52. local_deep_research/advanced_search_system/strategies/concurrent_dual_confidence_strategy.py +446 -0
  53. local_deep_research/advanced_search_system/strategies/constrained_search_strategy.py +1348 -0
  54. local_deep_research/advanced_search_system/strategies/constraint_parallel_strategy.py +522 -0
  55. local_deep_research/advanced_search_system/strategies/direct_search_strategy.py +217 -0
  56. local_deep_research/advanced_search_system/strategies/dual_confidence_strategy.py +320 -0
  57. local_deep_research/advanced_search_system/strategies/dual_confidence_with_rejection.py +219 -0
  58. local_deep_research/advanced_search_system/strategies/early_stop_constrained_strategy.py +369 -0
  59. local_deep_research/advanced_search_system/strategies/entity_aware_source_strategy.py +140 -0
  60. local_deep_research/advanced_search_system/strategies/evidence_based_strategy.py +1248 -0
  61. local_deep_research/advanced_search_system/strategies/evidence_based_strategy_v2.py +1337 -0
  62. local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py +537 -0
  63. local_deep_research/advanced_search_system/strategies/improved_evidence_based_strategy.py +782 -0
  64. local_deep_research/advanced_search_system/strategies/iterative_reasoning_strategy.py +760 -0
  65. local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +55 -21
  66. local_deep_research/advanced_search_system/strategies/llm_driven_modular_strategy.py +865 -0
  67. local_deep_research/advanced_search_system/strategies/modular_strategy.py +1142 -0
  68. local_deep_research/advanced_search_system/strategies/parallel_constrained_strategy.py +506 -0
  69. local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +34 -16
  70. local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +29 -9
  71. local_deep_research/advanced_search_system/strategies/recursive_decomposition_strategy.py +492 -0
  72. local_deep_research/advanced_search_system/strategies/smart_decomposition_strategy.py +284 -0
  73. local_deep_research/advanced_search_system/strategies/smart_query_strategy.py +515 -0
  74. local_deep_research/advanced_search_system/strategies/source_based_strategy.py +48 -24
  75. local_deep_research/advanced_search_system/strategies/standard_strategy.py +34 -14
  76. local_deep_research/advanced_search_system/tools/base_tool.py +7 -2
  77. local_deep_research/api/benchmark_functions.py +6 -2
  78. local_deep_research/api/research_functions.py +10 -4
  79. local_deep_research/benchmarks/__init__.py +9 -7
  80. local_deep_research/benchmarks/benchmark_functions.py +6 -2
  81. local_deep_research/benchmarks/cli/benchmark_commands.py +27 -10
  82. local_deep_research/benchmarks/cli.py +38 -13
  83. local_deep_research/benchmarks/comparison/__init__.py +4 -2
  84. local_deep_research/benchmarks/comparison/evaluator.py +316 -239
  85. local_deep_research/benchmarks/datasets/__init__.py +1 -1
  86. local_deep_research/benchmarks/datasets/base.py +91 -72
  87. local_deep_research/benchmarks/datasets/browsecomp.py +54 -33
  88. local_deep_research/benchmarks/datasets/custom_dataset_template.py +19 -19
  89. local_deep_research/benchmarks/datasets/simpleqa.py +14 -14
  90. local_deep_research/benchmarks/datasets/utils.py +48 -29
  91. local_deep_research/benchmarks/datasets.py +4 -11
  92. local_deep_research/benchmarks/efficiency/__init__.py +8 -4
  93. local_deep_research/benchmarks/efficiency/resource_monitor.py +223 -171
  94. local_deep_research/benchmarks/efficiency/speed_profiler.py +62 -48
  95. local_deep_research/benchmarks/evaluators/browsecomp.py +3 -1
  96. local_deep_research/benchmarks/evaluators/composite.py +6 -2
  97. local_deep_research/benchmarks/evaluators/simpleqa.py +36 -13
  98. local_deep_research/benchmarks/graders.py +32 -10
  99. local_deep_research/benchmarks/metrics/README.md +1 -1
  100. local_deep_research/benchmarks/metrics/calculation.py +25 -10
  101. local_deep_research/benchmarks/metrics/reporting.py +7 -3
  102. local_deep_research/benchmarks/metrics/visualization.py +42 -23
  103. local_deep_research/benchmarks/metrics.py +1 -1
  104. local_deep_research/benchmarks/optimization/__init__.py +3 -1
  105. local_deep_research/benchmarks/optimization/api.py +7 -1
  106. local_deep_research/benchmarks/optimization/optuna_optimizer.py +75 -26
  107. local_deep_research/benchmarks/runners.py +48 -15
  108. local_deep_research/citation_handler.py +65 -92
  109. local_deep_research/citation_handlers/__init__.py +15 -0
  110. local_deep_research/citation_handlers/base_citation_handler.py +70 -0
  111. local_deep_research/citation_handlers/forced_answer_citation_handler.py +179 -0
  112. local_deep_research/citation_handlers/precision_extraction_handler.py +550 -0
  113. local_deep_research/citation_handlers/standard_citation_handler.py +80 -0
  114. local_deep_research/config/llm_config.py +271 -169
  115. local_deep_research/config/search_config.py +14 -5
  116. local_deep_research/defaults/__init__.py +0 -1
  117. local_deep_research/metrics/__init__.py +13 -0
  118. local_deep_research/metrics/database.py +58 -0
  119. local_deep_research/metrics/db_models.py +115 -0
  120. local_deep_research/metrics/migrate_add_provider_to_token_usage.py +148 -0
  121. local_deep_research/metrics/migrate_call_stack_tracking.py +105 -0
  122. local_deep_research/metrics/migrate_enhanced_tracking.py +75 -0
  123. local_deep_research/metrics/migrate_research_ratings.py +31 -0
  124. local_deep_research/metrics/models.py +61 -0
  125. local_deep_research/metrics/pricing/__init__.py +12 -0
  126. local_deep_research/metrics/pricing/cost_calculator.py +237 -0
  127. local_deep_research/metrics/pricing/pricing_cache.py +143 -0
  128. local_deep_research/metrics/pricing/pricing_fetcher.py +240 -0
  129. local_deep_research/metrics/query_utils.py +51 -0
  130. local_deep_research/metrics/search_tracker.py +380 -0
  131. local_deep_research/metrics/token_counter.py +1078 -0
  132. local_deep_research/migrate_db.py +3 -1
  133. local_deep_research/report_generator.py +22 -8
  134. local_deep_research/search_system.py +390 -9
  135. local_deep_research/test_migration.py +15 -5
  136. local_deep_research/utilities/db_utils.py +7 -4
  137. local_deep_research/utilities/es_utils.py +115 -104
  138. local_deep_research/utilities/llm_utils.py +15 -5
  139. local_deep_research/utilities/log_utils.py +151 -0
  140. local_deep_research/utilities/search_cache.py +387 -0
  141. local_deep_research/utilities/search_utilities.py +14 -6
  142. local_deep_research/utilities/threading_utils.py +92 -0
  143. local_deep_research/utilities/url_utils.py +6 -0
  144. local_deep_research/web/api.py +347 -0
  145. local_deep_research/web/app.py +13 -17
  146. local_deep_research/web/app_factory.py +71 -66
  147. local_deep_research/web/database/migrate_to_ldr_db.py +12 -4
  148. local_deep_research/web/database/migrations.py +20 -3
  149. local_deep_research/web/database/models.py +74 -25
  150. local_deep_research/web/database/schema_upgrade.py +49 -29
  151. local_deep_research/web/models/database.py +63 -83
  152. local_deep_research/web/routes/api_routes.py +56 -22
  153. local_deep_research/web/routes/benchmark_routes.py +4 -1
  154. local_deep_research/web/routes/globals.py +22 -0
  155. local_deep_research/web/routes/history_routes.py +71 -46
  156. local_deep_research/web/routes/metrics_routes.py +1155 -0
  157. local_deep_research/web/routes/research_routes.py +192 -54
  158. local_deep_research/web/routes/settings_routes.py +156 -55
  159. local_deep_research/web/services/research_service.py +412 -251
  160. local_deep_research/web/services/resource_service.py +36 -11
  161. local_deep_research/web/services/settings_manager.py +55 -17
  162. local_deep_research/web/services/settings_service.py +12 -4
  163. local_deep_research/web/services/socket_service.py +295 -188
  164. local_deep_research/web/static/css/custom_dropdown.css +180 -0
  165. local_deep_research/web/static/css/styles.css +39 -1
  166. local_deep_research/web/static/js/components/detail.js +633 -267
  167. local_deep_research/web/static/js/components/details.js +751 -0
  168. local_deep_research/web/static/js/components/fallback/formatting.js +11 -11
  169. local_deep_research/web/static/js/components/fallback/ui.js +23 -23
  170. local_deep_research/web/static/js/components/history.js +76 -76
  171. local_deep_research/web/static/js/components/logpanel.js +61 -13
  172. local_deep_research/web/static/js/components/progress.js +13 -2
  173. local_deep_research/web/static/js/components/research.js +99 -12
  174. local_deep_research/web/static/js/components/results.js +239 -106
  175. local_deep_research/web/static/js/main.js +40 -40
  176. local_deep_research/web/static/js/services/audio.js +1 -1
  177. local_deep_research/web/static/js/services/formatting.js +11 -11
  178. local_deep_research/web/static/js/services/keyboard.js +157 -0
  179. local_deep_research/web/static/js/services/pdf.js +80 -80
  180. local_deep_research/web/static/sounds/README.md +1 -1
  181. local_deep_research/web/templates/base.html +1 -0
  182. local_deep_research/web/templates/components/log_panel.html +7 -1
  183. local_deep_research/web/templates/components/mobile_nav.html +1 -1
  184. local_deep_research/web/templates/components/sidebar.html +3 -0
  185. local_deep_research/web/templates/pages/cost_analytics.html +1245 -0
  186. local_deep_research/web/templates/pages/details.html +325 -24
  187. local_deep_research/web/templates/pages/history.html +1 -1
  188. local_deep_research/web/templates/pages/metrics.html +1929 -0
  189. local_deep_research/web/templates/pages/progress.html +2 -2
  190. local_deep_research/web/templates/pages/research.html +53 -17
  191. local_deep_research/web/templates/pages/results.html +12 -1
  192. local_deep_research/web/templates/pages/star_reviews.html +803 -0
  193. local_deep_research/web/utils/formatters.py +9 -3
  194. local_deep_research/web_search_engines/default_search_engines.py +5 -3
  195. local_deep_research/web_search_engines/engines/full_search.py +8 -2
  196. local_deep_research/web_search_engines/engines/meta_search_engine.py +59 -20
  197. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +19 -6
  198. local_deep_research/web_search_engines/engines/search_engine_brave.py +6 -2
  199. local_deep_research/web_search_engines/engines/search_engine_ddg.py +3 -1
  200. local_deep_research/web_search_engines/engines/search_engine_elasticsearch.py +81 -58
  201. local_deep_research/web_search_engines/engines/search_engine_github.py +46 -15
  202. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +16 -6
  203. local_deep_research/web_search_engines/engines/search_engine_guardian.py +39 -15
  204. local_deep_research/web_search_engines/engines/search_engine_local.py +58 -25
  205. local_deep_research/web_search_engines/engines/search_engine_local_all.py +15 -5
  206. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +63 -21
  207. local_deep_research/web_search_engines/engines/search_engine_searxng.py +37 -11
  208. local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +27 -9
  209. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +12 -4
  210. local_deep_research/web_search_engines/engines/search_engine_wayback.py +31 -10
  211. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +12 -3
  212. local_deep_research/web_search_engines/search_engine_base.py +83 -35
  213. local_deep_research/web_search_engines/search_engine_factory.py +25 -8
  214. local_deep_research/web_search_engines/search_engines_config.py +9 -3
  215. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/METADATA +7 -1
  216. local_deep_research-0.5.2.dist-info/RECORD +265 -0
  217. local_deep_research-0.4.4.dist-info/RECORD +0 -177
  218. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/WHEEL +0 -0
  219. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/entry_points.txt +0 -0
  220. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -49,6 +49,12 @@ def normalize_url(raw_url: str) -> str:
49
49
  # At this point, we should have hostname:port or just hostname
50
50
  # Determine if this is localhost or an external host
51
51
  hostname = raw_url.split(":")[0].split("/")[0]
52
+
53
+ # Handle IPv6 addresses in brackets
54
+ if hostname.startswith("[") and "]" in raw_url:
55
+ # Extract the IPv6 address including brackets
56
+ hostname = raw_url.split("]")[0] + "]"
57
+
52
58
  is_localhost = hostname in ("localhost", "127.0.0.1", "[::1]", "0.0.0.0")
53
59
 
54
60
  # Use http for localhost, https for external hosts
@@ -0,0 +1,347 @@
1
+ """
2
+ REST API for Local Deep Research.
3
+ Provides HTTP access to programmatic search and research capabilities.
4
+ """
5
+
6
+ import logging
7
+ import time
8
+ from functools import wraps
9
+
10
+ from flask import Blueprint, jsonify, request
11
+
12
+ from ..api.research_functions import analyze_documents
13
+ from .services.settings_service import get_setting
14
+
15
+ # Create a blueprint for the API
16
+ api_blueprint = Blueprint("api_v1", __name__, url_prefix="/api/v1")
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Rate limiting data store: {ip_address: [timestamp1, timestamp2, ...]}
20
+ rate_limit_data = {}
21
+
22
+
23
+ def api_access_control(f):
24
+ """
25
+ Decorator to enforce API access control:
26
+ - Check if API is enabled
27
+ - Enforce rate limiting
28
+ """
29
+
30
+ @wraps(f)
31
+ def decorated_function(*args, **kwargs):
32
+ # Check if API is enabled
33
+ api_enabled = get_setting("app.enable_api", True) # Default to enabled
34
+ if not api_enabled:
35
+ return jsonify({"error": "API access is disabled"}), 403
36
+
37
+ # Implement rate limiting
38
+ rate_limit = get_setting(
39
+ "app.api_rate_limit", 60
40
+ ) # Default 60 requests per minute
41
+ if rate_limit:
42
+ client_ip = request.remote_addr
43
+ current_time = time.time()
44
+
45
+ # Initialize or clean up old requests for this IP
46
+ if client_ip not in rate_limit_data:
47
+ rate_limit_data[client_ip] = []
48
+
49
+ # Remove timestamps older than 1 minute
50
+ rate_limit_data[client_ip] = [
51
+ ts
52
+ for ts in rate_limit_data[client_ip]
53
+ if current_time - ts < 60
54
+ ]
55
+
56
+ # Check if rate limit is exceeded
57
+ if len(rate_limit_data[client_ip]) >= rate_limit:
58
+ return (
59
+ jsonify(
60
+ {
61
+ "error": f"Rate limit exceeded. Maximum {rate_limit} requests per minute allowed."
62
+ }
63
+ ),
64
+ 429,
65
+ )
66
+
67
+ # Add current timestamp to the list
68
+ rate_limit_data[client_ip].append(current_time)
69
+
70
+ return f(*args, **kwargs)
71
+
72
+ return decorated_function
73
+
74
+
75
+ @api_blueprint.route("/", methods=["GET"])
76
+ @api_access_control
77
+ def api_documentation():
78
+ """
79
+ Provide documentation on the available API endpoints.
80
+ """
81
+ api_docs = {
82
+ "api_version": "v1",
83
+ "description": "REST API for Local Deep Research",
84
+ "endpoints": [
85
+ {
86
+ "path": "/api/v1/quick_summary",
87
+ "method": "POST",
88
+ "description": "Generate a quick research summary",
89
+ "parameters": {
90
+ "query": "Research query (required)",
91
+ "search_tool": "Search engine to use (optional)",
92
+ "iterations": "Number of search iterations (optional)",
93
+ "temperature": "LLM temperature (optional)",
94
+ },
95
+ },
96
+ {
97
+ "path": "/api/v1/generate_report",
98
+ "method": "POST",
99
+ "description": "Generate a comprehensive research report",
100
+ "parameters": {
101
+ "query": "Research query (required)",
102
+ "output_file": "Path to save report (optional)",
103
+ "searches_per_section": "Searches per report section (optional)",
104
+ "model_name": "LLM model to use (optional)",
105
+ "temperature": "LLM temperature (optional)",
106
+ },
107
+ },
108
+ {
109
+ "path": "/api/v1/analyze_documents",
110
+ "method": "POST",
111
+ "description": "Search and analyze documents in a local collection",
112
+ "parameters": {
113
+ "query": "Search query (required)",
114
+ "collection_name": "Local collection name (required)",
115
+ "max_results": "Maximum results to return (optional)",
116
+ "temperature": "LLM temperature (optional)",
117
+ "force_reindex": "Force collection reindexing (optional)",
118
+ },
119
+ },
120
+ ],
121
+ }
122
+
123
+ return jsonify(api_docs)
124
+
125
+
126
+ @api_blueprint.route("/health", methods=["GET"])
127
+ def health_check():
128
+ """Simple health check endpoint."""
129
+ return jsonify(
130
+ {"status": "ok", "message": "API is running", "timestamp": time.time()}
131
+ )
132
+
133
+
134
+ @api_blueprint.route("/quick_summary_test", methods=["POST"])
135
+ @api_access_control
136
+ def api_quick_summary_test():
137
+ """Test endpoint using programmatic access with minimal parameters for fast testing."""
138
+ data = request.json
139
+ if not data or "query" not in data:
140
+ return jsonify({"error": "Query parameter is required"}), 400
141
+
142
+ query = data.get("query")
143
+
144
+ try:
145
+ # Import here to avoid circular imports
146
+ from ..api.research_functions import quick_summary
147
+
148
+ logger.info(f"Processing quick_summary_test request: query='{query}'")
149
+
150
+ # Use minimal parameters for faster testing
151
+ result = quick_summary(
152
+ query=query,
153
+ search_tool="wikipedia", # Use fast Wikipedia search for testing
154
+ iterations=1, # Single iteration for speed
155
+ temperature=0.7,
156
+ )
157
+
158
+ return jsonify(result)
159
+ except Exception as e:
160
+ logger.error(
161
+ f"Error in quick_summary_test API: {str(e)}", exc_info=True
162
+ )
163
+ return (
164
+ jsonify(
165
+ {
166
+ "error": "An internal error has occurred. Please try again later."
167
+ }
168
+ ),
169
+ 500,
170
+ )
171
+
172
+
173
+ @api_blueprint.route("/quick_summary", methods=["POST"])
174
+ @api_access_control
175
+ def api_quick_summary():
176
+ """
177
+ Generate a quick research summary via REST API.
178
+
179
+ POST /api/v1/quick_summary
180
+ {
181
+ "query": "Advances in fusion energy research",
182
+ "search_tool": "auto", # Optional: search engine to use
183
+ "iterations": 2, # Optional: number of search iterations
184
+ "temperature": 0.7 # Optional: LLM temperature
185
+ }
186
+ """
187
+ data = request.json
188
+ if not data or "query" not in data:
189
+ return jsonify({"error": "Query parameter is required"}), 400
190
+
191
+ # Extract query and optional parameters
192
+ query = data.get("query")
193
+ params = {k: v for k, v in data.items() if k != "query"}
194
+
195
+ try:
196
+ # Import here to avoid circular imports
197
+ from ..api.research_functions import quick_summary
198
+
199
+ logger.info(f"Processing quick_summary request: query='{query}'")
200
+
201
+ # Set reasonable defaults for API use
202
+ params.setdefault("temperature", 0.7)
203
+ params.setdefault("search_tool", "auto")
204
+ params.setdefault("iterations", 1)
205
+
206
+ # Call the actual research function
207
+ result = quick_summary(query, **params)
208
+
209
+ return jsonify(result)
210
+ except TimeoutError:
211
+ logger.error("Request timed out")
212
+ return (
213
+ jsonify(
214
+ {
215
+ "error": "Request timed out. Please try with a simpler query or fewer iterations."
216
+ }
217
+ ),
218
+ 504,
219
+ )
220
+ except Exception as e:
221
+ logger.error(f"Error in quick_summary API: {str(e)}", exc_info=True)
222
+ return (
223
+ jsonify(
224
+ {
225
+ "error": "An internal error has occurred. Please try again later."
226
+ }
227
+ ),
228
+ 500,
229
+ )
230
+
231
+
232
+ @api_blueprint.route("/generate_report", methods=["POST"])
233
+ @api_access_control
234
+ def api_generate_report():
235
+ """
236
+ Generate a comprehensive research report via REST API.
237
+
238
+ POST /api/v1/generate_report
239
+ {
240
+ "query": "Impact of climate change on agriculture",
241
+ "output_file": "/path/to/save/report.md", # Optional
242
+ "searches_per_section": 2, # Optional
243
+ "model_name": "gpt-4", # Optional
244
+ "temperature": 0.5 # Optional
245
+ }
246
+ """
247
+ data = request.json
248
+ if not data or "query" not in data:
249
+ return jsonify({"error": "Query parameter is required"}), 400
250
+
251
+ query = data.get("query")
252
+ params = {k: v for k, v in data.items() if k != "query"}
253
+
254
+ try:
255
+ # Import here to avoid circular imports
256
+ from ..api.research_functions import generate_report
257
+
258
+ # Set reasonable defaults for API use
259
+ params.setdefault("searches_per_section", 1)
260
+ params.setdefault("temperature", 0.7)
261
+
262
+ logger.info(
263
+ f"Processing generate_report request: query='{query}', params={params}"
264
+ )
265
+
266
+ result = generate_report(query, **params)
267
+
268
+ # Don't return the full content for large reports
269
+ if (
270
+ result
271
+ and "content" in result
272
+ and isinstance(result["content"], str)
273
+ and len(result["content"]) > 10000
274
+ ):
275
+ # Include a summary of the report content
276
+ content_preview = (
277
+ result["content"][:2000] + "... [Content truncated]"
278
+ )
279
+ result["content"] = content_preview
280
+ result["content_truncated"] = True
281
+
282
+ return jsonify(result)
283
+ except TimeoutError:
284
+ logger.error("Request timed out")
285
+ return (
286
+ jsonify(
287
+ {"error": "Request timed out. Please try with a simpler query."}
288
+ ),
289
+ 504,
290
+ )
291
+ except Exception as e:
292
+ logger.error(f"Error in generate_report API: {str(e)}", exc_info=True)
293
+ return (
294
+ jsonify(
295
+ {
296
+ "error": "An internal error has occurred. Please try again later."
297
+ }
298
+ ),
299
+ 500,
300
+ )
301
+
302
+
303
+ @api_blueprint.route("/analyze_documents", methods=["POST"])
304
+ @api_access_control
305
+ def api_analyze_documents():
306
+ """
307
+ Search and analyze documents in a local collection via REST API.
308
+
309
+ POST /api/v1/analyze_documents
310
+ {
311
+ "query": "neural networks in medicine",
312
+ "collection_name": "research_papers", # Required: local collection name
313
+ "max_results": 20, # Optional: max results to return
314
+ "temperature": 0.7, # Optional: LLM temperature
315
+ "force_reindex": false # Optional: force reindexing
316
+ }
317
+ """
318
+ data = request.json
319
+ if not data or "query" not in data or "collection_name" not in data:
320
+ return (
321
+ jsonify(
322
+ {
323
+ "error": "Both query and collection_name parameters are required"
324
+ }
325
+ ),
326
+ 400,
327
+ )
328
+
329
+ query = data.get("query")
330
+ collection_name = data.get("collection_name")
331
+ params = {
332
+ k: v for k, v in data.items() if k not in ["query", "collection_name"]
333
+ }
334
+
335
+ try:
336
+ result = analyze_documents(query, collection_name, **params)
337
+ return jsonify(result)
338
+ except Exception as e:
339
+ logger.error(f"Error in analyze_documents API: {str(e)}", exc_info=True)
340
+ return (
341
+ jsonify(
342
+ {
343
+ "error": "An internal error has occurred. Please try again later."
344
+ }
345
+ ),
346
+ 500,
347
+ )
@@ -5,6 +5,7 @@ from loguru import logger
5
5
 
6
6
  from ..setup_data_dir import setup_data_dir
7
7
  from ..utilities.db_utils import get_db_setting
8
+ from ..utilities.log_utils import config_logger
8
9
  from .app_factory import create_app
9
10
  from .models.database import (
10
11
  DB_PATH,
@@ -15,6 +16,9 @@ from .models.database import (
15
16
  # Ensure data directory exists
16
17
  setup_data_dir()
17
18
 
19
+ # Configure logging with milestone level
20
+ config_logger("ldr_web")
21
+
18
22
  # Run schema upgrades if database exists
19
23
  if os.path.exists(DB_PATH):
20
24
  try:
@@ -32,9 +36,9 @@ def check_migration_needed():
32
36
  """Check if database migration is needed, based on presence of legacy files and absence of new DB"""
33
37
  if not os.path.exists(DB_PATH):
34
38
  # The new database doesn't exist, check if legacy databases exist
35
- legacy_files_exist = os.path.exists(LEGACY_DEEP_RESEARCH_DB) or os.path.exists(
36
- LEGACY_RESEARCH_HISTORY_DB
37
- )
39
+ legacy_files_exist = os.path.exists(
40
+ LEGACY_DEEP_RESEARCH_DB
41
+ ) or os.path.exists(LEGACY_RESEARCH_HISTORY_DB)
38
42
 
39
43
  if legacy_files_exist:
40
44
  logger.info(
@@ -45,16 +49,15 @@ def check_migration_needed():
45
49
  return False
46
50
 
47
51
 
48
- # Create the Flask app and SocketIO instance
49
- app, socketio = create_app()
50
-
51
-
52
- @logger.catch()
52
+ @logger.catch
53
53
  def main():
54
54
  """
55
55
  Entry point for the web application when run as a command.
56
56
  This function is needed for the package's entry point to work properly.
57
57
  """
58
+ # Create the Flask app and SocketIO instance
59
+ app, socketio = create_app()
60
+
58
61
  # Check if migration is needed
59
62
  if check_migration_needed():
60
63
  logger.info(
@@ -93,15 +96,8 @@ def main():
93
96
  host = get_db_setting("web.host", "0.0.0.0")
94
97
  debug = get_db_setting("web.debug", True)
95
98
 
96
- logger.info(f"Starting web server on {host}:{port} (debug: {debug})")
97
- socketio.run(
98
- app,
99
- debug=debug,
100
- host=host,
101
- port=port,
102
- allow_unsafe_werkzeug=True,
103
- use_reloader=False,
104
- )
99
+ with app.app_context():
100
+ socketio.run(host, port, debug=debug)
105
101
 
106
102
 
107
103
  if __name__ == "__main__":
@@ -6,17 +6,15 @@ from flask import (
6
6
  Flask,
7
7
  jsonify,
8
8
  make_response,
9
- redirect,
10
9
  request,
11
10
  send_from_directory,
12
- url_for,
13
11
  )
14
- from flask_socketio import SocketIO
15
12
  from flask_wtf.csrf import CSRFProtect
16
13
  from loguru import logger
17
14
 
18
15
  from ..utilities.log_utils import InterceptHandler
19
16
  from .models.database import DB_PATH, init_db
17
+ from .services.socket_service import SocketIOService
20
18
 
21
19
 
22
20
  def create_app():
@@ -38,7 +36,9 @@ def create_app():
38
36
  TEMPLATE_DIR = (package_dir / "templates").as_posix()
39
37
 
40
38
  # Initialize Flask app with package directories
41
- app = Flask(__name__, static_folder=STATIC_DIR, template_folder=TEMPLATE_DIR)
39
+ app = Flask(
40
+ __name__, static_folder=STATIC_DIR, template_folder=TEMPLATE_DIR
41
+ )
42
42
  logger.debug(f"Using package static path: {STATIC_DIR}")
43
43
  logger.debug(f"Using package template path: {TEMPLATE_DIR}")
44
44
  except Exception:
@@ -58,6 +58,14 @@ def create_app():
58
58
  # Exempt Socket.IO from CSRF protection
59
59
  csrf.exempt("research.socket_io")
60
60
 
61
+ # Disable CSRF for API routes
62
+ @app.before_request
63
+ def disable_csrf_for_api():
64
+ if request.path.startswith("/api/v1/") or request.path.startswith(
65
+ "/research/api/"
66
+ ):
67
+ csrf.protect = lambda: None
68
+
61
69
  # Database configuration - Use unified ldr.db from the database module
62
70
  db_path = DB_PATH
63
71
  app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}"
@@ -65,26 +73,12 @@ def create_app():
65
73
  app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
66
74
  app.config["SQLALCHEMY_ECHO"] = False
67
75
 
68
- # Initialize extensions
69
- socketio = SocketIO(
70
- app,
71
- cors_allowed_origins="*",
72
- async_mode="threading",
73
- path="/research/socket.io",
74
- logger=False,
75
- engineio_logger=False,
76
- ping_timeout=20,
77
- ping_interval=5,
78
- )
79
-
80
76
  # Initialize the database
81
77
  create_database(app)
82
78
  init_db()
83
79
 
84
80
  # Register socket service
85
- from .services.socket_service import set_socketio
86
-
87
- set_socketio(socketio)
81
+ socket_service = SocketIOService(app=app)
88
82
 
89
83
  # Apply middleware
90
84
  apply_middleware(app)
@@ -95,10 +89,7 @@ def create_app():
95
89
  # Register error handlers
96
90
  register_error_handlers(app)
97
91
 
98
- # Register socket event handlers
99
- register_socket_events(socketio)
100
-
101
- return app, socketio
92
+ return app, socket_service
102
93
 
103
94
 
104
95
  def apply_middleware(app):
@@ -123,12 +114,18 @@ def apply_middleware(app):
123
114
  response.headers["X-Content-Security-Policy"] = csp
124
115
 
125
116
  # Add CORS headers for API requests
126
- if request.path.startswith("/api/"):
117
+ if request.path.startswith("/api/") or request.path.startswith(
118
+ "/research/api/"
119
+ ):
127
120
  response.headers["Access-Control-Allow-Origin"] = "*"
128
121
  response.headers["Access-Control-Allow-Methods"] = (
129
- "GET, POST, DELETE, OPTIONS"
122
+ "GET, POST, PUT, DELETE, OPTIONS"
123
+ )
124
+ response.headers["Access-Control-Allow-Headers"] = (
125
+ "Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override"
130
126
  )
131
- response.headers["Access-Control-Allow-Headers"] = "Content-Type"
127
+ response.headers["Access-Control-Allow-Credentials"] = "true"
128
+ response.headers["Access-Control-Max-Age"] = "3600"
132
129
 
133
130
  return response
134
131
 
@@ -144,28 +141,69 @@ def apply_middleware(app):
144
141
  # Return empty response to prevent further processing
145
142
  return "", 200
146
143
 
144
+ # Handle CORS preflight requests
145
+ @app.before_request
146
+ def handle_preflight():
147
+ if request.method == "OPTIONS":
148
+ if request.path.startswith("/api/") or request.path.startswith(
149
+ "/research/api/"
150
+ ):
151
+ response = app.make_default_options_response()
152
+ response.headers["Access-Control-Allow-Origin"] = "*"
153
+ response.headers["Access-Control-Allow-Methods"] = (
154
+ "GET, POST, PUT, DELETE, OPTIONS"
155
+ )
156
+ response.headers["Access-Control-Allow-Headers"] = (
157
+ "Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override"
158
+ )
159
+ response.headers["Access-Control-Allow-Credentials"] = "true"
160
+ response.headers["Access-Control-Max-Age"] = "3600"
161
+ return response
162
+
147
163
 
148
164
  def register_blueprints(app):
149
165
  """Register blueprints with the Flask app."""
150
166
 
151
167
  # Import blueprints
168
+ from .api import api_blueprint # Import the API blueprint
152
169
  from .routes.api_routes import api_bp # Import the API blueprint
153
170
  from .routes.history_routes import history_bp
171
+ from .routes.metrics_routes import metrics_bp
154
172
  from .routes.research_routes import research_bp
155
173
  from .routes.settings_routes import settings_bp
156
174
 
175
+ # Add root route
176
+ @app.route("/")
177
+ def index():
178
+ """Root route - redirect to research page"""
179
+ from flask import redirect, url_for
180
+
181
+ return redirect(url_for("research.index"))
182
+
157
183
  # Register blueprints
158
184
  app.register_blueprint(research_bp)
159
185
  app.register_blueprint(history_bp, url_prefix="/research/api")
186
+ app.register_blueprint(metrics_bp)
160
187
  app.register_blueprint(settings_bp)
161
188
  app.register_blueprint(
162
189
  api_bp, url_prefix="/research/api"
163
190
  ) # Register API blueprint with prefix
164
191
 
165
- # Add root route redirect
166
- @app.route("/")
167
- def root_index():
168
- return redirect(url_for("research.index"))
192
+ # Register API v1 blueprint
193
+ app.register_blueprint(api_blueprint) # Already has url_prefix='/api/v1'
194
+
195
+ # After registration, update CSRF exemptions
196
+ if hasattr(app, "extensions") and "csrf" in app.extensions:
197
+ csrf = app.extensions["csrf"]
198
+ # Exempt the API blueprint routes by actual endpoints
199
+ csrf.exempt("api_v1")
200
+ csrf.exempt("api")
201
+ for rule in app.url_map.iter_rules():
202
+ if rule.endpoint and (
203
+ rule.endpoint.startswith("api_v1.")
204
+ or rule.endpoint.startswith("api.")
205
+ ):
206
+ csrf.exempt(rule.endpoint)
169
207
 
170
208
  # Add favicon route
171
209
  @app.route("/favicon.ico")
@@ -192,41 +230,6 @@ def register_error_handlers(app):
192
230
  return make_response(jsonify({"error": "Server error"}), 500)
193
231
 
194
232
 
195
- def register_socket_events(socketio):
196
- """Register Socket.IO event handlers."""
197
-
198
- from .routes.research_routes import get_globals
199
- from .services.socket_service import (
200
- handle_connect,
201
- handle_default_error,
202
- handle_disconnect,
203
- handle_socket_error,
204
- handle_subscribe,
205
- )
206
-
207
- @socketio.on("connect")
208
- def on_connect():
209
- handle_connect(request)
210
-
211
- @socketio.on("disconnect")
212
- def on_disconnect():
213
- handle_disconnect(request)
214
-
215
- @socketio.on("subscribe_to_research")
216
- def on_subscribe(data):
217
- globals_dict = get_globals()
218
- active_research = globals_dict.get("active_research", {})
219
- handle_subscribe(data, request, active_research)
220
-
221
- @socketio.on_error
222
- def on_error(e):
223
- return handle_socket_error(e)
224
-
225
- @socketio.on_error_default
226
- def on_default_error(e):
227
- return handle_default_error(e)
228
-
229
-
230
233
  def create_database(app):
231
234
  """
232
235
  Create the database and tables for the application.
@@ -250,7 +253,9 @@ def create_database(app):
250
253
  Base.metadata.create_all(engine)
251
254
 
252
255
  # Configure session factory
253
- session_factory = sessionmaker(bind=engine, autocommit=False, autoflush=False)
256
+ session_factory = sessionmaker(
257
+ bind=engine, autocommit=False, autoflush=False
258
+ )
254
259
  app.db_session = scoped_session(session_factory)
255
260
 
256
261
  # Run migrations and setup predefined settings