local-deep-research 0.4.4__py3-none-any.whl → 0.5.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 (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 +5 -3
  149. local_deep_research/web/database/models.py +51 -2
  150. local_deep_research/web/database/schema_upgrade.py +49 -29
  151. local_deep_research/web/models/database.py +51 -61
  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 +227 -41
  158. local_deep_research/web/routes/settings_routes.py +156 -55
  159. local_deep_research/web/services/research_service.py +310 -103
  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.0.dist-info}/METADATA +7 -1
  216. local_deep_research-0.5.0.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.0.dist-info}/WHEEL +0 -0
  219. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.0.dist-info}/entry_points.txt +0 -0
  220. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -90,7 +90,9 @@ def migrate_to_ldr_db():
90
90
  )
91
91
 
92
92
  # Migrate from deep_research.db
93
- migrated_deep_research = migrate_deep_research_db(ldr_conn, LEGACY_DEEP_RESEARCH_DB)
93
+ migrated_deep_research = migrate_deep_research_db(
94
+ ldr_conn, LEGACY_DEEP_RESEARCH_DB
95
+ )
94
96
 
95
97
  # Re-enable foreign keys and commit
96
98
  ldr_cursor.execute("PRAGMA foreign_keys = ON")
@@ -127,7 +129,9 @@ def migrate_research_history_db(ldr_conn, legacy_path):
127
129
  logger.info(f"Connected to legacy database: {legacy_path}")
128
130
 
129
131
  # Get tables from legacy database
130
- legacy_cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
132
+ legacy_cursor.execute(
133
+ "SELECT name FROM sqlite_master WHERE type='table'"
134
+ )
131
135
  tables = [row[0] for row in legacy_cursor.fetchall()]
132
136
 
133
137
  for table in tables:
@@ -181,7 +185,9 @@ def migrate_research_history_db(ldr_conn, legacy_path):
181
185
  # Verify data was inserted
182
186
  ldr_cursor.execute(f"SELECT COUNT(*) FROM {table}")
183
187
  count = ldr_cursor.fetchone()[0]
184
- logger.info(f"Migrated {count} rows to {table} (expected {len(rows)})")
188
+ logger.info(
189
+ f"Migrated {count} rows to {table} (expected {len(rows)})"
190
+ )
185
191
  else:
186
192
  logger.info(f"No data to migrate from {table}")
187
193
 
@@ -218,7 +224,9 @@ def migrate_deep_research_db(ldr_conn, legacy_path):
218
224
  logger.info(f"Connected to legacy database: {legacy_path}")
219
225
 
220
226
  # Get tables from legacy database
221
- legacy_cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
227
+ legacy_cursor.execute(
228
+ "SELECT name FROM sqlite_master WHERE type='table'"
229
+ )
222
230
  tables = [row[0] for row in legacy_cursor.fetchall()]
223
231
 
224
232
  # Migrate each table
@@ -2,7 +2,7 @@ from loguru import logger
2
2
  from sqlalchemy import inspect
3
3
 
4
4
  from ..services.settings_manager import SettingsManager
5
- from .models import Base, Journal, Setting
5
+ from .models import Base, Journal, Setting, ResearchLog
6
6
 
7
7
 
8
8
  def import_default_settings_file(db_session):
@@ -24,14 +24,12 @@ def import_default_settings_file(db_session):
24
24
  settings_mgr.load_from_defaults_file(overwrite=False, delete_extra=True)
25
25
  logger.info("Successfully imported settings from files")
26
26
 
27
-
28
27
  # Update the saved version.
29
28
  settings_mgr.update_db_version()
30
29
  except Exception as e:
31
30
  logger.error("Error importing settings from files: %s", e)
32
31
 
33
32
 
34
-
35
33
  def run_migrations(engine, db_session=None):
