local-deep-research 0.1.26__py3-none-any.whl → 0.2.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 (140) hide show
  1. local_deep_research/__init__.py +23 -22
  2. local_deep_research/__main__.py +16 -0
  3. local_deep_research/advanced_search_system/__init__.py +7 -0
  4. local_deep_research/advanced_search_system/filters/__init__.py +8 -0
  5. local_deep_research/advanced_search_system/filters/base_filter.py +38 -0
  6. local_deep_research/advanced_search_system/filters/cross_engine_filter.py +200 -0
  7. local_deep_research/advanced_search_system/findings/base_findings.py +81 -0
  8. local_deep_research/advanced_search_system/findings/repository.py +452 -0
  9. local_deep_research/advanced_search_system/knowledge/__init__.py +1 -0
  10. local_deep_research/advanced_search_system/knowledge/base_knowledge.py +151 -0
  11. local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +159 -0
  12. local_deep_research/advanced_search_system/questions/__init__.py +1 -0
  13. local_deep_research/advanced_search_system/questions/base_question.py +64 -0
  14. local_deep_research/advanced_search_system/questions/decomposition_question.py +445 -0
  15. local_deep_research/advanced_search_system/questions/standard_question.py +119 -0
  16. local_deep_research/advanced_search_system/repositories/__init__.py +7 -0
  17. local_deep_research/advanced_search_system/strategies/__init__.py +1 -0
  18. local_deep_research/advanced_search_system/strategies/base_strategy.py +118 -0
  19. local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +450 -0
  20. local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +312 -0
  21. local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +270 -0
  22. local_deep_research/advanced_search_system/strategies/standard_strategy.py +300 -0
  23. local_deep_research/advanced_search_system/tools/__init__.py +1 -0
  24. local_deep_research/advanced_search_system/tools/base_tool.py +100 -0
  25. local_deep_research/advanced_search_system/tools/knowledge_tools/__init__.py +1 -0
  26. local_deep_research/advanced_search_system/tools/question_tools/__init__.py +1 -0
  27. local_deep_research/advanced_search_system/tools/search_tools/__init__.py +1 -0
  28. local_deep_research/api/__init__.py +5 -5
  29. local_deep_research/api/research_functions.py +154 -160
  30. local_deep_research/app.py +8 -0
  31. local_deep_research/citation_handler.py +25 -16
  32. local_deep_research/{config.py → config/config_files.py} +102 -110
  33. local_deep_research/config/llm_config.py +472 -0
  34. local_deep_research/config/search_config.py +77 -0
  35. local_deep_research/defaults/__init__.py +10 -5
  36. local_deep_research/defaults/main.toml +2 -2
  37. local_deep_research/defaults/search_engines.toml +60 -34
  38. local_deep_research/main.py +121 -19
  39. local_deep_research/migrate_db.py +147 -0
  40. local_deep_research/report_generator.py +87 -45
  41. local_deep_research/search_system.py +153 -283
  42. local_deep_research/setup_data_dir.py +35 -0
  43. local_deep_research/test_migration.py +178 -0
  44. local_deep_research/utilities/__init__.py +0 -0
  45. local_deep_research/utilities/db_utils.py +49 -0
  46. local_deep_research/{utilties → utilities}/enums.py +2 -2
  47. local_deep_research/{utilties → utilities}/llm_utils.py +63 -29
  48. local_deep_research/utilities/search_utilities.py +242 -0
  49. local_deep_research/{utilties → utilities}/setup_utils.py +4 -2
  50. local_deep_research/web/__init__.py +0 -1
  51. local_deep_research/web/app.py +86 -1709
  52. local_deep_research/web/app_factory.py +289 -0
  53. local_deep_research/web/database/README.md +70 -0
  54. local_deep_research/web/database/migrate_to_ldr_db.py +289 -0
  55. local_deep_research/web/database/migrations.py +447 -0
  56. local_deep_research/web/database/models.py +117 -0
  57. local_deep_research/web/database/schema_upgrade.py +107 -0
  58. local_deep_research/web/models/database.py +294 -0
  59. local_deep_research/web/models/settings.py +94 -0
  60. local_deep_research/web/routes/api_routes.py +559 -0
  61. local_deep_research/web/routes/history_routes.py +354 -0
  62. local_deep_research/web/routes/research_routes.py +715 -0
  63. local_deep_research/web/routes/settings_routes.py +1583 -0
  64. local_deep_research/web/services/research_service.py +947 -0
  65. local_deep_research/web/services/resource_service.py +149 -0
  66. local_deep_research/web/services/settings_manager.py +669 -0
  67. local_deep_research/web/services/settings_service.py +187 -0
  68. local_deep_research/web/services/socket_service.py +210 -0
  69. local_deep_research/web/static/css/custom_dropdown.css +277 -0
  70. local_deep_research/web/static/css/settings.css +1223 -0
  71. local_deep_research/web/static/css/styles.css +525 -48
  72. local_deep_research/web/static/js/components/custom_dropdown.js +428 -0
  73. local_deep_research/web/static/js/components/detail.js +348 -0
  74. local_deep_research/web/static/js/components/fallback/formatting.js +122 -0
  75. local_deep_research/web/static/js/components/fallback/ui.js +215 -0
  76. local_deep_research/web/static/js/components/history.js +487 -0
  77. local_deep_research/web/static/js/components/logpanel.js +949 -0
  78. local_deep_research/web/static/js/components/progress.js +1107 -0
  79. local_deep_research/web/static/js/components/research.js +1865 -0
  80. local_deep_research/web/static/js/components/results.js +766 -0
  81. local_deep_research/web/static/js/components/settings.js +3981 -0
  82. local_deep_research/web/static/js/components/settings_sync.js +106 -0
  83. local_deep_research/web/static/js/main.js +226 -0
  84. local_deep_research/web/static/js/services/api.js +253 -0
  85. local_deep_research/web/static/js/services/audio.js +31 -0
  86. local_deep_research/web/static/js/services/formatting.js +119 -0
  87. local_deep_research/web/static/js/services/pdf.js +622 -0
  88. local_deep_research/web/static/js/services/socket.js +882 -0
  89. local_deep_research/web/static/js/services/ui.js +546 -0
  90. local_deep_research/web/templates/base.html +72 -0
  91. local_deep_research/web/templates/components/custom_dropdown.html +47 -0
  92. local_deep_research/web/templates/components/log_panel.html +32 -0
  93. local_deep_research/web/templates/components/mobile_nav.html +22 -0
  94. local_deep_research/web/templates/components/settings_form.html +299 -0
  95. local_deep_research/web/templates/components/sidebar.html +21 -0
  96. local_deep_research/web/templates/pages/details.html +73 -0
  97. local_deep_research/web/templates/pages/history.html +51 -0
  98. local_deep_research/web/templates/pages/progress.html +57 -0
  99. local_deep_research/web/templates/pages/research.html +139 -0
  100. local_deep_research/web/templates/pages/results.html +59 -0
  101. local_deep_research/web/templates/settings_dashboard.html +78 -192
  102. local_deep_research/web/utils/__init__.py +0 -0
  103. local_deep_research/web/utils/formatters.py +76 -0
  104. local_deep_research/web_search_engines/engines/full_search.py +18 -16
  105. local_deep_research/web_search_engines/engines/meta_search_engine.py +182 -131
  106. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +224 -139
  107. local_deep_research/web_search_engines/engines/search_engine_brave.py +88 -71
  108. local_deep_research/web_search_engines/engines/search_engine_ddg.py +48 -39
  109. local_deep_research/web_search_engines/engines/search_engine_github.py +415 -204
  110. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +123 -90
  111. local_deep_research/web_search_engines/engines/search_engine_guardian.py +210 -157
  112. local_deep_research/web_search_engines/engines/search_engine_local.py +532 -369
  113. local_deep_research/web_search_engines/engines/search_engine_local_all.py +42 -36
  114. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +358 -266
  115. local_deep_research/web_search_engines/engines/search_engine_searxng.py +212 -160
  116. local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +213 -170
  117. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +84 -68
  118. local_deep_research/web_search_engines/engines/search_engine_wayback.py +186 -154
  119. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +115 -77
  120. local_deep_research/web_search_engines/search_engine_base.py +174 -99
  121. local_deep_research/web_search_engines/search_engine_factory.py +192 -102
  122. local_deep_research/web_search_engines/search_engines_config.py +22 -15
  123. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/METADATA +177 -97
  124. local_deep_research-0.2.2.dist-info/RECORD +135 -0
  125. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/WHEEL +1 -2
  126. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/entry_points.txt +3 -0
  127. local_deep_research/defaults/llm_config.py +0 -338
  128. local_deep_research/utilties/search_utilities.py +0 -114
  129. local_deep_research/web/static/js/app.js +0 -3763
  130. local_deep_research/web/templates/api_keys_config.html +0 -82
  131. local_deep_research/web/templates/collections_config.html +0 -90
  132. local_deep_research/web/templates/index.html +0 -348
  133. local_deep_research/web/templates/llm_config.html +0 -120
  134. local_deep_research/web/templates/main_config.html +0 -89
  135. local_deep_research/web/templates/search_engines_config.html +0 -154
  136. local_deep_research/web/templates/settings.html +0 -519
  137. local_deep_research-0.1.26.dist-info/RECORD +0 -61
  138. local_deep_research-0.1.26.dist-info/top_level.txt +0 -1
  139. /local_deep_research/{utilties → config}/__init__.py +0 -0
  140. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,472 @@
1
+ import logging
2
+ import os
3
+ from pathlib import Path
4
+
5
+ from dynaconf.vendor.box.exceptions import BoxKeyError
6
+ from langchain_anthropic import ChatAnthropic
7
+ from langchain_community.llms import VLLM
8
+ from langchain_ollama import ChatOllama
9
+ from langchain_openai import ChatOpenAI
10
+
11
+ from ..utilities.db_utils import get_db_setting
12
+ from ..utilities.search_utilities import remove_think_tags
13
+ from .config_files import CONFIG_DIR, settings
14
+
15
+ # Setup logging
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Valid provider options
19
+ VALID_PROVIDERS = [
20
+ "ollama",
21
+ "openai",
22
+ "anthropic",
23
+ "vllm",
24
+ "openai_endpoint",
25
+ "lmstudio",
26
+ "llamacpp",
27
+ "none",
28
+ ]
29
+ SECRETS_FILE = CONFIG_DIR / ".secrets.toml"
30
+
31
+
32
+ def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_url=None):
33
+ """
34
+ Get LLM instance based on model name and provider.
35
+
36
+ Args:
37
+ model_name: Name of the model to use (if None, uses database setting)
38
+ temperature: Model temperature (if None, uses database setting)
39
+ provider: Provider to use (if None, uses database setting)
40
+ openai_endpoint_url: Custom endpoint URL to use (if None, uses database
41
+ setting)
42
+
43
+ Returns:
44
+ A LangChain LLM instance with automatic think-tag removal
45
+ """
46
+
47
+ # Use database values for parameters if not provided
48
+ if model_name is None:
49
+ model_name = get_db_setting("llm.model", settings.llm.model)
50
+ if temperature is None:
51
+ temperature = get_db_setting("llm.temperature", settings.llm.temperature)
52
+ if provider is None:
53
+ provider = get_db_setting("llm.provider", settings.llm.provider)
54
+
55
+ # Clean model name: remove quotes and extra whitespace
56
+ if model_name:
57
+ model_name = model_name.strip().strip("\"'").strip()
58
+
59
+ # Clean provider: remove quotes and extra whitespace
60
+ if provider:
61
+ provider = provider.strip().strip("\"'").strip()
62
+
63
+ # Normalize provider: convert to lowercase
64
+ provider = provider.lower() if provider else None
65
+
66
+ # Validate provider
67
+ if provider not in VALID_PROVIDERS:
68
+ logger.error(f"Invalid provider in settings: {provider}")
69
+ raise ValueError(
70
+ f"Invalid provider: {provider}. Must be one of: {VALID_PROVIDERS}"
71
+ )
72
+ print(
73
+ f"Getting LLM with model: {model_name}, temperature: {temperature}, provider: {provider}"
74
+ )
75
+
76
+ # Common parameters for all models
77
+ common_params = {
78
+ "temperature": temperature,
79
+ }
80
+ try:
81
+ common_params["max_tokens"] = settings.llm.max_tokens
82
+ except BoxKeyError:
83
+ # Some providers don't support this parameter, in which case it can
84
+ # be omitted.
85
+ pass
86
+
87
+ # Handle different providers
88
+ if provider == "anthropic":
89
+ api_key_name = "ANTHROPIC_API_KEY"
90
+ api_key = settings.get(api_key_name, "")
91
+ if not api_key:
92
+ api_key = os.getenv(api_key_name)
93
+ if not api_key:
94
+ api_key = os.getenv("LDR_" + api_key_name)
95
+ if not api_key:
96
+ logger.warning(
97
+ "ANTHROPIC_API_KEY not found. Falling back to default model."
98
+ )
99
+ return get_fallback_model(temperature)
100
+
101
+ llm = ChatAnthropic(
102
+ model=model_name, anthropic_api_key=api_key, **common_params
103
+ )
104
+ return wrap_llm_without_think_tags(llm)
105
+
106
+ elif provider == "openai":
107
+ api_key_name = "OPENAI_API_KEY"
108
+ api_key = settings.get(api_key_name, "")
109
+ if not api_key:
110
+ api_key = os.getenv(api_key_name)
111
+ if not api_key:
112
+ api_key = os.getenv("LDR_" + api_key_name)
113
+ if not api_key:
114
+ logger.warning("OPENAI_API_KEY not found. Falling back to default model.")
115
+ return get_fallback_model(temperature)
116
+
117
+ llm = ChatOpenAI(model=model_name, api_key=api_key, **common_params)
118
+ return wrap_llm_without_think_tags(llm)
119
+
120
+ elif provider == "openai_endpoint":
121
+ api_key_name = "OPENAI_ENDPOINT_API_KEY"
122
+ api_key = settings.get(api_key_name, "")
123
+ if not api_key:
124
+ api_key = os.getenv(api_key_name)
125
+ if not api_key:
126
+ api_key = os.getenv("LDR_" + api_key_name)
127
+ if not api_key:
128
+ logger.warning(
129
+ "OPENAI_ENDPOINT_API_KEY not found. Falling back to default model."
130
+ )
131
+ return get_fallback_model(temperature)
132
+
133
+ # Get endpoint URL from settings
134
+ if openai_endpoint_url is not None:
135
+ openai_endpoint_url = get_db_setting(
136
+ "llm.openai_endpoint_url", settings.llm.openai_endpoint_url
137
+ )
138
+
139
+ llm = ChatOpenAI(
140
+ model=model_name,
141
+ api_key=api_key,
142
+ openai_api_base=openai_endpoint_url,
143
+ **common_params,
144
+ )
145
+ return wrap_llm_without_think_tags(llm)
146
+
147
+ elif provider == "vllm":
148
+ try:
149
+ llm = VLLM(
150
+ model=model_name,
151
+ trust_remote_code=True,
152
+ max_new_tokens=128,
153
+ top_k=10,
154
+ top_p=0.95,
155
+ temperature=temperature,
156
+ )
157
+ return wrap_llm_without_think_tags(llm)
158
+ except Exception as e:
159
+ logger.error(f"Error loading VLLM model: {e}")
160
+ logger.warning("Falling back.")
161
+ return get_fallback_model(temperature)
162
+
163
+ elif provider == "ollama":
164
+ try:
165
+ # Use the configurable Ollama base URL
166
+ base_url = os.getenv(
167
+ "OLLAMA_BASE_URL",
168
+ settings.llm.get("ollama_base_url", "http://localhost:11434"),
169
+ )
170
+
171
+ # Check if Ollama is available before trying to use it
172
+ if not is_ollama_available():
173
+ logger.error(
174
+ f"Ollama not available at {base_url}. Falling back to dummy model."
175
+ )
176
+ return get_fallback_model(temperature)
177
+
178
+ # Check if the requested model exists
179
+ import requests
180
+
181
+ try:
182
+ logger.info(f"Checking if model '{model_name}' exists in Ollama")
183
+ response = requests.get(f"{base_url}/api/tags", timeout=3.0)
184
+ if response.status_code == 200:
185
+ # Handle both newer and older Ollama API formats
186
+ data = response.json()
187
+ models = []
188
+ if "models" in data:
189
+ # Newer Ollama API
190
+ models = data.get("models", [])
191
+ else:
192
+ # Older Ollama API format
193
+ models = data
194
+
195
+ # Get list of model names
196
+ model_names = [m.get("name", "").lower() for m in models]
197
+ logger.info(
198
+ f"Available Ollama models: {', '.join(model_names[:5])}{' and more' if len(model_names) > 5 else ''}"
199
+ )
200
+
201
+ if model_name.lower() not in model_names:
202
+ logger.error(
203
+ f"Model '{model_name}' not found in Ollama. Available models: {', '.join(model_names[:5])}"
204
+ )
205
+ return get_fallback_model(temperature)
206
+ except Exception as model_check_error:
207
+ logger.error(
208
+ f"Error checking for model '{model_name}' in Ollama: {str(model_check_error)}"
209
+ )
210
+ # Continue anyway, let ChatOllama handle potential errors
211
+
212
+ logger.info(
213
+ f"Creating ChatOllama with model={model_name}, base_url={base_url}"
214
+ )
215
+ try:
216
+ llm = ChatOllama(model=model_name, base_url=base_url, **common_params)
217
+ # Test invoke to validate model works
218
+ logger.info("Testing Ollama model with simple invocation")
219
+ test_result = llm.invoke("Hello")
220
+ logger.info(
221
+ f"Ollama test successful. Response type: {type(test_result)}"
222
+ )
223
+ return wrap_llm_without_think_tags(llm)
224
+ except Exception as chat_error:
225
+ logger.error(f"Error creating or testing ChatOllama: {str(chat_error)}")
226
+ return get_fallback_model(temperature)
227
+ except Exception as e:
228
+ logger.error(f"Error in Ollama provider section: {str(e)}")
229
+ return get_fallback_model(temperature)
230
+
231
+ elif provider == "lmstudio":
232
+ # LM Studio supports OpenAI API format, so we can use ChatOpenAI directly
233
+ lmstudio_url = settings.llm.get("lmstudio_url", "http://localhost:1234")
234
+ lmstudio_url = get_db_setting("llm.lmstudio_url", lmstudio_url)
235
+
236
+ llm = ChatOpenAI(
237
+ model=model_name,
238
+ api_key="lm-studio", # LM Studio doesn't require a real API key
239
+ base_url=f"{lmstudio_url}/v1", # Use the configured URL with /v1 endpoint
240
+ temperature=temperature,
241
+ max_tokens=get_db_setting("llm.max_tokens", settings.llm.max_tokens),
242
+ )
243
+ return wrap_llm_without_think_tags(llm)
244
+
245
+ elif provider == "llamacpp":
246
+ # Import LlamaCpp
247
+ from langchain_community.llms import LlamaCpp
248
+
249
+ # Get LlamaCpp model path from settings
250
+ model_path = settings.llm.get("llamacpp_model_path", "")
251
+ model_path = get_db_setting("llm.llamacpp_model_path", model_path)
252
+ if not model_path:
253
+ logger.error("llamacpp_model_path not set in settings")
254
+ raise ValueError("llamacpp_model_path not set in settings")
255
+
256
+ # Get additional LlamaCpp parameters
257
+ n_gpu_layers = settings.llm.get("llamacpp_n_gpu_layers", 1)
258
+ n_gpu_layers = get_db_setting("llm.llamacpp_n_gpu_layers", n_gpu_layers)
259
+ n_batch = settings.llm.get("llamacpp_n_batch", 512)
260
+ n_batch = get_db_setting("llm.llamacpp_n_batch", n_batch)
261
+ f16_kv = settings.llm.get("llamacpp_f16_kv", True)
262
+ f16_kv = get_db_setting("llm.llamacpp_f16_kv", f16_kv)
263
+
264
+ # Create LlamaCpp instance
265
+ llm = LlamaCpp(
266
+ model_path=model_path,
267
+ temperature=temperature,
268
+ max_tokens=get_db_setting("llm.max_tokens", settings.llm.max_tokens),
269
+ n_gpu_layers=n_gpu_layers,
270
+ n_batch=n_batch,
271
+ f16_kv=f16_kv,
272
+ verbose=True,
273
+ )
274
+ return wrap_llm_without_think_tags(llm)
275
+
276
+ else:
277
+ return wrap_llm_without_think_tags(get_fallback_model(temperature))
278
+
279
+
280
+ def get_fallback_model(temperature=None):
281
+ """Create a dummy model for when no providers are available"""
282
+ from langchain_community.llms.fake import FakeListLLM
283
+
284
+ return FakeListLLM(
285
+ responses=[
286
+ "No language models are available. Please install Ollama or set up API keys."
287
+ ]
288
+ )
289
+
290
+
291
+ def wrap_llm_without_think_tags(llm):
292
+ """Create a wrapper class that processes LLM outputs with remove_think_tags"""
293
+
294
+ class ProcessingLLMWrapper:
295
+ def __init__(self, base_llm):
296
+ self.base_llm = base_llm
297
+
298
+ def invoke(self, *args, **kwargs):
299
+ response = self.base_llm.invoke(*args, **kwargs)
300
+
301
+ # Process the response content if it has a content attribute
302
+ if hasattr(response, "content"):
303
+ response.content = remove_think_tags(response.content)
304
+ elif isinstance(response, str):
305
+ response = remove_think_tags(response)
306
+
307
+ return response
308
+
309
+ # Pass through any other attributes to the base LLM
310
+ def __getattr__(self, name):
311
+ return getattr(self.base_llm, name)
312
+
313
+ return ProcessingLLMWrapper(llm)
314
+
315
+
316
+ def get_available_provider_types():
317
+ """Return available model providers"""
318
+ providers = {}
319
+
320
+ if is_ollama_available():
321
+ providers["ollama"] = "Ollama (local models)"
322
+
323
+ if is_openai_available():
324
+ providers["openai"] = "OpenAI API"
325
+
326
+ if is_anthropic_available():
327
+ providers["anthropic"] = "Anthropic API"
328
+
329
+ if is_openai_endpoint_available():
330
+ providers["openai_endpoint"] = "OpenAI-compatible Endpoint"
331
+
332
+ if is_lmstudio_available():
333
+ providers["lmstudio"] = "LM Studio (local models)"
334
+
335
+ if is_llamacpp_available():
336
+ providers["llamacpp"] = "LlamaCpp (local models)"
337
+
338
+ # Check for VLLM capability
339
+ try:
340
+ import torch # noqa: F401
341
+ import transformers # noqa: F401
342
+
343
+ providers["vllm"] = "VLLM (local models)"
344
+ except ImportError:
345
+ pass
346
+
347
+ # Default fallback
348
+ if not providers:
349
+ providers["none"] = "No model providers available"
350
+
351
+ return providers
352
+
353
+
354
+ def is_openai_available():
355
+ """Check if OpenAI is available"""
356
+ try:
357
+ api_key = settings.get("OPENAI_API_KEY", "")
358
+ if not api_key:
359
+ api_key = os.getenv("OPENAI_API_KEY")
360
+ return bool(api_key)
361
+ except Exception:
362
+ return False
363
+
364
+
365
+ def is_anthropic_available():
366
+ """Check if Anthropic is available"""
367
+ try:
368
+ api_key = settings.get("ANTHROPIC_API_KEY", "")
369
+ if not api_key:
370
+ api_key = os.getenv("ANTHROPIC_API_KEY")
371
+ return bool(api_key)
372
+ except Exception:
373
+ return False
374
+
375
+
376
+ def is_openai_endpoint_available():
377
+ """Check if OpenAI endpoint is available"""
378
+ try:
379
+ api_key = settings.get("OPENAI_ENDPOINT_API_KEY", "")
380
+ if not api_key:
381
+ api_key = os.getenv("OPENAI_ENDPOINT_API_KEY")
382
+ return bool(api_key)
383
+ except Exception:
384
+ return False
385
+
386
+
387
+ def is_ollama_available():
388
+ """Check if Ollama is running"""
389
+ try:
390
+ import requests
391
+
392
+ base_url = os.getenv(
393
+ "OLLAMA_BASE_URL",
394
+ settings.llm.get("ollama_base_url", "http://localhost:11434"),
395
+ )
396
+ logger.info(f"Checking Ollama availability at {base_url}/api/tags")
397
+
398
+ try:
399
+ response = requests.get(f"{base_url}/api/tags", timeout=3.0)
400
+ if response.status_code == 200:
401
+ logger.info(f"Ollama is available. Status code: {response.status_code}")
402
+ # Log first 100 chars of response to debug
403
+ logger.info(f"Response preview: {str(response.text)[:100]}")
404
+ return True
405
+ else:
406
+ logger.warning(
407
+ f"Ollama API returned status code: {response.status_code}"
408
+ )
409
+ return False
410
+ except requests.exceptions.RequestException as req_error:
411
+ logger.error(f"Request error when checking Ollama: {str(req_error)}")
412
+ return False
413
+ except Exception as e:
414
+ logger.error(f"Unexpected error when checking Ollama: {str(e)}")
415
+ return False
416
+ except Exception as outer_e:
417
+ logger.error(f"Error in is_ollama_available: {str(outer_e)}")
418
+ return False
419
+
420
+
421
+ def is_vllm_available():
422
+ """Check if VLLM capability is available"""
423
+ try:
424
+ import torch # noqa: F401
425
+ import transformers # noqa: F401
426
+
427
+ return True
428
+ except ImportError:
429
+ return False
430
+
431
+
432
+ def is_lmstudio_available():
433
+ """Check if LM Studio is available"""
434
+ try:
435
+ import requests
436
+
437
+ lmstudio_url = settings.llm.get("lmstudio_url", "http://localhost:1234")
438
+ lmstudio_url = get_db_setting("llm.lmstudio_url", lmstudio_url)
439
+ # LM Studio typically uses OpenAI-compatible endpoints
440
+ response = requests.get(f"{lmstudio_url}/v1/models", timeout=1.0)
441
+ return response.status_code == 200
442
+ except Exception:
443
+ return False
444
+
445
+
446
+ def is_llamacpp_available():
447
+ """Check if LlamaCpp is available and configured"""
448
+ try:
449
+ from langchain_community.llms import LlamaCpp # noqa: F401
450
+
451
+ model_path = settings.llm.get("llamacpp_model_path", "")
452
+ model_path = get_db_setting("llm.llamacpp_model_path", model_path)
453
+ return bool(model_path) and os.path.exists(model_path)
454
+ except Exception:
455
+ return False
456
+
457
+
458
+ def get_available_providers():
459
+ """Get dictionary of available providers"""
460
+ return get_available_provider_types()
461
+
462
+
463
+ secrets_file = Path(SECRETS_FILE)
464
+ AVAILABLE_PROVIDERS = get_available_providers()
465
+ selected_provider = get_db_setting("llm.provider", settings.llm.provider).lower()
466
+
467
+ # Log which providers are available
468
+ logger.info(f"Available providers: {list(AVAILABLE_PROVIDERS.keys())}")
469
+
470
+ # Check if selected provider is available
471
+ if selected_provider not in AVAILABLE_PROVIDERS and selected_provider != "none":
472
+ logger.warning(f"Selected provider {selected_provider} is not available.")
@@ -0,0 +1,77 @@
1
+ # local_deep_research/config.py
2
+ import logging
3
+
4
+ from ..utilities.db_utils import get_db_setting
5
+ from ..web_search_engines.search_engine_factory import get_search as factory_get_search
6
+ from .config_files import settings
7
+ from .llm_config import get_llm
8
+
9
+ # Setup logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ # Whether to check the quality search results using the LLM.
14
+ QUALITY_CHECK_DDG_URLS = True
15
+ # Whether to only retrieve snippets instead of full search results.
16
+ SEARCH_SNIPPETS_ONLY = get_db_setting(
17
+ "search.snippets_only", settings.search.snippets_only
18
+ )
19
+
20
+
21
+ # Expose get_search function
22
+ def get_search(search_tool=None, llm_instance=None):
23
+ """
24
+ Helper function to get search engine
25
+
26
+ Args:
27
+ search_tool: Override the search tool setting (e.g. searxng, wikipedia)
28
+ llm_instance: Override the LLM instance
29
+ """
30
+
31
+ # Use specified tool or default from settings
32
+ tool = search_tool or get_db_setting("search.tool", settings.search.tool)
33
+ logger.info(f"Creating search engine with tool: {tool}")
34
+
35
+ # Get LLM instance (use provided or get fresh one)
36
+ llm = llm_instance or get_llm()
37
+
38
+ # Get search parameters
39
+ params = {
40
+ "search_tool": tool,
41
+ "llm_instance": llm,
42
+ "max_results": get_db_setting(
43
+ "search.max_results", settings.search.max_results
44
+ ),
45
+ "region": get_db_setting("search.region", settings.search.region),
46
+ "time_period": get_db_setting(
47
+ "search.time_period", settings.search.time_period
48
+ ),
49
+ "safe_search": get_db_setting(
50
+ "search.safe_search", settings.search.safe_search
51
+ ),
52
+ "search_snippets_only": SEARCH_SNIPPETS_ONLY,
53
+ "search_language": get_db_setting(
54
+ "search.search_language", settings.search.search_language
55
+ ),
56
+ "max_filtered_results": get_db_setting(
57
+ "search.max_filtered_results", settings.search.max_filtered_results
58
+ ),
59
+ }
60
+
61
+ # Log NULL parameters for debugging
62
+ logger.info(
63
+ f"Search config: tool={tool}, max_results={params['max_results']}, time_period={params['time_period']}"
64
+ )
65
+
66
+ # Create search engine
67
+ search_engine = factory_get_search(**params)
68
+
69
+ # Log the created engine type
70
+ if search_engine:
71
+ logger.info(
72
+ f"Successfully created search engine of type: {type(search_engine).__name__}"
73
+ )
74
+ else:
75
+ logger.error(f"Failed to create search engine for tool: {tool}")
76
+
77
+ return search_engine
@@ -4,8 +4,9 @@ Default configuration module for Local Deep Research.
4
4
  This module is responsible for loading and initializing default
