Module: Vangrail::Prompt

Defined in:
lib/vangrail/prompt.rb

Overview

The slice of Jinja that guardrail prompts actually use.

NeMo prompt files address the turn through {{ user_input }} and {{ bot_response }}, occasionally with a {% if %} around an optional section. Rendering that needs variable substitution and one conditional, not a template engine, and a guardrail prompt is the last place to want arbitrary evaluation: the text being substituted is attacker-influenced by construction.

Supported, and nothing else:

{{ name }}                    substitute, HTML untouched
{{ name | upper }}            upper, lower, trim
{% if name %} ... {% endif %} include when truthy and non-empty

An unknown variable renders empty. An unknown filter or tag raises, because a prompt that silently drops the rule you wrote is worse than one that fails to load.

Constant Summary collapse

FILTERS =
{
  'upper' => lambda(&:upcase),
  'lower' => lambda(&:downcase),
  'trim' => lambda(&:strip)
}.freeze
TAG =
/\{%\s*(\w+)\s*([^%]*?)\s*%\}/
VAR =
/\{\{\s*([\w.]+)\s*(?:\|\s*(\w+)\s*)?\}\}/

Class Method Summary collapse

Class Method Details

.apply(filter, value) ⇒ Object

Raises:

  • (ArgumentError)


72
73
74
75
76
77
78
79
80
# File 'lib/vangrail/prompt.rb', line 72

def apply(filter, value)
  text = value.to_s
  return text if filter.nil?

  fn = FILTERS[filter]
  raise ArgumentError, "unsupported template filter |#{filter}" unless fn

  fn.call(text)
end

.check_tags(text) ⇒ Object



57
58
59
60
61
62
# File 'lib/vangrail/prompt.rb', line 57

def check_tags(text)
  text.scan(TAG) do |tag, _rest|
    raise ArgumentError, "unsupported template tag {% #{tag} %}" unless tag == 'raw'
  end
  text
end

.conditionals(text, vars) ⇒ Object

Only {% if x %}...{% endif %}, innermost first so nesting resolves.



44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/vangrail/prompt.rb', line 44

def conditionals(text, vars)
  out = text
  loop do
    replaced = out.sub(/\{%\s*if\s+([\w.]+)\s*%\}(.*?)\{%\s*endif\s*%\}/m) do
      truthy?(lookup(vars, Regexp.last_match(1))) ? Regexp.last_match(2) : ''
    end
    break out if replaced == out

    out = replaced
  end
  check_tags(out)
end

.lookup(vars, name) ⇒ Object



64
65
66
67
68
69
70
# File 'lib/vangrail/prompt.rb', line 64

def lookup(vars, name)
  name.split('.').reduce(vars) do |acc, part|
    break nil unless acc.respond_to?(:[])

    acc[part] || (acc.respond_to?(:key?) ? acc[part.to_sym] : nil)
  end
end

.render(template, vars = {}) ⇒ Object



34
35
36
37
38
39
40
41
# File 'lib/vangrail/prompt.rb', line 34

def render(template, vars = {})
  text = conditionals(template.to_s, vars)
  text.gsub(VAR) do
    name = Regexp.last_match(1)
    filter = Regexp.last_match(2)
    apply(filter, lookup(vars, name))
  end
end

.truthy?(value) ⇒ Boolean

Returns:

  • (Boolean)


82
83
84
85
86
# File 'lib/vangrail/prompt.rb', line 82

def truthy?(value)
  return false if value.nil? || value == false

  !(value.respond_to?(:empty?) && value.empty?)
end