boxcars 0.6.3 → 0.6.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c7137b49fcea80018efc8f6dc19361fa02b74a575f7e71218784986b366c350
4
- data.tar.gz: 9b2bba4216ab4f50024d158cc7b18977569fa7b9bada1a4734fb24f7b9e4a3be
3
+ metadata.gz: 164d55b95e134a691b4525f778a83f6d06cdaa1948028b87371cb6cb8a16fd43
4
+ data.tar.gz: 2707ac063bbc1127d75398be8770812ae258447ba021c0b9527cd3fbf113ff02
5
5
  SHA512:
6
- metadata.gz: 18b58fc68b3837e2f8c7ca1e77e1daf49655843903d39a355233ff1d8c7a68caa885530ba08bcfc3842189b070c547ce152e13ee1a1b5cdb4cdc0424fe8e8ddc
7
- data.tar.gz: e6240effad988b8cb819070ea37f54eab03dd99b32eff0caa07aa167e0f854492c12843d781e9614f429ffd3a311f426bc58c9569af22f7424bd9b65f60244fa
6
+ metadata.gz: 8b0d7909ef7c049d7c393b646abd072c01dd4b295a7af909bbe2f85fe48b8425c946f956440548fa3902182c636d3e57e81279332d331a9d7c4df4181cd4e4d5
7
+ data.tar.gz: ed1c903fb5575ae563ccab8adb0e80cfed9c00e529a5850fc7b89acaf165eecaf42e69cdd548cdec3622855645b5bf88591b3d6a6cb1af47bfbe97d5d6d16ad9
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.3.3
1
+ 3.3.5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.6.4](https://github.com/BoxcarsAI/boxcars/tree/v0.6.4) (2024-07-27)
4
+
5
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.6.3...v0.6.4)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - Add Ollama Engine [\#200](https://github.com/BoxcarsAI/boxcars/pull/200) ([francis](https://github.com/francis))
10
+
11
+ ## [v0.6.3](https://github.com/BoxcarsAI/boxcars/tree/v0.6.3) (2024-07-26)
12
+
13
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.6.2...v0.6.3)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - Add Groq engine [\#199](https://github.com/BoxcarsAI/boxcars/pull/199) ([francis](https://github.com/francis))
18
+
19
+ ## [v0.6.2](https://github.com/BoxcarsAI/boxcars/tree/v0.6.2) (2024-07-24)
20
+
21
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.6.1...v0.6.2)
22
+
23
+ **Merged pull requests:**
24
+
25
+ - add flag for symbolizing JSON Engine Boxcar results [\#198](https://github.com/BoxcarsAI/boxcars/pull/198) ([francis](https://github.com/francis))
26
+
3
27
  ## [v0.6.1](https://github.com/BoxcarsAI/boxcars/tree/v0.6.1) (2024-07-19)
4
28
 
5
29
  [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.5.1...v0.6.1)
data/Gemfile CHANGED
@@ -15,13 +15,13 @@ gem "sqlite3", "~> 1.7"
15
15
 
16
16
  gem "async", "~>1.32.1"
17
17
 
18
- gem "activerecord", "~> 7.0"
18
+ gem "activerecord", "~> 7.1"
19
19
 
20
20
  gem "github_changelog_generator", "~> 1.16"
21
21
 
22
22
  gem "faraday-retry", "~> 2.0"
23
23
 
24
- gem "activesupport", "~> 7.0"
24
+ gem "activesupport", "~> 7.1"
25
25
 
26
26
  gem "rest-client", "~> 2.1"
27
27
 
@@ -32,9 +32,9 @@ gem "pgvector", "~> 0.2.2"
32
32
 
33
33
  group :development, :test do
34
34
  gem "rspec", "~> 3.13"
35
- gem "rubocop", "~> 1.64"
36
- gem "vcr", "~> 6.2.0"
35
+ gem "rubocop", "~> 1.66"
36
+ gem "vcr", "~> 6.3.1"
37
37
  gem "webmock", "~> 3.23.1"
38
38
  gem "rubocop-rake", "~> 0.6.0"
39
- gem "rubocop-rspec", "~> 3.0"
39
+ gem "rubocop-rspec", "~> 3.1"
40
40
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- boxcars (0.6.3)
4
+ boxcars (0.6.5)
5
5
  anthropic (~> 0.1)
6
6
  google_search_results (~> 2.2)
7
7
  gpt4all (~> 0.0.4)
@@ -13,13 +13,13 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- activemodel (7.1.3.4)
17
- activesupport (= 7.1.3.4)
18
- activerecord (7.1.3.4)
19
- activemodel (= 7.1.3.4)
20
- activesupport (= 7.1.3.4)
16
+ activemodel (7.1.4)
17
+ activesupport (= 7.1.4)
18
+ activerecord (7.1.4)
19
+ activemodel (= 7.1.4)
20
+ activesupport (= 7.1.4)
21
21
  timeout (>= 0.4.0)
22
- activesupport (7.1.3.4)
22
+ activesupport (7.1.4)
23
23
  base64
24
24
  bigdecimal
25
25
  concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -57,7 +57,7 @@ GEM
57
57
  async (>= 1.25)
58
58
  base64 (0.2.0)
59
59
  bigdecimal (3.1.8)
60
- concurrent-ruby (1.3.3)
60
+ concurrent-ruby (1.3.4)
61
61
  connection_pool (2.4.1)
62
62
  console (1.27.0)
63
63
  fiber-annotation
@@ -71,7 +71,7 @@ GEM
71
71
  reline (>= 0.3.8)
72
72
  diff-lcs (1.5.1)
73
73
  domain_name (0.6.20240107)
74
- dotenv (3.1.2)
74
+ dotenv (3.1.4)
75
75
  drb (2.2.1)
76
76
  event_stream_parser (1.0.0)
77
77
  faraday (2.10.0)
@@ -120,7 +120,7 @@ GEM
120
120
  mime-types (3.5.2)
121
121
  mime-types-data (~> 3.2015)
122
122
  mime-types-data (3.2024.0702)
123
- minitest (5.24.1)
123
+ minitest (5.25.1)
124
124
  multi_json (1.15.0)
125
125
  multipart-post (2.4.1)
126
126
  mutex_m (0.2.0)
@@ -128,19 +128,19 @@ GEM
128
128
  uri
129
129
  netrc (0.11.0)
130
130
  nio4r (2.7.3)
131
- nokogiri (1.16.6-arm64-darwin)
131
+ nokogiri (1.16.7-arm64-darwin)
132
132
  racc (~> 1.4)
133
- nokogiri (1.16.6-x86_64-linux)
133
+ nokogiri (1.16.7-x86_64-linux)
134
134
  racc (~> 1.4)
135
135
  octokit (4.25.1)
136
136
  faraday (>= 1, < 3)
137
137
  sawyer (~> 0.9)
138
138
  os (1.1.4)
139
- parallel (1.25.1)
140
- parser (3.3.4.0)
139
+ parallel (1.26.3)
140
+ parser (3.3.5.0)
141
141
  ast (~> 2.4.1)
142
142
  racc
143
- pg (1.5.6)
143
+ pg (1.5.8)
144
144
  pgvector (0.2.2)
145
145
  protocol-hpack (1.4.3)
146
146
  protocol-http (0.26.8)
@@ -152,7 +152,7 @@ GEM
152
152
  psych (5.1.2)
153
153
  stringio
154
154
  public_suffix (6.0.0)
155
- racc (1.8.0)
155
+ racc (1.8.1)
156
156
  rainbow (3.1.1)
157
157
  rake (13.2.1)
158
158
  rdoc (6.7.0)
@@ -165,8 +165,7 @@ GEM
165
165
  http-cookie (>= 1.0.2, < 2.0)
166
166
  mime-types (>= 1.16, < 4.0)
167
167
  netrc (~> 0.8)
168
- rexml (3.3.2)
169
- strscan
168
+ rexml (3.3.8)
170
169
  rspec (3.13.0)
171
170
  rspec-core (~> 3.13.0)
172
171
  rspec-expectations (~> 3.13.0)
@@ -180,22 +179,21 @@ GEM
180
179
  diff-lcs (>= 1.2.0, < 2.0)
181
180
  rspec-support (~> 3.13.0)
182
181
  rspec-support (3.13.1)
183
- rubocop (1.65.0)
182
+ rubocop (1.66.1)
184
183
  json (~> 2.3)
185
184
  language_server-protocol (>= 3.17.0)
186
185
  parallel (~> 1.10)
187
186
  parser (>= 3.3.0.2)
188
187
  rainbow (>= 2.2.2, < 4.0)
189
188
  regexp_parser (>= 2.4, < 3.0)
190
- rexml (>= 3.2.5, < 4.0)
191
- rubocop-ast (>= 1.31.1, < 2.0)
189
+ rubocop-ast (>= 1.32.2, < 2.0)
192
190
  ruby-progressbar (~> 1.7)
193
191
  unicode-display_width (>= 2.4.0, < 3.0)
194
- rubocop-ast (1.31.3)
192
+ rubocop-ast (1.32.3)
195
193
  parser (>= 3.3.1.0)
196
194
  rubocop-rake (0.6.0)
197
195
  rubocop (~> 1.0)
198
- rubocop-rspec (3.0.3)
196
+ rubocop-rspec (3.1.0)
199
197
  rubocop (~> 1.61)
200
198
  ruby-openai (7.1.0)
201
199
  event_stream_parser (>= 0.3.0, < 2.0.0)
@@ -209,7 +207,6 @@ GEM
209
207
  sqlite3 (1.7.3-x86_64-linux)
210
208
  stringio (3.1.1)
211
209
  strings-ansi (0.2.0)
212
- strscan (3.1.0)
213
210
  timeout (0.4.1)
214
211
  timers (4.3.5)
215
212
  traces (0.11.1)
@@ -222,9 +219,10 @@ GEM
222
219
  tty-screen (0.8.2)
223
220
  tzinfo (2.0.6)
224
221
  concurrent-ruby (~> 1.0)
225
- unicode-display_width (2.5.0)
222
+ unicode-display_width (2.6.0)
226
223
  uri (0.13.0)
227
- vcr (6.2.0)
224
+ vcr (6.3.1)
225
+ base64
228
226
  webmock (3.23.1)
229
227
  addressable (>= 2.8.0)
230
228
  crack (>= 0.3.2)
@@ -233,11 +231,12 @@ GEM
233
231
  PLATFORMS
234
232
  arm64-darwin-22
235
233
  arm64-darwin-23
234
+ arm64-darwin-24
236
235
  x86_64-linux
237
236
 
238
237
  DEPENDENCIES
239
- activerecord (~> 7.0)
240
- activesupport (~> 7.0)
238
+ activerecord (~> 7.1)
239
+ activesupport (~> 7.1)
241
240
  async (~> 1.32.1)
242
241
  boxcars!
243
242
  debug (~> 1.9)
@@ -250,11 +249,11 @@ DEPENDENCIES
250
249
  rake (~> 13.2)
251
250
  rest-client (~> 2.1)
252
251
  rspec (~> 3.13)
253
- rubocop (~> 1.64)
252
+ rubocop (~> 1.66)
254
253
  rubocop-rake (~> 0.6.0)
255
- rubocop-rspec (~> 3.0)
254
+ rubocop-rspec (~> 3.1)
256
255
  sqlite3 (~> 1.7)
257
- vcr (~> 6.2.0)
256
+ vcr (~> 6.3.1)
258
257
  webmock (~> 3.23.1)
259
258
 
260
259
  BUNDLED WITH
@@ -50,12 +50,14 @@ module Boxcars
50
50
  # @param engine_output [String] The output from the engine.
51
51
  # @return [Result] The result.
52
52
  def get_answer(engine_output)
53
- # sometimes the LLM adds text in front of the JSON output, so let's strip it here
54
- json_start = engine_output.index("{")
55
- json_end = engine_output.rindex("}")
56
- extract_answer(JSON.parse(engine_output[json_start..json_end], symbolize_names: symbolize))
53
+ json_string = extract_json(engine_output)
54
+ reply = JSON.parse(json_string, symbolize_names: symbolize)
55
+ Result.new(status: :ok, answer: reply, explanation: reply)
56
+ rescue JSON::ParserError => e
57
+ Boxcars.debug "JSON: #{engine_output}", :red
58
+ Result.from_error("JSON parsing error: #{e.message}")
57
59
  rescue StandardError => e
58
- Result.from_error("Error: #{e.message}:\n#{engine_output}")
60
+ Result.from_error("Unexpected error: #{e.message}")
59
61
  end
60
62
 
61
63
  # get answer from parsed JSON
@@ -63,14 +65,29 @@ module Boxcars
63
65
  # @return [Result] The result.
64
66
  def extract_answer(data)
65
67
  reply = data
68
+ Result.new(status: :ok, answer: reply, explanation: reply)
69
+ end
70
+
71
+ private
72
+
73
+ def extract_json(text)
74
+ # Escape control characters (U+0000 to U+001F)
75
+ text = text.gsub(/[\u0000-\u001F]/, '')
76
+ # first strip hidden characters
77
+ # text = text.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
78
+
79
+ # sometimes the LLM adds text in front of the JSON output, so let's strip it here
80
+ json_start = text.index("{")
81
+ json_end = text.rindex("}")
82
+ text[json_start..json_end]
83
+ end
84
+
85
+ def extract_json2(text)
86
+ # Match the outermost JSON object
87
+ match = text.match(/\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\}/)
88
+ raise StandardError, "No valid JSON object found in the output" unless match
66
89
 
