boxcars 0.7.7 → 0.8.1
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 +17 -0
- data/Gemfile +3 -13
- data/Gemfile.lock +30 -25
- data/POSTHOG_TEST_README.md +118 -0
- data/README.md +305 -0
- data/boxcars.gemspec +2 -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 +115 -88
- data/lib/boxcars/vector_store/pgvector/build_from_files.rb +105 -80
- data/lib/boxcars/vector_store/pgvector/save_to_database.rb +147 -122
- data/lib/boxcars/vector_store/pgvector/search.rb +156 -131
- data/lib/boxcars/vector_store.rb +4 -4
- data/lib/boxcars/version.rb +1 -1
- data/lib/boxcars.rb +31 -20
- metadata +25 -21
data/boxcars.gemspec
CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.description = "You simply set an OpenAI key, give a number of Boxcars to a Train, and magic ensues when you run it."
|
13
13
|
spec.homepage = "https://github.com/BoxcarsAI/boxcars"
|
14
14
|
spec.license = "MIT"
|
15
|
-
spec.required_ruby_version = ">= 3.0"
|
15
|
+
spec.required_ruby_version = ">= 3.2.0"
|
16
16
|
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
18
18
|
spec.metadata["source_code_uri"] = spec.homepage
|
@@ -31,12 +31,12 @@ Gem::Specification.new do |spec|
|
|
31
31
|
spec.require_paths = ["lib"]
|
32
32
|
|
33
33
|
# runtime dependencies
|
34
|
+
spec.add_dependency "faraday-retry", "~> 2.0"
|
34
35
|
spec.add_dependency "google_search_results", "~> 2.2"
|
35
36
|
spec.add_dependency "gpt4all", "~> 0.0.5"
|
36
37
|
spec.add_dependency "hnswlib", "~> 0.9"
|
37
38
|
spec.add_dependency "intelligence", ">= 0.8"
|
38
39
|
spec.add_dependency "nokogiri", "~> 1.18"
|
39
|
-
spec.add_dependency "pgvector", "~> 0.2"
|
40
40
|
spec.add_dependency "ruby-anthropic", "~> 0.4"
|
41
41
|
spec.add_dependency "ruby-openai", ">= 7.3"
|
42
42
|
|
@@ -23,14 +23,14 @@ module Boxcars
|
|
23
23
|
@read_only = read_only.nil? ? !approval_callback : read_only
|
24
24
|
@code_only = kwargs.delete(:code_only) || false
|
25
25
|
kwargs[:name] ||= get_name
|
26
|
-
kwargs[:description] ||= format(ARDESC, name:
|
26
|
+
kwargs[:description] ||= format(ARDESC, name:)
|
27
27
|
kwargs[:prompt] ||= my_prompt
|
28
28
|
super(**kwargs)
|
29
29
|
end
|
30
30
|
|
31
31
|
# @return Hash The additional variables for this boxcar.
|
32
32
|
def prediction_additional(_inputs)
|
33
|
-
{ model_info:
|
33
|
+
{ model_info: }.merge super
|
34
34
|
end
|
35
35
|
|
36
36
|
CTEMPLATE = [
|
@@ -150,16 +150,15 @@ module Boxcars
|
|
150
150
|
# run the code in a safe environment
|
151
151
|
# @param code [String] The code to run
|
152
152
|
# @return [Object] The result of the code
|
153
|
-
def eval_safe_wrapper(code)
|
153
|
+
def eval_safe_wrapper(code, binding = TOPLEVEL_BINDING)
|
154
154
|
# if the code used ActiveRecord, we need to add :: in front of it to escape the module
|
155
155
|
new_code = code.gsub(/\b(ActiveRecord::)/, '::\1')
|
156
|
-
|
157
156
|
# sometimes the code will have a puts or print in it, which will miss. Remove them.
|
158
157
|
new_code = new_code.gsub(/\b(puts|print)\b/, '')
|
158
|
+
|
159
159
|
proc do
|
160
|
-
$SAFE = 4
|
161
160
|
# rubocop:disable Security/Eval
|
162
|
-
eval
|
161
|
+
eval(new_code, binding)
|
163
162
|
# rubocop:enable Security/Eval
|
164
163
|
end.call
|
165
164
|
end
|
@@ -231,24 +230,24 @@ module Boxcars
|
|
231
230
|
def get_active_record_answer(text)
|
232
231
|
changes_code = extract_code text.split('ARCode:').first.split('ARChanges:').last.strip if text =~ /^ARChanges:/
|
233
232
|
code = extract_code text.split('ARCode:').last.strip
|
234
|
-
return Result.new(status: :ok, explanation: "code to run", code
|
233
|
+
return Result.new(status: :ok, explanation: "code to run", code:, changes_code:) if code_only?
|
235
234
|
|
236
235
|
have_approval = false
|
237
236
|
begin
|
238
237
|
have_approval = approved?(changes_code, code)
|
239
238
|
rescue NameError, Error => e
|
240
|
-
return Result.new(status: :error, explanation: error_message(e, "ARChanges"), changes_code:
|
239
|
+
return Result.new(status: :error, explanation: error_message(e, "ARChanges"), changes_code:)
|
241
240
|
end
|
242
241
|
|
243
242
|
raise SecurityError, "Permission to run code that makes changes denied" unless have_approval
|
244
243
|
|
245
244
|
begin
|
246
245
|
output = clean_up_output(run_active_record_code(code))
|
247
|
-
Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code:
|
246
|
+
Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code:)
|
248
247
|
rescue SecurityError => e
|
249
248
|
raise e
|
250
249
|
rescue ::StandardError => e
|
251
|
-
Result.new(status: :error, answer: nil, explanation: error_message(e, "ARCode"), code:
|
250
|
+
Result.new(status: :error, answer: nil, explanation: error_message(e, "ARCode"), code:)
|
252
251
|
end
|
253
252
|
end
|
254
253
|
|
@@ -15,7 +15,7 @@ module Boxcars
|
|
15
15
|
kwargs[:stop] ||= ["```output"]
|
16
16
|
kwargs[:name] ||= "Calculator"
|
17
17
|
kwargs[:description] ||= CALCDESC
|
18
|
-
super(engine
|
18
|
+
super(engine:, prompt: the_prompt, **kwargs)
|
19
19
|
end
|
20
20
|
|
21
21
|
# our template
|
@@ -47,7 +47,7 @@ module Boxcars
|
|
47
47
|
code = text.split("```ruby\n").last.split("```").first.strip
|
48
48
|
# code = text[8..-4].split("```").first.strip
|
49
49
|
ruby_executor = Boxcars::RubyREPL.new
|
50
|
-
ruby_executor.call(code:
|
50
|
+
ruby_executor.call(code:)
|
51
51
|
end
|
52
52
|
|
53
53
|
def get_answer(text)
|
@@ -46,7 +46,7 @@ module Boxcars
|
|
46
46
|
stop = input_list[0][:stop]
|
47
47
|
the_prompt = current_conversation ? prompt.with_conversation(current_conversation) : prompt
|
48
48
|
prompts = input_list.map { |inputs| [the_prompt, inputs] }
|
49
|
-
engine.generate(prompts
|
49
|
+
engine.generate(prompts:, stop:)
|
50
50
|
end
|
51
51
|
|
52
52
|
# apply a response from the engine
|
@@ -54,7 +54,7 @@ module Boxcars
|
|
54
54
|
# @param current_conversation [Boxcars::Conversation] Optional ongoing conversation to use for the prompt.
|
55
55
|
# @return [Hash] A hash of the output key and the output value.
|
56
56
|
def apply(input_list:, current_conversation: nil)
|
57
|
-
response = generate(input_list
|
57
|
+
response = generate(input_list:, current_conversation:)
|
58
58
|
response.generations.to_h do |generation|
|
59
59
|
[output_key, generation[0].text]
|
60
60
|
end
|
@@ -65,7 +65,7 @@ module Boxcars
|
|
65
65
|
# @param kwargs [Hash] A hash of input values to use for the prompt.
|
66
66
|
# @return [String] The output value.
|
67
67
|
def predict(current_conversation: nil, **kwargs)
|
68
|
-
prediction = apply(current_conversation
|
68
|
+
prediction = apply(current_conversation:, input_list: [kwargs])[output_key]
|
69
69
|
Boxcars.debug(prediction, :white) if Boxcars.configuration.log_generated
|
70
70
|
prediction
|
71
71
|
end
|
@@ -115,7 +115,7 @@ module Boxcars
|
|
115
115
|
|
116
116
|
# @return Hash The additional variables for this boxcar.
|
117
117
|
def prediction_additional(_inputs)
|
118
|
-
{ stop
|
118
|
+
{ stop:, top_k: }
|
119
119
|
end
|
120
120
|
|
121
121
|
# @param inputs [Hash] The inputs to the boxcar.
|
@@ -14,8 +14,8 @@ module Boxcars
|
|
14
14
|
# @param description [String] A description of the boxcar. Defaults to SERPDESC.
|
15
15
|
# @param serpapi_api_key [String] The API key to use for the SerpAPI. Defaults to Boxcars.configuration.serpapi_api_key.
|
16
16
|
def initialize(name: "Search", description: SERPDESC, serpapi_api_key: nil)
|
17
|
-
super(name
|
18
|
-
api_key = Boxcars.configuration.serpapi_api_key(serpapi_api_key:
|
17
|
+
super(name:, description:)
|
18
|
+
api_key = Boxcars.configuration.serpapi_api_key(serpapi_api_key:)
|
19
19
|
::GoogleSearch.api_key = api_key
|
20
20
|
end
|
21
21
|
|
@@ -37,7 +37,7 @@ module Boxcars
|
|
37
37
|
SYSPR
|
38
38
|
stock_prompt += "\n\nImportant:\n#{important}\n" unless important.to_s.empty?
|
39
39
|
|
40
|
-
sprompt = format(stock_prompt, wanted_data
|
40
|
+
sprompt = format(stock_prompt, wanted_data:, data_description:)
|
41
41
|
ctemplate = [
|
42
42
|
Boxcar.syst(sprompt),
|
43
43
|
Boxcar.user("%<input>s")
|
@@ -20,7 +20,7 @@ module Boxcars
|
|
20
20
|
@connection = connection
|
21
21
|
check_tables(tables, except_tables)
|
22
22
|
kwargs[:name] ||= "Database"
|
23
|
-
kwargs[:description] ||= format(SQLDESC, name:
|
23
|
+
kwargs[:description] ||= format(SQLDESC, name:)
|
24
24
|
kwargs[:prompt] ||= my_prompt
|
25
25
|
kwargs[:stop] ||= ["SQLResult:"]
|
26
26
|
|
@@ -29,7 +29,7 @@ module Boxcars
|
|
29
29
|
|
30
30
|
# @return Hash The additional variables for this boxcar.
|
31
31
|
def prediction_additional(_inputs)
|
32
|
-
{ schema
|
32
|
+
{ schema:, dialect: }.merge super
|
33
33
|
end
|
34
34
|
|
35
35
|
CTEMPLATE = [
|
@@ -107,9 +107,9 @@ module Boxcars
|
|
107
107
|
code = extract_code text.split('SQLQuery:').last.strip
|
108
108
|
Boxcars.debug code, :yellow
|
109
109
|
output = clean_up_output(code)
|
110
|
-
Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code:
|
110
|
+
Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code:)
|
111
111
|
rescue StandardError => e
|
112
|
-
Result.new(status: :error, answer: nil, explanation: "Error: #{e.message}", code:
|
112
|
+
Result.new(status: :error, answer: nil, explanation: "Error: #{e.message}", code:)
|
113
113
|
end
|
114
114
|
|
115
115
|
def get_answer(text)
|
@@ -21,12 +21,12 @@ module Boxcars
|
|
21
21
|
kwargs[:stop] ||= ["```output"]
|
22
22
|
kwargs[:name] ||= "Swagger API"
|
23
23
|
kwargs[:description] ||= DESC
|
24
|
-
super(engine
|
24
|
+
super(engine:, prompt: the_prompt, **kwargs)
|
25
25
|
end
|
26
26
|
|
27
27
|
# @return Hash The additional variables for this boxcar.
|
28
28
|
def prediction_additional(_inputs)
|
29
|
-
{ swagger_url
|
29
|
+
{ swagger_url:, context: }.merge super
|
30
30
|
end
|
31
31
|
|
32
32
|
# our template
|
@@ -52,7 +52,7 @@ module Boxcars
|
|
52
52
|
def get_embedded_ruby_answer(text)
|
53
53
|
code = text.split("```ruby\n").last.split("```").first.strip
|
54
54
|
ruby_executor = Boxcars::RubyREPL.new
|
55
|
-
ruby_executor.call(code:
|
55
|
+
ruby_executor.call(code:)
|
56
56
|
end
|
57
57
|
|
58
58
|
def get_answer(text)
|
@@ -21,7 +21,7 @@ module Boxcars
|
|
21
21
|
kwargs[:stop] ||= ["```output"]
|
22
22
|
kwargs[:name] ||= "VectorAnswer"
|
23
23
|
kwargs[:description] ||= DESC
|
24
|
-
super(engine
|
24
|
+
super(engine:, prompt: the_prompt, **kwargs)
|
25
25
|
end
|
26
26
|
|
27
27
|
# @param inputs [Hash] The inputs to use for the prediction.
|
@@ -53,8 +53,8 @@ module Boxcars
|
|
53
53
|
# @params count [Integer] The number of results to return.
|
54
54
|
# @return [String] The content of the search results.
|
55
55
|
def get_search_content(question, count: 1)
|
56
|
-
search = Boxcars::VectorSearch.new(embeddings
|
57
|
-
results = search.call
|
56
|
+
search = Boxcars::VectorSearch.new(embeddings:, vector_documents:)
|
57
|
+
results = search.call(query: question, count:)
|
58
58
|
@search_content = get_results_content(results)
|
59
59
|
end
|
60
60
|
|
@@ -21,7 +21,7 @@ module Boxcars
|
|
21
21
|
def xn_get_answer(xnode)
|
22
22
|
reply = xnode.xtext("//reply")
|
23
23
|
|
24
|
-
if reply.
|
24
|
+
if reply && !reply.to_s.strip.empty?
|
25
25
|
Result.new(status: :ok, answer: reply, explanation: reply)
|
26
26
|
else
|
27
27
|
# we have an unexpected output from the engine
|
data/lib/boxcars/boxcar.rb
CHANGED
@@ -63,8 +63,8 @@ module Boxcars
|
|
63
63
|
# @param kwargs [Hash] The keyword arguments to pass to the boxcar.
|
64
64
|
# you can pass one or the other, but not both.
|
65
65
|
# @return [String] The answer to the question.
|
66
|
-
def run(
|
67
|
-
rv = conduct(
|
66
|
+
def run(*, **)
|
67
|
+
rv = conduct(*, **)
|
68
68
|
rv = rv[:answer] if rv.is_a?(Hash) && rv.key?(:answer)
|
69
69
|
return rv.answer if rv.is_a?(Result)
|
70
70
|
return rv[output_keys[0]] if rv.is_a?(Hash)
|
@@ -77,9 +77,9 @@ module Boxcars
|
|
77
77
|
# @param kwargs [Hash] The keyword arguments to pass to the boxcar.
|
78
78
|
# you can pass one or the other, but not both.
|
79
79
|
# @return [Boxcars::Result] The answer to the question.
|
80
|
-
def conduct(
|
80
|
+
def conduct(*, **)
|
81
81
|
Boxcars.info "> Entering #{name}#run", :gray, style: :bold
|
82
|
-
rv = depart(
|
82
|
+
rv = depart(*, **)
|
83
83
|
remember_history(rv)
|
84
84
|
Boxcars.info "< Exiting #{name}#run", :gray, style: :bold
|
85
85
|
rv
|
@@ -163,7 +163,7 @@ module Boxcars
|
|
163
163
|
inputs = our_inputs(inputs)
|
164
164
|
output = nil
|
165
165
|
begin
|
166
|
-
output = call(inputs:
|
166
|
+
output = call(inputs:)
|
167
167
|
rescue StandardError => e
|
168
168
|
Boxcars.error "Error in #{name} boxcar#call: #{e}\nbt:#{e.backtrace[0..5].join("\n ")}", :red
|
169
169
|
Boxcars.error("Response Body: #{e.response[:body]}", :red) if e.respond_to?(:response) && !e.response.nil?
|
@@ -199,7 +199,7 @@ module Boxcars
|
|
199
199
|
end
|
200
200
|
inputs = { input_keys.first => inputs }
|
201
201
|
end
|
202
|
-
validate_inputs(inputs:
|
202
|
+
validate_inputs(inputs:)
|
203
203
|
end
|
204
204
|
|
205
205
|
# the default answer is the text passed in
|
@@ -11,7 +11,7 @@ module Boxcars
|
|
11
11
|
# @param output_variables [Array<Symbol>] The output vars to use for the prompt. Defaults to [:output]
|
12
12
|
def initialize(conversation:, input_variables: nil, other_inputs: nil, output_variables: nil)
|
13
13
|
@conversation = conversation
|
14
|
-
super(template
|
14
|
+
super(template:, input_variables:, other_inputs:, output_variables:)
|
15
15
|
end
|
16
16
|
|
17
17
|
# prompt for chatGPT params
|
@@ -25,7 +25,7 @@ module Boxcars
|
|
25
25
|
# @param inputs [Hash] The inputs to use for the prompt.
|
26
26
|
# @return [Hash] The formatted prompt.
|
27
27
|
def as_prompt(inputs:, prefixes: default_prefixes, show_roles: false)
|
28
|
-
{ prompt: conversation.as_prompt(inputs
|
28
|
+
{ prompt: conversation.as_prompt(inputs:, prefixes:, show_roles:) }
|
29
29
|
end
|
30
30
|
|
31
31
|
# tack on the ongoing conversation if present to the prompt
|
@@ -56,7 +56,7 @@ module Boxcars
|
|
56
56
|
# @param inputs [Hash] The inputs to use for the prompt
|
57
57
|
# @return [Intelligence::Conversation] The converted conversation
|
58
58
|
def as_intelligence_conversation(inputs: nil)
|
59
|
-
conversation.as_intelligence_conversation(inputs:
|
59
|
+
conversation.as_intelligence_conversation(inputs:)
|
60
60
|
end
|
61
61
|
end
|
62
62
|
end
|
@@ -4,7 +4,9 @@ require 'anthropic'
|
|
4
4
|
# Boxcars is a framework for running a series of tools to get an answer to a question.
|
5
5
|
module Boxcars
|
6
6
|
# A engine that uses OpenAI's API.
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
7
8
|
class Anthropic < Engine
|
9
|
+
include UnifiedObservability
|
8
10
|
attr_reader :prompts, :llm_params, :model_kwargs, :batch_size
|
9
11
|
|
10
12
|
# The default parameters to use when asking the engine.
|
@@ -29,7 +31,7 @@ module Boxcars
|
|
29
31
|
@llm_params = DEFAULT_PARAMS.merge(kwargs)
|
30
32
|
@prompts = prompts
|
31
33
|
@batch_size = 20
|
32
|
-
super(description
|
34
|
+
super(description:, name:)
|
33
35
|
end
|
34
36
|
|
35
37
|
def conversation_model?(_model)
|
@@ -46,33 +48,50 @@ module Boxcars
|
|
46
48
|
# Defaults to Boxcars.configuration.anthropic_api_key.
|
47
49
|
# @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
|
48
50
|
def client(prompt:, inputs: {}, **kwargs)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
51
|
+
start_time = Time.now
|
52
|
+
response_data = { response_obj: nil, parsed_json: nil, success: false, error: nil, status_code: nil }
|
53
|
+
current_params = llm_params.merge(kwargs)
|
54
|
+
current_prompt_object = prompt.is_a?(Array) ? prompt.first : prompt
|
55
|
+
api_request_params = nil
|
56
|
+
|
57
|
+
begin
|
58
|
+
api_key = Boxcars.configuration.anthropic_api_key(**kwargs)
|
59
|
+
aclient = anthropic_client(anthropic_api_key: api_key)
|
60
|
+
api_request_params = convert_to_anthropic(current_prompt_object.as_messages(inputs).merge(current_params))
|
61
|
+
|
62
|
+
if Boxcars.configuration.log_prompts
|
63
|
+
if api_request_params[:messages].length < 2 && api_request_params[:system] && !api_request_params[:system].empty?
|
64
|
+
Boxcars.debug(">>>>>> Role: system <<<<<<\n#{api_request_params[:system]}")
|
65
|
+
end
|
66
|
+
Boxcars.debug(api_request_params[:messages].last(2).map do |p|
|
67
|
+
">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}"
|
68
|
+
end.join("\n"), :cyan)
|
57
69
|
end
|
58
|
-
|
70
|
+
|
71
|
+
raw_response = aclient.messages(parameters: api_request_params)
|
72
|
+
_process_anthropic_response(raw_response, response_data)
|
73
|
+
rescue StandardError => e
|
74
|
+
_handle_anthropic_error(e, response_data)
|
75
|
+
ensure
|
76
|
+
call_context = {
|
77
|
+
start_time:,
|
78
|
+
prompt_object: current_prompt_object,
|
79
|
+
inputs:,
|
80
|
+
api_request_params:,
|
81
|
+
current_params:
|
82
|
+
}
|
83
|
+
_track_anthropic_observability(call_context, response_data)
|
59
84
|
end
|
60
|
-
|
61
|
-
|
62
|
-
response.delete('content')
|
63
|
-
response
|
64
|
-
rescue StandardError => e
|
65
|
-
err = e.respond_to?(:response) ? e.response[:body] : e
|
66
|
-
Boxcars.warn("Anthropic Error #{e.class.name}: #{err}", :red)
|
67
|
-
raise
|
85
|
+
|
86
|
+
_anthropic_handle_call_outcome(response_data:)
|
68
87
|
end
|
69
88
|
|
70
89
|
# get an answer from the engine for a question.
|
71
90
|
# @param question [String] The question to ask the engine.
|
72
91
|
# @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
|
73
|
-
def run(question, **
|
92
|
+
def run(question, **)
|
74
93
|
prompt = Prompt.new(template: question)
|
75
|
-
response = client(prompt
|
94
|
+
response = client(prompt:, **)
|
76
95
|
|
77
96
|
raise Error, "Anthropic: No response from API" unless response
|
78
97
|
raise Error, "Anthropic: #{response['error']}" if response['error']
|
@@ -134,7 +153,7 @@ module Boxcars
|
|
134
153
|
# Includes prompt, completion, and total tokens used.
|
135
154
|
prompts.each_slice(batch_size) do |sub_prompts|
|
136
155
|
sub_prompts.each do |sprompts, inputs|
|
137
|
-
response = client(prompt: sprompts, inputs
|
156
|
+
response = client(prompt: sprompts, inputs:, **params)
|
138
157
|
check_response(response)
|
139
158
|
choices << response
|
140
159
|
end
|
@@ -146,7 +165,7 @@ module Boxcars
|
|
146
165
|
sub_choices = choices[i * n, (i + 1) * n]
|
147
166
|
generations.push(generation_info(sub_choices))
|
148
167
|
end
|
149
|
-
EngineResult.new(generations
|
168
|
+
EngineResult.new(generations:, engine_output: { token_usage: {} })
|
150
169
|
end
|
151
170
|
# rubocop:enable Metrics/AbcSize
|
152
171
|
|
@@ -191,12 +210,14 @@ module Boxcars
|
|
191
210
|
end
|
192
211
|
|
193
212
|
# convert generic parameters to Anthopic specific ones
|
213
|
+
# rubocop:disable Metrics/AbcSize
|
194
214
|
def convert_to_anthropic(params)
|
195
215
|
params[:stop_sequences] = params.delete(:stop) if params.key?(:stop)
|
196
216
|
params[:system] = params[:messages].shift[:content] if params.dig(:messages, 0, :role) == :system
|
197
|
-
params[:messages].pop if params[:messages].last[:content].
|
217
|
+
params[:messages].pop if params[:messages].last[:content].nil? || params[:messages].last[:content].strip.empty?
|
198
218
|
combine_assistant(params)
|
199
219
|
end
|
220
|
+
# rubocop:enable Metrics/AbcSize
|
200
221
|
|
201
222
|
def combine_assistant(params)
|
202
223
|
params[:messages] = combine_assistant_entries(params[:messages])
|
@@ -220,5 +241,82 @@ module Boxcars
|
|
220
241
|
def default_prefixes
|
221
242
|
{ system: "Human: ", user: "Human: ", assistant: "Assistant: ", history: :history }
|
222
243
|
end
|
244
|
+
|
245
|
+
private
|
246
|
+
|
247
|
+
# Process the raw response from Anthropic API
|
248
|
+
# rubocop:disable Metrics/AbcSize
|
249
|
+
def _process_anthropic_response(raw_response, response_data)
|
250
|
+
response_data[:response_obj] = raw_response
|
251
|
+
response_data[:parsed_json] = raw_response # Already parsed by Anthropic gem
|
252
|
+
|
253
|
+
if raw_response && !raw_response["error"]
|
254
|
+
response_data[:success] = true
|
255
|
+
response_data[:status_code] = 200 # Inferred
|
256
|
+
# Transform response to match expected format
|
257
|
+
raw_response['completion'] = raw_response.dig('content', 0, 'text')
|
258
|
+
raw_response.delete('content')
|
259
|
+
else
|
260
|
+
response_data[:success] = false
|
261
|
+
err_details = raw_response["error"] if raw_response
|
262
|
+
msg = err_details ? "#{err_details['type']}: #{err_details['message']}" : "Unknown Anthropic API Error"
|
263
|
+
response_data[:error] ||= StandardError.new(msg)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
# rubocop:enable Metrics/AbcSize
|
267
|
+
|
268
|
+
# Handle errors from Anthropic API calls
|
269
|
+
def _handle_anthropic_error(error, response_data)
|
270
|
+
response_data[:error] = error
|
271
|
+
response_data[:success] = false
|
272
|
+
response_data[:status_code] = error.respond_to?(:http_status) ? error.http_status : nil
|
273
|
+
end
|
274
|
+
|
275
|
+
# Track observability using the unified system
|
276
|
+
def _track_anthropic_observability(call_context, response_data)
|
277
|
+
duration_ms = ((Time.now - call_context[:start_time]) * 1000).round
|
278
|
+
request_context = {
|
279
|
+
prompt: call_context[:prompt_object],
|
280
|
+
inputs: call_context[:inputs],
|
281
|
+
conversation_for_api: call_context[:api_request_params]
|
282
|
+
}
|
283
|
+
|
284
|
+
track_ai_generation(
|
285
|
+
duration_ms:,
|
286
|
+
current_params: call_context[:current_params],
|
287
|
+
request_context:,
|
288
|
+
response_data:,
|
289
|
+
provider: :anthropic
|
290
|
+
)
|
291
|
+
end
|
292
|
+
|
293
|
+
# Handle the final outcome of the API call
|
294
|
+
def _anthropic_handle_call_outcome(response_data:)
|
295
|
+
if response_data[:error]
|
296
|
+
_handle_anthropic_error_outcome(response_data[:error])
|
297
|
+
elsif !response_data[:success]
|
298
|
+
_handle_anthropic_response_body_error(response_data[:response_obj])
|
299
|
+
else
|
300
|
+
response_data[:parsed_json] # Return the raw parsed JSON
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# Handle error outcomes
|
305
|
+
def _handle_anthropic_error_outcome(error_data)
|
306
|
+
detailed_error_message = error_data.message
|
307
|
+
if error_data.respond_to?(:response) && error_data.response
|
308
|
+
detailed_error_message += " - Details: #{error_data.response[:body]}"
|
309
|
+
end
|
310
|
+
Boxcars.error("Anthropic Error: #{detailed_error_message} (#{error_data.class.name})", :red)
|
311
|
+
raise error_data
|
312
|
+
end
|
313
|
+
|
314
|
+
# Handle response body errors
|
315
|
+
def _handle_anthropic_response_body_error(response_obj)
|
316
|
+
err_details = response_obj&.dig("error")
|
317
|
+
msg = err_details ? "#{err_details['type']}: #{err_details['message']}" : "Unknown error from Anthropic API"
|
318
|
+
raise Error, msg
|
319
|
+
end
|
223
320
|
end
|
321
|
+
# rubocop:enable Metrics/ClassLength
|
224
322
|
end
|
@@ -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: :cerebras, description
|
24
|
+
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **)
|
25
|
+
super(provider: :cerebras, description:, name:, prompts:, batch_size:, **)
|
26
26
|
end
|
27
27
|
|
28
28
|
def default_model_params
|