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
@@ -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,14 @@ 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 (
6
+ Base,
7
+ Journal,
8
+ Setting,
9
+ ResearchLog,
10
+ Research,
11
+ ResearchHistory,
12
+ )
6
13
 
7
14
 
8
15
  def import_default_settings_file(db_session):
@@ -24,14 +31,12 @@ def import_default_settings_file(db_session):
24
31
  settings_mgr.load_from_defaults_file(overwrite=False, delete_extra=True)
25
32
  logger.info("Successfully imported settings from files")
26
33
 
27
-
28
34
  # Update the saved version.
29
35
  settings_mgr.update_db_version()
30
36
  except Exception as e:
31
37
  logger.error("Error importing settings from files: %s", e)
32
38
 
33
39
 
34
-
35
40
  def run_migrations(engine, db_session=None):
36
41
  """
37
42
  Run any necessary database migrations
@@ -50,6 +55,18 @@ def run_migrations(engine, db_session=None):
50
55
  logger.info("Creating journals table.")
51
56
  Base.metadata.create_all(engine, tables=[Journal.__table__])
52
57
 
58
+ if not inspector.has_table(ResearchLog.__tablename__):
59
+ logger.info("Creating research logs table.")
60
+ Base.metadata.create_all(engine, tables=[ResearchLog.__table__])
61
+
62
+ if not inspector.has_table(Research.__tablename__):
63
+ logger.info("Creating research table.")
64
+ Base.metadata.create_all(engine, tables=[Research.__table__])
65
+
66
+ if not inspector.has_table(ResearchHistory.__tablename__):
67
+ logger.info("Creating research table.")
68
+ Base.metadata.create_all(engine, tables=[ResearchHistory.__table__])
69
+
53
70
  # Import existing settings from files
54
71
  if db_session:
55
72
  import_default_settings_file(db_session)
@@ -34,6 +34,37 @@ class ResearchStatus(enum.Enum):
34
34
  CANCELLED = "cancelled"
35
35
 
36
36
 
37
+ class ResearchHistory(Base):
38
+ """Represents the research table."""
39
+
40
+ __tablename__ = "research_history"
41
+
42
+ # Unique identifier for each record.
43
+ id = Column(Integer, primary_key=True, autoincrement=True)
44
+ # The search query.
45
+ query = Column(Text, nullable=False)
46
+ # The mode of research (e.g., 'quick_summary', 'detailed_report').
47
+ mode = Column(Text, nullable=False)
48
+ # Current status of the research.
49
+ status = Column(Text, nullable=False)
50
+ # The timestamp when the research started.
51
+ created_at = Column(Text, nullable=False)
52
+ # The timestamp when the research was completed.
53
+ completed_at = Column(Text)
54
+ # Duration of the research in seconds.
55
+ duration_seconds = Column(Integer)
56
+ # Path to the generated report.
57
+ report_path = Column(Text)
58
+ # Additional metadata about the research.
59
+ research_meta = Column(JSON)
60
+ # Latest progress log message.
61
+ progress_log = Column(JSON)
62
+ # Current progress of the research (as a percentage).
63
+ progress = Column(Integer)
64
+ # Title of the research report.
65
+ title = Column(Text)
66
+
67
+
37
68
  class Research(Base):
38
69
  __tablename__ = "research"
39
70
 
@@ -42,7 +73,9 @@ class Research(Base):
42
73
  status = Column(
43
74
  Enum(ResearchStatus), default=ResearchStatus.PENDING, nullable=False
44
75
  )
45
- mode = Column(Enum(ResearchMode), default=ResearchMode.QUICK, nullable=False)
76
+ mode = Column(
77
+ Enum(ResearchMode), default=ResearchMode.QUICK, nullable=False
78
+ )
46
79
  created_at = Column(DateTime, server_default=func.now(), nullable=False)
47
80
  updated_at = Column(
48
81
  DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
@@ -52,36 +85,30 @@ class Research(Base):
52
85
  end_time = Column(DateTime, nullable=True)
53
86
  error_message = Column(Text, nullable=True)
54
87
 
55
- # Relationships
56
- report = relationship(
57
- "ResearchReport",
58
- back_populates="research",
59
- uselist=False,
60
- cascade="all, delete-orphan",
61
- )
62
88
 
89
+ class ResearchLog(Base):
90
+ __tablename__ = "app_logs"
63
91
 
64
- class ResearchReport(Base):
65
- __tablename__ = "research_report"
92
+ id = Column(
93
+ Integer, Sequence("reseach_log_id_seq"), primary_key=True, index=True
94
+ )
66
95
 
67
- id = Column(Integer, primary_key=True, index=True)
96
+ timestamp = Column(DateTime, server_default=func.now(), nullable=False)
97
+ message = Column(Text, nullable=False)
98
+ # Module that the log message came from.
99
+ module = Column(Text, nullable=False)
100
+ # Function that the log message came from.
101
+ function = Column(Text, nullable=False)
102
+ # Line number that the log message came from.
103
+ line_no = Column(Integer, nullable=False)
104
+ # Log level.
105
+ level = Column(String(32), nullable=False)
68
106
  research_id = Column(
69
107
  Integer,
70
108
  ForeignKey("research.id", ondelete="CASCADE"),
71
- nullable=False,
72
- unique=True,
109
+ nullable=True,
110
+ index=True,
73
111
  )
74
- content = Column(Text, nullable=True)
75
- created_at = Column(DateTime, server_default=func.now(), nullable=False)
76
- updated_at = Column(
77
- DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
78
- )
79
- report_metadata = Column(
80
- JSON, nullable=True
81
- ) # Additional metadata about the report
82
-
83
- # Relationships
84
- research = relationship("Research", back_populates="report")
85
112
 
86
113
 
87
114
  class SettingType(enum.Enum):
@@ -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(
@@ -39,26 +43,6 @@ def init_db():
39
43
  conn = get_db_connection()
40
44
  cursor = conn.cursor()
41
45
 
42
- # Create the table if it doesn't exist
43
- cursor.execute(
44
- """
45
- CREATE TABLE IF NOT EXISTS research_history (
46
- id INTEGER PRIMARY KEY AUTOINCREMENT,
47
- query TEXT NOT NULL,
48
- mode TEXT NOT NULL,
49
- status TEXT NOT NULL,
50
- created_at TEXT NOT NULL,
51
- completed_at TEXT,
52
- duration_seconds INTEGER,
53
- report_path TEXT,
54
- metadata TEXT,
55
- progress_log TEXT,
56
- progress INTEGER,
57
- title TEXT
58
- )
59
- """
60
- )
61
-
62
46
  # Create a dedicated table for research logs
63
47
  cursor.execute(
64
48
  """
@@ -97,21 +81,35 @@ def init_db():
97
81
  columns = [column[1] for column in cursor.fetchall()]
98
82
 
99
83
  if "duration_seconds" not in columns:
100
- print("Adding missing 'duration_seconds' column to research_history table")
84
+ logger.info(
85
+ "Adding missing 'duration_seconds' column to research_history table"
86
+ )
101
87
  cursor.execute(
102
88
  "ALTER TABLE research_history ADD COLUMN duration_seconds INTEGER"
103
89
  )
104
90
 
105
91
  # Check if the progress column exists, add it if missing
106
92
  if "progress" not in columns:
107
- print("Adding missing 'progress' column to research_history table")
108
- cursor.execute("ALTER TABLE research_history ADD COLUMN progress INTEGER")
93
+ logger.info(
94
+ "Adding missing 'progress' column to research_history table"
95
+ )
96
+ cursor.execute(
97
+ "ALTER TABLE research_history ADD COLUMN progress INTEGER"
98
+ )
109
99
 
110
100
  # Check if the title column exists, add it if missing
111
101
  if "title" not in columns:
112
- print("Adding missing 'title' column to research_history table")
102
+ logger.info("Adding missing 'title' column to research_history table")
113
103
  cursor.execute("ALTER TABLE research_history ADD COLUMN title TEXT")
