local-deep-research 0.4.4__py3-none-any.whl → 0.5.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 (220) hide show
  1. local_deep_research/__init__.py +7 -0
  2. local_deep_research/__version__.py +1 -1
  3. local_deep_research/advanced_search_system/answer_decoding/__init__.py +5 -0
  4. local_deep_research/advanced_search_system/answer_decoding/browsecomp_answer_decoder.py +421 -0
  5. local_deep_research/advanced_search_system/candidate_exploration/README.md +219 -0
  6. local_deep_research/advanced_search_system/candidate_exploration/__init__.py +25 -0
  7. local_deep_research/advanced_search_system/candidate_exploration/adaptive_explorer.py +329 -0
  8. local_deep_research/advanced_search_system/candidate_exploration/base_explorer.py +341 -0
  9. local_deep_research/advanced_search_system/candidate_exploration/constraint_guided_explorer.py +436 -0
  10. local_deep_research/advanced_search_system/candidate_exploration/diversity_explorer.py +457 -0
  11. local_deep_research/advanced_search_system/candidate_exploration/parallel_explorer.py +250 -0
  12. local_deep_research/advanced_search_system/candidate_exploration/progressive_explorer.py +255 -0
  13. local_deep_research/advanced_search_system/candidates/__init__.py +5 -0
  14. local_deep_research/advanced_search_system/candidates/base_candidate.py +59 -0
  15. local_deep_research/advanced_search_system/constraint_checking/README.md +150 -0
  16. local_deep_research/advanced_search_system/constraint_checking/__init__.py +35 -0
  17. local_deep_research/advanced_search_system/constraint_checking/base_constraint_checker.py +122 -0
  18. local_deep_research/advanced_search_system/constraint_checking/constraint_checker.py +223 -0
  19. local_deep_research/advanced_search_system/constraint_checking/constraint_satisfaction_tracker.py +387 -0
  20. local_deep_research/advanced_search_system/constraint_checking/dual_confidence_checker.py +424 -0
  21. local_deep_research/advanced_search_system/constraint_checking/evidence_analyzer.py +174 -0
  22. local_deep_research/advanced_search_system/constraint_checking/intelligent_constraint_relaxer.py +503 -0
  23. local_deep_research/advanced_search_system/constraint_checking/rejection_engine.py +143 -0
  24. local_deep_research/advanced_search_system/constraint_checking/strict_checker.py +259 -0
  25. local_deep_research/advanced_search_system/constraint_checking/threshold_checker.py +213 -0
  26. local_deep_research/advanced_search_system/constraints/__init__.py +6 -0
  27. local_deep_research/advanced_search_system/constraints/base_constraint.py +58 -0
  28. local_deep_research/advanced_search_system/constraints/constraint_analyzer.py +143 -0
  29. local_deep_research/advanced_search_system/evidence/__init__.py +12 -0
  30. local_deep_research/advanced_search_system/evidence/base_evidence.py +57 -0
  31. local_deep_research/advanced_search_system/evidence/evaluator.py +159 -0
  32. local_deep_research/advanced_search_system/evidence/requirements.py +122 -0
  33. local_deep_research/advanced_search_system/filters/base_filter.py +3 -1
  34. local_deep_research/advanced_search_system/filters/cross_engine_filter.py +8 -2
  35. local_deep_research/advanced_search_system/filters/journal_reputation_filter.py +43 -29
  36. local_deep_research/advanced_search_system/findings/repository.py +54 -17
  37. local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +3 -1
  38. local_deep_research/advanced_search_system/query_generation/adaptive_query_generator.py +405 -0
  39. local_deep_research/advanced_search_system/questions/__init__.py +16 -0
  40. local_deep_research/advanced_search_system/questions/atomic_fact_question.py +171 -0
  41. local_deep_research/advanced_search_system/questions/browsecomp_question.py +287 -0
  42. local_deep_research/advanced_search_system/questions/decomposition_question.py +13 -4
  43. local_deep_research/advanced_search_system/questions/entity_aware_question.py +184 -0
  44. local_deep_research/advanced_search_system/questions/standard_question.py +9 -3
  45. local_deep_research/advanced_search_system/search_optimization/cross_constraint_manager.py +624 -0
  46. local_deep_research/advanced_search_system/source_management/diversity_manager.py +613 -0
  47. local_deep_research/advanced_search_system/strategies/__init__.py +42 -0
  48. local_deep_research/advanced_search_system/strategies/adaptive_decomposition_strategy.py +564 -0
  49. local_deep_research/advanced_search_system/strategies/base_strategy.py +4 -4
  50. local_deep_research/advanced_search_system/strategies/browsecomp_entity_strategy.py +1031 -0
  51. local_deep_research/advanced_search_system/strategies/browsecomp_optimized_strategy.py +778 -0
  52. local_deep_research/advanced_search_system/strategies/concurrent_dual_confidence_strategy.py +446 -0
  53. local_deep_research/advanced_search_system/strategies/constrained_search_strategy.py +1348 -0
  54. local_deep_research/advanced_search_system/strategies/constraint_parallel_strategy.py +522 -0
  55. local_deep_research/advanced_search_system/strategies/direct_search_strategy.py +217 -0
  56. local_deep_research/advanced_search_system/strategies/dual_confidence_strategy.py +320 -0
  57. local_deep_research/advanced_search_system/strategies/dual_confidence_with_rejection.py +219 -0
  58. local_deep_research/advanced_search_system/strategies/early_stop_constrained_strategy.py +369 -0
  59. local_deep_research/advanced_search_system/strategies/entity_aware_source_strategy.py +140 -0
  60. local_deep_research/advanced_search_system/strategies/evidence_based_strategy.py +1248 -0
  61. local_deep_research/advanced_search_system/strategies/evidence_based_strategy_v2.py +1337 -0
  62. local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py +537 -0
  63. local_deep_research/advanced_search_system/strategies/improved_evidence_based_strategy.py +782 -0
  64. local_deep_research/advanced_search_system/strategies/iterative_reasoning_strategy.py +760 -0
  65. local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +55 -21
  66. local_deep_research/advanced_search_system/strategies/llm_driven_modular_strategy.py +865 -0
  67. local_deep_research/advanced_search_system/strategies/modular_strategy.py +1142 -0
  68. local_deep_research/advanced_search_system/strategies/parallel_constrained_strategy.py +506 -0
  69. local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +34 -16
  70. local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +29 -9
  71. local_deep_research/advanced_search_system/strategies/recursive_decomposition_strategy.py +492 -0
  72. local_deep_research/advanced_search_system/strategies/smart_decomposition_strategy.py +284 -0
  73. local_deep_research/advanced_search_system/strategies/smart_query_strategy.py +515 -0
  74. local_deep_research/advanced_search_system/strategies/source_based_strategy.py +48 -24
  75. local_deep_research/advanced_search_system/strategies/standard_strategy.py +34 -14
  76. local_deep_research/advanced_search_system/tools/base_tool.py +7 -2
  77. local_deep_research/api/benchmark_functions.py +6 -2
  78. local_deep_research/api/research_functions.py +10 -4
  79. local_deep_research/benchmarks/__init__.py +9 -7
  80. local_deep_research/benchmarks/benchmark_functions.py +6 -2
  81. local_deep_research/benchmarks/cli/benchmark_commands.py +27 -10
  82. local_deep_research/benchmarks/cli.py +38 -13
  83. local_deep_research/benchmarks/comparison/__init__.py +4 -2
  84. local_deep_research/benchmarks/comparison/evaluator.py +316 -239
  85. local_deep_research/benchmarks/datasets/__init__.py +1 -1
  86. local_deep_research/benchmarks/datasets/base.py +91 -72
  87. local_deep_research/benchmarks/datasets/browsecomp.py +54 -33
  88. local_deep_research/benchmarks/datasets/custom_dataset_template.py +19 -19
  89. local_deep_research/benchmarks/datasets/simpleqa.py +14 -14
  90. local_deep_research/benchmarks/datasets/utils.py +48 -29
  91. local_deep_research/benchmarks/datasets.py +4 -11
  92. local_deep_research/benchmarks/efficiency/__init__.py +8 -4
  93. local_deep_research/benchmarks/efficiency/resource_monitor.py +223 -171
  94. local_deep_research/benchmarks/efficiency/speed_profiler.py +62 -48
  95. local_deep_research/benchmarks/evaluators/browsecomp.py +3 -1
  96. local_deep_research/benchmarks/evaluators/composite.py +6 -2
  97. local_deep_research/benchmarks/evaluators/simpleqa.py +36 -13
  98. local_deep_research/benchmarks/graders.py +32 -10
  99. local_deep_research/benchmarks/metrics/README.md +1 -1
  100. local_deep_research/benchmarks/metrics/calculation.py +25 -10
  101. local_deep_research/benchmarks/metrics/reporting.py +7 -3
  102. local_deep_research/benchmarks/metrics/visualization.py +42 -23
  103. local_deep_research/benchmarks/metrics.py +1 -1
  104. local_deep_research/benchmarks/optimization/__init__.py +3 -1
  105. local_deep_research/benchmarks/optimization/api.py +7 -1
  106. local_deep_research/benchmarks/optimization/optuna_optimizer.py +75 -26
  107. local_deep_research/benchmarks/runners.py +48 -15
  108. local_deep_research/citation_handler.py +65 -92
  109. local_deep_research/citation_handlers/__init__.py +15 -0
  110. local_deep_research/citation_handlers/base_citation_handler.py +70 -0
  111. local_deep_research/citation_handlers/forced_answer_citation_handler.py +179 -0
  112. local_deep_research/citation_handlers/precision_extraction_handler.py +550 -0
  113. local_deep_research/citation_handlers/standard_citation_handler.py +80 -0
  114. local_deep_research/config/llm_config.py +271 -169
  115. local_deep_research/config/search_config.py +14 -5
  116. local_deep_research/defaults/__init__.py +0 -1
  117. local_deep_research/metrics/__init__.py +13 -0
  118. local_deep_research/metrics/database.py +58 -0
  119. local_deep_research/metrics/db_models.py +115 -0
  120. local_deep_research/metrics/migrate_add_provider_to_token_usage.py +148 -0
  121. local_deep_research/metrics/migrate_call_stack_tracking.py +105 -0
  122. local_deep_research/metrics/migrate_enhanced_tracking.py +75 -0
  123. local_deep_research/metrics/migrate_research_ratings.py +31 -0
  124. local_deep_research/metrics/models.py +61 -0
  125. local_deep_research/metrics/pricing/__init__.py +12 -0
  126. local_deep_research/metrics/pricing/cost_calculator.py +237 -0
  127. local_deep_research/metrics/pricing/pricing_cache.py +143 -0
  128. local_deep_research/metrics/pricing/pricing_fetcher.py +240 -0
  129. local_deep_research/metrics/query_utils.py +51 -0
  130. local_deep_research/metrics/search_tracker.py +380 -0
  131. local_deep_research/metrics/token_counter.py +1078 -0
  132. local_deep_research/migrate_db.py +3 -1
  133. local_deep_research/report_generator.py +22 -8
  134. local_deep_research/search_system.py +390 -9
  135. local_deep_research/test_migration.py +15 -5
  136. local_deep_research/utilities/db_utils.py +7 -4
  137. local_deep_research/utilities/es_utils.py +115 -104
  138. local_deep_research/utilities/llm_utils.py +15 -5
  139. local_deep_research/utilities/log_utils.py +151 -0
  140. local_deep_research/utilities/search_cache.py +387 -0
  141. local_deep_research/utilities/search_utilities.py +14 -6
  142. local_deep_research/utilities/threading_utils.py +92 -0
  143. local_deep_research/utilities/url_utils.py +6 -0
  144. local_deep_research/web/api.py +347 -0
  145. local_deep_research/web/app.py +13 -17
  146. local_deep_research/web/app_factory.py +71 -66
  147. local_deep_research/web/database/migrate_to_ldr_db.py +12 -4
  148. local_deep_research/web/database/migrations.py +20 -3
  149. local_deep_research/web/database/models.py +74 -25
  150. local_deep_research/web/database/schema_upgrade.py +49 -29
  151. local_deep_research/web/models/database.py +63 -83
  152. local_deep_research/web/routes/api_routes.py +56 -22
  153. local_deep_research/web/routes/benchmark_routes.py +4 -1
  154. local_deep_research/web/routes/globals.py +22 -0
  155. local_deep_research/web/routes/history_routes.py +71 -46
  156. local_deep_research/web/routes/metrics_routes.py +1155 -0
  157. local_deep_research/web/routes/research_routes.py +192 -54
  158. local_deep_research/web/routes/settings_routes.py +156 -55
  159. local_deep_research/web/services/research_service.py +412 -251
  160. local_deep_research/web/services/resource_service.py +36 -11
  161. local_deep_research/web/services/settings_manager.py +55 -17
  162. local_deep_research/web/services/settings_service.py +12 -4
  163. local_deep_research/web/services/socket_service.py +295 -188
  164. local_deep_research/web/static/css/custom_dropdown.css +180 -0
  165. local_deep_research/web/static/css/styles.css +39 -1
  166. local_deep_research/web/static/js/components/detail.js +633 -267
  167. local_deep_research/web/static/js/components/details.js +751 -0
  168. local_deep_research/web/static/js/components/fallback/formatting.js +11 -11
  169. local_deep_research/web/static/js/components/fallback/ui.js +23 -23
  170. local_deep_research/web/static/js/components/history.js +76 -76
  171. local_deep_research/web/static/js/components/logpanel.js +61 -13
  172. local_deep_research/web/static/js/components/progress.js +13 -2
  173. local_deep_research/web/static/js/components/research.js +99 -12
  174. local_deep_research/web/static/js/components/results.js +239 -106
  175. local_deep_research/web/static/js/main.js +40 -40
  176. local_deep_research/web/static/js/services/audio.js +1 -1
  177. local_deep_research/web/static/js/services/formatting.js +11 -11
  178. local_deep_research/web/static/js/services/keyboard.js +157 -0
  179. local_deep_research/web/static/js/services/pdf.js +80 -80
  180. local_deep_research/web/static/sounds/README.md +1 -1
  181. local_deep_research/web/templates/base.html +1 -0
  182. local_deep_research/web/templates/components/log_panel.html +7 -1
  183. local_deep_research/web/templates/components/mobile_nav.html +1 -1
  184. local_deep_research/web/templates/components/sidebar.html +3 -0
  185. local_deep_research/web/templates/pages/cost_analytics.html +1245 -0
  186. local_deep_research/web/templates/pages/details.html +325 -24
  187. local_deep_research/web/templates/pages/history.html +1 -1
  188. local_deep_research/web/templates/pages/metrics.html +1929 -0
  189. local_deep_research/web/templates/pages/progress.html +2 -2
  190. local_deep_research/web/templates/pages/research.html +53 -17
  191. local_deep_research/web/templates/pages/results.html +12 -1
  192. local_deep_research/web/templates/pages/star_reviews.html +803 -0
  193. local_deep_research/web/utils/formatters.py +9 -3
  194. local_deep_research/web_search_engines/default_search_engines.py +5 -3
  195. local_deep_research/web_search_engines/engines/full_search.py +8 -2
  196. local_deep_research/web_search_engines/engines/meta_search_engine.py +59 -20
  197. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +19 -6
  198. local_deep_research/web_search_engines/engines/search_engine_brave.py +6 -2
  199. local_deep_research/web_search_engines/engines/search_engine_ddg.py +3 -1
  200. local_deep_research/web_search_engines/engines/search_engine_elasticsearch.py +81 -58
  201. local_deep_research/web_search_engines/engines/search_engine_github.py +46 -15
  202. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +16 -6
  203. local_deep_research/web_search_engines/engines/search_engine_guardian.py +39 -15
  204. local_deep_research/web_search_engines/engines/search_engine_local.py +58 -25
  205. local_deep_research/web_search_engines/engines/search_engine_local_all.py +15 -5
  206. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +63 -21
  207. local_deep_research/web_search_engines/engines/search_engine_searxng.py +37 -11
  208. local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +27 -9
  209. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +12 -4
  210. local_deep_research/web_search_engines/engines/search_engine_wayback.py +31 -10
  211. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +12 -3
  212. local_deep_research/web_search_engines/search_engine_base.py +83 -35
  213. local_deep_research/web_search_engines/search_engine_factory.py +25 -8
  214. local_deep_research/web_search_engines/search_engines_config.py +9 -3
  215. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/METADATA +7 -1
  216. local_deep_research-0.5.2.dist-info/RECORD +265 -0
  217. local_deep_research-0.4.4.dist-info/RECORD +0 -177
  218. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/WHEEL +0 -0
  219. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/entry_points.txt +0 -0
  220. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,446 @@
