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
@@ -21,8 +21,8 @@ module Boxcars
|
|
21
21
|
# @param prompts [Array<Prompt>] The prompts to use for the Engine.
|
22
22
|
# @param batch_size [Integer] The number of prompts to send to the Engine at a time.
|
23
23
|
# @param kwargs [Hash] Additional parameters to pass to the Engine.
|
24
|
-
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **
|
25
|
-
super(provider: :google, description
|
24
|
+
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **)
|
25
|
+
super(provider: :google, description:, name:, prompts:, batch_size:, **)
|
26
26
|
end
|
27
27
|
|
28
28
|
def default_model_params
|
@@ -1,56 +1,114 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'gpt4all'
|
4
|
-
|
4
|
+
require 'json' # For pretty_generate
|
5
|
+
|
5
6
|
module Boxcars
|
6
7
|
# A engine that uses local GPT4All API.
|
7
8
|
class Gpt4allEng < Engine
|
8
|
-
|
9
|
+
include UnifiedObservability
|
10
|
+
attr_reader :prompts, :model_kwargs, :batch_size, :gpt4all_params # Added gpt4all_params
|
9
11
|
|
10
|
-
# the default name of the engine
|
11
12
|
DEFAULT_NAME = "Gpt4all engine"
|
12
|
-
# the default description of the engine
|
13
13
|
DEFAULT_DESCRIPTION = "useful for when you need to use local AI to answer questions. " \
|
14
14
|
"You should ask targeted questions"
|
15
|
+
# GPT4All doesn't have typical API params like temperature or model selection via params in the same way.
|
16
|
+
# Model is usually pre-loaded. We can add a placeholder for model_name if needed for tracking.
|
17
|
+
DEFAULT_PARAMS = {
|
18
|
+
model_name: "gpt4all-j-v1.3-groovy" # Example, actual model depends on local setup
|
19
|
+
}.freeze
|
15
20
|
|
16
|
-
|
17
|
-
|
18
|
-
# @param description [String] A description of the engine. Defaults to:
|
19
|
-
# useful for when you need to use AI to answer questions. You should ask targeted questions".
|
20
|
-
# @param prompts [Array<String>] The prompts to use when asking the engine. Defaults to [].
|
21
|
-
# @param batch_size [Integer] The number of prompts to send to the engine at once. Defaults to 2.
|
22
|
-
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 2, **_kwargs)
|
21
|
+
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 2, **kwargs)
|
22
|
+
@gpt4all_params = DEFAULT_PARAMS.merge(kwargs) # Store merged params
|
23
23
|
@prompts = prompts
|
24
|
-
@batch_size = batch_size
|
25
|
-
super(description
|
24
|
+
@batch_size = batch_size # Retain if used by other methods
|
25
|
+
super(description:, name:)
|
26
26
|
end
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
28
|
+
def client(prompt:, inputs: {}, **kwargs)
|
29
|
+
start_time = Time.now
|
30
|
+
response_data = { response_obj: nil, parsed_json: nil, success: false, error: nil, status_code: nil }
|
31
|
+
# current_params are the effective parameters for this call, including defaults and overrides
|
32
|
+
current_params = @gpt4all_params.merge(kwargs)
|
33
|
+
# api_request_params for GPT4All is just the input text.
|
34
|
+
api_request_params = nil
|
35
|
+
current_prompt_object = prompt.is_a?(Array) ? prompt.first : prompt
|
36
|
+
gpt4all_instance = nil # To ensure it's in scope for ensure block
|
37
|
+
|
38
|
+
begin
|
39
|
+
gpt4all_instance = Gpt4all::ConversationalAI.new
|
40
|
+
# prepare_resources might download models, could take time.
|
41
|
+
# Consider if this setup should be outside the timed/tracked client call for long-running setup.
|
42
|
+
# For now, including it as it's part of the interaction.
|
43
|
+
gpt4all_instance.prepare_resources(force_download: false)
|
44
|
+
gpt4all_instance.start_bot
|
45
|
+
|
46
|
+
# GPT4All gem's prompt method takes a string.
|
47
|
+
prompt_text_for_api = current_prompt_object.as_prompt(inputs:)
|
48
|
+
prompt_text_for_api = prompt_text_for_api[:prompt] if prompt_text_for_api.is_a?(Hash) && prompt_text_for_api.key?(:prompt)
|
49
|
+
api_request_params = { prompt: prompt_text_for_api } # Store what's sent
|
50
|
+
|
51
|
+
Boxcars.debug("Prompt after formatting:\n#{prompt_text_for_api}", :cyan) if Boxcars.configuration.log_prompts
|
52
|
+
|
53
|
+
raw_response_text = gpt4all_instance.prompt(prompt_text_for_api) # Actual call
|
54
|
+
|
55
|
+
# GPT4All gem returns a string directly, or raises error.
|
56
|
+
response_data[:response_obj] = raw_response_text # Store the raw string
|
57
|
+
response_data[:parsed_json] = { "text" => raw_response_text } # Create a simple hash for consistency
|
58
|
+
response_data[:success] = true
|
59
|
+
response_data[:status_code] = 200 # Inferred for local success
|
60
|
+
rescue StandardError => e
|
61
|
+
response_data[:error] = e
|
62
|
+
response_data[:success] = false
|
63
|
+
# No HTTP status code for local errors typically, unless the gem provides one.
|
64
|
+
ensure
|
65
|
+
gpt4all_instance&.stop_bot # Ensure bot is stopped even if errors occur
|
66
|
+
|
67
|
+
duration_ms = ((Time.now - start_time) * 1000).round
|
68
|
+
request_context = {
|
69
|
+
prompt: current_prompt_object,
|
70
|
+
inputs:,
|
71
|
+
conversation_for_api: api_request_params&.dig(:prompt) # The text prompt
|
72
|
+
}
|
73
|
+
|
74
|
+
track_ai_generation(
|
75
|
+
duration_ms:,
|
76
|
+
current_params:,
|
77
|
+
request_context:,
|
78
|
+
response_data:,
|
79
|
+
provider: :gpt4all
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
_gpt4all_handle_call_outcome(response_data:)
|
44
84
|
end
|
45
85
|
|
46
|
-
|
47
|
-
# @param question [String] The question to ask the engine.
|
48
|
-
# @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
|
49
|
-
def run(question, **kwargs)
|
86
|
+
def run(question, **)
|
50
87
|
prompt = Prompt.new(template: question)
|
51
|
-
answer = client(prompt:
|
88
|
+
answer = client(prompt:, inputs: {}, **)
|
52
89
|
Boxcars.debug("Answer: #{answer}", :cyan)
|
53
90
|
answer
|
54
91
|
end
|
92
|
+
|
93
|
+
# Added for consistency
|
94
|
+
def default_params
|
95
|
+
@gpt4all_params
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def _gpt4all_handle_call_outcome(response_data:)
|
101
|
+
if response_data[:error]
|
102
|
+
# The original code had a specific error logging format.
|
103
|
+
Boxcars.error(["Error from gpt4all engine: #{response_data[:error].message}",
|
104
|
+
response_data[:error].backtrace&.first(5)&.join("\n ")].compact.join("\n "), :red)
|
105
|
+
raise response_data[:error]
|
106
|
+
elsif !response_data[:success]
|
107
|
+
# This case might be redundant if gpt4all gem always raises on error
|
108
|
+
raise Error, "Unknown error from Gpt4all"
|
109
|
+
else
|
110
|
+
response_data.dig(:parsed_json, "text") # Extract the text from our structured hash
|
111
|
+
end
|
112
|
+
end
|
55
113
|
end
|
56
114
|
end
|
data/lib/boxcars/engine/groq.rb
CHANGED
@@ -1,119 +1,170 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
require 'openai' # Groq uses the OpenAI gem with a custom URI base
|
4
|
+
require 'json'
|
5
|
+
|
4
6
|
module Boxcars
|
5
7
|
# A engine that uses Groq's API.
|
6
8
|
class Groq < Engine
|
7
|
-
|
9
|
+
include UnifiedObservability
|
10
|
+
attr_reader :prompts, :groq_params, :model_kwargs, :batch_size
|
8
11
|
|
9
|
-
# The default parameters to use when asking the engine.
|
10
12
|
DEFAULT_PARAMS = {
|
11
13
|
model: "llama3-70b-8192",
|
12
14
|
temperature: 0.1,
|
13
|
-
max_tokens: 4096
|
15
|
+
max_tokens: 4096 # Groq API might have specific limits or naming for this
|
14
16
|
}.freeze
|
15
|
-
|
16
|
-
# the default name of the engine
|
17
17
|
DEFAULT_NAME = "Groq engine"
|
18
|
-
|
19
|
-
DEFAULT_DESCRIPTION = "useful for when you need to use AI to answer questions. " \
|
18
|
+
DEFAULT_DESCRIPTION = "useful for when you need to use Groq 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 "Groq 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 20.
|
28
21
|
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **kwargs)
|
29
|
-
@
|
22
|
+
@groq_params = DEFAULT_PARAMS.merge(kwargs) # Corrected typo here
|
30
23
|
@prompts = prompts
|
31
24
|
@batch_size = batch_size
|
32
|
-
super(description
|
25
|
+
super(description:, name:)
|
33
26
|
end
|
34
27
|
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
access_token = Boxcars.configuration.groq_api_key(groq_api_key: groq_api_key)
|
41
|
-
::OpenAI::Client.new(access_token: access_token, uri_base: "https://api.groq.com/openai")
|
28
|
+
# Renamed from open_ai_client to groq_client for clarity
|
29
|
+
def self.groq_client(groq_api_key: nil)
|
30
|
+
access_token = Boxcars.configuration.groq_api_key(groq_api_key:)
|
31
|
+
::OpenAI::Client.new(access_token:, uri_base: "https://api.groq.com/openai/v1")
|
32
|
+
# Adjusted uri_base to include /v1 as is common for OpenAI-compatible APIs
|
42
33
|
end
|
43
34
|
|
44
|
-
|
35
|
+
# Groq models are typically conversational.
|
36
|
+
def conversation_model?(_model_name)
|
45
37
|
true
|
46
38
|
end
|
47
39
|
|
48
|
-
# Get an answer from the engine.
|
49
|
-
# @param prompt [String] The prompt to use when asking the engine.
|
50
|
-
# @param groq_api_key [String] The access token to use when asking the engine.
|
51
|
-
# Defaults to Boxcars.configuration.groq_api_key.
|
52
|
-
# @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
|
53
40
|
def client(prompt:, inputs: {}, groq_api_key: nil, **kwargs)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
41
|
+
start_time = Time.now
|
42
|
+
response_data = { response_obj: nil, parsed_json: nil, success: false, error: nil, status_code: nil }
|
43
|
+
current_params = @groq_params.merge(kwargs)
|
44
|
+
current_prompt_object = prompt.is_a?(Array) ? prompt.first : prompt
|
45
|
+
api_request_params = nil # Initialize
|
46
|
+
|
47
|
+
begin
|
48
|
+
clnt = Groq.groq_client(groq_api_key:)
|
49
|
+
api_request_params = _prepare_groq_request_params(current_prompt_object, inputs, current_params)
|
50
|
+
|
51
|
+
log_messages_debug(api_request_params[:messages]) if Boxcars.configuration.log_prompts && api_request_params[:messages]
|
52
|
+
|
53
|
+
_execute_and_process_groq_call(clnt, api_request_params, response_data)
|
54
|
+
rescue ::OpenAI::Error => e
|
55
|
+
_handle_openai_error_for_groq(e, response_data)
|
56
|
+
rescue StandardError => e
|
57
|
+
_handle_standard_error_for_groq(e, response_data)
|
58
|
+
ensure
|
59
|
+
duration_ms = ((Time.now - start_time) * 1000).round
|
60
|
+
request_context = {
|
61
|
+
prompt: current_prompt_object,
|
62
|
+
inputs:,
|
63
|
+
conversation_for_api: api_request_params&.dig(:messages)
|
64
|
+
}
|
65
|
+
track_ai_generation(
|
66
|
+
duration_ms:,
|
67
|
+
current_params:,
|
68
|
+
request_context:,
|
69
|
+
response_data:,
|
70
|
+
provider: :groq
|
71
|
+
)
|
60
72
|
end
|
61
|
-
|
62
|
-
|
63
|
-
Boxcars.error(e, :red)
|
64
|
-
raise
|
73
|
+
|
74
|
+
_groq_handle_call_outcome(response_data:)
|
65
75
|
end
|
66
76
|
|
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)
|
77
|
+
def run(question, **)
|
71
78
|
prompt = Prompt.new(template: question)
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
check_response(response)
|
76
|
-
response["choices"].map { |c| c.dig("message", "content") || c["text"] }.join("\n").strip
|
79
|
+
answer = client(prompt:, inputs: {}, **)
|
80
|
+
Boxcars.debug("Answer: #{answer}", :cyan)
|
81
|
+
answer
|
77
82
|
end
|
78
83
|
|
79
|
-
# Get the default parameters for the engine.
|
80
84
|
def default_params
|
81
|
-
|
85
|
+
@groq_params # Use instance variable
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# Helper methods for the client method
|
91
|
+
def _prepare_groq_request_params(prompt_object, inputs, current_params)
|
92
|
+
messages_hash_from_prompt = prompt_object.as_messages(inputs)
|
93
|
+
actual_messages_for_api = messages_hash_from_prompt[:messages]
|
94
|
+
{ messages: actual_messages_for_api }.merge(current_params)
|
82
95
|
end
|
83
96
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
97
|
+
def _execute_and_process_groq_call(clnt, api_request_params, response_data)
|
98
|
+
raw_response = clnt.chat(parameters: api_request_params)
|
99
|
+
response_data[:response_obj] = raw_response
|
100
|
+
response_data[:parsed_json] = raw_response # OpenAI gem returns Hash
|
101
|
+
|
102
|
+
if raw_response && !raw_response["error"] && raw_response["choices"]
|
103
|
+
response_data[:success] = true
|
104
|
+
response_data[:status_code] = 200 # Inferred
|
105
|
+
else
|
106
|
+
response_data[:success] = false
|
107
|
+
err_details = raw_response["error"] if raw_response
|
108
|
+
msg = if err_details
|
109
|
+
(err_details.is_a?(Hash) ? err_details['message'] : err_details).to_s
|
110
|
+
else
|
111
|
+
"Unknown Groq API Error"
|
112
|
+
end
|
113
|
+
response_data[:error] ||= StandardError.new(msg) # Use ||= to not overwrite existing exception
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def _handle_openai_error_for_groq(error, response_data)
|
118
|
+
response_data[:error] = error
|
119
|
+
response_data[:success] = false
|
120
|
+
response_data[:status_code] = error.http_status if error.respond_to?(:http_status)
|
121
|
+
end
|
122
|
+
|
123
|
+
def _handle_standard_error_for_groq(error, response_data)
|
124
|
+
response_data[:error] = error
|
125
|
+
response_data[:success] = false
|
126
|
+
end
|
127
|
+
|
128
|
+
def log_messages_debug(messages)
|
129
|
+
return unless messages.is_a?(Array)
|
130
|
+
|
131
|
+
Boxcars.debug(messages.last(2).map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
|
132
|
+
end
|
133
|
+
|
134
|
+
def _groq_handle_call_outcome(response_data:)
|
135
|
+
if response_data[:error]
|
136
|
+
Boxcars.error("Groq Error: #{response_data[:error].message} (#{response_data[:error].class.name})", :red)
|
137
|
+
raise response_data[:error]
|
138
|
+
elsif !response_data[:success]
|
139
|
+
err_details = response_data.dig(:response_obj, "error")
|
140
|
+
msg = if err_details
|
141
|
+
err_details.is_a?(Hash) ? "#{err_details['type']}: #{err_details['message']}" : err_details.to_s
|
142
|
+
else
|
143
|
+
"Unknown error from Groq API"
|
144
|
+
end
|
145
|
+
raise Error, msg
|
146
|
+
else
|
147
|
+
choices = response_data.dig(:parsed_json, "choices")
|
148
|
+
raise Error, "Groq: No choices found in response" unless choices.is_a?(Array) && !choices.empty?
|
149
|
+
|
150
|
+
choices.map { |c| c.dig("message", "content") || c["text"] }.join("\n").strip
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Retaining check_response if run method or other parts still use it.
|
89
155
|
def check_response(response, must_haves: %w[choices])
|
90
156
|
if response['error'].is_a?(Hash)
|
91
157
|
code = response.dig('error', 'code')
|
92
158
|
msg = response.dig('error', 'message') || 'unknown error'
|
93
|
-
|
159
|
+
# GROQ_API_TOKEN is not standard, usually it's an API key.
|
160
|
+
raise KeyError, "Groq API Key not valid or permission issue" if ['invalid_api_key', 'permission_denied'].include?(code)
|
94
161
|
|
95
162
|
raise ValueError, "Groq error: #{msg}"
|
96
163
|
end
|
97
164
|
|
98
165
|
must_haves.each do |key|
|
99
|
-
raise ValueError, "Expecting key #{key} in response" unless response.key?(key)
|
166
|
+
raise ValueError, "Expecting key #{key} in response" unless response.key?(key) && !response[key].empty?
|
100
167
|
end
|
101
168
|
end
|
102
|
-
|
103
|
-
# the engine type
|
104
|
-
def engine_type
|
105
|
-
"groq"
|
106
|
-
end
|
107
|
-
|
108
|
-
# Calculate the maximum number of tokens possible to generate for a prompt.
|
109
|
-
# @param prompt_text [String] The prompt text to use.
|
110
|
-
# @return [Integer] the number of tokens possible to generate.
|
111
|
-
def max_tokens_for_prompt(prompt_text)
|
112
|
-
num_tokens = get_num_tokens(prompt_text)
|
113
|
-
|
114
|
-
# get max context size for model by name
|
115
|
-
max_size = 8096
|
116
|
-
max_size - num_tokens
|
117
|
-
end
|
118
169
|
end
|
119
170
|
end
|
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'intelligence'
|
4
|
+
require_relative 'unified_observability'
|
4
5
|
|
5
6
|
module Boxcars
|
6
7
|
# A Base class for all Intelligence Engines
|
7
8
|
class IntelligenceBase < Engine
|
9
|
+
include Boxcars::UnifiedObservability
|
8
10
|
attr_reader :provider, :all_params
|
9
11
|
|
10
12
|
# The base Intelligence Engine is used by other engines to generate output from prompts
|
@@ -16,8 +18,9 @@ module Boxcars
|
|
16
18
|
# @param kwargs [Hash] Additional parameters to pass to the Engine.
|
17
19
|
def initialize(provider:, description:, name:, prompts: [], batch_size: 20, **kwargs)
|
18
20
|
@provider = provider
|
21
|
+
# Start with defaults, merge other kwargs, then explicitly set model if provided in initialize
|
19
22
|
@all_params = default_model_params.merge(kwargs)
|
20
|
-
super(description
|
23
|
+
super(description:, name:, prompts:, batch_size:)
|
21
24
|
end
|
22
25
|
|
23
26
|
# can be overridden by provider subclass
|
@@ -30,9 +33,10 @@ module Boxcars
|
|
30
33
|
end
|
31
34
|
|
32
35
|
def adapter(params:, api_key:)
|
33
|
-
Intelligence::Adapter
|
34
|
-
|
35
|
-
|
36
|
+
Intelligence::Adapter.build! @provider do |config|
|
37
|
+
config.key api_key
|
38
|
+
config.chat_options params
|
39
|
+
end
|
36
40
|
end
|
37
41
|
|
38
42
|
# Process different content types
|
@@ -59,25 +63,56 @@ module Boxcars
|
|
59
63
|
# Get an answer from the engine
|
60
64
|
def client(prompt:, inputs: {}, api_key: nil, **kwargs)
|
61
65
|
params = all_params.merge(kwargs)
|
62
|
-
api_key ||= lookup_provider_api_key(params:
|
66
|
+
api_key ||= lookup_provider_api_key(params:)
|
63
67
|
raise Error, "No API key found for #{provider}" unless api_key
|
64
68
|
|
65
|
-
adapter = adapter(api_key
|
66
|
-
convo = prompt.as_intelligence_conversation(inputs:
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
69
|
+
adapter = adapter(api_key:, params:)
|
70
|
+
convo = prompt.as_intelligence_conversation(inputs:)
|
71
|
+
request_context = { prompt: prompt&.as_prompt(inputs:)&.[](:prompt), inputs:, conversation_for_api: convo.to_h }
|
72
|
+
request = Intelligence::ChatRequest.new(adapter:)
|
73
|
+
|
74
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
75
|
+
response_obj = nil
|
76
|
+
|
77
|
+
begin
|
78
|
+
response_obj = request.chat(convo)
|
79
|
+
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000
|
80
|
+
|
81
|
+
if response_obj.success?
|
82
|
+
success = true
|
83
|
+
parsed_json_response = JSON.parse(response_obj.body)
|
84
|
+
response_data = { success:, parsed_json: parsed_json_response, response_obj:,
|
85
|
+
status_code: response_obj.status }
|
86
|
+
track_ai_generation(duration_ms:, current_params: params, request_context:, response_data:, provider:)
|
87
|
+
parsed_json_response
|
88
|
+
else
|
89
|
+
success = false
|
90
|
+
error_message = response_obj&.reason_phrase || "No response from API #{provider}"
|
91
|
+
response_data = { success:, error: StandardError.new(error_message), response_obj:,
|
92
|
+
status_code: response_obj.status }
|
93
|
+
track_ai_generation(duration_ms:, current_params: params, request_context:, response_data:, provider:)
|
94
|
+
raise Error, error_message
|
95
|
+
end
|
96
|
+
rescue Error => e
|
97
|
+
# Re-raise Error exceptions (like the one above) without additional tracking
|
98
|
+
# since they were already tracked in the else branch
|
99
|
+
Boxcars.error("#{provider} Error: #{e.message}", :red)
|
100
|
+
raise
|
101
|
+
rescue StandardError => e
|
102
|
+
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000
|
103
|
+
success = false
|
104
|
+
error_obj = e
|
105
|
+
response_data = { success:, error: error_obj, response_obj:, status_code: response_obj&.status }
|
106
|
+
track_ai_generation(duration_ms:, current_params: params, request_context:, response_data:, provider:)
|
107
|
+
Boxcars.error("#{provider} Error: #{e.message}", :red)
|
108
|
+
raise
|
109
|
+
end
|
75
110
|
end
|
76
111
|
|
77
112
|
# Run the engine with a question
|
78
|
-
def run(question, **
|
113
|
+
def run(question, **)
|
79
114
|
prompt = Prompt.new(template: question)
|
80
|
-
response = client(prompt
|
115
|
+
response = client(prompt:, **)
|
81
116
|
extract_answer(response)
|
82
117
|
end
|
83
118
|
|