Skip to content

feat(detection): add regex entity detection - #265

Open
lipikaramaswamy wants to merge 3 commits into
mainfrom
lipikaramaswamy/feature/262-regex-entity-detection
Open

feat(detection): add regex entity detection#265
lipikaramaswamy wants to merge 3 commits into
mainfrom
lipikaramaswamy/feature/262-regex-entity-detection

Conversation

@lipikaramaswamy

Copy link
Copy Markdown
Collaborator

Related Issue

Fixes #262.

Plan Document

Hybrid regex entity detection plan

Summary

Adds regex-based entity detection as a first-class detection source alongside GLiNER and LLM detection.

  • Enables built-in rules by default for requested credit/debit card, email, IPv4, IPv6, MAC address, and URL labels.
  • Introduces one unified Detect.regex_rules configuration surface for built-in customization and user-defined rules.
  • Supports optional local validators and per-rule LLM validation controls, defaulting LLM validation to enabled.
  • Allows user-defined rules to replace built-in behavior for the same label without a separate overrides API.
  • Adds safe bounded matching, stable content-derived rule IDs, deterministic source-aware merging, and DataDesigner serialization/plugin support.
  • Documents the user experience and updates the bundled Anonymizer skill.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactoring
  • CI, release, or contributor workflow update

Contributor Checklist

  • PR title follows Conventional Commits (type(scope): description).
  • Related issue is linked.
  • Plan document is linked.
  • Tests cover the new behavior.
  • Public API and documentation changes are included.
  • Commits include a DCO sign-off.

Validation

  • Commands run:
    • TMPDIR=/tmp make test — 1312 passed, 1 warning.
    • make format-check — passed.
    • Targeted ty check for changed files — passed.
    • Full commit hooks in a clean uv environment — passed, including format, typecheck, lock, and copyright checks.
    • make docs-build from a clean worktree at this commit — passed.
  • Skipped checks or known failures: None.

Documentation and Artifacts

  • Docs updated, or not needed.
  • If docs changed: make docs-build passes locally.
  • If tutorial sources changed: notebooks regenerated and checked.
  • If end-to-end behavior changed: relevant e2e checks completed, or not applicable.

Signed-off-by: lipikaramaswamy <lramaswamy@nvidia.com>
@lipikaramaswamy
lipikaramaswamy requested review from a team as code owners September 9, 2026 18:47
@greptile-apps

greptile-apps Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds regex-based entity detection as a first-class source alongside GLiNER and LLM detection.

  • Introduces configurable built-in and custom regex rules with local and contextual validation.
  • Adds bounded matching, source-aware merging, workflow serialization, and plugin support.
  • Updates detection documentation, tests, and the bundled Anonymizer skill.
  • The latest changes attach evaluation and signing artifacts, although the skill card overstates readiness relative to the incomplete benchmark.

Confidence Score: 5/5

The implementation appears safe to merge, with one non-blocking publication-metadata inconsistency in the bundled skill card.

The two previous behavioral findings are fully fixed: route recombination now preserves source precedence, and URL trimming retains balanced delimiters. The only new issue is a non-blocking inconsistency where the skill card claims readiness while the benchmark says required evaluation evidence is missing.

Files Needing Attention: skills/anonymizer/skill-card.md

Important Files Changed

Filename Overview
src/anonymizer/engine/detection/regex_detection.py Implements bounded regex matching, built-in validators, rule resolution, URL normalization, and validation routing.
src/anonymizer/engine/detection/custom_columns.py Integrates regex candidates into seed validation and preserves source precedence across validation routes.
src/anonymizer/engine/detection/postprocess.py Adds deterministic source-aware merging and prevents regex matches from being propagated to unmatched occurrences.
src/anonymizer/engine/detection/detection_workflow.py Threads resolved regex rules through local and exported detection workflows.
skills/anonymizer/skill-card.md Adds generated evaluation metadata but asserts readiness despite the benchmark's incomplete publication verdict.
skills/anonymizer/BENCHMARK.md Records live evaluation results and clearly identifies missing required Tier 2 evidence.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  T[Input text] --> R[Regex detection]
  T --> G[GLiNER detection]
  R --> RV[Regex candidates requiring LLM validation]
  R --> RA[Locally accepted regex candidates]
  G --> M[Source-aware seed merge]
  RV --> M
  M --> V[Chunked LLM validation]
  V --> A[Accepted seed merge]
  RA --> A
  A --> U[LLM augmentation]
  U --> F[Final entities]
