boxcars 0.8.7 → 0.8.9
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/Gemfile.lock +50 -44
- data/lib/boxcars/boxcar/engine_boxcar.rb +7 -3
- data/lib/boxcars/boxcar/json_engine_boxcar.rb +1 -1
- data/lib/boxcars/engine/anthropic.rb +1 -0
- data/lib/boxcars/engine/cohere.rb +1 -0
- data/lib/boxcars/engine/gemini_ai.rb +1 -0
- data/lib/boxcars/engine/gpt4all_eng.rb +1 -0
- data/lib/boxcars/engine/groq.rb +1 -0
- data/lib/boxcars/engine/intelligence_base.rb +1 -0
- data/lib/boxcars/engine/ollama.rb +1 -0
- data/lib/boxcars/engine/openai.rb +173 -8
- data/lib/boxcars/engine/perplexityai.rb +1 -0
- data/lib/boxcars/engine.rb +6 -2
- data/lib/boxcars/engines.rb +8 -5
- data/lib/boxcars/prompt.rb +6 -3
- data/lib/boxcars/result.rb +2 -1
- data/lib/boxcars/vector_store/embed_via_tensorflow.rb +1 -0
- data/lib/boxcars/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1809b25de376f58babd70eda5795a6f757ed570b62b44fbebe8d8caec92c8c81
|
4
|
+
data.tar.gz: 9c77d377b6c8e2ebaa9b9376885a78bcdf61bd1bb9067d01ff8c8a4230a6fc24
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 73c859810559e64f6cc33b092cea6a0ee34ba50cff65f1224c82e85f17dee54c064b1b62e1f2ed824003e2d6c7a4992b434e6370807d77b09324ba06eb509abb
|
7
|
+
data.tar.gz: c5e5ddcf6b4e2599acdf8ba77f3011bc97920a450801b9c3d992edf502b156633830805e62b78a7e5fbe371b52ef16c7e02de4a09de23b538ece47b71af8e0ea
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
boxcars (0.8.
|
4
|
+
boxcars (0.8.9)
|
5
5
|
faraday-retry (~> 2.0)
|
6
6
|
google_search_results (~> 2.2)
|
7
7
|
gpt4all (~> 0.0.5)
|
@@ -14,13 +14,13 @@ PATH
|
|
14
14
|
GEM
|
15
15
|
remote: https://rubygems.org/
|
16
16
|
specs:
|
17
|
-
activemodel (7.2.2.
|
18
|
-
activesupport (= 7.2.2.
|
19
|
-
activerecord (7.2.2.
|
20
|
-
activemodel (= 7.2.2.
|
21
|
-
activesupport (= 7.2.2.
|
17
|
+
activemodel (7.2.2.2)
|
18
|
+
activesupport (= 7.2.2.2)
|
19
|
+
activerecord (7.2.2.2)
|
20
|
+
activemodel (= 7.2.2.2)
|
21
|
+
activesupport (= 7.2.2.2)
|
22
22
|
timeout (>= 0.4.0)
|
23
|
-
activesupport (7.2.2.
|
23
|
+
activesupport (7.2.2.2)
|
24
24
|
base64
|
25
25
|
benchmark (>= 0.3)
|
26
26
|
bigdecimal
|
@@ -47,7 +47,7 @@ GEM
|
|
47
47
|
protocol-http1 (~> 0.19.0)
|
48
48
|
protocol-http2 (~> 0.16.0)
|
49
49
|
traces (>= 0.10.0)
|
50
|
-
async-http-faraday (0.22.
|
50
|
+
async-http-faraday (0.22.1)
|
51
51
|
async-http (~> 0.42)
|
52
52
|
faraday
|
53
53
|
async-io (1.43.2)
|
@@ -59,7 +59,7 @@ GEM
|
|
59
59
|
bigdecimal (3.2.2)
|
60
60
|
concurrent-ruby (1.3.5)
|
61
61
|
connection_pool (2.5.3)
|
62
|
-
console (1.
|
62
|
+
console (1.33.0)
|
63
63
|
fiber-annotation
|
64
64
|
fiber-local (~> 1.1)
|
65
65
|
json
|
@@ -74,10 +74,10 @@ GEM
|
|
74
74
|
domain_name (0.6.20240107)
|
75
75
|
dotenv (3.1.8)
|
76
76
|
drb (2.2.3)
|
77
|
-
dynamicschema (1.0.
|
78
|
-
erb (5.0.
|
77
|
+
dynamicschema (1.0.1)
|
78
|
+
erb (5.0.2)
|
79
79
|
event_stream_parser (1.0.0)
|
80
|
-
faraday (2.13.
|
80
|
+
faraday (2.13.4)
|
81
81
|
faraday-net_http (>= 2.0, < 3.5)
|
82
82
|
json
|
83
83
|
logger
|
@@ -119,12 +119,12 @@ GEM
|
|
119
119
|
faraday (~> 2.7)
|
120
120
|
json-repair (~> 0.2)
|
121
121
|
mime-types (~> 3.6)
|
122
|
-
io-console (0.8.
|
122
|
+
io-console (0.8.1)
|
123
123
|
irb (1.15.2)
|
124
124
|
pp (>= 0.6.0)
|
125
125
|
rdoc (>= 4.0.0)
|
126
126
|
reline (>= 0.4.2)
|
127
|
-
json (2.
|
127
|
+
json (2.13.2)
|
128
128
|
json-repair (0.2.0)
|
129
129
|
language_server-protocol (3.17.0.5)
|
130
130
|
lint_roller (1.1.0)
|
@@ -132,41 +132,47 @@ GEM
|
|
132
132
|
mime-types (3.7.0)
|
133
133
|
logger
|
134
134
|
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
135
|
-
mime-types-data (3.2025.
|
135
|
+
mime-types-data (3.2025.0819)
|
136
136
|
minitest (5.25.5)
|
137
|
-
multi_json (1.
|
137
|
+
multi_json (1.17.0)
|
138
138
|
multipart-post (2.4.1)
|
139
139
|
net-http (0.6.0)
|
140
140
|
uri
|
141
141
|
netrc (0.11.0)
|
142
142
|
nio4r (2.7.4)
|
143
|
-
nokogiri (1.18.
|
143
|
+
nokogiri (1.18.9-aarch64-linux-gnu)
|
144
144
|
racc (~> 1.4)
|
145
|
-
nokogiri (1.18.
|
145
|
+
nokogiri (1.18.9-aarch64-linux-musl)
|
146
146
|
racc (~> 1.4)
|
147
|
-
nokogiri (1.18.
|
147
|
+
nokogiri (1.18.9-arm-linux-gnu)
|
148
148
|
racc (~> 1.4)
|
149
|
-
nokogiri (1.18.
|
149
|
+
nokogiri (1.18.9-arm-linux-musl)
|
150
150
|
racc (~> 1.4)
|
151
|
-
nokogiri (1.18.
|
151
|
+
nokogiri (1.18.9-arm64-darwin)
|
152
152
|
racc (~> 1.4)
|
153
|
-
nokogiri (1.18.
|
153
|
+
nokogiri (1.18.9-x86_64-darwin)
|
154
154
|
racc (~> 1.4)
|
155
|
-
nokogiri (1.18.
|
155
|
+
nokogiri (1.18.9-x86_64-linux-gnu)
|
156
156
|
racc (~> 1.4)
|
157
|
-
nokogiri (1.18.
|
157
|
+
nokogiri (1.18.9-x86_64-linux-musl)
|
158
158
|
racc (~> 1.4)
|
159
159
|
octokit (4.25.1)
|
160
160
|
faraday (>= 1, < 3)
|
161
161
|
sawyer (~> 0.9)
|
162
162
|
os (1.1.4)
|
163
163
|
parallel (1.27.0)
|
164
|
-
parser (3.3.
|
164
|
+
parser (3.3.9.0)
|
165
165
|
ast (~> 2.4.1)
|
166
166
|
racc
|
167
|
-
pg (1.
|
167
|
+
pg (1.6.1)
|
168
|
+
pg (1.6.1-aarch64-linux)
|
169
|
+
pg (1.6.1-aarch64-linux-musl)
|
170
|
+
pg (1.6.1-arm64-darwin)
|
171
|
+
pg (1.6.1-x86_64-darwin)
|
172
|
+
pg (1.6.1-x86_64-linux)
|
173
|
+
pg (1.6.1-x86_64-linux-musl)
|
168
174
|
pgvector (0.2.2)
|
169
|
-
posthog-ruby (3.
|
175
|
+
posthog-ruby (3.1.2)
|
170
176
|
concurrent-ruby (~> 1)
|
171
177
|
pp (0.6.2)
|
172
178
|
prettyprint
|
@@ -186,11 +192,11 @@ GEM
|
|
186
192
|
racc (1.8.1)
|
187
193
|
rainbow (3.1.1)
|
188
194
|
rake (13.3.0)
|
189
|
-
rdoc (6.14.
|
195
|
+
rdoc (6.14.2)
|
190
196
|
erb
|
191
197
|
psych (>= 4.0.0)
|
192
|
-
regexp_parser (2.
|
193
|
-
reline (0.6.
|
198
|
+
regexp_parser (2.11.2)
|
199
|
+
reline (0.6.2)
|
194
200
|
io-console (~> 0.5)
|
195
201
|
rest-client (2.1.0)
|
196
202
|
http-accept (>= 1.7.0, < 2.0)
|
@@ -210,8 +216,8 @@ GEM
|
|
210
216
|
rspec-mocks (3.13.5)
|
211
217
|
diff-lcs (>= 1.2.0, < 2.0)
|
212
218
|
rspec-support (~> 3.13.0)
|
213
|
-
rspec-support (3.13.
|
214
|
-
rubocop (1.
|
219
|
+
rspec-support (3.13.5)
|
220
|
+
rubocop (1.80.0)
|
215
221
|
json (~> 2.3)
|
216
222
|
language_server-protocol (~> 3.17.0.2)
|
217
223
|
lint_roller (~> 1.1.0)
|
@@ -219,10 +225,10 @@ GEM
|
|
219
225
|
parser (>= 3.3.0.2)
|
220
226
|
rainbow (>= 2.2.2, < 4.0)
|
221
227
|
regexp_parser (>= 2.9.3, < 3.0)
|
222
|
-
rubocop-ast (>= 1.
|
228
|
+
rubocop-ast (>= 1.46.0, < 2.0)
|
223
229
|
ruby-progressbar (~> 1.7)
|
224
230
|
unicode-display_width (>= 2.4.0, < 4.0)
|
225
|
-
rubocop-ast (1.
|
231
|
+
rubocop-ast (1.46.0)
|
226
232
|
parser (>= 3.3.7.2)
|
227
233
|
prism (~> 1.4)
|
228
234
|
rubocop-rake (0.6.0)
|
@@ -234,7 +240,7 @@ GEM
|
|
234
240
|
event_stream_parser (>= 0.3.0, < 2.0.0)
|
235
241
|
faraday (>= 1)
|
236
242
|
faraday-multipart (>= 1)
|
237
|
-
ruby-openai (8.
|
243
|
+
ruby-openai (8.2.0)
|
238
244
|
event_stream_parser (>= 0.3.0, < 2.0.0)
|
239
245
|
faraday (>= 1)
|
240
246
|
faraday-multipart (>= 1)
|
@@ -243,19 +249,19 @@ GEM
|
|
243
249
|
addressable (>= 2.3.5)
|
244
250
|
faraday (>= 0.17.3, < 3)
|
245
251
|
securerandom (0.4.1)
|
246
|
-
sqlite3 (2.7.
|
247
|
-
sqlite3 (2.7.
|
248
|
-
sqlite3 (2.7.
|
249
|
-
sqlite3 (2.7.
|
250
|
-
sqlite3 (2.7.
|
251
|
-
sqlite3 (2.7.
|
252
|
-
sqlite3 (2.7.
|
253
|
-
sqlite3 (2.7.
|
252
|
+
sqlite3 (2.7.3-aarch64-linux-gnu)
|
253
|
+
sqlite3 (2.7.3-aarch64-linux-musl)
|
254
|
+
sqlite3 (2.7.3-arm-linux-gnu)
|
255
|
+
sqlite3 (2.7.3-arm-linux-musl)
|
256
|
+
sqlite3 (2.7.3-arm64-darwin)
|
257
|
+
sqlite3 (2.7.3-x86_64-darwin)
|
258
|
+
sqlite3 (2.7.3-x86_64-linux-gnu)
|
259
|
+
sqlite3 (2.7.3-x86_64-linux-musl)
|
254
260
|
stringio (3.1.7)
|
255
261
|
strings-ansi (0.2.0)
|
256
262
|
timeout (0.4.3)
|
257
263
|
timers (4.4.0)
|
258
|
-
traces (0.
|
264
|
+
traces (0.17.0)
|
259
265
|
tty-cursor (0.7.1)
|
260
266
|
tty-progressbar (0.18.3)
|
261
267
|
strings-ansi (~> 0.2)
|
@@ -56,7 +56,7 @@ module Boxcars
|
|
56
56
|
def apply(input_list:, current_conversation: nil)
|
57
57
|
response = generate(input_list:, current_conversation:)
|
58
58
|
response.generations.to_h do |generation|
|
59
|
-
[output_key, generation[0].
|
59
|
+
[output_key, generation[0]&.text.to_s]
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
@@ -86,8 +86,12 @@ module Boxcars
|
|
86
86
|
conversation = nil
|
87
87
|
answer = nil
|
88
88
|
4.times do
|
89
|
-
text = predict(current_conversation: conversation, **prediction_variables(inputs)).strip
|
90
|
-
answer =
|
89
|
+
text = predict(current_conversation: conversation, **prediction_variables(inputs)).to_s.strip
|
90
|
+
answer = if text.empty?
|
91
|
+
Result.from_error("Empty response from engine")
|
92
|
+
else
|
93
|
+
get_answer(text)
|
94
|
+
end
|
91
95
|
if answer.status == :error
|
92
96
|
Boxcars.debug "have error, trying again: #{answer.answer}", :red
|
93
97
|
conversation ||= Conversation.new
|
@@ -7,6 +7,7 @@ module Boxcars
|
|
7
7
|
# A engine that uses GeminiAI's API via an OpenAI-compatible interface.
|
8
8
|
class GeminiAi < Engine
|
9
9
|
include UnifiedObservability
|
10
|
+
|
10
11
|
attr_reader :prompts, :llm_params, :model_kwargs, :batch_size # Corrected typo llm_parmas to llm_params
|
11
12
|
|
12
13
|
DEFAULT_PARAMS = {
|
data/lib/boxcars/engine/groq.rb
CHANGED
@@ -7,6 +7,7 @@ module Boxcars
|
|
7
7
|
# A Base class for all Intelligence Engines
|
8
8
|
class IntelligenceBase < Engine
|
9
9
|
include Boxcars::UnifiedObservability
|
10
|
+
|
10
11
|
attr_reader :provider, :all_params
|
11
12
|
|
12
13
|
# The base Intelligence Engine is used by other engines to generate output from prompts
|
@@ -6,11 +6,13 @@ require "securerandom"
|
|
6
6
|
|
7
7
|
module Boxcars
|
8
8
|
# Engine that talks to OpenAI’s REST API.
|
9
|
-
class Openai < Engine
|
9
|
+
class Openai < Engine # rubocop:disable Metrics/ClassLength
|
10
|
+
# include Boxcars::EngineHelpers
|
10
11
|
include UnifiedObservability
|
11
12
|
|
12
13
|
CHAT_MODEL_REGEX = /(^gpt-4)|(-turbo\b)|(^o\d)|(gpt-3\.5-turbo)/
|
13
14
|
O_SERIES_REGEX = /^o/
|
15
|
+
GPT5_MODEL_REGEX = /\Agpt-[56].*/
|
14
16
|
|
15
17
|
DEFAULT_PARAMS = {
|
16
18
|
model: "gpt-4o-mini",
|
@@ -80,9 +82,9 @@ module Boxcars
|
|
80
82
|
def run(question, **)
|
81
83
|
prompt = Prompt.new(template: question)
|
82
84
|
raw_json = client(prompt:, inputs: {}, **)
|
83
|
-
|
84
|
-
|
85
|
-
|
85
|
+
ans = extract_answer(raw_json)
|
86
|
+
Boxcars.debug("Answer: #{ans}", :cyan)
|
87
|
+
ans
|
86
88
|
end
|
87
89
|
|
88
90
|
# Expose the defaults so callers can introspect or dup/merge them
|
@@ -103,14 +105,20 @@ module Boxcars
|
|
103
105
|
# Some callers outside this class still invoke `validate_response!` directly.
|
104
106
|
# It simply raises if the JSON body contains an "error" payload.
|
105
107
|
def validate_response!(response, must_haves: %w[choices])
|
106
|
-
|
108
|
+
if response.is_a?(Hash) && response.key?("output")
|
109
|
+
super(response, must_haves: %w[output])
|
110
|
+
else
|
111
|
+
super
|
112
|
+
end
|
107
113
|
end
|
108
114
|
|
109
115
|
private
|
110
116
|
|
111
117
|
# -- Request construction ---------------------------------------------------
|
112
118
|
def build_api_request(prompt_object, inputs, params, chat:)
|
113
|
-
if
|
119
|
+
if gpt5_model?(params[:model])
|
120
|
+
build_responses_params(prompt_object, inputs, params.dup)
|
121
|
+
elsif chat
|
114
122
|
build_chat_params(prompt_object, inputs, params.dup)
|
115
123
|
else
|
116
124
|
build_completion_params(prompt_object, inputs, params.dup)
|
@@ -133,9 +141,51 @@ module Boxcars
|
|
133
141
|
{ prompt: prompt_txt }.merge(params).tap { |h| h.delete(:messages) }
|
134
142
|
end
|
135
143
|
|
144
|
+
def build_responses_params(prompt_object, inputs, params)
|
145
|
+
po = if prompt_object.is_a?(Boxcars::Prompt)
|
146
|
+
prompt_object
|
147
|
+
else
|
148
|
+
Boxcars::Prompt.new(template: prompt_object.to_s)
|
149
|
+
end
|
150
|
+
|
151
|
+
msg_hash = po.as_messages(inputs)
|
152
|
+
messages = msg_hash[:messages].is_a?(Array) ? msg_hash[:messages] : []
|
153
|
+
input_str = messages_to_input(messages)
|
154
|
+
|
155
|
+
p = params.dup
|
156
|
+
p.delete(:messages)
|
157
|
+
p.delete(:response_format)
|
158
|
+
p.delete(:stop)
|
159
|
+
p.delete(:temperature)
|
160
|
+
p[:max_output_tokens] = p.delete(:max_tokens) if p.key?(:max_tokens) && !p.key?(:max_output_tokens)
|
161
|
+
if (effort = p.delete(:reasoning_effort))
|
162
|
+
p[:reasoning] = { effort: effort }
|
163
|
+
end
|
164
|
+
|
165
|
+
formatted = { model: p[:model], input: input_str, _use_responses_api: true }
|
166
|
+
p.each { |k, v| formatted[k] = v unless k == :model }
|
167
|
+
formatted
|
168
|
+
end
|
169
|
+
|
170
|
+
def messages_to_input(messages)
|
171
|
+
return "" unless messages.is_a?(Array)
|
172
|
+
|
173
|
+
messages.map { |m| "#{m[:role]}: #{m[:content]}" }.join("\n")
|
174
|
+
end
|
175
|
+
|
136
176
|
# -- API call / response ----------------------------------------------------
|
137
177
|
def execute_api_call(client, chat_mode, api_request)
|
138
|
-
if
|
178
|
+
if api_request[:_use_responses_api]
|
179
|
+
call_params = api_request.dup
|
180
|
+
call_params.delete(:_use_responses_api)
|
181
|
+
Boxcars.debug("Input after formatting:\n#{call_params[:input]}", :cyan) if Boxcars.configuration.log_prompts
|
182
|
+
unless client.respond_to?(:responses)
|
183
|
+
raise StandardError,
|
184
|
+
"OpenAI Responses API not supported by installed ruby-openai gem. Upgrade ruby-openai to >7.0 version."
|
185
|
+
end
|
186
|
+
|
187
|
+
client.responses.create(parameters: call_params)
|
188
|
+
elsif chat_mode
|
139
189
|
log_messages_debug(api_request[:messages]) if Boxcars.configuration.log_prompts
|
140
190
|
client.chat(parameters: api_request)
|
141
191
|
else
|
@@ -187,8 +237,117 @@ module Boxcars
|
|
187
237
|
raise Error, "OpenAI: Could not extract answer from choices"
|
188
238
|
end
|
189
239
|
|
240
|
+
def extract_answer_from_output(output_items) # rubocop:disable Metrics/PerceivedComplexity,Metrics/MethodLength
|
241
|
+
return nil unless output_items.is_a?(Array) && output_items.any?
|
242
|
+
|
243
|
+
texts = []
|
244
|
+
|
245
|
+
output_items.each do |i| # rubocop:disable Metrics/BlockLength
|
246
|
+
next unless i.is_a?(Hash)
|
247
|
+
|
248
|
+
case i["type"]
|
249
|
+
when "output_text"
|
250
|
+
content = i["content"]
|
251
|
+
if content.is_a?(Array)
|
252
|
+
# rubocop:disable Metrics/BlockNesting
|
253
|
+
texts << content.filter_map { |c|
|
254
|
+
if c.is_a?(Hash)
|
255
|
+
if c["text"].is_a?(String)
|
256
|
+
c["text"]
|
257
|
+
else
|
258
|
+
(c["text"].is_a?(Hash) ? (c["text"]["value"] || c["text"]["text"]) : nil)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
}.join
|
262
|
+
# rubocop:enable Metrics/BlockNesting
|
263
|
+
elsif content.is_a?(String)
|
264
|
+
texts << content
|
265
|
+
end
|
266
|
+
# Some Responses payloads may include a direct "text" field.
|
267
|
+
if i["text"].is_a?(String)
|
268
|
+
texts << i["text"]
|
269
|
+
elsif i["text"].is_a?(Hash)
|
270
|
+
texts << (i["text"]["value"] || i["text"]["text"])
|
271
|
+
end
|
272
|
+
when "message"
|
273
|
+
content = i["content"]
|
274
|
+
if content.is_a?(Array)
|
275
|
+
parts = content.filter_map do |c|
|
276
|
+
next unless c.is_a?(Hash) && ["output_text", "text"].include?(c["type"])
|
277
|
+
|
278
|
+
t = c["text"]
|
279
|
+
if t.is_a?(String)
|
280
|
+
t
|
281
|
+
elsif t.is_a?(Hash)
|
282
|
+
t["value"] || t["text"]
|
283
|
+
elsif c["content"].is_a?(String)
|
284
|
+
c["content"]
|
285
|
+
end
|
286
|
+
end
|
287
|
+
texts << parts.join
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
return nil if texts.empty?
|
293
|
+
|
294
|
+
texts.join("\n").strip
|
295
|
+
end
|
296
|
+
|
297
|
+
def extract_answer(json)
|
298
|
+
if json.is_a?(Hash)
|
299
|
+
if json["output_text"].is_a?(String) && !json["output_text"].strip.empty?
|
300
|
+
return json["output_text"].strip
|
301
|
+
elsif json["output_text"].is_a?(Array)
|
302
|
+
joined = json["output_text"].map do |t|
|
303
|
+
if t.is_a?(String)
|
304
|
+
t
|
305
|
+
elsif t.is_a?(Hash)
|
306
|
+
t["value"] || t["text"] || t["content"]
|
307
|
+
end
|
308
|
+
end.compact.join("\n").strip
|
309
|
+
return joined unless joined.empty?
|
310
|
+
end
|
311
|
+
|
312
|
+
if json["output"].is_a?(Array)
|
313
|
+
out = extract_answer_from_output(json["output"])
|
314
|
+
return out unless out.nil? || out.strip.empty?
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
choices = json["choices"]
|
319
|
+
return extract_answer_from_choices(choices) if choices
|
320
|
+
|
321
|
+
# Fallback: attempt to find any text in nested Responses payloads
|
322
|
+
fallback = deep_extract_texts(json)
|
323
|
+
return fallback unless fallback.nil? || fallback.strip.empty?
|
324
|
+
|
325
|
+
raise Error, "OpenAI: Could not extract answer"
|
326
|
+
end
|
327
|
+
|
328
|
+
def deep_extract_texts(obj)
|
329
|
+
texts = []
|
330
|
+
stack = [obj]
|
331
|
+
while (cur = stack.pop)
|
332
|
+
case cur
|
333
|
+
when Hash
|
334
|
+
texts << cur["output_text"] if cur["output_text"].is_a?(String)
|
335
|
+
texts << cur["text"] if cur["text"].is_a?(String)
|
336
|
+
texts << cur["content"] if cur["content"].is_a?(String)
|
337
|
+
cur.each_value do |v|
|
338
|
+
stack << v if v.is_a?(Hash) || v.is_a?(Array)
|
339
|
+
end
|
340
|
+
when Array
|
341
|
+
cur.each { |v| stack << v if v.is_a?(Hash) || v.is_a?(Array) }
|
342
|
+
end
|
343
|
+
end
|
344
|
+
aggregated = texts.map { |t| t.to_s.strip }.reject(&:empty?).join("\n")
|
345
|
+
aggregated.empty? ? nil : aggregated
|
346
|
+
end
|
347
|
+
|
190
348
|
# -- Utility helpers --------------------------------------------------------
|
191
349
|
def chat_model?(model_name) = CHAT_MODEL_REGEX.match?(model_name)
|
350
|
+
def gpt5_model?(model_name) = GPT5_MODEL_REGEX.match?(model_name.to_s)
|
192
351
|
|
193
352
|
def openai_error_message(json)
|
194
353
|
err = json&.dig("error")
|
@@ -244,7 +403,13 @@ module Boxcars
|
|
244
403
|
prompt: call_ctx[:prompt_object],
|
245
404
|
inputs: call_ctx[:inputs],
|
246
405
|
user_id: user_id,
|
247
|
-
conversation_for_api:
|
406
|
+
conversation_for_api: if api_req.key?(:input)
|
407
|
+
api_req[:input]
|
408
|
+
elsif call_ctx[:is_chat_model]
|
409
|
+
api_req[:messages]
|
410
|
+
else
|
411
|
+
api_req[:prompt]
|
412
|
+
end
|
248
413
|
},
|
249
414
|
response_data: response_data,
|
250
415
|
provider: :openai
|
@@ -8,6 +8,7 @@ module Boxcars
|
|
8
8
|
# A engine that uses PerplexityAI's API.
|
9
9
|
class Perplexityai < Engine
|
10
10
|
include UnifiedObservability
|
11
|
+
|
11
12
|
attr_reader :prompts, :perplexity_params, :model_kwargs, :batch_size
|
12
13
|
|
13
14
|
DEFAULT_PARAMS = { # Renamed from DEFAULT_PER_PARAMS for consistency
|
data/lib/boxcars/engine.rb
CHANGED
@@ -36,7 +36,7 @@ module Boxcars
|
|
36
36
|
def generation_info(sub_choices)
|
37
37
|
sub_choices.map do |choice|
|
38
38
|
Generation.new(
|
39
|
-
text: choice.dig("message", "content") || choice["text"],
|
39
|
+
text: (choice.dig("message", "content") || choice["text"]).to_s,
|
40
40
|
generation_info: {
|
41
41
|
finish_reason: choice.fetch("finish_reason", nil),
|
42
42
|
logprobs: choice.fetch("logprobs", nil)
|
@@ -75,8 +75,12 @@ module Boxcars
|
|
75
75
|
current_choices = api_response_hash["choices"]
|
76
76
|
if current_choices.is_a?(Array)
|
77
77
|
choices.concat(current_choices)
|
78
|
+
elsif api_response_hash["output"]
|
79
|
+
# Synthesize a choice from non-Chat providers (e.g., OpenAI Responses API for GPT-5)
|
80
|
+
synthesized_text = extract_answer(api_response_hash)
|
81
|
+
choices << { "message" => { "content" => synthesized_text }, "finish_reason" => "stop" }
|
78
82
|
else
|
79
|
-
Boxcars.logger&.warn "No 'choices' found in API response: #{api_response_hash.inspect}"
|
83
|
+
Boxcars.logger&.warn "No 'choices' or 'output' found in API response: #{api_response_hash.inspect}"
|
80
84
|
end
|
81
85
|
|
82
86
|
api_usage = api_response_hash["usage"]
|
data/lib/boxcars/engines.rb
CHANGED
@@ -4,7 +4,7 @@ module Boxcars
|
|
4
4
|
# Factory class for creating engine instances based on model names
|
5
5
|
# Provides convenient shortcuts and aliases for different AI models
|
6
6
|
class Engines
|
7
|
-
DEFAULT_MODEL = "gemini-2.5-flash
|
7
|
+
DEFAULT_MODEL = "gemini-2.5-flash"
|
8
8
|
|
9
9
|
# Create an engine instance based on the model name
|
10
10
|
# @param model [String] The model name or alias
|
@@ -36,9 +36,9 @@ module Boxcars
|
|
36
36
|
when "huge", "online_huge", "sonar-huge", "sonar-pro", "sonar_pro"
|
37
37
|
Boxcars::Perplexityai.new(model: "sonar-pro", **kw_args)
|
38
38
|
when "flash", "gemini-flash"
|
39
|
-
Boxcars::GeminiAi.new(model: "gemini-2.5-flash
|
39
|
+
Boxcars::GeminiAi.new(model: "gemini-2.5-flash", **kw_args)
|
40
40
|
when "gemini-pro"
|
41
|
-
Boxcars::GeminiAi.new(model: "gemini-2.5-pro
|
41
|
+
Boxcars::GeminiAi.new(model: "gemini-2.5-pro", **kw_args)
|
42
42
|
when /gemini-/
|
43
43
|
Boxcars::GeminiAi.new(model:, **kw_args)
|
44
44
|
when /-sonar-/
|
@@ -59,8 +59,11 @@ module Boxcars
|
|
59
59
|
# @param kw_args [Hash] Additional arguments to pass to the engine
|
60
60
|
# @return [Boxcars::Engine] An instance of the appropriate engine class
|
61
61
|
def self.json_engine(model: nil, **kw_args)
|
62
|
-
|
63
|
-
|
62
|
+
default_options = { temperature: 0.1 }
|
63
|
+
name = model.to_s
|
64
|
+
blocked = name.start_with?("gpt-5", "llama") || name.match?(/sonnet|opus|sonar/)
|
65
|
+
default_options[:response_format] = { type: "json_object" } unless blocked
|
66
|
+
options = default_options.merge(kw_args)
|
64
67
|
engine(model:, **options)
|
65
68
|
end
|
66
69
|
|
data/lib/boxcars/prompt.rb
CHANGED
@@ -36,9 +36,12 @@ module Boxcars
|
|
36
36
|
def with_conversation(conversation)
|
37
37
|
return self unless conversation
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
Prompt.new(
|
40
|
+
template: "#{template}\n\n#{conversation.message_text}",
|
41
|
+
input_variables: input_variables,
|
42
|
+
other_inputs: other_inputs,
|
43
|
+
output_variables: output_variables
|
44
|
+
)
|
42
45
|
end
|
43
46
|
|
44
47
|
def default_prefixes
|
data/lib/boxcars/result.rb
CHANGED
@@ -43,7 +43,8 @@ module Boxcars
|
|
43
43
|
# @param kwargs [Hash] Any additional kwargs to pass to the result
|
44
44
|
# @return [Boxcars::Result] The result
|
45
45
|
def self.from_text(text, **)
|
46
|
-
|
46
|
+
str = text.to_s
|
47
|
+
answer = str.delete_prefix('"').delete_suffix('"').strip
|
47
48
|
answer = Regexp.last_match(:answer) if answer =~ /^Answer:\s*(?<answer>.*)$/
|
48
49
|
explanation = "Answer: #{answer}"
|
49
50
|
new(status: :ok, answer:, explanation:, **)
|
data/lib/boxcars/version.rb
CHANGED