chunksilo 2.4.0__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chunksilo-3.0.0/PKG-INFO +174 -0
- chunksilo-3.0.0/README.md +132 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/pyproject.toml +1 -1
- {chunksilo-2.4.0 → chunksilo-3.0.0}/requirements.txt +1 -1
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/__init__.py +1 -1
- chunksilo-3.0.0/src/chunksilo/cfgload.py +350 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/cli.py +37 -3
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/docx_utils.py +46 -12
- chunksilo-3.0.0/src/chunksilo/filecheck.py +461 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/index.py +579 -172
- chunksilo-3.0.0/src/chunksilo/revisions.py +233 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/search.py +352 -95
- chunksilo-3.0.0/src/chunksilo/server.py +308 -0
- chunksilo-3.0.0/src/chunksilo/shareuri.py +109 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/ui.py +1 -3
- chunksilo-3.0.0/src/chunksilo.egg-info/PKG-INFO +174 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo.egg-info/SOURCES.txt +11 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo.egg-info/requires.txt +1 -1
- chunksilo-3.0.0/test/test_cfgload.py +129 -0
- chunksilo-3.0.0/test/test_check_files.py +558 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_chunk_location.py +116 -30
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_docx_utils.py +88 -10
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_error_handling.py +16 -0
- chunksilo-3.0.0/test/test_incremental_ingest.py +399 -0
- chunksilo-3.0.0/test/test_never_mutates.py +154 -0
- chunksilo-3.0.0/test/test_revision_indexing.py +294 -0
- chunksilo-3.0.0/test/test_revisions.py +277 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_scan_timeouts.py +7 -5
- chunksilo-3.0.0/test/test_search_concurrency.py +169 -0
- chunksilo-3.0.0/test/test_search_diversity.py +296 -0
- chunksilo-3.0.0/test/test_server.py +348 -0
- chunksilo-3.0.0/test/test_share_uri.py +132 -0
- chunksilo-2.4.0/PKG-INFO +0 -431
- chunksilo-2.4.0/README.md +0 -389
- chunksilo-2.4.0/src/chunksilo/cfgload.py +0 -253
- chunksilo-2.4.0/src/chunksilo/server.py +0 -110
- chunksilo-2.4.0/src/chunksilo.egg-info/PKG-INFO +0 -431
- chunksilo-2.4.0/test/test_incremental_ingest.py +0 -128
- chunksilo-2.4.0/test/test_server.py +0 -107
- {chunksilo-2.4.0 → chunksilo-3.0.0}/LICENSE +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/NOTICE +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/setup.cfg +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/__main__.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/confluence_html_formatter.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/models.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo.egg-info/dependency_links.txt +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo.egg-info/entry_points.txt +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo.egg-info/top_level.txt +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_cli.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_confluence_html_formatter.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_heading_path_integration.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_indexing_benchmark.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_indexing_ui.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_jira_integration.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_quoted_phrases.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_rag_metrics.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_retrieval_only.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_system.py +0 -0
- {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_utils.py +0 -0
chunksilo-3.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chunksilo
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: Local RAG-based semantic document search with MCP server interface
|
|
5
|
+
Author: Fredrik Reveny
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/Chetic/chunksilo
|
|
8
|
+
Project-URL: Issues, https://github.com/Chetic/chunksilo/issues
|
|
9
|
+
Keywords: rag,mcp,semantic-search,document-search,llm
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
License-File: NOTICE
|
|
21
|
+
Requires-Dist: llama-index<1,>=0.10.0
|
|
22
|
+
Requires-Dist: llama-index-readers-file<1,>=0.1.0
|
|
23
|
+
Requires-Dist: llama-index-embeddings-fastembed<1,>=0.5.0
|
|
24
|
+
Requires-Dist: llama-index-retrievers-bm25<1,>=0.1.0
|
|
25
|
+
Requires-Dist: pillow<11,>=10.3.0
|
|
26
|
+
Requires-Dist: pypdf<7,>=5.1.0
|
|
27
|
+
Requires-Dist: python-docx<2,>=1.1.0
|
|
28
|
+
Requires-Dist: mcp<2,>=1.10.0
|
|
29
|
+
Requires-Dist: huggingface-hub<2,>=0.22.0
|
|
30
|
+
Requires-Dist: flashrank<1,>=0.1.0
|
|
31
|
+
Requires-Dist: fastembed<1,>=0.5.0
|
|
32
|
+
Requires-Dist: pyyaml<7,>=6.0
|
|
33
|
+
Requires-Dist: llama-index-readers-confluence<1,>=0.6.0
|
|
34
|
+
Requires-Dist: jira<4,>=3.5.0
|
|
35
|
+
Provides-Extra: confluence
|
|
36
|
+
Provides-Extra: jira
|
|
37
|
+
Provides-Extra: test
|
|
38
|
+
Requires-Dist: pytest<9,>=7.4.0; extra == "test"
|
|
39
|
+
Requires-Dist: pytest-cov>=4.0; extra == "test"
|
|
40
|
+
Requires-Dist: requests<3,>=2.31.0; extra == "test"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
<p align="center">
|
|
44
|
+
<img src="https://raw.githubusercontent.com/Chetic/chunksilo/main/chunksilo.png" alt="ChunkSilo Logo" width="500">
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<img src="demo/demo.gif" alt="ChunkSilo terminal demo" width="720">
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
# ChunkSilo MCP Server
|
|
52
|
+
|
|
53
|
+
ChunkSilo is like a local Google for your documents. It uses semantic search — matching by meaning rather than exact keywords — so your LLM can find relevant information across all your files even when the wording differs from your query. Point it at your PDFs, Word docs, Markdown, and text files, and it builds a fully searchable index locally on your machine.
|
|
54
|
+
|
|
55
|
+
- Runs entirely on your machine — no servers, no infrastructure
|
|
56
|
+
- Semantic search + keyword filename matching across PDF, DOCX, DOC, Markdown, and TXT
|
|
57
|
+
- Incremental indexing — only reprocesses new or changed files, and never writes inside an indexed directory
|
|
58
|
+
- Revision-aware indexing — files that are revisions of one document (`Spec_v2.pdf`, `.../Rev B/Spec.docx`) are grouped and only the newest is indexed; review-comment copies are skipped
|
|
59
|
+
- Heading-aware results with source links back to the original file, spread across documents instead of one document's every chunk
|
|
60
|
+
- Date filtering and recency boosting
|
|
61
|
+
- Files indexed from a mounted network share can be presented at the share's own location (an `smb://` URI plus the Windows UNC path)
|
|
62
|
+
- Serves MCP over stdio to a local client, or over streamable-http behind a reverse proxy of your own (oauth2-proxy + KeyCloak walkthrough included)
|
|
63
|
+
- Optional Confluence and Jira integrations (supports Cloud and Server/Data Center)
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
### Option A: Install from PyPI (Recommended)
|
|
68
|
+
|
|
69
|
+
Requires Python 3.11 or later. Models are downloaded automatically on first run (~250MB). The first run may appear to pause while models download — this is normal.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install chunksilo
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Confluence and Jira support is included by default — just provide a config file
|
|
76
|
+
to enable them. (`pip install chunksilo[confluence,jira]` still works as an alias
|
|
77
|
+
for backward compatibility.)
|
|
78
|
+
|
|
79
|
+
Then:
|
|
80
|
+
1. **Create** a config file at `~/.config/chunksilo/config.yaml` (see [Configuration](#configuration))
|
|
81
|
+
2. **Build** the index: `chunksilo --build-index`
|
|
82
|
+
3. **Configure** your MCP client (see [docs/mcp-clients.md](docs/mcp-clients.md))
|
|
83
|
+
|
|
84
|
+
### Option B: Offline Bundle
|
|
85
|
+
|
|
86
|
+
A self-contained package with pre-downloaded models, ideal for air-gapped environments or systems without Python installed.
|
|
87
|
+
|
|
88
|
+
Download from the [Releases page](https://github.com/Chetic/chunksilo/releases):
|
|
89
|
+
|
|
90
|
+
1. **Download** the `chunksilo-vX.Y.Z-manylinux_2_34_x86_64.tar.gz` file
|
|
91
|
+
2. **Extract** and install:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
tar -xzf chunksilo-vX.Y.Z-manylinux_2_34_x86_64.tar.gz
|
|
95
|
+
cd chunksilo
|
|
96
|
+
./setup.sh
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
3. **Edit** `config.yaml` to set your document directories
|
|
100
|
+
4. **Build** the index: `./venv/bin/chunksilo --build-index`
|
|
101
|
+
5. **Configure** your MCP client (see [docs/mcp-clients.md](docs/mcp-clients.md))
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
ChunkSilo reads one file, `config.yaml`, taken from `--config`, else the
|
|
106
|
+
`CHUNKSILO_CONFIG` environment variable, else `./config.yaml`, else
|
|
107
|
+
`~/.config/chunksilo/config.yaml`. Every setting is optional and
|
|
108
|
+
`chunksilo --dump-defaults` prints them all with their defaults.
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
indexing:
|
|
112
|
+
directories:
|
|
113
|
+
- "./data"
|
|
114
|
+
- "/mnt/docs"
|
|
115
|
+
|
|
116
|
+
retrieval:
|
|
117
|
+
rerank_top_k: 5
|
|
118
|
+
|
|
119
|
+
storage:
|
|
120
|
+
storage_dir: "./storage"
|
|
121
|
+
model_cache_dir: "./models"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The full reference — per-directory filters, revision handling, retrieval
|
|
125
|
+
tuning, Confluence and Jira, share locations, the HTTP transport, and what
|
|
126
|
+
changed for users upgrading from 2.x — is in
|
|
127
|
+
[docs/configuration.md](docs/configuration.md).
|
|
128
|
+
|
|
129
|
+
## The `search_docs` tool
|
|
130
|
+
|
|
131
|
+
`search_docs` answers with the files whose names matched and the individual
|
|
132
|
+
chunks that carry the answer, each located by a URI, the page or line it came
|
|
133
|
+
from, and its heading path:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"matched_files": [
|
|
138
|
+
{ "uri": "file:///docs/database-configuration.docx", "unc": null, "score": 0.8432 }
|
|
139
|
+
],
|
|
140
|
+
"num_matched_files": 1,
|
|
141
|
+
"chunks": [
|
|
142
|
+
{
|
|
143
|
+
"text": "To configure the database connection, set the DATABASE_URL environment variable...",
|
|
144
|
+
"score": 0.912,
|
|
145
|
+
"location": {
|
|
146
|
+
"uri": "file:///docs/setup-guide.pdf",
|
|
147
|
+
"unc": null,
|
|
148
|
+
"page": 12,
|
|
149
|
+
"line": null,
|
|
150
|
+
"heading_path": ["Getting Started", "Configuration", "Database"]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
"num_chunks": 1,
|
|
155
|
+
"query": "how to configure the database",
|
|
156
|
+
"retrieval_time": "0.42s"
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
See [docs/tools.md](docs/tools.md) for every field, including how files on a
|
|
161
|
+
mounted network share are presented.
|
|
162
|
+
|
|
163
|
+
## Documentation
|
|
164
|
+
|
|
165
|
+
- [docs/configuration.md](docs/configuration.md) — the config file, where it is discovered, and a reference for every setting.
|
|
166
|
+
- [docs/cli.md](docs/cli.md) — `chunksilo` and `chunksilo-mcp` options, including `--check-files` for explaining why a file is or is not indexed.
|
|
167
|
+
- [docs/tools.md](docs/tools.md) — the `search_docs` result shape.
|
|
168
|
+
- [docs/mcp-clients.md](docs/mcp-clients.md) — client setup for Claude Code, Claude Desktop, Cline, Roo Code and opencode, over stdio or HTTP.
|
|
169
|
+
- [docs/reverse-proxy.md](docs/reverse-proxy.md) — sharing an instance behind an authenticating reverse proxy, with oauth2-proxy and KeyCloak group-based access.
|
|
170
|
+
- [docs/troubleshooting.md](docs/troubleshooting.md) — indexing surprises, network mounts, offline mode and more.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Chetic/chunksilo/main/chunksilo.png" alt="ChunkSilo Logo" width="500">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="demo/demo.gif" alt="ChunkSilo terminal demo" width="720">
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
# ChunkSilo MCP Server
|
|
10
|
+
|
|
11
|
+
ChunkSilo is like a local Google for your documents. It uses semantic search — matching by meaning rather than exact keywords — so your LLM can find relevant information across all your files even when the wording differs from your query. Point it at your PDFs, Word docs, Markdown, and text files, and it builds a fully searchable index locally on your machine.
|
|
12
|
+
|
|
13
|
+
- Runs entirely on your machine — no servers, no infrastructure
|
|
14
|
+
- Semantic search + keyword filename matching across PDF, DOCX, DOC, Markdown, and TXT
|
|
15
|
+
- Incremental indexing — only reprocesses new or changed files, and never writes inside an indexed directory
|
|
16
|
+
- Revision-aware indexing — files that are revisions of one document (`Spec_v2.pdf`, `.../Rev B/Spec.docx`) are grouped and only the newest is indexed; review-comment copies are skipped
|
|
17
|
+
- Heading-aware results with source links back to the original file, spread across documents instead of one document's every chunk
|
|
18
|
+
- Date filtering and recency boosting
|
|
19
|
+
- Files indexed from a mounted network share can be presented at the share's own location (an `smb://` URI plus the Windows UNC path)
|
|
20
|
+
- Serves MCP over stdio to a local client, or over streamable-http behind a reverse proxy of your own (oauth2-proxy + KeyCloak walkthrough included)
|
|
21
|
+
- Optional Confluence and Jira integrations (supports Cloud and Server/Data Center)
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
### Option A: Install from PyPI (Recommended)
|
|
26
|
+
|
|
27
|
+
Requires Python 3.11 or later. Models are downloaded automatically on first run (~250MB). The first run may appear to pause while models download — this is normal.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install chunksilo
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Confluence and Jira support is included by default — just provide a config file
|
|
34
|
+
to enable them. (`pip install chunksilo[confluence,jira]` still works as an alias
|
|
35
|
+
for backward compatibility.)
|
|
36
|
+
|
|
37
|
+
Then:
|
|
38
|
+
1. **Create** a config file at `~/.config/chunksilo/config.yaml` (see [Configuration](#configuration))
|
|
39
|
+
2. **Build** the index: `chunksilo --build-index`
|
|
40
|
+
3. **Configure** your MCP client (see [docs/mcp-clients.md](docs/mcp-clients.md))
|
|
41
|
+
|
|
42
|
+
### Option B: Offline Bundle
|
|
43
|
+
|
|
44
|
+
A self-contained package with pre-downloaded models, ideal for air-gapped environments or systems without Python installed.
|
|
45
|
+
|
|
46
|
+
Download from the [Releases page](https://github.com/Chetic/chunksilo/releases):
|
|
47
|
+
|
|
48
|
+
1. **Download** the `chunksilo-vX.Y.Z-manylinux_2_34_x86_64.tar.gz` file
|
|
49
|
+
2. **Extract** and install:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
tar -xzf chunksilo-vX.Y.Z-manylinux_2_34_x86_64.tar.gz
|
|
53
|
+
cd chunksilo
|
|
54
|
+
./setup.sh
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
3. **Edit** `config.yaml` to set your document directories
|
|
58
|
+
4. **Build** the index: `./venv/bin/chunksilo --build-index`
|
|
59
|
+
5. **Configure** your MCP client (see [docs/mcp-clients.md](docs/mcp-clients.md))
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
ChunkSilo reads one file, `config.yaml`, taken from `--config`, else the
|
|
64
|
+
`CHUNKSILO_CONFIG` environment variable, else `./config.yaml`, else
|
|
65
|
+
`~/.config/chunksilo/config.yaml`. Every setting is optional and
|
|
66
|
+
`chunksilo --dump-defaults` prints them all with their defaults.
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
indexing:
|
|
70
|
+
directories:
|
|
71
|
+
- "./data"
|
|
72
|
+
- "/mnt/docs"
|
|
73
|
+
|
|
74
|
+
retrieval:
|
|
75
|
+
rerank_top_k: 5
|
|
76
|
+
|
|
77
|
+
storage:
|
|
78
|
+
storage_dir: "./storage"
|
|
79
|
+
model_cache_dir: "./models"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The full reference — per-directory filters, revision handling, retrieval
|
|
83
|
+
tuning, Confluence and Jira, share locations, the HTTP transport, and what
|
|
84
|
+
changed for users upgrading from 2.x — is in
|
|
85
|
+
[docs/configuration.md](docs/configuration.md).
|
|
86
|
+
|
|
87
|
+
## The `search_docs` tool
|
|
88
|
+
|
|
89
|
+
`search_docs` answers with the files whose names matched and the individual
|
|
90
|
+
chunks that carry the answer, each located by a URI, the page or line it came
|
|
91
|
+
from, and its heading path:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"matched_files": [
|
|
96
|
+
{ "uri": "file:///docs/database-configuration.docx", "unc": null, "score": 0.8432 }
|
|
97
|
+
],
|
|
98
|
+
"num_matched_files": 1,
|
|
99
|
+
"chunks": [
|
|
100
|
+
{
|
|
101
|
+
"text": "To configure the database connection, set the DATABASE_URL environment variable...",
|
|
102
|
+
"score": 0.912,
|
|
103
|
+
"location": {
|
|
104
|
+
"uri": "file:///docs/setup-guide.pdf",
|
|
105
|
+
"unc": null,
|
|
106
|
+
"page": 12,
|
|
107
|
+
"line": null,
|
|
108
|
+
"heading_path": ["Getting Started", "Configuration", "Database"]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"num_chunks": 1,
|
|
113
|
+
"query": "how to configure the database",
|
|
114
|
+
"retrieval_time": "0.42s"
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
See [docs/tools.md](docs/tools.md) for every field, including how files on a
|
|
119
|
+
mounted network share are presented.
|
|
120
|
+
|
|
121
|
+
## Documentation
|
|
122
|
+
|
|
123
|
+
- [docs/configuration.md](docs/configuration.md) — the config file, where it is discovered, and a reference for every setting.
|
|
124
|
+
- [docs/cli.md](docs/cli.md) — `chunksilo` and `chunksilo-mcp` options, including `--check-files` for explaining why a file is or is not indexed.
|
|
125
|
+
- [docs/tools.md](docs/tools.md) — the `search_docs` result shape.
|
|
126
|
+
- [docs/mcp-clients.md](docs/mcp-clients.md) — client setup for Claude Code, Claude Desktop, Cline, Roo Code and opencode, over stdio or HTTP.
|
|
127
|
+
- [docs/reverse-proxy.md](docs/reverse-proxy.md) — sharing an instance behind an authenticating reverse proxy, with oauth2-proxy and KeyCloak group-based access.
|
|
128
|
+
- [docs/troubleshooting.md](docs/troubleshooting.md) — indexing surprises, network mounts, offline mode and more.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
Shared configuration loading for ChunkSilo.
|
|
5
|
+
|
|
6
|
+
Loads configuration from config.yaml, searching in standard locations. The
|
|
7
|
+
first load with an explicit path (``--config``) becomes the process-wide
|
|
8
|
+
active configuration: every later no-argument ``load_config()`` or ``get()``
|
|
9
|
+
call answers from it, so a path given on the command line governs the whole
|
|
10
|
+
process rather than only the call sites it was threaded through.
|
|
11
|
+
"""
|
|
12
|
+
import copy
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Retrieval models bundled inside the installed package (populated at wheel-build
|
|
23
|
+
# time). Present in air-gapped wheel installs; absent in plain source checkouts.
|
|
24
|
+
_BUNDLED_MODELS_DIR = Path(__file__).resolve().parent / "_bundled_models"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _find_config() -> Path:
|
|
28
|
+
"""Find config.yaml using a priority-based search.
|
|
29
|
+
|
|
30
|
+
Search order:
|
|
31
|
+
1. CHUNKSILO_CONFIG environment variable
|
|
32
|
+
2. ./config.yaml (current working directory)
|
|
33
|
+
3. ~/.config/chunksilo/config.yaml (XDG standard)
|
|
34
|
+
"""
|
|
35
|
+
env_path = os.environ.get("CHUNKSILO_CONFIG")
|
|
36
|
+
if env_path:
|
|
37
|
+
return Path(env_path)
|
|
38
|
+
|
|
39
|
+
cwd_path = Path.cwd() / "config.yaml"
|
|
40
|
+
if cwd_path.exists():
|
|
41
|
+
return cwd_path
|
|
42
|
+
|
|
43
|
+
xdg_path = Path.home() / ".config" / "chunksilo" / "config.yaml"
|
|
44
|
+
if xdg_path.exists():
|
|
45
|
+
return xdg_path
|
|
46
|
+
|
|
47
|
+
# Return cwd path as default (will fall through to defaults if not found)
|
|
48
|
+
return cwd_path
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
CONFIG_PATH = _find_config()
|
|
52
|
+
|
|
53
|
+
# Default file type patterns - the single definition; index.py imports these.
|
|
54
|
+
DEFAULT_INCLUDE_PATTERNS = ["**/*.pdf", "**/*.md", "**/*.txt", "**/*.docx", "**/*.doc"]
|
|
55
|
+
|
|
56
|
+
DEFAULT_EXCLUDE_PATTERNS = [
|
|
57
|
+
"**/.git/**",
|
|
58
|
+
"**/node_modules/**",
|
|
59
|
+
"**/__pycache__/**",
|
|
60
|
+
"**/.venv/**",
|
|
61
|
+
"**/venv/**",
|
|
62
|
+
"**/.tox/**",
|
|
63
|
+
"**/.mypy_cache/**",
|
|
64
|
+
"**/.pytest_cache/**",
|
|
65
|
+
"**/.eggs/**",
|
|
66
|
+
"**/*.egg-info/**",
|
|
67
|
+
"**/.DS_Store",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# Filename markers of copies made to collect review comments, matched
|
|
71
|
+
# case-insensitively against the filename without its extension. Setting
|
|
72
|
+
# indexing.versioning.review_copy_patterns replaces this list wholesale;
|
|
73
|
+
# [] turns detection off.
|
|
74
|
+
DEFAULT_REVIEW_COPY_PATTERNS = [
|
|
75
|
+
r"\breview\b",
|
|
76
|
+
r"\bcomments?\b",
|
|
77
|
+
r"\bkommentar(?:er)?\b",
|
|
78
|
+
r"\bgranskning\b",
|
|
79
|
+
r"\bcopy\b", # "Copy of Spec", "Spec - Copy (2)"
|
|
80
|
+
r"\bkopia\b", # "Kopia av Spec", "Spec - kopia"
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
_DEFAULTS: dict[str, Any] = {
|
|
84
|
+
"indexing": {
|
|
85
|
+
"directories": ["./data"],
|
|
86
|
+
"defaults": {
|
|
87
|
+
"include": DEFAULT_INCLUDE_PATTERNS,
|
|
88
|
+
"exclude": DEFAULT_EXCLUDE_PATTERNS,
|
|
89
|
+
"recursive": True,
|
|
90
|
+
"case_sensitive": False,
|
|
91
|
+
},
|
|
92
|
+
"chunk_size": 512,
|
|
93
|
+
"chunk_overlap": 50,
|
|
94
|
+
"batch_size": 200, # upper bound; adaptive sizing may shrink a batch
|
|
95
|
+
"parallel_workers": 8, # 1 = load files serially
|
|
96
|
+
"checkpoint_interval_files": 500,
|
|
97
|
+
"checkpoint_interval_seconds": 300,
|
|
98
|
+
"per_file_seconds": 300, # give up on a single file; 0 disables
|
|
99
|
+
"scan_item_seconds": 30, # timeout for stat/hash/walk during scanning
|
|
100
|
+
"slow_file_threshold_seconds": 30, # warn when one file takes longer
|
|
101
|
+
"versioning": {
|
|
102
|
+
# Group files that are revisions of one document (revision-named
|
|
103
|
+
# directories or filename tokens like "Spec PB3.docx") and index
|
|
104
|
+
# only the newest member of each group, by modification time.
|
|
105
|
+
"enabled": True,
|
|
106
|
+
# Regexes tried in order against the full path; files sharing an
|
|
107
|
+
# extracted document ID form one group. The first capture group
|
|
108
|
+
# (or the whole match) is the ID. Empty = group by normalized
|
|
109
|
+
# name/path only.
|
|
110
|
+
"doc_id_patterns": [],
|
|
111
|
+
# Extra revision-token regex fragments (case-insensitive), added
|
|
112
|
+
# to the built-in grammar for directory names and filename
|
|
113
|
+
# suffixes alike.
|
|
114
|
+
"extra_revision_tokens": [],
|
|
115
|
+
"review_copy_patterns": DEFAULT_REVIEW_COPY_PATTERNS,
|
|
116
|
+
# Index older revisions too, stamped superseded and ranked below
|
|
117
|
+
# current documents in results.
|
|
118
|
+
"index_superseded": False,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
"retrieval": {
|
|
122
|
+
"embed_model_name": "BAAI/bge-small-en-v1.5",
|
|
123
|
+
"embed_top_k": 20,
|
|
124
|
+
"rerank_model_name": "ms-marco-MiniLM-L-12-v2",
|
|
125
|
+
"rerank_top_k": 5,
|
|
126
|
+
"rerank_candidates": 100,
|
|
127
|
+
"score_threshold": 0.1,
|
|
128
|
+
"recency_boost": 0.3,
|
|
129
|
+
"recency_half_life_days": 365,
|
|
130
|
+
# At most this many chunks of one document in the final results;
|
|
131
|
+
# freed slots go to the next-best other documents. 0 disables.
|
|
132
|
+
"max_chunks_per_doc": 2,
|
|
133
|
+
"offline": False,
|
|
134
|
+
},
|
|
135
|
+
"confluence": {
|
|
136
|
+
"url": "",
|
|
137
|
+
"username": "",
|
|
138
|
+
"api_token": "",
|
|
139
|
+
"timeout": 10.0,
|
|
140
|
+
"max_results": 30,
|
|
141
|
+
},
|
|
142
|
+
"jira": {
|
|
143
|
+
"url": "",
|
|
144
|
+
"username": "",
|
|
145
|
+
"api_token": "",
|
|
146
|
+
"timeout": 10.0,
|
|
147
|
+
"max_results": 30,
|
|
148
|
+
"projects": [], # Empty list = all accessible projects
|
|
149
|
+
"include_comments": False,
|
|
150
|
+
"include_custom_fields": False,
|
|
151
|
+
},
|
|
152
|
+
"ssl": {
|
|
153
|
+
# CA bundle for environments with TLS interception or a private CA:
|
|
154
|
+
# used for model downloads and for the Confluence/Jira APIs.
|
|
155
|
+
"ca_bundle_path": "",
|
|
156
|
+
},
|
|
157
|
+
"storage": {
|
|
158
|
+
"storage_dir": "./storage",
|
|
159
|
+
"model_cache_dir": "./models",
|
|
160
|
+
},
|
|
161
|
+
"server": {
|
|
162
|
+
"transport": "stdio", # stdio | streamable-http
|
|
163
|
+
# Loopback by default. The HTTP transport performs no authentication
|
|
164
|
+
# of its own: exposing the port is a deliberate act, taken together
|
|
165
|
+
# with TLS termination and a login layer in front of this server.
|
|
166
|
+
"host": "127.0.0.1",
|
|
167
|
+
"port": 8400,
|
|
168
|
+
# The URL clients reach this server on when a reverse proxy sits in
|
|
169
|
+
# front, e.g. "https://search.example.com". The MCP transport rejects
|
|
170
|
+
# any Host header other than the bind address unless it is allowed
|
|
171
|
+
# here, and a proxy forwards the public hostname. Empty = only the
|
|
172
|
+
# bind address is accepted.
|
|
173
|
+
"public_url": "",
|
|
174
|
+
# Further Host header values and Origins to accept ("host:*" = any port).
|
|
175
|
+
"allowed_hosts": [],
|
|
176
|
+
"allowed_origins": [],
|
|
177
|
+
# Issuer URLs of the authorization servers (e.g. a KeyCloak realm) MCP
|
|
178
|
+
# clients should obtain a token from. Published as RFC 9728
|
|
179
|
+
# protected-resource metadata at /.well-known/oauth-protected-resource
|
|
180
|
+
# so clients can discover them; this server never verifies tokens -
|
|
181
|
+
# that is the reverse proxy's job. Empty = the metadata is not served.
|
|
182
|
+
"authorization_servers": [],
|
|
183
|
+
},
|
|
184
|
+
# Present files indexed from a mounted network share at the share's own
|
|
185
|
+
# location (an smb:// URI plus the Windows UNC path) instead of the
|
|
186
|
+
# server-local mount path, so a client on another machine gets a location
|
|
187
|
+
# it can open. Uncovered files keep their file:// URIs.
|
|
188
|
+
# [{prefix: "/mnt/docs", unc: "//nas/docs"}]
|
|
189
|
+
"shares": [],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# Options that no longer exist, mapped to what replaced them. A config file
|
|
193
|
+
# that still sets one gets a warning instead of silently falling back to the
|
|
194
|
+
# built-in default.
|
|
195
|
+
_REMOVED_KEYS = {
|
|
196
|
+
"indexing.timeout": "indexing.per_file_seconds (0 disables) and indexing.scan_item_seconds",
|
|
197
|
+
"indexing.logging": "indexing.slow_file_threshold_seconds",
|
|
198
|
+
"indexing.enable_parallel_loading": "indexing.parallel_workers (1 = serial)",
|
|
199
|
+
"indexing.enable_adaptive_batching": "indexing.batch_size (an upper bound; sizing adapts automatically)",
|
|
200
|
+
"indexing.max_memory_mb": "indexing.batch_size (an upper bound; sizing adapts automatically)",
|
|
201
|
+
"retrieval.bm25_similarity_top_k": "nothing; the number of file-name matches is fixed",
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# The active configuration: the merged result of the last load, plus the path
|
|
205
|
+
# it came from. An explicit-path load replaces it; no-argument loads reuse it.
|
|
206
|
+
_config_cache: dict[str, Any] | None = None
|
|
207
|
+
_active_path: Path | None = None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _warn_removed_keys(user_config: dict[str, Any], path: Path) -> None:
|
|
211
|
+
"""Warn about options the file sets that the schema no longer has."""
|
|
212
|
+
for dotted, replacement in _REMOVED_KEYS.items():
|
|
213
|
+
section, _, key = dotted.partition(".")
|
|
214
|
+
block = user_config.get(section)
|
|
215
|
+
if isinstance(block, dict) and key in block:
|
|
216
|
+
logger.warning(
|
|
217
|
+
"%s: '%s' is no longer used and is ignored; use %s instead",
|
|
218
|
+
path, dotted, replacement,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
223
|
+
"""Deep merge override into base, returning a new dict."""
|
|
224
|
+
result = base.copy()
|
|
225
|
+
for key, value in override.items():
|
|
226
|
+
if value is None and key in result:
|
|
227
|
+
# An empty YAML value ("chunk_size:" or a commented-out section)
|
|
228
|
+
# parses as None; keep the default instead of poisoning it.
|
|
229
|
+
continue
|
|
230
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
231
|
+
result[key] = _deep_merge(result[key], value)
|
|
232
|
+
else:
|
|
233
|
+
result[key] = value
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _has_embedding_cache(cache_dir: Path) -> bool:
|
|
238
|
+
"""True if ``cache_dir`` holds a HuggingFace-style embedding model cache."""
|
|
239
|
+
try:
|
|
240
|
+
return any(cache_dir.glob("models--*"))
|
|
241
|
+
except OSError:
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _resolve_bundled_models(config: dict[str, Any]) -> dict[str, Any]:
|
|
246
|
+
"""Transparently fall back to models bundled in the package.
|
|
247
|
+
|
|
248
|
+
If the wheel ships the retrieval models and the configured cache directory
|
|
249
|
+
does not already contain them, point the cache at the bundled copy and enable
|
|
250
|
+
offline mode. This lets a pip-installed wheel run in an air-gapped environment
|
|
251
|
+
with zero extra configuration. A user-configured cache that already holds the
|
|
252
|
+
models always wins, so explicit setups are never overridden.
|
|
253
|
+
|
|
254
|
+
Detection looks for any HuggingFace-style ``models--*`` cache directory rather
|
|
255
|
+
than matching the configured embedding name: fastembed stores the model under
|
|
256
|
+
its upstream source repo (e.g. ``models--qdrant--bge-small-en-v1.5-onnx-q``),
|
|
257
|
+
not under the configured alias ``BAAI/bge-small-en-v1.5``.
|
|
258
|
+
"""
|
|
259
|
+
if not _has_embedding_cache(_BUNDLED_MODELS_DIR):
|
|
260
|
+
return config # No models bundled in this install.
|
|
261
|
+
|
|
262
|
+
configured = Path(config["storage"]["model_cache_dir"]).expanduser()
|
|
263
|
+
if configured.resolve() != _BUNDLED_MODELS_DIR.resolve() and _has_embedding_cache(
|
|
264
|
+
configured
|
|
265
|
+
):
|
|
266
|
+
return config # Configured cache already has models; respect it.
|
|
267
|
+
|
|
268
|
+
result = copy.deepcopy(config)
|
|
269
|
+
result["storage"]["model_cache_dir"] = str(_BUNDLED_MODELS_DIR)
|
|
270
|
+
result["retrieval"]["offline"] = True
|
|
271
|
+
logger.info(
|
|
272
|
+
"Using bundled retrieval models at %s (offline mode)", _BUNDLED_MODELS_DIR
|
|
273
|
+
)
|
|
274
|
+
return result
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def load_config(config_path: Path | str | None = None) -> dict[str, Any]:
|
|
278
|
+
"""Load configuration from YAML file with defaults.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
config_path: Optional path to config file. When given, the result
|
|
282
|
+
becomes the process-wide active configuration that
|
|
283
|
+
no-argument calls (and :func:`get`) answer from. When
|
|
284
|
+
None, returns the active configuration, loading from the
|
|
285
|
+
auto-discovered CONFIG_PATH on first use.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Configuration dictionary with defaults merged in.
|
|
289
|
+
"""
|
|
290
|
+
global _config_cache, _active_path
|
|
291
|
+
|
|
292
|
+
if _config_cache is not None and (
|
|
293
|
+
config_path is None or Path(config_path) == _active_path
|
|
294
|
+
):
|
|
295
|
+
return _config_cache
|
|
296
|
+
|
|
297
|
+
path = Path(config_path) if config_path else CONFIG_PATH
|
|
298
|
+
|
|
299
|
+
# Callers may mutate the returned dict; never hand out the defaults themselves.
|
|
300
|
+
defaults = copy.deepcopy(_DEFAULTS)
|
|
301
|
+
|
|
302
|
+
if not path.exists():
|
|
303
|
+
logger.info("Config file not found at %s; using built-in defaults", path)
|
|
304
|
+
result = _resolve_bundled_models(defaults)
|
|
305
|
+
else:
|
|
306
|
+
logger.info("Using config: %s", path)
|
|
307
|
+
with open(path, encoding="utf-8") as f:
|
|
308
|
+
user_config = yaml.safe_load(f) or {}
|
|
309
|
+
_warn_removed_keys(user_config, path)
|
|
310
|
+
result = _deep_merge(defaults, user_config)
|
|
311
|
+
result = _resolve_bundled_models(result)
|
|
312
|
+
|
|
313
|
+
_config_cache = result
|
|
314
|
+
_active_path = path
|
|
315
|
+
return result
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def get(key: str, default: Any = None) -> Any:
|
|
319
|
+
"""Get a value from the active configuration by dot-notation key.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
key: Dot-separated key path (e.g., 'retrieval.embed_top_k')
|
|
323
|
+
default: Value to return if key not found
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Configuration value or default.
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
>>> get('retrieval.embed_top_k')
|
|
330
|
+
20
|
|
331
|
+
>>> get('storage.storage_dir')
|
|
332
|
+
'./storage'
|
|
333
|
+
"""
|
|
334
|
+
config = load_config()
|
|
335
|
+
keys = key.split(".")
|
|
336
|
+
value: Any = config
|
|
337
|
+
for k in keys:
|
|
338
|
+
if isinstance(value, dict) and k in value:
|
|
339
|
+
value = value[k]
|
|
340
|
+
else:
|
|
341
|
+
return default
|
|
342
|
+
return value
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def reload_config() -> dict[str, Any]:
|
|
346
|
+
"""Force reload configuration from disk, clearing the active config."""
|
|
347
|
+
global _config_cache, _active_path
|
|
348
|
+
_config_cache = None
|
|
349
|
+
_active_path = None
|
|
350
|
+
return load_config()
|