boxcars 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,18 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'openai'
3
4
  # Boxcars is a framework for running a series of tools to get an answer to a question.
4
5
  module Boxcars
5
6
  # A engine that uses OpenAI's API.
6
7
  class Openai < Engine
7
8
  attr_reader :prompts, :open_ai_params, :model_kwargs, :batch_size
8
9
 
10
+ # The default parameters to use when asking the engine.
9
11
  DEFAULT_PARAMS = {
10
12
  model: "text-davinci-003",
11
13
  temperature: 0.7,
12
14
  max_tokens: 256
13
15
  }.freeze
14
16
 
17
+ # the default name of the engine
15
18
  DEFAULT_NAME = "OpenAI engine"
19
+ # the default description of the engine
16
20
  DEFAULT_DESCRIPTION = "useful for when you need to use AI to answer questions. " \
17
21
  "You should ask targeted questions"
18
22
 
@@ -30,7 +34,9 @@ module Boxcars
30
34
  end
31
35
 
32
36
  # Get an answer from the engine.
33
- # @param question [String] The question to ask the engine.
37
+ # @param prompt [String] The prompt to use when asking the engine.
38
+ # @param openai_access_token [String] The access token to use when asking the engine.
39
+ # Defaults to Boxcars.configuration.openai_access_token.
34
40
  # @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
35
41
  def client(prompt:, openai_access_token: 'not set', **kwargs)
36
42
  access_token = Boxcars.configuration.openai_access_token(openai_access_token: openai_access_token)
@@ -51,15 +57,20 @@ module Boxcars
51
57
  end
52
58
 
53
59
  # Build extra kwargs from additional params that were passed in.
60
+ # @param values [Hash] The values to build extra kwargs from.
54
61
  def build_extra(values:)
55
62
  values[:model_kw_args] = @open_ai_params.merge(values)
56
63
  values
57
64
  end
58
65
 
66
+ # Get the default parameters for the engine.
59
67
  def default_params
60
68
  open_ai_params
61
69
  end
62
70
 
71
+ # Get generation informaton
72
+ # @param sub_choices [Array<Hash>] The choices to get generation info for.
73
+ # @return [Array<Generation>] The generation information.
63
74
  def generation_info(sub_choices)
64
75
  sub_choices.map do |choice|
