nano-bots 0.1.1 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,9 +2,14 @@
2
2
 
3
3
  require 'openai'
4
4
 
5
- require_relative './base'
5
+ require_relative 'base'
6
6
  require_relative '../crypto'
7
7
 
8
+ require_relative '../../logic/providers/openai/tools'
9
+ require_relative '../../controllers/interfaces/tools'
10
+
11
+ require_relative 'openai/tools'
12
+
8
13
  module NanoBot
9
14
  module Components
10
15
  module Providers
@@ -32,17 +37,16 @@ module NanoBot
32
37
  @client = ::OpenAI::Client.new(uri_base:, access_token: @credentials[:'access-token'])
33
38
  end
34
39
 
35
- def stream(input)
36
- provider = @settings.key?(:stream) ? @settings[:stream] : true
37
- interface = input[:interface].key?(:stream) ? input[:interface][:stream] : true
38
-
39
- provider && interface
40
- end
41
-
42
- def evaluate(input, &block)
40
+ def evaluate(input, streaming, cartridge, &feedback)
43
41
  messages = input[:history].map do |event|
44
- { role: event[:who] == 'user' ? 'user' : 'assistant',
45
- content: event[:message] }
42
+ if event[:message].nil? && event[:meta] && event[:meta][:tool_calls]
43
+ { role: 'assistant', content: nil, tool_calls: event[:meta][:tool_calls] }
44
+ elsif event[:who] == 'tool'
45
+ { role: event[:who], content: event[:message].to_s,
46
+ tool_call_id: event[:meta][:id], name: event[:meta][:name] }
47
+ else
48
+ { role: event[:who] == 'user' ? 'user' : 'assistant', content: event[:message] }
49
+ end
46
50
  end
47
51
 
48
52
  %i[instruction backdrop directive].each do |key|
@@ -62,26 +66,105 @@ module NanoBot
62
66
 
63
67
  payload.delete(:logit_bias) if payload.key?(:logit_bias) && payload[:logit_bias].nil?
64
68
 
65
- if stream(input)
69
+ payload[:tools] = input[:tools].map { |raw| NanoBot::Logic::OpenAI::Tools.adapt(raw) } if input[:tools]
70
+
71
+ if streaming
66
72
  content = ''
73
+ tools = []
67
74
 
68
75
  payload[:stream] = proc do |chunk, _bytesize|
69
- partial = chunk.dig('choices', 0, 'delta', 'content')
70
- if partial
71
- content += partial
72
- block.call({ who: 'AI', message: partial }, false)
76
+ partial_content = chunk.dig('choices', 0, 'delta', 'content')
77
+ partial_tools = chunk.dig('choices', 0, 'delta', 'tool_calls')
78
+
79
+ if partial_tools
80
+ partial_tools.each do |partial_tool|
81
+ tools[partial_tool['index']] = {} if tools[partial_tool['index']].nil?
82
+
83
+ partial_tool.keys.reject { |key| ['index'].include?(key) }.each do |key|
84
+ target = tools[partial_tool['index']]
85
+
86
+ if partial_tool[key].is_a?(Hash)
87
+ target[key] = {} if target[key].nil?
88
+ partial_tool[key].each_key do |sub_key|
89
+ target[key][sub_key] = '' if target[key][sub_key].nil?
90
+
91
+ target[key][sub_key] += partial_tool[key][sub_key]
92
+ end
93
+ else
94
+ target[key] = '' if target[key].nil?
95
+
96
+ target[key] += partial_tool[key]
97
+ end
98
+ end
99
+ end
73
100
  end
74
101
 
75
- block.call({ who: 'AI', message: content }, true) if chunk.dig('choices', 0, 'finish_reason')
102
+ if partial_content
103
+ content += partial_content
104
+ feedback.call(
105
+ { should_be_stored: false,
106
+ interaction: { who: 'AI', message: partial_content } }
107
+ )
108
+ end
109
+
110
+ if chunk.dig('choices', 0, 'finish_reason')
111
+ if tools&.size&.positive?
112
+ feedback.call(
113
+ { should_be_stored: true,
114
+ needs_another_round: true,
115
+ interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } }
116
+ )
117
+ Tools.apply(cartridge, input[:tools], tools, feedback).each do |interaction|
118
+ feedback.call({ should_be_stored: true, needs_another_round: true, interaction: })
119
+ end
120
+ end
121
+
122
+ feedback.call(
123
+ { should_be_stored: !(content.nil? || content == ''),
124
+ interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content },
125
+ finished: true }
126
+ )
127
+ end
76
128
  end
