In conversazione con i tuoi documenti.
ThreadLeaf is a local document chatbot with a terminal interface and a web app. It combines Ollama, ChromaDB, hybrid retrieval and a multilingual reranker to answer questions using your PDF, TXT and Markdown documents.
The goal is to keep ThreadLeaf as lightweight as possible so it can run locally on everyday hardware. Development favors small multilingual models, CPU-friendly inference and only the dependencies needed for useful features. Improvements should balance answer quality with memory use and response time, while keeping documents and conversations on your own machine.
Reusable logo exports are in assets/brand: standalone SVGs, transparent PNGs in eight sizes, and a multi-resolution ICO. Download the icon bundle to use them in other applications.
Copyright © 2026 Pasquale Esposito. ThreadLeaf's original code, documentation and
branding are licensed under the GNU General Public License, version 3 or later
(GPL-3.0-or-later). The project's copyright and license notice is in COPYRIGHT.
You may use, modify and redistribute ThreadLeaf, including commercially and for a fee, subject to GPLv3. When distributing covered software or modified versions, preserve the notices and provide the corresponding source as required by GPLv3. Covered derivative works must be distributed under GPLv3; distributing container images or executable packages does not remove the source obligations. Private internal modifications need not be published merely because they are made. Providing network access alone does not trigger AGPL-style source disclosure: this project uses GPLv3, not AGPLv3.
ThreadLeaf is provided without warranty, to the extent permitted by applicable law. The full license controls; this paragraph is a summary. The previous custom commercial restrictions no longer apply to this release.
Third-party dependencies, models and icons retain their own licenses and notices; the GPL grant applies to the original material whose rights the project holds. User documents remain subject to their owners' rights. Keep the attributions below and applicable upstream notices when distributing copies.
- Web chat with Italian and English controls, Markdown answers and source counts with details on demand.
- Document panel with folders, search, selection, uploads, deletion and indexing progress.
- Conversation history with full-message search and SQLite persistence through LangGraph.
- Conversation deletion with confirmation, and an installed-model selector in the composer.
- Page-aware PDF ingestion with optional local Paddle layout detection and JSON archives.
- Terminal chat with Rich formatting, Markdown, source references and retrieval debugging.
- Semantic search combined with BM25 and section-heading matches, followed by optional CPU reranking.
- Document display titles generated by the configured chat model at server startup.
- Python 3.11 or newer.
- Node.js 20 or newer and npm for the web interface.
- Ollama installed and running locally.
- Internet access for the initial dependency and model downloads.
Once the dependencies and models are available locally, inference uses local Ollama and ONNX models. Documents are not sent to a hosted model by this application. The web/API is designed for local, single-user use; it has no authentication.
Install Docker Engine with Compose on Linux, or Docker Desktop on macOS/Windows. Docker builds the frontend and Python runtime, so host Python, Node and Ollama are not needed for this setup. The default image uses Python 3.13 and excludes the optional Paddle dependencies to keep it smaller.
From the repository root, create documents/ and add your files. Build and start:
docker compose up --build -d
docker compose logs -f initializeThe initialization container connects to the Compose Ollama service, downloads
the chat and embedding models from config.toml, and prepares the enabled
reranker. First startup requires internet access and may take several minutes.
The web server starts after initialization finishes. Coreference weights are not
downloaded automatically; without them, follow-up questions remain unchanged.
Index files already present in documents/ before starting conversations:
docker compose run --rm threadleaf python ingest.pyOpen http://127.0.0.1:4321. Uploads from the UI are indexed automatically. Reindex existing files when changing the embedding model or extraction settings. Avoid ingestion while chatting or changing documents in the UI.
Useful commands:
docker compose build threadleaf # Build threadleaf:local without starting it
docker compose logs -f threadleaf # Application logs
docker compose run --rm threadleaf python cli.py
docker compose run --rm threadleaf python -m scripts.prepare_coreference
docker compose down # Stop; retain saved dataThe documents/ directory is mounted from your host and supports uploads and
deletion. Configuration and system prompt are mounted read-only. Named volumes
retain the vector database, conversation history, extraction archives and model
files across container rebuilds. Existing host db/, state/, models/ and
Ollama downloads are not imported into those volumes. Back up the volumes and
documents/ to preserve your data. docker compose down --volumes deletes the
named volumes, including saved conversations and downloaded models.
Volume mounts assume the default documents/ and db/ paths; update Compose
mounts if you change those paths in config.toml. The built image contains code
and dependencies, while downloaded model weights stay in volumes.
The container listens on 0.0.0.0:4321, but Compose publishes the web port only
on your host's loopback address. The container's listener overrides the server
host/port in config.toml; set THREADLEAF_PORT to change the host port.
Ollama is reached at http://ollama:11434 over the private Compose network and
has no published host port. This setup uses CPU inference; GPU access is not
configured. Native Ollama remains an alternative, particularly for macOS GPU use.
See Ollama's Docker guide for hardware options.
Enable the [layout] section in config.toml, then build with Paddle support:
INSTALL_LAYOUT=true docker compose up --build -dIn PowerShell:
$env:INSTALL_LAYOUT = "true"
docker compose up --build -dThis adds PaddleOCR, PaddlePaddle, PDFium and Pillow to the image. Initialization
downloads PP-DocLayout-M into the model volume. Run ingestion again to apply it
to existing documents. Keep INSTALL_LAYOUT set for subsequent Compose commands.
Paddle builds require supported platform wheels; the optional image is intended
for Linux AMD64, including Docker Desktop using Linux containers on Windows.
Apple Silicon users should use the lightweight image or explicitly evaluate AMD64
emulation for Paddle, which can be slower.
Paddle layout detects regions; this implementation still does not perform OCR or reconstruct table cells. The detector disables MKL-DNN to avoid the observed oneDNN/PIR conversion failure. Missing dependencies, weights or inference errors produce warnings and fall back to native page-aware extraction.
Clone this repository and open its root directory. Follow the instructions for your operating system, then continue with the shared installation steps below.
Install Python 3.11+ with virtual-environment support, Node.js 20+ and npm using your distribution's package manager or their official installers. Check the versions:
python3 --version
node --version
npm --versionInstall and start Ollama using the official Linux instructions.
If Ollama is not already running as a service, run ollama serve in a separate
terminal and leave it running while you use ThreadLeaf.
Create and activate the project environment:
python3 -m venv .venv
source .venv/bin/activate
mkdir -p documentsIf creating the environment reports that venv or ensurepip is missing, install
the virtual-environment package for your Python version and retry.
Install Python 3.11+ and Node.js 20+ with npm. Check python3 --version,
node --version and npm --version in Terminal.
Install Ollama using the official macOS instructions,
move the application to Applications and open it. Complete the CLI setup if prompted
so that the ollama command is available in Terminal.
From the project root, create and activate the environment:
python3 -m venv .venv
source .venv/bin/activate
mkdir -p documentsInstall the prerequisites listed above and start Ollama. From the project root:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
New-Item -ItemType Directory -Path documents -ForceWith the virtual environment active, use python for the commands below on all
three platforms. Re-activate the environment in every new terminal session:
source .venv/bin/activate on Linux/macOS or .\.venv\Scripts\Activate.ps1 on Windows.
Install the Python dependencies and configure the models:
python -m pip install -r requirements.txt
python setup.pySetup lets you choose the chat model, embedding model and system prompt. Press
Enter to keep the current settings. It downloads the Ollama models and can rebuild
the document index. If needed, start Ollama with ollama serve in another terminal.
Create a documents/ directory, add PDF, TXT or MD files, and index them:
python ingest.pySubfolders are supported. Re-run ingestion after editing documents outside the web app. To enable the configured multilingual reranker, download its files once:
python -m scripts.prepare_rerankerIf its local files are unavailable, the application falls back to hybrid retrieval.
Build the frontend from the repository root:
cd web
npm ci
npm run build
cd ..
python server.pyOpen http://127.0.0.1:4321. Startup warms the reranker and generates document titles before serving requests, which can take time with many documents.
Use the vertical tabs to open or collapse Documents and Conversation history.
Select the documents to use for the next question; at least one must be selected
in the web interface. The document panel mirrors the indexed folder structure.
README.txt and REEADME.txt are hidden from this panel.
Uploads support PDF, TXT and MD files up to 25 MB, with an optional destination folder. Each upload is indexed individually and gets a generated display title. Existing files are not overwritten. Deletion removes the file and its indexed passages. Chat pauses while a document operation is running.
Open the settings gear to choose Italiano or English. Interface language does not change existing messages, document titles or the configured answer language. The icon beside settings starts a new conversation. Choose an installed chat model with the selector above the message input; the browser remembers the choice. The trash icon beside a saved conversation asks for confirmation before deletion.
python cli.py
python cli.py --list-models
python cli.py --model qwen2.5:0.5b --k 4 --lang auto --debugThe CLI interface is in English. Answers render as Markdown, with
a muted source count below document answers. /sources reveals the filenames
for the latest reply. Colors adapt to the terminal and respect NO_COLOR.
| Command | Action |
|---|---|
/help |
Show available commands |
/settings |
Show the current configuration |
/models |
List locally installed Ollama models, sizes and model details |
/sources |
Show document sources for the latest reply |
/model NAME |
Change the chat model |
/k N |
Set a positive number of retrieved passages |
/lang NAME |
Set the answer language; auto follows the question |
/debug |
Toggle retrieved passages and prompt output |
/clear |
Clear the screen and show the welcome panel |
exit or quit |
Leave the chat |
The CLI calls RAG directly. Persistent conversations currently apply to the web/API.
python cli.py --list-models lists models without starting a chat. Ollama must
be running. The table marks the configured chat and embedding models; /model NAME
changes the chat model for the current CLI session. Only installed models are
listed. Download another model with ollama pull NAME before selecting it.
--lang and /lang control answer language independently of the English controls.
Edit config.toml and system_prompt.txt, or run
python setup.py again. Restart the app after configuration changes.
| Setting | Current configuration |
|---|---|
| Chat model | llama3.2:1b |
| Embedding model | nomic-embed-text-v2-moe |
| Retrieval | 24 candidates, 4 final passages |
| Reranker | Quantized ONNX cross-encoder/mmarco-mMiniLMv2-L12-H384-v1 |
| Answer mode | generated |
| Answer language | auto |
| Web server | 127.0.0.1:4321 |
Change llm.answer_mode to extractive to return a model-selected source passage
instead of a generated summary. Extractive mode preserves source wording but can
still select an irrelevant passage. Generated mode permits summaries and can
introduce unsupported claims. Source filenames identify retrieved documents;
they do not guarantee that every statement is supported.
Changing the embedding model requires rebuilding the index. Reranking does not. Adjust candidate counts, BM25 weighting, token limits and CPU threads in the config. The lexical tokenizer retains every word, with no hand-written stopword or question-word lists. BM25 weights words using their frequencies in the indexed documents. Heading matches use normalized token overlap without special vocabulary.
For non-interactive setup:
python setup.py --non-interactive --llm qwen2.5:0.5b --ingestUse --skip-pull to skip model downloads or --prompt-file PATH to replace the
system prompt. Run python setup.py --help for all options.
Ingestion now keeps PDF page boundaries and tries pypdf's layout-preserving text
extraction. Both original and layout text are archived under
state/document_layout/, alongside detected regions and extraction warnings.
Chunks prefer paragraph or line boundaries and carry page and block metadata
through hybrid retrieval and reranking into the answer context.
To add local CPU region detection with PP-DocLayout-M:
python -m pip install -r requirements-layout.txt
python -m scripts.prepare_layout
python ingest.pyThe layout dependencies are optional and separate from the core requirements.
Use a Python version supported by PaddlePaddle's wheels for your platform.
For this application's optional Paddle backend, use Python 3.11–3.13;
the current Python 3.14 environment can use native extraction, but cannot install
the documented PaddlePaddle wheels. See the
official installation guide.
The model is downloaded explicitly into models/layout/; ingestion does not
download it. Configure [layout] in config.toml, or set enabled = false to
retain only native page-aware extraction. Reindex existing documents after enabling
this feature. The original PDF files remain in documents/.
This first implementation detects regions and extracts their native PDF text. It does not perform OCR or reconstruct table cells. Image-only pages are reported as needing OCR. Native page text remains searchable even when detection misses a region; raw extraction is also retained because layout extraction can omit rotated text. Reading order is a basic geometric approximation, so complex columns still need review. Detected regions without native text remain in the JSON archive. Archives are reused for unchanged files and configuration, and removed when a document is deleted through the application. Extra region and fallback chunks can increase embedding work and index size.
Optional tools: PaddleOCR and PaddlePaddle (Apache-2.0), PP-DocLayout-M (Apache-2.0), pypdfium2 license notices, and Pillow license.
Web document answers show an info icon with a source count. Hover, keyboard focus
or click opens the source list, with readable document titles and original paths.
Click outside or press Escape to close it. Restored replies use the same display.
The CLI shows [2 sources · /sources] instead of filenames in every answer.
Small talk and extractive insufficient-evidence replies have no source indicator.
A learned linear classifier over the configured multilingual sentence embeddings
routes social conversation before retrieval. It trains once per process from
complete labeled utterances in threadleaf/data/intent_training.json, then caches
its weights. Messages are not matched against greeting or keyword lists. The first
request embeds the small training corpus; later requests add one embedding call.
Classification failures use RAG, and incorrect predictions remain possible.
Small talk uses a separate prompt without document-language hints or context labels,
so it can answer directly in the message's language when answer = "auto".
Extractive answers expose the selected passage's document only. Generated answers
list the retrieved context documents; this doesn't prove every claim is supported.
Follow-up questions use a dedicated multilingual coreference model to predict links between mentions in recent conversation and the latest question. For example, a predicted link from "he" to "Alex" makes "What did he study?" search for "What did Alex study?". The remaining wording is preserved. The resolver does not ask Ollama to rewrite the query, use keyword overlap to approve rewrites, or append previous answers to the search query. Hybrid retrieval and reranking remain separate.
Prepare the model once:
python -m scripts.prepare_coreferenceThe pinned model and tokenizer download is approximately 531 MB and stays in
ignored models/coreference/. It runs locally using the existing ONNX Runtime
and Tokenizers dependencies, without requiring spaCy or PyTorch. Runtime never
downloads missing weights. If the model is missing, fails, finds no link, or the
input exceeds its span/token limits, the original question is used unchanged.
The resolver uses a bounded recent-history window and makes learned predictions;
incorrect links and unresolved references remain possible, especially with
ambiguous names, possessives, omitted subjects or long conversations. Its quality
must be checked on your own English and Italian follow-up questions.
Set [coreference].enabled = false to disable it. Restart after preparation or
configuration changes. Earlier assistant messages supply reference context only,
not documentary evidence for the final answer.
LangGraph checkpoints persist the full web transcript and selected documents in
state/conversations.sqlite3. Only the latest six messages enter the model's
conversation context. The browser remembers the current conversation UUID and
restores its messages after reloads and server restarts.
History lists conversations newest first, using the first question as the title.
Search matches saved user and assistant messages without case sensitivity.
Reopening a conversation restores its available selected documents.
New conversation starts a separate thread; it does not delete previous chats.
Use the trash icon beside a saved conversation to delete it after confirmation.
Deleting the active conversation opens a fresh thread. Deleted chats cannot be restored
without a backup.
Back up state/ to preserve conversation history.
The API uses the same local server as the web app.
| Endpoint | Purpose |
|---|---|
POST /chat |
Ask a question; returns a reply, sources and conversation UUID |
GET /conversations?q=... |
List or search saved conversations |
GET /conversations/{thread_id} |
Restore a transcript and its document selection |
DELETE /conversations/{thread_id} |
Delete all saved checkpoints and writes for a conversation |
GET /models |
List installed chat models for the composer selector |
GET /documents |
List indexed documents and display titles |
POST /documents/upload?source=folder/file.txt |
Upload raw file bytes |
DELETE /documents |
Delete a document using a JSON source |
POST /documents/reindex |
Retry indexing using a JSON source |
GET /documents/operations |
Read document-operation progress |
Example chat request:
{
"message": "Summarize this document",
"lang": "en",
"sources": ["notes.md"]
}Reuse the returned thread_id in subsequent requests to continue the conversation.
The server owns message history; do not send a history field. Omitting sources
uses the entire index; empty or stale selections return HTTP 400. Concurrent
document mutations and chat during indexing return HTTP 409.
Run the Python checks from the repository root:
python -m unittest discover -s tests -vThe optional live intent-routing regression check requires Ollama and the configured embedding model. It includes English/Italian small talk and mixed greeting/document requests, separate from the router's training utterances:
THREADLEAF_TEST_OLLAMA=1 python -m unittest tests.test_intent_integration -vOn PowerShell, set $env:THREADLEAF_TEST_OLLAMA = "1" before running the same
python -m unittest tests.test_intent_integration -v command. The regular test suite
skips this live check.
Check and rebuild the frontend:
cd web
npm run check
npm run buildThe frontend uses Marked, DOMPurify and Font Awesome from npm. The build compiles
TypeScript and generates/copies the browser assets; these generated files are ignored
by Git. Keep web/package-lock.json committed and use npm ci for installation.
The build also copies the browser dependencies' license texts and ThreadLeaf's
license and copyright notice into web/public/vendor/, so deployed assets retain
the accompanying notices.
Run python -m scripts.evaluate_rag to compare retrieval methods or add --generate to
inspect model answers. Edit rag_eval_cases.json for your own
documents. The included Italian legal-document cases require a local
codice-civile.pdf, which is not distributed with the repository; they are a small
regression sample, not a general accuracy benchmark.
ThreadLeaf uses the following open-source projects. Thanks to their authors and contributors. Links below lead to upstream license texts; bundled components and transitive dependencies retain their own notices.
| Tool | Use in ThreadLeaf | Upstream license |
|---|---|---|
| Python — Python Software Foundation | Application runtime | PSF-2.0 and incorporated-software notices |
| Node.js — Node.js contributors | Frontend build runtime | MIT and bundled-component notices |
| Ollama — Ollama | Local model inference | MIT |
| Ollama Python — Ollama | Model API client | MIT |
| ChromaDB — Chroma | Vector index | Apache-2.0 |
| pypdf — pypdf contributors | PDF extraction | BSD-3-Clause |
| FastAPI — FastAPI contributors | HTTP API | MIT |
| Uvicorn — Uvicorn contributors | ASGI server | BSD-3-Clause |
| Rich — Will McGugan and contributors | Terminal rendering | MIT |
| LangGraph — LangChain | Conversation workflow | MIT |
| LangGraph SQLite checkpointer — LangChain | Conversation checkpoints | MIT |
| SQLite — SQLite authors | Local checkpoint storage | Public domain |
| ONNX Runtime — Microsoft and contributors | CPU reranking | MIT |
| Tokenizers — Hugging Face | Reranker tokenization | Apache-2.0 |
| Hugging Face Hub — Hugging Face | Reranker downloads | Apache-2.0 |
| NumPy — NumPy developers | Numerical arrays | BSD-3-Clause; bundled components have additional licenses |
| TypeScript — Microsoft and contributors | Frontend compiler | Apache-2.0 |
| Marked — MarkedJS, Christopher Jeffrey and contributors | Markdown rendering | MIT and included Markdown notices |
| DOMPurify — Cure53 and contributors | HTML sanitization | Apache-2.0 or MPL-2.0 |
| Font Awesome Free — Fonticons, Inc. | SVG interface icons and rendering code | CC BY 4.0 for icons; MIT for code |
Font Awesome icons are converted to SVG markup for the interface; the project uses no Font Awesome font files. The ThreadLeaf logo is an original inline SVG.
Model licenses are separate from inference-library licenses. The current default models are credited below; other models selected through Ollama retain their own terms.
| Model | Attribution / role | License |
|---|---|---|
llama3.2:1b / Llama 3.2 1B |
Meta — configured chat model | Llama 3.2 Community License, with its Acceptable Use Policy |
qwen2.5:0.5b / Qwen2.5-0.5B-Instruct |
Qwen team — optional chat model | Apache-2.0 |
nomic-embed-text-v2-moe |
Nomic AI — multilingual embeddings | Apache-2.0 |
cross-encoder/mmarco-mMiniLMv2-L12-H384-v1 |
Sentence Transformers / Cross-Encoders — multilingual reranking | Apache-2.0 |
talmago/allennlp-coref-onnx-mMiniLMv2-L12-H384-distilled-from-XLMR-Large |
Tal Almagor; AllenNLP / crosslingual-coreference lineage — multilingual mention linking | MIT, as declared in the model card |
cli.py Terminal interface
server.py FastAPI server
setup.py Interactive configuration
ingest.py Document indexing entry point
threadleaf/ Supporting application modules
rag.py Ingestion and answering
retrieval.py Hybrid retrieval
reranker.py Local ONNX reranking
conversation.py Follow-up question resolution
coreference.py Trained ONNX mention linking
grounded_answers.py Extractive answer rendering
chat_workflow.py LangGraph conversation persistence
document_management.py Upload, delete and indexing jobs
document_titles.py Generated document display titles
document_layout.py Page-aware extraction and optional Paddle regions
paths.py Shared repository paths
scripts/ Maintenance tools (run with python -m)
prepare_reranker.py Model preparation
prepare_coreference.py Coreference model preparation
prepare_layout.py Paddle layout model preparation
container_setup.py Compose model initialization
evaluate_rag.py Retrieval evaluation
config.toml Shared configuration
compose.yaml Local app, Ollama and initialization containers
Dockerfile Frontend build and Python runtime image
LICENSE Unmodified GNU GPLv3 license text
COPYRIGHT Project notice: GPLv3 or later
requirements-layout.txt Optional Paddle extraction dependencies
system_prompt.txt Chat instructions
web/src/ TypeScript interface
web/scripts/ Browser-asset build scripts
web/public/index.html Interface markup and styles
tests/ Python checks and shared test helpers
tests/fixtures/ Sample retrieval evaluation cases
reports/ Local output and evaluation reports (ignored)
Documents (documents/), Chroma indexes (db/), downloaded models (models/),
conversation checkpoints (state/), environments, logs and build output stay local
through .gitignore. If you configure different data directories, add
those paths to your ignore rules before committing.