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.
Files changed (77) hide show
  1. {okfgraph-0.2.7 → okfgraph-0.2.12}/PKG-INFO +24 -18
  2. {okfgraph-0.2.7 → okfgraph-0.2.12}/README.md +22 -16
  3. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/__init__.py +2 -1
  4. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/embedding.py +234 -15
  5. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/router.py +83 -21
  6. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph.egg-info/PKG-INFO +24 -18
  7. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph.egg-info/SOURCES.txt +3 -0
  8. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph.egg-info/requires.txt +1 -1
  9. {okfgraph-0.2.7 → okfgraph-0.2.12}/pyproject.toml +7 -7
  10. okfgraph-0.2.12/tests/test_explicit_files.py +147 -0
  11. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_gpu_integration.py +5 -1
  12. okfgraph-0.2.12/tests/test_lazy_encoder.py +202 -0
  13. okfgraph-0.2.12/tests/test_ort.py +196 -0
  14. okfgraph-0.2.12/tests/test_packaging.py +28 -0
  15. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_parity.py +8 -8
  16. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_rust_backend.py +36 -5
  17. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_rust_e2e.py +6 -3
  18. okfgraph-0.2.7/tests/test_packaging.py +0 -29
  19. {okfgraph-0.2.7 → okfgraph-0.2.12}/LICENSE +0 -0
  20. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/__init__.py +0 -0
  21. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/cli.py +0 -0
  22. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/converters.py +0 -0
  23. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/delta.py +0 -0
  24. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/diff.py +0 -0
  25. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/doctor.py +0 -0
  26. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/export.py +0 -0
  27. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/image_assets.py +0 -0
  28. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/import_.py +0 -0
  29. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/ingest.py +0 -0
  30. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/links.py +0 -0
  31. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/lint.py +0 -0
  32. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/purge.py +0 -0
  33. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/ranking.py +0 -0
  34. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/schema.py +0 -0
  35. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/components/search.py +0 -0
  36. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/config.py +0 -0
  37. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/images.py +0 -0
  38. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/mcp_server.py +0 -0
  39. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/models.py +0 -0
  40. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/security.py +0 -0
  41. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph/tools.py +0 -0
  42. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph.egg-info/dependency_links.txt +0 -0
  43. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph.egg-info/entry_points.txt +0 -0
  44. {okfgraph-0.2.7 → okfgraph-0.2.12}/okfgraph.egg-info/top_level.txt +0 -0
  45. {okfgraph-0.2.7 → okfgraph-0.2.12}/setup.cfg +0 -0
  46. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_chunk_search.py +0 -0
  47. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_chunking.py +0 -0
  48. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_cli.py +0 -0
  49. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_config.py +0 -0
  50. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_converter.py +0 -0
  51. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_delta.py +0 -0
  52. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_diff.py +0 -0
  53. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_directory_hash.py +0 -0
  54. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_doctor.py +0 -0
  55. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_export_compliance.py +0 -0
  56. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_graph_enrichment.py +0 -0
  57. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_images.py +0 -0
  58. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_ingest.py +0 -0
  59. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_ingest_tool.py +0 -0
  60. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_integration.py +0 -0
  61. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_lint.py +0 -0
  62. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_logging.py +0 -0
  63. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_mcp_server.py +0 -0
  64. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_models.py +0 -0
  65. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_obsidian.py +0 -0
  66. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_okf_ingest_tool.py +0 -0
  67. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_pdf_e2e.py +0 -0
  68. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_ppr_search.py +0 -0
  69. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_ranking.py +0 -0
  70. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_reconstruction.py +0 -0
  71. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_reserved.py +0 -0
  72. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_roundup_cli.py +0 -0
  73. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_router.py +0 -0
  74. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_router_misc.py +0 -0
  75. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_sanitize.py +0 -0
  76. {okfgraph-0.2.7 → okfgraph-0.2.12}/tests/test_search_browser.py +0 -0
  77. {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.7
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: okf-embed>=0.1
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 `okf-embed` Rust wheel (ONNX Runtime, no torch / transformers /
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 `okf-embed` Rust wheel; last-token pooling, Matryoshka truncation (32–1024, default 512); omni model (`…-omni-small-retrieval`) lazy-loaded for images only |
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 (okf-embed ships platform wheels; published)
79
+ pip install "okfgraph[pdf,omni]" # PyPI (embeddings come from the published `embroider` wheels)
80
80
  ```
81
81
 
82
- Or from source with `uv` (also builds the `okf-embed` Rust wheel from
83
- `rust/okf-embed` via `[tool.uv.sources]`):
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, okf-embed, onnxruntime, mordant, mcp, …
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`, `okf-embed`,
94
- `onnxruntime==1.29.0` (one pinned ORT binary shared by bobine + okf-embed;
95
- `ORT_DYLIB_PATH`-overridable), `mordant`, `mcp>=2.0`, `pydantic`, `pyyaml`,
96
- `numpy`, `python-frontmatter`, `fasteners`. There is no torch, no
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 okf-embed (Jina v5, ORT) — tokenize, last-token
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 + okf-embed.
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
- cd rust/okf-embed && cargo test --locked # Rust unit tests (pure, no model)
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`, `okf-embed` (built from `rust/okf-embed`),
344
- `onnxruntime==1.29.0`, `mordant>=0.9`, `mcp>=2.0`, `pydantic>=2`, `pyyaml>=6`,
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 `okf-embed` Rust wheel (ONNX Runtime, no torch / transformers /
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 `okf-embed` Rust wheel; last-token pooling, Matryoshka truncation (32–1024, default 512); omni model (`…-omni-small-retrieval`) lazy-loaded for images only |
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 (okf-embed ships platform wheels; published)
52
+ pip install "okfgraph[pdf,omni]" # PyPI (embeddings come from the published `embroider` wheels)
53
53
  ```
54
54
 
55
- Or from source with `uv` (also builds the `okf-embed` Rust wheel from
56
- `rust/okf-embed` via `[tool.uv.sources]`):
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, okf-embed, onnxruntime, mordant, mcp, …
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`, `okf-embed`,
67
- `onnxruntime==1.29.0` (one pinned ORT binary shared by bobine + okf-embed;
68
- `ORT_DYLIB_PATH`-overridable), `mordant`, `mcp>=2.0`, `pydantic`, `pyyaml`,
69
- `numpy`, `python-frontmatter`, `fasteners`. There is no torch, no
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 okf-embed (Jina v5, ORT) — tokenize, last-token
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 + okf-embed.
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
- cd rust/okf-embed && cargo test --locked # Rust unit tests (pure, no model)
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`, `okf-embed` (built from `rust/okf-embed`),
317
- `onnxruntime==1.29.0`, `mordant>=0.9`, `mcp>=2.0`, `pydantic>=2`, `pyyaml>=6`,
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
- def resolve_ort_dylib() -> Optional[str]:
16
- """Point ``ORT_DYLIB_PATH`` at the pip-installed ORT build when unset.
16
+ _ORT_MODULE_NAMES = ("onnxruntime", "onnxruntime-gpu")
17
17
 
18
- Both bobine and okf-embed load ONNX Runtime dynamically; sharing one
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
- if os.environ.get("ORT_DYLIB_PATH"):
24
- return os.environ["ORT_DYLIB_PATH"]
25
- try:
26
- import onnxruntime
27
- dll = Path(str(onnxruntime.__file__)).parent / "capi" / "onnxruntime.dll"
28
- if dll.exists():
29
- os.environ["ORT_DYLIB_PATH"] = str(dll)
30
- return str(dll)
31
- except ImportError:
32
- pass
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 okf_embed wheel (Jina v5 via ORT):
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.