Class: Boxcars::Engine Abstract
- Inherits:
-
Object
- Object
- Boxcars::Engine
- Defined in:
- lib/boxcars/engine.rb
Overview
Direct Known Subclasses
Anthropic, Cohere, GeminiAi, Gpt4allEng, Groq, IntelligenceBase, Ollama, Openai, Perplexityai
Instance Attribute Summary collapse
-
#batch_size ⇒ Object
readonly
Returns the value of attribute batch_size.
-
#prompts ⇒ Object
readonly
Returns the value of attribute prompts.
-
#user_id ⇒ Object
readonly
Returns the value of attribute user_id.
Instance Method Summary collapse
- #extract_answer(response) ⇒ Object
-
#generate(prompts:, stop: nil) ⇒ EngineResult
Call out to LLM’s endpoint with k unique prompts.
-
#generation_info(sub_choices) ⇒ Array<Generation>
Get generation informaton.
-
#get_num_tokens(text:) ⇒ Object
calculate the number of tokens used.
-
#initialize(description: 'Engine', name: nil, prompts: [], batch_size: 20, user_id: nil) ⇒ Engine
constructor
An Engine is used by Boxcars to generate output from prompts.
-
#run(question) ⇒ Object
Get an answer from the Engine.
-
#validate_response!(response, must_haves: %w[choices])) ⇒ Object
Validate API response and raise appropriate errors.
Constructor Details
#initialize(description: 'Engine', name: nil, prompts: [], batch_size: 20, user_id: nil) ⇒ Engine
An Engine is used by Boxcars to generate output from prompts
14 15 16 17 18 19 20 |
# File 'lib/boxcars/engine.rb', line 14 def initialize(description: 'Engine', name: nil, prompts: [], batch_size: 20, user_id: nil) @name = name || self.class.name @description = description @prompts = prompts @batch_size = batch_size @user_id = user_id end |
Instance Attribute Details
#batch_size ⇒ Object (readonly)
Returns the value of attribute batch_size.
6 7 8 |
# File 'lib/boxcars/engine.rb', line 6 def batch_size @batch_size end |
#prompts ⇒ Object (readonly)
Returns the value of attribute prompts.
6 7 8 |
# File 'lib/boxcars/engine.rb', line 6 def prompts @prompts end |
#user_id ⇒ Object (readonly)
Returns the value of attribute user_id.
6 7 8 |
# File 'lib/boxcars/engine.rb', line 6 def user_id @user_id end |
Instance Method Details
#extract_answer(response) ⇒ Object
105 106 107 108 109 110 111 112 113 114 |
# File 'lib/boxcars/engine.rb', line 105 def extract_answer(response) # Handle different response formats if response["choices"] response["choices"].map { |c| c.dig("message", "content") || c["text"] }.join("\n").strip elsif response["candidates"] response["candidates"].map { |c| c.dig("content", "parts", 0, "text") }.join("\n").strip else response["output"] || response.to_s end end |
#generate(prompts:, stop: nil) ⇒ EngineResult
Call out to LLM’s endpoint with k unique prompts.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/boxcars/engine.rb', line 53 def generate(prompts:, stop: nil) params = {} params[:stop] = stop if stop choices = [] token_usage = {} # Get the token usage from the response. # Includes prompt, completion, and total tokens used. inkeys = %w[completion_tokens prompt_tokens total_tokens].freeze prompts.each_slice(batch_size) do |sub_prompts| sub_prompts.each do |sprompt, inputs| client_response = client(prompt: sprompt, inputs:, **params) # All engines now return the parsed API response hash directly api_response_hash = client_response # Ensure we have a hash to work with unless api_response_hash.is_a?(Hash) raise TypeError, "Expected Hash from client method, got #{api_response_hash.class}: #{api_response_hash.inspect}" end validate_response!(api_response_hash) current_choices = api_response_hash["choices"] if current_choices.is_a?(Array) choices.concat(current_choices) elsif api_response_hash["output"] # Synthesize a choice from non-Chat providers (e.g., OpenAI Responses API for GPT-5) synthesized_text = extract_answer(api_response_hash) choices << { "message" => { "content" => synthesized_text }, "finish_reason" => "stop" } else Boxcars.logger&.warn "No 'choices' or 'output' found in API response: #{api_response_hash.inspect}" end api_usage = api_response_hash["usage"] if api_usage.is_a?(Hash) usage_keys = inkeys & api_usage.keys usage_keys.each { |key| token_usage[key] = token_usage[key].to_i + api_usage[key] } else Boxcars.logger&.warn "No 'usage' data found in API response: #{api_response_hash.inspect}" end end end n = params.fetch(:n, 1) generations = [] prompts.each_with_index do |_prompt, i| sub_choices = choices[i * n, (i + 1) * n] generations.push(generation_info(sub_choices)) end EngineResult.new(generations:, engine_output: { token_usage: }) end |
#generation_info(sub_choices) ⇒ Array<Generation>
Get generation informaton
36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/boxcars/engine.rb', line 36 def generation_info(sub_choices) sub_choices.map do |choice| Generation.new( text: (choice.dig("message", "content") || choice["text"]).to_s, generation_info: { finish_reason: choice.fetch("finish_reason", nil), logprobs: choice.fetch("logprobs", nil) } ) end end |
#get_num_tokens(text:) ⇒ Object
calculate the number of tokens used
29 30 31 |
# File 'lib/boxcars/engine.rb', line 29 def get_num_tokens(text:) text.split.length # TODO: hook up to token counting gem end |
#run(question) ⇒ Object
Get an answer from the Engine.
24 25 26 |
# File 'lib/boxcars/engine.rb', line 24 def run(question) raise NotImplementedError end |
#validate_response!(response, must_haves: %w[choices])) ⇒ Object
Validate API response and raise appropriate errors
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/boxcars/engine.rb', line 121 def validate_response!(response, must_haves: %w[choices]) # Check for API errors first if response['error'] error_details = response['error'] raise Boxcars::Error, "API error: #{error_details}" unless error_details.is_a?(Hash) code = error_details['code'] = error_details['message'] || 'unknown error' # Handle common API key errors raise KeyError, "API key not valid or permission denied" if ['invalid_api_key', 'permission_denied'].include?(code) raise Boxcars::Error, "API error: #{}" end # Check for required keys in response has_required_content = must_haves.any? { |key| response.key?(key) && !response[key].nil? } return if has_required_content raise Boxcars::Error, "Response missing required keys. Expected one of: #{must_haves.join(', ')}" end |