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,450 @@
1
+ """
2
+ IterDRAG strategy implementation.
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ from datetime import datetime
8
+ from typing import Dict, List
9
+
10
+ from langchain_core.language_models import BaseLLM
11
+
12
+ from ...citation_handler import CitationHandler
13
+ from ...config.config_files import settings
14
+ from ...config.llm_config import get_llm
15
+ from ...config.search_config import get_search
16
+ from ...utilities.db_utils import get_db_setting
17
+ from ...utilities.search_utilities import extract_links_from_search_results
18
+ from ..findings.repository import FindingsRepository
19
+ from ..knowledge.standard_knowledge import StandardKnowledge
20
+ from ..questions.decomposition_question import DecompositionQuestionGenerator
21
+ from .base_strategy import BaseSearchStrategy
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class IterDRAGStrategy(BaseSearchStrategy):
27
+ """IterDRAG strategy that breaks queries into sub-queries."""
28
+
29
+ def __init__(
30
+ self, model: BaseLLM | None = None, search=None, citation_handler=None
31
+ ):
32
+ """Initialize the strategy with optional dependency injection for testing."""
33
+ super().__init__()
34
+ self.model = model or get_llm()
35
+ self.search = search or get_search()
36
+ self.progress_callback = None
37
+ self.all_links_of_system = list()
38
+ self.questions_by_iteration = {}
39
+
40
+ # Use provided citation_handler or create one
41
+ self.citation_handler = citation_handler or CitationHandler(self.model)
42
+
43
+ # Initialize components
44
+ self.question_generator = DecompositionQuestionGenerator(self.model)
45
+ self.knowledge_generator = StandardKnowledge(self.model)
46
+ self.findings_repository = FindingsRepository(self.model)
47
+
48
+ def _update_progress(
49
+ self, message: str, progress_percent: int = None, metadata: dict = None
50
+ ) -> None:
51
+ """Send a progress update via the callback if available."""
52
+ if self.progress_callback:
53
+ self.progress_callback(message, progress_percent, metadata or {})
54
+
55
+ def _generate_subqueries(
56
+ self, query: str, initial_results: List[Dict], current_knowledge: str
57
+ ) -> List[str]:
58
+ """Generate sub-queries based on initial search results and the main query."""
59
+ try:
60
+ # Format context for question generation
61
+ context = f"""Current Query: {query}
62
+ Current Date: {datetime.now().strftime('%Y-%m-%d')}
63
+ Past Questions: {self.questions_by_iteration}
64
+ Current Knowledge: {current_knowledge}
65
+
66
+ Initial Search Results:
67
+ {json.dumps(initial_results, indent=2)}"""
68
+
69
+ # Generate sub-queries using the question generator
70
+ return self.question_generator.generate_questions(
71
+ query, context, int(get_db_setting("search.questions_per_iteration"))
72
+ )
73
+ except Exception as e:
74
+ logger.error(f"Error generating sub-queries: {str(e)}")
75
+ return []
76
+
77
+ def analyze_topic(self, query: str) -> Dict:
78
+ """IterDRAG implementation of the topic analysis process."""
79
+ findings = []
80
+ current_knowledge = ""
81
+ section_links = list()
82
+
83
+ self._update_progress(
84
+ "Initializing IterDRAG research system",
85
+ 5,
86
+ {"phase": "init", "strategy": "iterdrag"},
87
+ )
88
+
89
+ # Check if search engine is available
90
+ if self.search is None:
91
+ error_msg = (
92
+ "Error: No search engine available. Please check your configuration."
93
+ )
94
+ self._update_progress(
95
+ error_msg,
96
+ 100,
97
+ {
98
+ "phase": "error",
99
+ "error": "No search engine available",
100
+ "status": "failed",
101
+ },
102
+ )
103
+ return {
104
+ "findings": [],
105
+ "iterations": 0,
106
+ "questions": {},
107
+ "formatted_findings": "Error: Unable to conduct research without a search engine.",
108
+ "current_knowledge": "",
109
+ "error": error_msg,
110
+ }
111
+
112
+ # Initial search for the main query
113
+ self._update_progress(
114
+ "Performing initial search for main query",
115
+ 10,
116
+ {"phase": "search", "iteration": 1},
117
+ )
118
+
119
+ initial_results = self.search.run(query)
120
+ if not initial_results:
121
+ self._update_progress(
122
+ "No initial results found",
123
+ 15,
124
+ {"phase": "search_complete", "result_count": 0},
125
+ )
126
+ initial_results = []
127
+ else:
128
+ self._update_progress(
129
+ f"Found {len(initial_results)} initial results",
130
+ 15,
131
+ {"phase": "search_complete", "result_count": len(initial_results)},
132
+ )
133
+
134
+ # Extract and save links
135
+ initial_links = extract_links_from_search_results(initial_results)
136
+ self.all_links_of_system.extend(initial_links)
137
+ section_links.extend(initial_links)
138
+
139
+ # Generate sub-queries
140
+ self._update_progress(
141
+ "Generating sub-queries for IterDRAG analysis",
142
+ 20,
143
+ {"phase": "iterdrag_decomposition"},
144
+ )
145
+
146
+ sub_queries = self._generate_subqueries(
147
+ query, initial_results, current_knowledge
148
+ )
149
+
150
+ # Store questions in repository and in self.questions_by_iteration
151
+ self.questions_by_iteration = {0: sub_queries}
152
+ self.findings_repository.set_questions_by_iteration(self.questions_by_iteration)
153
+
154
+ if not sub_queries:
155
+ logger.error("No sub-queries were generated to analyze.")
156
+ finding = {
157
+ "phase": "Analysis Error",
158
+ "content": "No sub-queries could be generated for the main question.",
159
+ "question": query,
160
+ "search_results": [],
161
+ }
162
+ findings.append(finding)
163
+ else:
164
+ # Process each sub-query
165
+ total_subqueries = len(sub_queries)
166
+
167
+ for i, sub_query in enumerate(sub_queries, 1):
168
+ progress_base = 25 + (i / total_subqueries * 50)
169
+ self._update_progress(
170
+ f"Processing sub-query {i} of {total_subqueries}: {sub_query}",
171
+ int(progress_base),
172
+ {"phase": "subquery", "subquery_index": i},
173
+ )
174
+
175
+ # Search for the sub-query
176
+ try:
177
+ sub_results = self.search.run(sub_query)
178
+ if not sub_results:
179
+ self._update_progress(
180
+ f"No results for sub-query: {sub_query}",
181
+ int(progress_base + 2),
182
+ {"phase": "search_complete", "result_count": 0},
183
+ )
184
+ sub_results = []
185
+ else:
186
+ self._update_progress(
187
+ f"Found {len(sub_results)} results for sub-query: {sub_query}",
188
+ int(progress_base + 2),
189
+ {
190
+ "phase": "search_complete",
191
+ "result_count": len(sub_results),
192
+ },
193
+ )
194
+ except Exception as e:
195
+ logger.error(f"Error searching for sub-query: {str(e)}")
196
+ sub_results = []
197
+
198
+ try:
199
+ # Use previous knowledge to answer this sub-query
200
+ result = self.citation_handler.analyze_followup(
201
+ sub_query,
202
+ sub_results,
203
+ current_knowledge,
204
+ nr_of_links=len(self.all_links_of_system),
205
+ )
206
+
207
+ # Extract and save links AFTER citation handler processes results
208
+ sub_links = extract_links_from_search_results(sub_results)
209
+ self.all_links_of_system.extend(sub_links)
210
+ section_links.extend(sub_links)
211
+
212
+ if result is not None:
213
+ finding = {
214
+ "phase": f"Sub-query {i}",
215
+ "content": result["content"],
216
+ "question": sub_query,
217
+ "search_results": sub_results,
218
+ "documents": result["documents"],
219
+ }
220
+ findings.append(finding)
221
+ self.findings_repository.add_finding(sub_query, finding)
222
+ self.findings_repository.add_documents(result["documents"])
223
+
224
+ # Add to current knowledge with space around +
225
+ current_knowledge = (
226
+ current_knowledge + "\n\n\n New: \n" + result["content"]
227
+ )
228
+ except Exception as e:
229
+ logger.error(f"Error analyzing sub-query results: {str(e)}")
230
+ finding = {
231
+ "phase": f"Follow-up Iteration 0.{i + 1}",
232
+ "content": "Error analyzing sub-query results.",
233
+ "question": sub_query,
234
+ "search_results": [],
235
+ "documents": [],
236
+ }
237
+ findings.append(finding)
238
+ # Optionally update current_knowledge even if analysis failed?
239
+ # current_knowledge += f"\n\nError analyzing results for: {sub_query}"
240
+
241
+ # Final answer synthesis based on all sub-query findings
242
+ self._update_progress(
243
+ "Synthesizing final answer from sub-queries",
244
+ 80,
245
+ {"phase": "final_synthesis"},
246
+ )
247
+
248
+ try:
249
+ # Extract finding contents for synthesis
250
+ finding_contents = [f["content"] for f in findings if "content" in f]
251
+
252
+ # Synthesize findings into a final answer
253
+ final_answer = self.findings_repository.synthesize_findings(
254
+ query=query,
255
+ sub_queries=sub_queries,
256
+ findings=finding_contents,
257
+ accumulated_knowledge=current_knowledge,
258
+ )
259
+
260
+ # Check if the synthesis failed with an error
261
+ if isinstance(final_answer, str) and final_answer.startswith("Error:"):
262
+ logger.error(f"Synthesis returned an error: {final_answer}")
263
+
264
+ # Extract error type for better handling
265
+ error_type = "unknown"
266
+ error_message = final_answer.lower()
267
+
268
+ if "timeout" in error_message:
269
+ error_type = "timeout"
270
+ elif "token limit" in error_message:
271
+ error_type = "token_limit"
272
+ elif "rate limit" in error_message:
273
+ error_type = "rate_limit"
274
+ elif "connection" in error_message:
275
+ error_type = "connection"
276
+
277
+ # Log with specific error type
278
+ logger.error(f"Synthesis failed with error type: {error_type}")
279
+
280
+ # Add error information to progress update
281
+ self._update_progress(
282
+ f"Synthesis failed: {error_type}. Attempting fallback...",
283
+ 85,
284
+ {"phase": "synthesis_error", "error_type": error_type},
285
+ )
286
+
287
+ # Try fallback: use the best individual finding as the final answer
288
+ longest_finding = ""
289
+ for f in findings:
290
+ if isinstance(f, dict) and "content" in f:
291
+ if len(f["content"]) > len(longest_finding):
292
+ longest_finding = f["content"]
293
+
294
+ if longest_finding:
295
+ logger.info("Using longest finding as fallback synthesis")
296
+ final_answer = f"""
297
+ # Research Results (Fallback Mode)
298
+
299
+ {longest_finding}
300
+
301
+ ## Note
302
+ The final synthesis could not be completed due to an error: {final_answer}
303
+ This is a fallback response using the most detailed individual finding.
304
+ """
305
+ else:
306
+ # If we don't have any findings with content, use current_knowledge
307
+ logger.info("Using current knowledge as fallback synthesis")
308
+ final_answer = f"""
309
+ # Research Results (Fallback Mode)
310
+
311
+ {current_knowledge}
312
+
313
+ ## Note
314
+ The final synthesis could not be completed due to an error: {final_answer}
315
+ This is a fallback response using the accumulated knowledge.
316
+ """
317
+
318
+ # Create a synthesis finding
319
+ finding = {
320
+ "phase": "Final synthesis",
321
+ "content": final_answer,
322
+ "question": query,
323
+ "search_results": [],
324
+ "documents": [],
325
+ }
326
+ findings.append(finding)
327
+
328
+ # Store the synthesized content
329
+ self.findings_repository.add_finding(query + "_synthesis", final_answer)
330
+
331
+ # Update current knowledge with the synthesized version
332
+ current_knowledge = final_answer
333
+ except Exception as e:
334
+ logger.error(f"Error synthesizing final answer: {str(e)}")
335
+ import traceback
336
+
337
+ logger.error(traceback.format_exc())
338
+
339
+ # Create an error finding
340
+ error_finding = {
341
+ "phase": "Final synthesis error",
342
+ "content": f"Error during synthesis: {str(e)}",
343
+ "question": query,
344
+ "search_results": [],
345
+ "documents": [],
346
+ }
347
+ findings.append(error_finding)
348
+
349
+ # If synthesis completely fails, construct a fallback answer from the most relevant findings
350
+ self._update_progress(
351
+ "Synthesis failed. Creating fallback summary...",
352
+ 85,
353
+ {"phase": "synthesis_fallback"},
354
+ )
355
+
356
+ try:
357
+ # Extract best content from findings
358
+ key_findings = []
359
+ for i, f in enumerate(findings):
360
+ if isinstance(f, dict) and "content" in f and f.get("content"):
361
+ # Only take the first 500 chars of each finding for the fallback
362
+ content_preview = f.get("content", "")[:500]
363
+ if content_preview:
364
+ key_findings.append(
365
+ f"### Finding {i + 1}\n\n{content_preview}..."
366
+ )
367
+
368
+ # Create fallback content
369
+ fallback_content = f"""
370
+ # Research Results (Error Recovery Mode)
371
+
372
+ ## Original Query
373
+ {query}
374
+
375
+ ## Key Findings
376
+ {chr(10).join(key_findings[:5]) if key_findings else "No valid findings were generated."}
377
+
378
+ ## Error Information
379
+ The system encountered an error during final synthesis: {str(e)}
380
+ This is an automatically generated fallback response.
381
+ """
382
+
383
+ final_answer = fallback_content
384
+ except Exception as fallback_error:
385
+ # Last resort fallback
386
+ logger.error(f"Even fallback creation failed: {fallback_error}")
387
+ final_answer = f"""
388
+ # Research Error
389
+
390
+ The system encountered multiple errors while processing your query: "{query}"
391
+
392
+ Primary error: {str(e)}
393
+ Fallback error: {str(fallback_error)}
394
+
395
+ Please try again with a different query or contact support.
396
+ """
397
+
398
+ # Compress knowledge if needed
399
+ if (
400
+ get_db_setting(
401
+ "general.knowledge_accumulation",
402
+ settings.general.knowledge_accumulation,
403
+ )
404
+ == "ITERATION"
405
+ ):
406
+ try:
407
+ self._update_progress(
408
+ "Compressing knowledge", 90, {"phase": "knowledge_compression"}
409
+ )
410
+ current_knowledge = self.knowledge_generator.compress_knowledge(
411
+ current_knowledge, query, section_links
412
+ )
413
+ except Exception as e:
414
+ logger.error(f"Error compressing knowledge: {str(e)}")
415
+
416
+ # Format and save findings
417
+ self._update_progress(
418
+ "Formatting and saving findings", 95, {"phase": "formatting"}
419
+ )
420
+
421
+ try:
422
+ # First, get just the synthesized content without formatting
423
+ if not isinstance(final_answer, str):
424
+ logger.error(
425
+ "final_answer is not a string, using current_knowledge as fallback"
426
+ )
427
+ final_answer = current_knowledge
428
+
429
+ # Ensure latest questions are in the repository
430
+ self.findings_repository.set_questions_by_iteration(
431
+ self.questions_by_iteration
432
+ )
433
+
434
+ # Now format the findings with search questions and sources
435
+ formatted_findings = self.findings_repository.format_findings_to_text(
436
+ findings, final_answer
437
+ )
438
+ except Exception as e:
439
+ logger.error(f"Error formatting final findings: {str(e)}")
440
+ formatted_findings = "Error: Could not format findings due to an error."
441
+
442
+ self._update_progress("Research complete", 100, {"phase": "complete"})
443
+
444
+ return {
445
+ "findings": findings, # Keep the detailed findings list
446
+ "iterations": 1,
447
+ "questions": self.questions_by_iteration, # Use the member variable instead of {"0": sub_queries}
448
+ "formatted_findings": formatted_findings, # This is the fully formatted string for UI
449
+ "current_knowledge": current_knowledge, # This is the synthesized content for knowledge base
450
+ }