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,217 @@
1
+ """
2
+ Direct search strategy for focused single-query searches.
3
+
4
+ This strategy is optimized for:
5
+ 1. Entity identification queries (who, what, which)
6
+ 2. Single-pass searches without iteration
7
+ 3. Minimal LLM calls for efficiency
8
+ """
9
+
10
+ import logging
11
+ from typing import Dict
12
+
13
+ from ...citation_handler import CitationHandler
14
+ from ...config.llm_config import get_llm
15
+ from ...config.search_config import get_search
16
+ from ..filters.cross_engine_filter import CrossEngineFilter
17
+ from ..findings.repository import FindingsRepository
18
+ from .base_strategy import BaseSearchStrategy
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class DirectSearchStrategy(BaseSearchStrategy):
24
+ """
25
+ Direct search strategy that performs a single focused search and synthesis.
26
+
27
+ Key features:
28
+ 1. No question generation - uses the original query directly
29
+ 2. Single search pass - no iterations
30
+ 3. Minimal LLM calls - only for final synthesis
31
+ 4. Optimized for entity identification and focused queries
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ search=None,
37
+ model=None,
38
+ citation_handler=None,
39
+ include_text_content: bool = True,
40
+ use_cross_engine_filter: bool = True,
41
+ filter_reorder: bool = True,
42
+ filter_reindex: bool = True,
43
+ cross_engine_max_results: int = None,
44
+ all_links_of_system=None,
45
+ ):
46
+ """Initialize with minimal components for efficiency."""
47
+ super().__init__(all_links_of_system=all_links_of_system)
48
+ self.search = search or get_search()
49
+ self.model = model or get_llm()
50
+ self.progress_callback = None
51
+
52
+ self.include_text_content = include_text_content
53
+ self.use_cross_engine_filter = use_cross_engine_filter
54
+ self.filter_reorder = filter_reorder
55
+ self.filter_reindex = filter_reindex
56
+
57
+ # Initialize the cross-engine filter
58
+ self.cross_engine_filter = CrossEngineFilter(
59
+ model=self.model,
60
+ max_results=cross_engine_max_results,
61
+ default_reorder=filter_reorder,
62
+ default_reindex=filter_reindex,
63
+ )
64
+
65
+ # Use provided citation_handler or create one
66
+ self.citation_handler = citation_handler or CitationHandler(self.model)
67
+ self.findings_repository = FindingsRepository(self.model)
68
+
69
+ def analyze_topic(self, query: str) -> Dict:
70
+ """
71
+ Direct search implementation - single query, single synthesis.
72
+ """
73
+ logger.info(f"Starting direct search on topic: {query}")
74
+ findings = []
75
+ total_citation_count_before_this_search = len(self.all_links_of_system)
76
+
77
+ self._update_progress(
78
+ "Initializing direct search",
79
+ 5,
80
+ {
81
+ "phase": "init",
82
+ "strategy": "direct",
83
+ "query": query[:100],
84
+ },
85
+ )
86
+
87
+ # Check search engine
88
+ if not self._validate_search_engine():
89
+ return {
90
+ "findings": [],
91
+ "iterations": 1,
92
+ "questions_by_iteration": {1: [query]},
93
+ "formatted_findings": "Error: Unable to conduct research without a search engine.",
94
+ "current_knowledge": "",
95
+ "error": "No search engine available",
96
+ }
97
+
98
+ try:
99
+ # Single direct search
100
+ self._update_progress(
101
+ f"Searching: {query[:200]}...",
102
+ 20,
103
+ {"phase": "searching", "query": query},
104
+ )
105
+
106
+ search_results = self.search.run(query)
107
+
108
+ if not search_results:
109
+ search_results = []
110
+
111
+ self._update_progress(
112
+ f"Found {len(search_results)} results",
113
+ 40,
114
+ {
115
+ "phase": "search_complete",
116
+ "result_count": len(search_results),
117
+ },
118
+ )
119
+
120
+ # Optional: Apply cross-engine filter
121
+ if self.use_cross_engine_filter and search_results:
122
+ self._update_progress(
123
+ "Filtering search results",
124
+ 50,
125
+ {"phase": "filtering"},
126
+ )
127
+
128
+ filtered_results = self.cross_engine_filter.filter_results(
129
+ search_results,
130
+ query,
131
+ reorder=self.filter_reorder,
132
+ reindex=self.filter_reindex,
133
+ start_index=len(self.all_links_of_system),
134
+ )
135
+
136
+ self._update_progress(
137
+ f"Filtered to {len(filtered_results)} results",
138
+ 60,
139
+ {
140
+ "phase": "filtering_complete",
141
+ "filtered_count": len(filtered_results),
142
+ },
143
+ )
144
+ else:
145
+ filtered_results = search_results
146
+
147
+ # Add to all links
148
+ self.all_links_of_system.extend(filtered_results)
149
+
150
+ # Final synthesis
151
+ self._update_progress(
152
+ "Generating synthesis",
153
+ 80,
154
+ {"phase": "synthesis"},
155
+ )
156
+
157
+ final_citation_result = self.citation_handler.analyze_followup(
158
+ query,
159
+ filtered_results,
160
+ previous_knowledge="",
161
+ nr_of_links=total_citation_count_before_this_search,
162
+ )
163
+
164
+ if final_citation_result:
165
+ synthesized_content = final_citation_result["content"]
166
+ documents = final_citation_result.get("documents", [])
167
+ else:
168
+ synthesized_content = "No relevant results found."
169
+ documents = []
170
+
171
+ # Create finding
172
+ finding = {
173
+ "phase": "Direct Search",
174
+ "content": synthesized_content,
175
+ "question": query,
176
+ "search_results": filtered_results,
177
+ "documents": documents,
178
+ }
179
+ findings.append(finding)
180
+
181
+ # Add documents to repository
182
+ self.findings_repository.add_documents(documents)
183
+
184
+ # Format findings
185
+ formatted_findings = (
186
+ self.findings_repository.format_findings_to_text(
187
+ findings, synthesized_content
188
+ )
189
+ )
190
+
191
+ except Exception as e:
192
+ import traceback
193
+
194
+ error_msg = f"Error in direct search: {str(e)}"
195
+ logger.error(error_msg)
196
+ logger.error(traceback.format_exc())
197
+ synthesized_content = f"Error: {str(e)}"
198
+ formatted_findings = f"Error: {str(e)}"
199
+ finding = {
200
+ "phase": "Error",
201
+ "content": synthesized_content,
202
+ "question": query,
203
+ "search_results": [],
204
+ "documents": [],
205
+ }
206
+ findings.append(finding)
207
+
208
+ self._update_progress("Search complete", 100, {"phase": "complete"})
209
+
210
+ return {
211
+ "findings": findings,
212
+ "iterations": 1,
213
+ "questions_by_iteration": {1: [query]},
214
+ "formatted_findings": formatted_findings,
215
+ "current_knowledge": synthesized_content,
216
+ "all_links_of_system": self.all_links_of_system,
217
+ }
@@ -0,0 +1,320 @@
1
+ """
2
+ Enhanced strategy with dual confidence scoring (positive/negative) for better accuracy.
3
+ """
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from typing import Dict, List
8
+
9
+ from loguru import logger
10
+
11
+ from ..candidates.base_candidate import Candidate
12
+ from ..constraints.base_constraint import Constraint
13
+ from .smart_query_strategy import SmartQueryStrategy
14
+
15
+
16
+ @dataclass
17
+ class ConstraintEvidence:
18
+ """Evidence for a constraint with dual confidence scores."""
19
+
20
+ positive_confidence: float # How sure we are the constraint IS satisfied
21
+ negative_confidence: (
22
+ float # How sure we are the constraint is NOT satisfied
23
+ )
24
+ uncertainty: float # How uncertain we are (neither positive nor negative)
25
+ evidence_text: str
26
+ source: str
27
+
28
+
29
+ class DualConfidenceStrategy(SmartQueryStrategy):
30
+ """
31
+ Enhanced strategy that uses dual confidence scoring:
32
+ - Positive confidence: Evidence that constraint IS satisfied
33
+ - Negative confidence: Evidence that constraint is NOT satisfied
34
+ - Uncertainty: Lack of clear evidence either way
35
+
36
+ This allows for more accurate scoring and better early stopping decisions.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ *args,
42
+ uncertainty_penalty: float = 0.2,
43
+ negative_weight: float = 0.5,
44
+ **kwargs,
45
+ ):
46
+ super().__init__(*args, **kwargs)
47
+ self.uncertainty_penalty = uncertainty_penalty
48
+ self.negative_weight = negative_weight
49
+
50
+ def _evaluate_evidence(
51
+ self, evidence_list: List[Dict], constraint: Constraint
52
+ ) -> float:
53
+ """Enhanced evidence evaluation with dual confidence."""
54
+ if not evidence_list:
55
+ # No evidence means high uncertainty
56
+ return 0.5 - self.uncertainty_penalty
57
+
58
+ # Convert evidence to dual confidence format
59
+ constraint_evidence = []
60
+ for evidence in evidence_list:
61
+ dual_evidence = self._analyze_evidence_dual_confidence(
62
+ evidence, constraint
63
+ )
64
+ constraint_evidence.append(dual_evidence)
65
+
66
+ # Calculate overall score
67
+ total_positive = sum(e.positive_confidence for e in constraint_evidence)
68
+ total_negative = sum(e.negative_confidence for e in constraint_evidence)
69
+ total_uncertainty = sum(e.uncertainty for e in constraint_evidence)
70
+
71
+ # Normalize
72
+ evidence_count = len(constraint_evidence)
73
+ avg_positive = total_positive / evidence_count
74
+ avg_negative = total_negative / evidence_count
75
+ avg_uncertainty = total_uncertainty / evidence_count
76
+
77
+ # Calculate final score
78
+ # High positive + low negative = high score
79
+ # Low positive + high negative = low score
80
+ # High uncertainty = penalty
81
+ score = (
82
+ avg_positive
83
+ - (avg_negative * self.negative_weight)
84
+ - (avg_uncertainty * self.uncertainty_penalty)
85
+ )
86
+
87
+ # Clamp to [0, 1]
88
+ return max(0.0, min(1.0, score))
89
+
90
+ def _analyze_evidence_dual_confidence(
91
+ self, evidence: Dict, constraint: Constraint
92
+ ) -> ConstraintEvidence:
93
+ """Analyze evidence to extract dual confidence scores."""
94
+ text = evidence.get("text", "")
95
+
96
+ # Use LLM to analyze evidence with dual confidence
97
+ prompt = f"""
98
+ Analyze this evidence for the constraint "{constraint.value}" (type: {constraint.type.value}).
99
+
100
+ Evidence:
101
+ {text[:1000]}
102
+
103
+ Provide three confidence scores (0-1):
104
+ 1. POSITIVE_CONFIDENCE: How confident are you that this constraint IS satisfied?
105
+ 2. NEGATIVE_CONFIDENCE: How confident are you that this constraint is NOT satisfied?
106
+ 3. UNCERTAINTY: How uncertain are you (lack of clear evidence)?
107
+
108
+ The three scores should approximately sum to 1.0.
109
+
110
+ Format:
111
+ POSITIVE: [score]
112
+ NEGATIVE: [score]
113
+ UNCERTAINTY: [score]
114
+ """
115
+
116
+ try:
117
+ response = self.model.invoke(prompt).content
118
+
119
+ # Extract scores
120
+ positive = self._extract_score(response, "POSITIVE")
121
+ negative = self._extract_score(response, "NEGATIVE")
122
+ uncertainty = self._extract_score(response, "UNCERTAINTY")
123
+
124
+ # Normalize if needed
125
+ total = positive + negative + uncertainty
126
+ if total > 0:
127
+ positive /= total
128
+ negative /= total
129
+ uncertainty /= total
130
+ else:
131
+ # Default to high uncertainty
132
+ uncertainty = 0.8
133
+ positive = 0.1
134
+ negative = 0.1
135
+
136
+ return ConstraintEvidence(
137
+ positive_confidence=positive,
138
+ negative_confidence=negative,
139
+ uncertainty=uncertainty,
140
+ evidence_text=text[:500],
141
+ source=evidence.get("source", "search"),
142
+ )
143
+
144
+ except Exception as e:
145
+ logger.error(f"Error analyzing evidence: {e}")
146
+ # Default to high uncertainty
147
+ return ConstraintEvidence(
148
+ positive_confidence=0.1,
149
+ negative_confidence=0.1,
150
+ uncertainty=0.8,
151
+ evidence_text=text[:500],
152
+ source=evidence.get("source", "search"),
153
+ )
154
+
155
+ def _extract_score(self, text: str, label: str) -> float:
156
+ """Extract confidence score from LLM response."""
157
+ pattern = rf"{label}:\s*\[?(\d*\.?\d+)\]?"
158
+ match = re.search(pattern, text, re.IGNORECASE)
159
+ if match:
160
+ try:
161
+ return float(match.group(1))
162
+ except:
163
+ pass
164
+ return 0.1 # Default low score
165
+
166
+ def _gather_evidence_for_constraint(
167
+ self, candidate: Candidate, constraint: Constraint
168
+ ) -> List[Dict]:
169
+ """Enhanced evidence gathering with targeted searches."""
170
+ evidence = []
171
+
172
+ # Build specific queries for this constraint
173
+ queries = [
174
+ f'"{candidate.name}" {constraint.value} verification',
175
+ f'"{candidate.name}" {constraint.value} true or false',
176
+ f"Is {candidate.name} {constraint.value}?",
177
+ ]
178
+
179
+ # For negative evidence, also search for contradictions
180
+ if constraint.type.value == "property":
181
+ queries.append(f'"{candidate.name}" NOT {constraint.value}')
182
+ queries.append(f'"{candidate.name}" opposite of {constraint.value}')
183
+
184
+ # Execute searches
185
+ for query in queries[:3]: # Limit searches
186
+ if query.lower() not in self.searched_queries:
187
+ self.searched_queries.add(query.lower())
188
+ try:
189
+ results = self._execute_search(query)
190
+ content = results.get("current_knowledge", "")
191
+
192
+ if content:
193
+ evidence.append(
194
+ {
195
+ "text": content,
196
+ "source": f"search: {query}",
197
+ "query": query,
198
+ }
199
+ )
200
+ except Exception as e:
201
+ logger.error(f"Error gathering evidence for {query}: {e}")
202
+
203
+ return evidence
204
+
205
+ def _evaluate_candidate_immediately(self, candidate: Candidate) -> float:
206
+ """Enhanced evaluation with detailed constraint scoring."""
207
+ try:
208
+ logger.info(
209
+ f"Evaluating candidate: {candidate.name} with dual confidence"
210
+ )
211
+
212
+ total_score = 0.0
213
+ constraint_scores = []
214
+ detailed_results = []
215
+
216
+ for i, constraint in enumerate(self.constraint_ranking):
217
+ # Gather evidence
218
+ evidence = self._gather_evidence_for_constraint(
219
+ candidate, constraint
220
+ )
221
+
222
+ # Evaluate with dual confidence
223
+ score = self._evaluate_evidence(evidence, constraint)
224
+ constraint_scores.append(score)
225
+
226
+ # Store detailed results
227
+ if evidence:
228
+ dual_evidence = [
229
+ self._analyze_evidence_dual_confidence(e, constraint)
230
+ for e in evidence
231
+ ]
232
+ avg_positive = sum(
233
+ e.positive_confidence for e in dual_evidence
234
+ ) / len(dual_evidence)
235
+ avg_negative = sum(
236
+ e.negative_confidence for e in dual_evidence
237
+ ) / len(dual_evidence)
238
+ avg_uncertainty = sum(
239
+ e.uncertainty for e in dual_evidence
240
+ ) / len(dual_evidence)
241
+
242
+ detailed_results.append(
243
+ {
244
+ "constraint": constraint.value,
245
+ "score": score,
246
+ "positive": avg_positive,
247
+ "negative": avg_negative,
248
+ "uncertainty": avg_uncertainty,
249
+ }
250
+ )
251
+
252
+ symbol = (
253
+ "✓" if score >= 0.8 else "○" if score >= 0.5 else "✗"
254
+ )
255
+ logger.info(
256
+ f"{symbol} {candidate.name} | {constraint.value}: {int(score * 100)}% "
257
+ f"(+{int(avg_positive * 100)}% -{int(avg_negative * 100)}% ?{int(avg_uncertainty * 100)}%)"
258
+ )
259
+
260
+ # Early exit for critical failures with high negative confidence
261
+ if score < 0.2 and constraint.weight > 0.8:
262
+ logger.info(
263
+ f"Critical constraint failed with high confidence: {constraint.value}"
264
+ )
265
+ break
266
+
267
+ # Calculate weighted average
268
+ if constraint_scores:
269
+ weights = [
270
+ c.weight
271
+ for c in self.constraint_ranking[: len(constraint_scores)]
272
+ ]
273
+ total_score = sum(
274
+ s * w for s, w in zip(constraint_scores, weights)
275
+ ) / sum(weights)
276
+
277
+ # Log detailed breakdown
278
+ logger.info(f"\nDetailed analysis for {candidate.name}:")
279
+ for result in detailed_results:
280
+ logger.info(
281
+ f" {result['constraint']}: {result['score']:.2%} "
282
+ f"(+{result['positive']:.0%} -{result['negative']:.0%} ?{result['uncertainty']:.0%})"
283
+ )
284
+
285
+ # Update best candidate if needed
286
+ with self.evaluation_lock:
287
+ self.evaluated_candidates[candidate.name] = total_score
288
+
289
+ if total_score > self.best_score:
290
+ self.best_score = total_score
291
+ self.best_candidate = candidate.name
292
+
293
+ logger.info(
294
+ f"New best: {candidate.name} with {total_score:.2%}"
295
+ )
296
+
297
+ # Check for early stop
298
+ if total_score >= self.early_stop_threshold:
299
+ logger.info(
300
+ f"EARLY STOP: {candidate.name} reached {total_score:.2%}!"
301
+ )
302
+ self.found_answer.set()
303
+
304
+ if self.progress_callback:
305
+ self.progress_callback(
306
+ f"Found answer: {candidate.name} ({int(total_score * 100)}% confidence)",
307
+ 95,
308
+ {
309
+ "phase": "early_stop",
310
+ "final_answer": candidate.name,
311
+ "confidence": total_score,
312
+ "detailed_results": detailed_results,
313
+ },
314
+ )
315
+
316
+ return total_score
317
+
318
+ except Exception as e:
319
+ logger.error(f"Error evaluating {candidate.name}: {e}")
320
+ return 0.0