Coverage for src/local_deep_research/constants.py: 100%

64 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-06 15:42 +0000

1"""Project-wide constants for Local Deep Research.""" 

2 

3from enum import StrEnum 

4from typing import Dict, List 

5 

6from .__version__ import __version__ 

7 

8# Honest, identifying User-Agent for APIs that prefer/require identification 

9# (e.g., academic APIs like arXiv, PubMed, OpenAlex) 

10USER_AGENT = ( 

11 f"Local-Deep-Research/{__version__} " 

12 "(Academic Research Tool; https://github.com/LearningCircuit/local-deep-research)" 

13) 

14 

15# Browser-like User-Agent for sites that may block bot requests 

16# Use sparingly and only when necessary 

17BROWSER_USER_AGENT = ( 

18 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 

19 "AppleWebKit/537.36 (KHTML, like Gecko) " 

20 "Chrome/120.0.0.0 Safari/537.36" 

21) 

22 

23 

24# Code-side single source of truth for the default search engine, used by 

25# every reader that needs a fallback for a MISSING ``search.tool`` setting 

26# (partial snapshots from the programmatic API, un-bootstrapped settings 

27# DBs). Import THIS instead of hardcoding the string literal: scattered 

28# literals are how the old ``"auto"`` default lingered across ~30 sites and 

29# had to be hunted down one by one when the meta engines were removed. 

30# Must match the registered default in defaults/default_settings.json — 

31# pinned by tests/test_constants.py::test_default_search_tool_matches_registry. 

32# Mirrors the DEFAULT_EGRESS_SCOPE pattern in security/egress/policy.py. 

33DEFAULT_SEARCH_TOOL: str = "searxng" 

34 

35 

36# --- Research status values --- 

37# Frontend helpers: src/local_deep_research/web/static/js/config/constants.js 

38# Injected via: src/local_deep_research/web/template_config.py (_LDRTemplates.TemplateResponse) 

39# Template: src/local_deep_research/web/templates/base.html 

40# If you add/remove/rename a status here, the frontend picks it up automatically. 

41class ResearchStatus(StrEnum): 

42 """Status values for research records. 

43 

44 Uses StrEnum so values compare equal to plain strings, 

45 e.g. ``ResearchStatus.COMPLETED == "completed"`` is True. 

46 

47 Lifecycle:: 

48 

49 [*] ─┬─► QUEUED ─┬─► IN_PROGRESS ─┬─► COMPLETED 

50 │ │ ├─► FAILED 

51 │ └─► SUSPENDED └─► SUSPENDED 

52 │ (concurrency limit) (terminated while queued) 

53 

54 └─► IN_PROGRESS (slots available, skips queue) 

55 

56 Notes: 

57 - PENDING is declared as a model default but no creation path 

58 actually sets it. All routes use QUEUED or IN_PROGRESS. 

59 - ERROR is checked as a terminal state but never set by current 

60 code. It predates FAILED and exists for backward compatibility 

61 with older database records. 

62 - CANCELLED is not used by the research workflow. It is used by 

63 the benchmark subsystem (BenchmarkStatus, BenchmarkTaskStatus). 

64 """ 

65 

66 # --- Active lifecycle states --- 

67 PENDING = "pending" # Model default; never set by any creation path 

68 QUEUED = "queued" # Waiting for a worker slot 

69 IN_PROGRESS = "in_progress" # Worker actively executing 

70 

71 # --- Terminal states --- 

72 COMPLETED = "completed" # Finished successfully 

73 SUSPENDED = "suspended" # User terminated the research 

74 FAILED = "failed" # Unrecoverable error during execution 

75 

76 # --- Legacy / compatibility --- 

77 ERROR = "error" # Never set; predates FAILED 

78 CANCELLED = "cancelled" # Unused by research; for benchmarks 

79 

80 

81# --- Research library file_path sentinel values --- 

82FILE_PATH_METADATA_ONLY = "metadata_only" 

83FILE_PATH_TEXT_ONLY = "text_only_not_stored" 

84FILE_PATH_BLOB_DELETED = "blob_deleted" 

85FILE_PATH_SENTINELS = ( 

86 FILE_PATH_METADATA_ONLY, 

87 FILE_PATH_TEXT_ONLY, 

88 FILE_PATH_BLOB_DELETED, 

89) 

90 

91# --- Default RAG / Local Search settings --- 

92# Code-side single source of truth for local search / embedding defaults, 

93# used by every reader needing a fallback for missing settings. 

94# Must match the registered defaults in defaults/settings_local_search.json — 

95# pinned by tests/test_constants.py::test_default_local_search_settings_match_registry. 

96DEFAULT_LOCAL_SEARCH_PROVIDER: str = "sentence_transformers" 

97DEFAULT_LOCAL_SEARCH_MODEL: str = "all-MiniLM-L6-v2" 

98DEFAULT_LOCAL_SEARCH_CHUNK_SIZE: int = 1000 

99DEFAULT_LOCAL_SEARCH_CHUNK_OVERLAP: int = 200 

