Skip to content

Commit b6ed40d

Browse files
fix(pii): keep detect-all when a custom pattern is added; custom patterns win overlaps
1 parent 5e86c7d commit b6ed40d

2 files changed

Lines changed: 31 additions & 14 deletions

File tree

apps/pii/server.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,11 @@ def build_custom_recognizers(
326326
recognizers.append(
327327
PatternRecognizer(
328328
supported_entity=entity,
329-
patterns=[Pattern(name=p.name or entity, regex=p.regex, score=0.5)],
329+
# Score 1.0 so a user's explicit pattern wins any overlap with a
330+
# built-in detector (e.g. spaCy tagging "EMP-123456" as ORGANIZATION
331+
# under detect-all). Presidio resolves overlapping spans by score, so
332+
# the custom replacement — not the built-in token — is applied.
333+
patterns=[Pattern(name=p.name or entity, regex=p.regex, score=1.0)],
330334
supported_language=language,
331335
)
332336
)
@@ -337,12 +341,17 @@ def build_custom_recognizers(
337341
def resolve_entities(
338342
req_entities: list[str] | None, custom_entity_ids: list[str]
339343
) -> list[str] | None:
340-
"""Effective entity filter: the requested built-ins plus the custom entity ids.
341-
`None` (detect all built-ins) is preserved only when neither is present, so a
342-
custom-only request never widens into detecting every built-in entity."""
343-
if req_entities is None and not custom_entity_ids:
344+
"""Effective entity filter.
345+
346+
`None` means detect-all built-ins (the guardrails "empty selection = detect
347+
everything" convention); the ad-hoc custom recognizers still fire under `None`,
348+
so adding a custom pattern augments detect-all rather than silently disabling
349+
the built-in detectors. An explicit list — including the empty list, which is
350+
the data-retention "only these custom patterns" shape — is used verbatim, with
351+
the custom ids appended."""
352+
if req_entities is None:
344353
return None
345-
return list(req_entities or []) + custom_entity_ids
354+
return list(req_entities) + custom_entity_ids
346355

347356

348357
class AnalyzeRequest(BaseModel):

apps/sim/lib/guardrails/validate_pii.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ import type { CustomPiiPattern } from '@/lib/guardrails/pii-entities'
88
const logger = createLogger('PIIValidator')
99

1010
/**
11-
* Compute the explicit entity list to send Presidio. When custom patterns are
12-
* present we must always send an explicit list (even empty) so the server detects
13-
* only the requested built-ins plus the custom entities — never "all". With no
14-
* built-ins and no patterns, `undefined` preserves the legacy "detect all" default.
11+
* Entity list for the batch (data-retention) paths, where an empty selection with
12+
* custom patterns means "redact ONLY these custom patterns" (the user unchecked
13+
* every built-in entity). An explicit empty array is sent so the server detects
14+
* only the custom entities, never "all". With neither, `undefined` preserves the
15+
* legacy "detect all" default.
16+
*
17+
* The guardrails single-text path uses the opposite convention — empty selection
18+
* means "detect all" — so it does NOT use this helper (see {@link analyze}).
1519
*/
16-
function resolveEntities(
20+
function resolveBatchEntities(
1721
entityTypes: string[],
1822
patterns?: CustomPiiPattern[]
1923
): string[] | undefined {
@@ -83,7 +87,11 @@ async function analyze(
8387
language: string,
8488
patterns?: CustomPiiPattern[]
8589
): Promise<AnalyzerSpan[]> {
86-
const entities = resolveEntities(entityTypes, patterns)
90+
// Guardrails convention: an empty selection means "detect all". Sending no
91+
// `entities` keeps that, and the server still runs the custom recognizers under
92+
// detect-all — so a custom pattern augments the built-in detectors, never
93+
// silently replaces them.
94+
const entities = entityTypes.length > 0 ? entityTypes : undefined
8795

8896
// boundary-raw-fetch: internal call to the Presidio analyzer service via PII_URL
8997
const response = await fetch(`${PII_URL}/analyze`, {
@@ -114,7 +122,7 @@ async function analyzeBatch(
114122
language: string,
115123
patterns?: CustomPiiPattern[]
116124
): Promise<AnalyzerSpan[][]> {
117-
const entities = resolveEntities(entityTypes, patterns)
125+
const entities = resolveBatchEntities(entityTypes, patterns)
118126

119127
// boundary-raw-fetch: internal call to the Presidio analyzer service via PII_URL
120128
const response = await fetch(`${PII_URL}/analyze_batch`, {
@@ -188,7 +196,7 @@ async function redactBatch(
188196
language: string,
189197
patterns?: CustomPiiPattern[]
190198
): Promise<string[] | null> {
191-
const entities = resolveEntities(entityTypes, patterns)
199+
const entities = resolveBatchEntities(entityTypes, patterns)
192200

193201
// boundary-raw-fetch: internal call to the Presidio combined redact service via PII_URL
194202
const response = await fetch(`${PII_URL}/redact_batch`, {

0 commit comments

Comments
 (0)