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
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from functools import cache
2
3
 
3
4
  from langchain_anthropic import ChatAnthropic
4
5
  from langchain_community.llms import VLLM
@@ -24,7 +25,163 @@ VALID_PROVIDERS = [
24
25
  ]
25
26
 
26
27
 
27
- def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_url=None):
28
+ def is_openai_available():
29
+ """Check if OpenAI is available"""
30
+ try:
31
+ api_key = get_db_setting("llm.openai.api_key")
32
+ return bool(api_key)
33
+ except Exception:
34
+ return False
35
+
36
+
37
+ def is_anthropic_available():
38
+ """Check if Anthropic is available"""
39
+ try:
40
+ api_key = get_db_setting("llm.anthropic.api_key")
41
+ return bool(api_key)
42
+ except Exception:
43
+ return False
44
+
45
+
46
+ def is_openai_endpoint_available():
47
+ """Check if OpenAI endpoint is available"""
48
+ try:
49
+ api_key = get_db_setting("llm.openai_endpoint.api_key")
50
+ return bool(api_key)
51
+ except Exception:
52
+ return False
53
+
54
+
55
+ def is_ollama_available():
56
+ """Check if Ollama is running"""
57
+ try:
58
+ import requests
59
+
60
+ raw_base_url = get_db_setting(
61
+ "llm.ollama.url", "http://localhost:11434"
62
+ )
63
+ base_url = (
64
+ normalize_url(raw_base_url)
65
+ if raw_base_url
66
+ else "http://localhost:11434"
67
+ )
68
+ logger.info(f"Checking Ollama availability at {base_url}/api/tags")
69
+
70
+ try:
71
+ response = requests.get(f"{base_url}/api/tags", timeout=3.0)
72
+ if response.status_code == 200:
73
+ logger.info(
74
+ f"Ollama is available. Status code: {response.status_code}"
75
+ )
76
+ # Log first 100 chars of response to debug
77
+ logger.info(f"Response preview: {str(response.text)[:100]}")
78
+ return True
79
+ else:
80
+ logger.warning(
81
+ f"Ollama API returned status code: {response.status_code}"
82
+ )
83
+ return False
84
+ except requests.exceptions.RequestException as req_error:
85
+ logger.error(
86
+ f"Request error when checking Ollama: {str(req_error)}"
87
+ )
88
+ return False
89
+ except Exception:
90
+ logger.exception("Unexpected error when checking Ollama")
91
+ return False
92
+ except Exception:
93
+ logger.exception("Error in is_ollama_available")
94
+ return False
95
+
96
+
97
+ def is_vllm_available():
98
+ """Check if VLLM capability is available"""
99
+ try:
100
+ import torch # noqa: F401
101
+ import transformers # noqa: F401
102
+
103
+ return True
104
+ except ImportError:
105
+ return False
106
+
107
+
108
+ def is_lmstudio_available():
109
+ """Check if LM Studio is available"""
110
+ try:
111
+ import requests
112
+
113
+ lmstudio_url = get_db_setting(
114
+ "llm.lmstudio.url", "http://localhost:1234"
115
+ )
116
+ # LM Studio typically uses OpenAI-compatible endpoints
117
+ response = requests.get(f"{lmstudio_url}/v1/models", timeout=1.0)
118
+ return response.status_code == 200
119
+ except Exception:
120
+ return False
121
+
122
+
123
+ def is_llamacpp_available():
124
+ """Check if LlamaCpp is available and configured"""
125
+ try:
126
+ from langchain_community.llms import LlamaCpp # noqa: F401
127
+
128
+ model_path = get_db_setting("llm.llamacpp_model_path")
129
+ return bool(model_path) and os.path.exists(model_path)
130
+ except Exception:
131
+ return False
132
+
133
+
134
+ @cache
135
+ def get_available_providers():
136
+ """Return available model providers"""
137
+ providers = {}
138
+
139
+ if is_ollama_available():
140
+ providers["ollama"] = "Ollama (local models)"
141
+
142
+ if is_openai_available():
143
+ providers["openai"] = "OpenAI API"
144
+
145
+ if is_anthropic_available():
146
+ providers["anthropic"] = "Anthropic API"
147
+
148
+ if is_openai_endpoint_available():
149
+ providers["openai_endpoint"] = "OpenAI-compatible Endpoint"
150
+
151
+ if is_lmstudio_available():
152
+ providers["lmstudio"] = "LM Studio (local models)"
153
+
154
+ if is_llamacpp_available():
155
+ providers["llamacpp"] = "LlamaCpp (local models)"
156
+
157
+ # Check for VLLM capability
158
+ try:
159
+ import torch # noqa: F401
160
+ import transformers # noqa: F401
161
+
162
+ providers["vllm"] = "VLLM (local models)"
163
+ except ImportError:
164
+ pass
165
+
166
+ # Default fallback
167
+ if not providers:
168
+ providers["none"] = "No model providers available"
169
+
170
+ return providers
171
+
172
+
173
+ def get_selected_llm_provider():
174
+ return get_db_setting("llm.provider", "ollama").lower()
175
+
176
+
177
+ def get_llm(
178
+ model_name=None,
179
+ temperature=None,
180
+ provider=None,
181
+ openai_endpoint_url=None,
182
+ research_id=None,
183
+ research_context=None,
184
+ ):
28
185
  """
29
186
  Get LLM instance based on model name and provider.
30
187
 
@@ -34,6 +191,8 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
34
191
  provider: Provider to use (if None, uses database setting)
35
192
  openai_endpoint_url: Custom endpoint URL to use (if None, uses database
36
193
  setting)
194
+ research_id: Optional research ID for token tracking
195
+ research_context: Optional research context for enhanced token tracking
37
196
 
38
197
  Returns:
39
198
  A LangChain LLM instance with automatic think-tag removal
@@ -47,6 +206,16 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
47
206
  if provider is None:
48
207
  provider = get_db_setting("llm.provider", "ollama")
49
208
 
209
+ # Check if we're in testing mode and should use fallback
210
+ if os.environ.get("LDR_USE_FALLBACK_LLM", ""):
211
+ logger.info("LDR_USE_FALLBACK_LLM is set, using fallback model")
212
+ return wrap_llm_without_think_tags(
213
+ get_fallback_model(temperature),
214
+ research_id=research_id,
215
+ provider="fallback",
216
+ research_context=research_context,
217
+ )
218
+
50
219
  # Clean model name: remove quotes and extra whitespace
51
220
  if model_name:
52
221
  model_name = model_name.strip().strip("\"'").strip()
@@ -79,7 +248,8 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
79
248
  if get_db_setting("llm.supports_max_tokens", True):
80
249
  # Use 80% of context window to leave room for prompts
81
250
  max_tokens = min(
82
- int(get_db_setting("llm.max_tokens", 30000)), int(context_window_size * 0.8)
251
+ int(get_db_setting("llm.max_tokens", 30000)),
252
+ int(context_window_size * 0.8),
83
253
  )
84
254
  common_params["max_tokens"] = max_tokens
85
255
 
@@ -95,16 +265,28 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
95
265
  llm = ChatAnthropic(
96
266
  model=model_name, anthropic_api_key=api_key, **common_params
97
267
  )
98
- return wrap_llm_without_think_tags(llm)
268
+ return wrap_llm_without_think_tags(
269
+ llm,
270
+ research_id=research_id,
271
+ provider=provider,
272
+ research_context=research_context,
273
+ )
99
274
 
100
275
  elif provider == "openai":
101
276
  api_key = get_db_setting("llm.openai.api_key")
102
277
  if not api_key:
103
- logger.warning("OPENAI_API_KEY not found. Falling back to default model.")
278
+ logger.warning(
279
+ "OPENAI_API_KEY not found. Falling back to default model."
280
+ )
104
281
  return get_fallback_model(temperature)
105
282
 
106
283
  llm = ChatOpenAI(model=model_name, api_key=api_key, **common_params)
107
- return wrap_llm_without_think_tags(llm)
284
+ return wrap_llm_without_think_tags(
285
+ llm,
286
+ research_id=research_id,
287
+ provider=provider,
288
+ research_context=research_context,
289
+ )
108
290
 
109
291
  elif provider == "openai_endpoint":
110
292
  api_key = get_db_setting("llm.openai_endpoint.api_key")
@@ -126,7 +308,12 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
126
308
  openai_api_base=openai_endpoint_url,
127
309
  **common_params,
128
310
  )
129
- return wrap_llm_without_think_tags(llm)
311
+ return wrap_llm_without_think_tags(
312
+ llm,
313
+ research_id=research_id,
314
+ provider=provider,
315
+ research_context=research_context,
316
+ )
130
317
 
131
318
  elif provider == "vllm":
132
319
  try:
@@ -138,7 +325,12 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
138
325
  top_p=0.95,
139
326
  temperature=temperature,
140
327
  )
141
- return wrap_llm_without_think_tags(llm)
328
+ return wrap_llm_without_think_tags(
329
+ llm,
330
+ research_id=research_id,
331
+ provider=provider,
332
+ research_context=research_context,
333
+ )
142
334
  except Exception:
143
335
  logger.exception("Error loading VLLM model")
144
336
  return get_fallback_model(temperature)
@@ -146,7 +338,9 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
146
338
  elif provider == "ollama":
147
339
  try:
148
340
  # Use the configurable Ollama base URL
149
- raw_base_url = get_db_setting("llm.ollama.url", "http://localhost:11434")
341
+ raw_base_url = get_db_setting(
342
+ "llm.ollama.url", "http://localhost:11434"
343
+ )
150
344
  base_url = (
151
345
  normalize_url(raw_base_url)
152
346
  if raw_base_url
@@ -164,7 +358,9 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
164
358
  import requests
165
359
 
166
360
  try:
167
- logger.info(f"Checking if model '{model_name}' exists in Ollama")
361
+ logger.info(
362
+ f"Checking if model '{model_name}' exists in Ollama"
363
+ )
168
364
  response = requests.get(f"{base_url}/api/tags", timeout=3.0)
169
365
  if response.status_code == 200:
170
366
  # Handle both newer and older Ollama API formats
@@ -189,21 +385,30 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
189
385
  )
190
386
  return get_fallback_model(temperature)
191
387
  except Exception:
192
- logger.exception(f"Error checking for model '{model_name}' in Ollama")
388
+ logger.exception(
389
+ f"Error checking for model '{model_name}' in Ollama"
390
+ )
193
391
  # Continue anyway, let ChatOllama handle potential errors
194
392
 
195
393
  logger.info(
196
394
  f"Creating ChatOllama with model={model_name}, base_url={base_url}"
197
395
  )
198
396
  try:
199
- llm = ChatOllama(model=model_name, base_url=base_url, **common_params)
397
+ llm = ChatOllama(
398
+ model=model_name, base_url=base_url, **common_params
399
+ )
200
400
  # Test invoke to validate model works
201
401
  logger.info("Testing Ollama model with simple invocation")
202
402
  test_result = llm.invoke("Hello")
203
403
  logger.info(
204
404
  f"Ollama test successful. Response type: {type(test_result)}"
205
405
  )
206
- return wrap_llm_without_think_tags(llm)
406
+ return wrap_llm_without_think_tags(
407
+ llm,
408
+ research_id=research_id,
409
+ provider=provider,
410
+ research_context=research_context,
411
+ )
207
412
  except Exception:
208
413
  logger.exception("Error creating or testing ChatOllama")
209
414
  return get_fallback_model(temperature)
@@ -213,7 +418,9 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
213
418
 
214
419
  elif provider == "lmstudio":
215
420
  # LM Studio supports OpenAI API format, so we can use ChatOpenAI directly
216
- lmstudio_url = get_db_setting("llm.lmstudio.url", "http://localhost:1234")
421
+ lmstudio_url = get_db_setting(
422
+ "llm.lmstudio.url", "http://localhost:1234"
423
+ )
217
424
 
218
425
  llm = ChatOpenAI(
219
426
  model=model_name,
@@ -222,7 +429,12 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
222
429
  temperature=temperature,
223
430
  max_tokens=max_tokens, # Use calculated max_tokens based on context size
224
431
  )
225
- return wrap_llm_without_think_tags(llm)
432
+ return wrap_llm_without_think_tags(
433
+ llm,
434
+ research_id=research_id,
435
+ provider=provider,
436
+ research_context=research_context,
437
+ )
226
438
 
227
439
  # Update the llamacpp section in get_llm function
228
440
  elif provider == "llamacpp":
@@ -230,7 +442,9 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
230
442
  from langchain_community.llms import LlamaCpp
231
443
 
232
444
  # Get LlamaCpp connection mode from settings
233
- connection_mode = get_db_setting("llm.llamacpp_connection_mode", "local")
445
+ connection_mode = get_db_setting(
446
+ "llm.llamacpp_connection_mode", "local"
447
+ )
234
448
 
235
449
  if connection_mode == "http":
236
450
  # Use HTTP client mode
@@ -270,10 +484,20 @@ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_ur
270
484
  verbose=True,
271
485
  )
272
486
 
273
- return wrap_llm_without_think_tags(llm)
487
+ return wrap_llm_without_think_tags(
488
+ llm,
489
+ research_id=research_id,
490
+ provider=provider,
491
+ research_context=research_context,
492
+ )
274
493
 
275
494
  else:
276
- return wrap_llm_without_think_tags(get_fallback_model(temperature))
495
+ return wrap_llm_without_think_tags(
496
+ get_fallback_model(temperature),
497
+ research_id=research_id,
498
+ provider=provider,
499
+ research_context=research_context,
500
+ )
277
501
 
278
502
 
279
503
  def get_fallback_model(temperature=None):
@@ -285,8 +509,36 @@ def get_fallback_model(temperature=None):
285
509
  )
286
510
 
287
511
 
288
- def wrap_llm_without_think_tags(llm):
289
- """Create a wrapper class that processes LLM outputs with remove_think_tags"""
512
+ def wrap_llm_without_think_tags(
513
+ llm, research_id=None, provider=None, research_context=None
514
+ ):
515
+ """Create a wrapper class that processes LLM outputs with remove_think_tags and token counting"""
516
+
517
+ # Import token counting functionality if research_id is provided
518
+ callbacks = []
519
+ if research_id is not None:
520
+ from ..metrics import TokenCounter
521
+
522
+ token_counter = TokenCounter()
523
+ token_callback = token_counter.create_callback(
524
+ research_id, research_context
525
+ )
526
+ # Set provider and model info on the callback
527
+ if provider:
528
+ token_callback.preset_provider = provider
529
+ # Try to extract model name from the LLM instance
530
+ if hasattr(llm, "model_name"):
531
+ token_callback.preset_model = llm.model_name
532
+ elif hasattr(llm, "model"):
533
+ token_callback.preset_model = llm.model
534
+ callbacks.append(token_callback)
535
+
536
+ # Add callbacks to the LLM if it supports them
537
+ if callbacks and hasattr(llm, "callbacks"):
538
+ if llm.callbacks is None:
539
+ llm.callbacks = callbacks
540
+ else:
541
+ llm.callbacks.extend(callbacks)
290
542
 
291
543
  class ProcessingLLMWrapper:
292
544
  def __init__(self, base_llm):
@@ -308,153 +560,3 @@ def wrap_llm_without_think_tags(llm):
308
560
  return getattr(self.base_llm, name)
309
561
 
310
562
  return ProcessingLLMWrapper(llm)
311
-
312
-
313
- def get_available_provider_types():
314
- """Return available model providers"""
315
- providers = {}
316
-
317
- if is_ollama_available():
318
- providers["ollama"] = "Ollama (local models)"
319
-
320
- if is_openai_available():
321
- providers["openai"] = "OpenAI API"
322
-
323
- if is_anthropic_available():
324
- providers["anthropic"] = "Anthropic API"
325
-
326
- if is_openai_endpoint_available():
327
- providers["openai_endpoint"] = "OpenAI-compatible Endpoint"
328
-
329
- if is_lmstudio_available():
330
- providers["lmstudio"] = "LM Studio (local models)"
331
-
332
- if is_llamacpp_available():
333
- providers["llamacpp"] = "LlamaCpp (local models)"
334
-
335
- # Check for VLLM capability
336
- try:
337
- import torch # noqa: F401
338
- import transformers # noqa: F401
339
-
340
- providers["vllm"] = "VLLM (local models)"
341
- except ImportError:
342
- pass
343
-
344
- # Default fallback
345
- if not providers:
346
- providers["none"] = "No model providers available"
347
-
348
- return providers
349
-
350
-
351
- def is_openai_available():
352
- """Check if OpenAI is available"""
353
- try:
354
- api_key = get_db_setting("llm.openai.api_key")
355
- return bool(api_key)
356
- except Exception:
357
- return False
358
-
359
-
360
- def is_anthropic_available():
361
- """Check if Anthropic is available"""
362
- try:
363
- api_key = get_db_setting("llm.anthropic.api_key")
364
- return bool(api_key)
365
- except Exception:
366
- return False
367
-
368
-
369
- def is_openai_endpoint_available():
370
- """Check if OpenAI endpoint is available"""
371
- try:
372
- api_key = get_db_setting("llm.openai_endpoint.api_key")
373
- return bool(api_key)
374
- except Exception:
375
- return False
376
-
377
-
378
- def is_ollama_available():
379
- """Check if Ollama is running"""
380
- try:
381
- import requests
382
-
383
- raw_base_url = get_db_setting("llm.ollama.url", "http://localhost:11434")
384
- base_url = (
385
- normalize_url(raw_base_url) if raw_base_url else "http://localhost:11434"
386
- )
387
- logger.info(f"Checking Ollama availability at {base_url}/api/tags")
388
-
389
- try:
390
- response = requests.get(f"{base_url}/api/tags", timeout=3.0)
391
- if response.status_code == 200:
392
- logger.info(f"Ollama is available. Status code: {response.status_code}")
393
- # Log first 100 chars of response to debug
394
- logger.info(f"Response preview: {str(response.text)[:100]}")
395
- return True
396
- else:
397
- logger.warning(
398
- f"Ollama API returned status code: {response.status_code}"
399
- )
400
- return False
401
- except requests.exceptions.RequestException as req_error:
402
- logger.error(f"Request error when checking Ollama: {str(req_error)}")
403
- return False
404
- except Exception:
405
- logger.exception("Unexpected error when checking Ollama")
406
- return False
407
- except Exception:
408
- logger.exception("Error in is_ollama_available")
409
- return False
410
-
411
-
412
- def is_vllm_available():
413
- """Check if VLLM capability is available"""
414
- try:
415
- import torch # noqa: F401
416
- import transformers # noqa: F401
417
-
418
- return True
419
- except ImportError:
420
- return False
421
-
422
-
423
- def is_lmstudio_available():
424
- """Check if LM Studio is available"""
425
- try:
426
- import requests
427
-
428
- lmstudio_url = get_db_setting("llm.lmstudio.url", "http://localhost:1234")
429
- # LM Studio typically uses OpenAI-compatible endpoints
430
- response = requests.get(f"{lmstudio_url}/v1/models", timeout=1.0)
431
- return response.status_code == 200
432
- except Exception:
433
- return False
434
-
435
-
436
- def is_llamacpp_available():
437
- """Check if LlamaCpp is available and configured"""
438
- try:
439
- from langchain_community.llms import LlamaCpp # noqa: F401
440
-
441
- model_path = get_db_setting("llm.llamacpp_model_path")
442
- return bool(model_path) and os.path.exists(model_path)
443
- except Exception:
444
- return False
445
-
446
-
447
- def get_available_providers():
448
- """Get dictionary of available providers"""
449
- return get_available_provider_types()
450
-
451
-
452
- AVAILABLE_PROVIDERS = get_available_providers()
453
- selected_provider = get_db_setting("llm.provider", "ollama").lower()
454
-
455
- # Log which providers are available
456
- logger.info(f"Available providers: {list(AVAILABLE_PROVIDERS.keys())}")
457
-
458
- # Check if selected provider is available
459
- if selected_provider not in AVAILABLE_PROVIDERS and selected_provider != "none":
460
- logger.warning(f"Selected provider {selected_provider} is not available.")
@@ -2,13 +2,20 @@
2
2
  from loguru import logger
3
3
 
4
4
  from ..utilities.db_utils import get_db_setting
5
- from ..web_search_engines.search_engine_factory import get_search as factory_get_search
5
+ from ..web_search_engines.search_engine_factory import (
6
+ get_search as factory_get_search,
7
+ )
6
8
  from .llm_config import get_llm
7
9
 
8
10
  # Whether to check the quality search results using the LLM.
9
11
  QUALITY_CHECK_DDG_URLS = True
10
- # Whether to only retrieve snippets instead of full search results.
11
- SEARCH_SNIPPETS_ONLY = get_db_setting("search.snippets_only", True)
12
+
13
+
14
+ def get_search_snippets_only_setting():
15
+ """
16
+ Lazily retrieve the 'search.snippets_only' setting.
17
+ """
18
+ return get_db_setting("search.snippets_only", True)
12
19
 
13
20
 
14
21
  # Expose get_search function
@@ -36,9 +43,11 @@ def get_search(search_tool=None, llm_instance=None):
36
43
  "region": get_db_setting("search.region", "wt-wt"),
37
44
  "time_period": get_db_setting("search.time_period", "all"),
38
45
  "safe_search": get_db_setting("search.safe_search", True),
39
- "search_snippets_only": SEARCH_SNIPPETS_ONLY,
46
+ "search_snippets_only": get_search_snippets_only_setting(),
40
47
  "search_language": get_db_setting("search.search_language", "English"),
41
- "max_filtered_results": get_db_setting("search.max_filtered_results", 5),
48
+ "max_filtered_results": get_db_setting(
49
+ "search.max_filtered_results", 5
50
+ ),
42
51
  }
43
52
 
44
53
  # Log NULL parameters for debugging
@@ -6,7 +6,6 @@ configuration files and resources used throughout the application.
6
6
  """