100DEFAULT_LOCAL_SEARCH_SPLITTER_TYPE: str = "recursive" 

101DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS: List[str] = ["\n\n", "\n", ". ", " ", ""] 

102DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS_JSON = '["\\n\\n", "\\n", ". ", " ", ""]' 

103DEFAULT_LOCAL_SEARCH_DISTANCE_METRIC: str = "cosine" 

104DEFAULT_LOCAL_SEARCH_NORMALIZE_VECTORS: bool = True 

105DEFAULT_LOCAL_SEARCH_INDEX_TYPE: str = "flat" 

106 

107 

108# --- Snippet / truncation lengths --- 

109SNIPPET_LENGTH_SHORT = 250 

110SNIPPET_LENGTH_LONG = 500 

111 

112# --- /history/logs/<id> pagination caps --- 

113# Default matches the frontend logpanel DOM cap (MAX_LOG_ENTRIES); the 

114# hard cap is the ceiling the route clamps to so a client cannot force 

115# an unbounded load. Shared with the frontend by the template constants 

116# injection (see web/template_config.py) → window.LDR_LOG_LIMITS (see 

117# base.html). 

118HISTORY_LOGS_DEFAULT_LIMIT = 500 

119HISTORY_LOGS_HARD_CAP = 5000 

120 

121# --- Research history collection --- 

122RESEARCH_HISTORY_COLLECTION_NAME = "History" 

123RESEARCH_HISTORY_COLLECTION_DESCRIPTION = ( 

124 "Your research history indexed for AI-powered semantic search. " 

125 "Indexing converts past research reports and their sources into " 

126 "searchable content, enabling natural-language queries across all " 

127 "your previous research. Used by the History page search when in " 

128 "AI or Hybrid mode." 

129) 

130 

131# --- Available search strategies (UI-facing) --- 

132# Single source of truth for strategies shown in all UI dropdowns. 

133# create_strategy() in search_system_factory.py handles additional names 

134# (aliases, internal strategies such as "news_aggregation") — this list is 

135# purely for the UI. 

136AVAILABLE_STRATEGIES: List[Dict[str, str]] = [ 

137 { 

138 "name": "source-based", 

139 "label": "Source-Based (Best for small <16,000 context window)", 

140 "description": "Comprehensive research with inline citations. Focuses on finding and extracting information from authoritative sources.", 

141 }, 

142 { 

143 "name": "focused-iteration", 

144 "label": "Focused Iteration - Quick (Minimal text output)", 

145 "description": "Fast & precise Q&A with iterative search. Good for complex queries requiring specific answers.", 

146 }, 

147 { 

148 "name": "focused-iteration-standard", 

149 "label": "Focused Iteration - Comprehensive (Needs >16,000 context window)", 

150 "description": "Detailed long-form output with citations. Uses standard citation handler for comprehensive answers.", 

151 }, 

152 { 

153 "name": "topic-organization", 

154 "label": "Topic Organization (Clusters by topic)", 

155 "description": "Clusters sources into topics with lead texts. Organizes research by themes for structured output.", 

156 }, 

157 { 

158 "name": "langgraph-agent", 

159 "label": "LangGraph Agent (Autonomous agentic research)", 

160 "description": "Agentic research where the LLM autonomously decides what to search, which engines to use, and when to synthesize. Supports all search engines as tools.", 

161 }, 

162] 

163 

164LANGGRAPH_STRATEGY_NAME = "langgraph-agent" 

165LANGGRAPH_STRATEGY_ALIASES = ( 

166 "langgraph-agent", 

167 "langgraph_agent", 

168 "mcp", 

169 "agentic", 

170) 

171 

172 

173# --- Journal quality scoring thresholds --- 

174# Used by journal_quality.scoring.derive_quality_score and 

175# journal_quality.scoring.institution_score_from_h_index. Single source of 

176# truth so the build phase, the runtime filter, and the dashboard agree on 

177# what each h-index threshold means. 

178# 

179# Thresholds calibrated from real OpenAlex data: 

180# - Nature h-index ≈ 1,442 

181# - PLOS ONE h-index ≈ 467 

182# - Only ~3 journals globally have h-index > 1,000 

183# h-index has field-dependent bias (math vs biomed); these are general-purpose. 

184 

185# Journal h-index thresholds → quality scores 

186JOURNAL_HINDEX_ELITE = 150 # Nature/Science/NEJM tier 

187JOURNAL_HINDEX_STRONG = 75 

188JOURNAL_HINDEX_VERY_GOOD = 40 

189JOURNAL_HINDEX_GOOD = 20 

190JOURNAL_HINDEX_ACCEPTABLE = 10 

191 

192# Journal quality scores (1–10 scale) 

193JOURNAL_QUALITY_PREDATORY = 1 

194JOURNAL_QUALITY_DEFAULT = 4 

195JOURNAL_QUALITY_ACCEPTABLE = 5 

196JOURNAL_QUALITY_GOOD = 6 

197JOURNAL_QUALITY_VERY_GOOD = 7 

198JOURNAL_QUALITY_STRONG = 8 

