okfgraph 0.2.7__tar.gz → 0.2.12__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.
- {okfgraph-0.2.7 → okfgraph-0.2.12}/PKG-INFO +24 -18
- {okfgraph-0.2.7 → okfgraph-0.2.12}/README.md +22 -16
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/__init__.py +2 -1
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/embedding.py +234 -15
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/router.py +83 -21
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph.egg-info/PKG-INFO +24 -18
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph.egg-info/SOURCES.txt +3 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph.egg-info/requires.txt +1 -1
- {okfgraph-0.2.7 → okfgraph-0.2.12}/pyproject.toml +7 -7
- okfgraph-0.2.12/tests/test_explicit_files.py +147 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_gpu_integration.py +5 -1
- okfgraph-0.2.12/tests/test_lazy_encoder.py +202 -0
- okfgraph-0.2.12/tests/test_ort.py +196 -0
- okfgraph-0.2.12/tests/test_packaging.py +28 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_parity.py +8 -8
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_rust_backend.py +36 -5
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_rust_e2e.py +6 -3
- okfgraph-0.2.7/tests/test_packaging.py +0 -29
- {okfgraph-0.2.7 → okfgraph-0.2.12}/LICENSE +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/__init__.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/cli.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/converters.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/delta.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/diff.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/doctor.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/export.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/image_assets.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/import_.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/ingest.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/links.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/lint.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/purge.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/ranking.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/schema.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/search.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/config.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/images.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/mcp_server.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/models.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/security.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/tools.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph.egg-info/dependency_links.txt +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph.egg-info/entry_points.txt +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph.egg-info/top_level.txt +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/setup.cfg +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_chunk_search.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_chunking.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_cli.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_config.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_converter.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_delta.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_diff.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_directory_hash.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_doctor.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_export_compliance.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_graph_enrichment.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_images.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_ingest.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_ingest_tool.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_integration.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_lint.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_logging.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_mcp_server.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_models.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_obsidian.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_okf_ingest_tool.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_pdf_e2e.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_ppr_search.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_ranking.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_reconstruction.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_reserved.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_roundup_cli.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_router.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_router_misc.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_sanitize.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_search_browser.py +0 -0
- {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_security.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: okfgraph
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
4
4
|
Summary: Ladybug-backed OKF knowledge graph with ONNX + Jina v5 embeddings
|
|
5
5
|
License-Expression: Apache-2.0 OR MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: ladybug==0.20.3
|
|
10
|
-
Requires-Dist:
|
|
10
|
+
Requires-Dist: embroider<0.2,>=0.1
|
|
11
11
|
Requires-Dist: pydantic>=2.0
|
|
12
12
|
Requires-Dist: python-frontmatter>=1.0
|
|
13
13
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -44,7 +44,7 @@ model loaded**, structural diffing, scored health checks, and Obsidian-vault
|
|
|
44
44
|
compatible wikilinks — in a single LadybugDB file.
|
|
45
45
|
|
|
46
46
|
Design stance: **slim dependencies, no legacy fallbacks.** Embeddings run
|
|
47
|
-
through the `
|
|
47
|
+
through the `embroider` Rust wheel (ONNX Runtime, no torch / transformers /
|
|
48
48
|
optimum anywhere). PDF conversion runs through the `bobine` Rust engine behind
|
|
49
49
|
a swappable `DocumentConverter` seam. What isn't needed isn't installed.
|
|
50
50
|
|
|
@@ -54,7 +54,7 @@ a swappable `DocumentConverter` seam. What isn't needed isn't installed.
|
|
|
54
54
|
|
|
55
55
|
| Category | Features |
|
|
56
56
|
|---|---|
|
|
57
|
-
| **Embeddings** | Jina v5 (`jina-embeddings-v5-text-small-retrieval`) via the `
|
|
57
|
+
| **Embeddings** | Jina v5 (`jina-embeddings-v5-text-small-retrieval`) via the `embroider` Rust wheel; last-token pooling, Matryoshka truncation (32–1024, default 512); omni model (`…-omni-small-retrieval`) lazy-loaded for images only |
|
|
58
58
|
| **Search** | Hybrid RRF fusion (vector + FTS) at concept and chunk granularity; `rank=none\|hub\|ppr` — including **PPR**: lexical seeds → exact Personalized PageRank, zero model load, deterministic |
|
|
59
59
|
| **Read** | Body / chunks / rebuilt document / graph context, with optional **token budgets** (`max_tokens`): self first, then PPR-ranked neighbours, index-first for context |
|
|
60
60
|
| **Storage** | LadybugDB `==0.20.3` (pinned — newer 0.20.x segfaults index builds): graph + vector + FTS in one file |
|
|
@@ -76,24 +76,25 @@ a swappable `DocumentConverter` seam. What isn't needed isn't installed.
|
|
|
76
76
|
Requires Python ≥ 3.11.
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
|
-
pip install "okfgraph[pdf,omni]" # PyPI (
|
|
79
|
+
pip install "okfgraph[pdf,omni]" # PyPI (embeddings come from the published `embroider` wheels)
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
Or from source with `uv` (
|
|
83
|
-
|
|
82
|
+
Or from source with `uv` (no Rust toolchain needed — the embedding
|
|
83
|
+
engine is the external `embroider` package):
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
86
|
git clone <repo> && cd OKFgraph
|
|
87
|
-
uv sync # core: ladybug,
|
|
87
|
+
uv sync # core: ladybug, embroider, onnxruntime, mordant, mcp, …
|
|
88
88
|
uv sync --extra pdf # bobine PDF converter
|
|
89
89
|
uv sync --extra omni # sentence-transformers + Pillow (image embeddings)
|
|
90
90
|
uv sync --extra dev # pytest
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
Core dependencies are deliberately few: `ladybug==0.20.3`, `
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`
|
|
93
|
+
Core dependencies are deliberately few: `ladybug==0.20.3`, `embroider>=0.1,<0.2`
|
|
94
|
+
(the shared embedding engine — github.com/opticsWolf/embroider, also used by
|
|
95
|
+
bobine), `onnxruntime==1.29.0` (one pinned ORT binary shared by bobine +
|
|
96
|
+
embroider; `ORT_DYLIB_PATH`-overridable), `mordant`, `mcp>=2.0`, `pydantic`,
|
|
97
|
+
`pyyaml`, `numpy`, `python-frontmatter`, `fasteners`. There is no torch, no
|
|
97
98
|
transformers, no optimum, no PDF stack in the core install.
|
|
98
99
|
|
|
99
100
|
---
|
|
@@ -242,7 +243,7 @@ Delta ──► Schema ──► ImageAssets ──► Purge ──► Ingest
|
|
|
242
243
|
│ (bobine seam)
|
|
243
244
|
└──────────────── LadybugDB (graph + vector + FTS, one file) ──┘
|
|
244
245
|
▲
|
|
245
|
-
Rust
|
|
246
|
+
Rust embroider (Jina v5, ORT) — tokenize, last-token
|
|
246
247
|
pool, L2-norm, Matryoshka truncate; numerics pinned
|
|
247
248
|
by tests/test_parity.py
|
|
248
249
|
```
|
|
@@ -259,10 +260,14 @@ Key design decisions:
|
|
|
259
260
|
|
|
260
261
|
- **Rust-only embeddings, fail-fast** — no Python fallback; a mid-run stack
|
|
261
262
|
switch would silently mix vector spaces in one index.
|
|
263
|
+
- **Lazy session init** — router construction never downloads the model or
|
|
264
|
+
builds the ONNX session; first encode opens once (thread-safe), token
|
|
265
|
+
counting uses a tokenizer-only handle, so PPR search, budgeted reads,
|
|
266
|
+
diff, and doctor stay cold.
|
|
262
267
|
- **Last-token pooling** (not mean) — required by Jina v5; mean pooling
|
|
263
268
|
breaks alignment with omni image embeddings.
|
|
264
269
|
- **Single pinned ORT** (`onnxruntime==1.29.0`, `ORT_DYLIB_PATH`-overridable)
|
|
265
|
-
shared by bobine +
|
|
270
|
+
shared by bobine + embroider.
|
|
266
271
|
- **Bobine is a plugin, not a dependency** — `DocumentConverter.convert()`
|
|
267
272
|
is the seam; provider owns its options (`routing_mode`,
|
|
268
273
|
`extract_images`); missing bobine → clear `RuntimeError`, never a silent
|
|
@@ -300,7 +305,8 @@ Key design decisions:
|
|
|
300
305
|
```bash
|
|
301
306
|
uv run --project . pytest tests/ -q # full suite (model loads; takes a while)
|
|
302
307
|
uv run --project . pytest tests/test_ranking.py tests/test_mcp_server.py -q # fast subset
|
|
303
|
-
|
|
308
|
+
# Rust unit tests live in the embroider repo:
|
|
309
|
+
# https://github.com/opticsWolf/embroider (cargo test --locked)
|
|
304
310
|
```
|
|
305
311
|
|
|
306
312
|
Golden fixtures under `tests/fixtures/` (`ppr_graph`, `diff_a`/`diff_b`,
|
|
@@ -327,7 +333,6 @@ okfgraph/
|
|
|
327
333
|
│ ├── tools.py # legacy tool definitions (superseded by mcp_server)
|
|
328
334
|
│ └── components/ # ranking, links, search, lint, import_, export, diff, doctor,
|
|
329
335
|
│ # embedding, converters, image_assets, delta, purge, schema, ingest
|
|
330
|
-
├── rust/okf-embed/ # Jina v5 Rust loader (ort, CUDA-opportunistic) + Python wheel
|
|
331
336
|
├── skills/ # okfgraph-mcp, okfgraph-cli, okfgraph-ingest (harness-neutral source)
|
|
332
337
|
├── tests/fixtures/ # conformance corpus: ppr, diff, doctor, obsidian, bundles
|
|
333
338
|
├── docs/ # converters, harness-integration, plan-retrieval-roundup, diagnostics…
|
|
@@ -340,8 +345,9 @@ okfgraph/
|
|
|
340
345
|
|
|
341
346
|
## Requirements
|
|
342
347
|
|
|
343
|
-
Core (`uv sync`): `ladybug==0.20.3`, `
|
|
344
|
-
`onnxruntime==1.29.0`, `mordant>=0.9`,
|
|
348
|
+
Core (`uv sync`): `ladybug==0.20.3`, `embroider>=0.1,<0.2` (PyPI wheels,
|
|
349
|
+
github.com/opticsWolf/embroider), `onnxruntime==1.29.0`, `mordant>=0.9`,
|
|
350
|
+
`mcp>=2.0`, `pydantic>=2`, `pyyaml>=6`,
|
|
345
351
|
`numpy>=1.26`, `python-frontmatter>=1`, `fasteners>=0.19`. Python ≥ 3.11.
|
|
346
352
|
|
|
347
353
|
- `--extra pdf`: `bobine>=0.5` (default PDF converter).
|
|
@@ -17,7 +17,7 @@ model loaded**, structural diffing, scored health checks, and Obsidian-vault
|
|
|
17
17
|
compatible wikilinks — in a single LadybugDB file.
|
|
18
18
|
|
|
19
19
|
Design stance: **slim dependencies, no legacy fallbacks.** Embeddings run
|
|
20
|
-
through the `
|
|
20
|
+
through the `embroider` Rust wheel (ONNX Runtime, no torch / transformers /
|
|
21
21
|
optimum anywhere). PDF conversion runs through the `bobine` Rust engine behind
|
|
22
22
|
a swappable `DocumentConverter` seam. What isn't needed isn't installed.
|
|
23
23
|
|
|
@@ -27,7 +27,7 @@ a swappable `DocumentConverter` seam. What isn't needed isn't installed.
|
|
|
27
27
|
|
|
28
28
|
| Category | Features |
|
|
29
29
|
|---|---|
|
|
30
|
-
| **Embeddings** | Jina v5 (`jina-embeddings-v5-text-small-retrieval`) via the `
|
|
30
|
+
| **Embeddings** | Jina v5 (`jina-embeddings-v5-text-small-retrieval`) via the `embroider` Rust wheel; last-token pooling, Matryoshka truncation (32–1024, default 512); omni model (`…-omni-small-retrieval`) lazy-loaded for images only |
|
|
31
31
|
| **Search** | Hybrid RRF fusion (vector + FTS) at concept and chunk granularity; `rank=none\|hub\|ppr` — including **PPR**: lexical seeds → exact Personalized PageRank, zero model load, deterministic |
|
|
32
32
|
| **Read** | Body / chunks / rebuilt document / graph context, with optional **token budgets** (`max_tokens`): self first, then PPR-ranked neighbours, index-first for context |
|
|
33
33
|
| **Storage** | LadybugDB `==0.20.3` (pinned — newer 0.20.x segfaults index builds): graph + vector + FTS in one file |
|
|
@@ -49,24 +49,25 @@ a swappable `DocumentConverter` seam. What isn't needed isn't installed.
|
|
|
49
49
|
Requires Python ≥ 3.11.
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
pip install "okfgraph[pdf,omni]" # PyPI (
|
|
52
|
+
pip install "okfgraph[pdf,omni]" # PyPI (embeddings come from the published `embroider` wheels)
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Or from source with `uv` (
|
|
56
|
-
|
|
55
|
+
Or from source with `uv` (no Rust toolchain needed — the embedding
|
|
56
|
+
engine is the external `embroider` package):
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
59
|
git clone <repo> && cd OKFgraph
|
|
60
|
-
uv sync # core: ladybug,
|
|
60
|
+
uv sync # core: ladybug, embroider, onnxruntime, mordant, mcp, …
|
|
61
61
|
uv sync --extra pdf # bobine PDF converter
|
|
62
62
|
uv sync --extra omni # sentence-transformers + Pillow (image embeddings)
|
|
63
63
|
uv sync --extra dev # pytest
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Core dependencies are deliberately few: `ladybug==0.20.3`, `
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
66
|
+
Core dependencies are deliberately few: `ladybug==0.20.3`, `embroider>=0.1,<0.2`
|
|
67
|
+
(the shared embedding engine — github.com/opticsWolf/embroider, also used by
|
|
68
|
+
bobine), `onnxruntime==1.29.0` (one pinned ORT binary shared by bobine +
|
|
69
|
+
embroider; `ORT_DYLIB_PATH`-overridable), `mordant`, `mcp>=2.0`, `pydantic`,
|
|
70
|
+
`pyyaml`, `numpy`, `python-frontmatter`, `fasteners`. There is no torch, no
|
|
70
71
|
transformers, no optimum, no PDF stack in the core install.
|
|
71
72
|
|
|
72
73
|
---
|
|
@@ -215,7 +216,7 @@ Delta ──► Schema ──► ImageAssets ──► Purge ──► Ingest
|
|
|
215
216
|
│ (bobine seam)
|
|
216
217
|
└──────────────── LadybugDB (graph + vector + FTS, one file) ──┘
|
|
217
218
|
▲
|
|
218
|
-
Rust
|
|
219
|
+
Rust embroider (Jina v5, ORT) — tokenize, last-token
|
|
219
220
|
pool, L2-norm, Matryoshka truncate; numerics pinned
|
|
220
221
|
by tests/test_parity.py
|
|
221
222
|
```
|
|
@@ -232,10 +233,14 @@ Key design decisions:
|
|
|
232
233
|
|
|
233
234
|
- **Rust-only embeddings, fail-fast** — no Python fallback; a mid-run stack
|
|
234
235
|
switch would silently mix vector spaces in one index.
|
|
236
|
+
- **Lazy session init** — router construction never downloads the model or
|
|
237
|
+
builds the ONNX session; first encode opens once (thread-safe), token
|
|
238
|
+
counting uses a tokenizer-only handle, so PPR search, budgeted reads,
|
|
239
|
+
diff, and doctor stay cold.
|
|
235
240
|
- **Last-token pooling** (not mean) — required by Jina v5; mean pooling
|
|
236
241
|
breaks alignment with omni image embeddings.
|
|
237
242
|
- **Single pinned ORT** (`onnxruntime==1.29.0`, `ORT_DYLIB_PATH`-overridable)
|
|
238
|
-
shared by bobine +
|
|
243
|
+
shared by bobine + embroider.
|
|
239
244
|
- **Bobine is a plugin, not a dependency** — `DocumentConverter.convert()`
|
|
240
245
|
is the seam; provider owns its options (`routing_mode`,
|
|
241
246
|
`extract_images`); missing bobine → clear `RuntimeError`, never a silent
|
|
@@ -273,7 +278,8 @@ Key design decisions:
|
|
|
273
278
|
```bash
|
|
274
279
|
uv run --project . pytest tests/ -q # full suite (model loads; takes a while)
|
|
275
280
|
uv run --project . pytest tests/test_ranking.py tests/test_mcp_server.py -q # fast subset
|
|
276
|
-
|
|
281
|
+
# Rust unit tests live in the embroider repo:
|
|
282
|
+
# https://github.com/opticsWolf/embroider (cargo test --locked)
|
|
277
283
|
```
|
|
278
284
|
|
|
279
285
|
Golden fixtures under `tests/fixtures/` (`ppr_graph`, `diff_a`/`diff_b`,
|
|
@@ -300,7 +306,6 @@ okfgraph/
|
|
|
300
306
|
│ ├── tools.py # legacy tool definitions (superseded by mcp_server)
|
|
301
307
|
│ └── components/ # ranking, links, search, lint, import_, export, diff, doctor,
|
|
302
308
|
│ # embedding, converters, image_assets, delta, purge, schema, ingest
|
|
303
|
-
├── rust/okf-embed/ # Jina v5 Rust loader (ort, CUDA-opportunistic) + Python wheel
|
|
304
309
|
├── skills/ # okfgraph-mcp, okfgraph-cli, okfgraph-ingest (harness-neutral source)
|
|
305
310
|
├── tests/fixtures/ # conformance corpus: ppr, diff, doctor, obsidian, bundles
|
|
306
311
|
├── docs/ # converters, harness-integration, plan-retrieval-roundup, diagnostics…
|
|
@@ -313,8 +318,9 @@ okfgraph/
|
|
|
313
318
|
|
|
314
319
|
## Requirements
|
|
315
320
|
|
|
316
|
-
Core (`uv sync`): `ladybug==0.20.3`, `
|
|
317
|
-
`onnxruntime==1.29.0`, `mordant>=0.9`,
|
|
321
|
+
Core (`uv sync`): `ladybug==0.20.3`, `embroider>=0.1,<0.2` (PyPI wheels,
|
|
322
|
+
github.com/opticsWolf/embroider), `onnxruntime==1.29.0`, `mordant>=0.9`,
|
|
323
|
+
`mcp>=2.0`, `pydantic>=2`, `pyyaml>=6`,
|
|
318
324
|
`numpy>=1.26`, `python-frontmatter>=1`, `fasteners>=0.19`. Python ≥ 3.11.
|
|
319
325
|
|
|
320
326
|
- `--extra pdf`: `bobine>=0.5` (default PDF converter).
|
|
@@ -13,7 +13,7 @@ stubs (``...``) until their respective phase moves the implementation over.
|
|
|
13
13
|
from okfgraph.components.schema import SchemaManager
|
|
14
14
|
from okfgraph.components.delta import DeltaDetector
|
|
15
15
|
from okfgraph.components.purge import PurgeManager
|
|
16
|
-
from okfgraph.components.embedding import EmbeddingEngine
|
|
16
|
+
from okfgraph.components.embedding import EmbeddingEngine, LazyRustEncoder
|
|
17
17
|
from okfgraph.components.image_assets import ImageAssetManager
|
|
18
18
|
from okfgraph.components.search import SearchEngine
|
|
19
19
|
from okfgraph.components.import_ import ImportManager, parse_source_file
|
|
@@ -37,6 +37,7 @@ __all__ = [
|
|
|
37
37
|
"DeltaDetector",
|
|
38
38
|
"PurgeManager",
|
|
39
39
|
"EmbeddingEngine",
|
|
40
|
+
"LazyRustEncoder",
|
|
40
41
|
"ImageAssetManager",
|
|
41
42
|
"SearchEngine",
|
|
42
43
|
"ImportManager",
|
|
@@ -6,44 +6,262 @@ here. Public callers reach these via router.<method> (component bridge).
|
|
|
6
6
|
"""
|
|
7
7
|
import logging
|
|
8
8
|
import math
|
|
9
|
+
import threading
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
import mordant
|
|
12
13
|
from typing import Any, Dict, List, Optional
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
"""Point ``ORT_DYLIB_PATH`` at the pip-installed ORT build when unset.
|
|
16
|
+
_ORT_MODULE_NAMES = ("onnxruntime", "onnxruntime-gpu")
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
def _candidate_ort_library_names(os_name=None, sys_platform=None):
|
|
20
|
+
"""Return the ORT library filenames for an OS/platform pair.
|
|
21
|
+
|
|
22
|
+
Takes explicit arguments so unit tests can cover Windows/macOS/Linux
|
|
23
|
+
from any host. Linux uses a versioned ``libonnxruntime.so.*`` glob
|
|
24
|
+
(handled by :func:`_find_runtime_in_package`); this helper returns the
|
|
25
|
+
unversioned fallback name for that platform.
|
|
26
|
+
"""
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
if os_name is None:
|
|
30
|
+
os_name = os.name
|
|
31
|
+
if sys_platform is None:
|
|
32
|
+
sys_platform = sys.platform
|
|
33
|
+
if os_name == "nt":
|
|
34
|
+
return ("onnxruntime.dll",)
|
|
35
|
+
if sys_platform == "darwin":
|
|
36
|
+
return ("libonnxruntime.dylib",)
|
|
37
|
+
return ("libonnxruntime.so",)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _find_runtime_in_package(package_dir, os_name=None, sys_platform=None):
|
|
41
|
+
"""Find an ORT shared library inside a pip-installed package directory.
|
|
42
|
+
|
|
43
|
+
Returns a :class:`Path` or ``None``. Prefers versioned Linux libraries
|
|
44
|
+
(``libonnxruntime.so.*``) over the unversioned ``libonnxruntime.so``.
|
|
45
|
+
"""
|
|
46
|
+
import os
|
|
47
|
+
import sys
|
|
48
|
+
if os_name is None:
|
|
49
|
+
os_name = os.name
|
|
50
|
+
if sys_platform is None:
|
|
51
|
+
sys_platform = sys.platform
|
|
52
|
+
capi_dir = Path(package_dir) / "capi"
|
|
53
|
+
if not capi_dir.is_dir():
|
|
54
|
+
return None
|
|
55
|
+
if os_name == "nt" or sys_platform == "darwin":
|
|
56
|
+
candidate = capi_dir / _candidate_ort_library_names(os_name, sys_platform)[0]
|
|
57
|
+
return candidate if candidate.is_file() else None
|
|
58
|
+
versioned = sorted(capi_dir.glob("libonnxruntime.so.*"))
|
|
59
|
+
if versioned:
|
|
60
|
+
return versioned[-1]
|
|
61
|
+
candidate = capi_dir / "libonnxruntime.so"
|
|
62
|
+
return candidate if candidate.is_file() else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _configure_windows_ort_dll_directory(runtime_path) -> bool:
|
|
66
|
+
"""Add the ORT ``capi`` directory to Windows DLL resolution.
|
|
67
|
+
|
|
68
|
+
Best-effort only: returns ``False`` (never raises) when unavailable.
|
|
69
|
+
"""
|
|
70
|
+
import os
|
|
71
|
+
if os.name != "nt":
|
|
72
|
+
return False
|
|
73
|
+
add_dll_directory = getattr(os, "add_dll_directory", None)
|
|
74
|
+
if not callable(add_dll_directory):
|
|
75
|
+
return False
|
|
76
|
+
try:
|
|
77
|
+
add_dll_directory(str(Path(runtime_path).parent))
|
|
78
|
+
return True
|
|
79
|
+
except OSError as exc:
|
|
80
|
+
logger.debug("ORT DLL directory configuration failed: %s", exc)
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _warm_ort_gpu_dlls(module) -> bool:
|
|
85
|
+
"""Preload NVIDIA DLLs when the installed ORT package exposes CUDA.
|
|
86
|
+
|
|
87
|
+
This mirrors bobine's GPU bootstrap: only attempt the preload when the
|
|
88
|
+
package reports a CUDA execution provider, and never let it fail
|
|
89
|
+
runtime discovery.
|
|
90
|
+
"""
|
|
91
|
+
preload = getattr(module, "preload_dlls", None)
|
|
92
|
+
if not callable(preload):
|
|
93
|
+
return False
|
|
94
|
+
try:
|
|
95
|
+
providers = list(module.get_available_providers())
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
logger.debug("ORT provider query failed: %s", exc)
|
|
98
|
+
return False
|
|
99
|
+
if "CUDAExecutionProvider" not in providers:
|
|
100
|
+
return False
|
|
101
|
+
try:
|
|
102
|
+
preload()
|
|
103
|
+
return True
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
logger.debug("ORT GPU DLL preload failed: %s", exc)
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resolve_ort_dylib(*, warm_gpu: bool = True, os_name=None, sys_platform=None) -> Optional[str]:
|
|
110
|
+
"""Point ``ORT_DYLIB_PATH`` at a pip-installed ORT build when unset.
|
|
111
|
+
|
|
112
|
+
Both bobine and embroider load ONNX Runtime dynamically; sharing one
|
|
19
113
|
binary avoids version/CUDA drift between the two runtimes. Explicit
|
|
20
114
|
user configuration always wins — this only fills the gap.
|
|
115
|
+
|
|
116
|
+
Resolution order:
|
|
117
|
+
|
|
118
|
+
1. Existing ``ORT_DYLIB_PATH``.
|
|
119
|
+
2. Pip-installed ``onnxruntime`` or ``onnxruntime-gpu`` package.
|
|
120
|
+
3. OS loader path (represented by returning ``None``).
|
|
121
|
+
|
|
122
|
+
Missing runtimes never raise here; session creation or encoding is the
|
|
123
|
+
fail-fast boundary. ``os_name``/``sys_platform`` are explicit so unit
|
|
124
|
+
tests can cover every platform from any host (patching ``os.name``
|
|
125
|
+
would break ``pathlib`` dispatch on Windows).
|
|
21
126
|
"""
|
|
22
127
|
import os
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
128
|
+
explicit = os.environ.get("ORT_DYLIB_PATH")
|
|
129
|
+
if explicit:
|
|
130
|
+
return explicit
|
|
131
|
+
for module_name in _ORT_MODULE_NAMES:
|
|
132
|
+
try:
|
|
133
|
+
module = __import__(module_name)
|
|
134
|
+
except ImportError:
|
|
135
|
+
continue
|
|
136
|
+
try:
|
|
137
|
+
package_dir = Path(str(module.__file__)).parent
|
|
138
|
+
except Exception:
|
|
139
|
+
continue
|
|
140
|
+
found = _find_runtime_in_package(package_dir, os_name, sys_platform)
|
|
141
|
+
if found is None:
|
|
142
|
+
continue
|
|
143
|
+
os.environ["ORT_DYLIB_PATH"] = str(found)
|
|
144
|
+
_configure_windows_ort_dll_directory(found)
|
|
145
|
+
if warm_gpu:
|
|
146
|
+
_warm_ort_gpu_dlls(module)
|
|
147
|
+
logger.debug("resolved ORT runtime: %s", found)
|
|
148
|
+
return str(found)
|
|
33
149
|
return None
|
|
34
150
|
|
|
35
151
|
|
|
152
|
+
class LazyRustEncoder:
|
|
153
|
+
"""Defers the ONNX session open until the first real encode.
|
|
154
|
+
|
|
155
|
+
Router construction stays cheap: the ``embroider`` wheel import is still
|
|
156
|
+
validated eagerly (fail fast on a missing install), but ``JinaV5.open``
|
|
157
|
+
— model download + session build — waits for the first ``encode`` /
|
|
158
|
+
``encode_batch`` / ``used_cuda`` access. Token counting uses the
|
|
159
|
+
separate lightweight ``JinaTokenizer`` handle, so budgeted reads and the
|
|
160
|
+
context-window guard stay cold too.
|
|
161
|
+
|
|
162
|
+
A failed session open is cached and re-raised: configuration errors stay
|
|
163
|
+
fail-fast (once, at first encode) instead of retrying network/model
|
|
164
|
+
acquisition on every call. Thread-safe: concurrent first encodes open
|
|
165
|
+
exactly one session.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(self, *, model_id, truncate_dim, device,
|
|
169
|
+
session_factory, tokenizer_factory, on_open=None):
|
|
170
|
+
self._model_id = model_id
|
|
171
|
+
self._truncate_dim = truncate_dim
|
|
172
|
+
self._device = device
|
|
173
|
+
self._session_factory = session_factory
|
|
174
|
+
self._tokenizer_factory = tokenizer_factory
|
|
175
|
+
self._on_open = on_open
|
|
176
|
+
self._lock = threading.Lock()
|
|
177
|
+
self._encoder = None
|
|
178
|
+
self._encoder_error = None
|
|
179
|
+
self._open_reported = False
|
|
180
|
+
self._tokenizer = None
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def is_loaded(self) -> bool:
|
|
184
|
+
"""True once the ONNX session has been opened."""
|
|
185
|
+
return self._encoder is not None
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def model_id(self) -> str:
|
|
189
|
+
return self._model_id
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def dim(self) -> int:
|
|
193
|
+
return self._truncate_dim
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def used_cuda(self) -> bool:
|
|
197
|
+
"""Effective device — opens the session on first access."""
|
|
198
|
+
return bool(self._get_encoder().used_cuda)
|
|
199
|
+
|
|
200
|
+
def encode(self, text: str, task: str = "Document"):
|
|
201
|
+
return self._get_encoder().encode(text, task=task)
|
|
202
|
+
|
|
203
|
+
def encode_batch(self, texts, task: str = "Document"):
|
|
204
|
+
return self._get_encoder().encode_batch(texts, task=task)
|
|
205
|
+
|
|
206
|
+
def count_tokens(self, text: str) -> int:
|
|
207
|
+
"""Exact count via the tokenizer-only handle (never opens the session)."""
|
|
208
|
+
return int(self._get_tokenizer().count_tokens(text))
|
|
209
|
+
|
|
210
|
+
def _get_encoder(self):
|
|
211
|
+
encoder = self._encoder
|
|
212
|
+
if encoder is not None:
|
|
213
|
+
return encoder
|
|
214
|
+
with self._lock:
|
|
215
|
+
if self._encoder is not None:
|
|
216
|
+
return self._encoder
|
|
217
|
+
if self._encoder_error is not None:
|
|
218
|
+
raise self._encoder_error
|
|
219
|
+
try:
|
|
220
|
+
encoder = self._session_factory()
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
self._encoder_error = exc
|
|
223
|
+
raise
|
|
224
|
+
self._encoder = encoder
|
|
225
|
+
if self._on_open is not None and not self._open_reported:
|
|
226
|
+
self._open_reported = True
|
|
227
|
+
self._on_open(encoder)
|
|
228
|
+
return encoder
|
|
229
|
+
|
|
230
|
+
def _get_tokenizer(self):
|
|
231
|
+
tokenizer = self._tokenizer
|
|
232
|
+
if tokenizer is not None:
|
|
233
|
+
return tokenizer
|
|
234
|
+
with self._lock:
|
|
235
|
+
if self._tokenizer is not None:
|
|
236
|
+
return self._tokenizer
|
|
237
|
+
try:
|
|
238
|
+
tokenizer = self._tokenizer_factory()
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
logger.debug("tokenizer-only open failed: %s", exc)
|
|
241
|
+
raise
|
|
242
|
+
self._tokenizer = tokenizer
|
|
243
|
+
return tokenizer
|
|
244
|
+
|
|
245
|
+
def __repr__(self) -> str:
|
|
246
|
+
state = "loaded" if self._encoder is not None else "cold"
|
|
247
|
+
return (
|
|
248
|
+
f"LazyRustEncoder({self._model_id}, "
|
|
249
|
+
f"dim={self._truncate_dim}, {state})"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
36
253
|
class EmbeddingEngine:
|
|
37
254
|
"""Owns the embedding model and chunking logic.
|
|
38
255
|
|
|
39
|
-
Text embeddings come from the Rust
|
|
256
|
+
Text embeddings come from the Rust embroider wheel (Jina v5 via ORT):
|
|
40
257
|
prefixed, last-token pooled, truncated. There is no Python fallback —
|
|
41
258
|
a mid-run stack switch would silently mix vector spaces in one index.
|
|
42
259
|
"""
|
|
43
260
|
|
|
44
261
|
def __init__(self, rust_encoder, embedding_dim, device,
|
|
45
262
|
cache_dir, model_id, omni_model_id, omni,
|
|
46
|
-
chunk_size, chunk_overlap, enable_chunking, conn
|
|
263
|
+
chunk_size, chunk_overlap, enable_chunking, conn,
|
|
264
|
+
ort_dylib=None):
|
|
47
265
|
self.encoder = rust_encoder
|
|
48
266
|
self.embedding_dim = embedding_dim
|
|
49
267
|
self.device = device
|
|
@@ -55,6 +273,7 @@ class EmbeddingEngine:
|
|
|
55
273
|
self.chunk_overlap = chunk_overlap
|
|
56
274
|
self.enable_chunking = enable_chunking
|
|
57
275
|
self.conn = conn
|
|
276
|
+
self.ort_dylib = ort_dylib
|
|
58
277
|
|
|
59
278
|
def _encode(self, text: str, task: str = "Document") -> List[float]:
|
|
60
279
|
"""Encode text with the Rust Jina v5 encoder.
|