Free lesson · Forward Deployed GenAI Engineering
Detect and redact PII with Presidio and LlamaGuard 4
You build a PIIRedactionPipeline combining Presidio (structured PII) and LlamaGuard 4 (contextual PII) with mask/hash/synthetic redaction strategies and an audit log.
Course: AI Solution Delivery · Chapter 5 · Customer Data Integration Pipelines
Free to read — no subscription required.
Introduction
When you build customer data integration pipelines, raw records often carry names, email addresses, phone numbers, and subtler identifiers — a job title combined with a company name and city can uniquely pinpoint a person even without an explicit name field. Sending that data downstream unguarded risks regulatory violations and damaged customer trust. A dual-layer redaction approach — combining pattern-based detection for structured fields with a language-model safety check for contextual identifiers — closes both gaps. By the end of this lesson, you'll be able to implement a PII redaction pipeline that detects and masks sensitive data before it reaches any embedding or storage layer.
Key Terminology
- Structured PII — Personally identifiable information that follows predictable formats and can be caught by pattern matching, such as email addresses, phone numbers, US Social Security numbers, credit card numbers, and IP addresses; these are the entity types declared in
PresidioLayer.detectand passed toAnalyzerEngine. - Contextual PII — Indirect identifiers that appear benign in isolation but together uniquely pinpoint a real person, such as a job title combined with a company name and city; because no single field is a PII field, contextual PII requires a language-model safety check rather than a regex or span recognizer.
- AnalyzerEngine — The Presidio component that scans input text and returns a list of
RecognizerResultspans, each carrying the detected entity type, character offsets, and a confidence score, which downstream logic can inspect or filter before committing to redaction. - AnonymizerEngine — The Presidio component that consumes
RecognizerResultspans fromAnalyzerEngineand rewrites each detected region according to the per-entityOperatorConfigstrategy (mask,replace, orhash), returning the sanitized string. - OperatorConfig — A Presidio configuration object that binds a redaction strategy to a specific entity type; in the pipeline, a single strategy string (e.g.,
"mask") is mapped uniformly across all declared entity types beforeAnonymizerEngine.anonymizeis called. - Dual-layer redaction pipeline — The two-pass architecture in which
PresidioLayerhandles structured fields first (cheap, deterministic) andLlamaGuardLayerhandles contextual identifiers second (semantic, LLM-powered), together closing the coverage gap that either approach leaves on its own.
Concepts
Why One Detection Strategy Is Never Enough
Customer records rarely carry PII in a single, cleanly formatted field. A record might contain a well-formed email address alongside a free-text note reading "Head of Operations at FinServ Partners, usually in the Chicago loop." The email is trivially detected by a pattern matcher; the note contains no PII field by any regex definition, yet the combination of title, company, and city narrows to one identifiable person. This is the core tension in PII redaction: structured fields are deterministic and fast to catch, while contextual fields require semantic reasoning about the whole sentence.
A single-layer system forces a trade-off — run an LLM on every record (high coverage, high cost) or run only pattern matching (low cost, incomplete). The dual-layer approach in this lesson resolves that trade-off by sequencing the two strategies: pattern matching handles the common case cheaply, and the LLM pass is reserved for edge cases that would otherwise slip through silently.
Pattern-Based Detection with Presidio
Presidio separates detection and anonymization into two distinct engines, which is the key to its composability. AnalyzerEngine.analyze scans text for a declared set of entity types and returns a list of RecognizerResult spans with character offsets and confidence scores. Those spans are then handed to AnonymizerEngine.anonymize along with an OperatorConfig map that assigns a redaction strategy to each entity type. Because detection and anonymization are decoupled, the pipeline can inspect or filter spans — for example, dropping low-confidence hits — before writing any masked output. This clean separation also makes PresidioLayer straightforward to unit-test in isolation (see Code Walkthrough).
Contextual Detection with LlamaGuard 4
LlamaGuard 4 acts as a policy evaluator rather than a span extractor. The pipeline sends the full text as a user message and a system prompt that defines the safety policy — including indirect identification through combinations of role, company, and location. The model returns a verdict containing safe or a description of the violation. Because LlamaGuard 4 reasons over the entire text holistically, it catches a pattern like "VP of Engineering at Acme Corp, Portland office on Tuesdays" that no span-based recognizer would flag (see Code Walkthrough). The {"safe": ..., "raw": ...} response shape keeps downstream handling simple: check the boolean, log the raw content for audit, and decide whether to block or redact.
Pipeline Ordering for Latency and Coverage
Presidio always runs first because it is fast and its false-negative rate on structured fields is very low. LlamaGuardLayer.detect_contextual_pii is invoked only when Presidio returns low-confidence results or when the text contains no structured fields at all. This ordering ensures the slower, more expensive LLM call is reserved for records that actually need it — keeping per-record latency acceptable on the common case while preserving full coverage on the contextual edge cases that matter most for regulatory compliance.
Code Walkthrough
Now that you understand how structured and contextual PII differ, the two-layer pipeline stitches those detection strategies into a single redaction pass.
The first layer uses Presidio to catch predictable, pattern-bound fields: email addresses, phone numbers, US Social Security numbers, credit card numbers, and IP addresses. AnalyzerEngine scans the text and returns a list of detected spans; AnonymizerEngine replaces each span according to the chosen strategy — mask, replace, or hash:
Code snippetpython
1from presidio_analyzer import AnalyzerEngine, RecognizerResult 2from presidio_anonymizer import AnonymizerEngine 3from presidio_anonymizer.entities import OperatorConfig 4from typing import List 5 6class PresidioLayer: 7 """Structured PII detection using Presidio.""" 8 9 def __init__(self): 10 self.analyzer = AnalyzerEngine() 11 self.anonymizer = AnonymizerEngine() 12 13 def detect(self, text: str) -> List[RecognizerResult]: 14 return self.analyzer.analyze( 15 text=text, 16 language="en", 17 entities=[ 18 "PERSON", "EMAIL_ADDRESS", "PHONE_NUMBER", 19 "US_SSN", "CREDIT_CARD", "IP_ADDRESS", 20 ], 21 ) 22 23 def redact(self, text: str, results: List[RecognizerResult], strategy: str = "mask") -> str: 24 entities = ["PERSON", "EMAIL_ADDRESS", "PHONE_NUMBER", "US_SSN", "CREDIT_CARD", "IP_ADDRESS"] 25 operators = {e: OperatorConfig(strategy) for e in entities} 26 anonymized = self.anonymizer.anonymize( 27 text=text, 28 analyzer_results=results, 29 operators=operators, 30 ) 31 return anonymized.text
The second layer routes text through LlamaGuard 4 to catch contextual identifiers that pattern matching misses. A support ticket reading "the VP of Engineering at Acme Corp, usually in the Portland office on Tuesdays" contains no single PII field — but the combination of role, company, and location narrows to one specific person. LlamaGuard 4 evaluates the full text against a safety policy that treats indirect identification as a violation:
Code snippetpython
1import openai 2 3class LlamaGuardLayer: 4 """Contextual PII detection using LlamaGuard 4.""" 5 6 def __init__(self, proxy_url: str): 7 self.client = openai.OpenAI( 8 api_key="student-token", 9 base_url=proxy_url, 10 ) 11 12 def detect_contextual_pii(self, text: str) -> dict: 13 response = self.client.chat.completions.create( 14 model="llamaguard-4", 15 messages=[ 16 { 17 "role": "system", 18 "content": ( 19 "Analyze the following text for PII exposure. " 20 "Identify any information that could be used to " 21 "identify a specific individual, including indirect " 22 "identifiers like job title + company + location." 23 ), 24 }, 25 {"role": "user", "content": text}, 26 ], 27 ) 28 content = response.choices[0].message.content 29 return {"safe": "safe" in content.lower(), "raw": content}
In practice, PresidioLayer.detect runs first. Only when Presidio returns low-confidence results — or when the text has no structured fields at all — does the pipeline invoke LlamaGuardLayer.detect_contextual_pii for a second pass. This ordering keeps latency low on the common case while ensuring contextual edge cases are not silently passed through.
Verify by running the pipeline against a sample record that contains an email address, a phone number, and a contextual combination such as "Director of Finance at RegTech Partners, Seattle office" — all three identifiers should appear masked or flagged in the redacted output.
Do's and Don'ts
Having walked through PII redaction above, the following Do's and Don'ts distill it into practice.
Do's
- ✓Do run
PresidioLayer.detectfirst on every record — pattern-based detection handles the common case (emails, phone numbers, SSNs, credit card numbers) at low latency, and only escalate toLlamaGuardLayer.detect_contextual_piiwhen Presidio returns low-confidence results or the text contains no structured fields, keeping pipeline throughput high without silently passing contextual PII. - ✓Do configure an
OperatorConfigfor every entity type you pass toAnonymizerEngine— if an entity appears inAnalyzerEngine.analyzeresults but has no corresponding operator, Presidio falls back to a default behavior that may not match your masking policy, leaving spans inconsistently redacted across record types. - ✓Do treat quasi-identifier combinations (role + company + location) as PII warranting a LlamaGuard 4 pass — a support ticket reading "VP of Engineering at Acme Corp, Portland office on Tuesdays" contains no single structured field, yet it uniquely identifies one person; routing such text through the contextual layer is the only way to catch indirect identification before it reaches the embedding or storage layer.
Don'ts
- ✗Don't call
LlamaGuardLayer.detect_contextual_piion every record unconditionally — routing all text through LlamaGuard 4 when Presidio would have caught the PII with high confidence adds unnecessary LLM-call latency to the common case and undermines the dual-layer ordering the pipeline depends on. - ✗Don't pass the same entity list to
AnonymizerEnginewithout first passing it toAnalyzerEngine— the anonymizer operates on the span offsets that the analyzer emits; constructing operators for entities the analyzer was never asked to detect produces orphaned configs and misses PII that was never scanned. - ✗Don't assume contextual PII is absent just because
PresidioLayer.detectreturns an empty result list — Presidio is pattern-bound and will return no spans for text like "Director of Finance at RegTech Partners, Seattle office"; treating an empty analyzer result as a redaction clearance skips the LlamaGuard 4 check that the pipeline specifically exists to provide.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the Forward Deployed GenAI Engineering subscription.
From · cancel anytime
More free lessons in AI Solution Delivery
- Ch 3Detect risky contract language with NeMo Guardrails
- Ch 4Build a RAG prototype with pgvector retrieval
- Ch 4Package prototypes with Dockerfiles, Helm charts, and K8s manifests
- Ch 5Detect and redact PII with Presidio and LlamaGuard 4You are here
- Ch 6Generate K8s manifests from customer-parameterized Jinja2 templates
- Ch 6Manage K8s secrets with rotation and init-container injection
- Ch 6Log compliance events as OTEL traces with structured attributes