mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Fix semantic extract spaCy fallback review issues
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# PR Title
|
||||
Harden spaCy NER Fallback in Semantic Extract
|
||||
|
||||
## Summary
|
||||
This PR fixes a runtime failure in Semantic Extract when spaCy is installed but not actually usable at runtime, such as Python 3.12 / Pydantic v2 environments where `spacy.load("en_core_web_sm")` fails during config validation.
|
||||
|
||||
Instead of crashing during `NERExtractor(method="ml")` initialization or ML entity extraction, Semantica now logs the failure and falls back cleanly to non-ML extraction behavior.
|
||||
|
||||
## What Changed
|
||||
|
||||
### spaCy Initialization Hardening
|
||||
Updated `semantica/semantic_extract/ner_extractor.py` so `NERExtractor` no longer crashes if:
|
||||
|
||||
- spaCy is importable
|
||||
- the configured model exists
|
||||
- but `spacy.load(...)` fails at runtime for reasons other than missing files
|
||||
|
||||
This now degrades gracefully by leaving `self.nlp = None` and allowing fallback behavior.
|
||||
|
||||
### ML Extraction Fallback Hardening
|
||||
Updated `semantica/semantic_extract/methods.py` so `extract_entities_ml()` now catches:
|
||||
|
||||
- missing spaCy model errors
|
||||
- generic spaCy runtime initialization failures
|
||||
|
||||
If spaCy cannot initialize, extraction falls back to pattern-based extraction instead of raising.
|
||||
|
||||
### Regression Test
|
||||
Added a regression test in `tests/test_ner_configurations.py` covering the case where:
|
||||
|
||||
- spaCy is available
|
||||
- `spacy.load(...)` raises a runtime exception
|
||||
- `NERExtractor(method="ml")` still initializes safely
|
||||
|
||||
### Changelog
|
||||
Added an `Unreleased` changelog entry documenting the spaCy runtime fallback fix.
|
||||
|
||||
## Why This Matters
|
||||
This fixes benchmark and CI instability caused by spaCy runtime incompatibilities outside Semantica’s control.
|
||||
|
||||
It ensures Semantic Extract remains resilient when spaCy is present in the environment but broken due to dependency mismatches.
|
||||
|
||||
## Validation
|
||||
Tested with:
|
||||
|
||||
```bash
|
||||
pytest tests/test_ner_configurations.py -q -k "spacy_runtime_is_broken"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```bash
|
||||
1 passed
|
||||
```
|
||||
|
||||
Note:
|
||||
The benchmark failure path was fixed directly, but the local `semantic-extract` branch did not contain the benchmark file path used in CI, so only the targeted regression path was verified locally.
|
||||
|
||||
## Files Changed
|
||||
|
||||
- `semantica/semantic_extract/ner_extractor.py`
|
||||
- `semantica/semantic_extract/methods.py`
|
||||
- `tests/test_ner_configurations.py`
|
||||
- `CHANGELOG.md`
|
||||
@@ -685,12 +685,15 @@ def extract_entities_ml(
|
||||
return extract_entities_pattern(text, **kwargs)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
f"spaCy model failed to initialize ({exc}), falling back to pattern extraction"
|
||||
"spaCy fallback triggered because the default model failed to initialize. Falling back to pattern extraction.",
|
||||
exc_info=True,
|
||||
)
|
||||
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"
|
||||
"spaCy model %s failed to initialize, falling back to pattern extraction.",
|
||||
model,
|
||||
exc_info=True,
|
||||
)
|
||||
return extract_entities_pattern(text, **kwargs)
|
||||
|
||||
|
||||
@@ -147,6 +147,7 @@ class NERExtractor:
|
||||
|
||||
# Initialize spaCy model if ML method is used
|
||||
self.nlp = None
|
||||
self._ml_runtime_usable = True
|
||||
if "ml" in self.method and SPACY_AVAILABLE:
|
||||
try:
|
||||
self.nlp = spacy.load(self.model_name)
|
||||
@@ -155,8 +156,11 @@ class NERExtractor:
|
||||
f"spaCy model {self.model_name} not found. ML method will fallback."
|
||||
)
|
||||
except Exception as exc:
|
||||
self._ml_runtime_usable = False
|
||||
self.logger.warning(
|
||||
f"spaCy model {self.model_name} failed to initialize ({exc}). ML method will fallback."
|
||||
"spaCy model %s failed to initialize and will be disabled for this extractor instance. ML method will fallback.",
|
||||
self.model_name,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
def extract(self, text: Union[str, List[Dict[str, Any]], List[str]], pipeline_id: Optional[str] = None, **kwargs) -> Union[List[Entity], List[List[Entity]]]:
|
||||
@@ -346,6 +350,7 @@ class NERExtractor:
|
||||
methods = options.get("method", self.method)
|
||||
if isinstance(methods, str):
|
||||
methods = [methods]
|
||||
methods = self._filter_unusable_methods(methods)
|
||||
|
||||
min_confidence = options.get("min_confidence", self.min_confidence)
|
||||
entity_types = options.get("entity_types", self.entity_types)
|
||||
@@ -461,6 +466,24 @@ class NERExtractor:
|
||||
)
|
||||
raise
|
||||
|
||||
def _filter_unusable_methods(self, methods: List[str]) -> List[str]:
|
||||
"""Skip ML dispatch after a known spaCy runtime initialization failure."""
|
||||
filtered = []
|
||||
skipped_ml = False
|
||||
|
||||
for method_name in methods:
|
||||
if method_name in {"ml", "spacy"} and not self._ml_runtime_usable:
|
||||
skipped_ml = True
|
||||
continue
|
||||
filtered.append(method_name)
|
||||
|
||||
if skipped_ml:
|
||||
self.logger.debug(
|
||||
"Skipping ML entity extraction because spaCy runtime initialization previously failed for this extractor."
|
||||
)
|
||||
|
||||
return filtered
|
||||
|
||||
def _vote_entities(
|
||||
self, results: List[List[Entity]], threshold: float = 0.5
|
||||
) -> List[Entity]:
|
||||
|
||||
@@ -110,6 +110,32 @@ class TestNERConfigurations(unittest.TestCase):
|
||||
extractor = NERExtractor(method="ml", model="en_core_web_sm")
|
||||
|
||||
self.assertIsNone(extractor.nlp)
|
||||
self.assertFalse(extractor._ml_runtime_usable)
|
||||
|
||||
@patch('semantica.semantic_extract.ner_extractor.spacy')
|
||||
@patch('semantica.semantic_extract.methods.get_entity_method')
|
||||
@patch('semantica.semantic_extract.methods.spacy')
|
||||
def test_ner_ml_runtime_failure_disables_repeated_ml_load_attempts(
|
||||
self,
|
||||
mock_methods_spacy,
|
||||
mock_get_method,
|
||||
mock_init_spacy,
|
||||
):
|
||||
"""Test degraded ML mode skips repeated spaCy load attempts after init failure."""
|
||||
mock_init_spacy.load.side_effect = RuntimeError("ConfigSchemaNlp is not fully defined")
|
||||
mock_ml_method = MagicMock(return_value=[])
|
||||
mock_get_method.side_effect = lambda name: mock_ml_method if name == "ml" else (lambda *_args, **_kwargs: [])
|
||||
|
||||
with patch('semantica.semantic_extract.ner_extractor.SPACY_AVAILABLE', True):
|
||||
extractor = NERExtractor(method=["ml", "pattern"], model="en_core_web_sm")
|
||||
|
||||
entities = extractor.extract_entities(self.text)
|
||||
|
||||
self.assertFalse(extractor._ml_runtime_usable)
|
||||
self.assertEqual(mock_init_spacy.load.call_count, 1)
|
||||
self.assertEqual(mock_methods_spacy.load.call_count, 0)
|
||||
self.assertEqual(mock_ml_method.call_count, 0)
|
||||
self.assertIsInstance(entities, list)
|
||||
|
||||
def test_ner_regex_config(self):
|
||||
"""Test NER with Regex configuration"""
|
||||
|
||||
Reference in New Issue
Block a user