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.
- local_deep_research/__init__.py +7 -0
- local_deep_research/__version__.py +1 -1
- local_deep_research/advanced_search_system/answer_decoding/__init__.py +5 -0
- local_deep_research/advanced_search_system/answer_decoding/browsecomp_answer_decoder.py +421 -0
- local_deep_research/advanced_search_system/candidate_exploration/README.md +219 -0
- local_deep_research/advanced_search_system/candidate_exploration/__init__.py +25 -0
- local_deep_research/advanced_search_system/candidate_exploration/adaptive_explorer.py +329 -0
- local_deep_research/advanced_search_system/candidate_exploration/base_explorer.py +341 -0
- local_deep_research/advanced_search_system/candidate_exploration/constraint_guided_explorer.py +436 -0
- local_deep_research/advanced_search_system/candidate_exploration/diversity_explorer.py +457 -0
- local_deep_research/advanced_search_system/candidate_exploration/parallel_explorer.py +250 -0
- local_deep_research/advanced_search_system/candidate_exploration/progressive_explorer.py +255 -0
- local_deep_research/advanced_search_system/candidates/__init__.py +5 -0
- local_deep_research/advanced_search_system/candidates/base_candidate.py +59 -0
- local_deep_research/advanced_search_system/constraint_checking/README.md +150 -0
- local_deep_research/advanced_search_system/constraint_checking/__init__.py +35 -0
- local_deep_research/advanced_search_system/constraint_checking/base_constraint_checker.py +122 -0
- local_deep_research/advanced_search_system/constraint_checking/constraint_checker.py +223 -0
- local_deep_research/advanced_search_system/constraint_checking/constraint_satisfaction_tracker.py +387 -0
- local_deep_research/advanced_search_system/constraint_checking/dual_confidence_checker.py +424 -0
- local_deep_research/advanced_search_system/constraint_checking/evidence_analyzer.py +174 -0
- local_deep_research/advanced_search_system/constraint_checking/intelligent_constraint_relaxer.py +503 -0
- local_deep_research/advanced_search_system/constraint_checking/rejection_engine.py +143 -0
- local_deep_research/advanced_search_system/constraint_checking/strict_checker.py +259 -0
- local_deep_research/advanced_search_system/constraint_checking/threshold_checker.py +213 -0
- local_deep_research/advanced_search_system/constraints/__init__.py +6 -0
- local_deep_research/advanced_search_system/constraints/base_constraint.py +58 -0
- local_deep_research/advanced_search_system/constraints/constraint_analyzer.py +143 -0
- local_deep_research/advanced_search_system/evidence/__init__.py +12 -0
- local_deep_research/advanced_search_system/evidence/base_evidence.py +57 -0
- local_deep_research/advanced_search_system/evidence/evaluator.py +159 -0
- local_deep_research/advanced_search_system/evidence/requirements.py +122 -0
- local_deep_research/advanced_search_system/filters/base_filter.py +3 -1
- local_deep_research/advanced_search_system/filters/cross_engine_filter.py +8 -2
- local_deep_research/advanced_search_system/filters/journal_reputation_filter.py +43 -29
- local_deep_research/advanced_search_system/findings/repository.py +54 -17
- local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +3 -1
- local_deep_research/advanced_search_system/query_generation/adaptive_query_generator.py +405 -0
- local_deep_research/advanced_search_system/questions/__init__.py +16 -0
- local_deep_research/advanced_search_system/questions/atomic_fact_question.py +171 -0
- local_deep_research/advanced_search_system/questions/browsecomp_question.py +287 -0
- local_deep_research/advanced_search_system/questions/decomposition_question.py +13 -4
- local_deep_research/advanced_search_system/questions/entity_aware_question.py +184 -0
- local_deep_research/advanced_search_system/questions/standard_question.py +9 -3
- local_deep_research/advanced_search_system/search_optimization/cross_constraint_manager.py +624 -0
- local_deep_research/advanced_search_system/source_management/diversity_manager.py +613 -0
- local_deep_research/advanced_search_system/strategies/__init__.py +42 -0
- local_deep_research/advanced_search_system/strategies/adaptive_decomposition_strategy.py +564 -0
- local_deep_research/advanced_search_system/strategies/base_strategy.py +4 -4
- local_deep_research/advanced_search_system/strategies/browsecomp_entity_strategy.py +1031 -0
- local_deep_research/advanced_search_system/strategies/browsecomp_optimized_strategy.py +778 -0
- local_deep_research/advanced_search_system/strategies/concurrent_dual_confidence_strategy.py +446 -0
- local_deep_research/advanced_search_system/strategies/constrained_search_strategy.py +1348 -0
- local_deep_research/advanced_search_system/strategies/constraint_parallel_strategy.py +522 -0
- local_deep_research/advanced_search_system/strategies/direct_search_strategy.py +217 -0
- local_deep_research/advanced_search_system/strategies/dual_confidence_strategy.py +320 -0
- local_deep_research/advanced_search_system/strategies/dual_confidence_with_rejection.py +219 -0
- local_deep_research/advanced_search_system/strategies/early_stop_constrained_strategy.py +369 -0
- local_deep_research/advanced_search_system/strategies/entity_aware_source_strategy.py +140 -0
- local_deep_research/advanced_search_system/strategies/evidence_based_strategy.py +1248 -0
- local_deep_research/advanced_search_system/strategies/evidence_based_strategy_v2.py +1337 -0
- local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py +537 -0
- local_deep_research/advanced_search_system/strategies/improved_evidence_based_strategy.py +782 -0
- local_deep_research/advanced_search_system/strategies/iterative_reasoning_strategy.py +760 -0
- local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +55 -21
- local_deep_research/advanced_search_system/strategies/llm_driven_modular_strategy.py +865 -0
- local_deep_research/advanced_search_system/strategies/modular_strategy.py +1142 -0
- local_deep_research/advanced_search_system/strategies/parallel_constrained_strategy.py +506 -0
- local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +34 -16
- local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +29 -9
- local_deep_research/advanced_search_system/strategies/recursive_decomposition_strategy.py +492 -0
- local_deep_research/advanced_search_system/strategies/smart_decomposition_strategy.py +284 -0
- local_deep_research/advanced_search_system/strategies/smart_query_strategy.py +515 -0
- local_deep_research/advanced_search_system/strategies/source_based_strategy.py +48 -24
- local_deep_research/advanced_search_system/strategies/standard_strategy.py +34 -14
- local_deep_research/advanced_search_system/tools/base_tool.py +7 -2
- local_deep_research/api/benchmark_functions.py +6 -2
- local_deep_research/api/research_functions.py +10 -4
- local_deep_research/benchmarks/__init__.py +9 -7
- local_deep_research/benchmarks/benchmark_functions.py +6 -2
- local_deep_research/benchmarks/cli/benchmark_commands.py +27 -10
- local_deep_research/benchmarks/cli.py +38 -13
- local_deep_research/benchmarks/comparison/__init__.py +4 -2
- local_deep_research/benchmarks/comparison/evaluator.py +316 -239
- local_deep_research/benchmarks/datasets/__init__.py +1 -1
- local_deep_research/benchmarks/datasets/base.py +91 -72
- local_deep_research/benchmarks/datasets/browsecomp.py +54 -33
- local_deep_research/benchmarks/datasets/custom_dataset_template.py +19 -19
- local_deep_research/benchmarks/datasets/simpleqa.py +14 -14
- local_deep_research/benchmarks/datasets/utils.py +48 -29
- local_deep_research/benchmarks/datasets.py +4 -11
- local_deep_research/benchmarks/efficiency/__init__.py +8 -4
- local_deep_research/benchmarks/efficiency/resource_monitor.py +223 -171
- local_deep_research/benchmarks/efficiency/speed_profiler.py +62 -48
- local_deep_research/benchmarks/evaluators/browsecomp.py +3 -1
- local_deep_research/benchmarks/evaluators/composite.py +6 -2
- local_deep_research/benchmarks/evaluators/simpleqa.py +36 -13
- local_deep_research/benchmarks/graders.py +32 -10
- local_deep_research/benchmarks/metrics/README.md +1 -1
- local_deep_research/benchmarks/metrics/calculation.py +25 -10
- local_deep_research/benchmarks/metrics/reporting.py +7 -3
- local_deep_research/benchmarks/metrics/visualization.py +42 -23
- local_deep_research/benchmarks/metrics.py +1 -1
- local_deep_research/benchmarks/optimization/__init__.py +3 -1
- local_deep_research/benchmarks/optimization/api.py +7 -1
- local_deep_research/benchmarks/optimization/optuna_optimizer.py +75 -26
- local_deep_research/benchmarks/runners.py +48 -15
- local_deep_research/citation_handler.py +65 -92
- local_deep_research/citation_handlers/__init__.py +15 -0
- local_deep_research/citation_handlers/base_citation_handler.py +70 -0
- local_deep_research/citation_handlers/forced_answer_citation_handler.py +179 -0
- local_deep_research/citation_handlers/precision_extraction_handler.py +550 -0
- local_deep_research/citation_handlers/standard_citation_handler.py +80 -0
- local_deep_research/config/llm_config.py +271 -169
- local_deep_research/config/search_config.py +14 -5
- local_deep_research/defaults/__init__.py +0 -1
- local_deep_research/metrics/__init__.py +13 -0
- local_deep_research/metrics/database.py +58 -0
- local_deep_research/metrics/db_models.py +115 -0
- local_deep_research/metrics/migrate_add_provider_to_token_usage.py +148 -0
- local_deep_research/metrics/migrate_call_stack_tracking.py +105 -0
- local_deep_research/metrics/migrate_enhanced_tracking.py +75 -0
- local_deep_research/metrics/migrate_research_ratings.py +31 -0
- local_deep_research/metrics/models.py +61 -0
- local_deep_research/metrics/pricing/__init__.py +12 -0
- local_deep_research/metrics/pricing/cost_calculator.py +237 -0
- local_deep_research/metrics/pricing/pricing_cache.py +143 -0
- local_deep_research/metrics/pricing/pricing_fetcher.py +240 -0
- local_deep_research/metrics/query_utils.py +51 -0
- local_deep_research/metrics/search_tracker.py +380 -0
- local_deep_research/metrics/token_counter.py +1078 -0
- local_deep_research/migrate_db.py +3 -1
- local_deep_research/report_generator.py +22 -8
- local_deep_research/search_system.py +390 -9
- local_deep_research/test_migration.py +15 -5
- local_deep_research/utilities/db_utils.py +7 -4
- local_deep_research/utilities/es_utils.py +115 -104
- local_deep_research/utilities/llm_utils.py +15 -5
- local_deep_research/utilities/log_utils.py +151 -0
- local_deep_research/utilities/search_cache.py +387 -0
- local_deep_research/utilities/search_utilities.py +14 -6
- local_deep_research/utilities/threading_utils.py +92 -0
- local_deep_research/utilities/url_utils.py +6 -0
- local_deep_research/web/api.py +347 -0
- local_deep_research/web/app.py +13 -17
- local_deep_research/web/app_factory.py +71 -66
- local_deep_research/web/database/migrate_to_ldr_db.py +12 -4
- local_deep_research/web/database/migrations.py +20 -3
- local_deep_research/web/database/models.py +74 -25
- local_deep_research/web/database/schema_upgrade.py +49 -29
- local_deep_research/web/models/database.py +63 -83
- local_deep_research/web/routes/api_routes.py +56 -22
- local_deep_research/web/routes/benchmark_routes.py +4 -1
- local_deep_research/web/routes/globals.py +22 -0
- local_deep_research/web/routes/history_routes.py +71 -46
- local_deep_research/web/routes/metrics_routes.py +1155 -0
- local_deep_research/web/routes/research_routes.py +192 -54
- local_deep_research/web/routes/settings_routes.py +156 -55
- local_deep_research/web/services/research_service.py +412 -251
- local_deep_research/web/services/resource_service.py +36 -11
- local_deep_research/web/services/settings_manager.py +55 -17
- local_deep_research/web/services/settings_service.py +12 -4
- local_deep_research/web/services/socket_service.py +295 -188
- local_deep_research/web/static/css/custom_dropdown.css +180 -0
- local_deep_research/web/static/css/styles.css +39 -1
- local_deep_research/web/static/js/components/detail.js +633 -267
- local_deep_research/web/static/js/components/details.js +751 -0
- local_deep_research/web/static/js/components/fallback/formatting.js +11 -11
- local_deep_research/web/static/js/components/fallback/ui.js +23 -23
- local_deep_research/web/static/js/components/history.js +76 -76
- local_deep_research/web/static/js/components/logpanel.js +61 -13
- local_deep_research/web/static/js/components/progress.js +13 -2
- local_deep_research/web/static/js/components/research.js +99 -12
- local_deep_research/web/static/js/components/results.js +239 -106
- local_deep_research/web/static/js/main.js +40 -40
- local_deep_research/web/static/js/services/audio.js +1 -1
- local_deep_research/web/static/js/services/formatting.js +11 -11
- local_deep_research/web/static/js/services/keyboard.js +157 -0
- local_deep_research/web/static/js/services/pdf.js +80 -80
- local_deep_research/web/static/sounds/README.md +1 -1
- local_deep_research/web/templates/base.html +1 -0
- local_deep_research/web/templates/components/log_panel.html +7 -1
- local_deep_research/web/templates/components/mobile_nav.html +1 -1
- local_deep_research/web/templates/components/sidebar.html +3 -0
- local_deep_research/web/templates/pages/cost_analytics.html +1245 -0
- local_deep_research/web/templates/pages/details.html +325 -24
- local_deep_research/web/templates/pages/history.html +1 -1
- local_deep_research/web/templates/pages/metrics.html +1929 -0
- local_deep_research/web/templates/pages/progress.html +2 -2
- local_deep_research/web/templates/pages/research.html +53 -17
- local_deep_research/web/templates/pages/results.html +12 -1
- local_deep_research/web/templates/pages/star_reviews.html +803 -0
- local_deep_research/web/utils/formatters.py +9 -3
- local_deep_research/web_search_engines/default_search_engines.py +5 -3
- local_deep_research/web_search_engines/engines/full_search.py +8 -2
- local_deep_research/web_search_engines/engines/meta_search_engine.py +59 -20
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +19 -6
- local_deep_research/web_search_engines/engines/search_engine_brave.py +6 -2
- local_deep_research/web_search_engines/engines/search_engine_ddg.py +3 -1
- local_deep_research/web_search_engines/engines/search_engine_elasticsearch.py +81 -58
- local_deep_research/web_search_engines/engines/search_engine_github.py +46 -15
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +16 -6
- local_deep_research/web_search_engines/engines/search_engine_guardian.py +39 -15
- local_deep_research/web_search_engines/engines/search_engine_local.py +58 -25
- local_deep_research/web_search_engines/engines/search_engine_local_all.py +15 -5
- local_deep_research/web_search_engines/engines/search_engine_pubmed.py +63 -21
- local_deep_research/web_search_engines/engines/search_engine_searxng.py +37 -11
- local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +27 -9
- local_deep_research/web_search_engines/engines/search_engine_serpapi.py +12 -4
- local_deep_research/web_search_engines/engines/search_engine_wayback.py +31 -10
- local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +12 -3
- local_deep_research/web_search_engines/search_engine_base.py +83 -35
- local_deep_research/web_search_engines/search_engine_factory.py +25 -8
- local_deep_research/web_search_engines/search_engines_config.py +9 -3
- {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/METADATA +7 -1
- local_deep_research-0.5.2.dist-info/RECORD +265 -0
- local_deep_research-0.4.4.dist-info/RECORD +0 -177
- {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/WHEEL +0 -0
- {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|