65
76
  Generation.new(
@@ -73,18 +84,9 @@ module Boxcars
73
84
  end
74
85
 
75
86
  # Call out to OpenAI's endpoint with k unique prompts.
76
-
77
- # Args:
78
- # prompts: The prompts to pass into the model.
79
- # stop: Optional list of stop words to use when generating.
80
-
81
- # Returns:
82
- # The full engine output.
83
-
84
- # Example:
85
- # .. code-block:: ruby
86
-
87
- # response = openai.generate(["Tell me a joke."])
87
+ # @param prompts [Array<String>] The prompts to pass into the model.
88
+ # @param stop [Array<String>] Optional list of stop words to use when generating.
89
+ # @return [EngineResult] The full engine output.
88
90
  def generate(prompts:, stop: nil)
89
91
  params = {}
90
92
  params[:stop] = stop if stop
@@ -112,21 +114,25 @@ module Boxcars
112
114
  # rubocop:enable Metrics/AbcSize
113
115
  end
114
116
 
117
+ # the identifying parameters for the engine
115
118
  def identifying_params
116
119
  params = { model_name: model_name }
117
120
  params.merge!(default_params)
118
121
  params
119
122
  end
120
123
 
124
+ # the engine type
121
125
  def engine_type
122
126
  "openai"
123
127
  end
124
128
 
125
129
  # calculate the number of tokens used
126
130
  def get_num_tokens(text:)
127
- text.split.length
131
+ text.split.length # TODO: hook up to token counting gem
128
132
  end
129
133
 
134
+ # lookup the context size for a model by name
135
+ # @param modelname [String] The name of the model to lookup.
130
136
  def modelname_to_contextsize(modelname)
131
137
  model_lookup = {
132
138
  'text-davinci-003': 4097,
@@ -140,14 +146,10 @@ module Boxcars
140
146
  end
141
147
 
142
148
  # Calculate the maximum number of tokens possible to generate for a prompt.
143
-
144
- # Args:
145
- # prompt: The prompt to use.
146
-
147
- # Returns:
148
- # The maximum number of tokens possible to generate for a prompt.
149
- def max_tokens_for_prompt(prompt)
150
- num_tokens = get_num_tokens(prompt)
149
+ # @param prompt_text [String] The prompt text to use.
150
+ # @return [Integer] the number of tokens possible to generate.
151
+ def max_tokens_for_prompt(prompt_text)
152
+ num_tokens = get_num_tokens(prompt_text)
151
153
 
152
154
  # get max context size for model by name
153
155
  max_size = modelname_to_contextsize(model_name)
@@ -3,6 +3,8 @@
3
3
  module Boxcars
4
4
  # used by Boxcars to run ruby code
5
5
  class RubyREPL
6
+ # Execute ruby code
7
+ # @param code [String] The code to run
6
8
  def call(code:)
7
9
  puts "RubyREPL: #{code}".colorize(:red)
8
10
  output = ""
@@ -15,6 +17,8 @@ module Boxcars
15
17
  output
16
18
  end
17
19
 
20
+ # Execute ruby code
21
+ # @param command [String] The code to run
18
22
  def run(command)
19
23
  call(code: command)
20
24
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Boxcars
4
- # Conductor's action to take.
5
- class ConductorAction
4
+ # Train's action to take.
5
+ class TrainAction
6
6
  attr_accessor :boxcar, :boxcar_input, :log
7
7
 
8
8
  def initialize(boxcar: nil, boxcar_input: nil, log: nil)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Boxcars
4
- # Conductor's return value
5
- class ConductorFinish
4
+ # Train's return value
5
+ class TrainFinish
6
6
  attr_accessor :return_values, :log
7
7
 
8
8
  def initialize(return_values, log:)
@@ -1,10 +1,13 @@
1
1
  # Agent for the MRKL chain
2
2
  module Boxcars
3
- # A Conductor using the zero-shot react method.
4
- class ZeroShot < Conductor
3
+ # A Train using the zero-shot react method.
4
+ class ZeroShot < Train
5
5
  attr_reader :boxcars, :observation_prefix, :engine_prefix
6
6
 
7
+ # default prompt prefix
7
8
  PREFIX = "Answer the following questions as best you can. You have access to the following actions:".freeze
9
+
10
+ # default prompt instructions
8
11
  FORMAT_INSTRUCTIONS = <<~FINPUT.freeze
9
12
  Use the following format:
10
13
 
@@ -18,6 +21,7 @@ module Boxcars
18
21
  Final Answer: the final answer to the original input question
19
22
  FINPUT
20
23
 
24
+ # default prompt suffix
21
25
  SUFFIX = <<~SINPUT.freeze
22
26
  Begin!
23
27
 
@@ -25,7 +29,11 @@ module Boxcars
25
29
  Thought:%<agent_scratchpad>s
26
30
  SINPUT
27
31
 
28
- def initialize(boxcars:, engine:, name: 'Zero Shot', description: 'Zero Shot Conductor')
32
+ # @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
33
+ # @param engine [Boxcars::Engine] The engine to use for this train.
34
+ # @param name [String] The name of the train. Defaults to 'Zero Shot'.
35
+ # @param description [String] The description of the train. Defaults to 'Zero Shot Train'.
36
+ def initialize(boxcars:, engine: nil, name: 'Zero Shot', description: 'Zero Shot Train')
29
37
  @observation_prefix = 'Observation: '
30
38
  @engine_prefix = 'Thought:'
31
39
  prompt = self.class.create_prompt(boxcars: boxcars)
@@ -33,16 +41,11 @@ module Boxcars
33
41
  end
34
42
 
35
43
  # Create prompt in the style of the zero shot agent.
36
-
37
- # Args:
38
- # boxcars: List of boxcars the agent will have access to, used to format the prompt.
39
- # prefix: String to put before the list of boxcars.
40
- # suffix: String to put after the list of boxcars.
41
- # input_variables: List of input variables the final prompt will expect.
42
-
43
- # Returns:
44
- # A Prompt with the template assembled from the pieces here.
45
-
44
+ # @param boxcars [Array<Boxcars::Boxcar>] List of boxcars the agent will have access to, used to format the prompt.
45
+ # @param prefix [String] String to put before the main prompt.
46
+ # @param suffix [String] String to put after the main prompt.
47
+ # @param input_variables [Array<Symbol>] List of input variables the final prompt will expect.
48
+ # @return [Boxcars::Prompt] A Prompt with the template assembled from the pieces here.
46
49
  def self.create_prompt(boxcars:, prefix: PREFIX, suffix: SUFFIX, input_variables: [:input, :agent_scratchpad])
47
50
  boxcar_strings = boxcars.map { |boxcar| "#{boxcar.name}: #{boxcar.description}" }.join("\n")
48
51
  boxcar_names = boxcars.map(&:name)
@@ -51,9 +54,12 @@ module Boxcars
51
54
  Prompt.new(template: template, input_variables: input_variables)
52
55
  end
53
56
 
57
+ # the final answer action string
54
58
  FINAL_ANSWER_ACTION = "Final Answer:".freeze
55
59
 
56
60
  # Parse out the action and input from the engine output.
61
+ # @param engine_output [String] The output from the engine.
62
+ # @return [Array<String>] The action and input.
57
63
  def get_action_and_input(engine_output:)
58
64
  # NOTE: if you're specifying a custom prompt for the ZeroShotAgent,
59
65
  # you will need to ensure that it meets the following Regex requirements.
@@ -74,6 +80,9 @@ module Boxcars
74
80
  end
75
81
  end
76
82
 
83
+ # Extract the boxcar and input from the engine output.
84
+ # @param text [String] The output from the engine.
85
+ # @return [Array<Boxcars::Boxcar, String>] The boxcar and input.
77
86
  def extract_boxcar_and_input(text)
78
87
  get_action_and_input(engine_output: text)
79
88
  end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ # @abstract
5
+ class Train < EngineBoxcar
6
+ attr_reader :engine, :boxcars, :name, :description, :prompt, :return_values, :return_intermediate_steps,
7
+ :max_iterations, :early_stopping_method
8
+
9
+ # A Train will use a engine to run a series of boxcars.
10
+ # @param engine [Boxcars::Engine] The engine to use for this train.
11
+ # @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
12
+ # @param prompt [String] The prompt to use.
13
+ # @abstract
14
+ def initialize(boxcars:, prompt:, engine: nil, **kwargs)
15
+ @boxcars = boxcars
16
+ @name = name || self.class.name
17
+ @return_values = [:output]
18
+ @return_intermediate_steps = kwargs[:return_intermediate_steps] || false
19
+ @max_iterations = kwargs[:max_iterations]
20
+ @early_stopping_method = kwargs[:early_stopping_method] || "force"
21
+
22
+ super(prompt: prompt, engine: engine, name: kwargs[:name], description: kwargs[:description])
23
+ end
24
+
25
+ # Extract the boxcar name and input from the text.
26
+ # @param text [String] The text to extract from.
27
+ def extract_boxcar_and_input(text)
28
+ end
29
+
30
+ # the stop strings list
31
+ def stop
32
+ ["\n#{observation_prefix}"]
33
+ end
34
+
35
+ # build the scratchpad for the engine
36
+ # @param intermediate_steps [Array] The intermediate steps to build the scratchpad from.
37
+ # @return [String] The scratchpad.
38
+ def construct_scratchpad(intermediate_steps)
39
+ thoughts = ""
40
+ intermediate_steps.each do |action, observation|
41
+ thoughts += action.is_a?(String) ? action : action.log
42
+ thoughts += "\n#{observation_prefix}#{observation}\n#{engine_prefix}"
43
+ end
44
+ thoughts
45
+ end
46
+
47
+ # determine the next action
48
+ # @param full_inputs [Hash] The inputs to the engine.
49
+ # @return [Boxcars::Action] The next action.
50
+ def get_next_action(full_inputs)
51
+ full_output = predict(**full_inputs)
52
+ parsed_output = extract_boxcar_and_input(full_output)
53
+ while parsed_output.nil?
54
+ full_output = _fix_text(full_output)
55
+ full_inputs[:agent_scratchpad] += full_output
56
+ output = predict(**full_inputs)
57
+ full_output += output
58
+ parsed_output = extract_boxcar_and_input(full_output)
59
+ end
60
+ TrainAction.new(boxcar: parsed_output[0], boxcar_input: parsed_output[1], log: full_output)
61
+ end
62
+
63
+ # Given input, decided what to do.
64
+ # @param intermediate_steps [Array<Hash>] The intermediate steps taken so far along with observations.
65
+ # @param kwargs [Hash] User inputs.
66
+ # @return [Boxcars::Action] Action specifying what boxcar to use.
67
+ def plan(intermediate_steps, **kwargs)
68
+ thoughts = construct_scratchpad(intermediate_steps)
69
+ new_inputs = { agent_scratchpad: thoughts, stop: stop }
70
+ full_inputs = kwargs.merge(new_inputs)
71
+ action = get_next_action(full_inputs)
72
+ return TrainFinish.new({ output: action.boxcar_input }, log: action.log) if action.boxcar == finish_boxcar_name
73
+
74
+ action
75
+ end
76
+
77
+ # Prepare the agent for new call, if needed
78
+ def prepare_for_new_call
79
+ end
80
+
81
+ # Name of the boxcar to use to finish the chain
82
+ def finish_boxcar_name
83
+ "Final Answer"
84
+ end
85
+
86
+ # the input keys
87
+ # @return [Array<Symbol>] The input keys.
88
+ def input_keys
89
+ list = prompt.input_variables
90
+ list.delete(:agent_scratchpad)
91
+ list
92
+ end
93
+
94
+ # the output keys
95
+ def output_keys
96
+ return return_values + ["intermediate_steps"] if return_intermediate_steps
97
+
98
+ return_values
99
+ end
100
+
101
+ # should we continue to run?
102
+ # @param iterations [Integer] The number of iterations.
103
+ # @return [Boolean] Whether to continue.
104
+ def should_continue?(iterations)
105
+ return true if max_iterations.nil?
106
+
107
+ iterations < max_iterations
108
+ end
109
+
110
+ # handler before returning
111
+ # @param output [Boxcars::TrainFinish] The output.
112
+ # @param intermediate_steps [Array<Hash>] The intermediate steps.
113
+ # @return [Hash] The final output.
114
+ def pre_return(output, intermediate_steps)
115
+ puts output.log.colorize(:yellow)
116
+ final_output = output.return_values
117
+ final_output["intermediate_steps"] = intermediate_steps if return_intermediate_steps
118
+ final_output
119
+ end
120
+
121
+ # the prefix for the engine
122
+ # @param return_direct [Boolean] Whether to return directly.
123
+ # @return [String] The prefix.
124
+ def engine_prefix(return_direct)
125
+ return_direct ? "" : engine_prefix
126
+ end
127
+
128
+ # validate the prompt
129
+ # @param values [Hash] The values to validate.
130
+ # @return [Hash] The validated values.
131
+ # @raise [RuntimeError] If the prompt is invalid.
132
+ def validate_prompt(values:)
133
+ prompt = values["engine_chain"].prompt
134
+ unless prompt.input_variables.include?(:agent_scratchpad)
135
+ logger.warning("`agent_scratchpad` should be a variable in prompt.input_variables. Not found, adding it at the end.")
136
+ prompt.input_variables.append(:agent_scratchpad)
137
+ case prompt
138
+ when Prompt
139
+ prompt.template += "\n%<agent_scratchpad>s"
140
+ # when FewShotPromptTemplate
141
+ # prompt.suffix += "\n%<agent_scratchpad>s"
142
+ else
143
+ raise ValueError, "Got unexpected prompt type #{type(prompt)}"
144
+ end
145
+ end
146
+ values
147
+ end
148
+
149
+ # get the stopped response
150
+ # @param early_stopping_method [String] The early stopping method.
151
+ # @param intermediate_steps [Array] The intermediate steps.
152
+ # @param kwargs [Hash] extra keword arguments.
153
+ # @return [Boxcars::Action] The action to take.
154
+ def return_stopped_response(early_stopping_method, intermediate_steps, **kwargs)
155
+ case early_stopping_method
156
+ when "force"
157
+ TrainFinish({ output: "Agent stopped due to max iterations." }, "")
158
+ when "generate"
159
+ thoughts = ""
160
+ intermediate_steps.each do |action, observation|
161
+ thoughts += action.log
162
+ thoughts += "\n#{observation_prefix}#{observation}\n#{engine_prefix}"
163
+ end
164
+ thoughts += "\n\nI now need to return a final answer based on the previous steps:"
165
+ new_inputs = { agent_scratchpad: thoughts, stop: _stop }
166
+ full_inputs = kwargs.merge(new_inputs)
167
+ full_output = predict(**full_inputs)
168
+ parsed_output = extract_boxcar_and_input(full_output)
169
+ if parsed_output.nil?
170
+ TrainFinish({ output: full_output }, full_output)
171
+ else
172
+ boxcar, boxcar_input = parsed_output
173
+ if boxcar == finish_boxcar_name
174
+ TrainFinish({ output: boxcar_input }, full_output)
175
+ else
176
+ TrainFinish({ output: full_output }, full_output)
177
+ end
178
+ end
179
+ else
180
+ raise "early_stopping_method should be one of `force` or `generate`, got #{early_stopping_method}"
181
+ end
182
+ end
183
+
184
+ # execute the train train
185
+ # @param inputs [Hash] The inputs.
186
+ # @return [Hash] The output.
187
+ def call(inputs:)
188
+ prepare_for_new_call
189
+ name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
190
+ intermediate_steps = []
191
+ iterations = 0
192
+ while should_continue?(iterations)
193
+ output = plan(intermediate_steps, **inputs)
194
+ return pre_return(output, intermediate_steps) if output.is_a?(TrainFinish)
195
+
196
+ if (boxcar = name_to_boxcar_map[output.boxcar])
197
+ begin
198
+ observation = boxcar.run(output.boxcar_input)
199
+ return_direct = boxcar.return_direct
200
+ rescue StandardError => e
201
+ puts "Error in #{boxcar.name} boxcar#call: #{e}".colorize(:red)
202
+ raise e
203
+ end
204
+ else
205
+ observation = "#{output.boxcar} is not a valid boxcar, try another one."
206
+ return_direct = false
207
+ end
208
+ puts "#Observation: #{observation}".colorize(:green)
209
+ intermediate_steps.append([output, observation])
210
+ if return_direct
211
+ output = TrainFinish.new({ return_values[0] => observation }, "")
212
+ return pre_return(output, intermediate_steps)
213
+ end
214
+ iterations += 1
215
+ end
216
+ output = return_stopped_response(early_stopping_method, intermediate_steps, **inputs)
217
+ pre_return(output, intermediate_steps)
218
+ end
219
+ end
220
+ end
221
+
222
+ require "boxcars/train/train_action"
223
+ require "boxcars/train/train_finish"
224
+ require "boxcars/train/zero_shot"
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Boxcars
4
- VERSION = "0.1.1"
4
+ # The current version of the gem.
5
+ VERSION = "0.1.3"
5
6
  end
data/lib/boxcars.rb CHANGED
@@ -6,12 +6,21 @@ require 'logger'
6
6
  module Boxcars
7
7
  # Error class for all Boxcars errors.
8
8
  class Error < StandardError; end
9
+
10
+ # Error class for all Boxcars configuration errors.
9
11
  class ConfigurationError < Error; end
12
+
13
+ # Error class for all Boxcars argument errors.
10
14
  class ArgumentError < Error; end
15
+
16
+ # Error class for all Boxcars value errors.
11
17
  class ValueError < Error; end
12
18
 
13
19
  # simple string colorization
14
20
  class ::String
21
+ # colorize a string
22
+ # @param color [Symbol] The color to use.
23
+ # @param options [Hash] The options to use.
15
24
  def colorize(color, options = {})
16
25
  background = options[:background] || options[:bg] || false
17
26
  style = options[:style]
@@ -27,12 +36,13 @@ module Boxcars
27
36
  # Configuration contains gem settings
28
37
  class Configuration
29
38
  attr_writer :openai_access_token, :serpapi_api_key
30
- attr_accessor :organization_id, :logger
39
+ attr_accessor :organization_id, :logger, :log_prompts
31
40
 
32
41
  def initialize
33
42
  @organization_id = nil
34
43
  @logger = Rails.logger if defined?(Rails)
35
44
  @logger ||= Logger.new($stdout)
45
+ @log_prompts = false
36
46
  end
37
47
 
38
48
  # @return [String] The OpenAI Access Token either from arg or env.
@@ -84,6 +94,16 @@ module Boxcars
84
94
  def self.configure
85
95
  yield(configuration)
86
96
  end
97
+
98
+ # Return the default Train class.
99
+ def self.default_train
100
+ Boxcars::ZeroShot
101
+ end
102
+
103
+ # Return the default Engine class.
104
+ def self.default_engine
105
+ Boxcars::Openai
106
+ end
87
107
  end
88
108
 
89
109
  require "boxcars/version"
@@ -92,4 +112,4 @@ require "boxcars/generation"
92
112
  require "boxcars/ruby_repl"
93
113
  require "boxcars/engine"
94
114
  require "boxcars/boxcar"
95
- require "boxcars/conductor"
115
+ require "boxcars/train"
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.1.1
4
+ version: 0.1.3
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: 2023-02-16 00:00:00.000000000 Z
12
+ date: 2023-02-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -81,7 +81,7 @@ dependencies:
81
81
  - - "~>"
82
82
  - !ruby/object:Gem::Version
83
83
  version: '3.0'
84
- description: You simply give a number of boxcars to a conductor, and it does the magic.
84
+ description: You simply give a number of boxcars to a train, and it does the magic.
85
85
  email:
86
86
  - hi@boxcars.ai
87
87
  executables: []
@@ -106,17 +106,16 @@ files:
106
106
  - lib/boxcars/boxcar/engine_boxcar.rb
107
107
  - lib/boxcars/boxcar/serp.rb
108
108
  - lib/boxcars/boxcar/sql.rb
109
- - lib/boxcars/conductor.rb
110
- - lib/boxcars/conductor/conductor_action.rb
111
- - lib/boxcars/conductor/conductor_executer.rb
112
- - lib/boxcars/conductor/conductor_finish.rb
113
- - lib/boxcars/conductor/zero_shot.rb
114
109
  - lib/boxcars/engine.rb
115
110
  - lib/boxcars/engine/engine_result.rb
116
111
  - lib/boxcars/engine/openai.rb
117
112
  - lib/boxcars/generation.rb
118
113
  - lib/boxcars/prompt.rb
119
114
  - lib/boxcars/ruby_repl.rb
115
+ - lib/boxcars/train.rb
116
+ - lib/boxcars/train/train_action.rb
117
+ - lib/boxcars/train/train_finish.rb
118
+ - lib/boxcars/train/zero_shot.rb
120
119
  - lib/boxcars/version.rb
121
120
  homepage: https://github.com/BoxcarsAI/boxcars
122
121
  licenses:
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Boxcars
4
- # Consists of an conductor using boxcars.
5
- class ConductorExecuter < EngineBoxcar
6
- attr_accessor :conductor, :boxcars, :return_intermediate_steps, :max_iterations, :early_stopping_method
7
-
8
- # @param conductor [Boxcars::Conductor] The conductor to use.
9
- # @param boxcars [Array<Boxcars::Boxcar>] The boxcars to use.
10
- # @param return_intermediate_steps [Boolean] Whether to return the intermediate steps. Defaults to false.
11
- # @param max_iterations [Integer] The maximum number of iterations to run. Defaults to nil.
12
- # @param early_stopping_method [String] The early stopping method to use. Defaults to "force".
13
- def initialize(conductor:, boxcars:, return_intermediate_steps: false, max_iterations: nil,
14
- early_stopping_method: "force")
15
- @conductor = conductor
16
- @boxcars = boxcars
17
- @return_intermediate_steps = return_intermediate_steps
18
- @max_iterations = max_iterations
19
- @early_stopping_method = early_stopping_method
20
- # def initialize(prompt:, engine:, output_key: "text", name: nil, description: nil)
21
- super(prompt: conductor.prompt, engine: conductor.engine, name: conductor.name, description: conductor.description)
22
- end
23
-
24
- def same_boxcars?(boxcar_names)
25
- conductor.allowed_boxcars.sort == boxcar_names
26
- end
27
-
28
- def validate_boxcars
29
- boxcar_names = boxcars.map(&:name).sort
30
- return if same_boxcars?(boxcar_names)
31
-
32
- raise "Allowed boxcars (#{conductor.allowed_boxcars}) different than provided boxcars (#{boxcar_names})"
33
- end
34
-
35
- def input_keys
36
- conductor.input_keys
37
- end
38
-
39
- def output_keys
40
- return conductor.return_values + ["intermediate_steps"] if return_intermediate_steps
41
-
42
- conductor.return_values
43
- end
44
-
45
- def should_continue?(iterations)
46
- return true if max_iterations.nil?
47
-
48
- iterations < max_iterations
49
- end
50
-
51
- # handler before returning
52
- def pre_return(output, intermediate_steps)
53
- puts output.log.colorize(:yellow)
54
- final_output = output.return_values
55
- final_output["intermediate_steps"] = intermediate_steps if return_intermediate_steps
56
- final_output
57
- end
58
-
59
- def engine_prefix(return_direct)
60
- return_direct ? "" : conductor.engine_prefix
61
- end
62
-
63
- def call(inputs:)
64
- conductor.prepare_for_new_call
65
- name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
66
- intermediate_steps = []
67
- iterations = 0
68
- while should_continue?(iterations)
69
- output = conductor.plan(intermediate_steps, **inputs)
70
- return pre_return(output, intermediate_steps) if output.is_a?(ConductorFinish)
71
-
72
- if (boxcar = name_to_boxcar_map[output.boxcar])
73
- begin
74
- observation = boxcar.run(output.boxcar_input)
75
- return_direct = boxcar.return_direct
76
- rescue StandardError => e
77
- puts "Error in #{boxcar.name} boxcar#call: #{e}".colorize(:red)
78
- raise e
79
- end
80
- else
81
- observation = "#{output.boxcar} is not a valid boxcar, try another one."
82
- return_direct = false
83
- end
84
- puts "#Observation: #{observation}".colorize(:green)
85
- intermediate_steps.append([output, observation])
86
- if return_direct
87
- output = ConductorFinish.new({ conductor.return_values[0] => observation }, "")
88
- return pre_return(output, intermediate_steps)
89
- end
90
- iterations += 1
91
- end
92
- output = conductor.return_stopped_response(early_stopping_method, intermediate_steps, **inputs)
93
- pre_return(output, intermediate_steps)
94
- end
95
- end
96
- end