Class: Vangrail::Rails::Similarity
- Inherits:
-
Vangrail::Rail
- Object
- Vangrail::Rail
- Vangrail::Rails::Similarity
- Defined in:
- lib/vangrail/rails/similarity.rb
Overview
Catches the known attack that was edited rather than reworded.
A published jailbreak spreads by being pasted, and what arrives is a near copy: a typo, two words inserted, different capitals, a word inflected differently, an exclamation mark added. A regexp misses all of those unless somebody widens it for each one, and every widening is spent from the same false-positive budget. Near-duplicate detection is the standard answer, and the standard measure is containment over character n-grams: how much of the known sentence is present in the text, rather than how similar the two are overall.
Containment rather than Jaccard, because the case is a sentence inside a page. A wiki page with one pasted jailbreak in it is 99% ordinary prose, so overlap over union is near zero however exact the copy, and the measure that answers "is this in there" is the one that divides by the seed.
Clause by clause rather than page by page, and that is not a detail. A long page accidentally contains most of the four-character n-grams of any short English sentence: measured against the benign corpus in this repo, a page of ordinary documentation scores 0.94 against a seed it does not contain, and the same corpus split into clauses scores 0.67. Containment saturates with length, so the comparison has to be against a span the size of the thing being looked for.
What this does not do is judge. A clause either reproduces a sentence somebody published or it does not; there is no scoring of intent, and a novel attack in nobody's corpus scores zero here by construction.
Constant Summary collapse
- THRESHOLD =
Measured on the corpora in this repo: ordinary documentation tops out at 0.67 against the nearest seed, and edited copies of the seeds bottom out at 0.83. The gap is where the threshold goes, and 0.75 is the middle of it rather than a round number picked first.
0.75- SHINGLE =
Four characters: long enough that a shingle is a fragment of a word rather than a letter pair, short enough that a typo costs four shingles instead of a whole token.
4
Instance Attribute Summary collapse
-
#seeds ⇒ Object
readonly
Returns the value of attribute seeds.
-
#threshold ⇒ Object
readonly
Returns the value of attribute threshold.
Instance Method Summary collapse
- #cache_key(text, _context) ⇒ Object
- #call(text, _context) ⇒ Object
-
#initialize(seeds: KnownAttacks::ALL, threshold: THRESHOLD, shingle: SHINGLE, name: 'similarity', sides: %i[input context])) ⇒ Similarity
constructor
A new instance of Similarity.
-
#nearest(text) ⇒ Object
The closest seed and how much of it is present, for a caller that wants the number rather than the verdict.
- #offline? ⇒ Boolean
Constructor Details
#initialize(seeds: KnownAttacks::ALL, threshold: THRESHOLD, shingle: SHINGLE, name: 'similarity', sides: %i[input context])) ⇒ Similarity
Returns a new instance of Similarity.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/vangrail/rails/similarity.rb', line 51 def initialize(seeds: KnownAttacks::ALL, threshold: THRESHOLD, shingle: SHINGLE, name: 'similarity', sides: %i[input context]) super(name: name, sides: sides) @threshold = threshold @shingle = shingle @seeds = seeds.map { |seed| [seed, NLP.shingles(seed, size: shingle)] }.freeze # Every n-gram any seed contains, and the smallest number of them a # clause needs before any seed can possibly clear the threshold. One # intersection against this decides whether the clause is worth # comparing seed by seed, and on ordinary prose it decides no. The # bound is exact rather than heuristic: a clause sharing fewer grams # with the union than the shortest seed needs cannot contain any seed. @union = @seeds.flat_map { |(_, shingles)| shingles.to_a }.to_set.freeze @floor = (threshold * @seeds.map { |(_, shingles)| shingles.size }.min).ceil end |
Instance Attribute Details
#seeds ⇒ Object (readonly)
Returns the value of attribute seeds.
49 50 51 |
# File 'lib/vangrail/rails/similarity.rb', line 49 def seeds @seeds end |
#threshold ⇒ Object (readonly)
Returns the value of attribute threshold.
49 50 51 |
# File 'lib/vangrail/rails/similarity.rb', line 49 def threshold @threshold end |
Instance Method Details
#cache_key(text, _context) ⇒ Object
71 72 73 |
# File 'lib/vangrail/rails/similarity.rb', line 71 def cache_key(text, _context) "#{threshold}\n#{text}" end |
#call(text, _context) ⇒ Object
75 76 77 78 79 80 81 |
# File 'lib/vangrail/rails/similarity.rb', line 75 def call(text, _context) score, seed = nearest(text) return pass if score < threshold block(categories: ['known_attack'], reason: format('near copy of a known attack (%<score>.2f): %<seed>s', score: score, seed: seed)) end |
#nearest(text) ⇒ Object
The closest seed and how much of it is present, for a caller that wants the number rather than the verdict.
85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/vangrail/rails/similarity.rb', line 85 def nearest(text) best = [0.0, nil] NLP.clauses(text).each do |clause| present = NLP.shingles(clause, size: @shingle) next if (present & @union).size < @floor seeds.each do |(seed, shingles)| score = NLP.containment(shingles, present) best = [score, seed] if score > best.first end end best end |
#offline? ⇒ Boolean
67 68 69 |
# File 'lib/vangrail/rails/similarity.rb', line 67 def offline? true end |