local-deep-research 0.5.7__py3-none-any.whl → 0.6.0__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/__version__.py +1 -1
- local_deep_research/advanced_search_system/candidate_exploration/progressive_explorer.py +11 -1
- local_deep_research/advanced_search_system/questions/browsecomp_question.py +32 -6
- local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py +33 -8
- local_deep_research/advanced_search_system/strategies/source_based_strategy.py +2 -0
- local_deep_research/api/__init__.py +2 -0
- local_deep_research/api/research_functions.py +177 -3
- local_deep_research/benchmarks/graders.py +150 -5
- local_deep_research/benchmarks/models/__init__.py +19 -0
- local_deep_research/benchmarks/models/benchmark_models.py +283 -0
- local_deep_research/benchmarks/ui/__init__.py +1 -0
- local_deep_research/benchmarks/web_api/__init__.py +6 -0
- local_deep_research/benchmarks/web_api/benchmark_routes.py +862 -0
- local_deep_research/benchmarks/web_api/benchmark_service.py +920 -0
- local_deep_research/config/llm_config.py +106 -21
- local_deep_research/defaults/default_settings.json +448 -3
- local_deep_research/error_handling/report_generator.py +10 -0
- local_deep_research/llm/__init__.py +19 -0
- local_deep_research/llm/llm_registry.py +155 -0
- local_deep_research/metrics/db_models.py +3 -7
- local_deep_research/metrics/search_tracker.py +25 -11
- local_deep_research/report_generator.py +3 -2
- local_deep_research/search_system.py +12 -9
- local_deep_research/utilities/log_utils.py +23 -10
- local_deep_research/utilities/thread_context.py +99 -0
- local_deep_research/web/app_factory.py +32 -8
- local_deep_research/web/database/benchmark_schema.py +230 -0
- local_deep_research/web/database/convert_research_id_to_string.py +161 -0
- local_deep_research/web/database/models.py +55 -1
- local_deep_research/web/database/schema_upgrade.py +397 -2
- local_deep_research/web/database/uuid_migration.py +265 -0
- local_deep_research/web/routes/api_routes.py +62 -31
- local_deep_research/web/routes/history_routes.py +13 -6
- local_deep_research/web/routes/metrics_routes.py +264 -4
- local_deep_research/web/routes/research_routes.py +45 -18
- local_deep_research/web/routes/route_registry.py +352 -0
- local_deep_research/web/routes/settings_routes.py +382 -22
- local_deep_research/web/services/research_service.py +22 -29
- local_deep_research/web/services/settings_manager.py +53 -0
- local_deep_research/web/services/settings_service.py +2 -0
- local_deep_research/web/static/css/styles.css +8 -0
- local_deep_research/web/static/js/components/detail.js +7 -14
- local_deep_research/web/static/js/components/details.js +8 -10
- local_deep_research/web/static/js/components/fallback/ui.js +4 -4
- local_deep_research/web/static/js/components/history.js +6 -6
- local_deep_research/web/static/js/components/logpanel.js +14 -11
- local_deep_research/web/static/js/components/progress.js +51 -46
- local_deep_research/web/static/js/components/research.js +250 -89
- local_deep_research/web/static/js/components/results.js +5 -7
- local_deep_research/web/static/js/components/settings.js +32 -26
- local_deep_research/web/static/js/components/settings_sync.js +24 -23
- local_deep_research/web/static/js/config/urls.js +285 -0
- local_deep_research/web/static/js/main.js +8 -8
- local_deep_research/web/static/js/research_form.js +267 -12
- local_deep_research/web/static/js/services/api.js +18 -18
- local_deep_research/web/static/js/services/keyboard.js +8 -8
- local_deep_research/web/static/js/services/socket.js +53 -35
- local_deep_research/web/static/js/services/ui.js +1 -1
- local_deep_research/web/templates/base.html +4 -1
- local_deep_research/web/templates/components/custom_dropdown.html +5 -3
- local_deep_research/web/templates/components/mobile_nav.html +3 -3
- local_deep_research/web/templates/components/sidebar.html +9 -3
- local_deep_research/web/templates/pages/benchmark.html +2697 -0
- local_deep_research/web/templates/pages/benchmark_results.html +1136 -0
- local_deep_research/web/templates/pages/benchmark_simple.html +453 -0
- local_deep_research/web/templates/pages/cost_analytics.html +1 -1
- local_deep_research/web/templates/pages/metrics.html +212 -39
- local_deep_research/web/templates/pages/research.html +8 -6
- local_deep_research/web/templates/pages/star_reviews.html +1 -1
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +14 -1
- local_deep_research/web_search_engines/engines/search_engine_brave.py +15 -1
- local_deep_research/web_search_engines/engines/search_engine_ddg.py +20 -1
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +26 -2
- local_deep_research/web_search_engines/engines/search_engine_pubmed.py +15 -1
- local_deep_research/web_search_engines/engines/search_engine_retriever.py +192 -0
- local_deep_research/web_search_engines/engines/search_engine_tavily.py +307 -0
- local_deep_research/web_search_engines/rate_limiting/__init__.py +14 -0
- local_deep_research/web_search_engines/rate_limiting/__main__.py +9 -0
- local_deep_research/web_search_engines/rate_limiting/cli.py +209 -0
- local_deep_research/web_search_engines/rate_limiting/exceptions.py +21 -0
- local_deep_research/web_search_engines/rate_limiting/tracker.py +506 -0
- local_deep_research/web_search_engines/retriever_registry.py +108 -0
- local_deep_research/web_search_engines/search_engine_base.py +161 -43
- local_deep_research/web_search_engines/search_engine_factory.py +14 -0
- local_deep_research/web_search_engines/search_engines_config.py +20 -0
- local_deep_research-0.6.0.dist-info/METADATA +374 -0
- {local_deep_research-0.5.7.dist-info → local_deep_research-0.6.0.dist-info}/RECORD +90 -65
- local_deep_research-0.5.7.dist-info/METADATA +0 -420
- {local_deep_research-0.5.7.dist-info → local_deep_research-0.6.0.dist-info}/WHEEL +0 -0
- {local_deep_research-0.5.7.dist-info → local_deep_research-0.6.0.dist-info}/entry_points.txt +0 -0
- {local_deep_research-0.5.7.dist-info → local_deep_research-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -346,6 +346,16 @@ We're here to help you get this working:
|
|
346
346
|
"- Check search engine settings in Advanced Options\n"
|
347
347
|
"- Ensure required API keys are set for external search engines"
|
348
348
|
),
|
349
|
+
"No search results found|All search engines.*blocked.*rate.*limited": (
|
350
|
+
"No search results were found for your query. This could mean all search engines are unavailable.\n\n"
|
351
|
+
"**Try this:**\n"
|
352
|
+
"- **If using SearXNG:** Check if your SearXNG Docker container is running: `docker ps`\n"
|
353
|
+
"- **Start SearXNG:** `docker run -d -p 8080:8080 searxng/searxng` then set URL to `http://localhost:8080`\n"
|
354
|
+
"- **Try different search terms:** Use broader, more general keywords\n"
|
355
|
+
"- **Check network connection:** Ensure you can access the internet\n"
|
356
|
+
"- **Switch search engines:** Try DuckDuckGo, Brave, or Google (if API key configured)\n"
|
357
|
+
"- **Check for typos** in your research query"
|
358
|
+
),
|
349
359
|
"TypeError.*Context.*Size|'<' not supported between": (
|
350
360
|
"Model configuration issue. The context size setting might not be compatible with your model.\n\n"
|
351
361
|
"**Try this:**\n"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
"""LLM module for Local Deep Research."""
|
2
|
+
|
3
|
+
from .llm_registry import (
|
4
|
+
register_llm,
|
5
|
+
unregister_llm,
|
6
|
+
get_llm_from_registry,
|
7
|
+
is_llm_registered,
|
8
|
+
list_registered_llms,
|
9
|
+
clear_llm_registry,
|
10
|
+
)
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
"register_llm",
|
14
|
+
"unregister_llm",
|
15
|
+
"get_llm_from_registry",
|
16
|
+
"is_llm_registered",
|
17
|
+
"list_registered_llms",
|
18
|
+
"clear_llm_registry",
|
19
|
+
]
|
@@ -0,0 +1,155 @@
|
|
1
|
+
"""Registry for custom LangChain LLMs.
|
2
|
+
|
3
|
+
This module provides a global registry for registering and managing custom LangChain
|
4
|
+
LLMs that can be used with Local Deep Research.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Dict, Optional, Union, Callable
|
8
|
+
from langchain.chat_models.base import BaseChatModel
|
9
|
+
import threading
|
10
|
+
import logging
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class LLMRegistry:
|
16
|
+
"""Thread-safe registry for custom LangChain LLMs."""
|
17
|
+
|
18
|
+
def __init__(self):
|
19
|
+
self._llms: Dict[
|
20
|
+
str, Union[BaseChatModel, Callable[..., BaseChatModel]]
|
21
|
+
] = {}
|
22
|
+
self._lock = threading.Lock()
|
23
|
+
|
24
|
+
def register(
|
25
|
+
self, name: str, llm: Union[BaseChatModel, Callable[..., BaseChatModel]]
|
26
|
+
) -> None:
|
27
|
+
"""Register a custom LLM.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
name: Unique name for the LLM
|
31
|
+
llm: Either a BaseChatModel instance or a factory function that returns one
|
32
|
+
"""
|
33
|
+
with self._lock:
|
34
|
+
if name in self._llms:
|
35
|
+
logger.warning(f"Overwriting existing LLM: {name}")
|
36
|
+
self._llms[name] = llm
|
37
|
+
logger.info(f"Registered custom LLM: {name}")
|
38
|
+
|
39
|
+
def unregister(self, name: str) -> None:
|
40
|
+
"""Unregister a custom LLM.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
name: Name of the LLM to unregister
|
44
|
+
"""
|
45
|
+
with self._lock:
|
46
|
+
if name in self._llms:
|
47
|
+
del self._llms[name]
|
48
|
+
logger.info(f"Unregistered custom LLM: {name}")
|
49
|
+
|
50
|
+
def get(
|
51
|
+
self, name: str
|
52
|
+
) -> Optional[Union[BaseChatModel, Callable[..., BaseChatModel]]]:
|
53
|
+
"""Get a registered LLM.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
name: Name of the LLM to retrieve
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
The LLM instance/factory or None if not found
|
60
|
+
"""
|
61
|
+
with self._lock:
|
62
|
+
return self._llms.get(name)
|
63
|
+
|
64
|
+
def is_registered(self, name: str) -> bool:
|
65
|
+
"""Check if an LLM is registered.
|
66
|
+
|
67
|
+
Args:
|
68
|
+
name: Name to check
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
True if registered, False otherwise
|
72
|
+
"""
|
73
|
+
with self._lock:
|
74
|
+
return name in self._llms
|
75
|
+
|
76
|
+
def list_registered(self) -> list[str]:
|
77
|
+
"""Get list of all registered LLM names.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
List of registered LLM names
|
81
|
+
"""
|
82
|
+
with self._lock:
|
83
|
+
return list(self._llms.keys())
|
84
|
+
|
85
|
+
def clear(self) -> None:
|
86
|
+
"""Clear all registered LLMs."""
|
87
|
+
with self._lock:
|
88
|
+
self._llms.clear()
|
89
|
+
logger.info("Cleared all registered custom LLMs")
|
90
|
+
|
91
|
+
|
92
|
+
# Global registry instance
|
93
|
+
_llm_registry = LLMRegistry()
|
94
|
+
|
95
|
+
|
96
|
+
# Public API functions
|
97
|
+
def register_llm(
|
98
|
+
name: str, llm: Union[BaseChatModel, Callable[..., BaseChatModel]]
|
99
|
+
) -> None:
|
100
|
+
"""Register a custom LLM in the global registry.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
name: Unique name for the LLM
|
104
|
+
llm: Either a BaseChatModel instance or a factory function
|
105
|
+
"""
|
106
|
+
_llm_registry.register(name, llm)
|
107
|
+
|
108
|
+
|
109
|
+
def unregister_llm(name: str) -> None:
|
110
|
+
"""Unregister a custom LLM from the global registry.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
name: Name of the LLM to unregister
|
114
|
+
"""
|
115
|
+
_llm_registry.unregister(name)
|
116
|
+
|
117
|
+
|
118
|
+
def get_llm_from_registry(
|
119
|
+
name: str,
|
120
|
+
) -> Optional[Union[BaseChatModel, Callable[..., BaseChatModel]]]:
|
121
|
+
"""Get a registered LLM from the global registry.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
name: Name of the LLM to retrieve
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
The LLM instance/factory or None if not found
|
128
|
+
"""
|
129
|
+
return _llm_registry.get(name)
|
130
|
+
|
131
|
+
|
132
|
+
def is_llm_registered(name: str) -> bool:
|
133
|
+
"""Check if an LLM is registered in the global registry.
|
134
|
+
|
135
|
+
Args:
|
136
|
+
name: Name to check
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
True if registered, False otherwise
|
140
|
+
"""
|
141
|
+
return _llm_registry.is_registered(name)
|
142
|
+
|
143
|
+
|
144
|
+
def list_registered_llms() -> list[str]:
|
145
|
+
"""Get list of all registered LLM names.
|
146
|
+
|
147
|
+
Returns:
|
148
|
+
List of registered LLM names
|
149
|
+
"""
|
150
|
+
return _llm_registry.list_registered()
|
151
|
+
|
152
|
+
|
153
|
+
def clear_llm_registry() -> None:
|
154
|
+
"""Clear all registered LLMs from the global registry."""
|
155
|
+
_llm_registry.clear()
|
@@ -20,9 +20,7 @@ class TokenUsage(Base):
|
|
20
20
|
__tablename__ = "token_usage"
|
21
21
|
|
22
22
|
id = Column(Integer, primary_key=True)
|
23
|
-
research_id = Column(
|
24
|
-
Integer
|
25
|
-
) # Removed foreign key constraint to fix token tracking
|
23
|
+
research_id = Column(String(36), index=True) # UUID string
|
26
24
|
model_name = Column(String)
|
27
25
|
provider = Column(
|
28
26
|
String
|
@@ -63,9 +61,7 @@ class ModelUsage(Base):
|
|
63
61
|
__table_args__ = (UniqueConstraint("research_id", "model_name"),)
|
64
62
|
|
65
63
|
id = Column(Integer, primary_key=True)
|
66
|
-
research_id = Column(
|
67
|
-
Integer
|
68
|
-
) # Removed foreign key constraint to fix token tracking
|
64
|
+
research_id = Column(String(36), index=True) # UUID string
|
69
65
|
model_name = Column(String)
|
70
66
|
provider = Column(String)
|
71
67
|
prompt_tokens = Column(Integer, default=0)
|
@@ -95,7 +91,7 @@ class SearchCall(Base):
|
|
95
91
|
__tablename__ = "search_calls"
|
96
92
|
|
97
93
|
id = Column(Integer, primary_key=True)
|
98
|
-
research_id = Column(
|
94
|
+
research_id = Column(String(36), index=True) # UUID string
|
99
95
|
research_query = Column(Text)
|
100
96
|
research_mode = Column(String)
|
101
97
|
research_phase = Column(String)
|
@@ -3,6 +3,7 @@ Search call tracking system for metrics collection.
|
|
3
3
|
Similar to token_counter.py but tracks search engine usage.
|
4
4
|
"""
|
5
5
|
|
6
|
+
import threading
|
6
7
|
from typing import Any, Dict, List, Optional
|
7
8
|
|
8
9
|
from loguru import logger
|
@@ -19,12 +20,20 @@ class SearchTracker:
|
|
19
20
|
def __init__(self, db: Optional[MetricsDatabase] = None):
|
20
21
|
"""Initialize the search tracker."""
|
21
22
|
self.db = db or MetricsDatabase()
|
22
|
-
self.
|
23
|
+
self._local = threading.local()
|
23
24
|
|
24
25
|
def set_research_context(self, context: Dict[str, Any]) -> None:
|
25
|
-
"""Set the current research context for search tracking."""
|
26
|
-
self.research_context = context or {}
|
27
|
-
logger.debug(
|
26
|
+
"""Set the current research context for search tracking (thread-safe)."""
|
27
|
+
self._local.research_context = context or {}
|
28
|
+
logger.debug(
|
29
|
+
f"Search tracker context updated (thread {threading.current_thread().ident}): {self._local.research_context}"
|
30
|
+
)
|
31
|
+
|
32
|
+
def _get_research_context(self) -> Dict[str, Any]:
|
33
|
+
"""Get the research context for the current thread."""
|
34
|
+
if not hasattr(self._local, "research_context"):
|
35
|
+
self._local.research_context = {}
|
36
|
+
return self._local.research_context
|
28
37
|
|
29
38
|
def record_search(
|
30
39
|
self,
|
@@ -37,12 +46,17 @@ class SearchTracker:
|
|
37
46
|
) -> None:
|
38
47
|
"""Record a completed search operation directly to database."""
|
39
48
|
|
40
|
-
# Extract research context
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
49
|
+
# Extract research context (thread-safe)
|
50
|
+
context = self._get_research_context()
|
51
|
+
research_id = context.get("research_id")
|
52
|
+
|
53
|
+
# Convert research_id to string if it's an integer (for backward compatibility)
|
54
|
+
if isinstance(research_id, int):
|
55
|
+
research_id = str(research_id)
|
56
|
+
research_query = context.get("research_query")
|
57
|
+
research_mode = context.get("research_mode", "unknown")
|
58
|
+
research_phase = context.get("research_phase", "search")
|
59
|
+
search_iteration = context.get("search_iteration", 0)
|
46
60
|
|
47
61
|
# Determine success status
|
48
62
|
success_status = "success" if success else "error"
|
@@ -59,7 +73,7 @@ class SearchTracker:
|
|
59
73
|
with self.db.get_session() as session:
|
60
74
|
# Create search call record
|
61
75
|
search_call = SearchCall(
|
62
|
-
research_id=research_id,
|
76
|
+
research_id=research_id, # String research_id (UUID or converted integer)
|
63
77
|
research_query=research_query,
|
64
78
|
research_mode=research_mode,
|
65
79
|
research_phase=research_phase,
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import importlib
|
2
2
|
from typing import Dict, List
|
3
|
+
from loguru import logger
|
3
4
|
|
4
5
|
from langchain_core.language_models import BaseChatModel
|
5
6
|
|
@@ -125,7 +126,7 @@ class IntegratedReportGenerator:
|
|
125
126
|
sections = {}
|
126
127
|
|
127
128
|
for section in structure:
|
128
|
-
|
129
|
+
logger.info(f"Processing section: {section['name']}")
|
129
130
|
section_content = []
|
130
131
|
section_content.append(f"# {section['name']}\n")
|
131
132
|
|
@@ -138,7 +139,7 @@ class IntegratedReportGenerator:
|
|
138
139
|
# Generate a specific search query for this subsection
|
139
140
|
subsection_query = f"{query} {section['name']} {subsection['name']} {subsection['purpose']}"
|
140
141
|
|
141
|
-
|
142
|
+
logger.info(
|
142
143
|
f"Researching subsection: {subsection['name']} with query: {subsection_query}"
|
143
144
|
)
|
144
145
|
|
@@ -47,9 +47,8 @@ from .advanced_search_system.strategies.smart_decomposition_strategy import (
|
|
47
47
|
from .advanced_search_system.strategies.source_based_strategy import (
|
48
48
|
SourceBasedSearchStrategy,
|
49
49
|
)
|
50
|
-
|
51
|
-
|
52
|
-
)
|
50
|
+
|
51
|
+
# StandardSearchStrategy imported lazily to avoid database access during module import
|
53
52
|
from .citation_handler import CitationHandler
|
54
53
|
from .config.llm_config import get_llm
|
55
54
|
from .config.search_config import get_search
|
@@ -150,7 +149,7 @@ class AdvancedSearchSystem:
|
|
150
149
|
search=self.search,
|
151
150
|
all_links_of_system=self.all_links_of_system,
|
152
151
|
)
|
153
|
-
elif strategy_name.lower()
|
152
|
+
elif strategy_name.lower() in ["source-based", "source_based"]:
|
154
153
|
logger.info("Creating SourceBasedSearchStrategy instance")
|
155
154
|
self.strategy = SourceBasedSearchStrategy(
|
156
155
|
model=self.model,
|
@@ -465,15 +464,14 @@ class AdvancedSearchSystem:
|
|
465
464
|
)
|
466
465
|
|
467
466
|
logger.info("Creating FocusedIterationStrategy instance")
|
468
|
-
#
|
469
|
-
# Original optimal settings: max_iterations=8, questions_per_iteration=5
|
467
|
+
# Use database settings for iterations and questions_per_iteration
|
470
468
|
self.strategy = FocusedIterationStrategy(
|
471
469
|
model=self.model,
|
472
470
|
search=self.search,
|
473
471
|
all_links_of_system=self.all_links_of_system,
|
474
|
-
max_iterations=
|
475
|
-
questions_per_iteration=
|
476
|
-
use_browsecomp_optimization=True, # Enable BrowseComp optimizations
|
472
|
+
max_iterations=self.max_iterations, # Use database setting
|
473
|
+
questions_per_iteration=self.questions_per_iteration, # Use database setting
|
474
|
+
use_browsecomp_optimization=True, # Enable BrowseComp optimizations for 95% accuracy
|
477
475
|
)
|
478
476
|
elif strategy_name.lower() in [
|
479
477
|
"browsecomp-entity",
|
@@ -491,6 +489,11 @@ class AdvancedSearchSystem:
|
|
491
489
|
)
|
492
490
|
else:
|
493
491
|
logger.info("Creating StandardSearchStrategy instance")
|
492
|
+
# Import lazily to avoid database access during module import
|
493
|
+
from .advanced_search_system.strategies.standard_strategy import (
|
494
|
+
StandardSearchStrategy,
|
495
|
+
)
|
496
|
+
|
494
497
|
self.strategy = StandardSearchStrategy(
|
495
498
|
model=self.model,
|
496
499
|
search=self.search,
|
@@ -75,23 +75,33 @@ def log_for_research(
|
|
75
75
|
@wraps(to_wrap)
|
76
76
|
def wrapped(research_id: int, *args: Any, **kwargs: Any) -> Any:
|
77
77
|
g.research_id = research_id
|
78
|
-
to_wrap(research_id, *args, **kwargs)
|
78
|
+
result = to_wrap(research_id, *args, **kwargs)
|
79
79
|
g.pop("research_id")
|
80
|
+
return result
|
80
81
|
|
81
82
|
return wrapped
|
82
83
|
|
83
84
|
|
84
|
-
def _get_research_id() -> int | None:
|
85
|
+
def _get_research_id(record=None) -> int | None:
|
85
86
|
"""
|
86
87
|
Gets the current research ID, if present.
|
87
88
|
|
89
|
+
Args:
|
90
|
+
record: Optional loguru record that might contain bound research_id
|
91
|
+
|
88
92
|
Returns:
|
89
93
|
The current research ID, or None if it does not exist.
|
90
94
|
|
91
95
|
"""
|
92
96
|
research_id = None
|
93
|
-
|
97
|
+
|
98
|
+
# First check if research_id is bound to the log record
|
99
|
+
if record and "extra" in record and "research_id" in record["extra"]:
|
100
|
+
research_id = record["extra"]["research_id"]
|
101
|
+
# Then check Flask context
|
102
|
+
elif has_app_context():
|
94
103
|
research_id = g.get("research_id")
|
104
|
+
|
95
105
|
return research_id
|
96
106
|
|
97
107
|
|
@@ -104,16 +114,18 @@ def database_sink(message: loguru.Message) -> None:
|
|
104
114
|
|
105
115
|
"""
|
106
116
|
record = message.record
|
107
|
-
research_id = _get_research_id()
|
117
|
+
research_id = _get_research_id(record)
|
108
118
|
|
109
119
|
# Create a new database entry.
|
110
120
|
db_log = ResearchLog(
|
111
121
|
timestamp=record["time"],
|
112
|
-
message=
|
122
|
+
message=record[
|
123
|
+
"message"
|
124
|
+
], # Use raw message to avoid formatting artifacts in web UI
|
113
125
|
module=record["name"],
|
114
126
|
function=record["function"],
|
115
127
|
line_no=int(record["line"]),
|
116
|
-
level=record["level"].name,
|
128
|
+
level=record["level"].name, # Keep original case
|
117
129
|
research_id=research_id,
|
118
130
|
)
|
119
131
|
|
@@ -137,16 +149,17 @@ def frontend_progress_sink(message: loguru.Message) -> None:
|
|
137
149
|
message: The log message to send.
|
138
150
|
|
139
151
|
"""
|
140
|
-
|
152
|
+
record = message.record
|
153
|
+
research_id = _get_research_id(record)
|
141
154
|
if research_id is None:
|
142
155
|
# If we don't have a research ID, don't send anything.
|
156
|
+
# Can't use logger here as it causes deadlock
|
143
157
|
return
|
144
158
|
|
145
|
-
record = message.record
|
146
159
|
frontend_log = dict(
|
147
160
|
log_entry=dict(
|
148
161
|
message=record["message"],
|
149
|
-
type=record["level"].name,
|
162
|
+
type=record["level"].name, # Keep original case
|
150
163
|
time=record["time"].isoformat(),
|
151
164
|
),
|
152
165
|
)
|
@@ -181,7 +194,7 @@ def config_logger(name: str) -> None:
|
|
181
194
|
|
182
195
|
# Add a special log level for milestones.
|
183
196
|
try:
|
184
|
-
logger.level("
|
197
|
+
logger.level("MILESTONE", no=26, color="<magenta><bold>")
|
185
198
|
except ValueError:
|
186
199
|
# Level already exists, that's fine
|
187
200
|
pass
|
@@ -0,0 +1,99 @@
|
|
1
|
+
"""
|
2
|
+
Utility functions for handling thread-local context propagation.
|
3
|
+
|
4
|
+
This module provides helpers for propagating research context across thread boundaries,
|
5
|
+
which is necessary when strategies use ThreadPoolExecutor for parallel searches.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import functools
|
9
|
+
from typing import Any, Callable, Dict
|
10
|
+
|
11
|
+
from ..metrics.search_tracker import get_search_tracker
|
12
|
+
|
13
|
+
|
14
|
+
def preserve_research_context(func: Callable) -> Callable:
|
15
|
+
"""
|
16
|
+
Decorator that preserves research context across thread boundaries.
|
17
|
+
|
18
|
+
Use this decorator on functions that will be executed in ThreadPoolExecutor
|
19
|
+
to ensure the research context (including research_id) is properly propagated.
|
20
|
+
|
21
|
+
Example:
|
22
|
+
@preserve_research_context
|
23
|
+
def search_task(query):
|
24
|
+
return search_engine.run(query)
|
25
|
+
"""
|
26
|
+
|
27
|
+
@functools.wraps(func)
|
28
|
+
def wrapper(*args, **kwargs):
|
29
|
+
# The context should already be captured in the closure when the decorator runs
|
30
|
+
# Set it in the new thread
|
31
|
+
tracker = get_search_tracker()
|
32
|
+
if hasattr(wrapper, "_research_context"):
|
33
|
+
tracker.set_research_context(wrapper._research_context)
|
34
|
+
return func(*args, **kwargs)
|
35
|
+
|
36
|
+
# Capture the current context when the decorator is applied
|
37
|
+
wrapper._research_context = get_search_tracker()._get_research_context()
|
38
|
+
return wrapper
|
39
|
+
|
40
|
+
|
41
|
+
def create_context_preserving_wrapper(
|
42
|
+
func: Callable, context: Dict[str, Any] = None
|
43
|
+
) -> Callable:
|
44
|
+
"""
|
45
|
+
Create a wrapper function that preserves research context.
|
46
|
+
|
47
|
+
This is useful when you need to create the wrapper dynamically and can't use a decorator.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
func: The function to wrap
|
51
|
+
context: Optional explicit context to use. If None, captures current context.
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
A wrapped function that sets the research context before executing
|
55
|
+
"""
|
56
|
+
# Capture context at wrapper creation time if not provided
|
57
|
+
if context is None:
|
58
|
+
context = get_search_tracker()._get_research_context()
|
59
|
+
|
60
|
+
@functools.wraps(func)
|
61
|
+
def wrapper(*args, **kwargs):
|
62
|
+
# Set the captured context in the new thread
|
63
|
+
get_search_tracker().set_research_context(context)
|
64
|
+
return func(*args, **kwargs)
|
65
|
+
|
66
|
+
return wrapper
|
67
|
+
|
68
|
+
|
69
|
+
def run_with_context(
|
70
|
+
func: Callable, *args, context: Dict[str, Any] = None, **kwargs
|
71
|
+
) -> Any:
|
72
|
+
"""
|
73
|
+
Run a function with a specific research context.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
func: The function to run
|
77
|
+
*args: Positional arguments for the function
|
78
|
+
context: Optional explicit context. If None, uses current context.
|
79
|
+
**kwargs: Keyword arguments for the function
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
The result of the function call
|
83
|
+
"""
|
84
|
+
tracker = get_search_tracker()
|
85
|
+
|
86
|
+
# Save current context
|
87
|
+
original_context = tracker._get_research_context()
|
88
|
+
|
89
|
+
try:
|
90
|
+
# Set new context
|
91
|
+
if context is None:
|
92
|
+
context = original_context
|
93
|
+
tracker.set_research_context(context)
|
94
|
+
|
95
|
+
# Run the function
|
96
|
+
return func(*args, **kwargs)
|
97
|
+
finally:
|
98
|
+
# Restore original context
|
99
|
+
tracker.set_research_context(original_context)
|
@@ -61,8 +61,10 @@ def create_app():
|
|
61
61
|
# Disable CSRF for API routes
|
62
62
|
@app.before_request
|
63
63
|
def disable_csrf_for_api():
|
64
|
-
if
|
65
|
-
"/
|
64
|
+
if (
|
65
|
+
request.path.startswith("/api/v1/")
|
66
|
+
or request.path.startswith("/research/api/")
|
67
|
+
or request.path.startswith("/benchmark/api/")
|
66
68
|
):
|
67
69
|
csrf.protect = lambda: None
|
68
70
|
|
@@ -171,23 +173,45 @@ def register_blueprints(app):
|
|
171
173
|
from .routes.metrics_routes import metrics_bp
|
172
174
|
from .routes.research_routes import research_bp
|
173
175
|
from .routes.settings_routes import settings_bp
|
176
|
+
from ..benchmarks.web_api.benchmark_routes import benchmark_bp
|
174
177
|
|
175
178
|
# Add root route
|
176
179
|
@app.route("/")
|
177
180
|
def index():
|
178
|
-
"""Root route -
|
179
|
-
from
|
180
|
-
|
181
|
-
|
181
|
+
"""Root route - serve the research page directly"""
|
182
|
+
from .utils.templates import render_template_with_defaults
|
183
|
+
from ..utilities.db_utils import get_db_setting
|
184
|
+
|
185
|
+
# Load current settings from database
|
186
|
+
settings = {
|
187
|
+
"llm_provider": get_db_setting("llm.provider", "ollama"),
|
188
|
+
"llm_model": get_db_setting("llm.model", ""),
|
189
|
+
"llm_openai_endpoint_url": get_db_setting(
|
190
|
+
"llm.openai_endpoint.url", ""
|
191
|
+
),
|
192
|
+
"search_tool": get_db_setting("search.tool", ""),
|
193
|
+
"search_iterations": get_db_setting("search.iterations", 2),
|
194
|
+
"search_questions_per_iteration": get_db_setting(
|
195
|
+
"search.questions_per_iteration", 3
|
196
|
+
),
|
197
|
+
}
|
198
|
+
|
199
|
+
# Debug logging
|
200
|
+
logger.debug(f"Settings loaded: {settings}")
|
201
|
+
|
202
|
+
return render_template_with_defaults(
|
203
|
+
"pages/research.html", settings=settings
|
204
|
+
)
|
182
205
|
|
183
206
|
# Register blueprints
|
184
207
|
app.register_blueprint(research_bp)
|
185
|
-
app.register_blueprint(history_bp
|
208
|
+
app.register_blueprint(history_bp) # Already has url_prefix="/history"
|
186
209
|
app.register_blueprint(metrics_bp)
|
187
|
-
app.register_blueprint(settings_bp)
|
210
|
+
app.register_blueprint(settings_bp) # Already has url_prefix="/settings"
|
188
211
|
app.register_blueprint(
|
189
212
|
api_bp, url_prefix="/research/api"
|
190
213
|
) # Register API blueprint with prefix
|
214
|
+
app.register_blueprint(benchmark_bp) # Register benchmark blueprint
|
191
215
|
|
192
216
|
# Register API v1 blueprint
|
193
217
|
app.register_blueprint(api_blueprint) # Already has url_prefix='/api/v1'
|