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.
Files changed (91) hide show
  1. local_deep_research/__version__.py +1 -1
  2. local_deep_research/advanced_search_system/candidate_exploration/progressive_explorer.py +11 -1
  3. local_deep_research/advanced_search_system/questions/browsecomp_question.py +32 -6
  4. local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py +33 -8
  5. local_deep_research/advanced_search_system/strategies/source_based_strategy.py +2 -0
  6. local_deep_research/api/__init__.py +2 -0
  7. local_deep_research/api/research_functions.py +177 -3
  8. local_deep_research/benchmarks/graders.py +150 -5
  9. local_deep_research/benchmarks/models/__init__.py +19 -0
  10. local_deep_research/benchmarks/models/benchmark_models.py +283 -0
  11. local_deep_research/benchmarks/ui/__init__.py +1 -0
  12. local_deep_research/benchmarks/web_api/__init__.py +6 -0
  13. local_deep_research/benchmarks/web_api/benchmark_routes.py +862 -0
  14. local_deep_research/benchmarks/web_api/benchmark_service.py +920 -0
  15. local_deep_research/config/llm_config.py +106 -21
  16. local_deep_research/defaults/default_settings.json +448 -3
  17. local_deep_research/error_handling/report_generator.py +10 -0
  18. local_deep_research/llm/__init__.py +19 -0
  19. local_deep_research/llm/llm_registry.py +155 -0
  20. local_deep_research/metrics/db_models.py +3 -7
  21. local_deep_research/metrics/search_tracker.py +25 -11
  22. local_deep_research/report_generator.py +3 -2
  23. local_deep_research/search_system.py +12 -9
  24. local_deep_research/utilities/log_utils.py +23 -10
  25. local_deep_research/utilities/thread_context.py +99 -0
  26. local_deep_research/web/app_factory.py +32 -8
  27. local_deep_research/web/database/benchmark_schema.py +230 -0
  28. local_deep_research/web/database/convert_research_id_to_string.py +161 -0
  29. local_deep_research/web/database/models.py +55 -1
  30. local_deep_research/web/database/schema_upgrade.py +397 -2
  31. local_deep_research/web/database/uuid_migration.py +265 -0
  32. local_deep_research/web/routes/api_routes.py +62 -31
  33. local_deep_research/web/routes/history_routes.py +13 -6
  34. local_deep_research/web/routes/metrics_routes.py +264 -4
  35. local_deep_research/web/routes/research_routes.py +45 -18
  36. local_deep_research/web/routes/route_registry.py +352 -0
  37. local_deep_research/web/routes/settings_routes.py +382 -22
  38. local_deep_research/web/services/research_service.py +22 -29
  39. local_deep_research/web/services/settings_manager.py +53 -0
  40. local_deep_research/web/services/settings_service.py +2 -0
  41. local_deep_research/web/static/css/styles.css +8 -0
  42. local_deep_research/web/static/js/components/detail.js +7 -14
  43. local_deep_research/web/static/js/components/details.js +8 -10
  44. local_deep_research/web/static/js/components/fallback/ui.js +4 -4
  45. local_deep_research/web/static/js/components/history.js +6 -6
  46. local_deep_research/web/static/js/components/logpanel.js +14 -11
  47. local_deep_research/web/static/js/components/progress.js +51 -46
  48. local_deep_research/web/static/js/components/research.js +250 -89
  49. local_deep_research/web/static/js/components/results.js +5 -7
  50. local_deep_research/web/static/js/components/settings.js +32 -26
  51. local_deep_research/web/static/js/components/settings_sync.js +24 -23
  52. local_deep_research/web/static/js/config/urls.js +285 -0
  53. local_deep_research/web/static/js/main.js +8 -8
  54. local_deep_research/web/static/js/research_form.js +267 -12
  55. local_deep_research/web/static/js/services/api.js +18 -18
  56. local_deep_research/web/static/js/services/keyboard.js +8 -8
  57. local_deep_research/web/static/js/services/socket.js +53 -35
  58. local_deep_research/web/static/js/services/ui.js +1 -1
  59. local_deep_research/web/templates/base.html +4 -1
  60. local_deep_research/web/templates/components/custom_dropdown.html +5 -3
  61. local_deep_research/web/templates/components/mobile_nav.html +3 -3
  62. local_deep_research/web/templates/components/sidebar.html +9 -3
  63. local_deep_research/web/templates/pages/benchmark.html +2697 -0
  64. local_deep_research/web/templates/pages/benchmark_results.html +1136 -0
  65. local_deep_research/web/templates/pages/benchmark_simple.html +453 -0
  66. local_deep_research/web/templates/pages/cost_analytics.html +1 -1
  67. local_deep_research/web/templates/pages/metrics.html +212 -39
  68. local_deep_research/web/templates/pages/research.html +8 -6
  69. local_deep_research/web/templates/pages/star_reviews.html +1 -1
  70. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +14 -1
  71. local_deep_research/web_search_engines/engines/search_engine_brave.py +15 -1
  72. local_deep_research/web_search_engines/engines/search_engine_ddg.py +20 -1
  73. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +26 -2
  74. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +15 -1
  75. local_deep_research/web_search_engines/engines/search_engine_retriever.py +192 -0
  76. local_deep_research/web_search_engines/engines/search_engine_tavily.py +307 -0
  77. local_deep_research/web_search_engines/rate_limiting/__init__.py +14 -0
  78. local_deep_research/web_search_engines/rate_limiting/__main__.py +9 -0
  79. local_deep_research/web_search_engines/rate_limiting/cli.py +209 -0
  80. local_deep_research/web_search_engines/rate_limiting/exceptions.py +21 -0
  81. local_deep_research/web_search_engines/rate_limiting/tracker.py +506 -0
  82. local_deep_research/web_search_engines/retriever_registry.py +108 -0
  83. local_deep_research/web_search_engines/search_engine_base.py +161 -43
  84. local_deep_research/web_search_engines/search_engine_factory.py +14 -0
  85. local_deep_research/web_search_engines/search_engines_config.py +20 -0
  86. local_deep_research-0.6.0.dist-info/METADATA +374 -0
  87. {local_deep_research-0.5.7.dist-info → local_deep_research-0.6.0.dist-info}/RECORD +90 -65
  88. local_deep_research-0.5.7.dist-info/METADATA +0 -420
  89. {local_deep_research-0.5.7.dist-info → local_deep_research-0.6.0.dist-info}/WHEEL +0 -0
  90. {local_deep_research-0.5.7.dist-info → local_deep_research-0.6.0.dist-info}/entry_points.txt +0 -0
  91. {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(Integer)
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.research_context = {}
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(f"Search tracker context updated: {self.research_context}")
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
- research_id = self.research_context.get("research_id")
42
- research_query = self.research_context.get("research_query")
43
- research_mode = self.research_context.get("research_mode", "unknown")
44
- research_phase = self.research_context.get("research_phase", "search")
45
- search_iteration = self.research_context.get("search_iteration", 0)
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
- print(f"Processing section: {section['name']}")
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
- print(
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
- from .advanced_search_system.strategies.standard_strategy import (
51
- StandardSearchStrategy,
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() == "source-based":
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
- # PRESERVE SIMPLEQA PERFORMANCE: Keep proven 96.51% accuracy configuration
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=8, # PROVEN OPTIMAL FOR SIMPLEQA (96.51% accuracy)
475
- questions_per_iteration=5, # PROVEN OPTIMAL FOR SIMPLEQA
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
- if has_app_context():
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=str(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
- research_id = _get_research_id()
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("milestone", no=26, color="<magenta><bold>")
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 request.path.startswith("/api/v1/") or request.path.startswith(
65
- "/research/api/"
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 - redirect to research page"""
179
- from flask import redirect, url_for
180
-
181
- return redirect(url_for("research.index"))
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, url_prefix="/research/api")
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'