okfgraph 0.2.4__tar.gz → 0.2.6__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.4 → okfgraph-0.2.6}/PKG-INFO +1 -1
- {okfgraph-0.2.4 → okfgraph-0.2.6}/README.md +4 -3
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/cli.py +6 -3
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/import_.py +34 -20
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph.egg-info/PKG-INFO +1 -1
- {okfgraph-0.2.4 → okfgraph-0.2.6}/pyproject.toml +1 -1
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_chunking.py +17 -16
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_cli.py +8 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_router.py +10 -6
- {okfgraph-0.2.4 → okfgraph-0.2.6}/LICENSE +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/__init__.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/__init__.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/converters.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/delta.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/diff.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/doctor.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/embedding.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/export.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/image_assets.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/ingest.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/links.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/lint.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/purge.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/ranking.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/schema.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/search.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/config.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/images.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/mcp_server.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/models.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/router.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/security.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/tools.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph.egg-info/SOURCES.txt +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph.egg-info/dependency_links.txt +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph.egg-info/entry_points.txt +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph.egg-info/requires.txt +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph.egg-info/top_level.txt +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/setup.cfg +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_chunk_search.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_config.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_converter.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_delta.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_diff.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_directory_hash.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_doctor.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_export_compliance.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_gpu_integration.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_graph_enrichment.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_images.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_ingest.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_ingest_tool.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_integration.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_lint.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_logging.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_mcp_server.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_models.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_obsidian.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_okf_ingest_tool.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_parity.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_pdf_e2e.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_ppr_search.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_ranking.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_reconstruction.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_reserved.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_roundup_cli.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_router_misc.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_rust_backend.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_rust_e2e.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_sanitize.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_search_browser.py +0 -0
- {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_security.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# OKFgraph 0.2.
|
|
1
|
+
# OKFgraph 0.2.5
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/okfgraph/)
|
|
4
4
|
[](https://www.python.org/)
|
|
@@ -49,7 +49,7 @@ 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 (okf-embed ships platform wheels; published)
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Or from source with `uv` (also builds the `okf-embed` Rust wheel from
|
|
@@ -273,6 +273,7 @@ Key design decisions:
|
|
|
273
273
|
```bash
|
|
274
274
|
uv run --project . pytest tests/ -q # full suite (model loads; takes a while)
|
|
275
275
|
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)
|
|
276
277
|
```
|
|
277
278
|
|
|
278
279
|
Golden fixtures under `tests/fixtures/` (`ppr_graph`, `diff_a`/`diff_b`,
|
|
@@ -297,7 +298,7 @@ okfgraph/
|
|
|
297
298
|
│ ├── images.py # IngestMode (text|optional|omni), planning helpers
|
|
298
299
|
│ ├── security.py # SSRF/domain guards for remote images
|
|
299
300
|
│ ├── tools.py # legacy tool definitions (superseded by mcp_server)
|
|
300
|
-
│ └── components/ # ranking, links, search, import_, export, diff, doctor,
|
|
301
|
+
│ └── components/ # ranking, links, search, lint, import_, export, diff, doctor,
|
|
301
302
|
│ # embedding, converters, image_assets, delta, purge, schema, ingest
|
|
302
303
|
├── rust/okf-embed/ # Jina v5 Rust loader (ort, CUDA-opportunistic) + Python wheel
|
|
303
304
|
├── skills/ # okfgraph-mcp, okfgraph-cli, okfgraph-ingest (harness-neutral source)
|
|
@@ -695,7 +695,10 @@ def _ingest(args):
|
|
|
695
695
|
"""
|
|
696
696
|
logger = logging.getLogger("cli")
|
|
697
697
|
router = _router(args)
|
|
698
|
-
kind = getattr(args, "kind",
|
|
698
|
+
kind = getattr(args, "kind", None)
|
|
699
|
+
if kind not in ("md", "pdf", "thoughts"):
|
|
700
|
+
print("[ERROR] --kind is required: md|pdf|thoughts")
|
|
701
|
+
return
|
|
699
702
|
tags = args.tags.split(",") if getattr(args, "tags", None) else None
|
|
700
703
|
if kind == "md":
|
|
701
704
|
md_file = getattr(args, "md_file", None)
|
|
@@ -1171,8 +1174,8 @@ def build_parser():
|
|
|
1171
1174
|
p = sub.add_parser("ingest", help="Add content: markdown file, PDF, or thoughts")
|
|
1172
1175
|
_add_global(p)
|
|
1173
1176
|
_add_logging_flags(p)
|
|
1174
|
-
p.add_argument("--kind",
|
|
1175
|
-
help="What to ingest
|
|
1177
|
+
p.add_argument("--kind", required=True, choices=["md", "pdf", "thoughts"],
|
|
1178
|
+
help="What to ingest")
|
|
1176
1179
|
p.add_argument("--md-file", default=None, help="Markdown file (--kind md)")
|
|
1177
1180
|
p.add_argument("--pdf-file", default=None, help="PDF file (--kind pdf)")
|
|
1178
1181
|
p.add_argument("--thoughts", default=None, help="Raw reasoning text (--kind thoughts)")
|
|
@@ -145,6 +145,36 @@ class ImportManager:
|
|
|
145
145
|
self.image_mgr = image_mgr
|
|
146
146
|
self.purge_mgr = purge_mgr
|
|
147
147
|
|
|
148
|
+
def _merge_directory_contains(self, parent_id: str, child_id: str):
|
|
149
|
+
"""Create a Directory→Directory CONTAINS edge without crashing Ladybug.
|
|
150
|
+
|
|
151
|
+
Ladybug 0.20.3 segfaults when one Cypher statement combines an
|
|
152
|
+
existing-node ``MERGE``, a new-node ``MERGE``, and a relationship
|
|
153
|
+
``MERGE`` on a long-lived connection. Separate idempotent statements
|
|
154
|
+
preserve the same graph while avoiding that planner path.
|
|
155
|
+
"""
|
|
156
|
+
self.conn.execute("MERGE (p:Directory {id: $id})", {"id": parent_id})
|
|
157
|
+
self.conn.execute("MERGE (d:Directory {id: $id})", {"id": child_id})
|
|
158
|
+
self.conn.execute("""
|
|
159
|
+
MATCH (p:Directory {id: $parent}), (d:Directory {id: $child})
|
|
160
|
+
MERGE (p)-[:CONTAINS]->(d)
|
|
161
|
+
""", {"parent": parent_id, "child": child_id})
|
|
162
|
+
|
|
163
|
+
def _merge_concept_contains(self, parent_id: str, child_id: str):
|
|
164
|
+
"""Create a Directory→Concept CONTAINS edge without crashing Ladybug.
|
|
165
|
+
|
|
166
|
+
See ``_merge_directory_contains``: a single three-clause ``MERGE`` can
|
|
167
|
+
segfault ladybug 0.20.3 after matching an existing Directory and
|
|
168
|
+
creating the next sibling Concept. The separate statements below are
|
|
169
|
+
semantically equivalent.
|
|
170
|
+
"""
|
|
171
|
+
self.conn.execute("MERGE (d:Directory {id: $id})", {"id": parent_id})
|
|
172
|
+
self.conn.execute("MERGE (c:Concept {id: $id})", {"id": child_id})
|
|
173
|
+
self.conn.execute("""
|
|
174
|
+
MATCH (d:Directory {id: $parent}), (c:Concept {id: $child})
|
|
175
|
+
MERGE (d)-[:CONTAINS]->(c)
|
|
176
|
+
""", {"parent": parent_id, "child": child_id})
|
|
177
|
+
|
|
148
178
|
def _batch_build_directories(self, cids: List[str]):
|
|
149
179
|
"""Build directory hierarchy for a batch of concept IDs.
|
|
150
180
|
|
|
@@ -166,11 +196,7 @@ class ImportManager:
|
|
|
166
196
|
for d in sorted_dirs:
|
|
167
197
|
parent = "/".join(d.split("/")[:-1]) if "/" in d else None
|
|
168
198
|
if parent and parent in dir_paths:
|
|
169
|
-
self.
|
|
170
|
-
MERGE (p:Directory {id: $parent})
|
|
171
|
-
MERGE (d:Directory {id: $child})
|
|
172
|
-
MERGE (p)-[:CONTAINS]->(d)
|
|
173
|
-
""", {"parent": parent, "child": d})
|
|
199
|
+
self._merge_directory_contains(parent, d)
|
|
174
200
|
elif parent:
|
|
175
201
|
# Parent is root (not a directory node)
|
|
176
202
|
self.conn.execute("""
|
|
@@ -186,11 +212,7 @@ class ImportManager:
|
|
|
186
212
|
parts = cid.split("/")
|
|
187
213
|
if len(parts) > 1:
|
|
188
214
|
parent_dir = "/".join(parts[:-1])
|
|
189
|
-
self.
|
|
190
|
-
MERGE (d:Directory {id: $parent})
|
|
191
|
-
MERGE (c:Concept {id: $child})
|
|
192
|
-
MERGE (d)-[:CONTAINS]->(c)
|
|
193
|
-
""", {"parent": parent_dir, "child": cid})
|
|
215
|
+
self._merge_concept_contains(parent_dir, cid)
|
|
194
216
|
|
|
195
217
|
|
|
196
218
|
def _load_link_index(self):
|
|
@@ -867,17 +889,9 @@ class ImportManager:
|
|
|
867
889
|
parent = "/".join(path_parts[:i])
|
|
868
890
|
child = "/".join(path_parts[: i + 1])
|
|
869
891
|
if i == len(path_parts) - 1:
|
|
870
|
-
self.
|
|
871
|
-
MERGE (d:Directory {id: $parent})
|
|
872
|
-
MERGE (c:Concept {id: $child})
|
|
873
|
-
MERGE (d)-[:CONTAINS]->(c)
|
|
874
|
-
""", {"parent": parent, "child": child})
|
|
892
|
+
self._merge_concept_contains(parent, child)
|
|
875
893
|
else:
|
|
876
|
-
self.
|
|
877
|
-
MERGE (p:Directory {id: $parent})
|
|
878
|
-
MERGE (d:Directory {id: $child})
|
|
879
|
-
MERGE (p)-[:CONTAINS]->(d)
|
|
880
|
-
""", {"parent": parent, "child": child})
|
|
894
|
+
self._merge_directory_contains(parent, child)
|
|
881
895
|
|
|
882
896
|
# NOTE: link extraction intentionally lives outside _insert_concept
|
|
883
897
|
# (callers run _extract_links_for_concept after the node exists), so
|
|
@@ -449,26 +449,27 @@ class TestPhase5_CLI:
|
|
|
449
449
|
actual commands with real data and verify output formatting.
|
|
450
450
|
"""
|
|
451
451
|
|
|
452
|
-
def
|
|
452
|
+
def test_removed_granular_commands_gone(self, capsys):
|
|
453
|
+
# search-chunks/chunks/reconstruct were folded into search/read in the
|
|
454
|
+
# 0.2.0 16→5 consolidation; assert argparse rejects them outright.
|
|
453
455
|
from okfgraph.cli import build_parser
|
|
454
456
|
parser = build_parser()
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
assert args.command == "chunks"
|
|
464
|
-
assert args.concept_id == "some_id"
|
|
465
|
-
|
|
466
|
-
def test_cli_has_reconstruct_command(self):
|
|
457
|
+
for dead in ("search-chunks", "chunks", "reconstruct"):
|
|
458
|
+
with pytest.raises(SystemExit):
|
|
459
|
+
parser.parse_args([dead, "test"])
|
|
460
|
+
capsys.readouterr() # swallow argparse usage noise
|
|
461
|
+
|
|
462
|
+
def test_replacement_verbs_cover_removed_commands(self):
|
|
463
|
+
# search --target chunks ≙ search-chunks; read --include chunks ≙ chunks;
|
|
464
|
+
# read --include document ≙ reconstruct.
|
|
467
465
|
from okfgraph.cli import build_parser
|
|
468
466
|
parser = build_parser()
|
|
469
|
-
args = parser.parse_args(["
|
|
470
|
-
assert args.command == "
|
|
471
|
-
|
|
467
|
+
args = parser.parse_args(["search", "test", "--target", "chunks"])
|
|
468
|
+
assert args.command == "search" and args.target == "chunks"
|
|
469
|
+
args = parser.parse_args(["read", "--include", "chunks", "some_id"])
|
|
470
|
+
assert args.concept_id == "some_id" and args.include == "chunks"
|
|
471
|
+
args = parser.parse_args(["read", "--include", "document", "some_id"])
|
|
472
|
+
assert args.concept_id == "some_id" and args.include == "document"
|
|
472
473
|
|
|
473
474
|
def test_cli_has_chunking_options(self):
|
|
474
475
|
from okfgraph.cli import build_parser
|
|
@@ -74,6 +74,14 @@ class TestCLIHelp:
|
|
|
74
74
|
assert result.returncode == 0
|
|
75
75
|
assert "--kind" in result.stdout
|
|
76
76
|
|
|
77
|
+
def test_ingest_kind_required(self):
|
|
78
|
+
result = subprocess.run(
|
|
79
|
+
[sys.executable, "-m", "okfgraph.cli", "ingest", "--md-file", "note.md"],
|
|
80
|
+
capture_output=True, text=True,
|
|
81
|
+
)
|
|
82
|
+
assert result.returncode == 2
|
|
83
|
+
assert "--kind" in result.stderr
|
|
84
|
+
|
|
77
85
|
def test_export_help(self):
|
|
78
86
|
result = subprocess.run(
|
|
79
87
|
[sys.executable, "-m", "okfgraph.cli", "export", "--help"],
|
|
@@ -166,17 +166,21 @@ class TestDeviceSelection:
|
|
|
166
166
|
assert r.device == "cpu"
|
|
167
167
|
assert r.encoder.used_cuda is False
|
|
168
168
|
|
|
169
|
-
def test_device_cuda_uses_gpu_when_available(self, tmp_path,
|
|
169
|
+
def test_device_cuda_uses_gpu_when_available(self, tmp_path, caplog):
|
|
170
|
+
# The fallback warning travels via logging (bound to the real stderr
|
|
171
|
+
# fd at handler creation), so capsys can never see it — assert on the
|
|
172
|
+
# log record instead. Deterministic on GPU and CPU machines alike.
|
|
173
|
+
import logging
|
|
170
174
|
from okfgraph.router import OKFRouter
|
|
171
|
-
|
|
175
|
+
with caplog.at_level(logging.WARNING, logger="okfgraph.router"):
|
|
176
|
+
r = OKFRouter(db_path=str(tmp_path / "test.db"), bundle_root=str(tmp_path), device="cuda")
|
|
172
177
|
assert r.device == "cuda"
|
|
173
|
-
err = capsys.readouterr().err
|
|
174
178
|
if r.encoder.used_cuda:
|
|
175
179
|
# Happy path: GPU active, no fallback warning.
|
|
176
|
-
assert "CUDA"
|
|
180
|
+
assert not any("CUDA" in m for m in caplog.messages)
|
|
177
181
|
else:
|
|
178
|
-
# CPU fallback: the
|
|
179
|
-
assert "CUDA" in
|
|
182
|
+
# CPU fallback: the router warns about the missing EP.
|
|
183
|
+
assert any("no CUDA execution provider" in m for m in caplog.messages)
|
|
180
184
|
|
|
181
185
|
def test_device_cuda_warning_only_once(self, tmp_path, capsys):
|
|
182
186
|
from okfgraph.router import OKFRouter
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|