Class: Boxcars::Boxcar Abstract

Inherits:
Object
  • Object
show all
Defined in:
lib/boxcars/boxcar.rb

Overview

This class is abstract.

Constant Summary collapse

SCHEMA_KEY_ALIASES =
{
  additional_properties: "additionalProperties",
  one_of: "oneOf",
  any_of: "anyOf",
  all_of: "allOf"
}.freeze
TYPE_ALIASES =
{
  int: "integer",
  integer: "integer",
  float: "number",
  double: "number",
  decimal: "number",
  number: "number",
  string: "string",
  bool: "boolean",
  boolean: "boolean",
  array: "array",
  object: "object",
  null: "null"
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(description:, name: nil, return_direct: false, parameters: nil) ⇒ Boxcar

A Boxcar is a container for a single tool to run.

Parameters:

  • name (String) (defaults to: nil)

    The name of the boxcar. Defaults to classname.

  • description (String)

    A description of the boxcar.

  • return_direct (Boolean) (defaults to: false)

    If true, return the output of this boxcar directly, without merging it with the inputs.

  • parameters (Hash) (defaults to: nil)

    The parameters for this boxcar.



35
36
37
38
39
40
# File 'lib/boxcars/boxcar.rb', line 35

def initialize(description:, name: nil, return_direct: false, parameters: nil)
  @name = name || self.class.name
  @description = description || @name
  @return_direct = return_direct
  @parameters = parameters || { question: { type: :string, description: "the input question", required: true } }
end

Instance Attribute Details

#descriptionObject (readonly)

Returns the value of attribute description.



6
7
8
# File 'lib/boxcars/boxcar.rb', line 6

def description
  @description
end

#nameObject (readonly)

Returns the value of attribute name.



6
7
8
# File 'lib/boxcars/boxcar.rb', line 6

def name
  @name
end

#parametersObject (readonly)

Returns the value of attribute parameters.



6
7
8
# File 'lib/boxcars/boxcar.rb', line 6

def parameters
  @parameters
end

#return_directObject (readonly)

Returns the value of attribute return_direct.



6
7
8
# File 'lib/boxcars/boxcar.rb', line 6

def return_direct
  @return_direct
end

Class Method Details

.assi(*strs) ⇒ Object

helpers for conversation prompt building assistant message



136
137
138
# File 'lib/boxcars/boxcar.rb', line 136

def self.assi(*strs)
  [:assistant, strs.join]
end

.histObject

history entries



151
152
153
# File 'lib/boxcars/boxcar.rb', line 151

def self.hist
  [:history, ""]
end

.syst(*strs) ⇒ Object

system message



141
142
143
# File 'lib/boxcars/boxcar.rb', line 141

def self.syst(*strs)
  [:system, strs.join]
end

.user(*strs) ⇒ Object

user message



146
147
148
# File 'lib/boxcars/boxcar.rb', line 146

def self.user(*strs)
  [:user, strs.join]
end

Instance Method Details

#apply(input_list:) ⇒ Array<Hash>

Apply the boxcar to a list of inputs. Override this when a subclass can batch requests more efficiently.

Parameters:

  • input_list (Array<Hash>)

    The list of inputs.

Returns:

  • (Array<Hash>)

    One output hash per input hash.



85
86
87
# File 'lib/boxcars/boxcar.rb', line 85

def apply(input_list:)
  input_list.map { |inputs| call(inputs:) }
end

#call(inputs:) ⇒ Hash

Run the core logic for one invocation.

Parameters:

  • inputs (Hash)

    Input values keyed by ‘input_keys`.

Returns:

  • (Hash)

    Output values keyed by ‘output_keys`.

Raises:

  • (NotImplementedError)


77
78
79
# File 'lib/boxcars/boxcar.rb', line 77

def call(inputs:)
  raise NotImplementedError
end

#conductHash

Run the boxcar and return full input/output context. you can pass one or the other, but not both.

Parameters:

  • args (Array)

    The positional arguments to pass to the boxcar.

  • kwargs (Hash)

    The keyword arguments to pass to the boxcar.

Returns:

  • (Hash)

    A hash that includes original inputs and call outputs.



126
127
128
129
130
131
132
# File 'lib/boxcars/boxcar.rb', line 126

def conduct(*, **)
  Boxcars.info "> Entering #{name}#run", :gray, style: :bold
  rv = depart(*, **)
  remember_history(rv)
  Boxcars.info "< Exiting #{name}#run", :gray, style: :bold
  rv
end

#conduct_resultBoxcars::Result?

Alias for ‘run_result` to make intent explicit when callers want full context first.

Parameters:

  • args (Array)

    The positional arguments to pass to the boxcar.

  • kwargs (Hash)

    The keyword arguments to pass to the boxcar.

Returns:

  • (Boxcars::Result, nil)

    Extracted result when this boxcar returns structured output.



117
118
119
# File 'lib/boxcars/boxcar.rb', line 117

def conduct_result(*, **)
  run_result(*, **)
end

#input_keysObject

Input keys this chain expects.



43
44
45
# File 'lib/boxcars/boxcar.rb', line 43

def input_keys
  [:question]
end

#output_keysObject

Output keys this chain expects.



48
49
50
# File 'lib/boxcars/boxcar.rb', line 48

def output_keys
  [:answer]
end

#parameters_json_schemaObject

Convert legacy Boxcar parameter definitions into JSON Schema.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/boxcars/boxcar.rb', line 178

def parameters_json_schema
  props = {}
  required = []

  parameters.each do |param_name, info|
    param_key = param_name.to_s
    props[param_key] = parameter_descriptor_to_json_schema(info)
    required << param_key if parameter_required?(info)
  end

  schema = {
    "type" => "object",
    "properties" => props,
    "additionalProperties" => false
  }
  schema["required"] = required if required.any?
  schema
end

#runObject

Convenience wrapper around ‘conduct` that returns only the first output value. you can pass one or the other, but not both.

Parameters:

  • args (Array)

    The positional arguments to pass to the boxcar.

  • kwargs (Hash)

    The keyword arguments to pass to the boxcar.

Returns:

  • (Object)

    The first output value. If that value is a ‘Boxcars::Result`, this method returns `result.answer`.



95
96
97
98
99
100
101
102
103
# File 'lib/boxcars/boxcar.rb', line 95

def run(*, **)
  rv = conduct(*, **)
  result = Result.extract(rv)
  return result.answer if result
  return rv.output_for(output_keys[0]) if rv.respond_to?(:output_for)
  return rv[output_keys[0]] if rv.is_a?(Hash)

  rv
end

#run_resultBoxcars::Result?

Convenience helper that returns the structured ‘Boxcars::Result` from `conduct`.

Parameters:

  • args (Array)

    The positional arguments to pass to the boxcar.

  • kwargs (Hash)

    The keyword arguments to pass to the boxcar.

Returns:

  • (Boxcars::Result, nil)

    Extracted result when this boxcar returns structured output.



109
110
111
# File 'lib/boxcars/boxcar.rb', line 109

def run_result(*, **)
  Result.extract(conduct(*, **))
end

#schemaObject



155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/boxcars/boxcar.rb', line 155

def schema
  params = parameters.map do |name, info|
    "<param name=#{name.to_s.inspect} data-type=#{info[:type].to_s.inspect} required=\"#{info[:required] == true}\" " \
      "description=#{info[:description].inspect} />"
  end.join("\n")
  <<~SCHEMA.freeze
    <tool name="#{name}" description="#{description}">
      <params>
        #{params}
      </params>
    </tool>
  SCHEMA
end

#tool_call_name(max_length: 64) ⇒ Object

A provider-safe function/tool name for LLM tool-calling APIs.



170
171
172
173
174
175
# File 'lib/boxcars/boxcar.rb', line 170

def tool_call_name(max_length: 64)
  sanitized = name.to_s.gsub(/[^\w-]+/, "_").gsub(/\A_+|_+\z/, "")
  sanitized = "boxcar" if sanitized.empty?
  sanitized = "boxcar_#{sanitized}" unless sanitized.match?(/\A[a-zA-Z_]/)
  sanitized[0, max_length]
end

#tool_definitionObject

Provider-agnostic normalized tool definition.



198
199
200
201
202
203
204
205
# File 'lib/boxcars/boxcar.rb', line 198

def tool_definition
  {
    name: tool_call_name,
    display_name: name,
    description: description,
    input_schema: parameters_json_schema
  }
end

#tool_specObject

OpenAI-compatible tool spec shape (also usable by many compatible providers).



208
209
210
211
212
213
214
215
216
217
# File 'lib/boxcars/boxcar.rb', line 208

def tool_spec
  {
    type: "function",
    function: {
      name: tool_call_name,
      description: description,
      parameters: parameters_json_schema
    }
  }
end

#validate_inputs(inputs:) ⇒ Object

Check that all inputs are present.

Parameters:

  • inputs (Hash)

    The inputs.

Raises:

  • (RuntimeError)

    If the inputs are not the same.



55
56
57
58
59
60
# File 'lib/boxcars/boxcar.rb', line 55

def validate_inputs(inputs:)
  missing_keys = input_keys.reject { |key| key_present?(inputs, key) }
  raise "Missing some input keys: #{missing_keys}" if missing_keys.any?

  inputs
end

#validate_outputs(outputs:) ⇒ Object

check that all outputs are present

Parameters:

  • outputs (Array<String>)

    The output keys.

Raises:

  • (RuntimeError)

    If the outputs are not the same.



65
66
67
68
69
70
71
72
# File 'lib/boxcars/boxcar.rb', line 65

def validate_outputs(outputs:)
  unexpected = outputs.reject do |key|
    output_keys.any? { |expected| same_key?(expected, key) } || same_key?(:log, key)
  end
  return if unexpected.empty?

  raise "Did not get output keys that were expected, got: #{outputs}. Expected: #{output_keys}"
end