From 46638e0b4e1809d683f470922f9cc27ab161c248 Mon Sep 17 00:00:00 2001 From: icebaker Date: Thu, 28 Dec 2023 19:43:00 -0300 Subject: upgrading gemini-ai and adding support for mistral-ai --- Gemfile.lock | 14 ++-- components/provider.rb | 5 +- components/providers/google.rb | 21 +++++- components/providers/mistral.rb | 115 +++++++++++++++++++++++++++++++ controllers/session.rb | 1 - logic/cartridge/streaming.rb | 2 +- logic/providers/google/tokens.rb | 2 - logic/providers/mistral/tokens.rb | 14 ++++ logic/providers/openai/tokens.rb | 2 - nano-bots.gemspec | 3 +- spec/logic/cartridge/interaction_spec.rb | 2 +- static/gem.rb | 8 +-- 12 files changed, 168 insertions(+), 21 deletions(-) create mode 100644 components/providers/mistral.rb create mode 100644 logic/providers/mistral/tokens.rb diff --git a/Gemfile.lock b/Gemfile.lock index be466f6..3355970 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,12 @@ PATH remote: . specs: - nano-bots (2.2.0) + nano-bots (2.3.0) babosa (~> 2.0) concurrent-ruby (~> 1.2, >= 1.2.2) dotenv (~> 2.8, >= 2.8.1) - gemini-ai (~> 2.2) + gemini-ai (~> 3.1) + mistral-ai (~> 1.0) pry (~> 0.14.2) rainbow (~> 3.1, >= 3.1.1) rbnacl (~> 7.1, >= 7.1.1) @@ -26,7 +27,7 @@ GEM diff-lcs (1.5.0) dotenv (2.8.1) event_stream_parser (1.0.0) - faraday (2.7.12) + faraday (2.8.1) base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) @@ -34,9 +35,9 @@ GEM multipart-post (~> 2) faraday-net_http (3.0.2) ffi (1.16.3) - gemini-ai (2.2.0) + gemini-ai (3.1.0) event_stream_parser (~> 1.0) - faraday (~> 2.7, >= 2.7.12) + faraday (~> 2.8, >= 2.8.1) googleauth (~> 1.9, >= 1.9.1) google-cloud-env (2.1.0) faraday (>= 1.0, < 3.a) @@ -51,6 +52,9 @@ GEM jwt (2.7.1) language_server-protocol (3.17.0.3) method_source (1.0.0) + mistral-ai (1.0.0) + event_stream_parser (~> 1.0) + faraday (~> 2.8, >= 2.8.1) multi_json (1.15.0) multipart-post (2.3.0) os (1.1.4) diff --git a/components/provider.rb b/components/provider.rb index 57f1cca..bdf3639 100644 --- a/components/provider.rb +++ b/components/provider.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true -require_relative 'providers/openai' require_relative 'providers/google' +require_relative 'providers/mistral' +require_relative 'providers/openai' module NanoBot module Components @@ -12,6 +13,8 @@ module NanoBot Providers::OpenAI.new(nil, provider[:settings], provider[:credentials], environment:) when 'google' Providers::Google.new(provider[:options], provider[:settings], provider[:credentials], environment:) + when 'mistral' + Providers::Mistral.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 25ffbde..c73269b 100644 --- a/components/providers/google.rb +++ b/components/providers/google.rb @@ -6,6 +6,8 @@ require_relative 'base' require_relative '../../logic/providers/google/tools' require_relative '../../logic/providers/google/tokens' +require_relative '../../logic/helpers/hash' +require_relative '../../logic/cartridge/default' require_relative 'tools' @@ -26,9 +28,19 @@ module NanoBot def initialize(options, settings, credentials, _environment) @settings = settings + gemini_options = options.transform_keys { |key| key.to_s.gsub('-', '_').to_sym } + + unless gemini_options.key?(:stream) + gemini_options[:stream] = Logic::Helpers::Hash.fetch( + Logic::Cartridge::Default.instance.values, %i[provider settings stream] + ) + end + + gemini_options[:server_sent_events] = gemini_options.delete(:stream) if gemini_options.key?(:stream) + @client = Gemini.new( credentials: credentials.transform_keys { |key| key.to_s.gsub('-', '_').to_sym }, - options: options.transform_keys { |key| key.to_s.gsub('-', '_').to_sym } + options: gemini_options ) end @@ -105,6 +117,9 @@ module NanoBot tools = [] stream_call_back = proc do |event, _parsed, _raw| + # TODO: How to better handle finishReason == 'OTHER'? + return if event.dig('candidates', 0, 'finishReason') == 'OTHER' + partial_content = event.dig('candidates', 0, 'content', 'parts').filter do |part| part.key?('text') end.map { |part| part['text'] }.join @@ -132,7 +147,7 @@ module NanoBot @client.stream_generate_content( Logic::Google::Tokens.apply_policies!(cartridge, payload), - stream: true, &stream_call_back + server_sent_events: true, &stream_call_back ) if tools&.size&.positive? @@ -156,7 +171,7 @@ module NanoBot else result = @client.stream_generate_content( Logic::Google::Tokens.apply_policies!(cartridge, payload), - stream: false + server_sent_events: false ) tools = result.dig(0, 'candidates', 0, 'content', 'parts').filter do |part| diff --git a/components/providers/mistral.rb b/components/providers/mistral.rb new file mode 100644 index 0000000..9b5c6c4 --- /dev/null +++ b/components/providers/mistral.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'mistral-ai' + +require_relative 'base' + +require_relative '../../logic/providers/mistral/tokens' +require_relative '../../logic/helpers/hash' +require_relative '../../logic/cartridge/default' + +module NanoBot + module Components + module Providers + class Mistral < Base + attr_reader :settings + + CHAT_SETTINGS = %i[ + model temperature top_p max_tokens stream safe_mode random_seed + ].freeze + + def initialize(options, settings, credentials, _environment) + @settings = settings + + mistral_options = if options + options.transform_keys { |key| key.to_s.gsub('-', '_').to_sym } + else + {} + end + + unless @settings.key?(:stream) + @settings = Marshal.load(Marshal.dump(@settings)) + @settings[:stream] = Logic::Helpers::Hash.fetch( + Logic::Cartridge::Default.instance.values, %i[provider settings stream] + ) + end + + mistral_options[:server_sent_events] = @settings[:stream] + + @client = ::Mistral.new( + credentials: credentials.transform_keys { |key| key.to_s.gsub('-', '_').to_sym }, + options: mistral_options + ) + end + + def evaluate(input, streaming, cartridge, &feedback) + messages = input[:history].map do |event| + { role: event[:who] == 'user' ? 'user' : 'assistant', + content: event[:message], + _meta: { at: event[:at] } } + end + + %i[backdrop directive].each do |key| + next unless input[:behavior][key] + + messages.prepend( + { role: key == :directive ? 'system' : 'user', + content: input[:behavior][key], + _meta: { at: Time.now } } + ) + end + + payload = { messages: } + + CHAT_SETTINGS.each do |key| + payload[key] = @settings[key] unless payload.key?(key) || !@settings.key?(key) + end + + raise 'Mistral does not support tools.' if input[:tools] + + if streaming + content = '' + + stream_call_back = proc do |event, _parsed, _raw| + partial_content = event.dig('choices', 0, 'delta', 'content') + + if partial_content + content += partial_content + feedback.call( + { should_be_stored: false, + interaction: { who: 'AI', message: partial_content } } + ) + end + + if event.dig('choices', 0, 'finish_reason') + feedback.call( + { should_be_stored: !(content.nil? || content == ''), + interaction: content.nil? || content == '' ? nil : { who: 'AI', message: content }, + finished: true } + ) + end + end + + @client.chat_completions( + Logic::Mistral::Tokens.apply_policies!(cartridge, payload), + server_sent_events: true, &stream_call_back + ) + else + result = @client.chat_completions( + Logic::Mistral::Tokens.apply_policies!(cartridge, payload), + server_sent_events: false + ) + + content = result.dig('choices', 0, 'message', 'content') + + 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/controllers/session.rb b/controllers/session.rb index e12ab86..dd89d6b 100644 --- a/controllers/session.rb +++ b/controllers/session.rb @@ -138,7 +138,6 @@ module NanoBot feedback[:interaction][:meta][:tool][:action] == 'confirming' Interfaces::Tool.confirming(self, @cartridge, mode, feedback[:interaction][:meta][:tool]) else - if feedback[:interaction] && feedback.dig(:interaction, :meta, :tool, :action) Interfaces::Tool.dispatch_feedback( self, @cartridge, mode, feedback[:interaction][:meta][:tool] diff --git a/logic/cartridge/streaming.rb b/logic/cartridge/streaming.rb index 6949b3a..0b9b19f 100644 --- a/logic/cartridge/streaming.rb +++ b/logic/cartridge/streaming.rb @@ -8,7 +8,7 @@ module NanoBot module Streaming def self.enabled?(cartridge, interface) provider_stream = case Helpers::Hash.fetch(cartridge, %i[provider id]) - when 'openai' + when 'openai', 'mistral' Helpers::Hash.fetch(cartridge, %i[provider settings stream]) when 'google' Helpers::Hash.fetch(cartridge, %i[provider options stream]) diff --git a/logic/providers/google/tokens.rb b/logic/providers/google/tokens.rb index 3d5492f..0d74928 100644 --- a/logic/providers/google/tokens.rb +++ b/logic/providers/google/tokens.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'openai' - module NanoBot module Logic module Google diff --git a/logic/providers/mistral/tokens.rb b/logic/providers/mistral/tokens.rb new file mode 100644 index 0000000..7aa64b7 --- /dev/null +++ b/logic/providers/mistral/tokens.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module NanoBot + module Logic + module Mistral + module Tokens + def self.apply_policies!(_cartridge, payload) + payload[:messages] = payload[:messages].map { |message| message.except(:_meta) } + payload + end + end + end + end +end diff --git a/logic/providers/openai/tokens.rb b/logic/providers/openai/tokens.rb index 60efa60..828b774 100644 --- a/logic/providers/openai/tokens.rb +++ b/logic/providers/openai/tokens.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'openai' - module NanoBot module Logic module OpenAI diff --git a/nano-bots.gemspec b/nano-bots.gemspec index 15fc8cf..2ff38cb 100644 --- a/nano-bots.gemspec +++ b/nano-bots.gemspec @@ -34,7 +34,8 @@ Gem::Specification.new do |spec| spec.add_dependency 'babosa', '~> 2.0' spec.add_dependency 'concurrent-ruby', '~> 1.2', '>= 1.2.2' spec.add_dependency 'dotenv', '~> 2.8', '>= 2.8.1' - spec.add_dependency 'gemini-ai', '~> 2.2' + spec.add_dependency 'gemini-ai', '~> 3.1' + spec.add_dependency 'mistral-ai', '~> 1.0' spec.add_dependency 'pry', '~> 0.14.2' spec.add_dependency 'rainbow', '~> 3.1', '>= 3.1.1' spec.add_dependency 'rbnacl', '~> 7.1', '>= 7.1.1' diff --git a/spec/logic/cartridge/interaction_spec.rb b/spec/logic/cartridge/interaction_spec.rb index f3ba46e..54fd956 100644 --- a/spec/logic/cartridge/interaction_spec.rb +++ b/spec/logic/cartridge/interaction_spec.rb @@ -26,7 +26,7 @@ RSpec.describe NanoBot::Logic::Cartridge::Interaction do ) end - it 'prepares the non-streamming output' do + it 'prepares the non-streaming output' do expect(described_class.output(cartridge, :repl, { message: 'hello' }, false, true)).to eq( { message: { content: 'hello', fennel: nil, lua: nil, clojure: nil } } ) diff --git a/static/gem.rb b/static/gem.rb index e9e5754..b6ce260 100644 --- a/static/gem.rb +++ b/static/gem.rb @@ -3,11 +3,11 @@ module NanoBot GEM = { name: 'nano-bots', - version: '2.2.0', - specification: '2.0.1', + version: '2.3.0', + specification: '2.1.0', author: 'icebaker', - summary: 'Ruby Implementation of Nano Bots: small, AI-powered bots for OpenAI ChatGPT and Google Gemini.', - description: 'Ruby Implementation of Nano Bots: small, AI-powered bots that can be easily shared as a single file, designed to support multiple providers such as OpenAI ChatGPT and Google Gemini, with support for calling Tools (Functions).', + summary: 'Ruby Implementation of Nano Bots: small, AI-powered bots for OpenAI ChatGPT, Mistral AI, and Google Gemini.', + description: 'Ruby Implementation of Nano Bots: small, AI-powered bots that can be easily shared as a single file, designed to support multiple providers such as OpenAI ChatGPT, Mistral AI, and Google Gemini, with support for calling Tools (Functions).', github: 'https://github.com/icebaker/ruby-nano-bots', gem_server: 'https://rubygems.org', license: 'MIT', -- cgit v1.2.3