36
34
  """
37
35
  Run any necessary database migrations
@@ -50,6 +48,10 @@ def run_migrations(engine, db_session=None):
50
48
  logger.info("Creating journals table.")
51
49
  Base.metadata.create_all(engine, tables=[Journal.__table__])
52
50
 
51
+ if not inspector.has_table(ResearchLog.__tablename__):
52
+ logger.info("Creating research logs table.")
53
+ Base.metadata.create_all(engine, tables=[ResearchLog.__table__])
54
+
53
55
  # Import existing settings from files
54
56
  if db_session:
55
57
  import_default_settings_file(db_session)
@@ -42,7 +42,9 @@ class Research(Base):
42
42
  status = Column(
43
43
  Enum(ResearchStatus), default=ResearchStatus.PENDING, nullable=False
44
44
  )
45
- mode = Column(Enum(ResearchMode), default=ResearchMode.QUICK, nullable=False)
45
+ mode = Column(
46
+ Enum(ResearchMode), default=ResearchMode.QUICK, nullable=False
47
+ )
46
48
  created_at = Column(DateTime, server_default=func.now(), nullable=False)
47
49
  updated_at = Column(
48
50
  DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
@@ -61,6 +63,31 @@ class Research(Base):
61
63
  )
62
64
 
63
65
 
66
+ class ResearchLog(Base):
67
+ __tablename__ = "app_logs"
68
+
69
+ id = Column(
70
+ Integer, Sequence("reseach_log_id_seq"), primary_key=True, index=True
71
+ )
72
+
73
+ timestamp = Column(DateTime, server_default=func.now(), nullable=False)
74
+ message = Column(Text, nullable=False)
75
+ # Module that the log message came from.
76
+ module = Column(Text, nullable=False)
77
+ # Function that the log message came from.
78
+ function = Column(Text, nullable=False)
79
+ # Line number that the log message came from.
80
+ line_no = Column(Integer, nullable=False)
81
+ # Log level.
82
+ level = Column(String(32), nullable=False)
83
+ research_id = Column(
84
+ Integer,
85
+ ForeignKey("research.id", ondelete="CASCADE"),
86
+ nullable=True,
87
+ index=True,
88
+ )
89
+
90
+
64
91
  class ResearchReport(Base):
65
92
  __tablename__ = "research_report"
66
93
 
@@ -118,6 +145,26 @@ class Setting(Base):
118
145
  __table_args__ = (UniqueConstraint("key", name="uix_settings_key"),)
119
146
 
120
147
 
148
+ class ResearchStrategy(Base):
149
+ """Database model for tracking research strategies used"""
150
+
151
+ __tablename__ = "research_strategies"
152
+
153
+ id = Column(Integer, primary_key=True, index=True)
154
+ research_id = Column(
155
+ Integer,
156
+ ForeignKey("research.id", ondelete="CASCADE"),
157
+ nullable=False,
158
+ unique=True,
159
+ index=True,
160
+ )
161
+ strategy_name = Column(String(100), nullable=False, index=True)
162
+ created_at = Column(DateTime, server_default=func.now(), nullable=False)
163
+
164
+ # Relationship
165
+ research = relationship("Research", backref="strategy")
166
+
167
+
121
168
  class Journal(Base):
122
169
  """
123
170
  Database model for storing information about academic journals.
@@ -125,7 +172,9 @@ class Journal(Base):
125
172
 
126
173
  __tablename__ = "journals"
127
174
 
128
- id = Column(Integer, Sequence("journal_id_seq"), primary_key=True, index=True)
175
+ id = Column(
176
+ Integer, Sequence("journal_id_seq"), primary_key=True, index=True
177
+ )
129
178
 
130
179
  # Name of the journal
131
180
  name = Column(String(255), nullable=False, unique=True, index=True)
@@ -4,10 +4,10 @@ Handles schema upgrades for existing ldr.db databases.
4
4
  """
5
5
 
6
6
  import os
7
- import sqlite3
8
7
  import sys
9
8
 
10
9
  from loguru import logger
10
+ from sqlalchemy import create_engine, inspect
11
11
 
12
12
  # Add the parent directory to sys.path to allow relative imports
13
13
  sys.path.append(
@@ -19,53 +19,71 @@ try:
19
19
  except ImportError:
20
20
  # Fallback path if import fails
21
21
  current_dir = os.path.dirname(os.path.abspath(__file__))
22
- project_root = os.path.abspath(os.path.join(current_dir, "..", "..", "..", ".."))
22
+ project_root = os.path.abspath(
23
+ os.path.join(current_dir, "..", "..", "..", "..")
24
+ )
23
25
  DB_PATH = os.path.join(project_root, "src", "data", "ldr.db")
24
26
 
27
+ from .models import Base, ResearchStrategy
28
+
25
29
 
26
- def check_table_exists(conn, table_name):
30
+ def remove_research_log_table(engine):
27
31
  """
28
- Check if a table exists in the database
32
+ Remove the redundant research_log table if it exists
29
33
 
30
34
  Args:
31
- conn: SQLite connection
32
- table_name: Name of the table
35
+ engine: SQLAlchemy engine
33
36
 
34
37
  Returns:
35
- bool: True if table exists, False otherwise
38
+ bool: True if operation was successful, False otherwise
36
39
  """
37
- cursor = conn.cursor()
38
- cursor.execute(
39
- "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,)
40
- )
41
- return cursor.fetchone() is not None
40
+ try:
41
+ inspector = inspect(engine)
42
+
43
+ # Check if table exists
44
+ if inspector.has_table("research_log"):
45
+ # For SQLite, we need to use raw SQL for DROP TABLE
46
+ with engine.connect() as conn:
47
+ conn.execute("DROP TABLE research_log")
48
+ conn.commit()
49
+ logger.info("Successfully removed redundant 'research_log' table")
50
+ return True
51
+ else:
52
+ logger.info("Table 'research_log' does not exist, no action needed")
53
+ return True
54
+ except Exception:
55
+ logger.exception("Error removing research_log table")
56
+ return False
42
57
 
43
58
 
44
- def remove_research_log_table(conn):
59
+ def create_research_strategy_table(engine):
45
60
  """
46
- Remove the redundant research_log table if it exists
61
+ Create the research_strategies table if it doesn't exist
47
62
 
48
63
  Args:
49
- conn: SQLite connection
64
+ engine: SQLAlchemy engine
50
65
 
51
66
  Returns:
52
67
  bool: True if operation was successful, False otherwise
53
68
  """
54
69
  try:
55
- cursor = conn.cursor()
70
+ inspector = inspect(engine)
56
71
 
57
72
  # Check if table exists
58
- if check_table_exists(conn, "research_log"):
59
- # For SQLite, DROP TABLE is the way to remove a table
60
- cursor.execute("DROP TABLE research_log")
61
- conn.commit()
62
- logger.info("Successfully removed redundant 'research_log' table")
73
+ if not inspector.has_table("research_strategies"):
74
+ # Create the table using ORM
75
+ Base.metadata.create_all(
76
+ engine, tables=[ResearchStrategy.__table__]
77
+ )
78
+ logger.info("Successfully created 'research_strategies' table")
63
79
  return True
64
80
  else:
65
- logger.info("Table 'research_log' does not exist, no action needed")
81
+ logger.info(
82
+ "Table 'research_strategies' already exists, no action needed"
83
+ )
66
84
  return True
67
85
  except Exception:
68
- logger.exception("Error removing research_log table")
86
+ logger.exception("Error creating research_strategies table")
69
87
  return False
70
88
 
71
89
 
@@ -78,20 +96,22 @@ def run_schema_upgrades():
78
96
  """
79
97
  # Check if database exists
80
98
  if not os.path.exists(DB_PATH):
81
- logger.warning(f"Database not found at {DB_PATH}, skipping schema upgrades")
99
+ logger.warning(
100
+ f"Database not found at {DB_PATH}, skipping schema upgrades"
101
+ )
82
102
  return False
83
103
 
84
104
  logger.info(f"Running schema upgrades on {DB_PATH}")
