Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file.
- Added `PhUmidRecognizer` for Philippine Unified Multi-Purpose ID (UMID/CRN) numbers in dashed and plain 12-digit formats; disabled by default (#2045) (Thanks @Surya-5555)

#### Fixed
- Prevented `SlimSpacyNlpEngine` from registering an inert spaCy recognizer and advertising unsupported spaCy NER entities. The new `NlpEngine.has_ner` property defaults to `True`; engines without native NER output, including `SlimSpacyNlpEngine` and `NoOpNlpEngine`, explicitly return `False` so Presidio skips NLP recognizer registration.
- `PhoneRecognizer.DEFAULT_SUPPORTED_REGIONS` used `"UK"`, which is not a valid `phonenumbers` (libphonenumber) region code — region codes are ISO 3166-1 alpha-2, where the United Kingdom is `"GB"`. The `"UK"` entry was a no-op, so UK numbers in national/local format (e.g. `020 7946 0958`) were never detected by default; only international-format `+44 …` numbers matched, because they carry the country code and match under any region. Replaced `"UK"` with `"GB"`.
- Language model recognizers (`BasicLangExtractRecognizer`, `AzureOpenAILangExtractRecognizer`) configured in a recognizer registry YAML now honour `config_path` (and other recognizer-specific kwargs). Previously these entries were validated by the strict `PredefinedRecognizerConfig` schema, which has no `config_path` field and does not allow extra keys, so `config_path` was silently dropped and the recognizer fell back to its bundled default model configuration. Added a `LangExtractRecognizerConfig` model (`extra="allow"`) and registered both recognizer class names in `CONFIG_MODEL_MAP`.
- `BasicLangExtractRecognizer` now honours values under `langextract.model.provider.language_model_params` (including `timeout` and `num_ctx`). Previously these were silently dropped because `langextract.extract()` ignores its `language_model_params` argument when a pre-built `ModelConfig` is passed via `config=`, causing Ollama-backed recognizers to fall back to langextract's 120s default regardless of the configured timeout. The recognizer now merges `language_model_params` into `ModelConfig.provider_kwargs`, which is the path that reaches the provider constructor. Explicit entries under `provider.kwargs:` still take precedence. Also fixed a `TypeError` when `kwargs:` or `language_model_params:` is `null` in the YAML. (#1943, Thanks @lsternlicht)
Expand Down
4 changes: 2 additions & 2 deletions docs/analyzer/analyzer_engine_provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ The default configuration of `AnalyzerEngine` is defined in the following files:
In general, recognizers that are not added to the configuration would not be created, with one exception.

### Enabling/Disabling the NLP recognizer
One exception to this is the recognizer which extracts the `NlpEngine` entities (e.g. `SpacyRecognizer` when the `NlpEngine` is `SpacyNlpEngine`; `TransformersRecognizer` when the engine is `TransformersNlpEngine` and `StanzaRecognizer` when the engine is `StanzaNlpEngine`).
One exception to this is the recognizer which extracts the `NlpEngine` entities when the engine provides native NER output (e.g. `SpacyRecognizer` when the `NlpEngine` is `SpacyNlpEngine`; `TransformersRecognizer` when the engine is `TransformersNlpEngine` and `StanzaRecognizer` when the engine is `StanzaNlpEngine`).

Recognizers (including the NLP recognizer) could be disabled by defining `enabled=false` in the YAML configuration. For example:
Recognizers (including the NLP recognizer) could be disabled by defining `enabled: false` in the YAML configuration. For example:
```yaml
recognizer_registry:
global_regex_flags: 26
Expand Down
17 changes: 16 additions & 1 deletion docs/analyzer/customizing_nlp_models.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,26 @@ In addition, other types of NLP frameworks [can be integrated into Presidio](dev
- [spaCy or stanza](nlp_engines/spacy_stanza.md)
- [transformers](nlp_engines/transformers.md)

## Declaring NER support for a custom NLP engine

The `NlpEngine.has_ner` property indicates whether an engine produces native
NER output. The base implementation returns `True`. Engines without native NER
output should override the property and return `False`; otherwise, they are
treated as NER-capable:

```python
@property
def has_ner(self) -> bool:
return False
```

Presidio uses this property to determine whether to register an NLP recognizer.

## Configure Presidio to use the new model

Configuration can be done in two ways:

- **Via code**: Create an `NlpEngine` using the `NlpEnginerProvider` class, and pass it to the `AnalyzerEngine` as input:
- **Via code**: Create an `NlpEngine` using the `NlpEngineProvider` class, and pass it to the `AnalyzerEngine` as input:

```python
from presidio_analyzer import AnalyzerEngine, RecognizerRegistry
Expand Down
1 change: 1 addition & 0 deletions docs/analyzer/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ classDiagram
}

