# PII Redaction POC — Solution Architecture ## Purpose This document describes the architectural design of the **PII Redaction POC**, a .NET proof-of-concept that intercepts user prompts containing regulated personally identifiable information (PII), redacts sensitive values into stable placeholders, and transmits **only sanitized text** across the LLM trust boundary. The solution is structured for enterprise adoption: clear layer separation, interface-driven composition, dependency injection, and swappable infrastructure adapters (ONNX NER, `Microsoft.Extensions.AI` chat clients). The POC validates a compliance-oriented pattern suitable for financial and customer-service workloads where raw PII must not leave the application process when invoking external language models. --- ## Canonical Example The console application ships with a **sample catalog** (11 prompts). The canonical demo is sample `FullFinancialWithCustomer`. The table below shows the exact strings produced by the production pipeline when the ONNX NER model is loaded (run `scripts/download-ner-model.ps1` first). | Stage | Value | |-------|-------| | **Input** | `Customer Ravi Kumar with email ravi.kumar@gmail.com and phone 9876543210 has LoanNumber LN-456789 and PAN ABCDE1234F. Please summarize this customer issue.` | | **Sanitized Output** | `Customer with email and phone has LoanNumber and PAN . Please summarize this customer issue.` | | **Mock LLM Response** | `[Mock LLM Response] Received sanitized prompt (146 chars). No original PII was transmitted.` | Detected entities for this prompt: | Type | Value | Detection Source | |------|-------|------------------| | PERSON | Ravi Kumar | Ner | | EMAIL | ravi.kumar@gmail.com | Regex | | PHONE | 9876543210 | Regex | | LOAN_NUMBER | LN-456789 | Domain | | PAN | ABCDE1234F | Regex | The internal placeholder map (`` → `Ravi Kumar`, etc.) is retained in-process and is **not** included in the outbound LLM request. --- ## Console Sample Catalog Running `dotnet run --project src/PiiRedaction.ConsoleApp` executes all samples sequentially. Use `--list`, `--sample N`, or `--name SampleName` to filter. ### NER / person-name samples These prompts exercise `OnnxNerPiiDetector` and `OnnxNerModelRunner`. Person names require the ONNX model (`models/ner-model.onnx` plus `vocab.txt` and `ner-labels.txt`). Without the model, person spans are not detected. | Sample | Input (excerpt) | Detected person | Sanitized (excerpt) | |--------|-----------------|-----------------|---------------------| | **CustomerNameOnly** | Customer Anita Sharma reported unauthorized… | Anita Sharma | Customer `` reported unauthorized… | | **MrTitlePerson** | Mr. John Smith called about a duplicate debit… | John Smith | `` called about a duplicate debit… | | **MrsTitlePerson** | Mrs. Lakshmi Reddy requested a callback regarding LN-112233. | Lakshmi Reddy | `` requested a callback regarding ``. | | **DrTitlePerson** | Dr. Jane Doe escalated a complaint… | Jane Doe | `` escalated a complaint… | | **TwoCustomersInOnePrompt** | Customer Ravi Kumar and Customer Priya Nair… | Ravi Kumar, Priya Nair | Customer `` and Customer ``… | | **PersonWithDomainIds** | Customer Meera Iyer holds CID-7070… | Meera Iyer | Customer `` holds ``… | | **PersonWithEmailNoPhone** | Customer Arjun Mehta wrote from arjun.mehta@company.in… | Arjun Mehta | Customer `` wrote from ``… | ### Other sample categories | Category | Sample | Purpose | |----------|--------|---------| | NER + Regex + Domain | FullFinancialWithCustomer | End-to-end financial prompt (canonical) | | Regex only | AllRegexTypes | Email, phone, PAN, Aadhaar, credit card | | Domain only | AllDomainIds | Loan number, customer ID, account number | | Negative | NoPiiCleanTicket | Passthrough with no detected PII | Sample definitions live in [`SamplePromptCatalog.cs`](../src/PiiRedaction.ConsoleApp/Samples/SamplePromptCatalog.cs). --- ## High-Level Data Flow The diagram below traces the canonical example from console input through Core sanitization to the Infrastructure LLM adapter. Data labels reflect the canonical strings at each stage. ```mermaid flowchart TB subgraph consoleApp [PiiRedaction.ConsoleApp] program["Program.cs"] di["ServiceRegistration"] end subgraph core [PiiRedaction.Core] sanitizer["PromptSanitizer"] composite["CompositePiiDetector"] regexDet["RegexPiiDetector"] domainDet["DomainRulePiiDetector"] onnxDet["OnnxNerPiiDetector"] redactor["PlaceholderPiiRedactor"] end subgraph infra [PiiRedaction.Infrastructure] onnxRunner["OnnxNerModelRunner"] mockLlm["MockLlmPromptService"] mockChat["MockChatClient"] end rawPrompt["Raw prompt with PII"] sanitizedPrompt["Sanitized prompt with placeholders"] llmResponse["Mock LLM acknowledgment"] program -->|"Customer Ravi Kumar ... PAN ABCDE1234F"| sanitizer sanitizer --> composite composite --> domainDet composite --> regexDet composite --> onnxDet onnxDet --> onnxRunner sanitizer --> redactor redactor -->|"Customer PERSON_1 ... PAN PAN_1"| sanitizedPrompt program -->|"SanitizedPrompt only"| mockLlm mockLlm --> mockChat mockChat --> llmResponse rawPrompt -.-> program di -.-> sanitizer di -.-> mockLlm ``` --- ## Detection to Redaction Detail `PromptSanitizer` orchestrates a two-phase pipeline: **detect** then **redact**. `CompositePiiDetector` aggregates spans from all registered detectors, resolves overlaps by registration order and source priority, and returns a merged entity list. `PlaceholderPiiRedactor` replaces spans right-to-left to preserve indices, assigns stable per-type counters, and builds the in-process placeholder map. ```mermaid flowchart LR inputText["Original prompt text"] subgraph detectPhase [Detection Phase] domainDet["DomainRulePiiDetector"] regexDet["RegexPiiDetector"] onnxDet["OnnxNerPiiDetector"] composite["CompositePiiDetector"] merge["Overlap merge and source priority"] entityList["PiiEntity list"] end subgraph redactPhase [Redaction Phase] redactor["PlaceholderPiiRedactor"] replace["Right-to-left span replacement"] placeholderMap["Placeholder map in-process"] sanitizedText["Sanitized text"] end inputText --> domainDet inputText --> regexDet inputText --> onnxDet domainDet --> composite regexDet --> composite onnxDet --> composite composite --> merge merge --> entityList entityList --> redactor inputText --> redactor redactor --> replace replace --> sanitizedText replace --> placeholderMap ``` **Overlap resolution rules** (applied by `CompositePiiDetector`): 1. Detectors run in registration order: **Domain → Regex → ONNX NER**. 2. On overlapping spans, the first registered detector wins. 3. Tie-breaking uses source priority: Domain (3) > Regex (2) > NER (1). **Placeholder assignment** (applied by `PlaceholderPiiRedactor`): - Format: `<{TYPE}_{n}>` (e.g. ``, ``). - Duplicate values of the same type reuse the same placeholder. - Replacement proceeds from highest `StartIndex` to lowest to avoid index drift. --- ## Runtime Sequence ```mermaid sequenceDiagram participant User participant Program as Program.cs participant DI as ServiceProvider participant Sanitizer as PromptSanitizer participant Detector as CompositePiiDetector participant Redactor as PlaceholderPiiRedactor participant LlmSvc as MockLlmPromptService participant Chat as MockChatClient User->>Program: Start application Program->>DI: Resolve IPromptSanitizer, ILlmPromptService DI-->>Program: Sanitizer, LlmService alt Interactive mode User->>Program: Enter prompt via console else Default mode Program->>Program: Load canonical sample prompt end Program->>Sanitizer: Sanitize(SanitizationRequest) Sanitizer->>Detector: Detect(originalPrompt) Detector-->>Sanitizer: IReadOnlyList PiiEntity Sanitizer->>Redactor: Redact(originalPrompt, entities) Redactor-->>Sanitizer: RedactionResult Sanitizer-->>Program: SanitizationResult Program->>Program: Display detected entities Program->>Program: Display sanitized prompt Program->>Program: Display placeholder map in-process Program->>LlmSvc: SendPromptAsync(sanitizedPrompt) Note over Program,LlmSvc: Placeholder map never passed LlmSvc->>Chat: GetResponseAsync(user message) Chat-->>LlmSvc: Assistant response LlmSvc-->>Program: Mock LLM response string Program->>User: Write LLM response to console ``` --- ## Trust Boundary The LLM boundary is the point at which data leaves the application process via `ILlmPromptService` / `IChatClient`. Only the sanitized prompt crosses this boundary. Original PII values, detection metadata, and the placeholder-to-value map remain in-process. ```mermaid flowchart TB subgraph inProcess [In-Process Trust Zone] originalPrompt["Original prompt with raw PII"] detectedEntities["Detected PiiEntity list"] placeholderMap["Placeholder map"] sanitizationResult["SanitizationResult"] consoleDisplay["Console audit output"] end subgraph llmBoundary [LLM Trust Boundary] sanitizedOnly["Sanitized prompt text only"] end subgraph externalLlm [External LLM Provider] chatClient["IChatClient implementation"] modelInference["Model inference"] end originalPrompt --> sanitizationResult detectedEntities --> sanitizationResult placeholderMap --> sanitizationResult sanitizationResult --> consoleDisplay sanitizationResult -->|"SendPromptAsync"| sanitizedOnly sanitizedOnly --> chatClient chatClient --> modelInference originalPrompt -.-x|"Never transmitted"| chatClient placeholderMap -.-x|"Never transmitted"| chatClient detectedEntities -.-x|"Never transmitted"| chatClient ``` In the POC, `MockChatClient` simulates the external provider without network I/O. Replacing it with Azure OpenAI or another `IChatClient` implementation does not change the trust model: `MockLlmPromptService` (or a future production adapter) continues to accept only the sanitized string. --- ## Project Responsibilities | Project | Layer | Responsibility | |---------|-------|----------------| | `PiiRedaction.ConsoleApp` | Presentation | Application entry point; reads prompt (sample or interactive); bootstraps `IHost` and DI via `AddPiiRedactionServices`; orchestrates sanitization and LLM invocation; renders audit output (detected entities, sanitized text, placeholder map). | | `PiiRedaction.Core` | Domain / Application | Defines abstractions (`IPiiDetector`, `IPiiRedactor`, `IPromptSanitizer`, `ILlmPromptService`); implements detection strategies (`RegexPiiDetector`, `DomainRulePiiDetector`, `OnnxNerPiiDetector`, `CompositePiiDetector`); implements `PlaceholderPiiRedactor` and `PromptSanitizer`; owns domain models (`PiiEntity`, `SanitizationResult`, `RedactionResult`) and configuration (`PiiRedactionOptions`). Has no dependency on ONNX Runtime or LLM SDKs. | | `PiiRedaction.Infrastructure` | Infrastructure | Implements technical adapters: `OnnxNerModelRunner` (ONNX Runtime inference), `MockChatClient` and `MockLlmPromptService` (`Microsoft.Extensions.AI`); depends on Core abstractions and is swappable without changing domain logic. | | `tests/PiiRedaction.Core.Tests` | Test | Unit and integration tests for detectors, redactor, sanitizer, overlap rules, golden prompt scenarios (`PromptScenarioCatalog`), and LLM boundary assertions. | | `tests/PiiRedaction.Infrastructure.Tests` | Test | Tests for mock LLM behavior and ONNX runner load semantics. | **Dependency direction:** `ConsoleApp` → `Infrastructure` → `Core`. Core references no outer layers, preserving the Dependency Inversion Principle and enabling future hosts (ASP.NET Core API, worker services) to reuse the same Core and Infrastructure assemblies. --- ## Key Abstractions and Extension Points | Abstraction | Defined In | Default Implementation | Extension | |-------------|------------|------------------------|-----------| | `IPiiDetector` | Core | `CompositePiiDetector` wrapping Domain, Regex, ONNX | Add new detector; register in composite order | | `IPiiRedactor` | Core | `PlaceholderPiiRedactor` | Replace with hashing, vault-backed tokens, etc. | | `IPromptSanitizer` | Core | `PromptSanitizer` | Unlikely to change; orchestrates detect + redact | | `ILlmPromptService` | Core | `MockLlmPromptService` | Production adapter with telemetry, retry, policy | | `IChatClient` | Microsoft.Extensions.AI | `MockChatClient` | Azure OpenAI, OpenAI, or other provider SDK | | `IOnnxNerModelRunner` | Core | `OnnxNerModelRunner` | BERT WordPiece tokenization, ONNX inference, BIO label decoding | --- ## Configuration Surface Runtime behavior is controlled via `appsettings.json` under the `PiiRedaction` section: | Setting | Effect | |---------|--------| | `OnnxModelPath` | Path to ONNX NER model (`models/ner-model.onnx` by default). Companion files `vocab.txt` and `ner-labels.txt` must live in the same directory. | Download the model assets with `scripts/download-ner-model.ps1` (exports `dslim/bert-base-NER`). --- ## Related Documentation - [README](../README.md) — build, run, configuration, and testing instructions - [ServiceCollectionExtensions.cs](../src/PiiRedaction.ConsoleApp/DependencyInjection/ServiceCollectionExtensions.cs) — DI registration and detector ordering - [PromptScenarioCatalog.cs](../tests/PiiRedaction.Core.Tests/TestSupport/PromptScenarioCatalog.cs) — focused golden pipeline scenarios including the canonical example