7
7
 
8
8
  import logging
9
- import os
10
9
  from pathlib import Path
11
10
 
12
11
  logger = logging.getLogger(__name__)
@@ -0,0 +1,13 @@
1
+ """Metrics module for tracking LLM usage and token counts."""
2
+
3
+ from .database import get_metrics_db
4
+ from .db_models import ModelUsage, TokenUsage
5
+ from .token_counter import TokenCounter, TokenCountingCallback
6
+
7
+ __all__ = [
8
+ "TokenCounter",
9
+ "TokenCountingCallback",
10
+ "TokenUsage",
11
+ "ModelUsage",
12
+ "get_metrics_db",
13
+ ]
@@ -0,0 +1,58 @@
1
+ """Database utilities for metrics module with SQLAlchemy."""
2
+
3
+ from contextlib import contextmanager
4
+ from typing import Generator
5
+
6
+ from loguru import logger
7
+ from sqlalchemy import create_engine
8
+ from sqlalchemy.orm import Session, sessionmaker
9
+
10
+ from ..utilities.db_utils import DB_PATH
11
+ from .db_models import Base
12
+
13
+
14
+ class MetricsDatabase:
15
+ """Database manager for metrics using SQLAlchemy."""
16
+
17
+ def __init__(self):
18
+ # Use the same database as the rest of the app
19
+ self.engine = create_engine(
20
+ f"sqlite:///{DB_PATH}", connect_args={"check_same_thread": False}
21
+ )
22
+ self.SessionLocal = sessionmaker(
23
+ bind=self.engine, autocommit=False, autoflush=False
24
+ )
25
+ self._init_database()
26
+
27
+ def _init_database(self):
28
+ """Initialize database tables for metrics."""
29
+ try:
30
+ Base.metadata.create_all(self.engine)
31
+ logger.info("Metrics tables initialized successfully")
32
+ except Exception as e:
33
+ logger.exception(f"Error initializing metrics tables: {e}")
34
+
35
+ @contextmanager
36
+ def get_session(self) -> Generator[Session, None, None]:
37
+ """Get a database session with automatic cleanup."""
38
+ session = self.SessionLocal()
39
+ try:
40
+ yield session
41
+ session.commit()
42
+ except Exception:
43
+ session.rollback()
44
+ raise
45
+ finally:
46
+ session.close()
47
+
48
+
49
+ # Singleton instance
50
+ _metrics_db = None
51
+
52
+
53
+ def get_metrics_db() -> MetricsDatabase:
54
+ """Get the singleton metrics database instance."""
55
+ global _metrics_db
56
+ if _metrics_db is None:
57
+ _metrics_db = MetricsDatabase()
58
+ return _metrics_db