boxcars 0.7.6 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -3
- data/.ruby-version +1 -1
- data/CHANGELOG.md +41 -0
- data/Gemfile +3 -13
- data/Gemfile.lock +29 -25
- data/POSTHOG_TEST_README.md +118 -0
- data/README.md +305 -0
- data/boxcars.gemspec +1 -2
- data/lib/boxcars/boxcar/active_record.rb +9 -10
- data/lib/boxcars/boxcar/calculator.rb +2 -2
- data/lib/boxcars/boxcar/engine_boxcar.rb +4 -4
- data/lib/boxcars/boxcar/google_search.rb +2 -2
- data/lib/boxcars/boxcar/json_engine_boxcar.rb +1 -1
- data/lib/boxcars/boxcar/ruby_calculator.rb +1 -1
- data/lib/boxcars/boxcar/sql_base.rb +4 -4
- data/lib/boxcars/boxcar/swagger.rb +3 -3
- data/lib/boxcars/boxcar/vector_answer.rb +3 -3
- data/lib/boxcars/boxcar/xml_engine_boxcar.rb +1 -1
- data/lib/boxcars/boxcar.rb +6 -6
- data/lib/boxcars/conversation_prompt.rb +3 -3
- data/lib/boxcars/engine/anthropic.rb +121 -23
- data/lib/boxcars/engine/cerebras.rb +2 -2
- data/lib/boxcars/engine/cohere.rb +135 -9
- data/lib/boxcars/engine/gemini_ai.rb +151 -76
- data/lib/boxcars/engine/google.rb +2 -2
- data/lib/boxcars/engine/gpt4all_eng.rb +92 -34
- data/lib/boxcars/engine/groq.rb +124 -73
- data/lib/boxcars/engine/intelligence_base.rb +52 -17
- data/lib/boxcars/engine/ollama.rb +127 -47
- data/lib/boxcars/engine/openai.rb +186 -103
- data/lib/boxcars/engine/perplexityai.rb +116 -136
- data/lib/boxcars/engine/together.rb +2 -2
- data/lib/boxcars/engine/unified_observability.rb +430 -0
- data/lib/boxcars/engine.rb +4 -3
- data/lib/boxcars/engines.rb +74 -0
- data/lib/boxcars/observability.rb +44 -0
- data/lib/boxcars/observability_backend.rb +17 -0
- data/lib/boxcars/observability_backends/multi_backend.rb +42 -0
- data/lib/boxcars/observability_backends/posthog_backend.rb +89 -0
- data/lib/boxcars/observation.rb +8 -8
- data/lib/boxcars/prompt.rb +16 -4
- data/lib/boxcars/result.rb +7 -12
- data/lib/boxcars/ruby_repl.rb +1 -1
- data/lib/boxcars/train/train_action.rb +1 -1
- data/lib/boxcars/train/xml_train.rb +3 -3
- data/lib/boxcars/train/xml_zero_shot.rb +1 -1
- data/lib/boxcars/train/zero_shot.rb +3 -3
- data/lib/boxcars/train.rb +1 -1
- data/lib/boxcars/vector_search.rb +5 -5
- data/lib/boxcars/vector_store/pgvector/build_from_array.rb +116 -88
- data/lib/boxcars/vector_store/pgvector/build_from_files.rb +106 -80
- data/lib/boxcars/vector_store/pgvector/save_to_database.rb +148 -122
- data/lib/boxcars/vector_store/pgvector/search.rb +157 -131
- data/lib/boxcars/vector_store.rb +4 -4
- data/lib/boxcars/version.rb +1 -1
- data/lib/boxcars.rb +31 -20
- metadata +11 -21
@@ -1,80 +1,160 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
require 'openai' # Ollama uses the OpenAI gem with a custom URI base
|
4
|
+
require 'json'
|
5
|
+
|
4
6
|
module Boxcars
|
5
|
-
# A engine that uses local
|
7
|
+
# A engine that uses a local Ollama API (OpenAI-compatible).
|
6
8
|
class Ollama < Engine
|
9
|
+
include UnifiedObservability
|
7
10
|
attr_reader :prompts, :model_kwargs, :batch_size, :ollama_params
|
8
11
|
|
9
|
-
# The default parameters to use when asking the engine.
|
10
12
|
DEFAULT_PARAMS = {
|
11
|
-
model: "llama3",
|
13
|
+
model: "llama3", # Default model for Ollama
|
12
14
|
temperature: 0.1,
|
13
|
-
max_tokens: 4096
|
15
|
+
max_tokens: 4096 # Check if Ollama respects this or has its own limits
|
14
16
|
}.freeze
|
15
|
-
|
16
|
-
# the default name of the engine
|
17
17
|
DEFAULT_NAME = "Ollama engine"
|
18
|
-
# the default description of the engine
|
19
18
|
DEFAULT_DESCRIPTION = "useful for when you need to use local AI to answer questions. " \
|
20
19
|
"You should ask targeted questions"
|
21
20
|
|
22
|
-
# A engine is a container for a single tool to run.
|
23
|
-
# @param name [String] The name of the engine. Defaults to "OpenAI engine".
|
24
|
-
# @param description [String] A description of the engine. Defaults to:
|
25
|
-
# useful for when you need to use AI to answer questions. You should ask targeted questions".
|
26
|
-
# @param prompts [Array<String>] The prompts to use when asking the engine. Defaults to [].
|
27
|
-
# @param batch_size [Integer] The number of prompts to send to the engine at once. Defaults to 2.
|
28
21
|
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 2, **kwargs)
|
29
22
|
@ollama_params = DEFAULT_PARAMS.merge(kwargs)
|
30
23
|
@prompts = prompts
|
31
|
-
@batch_size = batch_size
|
32
|
-
super(description
|
24
|
+
@batch_size = batch_size # Retain if used by other methods
|
25
|
+
super(description:, name:)
|
33
26
|
end
|
34
27
|
|
35
|
-
#
|
36
|
-
#
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
::OpenAI::Client.new(uri_base: "http://localhost:11434")
|
28
|
+
# Renamed from open_ai_client to ollama_client for clarity
|
29
|
+
# Ollama doesn't use an API key by default.
|
30
|
+
def self.ollama_client
|
31
|
+
# The OpenAI gem requires an access_token, even if the local service doesn't.
|
32
|
+
# Provide a dummy one if not needed, or allow configuration if Ollama setup requires one.
|
33
|
+
::OpenAI::Client.new(access_token: "ollama-dummy-key", uri_base: "http://localhost:11434/v1")
|
34
|
+
# Added /v1 to uri_base, as OpenAI-compatible endpoints often version this way.
|
35
|
+
# Verify Ollama's actual OpenAI-compatible endpoint path.
|
41
36
|
end
|
42
37
|
|
43
|
-
|
38
|
+
# Ollama models are typically conversational.
|
39
|
+
def conversation_model?(_model_name)
|
44
40
|
true
|
45
41
|
end
|
46
42
|
|
47
|
-
# Get an answer from the engine.
|
48
|
-
# @param prompt [String] The prompt to use when asking the engine.
|
49
|
-
# @param groq_api_key [String] The access token to use when asking the engine.
|
50
|
-
# Defaults to Boxcars.configuration.groq_api_key.
|
51
|
-
# @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
|
52
43
|
def client(prompt:, inputs: {}, **kwargs)
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
44
|
+
start_time = Time.now
|
45
|
+
response_data = { response_obj: nil, parsed_json: nil, success: false, error: nil, status_code: nil }
|
46
|
+
current_params = @ollama_params.merge(kwargs)
|
47
|
+
current_prompt_object = prompt.is_a?(Array) ? prompt.first : prompt
|
48
|
+
api_request_params = nil # Initialize
|
49
|
+
|
50
|
+
begin
|
51
|
+
clnt = Ollama.ollama_client
|
52
|
+
api_request_params = _prepare_ollama_request_params(current_prompt_object, inputs, current_params)
|
53
|
+
|
54
|
+
log_messages_debug(api_request_params[:messages]) if Boxcars.configuration.log_prompts && api_request_params[:messages]
|
55
|
+
|
56
|
+
_execute_and_process_ollama_call(clnt, api_request_params, response_data)
|
57
|
+
rescue ::OpenAI::Error => e
|
58
|
+
_handle_openai_error_for_ollama(e, response_data)
|
59
|
+
rescue StandardError => e
|
60
|
+
_handle_standard_error_for_ollama(e, response_data)
|
61
|
+
ensure
|
62
|
+
duration_ms = ((Time.now - start_time) * 1000).round
|
63
|
+
request_context = {
|
64
|
+
prompt: current_prompt_object,
|
65
|
+
inputs:,
|
66
|
+
conversation_for_api: api_request_params&.dig(:messages)
|
67
|
+
}
|
68
|
+
track_ai_generation(
|
69
|
+
duration_ms:,
|
70
|
+
current_params:,
|
71
|
+
request_context:,
|
72
|
+
response_data:,
|
73
|
+
provider: :ollama
|
74
|
+
)
|
59
75
|
end
|
60
|
-
|
61
|
-
|
62
|
-
rescue => e
|
63
|
-
Boxcars.error(e, :red)
|
64
|
-
raise
|
76
|
+
|
77
|
+
_ollama_handle_call_outcome(response_data:)
|
65
78
|
end
|
66
79
|
|
67
|
-
|
68
|
-
# @param question [String] The question to ask the engine.
|
69
|
-
# @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
|
70
|
-
def run(question, **kwargs)
|
80
|
+
def run(question, **)
|
71
81
|
prompt = Prompt.new(template: question)
|
72
|
-
answer = client(prompt:
|
73
|
-
raise Error, "Ollama: No response from API" unless answer
|
74
|
-
|
75
|
-
# raise Error, "Ollama: #{response['error']}" if response["error"]
|
82
|
+
answer = client(prompt:, inputs: {}, **) # Pass empty inputs hash
|
76
83
|
Boxcars.debug("Answer: #{answer}", :cyan)
|
77
84
|
answer
|
78
85
|
end
|
86
|
+
|
87
|
+
def default_params
|
88
|
+
@ollama_params
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# Helper methods for the client method
|
94
|
+
def _prepare_ollama_request_params(prompt_object, inputs, current_params)
|
95
|
+
# prompt_object.as_messages(inputs) returns a hash like { messages: [...] }
|
96
|
+
# We need to extract the array of messages for the API call.
|
97
|
+
actual_messages_array = prompt_object.as_messages(inputs)[:messages]
|
98
|
+
{ messages: actual_messages_array }.merge(current_params)
|
99
|
+
end
|
100
|
+
|
101
|
+
def _execute_and_process_ollama_call(clnt, api_request_params, response_data)
|
102
|
+
raw_response = clnt.chat(parameters: api_request_params)
|
103
|
+
response_data[:response_obj] = raw_response
|
104
|
+
response_data[:parsed_json] = raw_response # OpenAI gem returns Hash
|
105
|
+
|
106
|
+
if raw_response && !raw_response["error"] && raw_response["choices"]
|
107
|
+
response_data[:success] = true
|
108
|
+
response_data[:status_code] = 200 # Inferred for local success
|
109
|
+
else
|
110
|
+
response_data[:success] = false
|
111
|
+
err_details = raw_response["error"] if raw_response
|
112
|
+
msg = if err_details
|
113
|
+
(err_details.is_a?(Hash) ? err_details['message'] : err_details).to_s
|
114
|
+
else
|
115
|
+
"Unknown Ollama API Error"
|
116
|
+
end
|
117
|
+
response_data[:error] ||= Error.new(msg) # Use ||= to not overwrite existing exception
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def _handle_openai_error_for_ollama(error, response_data)
|
122
|
+
response_data[:error] = error
|
123
|
+
response_data[:success] = false
|
124
|
+
response_data[:status_code] = error.http_status if error.respond_to?(:http_status)
|
125
|
+
end
|
126
|
+
|
127
|
+
def _handle_standard_error_for_ollama(error, response_data)
|
128
|
+
response_data[:error] = error
|
129
|
+
response_data[:success] = false
|
130
|
+
end
|
131
|
+
|
132
|
+
def log_messages_debug(messages)
|
133
|
+
return unless messages.is_a?(Array)
|
134
|
+
|
135
|
+
Boxcars.debug(messages.last(2).map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
|
136
|
+
end
|
137
|
+
|
138
|
+
def _ollama_handle_call_outcome(response_data:)
|
139
|
+
if response_data[:error]
|
140
|
+
Boxcars.error("Ollama Error: #{response_data[:error].message} (#{response_data[:error].class.name})", :red)
|
141
|
+
raise response_data[:error] # Re-raise the original error
|
142
|
+
elsif !response_data[:success]
|
143
|
+
# This case handles errors returned in the response body but not raised as OpenAI::Error
|
144
|
+
err_details = response_data.dig(:response_obj, "error")
|
145
|
+
msg = if err_details
|
146
|
+
err_details.is_a?(Hash) ? err_details['message'] : err_details.to_s
|
147
|
+
else
|
148
|
+
"Unknown error from Ollama API"
|
149
|
+
end
|
150
|
+
raise Error, msg
|
151
|
+
else
|
152
|
+
# Extract answer from successful response (assuming OpenAI-like structure)
|
153
|
+
choices = response_data.dig(:parsed_json, "choices")
|
154
|
+
raise Error, "Ollama: No choices found in response" unless choices.is_a?(Array) && !choices.empty?
|
155
|
+
|
156
|
+
choices.map { |c| c.dig("message", "content") }.join("\n").strip
|
157
|
+
end
|
158
|
+
end
|
79
159
|
end
|
80
160
|
end
|
@@ -1,161 +1,244 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'openai'
|
4
|
-
|
4
|
+
require 'json'
|
5
|
+
require 'securerandom'
|
6
|
+
|
5
7
|
module Boxcars
|
6
8
|
# A engine that uses OpenAI's API.
|
7
9
|
class Openai < Engine
|
10
|
+
include UnifiedObservability
|
8
11
|
attr_reader :prompts, :open_ai_params, :model_kwargs, :batch_size
|
9
12
|
|
10
|
-
# The default parameters to use when asking the engine.
|
11
13
|
DEFAULT_PARAMS = {
|
12
14
|
model: "gpt-4o-mini",
|
13
15
|
temperature: 0.1,
|
14
16
|
max_tokens: 4096
|
15
17
|
}.freeze
|
16
|
-
|
17
|
-
# the default name of the engine
|
18
18
|
DEFAULT_NAME = "OpenAI engine"
|
19
|
-
# the default description of the engine
|
20
19
|
DEFAULT_DESCRIPTION = "useful for when you need to use AI to answer questions. " \
|
21
20
|
"You should ask targeted questions"
|
22
21
|
|
23
|
-
# A engine is a container for a single tool to run.
|
24
|
-
# @param name [String] The name of the engine. Defaults to "OpenAI engine".
|
25
|
-
# @param description [String] A description of the engine. Defaults to:
|
26
|
-
# useful for when you need to use AI to answer questions. You should ask targeted questions".
|
27
|
-
# @param prompts [Array<String>] The prompts to use when asking the engine. Defaults to [].
|
28
|
-
# @param batch_size [Integer] The number of prompts to send to the engine at once. Defaults to 20.
|
29
22
|
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **kwargs)
|
30
23
|
@open_ai_params = DEFAULT_PARAMS.merge(kwargs)
|
31
|
-
|
24
|
+
# Special handling for o1-mini model (deprecated?)
|
25
|
+
if @open_ai_params[:model] =~ /^o/ && @open_ai_params[:max_tokens]
|
32
26
|
@open_ai_params[:max_completion_tokens] = @open_ai_params.delete(:max_tokens)
|
33
|
-
@open_ai_params.delete(:temperature)
|
27
|
+
@open_ai_params.delete(:temperature) # o1-mini might not support temperature
|
34
28
|
end
|
35
29
|
|
36
30
|
@prompts = prompts
|
37
31
|
@batch_size = batch_size
|
38
|
-
super(description
|
32
|
+
super(description:, name:)
|
39
33
|
end
|
40
34
|
|
41
|
-
# Get the OpenAI API client
|
42
|
-
# @param openai_access_token [String] The access token to use when asking the engine.
|
43
|
-
# Defaults to Boxcars.configuration.openai_access_token.
|
44
|
-
# @return [OpenAI::Client] The OpenAI API client.
|
45
35
|
def self.open_ai_client(openai_access_token: nil)
|
46
|
-
access_token = Boxcars.configuration.openai_access_token(openai_access_token:
|
36
|
+
access_token = Boxcars.configuration.openai_access_token(openai_access_token:)
|
47
37
|
organization_id = Boxcars.configuration.organization_id
|
48
|
-
|
38
|
+
# log_errors is good for the gem's own logging
|
39
|
+
::OpenAI::Client.new(access_token:, organization_id:, log_errors: true)
|
49
40
|
end
|
50
41
|
|
51
|
-
def conversation_model?(
|
52
|
-
!!(
|
42
|
+
def conversation_model?(model_name)
|
43
|
+
!!(model_name =~ /(^gpt-4)|(-turbo\b)|(^o\d)|(gpt-3\.5-turbo)/) # Added gpt-3.5-turbo
|
53
44
|
end
|
54
45
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
params = get_params(prompt, inputs, params)
|
70
|
-
clnt.chat(parameters: params)
|
46
|
+
def _prepare_openai_chat_request(prompt_object, inputs, current_params)
|
47
|
+
get_params(prompt_object, inputs, current_params.dup)
|
48
|
+
end
|
49
|
+
|
50
|
+
def _prepare_openai_completion_request(prompt_object, inputs, current_params)
|
51
|
+
prompt_text_for_api = prompt_object.as_prompt(inputs:)
|
52
|
+
prompt_text_for_api = prompt_text_for_api[:prompt] if prompt_text_for_api.is_a?(Hash) && prompt_text_for_api.key?(:prompt)
|
53
|
+
{ prompt: prompt_text_for_api }.merge(current_params).tap { |p| p.delete(:messages) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def _execute_openai_api_call(client, is_chat_model, api_request_params)
|
57
|
+
if is_chat_model
|
58
|
+
log_messages_debug(api_request_params[:messages]) if Boxcars.configuration.log_prompts && api_request_params[:messages]
|
59
|
+
client.chat(parameters: api_request_params)
|
71
60
|
else
|
72
|
-
|
73
|
-
|
74
|
-
clnt.completions(parameters: params)
|
61
|
+
Boxcars.debug("Prompt after formatting:\n#{api_request_params[:prompt]}", :cyan) if Boxcars.configuration.log_prompts
|
62
|
+
client.completions(parameters: api_request_params)
|
75
63
|
end
|
76
|
-
rescue StandardError => e
|
77
|
-
err = e.respond_to?(:response) ? e.response[:body] : e
|
78
|
-
Boxcars.warn("OpenAI Error #{e.class.name}: #{err}", :red)
|
79
|
-
raise
|
80
64
|
end
|
81
65
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
66
|
+
def _process_openai_response(raw_response, response_data)
|
67
|
+
response_data[:response_obj] = raw_response
|
68
|
+
response_data[:parsed_json] = raw_response # Already parsed by OpenAI gem
|
69
|
+
|
70
|
+
if raw_response && !raw_response["error"]
|
71
|
+
response_data[:success] = true
|
72
|
+
response_data[:status_code] = 200 # Inferred
|
73
|
+
else
|
74
|
+
response_data[:success] = false
|
75
|
+
err_details = raw_response["error"] if raw_response
|
76
|
+
msg = err_details ? "#{err_details['type']}: #{err_details['message']}" : "Unknown OpenAI API Error"
|
77
|
+
response_data[:error] ||= StandardError.new(msg) # Use ||= to not overwrite existing exception
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def _handle_openai_api_error(error, response_data)
|
82
|
+
response_data[:error] = error
|
83
|
+
response_data[:success] = false
|
84
|
+
response_data[:status_code] = error.http_status if error.respond_to?(:http_status)
|
85
|
+
end
|
86
|
+
|
87
|
+
def _handle_openai_standard_error(error, response_data)
|
88
|
+
response_data[:error] = error
|
89
|
+
response_data[:success] = false
|
90
|
+
end
|
91
|
+
|
92
|
+
def client(prompt:, inputs: {}, openai_access_token: nil, **kwargs)
|
93
|
+
start_time = Time.now
|
94
|
+
response_data = { response_obj: nil, parsed_json: nil, success: false, error: nil, status_code: nil }
|
95
|
+
current_params = open_ai_params.merge(kwargs)
|
96
|
+
current_prompt_object = prompt.is_a?(Array) ? prompt.first : prompt
|
97
|
+
api_request_params = nil
|
98
|
+
is_chat_model = conversation_model?(current_params[:model])
|
99
|
+
|
100
|
+
begin
|
101
|
+
clnt = Openai.open_ai_client(openai_access_token:)
|
102
|
+
api_request_params = if is_chat_model
|
103
|
+
_prepare_openai_chat_request(current_prompt_object, inputs, current_params)
|
104
|
+
else
|
105
|
+
_prepare_openai_completion_request(current_prompt_object, inputs, current_params)
|
106
|
+
end
|
107
|
+
raw_response = _execute_openai_api_call(clnt, is_chat_model, api_request_params)
|
108
|
+
_process_openai_response(raw_response, response_data)
|
109
|
+
rescue ::OpenAI::Error => e
|
110
|
+
_handle_openai_api_error(e, response_data)
|
111
|
+
rescue StandardError => e
|
112
|
+
_handle_openai_standard_error(e, response_data)
|
113
|
+
ensure
|
114
|
+
call_context = {
|
115
|
+
start_time:,
|
116
|
+
prompt_object: current_prompt_object,
|
117
|
+
inputs:,
|
118
|
+
api_request_params:,
|
119
|
+
current_params:,
|
120
|
+
is_chat_model:
|
121
|
+
}
|
122
|
+
_track_openai_observability(call_context, response_data)
|
123
|
+
end
|
124
|
+
|
125
|
+
_openai_handle_call_outcome(response_data:)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Called by Engine#generate to check the response from the client.
|
129
|
+
# @param response [Hash] The parsed JSON response from the OpenAI API.
|
130
|
+
# @raise [Boxcars::Error] if the response contains an error.
|
131
|
+
def check_response(response)
|
132
|
+
if response.is_a?(Hash) && response["error"]
|
133
|
+
err_details = response["error"]
|
134
|
+
msg = err_details ? "#{err_details['type']}: #{err_details['message']}" : "Unknown OpenAI API Error in check_response"
|
135
|
+
raise Boxcars::Error, msg
|
136
|
+
end
|
137
|
+
true
|
138
|
+
end
|
90
139
|
|
91
|
-
|
140
|
+
def run(question, **)
|
141
|
+
prompt = Prompt.new(template: question)
|
142
|
+
# client now returns the raw JSON response. We need to extract the answer.
|
143
|
+
raw_response = client(prompt:, inputs: {}, **)
|
144
|
+
answer = _extract_openai_answer_from_choices(raw_response["choices"])
|
92
145
|
Boxcars.debug("Answer: #{answer}", :cyan)
|
93
146
|
answer
|
94
147
|
end
|
95
148
|
|
96
|
-
# Get the default parameters for the engine.
|
97
149
|
def default_params
|
98
150
|
open_ai_params
|
99
151
|
end
|
100
152
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
153
|
+
private
|
154
|
+
|
155
|
+
def log_messages_debug(messages)
|
156
|
+
return unless messages.is_a?(Array)
|
157
|
+
|
158
|
+
Boxcars.debug(messages.last(2).map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
|
159
|
+
end
|
160
|
+
|
161
|
+
def get_params(prompt_object, inputs, params)
|
162
|
+
# Ensure prompt_object is a Boxcars::Prompt
|
163
|
+
current_prompt_object = if prompt_object.is_a?(Boxcars::Prompt)
|
164
|
+
prompt_object
|
165
|
+
else
|
166
|
+
Boxcars::Prompt.new(template: prompt_object.to_s)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Use as_messages for chat models
|
170
|
+
formatted_params = current_prompt_object.as_messages(inputs).merge(params)
|
111
171
|
|
112
|
-
|
172
|
+
# Handle models like o1-mini that don't support the system role
|
173
|
+
if formatted_params[:model] =~ /^o/ && formatted_params[:messages].first&.fetch(:role)&.to_s == 'system'
|
174
|
+
formatted_params[:messages].first[:role] = :user
|
175
|
+
end
|
176
|
+
# o1-mini specific param adjustments (already in initialize, but good to ensure here if params are rebuilt)
|
177
|
+
if formatted_params[:model] =~ /^o/
|
178
|
+
formatted_params.delete(:response_format)
|
179
|
+
formatted_params.delete(:stop)
|
180
|
+
if formatted_params.key?(:max_tokens) && !formatted_params.key?(:max_completion_tokens)
|
181
|
+
formatted_params[:max_completion_tokens] = formatted_params.delete(:max_tokens)
|
182
|
+
end
|
183
|
+
formatted_params.delete(:temperature)
|
113
184
|
end
|
185
|
+
formatted_params
|
186
|
+
end
|
114
187
|
|
115
|
-
|
116
|
-
|
188
|
+
def _handle_openai_error_outcome(error_data)
|
189
|
+
detailed_error_message = error_data.message
|
190
|
+
if error_data.respond_to?(:json_body) && error_data.json_body
|
191
|
+
detailed_error_message += " - Details: #{error_data.json_body}"
|
117
192
|
end
|
193
|
+
Boxcars.error("OpenAI Error: #{detailed_error_message} (#{error_data.class.name})", :red)
|
194
|
+
raise error_data
|
118
195
|
end
|
119
|
-
end
|
120
196
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
197
|
+
def _handle_openai_response_body_error(response_obj)
|
198
|
+
err_details = response_obj&.dig("error")
|
199
|
+
msg = err_details ? "#{err_details['type']}: #{err_details['message']}" : "Unknown error from OpenAI API"
|
200
|
+
raise Error, msg
|
201
|
+
end
|
125
202
|
|
126
|
-
|
127
|
-
|
128
|
-
def modelname_to_contextsize(modelname)
|
129
|
-
model_lookup = {
|
130
|
-
'text-davinci-003': 4097,
|
131
|
-
'text-curie-001': 2048,
|
132
|
-
'text-babbage-001': 2048,
|
133
|
-
'text-ada-001': 2048,
|
134
|
-
'code-davinci-002': 8000,
|
135
|
-
'code-cushman-001': 2048,
|
136
|
-
'gpt-3.5-turbo-1': 4096
|
137
|
-
}.freeze
|
138
|
-
model_lookup[modelname] || 4097
|
139
|
-
end
|
203
|
+
def _extract_openai_answer_from_choices(choices)
|
204
|
+
raise Error, "OpenAI: No choices found in response" unless choices.is_a?(Array) && !choices.empty?
|
140
205
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
206
|
+
if choices.first&.dig("message", "content")
|
207
|
+
choices.map { |c| c.dig("message", "content") }.join("\n").strip
|
208
|
+
elsif choices.first&.dig("text")
|
209
|
+
choices.map { |c| c["text"] }.join("\n").strip
|
210
|
+
else
|
211
|
+
raise Error, "OpenAI: Could not extract answer from choices"
|
212
|
+
end
|
213
|
+
end
|
146
214
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
215
|
+
def _openai_handle_call_outcome(response_data:)
|
216
|
+
if response_data[:error]
|
217
|
+
_handle_openai_error_outcome(response_data[:error])
|
218
|
+
elsif !response_data[:success] # e.g. raw_response["error"] was present
|
219
|
+
_handle_openai_response_body_error(response_data[:response_obj]) # Raises an error
|
220
|
+
else
|
221
|
+
response_data[:parsed_json] # Return the raw parsed JSON for Engine#generate
|
222
|
+
end
|
223
|
+
end
|
151
224
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
225
|
+
def _track_openai_observability(call_context, response_data)
|
226
|
+
duration_ms = ((Time.now - call_context[:start_time]) * 1000).round
|
227
|
+
is_chat_model = call_context[:is_chat_model]
|
228
|
+
api_request_params = call_context[:api_request_params] || {}
|
229
|
+
request_context = {
|
230
|
+
prompt: call_context[:prompt_object],
|
231
|
+
inputs: call_context[:inputs],
|
232
|
+
conversation_for_api: is_chat_model ? api_request_params[:messages] : api_request_params[:prompt]
|
233
|
+
}
|
234
|
+
|
235
|
+
track_ai_generation(
|
236
|
+
duration_ms:,
|
237
|
+
current_params: call_context[:current_params],
|
238
|
+
request_context:,
|
239
|
+
response_data:,
|
240
|
+
provider: :openai
|
241
|
+
)
|
158
242
|
end
|
159
|
-
params
|
160
243
|
end
|
161
244
|
end
|