Skip to content

Commit 3ec97e4

Browse files
Cheticclaude
andcommitted
feat: bundle retrieval models into an offline wheel for air-gapped installs
Ship the embedding (BAAI/bge-small-en-v1.5) and rerank (ms-marco-MiniLM-L-12-v2) models inside the package so a pip-installed wheel runs with no network access. cfgload transparently falls back to the bundled models (offline mode) when the configured cache lacks them, while still respecting a user-provided cache that already has them. The model-bundled wheel (~260 MB) exceeds public PyPI's 100 MB/file limit, so it is built in a dedicated release job and attached to the GitHub Release for download into a self-hosted index (Artifactory); the lean wheel continues to publish to PyPI unchanged. The offline wheel is built against chunksilo-2.3.3-linux-x86_64.lock (pip --no-deps) so the dependency closure is identical to v2.3.3 with no upgrades. - cfgload: _resolve_bundled_models() picks the packaged cache + offline - pyproject: package-data includes _bundled_models, excludes HF blobs/ - manual-release: build_offline_wheel job downloads/dereferences models - add v2.3.3 fully-pinned transitive lock; gitignore the bundle dir Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 03978a5 commit 3ec97e4

5 files changed

Lines changed: 257 additions & 1 deletion

File tree

.github/workflows/manual-release.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,59 @@ jobs:
251251
packages/*.tar.gz
252252
253253
254+
# Offline wheel: the same chunksilo wheel with the embedding + rerank models
255+
# bundled in (~260 MB). Too large for public PyPI's 100 MB/file limit, so it is
256+
# NEVER published there — it is attached to the GitHub Release for download into
257+
# an air-gapped, self-hosted index (Artifactory). Installs with zero network.
258+
build_offline_wheel:
259+
needs: [prepare, release]
260+
runs-on: ubuntu-latest
261+
steps:
262+
- uses: actions/checkout@v4
263+
264+
- uses: actions/setup-python@v5
265+
with:
266+
python-version: '3.11'
267+
268+
- name: Set package version
269+
run: |
270+
VERSION="${{ needs.prepare.outputs.version }}"
271+
sed -i "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
272+
sed -i "s/^__version__ = .*/__version__ = \"$VERSION\"/" src/chunksilo/__init__.py
273+
274+
- name: Bundle retrieval models into the wheel
275+
env:
276+
# Prefer real files in snapshots/ over symlinks into blobs/ so the models
277+
# survive being packed into a wheel (a zip cannot carry symlinks).
278+
HF_HUB_DISABLE_SYMLINKS: "1"
279+
run: |
280+
# Install the exact dependency set the release used; no upgrades.
281+
pip install --no-deps -r chunksilo-2.3.3-linux-x86_64.lock
282+
BUNDLE=src/chunksilo/_bundled_models
283+
PYTHONPATH=src python -m chunksilo.index --download-models \
284+
--model-cache-dir "$BUNDLE"
285+
# Belt-and-suspenders: materialize any remaining symlinks into real files
286+
# (independent of the hf_hub version honoring HF_HUB_DISABLE_SYMLINKS),
287+
# then drop the now-redundant blob store. exclude-package-data also keeps
288+
# blobs/ out of the wheel in case anything is left behind.
289+
find "$BUNDLE" -type l | while read -r link; do
290+
target=$(readlink -f "$link"); rm "$link"; cp "$target" "$link"
291+
done
292+
find "$BUNDLE" -type d -name blobs -prune -exec rm -rf {} +
293+
294+
- name: Build offline wheel
295+
run: |
296+
pip install --no-deps build
297+
python -m build --wheel
298+
ls -lh dist/
299+
300+
- name: Attach offline wheel to the GitHub Release
301+
env:
302+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
303+
run: |
304+
set -eo pipefail
305+
gh release upload "v${{ needs.prepare.outputs.version }}" dist/*.whl --clobber
306+
254307
publish_pypi:
255308
needs: [prepare, release]
256309
runs-on: ubuntu-latest

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ wheels/
2929
data/
3030
storage/
3131
models/
32+
# Retrieval models bundled into the wheel at build time (never committed)
33+
src/chunksilo/_bundled_models/
3234

3335
# Test artifacts (generated by test suite)
3436
test_data/

chunksilo-2.3.3-linux-x86_64.lock

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Fully-pinned transitive dependency lock for chunksilo 2.3.3
2+
#
3+
# Derived from the exact wheels/sdists bundled in the official release:
4+
# asset: chunksilo-2.3.3-manylinux_2_34_x86_64.tar.gz (dependencies/)
5+
# digest: sha256:6d00e7b2a216932e6032ab7d4dae8e901a34cd819161135256520d2d7aa0fd2d
6+
# platform: manylinux_2_34 x86_64 / CPython 3.11
7+
# includes: base deps + 'confluence' and 'jira' extras (as the release bundles them)
8+
#
9+
# Reproduce the release's exact dependency set:
10+
# pip install --no-deps -r chunksilo-2.3.3-linux-x86_64.lock
11+
#
12+
# Notes:
13+
# - No --hash lines: versions come from bundled wheel filenames, not a hashed index.
14+
# - Linux/x86_64/py3.11 closure: some pins are platform-specific and won't
15+
# resolve on macOS/Windows or other Python versions.
16+
# - chunksilo==2.3.3 itself is the app, not listed as a dependency.
17+
#
18+
aiohappyeyeballs==2.6.1
19+
aiohttp==3.13.2
20+
aiosignal==1.4.0
21+
aiosqlite==0.21.0
22+
annotated-types==0.7.0
23+
anyio==4.12.0
24+
atlassian-python-api==4.0.7
25+
attrs==25.4.0
26+
banks==2.2.0
27+
beautifulsoup4==4.14.3
28+
bm25s==0.2.14
29+
certifi==2025.11.12
30+
cffi==2.0.0
31+
charset-normalizer==3.4.4
32+
click==8.3.1
33+
colorama==0.4.6
34+
coloredlogs==15.0.1
35+
cryptography==46.0.3
36+
cssselect2==0.8.0
37+
dataclasses-json==0.6.7
38+
defusedxml==0.7.1
39+
Deprecated==1.2.18
40+
dirtyjson==1.0.8
41+
distro==1.9.0
42+
docx2txt==0.8
43+
fastembed==0.7.4
44+
filelock==3.20.0
45+
filetype==1.2.0
46+
FlashRank==0.2.10
47+
flatbuffers==25.9.23
48+
frozenlist==1.8.0
49+
fsspec==2025.12.0
50+
greenlet==3.3.0
51+
griffe==1.15.0
52+
h11==0.16.0
53+
hf-xet==1.2.0
54+
httpcore==1.0.9
55+
httpx-sse==0.4.3
56+
httpx==0.28.1
57+
huggingface-hub==1.1.7
58+
humanfriendly==10.0
59+
idna==3.11
60+
jinja2==3.1.6
61+
jira==3.10.5
62+
jiter==0.12.0
63+
jmespath==1.0.1
64+
joblib==1.5.2
65+
jsonschema-specifications==2025.9.1
66+
jsonschema==4.25.1
67+
llama-cloud-services==0.6.54
68+
llama-cloud==0.1.35
69+
llama-index-cli==0.5.3
70+
llama-index-core==0.14.10
71+
llama-index-embeddings-fastembed==0.5.0
72+
llama-index-embeddings-openai==0.5.1
73+
llama-index-indices-managed-llama-cloud==0.9.4
74+
llama-index-instrumentation==0.4.2
75+
llama-index-llms-openai==0.6.10
76+
llama-index-readers-confluence==0.6.0
77+
llama-index-readers-file==0.5.5
78+
llama-index-readers-llama-parse==0.5.1
79+
llama-index-retrievers-bm25==0.6.5
80+
llama-index-workflows==2.11.5
81+
llama-index==0.14.10
82+
llama-parse==0.6.54
83+
loguru==0.7.3
84+
lxml==6.0.2
85+
markdownify==1.2.2
86+
markupsafe==3.0.3
87+
marshmallow==3.26.1
88+
mcp==1.23.1
89+
mmh3==5.2.0
90+
mpmath==1.3.0
91+
multidict==6.7.0
92+
mypy-extensions==1.1.0
93+
nest-asyncio==1.6.0
94+
networkx==3.6
95+
nltk==3.9.2
96+
numpy==2.3.5
97+
oauthlib==3.3.1
98+
onnxruntime==1.23.2
99+
openai==2.9.0
100+
packaging==25.0
101+
pandas==2.2.3
102+
pdf2image==1.17.0
103+
pillow==10.3.0
104+
platformdirs==4.5.0
105+
propcache==0.4.1
106+
protobuf==6.33.1
107+
py-rust-stemmers==0.1.5
108+
pycparser==2.23
109+
pydantic-core==2.41.5
110+
pydantic-settings==2.12.0
111+
pydantic==2.12.5
112+
PyJWT==2.10.1
113+
pypdf==6.4.0
114+
PyStemmer==2.2.0.3
115+
pytesseract==0.3.13
116+
python-dateutil==2.9.0.post0
117+
python-docx==1.2.0
118+
python-dotenv==1.2.1
119+
python-multipart==0.0.20
120+
pytz==2025.2
121+
pyyaml==6.0.3
122+
referencing==0.37.0
123+
regex==2025.11.3
124+
reportlab==4.4.9
125+
requests-oauthlib==2.0.0
126+
requests-toolbelt==1.0.0
127+
requests==2.32.5
128+
retrying==1.4.2
129+
rpds-py==0.30.0
130+
scipy==1.17.1
131+
setuptools==82.0.0
132+
shellingham==1.5.4
133+
six==1.17.0
134+
sniffio==1.3.1
135+
soupsieve==2.8
136+
sqlalchemy==2.0.44
137+
sse-starlette==3.0.3
138+
starlette==0.50.0
139+
striprtf==0.0.26
140+
svglib==1.5.1
141+
sympy==1.14.0
142+
tenacity==9.1.2
143+
tiktoken==0.12.0
144+
tinycss2==1.5.1
145+
tokenizers==0.22.1
146+
tqdm==4.67.1
147+
typer-slim==0.20.0
148+
typing-extensions==4.15.0
149+
typing-inspect==0.9.0
150+
typing-inspection==0.4.2
151+
tzdata==2025.2
152+
urllib3==2.5.0
153+
uvicorn==0.38.0
154+
webencodings==0.5.1
155+
wrapt==1.17.3
156+
xlrd==2.0.2
157+
yarl==1.22.0

pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ dependencies = {file = ["requirements.txt"]}
4343
[tool.setuptools.packages.find]
4444
where = ["src"]
4545

46+
# Bundle the retrieval models into the wheel so air-gapped installs work with no
47+
# network access. Populated at build time (see the publish job / prepare-common.sh).
48+
# The HuggingFace `blobs/` copies are excluded: with HF_HUB_DISABLE_SYMLINKS=1 the
49+
# `snapshots/` tree holds real files, so blobs would only duplicate the bytes.
50+
[tool.setuptools.package-data]
51+
chunksilo = ["_bundled_models/**/*"]
52+
53+
[tool.setuptools.exclude-package-data]
54+
chunksilo = ["_bundled_models/**/blobs/**"]
55+
4656
[tool.ruff]
4757
target-version = "py311"
4858
line-length = 120

