diff options
author | icebaker <113217272+icebaker@users.noreply.github.com> | 2023-11-29 07:53:19 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-29 07:53:19 -0300 |
commit | 9f79a161905f5af8e331930cc77c7be10703596f (patch) | |
tree | b2ab75665e42de79f22cca82bf03cfc49759f485 /components/providers | |
parent | e1ab6853262b83f483060961f17bf895989a19c0 (diff) | |
parent | 154aa68caf50a18af5c0dff1d368fc639314e0ba (diff) |
Merge pull request #5 from icebaker/ib-tools
Adding support for Spec 1.0.0: Tools (Functions)
Diffstat (limited to 'components/providers')
-rw-r--r-- | components/providers/openai.rb | 123 | ||||
-rw-r--r-- | components/providers/openai/tools.rb | 101 |
2 files changed, 204 insertions, 20 deletions
diff --git a/components/providers/openai.rb b/components/providers/openai.rb index ce6fb33..87f2bc5 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 @@ -32,17 +37,16 @@ module NanoBot @client = ::OpenAI::Client.new(uri_base:, access_token: @credentials[:'access-token']) end - def stream(input) - provider = @settings.key?(:stream) ? @settings[:stream] : true - interface = input[:interface].key?(:stream) ? input[:interface][:stream] : true - - provider && interface - end - - def evaluate(input, &block) + def evaluate(input, streaming, cartridge, &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].to_s, + 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,26 +66,105 @@ module NanoBot payload.delete(:logit_bias) if payload.key?(:logit_bias) && payload[:logit_bias].nil? - if stream(input) + payload[:tools] = input[:tools].map { |raw| NanoBot::Logic::OpenAI::Tools.adapt(raw) } if input[:tools] + + if streaming 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 - block.call({ who: 'AI', message: content }, true) if chunk.dig('choices', 0, 'finish_reason') + if partial_content + content += partial_content + feedback.call( + { should_be_stored: false, + interaction: { who: 'AI', message: partial_content } } + ) + end + + 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(cartridge, 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) + begin + @client.chat(parameters: payload) + rescue StandardError => e + raise e.class, e.response[:body] if e.response && e.response[:body] + + raise e + end else - result = @client.chat(parameters: payload) + begin + result = @client.chat(parameters: payload) + rescue StandardError => e + raise e.class, e.response[:body] if e.response && e.response[:body] + + raise e + end 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(cartridge, 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..cd35e80 --- /dev/null +++ b/components/providers/openai/tools.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require_relative '../../embedding' +require_relative '../../../logic/cartridge/safety' + +require 'concurrent' + +module NanoBot + module Components + module Providers + class OpenAI < Base + module Tools + def self.confirming(tool, feedback) + feedback.call( + { should_be_stored: false, + interaction: { who: 'AI', message: nil, meta: { + tool: { action: 'confirming', id: tool[:id], name: tool[:name], parameters: tool[:parameters] } + } } } + ) + end + + def self.apply(cartridge, function_cartridge, tools, feedback) + prepared_tools = NanoBot::Logic::OpenAI::Tools.prepare(function_cartridge, tools) + + if Logic::Cartridge::Safety.confirmable?(cartridge) + prepared_tools.each { |tool| tool[:allowed] = confirming(tool, feedback) } + else + prepared_tools.each { |tool| tool[:allowed] = true } + end + + futures = prepared_tools.map do |tool| + Concurrent::Promises.future do + if tool[:allowed] + process!(tool, feedback, function_cartridge, cartridge) + else + tool[:output] = + "We asked the user you're chatting with for permission, but the user did not allow you to run this tool or function." + tool + end + end + 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, _function_cartridge, cartridge) + feedback.call( + { should_be_stored: false, + interaction: { who: 'AI', message: nil, meta: { + tool: { action: 'executing', id: tool[:id], name: tool[:name], parameters: tool[:parameters] } + } } } + ) + + call = { + parameters: %w[parameters], + values: [tool[:parameters]], + safety: { sandboxed: Logic::Cartridge::Safety.sandboxed?(cartridge) } + } + + 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: 'responding', id: tool[:id], name: tool[:name], + parameters: tool[:parameters], output: tool[:output] + } + } } } + ) + + tool + end + end + end + end + end +end |