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
@@ -1,338 +0,0 @@
1
- """
2
- LLM configuration for Local Deep Research.
3
-
4
- This file controls which language models are available and how they're configured.
5
- You can customize model selection, parameters, and fallbacks here.
6
- """
7
-
8
- from langchain_anthropic import ChatAnthropic
9
- from langchain_openai import ChatOpenAI
10
- from langchain_ollama import ChatOllama
11
- from langchain_community.llms import VLLM
12
- from local_deep_research.utilties.search_utilities import remove_think_tags
13
- from local_deep_research.config import settings
14
- import os
15
- import logging
16
-
17
- # Initialize environment
18
- logger = logging.getLogger(__name__)
19
-
20
- # Valid provider options
21
- VALID_PROVIDERS = ["ollama", "openai", "anthropic", "vllm", "openai_endpoint", "lmstudio", "llamacpp", "none"]
22
-
23
- # ================================
24
- # LLM FUNCTIONS
25
- # ================================
26
-
27
-
28
-
29
- def get_llm(model_name=None, temperature=None, provider=None):
30
- """
31
- Get LLM instance based on model name and provider.
32
-
33
- Args:
34
- model_name: Name of the model to use (if None, uses settings.llm.model)
35
- temperature: Model temperature (if None, uses settings.llm.temperature)
36
- provider: Provider to use (if None, uses settings.llm.provider)
37
-
38
- Returns:
39
- A LangChain LLM instance with automatic think-tag removal
40
- """
41
- # Use settings values for parameters if not provided
42
- if model_name is None:
43
- model_name = settings.llm.model
44
-
45
- if temperature is None:
46
- temperature = settings.llm.temperature
47
-
48
- if provider is None:
49
- provider = settings.llm.provider.lower()
50
- if provider not in VALID_PROVIDERS:
51
- logger.error(f"Invalid provider in settings: {provider}")
52
- raise ValueError(f"Invalid provider: {provider}. Must be one of: {VALID_PROVIDERS}")
53
-
54
- # Common parameters for all models
55
- common_params = {
56
- "temperature": temperature,
57
- "max_tokens": settings.llm.max_tokens,
58
- }
59
-
60
- # Handle different providers
61
- if provider == "anthropic":
62
- api_key_name = 'ANTHROPIC_API_KEY'
63
- api_key = settings.get(api_key_name, '')
64
- if not api_key:
65
- api_key = os.getenv(api_key_name)
66
- if not api_key:
67
- api_key = os.getenv("LDR_" + api_key_name)
68
- if not api_key:
69
- logger.warning("ANTHROPIC_API_KEY not found. Falling back to default model.")
70
- return get_fallback_model(temperature)
71
-
72
- llm = ChatAnthropic(
73
- model=model_name, anthropic_api_key=api_key, **common_params
74
- )
75
- return wrap_llm_without_think_tags(llm)
76
-
77
- elif provider == "openai":
78
- api_key_name = 'OPENAI_API_KEY'
79
- api_key = settings.get(api_key_name, '')
80
- if not api_key:
81
- api_key = os.getenv(api_key_name)
82
- if not api_key:
83
- api_key = os.getenv("LDR_" + api_key_name)
84
- if not api_key:
85
- logger.warning("OPENAI_API_KEY not found. Falling back to default model.")
86
- return get_fallback_model(temperature)
87
-
88
- llm = ChatOpenAI(model=model_name, api_key=api_key, **common_params)
89
- return wrap_llm_without_think_tags(llm)
90
-
91
- elif provider == "openai_endpoint":
92
- api_key_name = 'OPENAI_ENDPOINT_API_KEY'
93
- api_key = settings.get(api_key_name, '')
94
- if not api_key:
95
- api_key = os.getenv(api_key_name)
96
- if not api_key:
97
- api_key = os.getenv("LDR_" + api_key_name)
98
- if not api_key:
99
- logger.warning("OPENAI_ENDPOINT_API_KEY not found. Falling back to default model.")
100
- return get_fallback_model(temperature)
101
-
102
- # Get endpoint URL from settings
103
- openai_endpoint_url = settings.llm.openai_endpoint_url
104
-
105
- llm = ChatOpenAI(
106
- model=model_name,
107
- api_key=api_key,
108
- openai_api_base=openai_endpoint_url,
109
- **common_params
110
- )
111
- return wrap_llm_without_think_tags(llm)
112
-
113
- elif provider == "vllm":
114
- try:
115
- llm = VLLM(
116
- model=model_name,
117
- trust_remote_code=True,
118
- max_new_tokens=128,
119
- top_k=10,
120
- top_p=0.95,
121
- temperature=temperature,
122
- )
123
- return wrap_llm_without_think_tags(llm)
124
- except Exception as e:
125
- logger.error(f"Error loading VLLM model: {e}")
126
- logger.warning("Falling back.")
127
- return get_fallback_model(temperature)
128
-
129
- elif provider == "ollama":
130
- try:
131
- # Use the configurable Ollama base URL
132
- base_url = settings.get('OLLAMA_BASE_URL', settings.llm.get('ollama_base_url', 'http://localhost:11434'))
133
- llm = ChatOllama(model=model_name, base_url=base_url, **common_params)
134
- return wrap_llm_without_think_tags(llm)
135
- except Exception as e:
136
- logger.error(f"Error loading Ollama model: {e}")
137
- return get_fallback_model(temperature)
138
-
139
- elif provider == "lmstudio":
140
- # LM Studio supports OpenAI API format, so we can use ChatOpenAI directly
141
- lmstudio_url = settings.llm.get('lmstudio_url', "http://localhost:1234")
142
-
143
- llm = ChatOpenAI(
144
- model=model_name,
145
- api_key="lm-studio", # LM Studio doesn't require a real API key
146
- base_url=f"{lmstudio_url}/v1", # Use the configured URL with /v1 endpoint
147
- temperature=temperature,
148
- max_tokens=settings.llm.max_tokens
149
- )
150
- return wrap_llm_without_think_tags(llm)
151
-
152
- elif provider == "llamacpp":
153
- # Import LlamaCpp
154
- from langchain_community.llms import LlamaCpp
155
-
156
- # Get LlamaCpp model path from settings
157
- model_path = settings.llm.get('llamacpp_model_path', "")
158
- if not model_path:
159
- logger.error("llamacpp_model_path not set in settings")
160
- raise ValueError("llamacpp_model_path not set in settings.toml")
161
-
162
- # Get additional LlamaCpp parameters
163
- n_gpu_layers = settings.llm.get('llamacpp_n_gpu_layers', 1)
164
- n_batch = settings.llm.get('llamacpp_n_batch', 512)
165
- f16_kv = settings.llm.get('llamacpp_f16_kv', True)
166
-
167
- # Create LlamaCpp instance
168
- llm = LlamaCpp(
169
- model_path=model_path,
170
- temperature=temperature,
171
- max_tokens=settings.llm.max_tokens,
172
- n_gpu_layers=n_gpu_layers,
173
- n_batch=n_batch,
174
- f16_kv=f16_kv,
175
- verbose=True
176
- )
177
- return wrap_llm_without_think_tags(llm)
178
-
179
- else:
180
- return wrap_llm_without_think_tags(get_fallback_model(temperature))
181
-
182
- def get_fallback_model(temperature=None):
183
- """Create a dummy model for when no providers are available"""
184
- from langchain_community.llms.fake import FakeListLLM
185
- return FakeListLLM(
186
- responses=["No language models are available. Please install Ollama or set up API keys."]
187
- )
188
-
189
- # ================================
190
- # COMPATIBILITY FUNCTIONS
191
- # ================================
192
-
193
- def wrap_llm_without_think_tags(llm):
194
- """Create a wrapper class that processes LLM outputs with remove_think_tags"""
195
-
196
-
197
- class ProcessingLLMWrapper:
198
- def __init__(self, base_llm):
199
- self.base_llm = base_llm
200
-
201
- def invoke(self, *args, **kwargs):
202
- response = self.base_llm.invoke(*args, **kwargs)
203
-
204
- # Process the response content if it has a content attribute
205
- if hasattr(response, 'content'):
206
- response.content = remove_think_tags(response.content)
207
- elif isinstance(response, str):
208
- response = remove_think_tags(response)
209
-
210
- return response
211
-
212
- # Pass through any other attributes to the base LLM
213
- def __getattr__(self, name):
214
- return getattr(self.base_llm, name)
215
-
216
- return ProcessingLLMWrapper(llm)
217
-
218
- def get_available_provider_types():
219
- """Return available model providers"""
220
- providers = {}
221
-
222
- if is_ollama_available():
223
- providers["ollama"] = "Ollama (local models)"
224
-
225
- if is_openai_available():
226
- providers["openai"] = "OpenAI API"
227
-
228
- if is_anthropic_available():
229
- providers["anthropic"] = "Anthropic API"
230
-
231
- if is_openai_endpoint_available():
232
- providers["openai_endpoint"] = "OpenAI-compatible Endpoint"
233
-
234
- if is_lmstudio_available():
235
- providers["lmstudio"] = "LM Studio (local models)"
236
-
237
- if is_llamacpp_available():
238
- providers["llamacpp"] = "LlamaCpp (local models)"
239
-
240
- # Check for VLLM capability
241
- try:
242
- import torch
243
- import transformers
244
- providers["vllm"] = "VLLM (local models)"
245
- except ImportError:
246
- pass
247
-
248
- # Default fallback
249
- if not providers:
250
- providers["none"] = "No model providers available"
251
-
252
- return providers
253
-
254
- # ================================
255
- # HELPER FUNCTIONS
256
- # ================================
257
-
258
- def is_openai_available():
259
- """Check if OpenAI is available"""
260
- try:
261
- api_key = settings.get('OPENAI_API_KEY', '')
262
- if not api_key:
263
- api_key = os.getenv('OPENAI_API_KEY')
264
- return bool(api_key)
265
- except:
266
- return False
267
-
268
- def is_anthropic_available():
269
- """Check if Anthropic is available"""
270
- try:
271
- api_key = settings.get('ANTHROPIC_API_KEY', '')
272
- if not api_key:
273
- api_key = os.getenv('ANTHROPIC_API_KEY')
274
- return bool(api_key)
275
- except:
276
- return False
277
-
278
- def is_openai_endpoint_available():
279
- """Check if OpenAI endpoint is available"""
280
- try:
281
- api_key = settings.get('OPENAI_ENDPOINT_API_KEY', '')
282
- if not api_key:
283
- api_key = os.getenv('OPENAI_ENDPOINT_API_KEY')
284
- return bool(api_key)
285
- except:
286
- return False
287
-
288
- def is_ollama_available():
289
- """Check if Ollama is running"""
290
- try:
291
- import requests
292
- base_url = settings.get('OLLAMA_BASE_URL', settings.llm.get('ollama_base_url', 'http://localhost:11434'))
293
- response = requests.get(f"{base_url}/api/tags", timeout=1.0)
294
- return response.status_code == 200
295
- except:
296
- return False
297
-
298
- def is_vllm_available():
299
- """Check if VLLM capability is available"""
300
- try:
301
- import torch
302
- import transformers
303
- return True
304
- except ImportError:
305
- return False
306
-
307
- def is_lmstudio_available():
308
- """Check if LM Studio is available"""
309
- try:
310
- import requests
311
- lmstudio_url = settings.llm.get('lmstudio_url', 'http://localhost:1234')
312
- # LM Studio typically uses OpenAI-compatible endpoints
313
- response = requests.get(f"{lmstudio_url}/v1/models", timeout=1.0)
314
- return response.status_code == 200
315
- except:
316
- return False
317
-
318
- def is_llamacpp_available():
319
- """Check if LlamaCpp is available and configured"""
320
- try:
321
- from langchain_community.llms import LlamaCpp
322
- model_path = settings.llm.get('llamacpp_model_path', '')
323
- return bool(model_path) and os.path.exists(model_path)
324
- except:
325
- return False
326
-
327
- def get_available_providers():
328
- """Get dictionary of available providers"""
329
- return get_available_provider_types()
330
-
331
- # Log which providers are available
332
- AVAILABLE_PROVIDERS = get_available_providers()
333
- logger.info(f"Available providers: {list(AVAILABLE_PROVIDERS.keys())}")
334
-
335
- # Check if selected provider is available
336
- selected_provider = settings.llm.provider.lower()
337
- if selected_provider not in AVAILABLE_PROVIDERS and selected_provider != "none":
338
- logger.warning(f"Selected provider {selected_provider} is not available.")
@@ -1,114 +0,0 @@
1
- import re
2
-
3
-
4
- def remove_think_tags(text: str) -> str:
5
- text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip()
6
- return text
7
-
8
-
9
- def extract_links_from_search_results(search_results: list) -> list:
10
- """
11
- Extracts links and titles from a list of search result dictionaries.
12
-
13
- Each dictionary is expected to have at least the keys "title" and "link".
14
-
15
- Returns a list of dictionaries with 'title' and 'url' keys.
16
- """
17
- links = []
18
- for result in search_results:
19
-
20
- try:
21
-
22
- title = result.get("title", "").strip()
23
- url = result.get("link", "").strip()
24
- index = result.get("index", "").strip()
25
-
26
- if title and url:
27
- links.append({"title": title, "url": url, "index": index})
28
- except Exception:
29
- continue
30
- return links
31
-
32
- def format_links(links):
33
- formatted_links =""
34
- formatted_links += "SOURCES:\n"
35
- for i, link in enumerate(links, 1):
36
- formatted_links += f"{link['index']}. {link['title']}\n URL: {link['url']}\n"
37
- formatted_links += "\n"
38
- return formatted_links
39
-
40
-
41
- def format_findings_to_text(findings_list, current_knowledge, questions_by_iteration):
42
- formatted_text = "COMPLETE RESEARCH OUTPUT \n\n"
43
-
44
- # Store the full current knowledge
45
-
46
- formatted_text += f"{current_knowledge}\n\n"
47
- formatted_text += "=" * 80 + "\n\n"
48
-
49
- # Store questions by iteration
50
- formatted_text += "SEARCH QUESTIONS BY ITERATION:\n"
51
- for iter_num, questions in questions_by_iteration.items():
52
- formatted_text += f"\nIteration {iter_num}:\n"
53
- for i, q in enumerate(questions, 1):
54
- formatted_text += f"{i}. {q}\n"
55
- formatted_text += "\n" + "=" * 80 + "\n\n"
56
-
57
- # Store detailed findings
58
- formatted_text += "DETAILED FINDINGS:\n\n"
59
- all_links = [] # To collect all sources
60
-
61
- for finding in findings_list:
62
- # Phase header
63
- formatted_text += f"{'='*80}\n"
64
- formatted_text += f"PHASE: {finding['phase']}\n"
65
- formatted_text += f"{'='*80}\n\n"
66
-
67
- # If this is a follow-up phase, show the corresponding question
68
- if finding["phase"].startswith("Follow-up"):
69
- iteration = int(finding["phase"].split(".")[0].split()[-1])
70
- question_index = int(finding["phase"].split(".")[-1]) - 1
71
- if iteration in questions_by_iteration and question_index < len(
72
- questions_by_iteration[iteration]
73
- ):
74
- formatted_text += f"SEARCH QUESTION:\n{questions_by_iteration[iteration][question_index]}\n\n"
75
-
76
- # Content
77
- formatted_text += f"CONTENT:\n{finding['content']}\n\n"
78
-
79
- # Search results if they exist
80
- if "search_results" in finding:
81
- # formatted_text += "SEARCH RESULTS:\n"
82
- # formatted_text += f"{finding['search_results']}\n\n"
83
-
84
- # Extract and format links for this finding
85
- links = extract_links_from_search_results(finding["search_results"])
86
- if links:
87
- formatted_text += "SOURCES USED IN THIS SECTION:\n"
88
- for i, link in enumerate(links, 1):
89
- formatted_text += f"{i}. {link['title']}\n URL: {link['url']}\n"
90
- formatted_text += "\n"
91
- all_links.extend(links)
92
-
93
- formatted_text += f"{'_'*80}\n\n"
94
-
95
- # Add summary of all sources at the end
96
- if all_links:
97
- formatted_text += "\nALL SOURCES USED IN RESEARCH:\n"
98
- formatted_text += "=" * 80 + "\n\n"
99
- seen_urls = set() # To prevent duplicates
100
- for i, link in enumerate(all_links, 1):
101
- if link["url"] not in seen_urls:
102
- formatted_text += f"{i}. {link['title']}\n URL: {link['url']}\n"
103
- seen_urls.add(link["url"])
104
- formatted_text += "\n" + "=" * 80 + "\n"
105
-
106
- return formatted_text
107
-
108
- def print_search_results(search_results):
109
- formatted_text=""
110
- links = extract_links_from_search_results(search_results)
111
- if links:
112
- formatted_text=format_links(links=links)
113
- logger.info(formatted_text)
114
-