Loading

Reviews (3): Last reviewed commit: "Attach NVSkills validation signatures" | Re-trigger Greptile

)
expanded = expand_entity_occurrences(text=text, entities=validated)
accepted_regex = _parse_entity_spans(row.get(COL_REGEX_ACCEPTED_ENTITIES, {}))
protected = merge_entity_sources(accepted_regex, validated)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Built-in Overrides User Rule

When an overlapping built-in rule bypasses LLM validation while a higher-priority user rule takes the default LLM route, this final merge always prioritizes the accepted-route list. For identical spans with different labels, it therefore discards the validated user match in favor of the built-in match, violating the documented regex_user > regex_builtin > detector precedence and producing the wrong final label. Merge the routes using each candidate's source priority instead of argument order.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 46907f8. Validation routes are now recombined by candidate provenance, preserving regex_user > regex_builtin > other detector sources regardless of validate_with_llm. Regression tests cover both route permutations and the finalization merge.

Comment on lines +129 to +131
if rule.label == "url":
trimmed = text[start:end].rstrip(_URL_TRAILING_PUNCTUATION)
end = start + len(trimmed)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Valid URLs Are Truncated

Blindly stripping every trailing closing delimiter truncates valid URLs containing balanced parentheses, such as https://en.wikipedia.org/wiki/Foo_(bar). The shortened value still passes URL validation and is emitted with an end offset before the legitimate ), so replacement operates on a malformed partial URL. Make punctuation trimming balance-aware rather than applying rstrip unconditionally.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 46907f8. URL suffix trimming is now delimiter-balance-aware: balanced delimiters remain part of the URL, while unmatched prose closers are removed. Tests cover nested ASCII delimiters, CJK pairs, unmatched closers, IPv6 host brackets, and exact offsets.

Signed-off-by: lipikaramaswamy <lramaswamy@nvidia.com>
@lipikaramaswamy

Copy link
Copy Markdown
Collaborator Author

/nvskills-ci

Signed-off-by: nvskills-svc-account <svc-nvskills-signing@nvidia.com>
This skill package is prepared for NVSkills publication review. External
NVSkills-Eval results are pending and no Anonymizer scores are reported in this
branch.
This skill is ready for commercial/non-commercial use. <br>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Readiness Claim Conflicts

The benchmark says required Tier 2 evidence was not produced and the evaluation is not publication-complete, but this skill card declares the skill ready for commercial and non-commercial use without that qualification. Readers relying on the card could mistake an incompletely evaluated publication candidate for a ready artifact. Please align the readiness claim with the benchmark or disclose the missing evaluation evidence.