85
105
 
86
106
  try:
87
- # Connect to the database
88
- conn = sqlite3.connect(DB_PATH)
107
+ # Create SQLAlchemy engine
108
+ engine = create_engine(f"sqlite:///{DB_PATH}")
89
109
 
90
110
  # 1. Remove the redundant research_log table
91
- remove_research_log_table(conn)
111
+ remove_research_log_table(engine)
92
112
 
93
- # Close connection
94
- conn.close()
113
+ # 2. Create research_strategies table
114
+ create_research_strategy_table(engine)
95
115
 
96
116
  logger.info("Schema upgrades completed successfully")
97
117
  return True
@@ -1,10 +1,12 @@
1
- import json
2
1
  import os
3
2
  import sqlite3
4
3
  from datetime import datetime
5
4
 
6
5
  from loguru import logger
7
6
 
7
+ from ...utilities.db_utils import get_db_session
8
+ from ..database.models import ResearchLog
9
+
8
10
  # Database path
9
11
  # Use unified database in data directory
10
12
  DATA_DIR = os.path.abspath(
@@ -15,7 +17,9 @@ DB_PATH = os.path.join(DATA_DIR, "ldr.db")
15
17
 
16
18
  # Legacy database paths (for migration)
17
19
  LEGACY_RESEARCH_HISTORY_DB = os.path.abspath(
18
- os.path.join(os.path.dirname(__file__), "..", "..", "..", "research_history.db")
20
+ os.path.join(
21
+ os.path.dirname(__file__), "..", "..", "..", "research_history.db"
22
+ )
19
23
  )
20
24
  LEGACY_DEEP_RESEARCH_DB = os.path.join(
21
25
  os.path.abspath(
@@ -97,7 +101,9 @@ def init_db():
97
101
  columns = [column[1] for column in cursor.fetchall()]
98
102
 
99
103
  if "duration_seconds" not in columns:
100
- print("Adding missing 'duration_seconds' column to research_history table")
104
+ print(
105
+ "Adding missing 'duration_seconds' column to research_history table"
106
+ )
101
107
  cursor.execute(
102
108
  "ALTER TABLE research_history ADD COLUMN duration_seconds INTEGER"
103
109
  )
@@ -105,7 +111,9 @@ def init_db():
105
111
  # Check if the progress column exists, add it if missing
106
112
  if "progress" not in columns:
107
113
  print("Adding missing 'progress' column to research_history table")
108
- cursor.execute("ALTER TABLE research_history ADD COLUMN progress INTEGER")
114
+ cursor.execute(
115
+ "ALTER TABLE research_history ADD COLUMN progress INTEGER"
116
+ )
109
117
 
110
118
  # Check if the title column exists, add it if missing
111
119
  if "title" not in columns:
@@ -180,10 +188,14 @@ def calculate_duration(created_at_str, completed_at_str=None):
180
188
  else: # Older format without T
181
189
  # Try different formats
182
190
  try:
183
- start_time = datetime.strptime(created_at_str, "%Y-%m-%d %H:%M:%S.%f")
191
+ start_time = datetime.strptime(
192
+ created_at_str, "%Y-%m-%d %H:%M:%S.%f"
193
+ )
184
194
  except ValueError:
185
195
  try:
186
- start_time = datetime.strptime(created_at_str, "%Y-%m-%d %H:%M:%S")
196
+ start_time = datetime.strptime(
197
+ created_at_str, "%Y-%m-%d %H:%M:%S"
198
+ )
187
199
  except ValueError:
188
200
  # Last resort fallback
189
201
  start_time = datetime.fromisoformat(
@@ -198,7 +210,7 @@ def calculate_duration(created_at_str, completed_at_str=None):
198
210
  start_time = parser.parse(created_at_str)
199
211
  except Exception:
200
212
  logger.exception(
201
- f"Fallback parsing also failed for created_at:" f" {created_at_str}"
213
+ f"Fallback parsing also failed for created_at: {created_at_str}"
202
214
  )
203
215
  return None
204
216
 
@@ -212,36 +224,6 @@ def calculate_duration(created_at_str, completed_at_str=None):
212
224
  return None
213
225
 
214
226
 
215
- def add_log_to_db(research_id, message, log_type="info", progress=None, metadata=None):
216
- """
217
- Store a log entry in the database
218
-
219
- Args:
220
- research_id: ID of the research
221
- message: Log message text
222
- log_type: Type of log (info, error, milestone)
223
- progress: Progress percentage (0-100)
224
- metadata: Additional metadata as dictionary (will be stored as JSON)
225
- """
226
- try:
227
- timestamp = datetime.utcnow().isoformat()
228
- metadata_json = json.dumps(metadata) if metadata else None
229
-
230
- conn = get_db_connection()
231
- cursor = conn.cursor()
232
- cursor.execute(
233
- "INSERT INTO research_logs (research_id, timestamp, message, log_type, progress, metadata) "
234
- "VALUES (?, ?, ?, ?, ?, ?)",
235
- (research_id, timestamp, message, log_type, progress, metadata_json),
236
- )
237
- conn.commit()
238
- conn.close()
239
- return True
240
- except Exception:
241
- logger.exception("Error adding log to database")
242
- return False
243
-
244
-
245
227
  def get_logs_for_research(research_id):
246
228
  """
247
229
  Retrieve all logs for a specific research ID
@@ -253,35 +235,23 @@ def get_logs_for_research(research_id):
253
235
  List of log entries as dictionaries
254
236
  """
255
237
  try:
256
- conn = get_db_connection()
257
- conn.row_factory = sqlite3.Row
258
- cursor = conn.cursor()
259
- cursor.execute(
260
- "SELECT * FROM research_logs WHERE research_id = ? ORDER BY timestamp ASC",
261
- (research_id,),
238
+ session = get_db_session()
239
+ log_results = (
240
+ session.query(ResearchLog)
241
+ .filter(ResearchLog.research_id == research_id)
242
+ .order_by(ResearchLog.timestamp.asc())
243
+ .all()
262
244
  )
263
- results = cursor.fetchall()
264
- conn.close()
265
245
 
266
246
  logs = []
267
- for result in results:
268
- log_entry = dict(result)
269
- # Parse metadata JSON if it exists
270
- if log_entry.get("metadata"):
271
- try:
272
- log_entry["metadata"] = json.loads(log_entry["metadata"])
273
- except Exception:
274
- log_entry["metadata"] = {}
275
- else:
276
- log_entry["metadata"] = {}
277
-
247
+ for result in log_results:
278
248
  # Convert entry for frontend consumption
279
249
  formatted_entry = {
280
- "time": log_entry["timestamp"],
281
- "message": log_entry["message"],
282
- "progress": log_entry["progress"],
283
- "metadata": log_entry["metadata"],
284
- "type": log_entry["log_type"],
250
+ "time": result.timestamp,
251
+ "message": result.message,
252
+ "type": result.level,
253
+ "module": result.module,
254
+ "line_no": result.line_no,
285
255
  }
286
256
  logs.append(formatted_entry)
287
257
 
@@ -289,3 +259,23 @@ def get_logs_for_research(research_id):
289
259
  except Exception:
290
260
  logger.exception("Error retrieving logs from database")
291
261
  return []
262
+
263
+
264
+ @logger.catch
265
+ def get_total_logs_for_research(research_id):
266
+ """
267
+ Returns the total number of logs for a given `research_id`.
268
+
269
+ Args:
270
+ research_id (int): The ID of the research.
271
+
272
+ Returns:
273
+ int: Total number of logs for the specified research ID.
274
+ """
275
+ session = get_db_session()
276
+ total_logs = (
277
+ session.query(ResearchLog)
278
+ .filter(ResearchLog.research_id == research_id)
279
+ .count()
280
+ )
281
+ return total_logs
@@ -59,9 +59,7 @@ def api_start_research():
59
59
  mode,
60
60
  "in_progress",
61
61
  created_at,
62
- json.dumps(
63
- [{"time": created_at, "message": "Research started", "progress": 0}]
64
- ),
62
+ json.dumps([{"time": created_at, "progress": 0}]),
65
63
  json.dumps(research_settings),
66
64
  ),
67
65
  )
@@ -89,9 +87,11 @@ def api_start_research():
89
87
  "research_id": research_id,
90
88
  }
91
89
  )
92
- except Exception as e:
93
- logger.error(f"Error starting research: {str(e)}")
94
- return jsonify({"status": "error", "message": str(e)}), 500
90
+ except Exception:
91
+ logger.exception("Error starting research")
92
+ return jsonify(
93
+ {"status": "error", "message": "Failed to start research"}, 500
94
+ )
95
95
 
96
96
 
97
97
  @api_bp.route("/status/<int:research_id>", methods=["GET"])
@@ -120,7 +120,9 @@ def api_research_status(research_id):
120
120
  try:
121
121
  metadata = json.loads(metadata_str)
122
122
  except json.JSONDecodeError:
123
- logger.warning(f"Invalid JSON in metadata for research {research_id}")
123
+ logger.warning(
124
+ f"Invalid JSON in metadata for research {research_id}"
125
+ )
124
126
 
125
127
  return jsonify(
126
128
  {
@@ -144,10 +146,18 @@ def api_terminate_research(research_id):
144
146
  try:
145
147
  result = cancel_research(research_id)
146
148
  return jsonify(
147
- {"status": "success", "message": "Research terminated", "result": result}
149
+ {
150
+ "status": "success",
151
+ "message": "Research terminated",
152
+ "result": result,
153
+ }
154
+ )
155
+ except Exception:
156
+ logger.exception("Error terminating research")
157
+ return (
158
+ jsonify({"status": "error", "message": "Failed to stop research."}),
159
+ 500,
148
160
  )
149
- except Exception as e:
150
- return jsonify({"status": "error", "message": str(e)}), 500
151
161
 
152
162
 
153
163
  @api_bp.route("/resources/<int:research_id>", methods=["GET"])
@@ -158,8 +168,11 @@ def api_get_resources(research_id):
158
168
  try:
159
169
  resources = get_resources_for_research(research_id)
160
170
  return jsonify({"status": "success", "resources": resources})
161
- except Exception as e:
162
- return jsonify({"status": "error", "message": str(e)}), 500
171
+ except Exception:
172
+ logger.exception("Error getting resources for research")
173
+ return jsonify(
174
+ {"status": "error", "message": "Failed to get resources"}, 500
175
+ )
163
176
 
164
177
 
165
178
  @api_bp.route("/resources/<int:research_id>", methods=["POST"])
@@ -182,19 +195,25 @@ def api_add_resource(research_id):
182
195
  # Validate required fields
183
196
  if not title or not url:
184
197
  return (
185
- jsonify({"status": "error", "message": "Title and URL are required"}),
198
+ jsonify(
199
+ {"status": "error", "message": "Title and URL are required"}
200
+ ),
186
201
  400,
187
202
  )
188
203
 
189
204
  # Check if the research exists
190
205
  conn = get_db_connection()
191
206
  cursor = conn.cursor()
192
- cursor.execute("SELECT id FROM research_history WHERE id = ?", (research_id,))
207
+ cursor.execute(
208
+ "SELECT id FROM research_history WHERE id = ?", (research_id,)
209
+ )
193
210
  result = cursor.fetchone()
194
211
  conn.close()
195
212
 
196
213
  if not result:
197
- return jsonify({"status": "error", "message": "Research not found"}), 404
214
+ return jsonify(
215
+ {"status": "error", "message": "Research not found"}
216
+ ), 404
198
217
 
199
218
  # Add the resource
200
219
  resource_id = add_resource(
@@ -231,10 +250,15 @@ def api_delete_resource(research_id, resource_id):
231
250
 
232
251
  if success:
233
252
  return jsonify(
234
- {"status": "success", "message": "Resource deleted successfully"}
253
+ {
254
+ "status": "success",
255
+ "message": "Resource deleted successfully",
256
+ }
235
257
  )
236
258
  else:
237
- return jsonify({"status": "error", "message": "Resource not found"}), 404
259
+ return jsonify(
260
+ {"status": "error", "message": "Resource not found"}
261
+ ), 404
238
262
  except Exception as e:
239
263
  logger.error(f"Error deleting resource: {str(e)}")
240
264
  return jsonify({"status": "error", "message": str(e)}), 500
@@ -252,7 +276,10 @@ def check_ollama_status():
252
276
 
253
277
  if provider.lower() != "ollama":
254
278
  return jsonify(
255
- {"running": True, "message": f"Using provider: {provider}, not Ollama"}
279
+ {
280
+ "running": True,
281
+ "message": f"Using provider: {provider}, not Ollama",
282
+ }
256
283
  )
257
284
 
258
285
  # Get Ollama API URL from LLM config
@@ -272,7 +299,9 @@ def check_ollama_status():
272
299
  response = requests.get(f"{ollama_base_url}/api/tags", timeout=5)
273
300
 
274
301
  # Add response details for debugging
275
- logger.debug(f"Ollama status check response code: {response.status_code}")
302
+ logger.debug(
303
+ f"Ollama status check response code: {response.status_code}"
304
+ )
276
305
 
277
306
  if response.status_code == 200:
278
307
  # Try to validate the response content
@@ -420,7 +449,9 @@ def check_ollama_model():
420
449
 
421
450
  # Debug log the first bit of the response
422
451
  response_preview = (
423
- str(data)[:500] + "..." if len(str(data)) > 500 else str(data)
452
+ str(data)[:500] + "..."
453
+ if len(str(data)) > 500
454
+ else str(data)
424
455
  )
425
456
  logger.debug(f"Ollama API response data: {response_preview}")
426
457
 
@@ -448,7 +479,8 @@ def check_ollama_model():
448
479
 
449
480
  # Case-insensitive model name comparison
450
481
  model_exists = any(
451
- m.get("name", "").lower() == model_name.lower() for m in models
482
+ m.get("name", "").lower() == model_name.lower()
483
+ for m in models
452
484
  )
453
485
 
454
486
  if model_exists:
@@ -561,7 +593,9 @@ def api_get_config():
561
593
  "search_tool", "auto"
562
594
  ),
563
595
  "features": {
564
- "notifications": current_app.config.get("ENABLE_NOTIFICATIONS", False)
596
+ "notifications": current_app.config.get(
597
+ "ENABLE_NOTIFICATIONS", False
598
+ )
565
599
  },
566
600
  }
567
601
 
@@ -190,7 +190,10 @@ def get_benchmark_config():
190
190
  ],
191
191
  "evaluation_models": {
192
192
  "openai_endpoint": [
193
- {"id": "anthropic/claude-3.7-sonnet", "name": "Claude 3.7 Sonnet"}
193
+ {
194
+ "id": "anthropic/claude-3.7-sonnet",
195
+ "name": "Claude 3.7 Sonnet",
196
+ }
194
197
  ],
195
198
  "openai": [
196
199
  {"id": "gpt-4o", "name": "GPT-4o"},
@@ -0,0 +1,22 @@
1
+ """
2
+ Stores global state.
3
+ """
4
+
5
+ # Active research processes and socket subscriptions
6
+ active_research = {}
7
+ socket_subscriptions = {}
8
+ # Add termination flags dictionary
9
+ termination_flags = {}
10
+
11
+
12
+ def get_globals():
13
+ """
14
+ Returns:
15
+ Global state for other modules to access.
16
+
17
+ """
18
+ return {
19
+ "active_research": active_research,
20
+ "socket_subscriptions": socket_subscriptions,
21
+ "termination_flags": termination_flags,
22
+ }