Class: PromptSanitizer::Sanitizer

Inherits:
Object
  • Object
show all
Defined in:
lib/prompt_sanitizer/sanitizer.rb

Overview

Core sanitization class — the main public interface.

Ties together RegexEngine, SecretsEngine, optional NEREngine, SyntheticEngine, Vault, and AuditLog.

Usage (one-shot):

s = PromptSanitizer::Sanitizer.new(mode: :fast)
result = s.sanitize("Email me at [email protected]")
result.text     # => "Email me at [EMAIL_1]"
result.any?     # => true

Usage (multi-turn with session):

sess = s.session(session_id: "user-42")
clean = sess.anonymize(user_prompt)
raw_response = call_llm(clean)
final = sess.deanonymize(raw_response)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(mode: :fast, locale: "en", entities: nil, on_detect: :redact, audit_log: nil, ner_backend: :informers, ner_model: "distilbert") ⇒ Sanitizer

Returns a new instance of Sanitizer.

Parameters:

  • mode (Symbol) (defaults to: :fast)

    :fast (default), :smart, :full

  • locale (String) (defaults to: "en")

    Faker locale (e.g. "en", "fr")

  • entities (Array<Symbol>, nil) (defaults to: nil)

    whitelist; nil = all

  • on_detect (Symbol) (defaults to: :redact)

    :redact (default), :warn, :block

  • audit_log (Audit::Base, nil) (defaults to: nil)

    custom audit backend

  • ner_backend (Symbol) (defaults to: :informers)

    :informers (default) or :mitie

  • ner_model (String) (defaults to: "distilbert")

    NER model variant



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/prompt_sanitizer/sanitizer.rb', line 30

def initialize(
  mode: :fast,
  locale: "en",
  entities: nil,
  on_detect: :redact,
  audit_log: nil,
  ner_backend: :informers,
  ner_model: "distilbert"
)
  unless Mode.valid?(mode)
    raise ConfigurationError, "Invalid mode: #{mode.inspect}. Use :fast, :smart, or :full"
  end

  @mode      = mode
  @locale    = locale
  @on_detect = on_detect.to_sym
  @allowed   = entities ? Array(entities).map(&:to_sym).to_set : nil

  @regex   = Engines::RegexEngine.new
  @secrets = Engines::SecretsEngine.new
  @ner     = mode == :fast ? nil : Engines::NEREngine.new(backend: ner_backend, model: ner_model)

  @synthetic = SyntheticEngine.new(locale: locale)

  @audit = if audit_log && audit_log != :none
             audit_log
           elsif mode == :full
             Audit::MemoryAuditLog.new
           end
end

Instance Attribute Details

#auditAudit::Base? (readonly)

Returns:



65
66
67
# File 'lib/prompt_sanitizer/sanitizer.rb', line 65

def audit
  @audit
end

#modeSymbol (readonly)

Returns active detection mode.

Returns:

  • (Symbol)

    active detection mode



62
63
64
# File 'lib/prompt_sanitizer/sanitizer.rb', line 62

def mode
  @mode
end

Instance Method Details

#_run(text, vault, session_id: nil) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/prompt_sanitizer/sanitizer.rb', line 126