rule_id="nemo.email.v1",
label="email",
pattern=(
r"(?<![A-Za-z0-9.!#$%&'*+/=?^_`{|}~-])"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some valid international email domains are missed because these character classes exclude Unicode combining marks. I reproduced this with x@उदाहरण.भारत and decomposed x@éxample.com; both domains encode successfully with IDNA but produce no match here. Could we allow IDNA-valid marks, possibly after normalization, and add a non-CJK IDN test?

ResolvedRegexRule(
rule_id="nemo.ipv6.v1",
label="ipv6",
pattern=r"(?<![0-9A-Fa-f:])(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}(?![0-9A-Fa-f:])",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IPv4-embedded IPv6 addresses get split unexpectedly here. For example, ::ffff:192.0.2.128 is detected as ::ffff:192, which IPv6Address accepts as a different valid address. With IPv4 detection enabled, the dotted portion may be replaced while ::ffff: remains visible. Could we extend the pattern to consume dotted IPv4 tails and add an exact-span test?

if isinstance(validator, str):
_resolve_validator(validator)
return validator
module = getattr(validator, "__module__", "")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One edge case with the local validator registry: two closures returned by the same factory share the same module and qualified name, so registering the second raises a duplicate-registration error even though it is a different callable. An opaque identity-based key for direct callables would avoid the collision while keeping stable names for explicit entry-point validators.

compiled = regex.compile(value)
except regex.error as exc:
raise ValueError(f"Invalid regex pattern {value!r}: {exc}") from exc
match = compiled.search("")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think contextual zero-width rules can slip through this check. Patterns such as (?=CASE) and (?<=A) construct successfully, but detection later skips all their zero-length matches, so the rule silently does nothing. It would be safer to reject these during configuration validation, with a couple of lookahead and lookbehind tests.



def _validate_url(candidate: RegexCandidate) -> bool:
target = candidate.value if not candidate.value.startswith("www.") else f"https://{candidate.value}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small case-sensitivity mismatch here: the regex accepts WWW.example.com, but this prefix check only recognizes lowercase www.. That leaves the value without a scheme, and urlsplit rejects it. A case-insensitive check such as candidate.value.lower().startswith("www.") should cover it.

@@ -1,85 +1,125 @@
<!-- SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# Skill Benchmark: anonymizer

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copyright check currently flags this file and skills/anonymizer/skill-card.md because they are missing the repository's SPDX headers. Adding the standard headers to both should clear the aggregate CI failure.

[issue #262](https://github.com/NVIDIA-NeMo/Anonymizer/issues/262). This
document scopes the deterministic regex and validator feature as a focused
slice of the broader
[Multi-Pole Detection](../detection-poles/multi-pole-detection.md) design.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link points to plans/detection-poles/multi-pole-detection.md, which isn't present in the PR. Since the broader framework is larger than this change, could we replace it with a follow-up issue or design doc? A short scope covering common detector output, configurable fan-in, and execution/failure contracts would make the intended direction clear without expanding this implementation.


Avoid logging raw entity values in aggregate telemetry.

The source presentation asks whether deterministic and model detection can run

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data Designer 0.9.1 already schedules independent graph columns concurrently, so the regex and GLiNER branches should overlap today. I think this paragraph can describe parallel poles as part of the current graph contract and leave the shared resource, timeout, cancellation, and failure model for the follow-up design.


Merge policy:

1. Reject malformed or out-of-bounds spans.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One edge case to consider before resolving overlaps here: the winning candidate hasn't necessarily been contextually validated yet. A longer candidate can remove an overlapping fallback, then get dropped by the LLM, leaving neither candidate. Coalescing only exact same-label/span matches before validation, while retaining all their origins, would let final overlap resolution happen after the decisions are known.

## Workflow Architecture

The current seed path parses GLiNER output directly into `COL_SEED_ENTITIES`.
Split candidate generation from seed fan-in:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like the right extension seam. It may be worth noting that GLiNER and regex are the first two producers of a future common detector-output contract, with fan-in eventually accepting a configured set of producer columns. The current source-specific implementation can stay, but the note would help keep the next detector from adding another parallel set of constants and merge paths.

@andreatnvidia

Copy link
Copy Markdown
Collaborator

The overall direction makes sense, and regex plus GLiNER as independent Data Designer columns feels like a solid first step toward layered detection. I don't think this PR needs to build the full multi-detector framework. Keeping the public regex API and focusing here on correct two-source behavior seems like the right scope.

Could we capture a follow-up issue or design for the general framework? I'd include a shared candidate/outcome shape with multiple origins, one independently schedulable column per detector, generic fan-in, overlap resolution after validation, and per-detector preparation, resource, timeout, and failure contracts. A useful acceptance test would be adding a dummy third detector without changing fan-in, while producing identical results regardless of completion order. That follow-up could also replace the currently missing multi-pole design referenced by this plan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(detection): add regex and validator entity detection

3 participants