summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoricebaker <icebaker@proton.me>2023-11-18 19:07:10 -0300
committericebaker <icebaker@proton.me>2023-11-18 19:07:10 -0300
commit8ae78b954350755a47a13133668dba93bac15f37 (patch)
tree9cdc3bb770d778bd8d00675fdbc1f27a6e27e37c
parentab22d1bbe37093912cb7418b3c945153a15f4255 (diff)
adding support for tools
-rw-r--r--components/adapter.rb40
-rw-r--r--components/embedding.rb75
-rw-r--r--components/provider.rb2
-rw-r--r--components/providers/openai.rb100
-rw-r--r--components/providers/openai/tools.rb73
-rw-r--r--components/storage.rb2
-rw-r--r--components/stream.rb7
-rw-r--r--controllers/cartridges.rb2
-rw-r--r--controllers/instance.rb8
-rw-r--r--controllers/interfaces/repl.rb2
-rw-r--r--controllers/interfaces/tools.rb90
-rw-r--r--controllers/session.rb89
-rw-r--r--logic/cartridge/adapters.rb2
-rw-r--r--logic/cartridge/affixes.rb2
-rw-r--r--logic/cartridge/default.rb4
-rw-r--r--logic/cartridge/fetch.rb24
-rw-r--r--logic/cartridge/interaction.rb14
-rw-r--r--logic/cartridge/tools.rb53
-rw-r--r--logic/providers/openai.rb57
-rw-r--r--logic/providers/openai/tools.rb70
-rw-r--r--spec/data/cartridges/tools.yml28
-rw-r--r--spec/data/providers/openai/tools.yml11
-rw-r--r--spec/logic/cartridge/affixes_spec.rb6
-rw-r--r--spec/logic/cartridge/interaction_spec.rb18
-rw-r--r--spec/logic/cartridge/streaming_spec.rb34
-rw-r--r--spec/logic/cartridge/tools_spec.rb97
-rw-r--r--spec/logic/providers/openai/tools_spec.rb73
-rw-r--r--spec/spec_helper.rb4
-rw-r--r--static/cartridges/default.yml5
29 files changed, 889 insertions, 103 deletions
diff --git a/components/adapter.rb b/components/adapter.rb
index a361437..a79fee6 100644
--- a/components/adapter.rb
+++ b/components/adapter.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'sweet-moon'
+require_relative 'embedding'
module NanoBot
module Components
@@ -8,41 +8,23 @@ module NanoBot
def self.apply(_direction, params)
content = params[:content]
- if params[:fennel] && params[:lua]
- raise StandardError, 'Adapter conflict: You can only use either Lua or Fennel, not both.'
- end
+ raise StandardError, 'conflicting adapters' if %i[fennel lua clojure].count { |key| !params[key].nil? } > 1
+
+ call = { parameters: %w[content], values: [content], safety: false }
if params[:fennel]
- content = fennel(content, params[:fennel])
+ call[:source] = params[:fennel]
+ content = Components::Embedding.fennel(**call)
+ elsif params[:clojure]
+ call[:source] = params[:clojure]
+ content = Components::Embedding.clojure(**call)
elsif params[:lua]
- content = lua(content, params[:lua])
+ call[:source] = params[:lua]
+ content = Components::Embedding.lua(**call)
end
"#{params[:prefix]}#{content}#{params[:suffix]}"
end
-
- def self.fennel(content, expression)
- path = "#{File.expand_path('../static/fennel', __dir__)}/?.lua"
- state = SweetMoon::State.new(package_path: path).fennel
- # TODO: global is deprecated...
- state.fennel.eval(
- "(global adapter (fn [content] #{expression}))", 1,
- { allowedGlobals: %w[math string table] }
- )
- adapter = state.get(:adapter)
- adapter.call([content])
- end
-
- def self.lua(content, expression)
- state = SweetMoon::State.new
- code = "_, adapter = pcall(load('return function(content) return #{
- expression.gsub("'", "\\\\'")
- }; end', nil, 't', {math=math,string=string,table=table}))"
-
- state.eval(code)
- adapter = state.get(:adapter)
- adapter.call([content])
- end
end
end
end
diff --git a/components/embedding.rb b/components/embedding.rb
new file mode 100644
index 0000000..c464244
--- /dev/null
+++ b/components/embedding.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'sweet-moon'
+
+require 'open3'
+require 'json'
+require 'tempfile'
+
+module NanoBot
+ module Components
+ class Embedding
+ def self.lua(source:, parameters:, values:, safety:)
+ state = SweetMoon::State.new
+ # code = "_, embedded = pcall(load([[\nreturn function(#{parameters.join(', ')})\nreturn #{source}\nend\n]], nil, 't', {math=math,string=string,table=table}))"
+ code = "_, embedded = pcall(load([[\nreturn function(#{parameters.join(', ')})\n#{source}\nend\n]], nil, 't'))"
+
+ state.eval(code)
+ embedded = state.get(:embedded)
+ embedded.call(values)
+ end
+
+ def self.fennel(source:, parameters:, values:, safety:)
+ path = "#{File.expand_path('../static/fennel', __dir__)}/?.lua"
+ state = SweetMoon::State.new(package_path: path).fennel
+
+ # TODO: global is deprecated...
+ state.fennel.eval(
+ "(global embedded (fn [#{parameters.join(' ')}] #{source}))", 1,
+ safety ? { allowedGlobals: %w[math string table] } : nil
+ )
+ embedded = state.get(:embedded)
+ embedded.call(values)
+ end
+
+ def self.clojure(source:, parameters:, values:, safety:)
+ raise 'invalid Clojure parameter name' if parameters.include?('injected-parameters')
+
+ key_value = {}
+
+ parameters.each_with_index { |key, index| key_value[key] = values[index] }
+
+ parameters_json = key_value.to_json
+
+ json_file = Tempfile.new(['nano-bot', '.json'])
+ clojure_file = Tempfile.new(['nano-bot', '.clj'])
+
+ begin
+ json_file.write(parameters_json)
+ json_file.close
+
+ clojure_source = <<~CLOJURE
+ (require '[cheshire.core :as json])
+ (def injected-parameters (json/parse-string (slurp (java.io.FileReader. "#{json_file.path}"))))
+
+ #{parameters.map { |p| "(def #{p} (get injected-parameters \"#{p}\"))" }.join("\n")}
+
+ #{source}
+ CLOJURE
+
+ clojure_file.write(clojure_source)
+ clojure_file.close
+
+ bb_command = "bb --prn #{clojure_file.path} | bb -e \"(->> *in* slurp read-string print)\""
+
+ stdout, stderr, status = Open3.capture3(bb_command)
+
+ status.success? ? stdout : stderr
+ ensure
+ json_file&.unlink
+ clojure_file&.unlink
+ end
+ end
+ end
+ end
+end
diff --git a/components/provider.rb b/components/provider.rb
index 163b099..3414009 100644
--- a/components/provider.rb
+++ b/components/provider.rb
@@ -2,7 +2,7 @@
require 'openai'
-require_relative './providers/openai'
+require_relative 'providers/openai'
module NanoBot
module Components
diff --git a/components/providers/openai.rb b/components/providers/openai.rb
index ce6fb33..437114c 100644
--- a/components/providers/openai.rb
+++ b/components/providers/openai.rb
@@ -2,9 +2,14 @@
require 'openai'
-require_relative './base'
+require_relative 'base'
require_relative '../crypto'
+require_relative '../../logic/providers/openai/tools'
+require_relative '../../controllers/interfaces/tools'
+
+require_relative 'openai/tools'
+
module NanoBot
module Components
module Providers
@@ -34,15 +39,23 @@ module NanoBot
def stream(input)
provider = @settings.key?(:stream) ? @settings[:stream] : true
+
+ # TODO: There's a bug here...
interface = input[:interface].key?(:stream) ? input[:interface][:stream] : true
provider && interface
end
- def evaluate(input, &block)
+ def evaluate(input, &feedback)
messages = input[:history].map do |event|
- { role: event[:who] == 'user' ? 'user' : 'assistant',
- content: event[:message] }
+ if event[:message].nil? && event[:meta] && event[:meta][:tool_calls]
+ { role: 'assistant', content: nil, tool_calls: event[:meta][:tool_calls] }
+ elsif event[:who] == 'tool'
+ { role: event[:who], content: event[:message],
+ tool_call_id: event[:meta][:id], name: event[:meta][:name] }
+ else
+ { role: event[:who] == 'user' ? 'user' : 'assistant', content: event[:message] }
+ end
end
%i[instruction backdrop directive].each do |key|
@@ -62,17 +75,65 @@ module NanoBot
payload.delete(:logit_bias) if payload.key?(:logit_bias) && payload[:logit_bias].nil?
+ payload[:tools] = input[:tools].map { |raw| NanoBot::Logic::OpenAI::Tools.adapt(raw) } if input[:tools]
+
if stream(input)
content = ''
+ tools = []
payload[:stream] = proc do |chunk, _bytesize|
- partial = chunk.dig('choices', 0, 'delta', 'content')
- if partial
- content += partial
- block.call({ who: 'AI', message: partial }, false)
+ partial_content = chunk.dig('choices', 0, 'delta', 'content')
+ partial_tools = chunk.dig('choices', 0, 'delta', 'tool_calls')
+
+ if partial_tools
+ partial_tools.each do |partial_tool|
+ tools[partial_tool['index']] = {} if tools[partial_tool['index']].nil?
+
+ partial_tool.keys.reject { |key| ['index'].include?(key) }.each do |key|
+ target = tools[partial_tool['index']]
+
+ if partial_tool[key].is_a?(Hash)
+ target[key] = {} if target[key].nil?
+ partial_tool[key].each_key do |sub_key|
+ target[key][sub_key] = '' if target[key][sub_key].nil?
+
+ target[key][sub_key] += partial_tool[key][sub_key]
+ end
+ else
+ target[key] = '' if target[key].nil?
+
+ target[key] += partial_tool[key]
+ end
+ end
+ end
+ end
+
+ if partial_content
+ content += partial_content
+ feedback.call(
+ { should_be_stored: false,
+ interaction: { who: 'AI', message: partial_content } }
+ )
end
- block.call({ who: 'AI', message: content }, true) if chunk.dig('choices', 0, 'finish_reason')
+ if chunk.dig('choices', 0, 'finish_reason')
+ if tools&.size&.positive?
+ feedback.call(
+ { should_be_stored: true,
+ needs_another_round: true,
+ interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } }
+ )
+ Tools.apply(input[:tools], tools, feedback).each do |interaction|
+ feedback.call({ should_be_stored: true, needs_another_round: true, interaction: })
+ end
+ end
+
+ feedback.call(
+ { should_be_stored: !(content.nil? || content == ''),
+ interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content },
+ finished: true }
+ )
+ end
end
@client.chat(parameters: payload)
@@ -81,7 +142,26 @@ module NanoBot
raise StandardError, result['error'] if result['error']
- block.call({ who: 'AI', message: result.dig('choices', 0, 'message', 'content') }, true)
+ tools = result.dig('choices', 0, 'message', 'tool_calls')
+
+ if tools&.size&.positive?
+ feedback.call(
+ { should_be_stored: true,
+ needs_another_round: true,
+ interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } }
+ )
+ Tools.apply(input[:tools], tools, feedback).each do |interaction|
+ feedback.call({ should_be_stored: true, needs_another_round: true, interaction: })
+ end
+ end
+
+ content = result.dig('choices', 0, 'message', 'content')
+
+ feedback.call(
+ { should_be_stored: !(content.nil? || content == ''),
+ interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content },
+ finished: true }
+ )
end
end
diff --git a/components/providers/openai/tools.rb b/components/providers/openai/tools.rb
new file mode 100644
index 0000000..ea34ae6
--- /dev/null
+++ b/components/providers/openai/tools.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require_relative '../../embedding'
+
+require 'concurrent'
+
+module NanoBot
+ module Components
+ module Providers
+ class OpenAI < Base
+ module Tools
+ def self.apply(cartridge, tools, feedback)
+ prepared_tools = NanoBot::Logic::OpenAI::Tools.prepare(cartridge, tools)
+
+ futures = prepared_tools.map do |tool|
+ Concurrent::Promises.future { process!(tool, feedback) }
+ end
+
+ results = Concurrent::Promises.zip(*futures).value!
+
+ results.map do |applied_tool|
+ {
+ who: 'tool',
+ message: applied_tool[:output],
+ meta: { id: applied_tool[:id], name: applied_tool[:name] }
+ }
+ end
+ end
+
+ def self.process!(tool, feedback)
+ feedback.call(
+ { should_be_stored: false,
+ interaction: { who: 'AI', message: nil, meta: {
+ tool: { action: 'call', id: tool[:id], name: tool[:name], parameters: tool[:parameters] }
+ } } }
+ )
+
+ call = { parameters: %w[parameters], values: [tool[:parameters]], safety: false }
+
+ if %i[fennel lua clojure].count { |key| !tool[:source][key].nil? } > 1
+ raise StandardError, 'conflicting tools'
+ end
+
+ if !tool[:source][:fennel].nil?
+ call[:source] = tool[:source][:fennel]
+ tool[:output] = Components::Embedding.fennel(**call)
+ elsif !tool[:source][:clojure].nil?
+ call[:source] = tool[:source][:clojure]
+ tool[:output] = Components::Embedding.clojure(**call)
+ elsif !tool[:source][:lua].nil?
+ call[:source] = tool[:source][:lua]
+ tool[:output] = Components::Embedding.lua(**call)
+ else
+ raise 'missing source code'
+ end
+
+ feedback.call(
+ { should_be_stored: false,
+ interaction: { who: 'AI', message: nil, meta: {
+ tool: {
+ action: 'response', id: tool[:id], name: tool[:name],
+ parameters: tool[:parameters], output: tool[:output]
+ }
+ } } }
+ )
+
+ tool
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/components/storage.rb b/components/storage.rb
index 577ce67..6a3fe13 100644
--- a/components/storage.rb
+++ b/components/storage.rb
@@ -3,7 +3,7 @@
require 'babosa'
require_relative '../logic/helpers/hash'
-require_relative './crypto'
+require_relative 'crypto'
module NanoBot
module Components
diff --git a/components/stream.rb b/components/stream.rb
index 45c4a2b..347eb87 100644
--- a/components/stream.rb
+++ b/components/stream.rb
@@ -7,7 +7,12 @@ module NanoBot
class Stream < StringIO
def write(*args)
if @callback
- @accumulated += args.first
+ begin
+ @accumulated += args.first
+ rescue StandardError => _e
+ @accumulated = "#{@accumulated.force_encoding('UTF-8')}#{args.first.force_encoding('UTF-8')}"
+ end
+
@callback.call(@accumulated, args.first, false)
end
super
diff --git a/controllers/cartridges.rb b/controllers/cartridges.rb
index fe0d56e..df474a9 100644
--- a/controllers/cartridges.rb
+++ b/controllers/cartridges.rb
@@ -23,7 +23,7 @@ module NanoBot
files.values.uniq.map do |file|
cartridge = Logic::Helpers::Hash.symbolize_keys(
- YAML.safe_load(File.read(file[:path]), permitted_classes: [Symbol])
+ YAML.safe_load_file(file[:path], permitted_classes: [Symbol])
).merge({
system: {
id: file[:path].to_s.sub(/^#{Regexp.escape(file[:base])}/, '').sub(%r{^/}, '').sub(/\.[^.]+\z/,
diff --git a/controllers/instance.rb b/controllers/instance.rb
index d4e0c1b..259a548 100644
--- a/controllers/instance.rb
+++ b/controllers/instance.rb
@@ -6,9 +6,9 @@ require_relative '../logic/helpers/hash'
require_relative '../components/provider'
require_relative '../components/storage'
require_relative '../components/stream'
-require_relative './interfaces/repl'
-require_relative './interfaces/eval'
-require_relative './session'
+require_relative 'interfaces/repl'
+require_relative 'interfaces/eval'
+require_relative 'session'
module NanoBot
module Controllers
@@ -83,7 +83,7 @@ module NanoBot
raise StandardError, "Cartridge file not found: \"#{path}\""
end
- @cartridge = YAML.safe_load(File.read(elected_path), permitted_classes: [Symbol])
+ @cartridge = YAML.safe_load_file(elected_path, permitted_classes: [Symbol])
end
@safe_cartridge = Marshal.load(Marshal.dump(@cartridge))
diff --git a/controllers/interfaces/repl.rb b/controllers/interfaces/repl.rb
index fd16ea6..d4191b1 100644
--- a/controllers/interfaces/repl.rb
+++ b/controllers/interfaces/repl.rb
@@ -37,6 +37,8 @@ module NanoBot
[proc { prompt }, proc { 'MISSING INPUT' }]
)
+ Logic::Cartridge::Streaming.enabled?(cartridge, :repl)
+
Pry.commands.block_command(/(.*)/, 'handler') do |line|
session.print(prefix) unless prefix.nil?
session.evaluate_and_print(line, mode: 'repl')
diff --git a/controllers/interfaces/tools.rb b/controllers/interfaces/tools.rb
new file mode 100644
index 0000000..5105da1
--- /dev/null
+++ b/controllers/interfaces/tools.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'rainbow'
+
+require_relative '../../logic/cartridge/tools'
+require_relative '../../components/embedding'
+
+module NanoBot
+ module Controllers
+ module Interfaces
+ module Tool
+ def self.adapt(feedback, adapter)
+ call = {
+ parameters: %w[id name parameters parameters-as-json output],
+ values: [
+ feedback[:id], feedback[:name], feedback[:parameters],
+ feedback[:parameters].to_json,
+ feedback[:output]
+ ],
+ safety: false
+ }
+
+ raise StandardError, 'conflicting adapters' if %i[fennel lua clojure].count { |key| !adapter[key].nil? } > 1
+
+ if adapter[:fennel]
+ call[:source] = adapter[:fennel]
+ Components::Embedding.fennel(**call)
+ elsif adapter[:clojure]
+ call[:source] = adapter[:clojure]
+ Components::Embedding.clojure(**call)
+ elsif adapter[:lua]
+ call[:parameters] = %w[id name parameters parameters_as_json output]
+ call[:source] = adapter[:lua]
+ Components::Embedding.lua(**call)
+ else
+ raise 'missing handler for adapter'
+ end
+ end
+
+ def self.dispatch_feedback(session, cartridge, mode, feedback)
+ enabled = Logic::Cartridge::Tools.feedback?(cartridge, mode.to_sym, feedback[:action].to_sym)
+
+ return unless enabled
+
+ color = Logic::Cartridge::Tools.fetch_from_interface(
+ cartridge, mode.to_sym, feedback[:action].to_sym, [:color]
+ )
+
+ adapter = Tool.adapter(cartridge, mode, feedback)
+
+ if %i[fennel lua clojure].any? { |key| !adapter[key].nil? }
+ message = adapt(feedback, adapter)
+ else
+ message = "(#{feedback[:name]} #{feedback[:parameters].to_json})"
+
+ message += " =>\n#{feedback[:output]}" if feedback[:action].to_sym == :response
+ end
+
+ message = "#{adapter[:prefix]}#{message}#{adapter[:suffix]}"
+
+ session.print(color.nil? ? message : Rainbow(message).send(color))
+ end
+
+ def self.adapter(cartridge, mode, feedback)
+ prefix = Logic::Cartridge::Tools.fetch_from_interface(
+ cartridge, mode.to_sym, feedback[:action].to_sym, [:prefix]
+ )
+
+ suffix = Logic::Cartridge::Tools.fetch_from_interface(
+ cartridge, mode.to_sym, feedback[:action].to_sym, [:suffix]
+ )
+
+ fennel = Logic::Cartridge::Tools.fetch_from_interface(
+ cartridge, mode.to_sym, feedback[:action].to_sym, %i[adapter fennel]
+ )
+
+ lua = Logic::Cartridge::Tools.fetch_from_interface(
+ cartridge, mode.to_sym, feedback[:action].to_sym, %i[adapter lua]
+ )
+
+ clojure = Logic::Cartridge::Tools.fetch_from_interface(
+ cartridge, mode.to_sym, feedback[:action].to_sym, %i[adapter clojure]
+ )
+
+ { prefix:, suffix:, fennel:, lua:, clojure: }
+ end
+ end
+ end
+ end
+end
diff --git a/controllers/session.rb b/controllers/session.rb
index 4694911..d20deb2 100644
--- a/controllers/session.rb
+++ b/controllers/session.rb
@@ -3,10 +3,13 @@
require 'babosa'
require 'fileutils'
+require 'rainbow'
require_relative '../logic/helpers/hash'
require_relative '../logic/cartridge/streaming'
require_relative '../logic/cartridge/interaction'
+require_relative '../logic/cartridge/fetch'
+require_relative 'interfaces/tools'
require_relative '../components/storage'
require_relative '../components/adapter'
require_relative '../components/crypto'
@@ -41,9 +44,9 @@ module NanoBot
end
def load_state
- @state = Logic::Helpers::Hash.symbolize_keys(JSON.parse(
- Components::Crypto.decrypt(File.read(@state_path))
- ))
+ @state = Logic::Helpers::Hash.symbolize_keys(
+ JSON.parse(Components::Crypto.decrypt(File.read(@state_path)))
+ )
end
def store_state!
@@ -78,42 +81,78 @@ module NanoBot
end
def process(input, mode:)
+ interface = Logic::Helpers::Hash.fetch(@cartridge, [:interfaces, mode.to_sym]) || {}
+
+ input[:interface] = interface
+ input[:tools] = @cartridge[:tools]
+
+ needs_another_round = true
+
+ # TODO: Improve infinite loop prevention.
+ needs_another_round = process_interaction(input, mode:) while needs_another_round
+ end
+
+ def process_interaction(input, mode:)
prefix = Logic::Cartridge::Affixes.get(@cartridge, mode.to_sym, :output, :prefix)
suffix = Logic::Cartridge::Affixes.get(@cartridge, mode.to_sym, :output, :suffix)
- interface = Logic::Helpers::Hash.fetch(@cartridge, [:interfaces, mode.to_sym]) || {}
+ color = Logic::Cartridge::Fetch.cascate(@cartridge, [
+ [:interfaces, mode.to_sym, :output, :color],
+ %i[interfaces output color]
+ ])
- streaming = Logic::Cartridge::Streaming.enabled?(@cartridge, mode.to_sym)
+ color = color.to_sym if color
- input[:interface] = interface
+ streaming = Logic::Cartridge::Streaming.enabled?(@cartridge, mode.to_sym)
updated_at = Time.now
ready = false
- @provider.evaluate(input) do |output, finished|
- updated_at = Time.now
-
- if finished
- event = Marshal.load(Marshal.dump(output))
- output = Logic::Cartridge::Interaction.output(
- @cartridge, mode.to_sym, output, streaming, finished
- )
+ needs_another_round = false
- output[:message] = Components::Adapter.apply(:output, output[:message])
-
- event[:mode] = mode.to_s
- event[:output] = "#{prefix}#{output[:message]}#{suffix}"
+ @provider.evaluate(input) do |feedback|
+ updated_at = Time.now
- @state[:history] << event
+ needs_another_round = true if feedback[:needs_another_round]
- self.print(output[:message]) unless streaming
+ if feedback[:interaction] && feedback.dig(:interaction, :meta, :tool, :action)
+ Interfaces::Tool.dispatch_feedback(self, @cartridge, mode, feedback[:interaction][:meta][:tool])
+ end
- ready = true
- flush
- elsif streaming
- self.print(output[:message])
+ if feedback[:interaction]
+ event = Marshal.load(Marshal.dump(feedback[:interaction]))
+ event[:mode] = mode.to_s
+ event[:output] = nil
+
+ if feedback[:interaction][:who] == 'AI' && feedback[:interaction][:message]
+ event[:output] = feedback[:interaction][:message]
+ unless streaming
+ output = Logic::Cartridge::Interaction.output(
+ @cartridge, mode.to_sym, feedback[:interaction], streaming, feedback[:finished]
+ )
+ output[:message] = Components::Adapter.apply(:output, output[:message])
+ event[:output] = (output[:message]).to_s
+ end
+ end
+
+ @state[:history] << event if feedback[:should_be_stored]
+ if event[:output] && ((!feedback[:finished] && streaming) || (!streaming && feedback[:finished]))
+ # TODO: Color?
+ if color
+ self.print(Rainbow(event[:output]).send(color))
+ else
+ self.print(event[:output])
+ end
+
+ flush if feedback[:finished]
+ end
+
+ # `.print` already adds a prefix and suffix, so we add them after printing to avoid duplications.
+ event[:output] = "#{prefix}#{event[:output]}#{suffix}"
end
+
+ ready = true if feedback[:finished]
end
until ready
@@ -122,6 +161,8 @@ module NanoBot
end
store_state! unless @stateless
+
+ needs_another_round
end
def flush
diff --git a/logic/cartridge/adapters.rb b/logic/cartridge/adapters.rb
index bc8ddaf..4a3fadf 100644
--- a/logic/cartridge/adapters.rb
+++ b/logic/cartridge/adapters.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require_relative '../helpers/hash'
-require_relative './default'
+require_relative 'default'
module NanoBot
module Logic
diff --git a/logic/cartridge/affixes.rb b/logic/cartridge/affixes.rb
index 0b40fa2..bf94d92 100644
--- a/logic/cartridge/affixes.rb
+++ b/logic/cartridge/affixes.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require_relative '../helpers/hash'
-require_relative './default'
+require_relative 'default'
module NanoBot
module Logic
diff --git a/logic/cartridge/default.rb b/logic/cartridge/default.rb
index 43d45d4..24a8e7c 100644
--- a/logic/cartridge/default.rb
+++ b/logic/cartridge/default.rb
@@ -15,7 +15,7 @@ module NanoBot
return @values if @values
path = File.expand_path('../../static/cartridges/default.yml', __dir__)
- cartridge = YAML.safe_load(File.read(path), permitted_classes: [Symbol])
+ cartridge = YAML.safe_load_file(path, permitted_classes: [Symbol])
@values = Logic::Helpers::Hash.symbolize_keys(cartridge)
@values
end
@@ -24,7 +24,7 @@ module NanoBot
return @baseline if @baseline
path = File.expand_path('../../static/cartridges/baseline.yml', __dir__)
- cartridge = YAML.safe_load(File.read(path), permitted_classes: [Symbol])
+ cartridge = YAML.safe_load_file(path, permitted_classes: [Symbol])
@baseline = Logic::Helpers::Hash.symbolize_keys(cartridge)
@baseline
end
diff --git a/logic/cartridge/fetch.rb b/logic/cartridge/fetch.rb
new file mode 100644
index 0000000..2335358
--- /dev/null
+++ b/logic/cartridge/fetch.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require_relative 'default'
+require_relative '../helpers/hash'
+
+module NanoBot
+ module Logic
+ module Cartridge
+ module Fetch
+ def self.cascate(cartridge, paths)
+ results = paths.map { |path| Helpers::Hash.fetch(cartridge, path) }
+ result = results.find { |candidate| !candidate.nil? }
+ return result unless result.nil?
+
+ results = paths.map { |path| Helpers::Hash.fetch(Default.instance.values, path) }
+ result = results.find { |candidate| !candidate.nil? }
+ return result unless result.nil?
+
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/logic/cartridge/interaction.rb b/logic/cartridge/interaction.rb
index 160d818..5e8264e 100644
--- a/logic/cartridge/interaction.rb
+++ b/logic/cartridge/interaction.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
-require 'sweet-moon'
-
-require_relative './affixes'
-require_relative './adapters'
+require_relative 'affixes'
+require_relative 'adapters'
module NanoBot
module Logic
@@ -12,23 +10,25 @@ module NanoBot
def self.input(cartridge, interface, content)
lua = Adapter.expression(cartridge, interface, :input, :lua)
fennel = Adapter.expression(cartridge, interface, :input, :fennel)
+ clojure = Adapter.expression(cartridge, interface, :input, :clojure)
prefix = Affixes.get(cartridge, interface, :input, :prefix)
suffix = Affixes.get(cartridge, interface, :input, :suffix)
- { content:, prefix:, suffix:, lua:, fennel: }
+ { content:, prefix:, suffix:, lua:, fennel:, clojure: }
end
def self.output(cartridge, interface, result, streaming, _finished)
if streaming
- result[:message] = { content: result[:message], lua: nil, fennel: nil }
+ result[:message] = { content: result[:message], lua: nil, fennel: nil, clojure: nil }
return result
end
lua = Adapter.expression(cartridge, interface, :output, :lua)
fennel = Adapter.expression(cartridge, interface, :output, :fennel)
+ clojure = Adapter.expression(cartridge, interface, :output, :clojure)
- result[:message] = { content: result[:message], lua:, fennel: }
+ result[:message] = { content: result[:message], lua:, fennel:, clojure: }
result
end
diff --git a/logic/cartridge/tools.rb b/logic/cartridge/tools.rb
new file mode 100644
index 0000000..6a45bf0
--- /dev/null
+++ b/logic/cartridge/tools.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require_relative 'fetch'
+require_relative 'affixes'
+require_relative 'adapters'
+
+module NanoBot
+ module Logic
+ module Cartridge
+ module Tools
+ def self.fetch_from_interface(cartridge, interface, action, path)
+ Fetch.cascate(cartridge, [
+ [:interfaces, interface, :tools, action].concat(path),
+ [:interfaces, :tools, action].concat(path),
+ %i[interfaces tools].concat(path)
+ ])
+ end
+
+ def self.feedback?(cartridge, interface, action)
+ Fetch.cascate(cartridge, [
+ [:interfaces, interface, :tools, action, :feedback],
+ [:interfaces, :tools, action, :feedback],
+ %i[interfaces tools feedback]
+ ])
+ end
+
+ def self.input(cartridge, interface, content)
+ lua = Adapter.expression(cartridge, interface, :input, :lua)
+ fennel = Adapter.expression(cartridge, interface, :input, :fennel)
+
+ prefix = Affixes.get(cartridge, interface, :input, :prefix)
+ suffix = Affixes.get(cartridge, interface, :input, :suffix)
+
+ { content:, prefix:, suffix:, lua:, fennel: }
+ end
+
+ def self.output(cartridge, interface, result, streaming, _finished)
+ if streaming
+ result[:message] = { content: result[:message], lua: nil, fennel: nil }
+ return result
+ end
+
+ lua = Adapter.expression(cartridge, interface, :output, :lua)
+ fennel = Adapter.expression(cartridge, interface, :output, :fennel)
+
+ result[:message] = { content: result[:message], lua:, fennel: }
+
+ result
+ end
+ end
+ end
+ end
+end
diff --git a/logic/providers/openai.rb b/logic/providers/openai.rb
new file mode 100644
index 0000000..00e7a43
--- /dev/null
+++ b/logic/providers/openai.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'json'
+
+module NanoBot
+ module Logic
+ module OpenAI
+ def self.prepare_tools(cartridge, tools)
+ applies = []
+ tools.each do |tool|
+ cartridge.each do |candidate|
+ next unless candidate[:type] == 'function' &&
+ tool[:type] == candidate[:type] &&
+ tool[:function][:name] == candidate[:name]
+
+ source = {}
+
+ source[:fennel] = candidate[:fennel] if candidate[:fennel]
+ source[:lua] = candidate[:lua] if candidate[:lua]
+
+ applies << {
+ name: tool[:function][:name],
+ type: candidate[:type],
+ parameters: JSON.parse(tool[:function][:arguments]),
+ source:
+ }
+ end
+ end
+
+ applies
+ end
+
+ def self.adapt_tool(cartridge)
+ raise 'unsupported tool' if cartridge[:type] != 'function'
+
+ adapted = {
+ type: 'function',
+ function: {
+ name: cartridge[:name], description: cartridge[:description],
+ parameters: { type: 'object', properties: {} }
+ }
+ }
+
+ properties = adapted[:function][:parameters][:properties]
+
+ cartridge[:parameters].each do |parameter|
+ key = parameter[:name].to_sym
+ properties[key] = {}
+ properties[key][:type] = parameter[:type] || 'string'
+ properties[key][:description] = parameter[:description] if parameter[:description]
+ end
+
+ adapted
+ end
+ end
+ end
+end
diff --git a/logic/providers/openai/tools.rb b/logic/providers/openai/tools.rb
new file mode 100644
index 0000000..080d81e
--- /dev/null
+++ b/logic/providers/openai/tools.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'json'
+
+require_relative '../../helpers/hash'
+
+module NanoBot
+ module Logic
+ module OpenAI
+ module Tools
+ def self.prepare(cartridge, tools)
+ applies = []
+ tools.each do |tool|
+ # TODO: Does this mutate the hash?
+ tool = Helpers::Hash.symbolize_keys(tool)
+
+ cartridge.each do |candidate|
+ next unless (
+ candidate[:type].nil? ||
+ (candidate[:type] == 'function' && tool[:type] == candidate[:type])
+ ) && tool[:function][:name] == candidate[:name]
+
+ source = {}
+
+ source[:clojure] = candidate[:clojure] if candidate[:clojure]
+ source[:fennel] = candidate[:fennel] if candidate[:fennel]
+ source[:lua] = candidate[:lua] if candidate[:lua]
+
+ applies << {
+ id: tool[:id],
+ name: tool[:function][:name],
+ type: candidate[:type] || 'function',
+ parameters: JSON.parse(tool[:function][:arguments]),
+ source:
+ }
+ end
+ end
+
+ applies
+ end
+
+ def self.adapt(cartridge)
+ raise 'unsupported tool' if cartridge[:type] != 'function' && !cartridge[:type].nil?
+
+ adapted = {
+ type: cartridge[:type] || 'function',
+ function: {
+ name: cartridge[:name], description: cartridge[:description],
+ parameters: { type: 'object', properties: {} }
+ }
+ }
+
+ properties = adapted[:function][:parameters][:properties]
+
+ adapted[:function][:parameters][:required] = cartridge[:required] if cartridge[:required]
+
+ cartridge[:parameters]&.each do |parameter|
+ key = parameter[:name].to_sym
+ properties[key] = {}
+ properties[key][:type] = parameter[:type] || 'string'
+ properties[key][:description] = parameter[:description] if parameter[:description]
+ properties[key][:items] = parameter[:items].slice(:type) if parameter[:items]
+ end
+
+ adapted
+ end
+ end
+ end
+ end
+end
diff --git a/spec/data/cartridges/tools.yml b/spec/data/cartridges/tools.yml
new file mode 100644
index 0000000..0c2a30b
--- /dev/null
+++ b/spec/data/cartridges/tools.yml
@@ -0,0 +1,28 @@
+---
+tools:
+ - name: get-current-weather
+ type: function
+ description: Get the current weather in a given location.
+ parameters:
+ - name: location
+ - name: unit
+ fennel: |
+ (let [{:location location :unit unit} parameters]
+ (.. "Here is the weather in " location ", in " unit ": 35.8°C."))
+
+ - name: what-time-is-it
+ description: Returns the current date and time.
+ fennel: |
+ (os.date)
+
+ - name: sh
+ description: It has access to computer users' data and can be used to run shell commands, similar to those in a Linux terminal, to extract information. Please be mindful and careful to avoid running dangerous commands on users' computers.
+ parameters:
+ - name: command
+ type: array
+ items:
+ type: string
+ description: An array of strings that represents a shell command along with its arguments or options. For instance, `["df", "-h"]` executes the `df -h` command, where each array element specifies either the command itself or an associated argument/option.
+ clojure: |
+ (require '[clojure.java.shell :refer [sh]])
+ (println (apply sh (get parameters "command")))
diff --git a/spec/data/providers/openai/tools.yml b/spec/data/providers/openai/tools.yml
new file mode 100644
index 0000000..681fb2c
--- /dev/null
+++ b/spec/data/providers/openai/tools.yml
@@ -0,0 +1,11 @@
+---
+- id: call_XYZ
+ type: function
+ function:
+ name: get-current-weather
+ arguments: '{"location":"Tokyo, Japan"}'
+- id: call_ZYX
+ type: function
+ function:
+ name: what-time-is-it
+ arguments: "{}"
diff --git a/spec/logic/cartridge/affixes_spec.rb b/spec/logic/cartridge/affixes_spec.rb
index 8f08e1d..7263008 100644
--- a/spec/logic/cartridge/affixes_spec.rb
+++ b/spec/logic/cartridge/affixes_spec.rb
@@ -6,7 +6,7 @@ require_relative '../../../logic/cartridge/affixes'
RSpec.describe NanoBot::Logic::Cartridge::Affixes do
context 'interfaces' do
- let(:cartridge) { load_cartridge('affixes.yml') }
+ let(:cartridge) { load_symbolized('cartridges/affixes.yml') }
it 'gets the expected affixes' do
expect(described_class.get(cartridge, :repl, :input, :prefix)).to eq('E')
@@ -22,7 +22,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Affixes do
end
context 'interfaces fallback' do
- let(:cartridge) { load_cartridge('affixes.yml') }
+ let(:cartridge) { load_symbolized('cartridges/affixes.yml') }
it 'gets the expected affixes' do
cartridge[:interfaces][:repl][:input].delete(:prefix)
@@ -48,7 +48,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Affixes do
end
context 'interfaces nil' do
- let(:cartridge) { load_cartridge('affixes.yml') }
+ let(:cartridge) { load_symbolized('cartridges/affixes.yml') }
it 'gets the expected affixes' do
cartridge[:interfaces][:repl][:input][:prefix] = nil
diff --git a/spec/logic/cartridge/interaction_spec.rb b/spec/logic/cartridge/interaction_spec.rb
index 347ac45..f3ba46e 100644
--- a/spec/logic/cartridge/interaction_spec.rb
+++ b/spec/logic/cartridge/interaction_spec.rb
@@ -6,41 +6,41 @@ require_relative '../../../logic/cartridge/interaction'
RSpec.describe NanoBot::Logic::Cartridge::Interaction do
context 'input' do
- let(:cartridge) { load_cartridge('affixes.yml') }
+ let(:cartridge) { load_symbolized('cartridges/affixes.yml') }
it 'prepares the input' do
expect(described_class.input(cartridge, :repl, 'hello')).to eq(
- { content: 'hello', fennel: nil, lua: nil, prefix: 'E', suffix: 'F' }
+ { content: 'hello', fennel: nil, lua: nil, clojure: nil, prefix: 'E', suffix: 'F' }
)
expect(described_class.input({}, :repl, 'hello')).to eq(
- { content: 'hello', fennel: nil, lua: nil, prefix: nil, suffix: nil }
+ { content: 'hello', fennel: nil, lua: nil, clojure: nil, prefix: nil, suffix: nil }
)
expect(described_class.input(cartridge, :eval, 'hello')).to eq(
- { content: 'hello', fennel: nil, lua: nil, prefix: 'I', suffix: 'J' }
+ { content: 'hello', fennel: nil, lua: nil, clojure: nil, prefix: 'I', suffix: 'J' }
)
expect(described_class.input({}, :eval, 'hello')).to eq(
- { content: 'hello', fennel: nil, lua: nil, prefix: nil, suffix: nil }
+ { content: 'hello', fennel: nil, lua: nil, clojure: nil, prefix: nil, suffix: nil }
)
end
it 'prepares the non-streamming output' do
expect(described_class.output(cartridge, :repl, { message: 'hello' }, false, true)).to eq(
- { message: { content: 'hello', fennel: nil, lua: nil } }
+ { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } }
)
expect(described_class.output({}, :repl, { message: 'hello' }, false, true)).to eq(
- { message: { content: 'hello', fennel: nil, lua: nil } }
+ { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } }
)
expect(described_class.output(cartridge, :eval, { message: 'hello' }, false, true)).to eq(
- { message: { content: 'hello', fennel: nil, lua: nil } }
+ { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } }
)
expect(described_class.output({}, :eval, { message: 'hello' }, false, true)).to eq(
- { message: { content: 'hello', fennel: nil, lua: nil } }
+ { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } }
)
end
end
diff --git a/spec/logic/cartridge/streaming_spec.rb b/spec/logic/cartridge/streaming_spec.rb
index e5ad012..466dd0b 100644
--- a/spec/logic/cartridge/streaming_spec.rb
+++ b/spec/logic/cartridge/streaming_spec.rb
@@ -5,8 +5,28 @@ require 'yaml'
require_relative '../../../logic/cartridge/streaming'
RSpec.describe NanoBot::Logic::Cartridge::Streaming do
+ context 'interfaces override' do
+ context 'defaults' do
+ let(:cartridge) { {} }
+
+ it 'uses default values when appropriate' do
+ expect(described_class.enabled?(cartridge, :repl)).to be(true)
+ expect(described_class.enabled?(cartridge, :eval)).to be(true)
+ end
+ end
+
+ context 'top-level overrides' do
+ let(:cartridge) { { interfaces: { output: { stream: false } } } }
+
+ it 'overrides default values when appropriate' do
+ expect(described_class.enabled?(cartridge, :repl)).to be(false)
+ expect(described_class.enabled?(cartridge, :eval)).to be(false)
+ end
+ end
+ end
+
context 'provider' do
- let(:cartridge) { load_cartridge('streaming.yml') }
+ let(:cartridge) { load_symbolized('cartridges/streaming.yml') }
it 'checks if stream is enabled' do
cartridge[:provider][:settings][:stream] = false
@@ -15,7 +35,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Streaming do
end
context 'repl' do
- let(:cartridge) { load_cartridge('streaming.yml') }
+ let(:cartridge) { load_symbolized('cartridges/streaming.yml') }
it 'checks if stream is enabled' do
cartridge[:interfaces][:repl][:output][:stream] = false
@@ -24,7 +44,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Streaming do
end
context 'interface + repl' do
- let(:cartridge) { load_cartridge('streaming.yml') }
+ let(:cartridge) { load_symbolized('cartridges/streaming.yml') }
it 'checks if stream is enabled' do
cartridge[:interfaces][:output][:stream] = false
@@ -34,7 +54,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Streaming do
end
context 'interface' do
- let(:cartridge) { load_cartridge('streaming.yml') }
+ let(:cartridge) { load_symbolized('cartridges/streaming.yml') }
it 'checks if stream is enabled' do
cartridge[:interfaces][:output][:stream] = false
@@ -44,7 +64,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Streaming do
end
context '- repl' do
- let(:cartridge) { load_cartridge('streaming.yml') }
+ let(:cartridge) { load_symbolized('cartridges/streaming.yml') }
it 'checks if stream is enabled' do
cartridge[:interfaces][:repl][:output].delete(:stream)
@@ -53,7 +73,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Streaming do
end
context '- interface' do
- let(:cartridge) { load_cartridge('streaming.yml') }
+ let(:cartridge) { load_symbolized('cartridges/streaming.yml') }
it 'checks if stream is enabled' do
cartridge[:interfaces][:output].delete(:stream)
@@ -63,7 +83,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Streaming do
end
context '- provider' do
- let(:cartridge) { load_cartridge('streaming.yml') }
+ let(:cartridge) { load_symbolized('cartridges/streaming.yml') }
it 'checks if stream is enabled' do
cartridge[:provider][:settings].delete(:stream)
diff --git a/spec/logic/cartridge/tools_spec.rb b/spec/logic/cartridge/tools_spec.rb
new file mode 100644
index 0000000..42b8c57
--- /dev/null
+++ b/spec/logic/cartridge/tools_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'yaml'
+
+require_relative '../../../logic/cartridge/tools'
+
+RSpec.describe NanoBot::Logic::Cartridge::Tools do
+ context 'interfaces override' do
+ context 'defaults' do
+ let(:cartridge) { {} }
+
+ it 'uses default values when appropriate' do
+ expect(described_class.feedback?(cartridge, :repl, :call)).to be(true)
+ expect(described_class.feedback?(cartridge, :eval, :call)).to be(true)
+
+ expect(described_class.feedback?(cartridge, :repl, :response)).to be(false)
+ expect(described_class.feedback?(cartridge, :eval, :response)).to be(false)
+ end
+ end
+
+ context 'top-level overrides' do
+ let(:cartridge) do
+ { interfaces: { tools: { feedback: false } } }
+ end
+
+ it 'overrides default values when appropriate' do
+ expect(described_class.feedback?(cartridge, :repl, :call)).to be(false)
+ expect(described_class.feedback?(cartridge, :eval, :call)).to be(false)
+
+ expect(described_class.feedback?(cartridge, :repl, :response)).to be(false)
+ expect(described_class.feedback?(cartridge, :eval, :response)).to be(false)
+ end
+ end
+
+ context 'top-level overrides' do
+ let(:cartridge) do
+ { interfaces: { tools: { feedback: true } } }
+ end
+
+ it 'overrides default values when appropriate' do
+ expect(described_class.feedback?(cartridge, :repl, :call)).to be(true)
+ expect(described_class.feedback?(cartridge, :eval, :call)).to be(true)
+
+ expect(described_class.feedback?(cartridge, :repl, :response)).to be(true)
+ expect(described_class.feedback?(cartridge, :eval, :response)).to be(true)
+ end
+ end
+
+ context 'top-level-specific overrides' do
+ let(:cartridge) do
+ { interfaces: { tools: { call: { feedback: false }, response: { feedback: true } } } }
+ end
+
+ it 'overrides default values when appropriate' do
+ expect(described_class.feedback?(cartridge, :repl, :call)).to be(false)
+ expect(described_class.feedback?(cartridge, :eval, :call)).to be(false)
+
+ expect(described_class.feedback?(cartridge, :repl, :response)).to be(true)
+ expect(described_class.feedback?(cartridge, :eval, :response)).to be(true)
+ end
+ end
+
+ context 'repl interface overrides' do
+ let(:cartridge) do
+ { interfaces: {
+ tools: { call: { feedback: false }, response: { feedback: true } },
+ repl: { tools: { call: { feedback: true }, response: { feedback: false } } }
+ } }
+ end
+
+ it 'overrides default values when appropriate' do
+ expect(described_class.feedback?(cartridge, :repl, :call)).to be(true)
+ expect(described_class.feedback?(cartridge, :eval, :call)).to be(false)
+
+ expect(described_class.feedback?(cartridge, :repl, :response)).to be(false)
+ expect(described_class.feedback?(cartridge, :eval, :response)).to be(true)
+ end
+ end
+
+ context 'eval interface overrides' do
+ let(:cartridge) do
+ { interfaces: {
+ tools: { call: { feedback: false }, response: { feedback: true } },
+ eval: { tools: { call: { feedback: true }, response: { feedback: false } } }
+ } }
+ end
+
+ it 'overrides default values when appropriate' do
+ expect(described_class.feedback?(cartridge, :repl, :call)).to be(false)
+ expect(described_class.feedback?(cartridge, :eval, :call)).to be(true)
+
+ expect(described_class.feedback?(cartridge, :repl, :response)).to be(true)
+ expect(described_class.feedback?(cartridge, :eval, :response)).to be(false)
+ end
+ end
+ end
+end
diff --git a/spec/logic/providers/openai/tools_spec.rb b/spec/logic/providers/openai/tools_spec.rb
new file mode 100644
index 0000000..c92c374
--- /dev/null
+++ b/spec/logic/providers/openai/tools_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'yaml'
+
+require_relative '../../../../logic/providers/openai/tools'
+
+RSpec.describe NanoBot::Logic::OpenAI::Tools do
+ context 'tools' do
+ let(:cartridge) { load_symbolized('cartridges/tools.yml') }
+
+ context 'adapt' do
+ it 'adapts to OpenAI expected format' do
+ expect(described_class.adapt(cartridge[:tools][0])).to eq(
+ { type: 'function',
+ function: {
+ name: 'get-current-weather',
+ description: 'Get the current weather in a given location.',
+ parameters: {
+ type: 'object',
+ properties: {
+ location: { type: 'string' },
+ unit: { type: 'string' }
+ }
+ }
+ } }
+ )
+
+ expect(described_class.adapt(cartridge[:tools][1])).to eq(
+ { type: 'function',
+ function: {
+ name: 'what-time-is-it',
+ description: 'Returns the current date and time.',
+ parameters: { properties: {}, type: 'object' }
+ } }
+ )
+
+ expect(described_class.adapt(cartridge[:tools][2])).to eq(
+ { type: 'function',
+ function: {
+ name: 'sh',
+ description: "It has access to computer users' data and can be used to run shell commands, similar to those in a Linux terminal, to extract information. Please be mindful and careful to avoid running dangerous commands on users' computers.",
+ parameters: {
+ type: 'object',
+ properties: {
+ command: {
+ type: 'array',
+ description: 'An array of strings that represents a shell command along with its arguments or options. For instance, `["df", "-h"]` executes the `df -h` command, where each array element specifies either the command itself or an associated argument/option.',
+ items: { type: 'string' }
+ }
+ }
+ }
+ } }
+ )
+ end
+ end
+
+ context 'prepare' do
+ let(:tools) { load_symbolized('providers/openai/tools.yml') }
+
+ it 'prepare tools to be executed' do
+ expect(described_class.prepare(cartridge[:tools], tools)).to eq(
+ [{ id: 'call_XYZ',
+ name: 'get-current-weather',
+ type: 'function',
+ parameters: { 'location' => 'Tokyo, Japan' },
+ source: { fennel: "(let [{:location location :unit unit} parameters]\n (.. \"Here is the weather in \" location \", in \" unit \": 35.8°C.\"))\n" } },
+ { id: 'call_ZYX', name: 'what-time-is-it', type: 'function', parameters: {},
+ source: { fennel: "(os.date)\n" } }]
+ )
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index ad9038d..cb46554 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -16,8 +16,8 @@ RSpec.configure do |config|
config.shared_context_metadata_behavior = :apply_to_host_groups
end
-def load_cartridge(path)
- cartridge = YAML.safe_load(File.read("spec/data/cartridges/#{path}"), permitted_classes: [Symbol])
+def load_symbolized(path)
+ cartridge = YAML.safe_load_file("spec/data/#{path}", permitted_classes: [Symbol])
NanoBot::Logic::Helpers::Hash.symbolize_keys(cartridge)
end
diff --git a/static/cartridges/default.yml b/static/cartridges/default.yml
index 2cadc8c..167fa83 100644
--- a/static/cartridges/default.yml
+++ b/static/cartridges/default.yml
@@ -12,6 +12,11 @@ interfaces:
output:
stream: true
suffix: "\n"
+ tools:
+ call:
+ feedback: true
+ response:
+ feedback: false
provider:
settings: