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.
- local_deep_research/__init__.py +23 -22
- local_deep_research/__main__.py +16 -0
- local_deep_research/advanced_search_system/__init__.py +7 -0
- local_deep_research/advanced_search_system/filters/__init__.py +8 -0
- local_deep_research/advanced_search_system/filters/base_filter.py +38 -0
- local_deep_research/advanced_search_system/filters/cross_engine_filter.py +200 -0
- local_deep_research/advanced_search_system/findings/base_findings.py +81 -0
- local_deep_research/advanced_search_system/findings/repository.py +452 -0
- local_deep_research/advanced_search_system/knowledge/__init__.py +1 -0
- local_deep_research/advanced_search_system/knowledge/base_knowledge.py +151 -0
- local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +159 -0
- local_deep_research/advanced_search_system/questions/__init__.py +1 -0
- local_deep_research/advanced_search_system/questions/base_question.py +64 -0
- local_deep_research/advanced_search_system/questions/decomposition_question.py +445 -0
- local_deep_research/advanced_search_system/questions/standard_question.py +119 -0
- local_deep_research/advanced_search_system/repositories/__init__.py +7 -0
- local_deep_research/advanced_search_system/strategies/__init__.py +1 -0
- local_deep_research/advanced_search_system/strategies/base_strategy.py +118 -0
- local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +450 -0
- local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +312 -0
- local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +270 -0
- local_deep_research/advanced_search_system/strategies/standard_strategy.py +300 -0
- local_deep_research/advanced_search_system/tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/base_tool.py +100 -0
- local_deep_research/advanced_search_system/tools/knowledge_tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/question_tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/search_tools/__init__.py +1 -0
- local_deep_research/api/__init__.py +5 -5
- local_deep_research/api/research_functions.py +154 -160
- local_deep_research/app.py +8 -0
- local_deep_research/citation_handler.py +25 -16
- local_deep_research/{config.py → config/config_files.py} +102 -110
- local_deep_research/config/llm_config.py +472 -0
- local_deep_research/config/search_config.py +77 -0
- local_deep_research/defaults/__init__.py +10 -5
- local_deep_research/defaults/main.toml +2 -2
- local_deep_research/defaults/search_engines.toml +60 -34
- local_deep_research/main.py +121 -19
- local_deep_research/migrate_db.py +147 -0
- local_deep_research/report_generator.py +87 -45
- local_deep_research/search_system.py +153 -283
- local_deep_research/setup_data_dir.py +35 -0
- local_deep_research/test_migration.py +178 -0
- local_deep_research/utilities/__init__.py +0 -0
- local_deep_research/utilities/db_utils.py +49 -0
- local_deep_research/{utilties → utilities}/enums.py +2 -2
- local_deep_research/{utilties → utilities}/llm_utils.py +63 -29
- local_deep_research/utilities/search_utilities.py +242 -0
- local_deep_research/{utilties → utilities}/setup_utils.py +4 -2
- local_deep_research/web/__init__.py +0 -1
- local_deep_research/web/app.py +86 -1709
- local_deep_research/web/app_factory.py +289 -0
- local_deep_research/web/database/README.md +70 -0
- local_deep_research/web/database/migrate_to_ldr_db.py +289 -0
- local_deep_research/web/database/migrations.py +447 -0
- local_deep_research/web/database/models.py +117 -0
- local_deep_research/web/database/schema_upgrade.py +107 -0
- local_deep_research/web/models/database.py +294 -0
- local_deep_research/web/models/settings.py +94 -0
- local_deep_research/web/routes/api_routes.py +559 -0
- local_deep_research/web/routes/history_routes.py +354 -0
- local_deep_research/web/routes/research_routes.py +715 -0
- local_deep_research/web/routes/settings_routes.py +1583 -0
- local_deep_research/web/services/research_service.py +947 -0
- local_deep_research/web/services/resource_service.py +149 -0
- local_deep_research/web/services/settings_manager.py +669 -0
- local_deep_research/web/services/settings_service.py +187 -0
- local_deep_research/web/services/socket_service.py +210 -0
- local_deep_research/web/static/css/custom_dropdown.css +277 -0
- local_deep_research/web/static/css/settings.css +1223 -0
- local_deep_research/web/static/css/styles.css +525 -48
- local_deep_research/web/static/js/components/custom_dropdown.js +428 -0
- local_deep_research/web/static/js/components/detail.js +348 -0
- local_deep_research/web/static/js/components/fallback/formatting.js +122 -0
- local_deep_research/web/static/js/components/fallback/ui.js +215 -0
- local_deep_research/web/static/js/components/history.js +487 -0
- local_deep_research/web/static/js/components/logpanel.js +949 -0
- local_deep_research/web/static/js/components/progress.js +1107 -0
- local_deep_research/web/static/js/components/research.js +1865 -0
- local_deep_research/web/static/js/components/results.js +766 -0
- local_deep_research/web/static/js/components/settings.js +3981 -0
- local_deep_research/web/static/js/components/settings_sync.js +106 -0
- local_deep_research/web/static/js/main.js +226 -0
- local_deep_research/web/static/js/services/api.js +253 -0
- local_deep_research/web/static/js/services/audio.js +31 -0
- local_deep_research/web/static/js/services/formatting.js +119 -0
- local_deep_research/web/static/js/services/pdf.js +622 -0
- local_deep_research/web/static/js/services/socket.js +882 -0
- local_deep_research/web/static/js/services/ui.js +546 -0
- local_deep_research/web/templates/base.html +72 -0
- local_deep_research/web/templates/components/custom_dropdown.html +47 -0
- local_deep_research/web/templates/components/log_panel.html +32 -0
- local_deep_research/web/templates/components/mobile_nav.html +22 -0
- local_deep_research/web/templates/components/settings_form.html +299 -0
- local_deep_research/web/templates/components/sidebar.html +21 -0
- local_deep_research/web/templates/pages/details.html +73 -0
- local_deep_research/web/templates/pages/history.html +51 -0
- local_deep_research/web/templates/pages/progress.html +57 -0
- local_deep_research/web/templates/pages/research.html +139 -0
- local_deep_research/web/templates/pages/results.html +59 -0
- local_deep_research/web/templates/settings_dashboard.html +78 -192
- local_deep_research/web/utils/__init__.py +0 -0
- local_deep_research/web/utils/formatters.py +76 -0
- local_deep_research/web_search_engines/engines/full_search.py +18 -16
- local_deep_research/web_search_engines/engines/meta_search_engine.py +182 -131
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +224 -139
- local_deep_research/web_search_engines/engines/search_engine_brave.py +88 -71
- local_deep_research/web_search_engines/engines/search_engine_ddg.py +48 -39
- local_deep_research/web_search_engines/engines/search_engine_github.py +415 -204
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +123 -90
- local_deep_research/web_search_engines/engines/search_engine_guardian.py +210 -157
- local_deep_research/web_search_engines/engines/search_engine_local.py +532 -369
- local_deep_research/web_search_engines/engines/search_engine_local_all.py +42 -36
- local_deep_research/web_search_engines/engines/search_engine_pubmed.py +358 -266
- local_deep_research/web_search_engines/engines/search_engine_searxng.py +212 -160
- local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +213 -170
- local_deep_research/web_search_engines/engines/search_engine_serpapi.py +84 -68
- local_deep_research/web_search_engines/engines/search_engine_wayback.py +186 -154
- local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +115 -77
- local_deep_research/web_search_engines/search_engine_base.py +174 -99
- local_deep_research/web_search_engines/search_engine_factory.py +192 -102
- local_deep_research/web_search_engines/search_engines_config.py +22 -15
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/METADATA +177 -97
- local_deep_research-0.2.2.dist-info/RECORD +135 -0
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/WHEEL +1 -2
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/entry_points.txt +3 -0
- local_deep_research/defaults/llm_config.py +0 -338
- local_deep_research/utilties/search_utilities.py +0 -114
- local_deep_research/web/static/js/app.js +0 -3763
- local_deep_research/web/templates/api_keys_config.html +0 -82
- local_deep_research/web/templates/collections_config.html +0 -90
- local_deep_research/web/templates/index.html +0 -348
- local_deep_research/web/templates/llm_config.html +0 -120
- local_deep_research/web/templates/main_config.html +0 -89
- local_deep_research/web/templates/search_engines_config.html +0 -154
- local_deep_research/web/templates/settings.html +0 -519
- local_deep_research-0.1.26.dist-info/RECORD +0 -61
- local_deep_research-0.1.26.dist-info/top_level.txt +0 -1
- /local_deep_research/{utilties → config}/__init__.py +0 -0
- {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
|
+
}
|