67
- if reply.present?
68
- Result.new(status: :ok, answer: reply, explanation: reply)
69
- else
70
- # we have an unexpected output from the engine
71
- Result.new(status: :error, answer: nil,
72
- explanation: "You gave me an improperly formatted answer. I was expecting a valid reply.")
73
- end
90
+ match[0]
74
91
  end
75
92
  end
76
93
  end
@@ -166,7 +166,7 @@ module Boxcars
166
166
  output = call(inputs: 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
- Boxcars.error("Response Body: #{e.response[:body]}", :red) if e.respond_to?(:response)
169
+ Boxcars.error("Response Body: #{e.response[:body]}", :red) if e.respond_to?(:response) && e.response.present?
170
170
  raise e
171
171
  end
172
172
  validate_outputs(outputs: output.keys)
@@ -90,21 +90,6 @@ module Boxcars
90
90
  llm_params
91
91
  end
92
92
 
93
- # Get generation informaton
94
- # @param sub_choices [Array<Hash>] The choices to get generation info for.
95
- # @return [Array<Generation>] The generation information.
96
- def generation_info(sub_choices)
97
- sub_choices.map do |choice|
98
- Generation.new(
99
- text: choice["completion"],
100
- generation_info: {
101
- finish_reason: choice.fetch("stop_reason", nil),
102
- logprobs: choice.fetch("logprobs", nil)
103
- }
104
- )
105
- end
106
- end
107
-
108
93
  # make sure we got a valid response
109
94
  # @param response [Hash] The response to check.
110
95
  # @param must_haves [Array<String>] The keys that must be in the response. Defaults to %w[choices].
@@ -124,45 +109,11 @@ module Boxcars
124
109
  end
125
110
  end
126
111
 
127
- # Call out to OpenAI's endpoint with k unique prompts.
128
- # @param prompts [Array<String>] The prompts to pass into the model.
129
- # @param inputs [Array<String>] The inputs to subsitite into the prompt.
130
- # @param stop [Array<String>] Optional list of stop words to use when generating.
131
- # @return [EngineResult] The full engine output.
132
- def generate(prompts:, stop: nil)
133
- params = {}
134
- params[:stop] = stop if stop
135
- choices = []
136
- # Get the token usage from the response.
137
- # Includes prompt, completion, and total tokens used.
138
- prompts.each_slice(batch_size) do |sub_prompts|
139
- sub_prompts.each do |sprompts, inputs|
140
- response = client(prompt: sprompts, inputs: inputs, **params)
141
- check_response(response)
142
- choices << response
143
- end
144
- end
145
-
146
- n = params.fetch(:n, 1)
147
- generations = []
148
- prompts.each_with_index do |_prompt, i|
149
- sub_choices = choices[i * n, (i + 1) * n]
150
- generations.push(generation_info(sub_choices))
151
- end
152
- EngineResult.new(generations: generations, engine_output: { token_usage: {} })
153
- end
154
- # rubocop:enable Metrics/AbcSize
155
-
156
112
  # the engine type
157
113
  def engine_type
158
114
  "claude"
159
115
  end
160
116
 
161
- # calculate the number of tokens used
162
- def get_num_tokens(text:)
163
- text.split.length # TODO: hook up to token counting gem
164
- end
165
-
166
117
  # lookup the context size for a model by name
167
118
  # @param modelname [String] The name of the model to lookup.
168
119
  def modelname_to_contextsize(_modelname)
@@ -71,8 +71,8 @@ module Boxcars
71
71
  prompt = Prompt.new(template: question)
72
72
  response = client(prompt: prompt, **kwargs)
73
73
  raise Error, "Groq: No response from API" unless response
74
- raise Error, "Groq: #{response['error']}" if response["error"]
75
74
 
75
+ check_response(response)
76
76
  answer = response["choices"].map { |c| c.dig("message", "content") || c["text"] }.join("\n").strip
77
77
  puts answer
78
78
  answer
@@ -83,31 +83,16 @@ module Boxcars
83
83
  groq_parmas
84
84
  end
85
85
 
86
- # Get generation informaton
87
- # @param sub_choices [Array<Hash>] The choices to get generation info for.
88
- # @return [Array<Generation>] The generation information.
89
- def generation_info(sub_choices)
90
- sub_choices.map do |choice|
91
- Generation.new(
92
- text: choice.dig("message", "content") || choice["text"],
93
- generation_info: {
94
- finish_reason: choice.fetch("finish_reason", nil),
95
- logprobs: choice.fetch("logprobs", nil)
96
- }
97
- )
98
- end
99
- end
100
-
101
86
  # make sure we got a valid response
102
87
  # @param response [Hash] The response to check.
103
88
  # @param must_haves [Array<String>] The keys that must be in the response. Defaults to %w[choices].
104
89
  # @raise [KeyError] if there is an issue with the access token.
105
90
  # @raise [ValueError] if the response is not valid.
106
91
  def check_response(response, must_haves: %w[choices])
107
- if response['error']
92
+ if response['error'].is_a?(Hash)
108
93
  code = response.dig('error', 'code')
109
94
  msg = response.dig('error', 'message') || 'unknown error'
110
- raise KeyError, "OPENAI_ACCESS_TOKEN not valid" if code == 'invalid_api_key'
95
+ raise KeyError, "GROQ_API_TOKEN not valid" if code == 'invalid_api_key'
111
96
 
112
97
  raise ValueError, "Groq error: #{msg}"
113
98
  end
@@ -117,58 +102,20 @@ module Boxcars
117
102
  end
118
103
  end
119
104
 
120
- # Call out to Groq's endpoint with k unique prompts.
121
- # @param prompts [Array<String>] The prompts to pass into the model.
122
- # @param inputs [Array<String>] The inputs to subsitite into the prompt.
123
- # @param stop [Array<String>] Optional list of stop words to use when generating.
124
- # @return [EngineResult] The full engine output.
125
- def generate(prompts:, stop: nil)
126
- params = {}
127
- params[:stop] = stop if stop
128
- choices = []
129
- token_usage = {}
130
- # Get the token usage from the response.
131
- # Includes prompt, completion, and total tokens used.
132
- inkeys = %w[completion_tokens prompt_tokens total_tokens].freeze
133
- prompts.each_slice(batch_size) do |sub_prompts|
134
- sub_prompts.each do |sprompts, inputs|
135
- response = client(prompt: sprompts, inputs: inputs, **params)
136
- check_response(response)
137
- choices.concat(response["choices"])
138
- usage_keys = inkeys & response["usage"].keys
139
- usage_keys.each { |key| token_usage[key] = token_usage[key].to_i + response["usage"][key] }
140
- end
141
- end
142
-
143
- n = params.fetch(:n, 1)
144
- generations = []
145
- prompts.each_with_index do |_prompt, i|
146
- sub_choices = choices[i * n, (i + 1) * n]
147
- generations.push(generation_info(sub_choices))
148
- end
149
- EngineResult.new(generations: generations, engine_output: { token_usage: token_usage })
105
+ # the engine type
106
+ def engine_type
107
+ "groq"
150
108
  end
151
- # rubocop:enable Metrics/AbcSize
152
- end
153
-
154
- # the engine type
155
- def engine_type
156
- "groq"
157
- end
158
109
 
159
- # calculate the number of tokens used
160
- def get_num_tokens(text:)
161
- text.split.length # TODO: hook up to token counting gem
162
- end
163
-
164
- # Calculate the maximum number of tokens possible to generate for a prompt.
165
- # @param prompt_text [String] The prompt text to use.
166
- # @return [Integer] the number of tokens possible to generate.
167
- def max_tokens_for_prompt(prompt_text)
168
- num_tokens = get_num_tokens(prompt_text)
110
+ # Calculate the maximum number of tokens possible to generate for a prompt.
111
+ # @param prompt_text [String] The prompt text to use.
112
+ # @return [Integer] the number of tokens possible to generate.
113
+ def max_tokens_for_prompt(prompt_text)
114
+ num_tokens = get_num_tokens(prompt_text)
169
115
 
170
- # get max context size for model by name
171
- max_size = 8096
172
- max_size - num_tokens
116
+ # get max context size for model by name
117
+ max_size = 8096
118
+ max_size - num_tokens
119
+ end
173
120
  end
174
121
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Boxcars is a framework for running a series of tools to get an answer to a question.
4
+ module Boxcars
5
+ # A engine that uses local GPT4All API.
6
+ class Ollama < Engine
7
+ attr_reader :prompts, :model_kwargs, :batch_size, :ollama_params
8
+
9
+ # The default parameters to use when asking the engine.
10
+ DEFAULT_PARAMS = {
11
+ model: "llama3",
12
+ temperature: 0.1,
13
+ max_tokens: 4096
14
+ }.freeze
15
+
16
+ # the default name of the engine
17
+ DEFAULT_NAME = "Ollama engine"
18
+ # the default description of the engine
19
+ DEFAULT_DESCRIPTION = "useful for when you need to use local AI to answer questions. " \
20
+ "You should ask targeted questions"
21
+
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
+ def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 2, **kwargs)
29
+ @ollama_params = DEFAULT_PARAMS.merge(kwargs)
30
+ @prompts = prompts
31
+ @batch_size = batch_size
32
+ super(description: description, name: name)
33
+ end
34
+
35
+ # Get the OpenAI API client
36
+ # @param groq_api_key [String] The access token to use when asking the engine.
37
+ # Defaults to Boxcars.configuration.groq_api_key
38
+ # @return [OpenAI::Client] The OpenAI API gem client.
39
+ def self.open_ai_client
40
+ ::OpenAI::Client.new(uri_base: "http://localhost:11434")
41
+ end
42
+
43
+ def conversation_model?(_model)
44
+ true
45
+ end
46
+
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
+ def client(prompt:, inputs: {}, **kwargs)
53
+ clnt = Ollama.open_ai_client
54
+ params = ollama_params.merge(kwargs)
55
+ prompt = prompt.first if prompt.is_a?(Array)
56
+ params = prompt.as_messages(inputs).merge(params)
57
+ if Boxcars.configuration.log_prompts
58
+ Boxcars.debug(params[:messages].last(2).map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
59
+ end
60
+ ans = clnt.chat(parameters: params)
61
+ ans['choices'].pluck('message').pluck('content').join("\n")
62
+ rescue => e
63
+ Boxcars.error(e, :red)
64
+ raise
65
+ end
66
+
67
+ # get an answer from the engine for a question.
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)
71
+ prompt = Prompt.new(template: question)
72
+ answer = client(prompt: prompt, **kwargs)
73
+ raise Error, "Ollama: No response from API" unless answer
74
+
75
+ # raise Error, "Ollama: #{response['error']}" if response["error"]
76
+ Boxcars.debug("Answer: #{answer}", :cyan)
77
+ answer
78
+ end
79
+ end
80
+ end
@@ -28,6 +28,11 @@ module Boxcars
28
28
  # @param batch_size [Integer] The number of prompts to send to the engine at once. Defaults to 20.