77
129
 
78
- @client.chat(parameters: payload)
130
+ begin
131
+ @client.chat(parameters: payload)
132
+ rescue StandardError => e
133
+ raise e.class, e.response[:body] if e.response && e.response[:body]
134
+
135
+ raise e
136
+ end
79
137
  else
80
- result = @client.chat(parameters: payload)
138
+ begin
139
+ result = @client.chat(parameters: payload)
140
+ rescue StandardError => e
141
+ raise e.class, e.response[:body] if e.response && e.response[:body]
142
+
143
+ raise e
144
+ end
81
145
 
82
146
  raise StandardError, result['error'] if result['error']
83
147
 
84
- block.call({ who: 'AI', message: result.dig('choices', 0, 'message', 'content') }, true)
148
+ tools = result.dig('choices', 0, 'message', 'tool_calls')
149
+
150
+ if tools&.size&.positive?
151
+ feedback.call(
152
+ { should_be_stored: true,
153
+ needs_another_round: true,
154
+ interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } }
155
+ )
156
+ Tools.apply(cartridge, input[:tools], tools, feedback).each do |interaction|
157
+ feedback.call({ should_be_stored: true, needs_another_round: true, interaction: })
158
+ end
159
+ end
160
+
161
+ content = result.dig('choices', 0, 'message', 'content')
162
+
163
+ feedback.call(
164
+ { should_be_stored: !(content.nil? || content == ''),
165
+ interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content },
166
+ finished: true }
167
+ )
85
168
  end
86
169
  end
87
170
 
@@ -3,7 +3,7 @@
3
3
  require 'babosa'
4
4
 
5
5
  require_relative '../logic/helpers/hash'
6
- require_relative './crypto'
6
+ require_relative 'crypto'
7
7
 
8
8
  module NanoBot
9
9
  module Components
data/components/stream.rb CHANGED
@@ -7,7 +7,12 @@ module NanoBot
7
7
  class Stream < StringIO
8
8
  def write(*args)
9
9
  if @callback
10
- @accumulated += args.first
10
+ begin
11
+ @accumulated += args.first
12
+ rescue StandardError => _e
13
+ @accumulated = "#{@accumulated.force_encoding('UTF-8')}#{args.first.force_encoding('UTF-8')}"
14
+ end
15
+
11
16
  @callback.call(@accumulated, args.first, false)
12
17
  end
13
18
  super
@@ -23,7 +23,7 @@ module NanoBot
23
23
 
24
24
  files.values.uniq.map do |file|
