Class: Vangrail::Rails::GuardModel
- Inherits:
-
Vangrail::Rail
- Object
- Vangrail::Rail
- Vangrail::Rails::GuardModel
- 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
-
#chat ⇒ Object
readonly
Returns the value of attribute chat.
-
#middle ⇒ Object
readonly
Returns the value of attribute middle.
-
#model ⇒ Object
readonly
Returns the value of attribute model.
-
#preset ⇒ Object
readonly
Returns the value of attribute preset.
-
#reasoning ⇒ Object
readonly
Returns the value of attribute reasoning.
Instance Method Summary collapse
-
#cache_key(text, context) ⇒ Object
The verdict depends on the text and, on the output side, on the user turn sent with it.
- #decide(text, context) ⇒ Object
-
#initialize(provider: nil, model: nil, preset: nil, chat: nil, reasoning: false, middle: :unsafe, name: nil, sides: Rail::SIDES, max_tokens: nil, **chat_options) ⇒ GuardModel
constructor
A new instance of GuardModel.
- #offline? ⇒ Boolean
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.
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, **) @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, ) end |
Instance Attribute Details
#chat ⇒ Object (readonly)
Returns the value of attribute chat.
47 48 49 |
# File 'lib/vangrail/rails/guard_model.rb', line 47 def chat @chat end |
#middle ⇒ Object (readonly)
Returns the value of attribute middle.
47 48 49 |
# File 'lib/vangrail/rails/guard_model.rb', line 47 def middle @middle end |
#model ⇒ Object (readonly)
Returns the value of attribute model.
47 48 49 |
# File 'lib/vangrail/rails/guard_model.rb', line 47 def model @model end |
#preset ⇒ Object (readonly)
Returns the value of attribute preset.
47 48 49 |
# File 'lib/vangrail/rails/guard_model.rb', line 47 def preset @preset end |
#reasoning ⇒ Object (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((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
69 70 71 |
# File 'lib/vangrail/rails/guard_model.rb', line 69 def offline? false end |