Class: Vangrail::Rails::GuardModel

Inherits:
Vangrail::Rail show all
Defined in:
lib/vangrail/rails/guard_model.rb

Overview

A safety classifier as a rail: one chat call, the model's own template does the framing, and the label it answers becomes the decision.

:llama_guard   "safe" | "unsafe\nS1,S10"
:apriel_guard  "safe\nnon_adversarial" | "unsafe-O14,O12\nadversarial"
:harmful_yes_no  "Harmful request: yes" | "Yes", the WildGuard and
               ShieldGemma shape, where the polarity belongs to the
               question the model was trained on rather than to the token
:three_label     "safe" | "unsafe" | "controversial", the Qwen Guard shape

There are at least six of these formats in current use across fourteen published guard models (Sadeghi et al., arXiv:2605.28830), and a format this gem cannot read makes every check unchecked rather than wrong. That is the safe failure and it is still a rail that does nothing, which is why the list is longer than the two it started with.

Classifiers only ever pass or block. They cannot rewrite text, so this rail never returns :modified; a redaction or policy rail does that.

Needs a provider that actually hosts one. Where none exists, Rails

SelfCheck puts a written policy in front of an instruct model instead, which is the same job done differently rather than the same job skipped.

Constant Summary collapse

PRESETS =
%i[llama_guard apriel_guard harmful_yes_no three_label].freeze
MIDDLE_LABELS =

What a third label means, where a model has one. Counted as unsafe because the alternative is measurably worse: dropping "controversial" cost one model 37.2 points of recall in the benchmark above. :safe and :undecided are available for a deployment that has decided otherwise, and neither is the default.

%i[unsafe safe undecided].freeze
REASONING_KWARGS =

Chat-template switch that turns on an assessment before the verdict. Gateways forward these to the serving engine's template, so a written rationale costs tokens and latency and nothing else.

{ 'chat_template_kwargs' => { 'reasoning_mode' => 'on' } }.freeze
REASONING_MAX_TOKENS =
900

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(provider: nil, model: nil, preset: nil, chat: nil, reasoning: false, middle: :unsafe, name: nil, sides: Rail::SIDES, max_tokens: nil, **chat_options) ⇒ GuardModel

Returns a new instance of GuardModel.

Raises:

  • (ArgumentError)


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/vangrail/rails/guard_model.rb', line 49

def initialize(provider: nil, model: nil, preset: nil, chat: nil, reasoning: false,
               middle: :unsafe, name: nil, sides: Rail::SIDES, max_tokens: nil, **chat_options)
  @model = model || provider&.model(:guard)
  @preset = (preset || provider&.guard_preset)&.to_sym
  raise ArgumentError, 'a guard rail needs a model' if @model.nil?
  unless PRESETS.include?(@preset)
    raise ArgumentError, "preset must be one of #{PRESETS.join(', ')}; " \
                         'a model answering a written policy belongs in Rails::SelfCheck'
  end

  @middle = middle.to_sym
  unless MIDDLE_LABELS.include?(@middle)
    raise ArgumentError, "middle must be one of #{MIDDLE_LABELS.join(', ')}"
  end

  @reasoning = reasoning && @preset == :apriel_guard
  super(name: name || @preset.to_s, sides: sides)
  @chat = chat || build_chat(provider, max_tokens, chat_options)
end

Instance Attribute Details

#chatObject (readonly)

Returns the value of attribute chat.



47
48
49
# File 'lib/vangrail/rails/guard_model.rb', line 47

def chat
  @chat
end

#middleObject (readonly)

Returns the value of attribute middle.



47
48
49
# File 'lib/vangrail/rails/guard_model.rb', line 47

def middle
  @middle
end

#modelObject (readonly)

Returns the value of attribute model.



47
48
49
# File 'lib/vangrail/rails/guard_model.rb', line 47

def model
  @model
end

#presetObject (readonly)

Returns the value of attribute preset.



47
48
49
# File 'lib/vangrail/rails/guard_model.rb', line 47

def preset
  @preset
end

#reasoningObject (readonly)

Returns the value of attribute reasoning.



47
48
49
# File 'lib/vangrail/rails/guard_model.rb', line 47

def reasoning
  @reasoning
end

Instance Method Details

#cache_key(text, context) ⇒ Object

The verdict depends on the text and, on the output side, on the user turn sent with it.



75
76
77
78
79
# File 'lib/vangrail/rails/guard_model.rb', line 75

def cache_key(text, context)
  return text if context[:side] == :input

  "#{context[:user_input]} #{text}"
end

#decide(text, context) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/vangrail/rails/guard_model.rb', line 81

def decide(text, context)
  answer = chat.ask(messages_for(text, context))
  parsed = parse(answer.text)
  unless parsed[:decided]
    return unchecked("unparsed guard response: #{parsed[:reason]}",
                     model: model, latency_ms: answer.latency_ms, raw: answer.raw)
  end

  return pass(model: model, latency_ms: answer.latency_ms, raw: answer.raw) unless parsed[:violated]

  block(reason: parsed[:reason], categories: parsed[:categories], model: model,
        latency_ms: answer.latency_ms, raw: answer.raw)
end

#offline?Boolean

Returns:

  • (Boolean)


69
70
71
# File 'lib/vangrail/rails/guard_model.rb', line 69

def offline?
  false
end