vangrail 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +473 -0
  4. data/lib/vangrail/actions.rb +61 -0
  5. data/lib/vangrail/chat.rb +88 -0
  6. data/lib/vangrail/client/completion.rb +122 -0
  7. data/lib/vangrail/client.rb +219 -0
  8. data/lib/vangrail/colang/ast.rb +53 -0
  9. data/lib/vangrail/colang/interpreter.rb +131 -0
  10. data/lib/vangrail/colang/library.rb +53 -0
  11. data/lib/vangrail/colang/parser.rb +222 -0
  12. data/lib/vangrail/config.rb +270 -0
  13. data/lib/vangrail/confusables.rb +67 -0
  14. data/lib/vangrail/confusables_data.rb +1673 -0
  15. data/lib/vangrail/conversation.rb +105 -0
  16. data/lib/vangrail/engine.rb +240 -0
  17. data/lib/vangrail/errors.rb +48 -0
  18. data/lib/vangrail/http.rb +109 -0
  19. data/lib/vangrail/parsers.rb +181 -0
  20. data/lib/vangrail/policies.rb +202 -0
  21. data/lib/vangrail/prompt.rb +88 -0
  22. data/lib/vangrail/provider.rb +191 -0
  23. data/lib/vangrail/providers/gateway.rb +131 -0
  24. data/lib/vangrail/providers/llmlite.rb +71 -0
  25. data/lib/vangrail/providers.rb +72 -0
  26. data/lib/vangrail/rail.rb +93 -0
  27. data/lib/vangrail/rails/budget.rb +63 -0
  28. data/lib/vangrail/rails/canary.rb +76 -0
  29. data/lib/vangrail/rails/colang_flow.rb +40 -0
  30. data/lib/vangrail/rails/escalation.rb +178 -0
  31. data/lib/vangrail/rails/exfiltration.rb +167 -0
  32. data/lib/vangrail/rails/grounding.rb +64 -0
  33. data/lib/vangrail/rails/guard_model.rb +96 -0
  34. data/lib/vangrail/rails/hidden.rb +105 -0
  35. data/lib/vangrail/rails/injected_instructions.rb +86 -0
  36. data/lib/vangrail/rails/jailbreak.rb +114 -0
  37. data/lib/vangrail/rails/known_answer.rb +118 -0
  38. data/lib/vangrail/rails/many_shot.rb +80 -0
  39. data/lib/vangrail/rails/markup.rb +77 -0
  40. data/lib/vangrail/rails/missing.rb +38 -0
  41. data/lib/vangrail/rails/obfuscation.rb +186 -0
  42. data/lib/vangrail/rails/pattern.rb +57 -0
  43. data/lib/vangrail/rails/personal_data.rb +152 -0
  44. data/lib/vangrail/rails/remote.rb +40 -0
  45. data/lib/vangrail/rails/secrets.rb +77 -0
  46. data/lib/vangrail/rails/self_check.rb +81 -0
  47. data/lib/vangrail/rails/trajectory.rb +101 -0
  48. data/lib/vangrail/result.rb +114 -0
  49. data/lib/vangrail/result_cache.rb +0 -0
  50. data/lib/vangrail/spotlight.rb +157 -0
  51. data/lib/vangrail/stream_guard.rb +163 -0
  52. data/lib/vangrail/version.rb +5 -0
  53. data/lib/vangrail.rb +354 -0
  54. metadata +120 -0
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vangrail
4
+ class Client
5
+ # A guardrailed chat completion, read out of either server response shape.
6
+ #
7
+ # The OpenAI-compatible shape puts the answer in choices[0].message.content
8
+ # and rail bookkeeping under a top-level `guardrails` object. The older shape
9
+ # answers with a bare {role, content} message (or a list of them) and puts
10
+ # bookkeeping at the top level. Both appear in the wild depending on the
11
+ # server version, so this reads whichever is present.
12
+ class Completion
13
+ # Server-side names for the variables holding the rail that stopped a turn.
14
+ INPUT_RAIL_VAR = 'triggered_input_rail'
15
+ OUTPUT_RAIL_VAR = 'triggered_output_rail'
16
+
17
+ attr_reader :raw
18
+
19
+ def initialize(raw)
20
+ @raw = raw.is_a?(Hash) ? raw : {}
21
+ end
22
+
23
+ def content
24
+ choice = choices.first
25
+ if choice.is_a?(Hash)
26
+ msg = choice['message'] || choice['delta'] || {}
27
+ return msg['content'].to_s if msg.is_a?(Hash) && msg.key?('content')
28
+ end
29
+ return raw['content'].to_s if raw.key?('content')
30
+
31
+ messages = raw['messages']
32
+ if messages.is_a?(Array)
33
+ last = messages.reverse.find { |m| m.is_a?(Hash) && m['role'].to_s == 'assistant' }
34
+ return last['content'].to_s if last
35
+ end
36
+ ''
37
+ end
38
+
39
+ def config_id
40
+ guardrails['config_id']
41
+ end
42
+
43
+ # The rail that stopped this turn, or nil. Reported only when the request
44
+ # asked for the output variables that carry it.
45
+ def triggered_input_rail
46
+ output_data[INPUT_RAIL_VAR]
47
+ end
48
+
49
+ def triggered_output_rail
50
+ output_data[OUTPUT_RAIL_VAR]
51
+ end
52
+
53
+ def triggered_rail
54
+ triggered_input_rail || triggered_output_rail
55
+ end
56
+
57
+ # Rails that ran, from options.log.activated_rails. Empty unless logging
58
+ # was requested; an empty list is not evidence that no rail ran.
59
+ def activated_rails
60
+ entries = log['activated_rails']
61
+ entries.is_a?(Array) ? entries : []
62
+ end
63
+
64
+ def stopped_rails
65
+ activated_rails.select { |r| r.is_a?(Hash) && r['stop'] == true }
66
+ end
67
+
68
+ # True when a rail is known to have stopped the turn. Absent explicit
69
+ # signals this stays false, so a refusal the model wrote itself is never
70
+ # reported as a rail decision.
71
+ def blocked?
72
+ return true unless triggered_rail.nil?
73
+
74
+ !stopped_rails.empty?
75
+ end
76
+
77
+ def allowed?
78
+ !blocked?
79
+ end
80
+
81
+ def model
82
+ raw['model']
83
+ end
84
+
85
+ def guardrails
86
+ g = raw['guardrails']
87
+ g.is_a?(Hash) ? g : {}
88
+ end
89
+
90
+ def output_data
91
+ d = guardrails['output_data'] || raw['output_data']
92
+ d.is_a?(Hash) ? d : {}
93
+ end
94
+
95
+ def log
96
+ l = guardrails['log'] || raw['log']
97
+ l.is_a?(Hash) ? l : {}
98
+ end
99
+
100
+ def llm_calls
101
+ calls = log['llm_calls']
102
+ calls.is_a?(Array) ? calls : []
103
+ end
104
+
105
+ # Total tokens the rails plus the answer spent, when the server reports it.
106
+ def total_tokens
107
+ usage = raw['usage']
108
+ return usage['total_tokens'] if usage.is_a?(Hash) && usage['total_tokens']
109
+
110
+ sums = llm_calls.filter_map { |c| c['total_tokens'] if c.is_a?(Hash) }
111
+ sums.empty? ? nil : sums.sum
112
+ end
113
+
114
+ private
115
+
116
+ def choices
117
+ c = raw['choices']
118
+ c.is_a?(Array) ? c : []
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'client/completion'
4
+ require_relative 'errors'
5
+ require_relative 'http'
6
+ require_relative 'result'
7
+
8
+ module Vangrail
9
+ # Interop with a NeMo Guardrails server that already exists.
10
+ #
11
+ # Nothing in this gem needs one. It is here for the case where a team already
12
+ # runs the Python service, wants its configs to stay the source of truth, and
13
+ # wants Ruby to call rather than reimplement. Reach for Config#engine first:
14
+ # it runs the same folder in this process with nothing to deploy.
15
+ #
16
+ # `/v1/checks` is the endpoint that matches what a rail actually wants, and it
17
+ # answers in the same three states this gem models: passed, modified, blocked.
18
+ # Older servers do not have it, so `check` falls back to a chat completion with
19
+ # generation switched off and reads the rail-tracking variables out of that.
20
+ class Client
21
+ CONFIGS_PATH = '/v1/rails/configs'
22
+ CHECKS_PATH = '/v1/checks'
23
+ COMPLETIONS_PATH = '/v1/chat/completions'
24
+ PROTOCOLS = %i[auto nested flat].freeze
25
+
26
+ RAIL_VARS = [Completion::INPUT_RAIL_VAR, Completion::OUTPUT_RAIL_VAR].freeze
27
+
28
+ attr_reader :config_id, :model, :protocol, :http
29
+
30
+ # True once /v1/checks has answered, false once it has 404ed, nil until one
31
+ # of those happens, so a caller can report "not yet known" honestly.
32
+ attr_reader :checks_supported
33
+
34
+ def initialize(base_url:, config_id: nil, model: nil, api_key: nil, protocol: :auto,
35
+ open_timeout: HTTP::DEFAULT_OPEN_TIMEOUT, read_timeout: HTTP::DEFAULT_READ_TIMEOUT,
36
+ http: nil)
37
+ unless PROTOCOLS.include?(protocol)
38
+ raise ArgumentError,
39
+ "protocol must be one of #{PROTOCOLS.join(', ')}"
40
+ end
41
+
42
+ @config_id = config_id
43
+ @model = model
44
+ @protocol = protocol
45
+ @checks_supported = nil
46
+ @http = http || HTTP.new(
47
+ base_url: base_url, api_key: api_key,
48
+ open_timeout: open_timeout, read_timeout: read_timeout
49
+ )
50
+ end
51
+
52
+ def base_url
53
+ http.base_url
54
+ end
55
+
56
+ def configs
57
+ body = http.get_json(CONFIGS_PATH)
58
+ list = body.is_a?(Array) ? body : Array(body['configs'])
59
+ list.filter_map { |entry| entry.is_a?(Hash) ? entry['id'] : entry.to_s }
60
+ end
61
+
62
+ def available?
63
+ http.reachable?(CONFIGS_PATH)
64
+ end
65
+
66
+ def check_input(text, config_id: nil)
67
+ check([{ 'role' => 'user', 'content' => text.to_s }], rail: :input, config_id: config_id)
68
+ end
69
+
70
+ def check_output(text, user_input: nil, config_id: nil)
71
+ messages = []
72
+ messages << { 'role' => 'user', 'content' => user_input.to_s } unless user_input.to_s.strip.empty?
73
+ messages << { 'role' => 'assistant', 'content' => text.to_s }
74
+ check(messages, rail: :output, config_id: config_id)
75
+ end
76
+
77
+ # Runs rails without generation and returns a Result.
78
+ def check(messages, rail:, config_id: nil)
79
+ chosen = config_id || @config_id
80
+ if @checks_supported != false
81
+ begin
82
+ return from_checks(http.post_json(CHECKS_PATH, checks_payload(messages, rail, chosen)), rail)
83
+ rescue HTTPError => e
84
+ raise unless e.status == 404
85
+
86
+ @checks_supported = false
87
+ end
88
+ end
89
+ from_completion(chat(messages: messages, config_id: chosen, options: check_options(rail)), rail)
90
+ end
91
+
92
+ # A full guardrailed completion, for the case where the server generates the
93
+ # answer as well as checking it.
94
+ def chat(messages:, config_id: nil, config_ids: nil, options: nil, context: nil,
95
+ thread_id: nil, model: nil, **extra)
96
+ opts = merge_options(options)
97
+ body = extra.merge(messages: normalize(messages))
98
+ chosen = { config_id: config_id || @config_id, config_ids: config_ids }
99
+ Completion.new(send_payload(body, chosen, opts, context, thread_id, model))
100
+ end
101
+
102
+ private
103
+
104
+ def checks_payload(messages, rail, config_id)
105
+ payload = { 'messages' => normalize(messages), 'rail_types' => [rail.to_s] }
106
+ payload['config_id'] = config_id if config_id
107
+ payload
108
+ end
109
+
110
+ # {"status": "passed"|"modified"|"blocked", "content": "...", "rail": "..."}
111
+ def from_checks(body, rail)
112
+ @checks_supported = true
113
+ status = body['status'].to_s
114
+ unless Result::STATUSES.map(&:to_s).include?(status)
115
+ raise ProtocolError, "/v1/checks answered status #{status.inspect}"
116
+ end
117
+
118
+ Result.new(status: status.to_sym, rail: body['rail'] || rail.to_s,
119
+ content: body['content'], raw: body)
120
+ end
121
+
122
+ def from_completion(completion, rail)
123
+ return Result.passed(rail: rail.to_s, raw: completion.raw) if completion.allowed?
124
+
125
+ reason = completion.triggered_rail || completion.stopped_rails.first&.dig('name')
126
+ Result.blocked(rail: reason || rail.to_s, content: completion.content, reason: reason,
127
+ raw: completion.raw)
128
+ end
129
+
130
+ def check_options(rail)
131
+ { 'rails' => { 'input' => rail == :input, 'output' => rail == :output, 'dialog' => false } }
132
+ end
133
+
134
+ def merge_options(options)
135
+ base = { 'output_vars' => RAIL_VARS.dup, 'log' => { 'activated_rails' => true } }
136
+ return base unless options.is_a?(Hash)
137
+
138
+ stringified = deep_stringify(options)
139
+ merged = base.merge(stringified)
140
+ merged['output_vars'] = (RAIL_VARS + Array(stringified['output_vars'])).uniq
141
+ merged['log'] = base['log'].merge(stringified['log']) if stringified['log'].is_a?(Hash)
142
+ merged
143
+ end
144
+
145
+ def send_payload(body, chosen, opts, context, thread_id, model)
146
+ case protocol
147
+ when :flat
148
+ http.post_json(COMPLETIONS_PATH, flat_payload(body, chosen, opts, context, thread_id, model))
149
+ when :nested
150
+ http.post_json(COMPLETIONS_PATH, nested_payload(body, chosen, opts, context, thread_id, model))
151
+ else
152
+ try_nested_then_flat(body, chosen, opts, context, thread_id, model)
153
+ end
154
+ end
155
+
156
+ def try_nested_then_flat(body, chosen, opts, context, thread_id, model)
157
+ answer = http.post_json(COMPLETIONS_PATH, nested_payload(body, chosen, opts, context, thread_id, model))
158
+ @protocol = :nested
159
+ answer
160
+ rescue HTTPError => e
161
+ raise unless schema_rejection?(e)
162
+
163
+ answer = http.post_json(COMPLETIONS_PATH, flat_payload(body, chosen, opts, context, thread_id, model))
164
+ @protocol = :flat
165
+ answer
166
+ end
167
+
168
+ # A 400/422 naming a field is the server saying it speaks the other shape.
169
+ # Any other status is a real failure and stays raised.
170
+ def schema_rejection?(error)
171
+ return false unless [400, 422].include?(error.status)
172
+
173
+ error.body.match?(/extra fields not permitted|unexpected keyword|field required|guardrails|config_id/i)
174
+ end
175
+
176
+ def nested_payload(body, chosen, opts, context, thread_id, model)
177
+ guardrails = {}
178
+ guardrails['config_id'] = chosen[:config_id] if chosen[:config_id]
179
+ guardrails['config_ids'] = Array(chosen[:config_ids]) if chosen[:config_ids]
180
+ guardrails['options'] = opts if opts
181
+ guardrails['context'] = deep_stringify(context) if context
182
+ guardrails['thread_id'] = thread_id if thread_id
183
+
184
+ payload = body.transform_keys(&:to_s)
185
+ payload['model'] = model || @model if model || @model
186
+ payload['guardrails'] = guardrails unless guardrails.empty?
187
+ payload
188
+ end
189
+
190
+ def flat_payload(body, chosen, opts, context, thread_id, model)
191
+ payload = body.transform_keys(&:to_s)
192
+ payload['config_id'] = chosen[:config_id] if chosen[:config_id]
193
+ payload['config_ids'] = Array(chosen[:config_ids]) if chosen[:config_ids]
194
+ payload['options'] = opts if opts
195
+ payload['context'] = deep_stringify(context) if context
196
+ payload['thread_id'] = thread_id if thread_id
197
+ payload['model'] = model || @model if model || @model
198
+ payload
199
+ end
200
+
201
+ def normalize(messages)
202
+ Array(messages).map do |m|
203
+ if m.is_a?(Hash)
204
+ { 'role' => (m['role'] || m[:role]).to_s, 'content' => (m['content'] || m[:content]).to_s }
205
+ else
206
+ { 'role' => 'user', 'content' => m.to_s }
207
+ end
208
+ end
209
+ end
210
+
211
+ def deep_stringify(value)
212
+ case value
213
+ when Hash then value.to_h { |k, v| [k.to_s, deep_stringify(v)] }
214
+ when Array then value.map { |v| deep_stringify(v) }
215
+ else value
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vangrail
4
+ module Colang
5
+ # The whole grammar this gem executes, as data. Everything the parser can
6
+ # produce is one of these, which is also the honest statement of what a
7
+ # Colang file may contain here: anything else is refused at parse time
8
+ # rather than skipped at run time.
9
+ Program = Struct.new(:flows, :bot_messages, :user_messages, keyword_init: true) do
10
+ def flow(name)
11
+ flows[name.to_s]
12
+ end
13
+
14
+ def flow_names
15
+ flows.keys
16
+ end
17
+
18
+ def bot_message(name)
19
+ bot_messages[name.to_s]
20
+ end
21
+
22
+ def merge(other)
23
+ Program.new(
24
+ flows: flows.merge(other.flows),
25
+ bot_messages: bot_messages.merge(other.bot_messages),
26
+ user_messages: user_messages.merge(other.user_messages)
27
+ )
28
+ end
29
+ end
30
+
31
+ Flow = Struct.new(:name, :body, :subflow, keyword_init: true)
32
+
33
+ # $var = execute action(key="value")
34
+ Assign = Struct.new(:variable, :expression, keyword_init: true)
35
+
36
+ # execute action(key="value")
37
+ Execute = Struct.new(:action, :arguments, keyword_init: true)
38
+
39
+ # bot <message name>
40
+ Bot = Struct.new(:message, keyword_init: true)
41
+
42
+ # stop
43
+ Stop = Struct.new(:reason, keyword_init: true)
44
+
45
+ If = Struct.new(:condition, :then_body, :else_body, keyword_init: true)
46
+
47
+ # Conditions
48
+ Var = Struct.new(:name, keyword_init: true)
49
+ Not = Struct.new(:expression, keyword_init: true)
50
+ Compare = Struct.new(:left, :operator, :right, keyword_init: true)
51
+ Literal = Struct.new(:value, keyword_init: true)
52
+ end
53
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+ require_relative 'ast'
5
+
6
+ module Vangrail
7
+ module Colang
8
+ # Runs one flow and reports what it decided.
9
+ #
10
+ # The mapping onto rail statuses is the whole design:
11
+ #
12
+ # `bot <message>` then `stop` -> blocked, with the message as content
13
+ # assignment to $user_message or
14
+ # $bot_message -> modified, with the new value
15
+ # falls off the end -> passed
16
+ #
17
+ # A `stop` without a preceding `bot` still blocks; it just has no refusal
18
+ # text to show. Actions are plain Ruby callables, so the flow decides the
19
+ # control shape and Ruby does the work.
20
+ class Interpreter
21
+ # Variables a flow assigns to when it rewrites the turn rather than
22
+ # refusing it, matching the names the toolkit's own flows use.
23
+ CONTENT_VARS = %w[user_message bot_message user_input bot_response].freeze
24
+
25
+ Outcome = Struct.new(:status, :content, :reason, :variables, keyword_init: true)
26
+
27
+ # Raised through the interpreter to unwind a flow on `stop`.
28
+ class Stopped < StandardError
29
+ attr_reader :message_text
30
+
31
+ def initialize(message_text)
32
+ @message_text = message_text
33
+ super('flow stopped')
34
+ end
35
+ end
36
+
37
+ attr_reader :program, :actions
38
+
39
+ def initialize(program:, actions:)
40
+ @program = program
41
+ @actions = actions
42
+ end
43
+
44
+ def run(flow_name, context = {})
45
+ flow = program.flow(flow_name)
46
+ raise UnknownAction, "no flow named #{flow_name.inspect}" unless flow
47
+
48
+ state = { 'context' => context }
49
+ last_bot = nil
50
+ begin
51
+ last_bot = execute(flow.body, state, context)
52
+ rescue Stopped => e
53
+ return Outcome.new(status: :blocked, content: e.message_text, reason: flow_name, variables: state)
54
+ end
55
+
56
+ rewrite = CONTENT_VARS.filter_map { |v| state[v] if state.key?(v) }.last
57
+ if rewrite
58
+ return Outcome.new(status: :modified, content: rewrite.to_s, reason: flow_name,
59
+ variables: state)
60
+ end
61
+
62
+ Outcome.new(status: :passed, content: last_bot, reason: nil, variables: state)
63
+ end
64
+
65
+ private
66
+
67
+ # Returns the text of the last `bot` statement reached, so a flow that
68
+ # says something without stopping still hands that text back.
69
+ def execute(body, state, context)
70
+ said = nil
71
+ body.each do |node|
72
+ case node
73
+ when Assign then state[node.variable] = evaluate(node.expression, state, context)
74
+ when Execute then evaluate(node, state, context)
75
+ when Bot then said = bot_text(node.message)
76
+ when Stop then raise Stopped, said
77
+ when If then said = branch(node, state, context) || said
78
+ else raise ColangError, "cannot execute #{node.class}"
79
+ end
80
+ end
81
+ said
82
+ end
83
+
84
+ def branch(node, state, context)
85
+ taken = truthy?(evaluate(node.condition, state, context)) ? node.then_body : node.else_body
86
+ execute(Array(taken), state, context)
87
+ end
88
+
89
+ def evaluate(node, state, context)
90
+ case node
91
+ when Execute then call_action(node, state, context)
92
+ when Var then state[node.name]
93
+ when Literal then node.value
94
+ when Not then !truthy?(evaluate(node.expression, state, context))
95
+ when Compare then compare(node, state, context)
96
+ else node
97
+ end
98
+ end
99
+
100
+ def compare(node, state, context)
101
+ left = evaluate(node.left, state, context)
102
+ right = evaluate(node.right, state, context)
103
+ node.operator == '==' ? left == right : left != right
104
+ end
105
+
106
+ def call_action(node, state, context)
107
+ action = actions[node.action]
108
+ raise UnknownAction, "no action registered for #{node.action.inspect}" unless action
109
+
110
+ args = node.arguments.transform_values { |v| v.is_a?(Var) ? state[v.name] : v }
111
+ action.call(args, context)
112
+ end
113
+
114
+ def bot_text(message)
115
+ alternatives = program.bot_message(message)
116
+ raise UnknownAction, "no `define bot #{message}` for this flow" unless alternatives
117
+
118
+ # Deterministic: a guardrail refusal that varies between runs makes an
119
+ # incident report harder to read for no benefit.
120
+ alternatives.first
121
+ end
122
+
123
+ def truthy?(value)
124
+ return false if value.nil? || value == false
125
+ return false if value.respond_to?(:empty?) && value.empty?
126
+
127
+ true
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'parser'
4
+
5
+ module Vangrail
6
+ module Colang
7
+ # The flows a configuration can name without defining.
8
+ #
9
+ # A config folder that lists `self check input` under `rails.input.flows`
10
+ # and ships no `.co` file is the ordinary case: those flows are built into
11
+ # the toolkit. They are written out here as Colang rather than hand-built
12
+ # AST, so the parser this gem ships is the same one that reads them, and a
13
+ # bug in it fails loudly on the built-ins instead of hiding until someone
14
+ # writes their own flow.
15
+ module Library
16
+ SOURCE = <<~COLANG
17
+ define flow self check input
18
+ $allowed = execute self_check_input
19
+ if not $allowed
20
+ bot refuse to respond
21
+ stop
22
+
23
+ define flow self check output
24
+ $allowed = execute self_check_output
25
+ if not $allowed
26
+ bot refuse to respond
27
+ stop
28
+
29
+ define flow self check facts
30
+ $accurate = execute self_check_facts
31
+ if not $accurate
32
+ bot inform answer unknown
33
+ stop
34
+
35
+ define bot refuse to respond
36
+ "I'm sorry, I can't respond to that."
37
+
38
+ define bot inform answer unknown
39
+ "I don't know the answer to that."
40
+ COLANG
41
+
42
+ module_function
43
+
44
+ def program
45
+ @program ||= Parser.parse(SOURCE, filename: 'built-in')
46
+ end
47
+
48
+ def flow_names
49
+ program.flow_names
50
+ end
51
+ end
52
+ end
53
+ end