Make mask_sensitive_data honor configured score_threshold#1967
Conversation
`mask_sensitive_data` called `_get_analyzer()` with no arguments, so the analyzer was built (and `lru_cache`-d) at the function's default 0.4 regardless of what the user configured in `sensitive_data_detection.<source>.score_threshold` (which itself defaults to 0.2 in `SensitiveDataDetectionOptions`). `_get_analyzer` is the only place `default_score_threshold` is set on the Presidio `AnalyzerEngine`, so values between the configured threshold and 0.4 were still masked even though `detect_sensitive_data` (which already passes the configured threshold through) reported them as non-sensitive. Worse, because `_get_analyzer` is `@lru_cache`-d, the masking analyzer stays at the wrong threshold for the lifetime of the process even if the user later adjusts the configuration. Mirror `detect_sensitive_data`: fetch `default_score_threshold` from the options and pass it through. Adds a mock-based unit test that asserts the configured value reaches `_get_analyzer` (lives in a new sibling test file so it doesn't get caught by the existing module-level skip when the optional Presidio + spaCy stack isn't installed). Signed-off-by: Kymi808 <zeng.kyle13@gmail.com>
Greptile SummaryThis PR fixes a threshold-consistency bug in
|
| Filename | Overview |
|---|---|
| nemoguardrails/library/sensitive_data_detection/actions.py | Correct two-line fix: reads score_threshold from the configured options and forwards it to _get_analyzer, mirroring the already-correct detect_sensitive_data path. |
| tests/test_sensitive_data_detection.py | Adds a regression test with monkeypatching plus a guarded import spacy; the @pytest.mark.skipif on the new test is redundant with the module-level setup_module skip, and the test cannot run without SDD despite using full mocking — inconsistent with the PR description's intent. |
Sequence Diagram
sequenceDiagram
participant Caller
participant mask_sensitive_data
participant _get_analyzer (lru_cache)
participant AnalyzerEngine
participant AnonymizerEngine
Caller->>mask_sensitive_data: source, text, config
mask_sensitive_data->>mask_sensitive_data: read options.score_threshold (e.g. 0.85)
mask_sensitive_data->>_get_analyzer (lru_cache): score_threshold=0.85
note right of _get_analyzer (lru_cache): Cache key = 0.85 — returns existing or new AnalyzerEngine
_get_analyzer (lru_cache)-->>mask_sensitive_data: AnalyzerEngine(default_score_threshold=0.85)
mask_sensitive_data->>AnalyzerEngine: analyze(text, entities, ad_hoc_recognizers)
AnalyzerEngine-->>mask_sensitive_data: results (filtered at ≥0.85)
mask_sensitive_data->>AnonymizerEngine: anonymize(text, results, operators)
AnonymizerEngine-->>mask_sensitive_data: masked text
mask_sensitive_data-->>Caller: masked text
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
tests/test_sensitive_data_detection.py:426-430
**Redundant `skipif` defeats the stated unit-test intent**
The PR description says the test was placed in a separate file so it could run without the SDD stack (monkeypatching stubs out all Presidio/spaCy calls). In practice the test landed in this file, whose `setup_module` already calls `pytest.skip("Required dependencies not found")` for the whole module when `SDD_SETUP_PRESENT` is `False`. The additional `@pytest.mark.skipif(not SDD_SETUP_PRESENT, ...)` is therefore redundant — and means the test will never execute in an environment where Presidio/spaCy are absent, even though the monkeypatching would make it fully self-contained. If the intent is a lightweight, dependency-free regression test, the test should live in a separate file (as originally described) and the `skipif` should be dropped.
Reviews (3): Last reviewed commit: "review: consolidate sensitive-data test,..." | Re-trigger Greptile
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR fixes a threshold mismatch in sensitive data detection masking. The ChangesThreshold Propagation Fix
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes 🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Address the docstring-coverage warning on PR NVIDIA-NeMo#1967 — the stub classes, their methods, the fake `_get_analyzer`, and the inner result container in the regression test had no docstrings. Concise one-line docstrings now describe each stand-in. No behavior change. Signed-off-by: Kymi808 <zeng.kyle13@gmail.com>
| @@ -0,0 +1,92 @@ | |||
| # SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |||
There was a problem hiding this comment.
can you consolidate all your tests in tests/tests/test_sensitive_data_detection.py ?
i think having a test file solely for mask_sensitive_data adds bloat to the codebase and can lead to potential confusion. i suggest after updating tests/tests/test_sensitive_data_detection.py, you test your unit tests locally with SDD_SETUP_PRESENT dependencies installed
| return text | ||
|
|
||
| analyzer = _get_analyzer() | ||
| # Honor the configured score_threshold, mirroring detect_sensitive_data; |
There was a problem hiding this comment.
i don't think that these comments are necessary esp. since they point out an issue that won't exist anymore once your code is merged. it's clear from the code that you are passing in a score_threshold to the function
Address @christinaexyou's review on PR NVIDIA-NeMo#1967: - Move `test_mask_sensitive_data_honors_configured_score_threshold` from the standalone `tests/test_sensitive_data_detection_unit.py` into the existing `tests/test_sensitive_data_detection.py` (with the same `skipif` + `unit` + `asyncio` decorator stack used by the other tests). Delete the now-redundant unit file. - Drop the four-line explanatory comment above the fixed `_get_analyzer` call; the code is self-explanatory once the bug is gone and the reasoning lives in the commit message. Also add a `if SDD_SETUP_PRESENT: import spacy` shim so the module's `setup_module` (which references `spacy.util.is_package` without ever importing `spacy`) actually runs when the SDD extras are installed. Previously every test in the file was silently skipped via the bare `except Exception` fallthrough with "Unexpected error during setup: name 'spacy' is not defined" — making local verification impossible. Verified locally with `pip install presidio-analyzer presidio-anonymizer spacy && python -m spacy download en_core_web_lg`: the regression test PASSES with the fix and FAILS (AssertionError) on a manually reverted `actions.py`. Signed-off-by: Kymi808 <zeng.kyle13@gmail.com>
|
Thanks for the review @christinaexyou — both points addressed in
One small extra: while verifying locally as you suggested, every test in Verified with |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Quick note on the codecov 0% patch report: it reflects the existing CI tier pattern, not a regression introduced by this PR. All 8 tests in The Happy to split out the |
| pytest.skip("Required dependencies not found") | ||
|
|
||
| try: | ||
| # check if the model is already downloaded |
There was a problem hiding this comment.
can you add "import spacy" here and remove lines 36-38 ? otherwise, we check whether if SDD_SETUP_PRESENT twice
| chat << "Hi! My name is John as well." | ||
|
|
||
|
|
||
| @pytest.mark.skipif(not SDD_SETUP_PRESENT, reason="Sensitive Data Detection setup is not present.") |
There was a problem hiding this comment.
can you refactor your test to match the formatting of the existing ones ? i would use test_high_score_threshold_disables_rails() as a reference and create an example config where the score_threshold is 1.0. we know that a score_threshold of 1.0 would NOT mask anything so if the user message with a PERSON entity is unaltered then we know that mask_sensitive_data honors a configured score threshold
| @pytest.mark.unit | ||
| @pytest.mark.asyncio | ||
| async def test_mask_sensitive_data_honors_configured_score_threshold(monkeypatch): | ||
| """Regression: ``mask_sensitive_data`` must honor ``options.score_threshold``. |
There was a problem hiding this comment.
also, no need for this comment per my previous feedback
Summary
mask_sensitive_data(nemoguardrails/library/sensitive_data_detection/actions.py) called_get_analyzer()with no arguments, so the analyzer was built (and@lru_cache-d) at the function's default0.4regardless of what the user configured insensitive_data_detection.<source>.score_threshold(which itself defaults to0.2inSensitiveDataDetectionOptions)._get_analyzeris the only placedefault_score_thresholdis set on the underlying PresidioAnalyzerEngine, so values between the configured threshold and0.4were still masked even thoughdetect_sensitive_data(which already passes the configured threshold through, see line 121) reported them as non-sensitive. Worse, because_get_analyzeris@lru_cache-d, the masking analyzer stays at the wrong threshold for the lifetime of the process — even after the user later adjusts the configuration.Fix
Mirror
detect_sensitive_data: readdefault_score_thresholdfrom the options and pass it through to_get_analyzer. The behavior ofdetect_sensitive_datais unchanged.Test plan
Added a mock-based unit test in
tests/test_sensitive_data_detection_unit.pythat monkey-patches_get_analyzer(plus the optional Presidio anonymizer surface), invokesmask_sensitive_datawithscore_threshold: 0.85, and asserts_get_analyzerwas called withscore_threshold=0.85. Test lives in a separate file so it isn't caught by the existing module-level skip when the optional Presidio + spaCy stack isn't installed.pytest tests/test_sensitive_data_detection_unit.py→ 1 passed.main: the test FAILS —_get_analyzeris called with the default0.4instead of the configured0.85.ruff checkandruff format --checkclean on both files (using the repo's pinnedruff==0.14.6).Notes
score_thresholdtests intest_sensitive_data_detection.pyuse thedetect sensitive dataflow, so none pin the masking path's current (buggy) threshold behavior.CONTRIBUTING.md.Summary by CodeRabbit
Bug Fixes
Tests