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,312 @@
1
+ """
2
+ Parallel search strategy implementation for maximum search speed.
3
+ """
4
+
5
+ import concurrent.futures
6
+ import logging
7
+ from typing import Dict
8
+
9
+ from ...citation_handler import CitationHandler
10
+ from ...config.llm_config import get_llm
11
+ from ...config.search_config import get_search
12
+ from ...utilities.db_utils import get_db_setting
13
+ from ...utilities.search_utilities import extract_links_from_search_results
14
+ from ..filters.cross_engine_filter import CrossEngineFilter
15
+ from ..findings.repository import FindingsRepository
16
+ from ..questions.standard_question import StandardQuestionGenerator
17
+ from .base_strategy import BaseSearchStrategy
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class ParallelSearchStrategy(BaseSearchStrategy):
23
+ """
24
+ Parallel search strategy that generates questions and runs all searches
25
+ simultaneously for maximum speed.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ search=None,
31
+ model=None,
32
+ citation_handler=None,
33
+ include_text_content: bool = True,
34
+ use_cross_engine_filter: bool = True,
35
+ filter_reorder: bool = True,
36
+ filter_reindex: bool = True,
37
+ filter_max_results: int = 20,
38
+ ):
39
+ """Initialize with optional dependency injection for testing.
40
+
41
+ Args:
42
+ search: Optional search engine instance
43
+ model: Optional LLM model instance
44
+ citation_handler: Optional citation handler instance
45
+ include_text_content: If False, only includes metadata and links in search results
46
+ use_cross_engine_filter: If True, filter search results across engines
47
+ filter_reorder: Whether to reorder results by relevance
48
+ filter_reindex: Whether to update result indices after filtering
49
+ filter_max_results: Maximum number of results to keep after filtering
50
+ """
51
+ super().__init__()
52
+ self.search = search or get_search()
53
+ self.model = model or get_llm()
54
+ self.progress_callback = None
55
+ self.all_links_of_system = list()
56
+ self.questions_by_iteration = {}
57
+ self.include_text_content = include_text_content
58
+ self.use_cross_engine_filter = use_cross_engine_filter
59
+ self.filter_reorder = filter_reorder
60
+ self.filter_reindex = filter_reindex
61
+
62
+ # Initialize the cross-engine filter
63
+ self.cross_engine_filter = CrossEngineFilter(
64
+ model=self.model,
65
+ max_results=filter_max_results,
66
+ default_reorder=filter_reorder,
67
+ default_reindex=filter_reindex,
68
+ )
69
+
70
+ # Set include_full_content on the search engine if it supports it
71
+ if hasattr(self.search, "include_full_content"):
72
+ self.search.include_full_content = include_text_content
73
+
74
+ # Use provided citation_handler or create one
75
+ self.citation_handler = citation_handler or CitationHandler(self.model)
76
+
77
+ # Initialize components
78
+ self.question_generator = StandardQuestionGenerator(self.model)
79
+ self.findings_repository = FindingsRepository(self.model)
80
+
81
+ def analyze_topic(self, query: str) -> Dict:
82
+ """
83
+ Parallel implementation that generates questions and searches all at once.
84
+
85
+ Args:
86
+ query: The research query to analyze
87
+ """
88
+ logger.info(f"Starting parallel research on topic: {query}")
89
+
90
+ findings = []
91
+ all_search_results = []
92
+
93
+ self._update_progress(
94
+ "Initializing parallel research",
95
+ 5,
96
+ {
97
+ "phase": "init",
98
+ "strategy": "parallel",
99
+ "include_text_content": self.include_text_content,
100
+ },
101
+ )
102
+
103
+ # Check search engine
104
+ if not self._validate_search_engine():
105
+ return {
106
+ "findings": [],
107
+ "iterations": 0,
108
+ "questions": {},
109
+ "formatted_findings": "Error: Unable to conduct research without a search engine.",
110
+ "current_knowledge": "",
111
+ "error": "No search engine available",
112
+ }
113
+
114
+ try:
115
+ # Step 1: Generate questions first
116
+ self._update_progress(
117
+ "Generating search questions", 10, {"phase": "question_generation"}
118
+ )
119
+
120
+ # Generate 3 additional questions (plus the main query = 4 total)
121
+ questions = self.question_generator.generate_questions(
122
+ current_knowledge="", # No knowledge accumulation
123
+ query=query,
124
+ questions_per_iteration=int(
125
+ get_db_setting("search.questions_per_iteration")
126
+ ), # 3 additional questions
127
+ questions_by_iteration={},
128
+ )
129
+
130
+ # Add the original query as the first question
131
+ all_questions = [query] + questions
132
+
133
+ # Store in questions_by_iteration
134
+ self.questions_by_iteration[0] = questions
135
+ logger.info(f"Generated questions: {questions}")
136
+
137
+ # Step 2: Run all searches in parallel
138
+ self._update_progress(
139
+ "Running parallel searches for all questions",
140
+ 20,
141
+ {"phase": "parallel_search"},
142
+ )
143
+
144
+ # Function for thread pool
145
+ def search_question(q):
146
+ try:
147
+ result = self.search.run(q)
148
+ return {"question": q, "results": result or []}
149
+ except Exception as e:
150
+ logger.error(f"Error searching for '{q}': {str(e)}")
151
+ return {"question": q, "results": [], "error": str(e)}
152
+
153
+ # Run searches in parallel
154
+ with concurrent.futures.ThreadPoolExecutor(
155
+ max_workers=len(all_questions)
156
+ ) as executor:
157
+ futures = [executor.submit(search_question, q) for q in all_questions]
158
+ all_search_dict = {}
159
+
160
+ # Process results as they complete
161
+ for i, future in enumerate(concurrent.futures.as_completed(futures)):
162
+ result_dict = future.result()
163
+ question = result_dict["question"]
164
+ search_results = result_dict["results"]
165
+ all_search_dict[question] = search_results
166
+
167
+ self._update_progress(
168
+ f"Completed search {i + 1} of {len(all_questions)}: {question[:30]}...",
169
+ 20 + ((i + 1) / len(all_questions) * 40),
170
+ {
171
+ "phase": "search_complete",
172
+ "result_count": len(search_results),
173
+ "question": question,
174
+ },
175
+ )
176
+
177
+ # Extract and save links
178
+ if not self.use_cross_engine_filter:
179
+ links = extract_links_from_search_results(search_results)
180
+ self.all_links_of_system.extend(links)
181
+ all_search_results.extend(search_results)
182
+
183
+ # Step 3: Analysis of collected search results
184
+ self._update_progress(
185
+ "Analyzing all collected search results",
186
+ 70,
187
+ {"phase": "final_analysis"},
188
+ )
189
+ if self.use_cross_engine_filter:
190
+ self._update_progress(
191
+ "Filtering search results across engines",
192
+ 65,
193
+ {"phase": "cross_engine_filtering"},
194
+ )
195
+
196
+ # Get the current link count (for indexing)
197
+ existing_link_count = len(self.all_links_of_system)
198
+
199
+ # Filter the search results
200
+ filtered_search_results = self.cross_engine_filter.filter_results(
201
+ all_search_results,
202
+ query,
203
+ reorder=self.filter_reorder,
204
+ reindex=self.filter_reindex,
205
+ start_index=existing_link_count, # Start indexing after existing links
206
+ )
207
+
208
+ links = extract_links_from_search_results(filtered_search_results)
209
+ self.all_links_of_system.extend(links)
210
+
211
+ self._update_progress(
212
+ f"Filtered from {len(all_search_results)} to {len(filtered_search_results)} results",
213
+ 70,
214
+ {
215
+ "phase": "filtering_complete",
216
+ "links_count": len(self.all_links_of_system),
217
+ },
218
+ )
219
+
220
+ # Use filtered results for analysis
221
+ all_search_results = filtered_search_results
222
+
223
+ # Now when we use the citation handler, ensure we're using all_search_results:
224
+ if self.include_text_content:
225
+ # Use citation handler for analysis of all results together
226
+ citation_result = self.citation_handler.analyze_initial(
227
+ query, all_search_results
228
+ )
229
+
230
+ if citation_result:
231
+ synthesized_content = citation_result["content"]
232
+ finding = {
233
+ "phase": "Final synthesis",
234
+ "content": synthesized_content,
235
+ "question": query,
236
+ "search_results": all_search_results,
237
+ "documents": citation_result.get("documents", []),
238
+ }
239
+ findings.append(finding)
240
+
241
+ # Transfer questions to repository
242
+ self.findings_repository.set_questions_by_iteration(
243
+ self.questions_by_iteration
244
+ )
245
+
246
+ # Format findings
247
+ formatted_findings = self.findings_repository.format_findings_to_text(
248
+ findings, synthesized_content
249
+ )
250
+
251
+ # Add documents to repository
252
+ if "documents" in citation_result:
253
+ self.findings_repository.add_documents(citation_result["documents"])
254
+ else:
255
+ synthesized_content = "No relevant results found."
256
+ formatted_findings = synthesized_content
257
+ finding = {
258
+ "phase": "Error",
259
+ "content": "No relevant results found.",
260
+ "question": query,
261
+ "search_results": all_search_results,
262
+ "documents": [],
263
+ }
264
+ findings.append(finding)
265
+ else:
266
+ # Skip LLM analysis, just format the raw search results
267
+ synthesized_content = "LLM analysis skipped"
268
+ finding = {
269
+ "phase": "Raw search results",
270
+ "content": "LLM analysis was skipped. Displaying raw search results with links.",
271
+ "question": query,
272
+ "search_results": all_search_results,
273
+ "documents": [],
274
+ }
275
+ findings.append(finding)
276
+
277
+ # Transfer questions to repository
278
+ self.findings_repository.set_questions_by_iteration(
279
+ self.questions_by_iteration
280
+ )
281
+
282
+ # Format findings without synthesis
283
+ formatted_findings = self.findings_repository.format_findings_to_text(
284
+ findings, "Raw search results (LLM analysis skipped)"
285
+ )
286
+
287
+ except Exception as e:
288
+ import traceback
289
+
290
+ error_msg = f"Error in research process: {str(e)}"
291
+ logger.error(error_msg)
292
+ logger.error(traceback.format_exc())
293
+ synthesized_content = f"Error: {str(e)}"
294
+ formatted_findings = f"Error: {str(e)}"
295
+ finding = {
296
+ "phase": "Error",
297
+ "content": synthesized_content,
298
+ "question": query,
299
+ "search_results": [],
300
+ "documents": [],
301
+ }
302
+ findings.append(finding)
303
+
304
+ self._update_progress("Research complete", 100, {"phase": "complete"})
305
+
306
+ return {
307
+ "findings": findings,
308
+ "iterations": 1,
309
+ "questions": self.questions_by_iteration,
310
+ "formatted_findings": formatted_findings,
311
+ "current_knowledge": synthesized_content,
312
+ }
@@ -0,0 +1,270 @@
1
+ """
2
+ RapidSearch strategy implementation.
3
+ """
4
+
5
+ import logging
6
+ from typing import Dict
7
+
8
+ from ...citation_handler import CitationHandler
9
+ from ...config.llm_config import get_llm
10
+ from ...config.search_config import get_search
11
+ from ...utilities.search_utilities import extract_links_from_search_results
12
+ from ..findings.repository import FindingsRepository
13
+ from ..knowledge.standard_knowledge import StandardKnowledge
14
+ from ..questions.standard_question import StandardQuestionGenerator
15
+ from .base_strategy import BaseSearchStrategy
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class RapidSearchStrategy(BaseSearchStrategy):
21
+ """
22
+ Rapid search strategy that only analyzes snippets and performs
23
+ a single synthesis step at the end, optimized for speed.
24
+ """
25
+
26
+ def __init__(self, search=None, model=None, citation_handler=None):
27
+ """Initialize with optional dependency injection for testing."""
28
+ super().__init__()
29
+ self.search = search or get_search()
30
+ self.model = model or get_llm()
31
+ self.progress_callback = None
32
+ self.all_links_of_system = list()
33
+ self.questions_by_iteration = {}
34
+
35
+ # Use provided citation_handler or create one
36
+ self.citation_handler = citation_handler or CitationHandler(self.model)
37
+
38
+ # Initialize components
39
+ self.question_generator = StandardQuestionGenerator(self.model)
40
+ self.knowledge_generator = StandardKnowledge(self.model)
41
+ self.findings_repository = FindingsRepository(self.model)
42
+
43
+ def analyze_topic(self, query: str) -> Dict:
44
+ """
45
+ RapidSearch implementation that collects snippets, avoids intermediate
46
+ synthesis, and only performs a final synthesis at the end.
47
+ """
48
+ logger.info(f"Starting rapid research on topic: {query}")
49
+
50
+ findings = []
51
+ all_search_results = []
52
+ collected_snippets = []
53
+ section_links = list()
54
+
55
+ self._update_progress(
56
+ "Initializing rapid research system",
57
+ 5,
58
+ {"phase": "init", "strategy": "rapid"},
59
+ )
60
+
61
+ # Check if search engine is available
62
+ if not self._validate_search_engine():
63
+ return {
64
+ "findings": [],
65
+ "iterations": 0,
66
+ "questions": {},
67
+ "formatted_findings": "Error: Unable to conduct research without a search engine.",
68
+ "current_knowledge": "",
69
+ "error": "No search engine available",
70
+ }
71
+
72
+ # Step 1: Initial search for the main query
73
+ self._update_progress(
74
+ "Performing initial search for main query",
75
+ 10,
76
+ {"phase": "search", "iteration": 1},
77
+ )
78
+
79
+ try:
80
+ initial_results = self.search.run(query)
81
+
82
+ if not initial_results:
83
+ self._update_progress(
84
+ "No initial results found",
85
+ 15,
86
+ {"phase": "search_complete", "result_count": 0},
87
+ )
88
+ initial_results = []
89
+ else:
90
+ self._update_progress(
91
+ f"Found {len(initial_results)} initial results",
92
+ 15,
93
+ {"phase": "search_complete", "result_count": len(initial_results)},
94
+ )
95
+
96
+ # Extract snippets and links
97
+ for result in initial_results:
98
+ if "snippet" in result:
99
+ collected_snippets.append(
100
+ {
101
+ "text": result["snippet"],
102
+ "source": result.get("title", "Unknown Source"),
103
+ "link": result.get("link", ""),
104
+ "query": query,
105
+ }
106
+ )
107
+
108
+ # Extract and save links
109
+ initial_links = extract_links_from_search_results(initial_results)
110
+ self.all_links_of_system.extend(initial_links)
111
+ section_links.extend(initial_links)
112
+ all_search_results.extend(initial_results)
113
+
114
+ # No findings added here - just collecting data
115
+
116
+ except Exception as e:
117
+ error_msg = f"Error during initial search: {str(e)}"
118
+ logger.error(f"SEARCH ERROR: {error_msg}")
119
+ self._update_progress(
120
+ error_msg, 15, {"phase": "search_error", "error": str(e)}
121
+ )
122
+ initial_results = []
123
+
124
+ # Step 2: Generate a few follow-up questions (optional, can be skipped for ultimate speed)
125
+ self._update_progress(
126
+ "Generating follow-up questions", 25, {"phase": "question_generation"}
127
+ )
128
+
129
+ questions = self.question_generator.generate_questions(
130
+ current_knowledge="", # No knowledge accumulation in rapid mode
131
+ query=query,
132
+ questions_per_iteration=3, # Fewer questions for speed
133
+ questions_by_iteration={},
134
+ )
135
+
136
+ self.questions_by_iteration[0] = questions
137
+ logger.info(f"Generated questions: {questions}")
138
+
139
+ # Step 3: Process follow-up questions
140
+ question_count = len(questions)
141
+ for q_idx, question in enumerate(questions):
142
+ question_progress = 30 + ((q_idx + 1) / question_count * 40)
143
+
144
+ self._update_progress(
145
+ f"Searching for: {question}",
146
+ int(question_progress),
147
+ {"phase": "search", "question_index": q_idx + 1},
148
+ )
149
+
150
+ try:
151
+ search_results = self.search.run(question)
152
+
153
+ if not search_results:
154
+ self._update_progress(
155
+ f"No results found for question: {question}",
156
+ int(question_progress + 2),
157
+ {"phase": "search_complete", "result_count": 0},
158
+ )
159
+ continue
160
+
161
+ self._update_progress(
162
+ f"Found {len(search_results)} results for question: {question}",
163
+ int(question_progress + 5),
164
+ {"phase": "search_complete", "result_count": len(search_results)},
165
+ )
166
+
167
+ # Extract snippets only
168
+ for result in search_results:
169
+ if "snippet" in result:
170
+ collected_snippets.append(
171
+ {
172
+ "text": result["snippet"],
173
+ "source": result.get("title", "Unknown Source"),
174
+ "link": result.get("link", ""),
175
+ "query": question,
176
+ }
177
+ )
178
+
179
+ # Extract and save links
180
+ links = extract_links_from_search_results(search_results)
181
+ self.all_links_of_system.extend(links)
182
+ section_links.extend(links)
183
+ all_search_results.extend(search_results)
184
+
185
+ # No findings added here - just collecting data
186
+
187
+ except Exception as e:
188
+ error_msg = f"Error during search: {str(e)}"
189
+ logger.error(f"SEARCH ERROR: {error_msg}")
190
+ self._update_progress(
191
+ error_msg,
192
+ int(question_progress + 2),
193
+ {"phase": "search_error", "error": str(e)},
194
+ )
195
+
196
+ # Step 4: Perform a single final synthesis with all collected snippets using the citation handler
197
+ self._update_progress(
198
+ "Synthesizing all collected information", 80, {"phase": "final_synthesis"}
199
+ )
200
+
201
+ try:
202
+ # Use citation handler for the final analysis
203
+ # First, we need a stub of current knowledge
204
+
205
+ # Use the citation handler to analyze the results
206
+ result = self.citation_handler.analyze_initial(query, all_search_results)
207
+
208
+ if result:
209
+ synthesized_content = result["content"]
210
+
211
+ # Create a synthesis finding
212
+ finding = {
213
+ "phase": "Final synthesis",
214
+ "content": synthesized_content,
215
+ "question": query,
216
+ "search_results": all_search_results,
217
+ "documents": result.get("documents", []),
218
+ }
219
+ findings.append(finding)
220
+
221
+ # Transfer questions to the repository
222
+ self.findings_repository.set_questions_by_iteration(
223
+ self.questions_by_iteration
224
+ )
225
+
226
+ # Format the findings with search questions and sources
227
+ formatted_findings = self.findings_repository.format_findings_to_text(
228
+ findings, synthesized_content
229
+ )
230
+
231
+ # Also add to the repository
232
+ self.findings_repository.add_documents(result.get("documents", []))
233
+ else:
234
+ # Fallback if citation handler fails
235
+ synthesized_content = (
236
+ "Error: Failed to synthesize results with citation handler."
237
+ )
238
+ formatted_findings = synthesized_content
239
+ finding = {
240
+ "phase": "Error",
241
+ "content": synthesized_content,
242
+ "question": query,
243
+ "search_results": all_search_results,
244
+ "documents": [],
245
+ }
246
+ findings.append(finding)
247
+
248
+ except Exception as e:
249
+ error_msg = f"Error synthesizing final answer: {str(e)}"
250
+ logger.error(error_msg)
251
+ synthesized_content = f"Error generating synthesis: {str(e)}"
252
+ formatted_findings = f"Error: {str(e)}"
253
+ finding = {
254
+ "phase": "Error",
255
+ "content": synthesized_content,
256
+ "question": query,
257
+ "search_results": all_search_results,
258
+ "documents": [],
259
+ }
260
+ findings.append(finding)
261
+
262
+ self._update_progress("Research complete", 100, {"phase": "complete"})
263
+
264
+ return {
265
+ "findings": findings,
266
+ "iterations": 1, # Always 1 iteration in rapid mode
267
+ "questions": self.questions_by_iteration,
268
+ "formatted_findings": formatted_findings,
269
+ "current_knowledge": synthesized_content,
270
+ }