feat(analyzer): add recognizer-level threshold config#2116
Conversation
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Pull request overview
Adds support in presidio-analyzer for configuring score thresholds per recognizer (and optionally per entity within that recognizer) via YAML, while preserving the existing global default_score_threshold / request-level score_threshold behavior.
Changes:
- Introduces
recognizer_score_thresholdsconfiguration, applies it during result filtering (before duplicate collapse) when no request-levelscore_thresholdis provided. - Adds validation/normalization for the new configuration shape (including numeric shorthand) and expands unit test coverage for precedence and error cases.
- Updates analyzer configuration docs, the no-code tutorial, and the root changelog to document the new option.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| presidio-analyzer/presidio_analyzer/analyzer_engine.py | Adds recognizer/entity-specific threshold support and applies filtering before deduplication. |
| presidio-analyzer/presidio_analyzer/analyzer_engine_provider.py | Loads recognizer_score_thresholds from analyzer YAML and passes it into AnalyzerEngine. |
| presidio-analyzer/presidio_analyzer/input_validation/schemas.py | Validates and normalizes recognizer_score_thresholds in analyzer configuration validation. |
| presidio-analyzer/presidio_analyzer/conf/default_analyzer_full.yaml | Documents the new YAML option with a commented example. |
| presidio-analyzer/tests/test_analyzer_engine.py | Adds deterministic tests covering precedence, shorthand, invalid values, and dedupe ordering. |
| presidio-analyzer/tests/test_analyzer_engine_provider.py | Verifies provider passes/normalizes thresholds from YAML and that defaults remain unchanged otherwise. |
| presidio-analyzer/tests/test_configuration_validator.py | Adds validator tests for valid shorthand + invalid threshold structures/types/ranges. |
| docs/tutorial/08_no_code.md | Updates no-code YAML example and explains when to use recognizer-level thresholds. |
| docs/analyzer/analyzer_engine_provider.md | Documents the new configuration key and provides a YAML example. |
| CHANGELOG.md | Adds an Unreleased entry documenting the new analyzer YAML capability. |
9ebc0e8 to
adac24d
Compare
omri374
left a comment
There was a problem hiding this comment.
Thanks! Looks really good. A few minor and quickly fixable comments
|
|
||
|
|
||
| def validate_score_threshold(threshold: Any) -> float: | ||
| """Validate a score threshold without coercing its input type.""" |
There was a problem hiding this comment.
please align the docstring format to the rest of the repo (add :param: threshold)
There was a problem hiding this comment.
Do we have to keep threshold as Any? can we use a more specific type?
| return threshold | ||
|
|
||
|
|
||
| def normalize_score_thresholds(score_thresholds: Any) -> Dict[str, float]: |
| version: str = "0.0.1", | ||
| context: Optional[List[str]] = None, | ||
| country_code: Optional[str] = None, | ||
| score_thresholds: Any = None, |
There was a problem hiding this comment.
Can we provide a more specific type? I assume it's dict[str, float]?
| self, results: List[RecognizerResult], score_threshold: float = None | ||
| self, | ||
| results: List[RecognizerResult], | ||
| score_threshold: float = None, |
There was a problem hiding this comment.
| score_threshold: float = None, | |
| score_threshold: Optional[float] = None, |
| """ | ||
| ``` | ||
|
|
||
| Each recognizer can set a `default` cutoff and entity-specific overrides in `score_thresholds`. An explicit request threshold takes priority, followed by an entity override, the recognizer default, and the analyzer's `default_score_threshold`. |
There was a problem hiding this comment.
This is an important implementation detail. If we pass score_threshold, then the recognizer-level thresholds are bypassed completely. Please make this more explicit ("explicit request threshold" might not be clear) and put this in the docs, not just the tutorial.
There was a problem hiding this comment.
here also, please use "threshold" instead of "cutoff"
| """ | ||
| if score_threshold is None: | ||
| score_threshold = self.default_score_threshold | ||
| recognizers_by_id = { |
There was a problem hiding this comment.
if id is missing for some reason, it would not pick up the threshold. Consider adding a debug log
| @property | ||
| def score_thresholds(self) -> Dict[str, float]: | ||
| """Return a defensive copy of this recognizer's score thresholds.""" | ||
| return self._score_thresholds.copy() |
There was a problem hiding this comment.
This is copied on every result. Is it necessary?
| - `supported_entity`: the detected entity associated by the recognizer. | ||
| - `deny_list`: A list of words to detect, in case the recognizer uses a predefined list of words. | ||
| - `deny_list_score`: confidence score for a term identified using a deny-list. | ||
| - `score_thresholds`: optional score cutoffs for this recognizer. Use `default` as the recognizer-wide cutoff and entity names for overrides. An explicit request threshold takes priority, followed by an entity override, this recognizer default, and the analyzer's `default_score_threshold`. |
There was a problem hiding this comment.
please use "threshold" instead of "cutoff" to keep the jargon similar to the rest of the code base
| :param entities: List of PII entities that should be looked for in the text. | ||
| If entities=None then all entities are looked for. | ||
| :param correlation_id: cross call ID for this request | ||
| :param score_threshold: A minimum value for which |
There was a problem hiding this comment.
please update this threshold with the new logic
| assert scores == {"URL": 0.75, "DATE_TIME": 0.9} | ||
|
|
||
|
|
||
| def test_explicit_score_threshold_overrides_config(): |
There was a problem hiding this comment.
please make sure we are covering multiple competing thresholds scenarios.
Change Description
Adds recognizer-level score thresholds in
presidio-analyzer. Each recognizer entry can set a default threshold and entity-specific overrides, whileAnalyzerEnginekeeps final filtering after context enhancement and before duplicate removal.RecognizerRegistryProvidervalidates and attaches normalized thresholds to each recognizer instance. The analyzer resolves the producing recognizer byrecognizer_identifier, so same-name recognizers, programmatic recognizers, and request-only ad hoc recognizers retain independent policies.Precedence is explicit request
score_threshold, entity override, recognizer default, thendefault_score_threshold. The former top-levelrecognizer_score_thresholdskey is removed because this PR has not merged.This updates the analyzer and registry configuration docs, no-code example, default YAML examples, and changelog.
Issue reference
Fixes #1572
Tests
poetry run pytest tests/test_entity_recognizer.py tests/test_yaml_recognizer_models.py tests/test_recognizers_loader_utils.py tests/test_recognizer_registry_provider.py tests/test_recognizer_registry.py tests/test_configuration_validator.py tests/test_analyzer_engine.py tests/test_analyzer_engine_provider.py -qwith 303 passed and 1 skippedpoetry run ruff check presidio_analyzer/score_thresholds.py presidio_analyzer/entity_recognizer.py presidio_analyzer/analyzer_engine.py presidio_analyzer/analyzer_engine_provider.py presidio_analyzer/input_validation/schemas.py presidio_analyzer/input_validation/yaml_recognizer_models.py presidio_analyzer/recognizer_registry/recognizer_registry_provider.py presidio_analyzer/recognizer_registry/recognizers_loader_utils.py presidio_analyzer/recognizer_registry/recognizer_registry.py tests/test_entity_recognizer.py tests/test_yaml_recognizer_models.py tests/test_recognizers_loader_utils.py tests/test_recognizer_registry_provider.py tests/test_recognizer_registry.py tests/test_configuration_validator.py tests/test_analyzer_engine.py tests/test_analyzer_engine_provider.pyNote on CHANGELOG
Updated
CHANGELOG.mdunder[unreleased],Analyzer,Added.Checklist