vojmahdal commited on
Commit
4d975e0
·
verified ·
1 Parent(s): 1308029

improve anonymization of the names

Browse files
Files changed (1) hide show
  1. processors/anonymizer.py +38 -3
processors/anonymizer.py CHANGED
@@ -110,6 +110,41 @@ _PLACEHOLDERS = {
110
  }
111
 
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  def _regex_anonymize(text: str) -> str:
114
  """Pure-regex anonymization for the formatted identifiers we can match safely."""
115
  text = _EMAIL_RE.sub("[EMAIL]", text)
@@ -142,7 +177,7 @@ def anonymize_text(text: str, language: str = "en") -> str:
142
  "Refusing to process the text (set ANONYMIZER_ALLOW_FALLBACK=true only "
143
  "for local development)."
144
  )
145
- return _regex_anonymize(text)
146
 
147
  try:
148
  results = _analyzer.analyze(text=text, language=language)
@@ -158,14 +193,14 @@ def anonymize_text(text: str, language: str = "en") -> str:
158
  analyzer_results=results,
159
  operators=operators,
160
  )
161
- return anonymized.text
162
  except Exception as e: # pragma: no cover
163
  print(f"[anonymizer] Presidio failed at runtime: {e}")
164
  if not fallback_allowed():
165
  raise AnonymizationUnavailable(
166
  f"Presidio failed on this text ({e}); refusing to fall back to regex-only masking."
167
  ) from e
168
- return _regex_anonymize(text)
169
 
170
 
171
  def detect_pii(text: str, language: str = "en") -> list[dict[str, Any]]:
 
110
  }
111
 
112
 
113
+ # Personal names that NER-based masking (Presidio/spaCy) commonly misses in customer-support chat:
114
+ # agent sign-offs ("^Lynn", "*MelindaR", "- Laura :)", "-Dinesh") and greetings ("Hi Guy").
115
+ # Measured on 119 annotated person names, Presidio alone left ~32 % of them visible, mostly exactly
116
+ # these forms. The rules are deliberately narrow (a marker or a greeting is required) so that
117
+ # ordinary capitalised words are not masked.
118
+ _SIGNOFF_MARKER_RE = re.compile(r"(?<![\w])[\^~*]\s?[A-Z][A-Za-z]{1,14}(?![\w])")
119
+ _SIGNOFF_DASH_RE = re.compile(r"(?<=\s)[-\u2013]\s?[A-Z][a-z]{1,14}(?=\s*(?::\)|\d+/\d+|[.!]?\s*$))")
120
+ _GREETING_RE = re.compile(r"\b(Hi|Hello|Hey|Dear|Thanks|Thank you)(,?\s+)([A-Z][a-z]{1,14})\b(?!\s+[A-Z])")
121
+ _NOT_A_NAME = {
122
+ "There", "All", "Team", "Everyone", "Everybody", "Again", "So", "Much", "You", "Guys", "Support", "Care", "Help",
123
+ "Sir", "Madam", "Customer", "Friend", "Folks", "Amazon", "Apple", "Uber", "Spotify",
124
+ "Do", "My", "Is", "It", "We", "The", "How", "What", "Please", "Sorry", "Yes", "No", "Ok", "Okay",
125
+ }
126
+
127
+
128
+ def mask_name_patterns(text: str) -> str:
129
+ """Mask agent sign-offs and greeted first names that NER-based masking tends to miss."""
130
+
131
+ def token_of(m: re.Match) -> str:
132
+ return re.sub(r"^[\^~*\-–]\s?", "", m.group(0))
133
+
134
+ def signoff(m: re.Match) -> str:
135
+ return m.group(0) if token_of(m) in _NOT_A_NAME else "[NAME]"
136
+
137
+ def dash(m: re.Match) -> str:
138
+ return m.group(0) if token_of(m) in _NOT_A_NAME else "- [NAME]"
139
+
140
+ def greeting(m: re.Match) -> str:
141
+ return m.group(0) if m.group(3) in _NOT_A_NAME else f"{m.group(1)}{m.group(2)}[NAME]"
142
+
143
+ text = _SIGNOFF_MARKER_RE.sub(signoff, text)
144
+ text = _SIGNOFF_DASH_RE.sub(dash, text)
145
+ return _GREETING_RE.sub(greeting, text)
146
+
147
+
148
  def _regex_anonymize(text: str) -> str:
149
  """Pure-regex anonymization for the formatted identifiers we can match safely."""
150
  text = _EMAIL_RE.sub("[EMAIL]", text)
 
177
  "Refusing to process the text (set ANONYMIZER_ALLOW_FALLBACK=true only "
178
  "for local development)."
179
  )
180
+ return mask_name_patterns(_regex_anonymize(text))
181
 
182
  try:
183
  results = _analyzer.analyze(text=text, language=language)
 
193
  analyzer_results=results,
194
  operators=operators,
195
  )
196
+ return mask_name_patterns(anonymized.text)
197
  except Exception as e: # pragma: no cover
198
  print(f"[anonymizer] Presidio failed at runtime: {e}")
199
  if not fallback_allowed():
200
  raise AnonymizationUnavailable(
201
  f"Presidio failed on this text ({e}); refusing to fall back to regex-only masking."
202
  ) from e
203
+ return mask_name_patterns(_regex_anonymize(text))
204
 
205
 
206
  def detect_pii(text: str, language: str = "en") -> list[dict[str, Any]]: