boxcars 0.2.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,35 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Agent for the MRKL chain
2
4
  module Boxcars
3
5
  # A Train using the zero-shot react method.
4
6
  class ZeroShot < Train
5
7
  attr_reader :boxcars, :observation_prefix, :engine_prefix
6
8
 
7
- # default prompt prefix
8
- PREFIX = "Answer the following questions as best you can. You have access to the following actions:".freeze
9
-
10
- # default prompt instructions
11
- FORMAT_INSTRUCTIONS = <<~FINPUT.freeze
12
- Use the following format:
13
-
14
- Question: the input question you must answer
15
- Thought: you should always think about what to do
16
- Action: the action to take, should be one of [%<boxcar_names>s]
17
- Action Input: the input to the action
18
- Observation: the result of the action
19
- ... (this Thought/Action/Action Input/Observation sequence can repeat N times)
20
- Thought: I now know the final answer
21
- Final Answer: the final answer to the original input question
22
- Next Actions: If you have them, up to three suggested actions for the user to take after getting this answer.
23
- FINPUT
24
-
25
- # default prompt suffix
26
- SUFFIX = <<~SINPUT.freeze
27
- Begin!
28
-
29
- Question: %<input>s
30
- Thought:%<agent_scratchpad>s
31
- SINPUT
32
-
33
9
  # @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
34
10
  # @param engine [Boxcars::Engine] The engine to use for this train.
35
11
  # @param name [String] The name of the train. Defaults to 'Zero Shot'.
@@ -38,26 +14,26 @@ module Boxcars
38
14
  def initialize(boxcars:, engine: nil, name: 'Zero Shot', description: 'Zero Shot Train', prompt: nil)
39
15
  @observation_prefix = 'Observation: '
40
16
  @engine_prefix = 'Thought:'
41
- prompt ||= self.class.create_prompt(boxcars: boxcars)
17
+ prompt ||= my_prompt
42
18
  super(engine: engine, boxcars: boxcars, prompt: prompt, name: name, description: description)
43
19
  end
44
20
 
45
- # Create prompt in the style of the zero shot agent. Without arguments, returns the default prompt.
46
- # @param boxcars [Array<Boxcars::Boxcar>] List of boxcars the agent will have access to, used to format the prompt.
47
- # @param prefix [String] String to put before the main prompt.
48
- # @param suffix [String] String to put after the main prompt.
49
- # @param input_variables [Array<Symbol>] List of input variables the final prompt will expect.
50
- # @return [Boxcars::Prompt] A Prompt with the template assembled from the pieces here.
51
- def self.create_prompt(boxcars:, prefix: PREFIX, suffix: SUFFIX, input_variables: [:input, :agent_scratchpad])
52
- boxcar_strings = boxcars.map { |boxcar| "#{boxcar.name}: #{boxcar.description}" }.join("\n")
53
- boxcar_names = boxcars.map(&:name)
54
- format_instructions = format(FORMAT_INSTRUCTIONS, boxcar_names: boxcar_names.join(", "))
55
- template = [prefix, boxcar_strings, format_instructions, suffix].join("\n\n")
56
- Prompt.new(template: template, input_variables: input_variables)
21
+ # @return Hash The additional variables for this boxcar.
22
+ def prediction_additional
23
+ { boxcar_names: boxcar_names, boxcar_descriptions: boxcar_descriptions }.merge super
24
+ end
25
+
26
+ # Extract the boxcar and input from the engine output.
27
+ # @param text [String] The output from the engine.
28
+ # @return [Array<Boxcars::Boxcar, String>] The boxcar and input.
29
+ def extract_boxcar_and_input(text)
30
+ get_action_and_input(engine_output: text)
57
31
  end
58
32
 
33
+ private
34
+
59
35
  # the final answer action string
60
- FINAL_ANSWER_ACTION = "Final Answer:".freeze
36
+ FINAL_ANSWER_ACTION = "Final Answer:"
61
37
 
62
38
  # Parse out the action and input from the engine output.
63
39
  # @param engine_output [String] The output from the engine.
@@ -69,13 +45,14 @@ module Boxcars
69
45
  # with "Action Input:" should be separated by a newline.
70
46
  if engine_output.include?(FINAL_ANSWER_ACTION)
71
47
  answer = engine_output.split(FINAL_ANSWER_ACTION).last.strip
72
- ['Final Answer', answer]
48
+ Result.new(status: :ok, answer: answer, explanation: engine_output)
73
49
  else
74
50
  # the thought should be the frist line here if it doesn't start with "Action:"
75
51
  thought = engine_output.split(/\n+/).reject(&:empty?).first
76
52
  Boxcars.debug("Though: #{thought}", :cyan)
77
53
  regex = /Action: (?<action>.*)\nAction Input: (?<action_input>.*)/
78
54
  match = regex.match(engine_output)
55
+ # TODO: this should return an error to the results that can be used for corrections
79
56
  raise ValueError, "Could not parse engine output: #{engine_output}" unless match
80
57
 
81
58
  action = match[:action].strip
@@ -84,11 +61,40 @@ module Boxcars
84
61
  end
85
62
  end
86
63
 
87
- # Extract the boxcar and input from the engine output.
88
- # @param text [String] The output from the engine.
89
- # @return [Array<Boxcars::Boxcar, String>] The boxcar and input.
90
- def extract_boxcar_and_input(text)
91
- get_action_and_input(engine_output: text)
64
+ CTEMPLATE = [
65
+ syst("Answer the following questions as best you can. You have access to the following actions:\n",
66
+ "%<boxcar_descriptions>s"),
67
+ syst("Use the following format:\n",
68
+ "Question: the input question you must answer\n",
69
+ "Thought: you should always think about what to do\n",
70
+ "Action: the action to take, should be one of [%<boxcar_names>s]\n",
71
+ "Action Input: the input to the action\n",
72
+ "Observation: the result of the action\n",
73
+ "... (this Thought/Action/Action Input/Observation sequence can repeat N times)\n",
74
+ "Thought: I now know the final answer\n",
75
+ "Final Answer: the final answer to the original input question\n",
76
+ "Next Actions: If you have them, up to 3 suggested actions for the user to take after getting this answer.\n",
77
+ "Begin!"),
78
+ user("Question: %<input>s"),
79
+ assi("Thought: %<agent_scratchpad>s")
80
+ ].freeze
81
+
82
+ def boxcar_names
83
+ @boxcar_names ||= boxcars.map(&:name)
84
+ end
85
+
86
+ def boxcar_descriptions
87
+ @boxcar_descriptions ||= boxcars.map { |boxcar| "#{boxcar.name}: #{boxcar.description}" }.join("\n")
88
+ end
89
+
90
+ # The prompt to use for the train.
91
+ def my_prompt
92
+ @conversation ||= Conversation.new(lines: CTEMPLATE)
93
+ @my_prompt ||= ConversationPrompt.new(
94
+ conversation: @conversation,
95
+ input_variables: [:input],
96
+ other_inputs: [:boxcar_names, :boxcar_descriptions, :agent_scratchpad],
97
+ output_variables: [:answer])
92
98
  end
93
99
  end
94
100
  end
data/lib/boxcars/train.rb CHANGED
@@ -3,23 +3,25 @@
3
3
  module Boxcars
4
4
  # @abstract
5
5
  class Train < EngineBoxcar
6
- attr_reader :engine, :boxcars, :name, :description, :prompt, :return_values, :return_intermediate_steps,
7
- :max_iterations, :early_stopping_method
6
+ attr_reader :boxcars, :return_values, :return_intermediate_steps,
7
+ :max_iterations, :early_stopping_method, :name_to_boxcar_map
8
8
 
9
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
10
  # @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
12
- # @param prompt [String] The prompt to use.
11
+ # @param prompt [Boxcars::Prompt] The prompt to use.
12
+ # @param engine [Boxcars::Engine] The engine to use for this train.
13
+ # @param kwargs [Hash] Additional arguments including: name, description, top_k, return_direct, and stop
13
14
  # @abstract
14
15
  def initialize(boxcars:, prompt:, engine: nil, **kwargs)
15
16
  @boxcars = boxcars
16
- @name = name || self.class.name
17
+ @name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
17
18
  @return_values = [:output]
18
- @return_intermediate_steps = kwargs[:return_intermediate_steps] || false
19
- @max_iterations = kwargs[:max_iterations] || 25
20
- @early_stopping_method = kwargs[:early_stopping_method] || "force"
19
+ @return_intermediate_steps = kwargs.delete(:return_intermediate_steps) || false
20
+ @max_iterations = kwargs.delete(:max_iterations) || 25
21
+ @early_stopping_method = kwargs.delete(:early_stopping_method) || "force"
22
+ kwargs[:stop] ||= ["\n#{observation_prefix}"]
21
23
 
22
- super(prompt: prompt, engine: engine, name: kwargs[:name], description: kwargs[:description])
24
+ super(prompt: prompt, engine: engine, **kwargs)
23
25
  end
24
26
 
25
27
  # Extract the boxcar name and input from the text.
@@ -27,11 +29,6 @@ module Boxcars
27
29
  def extract_boxcar_and_input(text)
28
30
  end
29
31
 
30
- # the stop strings list
31
- def stop
32
- ["\n#{observation_prefix}"]
33
- end
34
-
35
32
  # build the scratchpad for the engine
36
33
  # @param intermediate_steps [Array] The intermediate steps to build the scratchpad from.
37
34
  # @return [String] The scratchpad.
@@ -48,16 +45,20 @@ module Boxcars
48
45
  # @param full_inputs [Hash] The inputs to the engine.
49
46
  # @return [Boxcars::Action] The next action.
50
47
  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)
48
+ full_output = ""
49
+ parsed_output = nil
50
+ loop do
55
51
  full_inputs[:agent_scratchpad] += full_output
56
52
  output = predict(**full_inputs)
57
53
  full_output += output
58
54
  parsed_output = extract_boxcar_and_input(full_output)
55
+ break unless parsed_output.nil?
56
+ end
57
+ if parsed_output.is_a?(Result)
58
+ TrainAction.from_result(boxcar: "Final Answer", result: parsed_output, log: full_output)
59
+ else
60
+ TrainAction.new(boxcar: parsed_output[0], boxcar_input: parsed_output[1], log: full_output)
59
61
  end
60
- TrainAction.new(boxcar: parsed_output[0], boxcar_input: parsed_output[1], log: full_output)
61
62
  end
62
63
 
63
64
  # Given input, decided what to do.
@@ -66,8 +67,7 @@ module Boxcars
66
67
  # @return [Boxcars::Action] Action specifying what boxcar to use.
67
68
  def plan(intermediate_steps, **kwargs)
68
69
  thoughts = construct_scratchpad(intermediate_steps)
69
- new_inputs = { agent_scratchpad: thoughts, stop: stop }
70
- full_inputs = kwargs.merge(new_inputs)
70
+ full_inputs = prediction_additional.merge(kwargs).merge(agent_scratchpad: thoughts)
71
71
  action = get_next_action(full_inputs)
72
72
  return TrainFinish.new({ output: action.boxcar_input }, log: action.log) if action.boxcar == finish_boxcar_name
73
73
 
@@ -184,7 +184,6 @@ module Boxcars
184
184
  # @return [Hash] The output.
185
185
  def call(inputs:)
186
186
  prepare_for_new_call
187
- name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
188
187
  intermediate_steps = []
189
188
  iterations = 0
190
189
  while should_continue?(iterations)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Boxcars
4
4
  # The current version of the gem.
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.2"
6
6
  end
data/lib/boxcars.rb CHANGED
@@ -107,6 +107,7 @@ module Boxcars
107
107
  end
108
108
 
109
109
  # Logging system
110
+ # debug log
110
111
  def self.debug(msg, color = nil, **options)
111
112
  msg = colorize(msg.to_s, color, **options) if color
112
113
  if logger
@@ -116,6 +117,7 @@ module Boxcars
116
117
  end
117
118
  end
118
119
 
120
+ # info log
119
121
  def self.info(msg, color = nil, **options)
120
122
  msg = colorize(msg.to_s, color, **options) if color
121
123
  if logger
@@ -125,6 +127,7 @@ module Boxcars
125
127
  end
126
128
  end
127
129
 
130
+ # warn log
128
131
  def self.warn(msg, color = nil, **options)
129
132
  msg = colorize(msg.to_s, color, **options) if color
130
133
  if logger
@@ -134,6 +137,7 @@ module Boxcars
134
137
  end
135
138
  end
136
139
 
140
+ # error log
137
141
  def self.error(msg, color = nil, **options)
138
142
  msg = colorize(msg.to_s, color, **options) if color
139
143
  if logger
@@ -143,6 +147,7 @@ module Boxcars
143
147
  end
144
148
  end
145
149
 
150
+ # simple colorization
146
151
  def self.colorize(str, color, **options)
147
152
  background = options[:background] || options[:bg] || false
148
153
  style = options[:style]
@@ -157,6 +162,8 @@ end
157
162
 
158
163
  require "boxcars/version"
159
164
  require "boxcars/prompt"
165
+ require "boxcars/conversation_prompt"
166
+ require "boxcars/conversation"
160
167
  require "boxcars/generation"
161
168
  require "boxcars/ruby_repl"
162
169
  require "boxcars/engine"
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.2.0
4
+ version: 0.2.2
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-03-07 00:00:00.000000000 Z
12
+ date: 2023-03-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -109,11 +109,14 @@ files:
109
109
  - lib/boxcars/boxcar/engine_boxcar.rb
110
110
  - lib/boxcars/boxcar/google_search.rb
111
111
  - lib/boxcars/boxcar/sql.rb
112
+ - lib/boxcars/conversation.rb
113
+ - lib/boxcars/conversation_prompt.rb
112
114
  - lib/boxcars/engine.rb
113
115
  - lib/boxcars/engine/engine_result.rb
114
116
  - lib/boxcars/engine/openai.rb
115
117
  - lib/boxcars/generation.rb
116
118
  - lib/boxcars/prompt.rb
119
+ - lib/boxcars/result.rb
117
120
  - lib/boxcars/ruby_repl.rb
118
121
  - lib/boxcars/train.rb
119
122
  - lib/boxcars/train/train_action.rb