Coverage for src/local_deep_research/search_system_factory.py: 90%
75 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-20 01:24 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-20 01:24 +0000
1"""
2Factory for creating search strategies.
3This module provides a centralized way to create search strategies
4to avoid code duplication.
5"""
7from loguru import logger
8from typing import Optional, Dict, Any, List
9from langchain_core.language_models import BaseChatModel
11from .utilities.type_utils import unwrap_setting
13# Re-export from constants so existing importers don't break
14from .constants import ( # noqa: F401
15 AVAILABLE_STRATEGIES,
16 get_available_strategies,
17)
20def _get_setting(
21 settings_snapshot: Optional[Dict], key: str, default: Any
22) -> Any:
23 """Get a setting value from the snapshot, handling nested dict structure."""
24 if not settings_snapshot or key not in settings_snapshot:
25 return default
26 value = settings_snapshot[key]
27 return unwrap_setting(value)
30def create_strategy(
31 strategy_name: str,
32 model: BaseChatModel,
33 search: Any,
34 all_links_of_system: Optional[List[Dict]] = None,
35 settings_snapshot: Optional[Dict] = None,
36 research_context: Optional[Dict] = None,
37 **kwargs,
38):
39 """
40 Create a search strategy by name.
42 Args:
43 strategy_name: Name of the strategy to create
44 model: Language model to use
45 search: Search engine instance
46 all_links_of_system: List of existing links
47 settings_snapshot: Settings snapshot
48 research_context: Research context for special strategies
49 **kwargs: Additional strategy-specific parameters
51 Returns:
52 Strategy instance
53 """
54 if all_links_of_system is None:
55 all_links_of_system = []
57 strategy_name_lower = strategy_name.lower()
59 # Source-based strategy
60 if strategy_name_lower in [
61 "source-based",
62 "source_based",
63 "source_based_search",
64 ]:
65 from .advanced_search_system.strategies.source_based_strategy import (
66 SourceBasedSearchStrategy,
67 )
69 return SourceBasedSearchStrategy(
70 model=model,
71 search=search,
72 include_text_content=kwargs.get("include_text_content", True),
73 use_cross_engine_filter=kwargs.get("use_cross_engine_filter", True),
74 all_links_of_system=all_links_of_system,
75 use_atomic_facts=kwargs.get("use_atomic_facts", False),
76 settings_snapshot=settings_snapshot,
77 search_original_query=kwargs.get("search_original_query", True),
78 )
80 # Focused iteration strategy
81 if strategy_name_lower in ["focused-iteration", "focused_iteration"]:
82 from .advanced_search_system.strategies.focused_iteration_strategy import (
83 FocusedIterationStrategy,
84 )
86 # Read focused_iteration settings with kwargs override
87 # adaptive_questions is stored as 0/1 integer, convert to bool
88 enable_adaptive = bool(
89 kwargs.get(
90 "enable_adaptive_questions",
91 _get_setting(
92 settings_snapshot, "focused_iteration.adaptive_questions", 0
93 ),
94 )
95 )
96 knowledge_limit = kwargs.get(
97 "knowledge_summary_limit",
98 _get_setting(
99 settings_snapshot,
100 "focused_iteration.knowledge_summary_limit",
101 10,
102 ),
103 )
104 snippet_truncate = kwargs.get(
105 "knowledge_snippet_truncate",
106 _get_setting(
107 settings_snapshot, "focused_iteration.snippet_truncate", 200
108 ),
109 )
110 question_gen_type = kwargs.get(
111 "question_generator",
112 _get_setting(
113 settings_snapshot,
114 "focused_iteration.question_generator",
115 "browsecomp",
116 ),
117 )
118 prompt_knowledge_truncate = kwargs.get(
119 "prompt_knowledge_truncate",
120 _get_setting(
121 settings_snapshot,
122 "focused_iteration.prompt_knowledge_truncate",
123 1500,
124 ),
125 )
126 previous_searches_limit = kwargs.get(
127 "previous_searches_limit",
128 _get_setting(
129 settings_snapshot,
130 "focused_iteration.previous_searches_limit",
131 10,
132 ),
133 )
134 # Convert 0 to None for "unlimited"
135 if knowledge_limit == 0:
136 knowledge_limit = None
137 if snippet_truncate == 0:
138 snippet_truncate = None
139 if prompt_knowledge_truncate == 0:
140 prompt_knowledge_truncate = None
141 if previous_searches_limit == 0:
142 previous_searches_limit = None
144 strategy = FocusedIterationStrategy(
145 model=model,
146 search=search,
147 all_links_of_system=all_links_of_system,
148 max_iterations=kwargs.get("max_iterations", 8),
149 questions_per_iteration=kwargs.get("questions_per_iteration", 5),
150 settings_snapshot=settings_snapshot,
151 # Options read from settings (with kwargs override)
152 enable_adaptive_questions=enable_adaptive,
153 enable_early_termination=kwargs.get(
154 "enable_early_termination", False
155 ),
156 knowledge_summary_limit=knowledge_limit,
157 knowledge_snippet_truncate=snippet_truncate,
158 prompt_knowledge_truncate=prompt_knowledge_truncate,
159 previous_searches_limit=previous_searches_limit,
160 )
162 # Override question generator if flexible is selected
163 if question_gen_type == "flexible":
164 from .advanced_search_system.questions.flexible_browsecomp_question import (
165 FlexibleBrowseCompQuestionGenerator,
166 )
168 # Pass truncation settings to flexible generator
169 strategy.question_generator = FlexibleBrowseCompQuestionGenerator(
170 model,
171 knowledge_truncate_length=prompt_knowledge_truncate,
172 previous_searches_limit=previous_searches_limit,
173 )
175 return strategy
177 # Focused iteration strategy with standard citation handler
178 if strategy_name_lower in [
179 "focused-iteration-standard",
180 "focused_iteration_standard",
181 ]:
182 from .advanced_search_system.strategies.focused_iteration_strategy import (
183 FocusedIterationStrategy,
184 )
185 from .citation_handler import CitationHandler
187 # Use standard citation handler (same question generator as regular focused-iteration)
188 standard_citation_handler = CitationHandler(
189 model, handler_type="standard", settings_snapshot=settings_snapshot
190 )
192 # Read focused_iteration settings with kwargs override
193 # adaptive_questions is stored as 0/1 integer, convert to bool
194 enable_adaptive = bool(
195 kwargs.get(
196 "enable_adaptive_questions",
197 _get_setting(
198 settings_snapshot, "focused_iteration.adaptive_questions", 0
199 ),
200 )
201 )
202 knowledge_limit = kwargs.get(
203 "knowledge_summary_limit",
204 _get_setting(
205 settings_snapshot,
206 "focused_iteration.knowledge_summary_limit",
207 10,
208 ),
209 )
210 snippet_truncate = kwargs.get(
211 "knowledge_snippet_truncate",
212 _get_setting(
213 settings_snapshot, "focused_iteration.snippet_truncate", 200
214 ),
215 )
216 question_gen_type = kwargs.get(
217 "question_generator",
218 _get_setting(
219 settings_snapshot,
220 "focused_iteration.question_generator",
221 "browsecomp",
222 ),
223 )
224 prompt_knowledge_truncate = kwargs.get(
225 "prompt_knowledge_truncate",
226 _get_setting(
227 settings_snapshot,
228 "focused_iteration.prompt_knowledge_truncate",
229 1500,
230 ),
231 )
232 previous_searches_limit = kwargs.get(
233 "previous_searches_limit",
234 _get_setting(
235 settings_snapshot,
236 "focused_iteration.previous_searches_limit",
237 10,
238 ),
239 )
240 # Convert 0 to None for "unlimited"
241 if knowledge_limit == 0: 241 ↛ 242line 241 didn't jump to line 242 because the condition on line 241 was never true
242 knowledge_limit = None
243 if snippet_truncate == 0: 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true
244 snippet_truncate = None
245 if prompt_knowledge_truncate == 0: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true
246 prompt_knowledge_truncate = None
247 if previous_searches_limit == 0: 247 ↛ 248line 247 didn't jump to line 248 because the condition on line 247 was never true
248 previous_searches_limit = None
250 strategy = FocusedIterationStrategy(
251 model=model,
252 search=search,
253 citation_handler=standard_citation_handler,
254 all_links_of_system=all_links_of_system,
255 max_iterations=kwargs.get("max_iterations", 8),
256 questions_per_iteration=kwargs.get("questions_per_iteration", 5),
257 use_browsecomp_optimization=True, # Keep BrowseComp features
258 settings_snapshot=settings_snapshot,
259 # Options read from settings (with kwargs override)
260 enable_adaptive_questions=enable_adaptive,
261 enable_early_termination=kwargs.get(
262 "enable_early_termination", False
263 ),
264 knowledge_summary_limit=knowledge_limit,
265 knowledge_snippet_truncate=snippet_truncate,
266 prompt_knowledge_truncate=prompt_knowledge_truncate,
267 previous_searches_limit=previous_searches_limit,
268 )
270 # Override question generator if flexible is selected
271 if question_gen_type == "flexible": 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true
272 from .advanced_search_system.questions.flexible_browsecomp_question import (
273 FlexibleBrowseCompQuestionGenerator,
274 )
276 # Pass truncation settings to flexible generator
277 strategy.question_generator = FlexibleBrowseCompQuestionGenerator(
278 model,
279 knowledge_truncate_length=prompt_knowledge_truncate,
280 previous_searches_limit=previous_searches_limit,
281 )
283 return strategy
285 # News aggregation strategy (used internally by the news subsystem)
286 if strategy_name_lower in [
287 "news",
288 "news_aggregation",
289 "news-aggregation",
290 ]:
291 from .advanced_search_system.strategies.news_strategy import (
292 NewsAggregationStrategy,
293 )
295 return NewsAggregationStrategy(
296 model=model,
297 search=search,
298 all_links_of_system=all_links_of_system,
299 )
301 # Topic organization strategy
302 if strategy_name_lower in [
303 "topic-organization",
304 "topic_organization",
305 "topic",
306 ]:
307 from .advanced_search_system.strategies.topic_organization_strategy import (
308 TopicOrganizationStrategy,
309 )
311 return TopicOrganizationStrategy(
312 model=model,
313 search=search,
314 all_links_of_system=all_links_of_system,
315 settings_snapshot=settings_snapshot,
316 min_sources_per_topic=1, # Allow single-source topics
317 use_cross_engine_filter=kwargs.get("use_cross_engine_filter", True),
318 filter_reorder=kwargs.get("filter_reorder", True),
319 filter_reindex=kwargs.get("filter_reindex", True),
320 cross_engine_max_results=kwargs.get( # type: ignore[arg-type]
321 "cross_engine_max_results", None
322 ),
323 search_original_query=kwargs.get("search_original_query", True),
324 max_topics=kwargs.get("max_topics", 5),
325 similarity_threshold=kwargs.get("similarity_threshold", 0.3),
326 use_focused_iteration=kwargs.get("use_focused_iteration", False),
327 enable_refinement=kwargs.get(
328 "enable_refinement", False
329 ), # Disable refinement iterations for now
330 max_refinement_iterations=kwargs.get(
331 "max_refinement_iterations",
332 1, # Set to 1 iteration for faster results
333 ),
334 generate_text=kwargs.get("generate_text", True),
335 )
337 # LangGraph agent strategy (parallel subagent research).
338 # ``mcp`` / ``agentic`` were removed (#4548); they remain here as
339 # deprecated aliases so existing saved settings, queued runs, and API
340 # callers route to the closest successor (langgraph-agent) instead of
341 # the source-based fallback below.
342 if strategy_name_lower in [
343 "langgraph-agent",
344 "langgraph_agent",
345 "mcp",
346 "agentic",
347 ]:
348 if strategy_name_lower in ("mcp", "agentic"):
349 logger.warning(
350 f"Strategy {strategy_name!r} was removed (#4548); "
351 "using 'langgraph-agent' instead."
352 )
353 from .advanced_search_system.strategies.langgraph_agent_strategy import (
354 LangGraphAgentStrategy,
355 )
357 return LangGraphAgentStrategy(
358 model=model,
359 search=search,
360 max_iterations=kwargs.get(
361 "max_iterations",
362 _get_setting(
363 settings_snapshot, "langgraph_agent.max_iterations", 50
364 ),
365 ),
366 max_sub_iterations=kwargs.get(
367 "max_sub_iterations",
368 _get_setting(
369 settings_snapshot, "langgraph_agent.max_sub_iterations", 8
370 ),
371 ),
372 include_sub_research=kwargs.get(
373 "include_sub_research",
374 _get_setting(
375 settings_snapshot,
376 "langgraph_agent.include_sub_research",
377 True,
378 ),
379 ),
380 programmatic_mode=kwargs.get("programmatic_mode", False),
381 all_links_of_system=all_links_of_system,
382 settings_snapshot=settings_snapshot,
383 )
385 # Default to source-based if unknown
386 logger.warning(
387 f"Unknown strategy: {strategy_name}, defaulting to source-based"
388 )
389 from .advanced_search_system.strategies.source_based_strategy import (
390 SourceBasedSearchStrategy,
391 )
393 return SourceBasedSearchStrategy(
394 model=model,
395 search=search,
396 include_text_content=True,
397 use_cross_engine_filter=True,
398 all_links_of_system=all_links_of_system,
399 use_atomic_facts=False,
400 settings_snapshot=settings_snapshot,
401 search_original_query=kwargs.get("search_original_query", True),
402 )