summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-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
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