199JOURNAL_QUALITY_ELITE = 10 

200 

201# The complete set of scores the scoring algorithm emits. Scores 2, 3, 9 

202# are deliberately never produced by the tiered scoring logic; LLM outputs 

203# outside this set are rejected as parse failures so prompt drift surfaces 

204# via the existing failure counter rather than silently snapping. 

205# INVARIANT: score 9 is intentionally NOT in this set. Tier 4 LLM prompts 

206# never produce it and Tier 1-3 thresholds skip directly from 8 (h>=75) 

207# to 10 (h>=150). Do not "add it for completeness" — downstream code in 

208# search_utilities._format_quality_tag has a defensive branch for 9 that 

209# is currently dead by design. 

210VALID_QUALITY_SCORES = frozenset( 

211 { 

212 JOURNAL_QUALITY_PREDATORY, 

213 JOURNAL_QUALITY_DEFAULT, 

214 JOURNAL_QUALITY_ACCEPTABLE, 

215 JOURNAL_QUALITY_GOOD, 

216 JOURNAL_QUALITY_VERY_GOOD, 

217 JOURNAL_QUALITY_STRONG, 

218 JOURNAL_QUALITY_ELITE, 

219 } 

220) 

221 

222# DOAJ scoring. There used to be a higher DOAJ_QUALITY_WITH_SEAL = 8 

223# tier, but DOAJ retired the Seal in April 2025 and removed it from 

224# their metadata, so listing is now the only DOAJ signal: 

225# https://blog.doaj.org/2025/04/09/our-metadata-changes-are-live-and-the-seal-has-been-retired/ 

226DOAJ_QUALITY_LISTED = 5 

227CONFERENCE_QUALITY_DEFAULT = ( 

228 5 # Neutral; in CS top conferences are Q1-equivalent 

229) 

230# Preprint repositories (arXiv, bioRxiv, SSRN, PsyArXiv, ...) are not 

231# peer-reviewed — the venue itself carries no quality signal. Cap all 

232# repository-type sources at this score regardless of their h-index, 

233# which is inflated by aggregating thousands of highly-cited papers 

234# (arXiv has h=674 because of its authors, not because of venue rigor). 

235# Matches the conference default: "acceptable, but the venue doesn't 

236# vouch for the paper". The filter's Tier 3.5 institution salvage can 

237# lift this to 6 when the authors are at a strong institution. 

238REPOSITORY_QUALITY_DEFAULT = 5 

239 

240# Predatory whitelist override threshold. A flagged journal is rescued 

241# if it's in DOAJ (evidence-based) OR has h-index strictly greater than 

242# this value (heuristic — `>`, not `>=`). 

243# 

244# Do not re-tune without literature support. The h-index is an impact 

245# metric, not an integrity signal. Reviews of predatory-vs-legitimate 

246# classification (Blacklists and Whitelists to Tackle Predatory 

247# Publishing, mBio 2019, and the PMC2020 review that followed) treat 

248# DOAJ indexing + COPE / OASPA membership as the evidence-based 

249# whitelist — NOT any specific h-index boundary. The value 10 and 

250# strict-> here are pragmatic defaults; tuning them only changes 

251# behavior at the boundary and has no published basis. If you want 

252# real improvement, ADD more signals (JCR listing, OASPA membership) 

253# rather than tweaking this number. (Investigated in PR #3081, 2026-04.) 

254PREDATORY_WHITELIST_HINDEX = 10 

255 

256# Institution h-index thresholds → quality scores. Capped at 

257# INSTITUTION_QUALITY_TOP — institution salvage scoring never beats a real 

258# venue match. 

259INSTITUTION_HINDEX_TOP = 250 # Top-tier research universities 

260INSTITUTION_HINDEX_HIGH = 50 

261INSTITUTION_QUALITY_TOP = 6 

262INSTITUTION_QUALITY_HIGH = 5 

263INSTITUTION_QUALITY_DEFAULT = 4 

264 

265 

266# --- API timeouts --- 

267# OpenAlex DOI→source_id batch enrichment. Distinct from the OpenAlex search 

268# engine timeout (which uses the safe_requests default of 30s) because batch 

269# metadata lookups are lightweight and we'd rather fail fast than block the 

270# pre-enrichment layer. 

271OPENALEX_ENRICHMENT_API_TIMEOUT = 15 

272 

273 

274# --- Journal-quality dataset download --- 

275# Minimum free disk space required before starting a bulk download. The 

276# five sources uncompress to ~1 GB total intermediate working set; the 2 

277# GB floor gives headroom for the atomic temp file + compiled DB while 

278# leaving room for the user's other work. 

279JOURNAL_QUALITY_MIN_FREE_DISK_BYTES = 2 * 1024**3 

280 

281 

282def get_available_strategies() -> List[Dict[str, str]]: 

283 """Get the list of available research strategies shown in the UI. 

284 

285 Returns: 

286 List of dictionaries with 'name', 'label', and 'description' keys. 

287 """ 

288 return AVAILABLE_STRATEGIES.copy()