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
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 15:42 +0000
1"""Project-wide constants for Local Deep Research."""
3from enum import StrEnum
4from typing import Dict, List
6from .__version__ import __version__
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)
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)
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"
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.
44 Uses StrEnum so values compare equal to plain strings,
45 e.g. ``ResearchStatus.COMPLETED == "completed"`` is True.
47 Lifecycle::
49 [*] ─┬─► QUEUED ─┬─► IN_PROGRESS ─┬─► COMPLETED
50 │ │ ├─► FAILED
51 │ └─► SUSPENDED └─► SUSPENDED
52 │ (concurrency limit) (terminated while queued)
53 │
54 └─► IN_PROGRESS (slots available, skips queue)
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 """
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
71 # --- Terminal states ---
72 COMPLETED = "completed" # Finished successfully
73 SUSPENDED = "suspended" # User terminated the research
74 FAILED = "failed" # Unrecoverable error during execution
76 # --- Legacy / compatibility ---
77 ERROR = "error" # Never set; predates FAILED
78 CANCELLED = "cancelled" # Unused by research; for benchmarks
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)
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"
108# --- Snippet / truncation lengths ---
109SNIPPET_LENGTH_SHORT = 250
110SNIPPET_LENGTH_LONG = 500
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
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)
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]
164LANGGRAPH_STRATEGY_NAME = "langgraph-agent"
165LANGGRAPH_STRATEGY_ALIASES = (
166 "langgraph-agent",
167 "langgraph_agent",
168 "mcp",
169 "agentic",
170)
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.
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
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
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)
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
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
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
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
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
282def get_available_strategies() -> List[Dict[str, str]]:
283 """Get the list of available research strategies shown in the UI.
285 Returns:
286 List of dictionaries with 'name', 'label', and 'description' keys.
287 """
288 return AVAILABLE_STRATEGIES.copy()