Module: Insika::Safety::Detectors
- Defined in:
- lib/insika/safety/detectors.rb
Overview
The compiler/matcher over the language-tagged pattern corpus (
C2). The pattern SOURCE lives in Safety::Corpus; every method here
reads a compiled Corpus::Compiled set, defaulting to the full shipped
corpus — byte-for-byte the runtime's pre- behavior.
Two families live here on purpose — the same lists back BOTH the runtime
guardrail AND the eval's must_not detectors: the eval
is a CLIENT of the runtime by design, so evals/lib/evals/assertions.rb
requires THIS file rather than keeping a divergent copy. The runtime must never
depend on evals/, so the file is deliberately self-contained (pure Ruby +
frozen regexes, no other Insika require) — it loads standalone from either side.
· OUTPUT side (PII/secret) — patterns that must never reach a customer turn.
Consumed by the OutputFilter (stream redaction) and the eval `pii_leak`.
· INPUT side (injection/abuse/sexual) — high-confidence heuristics that
short-circuit the turn with a safe refusal BEFORE the LLM runs.
Everything here is CONSERVATIVE by design: a false positive blocks a legitimate customer). The deterministic layer catches only the gross, unambiguous cases; the subtler judgment (social engineering, tone) is the LLM moderator's job, not regex.
LANGUAGE: the input heuristics are inherently language-specific. We ship pt-BR
- EN (the pilot + the OSS lingua franca) as a BEST-EFFORT net; other languages
rely on the LLM moderator, which is language-agnostic. A deployment clears a
language via
guardrails.corpora(docs/domain.md) — adding a language is adding data to Corpus, never core changes.
Constant Summary collapse
- OPEN_TAIL =
The universal open-tail pattern — the prefix of a PII/secret shape that might still be growing. Lives in the corpus data (universal, not language-tagged); kept here for the module's readers.
Corpus::OPEN_TAIL
Class Method Summary collapse
- .any?(patterns, text) ⇒ Boolean
-
.detect(name, text, corpus: nil) ⇒ Object
Runs a NAMED output detector over
text-> the matched substring | nil. - .first_match(patterns, text) ⇒ Object
-
.match_ranges(text, corpus: nil) ⇒ Object
[[begin, end), ...] byte-index ranges of every PII/secret match in
text(used by the OutputFilter to avoid splitting a complete match at a chunk boundary). -
.pii_names(corpus: nil) ⇒ Object
Names of the PII detectors (for iteration by the eval / config).
-
.redact(text, corpus: nil) ⇒ Object
Replaces every PII/secret occurrence with an opaque
[REDACTED:<name>]marker — the raw value NEVER survives (D "o redigido nunca aparece em claro"). -
.scan_input(text, categories: %i[injection sexual abuse],, corpus: nil) ⇒ Object
Scans a user message against the input heuristics, gated by strictness (see Insika::Safety::Config) and by the agent's compiled corpus.
Class Method Details
.any?(patterns, text) ⇒ Boolean
87 |
# File 'lib/insika/safety/detectors.rb', line 87 def any?(patterns, text) = patterns.any? { |re| re.match?(text) } |
.detect(name, text, corpus: nil) ⇒ Object
Runs a NAMED output detector over text -> the matched substring | nil.
"pii_leak" = union of all PII patterns; otherwise a single named pattern.
Unknown name fails LOUD (a typo'd assertion must never silently pass).
corpus: — the compiled set for the agent; nil = the full default.
46 47 48 |
# File 'lib/insika/safety/detectors.rb', line 46 def detect(name, text, corpus: nil) (corpus || Corpus.compile).detect_output(name, text) end |
.first_match(patterns, text) ⇒ Object
89 90 91 92 |
# File 'lib/insika/safety/detectors.rb', line 89 def first_match(patterns, text) patterns.each { |re| (m = text.match(re)) && (return m[0]) } nil end |
.match_ranges(text, corpus: nil) ⇒ Object
[[begin, end), ...] byte-index ranges of every PII/secret match in text
(used by the OutputFilter to avoid splitting a complete match at a chunk
boundary).
56 57 58 |
# File 'lib/insika/safety/detectors.rb', line 56 def match_ranges(text, corpus: nil) (corpus || Corpus.compile).match_ranges(text) end |
.pii_names(corpus: nil) ⇒ Object
Names of the PII detectors (for iteration by the eval / config).
51 |
# File 'lib/insika/safety/detectors.rb', line 51 def pii_names(corpus: nil) = (corpus || Corpus.compile).pii.keys |
.redact(text, corpus: nil) ⇒ Object
Replaces every PII/secret occurrence with an opaque [REDACTED:<name>]
marker — the raw value NEVER survives (D "o redigido nunca aparece em
claro"). -> [redacted_text, => count].
63 64 65 |
# File 'lib/insika/safety/detectors.rb', line 63 def redact(text, corpus: nil) (corpus || Corpus.compile).redact(text) end |
.scan_input(text, categories: %i[injection sexual abuse],, corpus: nil) ⇒ Object
Scans a user message against the input heuristics, gated by strictness
(see Insika::Safety::Config) and by the agent's compiled corpus.
Returns { category:, matched: } for the FIRST category that fires
(injection is checked first — the highest-stakes), or nil when the
message is clean. categories limits which families run.
:injection -> always high confidence
:sexual -> medium+
:abuse -> medium+
76 77 78 79 80 81 82 83 84 85 |
# File 'lib/insika/safety/detectors.rb', line 76 def scan_input(text, categories: %i[injection sexual abuse], corpus: nil) compiled = corpus || Corpus.compile s = text.to_s cats = compiled.input_categories(categories) return { category: :injection, matched: first_match(compiled.input["injection"], s) } if cats.include?(:injection) && any?(compiled.input["injection"], s) return { category: :sexual, matched: first_match(compiled.input["sexual"], s) } if cats.include?(:sexual) && any?(compiled.input["sexual"], s) return { category: :abuse, matched: first_match(compiled.input["abuse"], s) } if cats.include?(:abuse) && any?(compiled.input["abuse"], s) nil end |