Class: Riffer::Guardrails::Runner

Inherits:
Object
  • Object
show all
Defined in:
lib/riffer/guardrails/runner.rb,
sig/generated/riffer/guardrails/runner.rbs

Overview

Executes guardrails sequentially, passing each one's output to the next; if any blocks, execution stops and a tripwire is returned.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(guardrail_configs, phase:, context: nil, tags: {}) ⇒ Runner

-- : (Array[Hash[Symbol, untyped]], phase: Symbol, ?context: untyped, ?tags: Hash[String, String]) -> void

Parameters:

  • (Array[Hash[Symbol, untyped]])
  • phase: (Symbol)
  • context: (Object) (defaults to: nil)
  • tags: (Hash[String, String]) (defaults to: {})


21
22
23
24
25
26
# File 'lib/riffer/guardrails/runner.rb', line 21

def initialize(guardrail_configs, phase:, context: nil, tags: {})
  @guardrail_configs = guardrail_configs
  @phase = phase
  @context = context
  @tags = tags
end

Instance Attribute Details

#contextObject (readonly)

The context passed to guardrails.

Returns:

  • (Object)


14
15
16
# File 'lib/riffer/guardrails/runner.rb', line 14

def context
  @context
end

#guardrail_configsArray[Hash[Symbol, untyped]] (readonly)

The guardrail configs to execute.

Returns:

  • (Array[Hash[Symbol, untyped]])


8
9
10
# File 'lib/riffer/guardrails/runner.rb', line 8

def guardrail_configs
  @guardrail_configs
end

#phaseSymbol (readonly)

The execution phase (:before or :after).

Returns:

  • (Symbol)


11
12
13
# File 'lib/riffer/guardrails/runner.rb', line 11

def phase
  @phase
end

#tagsHash[String, String] (readonly)

The normalized per-call tags, stamped as riffer.tag.* on guardrail spans.

Returns:

  • (Hash[String, String])


17
18
19
# File 'lib/riffer/guardrails/runner.rb', line 17

def tags
  @tags
end

Instance Method Details

#detect_changed_indices(old_data, new_data) ⇒ Array[Integer]

-- : (untyped, untyped) -> Array

Parameters:

  • (Object)
  • (Object)

Returns:

  • (Array[Integer])


76
77
78
79
80
81
82
83
# File 'lib/riffer/guardrails/runner.rb', line 76

def detect_changed_indices(old_data, new_data)
  if old_data.is_a?(Array) && new_data.is_a?(Array)
    max_len = [old_data.length, new_data.length].max
    (0...max_len).select { |i| old_data[i] != new_data[i] }
  else
    (old_data == new_data) ? [] : [0]
  end
end

#execute_guardrail(guardrail, data, messages:) ⇒ Riffer::Guardrails::Result

-- : (Riffer::Guardrail, untyped, messages: Array?) -> Riffer::Guardrails::Result

Parameters:

Returns:



87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/riffer/guardrails/runner.rb', line 87

def execute_guardrail(guardrail, data, messages:)
  Riffer::Tracing.in_span("execute_guardrail #{guardrail.name}", attributes: guardrail_span_attributes(guardrail), kind: :internal) do |span|
    result = run_guardrail_phase(guardrail, data, messages: messages)
    record_guardrail_outcome(span, result)
    result
  rescue => error
    # The backend records the exception and error status on the re-raise;
    # error.type is the one semconv attribute it doesn't set.
    span.set_attribute("error.type", error.class.name)
    raise
  end
end

#guardrail_span_attributes(guardrail) ⇒ Hash[String, untyped]

-- : (Riffer::Guardrail) -> Hash[String, untyped]

Parameters:

Returns:

  • (Hash[String, untyped])


115
116
117
118
119
120
# File 'lib/riffer/guardrails/runner.rb', line 115

def guardrail_span_attributes(guardrail)
  {
    "riffer.guardrail.name" => guardrail.name,
    "riffer.guardrail.phase" => phase.to_s
  }.merge(tags.transform_keys { |key| "riffer.tag.#{key}" })
end

#instantiate_guardrail(config) ⇒ Riffer::Guardrail

-- : (Hash[Symbol, untyped]) -> Riffer::Guardrail

Parameters:

  • (Hash[Symbol, untyped])

Returns:



69
70
71
72
# File 'lib/riffer/guardrails/runner.rb', line 69

def instantiate_guardrail(config)
  options = config[:options] #: Hash[Symbol, untyped]
  config[:class].new(**options)
end

#record_guardrail_outcome(span, result) ⇒ void

This method returns an undefined value.

A block is a handled outcome, so its span status stays unset — an error span status is reserved for a raised exception.

: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Guardrails::Result) -> void



126
127
128
129
# File 'lib/riffer/guardrails/runner.rb', line 126

def record_guardrail_outcome(span, result)
  span.set_attribute("riffer.guardrail.action", result.type.to_s)
  span.set_attribute("riffer.tripwire.reason", result.data) if result.block?
end

#run(data, messages: nil) ⇒ [ untyped, Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification] ]

Runs the guardrails sequentially. For the :before phase data is the messages array; for :after it's the response (and messages must be provided).

: (untyped, ?messages: Array?) -> [untyped, Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification]]

Parameters:

Returns:



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/riffer/guardrails/runner.rb', line 33

def run(data, messages: nil)
  current_data = data
  modifications = [] #: Array[Riffer::Guardrails::Modification]

  guardrail_configs.each do |config|
    guardrail = instantiate_guardrail(config)
    result = execute_guardrail(guardrail, current_data, messages: messages)

    if result.block?
      tripwire = Riffer::Guardrails::Tripwire.new(
        reason: result.data,
        guardrail: guardrail.class,
        phase: phase,
        metadata: result.
      )
      return [current_data, tripwire, modifications]
    end

    if result.transform?
      modifications << Riffer::Guardrails::Modification.new(
        guardrail: guardrail.class,
        phase: phase,
        message_indices: detect_changed_indices(current_data, result.data)
      )
    end

    current_data = result.data
  end

  [current_data, nil, modifications]
end

#run_guardrail_phase(guardrail, data, messages:) ⇒ Riffer::Guardrails::Result

-- : (Riffer::Guardrail, untyped, messages: Array?) -> Riffer::Guardrails::Result

Parameters:

Returns:



102
103
104
105
106
107
108
109
110
111
# File 'lib/riffer/guardrails/runner.rb', line 102

def run_guardrail_phase(guardrail, data, messages:)
  case phase
  when :before
    guardrail.process_input(data, context: context)
  when :after
    guardrail.process_output(data, messages: messages || [], context: context)
  else
    raise Riffer::Error, "Unexpected guardrail phase: #{phase}. Valid phases: #{Riffer::Guardrails::PHASES.join(", ")}"
  end
end