src/chunksilo/cfgload.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
Loads configuration from config.yaml, searching in standard locations.
77
"""
8+
import copy
89
import logging
910
import os
1011
from pathlib import Path
@@ -14,6 +15,10 @@
1415

1516
logger = logging.getLogger(__name__)
1617

18+
# Retrieval models bundled inside the installed package (populated at wheel-build
19+
# time). Present in air-gapped wheel installs; absent in plain source checkouts.
20+
_BUNDLED_MODELS_DIR = Path(__file__).resolve().parent / "_bundled_models"
21+
1722

1823
def _find_config() -> Path:
1924
"""Find config.yaml using a priority-based search.
@@ -137,6 +142,34 @@ def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any
137142
return result
138143

139144

145+
def _resolve_bundled_models(config: dict[str, Any]) -> dict[str, Any]:
146+
"""Transparently fall back to models bundled in the package.
147+
148+
If the wheel ships the retrieval models and the configured cache directory
149+
does not already contain them, point the cache at the bundled copy and enable
150+
offline mode. This lets a pip-installed wheel run in an air-gapped environment
151+
with zero extra configuration. A user-configured cache that already holds the
152+
models always wins, so explicit setups are never overridden.
153+
"""
154+
embed_name = config["retrieval"]["embed_model_name"]
155+
hf_dirname = f"models--{embed_name.replace('/', '--')}"
156+
157+
if not (_BUNDLED_MODELS_DIR / hf_dirname).exists():
158+
return config # No bundled models for this embedding model.
159+
160+
configured = Path(config["storage"]["model_cache_dir"]).expanduser()
161+
if (configured / hf_dirname).exists():
162+
return config # Configured cache already has the models; respect it.
163+
164+
result = copy.deepcopy(config)
165+
result["storage"]["model_cache_dir"] = str(_BUNDLED_MODELS_DIR)
166+
result["retrieval"]["offline"] = True
167+
logger.info(
168+
"Using bundled retrieval models at %s (offline mode)", _BUNDLED_MODELS_DIR
169+
)
170+
return result
171+
172+
140173
def load_config(config_path: Path | None = None) -> dict[str, Any]:
141174
"""Load configuration from YAML file with defaults.
142175
@@ -157,14 +190,15 @@ def load_config(config_path: Path | None = None) -> dict[str, Any]:
157190

158191
if not path.exists():
159192
logger.info("Config file not found at %s; using built-in defaults", path)
160-
return _DEFAULTS.copy()
193+
return _resolve_bundled_models(_DEFAULTS.copy())
161194

162195
logger.info("Using config: %s", path)
163196

164197
with open(path, encoding="utf-8") as f:
165198
user_config = yaml.safe_load(f) or {}
166199

167200
result = _deep_merge(_DEFAULTS, user_config)
201+
result = _resolve_bundled_models(result)
168202

169203
# Cache result only for default path
170204
if config_path is None:

0 commit comments

Comments
 (0)