29
29
  def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **kwargs)
30
30
  @open_ai_params = DEFAULT_PARAMS.merge(kwargs)
31
+ if @open_ai_params[:model] =~ /^o/ && @open_ai_params[:max_tokens].present?
32
+ @open_ai_params[:max_completion_tokens] = @open_ai_params.delete(:max_tokens)
33
+ @open_ai_params.delete(:temperature)
34
+ end
35
+
31
36
  @prompts = prompts
32
37
  @batch_size = batch_size
33
38
  super(description: description, name: name)
@@ -44,7 +49,7 @@ module Boxcars
44
49
  end
45
50
 
46
51
  def conversation_model?(model)
47
- !!(model =~ /(^gpt-4)|(-turbo\b)/)
52
+ !!(model =~ /(^gpt-4)|(-turbo\b)|(^o\d)/)
48
53
  end
49
54
 
50
55
  # Get an answer from the engine.
@@ -57,6 +62,10 @@ module Boxcars
57
62
  params = open_ai_params.merge(kwargs)
58
63
  if conversation_model?(params[:model])
59
64
  prompt = prompt.first if prompt.is_a?(Array)
65
+ if params[:model] =~ /^o/
66
+ params.delete(:response_format)
67
+ params.delete(:stop)
68
+ end
60
69
  params = prompt.as_messages(inputs).merge(params)
