diff --git a/CHANGELOG.md b/CHANGELOG.md index 6933cd62..0200bb5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- **spaCy Runtime Fallback for NER Benchmarks**: + - Hardened `NERExtractor` spaCy initialization so installed-but-broken spaCy environments no longer crash during extractor construction. + - Updated ML entity extraction fallback behavior to catch runtime spaCy initialization failures, not just missing-model errors. + - Added regression coverage for the "spaCy present but unusable at runtime" initialization path. + ### Added - **Configurable LLM Retry Logic**: - Exposed `max_retries` parameter in `NERExtractor`, `RelationExtractor`, `TripletExtractor` and low-level extraction methods (`extract_entities_llm`, `extract_relations_llm`, `extract_triplets_llm`). diff --git a/semantica/semantic_extract/methods.py b/semantica/semantic_extract/methods.py index 2418bb0a..9038287e 100644 --- a/semantica/semantic_extract/methods.py +++ b/semantica/semantic_extract/methods.py @@ -683,6 +683,16 @@ def extract_entities_ml( "spaCy model not available, falling back to pattern extraction" ) return extract_entities_pattern(text, **kwargs) + except Exception as exc: + logger.warning( + f"spaCy model failed to initialize ({exc}), falling back to pattern extraction" + ) + return extract_entities_pattern(text, **kwargs) + except Exception as exc: + logger.warning( + f"spaCy model {model} failed to initialize ({exc}), falling back to pattern extraction" + ) + return extract_entities_pattern(text, **kwargs) doc = nlp(text) entities = [] diff --git a/semantica/semantic_extract/ner_extractor.py b/semantica/semantic_extract/ner_extractor.py index a6496636..ac6fea43 100644 --- a/semantica/semantic_extract/ner_extractor.py +++ b/semantica/semantic_extract/ner_extractor.py @@ -154,6 +154,10 @@ class NERExtractor: self.logger.warning( f"spaCy model {self.model_name} not found. ML method will fallback." ) + except Exception as exc: + self.logger.warning( + f"spaCy model {self.model_name} failed to initialize ({exc}). ML method will fallback." + ) def extract(self, text: Union[str, List[Dict[str, Any]], List[str]], pipeline_id: Optional[str] = None, **kwargs) -> Union[List[Entity], List[List[Entity]]]: """ diff --git a/tests/test_ner_configurations.py b/tests/test_ner_configurations.py index 1a395c07..58d6b62f 100644 --- a/tests/test_ner_configurations.py +++ b/tests/test_ner_configurations.py @@ -101,6 +101,16 @@ class TestNERConfigurations(unittest.TestCase): self.assertEqual(entities[0].metadata["extraction_method"], "ml") self.assertEqual(entities[0].metadata["model"], "en_core_web_trf") + @patch('semantica.semantic_extract.ner_extractor.spacy') + def test_ner_ml_init_falls_back_when_spacy_runtime_is_broken(self, mock_spacy): + """Test NER init does not crash when spaCy is installed but unusable at runtime.""" + mock_spacy.load.side_effect = RuntimeError("ConfigSchemaNlp is not fully defined") + + with patch('semantica.semantic_extract.ner_extractor.SPACY_AVAILABLE', True): + extractor = NERExtractor(method="ml", model="en_core_web_sm") + + self.assertIsNone(extractor.nlp) + def test_ner_regex_config(self): """Test NER with Regex configuration""" print("\nTesting NER with Regex configuration...")