lib/boxcars/engine/gemini_ai.rb in boxcars-0.6.8 vs lib/boxcars/engine/gemini_ai.rb in boxcars-0.6.9
- old
+ new
@@ -1,90 +1,80 @@
# frozen_string_literal: true
-# Boxcars - a framework for running a series of tools to get an answer to a question.
+# Boxcars is a framework for running a series of tools to get an answer to a question.
module Boxcars
- # A engine that uses Gemini's API.
+ # A engine that uses GeminiAI's API.
class GeminiAi < Engine
- attr_reader :prompts, :llm_params, :model_kwargs, :batch_size
+ attr_reader :prompts, :llm_parmas, :model_kwargs, :batch_size
# The default parameters to use when asking the engine.
DEFAULT_PARAMS = {
- model: "gemini-1.5-flash-latest"
+ model: "gemini-1.5-flash-latest",
+ temperature: 0.1
}.freeze
# the default name of the engine
- DEFAULT_NAME = "Google Gemini AI engine"
+ DEFAULT_NAME = "GeminiAI engine"
# the default description of the engine
- DEFAULT_DESCRIPTION = "useful for when you need to use Google Gemini AI to answer questions. " \
+ DEFAULT_DESCRIPTION = "useful for when you need to use AI to answer questions. " \
"You should ask targeted questions"
- # A engine is the driver for a single tool to run.
- # @param name [String] The name of the engine. Defaults to "OpenAI engine".
+ # A engine is a container for a single tool to run.
+ # @param name [String] The name of the engine. Defaults to "GeminiAI engine".
# @param description [String] A description of the engine. Defaults to:
# useful for when you need to use AI to answer questions. You should ask targeted questions".
# @param prompts [Array<String>] The prompts to use when asking the engine. Defaults to [].
- def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], **kwargs)
- @llm_params = DEFAULT_PARAMS.merge(kwargs)
+ # @param batch_size [Integer] The number of prompts to send to the engine at once. Defaults to 20.
+ def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **kwargs)
+ @llm_parmas = DEFAULT_PARAMS.merge(kwargs)
@prompts = prompts
- @batch_size = 20
+ @batch_size = batch_size
super(description: description, name: name)
end
+ # Get the OpenAI API client
+ # @param gemini_api_key [String] The access token to use when asking the engine.
+ # Defaults to Boxcars.configuration.gemini_api_key
+ # @return [OpenAI::Client] The OpenAI API gem client.
+ def self.open_ai_client(gemini_api_key: nil)
+ access_token = Boxcars.configuration.gemini_api_key(gemini_api_key: gemini_api_key)
+ ::OpenAI::Client.new(access_token: access_token, uri_base: "https://generativelanguage.googleapis.com/v1beta/openai/")
+ end
+
def conversation_model?(_model)
true
end
- def chat(params, gemini_api_key)
- raise Boxcars::ConfigurationError('Google AI API key not set') if gemini_api_key.blank?
-
- model_string = params.delete(:model_string)
- raise Boxcars::ConfigurationError('Google AI API key not set') if model_string.blank?
-
- # Define the API endpoint and parameters
- api_endpoint = "https://generativelanguage.googleapis.com/v1beta/models/#{model_string}:generateContent?key=#{gemini_api_key}"
-
- connection = Faraday.new(api_endpoint) do |faraday|
- faraday.request :url_encoded
- faraday.headers['Content-Type'] = 'application/json'
- end
-
- # Make the API call
- response = connection.post { |req| req.body = params.to_json }
-
- JSON.parse(response.body, symbolize_names: true)
- end
-
# Get an answer from the engine.
# @param prompt [String] The prompt to use when asking the engine.
- # @param gemini_api_key [String] Optional api key to use when asking the engine.
+ # @param gemini_api_key [String] The access token to use when asking the engine.
# Defaults to Boxcars.configuration.gemini_api_key.
# @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
- def client(prompt:, inputs: {}, **kwargs)
- api_key = Boxcars.configuration.gemini_api_key(**kwargs)
- option_params = llm_params.merge(kwargs)
- model_string = option_params.delete(:model) || DEFAULT_PARAMS[:model]
- convo = prompt.as_messages(inputs: inputs)
- # Convert conversation to Google Gemini format
- params = to_google_gemini_format(convo[:messages], option_params)
- params[:model_string] = model_string
- Boxcars.debug("Prompt after formatting:#{params[:message]}", :cyan) if Boxcars.configuration.log_prompts
- chat(params, api_key)
+ def client(prompt:, inputs: {}, gemini_api_key: nil, **kwargs)
+ clnt = GeminiAi.open_ai_client(gemini_api_key: gemini_api_key)
+ params = llm_parmas.merge(kwargs)
+ prompt = prompt.first if prompt.is_a?(Array)
+ params = prompt.as_messages(inputs).merge(params)
+ if Boxcars.configuration.log_prompts
+ Boxcars.debug(params[:messages].last(2).map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
+ end
+ clnt.chat(parameters: params)
+ rescue => e
+ Boxcars.error(e, :red)
+ raise
end
# get an answer from the engine for a question.
# @param question [String] The question to ask the engine.
# @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
def run(question, **kwargs)
prompt = Prompt.new(template: question)
response = client(prompt: prompt, **kwargs)
-
raise Error, "GeminiAI: No response from API" unless response
- raise Error, "GeminiAI: #{response[:error]}" if response[:error]
- answer = response[:candidates].first[:content][:parts].first[:text]
- Boxcars.debug(response, :yellow)
- answer
+ check_response(response)
+ response["choices"].map { |c| c.dig("message", "content") || c["text"] }.join("\n").strip
end
# Get the default parameters for the engine.
def default_params
llm_params
@@ -93,68 +83,36 @@
# make sure we got a valid response
# @param response [Hash] The response to check.
# @param must_haves [Array<String>] The keys that must be in the response. Defaults to %w[choices].
# @raise [KeyError] if there is an issue with the access token.
# @raise [ValueError] if the response is not valid.
- def check_response(response, must_haves: %w[completion])
- if response['error']
+ def check_response(response, must_haves: %w[choices])
+ if response['error'].is_a?(Hash)
code = response.dig('error', 'code')
msg = response.dig('error', 'message') || 'unknown error'
- raise KeyError, "ANTHOPIC_API_KEY not valid" if code == 'invalid_api_key'
+ raise KeyError, "GEMINI_API_TOKEN not valid" if code == 'invalid_api_key'
- raise ValueError, "Gemini error: #{msg}"
+ raise ValueError, "GeminiAI error: #{msg}"
end
must_haves.each do |key|
raise ValueError, "Expecting key #{key} in response" unless response.key?(key)
end
end
# the engine type
def engine_type
- "claude"
+ "gemini_ai"
end
- # lookup the context size for a model by name
- # @param modelname [String] The name of the model to lookup.
- def modelname_to_contextsize(_modelname)
- 100000
- end
-
# Calculate the maximum number of tokens possible to generate for a prompt.
# @param prompt_text [String] The prompt text to use.
# @return [Integer] the number of tokens possible to generate.
def max_tokens_for_prompt(prompt_text)
num_tokens = get_num_tokens(prompt_text)
# get max context size for model by name
- max_size = modelname_to_contextsize(model_name)
+ max_size = 8096
max_size - num_tokens
- end
-
- def to_google_gemini_format(convo, option_params)
- instructions = convo.shift.last if convo.first && convo.first[:role] == :system
- system_instructions = instructions || "You are a helpful assistant."
-
- # Convert conversation history to the format expected by Google
- contents = convo.map { |message| { text: message[:content] } }
-
- generation_config = {}
- if option_params.length.positive?
- generation_config.merge!(option_params)
- generation_config[:stopSequences] = [generation_config.delete(:stop)] if generation_config[:stop].present?
- end
-
- rv = {
- system_instruction: { parts: { text: system_instructions } }, # System instructions or context
- contents: { parts: contents } # The chat messages
- }
-
- rv[:generationConfig] = generation_config if generation_config.length.positive?
- rv
- end
-
- def default_prefixes
- { system: "SYSTEM: ", user: "USER: ", assistant: "CHATBOT: ", history: :history }
end
end
end