boxcars 0.10.1 → 0.10.2
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/CHANGELOG.md +15 -0
- data/Gemfile.lock +1 -1
- data/README.md +6 -5
- data/Rakefile +6 -1
- data/UPGRADING.md +13 -8
- data/lib/boxcars/engine/cohere.rb +66 -6
- data/lib/boxcars/engine/openai.rb +62 -1
- data/lib/boxcars/mcp.rb +11 -6
- data/lib/boxcars/train/{tool_calling_train.rb → tool_train.rb} +4 -1
- data/lib/boxcars/train.rb +1 -1
- data/lib/boxcars/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 224720d4944dee8add70e0cbb0db0768c7db74c2541547e600d485f7923cec20
|
|
4
|
+
data.tar.gz: 8f17b105ad4db7a707c08a6ed00923a20003bd30dbe76ad913e689a9de55bc68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 74c8326c4ba3512466437d2c35056f50f28255f7b97ae5b0b2ea21743751eca30b7ee91040fd59f2b2684900864cc079e76f3f1894299635cad441552f6ba8aa
|
|
7
|
+
data.tar.gz: 31700d6ac344c9d66490cd3957bd96cee085605acc8550aa9f03edc7017ee38662392b3cf19d8d547282806bb82c4b5ae569b3da0fccb8c11633b2c84b9b1a91
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Recent Updates
|
|
6
|
+
|
|
7
|
+
- Tool-calling runtime naming simplified:
|
|
8
|
+
- `Boxcars::ToolTrain` is now the preferred class name.
|
|
9
|
+
- `Boxcars::MCP.tool_train(...)` is the preferred MCP helper.
|
|
10
|
+
- Compatibility aliases remain available for `ToolCallingTrain` / `tool_calling_train`.
|
|
11
|
+
- Added live multi-provider smoke coverage:
|
|
12
|
+
- `spec/boxcars/llms_live_spec.rb` (opt-in via `RUN_LIVE_LLM_SPECS=true`)
|
|
13
|
+
- `rake spec:llms_live`
|
|
14
|
+
- Provider include/skip controls and per-provider timeout overrides for stable live runs.
|
|
15
|
+
- Provider and extraction reliability updates:
|
|
16
|
+
- Cohere default model updated to `command-a-03-2025`.
|
|
17
|
+
- OpenAI-compatible answer extraction hardened for provider-specific message payload shapes.
|
|
18
|
+
- Live smoke defaults updated for Gemini/Google/Cerebras token budgets.
|
|
19
|
+
|
|
5
20
|
### Upgrade Guide (v0.9 -> v1.0 planned)
|
|
6
21
|
|
|
7
22
|
This section tracks the modernization work that is being added in v0.9 with a compatibility window before v1.0 removals.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Inspired by LangChain, Boxcars brings a Ruby-first design that favors practical
|
|
|
19
19
|
|
|
20
20
|
- Tool composability by default: package domain logic as `Boxcar` objects and reuse them across assistants, jobs, and services.
|
|
21
21
|
- Lower cognitive load for Ruby/Rails developers: one consistent programming model (`Boxcar`, `Train`, `Engine`) for controllers, jobs, and service objects instead of one-off wrappers per provider.
|
|
22
|
-
- Multiple orchestration modes: keep legacy text ReAct (`Boxcars::ZeroShot`) or use native provider tool calling (`Boxcars::
|
|
22
|
+
- Multiple orchestration modes: keep legacy text ReAct (`Boxcars::ZeroShot`) or use native provider tool calling (`Boxcars::ToolTrain`).
|
|
23
23
|
- Structured output paths: enforce JSON contracts with JSON Schema through `JSONEngineBoxcar`.
|
|
24
24
|
- MCP-ready integration: connect MCP servers over stdio and merge MCP tools with local Boxcars in one tool-calling runtime.
|
|
25
25
|
- Provider flexibility without a rewrite: use OpenAI, Anthropic, Groq, Gemini, Ollama, Perplexity, and more through shared engine patterns.
|
|
@@ -39,6 +39,7 @@ Notebook migration expectations for the OpenAI client migration are documented i
|
|
|
39
39
|
### Current Upgrade Notes (toward v1.0)
|
|
40
40
|
|
|
41
41
|
- `Boxcars::Openai` now defaults to `gpt-5-mini` and uses the official OpenAI client path.
|
|
42
|
+
- `Boxcars::Cohere` now defaults to `command-a-03-2025` (legacy `command-r*` model IDs were retired by Cohere).
|
|
42
43
|
- Runtime ActiveSupport/ActiveRecord targets are now `~> 8.1`.
|
|
43
44
|
- Swagger workflows now use Faraday guidance. `rest-client` is no longer a Boxcars runtime dependency.
|
|
44
45
|
- `intelligence` and `gpt4all` are now optional dependencies:
|
|
@@ -59,7 +60,7 @@ gem "gpt4all"
|
|
|
59
60
|
All of these concepts are in a module named Boxcars:
|
|
60
61
|
|
|
61
62
|
- Boxcar - an encapsulation that performs something of interest (such as search, math, SQL, an Active Record Query, or an API call to a service). A Boxcar can use an Engine (described below) to do its work, and if not specified but needed, the default Engine is used `Boxcars.engine`.
|
|
62
|
-
- Train - Given a list of Boxcars and optionally an Engine, a Train breaks down a problem into pieces for individual Boxcars to solve. The individual results are then combined until a final answer is found. `Boxcars::ZeroShot` is the legacy text ReAct implementation, and `Boxcars::
|
|
63
|
+
- Train - Given a list of Boxcars and optionally an Engine, a Train breaks down a problem into pieces for individual Boxcars to solve. The individual results are then combined until a final answer is found. `Boxcars::ZeroShot` is the legacy text ReAct implementation, and `Boxcars::ToolTrain` is the newer native tool-calling runtime.
|
|
63
64
|
- Prompt - used by an Engine to generate text results. Our Boxcars have built-in prompts, but you have the flexibility to change or augment them if you so desire.
|
|
64
65
|
- Engine - an entity that generates text from a Prompt. OpenAI's LLM text generator is the default Engine if no other is specified, and you can override the default engine if so desired (`Boxcars.configuration.default_engine`). We have an Engine for Anthropic's Claude API named `Boxcars::Anthropic`, and another Engine for local GPT named `Boxcars::Gpt4allEng` (requires the optional `gpt4all` gem).
|
|
65
66
|
- VectorStore - a place to store and query vectors.
|
|
@@ -246,7 +247,7 @@ engine = Boxcars::Engines.engine(model: "gpt-4o")
|
|
|
246
247
|
mcp_client = Boxcars::MCP.stdio(command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
|
|
247
248
|
|
|
248
249
|
begin
|
|
249
|
-
train = Boxcars::MCP.
|
|
250
|
+
train = Boxcars::MCP.tool_train(
|
|
250
251
|
engine: engine,
|
|
251
252
|
boxcars: [Boxcars::Calculator.new],
|
|
252
253
|
clients: [mcp_client],
|
|
@@ -259,7 +260,7 @@ ensure
|
|
|
259
260
|
end
|
|
260
261
|
```
|
|
261
262
|
|
|
262
|
-
`Boxcars::MCP.
|
|
263
|
+
`Boxcars::MCP.tool_train(...)` combines local Boxcars and MCP-discovered tools into a `Boxcars::ToolTrain`.
|
|
263
264
|
|
|
264
265
|
### More Examples
|
|
265
266
|
See [this](https://github.com/BoxcarsAI/boxcars/blob/main/notebooks/boxcars_examples.ipynb) Jupyter Notebook for more examples.
|
|
@@ -512,7 +513,7 @@ end
|
|
|
512
513
|
Override the model for specific engine instances:
|
|
513
514
|
|
|
514
515
|
```ruby
|
|
515
|
-
# Global default is gemini-flash, but use different models per boxcar
|
|
516
|
+
# Global default is gemini-2.5-flash, but use different models per boxcar
|
|
516
517
|
default_engine = Boxcars::Engines.engine # Uses global default
|
|
517
518
|
gpt_engine = Boxcars::Engines.engine(model: "gpt-4o") # Uses GPT-4o
|
|
518
519
|
claude_engine = Boxcars::Engines.engine(model: "sonnet") # Uses Claude Sonnet
|
data/Rakefile
CHANGED
|
@@ -21,7 +21,7 @@ namespace :spec do
|
|
|
21
21
|
spec/boxcars/engines_spec.rb
|
|
22
22
|
spec/boxcars/engine/capabilities_spec.rb
|
|
23
23
|
spec/boxcars/boxcar_tool_spec.rb
|
|
24
|
-
spec/boxcars/
|
|
24
|
+
spec/boxcars/tool_train_spec.rb
|
|
25
25
|
spec/boxcars/json_engine_boxcar_schema_spec.rb
|
|
26
26
|
spec/boxcars/mcp_helpers_spec.rb
|
|
27
27
|
spec/boxcars/mcp_stdio_client_spec.rb
|
|
@@ -51,6 +51,11 @@ namespace :spec do
|
|
|
51
51
|
sh "bundle exec rspec #{NOTEBOOK_SMOKE_SPECS.join(' ')}"
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
desc "Run live smoke checks for all configured LLM providers (.env supported)"
|
|
55
|
+
task :llms_live do
|
|
56
|
+
sh "RUN_LIVE_LLM_SPECS=true NO_VCR=true bundle exec rspec spec/boxcars/llms_live_spec.rb"
|
|
57
|
+
end
|
|
58
|
+
|
|
54
59
|
desc "Run live notebook compatibility checks (requires OPENAI_ACCESS_TOKEN)"
|
|
55
60
|
task :notebooks_live do
|
|
56
61
|
sh "bundle exec ruby script/notebooks_live_check.rb"
|
data/UPGRADING.md
CHANGED
|
@@ -6,7 +6,7 @@ This guide covers the migration path for the modernization work added in v0.9 an
|
|
|
6
6
|
|
|
7
7
|
v0.9 introduces:
|
|
8
8
|
|
|
9
|
-
- Native tool-calling runtime via `Boxcars::
|
|
9
|
+
- Native tool-calling runtime via `Boxcars::ToolTrain`
|
|
10
10
|
- MCP as a first-class integration path
|
|
11
11
|
- JSON Schema support for `JSONEngineBoxcar`
|
|
12
12
|
- Deprecated model alias warnings with optional strict mode
|
|
@@ -16,6 +16,11 @@ v1.0 is expected to:
|
|
|
16
16
|
- Remove deprecated model aliases
|
|
17
17
|
- Prefer explicit model names (with a small curated alias set)
|
|
18
18
|
|
|
19
|
+
## Provider Model Refresh Notes (v0.10.x)
|
|
20
|
+
|
|
21
|
+
- Cohere retired legacy `command-r*` model IDs. Boxcars now defaults Cohere to `command-a-03-2025`.
|
|
22
|
+
- If you pinned older Cohere IDs (`command-r`, `command-r-plus`, etc.), update to an available model in your Cohere account.
|
|
23
|
+
|
|
19
24
|
## Constructor Cleanup: `prompts:` Removed From Engine Initializers
|
|
20
25
|
|
|
21
26
|
Engine constructors no longer accept a `prompts:` keyword argument.
|
|
@@ -151,7 +156,7 @@ Boxcars::Engines.strict_deprecated_aliases = true
|
|
|
151
156
|
|
|
152
157
|
## 3. Migrate ReAct/Text Trains to Native Tool Calling (Optional, Recommended)
|
|
153
158
|
|
|
154
|
-
Existing `ZeroShot` / XML trains continue to work. `
|
|
159
|
+
Existing `ZeroShot` / XML trains continue to work. `ToolTrain` is the opt-in modern runtime.
|
|
155
160
|
|
|
156
161
|
### Before (legacy text ReAct)
|
|
157
162
|
|
|
@@ -165,7 +170,7 @@ puts train.run("What is 12 * 9 and what is the weather in Austin?")
|
|
|
165
170
|
|
|
166
171
|
```ruby
|
|
167
172
|
boxcars = [Boxcars::Calculator.new, Boxcars::GoogleSearch.new]
|
|
168
|
-
train = Boxcars::
|
|
173
|
+
train = Boxcars::ToolTrain.new(
|
|
169
174
|
boxcars: boxcars,
|
|
170
175
|
engine: Boxcars::Engines.engine(model: "gpt-4o")
|
|
171
176
|
)
|
|
@@ -174,7 +179,7 @@ puts train.run("What is 12 * 9 and what is the weather in Austin?")
|
|
|
174
179
|
|
|
175
180
|
### Notes
|
|
176
181
|
|
|
177
|
-
- `
|
|
182
|
+
- `ToolTrain` requires an engine that supports native tool-calling.
|
|
178
183
|
- OpenAI chat models and OpenAI Responses API (`gpt-5` style) are supported by the current runtime path.
|
|
179
184
|
|
|
180
185
|
## 4. Add MCP Tools (Optional, Recommended)
|
|
@@ -189,7 +194,7 @@ mcp = Boxcars::MCP.stdio(
|
|
|
189
194
|
)
|
|
190
195
|
|
|
191
196
|
begin
|
|
192
|
-
train = Boxcars::MCP.
|
|
197
|
+
train = Boxcars::MCP.tool_train(
|
|
193
198
|
engine: engine,
|
|
194
199
|
boxcars: [Boxcars::Calculator.new],
|
|
195
200
|
clients: [mcp],
|
|
@@ -236,7 +241,7 @@ boxcar = Boxcars::JSONEngineBoxcar.new(json_schema: schema, json_schema_strict:
|
|
|
236
241
|
|
|
237
242
|
1. Replace deprecated aliases in application code.
|
|
238
243
|
2. Enable strict alias mode in CI.
|
|
239
|
-
3. Migrate one workflow from `ZeroShot` to `
|
|
244
|
+
3. Migrate one workflow from `ZeroShot` to `ToolTrain`.
|
|
240
245
|
4. Add MCP tools where they simplify app-specific integrations.
|
|
241
246
|
5. Add JSON Schema to `JSONEngineBoxcar` uses that need reliable structure.
|
|
242
247
|
6. Upgrade to v1.0 after strict mode stays green.
|
|
@@ -266,7 +271,7 @@ This shared factory seam is used by all OpenAI-compatible engines, so client wir
|
|
|
266
271
|
|
|
267
272
|
- Prefer `Boxcars::Engines.engine(...)` or engine classes directly (`Boxcars::Openai`, `Boxcars::Groq`, etc.).
|
|
268
273
|
- Avoid depending on the exact underlying client object class returned inside engine internals.
|
|
269
|
-
- Prefer explicit model names and `
|
|
274
|
+
- Prefer explicit model names and `ToolTrain` for new builds.
|
|
270
275
|
|
|
271
276
|
### OpenAI client defaults (v0.10+)
|
|
272
277
|
|
|
@@ -362,7 +367,7 @@ OPENAI_ACCESS_TOKEN=... bundle exec rake spec:vcr_openai_refresh
|
|
|
362
367
|
|
|
363
368
|
- `Boxcars::Openai` public constructor and `#run` behavior
|
|
364
369
|
- `Boxcars::Engines.engine(model: ...)`
|
|
365
|
-
- `
|
|
370
|
+
- `ToolTrain` usage
|
|
366
371
|
- `JSONEngineBoxcar` usage (including `json_schema:` support)
|
|
367
372
|
- MCP + Boxcar composition APIs
|
|
368
373
|
|
|
@@ -11,7 +11,7 @@ module Boxcars
|
|
|
11
11
|
|
|
12
12
|
# The default parameters to use when asking the engine.
|
|
13
13
|
DEFAULT_PARAMS = {
|
|
14
|
-
model: "command-
|
|
14
|
+
model: "command-a-03-2025",
|
|
15
15
|
max_tokens: 4000,
|
|
16
16
|
max_input_tokens: 1000,
|
|
17
17
|
temperature: 0.2
|
|
@@ -101,6 +101,7 @@ module Boxcars
|
|
|
101
101
|
|
|
102
102
|
if raw_response.status == 200
|
|
103
103
|
parsed_json = normalize_generate_response(JSON.parse(raw_response.body))
|
|
104
|
+
parsed_json["text"] ||= extract_cohere_text(parsed_json)
|
|
104
105
|
|
|
105
106
|
if parsed_json["error"]
|
|
106
107
|
response_data[:success] = false
|
|
@@ -112,9 +113,13 @@ module Boxcars
|
|
|
112
113
|
[]
|
|
113
114
|
end
|
|
114
115
|
input_tokens = parsed_json.dig("meta", "tokens", "input_tokens") ||
|
|
115
|
-
parsed_json.dig("meta", "billed_units", "input_tokens")
|
|
116
|
+
parsed_json.dig("meta", "billed_units", "input_tokens") ||
|
|
117
|
+
parsed_json.dig("usage", "tokens", "input_tokens") ||
|
|
118
|
+
parsed_json.dig("usage", "billed_units", "input_tokens")
|
|
116
119
|
output_tokens = parsed_json.dig("meta", "tokens", "output_tokens") ||
|
|
117
|
-
parsed_json.dig("meta", "billed_units", "output_tokens")
|
|
120
|
+
parsed_json.dig("meta", "billed_units", "output_tokens") ||
|
|
121
|
+
parsed_json.dig("usage", "tokens", "output_tokens") ||
|
|
122
|
+
parsed_json.dig("usage", "billed_units", "output_tokens")
|
|
118
123
|
if input_tokens || output_tokens
|
|
119
124
|
parsed_json["usage"] ||= {
|
|
120
125
|
"prompt_tokens" => input_tokens.to_i,
|
|
@@ -193,23 +198,78 @@ module Boxcars
|
|
|
193
198
|
end
|
|
194
199
|
|
|
195
200
|
def post_cohere_chat(params, api_key)
|
|
196
|
-
cohere_connection(api_key).post
|
|
201
|
+
v2_response = cohere_connection(api_key, version: :v2).post do |req|
|
|
202
|
+
req.body = cohere_v2_payload(params).to_json
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
return v2_response unless v2_response.status.to_i == 404
|
|
206
|
+
|
|
207
|
+
cohere_connection(api_key, version: :v1).post do |req|
|
|
208
|
+
req.body = cohere_v1_payload(params).to_json
|
|
209
|
+
end
|
|
197
210
|
end
|
|
198
211
|
|
|
199
|
-
def cohere_connection(api_key)
|
|
200
|
-
|
|
212
|
+
def cohere_connection(api_key, version:)
|
|
213
|
+
endpoint = version == :v2 ? 'https://api.cohere.com/v2/chat' : 'https://api.cohere.ai/v1/chat'
|
|
214
|
+
Faraday.new(endpoint) do |faraday|
|
|
201
215
|
faraday.request :url_encoded
|
|
202
216
|
faraday.headers['Authorization'] = "Bearer #{api_key}"
|
|
203
217
|
faraday.headers['Content-Type'] = 'application/json'
|
|
204
218
|
end
|
|
205
219
|
end
|
|
206
220
|
|
|
221
|
+
def cohere_v2_payload(params)
|
|
222
|
+
payload = {
|
|
223
|
+
model: params[:model],
|
|
224
|
+
messages: [{ role: "user", content: params[:message].to_s }]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
payload[:max_tokens] = params[:max_tokens] if params.key?(:max_tokens)
|
|
228
|
+
payload[:temperature] = params[:temperature] if params.key?(:temperature)
|
|
229
|
+
payload[:p] = params[:p] if params.key?(:p)
|
|
230
|
+
payload[:k] = params[:k] if params.key?(:k)
|
|
231
|
+
payload[:stop_sequences] = params[:stop_sequences] if params.key?(:stop_sequences)
|
|
232
|
+
payload
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def cohere_v1_payload(params)
|
|
236
|
+
params
|
|
237
|
+
end
|
|
238
|
+
|
|
207
239
|
def ensure_cohere_api_key!(api_key, error_class:, message:)
|
|
208
240
|
return unless api_key.to_s.strip.empty?
|
|
209
241
|
|
|
210
242
|
raise error_class, message
|
|
211
243
|
end
|
|
212
244
|
|
|
245
|
+
def extract_cohere_text(parsed_json)
|
|
246
|
+
text = parsed_json["text"]
|
|
247
|
+
return text if text.is_a?(String) && !text.strip.empty?
|
|
248
|
+
|
|
249
|
+
message_content = parsed_json.dig("message", "content")
|
|
250
|
+
case message_content
|
|
251
|
+
when String
|
|
252
|
+
stripped = message_content.strip
|
|
253
|
+
return stripped unless stripped.empty?
|
|
254
|
+
when Array
|
|
255
|
+
texts = message_content.filter_map do |part|
|
|
256
|
+
next unless part.is_a?(Hash)
|
|
257
|
+
|
|
258
|
+
if part["text"].is_a?(String)
|
|
259
|
+
part["text"]
|
|
260
|
+
elsif part["text"].is_a?(Hash)
|
|
261
|
+
part["text"]["value"] || part["text"]["text"]
|
|
262
|
+
elsif part["content"].is_a?(String)
|
|
263
|
+
part["content"]
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
joined = texts.join("\n").strip
|
|
267
|
+
return joined unless joined.empty?
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
nil
|
|
271
|
+
end
|
|
272
|
+
|
|
213
273
|
def parse_cohere_response_body(body)
|
|
214
274
|
JSON.parse(body, symbolize_names: true)
|
|
215
275
|
end
|
|
@@ -276,7 +276,9 @@ module Boxcars
|
|
|
276
276
|
def extract_answer_from_choices(choices)
|
|
277
277
|
raise Error, "OpenAI: No choices found in response" unless choices.is_a?(Array) && choices.any?
|
|
278
278
|
|
|
279
|
-
content = choices.
|
|
279
|
+
content = choices.filter_map do |choice|
|
|
280
|
+
extract_choice_message_text(choice)
|
|
281
|
+
end
|
|
280
282
|
return content.join("\n").strip unless content.empty?
|
|
281
283
|
|
|
282
284
|
text = choices.map { |c| c["text"] }.compact
|
|
@@ -285,6 +287,65 @@ module Boxcars
|
|
|
285
287
|
raise Error, "OpenAI: Could not extract answer from choices"
|
|
286
288
|
end
|
|
287
289
|
|
|
290
|
+
def extract_choice_message_text(choice)
|
|
291
|
+
return nil unless choice.is_a?(Hash)
|
|
292
|
+
|
|
293
|
+
message = choice["message"]
|
|
294
|
+
return nil unless message.is_a?(Hash)
|
|
295
|
+
|
|
296
|
+
content = message["content"]
|
|
297
|
+
extract_text_from_message_content(content)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def extract_text_from_message_content(content)
|
|
301
|
+
case content
|
|
302
|
+
when String
|
|
303
|
+
stripped = content.strip
|
|
304
|
+
return stripped unless stripped.empty?
|
|
305
|
+
when Array
|
|
306
|
+
texts = content.filter_map do |part|
|
|
307
|
+
extract_text_from_message_part(part)
|
|
308
|
+
end
|
|
309
|
+
joined = texts.join("\n").strip
|
|
310
|
+
return joined unless joined.empty?
|
|
311
|
+
when Hash
|
|
312
|
+
text = content["text"] || content["content"] || content["value"]
|
|
313
|
+
if text.is_a?(String)
|
|
314
|
+
stripped = text.strip
|
|
315
|
+
return stripped unless stripped.empty?
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
nil
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def extract_text_from_message_part(part)
|
|
323
|
+
return nil unless part.is_a?(Hash)
|
|
324
|
+
|
|
325
|
+
text = part["text"]
|
|
326
|
+
if text.is_a?(String)
|
|
327
|
+
stripped = text.strip
|
|
328
|
+
return stripped unless stripped.empty?
|
|
329
|
+
elsif text.is_a?(Hash)
|
|
330
|
+
value = text["value"] || text["text"]
|
|
331
|
+
if value.is_a?(String)
|
|
332
|
+
stripped = value.strip
|
|
333
|
+
return stripped unless stripped.empty?
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Some providers place text directly on the part.
|
|
338
|
+
%w[content value].each do |field|
|
|
339
|
+
value = part[field]
|
|
340
|
+
next unless value.is_a?(String)
|
|
341
|
+
|
|
342
|
+
stripped = value.strip
|
|
343
|
+
return stripped unless stripped.empty?
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
nil
|
|
347
|
+
end
|
|
348
|
+
|
|
288
349
|
def extract_answer_from_output(output_items) # rubocop:disable Metrics/PerceivedComplexity,Metrics/MethodLength
|
|
289
350
|
return nil unless output_items.is_a?(Array) && output_items.any?
|
|
290
351
|
|
data/lib/boxcars/mcp.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Boxcars
|
|
|
11
11
|
StdioClient.new(command:, args:, **kwargs).connect!
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
# Build a
|
|
14
|
+
# Build a ToolTrain from local Boxcars plus tools discovered from
|
|
15
15
|
# one or more MCP clients.
|
|
16
16
|
#
|
|
17
17
|
# @param engine [Boxcars::Engine] Tool-calling capable engine (required)
|
|
@@ -19,10 +19,10 @@ module Boxcars
|
|
|
19
19
|
# @param clients [Array<Boxcars::MCP::Client>] MCP clients to discover tools from
|
|
20
20
|
# @param client_name_prefixes [Hash,Integer=>String] Optional prefixes by client index or object_id
|
|
21
21
|
# @param mcp_return_direct [Boolean] Whether discovered MCP boxcars return direct
|
|
22
|
-
# @param train_kwargs [Hash] Additional args for Boxcars::
|
|
23
|
-
def self.
|
|
24
|
-
unless defined?(Boxcars::
|
|
25
|
-
raise Boxcars::Error, "Boxcars::
|
|
22
|
+
# @param train_kwargs [Hash] Additional args for Boxcars::ToolTrain
|
|
23
|
+
def self.tool_train(engine:, boxcars: [], clients: [], client_name_prefixes: {}, mcp_return_direct: false, **train_kwargs)
|
|
24
|
+
unless defined?(Boxcars::ToolTrain)
|
|
25
|
+
raise Boxcars::Error, "Boxcars::ToolTrain is not loaded. Require 'boxcars' before using MCP helpers."
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
combined_boxcars = Array(boxcars).dup
|
|
@@ -33,7 +33,12 @@ module Boxcars
|
|
|
33
33
|
)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
Boxcars::
|
|
36
|
+
Boxcars::ToolTrain.new(boxcars: combined_boxcars, engine:, **train_kwargs)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Backwards-compatible helper alias for initial v0.10 naming.
|
|
40
|
+
def self.tool_calling_train(...)
|
|
41
|
+
tool_train(...)
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
def self.mcp_client_prefix(client, index, client_name_prefixes)
|
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Boxcars
|
|
6
6
|
# A Train runtime that uses native LLM tool-calling instead of text ReAct parsing.
|
|
7
|
-
class
|
|
7
|
+
class ToolTrain < Train
|
|
8
8
|
attr_accessor :wants_next_actions
|
|
9
9
|
|
|
10
10
|
# Lightweight prompt wrapper so engine adapters can send an exact message list.
|
|
@@ -381,4 +381,7 @@ module Boxcars
|
|
|
381
381
|
)
|
|
382
382
|
end
|
|
383
383
|
end
|
|
384
|
+
|
|
385
|
+
# Backwards-compatible alias for the initial v0.10 naming.
|
|
386
|
+
ToolCallingTrain = ToolTrain
|
|
384
387
|
end
|
data/lib/boxcars/train.rb
CHANGED
data/lib/boxcars/version.rb
CHANGED
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.10.
|
|
4
|
+
version: 0.10.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Francis Sullivan
|
|
@@ -89,7 +89,7 @@ files:
|
|
|
89
89
|
- lib/boxcars/result.rb
|
|
90
90
|
- lib/boxcars/ruby_repl.rb
|
|
91
91
|
- lib/boxcars/train.rb
|
|
92
|
-
- lib/boxcars/train/
|
|
92
|
+
- lib/boxcars/train/tool_train.rb
|
|
93
93
|
- lib/boxcars/train/train_action.rb
|
|
94
94
|
- lib/boxcars/train/train_finish.rb
|
|
95
95
|
- lib/boxcars/train/xml_train.rb
|