Skip to main content

Tutorial: PII-redacting interceptor

In this tutorial, you will build a generic interceptor that detects personally identifiable information (PII) in user messages, replaces it with placeholders before the request reaches the model, and restores the original values in the response. By the end, you will have a working interceptor running alongside DIAL Core and a model.

Prerequisites​

  • Python 3.11 or later installed
  • Docker and Docker Compose installed
  • Basic familiarity with Python and DIAL concepts (What are interceptors)

What you will build​

A chat completion interceptor that:

  1. Scans every user message for email addresses, phone numbers, and Social Security numbers using regular expressions.
  2. Replaces each match with a numbered placeholder (e.g., john.doe@example.com → [EMAIL_1]).
  3. Forwards the anonymized request to the model.
  4. Restores the original values in the model's response.

The interceptor runs as a standalone service and is wired into DIAL Core as a local interceptor on a model deployment.

Project structure​

By the end of this tutorial, your project directory will look like this:

pii-interceptor/
├── app.py # Application entry point — registers the interceptor on DIALApp
├── interceptor.py # Interceptor logic — PII detection and replacement
├── requirements.txt # Python dependencies
├── Dockerfile # Container image for the interceptor service
├── docker-compose.yml # Runs DIAL Core + interceptor + a stand-in model
├── config.json # DIAL Core configuration — declares and assigns the interceptor
└── mock-model/ # Stand-in model, so no provider API key is needed
├── app.py
├── Dockerfile
└── requirements.txt

Step 1: Create the project​

mkdir pii-interceptor && cd pii-interceptor

Create pii-interceptor/requirements.txt:

aidial-interceptors-sdk==0.8.0
uvicorn==0.53.0

Install the dependencies:

pip install -r requirements.txt

Verify: pip show aidial-interceptors-sdk displays the package version.

Step 2: Implement the interceptor​

Create pii-interceptor/interceptor.py:

import re
from typing_extensions import override
from aidial_interceptors_sdk.chat_completion.base import (
ChatCompletionInterceptor,
)
from aidial_interceptors_sdk.utils.not_given import NotGiven


class PIIRedactorInterceptor(ChatCompletionInterceptor):
replacements: dict[str, str] = {}
reverse_replacements: dict[str, str] = {}
counter: int = 0

PII_PATTERNS: dict[str, str] = {
"EMAIL": r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
"PHONE": r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b",
"SSN": r"\b\d{3}-\d{2}-\d{4}\b",
}

def _anonymize(self, text: str) -> str:
for label, pattern in self.PII_PATTERNS.items():
for match in re.finditer(pattern, text):
original = match.group()
if original not in self.replacements:
self.counter += 1
placeholder = f"[{label}_{self.counter}]"
self.replacements[original] = placeholder
self.reverse_replacements[placeholder] = original
text = text.replace(original, self.replacements[original])
return text

def _deanonymize(self, text: str) -> str:
for placeholder, original in self.reverse_replacements.items():
text = text.replace(placeholder, original)
return text

@override
async def on_request_message(self, path, message: dict) -> list[dict]:
if content := message.get("content"):
message["content"] = self._anonymize(content)
return [message]

@override
async def on_response_message(
self, path, message: dict | NotGiven | None
) -> dict | NotGiven | None:
if isinstance(message, dict):
if content := message.get("content"):
message["content"] = self._deanonymize(content)
return message

This interceptor uses regular expressions to detect emails, phone numbers, and Social Security numbers. Production interceptors typically use NLP libraries like spaCy — see the spaCy anonymizer example for that approach.

Verify: python -c "import interceptor" runs without errors.

Step 3: Create the application entry point​

Create pii-interceptor/app.py. This file wires the interceptor class into a DIALApp instance so it can serve requests at the standard DIAL endpoint path:

import os

from aidial_sdk import DIALApp
from aidial_sdk.telemetry.types import TelemetryConfig
from aidial_interceptors_sdk.chat_completion import (
interceptor_to_chat_completion,
)
from aidial_interceptors_sdk.utils._http_client import get_http_client

from interceptor import PIIRedactorInterceptor

DIAL_URL = os.environ.get("DIAL_URL", "http://core:8080")


async def client_factory():
return get_http_client()


app = DIALApp(
dial_url=DIAL_URL,
telemetry_config=TelemetryConfig(),
add_healthcheck=True,
propagate_auth_headers=True,
allow_extra_request_fields=True,
)