61
70
  if Boxcars.configuration.log_prompts
62
71
  Boxcars.debug(params[:messages].last(2).map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
@@ -88,21 +97,6 @@ module Boxcars
88
97
  open_ai_params
89
98
  end
90
99
 
91
- # Get generation informaton
92
- # @param sub_choices [Array<Hash>] The choices to get generation info for.
93
- # @return [Array<Generation>] The generation information.
94
- def generation_info(sub_choices)
95
- sub_choices.map do |choice|
96
- Generation.new(
97
- text: choice.dig("message", "content") || choice["text"],
98
- generation_info: {
99
- finish_reason: choice.fetch("finish_reason", nil),
100
- logprobs: choice.fetch("logprobs", nil)
101
- }
102
- )
103
- end
104
- end
105
-
106
100
  # make sure we got a valid response
107
101
  # @param response [Hash] The response to check.
108
102
  # @param must_haves [Array<String>] The keys that must be in the response. Defaults to %w[choices].
@@ -121,39 +115,6 @@ module Boxcars
121
115
  raise ValueError, "Expecting key #{key} in response" unless response.key?(key)
122
116
  end
123
117
  end
124
-
125
- # Call out to OpenAI's endpoint with k unique prompts.
126
- # @param prompts [Array<String>] The prompts to pass into the model.
127
- # @param inputs [Array<String>] The inputs to subsitite into the prompt.
128
- # @param stop [Array<String>] Optional list of stop words to use when generating.
129
- # @return [EngineResult] The full engine output.
130
- def generate(prompts:, stop: nil)
131
- params = {}
132
- params[:stop] = stop if stop
133
- choices = []
134
- token_usage = {}
135
- # Get the token usage from the response.
136
- # Includes prompt, completion, and total tokens used.
137
- inkeys = %w[completion_tokens prompt_tokens total_tokens].freeze
138
- prompts.each_slice(batch_size) do |sub_prompts|
139
- sub_prompts.each do |sprompts, inputs|
140
- response = client(prompt: sprompts, inputs: inputs, **params)
141
- check_response(response)
142
- choices.concat(response["choices"])
143
- usage_keys = inkeys & response["usage"].keys
144
- usage_keys.each { |key| token_usage[key] = token_usage[key].to_i + response["usage"][key] }
145
- end
146
- end
147
-
148
- n = params.fetch(:n, 1)
149
- generations = []
150
- prompts.each_with_index do |_prompt, i|
151
- sub_choices = choices[i * n, (i + 1) * n]
152
- generations.push(generation_info(sub_choices))
153
- end
154
- EngineResult.new(generations: generations, engine_output: { token_usage: token_usage })
155
- end
156
- # rubocop:enable Metrics/AbcSize
157
118
  end
158
119
 
159
120
  # the engine type
@@ -161,11 +122,6 @@ module Boxcars
161
122
  "openai"
162
123
  end
163
124
 
164
- # calculate the number of tokens used
165
- def get_num_tokens(text:)
166
- text.split.length # TODO: hook up to token counting gem
167
- end
168
-
169
125
  # lookup the context size for a model by name
170
126
  # @param modelname [String] The name of the model to lookup.
171
127
  def modelname_to_contextsize(modelname)
@@ -8,9 +8,8 @@ module Boxcars
8
8
 
9
9
  # The default parameters to use when asking the engine.
10
10
  DEFAULT_PER_PARAMS = {
11
- model: "llama-2-70b-chat",
12
- temperature: 0.1,
13
- max_tokens: 3200
11
+ model: "'llama-3-sonar-large-32k-online'",
12
+ temperature: 0.1
14
13
  }.freeze
15
14
 
16
15
  # the default name of the engine
@@ -32,28 +31,27 @@ module Boxcars
32
31
  super(description: description, name: name)
33
32
  end
34
33
 
35
- def conversation_model?(model)
36
- ["mistral-7b-instruct", "llama-2-13b-chat", "llama-2-70b-chat", "openhermes-2-mistral-7b"].include?(model)
34
+ def conversation_model?(_model)
35
+ true
37
36
  end
38
37
 
39
38
  def chat(parameters:)
40
- url = URI("https://api.perplexity.ai/chat/completions")
41
-
42
- http = Net::HTTP.new(url.host, url.port)
43
- http.use_ssl = true
44
-
45
- request = Net::HTTP::Post.new(url)
46
- request["accept"] = 'application/json'
47
- request["authorization"] = "Bearer #{ENV.fetch('PERPLEXITY_API_KEY')}"
48
- request["content-type"] = 'application/json'
49
- the_body = {
50
- model: parameters[:model] || "mistral-7b-instruct",
51
- messages: parameters[:messages]
52
- }
53
- request.body = the_body.to_json
54
-
55
- response = http.request(request)
56
- JSON.parse(response.read_body)
39
+ conn = Faraday.new(url: "https://api.perplexity.ai/chat/completions") do |faraday|
40
+ faraday.request :json
41
+ faraday.response :json
42
+ faraday.response :raise_error
43
+ # faraday.options.timeout = 180 # 3 minutes
44
+ end
45
+
46
+ response = conn.post do |req|
47
+ req.headers['Authorization'] = "Bearer #{ENV.fetch('PERPLEXITY_API_KEY')}"
48
+ req.body = {
49
+ model: parameters[:model],
50
+ messages: parameters[:messages]
51
+ }
52
+ end
53
+
54
+ response.body
57
55
  end
58
56
 
59
57
  # Get an answer from the engine.
@@ -64,7 +62,6 @@ module Boxcars
64
62
  def client(prompt:, inputs: {}, **kwargs)
65
63
  prompt = prompt.first if prompt.is_a?(Array)
66
64
  params = prompt.as_messages(inputs).merge(default_params).merge(kwargs)
67
- params[:model] ||= "llama-2-70b-chat"
68
65
  if Boxcars.configuration.log_prompts
69
66
  Boxcars.debug(params[:messages].last(2).map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
70
67
  end
@@ -90,21 +87,6 @@ module Boxcars
90
87
  perplexity_params
91
88
  end
92
89
 
93
- # Get generation informaton
94
- # @param sub_choices [Array<Hash>] The choices to get generation info for.
95
- # @return [Array<Generation>] The generation information.
96
- def generation_info(sub_choices)
97
- sub_choices.map do |choice|
98
- Generation.new(
99
- text: choice.dig("message", "content") || choice["text"],
100
- generation_info: {
101
- finish_reason: choice.fetch("finish_reason", nil),
102
- logprobs: choice.fetch("logprobs", nil)
103
- }
104
- )
105
- end
106
- end
107
-
108
90
  # make sure we got a valid response
109
91
  # @param response [Hash] The response to check.
110
92
  # @param must_haves [Array<String>] The keys that must be in the response. Defaults to %w[choices].
@@ -123,39 +105,6 @@ module Boxcars
123
105
  raise ValueError, "Expecting key #{key} in response" unless response.key?(key)
124
106
  end
125
107
  end
126
-
127
- # Call out to OpenAI's endpoint with k unique prompts.
128
- # @param prompts [Array<String>] The prompts to pass into the model.
129
- # @param inputs [Array<String>] The inputs to subsitite into the prompt.
130
- # @param stop [Array<String>] Optional list of stop words to use when generating.
131
- # @return [EngineResult] The full engine output.
132
- def generate(prompts:, stop: nil)
133
- params = {}
134
- params[:stop] = stop if stop
135
- choices = []
136
- token_usage = {}
137
- # Get the token usage from the response.
138
- # Includes prompt, completion, and total tokens used.
139
- inkeys = %w[completion_tokens prompt_tokens total_tokens].freeze
140
- prompts.each_slice(batch_size) do |sub_prompts|
141
- sub_prompts.each do |sprompts, inputs|
142
- response = client(prompt: sprompts, inputs: inputs, **params)
143
- check_response(response)
144
- choices.concat(response["choices"])
145
- usage_keys = inkeys & response["usage"].keys
146
- usage_keys.each { |key| token_usage[key] = token_usage[key].to_i + response["usage"][key] }
147
- end
148
- end
149
-
150
- n = params.fetch(:n, 1)
151
- generations = []
152
- prompts.each_with_index do |_prompt, i|
153
- sub_choices = choices[i * n, (i + 1) * n]
154
- generations.push(generation_info(sub_choices))
155
- end
156
- EngineResult.new(generations: generations, engine_output: { token_usage: token_usage })
157
- end
158
- # rubocop:enable Metrics/AbcSize
159
108
  end
160
109
 
161
110
  # the engine type
@@ -168,29 +117,57 @@ module Boxcars
168
117
  text.split.length # TODO: hook up to token counting gem
169
118
  end
170
119
 
171
- # lookup the context size for a model by name
172
- # @param modelname [String] The name of the model to lookup.
173
- def modelname_to_contextsize(modelname)
174
- model_lookup = {
175
- 'text-davinci-003': 4097,
176
- 'text-curie-001': 2048,
177
- 'text-babbage-001': 2048,
178
- 'text-ada-001': 2048,
179
- 'code-davinci-002': 8000,
180
- 'code-cushman-001': 2048,
181
- 'gpt-3.5-turbo-1': 4096
182
- }.freeze
183
- model_lookup[modelname] || 4097
184
- end
185
-
186
120
  # Calculate the maximum number of tokens possible to generate for a prompt.
187
121
  # @param prompt_text [String] The prompt text to use.
188
122
  # @return [Integer] the number of tokens possible to generate.
189
- def max_tokens_for_prompt(prompt_text)
190
- num_tokens = get_num_tokens(prompt_text)
123
+ def max_tokens_for_prompt(_prompt_text)
124
+ 8096
125
+ end
191
126
 
192
- # get max context size for model by name
193
- max_size = modelname_to_contextsize(model_name)
194
- max_size - num_tokens
127
+ # Get generation informaton
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
+ )
139
+ end
140
+ end
141
+
142
+ # Call out to endpoint with k unique prompts.
143
+ # @param prompts [Array<String>] The prompts to pass into the model.
144
+ # @param inputs [Array<String>] The inputs to subsitite into the prompt.
145
+ # @param stop [Array<String>] Optional list of stop words to use when generating.
146
+ # @return [EngineResult] The full engine output.
147
+ def generate(prompts:, stop: nil)
148
+ params = {}
149
+ params[:stop] = stop if stop
150
+ choices = []
151
+ token_usage = {}
152
+ # Get the token usage from the response.
153
+ # Includes prompt, completion, and total tokens used.
154
+ inkeys = %w[completion_tokens prompt_tokens total_tokens].freeze
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] }
162
+ end
163
+ end
164
+
165
+ n = params.fetch(:n, 1)
166
+ generations = []
167
+ prompts.each_with_index do |_prompt, i|
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 })
195
172
  end