class NlpEngine {
+bool has_ner
+process_text(text, language) NlpArtifacts
+process_batch(texts, language) Iterator[NlpArtifacts]
}
Expand Down
2 changes: 1 addition & 1 deletion docs/analyzer/languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ In its default configuration, it contains recognizers and models for English.
To extend Presidio to detect PII in an additional language, these modules require modification:

1. The `NlpEngine` containing the NLP model which performs tokenization,
lemmatization, Named Entity Recognition and other NLP tasks.
lemmatization, Named Entity Recognition (if supported), and other NLP tasks.
2. PII recognizers (different `EntityRecognizer` objects) should be adapted or created.

!!! note "Note"
Expand Down
2 changes: 1 addition & 1 deletion docs/samples/python/customizing_presidio_analyzer.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@
"### Example 5: Supporting new languages\n",
"\n",
"Two main parts in Presidio handle the text, and should be adapted if a new language is required:\n",
"1. The `NlpEngine` containing the NLP model which performs tokenization, lemmatization, Named Entity Recognition and other NLP tasks.\n",
"1. The `NlpEngine` containing the NLP model which performs tokenization, lemmatization, Named Entity Recognition (if supported), and other NLP tasks.\n",
"2. The different PII recognizers (`EntityRecognizer` objects) should be adapted or created."
]
},
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial/05_languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Two main parts in Presidio handle the text, and should be adapted if a new language is required:

1. The `NlpEngine` containing the NLP model which performs tokenization, lemmatization, Named Entity Recognition and other NLP tasks.
1. The `NlpEngine` containing the NLP model which performs tokenization, lemmatization, Named Entity Recognition (if supported), and other NLP tasks.
2. The different PII recognizers (`EntityRecognizer` objects) should be adapted or created.

## Adapting the NLP engine
Expand Down
10 changes: 10 additions & 0 deletions presidio-analyzer/presidio_analyzer/nlp_engine/nlp_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ class NlpEngine(ABC):
on tokens.
"""

@property
def has_ner(self) -> bool:
"""Return whether this engine performs Named Entity Recognition natively.

The default is ``True``. Engines without native NER output should
override this property and return ``False``; otherwise, they are
treated as NER-capable.
"""
return True

@abstractmethod
def load(self) -> None:
"""Load the NLP model."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ class NoOpNlpEngine(NlpEngine):
engine_name = "no_op"
is_available = True

@property
def has_ner(self) -> bool:
"""Return False because this engine produces no NER output."""
return False

