components/providers/openai.rb in nano-bots-0.1.1 vs components/providers/openai.rb in nano-bots-1.0.0

- old
+ new

@@ -1,12 +1,17 @@ # frozen_string_literal: true 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 class OpenAI < Base DEFAULT_ADDRESS = 'https://api.openai.com' @@ -30,21 +35,20 @@ end @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| next unless input[:behavior][key] @@ -60,29 +64,108 @@ payload[key] = @settings[key] if @settings.key?(key) end 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 def self.end_user(settings, environment) user = ENV.fetch('NANO_BOTS_END_USER', nil)