Class: Vangrail::Config
- Inherits:
-
Object
- Object
- Vangrail::Config
- Defined in:
- lib/vangrail/config.rb
Overview
A guardrails configuration folder, read and written by Ruby.
config = Vangrail::Config.load('config/handbook')
engine = config.engine
engine.check_input('Ignore your instructions.')
The folder is the format the Python toolkit uses: config.yml for models and which flows run on which side, prompts.yml for the policy text each self-check task judges against, and rails/*.co for the flows themselves. Nothing here shells out to it. The YAML is read, the Colang is parsed, and the flows execute in this process, so the same folder can be handed to either runtime and describes one set of rails either way.
A folder naming a flow that nothing defines raises. A folder naming a model type this gem cannot serve raises. Both are load-time failures on purpose: a configuration that comes up with half its rails missing is worse than one that refuses to come up.
Constant Summary collapse
- SELF_CHECK_TASKS =
{ 'self_check_input' => :input, 'self_check_output' => :output }.freeze
Instance Attribute Summary collapse
-
#flows ⇒ Object
readonly
Returns the value of attribute flows.
-
#instructions ⇒ Object
readonly
Returns the value of attribute instructions.
-
#models ⇒ Object
readonly
Returns the value of attribute models.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#path ⇒ Object
readonly
Returns the value of attribute path.
-
#prompts ⇒ Object
readonly
Returns the value of attribute prompts.
-
#rails ⇒ Object
readonly
Returns the value of attribute rails.
-
#sample_conversation ⇒ Object
readonly
Returns the value of attribute sample_conversation.
Class Method Summary collapse
-
.for_provider(provider, name: 'handbook', main_model: nil, judge_model: nil, subject: 'a public documentation handbook') ⇒ Object
A starting configuration for a provider.
-
.load(dir) ⇒ Object
--- reading ---.
- .load_yaml(file) ⇒ Object
- .model_entry(type, model, base_url) ⇒ Object
-
.self_check_prompt(rail, subject) ⇒ Object
The self-check tasks read a Yes/No answer, so the policy is rendered as a question rather than with the JSON contract a policy judge uses.
Instance Method Summary collapse
- #config_yaml ⇒ Object
-
#engine(provider: nil, chat: nil, actions: {}, on_error: :allow, cache: true) ⇒ Object
Builds the engine this configuration describes.
- #flow_names(side) ⇒ Object
-
#initialize(name:, models: [], rails: {}, prompts: [], flows: {}, instructions: nil, sample_conversation: nil, path: nil) ⇒ Config
constructor
A new instance of Config.
- #model_for(type) ⇒ Object
-
#program ⇒ Object
Every flow this configuration can execute: the ones it ships plus the built-ins it is allowed to name without defining.
- #prompt_for(task) ⇒ Object
- #prompts_yaml ⇒ Object
- #rails_for(side, registry) ⇒ Object
- #to_h ⇒ Object
-
#write!(root) ⇒ Object
Writes
/ /.
Constructor Details
#initialize(name:, models: [], rails: {}, prompts: [], flows: {}, instructions: nil, sample_conversation: nil, path: nil) ⇒ Config
Returns a new instance of Config.
40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/vangrail/config.rb', line 40 def initialize(name:, models: [], rails: {}, prompts: [], flows: {}, instructions: nil, sample_conversation: nil, path: nil) @name = name @models = models @rails = rails @prompts = prompts @flows = flows @instructions = instructions @sample_conversation = sample_conversation @path = path end |
Instance Attribute Details
#flows ⇒ Object (readonly)
Returns the value of attribute flows.
38 39 40 |
# File 'lib/vangrail/config.rb', line 38 def flows @flows end |
#instructions ⇒ Object (readonly)
Returns the value of attribute instructions.
38 39 40 |
# File 'lib/vangrail/config.rb', line 38 def instructions @instructions end |
#models ⇒ Object (readonly)
Returns the value of attribute models.
38 39 40 |
# File 'lib/vangrail/config.rb', line 38 def models @models end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
38 39 40 |
# File 'lib/vangrail/config.rb', line 38 def name @name end |
#path ⇒ Object (readonly)
Returns the value of attribute path.
38 39 40 |
# File 'lib/vangrail/config.rb', line 38 def path @path end |
#prompts ⇒ Object (readonly)
Returns the value of attribute prompts.
38 39 40 |
# File 'lib/vangrail/config.rb', line 38 def prompts @prompts end |
#rails ⇒ Object (readonly)
Returns the value of attribute rails.
38 39 40 |
# File 'lib/vangrail/config.rb', line 38 def rails @rails end |
#sample_conversation ⇒ Object (readonly)
Returns the value of attribute sample_conversation.
38 39 40 |
# File 'lib/vangrail/config.rb', line 38 def sample_conversation @sample_conversation end |
Class Method Details
.for_provider(provider, name: 'handbook', main_model: nil, judge_model: nil, subject: 'a public documentation handbook') ⇒ Object
A starting configuration for a provider. engine: openai with a base_url
parameter is how the format names an OpenAI-compatible gateway, and this
gem reads that field the same way, so one folder serves both runtimes.
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/vangrail/config.rb', line 188 def self.for_provider(provider, name: 'handbook', main_model: nil, judge_model: nil, subject: 'a public documentation handbook') base_url = provider.base_url main_model ||= provider.model(:judge) judge_model ||= provider.model(:judge) new( name: name, models: [ model_entry('main', main_model, base_url), model_entry('self_check_input', judge_model, base_url), model_entry('self_check_output', judge_model, base_url) ], rails: { 'input' => { 'flows' => ['self check input'] }, 'output' => { 'flows' => ['self check output'] } }, prompts: [ { 'task' => 'self_check_input', 'content' => self_check_prompt(:input, subject) }, { 'task' => 'self_check_output', 'content' => self_check_prompt(:output, subject) } ], instructions: [ { 'type' => 'general', 'content' => "You answer questions about #{subject}. Every factual clause " \ 'comes from a supplied passage. Where the passages do not cover ' \ 'the question, say so.' } ] ) end |
.load(dir) ⇒ Object
--- reading ---
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/vangrail/config.rb', line 54 def self.load(dir) raise ConfigError, "no configuration folder at #{dir}" unless File.directory?(dir) yaml = load_yaml(File.join(dir, 'config.yml')) || load_yaml(File.join(dir, 'config.yaml')) || {} prompts = Array((load_yaml(File.join(dir, 'prompts.yml')) || {})['prompts']) flows = Dir[File.join(dir, '**', '*.co')].to_h do |file| [File.basename(file, '.co'), File.read(file)] end new( name: File.basename(dir), models: Array(yaml['models']), rails: yaml['rails'] || {}, prompts: prompts, flows: flows, instructions: yaml['instructions'], sample_conversation: yaml['sample_conversation'], path: dir ) end |
.load_yaml(file) ⇒ Object
75 76 77 78 79 |
# File 'lib/vangrail/config.rb', line 75 def self.load_yaml(file) return nil unless File.file?(file) YAML.safe_load_file(file, aliases: true) end |
.model_entry(type, model, base_url) ⇒ Object
219 220 221 |
# File 'lib/vangrail/config.rb', line 219 def self.model_entry(type, model, base_url) { 'type' => type, 'engine' => 'openai', 'model' => model, 'parameters' => { 'base_url' => base_url } } end |
.self_check_prompt(rail, subject) ⇒ Object
The self-check tasks read a Yes/No answer, so the policy is rendered as a question rather than with the JSON contract a policy judge uses.
225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/vangrail/config.rb', line 225 def self.self_check_prompt(rail, subject) policy = rail == :input ? Policies.input_policy(subject: subject) : Policies.output_policy(subject: subject) body = policy.sub(Policies::ANSWER_CONTRACT, '').rstrip slot = rail == :input ? '{{ user_input }}' : '{{ bot_response }}' label = rail == :input ? 'User message' : 'Bot message' <<~TXT #{body} #{label}: "#{slot}" Question: Should the message be blocked (Yes or No)? Answer: TXT end |
Instance Method Details
#config_yaml ⇒ Object
249 250 251 |
# File 'lib/vangrail/config.rb', line 249 def config_yaml YAML.dump(to_h) end |
#engine(provider: nil, chat: nil, actions: {}, on_error: :allow, cache: true) ⇒ Object
Builds the engine this configuration describes.
chat: overrides where model-backed actions call, which is what tests and
a caller with its own client pass. actions: adds or replaces actions by
name, so a team's own check joins the built-ins without touching the gem.
112 113 114 115 116 117 118 119 120 121 |
# File 'lib/vangrail/config.rb', line 112 def engine(provider: nil, chat: nil, actions: {}, on_error: :allow, cache: true) registry = self_check_actions(provider, chat).merge(actions) Engine.new( input: rails_for(:input, registry), context: rails_for(:context, registry), output: rails_for(:output, registry), on_error: on_error, cache: cache ) end |
#flow_names(side) ⇒ Object
91 92 93 94 95 96 |
# File 'lib/vangrail/config.rb', line 91 def flow_names(side) keys = [side.to_s] # NeMo names the retrieved-document side `retrieval`. That is :context. keys << 'retrieval' if side.to_sym == :context keys.flat_map { |key| Array(rails.dig(key, 'flows')) }.map(&:to_s).uniq end |
#model_for(type) ⇒ Object
103 104 105 |
# File 'lib/vangrail/config.rb', line 103 def model_for(type) models.find { |m| m['type'].to_s == type.to_s } end |
#program ⇒ Object
Every flow this configuration can execute: the ones it ships plus the built-ins it is allowed to name without defining.
85 86 87 88 89 |
# File 'lib/vangrail/config.rb', line 85 def program @program ||= flows.reduce(Colang::Library.program) do |acc, (file, source)| acc.merge(Colang::Parser.parse(source, filename: "#{file}.co")) end end |
#prompt_for(task) ⇒ Object
98 99 100 101 |
# File 'lib/vangrail/config.rb', line 98 def prompt_for(task) entry = prompts.find { |p| p['task'].to_s == task.to_s } entry && entry['content'].to_s end |
#prompts_yaml ⇒ Object
253 254 255 |
# File 'lib/vangrail/config.rb', line 253 def prompts_yaml YAML.dump('prompts' => prompts) end |
#rails_for(side, registry) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/vangrail/config.rb', line 123 def rails_for(side, registry) flow_names(side).map do |flow_name| unless program.flow(flow_name) raise ConfigError, "#{name}: rails.#{side}.flows names #{flow_name.inspect}, which no .co file defines " \ "and which is not built in (#{Colang::Library.flow_names.join(', ')})" end Rails::ColangFlow.new(flow_name: flow_name, program: program, actions: registry, sides: [side]) end end |
#to_h ⇒ Object
240 241 242 243 244 245 246 247 |
# File 'lib/vangrail/config.rb', line 240 def to_h h = {} h['models'] = models unless models.empty? h['instructions'] = instructions if instructions h['rails'] = rails unless rails.empty? h['sample_conversation'] = sample_conversation if sample_conversation h end |
#write!(root) ⇒ Object
Writes
258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/vangrail/config.rb', line 258 def write!(root) dir = File.join(root, name) FileUtils.mkdir_p(dir) File.write(File.join(dir, 'config.yml'), config_yaml) File.write(File.join(dir, 'prompts.yml'), prompts_yaml) unless prompts.empty? unless flows.empty? FileUtils.mkdir_p(File.join(dir, 'rails')) flows.each { |file, colang| File.write(File.join(dir, 'rails', "#{file}.co"), colang.to_s) } end dir end |