From a8194dfc60a17de99e926f153bc7c8fa3f3a8598 Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Mon, 17 Aug 2026 13:16:17 +0530 Subject: [PATCH] fix(split): catch broken-runtime spaCy failures in SemanticChunker SemanticChunker.__init__ only caught OSError around load_spacy_model(), while NERExtractor's identical call (fixed earlier in this PR) also catches generic Exception for a model that is installed but fails at runtime. Bring SemanticChunker in line so a broken spaCy config degrades to fallback chunking instead of crashing __init__. Adds a regression test mirroring the existing NERExtractor case, and a CHANGELOG entry for #998/#1042. --- CHANGELOG.md | 8 ++++++++ semantica/split/semantic_chunker.py | 7 +++++++ tests/split/test_spacy_model_cache.py | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78212679..f670cb22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **`split`/chunking paths bypassed the centralized spaCy model cache, reloading the model on every call** (#1042, closes #998) by @Accute9, reviewed by @Sameer6305 + - `semantica/split/methods.py`'s `split_by_sentences()` and `semantica/split/semantic_chunker.py`'s `SemanticChunker.__init__` each called `spacy.load()` directly instead of reusing the process-level cache added in #889/`semantic_extract/methods.py`'s `load_spacy_model()` — every call/construction re-paid the ~120ms model-load cost independently of `NERExtractor`, which already used the cache + - Both now route through `load_spacy_model()`, sharing one cached `Language` instance per model name across `split_by_sentences()`, `SemanticChunker`, and `NERExtractor`; a missing model still falls back to regex/paragraph chunking without poisoning the cache for a later successful load + - **Fixed during review** (@Sameer6305): `NERExtractor.__init__()` still had a direct `spacy.load()` call site with the same cache-bypass issue, outside the two files named in #998 but sharing the same root cause; routed through the cache alongside stale test patch targets and a strengthened cache-configuration assertion + - **Fixed during review** (@KaifAhmad1): `SemanticChunker.__init__` only caught `OSError` around `load_spacy_model()`, while the sibling fix to `NERExtractor` in this same PR added a broader `except Exception` for a model that is installed but fails at runtime (e.g. a config incompatible with the installed spaCy version). A broken-but-present model crashed `SemanticChunker()` outright instead of degrading to fallback chunking like every other path in this PR. Added the matching `except Exception` branch, leaving `self.nlp` as `None`; new `test_semantic_chunker_falls_back_when_spacy_runtime_is_broken` mirrors the existing `NERExtractor` regression test for the same scenario + - New `tests/split/test_spacy_model_cache.py`: cache reuse across repeated calls/instances, shared cache between `split_by_sentences()`/`SemanticChunker`/`NERExtractor`, distinct model names loading separately, missing-model fallback without poisoning the cache, and the broken-runtime fallback added above + - `pytest tests/split/test_spacy_model_cache.py tests/split/test_splitter.py tests/split/test_chunkers.py`: all passing (3 pre-existing, unrelated `tests/test_ner_configurations.py` failures confirmed present on `main` before this PR) + - **`export_yaml` raised a raw `AttributeError` on list input, silently wrote empty exports for unrecognized dict keys, and graph payloads were reconciled differently by every exporter** (#958, closes #956, #952, #953) by @pravit-amp, reviewed by @Sameer6305 - Graph payloads circulate under two vocabularies, `entities`/`relationships` and `nodes`/`edges`, and each exporter reconciled them locally with a different idiom — `LPGExporter` in particular dropped every entity whenever `nodes` was present but empty, the exact shape `JSONExporter` emits. A new `normalize_graph_payload()` in `utils/helpers.py` centralizes that decision once, adopted by `LPGExporter`, `ArangoAQLExporter`, `Neo4jCSVExporter`, and both YAML exporters; `ContextGraph.to_dict()` now round-trips through YAML correctly as a result - `export_yaml(records, path)` on a bare list previously failed with `AttributeError` from inside the exporter; it and the other YAML methods now reject non-mapping input with an actionable `ProcessingError` naming the expected keys, since these formats distinguish entities/relationships/triplets and guessing which one a list represents would mislabel the records diff --git a/semantica/split/semantic_chunker.py b/semantica/split/semantic_chunker.py index 2945bbd5..fc6fa6aa 100644 --- a/semantica/split/semantic_chunker.py +++ b/semantica/split/semantic_chunker.py @@ -86,6 +86,13 @@ class SemanticChunker: self.logger.warning( f"spaCy model {model_name} not found. Using fallback chunking." ) + except Exception: + self.logger.warning( + "spaCy model %s failed to initialize and will be disabled " + "for this chunker instance. Using fallback chunking.", + model_name, + exc_info=True, + ) def chunk(self, text: str, **options) -> List[Chunk]: """ diff --git a/tests/split/test_spacy_model_cache.py b/tests/split/test_spacy_model_cache.py index de21e433..00012030 100644 --- a/tests/split/test_spacy_model_cache.py +++ b/tests/split/test_spacy_model_cache.py @@ -177,6 +177,24 @@ class TestSpacyModelCache: ) assert chunker2.nlp is not None + def test_semantic_chunker_falls_back_when_spacy_runtime_is_broken( + self, monkeypatch + ): + """A spaCy model that is installed but unusable at runtime (e.g. a + config incompatible with the installed spaCy version) must degrade + SemanticChunker to fallback chunking, not crash __init__ -- mirrors + TestNERExtractorSpacyModelCache's equivalent broken-runtime test. + """ + + def broken_load(name, **_kwargs): + raise RuntimeError("ConfigSchemaNlp is not fully defined") + + monkeypatch.setattr(se_methods, "spacy", _fake_spacy(broken_load)) + + chunker = semantic_chunker.SemanticChunker() + + assert chunker.nlp is None + class TestNERExtractorSpacyModelCache: """NERExtractor(method="ml") must reuse the centralized cache in