1
+ """
2
+ Concurrent dual confidence strategy with progressive search.
3
+
4
+ Key features:
5
+ 1. Starts with all constraints combined for maximum specificity
6
+ 2. Evaluates candidates concurrently as they're found
7
+ 3. Progressively loosens constraints if needed
8
+ 4. Uses early rejection from dual confidence
9
+ 5. Dynamic stopping criteria instead of fixed limits
10
+ """
11
+
12
+ import concurrent.futures
13
+ import threading
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from typing import List, Optional, Tuple
17
+
18
+ from loguru import logger
19
+
20
+ from ..candidates.base_candidate import Candidate
21
+ from ..constraints.base_constraint import Constraint, ConstraintType
22
+ from .dual_confidence_with_rejection import DualConfidenceWithRejectionStrategy
23
+
24
+
25
+ @dataclass
26
+ class SearchState:
27
+ """Tracks the current state of the concurrent search."""
28
+
29
+ good_candidates: List[Tuple[Candidate, float]] = field(default_factory=list)
30
+ total_evaluated: int = 0
31
+ start_time: float = field(default_factory=time.time)
32
+ remaining_constraints: List[Constraint] = field(default_factory=list)
33
+ candidates_lock: threading.Lock = field(default_factory=threading.Lock)
34
+ stop_search: threading.Event = field(default_factory=threading.Event)
35
+ evaluation_futures: List[concurrent.futures.Future] = field(
36
+ default_factory=list
37
+ )
38
+
39
+
40
+ class ConcurrentDualConfidenceStrategy(DualConfidenceWithRejectionStrategy):
41
+ """
42
+ Enhanced strategy that combines concurrent evaluation with progressive search.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ *args,
48
+ # Concurrent execution settings
49
+ max_workers: int = 10,
50
+ # Candidate targets
51
+ min_good_candidates: int = 3,
52
+ target_candidates: int = 5,
53
+ max_candidates: int = 10,
54
+ # Quality thresholds
55
+ min_score_threshold: float = 0.65,
56
+ exceptional_score: float = 0.95,
57
+ quality_plateau_threshold: float = 0.1,
58
+ # Time and resource limits
59
+ max_search_time: float = 30.0,
60
+ max_evaluations: int = 30,
61
+ # Search behavior
62
+ initial_search_timeout: float = 5.0,
63
+ **kwargs,
64
+ ):
65
+ super().__init__(*args, **kwargs)
66
+
67
+ # Thread pool for concurrent evaluations
68
+ self.evaluation_executor = concurrent.futures.ThreadPoolExecutor(
69
+ max_workers=max_workers
70
+ )
71
+
72
+ # Candidate thresholds
73
+ self.min_good_candidates = min_good_candidates
74
+ self.target_candidates = target_candidates
75
+ self.max_candidates = max_candidates
76
+
77
+ # Quality settings
78
+ self.min_score_threshold = min_score_threshold
79
+ self.exceptional_score = exceptional_score
80
+ self.quality_plateau_threshold = quality_plateau_threshold
81
+
82
+ # Resource limits
83
+ self.max_search_time = max_search_time
84
+ self.max_evaluations = max_evaluations
85
+ self.initial_search_timeout = initial_search_timeout
86
+
87
+ # Search state
88
+ self.state: Optional[SearchState] = None
89
+
90
+ def find_relevant_information(self):
91
+ """Override to use concurrent search and evaluation."""
92
+ # Initialize state
93
+ self.state = SearchState(
94
+ remaining_constraints=self.constraint_ranking.copy(),
95
+ start_time=time.time(),
96
+ )
97
+
98
+ # Classify constraints by difficulty
99
+ self._classify_constraint_difficulty()
100
+
101
+ # Start progressive search with concurrent evaluation
102
+ try:
103
+ self._progressive_search_with_concurrent_eval()
104
+ finally:
105
+ # Clean up thread pool
106
+ self.evaluation_executor.shutdown(wait=False)
107
+
108
+ # Return best candidates
109
+ self.candidates = [
110
+ c
111
+ for c, _ in sorted(
112
+ self.state.good_candidates, key=lambda x: x[1], reverse=True
113
+ )[: self.max_candidates]
114
+ ]
115
+
116
+ logger.info(
117
+ f"Found {len(self.candidates)} candidates after concurrent search"
118
+ )
119
+
120
+ def _classify_constraint_difficulty(self):
121
+ """Rate constraints by how difficult they are to search for."""
122
+ difficulty_keywords = {
123
+ # Abstract/subjective (very hard)
124
+ "complex": 0.9,
125
+ "deep": 0.9,
126
+ "emotional": 0.9,
127
+ "acclaimed": 0.8,
128
+ "understanding": 0.9,
129
+ "sophisticated": 0.9,
130
+ "nuanced": 0.9,
131
+ # Relative terms (hard)
132
+ "famous": 0.7,
133
+ "popular": 0.7,
134
+ "well-known": 0.7,
135
+ "notable": 0.7,
136
+ # Time ranges (medium)
137
+ "between": 0.5,
138
+ "during": 0.5,
139
+ "era": 0.5,
140
+ "period": 0.5,
141
+ # Specific facts (easy)
142
+ "aired": 0.2,
143
+ "released": 0.2,
144
+ "episode": 0.1,
145
+ "season": 0.1,
146
+ "character": 0.2,
147
+ "show": 0.2,
148
+ "movie": 0.2,
149
+ "series": 0.2,
150
+ # Named entities (easiest)
151
+ "hbo": 0.1,
152
+ "netflix": 0.1,
153
+ "disney": 0.1,
154
+ "marvel": 0.1,
155
+ }
156
+
157
+ for constraint in self.state.remaining_constraints:
158
+ difficulty = 0.5 # Default medium difficulty
159
+
160
+ constraint_lower = constraint.value.lower()
161
+ for keyword, score in difficulty_keywords.items():
162
+ if keyword in constraint_lower:
163
+ difficulty = max(difficulty, score)
164
+
165
+ # Constraint type also affects difficulty
166
+ if constraint.type == ConstraintType.PROPERTY:
167
+ difficulty = min(difficulty, 0.6)
168
+ elif constraint.type == ConstraintType.EVENT:
169
+ difficulty = max(difficulty, 0.7)
170
+
171
+ # Store difficulty as attribute
172
+ constraint.search_difficulty = difficulty
173
+
174
+ # Sort constraints by difficulty (hardest first, so we can drop them first)
175
+ self.state.remaining_constraints.sort(
176
+ key=lambda c: getattr(c, "search_difficulty", 0.5), reverse=True
177
+ )
178
+
179
+ def _progressive_search_with_concurrent_eval(self):
180
+ """Main search loop with concurrent evaluation."""
181
+ iteration = 0
182
+
183
+ while (
184
+ not self.state.stop_search.is_set()
185
+ and self.state.remaining_constraints
186
+ ):
187
+ iteration += 1
188
+
189
+ # Build query from current constraints
190
+ query = self._build_combined_query(self.state.remaining_constraints)
191
+
192
+ logger.info(
193
+ f"Search iteration {iteration}: {len(self.state.remaining_constraints)} constraints"
194
+ )
195
+
196
+ if self.progress_callback:
197
+ self.progress_callback(
198
+ f"Searching with {len(self.state.remaining_constraints)} constraints",
199
+ min(20 + (iteration * 10), 80),
200
+ {
201
+ "phase": "concurrent_search",
202
+ "iteration": iteration,
203
+ "constraints": len(self.state.remaining_constraints),
204
+ "good_candidates": len(self.state.good_candidates),
205
+ },
206
+ )
207
+
208
+ # Execute search
209
+ try:
210
+ search_results = self._execute_search(query)
211
+ new_candidates = self._extract_relevant_candidates(
212
+ search_results,
213
+ self.state.remaining_constraints[
214
+ 0
215
+ ], # Use most important constraint
216
+ )
217
+
218
+ logger.info(
219
+ f"Found {len(new_candidates)} candidates in iteration {iteration}"
220
+ )
221
+
222
+ # Spawn evaluation threads for each candidate
223
+ for candidate in new_candidates:
224
+ if self.state.stop_search.is_set():
225
+ break
226
+
227
+ if self.state.total_evaluated >= self.max_evaluations:
228
+ logger.info("Reached maximum evaluations limit")
229
+ self.state.stop_search.set()
230
+ break
231
+
232
+ # Check if we already evaluated this candidate
233
+ if self._is_candidate_evaluated(candidate):
234
+ continue
235
+
236
+ # Submit for evaluation
237
+ future = self.evaluation_executor.submit(
238
+ self._evaluate_candidate_thread, candidate
239
+ )
240
+ self.state.evaluation_futures.append(future)
241
+ self.state.total_evaluated += 1
242
+
243
+ except Exception as e:
244
+ logger.error(f"Search error in iteration {iteration}: {e}")
245
+
246
+ # Check completed evaluations
247
+ self._check_evaluation_results()
248
+
249
+ # Determine if we should stop
250
+ if self._should_stop_search():
251
+ logger.info("Stopping criteria met")
252
+ break
253
+
254
+ # Drop hardest constraint if we haven't found enough
255
+ if len(self.state.good_candidates) < self.min_good_candidates:
256
+ if len(self.state.remaining_constraints) > 1:
257
+ dropped = self.state.remaining_constraints.pop(
258
+ 0
259
+ ) # Remove hardest
260
+ logger.info(
261
+ f"Dropping constraint: {dropped.value} (difficulty: {getattr(dropped, 'search_difficulty', 0.5):.2f})"
262
+ )
263
+ else:
264
+ logger.info("No more constraints to drop")
265
+ break
266
+
267
+ # Wait for remaining evaluations
268
+ self._finalize_evaluations()
269
+
270
+ def _build_combined_query(self, constraints: List[Constraint]) -> str:
271
+ """Build a query combining all constraints with AND logic."""
272
+ terms = []
273
+
274
+ for constraint in constraints:
275
+ value = constraint.value
276
+
277
+ # Quote multi-word values only if they don't already have quotes
278
+ if " " in value and '"' not in value:
279
+ value = f'"{value}"'
280
+
281
+ terms.append(value)
282
+
283
+ return " ".join(
284
+ terms
285
+ ) # Use space instead of AND for more natural queries
286
+
287
+ def _evaluate_candidate_thread(
288
+ self, candidate: Candidate
289
+ ) -> Tuple[Candidate, float]:
290
+ """Evaluate a candidate in a separate thread."""
291
+ try:
292
+ thread_name = threading.current_thread().name
293
+ logger.info(
294
+ f"[{thread_name}] Starting evaluation of {candidate.name}"
295
+ )
296
+
297
+ # Use parent's evaluation with early rejection
298
+ score = self._evaluate_candidate_immediately(candidate)
299
+
300
+ # Log result
301
+ if score >= self.min_score_threshold:
302
+ logger.info(
303
+ f"[{thread_name}] ✓ {candidate.name} passed (score: {score:.3f})"
304
+ )
305
+
306
+ # Add to good candidates
307
+ with self.state.candidates_lock:
308
+ self.state.good_candidates.append((candidate, score))
309
+
310
+ # Check if we should stop
311
+ if self._should_stop_search():
312
+ logger.info("Stopping criteria met after evaluation")
313
+ self.state.stop_search.set()
314
+ else:
315
+ logger.info(
316
+ f"[{thread_name}] ❌ {candidate.name} rejected (score: {score:.3f})"
317
+ )
318
+
319
+ return (candidate, score)
320
+
321
+ except Exception as e:
322
+ logger.error(
323
+ f"Error evaluating {candidate.name}: {e}", exc_info=True
324
+ )
325
+ return (candidate, 0.0)
326
+
327
+ def _check_evaluation_results(self):
328
+ """Check completed evaluation futures without blocking."""
329
+ completed = []
330
+
331
+ for future in self.state.evaluation_futures:
332
+ if future.done():
333
+ completed.append(future)
334
+ try:
335
+ future.result()
336
+ # Result is already processed in the thread
337
+ except Exception as e:
338
+ logger.error(f"Failed to get future result: {e}")
339
+
340
+ # Remove completed futures
341
+ for future in completed:
342
+ self.state.evaluation_futures.remove(future)
343
+
344
+ def _should_stop_search(self) -> bool:
345
+ """Determine if we should stop searching based on multiple criteria."""
346
+ # Always respect the stop flag
347
+ if self.state.stop_search.is_set():
348
+ return True
349
+
350
+ num_good = len(self.state.good_candidates)
351
+
352
+ # 1. Maximum candidates reached
353
+ if num_good >= self.max_candidates:
354
+ logger.info(f"Maximum candidates reached ({self.max_candidates})")
355
+ return True
356
+
357
+ # 2. Target reached with good quality
358
+ if num_good >= self.target_candidates:
359
+ avg_score = sum(s for _, s in self.state.good_candidates) / num_good
360
+ if avg_score >= 0.8:
361
+ logger.info(
362
+ f"Target reached with high quality (avg: {avg_score:.3f})"
363
+ )
364
+ return True
365
+
366
+ # 3. Minimum satisfied with exceptional candidates
367
+ if num_good >= self.min_good_candidates:
368
+ top_score = max(s for _, s in self.state.good_candidates)
369
+ if top_score >= self.exceptional_score:
370
+ logger.info(
371
+ f"Exceptional candidate found (score: {top_score:.3f})"
372
+ )
373
+ return True
374
+
375
+ # 4. Time limit reached
376
+ elapsed = time.time() - self.state.start_time
377
+ if elapsed > self.max_search_time:
378
+ logger.info(f"Time limit reached ({elapsed:.1f}s)")
379
+ return True
380
+
381
+ # 5. Too many evaluations
382
+ if self.state.total_evaluated >= self.max_evaluations:
383
+ logger.info(f"Evaluation limit reached ({self.max_evaluations})")
384
+ return True
385
+
386
+ # 6. No more constraints and sufficient candidates
387
+ if (
388
+ not self.state.remaining_constraints
389
+ and num_good >= self.min_good_candidates
390
+ ):
391
+ logger.info("No more constraints and minimum candidates found")
392
+ return True
393
+
394
+ # 7. Quality plateau detection
395
+ if num_good >= 5:
396
+ recent_scores = [s for _, s in self.state.good_candidates[-5:]]
397
+ score_range = max(recent_scores) - min(recent_scores)
398
+ if score_range < self.quality_plateau_threshold:
399
+ logger.info(
400
+ f"Quality plateau detected (range: {score_range:.3f})"
401
+ )
402
+ return True
403
+
404
+ return False
405
+
406
+ def _is_candidate_evaluated(self, candidate: Candidate) -> bool:
407
+ """Check if we already evaluated this candidate."""
408
+ with self.state.candidates_lock:
409
+ return any(
410
+ c.name == candidate.name for c, _ in self.state.good_candidates
411
+ )
412
+
413
+ def _finalize_evaluations(self):
414
+ """Wait for or cancel remaining evaluations."""
415
+ if self.state.evaluation_futures:
416
+ logger.info(
417
+ f"Finalizing {len(self.state.evaluation_futures)} remaining evaluations"
418
+ )
419
+
420
+ # Give them a short time to complete
421
+ wait_time = min(
422
+ 5.0,
423
+ self.max_search_time - (time.time() - self.state.start_time),
424
+ )
425
+ if wait_time > 0:
426
+ concurrent.futures.wait(
427
+ self.state.evaluation_futures,
428
+ timeout=wait_time,
429
+ return_when=concurrent.futures.FIRST_COMPLETED,
430
+ )
431
+
432
+ # Cancel any still running
433
+ for future in self.state.evaluation_futures:
434
+ if not future.done():
435
+ future.cancel()
436
+
437
+ # Final report
438
+ logger.info(
439
+ f"""
440
+ Search completed:
441
+ - Total evaluated: {self.state.total_evaluated}
442
+ - Good candidates found: {len(self.state.good_candidates)}
443
+ - Time taken: {time.time() - self.state.start_time:.1f}s
444
+ - Final constraints: {len(self.state.remaining_constraints)}
445
+ """
446
+ )