196
173
  end
@@ -16,6 +16,58 @@ module Boxcars
16
16
  def run(question)
17
17
  raise NotImplementedError
18
18
  end
19
+
20
+ # calculate the number of tokens used
21
+ def get_num_tokens(text:)
22
+ text.split.length # TODO: hook up to token counting gem
23
+ end
24
+
25
+ # Get generation informaton
26
+ # @param sub_choices [Array<Hash>] The choices to get generation info for.
27
+ # @return [Array<Generation>] The generation information.
28
+ def generation_info(sub_choices)
29
+ sub_choices.map do |choice|
30
+ Generation.new(
31
+ text: choice.dig("message", "content") || choice["text"],
32
+ generation_info: {
33
+ finish_reason: choice.fetch("finish_reason", nil),
34
+ logprobs: choice.fetch("logprobs", nil)
35
+ }
36
+ )
37
+ end
38
+ end
39
+
40
+ # Call out to OpenAI's endpoint with k unique prompts.
41
+ # @param prompts [Array<String>] The prompts to pass into the model.
42
+ # @param inputs [Array<String>] The inputs to subsitite into the prompt.
43
+ # @param stop [Array<String>] Optional list of stop words to use when generating.
44
+ # @return [EngineResult] The full engine output.
45
+ def generate(prompts:, stop: nil)
46
+ params = {}
47
+ params[:stop] = stop if stop
48
+ choices = []
49
+ token_usage = {}
50
+ # Get the token usage from the response.
51
+ # Includes prompt, completion, and total tokens used.
52
+ inkeys = %w[completion_tokens prompt_tokens total_tokens].freeze
53
+ prompts.each_slice(batch_size) do |sub_prompts|
54
+ sub_prompts.each do |sprompts, inputs|
55
+ response = client(prompt: sprompts, inputs: inputs, **params)
56
+ check_response(response)
57
+ choices.concat(response["choices"])
58
+ usage_keys = inkeys & response["usage"].keys
59
+ usage_keys.each { |key| token_usage[key] = token_usage[key].to_i + response["usage"][key] }
60
+ end
61
+ end
62
+
63
+ n = params.fetch(:n, 1)
64
+ generations = []
65
+ prompts.each_with_index do |_prompt, i|
66
+ sub_choices = choices[i * n, (i + 1) * n]
67
+ generations.push(generation_info(sub_choices))
68
+ end
69
+ EngineResult.new(generations: generations, engine_output: { token_usage: token_usage })
70
+ end
19
71
  end
