diff options
author | icebaker <icebaker@proton.me> | 2023-11-18 19:07:10 -0300 |
---|---|---|
committer | icebaker <icebaker@proton.me> | 2023-11-18 19:07:10 -0300 |
commit | 8ae78b954350755a47a13133668dba93bac15f37 (patch) | |
tree | 9cdc3bb770d778bd8d00675fdbc1f27a6e27e37c /components | |
parent | ab22d1bbe37093912cb7418b3c945153a15f4255 (diff) |
adding support for tools
Diffstat (limited to 'components')
-rw-r--r-- | components/adapter.rb | 40 | ||||
-rw-r--r-- | components/embedding.rb | 75 | ||||
-rw-r--r-- | components/provider.rb | 2 | ||||
-rw-r--r-- | components/providers/openai.rb | 100 | ||||
-rw-r--r-- | components/providers/openai/tools.rb | 73 | ||||
-rw-r--r-- | components/storage.rb | 2 | ||||
-rw-r--r-- | components/stream.rb | 7 |
7 files changed, 257 insertions, 42 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 |