From e6f0374cc8844d4a053db4e68feee23ffc793d73 Mon Sep 17 00:00:00 2001 From: icebaker Date: Thu, 14 Dec 2023 22:49:06 -0300 Subject: adding support for google gemini --- components/providers/google.rb | 188 +++++++++++++++++++++++++++++++++++ components/providers/openai.rb | 35 ++++--- components/providers/openai/tools.rb | 101 ------------------- components/providers/tools.rb | 99 ++++++++++++++++++ 4 files changed, 310 insertions(+), 113 deletions(-) create mode 100644 components/providers/google.rb delete mode 100644 components/providers/openai/tools.rb create mode 100644 components/providers/tools.rb (limited to 'components/providers') diff --git a/components/providers/google.rb b/components/providers/google.rb new file mode 100644 index 0000000..75b7658 --- /dev/null +++ b/components/providers/google.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true + +require 'gemini-ai' + +require_relative 'base' + +require_relative '../../logic/providers/google/tools' +require_relative '../../logic/providers/google/tokens' + +require_relative 'tools' + +module NanoBot + module Components + module Providers + class Google < Base + SETTINGS = { + safetySettings: %i[category threshold].freeze, + generationConfig: %i[temperature topP topK candidateCount maxOutputTokens stopSequences].freeze + }.freeze + + attr_reader :settings + + def initialize(model, settings, credentials, _environment) + @settings = settings + + @client = Gemini.new( + credentials: { + file_path: credentials[:'file-path'], + project_id: credentials[:'project-id'], + region: credentials[:region] + }, + settings: { model: } + ) + end + + def evaluate(input, streaming, cartridge, &feedback) + messages = input[:history].map do |event| + if event[:message].nil? && event[:meta] && event[:meta][:tool_calls] + { role: 'model', + parts: event[:meta][:tool_calls], + _meta: { at: event[:at] } } + elsif event[:who] == 'tool' + { role: 'function', + parts: [ + { functionResponse: { + name: event[:meta][:name], + response: { name: event[:meta][:name], content: event[:message].to_s } + } } + ], + _meta: { at: event[:at] } } + else + { role: event[:who] == 'user' ? 'user' : 'model', + parts: { text: event[:message] }, + _meta: { at: event[:at] } } + end + end + + %i[backdrop directive].each do |key| + next unless input[:behavior][key] + + # TODO: Does Gemini have system messages? + messages.prepend( + { role: key == :directive ? 'user' : 'user', + parts: { text: input[:behavior][key] }, + _meta: { at: Time.now } } + ) + end + + payload = { contents: messages, generationConfig: { candidateCount: 1 } } + + if @settings + SETTINGS.each_key do |key| + SETTINGS[key].each do |sub_key| + if @settings.key?(key) && @settings[key].key?(sub_key) + payload[key] = {} unless payload.key?(key) + payload[key][sub_key] = @settings[key][sub_key] + end + end + end + end + + if input[:tools] + payload[:tools] = { + function_declarations: input[:tools].map { |raw| Logic::Google::Tools.adapt(raw) } + } + end + + if streaming + content = '' + tools = [] + + stream_call_back = proc do |event, _parsed, _raw| + partial_content = event.dig('candidates', 0, 'content', 'parts').filter do |part| + part.key?('text') + end.map { |part| part['text'] }.join + + partial_tools = event.dig('candidates', 0, 'content', 'parts').filter do |part| + part.key?('functionCall') + end + + tools.concat(partial_tools) if partial_tools.size.positive? + + if partial_content + content += partial_content + feedback.call( + { should_be_stored: false, + interaction: { who: 'AI', message: partial_content } } + ) + end + + if event.dig('candidates', 0, 'finishReason') + 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, Logic::Google::Tools + ).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 + + begin + @client.stream_generate_content( + Logic::Google::Tokens.apply_policies!(cartridge, payload), + stream: true, &stream_call_back + ) + rescue StandardError => e + raise e.class, e.response[:body] if e.response && e.response[:body] + + raise e + end + else + begin + result = @client.stream_generate_content( + Logic::Google::Tokens.apply_policies!(cartridge, payload) + ) + rescue StandardError => e + raise e.class, e.response[:body] if e.response && e.response[:body] + + raise e + end + + tools = result.dig(0, 'candidates', 0, 'content', 'parts').filter do |part| + part.key?('functionCall') + end + + 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, Logic::Google::Tools + ).each do |interaction| + feedback.call({ should_be_stored: true, needs_another_round: true, interaction: }) + end + end + + content = result.map do |answer| + answer.dig('candidates', 0, 'content', 'parts').filter do |part| + part.key?('text') + end.map { |part| part['text'] }.join + end.join + + feedback.call( + { should_be_stored: !(content.nil? || content.to_s.strip == ''), + interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, + finished: true } + ) + end + end + end + end + end +end diff --git a/components/providers/openai.rb b/components/providers/openai.rb index 6384181..b70984b 100644 --- a/components/providers/openai.rb +++ b/components/providers/openai.rb @@ -6,9 +6,9 @@ require_relative 'base' require_relative '../crypto' require_relative '../../logic/providers/openai/tools' -require_relative '../../controllers/interfaces/tools' +require_relative '../../logic/providers/openai/tokens' -require_relative 'openai/tools' +require_relative 'tools' module NanoBot module Components @@ -40,12 +40,18 @@ module NanoBot def evaluate(input, streaming, cartridge, &feedback) messages = input[:history].map do |event| if event[:message].nil? && event[:meta] && event[:meta][:tool_calls] - { role: 'assistant', content: nil, tool_calls: event[:meta][:tool_calls] } + { role: 'assistant', content: nil, + tool_calls: event[:meta][:tool_calls], + _meta: { at: event[:at] } } elsif event[:who] == 'tool' { role: event[:who], content: event[:message].to_s, - tool_call_id: event[:meta][:id], name: event[:meta][:name] } + tool_call_id: event[:meta][:id], + name: event[:meta][:name], + _meta: { at: event[:at] } } else - { role: event[:who] == 'user' ? 'user' : 'assistant', content: event[:message] } + { role: event[:who] == 'user' ? 'user' : 'assistant', + content: event[:message], + _meta: { at: event[:at] } } end end @@ -54,7 +60,8 @@ module NanoBot messages.prepend( { role: key == :directive ? 'system' : 'user', - content: input[:behavior][key] } + content: input[:behavior][key], + _meta: { at: Time.now } } ) end @@ -66,7 +73,7 @@ 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] + payload[:tools] = input[:tools].map { |raw| Logic::OpenAI::Tools.adapt(raw) } if input[:tools] if streaming content = '' @@ -114,13 +121,15 @@ module NanoBot needs_another_round: true, interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } } ) - Tools.apply(cartridge, input[:tools], tools, feedback).each do |interaction| + Tools.apply( + cartridge, input[:tools], tools, feedback, Logic::OpenAI::Tools + ).each do |interaction| feedback.call({ should_be_stored: true, needs_another_round: true, interaction: }) end end feedback.call( - { should_be_stored: !(content.nil? || content == ''), + { should_be_stored: !(content.nil? || content.to_s.strip == ''), interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, finished: true } ) @@ -128,7 +137,7 @@ module NanoBot end begin - @client.chat(parameters: payload) + @client.chat(parameters: Logic::OpenAI::Tokens.apply_policies!(cartridge, payload)) rescue StandardError => e raise e.class, e.response[:body] if e.response && e.response[:body] @@ -136,7 +145,7 @@ module NanoBot end else begin - result = @client.chat(parameters: payload) + result = @client.chat(parameters: Logic::OpenAI::Tokens.apply_policies!(cartridge, payload)) rescue StandardError => e raise e.class, e.response[:body] if e.response && e.response[:body] @@ -153,7 +162,9 @@ module NanoBot needs_another_round: true, interaction: { who: 'AI', message: nil, meta: { tool_calls: tools } } } ) - Tools.apply(cartridge, input[:tools], tools, feedback).each do |interaction| + Tools.apply( + cartridge, input[:tools], tools, feedback, Logic::OpenAI::Tools + ).each do |interaction| feedback.call({ should_be_stored: true, needs_another_round: true, interaction: }) end end diff --git a/components/providers/openai/tools.rb b/components/providers/openai/tools.rb deleted file mode 100644 index cd35e80..0000000 --- a/components/providers/openai/tools.rb +++ /dev/null @@ -1,101 +0,0 @@ -# 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 diff --git a/components/providers/tools.rb b/components/providers/tools.rb new file mode 100644 index 0000000..122bc14 --- /dev/null +++ b/components/providers/tools.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require_relative '../embedding' +require_relative '../../logic/cartridge/safety' + +require 'concurrent' + +module NanoBot + module Components + module Providers + 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[:label], parameters: tool[:parameters] } + } } } + ) + end + + def self.apply(cartridge, function_cartridge, tools, feedback, tools_logic) + prepared_tools = tools_logic.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[:label], 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[:label], + parameters: tool[:parameters], output: tool[:output] + } + } } } + ) + + tool + end + end + end + end +end -- cgit v1.2.3 From 46945c5279edbe9fb820312dfa0d44c988dea278 Mon Sep 17 00:00:00 2001 From: icebaker Date: Thu, 14 Dec 2023 22:54:02 -0300 Subject: setting default stream --- components/providers/google.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'components/providers') diff --git a/components/providers/google.rb b/components/providers/google.rb index 75b7658..2a99bcb 100644 --- a/components/providers/google.rb +++ b/components/providers/google.rb @@ -29,7 +29,7 @@ module NanoBot project_id: credentials[:'project-id'], region: credentials[:region] }, - settings: { model: } + settings: { model:, stream: false } ) end -- cgit v1.2.3 From fef3d5b3b2f823999fae68276382fe33872350c4 Mon Sep 17 00:00:00 2001 From: icebaker Date: Fri, 15 Dec 2023 08:04:27 -0300 Subject: improving provider options --- components/provider.rb | 2 +- components/providers/google.rb | 24 +++++++++++++++++++----- components/providers/openai.rb | 2 +- logic/cartridge/streaming.rb | 9 ++++++++- spec/data/cartridges/streaming.yml | 1 + spec/logic/cartridge/streaming_spec.rb | 19 +++++++++++++++---- static/cartridges/default.yml | 2 ++ 7 files changed, 47 insertions(+), 12 deletions(-) (limited to 'components/providers') diff --git a/components/provider.rb b/components/provider.rb index 2ad35f4..d83319f 100644 --- a/components/provider.rb +++ b/components/provider.rb @@ -11,7 +11,7 @@ module NanoBot when 'openai' Providers::OpenAI.new(provider[:settings], provider[:credentials], environment:) when 'google' - Providers::Google.new(provider[:model], provider[:settings], provider[:credentials], environment:) + Providers::Google.new(provider[:options], provider[:settings], provider[:credentials], environment:) else raise "Unsupported provider \"#{provider[:id]}\"" end diff --git a/components/providers/google.rb b/components/providers/google.rb index 2a99bcb..f847677 100644 --- a/components/providers/google.rb +++ b/components/providers/google.rb @@ -14,13 +14,16 @@ module NanoBot module Providers class Google < Base SETTINGS = { - safetySettings: %i[category threshold].freeze, - generationConfig: %i[temperature topP topK candidateCount maxOutputTokens stopSequences].freeze + generationConfig: %i[ + temperature topP topK candidateCount maxOutputTokens stopSequences + ].freeze }.freeze + SAFETY_SETTINGS = %i[category threshold].freeze + attr_reader :settings - def initialize(model, settings, credentials, _environment) + def initialize(options, settings, credentials, _environment) @settings = settings @client = Gemini.new( @@ -29,7 +32,7 @@ module NanoBot project_id: credentials[:'project-id'], region: credentials[:region] }, - settings: { model:, stream: false } + settings: { model: options[:model], stream: options[:stream] } ) end @@ -77,6 +80,16 @@ module NanoBot end end end + + if @settings[:safetySettings].is_a?(Array) + payload[:safetySettings] = [] unless payload.key?(:safetySettings) + + @settings[:safetySettings].each do |safety_setting| + setting = {} + SAFETY_SETTINGS.each { |key| setting[key] = safety_setting[key] } + payload[:safetySettings] << setting + end + end end if input[:tools] @@ -143,7 +156,8 @@ module NanoBot else begin result = @client.stream_generate_content( - Logic::Google::Tokens.apply_policies!(cartridge, payload) + Logic::Google::Tokens.apply_policies!(cartridge, payload), + stream: false ) rescue StandardError => e raise e.class, e.response[:body] if e.response && e.response[:body] diff --git a/components/providers/openai.rb b/components/providers/openai.rb index b70984b..f6eafd4 100644 --- a/components/providers/openai.rb +++ b/components/providers/openai.rb @@ -18,7 +18,7 @@ module NanoBot CHAT_SETTINGS = %i[ model stream temperature top_p n stop max_tokens - presence_penalty frequency_penalty logit_bias + presence_penalty frequency_penalty logit_bias seed response_format ].freeze attr_reader :settings diff --git a/logic/cartridge/streaming.rb b/logic/cartridge/streaming.rb index a0f8700..6949b3a 100644 --- a/logic/cartridge/streaming.rb +++ b/logic/cartridge/streaming.rb @@ -7,7 +7,14 @@ module NanoBot module Cartridge module Streaming def self.enabled?(cartridge, interface) - return false if Helpers::Hash.fetch(cartridge, %i[provider settings stream]) == false + provider_stream = case Helpers::Hash.fetch(cartridge, %i[provider id]) + when 'openai' + Helpers::Hash.fetch(cartridge, %i[provider settings stream]) + when 'google' + Helpers::Hash.fetch(cartridge, %i[provider options stream]) + end + + return false if provider_stream == false specific_interface = Helpers::Hash.fetch(cartridge, [:interfaces, interface, :output, :stream]) diff --git a/spec/data/cartridges/streaming.yml b/spec/data/cartridges/streaming.yml index 8234d34..e004110 100644 --- a/spec/data/cartridges/streaming.yml +++ b/spec/data/cartridges/streaming.yml @@ -10,5 +10,6 @@ interfaces: stream: true provider: + id: openai settings: stream: true diff --git a/spec/logic/cartridge/streaming_spec.rb b/spec/logic/cartridge/streaming_spec.rb index 466dd0b..4b71dfd 100644 --- a/spec/logic/cartridge/streaming_spec.rb +++ b/spec/logic/cartridge/streaming_spec.rb @@ -7,11 +7,22 @@ require_relative '../../../logic/cartridge/streaming' RSpec.describe NanoBot::Logic::Cartridge::Streaming do context 'interfaces override' do context 'defaults' do - let(:cartridge) { {} } + context 'openai' do + let(:cartridge) { { provider: { id: 'openai' } } } - 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) + 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 'google' do + let(:cartridge) { { provider: { id: 'google' } } } + + 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 end diff --git a/static/cartridges/default.yml b/static/cartridges/default.yml index 98dd47b..fbf449b 100644 --- a/static/cartridges/default.yml +++ b/static/cartridges/default.yml @@ -30,5 +30,7 @@ interfaces: feedback: true provider: + options: + stream: true settings: stream: true -- cgit v1.2.3