The Python API#
Three levels, from the smallest boundary to full control. Pick the highest one that does the job.
Level |
Use it when |
|---|---|
one prompt, one reply |
|
a conversation: the same value keeps the same label across turns |
|
you choose the policy, the detectors and the overlap rule yourself |
One exchange: encode and decode#
Use encode when you want the smallest safe boundary around one prompt:
from scikitplot.cleanprompt import encode, decode
encoded = encode(
"Mail ada@example.com about the Acme renewal",
hide=["Acme"],
)
encoded.text
# 'Mail [EMAIL-1] about the [CUSTOM-1] renewal'
Only encoded.text goes to the model. Keep encoded.handle local:
model_reply = "I will contact [EMAIL-1] about [CUSTOM-1]."
decode(model_reply, encoded.handle)
# 'I will contact ada@example.com about Acme.'
encode returns an EncodedPrompt with three parts:
EncodedPrompt.textThe redacted text intended to cross the model boundary.
EncodedPrompt.handleThe local restoration authority. It owns a
Vaultcontaining the removed values and must not be sent with the prompt.EncodedPrompt.reportLocal diagnostics describing what happened and which detection surfaces were active. Reports can include suggested terms derived from the input, so review them before logging, attaching, or transmitting them.
It unpacks as a pair:
safe_text, handle = encode("Write to ada@example.com")
Call Handle.clear when the restoration authority is no longer needed.
A Handle is frozen and its representation shows counts, never
values. Handle.export and Handle.load make it JSON-portable,
which is an explicit operation whose output contains the values — store it as
you would a password.
decode(reply, handle, strict=True) refuses a reply that contains a
placeholder the handle never issued, instead of leaving it in place.
What encode can be told#
profile="minimal","balanced"(the default) or"strict"— see Profiles below.hide=Exact additional values that must be replaced, for example a project code name.
word_boundary=Truematches them only as whole words.allow=Values that must not be replaced even when a detector finds them.
kinds=Restrict the structural pattern categories considered. Narrow with care: switching
CREDIT_CARDoff does not leave a card alone, it lets a shorter pattern match a fragment of it. Preferallow=for a specific surface.ignore_case=Match exact terms regardless of case.
ner=TrueAdd a named-entity engine;
engine=("auto","spacy","nltk","both","none"),language=,size=andmodel=select it. A request that cannot be met raisesCapabilityErrorwith the remedy — see Names, organisations and places.policy=/registry=A full
RedactionPolicyorDetectorRegistry, for the cases the keywords do not cover.
Profiles#
Profile |
Intended starting point |
Trade-off |
|---|---|---|
|
email, phone, and URL contact details |
narrowest built-in pattern surface |
|
the default high-precision structural patterns |
normal general-purpose starting point |
|
the default set plus title-case detection, with case folding |
broader coverage and more false positives |
A conversation: Session#
A conversation needs one vault across turns so that the same value keeps the
same label. Session owns that lifetime deliberately:
from scikitplot.cleanprompt import session
with session() as chat:
first = chat.encode("Mail ada@example.com")
second = chat.encode("Remind ada@example.com tomorrow")
# both turns use the same EMAIL label
restored = chat.decode("I reminded [EMAIL-1].")
Leaving the context manager clears the session’s vault. Use one session per conversation.
Session.encode returns a plain string — unlike the module-level
encode — because the session owns the one handle. Handing back a
per-turn copy would invite decoding turn three with turn one’s handle, which
silently restores the wrong values.
For model clients that already accept a callable, Session.roundtrip
keeps the boundary narrow:
with session() as chat:
reply = chat.roundtrip(
"Mail ada@example.com",
send=lambda safe: "Sent to " + safe.split()[-1],
)
The callable receives only the redacted prompt and returns the model reply. That callable is the whole integration seam: any function taking a string and returning a string works, which is why no provider needs to be known here.
Session.decode_report returns the full restoration report;
Session.turns and Session.report describe the conversation so
far without showing a value.
Full control: Redactor and restore#
Use Redactor when you need explicit detector or overlap control:
from scikitplot.cleanprompt import Redactor, restore
redactor = Redactor()
result = redactor.redact(
"Mail ada@example.com about Project North",
extra_terms=["Project North"],
)
result.text
# 'Mail [EMAIL-1] about [CUSTOM-1]'
restored = restore(result.text, result.vault)
restored.text
# 'Mail ada@example.com about Project North'
A Redactor is stateless between documents. State that must survive
turns belongs in Session, Guard, or a persisted vault rather
than hidden process globals. To continue numbering from an earlier pass, pass
its entries explicitly: redactor.redact(text, seed=previous.entries).
restore returns a RestorationResult rather than a bare string:
text, the labels it restored, the rewritten spellings it repaired, the
unknown placeholders it could not resolve, and vault entries left unused.
A policy is data#
RedactionPolicy is a frozen dataclass; RedactionPolicy.evolve
returns a new one. Two policies with the same fields have the same
fingerprint in any process, which is what makes a redaction reproducible.
from scikitplot.cleanprompt import DEFAULT_POLICY, TagStyle
surrogate = DEFAULT_POLICY.evolve(tag_style=TagStyle(style="surrogate"))
Redactor(policy=surrogate).redact("Mail ada@example.com").text
For your own invented names, load a surrogate set (Your own invented names) and attach it to the grammar, or name it in a plan:
from scikitplot.cleanprompt import FluentCleanPrompt, TagStyle, load_surrogate_set
names = load_surrogate_set("nordic.yaml") # validated; names.identity is recorded
style = TagStyle(style="surrogate", surrogate_set=names)
cleaner = FluentCleanPrompt().style("surrogate").surrogates("nordic.yaml").materialize()
Its fields are kinds, allow, tag_style (the placeholder grammar and
style), overlap (an OverlapStrategy), limits (input size,
span and term bounds; exceeding one raises), case_insensitive,
min_confidence and preserve_placeholders.
Your own detector#
A detector is an object with a name, a kind, a priority and a
detect(text, policy) method yielding Span objects over the text it
was given. RegexDetector and LiteralDetector cover most needs:
from scikitplot.cleanprompt import DetectorRegistry, LiteralDetector, default_registry
registry = default_registry()
registry.add(LiteralDetector(["Project North", "Northwind"], kind="PROJECT"))
Redactor(registry=registry).redact("Northwind ships Project North").text
# '[PROJECT-1] ships [PROJECT-2]'
RegexDetector and LiteralDetector also read the detection
view (How it works), so a term written with an invisible
character inside it is still found. A detector of your own reads it only if
it sets the class attribute reads_view = True — which is correct only when
every offset it yields indexes the string passed to detect.
Diagnosis in Python#
diagnose reports what a policy and registry can and cannot see —
active kinds, inactive kinds, tier status and blind spots — without importing
an optional dependency:
from scikitplot.cleanprompt import diagnose
report = diagnose()
report.healthy, report.headline()
An empty redaction result is not evidence that the text contains no sensitive information: it can also mean that a relevant detector is disabled or unavailable. That is why every surface reports the result together with what was looking.