Class: Vangrail::Rails::Escalation
- Inherits:
-
Vangrail::Rail
- Object
- Vangrail::Rail
- Vangrail::Rails::Escalation
- Defined in:
- lib/vangrail/rails/escalation.rb
Overview
Watches what happens after a refusal.
The multi-turn attacks work because the guardrail forgets. A request is refused, the next message is the same request with the objectionable word removed, and the rail reads it as a fresh question because that is all it has ever been given. Repeat until something gets through. The published multi-turn methods differ in how they choose the rewrite, and they share that one assumption: that turn N+1 is judged without turn N.
So this rail judges the sequence rather than the message. It reads
:history from the context, which a Conversation fills in, and it has
exactly two things to say:
retry_after_refusal the last question was refused, and this one is
that question again: mostly the same words, or a
bare reference back to it, or a reframing opener
("hypothetically", "just for research") on top of
it
repeated_refusals several refusals in a short window, whatever this
particular message says
Both are cheap and neither is clever. A caller that never passes
:history gets a pass with an honest certain? of false, because a rail
that reads history and was handed none has not checked anything. A caller
that passes an empty one gets a certain pass: an empty dialogue is an
answer rather than a missing one.
The limit is worth stating: a genuine crescendo never triggers a refusal at all until the last turn, and this rail sees nothing until one happens. It raises the cost of the cheap version of the attack, where the attacker probes until something lands. Judging a dialogue that has never been refused needs a model reading the trajectory, which is a different rail and a round trip.
Constant Summary collapse
- REFERENCE_BACK =
A retry does not have to be a paraphrase. It can be a pointer.
/ \A[^.?!]{0,60}\b(?:as\s+i\s+(?:said|asked|mentioned)|like\s+i\s+(?:said|asked)| (?:the|my)\s+(?:previous|last|earlier)\s+(?:question|request|message)| try\s+again|answer\s+(?:it|that|the\s+question)\s+anyway| just\s+(?:answer|tell|say)|come\s+on|continue|go\s+on|please\s+continue)\b /xi- REFRAMING =
The openers that exist to relabel a refused request as something else.
/ \b(?:hypothetically|in\s+theory|for\s+(?:a\s+)?(?:friend|research|a\s+paper| educational\s+purposes|academic\s+purposes)|purely\s+(?:academic|hypothetical)| what\s+if\s+i\s+(?:told\s+you|said)|imagine\s+(?:that\s+)?you| let\s+me\s+rephrase|to\s+(?:re)?phrase\s+(?:it|that)\s+differently| you\s+misunderstood|that\s+is\s+not\s+what\s+i\s+(?:meant|asked))\b /xi- STOP =
%w[ the a an and or but is are was were be been being to of in on at for with from by as it its this that these those i you he she they we me my your do does did how what why when where can could would should will shall may might must not no yes if then than so about into over under please ].freeze
Constants inherited from Vangrail::Rail
Vangrail::Rail::DEFAULT_SIDES, Vangrail::Rail::SIDES
Instance Attribute Summary collapse
-
#overlap ⇒ Object
readonly
Returns the value of attribute overlap.
-
#tolerance ⇒ Object
readonly
Returns the value of attribute tolerance.
-
#window ⇒ Object
readonly
Returns the value of attribute window.
Attributes inherited from Vangrail::Rail
Instance Method Summary collapse
-
#cache_key(_text, _context) ⇒ Object
Not memoizable: the same question means different things depending on what came before it, which is the entire premise of the rail.
- #call(text, context) ⇒ Object
-
#initialize(overlap: 0.6, window: 6, tolerance: 2, name: 'escalation', sides: [:input]) ⇒ Escalation
constructor
overlapis the share of this question's content words that also appeared in the refused one. - #offline? ⇒ Boolean
Methods inherited from Vangrail::Rail
#applies_to?, #placeholder?, #to_s
Constructor Details
#initialize(overlap: 0.6, window: 6, tolerance: 2, name: 'escalation', sides: [:input]) ⇒ Escalation
overlap is the share of this question's content words that also
appeared in the refused one. Three fifths is where the corpus put it: a
rewrite keeps the nouns and changes the verb, so it lands near two
thirds, while a genuine follow-up on the same subject shares one or two
words out of seven. Higher and the measured rewrites walk through;
lower and one refusal makes the topic unaskable, which ends the
conversation rather than the attack.
74 75 76 77 78 79 |
# File 'lib/vangrail/rails/escalation.rb', line 74 def initialize(overlap: 0.6, window: 6, tolerance: 2, name: 'escalation', sides: [:input]) super(name: name, sides: sides) @overlap = overlap @window = window @tolerance = tolerance end |
Instance Attribute Details
#overlap ⇒ Object (readonly)
Returns the value of attribute overlap.
65 66 67 |
# File 'lib/vangrail/rails/escalation.rb', line 65 def overlap @overlap end |
#tolerance ⇒ Object (readonly)
Returns the value of attribute tolerance.
65 66 67 |
# File 'lib/vangrail/rails/escalation.rb', line 65 def tolerance @tolerance end |
#window ⇒ Object (readonly)
Returns the value of attribute window.
65 66 67 |
# File 'lib/vangrail/rails/escalation.rb', line 65 def window @window end |
Instance Method Details
#cache_key(_text, _context) ⇒ Object
Not memoizable: the same question means different things depending on what came before it, which is the entire premise of the rail.
87 88 89 |
# File 'lib/vangrail/rails/escalation.rb', line 87 def cache_key(_text, _context) nil end |
#call(text, context) ⇒ Object
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/vangrail/rails/escalation.rb', line 91 def call(text, context) # A caller that never passes :history is not threading a dialogue, and # this rail has not checked anything: say so. A caller that passes an # empty one is threading a dialogue that has just started, which is a # real answer rather than a missing one. The distinction matters # because an uncertain pass here would otherwise be the first uncertain # result in every single-turn engine, and would mask the reason a # model rail actually failed. return unchecked('no history was provided, so nothing was compared') unless context.key?(:history) history = Array(context[:history]) return pass if history.empty? refused = history.select { |t| user?(t) && t[:blocked] } return pass if refused.empty? recent = history.last(window).count { |t| user?(t) && t[:blocked] } if recent > tolerance return block(categories: ['repeated_refusals'], reason: "#{recent} refused questions in the last #{window} turns") end retry_of(text.to_s, refused.last) end |
#offline? ⇒ Boolean
81 82 83 |
# File 'lib/vangrail/rails/escalation.rb', line 81 def offline? true end |