114
104
 
105
+ # Check if the metadata column exists, and rename it to "research_meta"
106
+ # if it does.
107
+ if "metadata" in columns:
108
+ logger.info("Renaming 'metadata' column to 'research_meta'")
109
+ cursor.execute(
110
+ "ALTER TABLE research_history RENAME COLUMN metadata TO research_meta"
111
+ )
112
+
115
113
  # Enable foreign key support
116
114
  cursor.execute("PRAGMA foreign_keys = ON")
117
115
 
@@ -180,10 +178,14 @@ def calculate_duration(created_at_str, completed_at_str=None):
180
178
  else: # Older format without T
181
179
  # Try different formats
182
180
  try:
183
- start_time = datetime.strptime(created_at_str, "%Y-%m-%d %H:%M:%S.%f")
181
+ start_time = datetime.strptime(
182
+ created_at_str, "%Y-%m-%d %H:%M:%S.%f"
183
+ )
184
184
  except ValueError:
185
185
  try:
186
- start_time = datetime.strptime(created_at_str, "%Y-%m-%d %H:%M:%S")
186
+ start_time = datetime.strptime(
187
+ created_at_str, "%Y-%m-%d %H:%M:%S"
188
+ )
187
189
  except ValueError:
188
190
  # Last resort fallback
189
191
  start_time = datetime.fromisoformat(
@@ -198,7 +200,7 @@ def calculate_duration(created_at_str, completed_at_str=None):
198
200
  start_time = parser.parse(created_at_str)
199
201
  except Exception:
200
202
  logger.exception(
201
- f"Fallback parsing also failed for created_at:" f" {created_at_str}"
203
+ f"Fallback parsing also failed for created_at: {created_at_str}"
202
204
  )
203
205
  return None
204
206
 
@@ -212,36 +214,6 @@ def calculate_duration(created_at_str, completed_at_str=None):
212
214
  return None
213
215
 
214
216
 
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
217
  def get_logs_for_research(research_id):
246
218
  """
247
219
  Retrieve all logs for a specific research ID
@@ -253,35 +225,23 @@ def get_logs_for_research(research_id):
253
225
  List of log entries as dictionaries
254
226
  """
255
227
  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,),
228
+ session = get_db_session()
229
+ log_results = (
230
+ session.query(ResearchLog)
231
+ .filter(ResearchLog.research_id == research_id)
232
+ .order_by(ResearchLog.timestamp.asc())
233
+ .all()
262
234
  )
263
- results = cursor.fetchall()
264
- conn.close()
265
235
 
266
236
  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
-
237
+ for result in log_results:
278
238
  # Convert entry for frontend consumption
279
239
  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"],
240
+ "time": result.timestamp,
241
+ "message": result.message,
242
+ "type": result.level,
243
+ "module": result.module,
244
+ "line_no": result.line_no,
285
245
  }
286
246
  logs.append(formatted_entry)
287
247
 
@@ -289,3 +249,23 @@ def get_logs_for_research(research_id):
289
249
  except Exception:
290
250
  logger.exception("Error retrieving logs from database")
291
251
  return []
252
+
253
+
254
+ @logger.catch
255
+ def get_total_logs_for_research(research_id):
256
+ """
257
+ Returns the total number of logs for a given `research_id`.
258
+
259
+ Args:
260
+ research_id (int): The ID of the research.
261
+
262
+ Returns:
263
+ int: Total number of logs for the specified research ID.
264
+ """
265
+ session = get_db_session()
266
+ total_logs = (
267
+ session.query(ResearchLog)
268
+ .filter(ResearchLog.research_id == research_id)
269
+ .count()
270
+ )
271
+ return total_logs