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.
Files changed (59) hide show
  1. chunksilo-3.0.0/PKG-INFO +174 -0
  2. chunksilo-3.0.0/README.md +132 -0
  3. {chunksilo-2.4.0 → chunksilo-3.0.0}/pyproject.toml +1 -1
  4. {chunksilo-2.4.0 → chunksilo-3.0.0}/requirements.txt +1 -1
  5. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/__init__.py +1 -1
  6. chunksilo-3.0.0/src/chunksilo/cfgload.py +350 -0
  7. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/cli.py +37 -3
  8. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/docx_utils.py +46 -12
  9. chunksilo-3.0.0/src/chunksilo/filecheck.py +461 -0
  10. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/index.py +579 -172
  11. chunksilo-3.0.0/src/chunksilo/revisions.py +233 -0
  12. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/search.py +352 -95
  13. chunksilo-3.0.0/src/chunksilo/server.py +308 -0
  14. chunksilo-3.0.0/src/chunksilo/shareuri.py +109 -0
  15. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/ui.py +1 -3
  16. chunksilo-3.0.0/src/chunksilo.egg-info/PKG-INFO +174 -0
  17. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo.egg-info/SOURCES.txt +11 -0
  18. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo.egg-info/requires.txt +1 -1
  19. chunksilo-3.0.0/test/test_cfgload.py +129 -0
  20. chunksilo-3.0.0/test/test_check_files.py +558 -0
  21. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_chunk_location.py +116 -30
  22. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_docx_utils.py +88 -10
  23. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_error_handling.py +16 -0
  24. chunksilo-3.0.0/test/test_incremental_ingest.py +399 -0
  25. chunksilo-3.0.0/test/test_never_mutates.py +154 -0
  26. chunksilo-3.0.0/test/test_revision_indexing.py +294 -0
  27. chunksilo-3.0.0/test/test_revisions.py +277 -0
  28. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_scan_timeouts.py +7 -5
  29. chunksilo-3.0.0/test/test_search_concurrency.py +169 -0
  30. chunksilo-3.0.0/test/test_search_diversity.py +296 -0
  31. chunksilo-3.0.0/test/test_server.py +348 -0
  32. chunksilo-3.0.0/test/test_share_uri.py +132 -0
  33. chunksilo-2.4.0/PKG-INFO +0 -431
  34. chunksilo-2.4.0/README.md +0 -389
  35. chunksilo-2.4.0/src/chunksilo/cfgload.py +0 -253
  36. chunksilo-2.4.0/src/chunksilo/server.py +0 -110
  37. chunksilo-2.4.0/src/chunksilo.egg-info/PKG-INFO +0 -431
  38. chunksilo-2.4.0/test/test_incremental_ingest.py +0 -128
  39. chunksilo-2.4.0/test/test_server.py +0 -107
  40. {chunksilo-2.4.0 → chunksilo-3.0.0}/LICENSE +0 -0
  41. {chunksilo-2.4.0 → chunksilo-3.0.0}/NOTICE +0 -0
  42. {chunksilo-2.4.0 → chunksilo-3.0.0}/setup.cfg +0 -0
  43. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/__main__.py +0 -0
  44. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/confluence_html_formatter.py +0 -0
  45. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo/models.py +0 -0
  46. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo.egg-info/dependency_links.txt +0 -0
  47. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo.egg-info/entry_points.txt +0 -0
  48. {chunksilo-2.4.0 → chunksilo-3.0.0}/src/chunksilo.egg-info/top_level.txt +0 -0
  49. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_cli.py +0 -0
  50. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_confluence_html_formatter.py +0 -0
  51. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_heading_path_integration.py +0 -0
  52. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_indexing_benchmark.py +0 -0
  53. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_indexing_ui.py +0 -0
  54. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_jira_integration.py +0 -0
  55. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_quoted_phrases.py +0 -0
  56. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_rag_metrics.py +0 -0
  57. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_retrieval_only.py +0 -0
  58. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_system.py +0 -0
  59. {chunksilo-2.4.0 → chunksilo-3.0.0}/test/test_utils.py +0 -0
@@ -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).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "chunksilo"
7
- version = "2.4.0"
7
+ version = "3.0.0"
8
8
  description = "Local RAG-based semantic document search with MCP server interface"
9
9
  license = "Apache-2.0"
10
10
  requires-python = ">=3.11"
@@ -5,7 +5,7 @@ llama-index-retrievers-bm25>=0.1.0,<1
5
5
  pillow>=10.3.0,<11
6
6
  pypdf>=5.1.0,<7
7
7
  python-docx>=1.1.0,<2
8
- mcp>=1.0.0,<2
8
+ mcp>=1.10.0,<2
9
9
  huggingface-hub>=0.22.0,<2
10
10
  flashrank>=0.1.0,<1
11
11
  fastembed>=0.5.0,<1
@@ -1,4 +1,4 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
2
  """ChunkSilo - Local RAG-based semantic document search."""
3
3
 
4
- __version__ = "2.4.0"
4
+ __version__ = "3.0.0"
@@ -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()