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
@@ -1,73 +1,80 @@
|
|
1
1
|
# citation_handler.py
|
2
2
|
|
3
|
-
from typing import Any, Dict, List, Union
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
4
4
|
|
5
|
-
from
|
5
|
+
from loguru import logger
|
6
6
|
|
7
7
|
from .utilities.db_utils import get_db_setting
|
8
8
|
|
9
9
|
|
10
10
|
class CitationHandler:
|
11
|
-
|
11
|
+
"""
|
12
|
+
Configurable citation handler that delegates to specific implementations.
|
13
|
+
Maintains backward compatibility while allowing strategy-specific handlers.
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __init__(self, llm, handler_type: Optional[str] = None):
|
12
17
|
self.llm = llm
|
13
18
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
Convert search results to LangChain documents format and add index
|
19
|
-
to original search results.
|
20
|
-
"""
|
21
|
-
documents = []
|
22
|
-
if isinstance(search_results, str):
|
23
|
-
return documents
|
24
|
-
|
25
|
-
for i, result in enumerate(search_results):
|
26
|
-
if isinstance(result, dict):
|
27
|
-
# Add index to the original search result dictionary
|
28
|
-
result["index"] = str(i + nr_of_links + 1)
|
29
|
-
|
30
|
-
content = result.get("full_content", result.get("snippet", ""))
|
31
|
-
documents.append(
|
32
|
-
Document(
|
33
|
-
page_content=content,
|
34
|
-
metadata={
|
35
|
-
"source": result.get("link", f"source_{i + 1}"),
|
36
|
-
"title": result.get("title", f"Source {i + 1}"),
|
37
|
-
"index": i + nr_of_links + 1,
|
38
|
-
},
|
39
|
-
)
|
40
|
-
)
|
41
|
-
return documents
|
42
|
-
|
43
|
-
def _format_sources(self, documents: List[Document]) -> str:
|
44
|
-
"""Format sources with numbers for citation."""
|
45
|
-
sources = []
|
46
|
-
for doc in documents:
|
47
|
-
source_id = doc.metadata["index"]
|
48
|
-
sources.append(f"[{source_id}] {doc.page_content}")
|
49
|
-
return "\n\n".join(sources)
|
19
|
+
# Determine which handler to use
|
20
|
+
if handler_type is None:
|
21
|
+
# Try to get from settings, default to standard
|
22
|
+
handler_type = get_db_setting("citation.handler_type", "standard")
|
50
23
|
|
51
|
-
|
52
|
-
self
|
53
|
-
|
24
|
+
# Import and instantiate the appropriate handler
|
25
|
+
self._handler = self._create_handler(handler_type)
|
26
|
+
|
27
|
+
# For backward compatibility, expose internal methods
|
28
|
+
self._create_documents = self._handler._create_documents
|
29
|
+
self._format_sources = self._handler._format_sources
|
30
|
+
|
31
|
+
def _create_handler(self, handler_type: str):
|
32
|
+
"""Create the appropriate citation handler based on type."""
|
33
|
+
handler_type = handler_type.lower()
|
54
34
|
|
55
|
-
|
56
|
-
|
57
|
-
|
35
|
+
if handler_type == "standard":
|
36
|
+
from .citation_handlers.standard_citation_handler import (
|
37
|
+
StandardCitationHandler,
|
38
|
+
)
|
58
39
|
|
59
|
-
|
40
|
+
logger.info("Using StandardCitationHandler")
|
41
|
+
return StandardCitationHandler(self.llm)
|
60
42
|
|
61
|
-
|
62
|
-
|
43
|
+
elif handler_type in ["forced", "forced_answer", "browsecomp"]:
|
44
|
+
from .citation_handlers.forced_answer_citation_handler import (
|
45
|
+
ForcedAnswerCitationHandler,
|
46
|
+
)
|
47
|
+
|
48
|
+
logger.info(
|
49
|
+
"Using ForcedAnswerCitationHandler for better benchmark performance"
|
50
|
+
)
|
51
|
+
return ForcedAnswerCitationHandler(self.llm)
|
52
|
+
|
53
|
+
elif handler_type in ["precision", "precision_extraction", "simpleqa"]:
|
54
|
+
from .citation_handlers.precision_extraction_handler import (
|
55
|
+
PrecisionExtractionHandler,
|
56
|
+
)
|
57
|
+
|
58
|
+
logger.info(
|
59
|
+
"Using PrecisionExtractionHandler for precise answer extraction"
|
60
|
+
)
|
61
|
+
return PrecisionExtractionHandler(self.llm)
|
62
|
+
|
63
|
+
else:
|
64
|
+
logger.warning(
|
65
|
+
f"Unknown citation handler type: {handler_type}, falling back to standard"
|
66
|
+
)
|
67
|
+
from .citation_handlers.standard_citation_handler import (
|
68
|
+
StandardCitationHandler,
|
69
|
+
)
|
63
70
|
|
64
|
-
|
65
|
-
"""
|
71
|
+
return StandardCitationHandler(self.llm)
|
66
72
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
73
|
+
def analyze_initial(
|
74
|
+
self, query: str, search_results: Union[str, List[Dict]]
|
75
|
+
) -> Dict[str, Any]:
|
76
|
+
"""Delegate to the configured handler."""
|
77
|
+
return self._handler.analyze_initial(query, search_results)
|
71
78
|
|
72
79
|
def analyze_followup(
|
73
80
|
self,
|
@@ -76,41 +83,7 @@ Provide a detailed analysis with citations. Do not create the bibliography, it w
|
|
76
83
|
previous_knowledge: str,
|
77
84
|
nr_of_links: int,
|
78
85
|
) -> Dict[str, Any]:
|
79
|
-
"""
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
fact_check_prompt = f"""Analyze these sources for factual consistency:
|
84
|
-
1. Cross-reference major claims between sources
|
85
|
-
2. Identify and flag any contradictions
|
86
|
-
3. Verify basic facts (dates, company names, ownership)
|
87
|
-
4. Note when sources disagree
|
88
|
-
|
89
|
-
Previous Knowledge:
|
90
|
-
{previous_knowledge}
|
91
|
-
|
92
|
-
New Sources:
|
93
|
-
{formatted_sources}
|
94
|
-
|
95
|
-
Return any inconsistencies or conflicts found."""
|
96
|
-
if get_db_setting("general.enable_fact_checking", True):
|
97
|
-
fact_check_response = self.llm.invoke(fact_check_prompt).content
|
98
|
-
|
99
|
-
else:
|
100
|
-
fact_check_response = ""
|
101
|
-
|
102
|
-
prompt = f"""Using the previous knowledge and new sources, answer the question. Include citations using numbers in square brackets [1], [2], etc. When citing, use the source number provided at the start of each source. Reflect information from sources critically.
|
103
|
-
|
104
|
-
Previous Knowledge:
|
105
|
-
{previous_knowledge}
|
106
|
-
|
107
|
-
Question: {question}
|
108
|
-
|
109
|
-
New Sources:
|
110
|
-
{formatted_sources}
|
111
|
-
Reflect information from sources critically based on: {fact_check_response}. Never invent sources.
|
112
|
-
Provide a detailed answer with citations. Example format: "According to [1], ..." """
|
113
|
-
|
114
|
-
response = self.llm.invoke(prompt)
|
115
|
-
|
116
|
-
return {"content": response.content, "documents": documents}
|
86
|
+
"""Delegate to the configured handler."""
|
87
|
+
return self._handler.analyze_followup(
|
88
|
+
question, search_results, previous_knowledge, nr_of_links
|
89
|
+
)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"""
|
2
|
+
Citation handlers for different search strategies.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .base_citation_handler import BaseCitationHandler
|
6
|
+
from .forced_answer_citation_handler import ForcedAnswerCitationHandler
|
7
|
+
from .precision_extraction_handler import PrecisionExtractionHandler
|
8
|
+
from .standard_citation_handler import StandardCitationHandler
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"BaseCitationHandler",
|
12
|
+
"StandardCitationHandler",
|
13
|
+
"ForcedAnswerCitationHandler",
|
14
|
+
"PrecisionExtractionHandler",
|
15
|
+
]
|
@@ -0,0 +1,70 @@
|
|
1
|
+
"""
|
2
|
+
Base class for all citation handlers.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from abc import ABC, abstractmethod
|
6
|
+
from typing import Any, Dict, List, Union
|
7
|
+
|
8
|
+
from langchain_core.documents import Document
|
9
|
+
|
10
|
+
|
11
|
+
class BaseCitationHandler(ABC):
|
12
|
+
"""Abstract base class for citation handlers."""
|
13
|
+
|
14
|
+
def __init__(self, llm):
|
15
|
+
self.llm = llm
|
16
|
+
|
17
|
+
def _create_documents(
|
18
|
+
self, search_results: Union[str, List[Dict]], nr_of_links: int = 0
|
19
|
+
) -> List[Document]:
|
20
|
+
"""
|
21
|
+
Convert search results to LangChain documents format and add index
|
22
|
+
to original search results.
|
23
|
+
"""
|
24
|
+
documents = []
|
25
|
+
if isinstance(search_results, str):
|
26
|
+
return documents
|
27
|
+
|
28
|
+
for i, result in enumerate(search_results):
|
29
|
+
if isinstance(result, dict):
|
30
|
+
# Add index to the original search result dictionary
|
31
|
+
result["index"] = str(i + nr_of_links + 1)
|
32
|
+
|
33
|
+
content = result.get("full_content", result.get("snippet", ""))
|
34
|
+
documents.append(
|
35
|
+
Document(
|
36
|
+
page_content=content,
|
37
|
+
metadata={
|
38
|
+
"source": result.get("link", f"source_{i + 1}"),
|
39
|
+
"title": result.get("title", f"Source {i + 1}"),
|
40
|
+
"index": i + nr_of_links + 1,
|
41
|
+
},
|
42
|
+
)
|
43
|
+
)
|
44
|
+
return documents
|
45
|
+
|
46
|
+
def _format_sources(self, documents: List[Document]) -> str:
|
47
|
+
"""Format sources with numbers for citation."""
|
48
|
+
sources = []
|
49
|
+
for doc in documents:
|
50
|
+
source_id = doc.metadata["index"]
|
51
|
+
sources.append(f"[{source_id}] {doc.page_content}")
|
52
|
+
return "\n\n".join(sources)
|
53
|
+
|
54
|
+
@abstractmethod
|
55
|
+
def analyze_initial(
|
56
|
+
self, query: str, search_results: Union[str, List[Dict]]
|
57
|
+
) -> Dict[str, Any]:
|
58
|
+
"""Process initial analysis with citations."""
|
59
|
+
pass
|
60
|
+
|
61
|
+
@abstractmethod
|
62
|
+
def analyze_followup(
|
63
|
+
self,
|
64
|
+
question: str,
|
65
|
+
search_results: Union[str, List[Dict]],
|
66
|
+
previous_knowledge: str,
|
67
|
+
nr_of_links: int,
|
68
|
+
) -> Dict[str, Any]:
|
69
|
+
"""Process follow-up analysis with citations."""
|
70
|
+
pass
|
@@ -0,0 +1,179 @@
|
|
1
|
+
"""
|
2
|
+
Forced answer citation handler - optimized for BrowseComp-style questions.
|
3
|
+
Always provides a specific answer, never returns "cannot determine".
|
4
|
+
"""
|
5
|
+
|
6
|
+
from typing import Any, Dict, List, Union
|
7
|
+
|
8
|
+
from loguru import logger
|
9
|
+
|
10
|
+
from ..utilities.db_utils import get_db_setting
|
11
|
+
from .base_citation_handler import BaseCitationHandler
|
12
|
+
|
13
|
+
|
14
|
+
class ForcedAnswerCitationHandler(BaseCitationHandler):
|
15
|
+
"""Citation handler that forces direct answers for benchmark questions."""
|
16
|
+
|
17
|
+
def analyze_initial(
|
18
|
+
self, query: str, search_results: Union[str, List[Dict]]
|
19
|
+
) -> Dict[str, Any]:
|
20
|
+
"""Initial analysis with forced answer generation."""
|
21
|
+
documents = self._create_documents(search_results)
|
22
|
+
formatted_sources = self._format_sources(documents)
|
23
|
+
|
24
|
+
prompt = f"""Analyze the following information and provide a DIRECT answer to the question. Include citations using numbers in square brackets [1], [2], etc.
|
25
|
+
|
26
|
+
Question: {query}
|
27
|
+
|
28
|
+
Sources:
|
29
|
+
{formatted_sources}
|
30
|
+
|
31
|
+
CRITICAL INSTRUCTIONS:
|
32
|
+
1. Start your response with a direct answer to the question
|
33
|
+
2. NEVER say "I cannot determine" or "insufficient information"
|
34
|
+
3. If unsure between options, choose the MOST LIKELY based on evidence
|
35
|
+
4. After the direct answer, provide supporting analysis with citations
|
36
|
+
|
37
|
+
Example response format:
|
38
|
+
"[Direct Answer]. According to [1], this is supported by..."
|
39
|
+
"""
|
40
|
+
|
41
|
+
response = self.llm.invoke(prompt)
|
42
|
+
if not isinstance(response, str):
|
43
|
+
response = response.content
|
44
|
+
|
45
|
+
# If the response still doesn't have a direct answer, extract one
|
46
|
+
if self._needs_answer_extraction(response, query):
|
47
|
+
response = self._extract_direct_answer(
|
48
|
+
query, response, formatted_sources
|
49
|
+
)
|
50
|
+
|
51
|
+
return {"content": response, "documents": documents}
|
52
|
+
|
53
|
+
def analyze_followup(
|
54
|
+
self,
|
55
|
+
question: str,
|
56
|
+
search_results: Union[str, List[Dict]],
|
57
|
+
previous_knowledge: str,
|
58
|
+
nr_of_links: int,
|
59
|
+
) -> Dict[str, Any]:
|
60
|
+
"""Follow-up analysis with forced answer generation."""
|
61
|
+
documents = self._create_documents(
|
62
|
+
search_results, nr_of_links=nr_of_links
|
63
|
+
)
|
64
|
+
formatted_sources = self._format_sources(documents)
|
65
|
+
|
66
|
+
# Fact-checking step (if enabled)
|
67
|
+
fact_check_response = ""
|
68
|
+
if get_db_setting("general.enable_fact_checking", True):
|
69
|
+
fact_check_prompt = f"""Analyze these sources for factual consistency:
|
70
|
+
1. Cross-reference major claims between sources
|
71
|
+
2. Identify the most frequently mentioned answer
|
72
|
+
3. Note any conflicts but identify the most likely correct answer
|
73
|
+
|
74
|
+
Previous Knowledge:
|
75
|
+
{previous_knowledge}
|
76
|
+
|
77
|
+
New Sources:
|
78
|
+
{formatted_sources}
|
79
|
+
|
80
|
+
Return the most likely answer based on evidence consistency."""
|
81
|
+
fact_check_response = self.llm.invoke(fact_check_prompt).content
|
82
|
+
|
83
|
+
prompt = f"""Using the previous knowledge and new sources, provide a DIRECT answer to the question. Include citations using numbers in square brackets.
|
84
|
+
|
85
|
+
Previous Knowledge:
|
86
|
+
{previous_knowledge}
|
87
|
+
|
88
|
+
Question: {question}
|
89
|
+
|
90
|
+
New Sources:
|
91
|
+
{formatted_sources}
|
92
|
+
|
93
|
+
Fact Analysis: {fact_check_response}
|
94
|
+
|
95
|
+
CRITICAL INSTRUCTIONS:
|
96
|
+
1. You MUST start with a direct, specific answer
|
97
|
+
2. NEVER say "I cannot determine" or similar phrases
|
98
|
+
3. If the question asks for a name, provide a specific name
|
99
|
+
4. If the question asks for a place, provide a specific place
|
100
|
+
5. If unsure, choose the answer with the most supporting evidence
|
101
|
+
6. Format: "[Direct Answer]. Supporting evidence from [1], [2]..."
|
102
|
+
|
103
|
+
Remember: A wrong answer is better than no answer for this task."""
|
104
|
+
|
105
|
+
response = self.llm.invoke(prompt)
|
106
|
+
content = response.content
|
107
|
+
|
108
|
+
# Final check - if still no direct answer, force extraction
|
109
|
+
if self._needs_answer_extraction(content, question):
|
110
|
+
content = self._extract_direct_answer(
|
111
|
+
question, content, formatted_sources
|
112
|
+
)
|
113
|
+
logger.info(f"Forced answer extraction applied: {content[:100]}...")
|
114
|
+
|
115
|
+
return {"content": content, "documents": documents}
|
116
|
+
|
117
|
+
def _needs_answer_extraction(self, content: str, query: str) -> bool:
|
118
|
+
"""Check if the response needs forced answer extraction."""
|
119
|
+
no_answer_indicators = [
|
120
|
+
"cannot determine",
|
121
|
+
"unable to find",
|
122
|
+
"insufficient",
|
123
|
+
"unclear",
|
124
|
+
"not enough",
|
125
|
+
"cannot provide",
|
126
|
+
"no specific answer",
|
127
|
+
"cannot definitively",
|
128
|
+
]
|
129
|
+
|
130
|
+
content_lower = content.lower()
|
131
|
+
|
132
|
+
# Check for no-answer indicators
|
133
|
+
for indicator in no_answer_indicators:
|
134
|
+
if indicator in content_lower:
|
135
|
+
return True
|
136
|
+
|
137
|
+
# Check if it's a direct question but no direct answer given
|
138
|
+
if query.lower().startswith(
|
139
|
+
("what", "who", "which", "where", "when", "name")
|
140
|
+
):
|
141
|
+
# Look for a direct answer pattern in first 100 chars
|
142
|
+
first_part = content[:100].lower()
|
143
|
+
if not any(
|
144
|
+
word in first_part for word in ["is", "was", "are", "were", ":"]
|
145
|
+
):
|
146
|
+
return True
|
147
|
+
|
148
|
+
return False
|
149
|
+
|
150
|
+
def _extract_direct_answer(
|
151
|
+
self, query: str, content: str, sources: str
|
152
|
+
) -> str:
|
153
|
+
"""Force extraction of a direct answer using LLM."""
|
154
|
+
extraction_prompt = f"""Based on the content below, extract a SINGLE, DIRECT answer to the question.
|
155
|
+
|
156
|
+
Question: {query}
|
157
|
+
|
158
|
+
Content: {content[:1500]}
|
159
|
+
|
160
|
+
Sources: {sources[:1500]}
|
161
|
+
|
162
|
+
RULES:
|
163
|
+
1. Respond with ONLY the answer itself (name, place, number, etc.)
|
164
|
+
2. No explanations, just the answer
|
165
|
+
3. If multiple candidates exist, pick the one mentioned most
|
166
|
+
4. If truly no information exists, make an educated guess
|
167
|
+
|
168
|
+
Answer:"""
|
169
|
+
|
170
|
+
try:
|
171
|
+
answer = self.llm.invoke(extraction_prompt).content.strip()
|
172
|
+
|
173
|
+
# Format as a proper response
|
174
|
+
return f"{answer}. Based on the available sources, this appears to be the most likely answer. {content}"
|
175
|
+
|
176
|
+
except Exception as e:
|
177
|
+
logger.error(f"Error in forced answer extraction: {str(e)}")
|
178
|
+
# Fallback - just prepend a guess
|
179
|
+
return f"Based on the available evidence, the most likely answer appears to be related to the search results. {content}"
|