5
5
  configuration files and resources used throughout the application.
6
6
  """
7
- import os
7
+
8
8
  import logging
9
+ import os
9
10
  from pathlib import Path
10
11
 
11
12
  logger = logging.getLogger(__name__)
@@ -18,27 +19,31 @@ DEFAULT_FILES = {
18
19
  "main.toml": DEFAULTS_DIR / "main.toml",
19
20
  "local_collections.toml": DEFAULTS_DIR / "local_collections.toml",
20
21
  "search_engines.toml": DEFAULTS_DIR / "search_engines.toml",
21
- "llm_config.py": DEFAULTS_DIR / "llm_config.py"
22
22
  }
23
23
 
24
+
24
25
  def get_default_file_path(filename):
25
26
  """Get the path to a default configuration file."""
26
27
  if filename in DEFAULT_FILES:
27
28
  return DEFAULT_FILES[filename]
28
29
  return None
29
30
 
31
+
30
32
  def list_default_files():
31
33
  """List all available default configuration files."""
32
34
  return list(DEFAULT_FILES.keys())
33
35
 
36
+
34
37
  def ensure_defaults_exist():
35
38
  """Verify that all expected default files exist in the package."""
36
39
  missing = []
37
40
  for filename, filepath in DEFAULT_FILES.items():
38
41
  if not filepath.exists():
39
42
  missing.append(filename)
40
-
43
+
41
44
  if missing:
42
- logger.warning(f"The following default files are missing from the package: {', '.join(missing)}")
45
+ logger.warning(
46
+ f"The following default files are missing from the package: {', '.join(missing)}"
47
+ )
43
48
  return False
44
- return True
49
+ return True
@@ -38,7 +38,7 @@ enable_fact_checking = false
38
38
 
39
39
 
40
40
  [search]
41
- # Search tool to use (auto, wikipedia, arxiv, duckduckgo, serp, google_pse, etc.)
41
+ # Search tool to use (auto, wikipedia, arxiv, duckduckgo, serpapi, google_pse,etc.)
42
42
  # "auto" intelligently selects based on query content (recommended)
43
43
  # "local_all" searches only local document collections
44
44
  tool = "auto"
@@ -71,7 +71,7 @@ safe_search = true
71
71
  search_language = "English"
72
72
 
73
73
  # Return only snippets, not full content (faster but less detailed)
74
- snippets_only = false
74
+ snippets_only = true
75
75
 
76
76
  # Skip relevance filtering (return all results)
77
77
  skip_relevance_filter = false