25
25
  cartridge = Logic::Helpers::Hash.symbolize_keys(
26
- YAML.safe_load(File.read(file[:path]), permitted_classes: [Symbol])
26
+ YAML.safe_load_file(file[:path], permitted_classes: [Symbol])
27
27
  ).merge({
28
28
  system: {
29
29
  id: file[:path].to_s.sub(/^#{Regexp.escape(file[:base])}/, '').sub(%r{^/}, '').sub(/\.[^.]+\z/,
@@ -6,9 +6,9 @@ require_relative '../logic/helpers/hash'
6
6
  require_relative '../components/provider'
7
7
  require_relative '../components/storage'
8
8
  require_relative '../components/stream'
9
- require_relative './interfaces/repl'
10
- require_relative './interfaces/eval'
11
- require_relative './session'
9
+ require_relative 'interfaces/repl'
10
+ require_relative 'interfaces/eval'
11
+ require_relative 'session'
12
12
 
13
13
  module NanoBot
14
14
  module Controllers
@@ -83,7 +83,7 @@ module NanoBot
83
83
  raise StandardError, "Cartridge file not found: \"#{path}\""
84
84
  end
85
85
 
86
- @cartridge = YAML.safe_load(File.read(elected_path), permitted_classes: [Symbol])
86
+ @cartridge = YAML.safe_load_file(elected_path, permitted_classes: [Symbol])
87
87
  end
88
88
 
89
89
  @safe_cartridge = Marshal.load(Marshal.dump(@cartridge))
@@ -81,7 +81,7 @@ module NanoBot
81
81
  when 'cartridge'
82
82
  puts YAML.dump(bot.cartridge)
83
83
  else
84
- raise "TODO: [#{params[:command]}]"
84
+ raise "Command not found: [#{params[:command]}]"
85
85
  end
86
86
  end
87
87
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rainbow'
4
+
5
+ require_relative '../../logic/cartridge/tools'
6
+ require_relative '../../logic/cartridge/safety'
7
+ require_relative '../../components/embedding'
8
+
9
+ module NanoBot
10
+ module Controllers
11
+ module Interfaces
12
+ module Tool
13
+ def self.confirming(session, cartridge, mode, feedback)
14
+ yeses = Logic::Cartridge::Safety.yeses(cartridge)
15
+ default_answer = Logic::Cartridge::Safety.default_answer(cartridge)
16
+ dispatch_feedback(session, cartridge, mode, feedback)
17
+ session.flush
18
+ answer = $stdin.gets.chomp.to_s.downcase.strip
19
+ answer = default_answer if answer == ''
20
+ session.print("\n")
21
+ yeses.include?(answer)
22
+ end
23
+
24
+ def self.adapt(feedback, adapter, cartridge)
25
+ call = {
26
+ parameters: %w[id name parameters parameters-as-json output],
27
+ values: [
28
+ feedback[:id], feedback[:name], feedback[:parameters],
29
+ feedback[:parameters].to_json,
30
+ feedback[:output]
31
+ ],
32
+ safety: { sandboxed: Logic::Cartridge::Safety.sandboxed?(cartridge) }
33
+ }
34
+
35
+ raise StandardError, 'conflicting adapters' if %i[fennel lua clojure].count { |key| !adapter[key].nil? } > 1
36
+
37
+ if adapter[:fennel]
38
+ call[:source] = adapter[:fennel]
39
+ Components::Embedding.fennel(**call)
40
+ elsif adapter[:clojure]
41
+ call[:source] = adapter[:clojure]
42
+ Components::Embedding.clojure(**call)
43
+ elsif adapter[:lua]
44
+ call[:parameters] = %w[id name parameters parameters_as_json output]
45
+ call[:source] = adapter[:lua]
46
+ Components::Embedding.lua(**call)
47
+ else
48
+ raise 'missing handler for adapter'
49
+ end
50
+ end
51
+
52
+ def self.dispatch_feedback(session, cartridge, mode, feedback)
53
+ enabled = Logic::Cartridge::Tools.feedback?(cartridge, mode.to_sym, feedback[:action].to_sym)
54
+
55
+ enabled = true if feedback[:action].to_sym == :confirming
56
+
57
+ return unless enabled
58
+
59
+ color = Logic::Cartridge::Tools.fetch_from_interface(
60
+ cartridge, mode.to_sym, feedback[:action].to_sym, [:color]
61
+ )
62
+
63
+ adapter = Tool.adapter(cartridge, mode, feedback)
64
+
65
+ if %i[fennel lua clojure].any? { |key| !adapter[key].nil? }
66
+ message = adapt(feedback, adapter, cartridge)
67
+ else
68
+ message = "#{feedback[:name]} #{feedback[:parameters].to_json}"
69
+
70
+ message += "\n#{feedback[:output]}" if feedback[:action].to_sym == :responding
71
+ end
72
+
73
+ message = "#{adapter[:prefix]}#{message}#{adapter[:suffix]}"
74
+
75
+ session.print(color.nil? ? message : Rainbow(message).send(color))
76
+ end
77
+
78
+ def self.adapter(cartridge, mode, feedback)
79
+ prefix = Logic::Cartridge::Tools.fetch_from_interface(
80
+ cartridge, mode.to_sym, feedback[:action].to_sym, [:prefix]
81
+ )
82
+
83
+ suffix = Logic::Cartridge::Tools.fetch_from_interface(
84
+ cartridge, mode.to_sym, feedback[:action].to_sym, [:suffix]
85
+ )
86
+
87
+ fennel = Logic::Cartridge::Tools.fetch_from_interface(
88
+ cartridge, mode.to_sym, feedback[:action].to_sym, %i[adapter fennel]
89
+ )
90
+
91
+ lua = Logic::Cartridge::Tools.fetch_from_interface(
92
+ cartridge, mode.to_sym, feedback[:action].to_sym, %i[adapter lua]
93
+ )
94
+
95
+ clojure = Logic::Cartridge::Tools.fetch_from_interface(
96
+ cartridge, mode.to_sym, feedback[:action].to_sym, %i[adapter clojure]
97
+ )
98
+
99
+ { prefix:, suffix:, fennel:, lua:, clojure: }
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -18,11 +18,10 @@ module NanoBot
18
18
  password = 'UNSAFE' unless password && password != ''
19
19
 
20
20
  {
21
- encryption: (
21
+ encryption:
22
22
  Components::Crypto.encrypt('SAFE') != 'SAFE' &&
23
- Components::Crypto.encrypt('SAFE') != Components::Crypto.encrypt('SAFE') &&
24
- Components::Crypto.decrypt(Components::Crypto.encrypt('SAFE')) == 'SAFE'
25
- ),
23
+ Components::Crypto.encrypt('SAFE') != Components::Crypto.encrypt('SAFE') &&
24
+ Components::Crypto.decrypt(Components::Crypto.encrypt('SAFE')) == 'SAFE',
26
25
  password: password != 'UNSAFE'
27
26
  }
28
27
  end
@@ -3,10 +3,14 @@
3
3
  require 'babosa'
4
4
 
5
5
  require 'fileutils'
6
+ require 'rainbow'
6
7
 
7
8
  require_relative '../logic/helpers/hash'
9
+ require_relative '../logic/cartridge/safety'
8
10
  require_relative '../logic/cartridge/streaming'
9
11
  require_relative '../logic/cartridge/interaction'
12
+ require_relative '../logic/cartridge/fetch'
13
+ require_relative 'interfaces/tools'
10
14
  require_relative '../components/storage'
11
15
  require_relative '../components/adapter'
12
16
  require_relative '../components/crypto'
@@ -14,6 +18,7 @@ require_relative '../components/crypto'
14
18
  module NanoBot
15
19
  module Controllers
16
20
  STREAM_TIMEOUT_IN_SECONDS = 5
21
+ INFINITE_LOOP_PREVENTION = 10
17
22
 
18
23
  class Session
19
24
  attr_accessor :stream
@@ -41,9 +46,9 @@ module NanoBot
41
46
  end
42
47
 
43
48
  def load_state
44
- @state = Logic::Helpers::Hash.symbolize_keys(JSON.parse(
45
- Components::Crypto.decrypt(File.read(@state_path))
46
- ))
49
+ @state = Logic::Helpers::Hash.symbolize_keys(
50
+ JSON.parse(Components::Crypto.decrypt(File.read(@state_path)))
51
+ )
47
52
  end
48
53
 
49
54
  def store_state!
@@ -68,7 +73,7 @@ module NanoBot
68
73
  mode: mode.to_s,
69
74
  input: message,
70
75
  message: Components::Adapter.apply(
71
- :input, Logic::Cartridge::Interaction.input(@cartridge, mode.to_sym, message)
76
+ Logic::Cartridge::Interaction.input(@cartridge, mode.to_sym, message), @cartridge
72
77
  )
73
78
  }
74
79
 
@@ -78,41 +83,88 @@ module NanoBot
78
83
  end
79
84
 
80
85
  def process(input, mode:)
86
+ interface = Logic::Helpers::Hash.fetch(@cartridge, [:interfaces, mode.to_sym]) || {}
87
+
88
+ input[:interface] = interface
89
+ input[:tools] = @cartridge[:tools]
90
+
91
+ needs_another_round = true
92
+
93
+ rounds = 0
94
+
95
+ while needs_another_round
96
+ needs_another_round = process_interaction(input, mode:)
97
+ rounds += 1
98
+ raise StandardError, 'infinite loop prevention' if rounds > INFINITE_LOOP_PREVENTION
99
+ end
100
+ end
101
+
102
+ def process_interaction(input, mode:)
81
103
  prefix = Logic::Cartridge::Affixes.get(@cartridge, mode.to_sym, :output, :prefix)
82
104
  suffix = Logic::Cartridge::Affixes.get(@cartridge, mode.to_sym, :output, :suffix)
83
105
 
84
- interface = Logic::Helpers::Hash.fetch(@cartridge, [:interfaces, mode.to_sym]) || {}
106
+ color = Logic::Cartridge::Fetch.cascate(
107
+ @cartridge, [[:interfaces, mode.to_sym, :output, :color], %i[interfaces output color]]
108
+ )
85
109
 
86
- streaming = Logic::Cartridge::Streaming.enabled?(@cartridge, mode.to_sym)
110
+ color = color.to_sym if color
87
111
 
88
- input[:interface] = interface
112
+ streaming = Logic::Cartridge::Streaming.enabled?(@cartridge, mode.to_sym)
89
113
 
90
114
  updated_at = Time.now
91
115
 
92
116
  ready = false
93
- @provider.evaluate(input) do |output, finished|
94
- updated_at = Time.now
95
-
96
- if finished
97
- event = Marshal.load(Marshal.dump(output))
98
117
 
99
- output = Logic::Cartridge::Interaction.output(
100
- @cartridge, mode.to_sym, output, streaming, finished
101
- )
118
+ needs_another_round = false
102
119
 
103
- output[:message] = Components::Adapter.apply(:output, output[:message])
120
+ @provider.evaluate(input, streaming, @cartridge) do |feedback|
121
+ needs_another_round = true if feedback[:needs_another_round]
104
122
 
105
- event[:mode] = mode.to_s
106
- event[:output] = "#{prefix}#{output[:message]}#{suffix}"
107
-
108
- @state[:history] << event
109
-
110
- self.print(output[:message]) unless streaming
123
+ updated_at = Time.now
111
124
 
112
- ready = true
113
- flush
114
- elsif streaming
115
- self.print(output[:message])
125
+ if feedback[:interaction] &&
126
+ feedback.dig(:interaction, :meta, :tool, :action) &&
127
+ feedback[:interaction][:meta][:tool][:action] == 'confirming'
128
+ Interfaces::Tool.confirming(self, @cartridge, mode, feedback[:interaction][:meta][:tool])
129
+ else
130
+
131
+ if feedback[:interaction] && feedback.dig(:interaction, :meta, :tool, :action)
132
+ Interfaces::Tool.dispatch_feedback(
133
+ self, @cartridge, mode, feedback[:interaction][:meta][:tool]
134
+ )
135
+ end
136
+
137
+ if feedback[:interaction]
138
+ event = Marshal.load(Marshal.dump(feedback[:interaction]))
139
+ event[:mode] = mode.to_s
140
+ event[:output] = nil
141
+
142
+ if feedback[:interaction][:who] == 'AI' && feedback[:interaction][:message]
143
+ event[:output] = feedback[:interaction][:message]
144
+ unless streaming
145
+ output = Logic::Cartridge::Interaction.output(
146
+ @cartridge, mode.to_sym, feedback[:interaction], streaming, feedback[:finished]
147
+ )
148
+ output[:message] = Components::Adapter.apply(output[:message], @cartridge)
149
+ event[:output] = (output[:message]).to_s
150
+ end
151
+ end
152
+
153
+ @state[:history] << event if feedback[:should_be_stored]
154
+
155
+ if event[:output] && ((!feedback[:finished] && streaming) || (!streaming && feedback[:finished]))
156
+ self.print(color ? Rainbow(event[:output]).send(color) : event[:output])
157
+ end
158
+
159
+ # The `print` function already outputs a prefix and a suffix, so
160
+ # we should add them afterwards to avoid printing them twice.
161
+ event[:output] = "#{prefix}#{event[:output]}#{suffix}"
162
+ end
163
+
164
+ if feedback[:finished]
165
+ flush
166
+ ready = true
167
+ end
116
168
  end
117
169
  end
118
170
 
@@ -122,6 +174,8 @@ module NanoBot
122
174
  end
123
175
 
124
176
  store_state! unless @stateless
177
+
178
+ needs_another_round
125
179
  end
126
180
 
127
181
  def flush
@@ -1,9 +1,8 @@
1
- version: '3.7'
2
-
1
+ ---
3
2
  services:
4
3
  nano-bots:
5
- image: ruby:3.2.2-slim-bullseye
6
- command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libsodium-dev lua5.4-dev && gem install nano-bots -v 0.1.1 && bash"
4
+ image: ruby:3.2.2-slim-bookworm
5
+ command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev libsodium-dev lua5.4-dev curl && curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | bash && gem install nano-bots -v 1.0.1 && bash"
7
6
  environment:
8
7
  OPENAI_API_ADDRESS: https://api.openai.com
9
8
  OPENAI_API_KEY: your-access-token
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../helpers/hash'
4
- require_relative './default'
4
+ require_relative 'default'
5
5
 
6
6
  module NanoBot
7
7
  module Logic
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../helpers/hash'
4
- require_relative './default'
4
+ require_relative 'default'
5
5
 
6
6
  module NanoBot
7
7
  module Logic
@@ -15,7 +15,7 @@ module NanoBot
15
15
  return @values if @values
16
16
 
17
17
  path = File.expand_path('../../static/cartridges/default.yml', __dir__)
18
- cartridge = YAML.safe_load(File.read(path), permitted_classes: [Symbol])
18
+ cartridge = YAML.safe_load_file(path, permitted_classes: [Symbol])
19
19
  @values = Logic::Helpers::Hash.symbolize_keys(cartridge)
20
20
  @values
21
21
  end
@@ -24,7 +24,7 @@ module NanoBot
24
24
  return @baseline if @baseline
25
25
 
26
26
  path = File.expand_path('../../static/cartridges/baseline.yml', __dir__)
27
- cartridge = YAML.safe_load(File.read(path), permitted_classes: [Symbol])
27
+ cartridge = YAML.safe_load_file(path, permitted_classes: [Symbol])
28
28
  @baseline = Logic::Helpers::Hash.symbolize_keys(cartridge)
29
29
  @baseline
30
30
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'default'
4
+ require_relative '../helpers/hash'
5
+
6
+ module NanoBot
7
+ module Logic
8
+ module Cartridge
9
+ module Fetch
10
+ def self.cascate(cartridge, paths)
11
+ results = paths.map { |path| Helpers::Hash.fetch(cartridge, path) }
12
+ result = results.find { |candidate| !candidate.nil? }
13
+ return result unless result.nil?
14
+
15
+ results = paths.map { |path| Helpers::Hash.fetch(Default.instance.values, path) }
16
+ result = results.find { |candidate| !candidate.nil? }
17
+ return result unless result.nil?
18
+
19
+ nil
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'sweet-moon'
4
-
5
- require_relative './affixes'
6
- require_relative './adapters'
3
+ require_relative 'affixes'
4
+ require_relative 'adapters'
7
5
 
8
6
  module NanoBot
9
7
  module Logic
@@ -12,23 +10,25 @@ module NanoBot
12
10
  def self.input(cartridge, interface, content)
13
11
  lua = Adapter.expression(cartridge, interface, :input, :lua)
14
12
  fennel = Adapter.expression(cartridge, interface, :input, :fennel)
13
+ clojure = Adapter.expression(cartridge, interface, :input, :clojure)
15
14
 
16
15
  prefix = Affixes.get(cartridge, interface, :input, :prefix)
17
16
  suffix = Affixes.get(cartridge, interface, :input, :suffix)
18
17
 
19
- { content:, prefix:, suffix:, lua:, fennel: }
18
+ { content:, prefix:, suffix:, lua:, fennel:, clojure: }
20
19
  end
21
20
 
22
21
  def self.output(cartridge, interface, result, streaming, _finished)
23
22
  if streaming
24
- result[:message] = { content: result[:message], lua: nil, fennel: nil }
23
+ result[:message] = { content: result[:message], lua: nil, fennel: nil, clojure: nil }
25
24
  return result
26
25
  end
27
26
 
28
27
  lua = Adapter.expression(cartridge, interface, :output, :lua)
29
28
  fennel = Adapter.expression(cartridge, interface, :output, :fennel)
29
+ clojure = Adapter.expression(cartridge, interface, :output, :clojure)
30
30
 
31
- result[:message] = { content: result[:message], lua:, fennel: }
31
+ result[:message] = { content: result[:message], lua:, fennel:, clojure: }
32
32
 
33
33
  result
34
34
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fetch'
4
+
5
+ module NanoBot
6
+ module Logic
7
+ module Cartridge
8
+ module Safety
9
+ def self.default_answer(cartridge)
10
+ default = Fetch.cascate(cartridge, [%i[interfaces tools confirming default]])
11
+ return [] if default.nil?
12
+
13
+ default
14
+ end
15
+
16
+ def self.yeses(cartridge)
17
+ yeses_values = Fetch.cascate(cartridge, [%i[interfaces tools confirming yeses]])
18
+ return [] if yeses_values.nil?
19
+
20
+ yeses_values
21
+ end
22
+
23
+ def self.confirmable?(cartridge)
24
+ confirmable = Fetch.cascate(cartridge, [%i[safety tools confirmable]])
25
+ return true if confirmable.nil?
26
+
27
+ confirmable
28
+ end
29
+
30
+ def self.sandboxed?(cartridge)
31
+ sandboxed = Fetch.cascate(cartridge, [%i[safety functions sandboxed]])
32
+ return true if sandboxed.nil?
33
+
34
+ sandboxed
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end