def __init__(
self,
models: List[Dict[str, str]],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ class SlimSpacyNlpEngine(NlpEngine):
engine_name = "slim"
is_available = bool(spacy)

@property
def has_ner(self) -> bool:
"""Return False because the slim pipeline does not include NER."""
return False

def __init__(
self,
models: Optional[List[Dict[str, str]]] = None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ class SpacyNlpEngine(NlpEngine):
engine_name = "spacy"
is_available = bool(spacy)

@property
def has_ner(self) -> bool:
"""Returns True if this engine performs Named Entity Recognition natively."""
return True

def __init__(
self,
models: Optional[List[Dict[str, str]]] = None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
from presidio_analyzer import EntityRecognizer, PatternRecognizer
from presidio_analyzer.nlp_engine import (
NlpEngine,
NoOpNlpEngine,
SpacyNlpEngine,
StanzaNlpEngine,
TransformersNlpEngine,
)
Expand Down Expand Up @@ -59,7 +57,7 @@ def validate_nlp_engine_compatibility(
self, nlp_engine: Optional[NlpEngine]
) -> None:
"""Validate that registered recognizers can use the selected NLP engine."""
if not isinstance(nlp_engine, NoOpNlpEngine):
if nlp_engine is None or nlp_engine.has_ner:
return

nlp_recognizers = [
Expand All @@ -68,8 +66,9 @@ def validate_nlp_engine_compatibility(
if nlp_recognizers:
names = sorted({rec.__class__.__name__ for rec in nlp_recognizers})
raise ValueError(
"NoOpNlpEngine cannot be used with NLP engine recognizers. "
f"Remove or disable these recognizers: {names}."
f"{nlp_engine.__class__.__name__} does not provide NER output "
f"required by these recognizers: {names}. "
"Remove or disable them."
)

def _create_nlp_recognizer(
Expand All @@ -79,28 +78,35 @@ def _create_nlp_recognizer(
) -> SpacyRecognizer:
nlp_recognizer = self.get_nlp_recognizer(nlp_engine)

if nlp_engine:
if nlp_engine is not None:
return nlp_recognizer(
supported_language=supported_language,
supported_entities=nlp_engine.get_supported_entities(),
)

return nlp_recognizer(supported_language=supported_language)

def add_nlp_recognizer(self, nlp_engine: NlpEngine) -> None:
def add_nlp_recognizer(self, nlp_engine: Optional[NlpEngine]) -> None:
"""
Adding NLP recognizer in accordance with the nlp engine.
Add an NLP recognizer when the engine provides native NER output.

For engines without NER output, validate compatibility and skip
registration.

:param nlp_engine: The NLP engine.
:return: None
"""

if isinstance(nlp_engine, NoOpNlpEngine):
if nlp_engine is not None and not nlp_engine.has_ner:
self.validate_nlp_engine_compatibility(nlp_engine)
logger.info("Skipping NLP recognizer registration for no-op NLP engine.")
logger.debug(
"Skipping NLP recognizer registration for %s because it does not "
"provide NER output.",
nlp_engine.__class__.__name__,
)
return

if not nlp_engine:
if nlp_engine is None:
supported_languages = self.supported_languages
else:
supported_languages = nlp_engine.get_supported_languages()
Expand All @@ -117,7 +123,7 @@ def add_nlp_recognizer(self, nlp_engine: NlpEngine) -> None:
def load_predefined_recognizers(
self,
languages: Optional[List[str]] = None,
nlp_engine: NlpEngine = None,
nlp_engine: Optional[NlpEngine] = None,
countries: Optional[List[str]] = None,
) -> None:
"""
Expand Down Expand Up @@ -164,25 +170,22 @@ def load_predefined_recognizers(

@staticmethod
def get_nlp_recognizer(
nlp_engine: NlpEngine,
nlp_engine: Optional[NlpEngine],
) -> Type[SpacyRecognizer]:
"""Return the recognizer leveraging the selected NLP Engine."""
"""Return the recognizer leveraging the selected NLP Engine.

:raises ValueError: If the engine does not provide NER output.
"""

if nlp_engine is not None and not nlp_engine.has_ner:
raise ValueError(
f"{nlp_engine.__class__.__name__} does not have an NLP recognizer"
)
if isinstance(nlp_engine, StanzaNlpEngine):
return StanzaRecognizer
if isinstance(nlp_engine, TransformersNlpEngine):
return TransformersRecognizer
if isinstance(nlp_engine, NoOpNlpEngine):
raise ValueError("NoOpNlpEngine does not have an NLP recognizer")
if not nlp_engine or isinstance(nlp_engine, SpacyNlpEngine):
return SpacyRecognizer
else:
logger.warning(
"nlp engine should be either SpacyNlpEngine,"
"StanzaNlpEngine or TransformersNlpEngine"
)
# Returning default
return SpacyRecognizer
return SpacyRecognizer

def get_recognizers(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from presidio_analyzer import EntityRecognizer
from presidio_analyzer.input_validation import ConfigurationValidator
from presidio_analyzer.nlp_engine import NlpEngine, NoOpNlpEngine
from presidio_analyzer.nlp_engine import NlpEngine
from presidio_analyzer.predefined_recognizers import SpacyRecognizer
from presidio_analyzer.recognizer_registry import RecognizerRegistry
from presidio_analyzer.recognizer_registry.recognizers_loader_utils import (
Expand Down Expand Up @@ -101,8 +101,10 @@ def __update_based_on_nlp_recognizer_conf(
The method adds the NLP recognizer to the list of recognizers
if it is not already present,
or removes it if it is not enabled in the configuration.
Furthermore, it checks if there are
any inconsistencies in configuration. For example:
For engines without native NER output, this method leaves the
recognizer list unchanged.
For engines with native NER output, it also checks for configuration
inconsistencies. For example:
- Multiple enabled NLP recognizers in the configuration for one language.
- The NLP recognizer in the configuration does not match the Nlp Engine.

Expand All @@ -117,12 +119,14 @@ def __update_based_on_nlp_recognizer_conf(
"""
nlp_engine = self.nlp_engine

if not nlp_engine:
if nlp_engine is None:
return

if isinstance(nlp_engine, NoOpNlpEngine):
logger.info(
"Skipping NLP recognizer configuration updates for no-op NLP engine."
if not nlp_engine.has_ner:
logger.debug(
"Skipping NLP recognizer configuration updates for %s because it "
"does not provide NER output.",
nlp_engine.__class__.__name__,
)
return

Expand Down Expand Up @@ -160,9 +164,9 @@ def __update_based_on_nlp_recognizer_conf_and_lang(
f"NLP recognizer (e.g. SpacyRecognizer, StanzaRecognizer) "
f"is not in the list of recognizers "
f"for language {language}. "
f"Adding the default recognizer to the list."
f"Adding the default recognizer to the list. "
f"If you wish to remove the NLP recognizer, "
f"define it as `enabled=false`."
f"define it as `enabled: false`."
)
logger.warning(warning_text)
warnings.warn(warning_text)
Expand Down
13 changes: 11 additions & 2 deletions presidio-analyzer/tests/test_no_op_nlp_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ def no_op_nlp_engine():


class TestNoOpNlpEngine:
def test_has_ner_is_false(self, no_op_nlp_engine):
assert no_op_nlp_engine.has_ner is False

def test_when_init_without_models_then_raises(self):
with pytest.raises(TypeError):
NoOpNlpEngine()
Expand Down Expand Up @@ -285,7 +288,10 @@ def test_when_analyzer_uses_no_op_with_registered_nlp_recognizer_then_raises(
recognizers=[SpacyRecognizer()], supported_languages=["en"]
)

with pytest.raises(ValueError, match="NoOpNlpEngine cannot be used"):
with pytest.raises(
ValueError,
match="NoOpNlpEngine does not provide NER output",
):
AnalyzerEngine(registry=registry, nlp_engine=no_op_nlp_engine)

def test_when_get_nlp_recognizer_with_no_op_then_raises(self, no_op_nlp_engine):
Expand Down Expand Up @@ -428,5 +434,8 @@ def test_when_provider_uses_no_op_with_nlp_recognizer_then_raises(self, tmp_path

provider = AnalyzerEngineProvider(analyzer_engine_conf_file=analyzer_conf_file)

with pytest.raises(ValueError, match="NoOpNlpEngine cannot be used"):
with pytest.raises(
ValueError,
match="NoOpNlpEngine does not provide NER output",
):
provider.create_engine()
21 changes: 21 additions & 0 deletions presidio-analyzer/tests/test_recognizer_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
PatternRecognizer,
RecognizerRegistry,
)
from presidio_analyzer.nlp_engine import TransformersNlpEngine
from presidio_analyzer.predefined_recognizers import SpacyRecognizer, UsSsnRecognizer
from tests.mocks import NlpEngineMock


def create_mock_pattern_recognizer(lang, entity, name):
Expand Down Expand Up @@ -49,6 +51,25 @@ def mock_recognizer_registry():
)


def test_when_custom_engine_has_ner_then_spacy_recognizer_is_returned_without_warning(
caplog,
):
recognizer = RecognizerRegistry.get_nlp_recognizer(NlpEngineMock())

assert recognizer is SpacyRecognizer
assert not caplog.records


def test_when_known_engine_type_has_ner_false_then_get_nlp_recognizer_raises():
class TransformersNlpEngineWithoutNer(TransformersNlpEngine):
@property
def has_ner(self) -> bool:
return False

with pytest.raises(ValueError, match="does not have an NLP recognizer"):
RecognizerRegistry.get_nlp_recognizer(TransformersNlpEngineWithoutNer())


def test_when_get_recognizers_then_all_recognizers_returned(mock_recognizer_registry):
registry = mock_recognizer_registry
registry.load_predefined_recognizers()
Expand Down
Loading
Loading