boxcars 0.7.7 → 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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -3
  3. data/.ruby-version +1 -1
  4. data/Gemfile +3 -13
  5. data/Gemfile.lock +29 -25
  6. data/POSTHOG_TEST_README.md +118 -0
  7. data/README.md +305 -0
  8. data/boxcars.gemspec +1 -2
  9. data/lib/boxcars/boxcar/active_record.rb +9 -10
  10. data/lib/boxcars/boxcar/calculator.rb +2 -2
  11. data/lib/boxcars/boxcar/engine_boxcar.rb +4 -4
  12. data/lib/boxcars/boxcar/google_search.rb +2 -2
  13. data/lib/boxcars/boxcar/json_engine_boxcar.rb +1 -1
  14. data/lib/boxcars/boxcar/ruby_calculator.rb +1 -1
  15. data/lib/boxcars/boxcar/sql_base.rb +4 -4
  16. data/lib/boxcars/boxcar/swagger.rb +3 -3
  17. data/lib/boxcars/boxcar/vector_answer.rb +3 -3
  18. data/lib/boxcars/boxcar/xml_engine_boxcar.rb +1 -1
  19. data/lib/boxcars/boxcar.rb +6 -6
  20. data/lib/boxcars/conversation_prompt.rb +3 -3
  21. data/lib/boxcars/engine/anthropic.rb +121 -23
  22. data/lib/boxcars/engine/cerebras.rb +2 -2
  23. data/lib/boxcars/engine/cohere.rb +135 -9
  24. data/lib/boxcars/engine/gemini_ai.rb +151 -76
  25. data/lib/boxcars/engine/google.rb +2 -2
  26. data/lib/boxcars/engine/gpt4all_eng.rb +92 -34
  27. data/lib/boxcars/engine/groq.rb +124 -73
  28. data/lib/boxcars/engine/intelligence_base.rb +52 -17
  29. data/lib/boxcars/engine/ollama.rb +127 -47
  30. data/lib/boxcars/engine/openai.rb +186 -103
  31. data/lib/boxcars/engine/perplexityai.rb +116 -136
  32. data/lib/boxcars/engine/together.rb +2 -2
  33. data/lib/boxcars/engine/unified_observability.rb +430 -0
  34. data/lib/boxcars/engine.rb +4 -3
  35. data/lib/boxcars/engines.rb +74 -0
  36. data/lib/boxcars/observability.rb +44 -0
  37. data/lib/boxcars/observability_backend.rb +17 -0
  38. data/lib/boxcars/observability_backends/multi_backend.rb +42 -0
  39. data/lib/boxcars/observability_backends/posthog_backend.rb +89 -0
  40. data/lib/boxcars/observation.rb +8 -8
  41. data/lib/boxcars/prompt.rb +16 -4
  42. data/lib/boxcars/result.rb +7 -12
  43. data/lib/boxcars/ruby_repl.rb +1 -1
  44. data/lib/boxcars/train/train_action.rb +1 -1
  45. data/lib/boxcars/train/xml_train.rb +3 -3
  46. data/lib/boxcars/train/xml_zero_shot.rb +1 -1
  47. data/lib/boxcars/train/zero_shot.rb +3 -3
  48. data/lib/boxcars/train.rb +1 -1
  49. data/lib/boxcars/vector_search.rb +5 -5
  50. data/lib/boxcars/vector_store/pgvector/build_from_array.rb +116 -88
  51. data/lib/boxcars/vector_store/pgvector/build_from_files.rb +106 -80
  52. data/lib/boxcars/vector_store/pgvector/save_to_database.rb +148 -122
  53. data/lib/boxcars/vector_store/pgvector/search.rb +157 -131
  54. data/lib/boxcars/vector_store.rb +4 -4
  55. data/lib/boxcars/version.rb +1 -1
  56. data/lib/boxcars.rb +31 -20
  57. metadata +11 -21
@@ -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: 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: model_info }.merge super
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 new_code
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: code, changes_code: changes_code) if code_only?
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: 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: 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: 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: engine, prompt: the_prompt, **kwargs)
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: 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: prompts, stop: stop)
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: input_list, current_conversation: current_conversation)
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: current_conversation, input_list: [kwargs])[output_key]
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: stop, top_k: top_k }
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: name, description: description)
18
- api_key = Boxcars.configuration.serpapi_api_key(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: wanted_data, data_description: data_description)
40
+ sprompt = format(stock_prompt, wanted_data:, data_description:)
41
41
  ctemplate = [
42
42
  Boxcar.syst(sprompt),
43
43
  Boxcar.user("%<input>s")
@@ -30,7 +30,7 @@ module Boxcars
30
30
  def run(question)
31
31
  code = "puts(#{question})"
32
32
  ruby_executor = Boxcars::RubyREPL.new
33
- rv = ruby_executor.call(code: code)
33
+ rv = ruby_executor.call(code:)
34
34
  puts "Question: #{question}"
35
35
  puts "Answer: #{rv}"
36
36
  rv
@@ -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: 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: schema, dialect: dialect }.merge super
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: 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: 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: engine, prompt: the_prompt, **kwargs)
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: swagger_url, context: context }.merge super
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: 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: engine, prompt: the_prompt, **kwargs)
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: embeddings, vector_documents: vector_documents)
57
- results = search.call query: question, count: count
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.present?
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
@@ -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(*args, **kwargs)
67
- rv = conduct(*args, **kwargs)
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(*args, **kwargs)
80
+ def conduct(*, **)
81
81
  Boxcars.info "> Entering #{name}#run", :gray, style: :bold
82
- rv = depart(*args, **kwargs)
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: 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: 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: template, input_variables: input_variables, other_inputs: other_inputs, output_variables: output_variables)
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: inputs, prefixes: prefixes, show_roles: show_roles) }
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: 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: description, name: name)
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
- model_params = llm_params.merge(kwargs)
50
- api_key = Boxcars.configuration.anthropic_api_key(**kwargs)
51
- aclient = anthropic_client(anthropic_api_key: api_key)
52
- prompt = prompt.first if prompt.is_a?(Array)
53
- params = convert_to_anthropic(prompt.as_messages(inputs).merge(model_params))
54
- if Boxcars.configuration.log_prompts
55
- if params[:messages].length < 2 && params[:system].present?
56
- Boxcars.debug(">>>>>> Role: system <<<<<<\n#{params[:system]}")
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
- Boxcars.debug(params[:messages].last(2).map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
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
- response = aclient.messages(parameters: params)
61
- response['completion'] = response.dig('content', 0, 'text')
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, **kwargs)
92
+ def run(question, **)
74
93
  prompt = Prompt.new(template: question)
75
- response = client(prompt: prompt, **kwargs)
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: inputs, **params)
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: generations, engine_output: { token_usage: {} })
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].blank?
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, **kwargs)
25
- super(provider: :cerebras, description: description, name: name, prompts: prompts, batch_size: batch_size, **kwargs)
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