pip 安装后仍需自行准备 Ollama(或 OpenAI 兼容端点)与 SearXNG;Docker 的 --network host 仅在原生 Linux 可用,Mac/Windows/WSL2 需改用 Compose;自 v1.10.3 起私有/localhost 搜索引擎地址默认被拦截,需通过环境变量显式放行;Windows 导出 PDF 需额外安装 Pango。
local-deep-research
LearningCircuitMIT活跃维护本地优先的深度研究问答系统,接 llama.cpp、Ollama 等本地或云端模型,聚合 arXiv、PubMed 及私有文档等十余个检索源,全程加密。
~95% on SimpleQA (e.g. Qwen3.6-27B on a 3090). Supports all local and cloud LLMs (llama.cpp, Ollama, Google, ...). 10+ search engines - arXiv, PubMed, your private documents. Everything Local & Encrypted.
pip install local-deep-research
python -m local_deep_research.web.appREADME 称可在单张 RTX 3090 上以 Qwen3.6-27B 全本地运行;也提供 CPU-only 的 Docker Compose 方案;CPU 需支持 AVX(Intel Sandy Bridge / AMD Bulldozer 及更新)。
本地可跑的深度研究助手,用多 LLM 与多搜索引擎做带引用的 agentic 检索
本地优先的深度研究问答系统,接 llama.cpp、Ollama 等本地或云端模型,聚合 arXiv、PubMed 及私有文档等十余个检索源,全程加密。
核心能力
- 多 LLM 与 10+ 搜索引擎组合做带引用的深度研究
- 把检索到的论文网页下载进加密库并索引供后续检索
- 每用户独立 SQLCipher 数据库,密钥由密码派生
- 支持 llama.cpp、Ollama 及 OpenAI 兼容端点等本地与云端模型
- 提供 Docker、Docker Compose 与 pip 三种部署方式
容易踩的坑
- v2.0.0Release 2.0.0最新正式版2026-11-07 · 2026-11-07 20:10 UTC
LDR 2.0
LDR 2.0 is our biggest architectural change yet. We rebuilt the web layer on FastAPI and Uvicorn, hardened security across the whole app, added new search engines, and made Stop actually stop research mid-generation. It took 1,010 commits in ten weeks, from 40 contributors, 31 of them new to the project. Thank you to everyone who built, tested and reported issues.
Several defaults and API contracts changed. Read the upgrade notes before deploying, especially if you run LDR behind a reverse proxy or call its HTTP API from your own clients.
⚠️ Before you upgrade
- Take a verified offline backup first. 2.0 changes your databases, and v1.10.7 can't use them afterwards. LDR's built-in backups aren't a substitute (see the next point). Never start v1.10.7 on upgraded data, because doing so can delete your remaining built-in backups. Follow the upgrade and rollback guide, which also covers failed upgrades.
- Pre-migration backups now live in their own folder. Each user gets
encrypted_databases/backups/<user hash>/pre-migration/. Copy that folder out of the data directory after each user's first sign-in. With the default retention (1 backup, 7 days), the first login backup after the upgrade removes the regular backups made before it. Raise Max Backups and Backup Retention (days) first if you want to keep them. - Check your settings and environment overrides. Invalid configured values now raise an error instead of silently falling back to a default. This includes bad
LDR_*booleans (onlytrue/1/yes/on/enabledandfalse/0/no/off/disabledare accepted),llm.request_timeoutbelow 1 second, andllm.max_retriesoutside 0–5. The settings page, research form and API errors name the offending key. - Everyone must sign in again after upgrading.
Breaking changes for operators
- Reverse proxies:
- If a trusted proxy terminates TLS, set
TRUST_PROXY_HEADERS=trueand block direct access to the backend. - Rate limiting now trusts forwarded headers only from private or loopback peers.
- A proxy on a public or CGNAT address (cloud load balancers, Tailscale) needs
LDR_SERVER_FORWARDED_ALLOW_IPSset to its address. Otherwise all clients behind it share one rate-limit bucket and one login-lockout counter. - A single proxy must overwrite
X-Forwarded-ForandX-Real-IP, not append to them. - Proxy chains (for example CDN → nginx) can use the new
LDR_SERVER_TRUSTED_PROXY_COUNT. See "Proxy chains" indocs/deployment/reverse-proxy.md.
- If a trusted proxy terminates TLS, set
- Socket.IO moved from
/socket.ioto/ws/socket.io. Update any explicit proxylocationrules and custom clients. - Direct (non-container) launches bind to
127.0.0.1. SetLDR_WEB_HOST=0.0.0.0if you want remote access. Docker images already set this. - Research-library storage path is operator-managed. Users can no longer change it in Settings. Use
LDR_RESEARCH_LIBRARY_STORAGE_PATHinstead. The legacy shared-directory PDF read fallback has been removed. - LAN notification targets (self-hosted Apprise plugins on private hosts) now need
LDR_NOTIFICATIONS_ALLOW_PRIVATE_IPS=true, plus an egress scope that permits the host. - Linux servers are now non-dumpable. They write no core dumps, and same-user
py-spy,gdbandlsofno longer work against them. Startup fails if a seccomp profile deniesprctl. - Non-Docker installs must install NLTK data once. Uploaded Office, HTML and email documents are now parsed in bounded child processes, which need the
punkt_tabandaveraged_perceptron_tagger_engpackages. The Docker image ships them. - FAISS indexes convert on their next write. No re-embedding is needed. Older LDR versions must rebuild a converted index before they can read it.
Breaking changes for API clients
- CSRF tokens are now required for session-authent
- v1.10.7Release 1.10.72026-08-28 · 2026-08-28 21:43 UTC
查看更新说明
Security hardening for notification URLs and the news subscription API
This is a security release. It closes several server-side request forgery (SSRF) and local-file-read holes in outbound notifications and the news subscription API. If you have notifications enabled or use news subscriptions with a custom LLM endpoint, update promptly.
Notification URL validation
Three related fixes tighten what a notification URL is allowed to do:
- Unsafe Apprise query options are now rejected before any hostname parsing or DNS lookup (#5085). Options like
template,redirect, and mail-specific SMTP/PGP/WKD parameters could select a second, unvalidated destination or resource — for example,discord://id/token?template=/etc/passwdpreviously made the server read a local file and send it to the webhook. The dependency floor is nowapprise>=1.12.0,<2, with a test that fails CI if Apprise's query-parsing semantics ever drift from what the validator mirrors. - The test-notification endpoint now validates every URL in a multi-URL string individually (#5120, via #5132). Commas are legal URL characters, so previously only the leading entry was checked while Apprise still notified the rest — letting an unvetted destination (such as a cloud-metadata address) receive the test notification. Target counts are now capped, and the test is refused if Apprise registers more services than were validated.
- Legacy numeric IPv4 host forms are rejected before DNS resolution (#5086). Decimal, octal, hex, and mixed-base forms like
2130706433,0x7f000001, or0177.0.0.1— including percent-encoded and trailing-dot variants — could previously resolve to loopback or metadata addresses depending on the platform. They are now blocked deterministically on every platform, in both the notification validator and the shared SSRF validator.
News subscription API
An SSRF guard for the
custom_endpointfield (a user-supplied LLM base URL the server fetches) existed only on a blueprint nothing routes to; it is now applied on the live/news/apiblueprint for both subscription creation and update (#5603). The guard blocks cloud-metadata and link-local addresses and non-HTTP schemes, while deliberately keeping loopback and private-network addresses allowed so local backends like Ollama, LM Studio, and vLLM keep working. A wrong-typedcustom_endpointnow returns a 400 instead of a 500. Feed and history endpoints additionally reject non-UUID subscription ids as defence in depth (the underlying queries were already escaped and not exploitable).🔒 Security
- Notification URL validation now rejects Apprise query options that can select an unvalidated secondary destination or resource (
template,redirect, and mail-specific SMTP/PGP/WKD options) before any hostname parsing or DNS lookup. Atemplateoption is an unrestricted Apprise attachment, sodiscord://id/token?template=/etc/passwdpreviously made the server read a local file and ship it to the user's webhook. Query-key parsing mirrors Apprise's ownparse_qsdrules (∧separators,+-as-space, percent-decoding, duplicate keys) so encoded or semicolon-separated names cannot bypass the policy, and a malformed percent escape in a key is rejected outright. The published dependency now declaresapprise>=1.12.0,<2— 1.12.0 is the reviewed parser floor — and a test diffs LDR's canonical keys against the installed Apprise parser so the contract cannot drift silently. Notification security documentation and compatibility tests now cover mode-dependent destinations and the current direct scheme-prefix gaps. (#5085) - The notification test endpoint (
/api/notifications/test-url) now validates every URL in a comma/space-separated multi-URL string individually. Previously the input was validated as a single URL — commas are legal URL characters, so only the leading entry was checked while Appri
- Unsafe Apprise query options are now rejected before any hostname parsing or DNS lookup (#5085). Options like
- v1.10.6Release 1.10.62026-08-28 · 2026-08-28 14:38 UTC
查看更新说明
Cross-user isolation fixes and a reworked citation pipeline
This release is strongly recommended for multi-user deployments: it closes several ways one signed-in user could touch another user's benchmark runs, research runs, and live progress events. It also contains one small breaking API change for external programmatic users of
SearchResultsCollector— see the first section below.Breaking change:
SearchResultsCollector.add_results()return typeIf you use LDR's search collector from your own code,
add_results()now returns(start, indexed)instead of just the start index. Migratestart = collector.add_results(results)tostart, indexed = collector.add_results(results)and passindexedto_format_results()so deduped[N]citation markers stay aligned with the collector. All internal call sites were already updated. (#5381)Security: per-user isolation and hardening
- Benchmark runs were keyed by the per-user integer run id alone, so two users owning the same id (both "run 1") could cancel each other's runs, leak each other's results and credentials, and receive each other's live progress events in the browser. The run registry and Socket.IO subscriptions are now keyed by
(username, run id). Ordinary research is unaffected — those ids are UUIDs. (#5600, #5520) - Research ownership: starting a follow-up and terminating a research run now verify the caller owns the parent/target research before any shared state is touched. (#5600)
- Notification URL secrecy:
notifications.service_urlembeds credentials (e.g.discord://webhook/token) but was returned in plaintext by the settings API. It's now redacted like API keys, the UI handles the redaction sentinel correctly, and Test Notification falls back to the stored URL. (#5602) - Other hardening:
/library/document/...resolution rejects guessable filenames at the regex (#5496); relevance-filter parser diagnostics are scrubbed of credential-shaped content (#5213); theLDR_SECURITY_ALLOW_NAT64opt-in no longer widens the private-IP policy and decodes wrapped addresses at the correct RFC 6052 positions (#5594).
Citations: dedup that keeps unique evidence
On the LangGraph agent strategy, repeated hits of one source reuse their existing
[N]— but a genuinely different excerpt of that source now gets its own citation instead of being silently discarded (#5381, #5894). Library/RAG citations are chunk-targeted (/library/document/<id>/chunks#chunk-<n>), so clicking a citation scrolls to the cited chunk, and all views of one document collapse to a single bibliography line with every citation number it owns.fetch_contentcan read those chunk citations again,sources_countnow agrees with the rendered## Sourcesblock, and BibTeX/Quarto/LaTeX exports keep documents cited from several chunks (RIS export is still pending, tracked in #5687). (#5685, #5894)Notifications: Telegram scheme change
Telegram notification URLs must use Apprise's canonical
tgram://scheme;telegram://is now rejected with a dedicated migration error pointing at the right form. Multi-target URLs liketgram://...?to=id1,id2are no longer split into partial notifications, and Apprise log output that could leak notification credentials is suppressed. (#5576)SearXNG
- New
enginessetting lets you restrict LDR's queries to a specific set of engines on your instance — useful when premium-API engines (Kagi, Brave) should only be spent on demand. Empty keeps the current all-engines behavior. (#5622) - The rate limiter no longer evicts an in-use lock at capacity or loses its timestamp, so
delay_between_requestskeeps applying under pressure. (#5607, #5688)
Settings fixes
- The settings lock now also guards delete, import, and reset-to-defaults — and reset can no longer silently clear the lock. Locked operations return 403. (#5659)
- Importing settings validates values against the current defaults schema; invalid entries a
- Benchmark runs were keyed by the per-user integer run id alone, so two users owning the same id (both "run 1") could cancel each other's runs, leak each other's results and credentials, and receive each other's live progress events in the browser. The run registry and Socket.IO subscriptions are now keyed by
- v1.10.5Release 1.10.52026-08-16 · 2026-08-16 17:47 UTC
查看更新说明
Making the SearXNG private-URL guard livable
This release smooths out the private-URL protection introduced in v1.10.3, which blocked self-hosted SearXNG instances on localhost/LAN by default but made the failure hard to diagnose and the fix hard to apply.
If your self-hosted SearXNG stopped returning results
The research form now shows a dismissible warning banner when SearXNG is active but its private/localhost instance URL hasn't been approved by the server operator — the situation that previously caused the engine to silently self-disable and return no results. The banner includes the exact environment line needed to fix it. The runtime log message was also upgraded from WARNING to ERROR and now names all three remedies, as does the rejection message when saving such a URL in settings.
To approve a self-hosted instance, the server operator picks one of these in the server environment, then restarts:
- New in this release (recommended):
LDR_SEARCH_PRIVATE_ENGINE_URL_ALLOWLIST=http://localhost:8080— a comma-separated list of exact URL origins that may be private. Finer-grained than the blanket flag: only listed origins get private access. Matching is exact scheme + host + port (no wildcards); cloud-metadata addresses and non-http(s) schemes stay blocked even if listed, and the setting is env-only so it can't be changed through the web UI. - Env-lock the URL itself via
LDR_SEARCH_ENGINE_WEB_SEARXNG_DEFAULT_PARAMS_INSTANCE_URL(what the bundled docker-compose already does — Docker Compose users are unaffected). LDR_SEARCH_ALLOW_PRIVATE_ENGINE_URLS=trueto allow all private engine URLs (broadest).
Mirror-image warning for local document engines
A companion banner now appears when a local document engine (Paperless-ngx, Elasticsearch) points at a host that doesn't look private — since search queries about your private documents would then leave the machine, and a Private-only egress scope would silently exclude the engine. It's advisory and dismissible, for cases like a deliberate Elastic Cloud endpoint.
Docs
The README quick start, installation guides, FAQ, troubleshooting, and
.env.templatenow document the v1.10.3 guard next to everylocalhost:8080example, instead of describing the old allow-by-default behavior — so fresh installs following the docs won't hit the silent-breakage case.✨ New Features
- The research form now shows a dismissible warning when SearXNG would silently return no results because its private/localhost instance URL has not been approved by the server operator — including the exact environment line to fix it. A mirror-image warning appears when a local document engine (Paperless-ngx, Elasticsearch) points at a host that does not look private, since document-search queries would then leave the machine. (#5604)
- New env-only operator setting
LDR_SEARCH_PRIVATE_ENGINE_URL_ALLOWLIST: a comma-separated list of exact URL origins (e.g.http://localhost:8080) that public search-engine instance URLs (SearXNG) may use even though they are private/loopback/link-local. This is a finer-grained alternative toLDR_SEARCH_ALLOW_PRIVATE_ENGINE_URLS=true— private fetching is enabled only when the engine's configured URL matches a listed origin, instead of for any private address. Consulted at both save time and runtime; cloud-metadata addresses and non-http(s) schemes remain blocked even if listed, and the setting cannot be edited through the web UI. (#5604)
📝 Other Changes
- Clearer guidance when a self-hosted SearXNG URL is blocked by the private-URL guard: the runtime self-disable now logs at ERROR (was WARNING) and both it and the settings-save rejection name all three operator remedies; docs, installation guides, and
.env.templatenow document the v1.10.3 guard next to everylocalhost:8080example instead of desc
- New in this release (recommended):
- v1.10.4Release 1.10.42026-08-16 · 2026-08-16 13:20 UTC
查看更新说明
Critical login security fix and notification URL parsing
This release fixes a serious authentication bypass — upgrade promptly. While a user's encrypted database was open (the normal state whenever that user is signed in anywhere, and for up to 30 days afterward with "remember me"), the login route accepted any password for that account: knowing only the username was enough to sign in from another session, browser, or IP and read or write their data. The attempt also counted as a successful login, so it never tripped account lockout or rate limiting. The fix (#5596) records which password opened each cached connection and re-verifies it on every login, falling back to a real key-validating open on mismatch. Accounts that had never been opened were not affected, and the cold-open path always validated passwords correctly — but any currently-open account was exposed, so updating is strongly recommended.
Also in this release, notification service URLs are parsed more carefully (#5394): commas inside a single Apprise URL — such as multi-recipient
mailto:destinations or webhook query parameters — are now preserved instead of being split into bogus entries, while configurations containing scheme-less URL fragments are refused outright rather than partially dispatched. If a notification configuration that previously failed validation now works (or vice versa), this change is why.🔒 Security
-
Login now verifies your password even when your encrypted database is already open. The connection cache is keyed by username alone, so a cache hit proved only that someone had already opened that user's database — and login's sole credential check is whether that open succeeds.
This only affected an account that already existed and was currently open — the normal state while that user is signed in anywhere. It could not be used to sign in to an account that does not exist or that has never been opened, and the attacker needed no account of their own. The exposure also outlives an active browser session: the database stays open after the user closes the browser (until the idle timeout — up to 30 days with "remember me") and while a research job is running. For any account meeting that condition, knowing only the username — not the password — was enough to sign in as that user from a different session, browser, or IP and read (and write) their data. The attempt also counted as a successful login, so it never tripped the account lockout or the rate limiter and left nothing in the logs that looked like an attack.
The cache now records which password opened the connection and re-checks it on every login, falling back to a real key-validating open when it does not match.
🐛 Bug Fixes
- Preserve commas inside Apprise service URLs while refusing configurations that contain scheme-less URL fragments. (#5394)
What's Changed
🔒 Security Updates
- fix(notifications): parse service URL boundaries safely by @RerankerGuo in https://github.com/LearningCircuit/local-deep-research/pull/5394
- fix(security): verify the password on a cached database connection by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5596
📚 Documentation
- chore: clear changelog fragments for 1.10.3 by @github-actions[bot] in https://github.com/LearningCircuit/local-deep-research/pull/5598
🔧 CI/CD & Maintenance
- chore: bump patch version to 1.10.4 by @github-actions[bot] in https://github.com/LearningCircuit/local-deep-research/pull/5593
🧪 Tests
- chore(test): correct latent unprotected palette entry and document same-value select dependency by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5595
Full Changelog: https://github.c
-
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
Local Deep Research
AI-powered research assistant for deep, agentic research
Performs deep, agentic research using multiple LLMs and search engines with proper citations
🧪 First open-source project — fully-local on a single RTX 3090 (Qwen3.6-27B) — to report ~95% SimpleQA (n=500) and 77% xbench-DeepSearch (n=100) on local hardware. See the r/LocalLLaMA announcement and the benchmark dataset.
🚀 What is Local Deep Research?
AI research assistant you control. Run locally for privacy, use any LLM and build your own searchable knowledge base. You own your data and see exactly how it works.
⚡ Quick Start
Option 1: Docker Run (Linux)
# Step 1: Pull and run Ollama
docker run -d -p 11434:11434 --name ollama ollama/ollama
docker exec ollama ollama pull gpt-oss:20b
# Step 2: Pull and run SearXNG for optimal search results
docker run -d -p 8080:8080 --name searxng searxng/searxng
# Step 3: Pull and run Local Deep Research
# (the URL line pins SearXNG's address AND marks it operator-approved —
# private/localhost engine URLs are otherwise blocked by default since
# v1.10.3. The URL becomes read-only in the web UI; docs/SearXNG-Setup.md
# lists the alternatives, e.g. an origin allowlist.)
docker run -d --network host \
--name local-deep-research \
--volume "deep-research:/data" \
-e LDR_DATA_DIR=/data \
-e LDR_SEARCH_ENGINE_WEB_SEARXNG_DEFAULT_PARAMS_INSTANCE_URL=http://localhost:8080 \
localdeepresearch/local-deep-research
Mac / Windows / WSL2 users:
--network hostonly works on native Linux. On Docker Desktop it silently fails to publish port 5000 and leaveslocalhostpointing at the LDR container itself (so it can't reach Ollama/SearXNG). Use Option 2 below, or see the Windows/WSL2 FAQ entry for a workingdocker runrecipe.
Option 2: Docker Compose
CPU-only (all platforms):
curl -O https://raw.githubusercontent.com/LearningCircuit/local-deep-research/main/docker-compose.yml && docker compose up -d
With NVIDIA GPU (Linux):
curl -O https://raw.githubusercontent.com/LearningCircuit/local-deep-research/main/docker-compose.yml && \
curl -O https://raw.githubusercontent.com/LearningCircuit/local-deep-research/main/docker-compose.gpu.override.yml && \
docker compose -f docker-compose.yml -f docker-compose.gpu.override.yml up -d
Open http://localhost:5000 after ~30 seconds. For GPU setup, environment variables, and more, see the Docker Compose Guide.
Option 3: pip install
pip install local-deep-research
python -m local_deep_research.web.app # starts the web UI on http://localhost:5000
You'll also need Ollama (or any OpenAI-compatible LLM endpoint) and SearXNG running — see the pip install guide for the full recipe. Works on Windows, macOS, and Linux. SQLCipher encryption is included via pre-built wheels — no compilation needed. PDF export on Windows requires Pango (setup guide). Development fallback:
LDR_BOOTSTRAP_ALLOW_UNENCRYPTED=truepermits plaintext SQLite only when SQLCipher is unavailable or nonfunctional; working SQLCipher still takes priority. The fallback does not verify account passwords and is unsuitable for shared deployments. See fallback limitations.
Detailed install guides: Docker · Docker Compose · pip · Unraid · full install reference
Older CPU (x86-64)? LDR needs an AVX-capable CPU — Intel Sandy Bridge / AMD Bulldozer (2011) or newer. Several scientific Python dependencies (pandas, scikit-learn) ship wheels that crash with
Illegal instructionon older CPUs. ARM64 (aarch64) is fully supported. Every release is smoke-tested against this floor, including AVX-without-AVX2 CPUs (#4480).
🏗️ How It Works
Research
You ask a complex question. Local Deep Research (LDR):
- Does the research for you automatically
- Searches across web, academic papers, and your own documents
- Synthesizes everything into a report with proper citations
Choose the research strategy that fits: quick pipeline modes for fast facts, or fully agentic deep research for complex analysis and academic work.
LangGraph Agent Strategy — An autonomous agentic research mode where the LLM decides what to search, which specialized engines to use (arXiv, PubMed, Semantic Scholar, etc.), and when to synthesize. It adaptively switches between search engines based on what it finds and collects significantly more sources than pipeline-based strategies — this is the strategy behind the ~95% SimpleQA result above. Select langgraph-agent in Settings.
Build Your Knowledge Base
flowchart LR
R[Research] --> D[Download Sources]
D --> L[(Library)]
L --> I[Index & Embed]
I --> S[Search Your Docs]
S -.-> R
Every research session finds valuable sources. Download them directly into your encrypted library — academic papers from ArXiv, PubMed articles, web pages. LDR extracts text, indexes everything, and makes it searchable. Next time you research, ask questions across your own documents and the live web together. Your knowledge compounds over time.
🛡️ Security
flowchart LR
U1[User A] --> D1[(Encrypted DB)]
U2[User B] --> D2[(Encrypted DB)]
Your data stays yours. Each user gets their own isolated SQLCipher database encrypted with AES-256, with the key derived from your password. Your password is never stored — login works by attempting to decrypt your database, so the database files on their own are unusable to anyone who obtains them. Per-user LLM API keys live encrypted inside the same personal database rather than in a shared server-level store.
The Docker setup ships with cap_drop: ALL, no-new-privileges, and a non-root runtime, with the bundled Ollama and SearXNG images pinned by digest. With a model running in the bundled Ollama, inference runs on your own hardware. LDR judges locality mainly by provider and endpoint address, not by where the model runs, so a local endpoint that relays to a hosted model (an Ollama cloud model, or a local LiteLLM or other OpenAI-compatible proxy in front of a hosted API) can still send your prompts off the machine without being detected. A self-hosted SearXNG still forwards your search queries to its upstream engines on the internet. To keep a run on private sources, search only local sources (collections, your library) under the Private only egress scope described below. That scope is a best-effort guard rail, not an air gap: see the Egress control section below and SECURITY.md.
In-memory credentials: Credentials are held in process memory during active sessions. LDR limits their lifetime and applies process dump controls at web and MCP startup on POSIX, with additional dumpability controls on Linux. Other platforms and applications embedding LDR require operating-system configuration. See the Security Policy for scope and platform details.
Supply Chain Security: Docker images are signed with Cosign using GitHub's keyless OIDC flow, include SLSA provenance attestations, and ship with attested SPDX SBOMs. See Verifying images and SBOMs for the step-by-step verification commands.
Security Transparency: Scanner suppressions are documented with justifications in Security Alerts Assessment, Scorecard Compliance, Container CVE Suppressions, and SAST Rule Rationale. Some alerts (Dependabot, code scanning) can only be dismissed or are very difficult to suppress outside the GitHub Security tab, so the files above do not cover every dismissed finding.
Detailed Architecture → | Security Policy → | Security Review Process →
🌐 Egress control
Every research run has an egress scope that restricts, on a best-effort basis, where its traffic may go: which search engines run, whether the LLM and embeddings may be cloud services, and which result URLs may be fetched. Set it under Settings → Egress Scope or in the Privacy & Egress panel on the research form; choosing a scope in the panel also saves it as your default setting, so later runs use it too.
| Mode | Search sources | Inference |
|---|---|---|
| Adaptive (default) | follows your primary engine: a public engine behaves like Public only, a private collection or your library like Private only; a locally stored collection you marked public as primary allows any classified engine, private collections included | forced local only when the run is private |
| Public only | public web and academic engines; private collections and local document stores classified as private are excluded (a document store whose URL resolves to a public host, or does not resolve, is normally treated as public) | your configured providers; a document-store primary with a cloud model is normally refused at start for research run through the web research form (not for entry points that call the research functions directly, such as /api/v1/quick_summary or MCP) |
| Private only | local sources only (collections, library, local document stores such as Paperless) | forced local; cloud providers are normally refused |
| Strict | your single primary engine, no expansion | not forced local, but cloud models usually do not work under Strict (see the Strict section of Egress modes) |
| Unprotected | anything; unavailable unless the operator sets LDR_POLICY_ALLOW_UNPROTECTED_EGRESS=true | normally anything (an operator's environment lock on the require-local settings still applies) |
Strict and Private only (including an Adaptive run that resolves to it) also arm a best-effort socket-level guard on the research run that normally refuses connections to public addresses. It is a backstop with known gaps, not a guarantee; the Strict section of Egress modes lists some of them, such as a forward proxy or a relay on a private address. Under Strict, public engines, public result fetches and cloud model calls therefore usually fail, so pair it with a local engine and local models.
A locally-hosted SearXNG counts as a public engine — it proxies searches to the internet — so it is not available under Private only. Under Strict it may still run as your primary engine, and it then forwards your queries to its upstream engines on the internet.
LDR's guarded HTTP layer, used for most search-result and document fetches, also checks destinations against SSRF rules (including known cloud-metadata addresses such as 169.254.169.254) and re-checks redirects. With JavaScript rendering enabled (web.enable_javascript_rendering, off by default), pages that need it are loaded in headless Chromium, which resolves addresses itself. See the egress module README for the details.
Whether a signed-in user can point a search engine at a private address depends on the kind of engine:
- Public engines with a configurable address (currently SearXNG): a private or loopback URL is normally refused unless the operator opts in through the environment (
LDR_SEARCH_ALLOW_PRIVATE_ENGINE_URLS=true, an exact origin inLDR_SEARCH_PRIVATE_ENGINE_URL_ALLOWLIST, or pinning the URL itself withLDR_SEARCH_ENGINE_WEB_SEARXNG_DEFAULT_PARAMS_INSTANCE_URL). A SearXNG onlocalhosttherefore normally needs one of these. - Local document-store engines (Paperless-ngx and Elasticsearch): their address settings,
search.engine.web.paperless.default_params.api_urlandsearch.engine.web.elasticsearch.default_params.hosts, are meant to point at private hosts, so currently no opt-in applies: any signed-in user can set them to an internal address and the server will connect to it. If that is not acceptable, pin these settings with their environment variables (LDR_plus the key in upper case with dots replaced by underscores, for exampleLDR_SEARCH_ENGINE_WEB_PAPERLESS_DEFAULT_PARAMS_API_URL), or lock settings changes withLDR_APP_LOCK_SETTINGS=true.
The egress policy is an in-process guard rail against misconfiguration and prompt-injected fetches, not an air gap or a hard security boundary: for example, hostnames are still resolved through the configured DNS resolver before a fetch is refused, and it cannot reliably constrain code that runs inside the LDR process. For a hard boundary, add egress filtering at the container or firewall level as well.
Egress modes → | Threat model and caveats → | Enforcement points →
🔒 Privacy & Data
Local Deep Research contains no telemetry, no analytics, and no tracking. We do not collect, transmit, or store any data about you or your usage. No analytics SDKs, no phone-home calls, no crash reporting, no external scripts. Usage metrics stay in your local encrypted database.
The only network calls LDR makes are ones you initiate: search queries (to engines you configure), LLM API calls (to your chosen provider), and notifications (only if you set up Apprise).
Since we don't collect any usage data, we rely on you to tell us what works, what's broken, and what you'd like to see next — bug reports, feature ideas, and even which features you love or never use all help us improve LDR.
📊 Benchmarks
Headline results from the community benchmarks using the langgraph-agent strategy with Serper (cloud) search and the models running locally in Ollama:
| Model | SimpleQA | xbench-DeepSearch |
|---|---|---|
| Qwen3.6-27B | 95.7% (287/300) | 77.0% (77/100) |
| Qwen3.5-9B | 91.2% (182/200) | 59.0% (59/100) |
| gpt-oss-20B | 85.4% (295/346) | – |
Caveats: small samples, LLM-grader noise, and SimpleQA contamination risk on newer base models.
Picking a local model? The same community-maintained dataset tracks accuracy across models, search engines, and research strategies — the fastest way to see which Ollama / LM Studio / llama.cpp models actually work well for deep research before you download multi-GB weights. Browse the full leaderboard on Hugging Face →
Submit your own results → (contributors are listed in CONTRIBUTORS.md), or run benchmarks locally →.
✨ Key Features
🔍 Research Modes
- Quick Summary - Get answers in 30 seconds to 3 minutes with citations
- Detailed Research - Comprehensive analysis with structured findings
- Report Generation - Professional reports with sections and table of contents
- Document Analysis - Search your private documents with AI
🛠️ Advanced Capabilities
- LangChain Integration - Use any vector store as a search engine
- REST API - Authenticated HTTP access with per-user databases
- Benchmarking - Test and optimize your configuration
- Analytics Dashboard - Track costs, performance, and usage metrics
- Journal Quality System - Automatic journal reputation scoring with 212K+ indexed sources, predatory detection, and quality dashboard. Powered by OpenAlex (CC0), DOAJ (CC0), and Stop Predatory Journals (MIT). See the v1.6.0 announcement.
- Real-time Updates - WebSocket support for live research progress
- Chat Mode - Multi-turn research conversations with streaming progress and accumulated context across turns
- Export Options - Download results as PDF or Markdown
- Research History - Save, search, and revisit past research
- Adaptive Rate Limiting - Intelligent retry system that learns optimal wait times
- Keyboard Shortcuts - Navigate efficiently (ESC, Ctrl+Shift+1-4)
📰 News & Research Subscriptions
- Automated Research Digests - Subscribe to topics or specific queries; AI filters and summarizes only the most relevant developments
- Customizable Delivery - Daily, weekly, or custom schedules, as markdown reports or structured summaries
🌐 Search Sources
Free Search Engines
- Academic: arXiv, PubMed, Semantic Scholar
- General: Wikipedia, SearXNG
- Technical: GitHub, Elasticsearch
- Archived pages: Wayback Machine URL fetching
- News: The Guardian, Wikinews
Premium Search Engines
- Tavily - AI-powered search
- Google - Via SerpAPI or Programmable Search Engine
- Brave Search - Privacy-focused web search
Custom Sources
- Local Documents - Search your files with AI
- LangChain Retrievers - Any vector store or database
- Meta Search - Combine multiple engines intelligently
LDR respects robots.txt and identifies itself honestly when fetching web pages — no stealth or anti-detection techniques. In rare cases this means a page that blocks automated access won't be fetched, which we consider the right trade-off.
💻 Usage Examples
Python API
from local_deep_research.api import LDRClient, quick_query
# Option 1: Simplest - one line research
summary = quick_query("username", "password", "What is quantum computing?")
print(summary)
# Option 2: Client for multiple operations
client = LDRClient()
client.login("username", "password")
result = client.quick_research("What are the latest advances in quantum computing?")
print(result["summary"])
HTTP API
The code example below shows the basic API structure - for working examples, see the link below
import requests
from bs4 import BeautifulSoup
# Create session and authenticate
session = requests.Session()
login_page = session.get("http://localhost:5000/auth/login")
soup = BeautifulSoup(login_page.text, "html.parser")
login_csrf = soup.find("input", {"name": "csrf_token"}).get("value")
# Login and get API CSRF token
session.post("http://localhost:5000/auth/login",
data={"username": "user", "password": "pass", "csrf_token": login_csrf})
csrf = session.get("http://localhost:5000/auth/csrf-token").json()["csrf_token"]
# Make API request
response = session.post("http://localhost:5000/api/start_research",
json={"query": "Your research question"},
headers={"X-CSRF-Token": csrf})
🚀 Ready-to-use HTTP API Examples → examples/api_usage/http/
- ✅ Automatic user creation - works out of the box
- ✅ Complete authentication with CSRF handling
- ✅ Result retry logic - waits until research completes
- ✅ Progress monitoring and error handling
Command Line Tools
# Run benchmarks from CLI
python -m local_deep_research.benchmarks.cli.benchmark_commands simpleqa --examples 50
See the Command Line Tools guide for the full reference.
🔗 Bring Your Own Knowledge Base
Connect LDR to your existing knowledge base. Unlike the HTTP client above, quick_summary() runs LDR in-process — no server needed — so you can pass it live Python objects such as LangChain retrievers:
from local_deep_research.api import quick_summary
# Use your existing LangChain retriever
result = quick_summary(
query="What are our deployment procedures?",
retrievers={"company_kb": your_retriever},
search_tool="company_kb"
)
Works with: FAISS, Chroma, Pinecone, Weaviate, Elasticsearch, and any LangChain-compatible retriever.
🔌 MCP Server (Claude Integration)
LDR provides an MCP (Model Context Protocol) server that allows AI assistants like Claude Desktop and Claude Code to perform deep research. Full setup details in the MCP Server guide.
⚠️ Security Note: This MCP server is designed for local use only via STDIO transport (e.g., Claude Desktop). It has no built-in authentication or rate limiting. Do not expose over a network without implementing proper security controls. See the MCP Security Best Practices for network deployment requirements.
Installation
# Install with MCP extras
pip install "local-deep-research[mcp]"
Claude Desktop Configuration
Add to your claude_desktop_config.json:
{
"mcpServers": {
"local-deep-research": {
"command": "ldr-mcp",
"env": {
"LDR_LLM_PROVIDER": "openai",
"LDR_LLM_OPENAI_API_KEY": "sk-..."
}
}
}
}
Claude Code Configuration
Add to your .mcp.json (project-level) or ~/.claude/mcp.json (global):
{
"mcpServers": {
"local-deep-research": {
"command": "ldr-mcp",
"env": {
"LDR_LLM_PROVIDER": "ollama",
"LDR_LLM_OLLAMA_URL": "http://localhost:11434"
}
}
}
}
Any LDR setting can be passed in env as an LDR_* variable — see the auto-generated Full Configuration Reference for the complete list.
Available Tools
| Tool | Description | Duration | LLM Cost |
|---|---|---|---|
search | Raw results from a specific engine (arxiv, pubmed, wikipedia, ...) | 5-30s | None |
quick_research | Fast research summary | 1-5 min | Yes |
detailed_research | Comprehensive analysis | 5-15 min | Yes |
generate_report | Full markdown report | 10-30 min | Yes |
analyze_documents | Search local collections | 30s-2 min | Yes |
list_search_engines | List available search engines | instant | None |
list_strategies | List research strategies | instant | None |
get_configuration | Get current config | instant | None |
Individual Search Engines
The search tool lets you query specific search engines directly and get raw results (title, link, snippet) — no LLM processing, no cost, fast. This is especially useful for monitoring and subscriptions where you want to check for new content regularly without burning LLM tokens.
# Search arXiv for recent papers
search(query="transformer architecture improvements", engine="arxiv")
# Search PubMed for medical literature
search(query="CRISPR clinical trials 2024", engine="pubmed")
# Search Wikipedia for quick facts
search(query="quantum error correction", engine="wikipedia")
# Search GitHub for code and repositories
search(query="agentic research frameworks", engine="github")
# Use list_search_engines() to see all available engines
Example Usage
"Use quick_research to find information about quantum computing applications"
"Search arxiv for recent papers on diffusion models"
"Generate a detailed research report on renewable energy trends"
🤖 Supported LLMs
Local Models
- Ollama — connect to its native API (default
http://localhost:11434) - LM Studio — connect to its OpenAI-compatible server (default
http://localhost:1234/v1) - llama.cpp — connect to
llama-server's OpenAI-compatible endpoint (defaulthttp://localhost:8080/v1); start withllama-server -m <model.gguf> - Common models: Llama, Mistral, Gemma, DeepSeek, Qwen
- LLM processing runs on your own hardware when the model itself runs locally (search queries still go to the web). A local endpoint that relays to a hosted model, such as an Ollama cloud model or a local LiteLLM proxy, is not detected as remote. No API costs for locally-run models.
💡 Which local model should I pick? See the community benchmarks — community-submitted accuracy numbers across local and cloud models, so you can compare before downloading.
Cloud Models
- OpenAI
- Anthropic Claude
- Google Gemini
- 100+ models via OpenRouter
- 100+ models via Requesty
Custom Endpoints
- OpenAI-Compatible Endpoint — any service speaking the OpenAI chat-completions API (vLLM, llama.cpp, gateways)
- Anthropic-Compatible Endpoint — a self-hosted service speaking the Anthropic Messages API (
/v1/messages); setllm.anthropic_endpoint.url
🔄 Upgrading from Earlier Versions
For the first FastAPI-based major release, read the
upgrade and rollback guide before updating.
Back up the data first: the release's new database revisions prevent a
direct rollback to v1.10.7 against the upgraded databases. Copy each user's
automatic pre-migration backup from
encrypted_databases/backups/<user hash>/pre-migration/ to storage outside the
data directory immediately after that user's first sign-in on the new release,
and in any case before that user's next password change, which can remove it,
and before any downgrade attempt. Never start v1.10.7 against the upgraded
databases: each sign-in there first makes a backup, keeps only the newest
backup directly in the user's backup directory (that new copy of the upgraded
database), and then refuses the login. After any rollback to a release before
2.0, move every user's pre-migration/ folder out of the data directory (see
the upgrade guide).
-
llm.modelno longer has a default. Pre-1.6.3 installs auto-filledgemma3:12b(Ollama) when no model was configured, which silently downloaded a multi-GB binary. The field is now empty by default — pick a model in Settings → LLM, or research will fail loudly with a clear error. -
The
autoandparallelmeta search engines were removed. The default langgraph-agent strategy selects engines dynamically per query, which replaces them. Stored settings are migrated automatically (removed values becomesearxng); update any explicitsearch_tool="auto"API calls orLDR_SEARCH_TOOL=autoenv overrides to a concrete engine such assearxng. -
Behind a reverse proxy: the rate limiter reads the client address differently. Up to v1.10.7 the login and registration rate limits keyed on the left-most
X-Forwarded-Forentry, or elseX-Real-IP, sent by any peer, so any client could choose its own bucket. What happens now depends on your configuration:TRUST_PROXY_HEADERSunset (the default): those headers are read only when the direct peer is a private, loopback or link-local address (6to4, Teredo and NAT64 tunnel addresses do not count), or, once you setLDR_SERVER_FORWARDED_ALLOW_IPSto your proxy's address(es), only when it is listed there. The key is the right-mostX-Forwarded-Forentry, orX-Real-IPwhen noX-Forwarded-Foris present.TRUST_PROXY_HEADERS=truewithLDR_SERVER_FORWARDED_ALLOW_IPSset to your proxy's address(es): forwarded headers count only from a listed peer. uvicorn derives the client address from a listed peer'sX-Forwarded-For, andX-Real-IP(used when noX-Forwarded-Foris present) is read only from a listed peer. Any other peer is keyed on its own address.TRUST_PROXY_HEADERS=truewithoutLDR_SERVER_FORWARDED_ALLOW_IPS: the key is the right-mostX-Forwarded-Forentry sent by any peer, public addresses included. That stops a client behind one appending proxy from choosing its bucket with a forged prefix, but a client that connects straight to LDR writes the whole header and still chooses its own key.X-Real-IPis read only from a private, loopback or link-local peer. LDR logs a startup warning for this configuration: setLDR_SERVER_FORWARDED_ALLOW_IPS.
What it means for an existing proxy setup:
- Proxy reaching LDR from a public or carrier-grade-NAT address (a cloud load balancer, a Tailscale
100.64.x.xpeer, a public nginx host): withoutTRUST_PROXY_HEADERSits forwarded headers are now ignored; withTRUST_PROXY_HEADERS=trueand no allow-list itsX-Real-IPis ignored, while itsX-Forwarded-Forstill counts. When the header your proxy sends is ignored, every client shares the proxy's one rate-limit bucket and a few failed logins lock everyone out. SetLDR_SERVER_FORWARDED_ALLOW_IPSto the proxy's IP address(es). - Proxy on a private or Docker network: (if you set a restricted
LDR_SERVER_FORWARDED_ALLOW_IPS, only listed peers are trusted, so list this private proxy too.) With a single proxy, clients keep separate buckets, provided the proxy overwritesX-Forwarded-Forwith the address it observed (nginx:$remote_addr) and overwritesX-Real-IPthe same way. A proxy that passes a client-sent header through unchanged still lets that client choose its key; a proxy that sets onlyX-Real-IPmust also clearX-Forwarded-For, because LDR prefers it. Appending ($proxy_add_x_forwarded_for) gives the right rate-limit key, but underTRUST_PROXY_HEADERS=truewithoutLDR_SERVER_FORWARDED_ALLOW_IPSuvicorn reports the left-most, client-written entry as the client address. - Two or more proxies in a chain (CDN → nginx): by default every client is keyed on the outer proxy's address, so all of them share one rate-limit bucket and a few failed logins lock everyone out. Either have the inner proxy resolve the real client itself (nginx
realip) and overwrite with that address, or make every inner proxy append and set a multi-hop setting:LDR_SERVER_TRUSTED_PROXY_COUNT, or, withTRUST_PROXY_HEADERS=true, every hop listed inLDR_SERVER_FORWARDED_ALLOW_IPS. See Proxy chains.
With
TRUST_PROXY_HEADERS=truebut noLDR_SERVER_FORWARDED_ALLOW_IPS, LDR logs a startup warning naming the variable (or refuses to start ifLDR_SERVER_TRUSTED_PROXY_COUNTis also set to a positive value, or if the allow-list is set explicitly to*or another value that trusts every peer). See Reverse proxy configuration. -
The
llamacppprovider now uses HTTP instead of in-process loading. If you previously setllm.llamacpp_model_pathto a local.gguffile, that setting is no longer read. Instead, runllama-server -m <your-model.gguf>(it ships with every modern llama.cpp build) and the defaultllm.llamacpp.urlofhttp://localhost:8080/v1will pick it up. Optional API key support is available viallm.llamacpp.api_keyif you putllama-serverbehind an auth proxy.
📚 Documentation
Getting Started
- Installation Guide
- Frequently Asked Questions
- API Quickstart
- Configuration Guide
- Full Configuration Reference
Core Features
- All Features Guide
- Search Engines Guide
- Analytics Dashboard
- Incomplete Metrics Warning — Understand recording failures and missing metrics
Advanced Features
Development
- Docker Compose Guide
- Development Guide
- CodeQL Guide
- Release Guide
- CI/CD Infrastructure — pre-commit hooks, workflows, security scanning
- Workflow Status Dashboard — live health of every GitHub Actions workflow
- Test Coverage Report — pytest coverage published to GitHub Pages
Examples & Tutorials
📰 Featured In
"Local Deep Research deserves special mention for those who prioritize privacy... tuned to use open-source LLMs that can run on consumer GPUs or even CPUs. Journalists, researchers, or companies with sensitive topics can investigate information without queries ever hitting an external server."
News & Articles
- Korben.info - French tech blog ("Sherlock Holmes numérique")
- Roboto.fr - "L'alternative open-source gratuite à Deep Research d'OpenAI"
- KDJingPai AI Tools - AI productivity tools coverage
- AI Sharing Circle - AI resources coverage
Community Discussions
- r/LocalLLaMA: 95.7% SimpleQA on a single 3090, fully local - Qwen3.6-27B benchmark announcement (161K views)
- r/WebAfterAI: LDR as the agentic layer on top of Ollama / LM Studio / LocalAI - Stack-positioning explainer
- Hacker News - 190+ points, community discussion
- LangChain Twitter/X - Official LangChain promotion
- LangChain LinkedIn - 400+ likes
International Coverage
🇨🇳 Chinese
- Juejin (掘金) - Developer community
- Cnblogs (博客园) - Developer blogs
- GitHubDaily (Twitter/X) - Influential tech account
- Zhihu (知乎) - Tech community
- A姐分享 - AI resources
- CSDN - Installation guide
- NetEase (网易) - Tech news portal
🇯🇵 Japanese
- note.com: 調査革命:Local Deep Research徹底活用法 - Comprehensive tutorial
- Qiita: Local Deep Researchを試す - Docker setup guide
- LangChainJP (Twitter/X) - Japanese LangChain community
🇰🇷 Korean
- PyTorch Korea Forum - Korean ML community
- GeekNews (Hada.io) - Korean tech news
Reviews & Analysis
- BSAIL Lab: How useful is Deep Research in Academia? - Academic review by contributor @djpetti
- The Art Of The Terminal: Use Local LLMs Already! - Comprehensive review of local AI tools, featuring LDR's research capabilities
- Fahd Mirza: Local Deep Research + Ollama — Free AI Research Assistant You Control - Installation walkthrough (349K-subscriber channel). The SearXNG startup issue shown in the video has since been fixed in #3881 — clean Docker installs now start out of the box.
- BC Adventure Tech: Run ChatGPT Deep Research Locally (Unraid AI Agent Setup) - Unraid setup and showcase
Related Projects
- SearXNG LDR-Academic - Academic-focused SearXNG fork with 12 research engines (arXiv, Google Scholar, PubMed, etc.) designed for LDR
- DeepWiki Documentation - Third-party documentation and guides
Note: Third-party projects and articles are independently maintained. We link to them as useful resources but cannot guarantee their code quality or security.
🤝 Community & Support
- Discord - Get help and share research techniques
- Reddit - Updates and showcases
- GitHub Issues - Bug reports
🧑💻 Contributing
We welcome contributions of all sizes — from typo fixes to new features. The key rule: keep PRs small and atomic (one change per PR). You don't need to be assigned to an issue: comment that you're working on it and open an early (draft) PR so progress is visible. For hard or architectural changes, and for anything that could affect research results or benchmarks (LLM prompts, the advanced search system, search engines), outline your approach in the issue first, so your effort leads to a successful merge rather than a misaligned PR. See our Contributing Guide to get started.
Acknowledgements
Local Deep Research is built on the work of many open-access initiatives, academic databases, and open-source projects. We are grateful to:
Academic & Research Data
| Source | What It Provides | License |
|---|---|---|
| OpenAlex | Academic metadata for ~280K sources and ~120K institutions, including DOAJ status | CC0 |
| DOAJ | Directory of Open Access Journals — open-access verification (via OpenAlex) | CC0 |
| arXiv | Preprints in physics, mathematics, CS, and more | Various (see arXiv license) |
| PubMed / NCBI | Biomedical and life sciences literature | Public domain (US Gov) |
| Semantic Scholar | Cross-discipline academic search with citation data | Terms |
| NASA ADS | Astrophysics, physics, and astronomy papers | Terms |
| Zenodo | Open research data, datasets, and software | Various per record |
| PubChem | Chemistry and biochemistry database | Public domain (US Gov) |
| Stop Predatory Journals | Predatory journal/publisher blacklist | MIT |
| JabRef | Journal abbreviation database | CC0 |
Knowledge & Content Sources
Wikipedia • OpenLibrary • Project Gutenberg • GitHub • Stack Exchange • The Guardian • Wayback Machine
Infrastructure & Frameworks
LangChain • Ollama • SearXNG • FAISS
Support Open Access
These projects run on donations and grants, not paywalls. If Local Deep Research is useful to you, consider giving back to the open-access ecosystem that makes it possible:
- arXiv — free preprints for physics, math, CS, and more
- PubMed / NLM — open biomedical literature
- Wikipedia / Wikimedia — the free encyclopedia
- Internet Archive — the Wayback Machine and open digital library
- DOAJ — curating and verifying open-access journals worldwide
- OpenAlex — open scholarly metadata (sponsored by OurResearch)
- Project Gutenberg — free ebooks since 1971
📄 License
MIT License - see LICENSE file.
Dependencies: All third-party packages use permissive licenses (MIT, Apache-2.0, BSD, etc.) - see allowlist