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,173 +1,153 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday/retry'
|
5
|
+
require 'json'
|
6
|
+
|
4
7
|
module Boxcars
|
5
|
-
# A engine that uses
|
8
|
+
# A engine that uses PerplexityAI's API.
|
6
9
|
class Perplexityai < Engine
|
10
|
+
include UnifiedObservability
|
7
11
|
attr_reader :prompts, :perplexity_params, :model_kwargs, :batch_size
|
8
12
|
|
9
|
-
|
10
|
-
|
11
|
-
model: "'llama-3-sonar-large-32k-online'",
|
13
|
+
DEFAULT_PARAMS = { # Renamed from DEFAULT_PER_PARAMS for consistency
|
14
|
+
model: "llama-3-sonar-large-32k-online", # Removed extra quotes
|
12
15
|
temperature: 0.1
|
16
|
+
# max_tokens can be part of kwargs if needed
|
13
17
|
}.freeze
|
18
|
+
DEFAULT_NAME = "PerplexityAI engine" # Renamed from DEFAULT_PER_NAME
|
19
|
+
DEFAULT_DESCRIPTION = "useful for when you need to use Perplexity AI to answer questions. " \
|
20
|
+
"You should ask targeted questions"
|
14
21
|
|
15
|
-
|
16
|
-
|
17
|
-
# the default description of the engine
|
18
|
-
DEFAULT_PER_DESCRIPTION = "useful for when you need to use AI to answer questions. " \
|
19
|
-
"You should ask targeted questions"
|
20
|
-
|
21
|
-
# A engine is a container for a single tool to run.
|
22
|
-
# @param name [String] The name of the engine. Defaults to "PerplexityAI engine".
|
23
|
-
# @param description [String] A description of the engine. Defaults to:
|
24
|
-
# useful for when you need to use AI to answer questions. You should ask targeted questions".
|
25
|
-
# @param prompts [Array<String>] The prompts to use when asking the engine. Defaults to [].
|
26
|
-
# @param batch_size [Integer] The number of prompts to send to the engine at once. Defaults to 20.
|
27
|
-
def initialize(name: DEFAULT_PER_NAME, description: DEFAULT_PER_DESCRIPTION, prompts: [], batch_size: 20, **kwargs)
|
28
|
-
@perplexity_params = DEFAULT_PER_PARAMS.merge(kwargs)
|
22
|
+
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **kwargs)
|
23
|
+
@perplexity_params = DEFAULT_PARAMS.merge(kwargs)
|
29
24
|
@prompts = prompts
|
30
|
-
@batch_size = batch_size
|
31
|
-
super(description
|
25
|
+
@batch_size = batch_size # Retain if used by generate
|
26
|
+
super(description:, name:)
|
32
27
|
end
|
33
28
|
|
34
|
-
|
29
|
+
# Perplexity models are conversational.
|
30
|
+
def conversation_model?(_model_name)
|
35
31
|
true
|
36
32
|
end
|
37
33
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
34
|
+
# Main client method for interacting with the Perplexity API
|
35
|
+
# rubocop:disable Metrics/MethodLength
|
36
|
+
def client(prompt:, inputs: {}, perplexity_api_key: nil, **kwargs)
|
37
|
+
start_time = Time.now
|
38
|
+
response_data = { response_obj: nil, parsed_json: nil, success: false, error: nil, status_code: nil }
|
39
|
+
current_params = @perplexity_params.merge(kwargs)
|
40
|
+
api_request_params = nil # Parameters actually sent to Perplexity API
|
41
|
+
current_prompt_object = prompt.is_a?(Array) ? prompt.first : prompt
|
42
|
+
|
43
|
+
begin
|
44
|
+
api_key = perplexity_api_key || Boxcars.configuration.perplexity_api_key(**current_params.slice(:perplexity_api_key))
|
45
|
+
raise Boxcars::ConfigurationError, "Perplexity API key not set" if api_key.nil? || api_key.strip.empty?
|
46
|
+
|
47
|
+
conn = Faraday.new(url: "https://api.perplexity.ai") do |faraday|
|
48
|
+
faraday.request :json
|
49
|
+
faraday.response :json # Parse JSON response
|
50
|
+
faraday.response :raise_error # Raise exceptions on 4xx/5xx
|
51
|
+
faraday.adapter Faraday.default_adapter
|
52
|
+
end
|
53
|
+
|
54
|
+
messages_for_api = current_prompt_object.as_messages(inputs)[:messages]
|
55
|
+
# Perplexity expects a 'model' and 'messages' structure.
|
56
|
+
# Other params like temperature, max_tokens are top-level.
|
57
|
+
api_request_params = {
|
58
|
+
model: current_params[:model],
|
59
|
+
messages: messages_for_api
|
60
|
+
}.merge(current_params.except(:model, :messages, :perplexity_api_key)) # Add other relevant params
|
61
|
+
|
62
|
+
log_messages_debug(api_request_params[:messages]) if Boxcars.configuration.log_prompts && api_request_params[:messages]
|
63
|
+
|
64
|
+
response = conn.post('/chat/completions') do |req|
|
65
|
+
req.headers['Authorization'] = "Bearer #{api_key}"
|
66
|
+
req.body = api_request_params
|
67
|
+
end
|
68
|
+
|
69
|
+
response_data[:response_obj] = response # Faraday response object
|
70
|
+
response_data[:parsed_json] = response.body # Faraday with :json middleware parses body
|
71
|
+
response_data[:status_code] = response.status
|
72
|
+
|
73
|
+
if response.success? && response.body && response.body["choices"]
|
74
|
+
response_data[:success] = true
|
75
|
+
else
|
76
|
+
response_data[:success] = false
|
77
|
+
err_details = response.body["error"] if response.body.is_a?(Hash)
|
78
|
+
msg = if err_details
|
79
|
+
"#{err_details['type']}: #{err_details['message']}"
|
80
|
+
else
|
81
|
+
"Unknown Perplexity API Error (status: #{response.status})"
|
82
|
+
end
|
83
|
+
response_data[:error] = StandardError.new(msg)
|
84
|
+
end
|
85
|
+
rescue Faraday::Error => e # Catch Faraday specific errors (includes connection, timeout, 4xx/5xx)
|
86
|
+
response_data[:error] = e
|
87
|
+
response_data[:success] = false
|
88
|
+
response_data[:status_code] = e.response_status if e.respond_to?(:response_status)
|
89
|
+
response_data[:response_obj] = e.response if e.respond_to?(:response) # Store Faraday response if available
|
90
|
+
response_data[:parsed_json] = e.response[:body] if e.respond_to?(:response) && e.response[:body].is_a?(Hash)
|
91
|
+
rescue StandardError => e # Catch other unexpected errors
|
92
|
+
response_data[:error] = e
|
93
|
+
response_data[:success] = false
|
94
|
+
ensure
|
95
|
+
duration_ms = ((Time.now - start_time) * 1000).round
|
96
|
+
request_context = {
|
97
|
+
prompt: current_prompt_object,
|
98
|
+
inputs:,
|
99
|
+
conversation_for_api: api_request_params&.dig(:messages)
|
51
100
|
}
|
101
|
+
track_ai_generation(
|
102
|
+
duration_ms:,
|
103
|
+
current_params:,
|
104
|
+
request_context:,
|
105
|
+
response_data:,
|
106
|
+
provider: :perplexity_ai
|
107
|
+
)
|
52
108
|
end
|
53
109
|
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
# Get an answer from the engine.
|
58
|
-
# @param prompt [String] The prompt to use when asking the engine.
|
59
|
-
# @param openai_access_token [String] The access token to use when asking the engine.
|
60
|
-
# Defaults to Boxcars.configuration.openai_access_token.
|
61
|
-
# @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
|
62
|
-
def client(prompt:, inputs: {}, **kwargs)
|
63
|
-
prompt = prompt.first if prompt.is_a?(Array)
|
64
|
-
params = prompt.as_messages(inputs).merge(default_params).merge(kwargs)
|
65
|
-
if Boxcars.configuration.log_prompts
|
66
|
-
Boxcars.debug(params[:messages].last(2).map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
|
67
|
-
end
|
68
|
-
chat(parameters: params)
|
110
|
+
_perplexity_handle_call_outcome(response_data:)
|
69
111
|
end
|
112
|
+
# rubocop:enable Metrics/MethodLength
|
70
113
|
|
71
|
-
|
72
|
-
# @param question [String] The question to ask the engine.
|
73
|
-
# @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
|
74
|
-
def run(question, **kwargs)
|
114
|
+
def run(question, **)
|
75
115
|
prompt = Prompt.new(template: question)
|
76
|
-
|
77
|
-
raise Error, "PerplexityAI: No response from API" unless response
|
78
|
-
raise Error, "PerplexityAI: #{response['error']}" if response["error"]
|
79
|
-
|
80
|
-
answer = response["choices"].map { |c| c.dig("message", "content") || c["text"] }.join("\n").strip
|
116
|
+
answer = client(prompt:, inputs: {}, **)
|
81
117
|
Boxcars.debug("Answer: #{answer}", :cyan)
|
82
118
|
answer
|
83
119
|
end
|
84
120
|
|
85
|
-
# Get the default parameters for the engine.
|
86
121
|
def default_params
|
87
|
-
perplexity_params
|
88
|
-
end
|
89
|
-
|
90
|
-
# make sure we got a valid response
|
91
|
-
# @param response [Hash] The response to check.
|
92
|
-
# @param must_haves [Array<String>] The keys that must be in the response. Defaults to %w[choices].
|
93
|
-
# @raise [KeyError] if there is an issue with the access token.
|
94
|
-
# @raise [ValueError] if the response is not valid.
|
95
|
-
def check_response(response, must_haves: %w[choices])
|
96
|
-
if response['error']
|
97
|
-
code = response.dig('error', 'code')
|
98
|
-
msg = response.dig('error', 'message') || 'unknown error'
|
99
|
-
raise KeyError, "PERPLEXITY_API_KEY not valid" if code == 'invalid_api_key'
|
100
|
-
|
101
|
-
raise ValueError, "PerplexityAI error: #{msg}"
|
102
|
-
end
|
103
|
-
|
104
|
-
must_haves.each do |key|
|
105
|
-
raise ValueError, "Expecting key #{key} in response" unless response.key?(key)
|
106
|
-
end
|
122
|
+
@perplexity_params
|
107
123
|
end
|
108
|
-
end
|
109
|
-
|
110
|
-
# the engine type
|
111
|
-
def engine_type
|
112
|
-
"perplexityai"
|
113
|
-
end
|
114
124
|
|
115
|
-
|
116
|
-
def get_num_tokens(text:)
|
117
|
-
text.split.length # TODO: hook up to token counting gem
|
118
|
-
end
|
125
|
+
private
|
119
126
|
|
120
|
-
|
121
|
-
|
122
|
-
# @return [Integer] the number of tokens possible to generate.
|
123
|
-
def max_tokens_for_prompt(_prompt_text)
|
124
|
-
8096
|
125
|
-
end
|
127
|
+
def log_messages_debug(messages)
|
128
|
+
return unless messages.is_a?(Array)
|
126
129
|
|
127
|
-
|
128
|
-
# @param sub_choices [Array<Hash>] The choices to get generation info for.
|
129
|
-
# @return [Array<Generation>] The generation information.
|
130
|
-
def generation_info(sub_choices)
|
131
|
-
sub_choices.map do |choice|
|
132
|
-
Generation.new(
|
133
|
-
text: choice.dig("message", "content") || choice["text"],
|
134
|
-
generation_info: {
|
135
|
-
finish_reason: choice.fetch("finish_reason", nil),
|
136
|
-
logprobs: choice.fetch("logprobs", nil)
|
137
|
-
}
|
138
|
-
)
|
130
|
+
Boxcars.debug(messages.last(2).map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
|
139
131
|
end
|
140
|
-
end
|
141
132
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
prompts.each_slice(batch_size) do |sub_prompts|
|
156
|
-
sub_prompts.each do |sprompts, inputs|
|
157
|
-
response = client(prompt: sprompts, inputs: inputs, **params)
|
158
|
-
check_response(response)
|
159
|
-
choices.concat(response["choices"])
|
160
|
-
usage_keys = inkeys & response["usage"].keys
|
161
|
-
usage_keys.each { |key| token_usage[key] = token_usage[key].to_i + response["usage"][key] }
|
133
|
+
def _perplexity_handle_call_outcome(response_data:)
|
134
|
+
if response_data[:error]
|
135
|
+
Boxcars.error("PerplexityAI Error: #{response_data[:error].message} (#{response_data[:error].class.name})", :red)
|
136
|
+
raise response_data[:error]
|
137
|
+
elsif !response_data[:success]
|
138
|
+
err_details = response_data.dig(:parsed_json, "error")
|
139
|
+
msg = err_details ? "#{err_details['type']}: #{err_details['message']}" : "Unknown error from PerplexityAI API"
|
140
|
+
raise Error, msg
|
141
|
+
else
|
142
|
+
choices = response_data.dig(:parsed_json, "choices")
|
143
|
+
raise Error, "PerplexityAI: No choices found in response" unless choices.is_a?(Array) && !choices.empty?
|
144
|
+
|
145
|
+
choices.map { |c| c.dig("message", "content") }.join("\n").strip
|
162
146
|
end
|
163
147
|
end
|
164
148
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
sub_choices = choices[i * n, (i + 1) * n]
|
169
|
-
generations.push(generation_info(sub_choices))
|
170
|
-
end
|
171
|
-
EngineResult.new(generations: generations, engine_output: { token_usage: token_usage })
|
149
|
+
# Methods like `check_response`, `generate`, `generation_info` are removed or would need significant rework.
|
150
|
+
# `check_response` logic is now part of `_perplexity_handle_call_outcome`.
|
151
|
+
# `generate` would need to be re-implemented carefully if batching is desired with direct Faraday.
|
172
152
|
end
|
173
153
|
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: :together_ai, description
|
24
|
+
def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **)
|
25
|
+
super(provider: :together_ai, description:, name:, prompts:, batch_size:, **)
|
26
26
|
end
|
27
27
|
|
28
28
|
def default_model_params
|