feat: add entity coverage as the new Nemo Anonymizer feature#195
feat: add entity coverage as the new Nemo Anonymizer feature#195memadi-nv wants to merge 23 commits into
Conversation
f63da45 to
6186d0c
Compare
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
1520569 to
a532a6e
Compare
Separate exhaustive candidate extraction from deterministic coverage filtering and add regression coverage for structured responses and prompt behavior. Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
Signed-off-by: memadi <memadi@nvidia.com>
a532a6e to
f999832
Compare
The refactor that unified the replace/rewrite evaluate() code paths dropped check_rewrite_judge=True from validate_model_alias_references, causing a misconfigured rewrite_judge alias to go undetected until LLM calls were made. Pass check_rewrite_judge=is_rewrite to restore pre-refactor behaviour. Signed-off-by: memadi <memadi@nvidia.com>
Greptile SummaryThis PR adds an Entity Coverage Judge — a two-phase LLM + deterministic-postprocessing judge that reports residual PII leakage after anonymization. It also makes the existing Detection Validity judge opt-in (
Confidence Score: 3/5The core feature logic is well-designed and the deterministic postprocessing filters are thoroughly tested, but the rewrite-path coverage workflow has a shape-mismatch defect that silently discards all scored rows when the LLM adapter drops even one record. The entity coverage judge is a meaningful new capability with solid test coverage for the postprocessing helpers. The replace-mode path handles partial adapter failures correctly via a LEFT JOIN. However, the rewrite-mode path calls run_non_critical, which assigns result.dataframe[col].values (M rows) directly into a copy of the original N-row dataframe — if M < N due to any dropped row, pandas raises a length mismatch, the except swallows it, and all N records get entity_coverage=None instead of just the failed ones. This can trigger on any LLM timeout or parse error in production, quietly zeroing out all coverage scores for an entire batch. entity_coverage_judge.py — specifically run_non_critical (rewrite path, row-count mismatch when adapter drops records) and _build_prompt (dead-code stub with misleading defaults). replace_runner.py has a stale docstring but is otherwise correct. Important Files Changed
|
| @classmethod | ||
| def _build_prompt(cls) -> str: | ||
| return _coverage_prompt(entity_labels=None, strict_entity_protection=False) |
There was a problem hiding this comment.
_build_prompt classmethod ignores instance parameters — latent maintenance trap
The base class marks _build_prompt as the canonical prompt source (its column_config calls self._build_prompt()). This implementation hardcodes entity_labels=None, strict_entity_protection=False, so if the base-class column_config or evaluate path is ever reached — via super() in a subclass, a test calling the base method directly, or a refactor that removes the column_config override — the judge would silently scope to all PII types and non-strict mode regardless of the configured values. At minimum the method should document that it is intentionally unused in favour of the column_config override.
| @classmethod | |
| def _build_prompt(cls) -> str: | |
| return _coverage_prompt(entity_labels=None, strict_entity_protection=False) | |
| @classmethod | |
| def _build_prompt(cls) -> str: | |
| # NOTE: This stub exists only to satisfy the abstract base class interface. | |
| # EntityCoverageWorkflow overrides column_config() to build the prompt from | |
| # instance attributes (entity_labels, strict_entity_protection, data_summary). | |
| # _build_prompt() is never called in normal execution. If this class is | |
| # subclassed or the column_config override is removed, this fallback would | |
| # silently ignore instance configuration. | |
| return _coverage_prompt(entity_labels=None, strict_entity_protection=False) |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Changes
New: Entity Coverage Judge (
engine/evaluation/entity_coverage_judge.py)LLM extraction phase — the judge independently scans the original (pre-anonymization) text and identifies all in-scope sensitive values that appear in the anonymized output. It is scoped by
entity_labels(the detection taxonomy the user configured — e.g.["first_name", "email"]; whenNone, all PII types are in scope),data_summary(semantic context), andstrict_entity_protection(rewrite-only: also flags inferable/indirect values, not just literal identifiers).Deterministic postprocessing phase — before any result is surfaced, a pure-Python filter removes false positives from the LLM output:
_filter_nonliteral_entities): drops candidates whosevaluefield does not appear verbatim in the original text — guards against the LLM hallucinating entities._deduplicate_judge_entities): collapses duplicate candidates._filter_covered_leaked_entities): removes candidates already accounted for by Anonymizer's final entities via three matching modes:"AliceSmith"covered by["Alice", "Smith"]).Output columns —
entity_coverage(float:n_final / (n_final + n_leaked),1.0= nothing leaked,None= judge unavailable) andleaked_entities(list of{value, label, reasoning}dicts).Model role —
entity_coverage_judge, defaults tonemotron-superinevaluate.yaml.EvaluateConfig: detection validity is now opt-incompute_detection_validity: bool = FalsetoEvaluateConfig.EvaluateConfig(compute_detection_validity=True). This is an internal model/threshold diagnostic, not a customer-facing metric.EvaluateConfig.Interface (
anonymizer.py,results.py,display.py)Anonymizer.evaluate()now acceptsconfig: EvaluateConfig | Noneand routescompute_detection_validityto the underlying judge set.entity_coverageandleaked_entitiesare included inAnonymizerResultcolumns and surfaced indisplay_record().Docs (
docs/concepts/evaluation.md,docs/concepts/models.md)1.0, judge failure →None), and the two-phase (LLM + postprocessing) design.entity_coverage_judge.Skill (
skills/anonymizer/SKILL.md)EvaluateConfigknob.Tests
tests/engine/test_entity_coverage_judge.py— unit tests for the postprocessing helpers (exact/subspan/composite matching, non-literal filter, deduplication) and the full workflow.tests/interface/test_anonymizer_interface.py— integration coverage for the new evaluate path.tests/interface/test_anonymizer_logging.py— logging assertions for the new coverage columns.Type of Change
Testing
make testpasses locallymake checkpasses locally (format + lint + typecheck + lock-check)Documentation
make docs-buildpasses locallyRelated Issues
Closes #193