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,210 +1,317 @@
|
|
1
|
-
import
|
1
|
+
from datetime import datetime
|
2
|
+
from threading import Lock
|
3
|
+
from typing import Any, NoReturn
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
+
from flask import Flask, current_app, request
|
6
|
+
from flask_socketio import SocketIO
|
7
|
+
from loguru import logger
|
5
8
|
|
6
|
-
|
7
|
-
|
8
|
-
# Socket subscription tracking
|
9
|
-
socket_subscriptions = {}
|
9
|
+
from ..models.database import get_db_connection
|
10
|
+
from ..routes.globals import get_globals
|
10
11
|
|
11
12
|
|
12
|
-
|
13
|
-
"""Set the Socket.IO instance for the service."""
|
14
|
-
global socketio
|
15
|
-
socketio = socket_instance
|
16
|
-
logger.info("Socket.IO instance attached to socket service")
|
17
|
-
|
18
|
-
|
19
|
-
def emit_socket_event(event, data, room=None):
|
13
|
+
class SocketIOService:
|
20
14
|
"""
|
21
|
-
|
22
|
-
|
23
|
-
Args:
|
24
|
-
event: The event name to emit
|
25
|
-
data: The data to send with the event
|
26
|
-
room: Optional room ID to send to specific client
|
27
|
-
|
28
|
-
Returns:
|
29
|
-
bool: True if emission was successful, False otherwise
|
15
|
+
Singleton class for managing SocketIO connections and subscriptions.
|
30
16
|
"""
|
31
|
-
global socketio
|
32
|
-
|
33
|
-
if not socketio:
|
34
|
-
logger.error("Socket.IO not initialized when attempting to emit event")
|
35
|
-
return False
|
36
|
-
|
37
|
-
try:
|
38
|
-
# If room is specified, only emit to that room
|
39
|
-
if room:
|
40
|
-
socketio.emit(event, data, room=room)
|
41
|
-
else:
|
42
|
-
# Otherwise broadcast to all
|
43
|
-
socketio.emit(event, data)
|
44
|
-
return True
|
45
|
-
except Exception as e:
|
46
|
-
logger.error(f"Error emitting socket event {event}: {str(e)}")
|
47
|
-
return False
|
48
17
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
18
|
+
_instance = None
|
19
|
+
|
20
|
+
def __new__(cls, *args: Any, app: Flask | None = None, **kwargs: Any):
|
21
|
+
"""
|
22
|
+
Args:
|
23
|
+
app: The Flask app to bind this service to. It must be specified
|
24
|
+
the first time this is called and the singleton instance is
|
25
|
+
created, but will be ignored after that.
|
26
|
+
*args: Arguments to pass to the superclass's __new__ method.
|
27
|
+
**kwargs: Keyword arguments to pass to the superclass's __new__ method.
|
28
|
+
"""
|
29
|
+
if not cls._instance:
|
30
|
+
if app is None:
|
31
|
+
raise ValueError(
|
32
|
+
"Flask app must be specified to create a SocketIOService instance."
|
33
|
+
)
|
34
|
+
cls._instance = super(SocketIOService, cls).__new__(
|
35
|
+
cls, *args, **kwargs
|
36
|
+
)
|
37
|
+
cls._instance.__init_singleton(app)
|
38
|
+
return cls._instance
|
39
|
+
|
40
|
+
def __init_singleton(self, app: Flask) -> None:
|
41
|
+
"""
|
42
|
+
Initializes the singleton instance.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
app: The app to bind this service to.
|
46
|
+
|
47
|
+
"""
|
48
|
+
self.__socketio = SocketIO(
|
49
|
+
app,
|
50
|
+
cors_allowed_origins="*",
|
51
|
+
async_mode="threading",
|
52
|
+
path="/research/socket.io",
|
53
|
+
logger=False,
|
54
|
+
engineio_logger=False,
|
55
|
+
ping_timeout=20,
|
56
|
+
ping_interval=5,
|
85
57
|
)
|
86
|
-
return False
|
87
58
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
59
|
+
# Socket subscription tracking.
|
60
|
+
self.__socket_subscriptions = {}
|
61
|
+
# Set to false to disable logging in the event handlers. This can
|
62
|
+
# be necessary because it will sometimes run the handlers directly
|
63
|
+
# during a call to `emit` that was made in a logging handler.
|
64
|
+
self.__logging_enabled = True
|
65
|
+
# Protects access to shared state.
|
66
|
+
self.__lock = Lock()
|
67
|
+
|
68
|
+
# Register events.
|
69
|
+
@self.__socketio.on("connect")
|
70
|
+
def on_connect():
|
71
|
+
self.__handle_connect(request)
|
72
|
+
|
73
|
+
@self.__socketio.on("disconnect")
|
74
|
+
def on_disconnect(reason: str):
|
75
|
+
self.__handle_disconnect(request, reason)
|
76
|
+
|
77
|
+
@self.__socketio.on("subscribe_to_research")
|
78
|
+
def on_subscribe(data):
|
79
|
+
globals_dict = get_globals()
|
80
|
+
active_research = globals_dict.get("active_research", {})
|
81
|
+
self.__handle_subscribe(data, request, active_research)
|
82
|
+
|
83
|
+
@self.__socketio.on_error
|
84
|
+
def on_error(e):
|
85
|
+
return self.__handle_socket_error(e)
|
86
|
+
|
87
|
+
@self.__socketio.on_error_default
|
88
|
+
def on_default_error(e):
|
89
|
+
return self.__handle_default_error(e)
|
90
|
+
|
91
|
+
def __log_info(self, message: str, *args: Any, **kwargs: Any) -> None:
|
92
|
+
"""Log an info message."""
|
93
|
+
if self.__logging_enabled:
|
94
|
+
logger.info(message, *args, **kwargs)
|
95
|
+
|
96
|
+
def __log_error(self, message: str, *args: Any, **kwargs: Any) -> None:
|
97
|
+
"""Log an error message."""
|
98
|
+
if self.__logging_enabled:
|
99
|
+
logger.error(message, *args, **kwargs)
|
100
|
+
|
101
|
+
def __log_exception(self, message: str, *args: Any, **kwargs: Any) -> None:
|
102
|
+
"""Log an exception."""
|
103
|
+
if self.__logging_enabled:
|
104
|
+
logger.exception(message, *args, **kwargs)
|
105
|
+
|
106
|
+
def emit_socket_event(self, event, data, room=None):
|
107
|
+
"""
|
108
|
+
Emit a socket event to clients.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
event: The event name to emit
|
112
|
+
data: The data to send with the event
|
113
|
+
room: Optional room ID to send to specific client
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
bool: True if emission was successful, False otherwise
|
117
|
+
"""
|
118
|
+
try:
|
119
|
+
# If room is specified, only emit to that room
|
120
|
+
if room:
|
121
|
+
self.__socketio.emit(event, data, room=room)
|
122
|
+
else:
|
123
|
+
# Otherwise broadcast to all
|
124
|
+
self.__socketio.emit(event, data)
|
125
|
+
return True
|
126
|
+
except Exception as e:
|
127
|
+
logger.error(f"Error emitting socket event {event}: {str(e)}")
|
128
|
+
return False
|
129
|
+
|
130
|
+
def emit_to_subscribers(
|
131
|
+
self, event_base, research_id, data, enable_logging: bool = True
|
132
|
+
):
|
133
|
+
"""
|
134
|
+
Emit an event to all subscribers of a specific research.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
event_base: Base event name (will be formatted with research_id)
|
138
|
+
research_id: ID of the research
|
139
|
+
data: The data to send with the event
|
140
|
+
enable_logging: If set to false, this will disable all logging,
|
141
|
+
which is useful if we are calling this inside of a logging
|
142
|
+
handler.
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
bool: True if emission was successful, False otherwise
|
146
|
+
|
147
|
+
"""
|
148
|
+
if not enable_logging:
|
149
|
+
self.__logging_enabled = False
|
150
|
+
|
151
|
+
try:
|
152
|
+
# Emit to the general channel for the research
|
153
|
+
full_event = f"{event_base}_{research_id}"
|
154
|
+
self.__socketio.emit(full_event, data)
|
155
|
+
|
156
|
+
# Emit to specific subscribers
|
157
|
+
with self.__lock:
|
158
|
+
subscriptions = self.__socket_subscriptions.get(research_id)
|
159
|
+
if subscriptions is not None:
|
160
|
+
for sid in subscriptions:
|
161
|
+
try:
|
162
|
+
self.__socketio.emit(full_event, data, room=sid)
|
163
|
+
except Exception:
|
164
|
+
self.__log_exception(
|
165
|
+
f"Error emitting to subscriber {sid}"
|
166
|
+
)
|
167
|
+
|
168
|
+
return True
|
169
|
+
except Exception:
|
170
|
+
self.__log_exception(
|
171
|
+
f"Error emitting to subscribers for research {research_id}"
|
172
|
+
)
|
173
|
+
return False
|
174
|
+
finally:
|
175
|
+
self.__logging_enabled = True
|
176
|
+
|
177
|
+
def __handle_connect(self, request):
|
178
|
+
"""Handle client connection"""
|
179
|
+
self.__log_info(f"Client connected: {request.sid}")
|
180
|
+
|
181
|
+
def __handle_disconnect(self, request, reason: str):
|
182
|
+
"""Handle client disconnection"""
|
183
|
+
try:
|
184
|
+
self.__log_info(
|
185
|
+
f"Client {request.sid} disconnected because: {reason}"
|
186
|
+
)
|
187
|
+
# Clean up subscriptions for this client
|
188
|
+
with self.__lock:
|
189
|
+
if request.sid in self.__socket_subscriptions:
|
190
|
+
del self.__socket_subscriptions[request.sid]
|
191
|
+
self.__log_info(f"Removed subscription for client {request.sid}")
|
192
|
+
except Exception as e:
|
193
|
+
self.__log_error(f"Error handling disconnect: {e}")
|
194
|
+
|
195
|
+
def __handle_subscribe(self, data, request, active_research=None):
|
196
|
+
"""Handle client subscription to research updates"""
|
197
|
+
research_id = data.get("research_id")
|
198
|
+
if research_id:
|
199
|
+
# First check if this research is still active
|
200
|
+
conn = get_db_connection()
|
201
|
+
cursor = conn.cursor()
|
202
|
+
cursor.execute(
|
203
|
+
"SELECT status FROM research_history WHERE id = ?",
|
204
|
+
(research_id,),
|
205
|
+
)
|
206
|
+
result = cursor.fetchone()
|
207
|
+
conn.close()
|
208
|
+
|
209
|
+
# Only allow subscription to valid research
|
210
|
+
if result:
|
211
|
+
status = result[0]
|
212
|
+
|
213
|
+
# Initialize subscription set if needed
|
214
|
+
with self.__lock:
|
215
|
+
if research_id not in self.__socket_subscriptions:
|
216
|
+
self.__socket_subscriptions[research_id] = set()
|
217
|
+
|
218
|
+
# Add this client to the subscribers
|
219
|
+
self.__socket_subscriptions[research_id].add(
|
220
|
+
request.sid
|
221
|
+
)
|
222
|
+
self.__log_info(
|
223
|
+
f"Client {request.sid} subscribed to research {research_id}"
|
148
224
|
)
|
149
225
|
|
150
|
-
if
|
151
|
-
|
226
|
+
# Send current status immediately if available
|
227
|
+
if active_research and research_id in active_research:
|
228
|
+
progress = active_research[research_id]["progress"]
|
229
|
+
latest_log = (
|
230
|
+
active_research[research_id]["log"][-1]
|
231
|
+
if active_research[research_id]["log"]
|
232
|
+
else None
|
233
|
+
)
|
234
|
+
|
235
|
+
if latest_log:
|
236
|
+
self.emit_socket_event(
|
237
|
+
f"research_progress_{research_id}",
|
238
|
+
{
|
239
|
+
"progress": progress,
|
240
|
+
"message": latest_log.get(
|
241
|
+
"message", "Processing..."
|
242
|
+
),
|
243
|
+
"status": "in_progress",
|
244
|
+
"log_entry": latest_log,
|
245
|
+
},
|
246
|
+
room=request.sid,
|
247
|
+
)
|
248
|
+
elif status in ["completed", "failed", "suspended"]:
|
249
|
+
# Send final status for completed research
|
250
|
+
self.emit_socket_event(
|
152
251
|
f"research_progress_{research_id}",
|
153
252
|
{
|
154
|
-
"progress": progress,
|
155
|
-
"message": latest_log.get("message", "Processing..."),
|
156
|
-
"status": "in_progress",
|
157
|
-
"log_entry": latest_log,
|
158
|
-
},
|
159
|
-
room=request.sid,
|
160
|
-
)
|
161
|
-
elif status in ["completed", "failed", "suspended"]:
|
162
|
-
# Send final status for completed research
|
163
|
-
emit_socket_event(
|
164
|
-
f"research_progress_{research_id}",
|
165
|
-
{
|
166
|
-
"progress": 100 if status == "completed" else 0,
|
167
|
-
"message": (
|
168
|
-
"Research completed successfully"
|
169
|
-
if status == "completed"
|
170
|
-
else (
|
171
|
-
"Research failed"
|
172
|
-
if status == "failed"
|
173
|
-
else "Research was suspended"
|
174
|
-
)
|
175
|
-
),
|
176
|
-
"status": status,
|
177
|
-
"log_entry": {
|
178
|
-
"time": datetime.utcnow().isoformat(),
|
179
|
-
"message": f"Research is {status}",
|
180
253
|
"progress": 100 if status == "completed" else 0,
|
181
|
-
"
|
182
|
-
"
|
183
|
-
|
254
|
+
"message": (
|
255
|
+
"Research completed successfully"
|
256
|
+
if status == "completed"
|
257
|
+
else (
|
258
|
+
"Research failed"
|
259
|
+
if status == "failed"
|
260
|
+
else "Research was suspended"
|
184
261
|
)
|
262
|
+
),
|
263
|
+
"status": status,
|
264
|
+
"log_entry": {
|
265
|
+
"time": datetime.utcnow().isoformat(),
|
266
|
+
"message": f"Research is {status}",
|
267
|
+
"progress": 100 if status == "completed" else 0,
|
268
|
+
"metadata": {
|
269
|
+
"phase": (
|
270
|
+
"complete"
|
271
|
+
if status == "completed"
|
272
|
+
else "error"
|
273
|
+
)
|
274
|
+
},
|
185
275
|
},
|
186
276
|
},
|
187
|
-
|
277
|
+
room=request.sid,
|
278
|
+
)
|
279
|
+
else:
|
280
|
+
# Research not found
|
281
|
+
self.emit_socket_event(
|
282
|
+
"error",
|
283
|
+
{"message": f"Research ID {research_id} not found"},
|
188
284
|
room=request.sid,
|
189
285
|
)
|
190
|
-
else:
|
191
|
-
# Research not found
|
192
|
-
emit_socket_event(
|
193
|
-
"error",
|
194
|
-
{"message": f"Research ID {research_id} not found"},
|
195
|
-
room=request.sid,
|
196
|
-
)
|
197
286
|
|
287
|
+
def __handle_socket_error(self, e):
|
288
|
+
"""Handle Socket.IO errors"""
|
289
|
+
self.__log_error(f"Socket.IO error: {str(e)}")
|
290
|
+
# Don't propagate exceptions to avoid crashing the server
|
291
|
+
return False
|
198
292
|
|
199
|
-
def
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
293
|
+
def __handle_default_error(self, e):
|
294
|
+
"""Handle unhandled Socket.IO errors"""
|
295
|
+
self.__log_error(f"Unhandled Socket.IO error: {str(e)}")
|
296
|
+
# Don't propagate exceptions to avoid crashing the server
|
297
|
+
return False
|
205
298
|
|
206
|
-
def
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
299
|
+
def run(self, host: str, port: int, debug: bool = False) -> NoReturn:
|
300
|
+
"""
|
301
|
+
Runs the SocketIO server.
|
302
|
+
|
303
|
+
Args:
|
304
|
+
host: The hostname to bind the server to.
|
305
|
+
port: The port number to listen on.
|
306
|
+
debug: Whether to run in debug mode. Defaults to False.
|
307
|
+
|
308
|
+
"""
|
309
|
+
logger.info(f"Starting web server on {host}:{port} (debug: {debug})")
|
310
|
+
self.__socketio.run(
|
311
|
+
current_app,
|
312
|
+
debug=debug,
|
313
|
+
host=host,
|
314
|
+
port=port,
|
315
|
+
allow_unsafe_werkzeug=True,
|
316
|
+
use_reloader=False,
|
317
|
+
)
|