20
72
  end
21
73
 
@@ -23,6 +75,7 @@ require "boxcars/engine/engine_result"
23
75
  require "boxcars/engine/anthropic"
24
76
  require "boxcars/engine/cohere"
25
77
  require "boxcars/engine/groq"
78
+ require "boxcars/engine/ollama"
26
79
  require "boxcars/engine/openai"
27
80
  require "boxcars/engine/perplexityai"
28
81
  require "boxcars/engine/gpt4all_eng"
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Boxcars
4
4
  # The current version of the gem.
5
- VERSION = "0.6.3"
5
+ VERSION = "0.6.5"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: boxcars
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.6.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Francis Sullivan
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2024-07-26 00:00:00.000000000 Z
12
+ date: 2024-10-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: anthropic
@@ -161,6 +161,7 @@ files:
161
161
  - lib/boxcars/engine/engine_result.rb
162
162
  - lib/boxcars/engine/gpt4all_eng.rb
163
163
  - lib/boxcars/engine/groq.rb
164
+ - lib/boxcars/engine/ollama.rb
164
165
  - lib/boxcars/engine/openai.rb
165
166
  - lib/boxcars/engine/perplexityai.rb
166
167
  - lib/boxcars/generation.rb
@@ -218,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
219
  - !ruby/object:Gem::Version
219
220
  version: '0'
220
221
  requirements: []
221
- rubygems_version: 3.4.10
222
+ rubygems_version: 3.5.20
222
223
  signing_key:
223
224
  specification_version: 4
224
225
  summary: Boxcars is a gem that enables you to create new systems with AI composability.