def _run(text, vault, session_id: nil) # rubocop:disable Naming/MethodParameterName
  return SanitizeResult.new(text: text, original: text, entities: []) if text.nil? || text.empty?

  # 1. Collect detections from all active layers
  raw = []
  raw.concat(@regex.detect(text))
  raw.concat(@secrets.detect(text))
  raw.concat(@ner.detect(text)) if @ner

  # 2. Filter to allowed entity types
  raw = raw.select { |e| @allowed.include?(e.entity_type) } if @allowed

  # 3. Deduplicate overlapping spans
  entities = _deduplicate(raw)

  # 4. Handle on_detect modes
  if @on_detect == :block && entities.any?
    raise PIIDetectedError, entities
  end

  if @on_detect == :warn
    return SanitizeResult.new(text: text, original: text, entities: entities)
  end

  # on_detect == :redact (default)

  # 5. Assign replacements — reuse vault entry when same PII seen before
  entities.each do |entity|
    existing = vault.replacement_for(entity.original)
    if existing
      entity.replacement = existing
    else
      replacement = if @mode == :full
                      @synthetic.generate(entity.entity_type, entity.original)
                    else
                      @synthetic.placeholder(entity.entity_type)
                    end
      entity.replacement = vault.add(entity.original, replacement)
    end
  end

  # 6. Reconstruct text right-to-left to preserve byte offsets
  chars = text.dup
  entities.reverse_each do |entity|
    chars[entity.start_pos...entity.end_pos] = entity.replacement.to_s
  end

  # 7. Record audit events
  if @audit
    entities.each do |entity|
      method = entity.replacement.to_s.start_with?("[") ? :placeholder : :synthetic
      @audit.record(
        Audit::AuditEvent.new(
          timestamp:        Audit.now_iso,
          entity_type:      entity.entity_type,
          confidence:       entity.confidence,
          layer:            entity.layer,
          redaction_method: method,
          text_hash:        Audit.hash_value(entity.original),
          session_id:       session_id
        )
      )
    end
  end

  SanitizeResult.new(text: chars, original: text, entities: entities)
end

#add_pattern(entity_type, pattern, confidence: 0.85) ⇒ Object

Register a custom regex pattern.

Parameters:

  • entity_type (Symbol)

    e.g. :custom or any EntityType

  • pattern (String)

    Ruby regex string

  • confidence (Float) (defaults to: 0.85)


109
110
111
# File 'lib/prompt_sanitizer/sanitizer.rb', line 109

def add_pattern(entity_type, pattern, confidence: 0.85)
  @regex.add_pattern(entity_type.to_sym, pattern, confidence: confidence)
end

#restore(text, vault:) ⇒ String

Convenience: restore vault tokens in text back to originals. Useful when you hold a vault externally.

Parameters:

  • text (String)
  • vault (Vault)

Returns:

  • (String)


119
120
121
# File 'lib/prompt_sanitizer/sanitizer.rb', line 119

def restore(text, vault:)
  vault.restore(text)
end

#sanitize(text, session_id: nil) ⇒ SanitizeResult

Sanitize text in a single-use vault. Returns a SanitizeResult.

Parameters:

  • text (String)
  • session_id (String, nil) (defaults to: nil)

    included in audit events

Returns:



74
75
76
# File 'lib/prompt_sanitizer/sanitizer.rb', line 74

def sanitize(text, session_id: nil)
  _run(text, Vault.new, session_id: session_id)
end

#sanitize_batch(texts, session_id: nil) ⇒ Array<SanitizeResult>

Sanitize a list of texts, each with its own vault.

Parameters:

  • texts (Array<String>)
  • session_id (String, nil) (defaults to: nil)

Returns:



83
84
85
# File 'lib/prompt_sanitizer/sanitizer.rb', line 83

def sanitize_batch(texts, session_id: nil)
  texts.map { |t| sanitize(t, session_id: session_id) }
end

#session(session_id: nil) {|sess| ... } ⇒ Session, Object

Create a Session for multi-turn anonymize/deanonymize workflows. The session maintains a shared vault so the same PII always maps to the same token, and LLM responses can be deanonymized.

Without a block, returns a Session you manage yourself. With a block, yields the Session and clears the vault after the block.

Parameters:

  • session_id (String, nil) (defaults to: nil)

Yield Parameters:

Returns:

  • (Session, Object)

    the Session (no block) or block return value



97
98
99
100
101
102
# File 'lib/prompt_sanitizer/sanitizer.rb', line 97

def session(session_id: nil, &block)
  sess = Session.new(self, session_id: session_id)
  return sess unless block_given?

  sess.use(&block)
end