app.add_chat_completion(
"pii-redactor",
interceptor_to_chat_completion(
PIIRedactorInterceptor, DIAL_URL, client_factory
),
)

Verify: python -c "from app import app; print(type(app))" prints <class 'aidial_sdk.app.DIALApp'>.

Step 4: Create the Dockerfile​

Create pii-interceptor/Dockerfile:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY interceptor.py app.py ./

EXPOSE 5000

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "5000"]

Step 5: Create the Docker Compose configuration​

Create pii-interceptor/docker-compose.yml. This starts DIAL Core with Redis, the interceptor service, and a stand-in model:

services:
pii-interceptor:
build: .
environment:
DIAL_URL: "http://core:8080"
LOG_LEVEL: "DEBUG"
depends_on:
- core

mock-model:
build: mock-model

core:
image: epam/ai-dial-core:0.47.1
environment:
aidial.config.files: '["/opt/config/config.json"]'
aidial.storage.overrides: '{ "jclouds.filesystem.basedir": "data" }'
aidial.redis.singleServerConfig.address: "redis://redis:6379"
aidial.identityProviders.test.rolePath: 'roles'
aidial.identityProviders.test.disableJwtVerification: 'true'
aidial.encryption.secret: 'salt'
aidial.encryption.key: 'password'
ports:
- "8080:8080"
volumes:
- ./config.json:/opt/config/config.json
depends_on:
- redis

redis:
image: redis:7.2.4-alpine3.19
command: >
redis-server
--maxmemory 256mb
--maxmemory-policy volatile-lfu
--save ""
--appendonly no

The mock-model service stands in for a real model so that the tutorial runs with no provider account. It replies with your message and also reports what it received, which is how you will see the redaction in Step 8. Its source is in the sample repository; to point the interceptor at a real model instead, swap the endpoint in Step 6 for your adapter's.

Add DIAL Chat

To use the interceptor through the web UI rather than curl, add the chat and themes services from Run DIAL with the echo application to this file.

Step 6: Configure DIAL Core​

Create pii-interceptor/config.json. This file declares the interceptor and assigns it to a model deployment. Docker Compose mounts it into the DIAL Core container at /opt/config/config.json:

{
"keys": {
"dial_api_key": { "project": "TEST-PROJECT", "role": "default" }
},
"roles": {
"default": { "limits": { "protected-model": {} } }
},
"interceptors": {
"pii-redactor": {
"endpoint": "http://pii-interceptor:5000/openai/deployments/pii-redactor/chat/completions",
"displayName": "PII Redaction",
"description": "Anonymizes PII in requests and restores it in responses"
}
},
"models": {
"protected-model": {
"type": "chat",
"displayName": "Mock model (PII-protected)",
"endpoint": "http://mock-model:5000/openai/deployments/mock-model/chat/completions",
"interceptors": ["pii-redactor"]
}
}
}

Two things wire the interceptor in: the interceptors block declares it as a service, and the interceptors array on the model assigns it to that deployment. The roles block grants the API key access to the model. To protect a real model instead, change the model's endpoint to your adapter's — the interceptor configuration stays the same.

Step 7: Build and run​

docker compose up --build

Verify: All three services start without errors. The interceptor service logs Uvicorn running on http://0.0.0.0:5000.

Step 8: Test the interceptor​

Send a message containing PII to the protected model:

curl http://localhost:8080/openai/deployments/protected-model/chat/completions \
-H "Api-Key: dial_api_key" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "role": "user", "content": "My email is john.doe@example.com and my phone is 555-123-4567." }
]
}'

Verify: the response contains two parts.

My email is john.doe@example.com and my phone is 555-123-4567.

--- the model received ---
My email is «EMAIL_1» and my phone is «PHONE_2».

The first part is what reaches you: the interceptor has restored your original email and phone. The second is what the model actually saw — the values replaced with placeholders. The model never received the real PII.

(The stand-in model swaps the placeholder brackets for «» when reporting, so the interceptor does not restore them on the way out. A real model would simply reply to the redacted text.)

What you learned​

  • How to create a chat completion interceptor by subclassing ChatCompletionInterceptor
  • How to override on_request_message and on_response_message to transform requests and responses
  • How to wire an interceptor into a DIALApp using interceptor_to_chat_completion
  • How to register and assign an interceptor in DIAL Core configuration
  • How to run the interceptor alongside DIAL Core using Docker Compose

Further reading​