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"

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].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, name: nil, sides: Rail::SIDES, max_tokens: nil, **chat_options) ⇒ GuardModel

Returns a new instance of GuardModel.

Raises:

  • (ArgumentError)


32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/vangrail/rails/guard_model.rb', line 32

def initialize(provider: nil, model: nil, preset: nil, chat: nil, reasoning: false,
               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

  @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.



30
31
32
# File 'lib/vangrail/rails/guard_model.rb', line 30

def chat
  @chat
end

#modelObject (readonly)

Returns the value of attribute model.



30
31
32
# File 'lib/vangrail/rails/guard_model.rb', line 30

def model
  @model
end

#presetObject (readonly)

Returns the value of attribute preset.



30
31
32
# File 'lib/vangrail/rails/guard_model.rb', line 30

def preset
  @preset
end

#reasoningObject (readonly)

Returns the value of attribute reasoning.



30
31
32
# File 'lib/vangrail/rails/guard_model.rb', line 30

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.



49
50
51
52
53
# File 'lib/vangrail/rails/guard_model.rb', line 49

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

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

#call(text, context) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/vangrail/rails/guard_model.rb', line 55

def call(text, context)
  answer = chat.ask(messages_for(text, context))
  parsed = preset == :apriel_guard ? Parsers.apriel_guard(answer.text) : Parsers.llama_guard(answer.text)
  unless parsed[:decided]
    return Result.new(status: :passed, rail: name, certain: false, model: model,
                      latency_ms: answer.latency_ms, raw: answer.raw,
                      reason: "unparsed guard response: #{parsed[:reason]}")
  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