feat: apply direct identifier replacements before rewrite LLM call - #208
feat: apply direct identifier replacements before rewrite LLM call#208asteier2026 wants to merge 7 commits into
Conversation
Direct identifiers are now substituted programmatically from the replacement map before the rewrite LLM sees the text, ensuring all occurrences are replaced consistently without relying on the LLM to apply a <replacement_map> block. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: asteier2026 <asteier@nvidia.com>
Greptile SummaryThis PR moves the substitution of
Confidence Score: 4/5The core pre-replacement logic is correct, but a documented safety contract in The single-pass regex, cascade-prevention, and Unicode whitespace fallback work as designed. The gap is that
Important Files Changed
|
Sequential str.replace() calls could incorrectly replace a synthetic value that happened to match another entity's original string (e.g. Alice→Bob then Bob→Carlos making Alice appear as Carlos). A combined regex alternation matches all originals simultaneously, eliminating the cascade. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: asteier2026 <asteier@nvidia.com>
Moved the missing-map warning into _get_replace_pairs so the disposition is parsed in one place. _apply_direct_replacements no longer re-parses COL_SENSITIVITY_DISPOSITION when _get_replace_pairs returns an empty list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: asteier2026 <asteier@nvidia.com>
The LLM generating the replacement map occasionally normalises unusual Unicode whitespace (e.g. U+202F narrow no-break space) to a regular space in the original field. The exact-match lookup then misses the entity, triggering the unprotected warning. Add _normalize_ws and a second-pass lookup so that if the exact match fails, a whitespace-normalised comparison is tried. When a normalised match is found the disposition entity value (which reflects what is actually in the text) is used as the substitution key. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: asteier2026 <asteier@nvidia.com>
…cefully Any failure (malformed disposition, bad replacement map, regex error) previously caused the entire record to be skipped. Now the error is logged and the original text is passed through unchanged so the LLM rewrite step can still run. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: asteier2026 <asteier@nvidia.com>
The LLM generating the replacement map normalises unusual Unicode whitespace (e.g. U+202F narrow no-break space) to a regular space in the original field. _filter_replacement_map_to_input_entities was using an exact match against the detected entity values, so these entries were silently dropped from the map. Add a whitespace-normalized fallback: when the exact (original, label) pair is not in allowed_pairs, try a normalized comparison and, if it matches, rewrite original to the canonical detected value so all downstream lookups succeed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: asteier2026 <asteier@nvidia.com>
| pairs = _get_replace_pairs(row) | ||
| if pairs: | ||
| sorted_pairs = sorted(pairs, key=lambda p: len(p[0]), reverse=True) | ||
| pattern = re.compile("|".join(re.escape(original) for original, _ in sorted_pairs)) |
There was a problem hiding this comment.
This global regex does not preserve the span-aware matching semantics used by the Substitute workflow. For example, Ann → Maria transforms Ann met Anna into Maria met Mariaa, whereas _apply_replacement_map_to_text() replaces only detected entity spans.
Can the rewrite path reuse or adapt the existing span-aware replacement logic—including for tagged text—and add this case as a regression test?
There was a problem hiding this comment.
One additional thought for the span-aware approach: could the regression test also confirm that every detected span selected for replace was actually transformed? That would give us an explicit coverage check while preserving the existing boundary-aware semantics.
| unmatched = replace_values - matched | ||
| if unmatched: | ||
| logger.warning( | ||
| "Replace entities have no entry in the replacement map and will pass through unprotected: %s", |
There was a problem hiding this comment.
Small logging-safety suggestion: sorted(unmatched) includes raw detected entity values, so the PII being protected can be persisted in operational logs. If this becomes an exception, including the values there would have the same issue. Could we report only safe metadata such as the record ID, entity IDs or labels, and counts?
lipikaramaswamy
left a comment
There was a problem hiding this comment.
Thanks for moving direct replacement into deterministic preprocessing—the overall direction makes sense. I’m requesting changes for the two safety and correctness gaps discussed inline: incomplete replacement maps currently fail open after replace entities are omitted from the prompt, and the global regex can modify text outside detected entity spans. There is also a smaller logging-safety concern around including raw entity values. Fail-safe handling, span-aware replacement, and focused regression tests would put this in good shape for another look.
|
Made these changes:
|
Summary
the replacement map before the rewrite LLM call, using a single-pass regex to prevent cascade
replacements (e.g. Alice→Bob→Carlos)
replacements
_get_replace_pairs to handle LLM-normalised Unicode whitespace (e.g. U+202F → U+0020) in entity values
dropping the record
Motivation
The rewrite LLM was inconsistently applying replacement map entries, especially for entities that
appear multiple times. Programmatic pre-replacement guarantees all occurrences are substituted before
the LLM sees the text. The whitespace fixes handle cases where the LLM normalises unusual Unicode
whitespace in entity values, which caused map lookups to silently miss.