boxcars 0.2.1 → 0.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50a47cf449c063e7ca02cb36ad315008fa6a5813063a24d92ed4c0f94743c12e
4
- data.tar.gz: fad47ad6fefc7d580251edf1675ae575a9f4eebf28fb903a1c82ba2868310bc4
3
+ metadata.gz: ea9c5df0584f588d618c22fc44f8bba2638c0946ba32cea8487429ad6df694f5
4
+ data.tar.gz: a7cd6609da5d63c1b41763fc7c6e8b4e1c8df41992fd133d0bf359b28d50dd0c
5
5
  SHA512:
6
- metadata.gz: '073919902e84d95158573400ebe593d1e5aa72cc1ab6efb0c4cfccaf9806a91a0c55913d9d9ec47dd8b56f7fb81660fdfd81753092e2a3bd590e4ef782c51ba8'
7
- data.tar.gz: 87f1ec995889c7ca107ccf187f1b01874f136787861e5116abc13da5f204f4f3e7e8cf101692fed7285ac875f405e1f8e08701f3785e0c5e83f25d0e5a0b9b15
6
+ metadata.gz: aac8bd34e6ddca629d26ff0cb892e46de30e1f20db188b56477422e5526e03c80a22e1b06ab7d8263e8db067db6ec1966f34450ab63e98cea3e857aa7eb27286
7
+ data.tar.gz: 10ce3be58a020a5de80c27fdf46cbd7811afc45ac28e0712479b113eb6a34ca6a2f7043711659bb4b0a152e0981ded5a8f76fe0704f1a89751b3c8ec0a843ea8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased](https://github.com/BoxcarsAI/boxcars/tree/HEAD)
4
+
5
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.2...HEAD)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - better error handling and retry for Active Record Boxcar [\#39](https://github.com/BoxcarsAI/boxcars/pull/39) ([francis](https://github.com/francis))
10
+
11
+ ## [v0.2.2](https://github.com/BoxcarsAI/boxcars/tree/v0.2.2) (2023-03-16)
12
+
13
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.1...v0.2.2)
14
+
15
+ **Implemented enhancements:**
16
+
17
+ - return a structure from boxcars instead of just a string [\#31](https://github.com/BoxcarsAI/boxcars/issues/31)
18
+ - modify SQL boxcar to take a list of Tables. Handy if you have a large database with many tables. [\#19](https://github.com/BoxcarsAI/boxcars/issues/19)
19
+
20
+ **Closed issues:**
21
+
22
+ - make a new type of prompt that uses conversations [\#38](https://github.com/BoxcarsAI/boxcars/issues/38)
23
+ - Add test coverage [\#8](https://github.com/BoxcarsAI/boxcars/issues/8)
24
+
25
+ ## [v0.2.1](https://github.com/BoxcarsAI/boxcars/tree/v0.2.1) (2023-03-08)
26
+
27
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.0...v0.2.1)
28
+
29
+ **Implemented enhancements:**
30
+
31
+ - add the ability to use the ChatGPT API - a ChatGPT Boxcar::Engine [\#32](https://github.com/BoxcarsAI/boxcars/issues/32)
32
+ - Prompt simplification [\#29](https://github.com/BoxcarsAI/boxcars/issues/29)
33
+
34
+ **Merged pull requests:**
35
+
36
+ - use structured results internally and bring SQL up to parity with ActiveRecord boxcar. [\#36](https://github.com/BoxcarsAI/boxcars/pull/36) ([francis](https://github.com/francis))
37
+
3
38
  ## [v0.2.0](https://github.com/BoxcarsAI/boxcars/tree/v0.2.0) (2023-03-07)
4
39
 
5
40
  [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.8...v0.2.0)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- boxcars (0.2.1)
4
+ boxcars (0.2.3)
5
5
  google_search_results (~> 2.2)
6
6
  ruby-openai (~> 3.0)
7
7
 
@@ -71,13 +71,21 @@ module Boxcars
71
71
 
72
72
  # to be safe, we wrap the code in a transaction and rollback
73
73
  def rollback_after_running
74
- rv = nil
74
+ result = nil
75
+ runtime_exception = nil
75
76
  ::ActiveRecord::Base.transaction do
76
- rv = yield
77
+ begin
78
+ result = yield
79
+ rescue ::NameError, ::Error => e
80
+ Boxcars.error("Error while running code: #{e.message[0..60]} ...", :red)
81
+ runtime_exception = e
82
+ end
77
83
  ensure
78
84
  raise ::ActiveRecord::Rollback
79
85
  end
80
- rv
86
+ raise runtime_exception if runtime_exception
87
+
88
+ result
81
89
  end
82
90
 
83
91
  # check for dangerous code that is outside of ActiveRecord
@@ -122,8 +130,11 @@ module Boxcars
122
130
  return true unless changes&.positive?
123
131
 
124
132
  Boxcars.debug "#{name}(Pending Changes): #{changes}", :yellow
125
- change_str = "#{changes} change#{'s' if changes.to_i > 1}"
126
- raise SecurityError, "Can not run code that makes #{change_str} in read-only mode" if read_only?
133
+ if read_only?
134
+ change_str = "#{changes} change#{'s' if changes.to_i > 1}"
135
+ Boxcars.error("Can not run code that makes #{change_str} in read-only mode", :red)
136
+ return false
137
+ end
127
138
 
128
139
  return approval_callback.call(changes, code) if approval_callback.is_a?(Proc)
129
140
 
@@ -131,6 +142,7 @@ module Boxcars
131
142
  end
132
143
 
133
144
  def run_active_record_code(code)
145
+ code = ::Regexp.last_match(1) if code =~ /`(.+)`/
134
146
  Boxcars.debug code, :yellow
135
147
  if read_only?
136
148
  rollback_after_running do
@@ -150,17 +162,32 @@ module Boxcars
150
162
  output
151
163
  end
152
164
 
165
+ def error_message(err, stage)
166
+ msg = err.message
167
+ msg = ::Regexp.last_match(1) if msg =~ /^(.+)' for #<Boxcars::ActiveRecord/
168
+ "#{stage} Error: #{msg} - please fix \"#{stage}:\" to not have this error"
169
+ end
170
+
153
171
  def get_active_record_answer(text)
154
- code = text[/^ARCode: (.*)/, 1]
155
- changes_code = text[/^ARChanges: (.*)/, 1]
172
+ changes_code = extract_code text.split('ARCode:').first.split('ARChanges:').last.strip if text =~ /^ARChanges:/
173
+ code = extract_code text.split('ARCode:').last.strip
156
174
  return Result.new(status: :ok, explanation: "code to run", code: code, changes_code: changes_code) if code_only?
157
175
 
158
- raise SecurityError, "Permission to run code that makes changes denied" unless approved?(changes_code, code)
176
+ have_approval = false
177
+ begin
178
+ have_approval = approved?(changes_code, code)
179
+ rescue NameError, ::Error => e
180
+ return Result.new(status: :error, explanation: error_message(e, "ARChanges"), changes_code: changes_code)
181
+ end
182
+
183
+ raise SecurityError, "Permission to run code that makes changes denied" unless have_approval
159
184
 
160
- output = clean_up_output(run_active_record_code(code))
161
- Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code: code)
162
- rescue StandardError => e
163
- Result.new(status: :error, answer: nil, explanation: "Error: #{e.message}", code: code)
185
+ begin
186
+ output = clean_up_output(run_active_record_code(code))
187
+ Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code: code)
188
+ rescue NameError, ::Error => e
189
+ Result.new(status: :error, answer: nil, explanation: error_message(e, "ARCode"), code: code)
190
+ end
164
191
  end
165
192
 
166
193
  def get_answer(text)
@@ -170,37 +197,42 @@ module Boxcars
170
197
  when /^Answer:/
171
198
  Result.from_text(text)
172
199
  else
173
- Result.from_error("Unknown format from engine: #{text}")
200
+ Result.from_error("Error: Your answer wasn't formatted properly - try again. I expected your answer to " \
201
+ "start with \"ARChanges:\" or \"ARCode:\"")
174
202
  end
175
203
  end
176
204
 
177
- TEMPLATE = <<~IPT
178
- Given an input question, first create a syntactically correct Rails Active Record code to run,
179
- then look at the results of the code and return the answer. Unless the user specifies
180
- in her question a specific number of examples she wishes to obtain, limit your code
181
- to at most %<top_k>s results.
182
-
183
- Never query for all the columns from a specific model, only ask for a the few relevant attributes given the question.
184
-
185
- Pay attention to use only the attribute names that you can see in the model description. Be careful to not query for attributes that do not exist.
186
- Also, pay attention to which attribute is in which model.
187
-
188
- Use the following format:
189
- Question: "Question here"
190
- ARCode: "Active Record code to run"
191
- ARChanges: "Active Record code to compute the number of records going to change" - Only add this line if the ARCode on the line before will make data changes
192
- Answer: "Final answer here"
193
-
194
- Only use the following Active Record models:
195
- %<model_info>s
196
-
197
- Question: %<question>s
198
- IPT
205
+ CTEMPLATE = [
206
+ syst("You are a Ruby on Rails Active Record code generator"),
207
+ syst("Given an input question, first create a syntactically correct Rails Active Record code to run, ",
208
+ "then look at the results of the code and return the answer. Unless the user specifies ",
209
+ "in her question a specific number of examples she wishes to obtain, limit your code ",
210
+ "to at most %<top_k>s results.\n",
211
+ "Never query for all the columns from a specific model, ",
212
+ "only ask for the relevant attributes given the question.\n",
213
+ "Also, pay attention to which attribute is in which model.\n\n",
214
+ "Use the following format:\n",
215
+ "Question: ${{Question here}}\n",
216
+ "ARChanges: ${{Active Record code to compute the number of records going to change}} - ",
217
+ "Only add this line if the ARCode on the next line will make data changes.\n",
218
+ "ARCode: ${{Active Record code to run}} - make sure you use valid code\n",
219
+ "Answer: ${{Final answer here}}\n\n",
220
+ "Only use the following Active Record models: %<model_info>s\n",
221
+ "Pay attention to use only the attribute names that you can see in the model description.\n",
222
+ "Do not make up variable or attribute names, and do not share variables between the code in ARChanges and ARCode\n",
223
+ "Be careful to not query for attributes that do not exist, and to use the format specified above.\n"
224
+ ),
225
+ user("Question: %<question>s")
226
+ ].freeze
199
227
 
200
228
  # The prompt to use for the engine.
201
229
  def my_prompt
202
- @my_prompt ||= Prompt.new(input_variables: [:question], other_inputs: [:top_k], output_variables: [:answer],
203
- template: TEMPLATE)
230
+ @conversation ||= Conversation.new(lines: CTEMPLATE)
231
+ @my_prompt ||= ConversationPrompt.new(
232
+ conversation: @conversation,
233
+ input_variables: [:question],
234
+ other_inputs: [:top_k],
235
+ output_variables: [:answer])
204
236
  end
205
237
  end
206
238
  end
@@ -21,7 +21,8 @@ module Boxcars
21
21
  private
22
22
 
23
23
  def get_embedded_ruby_answer(text)
24
- code = text[8..-4].split("```").first.strip
24
+ code = text.split("```ruby\n").last.split("```").first.strip
25
+ # code = text[8..-4].split("```").first.strip
25
26
  ruby_executor = Boxcars::RubyREPL.new
26
27
  ruby_executor.call(code: code)
27
28
  end
@@ -33,52 +34,40 @@ module Boxcars
33
34
  when /^Answer:/
34
35
  Result.from_text(text)
35
36
  else
36
- Result.new(status: :error, explanation: "Unknown format from engine: #{text}")
37
+ Result.new(status: :error,
38
+ explanation: "Error: expecting your response to begin with '```ruby'. Try answering the question again.")
37
39
  end
38
40
  end
39
41
 
40
42
  # our template
41
- # rubocop:disable Style/RedundantHeredocDelimiterQuotes
42
- TEMPLATE = <<~'IPT'
43
- You are GPT-3, and you can't do math.
44
- You can do basic math, and your memorization abilities are impressive, but you can't do any complex calculations that a human could not do in their head. You also have an annoying tendency to just make up highly specific, but wrong, answers.
45
- So we hooked you up to a Ruby 3 kernel, and now you can execute code written in the Ruby programming language. If anyone gives you a hard math problem, just use this format and we’ll take care of the rest:
46
-
47
- Question: ${{Question with hard calculation.}}
48
- ```ruby
49
- ${{Code that prints what you need to know}}
50
- ```
51
- ```output
52
- ${{Output of your code}}
53
- ```
54
- Answer: ${{Answer}}
55
-
56
- Otherwise, use this simpler format:
57
-
58
- Question: ${{Question without hard calculation}}
59
- Answer: ${{Answer}}
60
-
61
- Begin.
62
-
63
- Question: What is 37593 * 67?
64
- ```ruby
65
- puts(37593 * 67)
66
- ```
67
- ```output
68
- 2518731
69
- ```
70
- Answer: 2518731
71
-
72
- Question: what is 2518731 + 0?
73
- Answer: 2518731
74
-
75
- Question: %<question>s
76
- IPT
77
- # rubocop:enable Style/RedundantHeredocDelimiterQuotes
43
+ CTEMPLATE = [
44
+ syst("You can do basic math, but for any hard calculations that a human could not do ",
45
+ "in their head, use the following approach instead. ",
46
+ "Return code written in the Ruby programming language that prints the results. ",
47
+ "If anyone gives you a hard math problem, just ",
48
+ "use the following format and we’ll take care of the rest:\n",
49
+ "${{Question with hard calculation.}}\n",
50
+ "reply only with the following format:\n",
51
+ "```ruby\n${{only Ruby code that prints the answer}}\n```\n",
52
+ "```output\n${{Output of your code}}\n```\n\n",
53
+ "Otherwise, you should use this simpler format:\n",
54
+ "${{Question without hard calculation}}\n",
55
+ "Answer: ${{Answer}}\n\n",
56
+ "Do not give an explanation of the answer and make sure your answer starts with either 'Answer:' or '```ruby'."),
57
+ syst("here is a hard example:\n", "the user asks: What is 37593 * 67?\n",
58
+ "your answer: ```ruby\nputs(37593 * 67)\n```\n```output\n2518731\n```\nAnswer: 2518731"),
59
+ syst("basic example:\n", "user asks: What is 2518731 + 0?\n", "you answer: Answer: 2518731"),
60
+ syst("Begin."),
61
+ user("%<question>s")
62
+ ].freeze
78
63
 
79
64
  # The prompt to use for the engine.
80
65
  def my_prompt
81
- @my_prompt ||= Prompt.new(input_variables: [:question], output_variables: [:answer], template: TEMPLATE)
66
+ @conversation ||= Conversation.new(lines: CTEMPLATE)
67
+ @my_prompt ||= ConversationPrompt.new(
68
+ conversation: @conversation,
69
+ input_variables: [:question],
70
+ output_variables: [:answer])
82
71
  end
83
72
  end
84
73
  end
@@ -8,15 +8,14 @@ module Boxcars
8
8
 
9
9
  # A Boxcar is a container for a single tool to run.
10
10
  # @param prompt [Boxcars::Prompt] The prompt to use for this boxcar with sane defaults.
11
- # @param name [String] The name of the boxcar. Defaults to classname.
12
- # @param description [String] A description of the boxcar.
13
11
  # @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a train if nil.
14
- def initialize(prompt:, engine: nil, name: nil, description: nil, **kwargs)
12
+ # @param kwargs [Hash] Additional arguments including: name, description, top_k, return_direct, and stop
13
+ def initialize(prompt:, engine: nil, **kwargs)
15
14
  @prompt = prompt
16
15
  @engine = engine || Boxcars.engine.new
17
- @top_k = kwargs[:top_k] || 5
18
- @stop = kwargs[:stop] || ["Answer:"]
19
- super(name: name, description: description, return_direct: kwargs[:return_direct])
16
+ @top_k = kwargs.delete(:top_k) || 5
17
+ @stop = kwargs.delete(:stop) || ["Answer:"]
18
+ super(**kwargs)
20
19
  end
21
20
 
22
21
  # input keys for the prompt
@@ -34,60 +33,41 @@ module Boxcars
34
33
  prompt.output_variables
35
34
  end
36
35
 
36
+ # the first output key
37
+ def output_key
38
+ output_keys.first
39
+ end
40
+
37
41
  # generate a response from the engine
38
42
  # @param input_list [Array<Hash>] A list of hashes of input values to use for the prompt.
43
+ # @param current_conversation [Boxcars::Conversation] Optional ongoing conversation to use for the prompt.
39
44
  # @return [Boxcars::EngineResult] The result from the engine.
40
- def generate(input_list:)
45
+ def generate(input_list:, current_conversation: nil)
41
46
  stop = input_list[0][:stop]
42
- prompts = []
43
- input_list.each do |inputs|
44
- # prompt.missing_variables?(inputs)
45
- new_prompt = prompt.format(**inputs)
46
- Boxcars.debug("Prompt after formatting:\n#{new_prompt}", :cyan) if Boxcars.configuration.log_prompts
47
- prompts.push(new_prompt)
48
- end
47
+ the_prompt = current_conversation ? prompt.with_conversation(current_conversation) : prompt
48
+ prompts = input_list.map { |inputs| [the_prompt, inputs] }
49
49
  engine.generate(prompts: prompts, stop: stop)
50
50
  end
51
51
 
52
52
  # apply a response from the engine
53
53
  # @param input_list [Array<Hash>] A list of hashes of input values to use for the prompt.
54
+ # @param current_conversation [Boxcars::Conversation] Optional ongoing conversation to use for the prompt.
54
55
  # @return [Hash] A hash of the output key and the output value.
55
- def apply(input_list:)
56
- response = generate(input_list: input_list)
56
+ def apply(input_list:, current_conversation: nil)
57
+ response = generate(input_list: input_list, current_conversation: current_conversation)
57
58
  response.generations.to_h do |generation|
58
59
  [output_keys.first, generation[0].text]
59
60
  end
60
61
  end
61
62
 
62
63
  # predict a response from the engine
64
+ # @param current_conversation [Boxcars::Conversation] Optional ongoing conversation to use for the prompt.
63
65
  # @param kwargs [Hash] A hash of input values to use for the prompt.
64
66
  # @return [String] The output value.
65
- def predict(**kwargs)
66
- apply(input_list: [kwargs])[output_keys.first]
67
- end
68
-
69
- # predict a response from the engine and parse it
70
- # @param kwargs [Hash] A hash of input values to use for the prompt.
71
- # @return [String] The output value.
72
- def predict_and_parse(**kwargs)
73
- result = predict(**kwargs)
74
- if prompt.output_parser
75
- prompt.output_parser.parse(result)
76
- else
77
- result
78
- end
79
- end
80
-
81
- # apply a response from the engine and parse it
82
- # @param input_list [Array<Hash>] A list of hashes of input values to use for the prompt.
83
- # @return [Array<String>] The output values.
84
- def apply_and_parse(input_list:)
85
- result = apply(input_list: input_list)
86
- if prompt.output_parser
87
- result.map { |r| prompt.output_parser.parse(r[output_keys.first]) }
88
- else
89
- result
90
- end
67
+ def predict(current_conversation: nil, **kwargs)
68
+ prediction = apply(current_conversation: current_conversation, input_list: [kwargs])[output_keys.first]
69
+ Boxcars.debug(prediction, :white) if Boxcars.configuration.log_generated
70
+ prediction
91
71
  end
92
72
 
93
73
  # check that there is exactly one output key
@@ -102,10 +82,29 @@ module Boxcars
102
82
  # @param inputs [Hash] The inputs to the boxcar.
103
83
  # @return [Hash] The outputs from the boxcar.
104
84
  def call(inputs:)
105
- t = predict(**prediction_variables(inputs)).strip
106
- answer = get_answer(t)
107
- Boxcars.debug answer.to_json, :magenta
108
- { output_keys.first => answer }
85
+ # if we get errors back, try predicting again giving the errors with the inputs
86
+ conversation = nil
87
+ answer = nil
88
+ 4.times do
89
+ text = predict(current_conversation: conversation, **prediction_variables(inputs)).strip
90
+ answer = get_answer(text)
91
+ if answer.status == :error
92
+ Boxcars.debug "have error, trying again: #{answer.answer}", :red
93
+ conversation ||= Conversation.new
94
+ conversation.add_assistant(text)
95
+ conversation.add_user(answer.answer)
96
+ else
97
+ Boxcars.debug answer.to_json, :magenta
98
+ return { output_keys.first => answer }
99
+ end
100
+ end
101
+ Boxcars.error answer.to_json, :red
102
+ { output_key => "Error: #{answer}" }
103
+ rescue Boxcars::ConfigurationError => e
104
+ raise e
105
+ rescue Boxcars::Error => e
106
+ Boxcars.error e.message, :red
107
+ { output_key => "Error: #{e.message}" }
109
108
  end
110
109
 
111
110
  # @param inputs [Hash] The inputs to the boxcar.
@@ -124,5 +123,19 @@ module Boxcars
124
123
  def prediction_variables(inputs)
125
124
  prediction_input(inputs).merge(prediction_additional)
126
125
  end
126
+
127
+ # remove backticks or triple backticks from the code
128
+ # @param code [String] The code to remove backticks from.
129
+ # @return [String] The code without backticks.
130
+ def extract_code(code)
131
+ case code
132
+ when /^```\w*/
133
+ code.split(/```\w*\n/).last.split('```').first.strip
134
+ when /^`(.+)`/
135
+ ::Regexp.last_match(1)
136
+ else
137
+ code.gsub("`", "")
138
+ end
139
+ end
127
140
  end
128
141
  end
@@ -20,6 +20,8 @@ module Boxcars
20
20
  kwargs[:name] ||= "Database"
21
21
  kwargs[:description] ||= format(SQLDESC, name: name)
22
22
  kwargs[:prompt] ||= my_prompt
23
+ kwargs[:stop] ||= ["SQLResult:"]
24
+
23
25
  super(**kwargs)
24
26
  end
25
27
 
@@ -73,6 +75,7 @@ module Boxcars
73
75
 
74
76
  def get_embedded_sql_answer(text)
75
77
  code = text[/^SQLQuery: (.*)/, 1]
78
+ code = extract_code text.split('SQLQuery:').last.strip
76
79
  Boxcars.debug code, :yellow
77
80
  output = clean_up_output(connection.exec_query(code))
78
81
  Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code: code)
@@ -87,41 +90,37 @@ module Boxcars
87
90
  when /^Answer:/
88
91
  Result.from_text(text)
89
92
  else
90
- Result.from_error("Unknown format from engine: #{text}")
93
+ Result.from_error("Your answer wasn't formatted properly - try again. I expected your answer to " \
94
+ "start with \"SQLQuery:\".")
91
95
  end
92
96
  end
93
97
 
94
- TEMPLATE = <<~IPT
95
- Given an input question, first create a syntactically correct %<dialect>s SQL query to run,
96
- then look at the results of the query and return the answer. Unless the user specifies
97
- in his question a specific number of examples he wishes to obtain, always limit your query
98
- to at most %<top_k>s results using a LIMIT clause. You can order the results by a relevant column
99
- to return the most interesting examples in the database.
100
-
101
- Never query for all the columns from a specific table, only ask for a the few relevant columns given the question.
102
-
103
- Pay attention to use only the column names that you can see in the schema description. Be careful to not query for columns that do not exist.
104
- Also, pay attention to which column is in which table.
105
-
106
- Use the following format:
107
- Question: "Question here"
108
- SQLQuery: "SQL Query to run"
109
- SQLResult: "Result of the SQLQuery"
110
- Answer: "Final answer here"
111
-
112
- Only use the following tables:
113
- %<schema>s
114
-
115
- Question: %<question>s
116
- IPT
98
+ CTEMPLATE = [
99
+ syst("Given an input question, first create a syntactically correct %<dialect>s SQL query to run, ",
100
+ "then look at the results of the query and return the answer. Unless the user specifies ",
101
+ "in her question a specific number of examples he wishes to obtain, always limit your query ",
102
+ "to at most %<top_k>s results using a LIMIT clause. You can order the results by a relevant column ",
103
+ "to return the most interesting examples in the database.\n",
104
+ "Never query for all the columns from a specific table, only ask for the elevant columns given the question.\n",
105
+ "Pay attention to use only the column names that you can see in the schema description. Be careful to ",
106
+ "not query for columns that do not exist. Also, pay attention to which column is in which table.\n",
107
+ "Use the following format:\n",
108
+ "Question: 'Question here'\n",
109
+ "SQLQuery: 'SQL Query to run'\n",
110
+ "SQLResult: 'Result of the SQLQuery'\n",
111
+ "Answer: 'Final answer here'"),
112
+ syst("Only use the following tables:\n%<schema>s"),
113
+ user("Question: %<question>s")
114
+ ].freeze
117
115
 
118
116
  # The prompt to use for the engine.
119
117
  def my_prompt
120
- @my_prompt ||= Prompt.new(
118
+ @conversation ||= Conversation.new(lines: CTEMPLATE)
119
+ @my_prompt ||= ConversationPrompt.new(
120
+ conversation: @conversation,
121
121
  input_variables: [:question],
122
122
  other_inputs: [:top_k, :dialect, :table_info],
123
- output_variables: [:answer],
124
- template: TEMPLATE)
123
+ output_variables: [:answer])
125
124
  end
126
125
  end
127
126
  end
@@ -78,6 +78,22 @@ module Boxcars
78
78
  rv
79
79
  end
80
80
 
81
+ # helpers for conversation prompt building
82
+ # assistant message
83
+ def self.assi(*strs)
84
+ [:assistant, strs.join]
85
+ end
86
+
87
+ # system message
88
+ def self.syst(*strs)
89
+ [:system, strs.join]
90
+ end
91
+
92
+ # user message
93
+ def self.user(*strs)
94
+ [:user, strs.join]
95
+ end
96
+
81
97
  private
82
98
 
83
99
  # Get an answer from the boxcar.
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ # used to keep track of the conversation
5
+ class Conversation
6
+ attr_reader :lines, :show_roles
7
+
8
+ PEOPLE = %i[system user assistant].freeze
9
+
10
+ def initialize(lines: [], show_roles: false)
11
+ @lines = lines
12
+ check_lines(@lines)
13
+ @show_roles = show_roles
14
+ end
15
+
16
+ # check the lines
17
+ def check_lines(lines)
18
+ raise ArgumentError, "Lines must be an array" unless lines.is_a?(Array)
19
+
20
+ lines.each do |ln|
21
+ raise ArgumentError, "Conversation item must be a array" unless ln.is_a?(Array)
22
+ raise ArgumentError, "Conversation item must have 2 items, role and text" unless ln.size == 2
23
+ raise ArgumentError, "Conversation item must have a role #{ln} in (#{PEOPLE})" unless PEOPLE.include? ln[0]
24
+ raise ArgumentError, "Conversation value must be a string" unless ln[1].is_a?(String)
25
+ end
26
+ end
27
+
28
+ # @return [Array] The result as a convesation array
29
+ def to_a
30
+ lines
31
+ end
32
+
33
+ # @return [String] A conversation string
34
+ def to_s
35
+ lines.map { |ln| "#{ln[0]}: #{ln[1]}" }.join("\n")
36
+ end
37
+
38
+ # add assistant text to the conversation at the end
39
+ # @param text [String] The text to add
40
+ def add_assistant(text)
41
+ @lines << [:assistant, text]
42
+ end
43
+
44
+ # add user text to the conversation at the end
45
+ # @param text [String] The text to add
46
+ def add_user(text)
47
+ @lines << [:user, text]
48
+ end
49
+
50
+ # add system text to the conversation at the end
51
+ # @param text [String] The text to add
52
+ def add_system(text)
53
+ @lines << [:system, text]
54
+ end
55
+
56
+ # add multiple lines to the conversation
57
+ def add_lines(lines)
58
+ check_lines(lines)
59
+ @lines += lines
60
+ end
61
+
62
+ # add a conversation to the conversation
63
+ def add_conversation(conversation)
64
+ @lines += conversation.lines
65
+ end
66
+
67
+ # return just the messages for the conversation
68
+ def message_text
69
+ lines.map(&:last).join("\n")
70
+ end
71
+
72
+ # compute the prompt parameters with input substitutions (used for chatGPT)
73
+ # @param inputs [Hash] The inputs to use for the prompt.
74
+ # @return [Hash] The formatted prompt { messages: ...}
75
+ def as_messages(inputs = nil)
76
+ { messages: lines.map { |ln| { role: ln.first, content: ln.last % inputs } } }
77
+ rescue ::KeyError => e
78
+ first_line = e.message.to_s.split("\n").first
79
+ Boxcars.error "Missing prompt input key: #{first_line}"
80
+ raise KeyError, "Prompt format error: #{first_line}"
81
+ end
82
+
83
+ # compute the prompt parameters with input substitutions
84
+ # @param inputs [Hash] The inputs to use for the prompt.
85
+ # @return [Hash] The formatted prompt { prompt: "..."}
86
+ def as_prompt(inputs = nil)
87
+ if show_roles
88
+ lines.map { |ln| format("#{ln.first}: #{ln.last}", inputs) }.join("\n\n")
89
+ else
90
+ lines.map { |ln| format(ln.last, inputs) }.join("\n\n")
91
+ end
92
+ rescue ::KeyError => e
93
+ first_line = e.message.to_s.split("\n").first
94
+ Boxcars.error "Missing prompt input key: #{first_line}"
95
+ raise KeyError, "Prompt format error: #{first_line}"
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ # used by Boxcars that have engine's to create a conversation prompt.
5
+ class ConversationPrompt < Prompt
6
+ attr_reader :conversation
7
+
8
+ # @param conversation [Boxcars::Conversation] The conversation to use for the prompt.
9
+ # @param input_variables [Array<Symbol>] The input vars to use for the prompt. Defaults to [:input]
10
+ # @param other_inputs [Array<Symbol>] The other input vars to use for the prompt. Defaults to []
11
+ # @param output_variables [Array<Symbol>] The output vars to use for the prompt. Defaults to [:output]
12
+ def initialize(conversation:, input_variables: nil, other_inputs: nil, output_variables: nil)
13
+ @conversation = conversation
14
+ super(template: template, input_variables: input_variables, other_inputs: other_inputs, output_variables: output_variables)
15
+ end
16
+
17
+ # prompt for chatGPT params
18
+ # @param inputs [Hash] The inputs to use for the prompt.
19
+ # @return [Hash] The formatted prompt.
20
+ def as_messages(inputs)
21
+ conversation.as_messages(inputs)
22
+ end
23
+
24
+ # prompt for non chatGPT params
25
+ # @param inputs [Hash] The inputs to use for the prompt.
26
+ # @return [Hash] The formatted prompt.
27
+ def as_prompt(inputs)
28
+ { prompt: conversation.as_prompt(inputs) }
29
+ end
30
+
31
+ # tack on the ongoing conversation if present to the prompt
32
+ def with_conversation(conversation)
33
+ return self unless conversation
34
+
35
+ new_prompt = dup
36
+ new_prompt.conversation.add_conversation(conversation)
37
+ new_prompt
38
+ end
39
+ end
40
+ end
@@ -10,7 +10,7 @@ module Boxcars
10
10
  # The default parameters to use when asking the engine.
11
11
  DEFAULT_PARAMS = {
12
12
  model: "gpt-3.5-turbo",
13
- temperature: 0.7,
13
+ temperature: 0.2,
14
14
  max_tokens: 512
15
15
  }.freeze
16
16
 
@@ -38,17 +38,22 @@ module Boxcars
38
38
  # @param openai_access_token [String] The access token to use when asking the engine.
39
39
  # Defaults to Boxcars.configuration.openai_access_token.
40
40
  # @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
41
- def client(prompt:, openai_access_token: nil, **kwargs)
41
+ def client(prompt:, inputs: {}, openai_access_token: nil, **kwargs)
42
42
  access_token = Boxcars.configuration.openai_access_token(openai_access_token: openai_access_token)
43
43
  organization_id = Boxcars.configuration.organization_id
44
44
  clnt = ::OpenAI::Client.new(access_token: access_token, organization_id: organization_id)
45
- the_params = { prompt: prompt }.merge(open_ai_params).merge(kwargs)
46
- if the_params[:model] == "gpt-3.5-turbo"
45
+ params = open_ai_params.merge(kwargs)
46
+ if params[:model] == "gpt-3.5-turbo"
47
47
  prompt = prompt.first if prompt.is_a?(Array)
48
- the_params = { messages: [{ role: "user", content: prompt }] }.merge(open_ai_params).merge(kwargs)
49
- clnt.chat(parameters: the_params)
48
+ params = prompt.as_messages(inputs).merge(params)
49
+ if Boxcars.configuration.log_prompts
50
+ Boxcars.debug(params[:messages].map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
51
+ end
52
+ clnt.chat(parameters: params)
50
53
  else
51
- clnt.completions(parameters: the_params)
54
+ params = prompt.as_prompt(inputs).merge(params)
55
+ Boxcars.debug("Prompt after formatting:\n#{params[:prompt]}", :cyan) if Boxcars.configuration.log_prompts
56
+ clnt.completions(parameters: params)
52
57
  end
53
58
  end
54
59
 
@@ -56,7 +61,8 @@ module Boxcars
56
61
  # @param question [String] The question to ask the engine.
57
62
  # @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
58
63
  def run(question, **kwargs)
59
- response = client(prompt: question, **kwargs)
64
+ prompt = Prompt.new(template: question)
65
+ response = client(prompt: prompt, **kwargs)
60
66
  answer = response["choices"].map { |c| c.dig("message", "content") || c["text"] }.join("\n").strip
61
67
  puts answer
62
68
  answer
@@ -110,6 +116,7 @@ module Boxcars
110
116
 
111
117
  # Call out to OpenAI's endpoint with k unique prompts.
112
118
  # @param prompts [Array<String>] The prompts to pass into the model.
119
+ # @param inputs [Array<String>] The inputs to subsitite into the prompt.
113
120
  # @param stop [Array<String>] Optional list of stop words to use when generating.
114
121
  # @return [EngineResult] The full engine output.
115
122
  def generate(prompts:, stop: nil)
@@ -120,13 +127,14 @@ module Boxcars
120
127
  # Get the token usage from the response.
121
128
  # Includes prompt, completion, and total tokens used.
122
129
  inkeys = %w[completion_tokens prompt_tokens total_tokens].freeze
123
- sub_prompts = prompts.each_slice(batch_size).to_a
124
- sub_prompts.each do |sprompts|
125
- response = client(prompt: sprompts, **params)
126
- check_response(response)
127
- choices.concat(response["choices"])
128
- keys_to_use = inkeys & response["usage"].keys
129
- keys_to_use.each { |key| token_usage[key] = token_usage[key].to_i + response["usage"][key] }
130
+ prompts.each_slice(batch_size) do |sub_prompts|
131
+ sub_prompts.each do |sprompts, inputs|
132
+ response = client(prompt: sprompts, inputs: inputs, **params)
133
+ check_response(response)
134
+ choices.concat(response["choices"])
135
+ usage_keys = inkeys & response["usage"].keys
136
+ usage_keys.each { |key| token_usage[key] = token_usage[key].to_i + response["usage"][key] }
137
+ end
130
138
  end
131
139
 
132
140
  n = params.fetch(:n, 1)
@@ -16,6 +16,31 @@ module Boxcars
16
16
  @output_variables = output_variables || [:output]
17
17
  end
18
18
 
19
+ # compute the prompt parameters with input substitutions (used for chatGPT)
20
+ # @param inputs [Hash] The inputs to use for the prompt.
21
+ # @return [Hash] The formatted prompt { messages: ...}
22
+ def as_prompt(inputs)
23
+ { prompt: format(inputs) }
24
+ end
25
+
26
+ # compute the prompt parameters with input substitutions
27
+ # @param inputs [Hash] The inputs to use for the prompt.
28
+ # @return [Hash] The formatted prompt { prompt: "..."}
29
+ def as_messages(inputs)
30
+ { messages: [{ role: :assistant, content: format(inputs) }] }
31
+ end
32
+
33
+ # tack on the ongoing conversation if present to the prompt
34
+ def with_conversation(conversation)
35
+ return self unless conversation
36
+
37
+ new_prompt = dup
38
+ new_prompt.template += "\n\n#{conversation.message_text}"
39
+ new_prompt
40
+ end
41
+
42
+ private
43
+
19
44
  # format the prompt with the input variables
20
45
  # @param inputs [Hash] The inputs to use for the prompt.
21
46
  # @return [String] The formatted prompt.
@@ -27,43 +52,5 @@ module Boxcars
27
52
  Boxcars.error "Missing prompt input key: #{first_line}"
28
53
  raise KeyError, "Prompt format error: #{first_line}"
29
54
  end
30
-
31
- # check if the template is valid
32
- def template_is_valid?
33
- all_vars = (input_variables + other_inputs + output_variables).sort
34
- template_vars = @template.scan(/%<(\w+)>s/).flatten.map(&:to_sym).sort
35
- all_vars == template_vars
36
- end
37
-
38
- # missing variables in the template
39
- def missing_variables?(inputs)
40
- input_vars = [input_variables, other_inputs].flatten.sort
41
- return if inputs.keys.sort == input_vars
42
-
43
- raise ArgumentError, "Missing expected input keys, got: #{inputs.keys}. Expected: #{input_vars}"
44
- end
45
-
46
- # create a prompt template from examples
47
- # @param examples [String] or [Array<String>] The example(s) to use for the template.
48
- # @param input_variables [Array<Symbol>] The input variables to use for the prompt.
49
- # @param example_separator [String] The separator to use between the examples. Defaults to "\n\n"
50
- # @param prefix [String] The prefix to use for the template. Defaults to ""
51
- def self.from_examples(examples:, suffix:, input_variables:, example_separator: "\n\n", prefix: "", **kwargs)
52
- template = [prefix, examples, suffix].join(example_separator)
53
- other_inputs = kwargs[:other_inputs] || []
54
- output_variables = kwargs[:output_variables] || [:output]
55
- Prompt.new(template: template, input_variables: input_variables, other_inputs: other_inputs,
56
- output_variables: output_variables)
57
- end
58
-
59
- # create a prompt template from a file
60
- # @param path [String] The path to the file to use for the template.
61
- # @param input_variables [Array<Symbol>] The input variables to use for the prompt. Defaults to [:input]
62
- # @param output_variables [Array<Symbol>] The output variables to use for the prompt. Defaults to [:output]
63
- def self.from_file(path:, input_variables: nil, other_inputs: nil, output_variables: nil)
64
- template = File.read(path)
65
- Prompt.new(template: template, input_variables: input_variables, other_inputs: other_inputs,
66
- output_variables: output_variables)
67
- end
68
55
  end
69
56
  end
@@ -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.
@@ -70,14 +46,18 @@ module Boxcars
70
46
  if engine_output.include?(FINAL_ANSWER_ACTION)
71
47
  answer = engine_output.split(FINAL_ANSWER_ACTION).last.strip
72
48
  Result.new(status: :ok, answer: answer, explanation: engine_output)
73
- # ['Final Answer', answer]
74
49
  else
75
50
  # the thought should be the frist line here if it doesn't start with "Action:"
76
51
  thought = engine_output.split(/\n+/).reject(&:empty?).first
77
- Boxcars.debug("Though: #{thought}", :cyan)
78
- regex = /Action: (?<action>.*)\nAction Input: (?<action_input>.*)/
52
+ Boxcars.debug("Thought: #{thought}", :yellow)
53
+ regex = /Action: (?<action>.*)\n+Action Input: (?<action_input>.*)/
79
54
  match = regex.match(engine_output)
80
- raise ValueError, "Could not parse engine output: #{engine_output}" unless match
55
+
56
+ # we have an unexpected output from the engine
57
+ unless match
58
+ return [:error, "You gave me an improperly fomatted answer - try again. For example, if you know the final anwwer, " \
59
+ "start with #{FINAL_ANSWER_ACTION.inspect}"]
60
+ end
81
61
 
82
62
  action = match[:action].strip
83
63
  action_input = match[:action_input].strip.delete_prefix('"').delete_suffix('"')
@@ -85,11 +65,41 @@ module Boxcars
85
65
  end
86
66
  end
87
67
 
88
- # Extract the boxcar and input from the engine output.
89
- # @param text [String] The output from the engine.
90
- # @return [Array<Boxcars::Boxcar, String>] The boxcar and input.
91
- def extract_boxcar_and_input(text)
92
- get_action_and_input(engine_output: text)
68
+ CTEMPLATE = [
69
+ syst("Answer the following questions as best you can. You have access to the following actions:\n",
70
+ "%<boxcar_descriptions>s\n",
71
+ "Use the following format:\n",
72
+ "Question: the input question you must answer\n",
73
+ "Thought: you should always think about what to do\n",
74
+ "Action: the action to take, should be one from this list: %<boxcar_names>s\n",
75
+ "Action Input: the input to the action\n",
76
+ "Observation: the result of the action\n",
77
+ "... (this Thought/Action/Action Input/Observation sequence can repeat N times)\n",
78
+ "Thought: I know the final answer\n",
79
+ "Final Answer: the final answer to the original input question\n",
80
+ "Next Actions: Up to 3 logical suggested next questions for the user to ask after getting this answer.\n",
81
+ "Remember to start a line with \"Final Answer:\" to give me the final answer.\n",
82
+ "Begin!"),
83
+ user("Question: %<input>s"),
84
+ assi("Thought: %<agent_scratchpad>s")
85
+ ].freeze
86
+
87
+ def boxcar_names
88
+ @boxcar_names ||= "[#{boxcars.map(&:name).join(', ')}]"
89
+ end
90
+
91
+ def boxcar_descriptions
92
+ @boxcar_descriptions ||= boxcars.map { |boxcar| "#{boxcar.name}: #{boxcar.description}" }.join("\n")
93
+ end
94
+
95
+ # The prompt to use for the train.
96
+ def my_prompt
97
+ @conversation ||= Conversation.new(lines: CTEMPLATE)
98
+ @my_prompt ||= ConversationPrompt.new(
99
+ conversation: @conversation,
100
+ input_variables: [:input],
101
+ other_inputs: [:boxcar_names, :boxcar_descriptions, :agent_scratchpad],
102
+ output_variables: [:answer])
93
103
  end
94
104
  end
95
105
  end
data/lib/boxcars/train.rb CHANGED
@@ -3,33 +3,31 @@
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.
26
28
  # @param text [String] The text to extract from.
27
29
  def extract_boxcar_and_input(text)
28
- end
29
-
30
- # the stop strings list
31
- def stop
32
- ["\n#{observation_prefix}"]
30
+ Result.new(status: :ok, answer: text, explanation: engine_output)
33
31
  end
34
32
 
35
33
  # build the scratchpad for the engine
@@ -48,16 +46,18 @@ module Boxcars
48
46
  # @param full_inputs [Hash] The inputs to the engine.
49
47
  # @return [Boxcars::Action] The next action.
50
48
  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?
49
+ full_output = ""
50
+ parsed_output = nil
51
+ loop do
54
52
  full_inputs[:agent_scratchpad] += full_output
55
53
  output = predict(**full_inputs)
56
54
  full_output += output
57
55
  parsed_output = extract_boxcar_and_input(full_output)
56
+ break unless parsed_output.nil?
58
57
  end
59
58
  if parsed_output.is_a?(Result)
60
59
  TrainAction.from_result(boxcar: "Final Answer", result: parsed_output, log: full_output)
60
+ # elsif parsed_output[0] == "Error"
61
61
  else
62
62
  TrainAction.new(boxcar: parsed_output[0], boxcar_input: parsed_output[1], log: full_output)
63
63
  end
@@ -69,8 +69,7 @@ module Boxcars
69
69
  # @return [Boxcars::Action] Action specifying what boxcar to use.
70
70
  def plan(intermediate_steps, **kwargs)
71
71
  thoughts = construct_scratchpad(intermediate_steps)
72
- new_inputs = { agent_scratchpad: thoughts, stop: stop }
73
- full_inputs = kwargs.merge(new_inputs)
72
+ full_inputs = prediction_additional.merge(kwargs).merge(agent_scratchpad: thoughts)
74
73
  action = get_next_action(full_inputs)
75
74
  return TrainFinish.new({ output: action.boxcar_input }, log: action.log) if action.boxcar == finish_boxcar_name
76
75
 
@@ -187,7 +186,6 @@ module Boxcars
187
186
  # @return [Hash] The output.
188
187
  def call(inputs:)
189
188
  prepare_for_new_call
190
- name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
191
189
  intermediate_steps = []
192
190
  iterations = 0
193
191
  while should_continue?(iterations)
@@ -202,6 +200,9 @@ module Boxcars
202
200
  Boxcars.error "Error in #{boxcar.name} boxcar#call: #{e}", :red
203
201
  observation = "Error - #{e}, correct and try again."
204
202
  end
203
+ elsif output.boxcar == :error
204
+ observation = output.log
205
+ return_direct = false
205
206
  else
206
207
  observation = "#{output.boxcar} is not a valid boxcar, try another one."
207
208
  return_direct = false
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Boxcars
4
4
  # The current version of the gem.
5
- VERSION = "0.2.1"
5
+ VERSION = "0.2.3"
6
6
  end
data/lib/boxcars.rb CHANGED
@@ -25,12 +25,13 @@ module Boxcars
25
25
  # Configuration contains gem settings
26
26
  class Configuration
27
27
  attr_writer :openai_access_token, :serpapi_api_key
28
- attr_accessor :organization_id, :logger, :log_prompts, :default_train, :default_engine
28
+ attr_accessor :organization_id, :logger, :log_prompts, :log_generated, :default_train, :default_engine
29
29
 
30
30
  def initialize
31
31
  @organization_id = nil
32
32
  @logger = Rails.logger if defined?(Rails)
33
33
  @log_prompts = ENV.fetch("LOG_PROMPTS", false)
34
+ @log_generated = ENV.fetch("LOG_GEN", false)
34
35
  end
35
36
 
36
37
  # @return [String] The OpenAI Access Token either from arg or env.
@@ -107,6 +108,7 @@ module Boxcars
107
108
  end
108
109
 
109
110
  # Logging system
111
+ # debug log
110
112
  def self.debug(msg, color = nil, **options)
111
113
  msg = colorize(msg.to_s, color, **options) if color
112
114
  if logger
@@ -116,6 +118,7 @@ module Boxcars
116
118
  end
117
119
  end
118
120
 
121
+ # info log
119
122
  def self.info(msg, color = nil, **options)
120
123
  msg = colorize(msg.to_s, color, **options) if color
121
124
  if logger
@@ -125,6 +128,7 @@ module Boxcars
125
128
  end
126
129
  end
127
130
 
131
+ # warn log
128
132
  def self.warn(msg, color = nil, **options)
129
133
  msg = colorize(msg.to_s, color, **options) if color
130
134
  if logger
@@ -134,6 +138,7 @@ module Boxcars
134
138
  end
135
139
  end
136
140
 
141
+ # error log
137
142
  def self.error(msg, color = nil, **options)
138
143
  msg = colorize(msg.to_s, color, **options) if color
139
144
  if logger
@@ -143,6 +148,7 @@ module Boxcars
143
148
  end
144
149
  end
145
150
 
151
+ # simple colorization
146
152
  def self.colorize(str, color, **options)
147
153
  background = options[:background] || options[:bg] || false
148
154
  style = options[:style]
@@ -157,6 +163,8 @@ end
157
163
 
158
164
  require "boxcars/version"
159
165
  require "boxcars/prompt"
166
+ require "boxcars/conversation_prompt"
167
+ require "boxcars/conversation"
160
168
  require "boxcars/generation"
161
169
  require "boxcars/ruby_repl"
162
170
  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.1
4
+ version: 0.2.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-03-08 00:00:00.000000000 Z
12
+ date: 2023-03-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -109,6 +109,8 @@ 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