boxcars 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 967f04279864f1419fbe3e9f02817cc8f5594317c6f2b06dd49d1c5e1baa67a9
4
- data.tar.gz: 156ca722488b91cefce4d03026490434004e1dcbd1f7c7cd714f1d3f6871cbc0
3
+ metadata.gz: ea9c5df0584f588d618c22fc44f8bba2638c0946ba32cea8487429ad6df694f5
4
+ data.tar.gz: a7cd6609da5d63c1b41763fc7c6e8b4e1c8df41992fd133d0bf359b28d50dd0c
5
5
  SHA512:
6
- metadata.gz: 14158dbd32be6d95e35c2331e26d2e8883bc7621585d5f24b54d1fce6ba78b15e3d67df73b9ed13faf1fd5c306b9341cd4705a3ecf9e5beaf9d47f05e7c1f438
7
- data.tar.gz: 90722b9f3b7f34607cd4ee4afe5888c625c5724904943a5160021b48ca1944af5a87269613aca554b595c73c32c923ee9fc442eb8b0271a712fe6aac5702f2ac
6
+ metadata.gz: aac8bd34e6ddca629d26ff0cb892e46de30e1f20db188b56477422e5526e03c80a22e1b06ab7d8263e8db067db6ec1966f34450ab63e98cea3e857aa7eb27286
7
+ data.tar.gz: 10ce3be58a020a5de80c27fdf46cbd7811afc45ac28e0712479b113eb6a34ca6a2f7043711659bb4b0a152e0981ded5a8f76fe0704f1a89751b3c8ec0a843ea8
data/CHANGELOG.md CHANGED
@@ -2,7 +2,15 @@
2
2
 
3
3
  ## [Unreleased](https://github.com/BoxcarsAI/boxcars/tree/HEAD)
4
4
 
5
- [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.1...HEAD)
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)
6
14
 
7
15
  **Implemented enhancements:**
8
16
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- boxcars (0.2.2)
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
 
@@ -151,30 +162,32 @@ module Boxcars
151
162
  output
152
163
  end
153
164
 
154
- def extract_code(code)
155
- case code
156
- when /^```ruby/
157
- code.split('```ruby').last.split('```').first.strip
158
- when /^`(.+)`/
159
- ::Regexp.last_match(1)
160
- else
161
- code
162
- end
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"
163
169
  end
164
170
 
165
171
  def get_active_record_answer(text)
166
- code = extract_code text.split('ARChanges:').first.strip.split('ARCode:').last.strip
167
- changes_code = extract_code text.split('ARChanges:').last.strip if text =~ /^ARChanges:/
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
168
174
  return Result.new(status: :ok, explanation: "code to run", code: code, changes_code: changes_code) if code_only?
169
175
 
170
- 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
171
184
 
172
- output = clean_up_output(run_active_record_code(code))
173
- Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code: code)
174
- rescue SecurityError, ConfigurationError => e
175
- raise e
176
- rescue Boxcars::Error => e
177
- 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
178
191
  end
179
192
 
180
193
  def get_answer(text)
@@ -184,7 +197,8 @@ module Boxcars
184
197
  when /^Answer:/
185
198
  Result.from_text(text)
186
199
  else
187
- Result.from_error("Try answering again. Expected your answer to start with 'ARCode:'. You gave me:\n#{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:\"")
188
202
  end
189
203
  end
190
204
 
@@ -196,18 +210,19 @@ module Boxcars
196
210
  "to at most %<top_k>s results.\n",
197
211
  "Never query for all the columns from a specific model, ",
198
212
  "only ask for the relevant attributes given the question.\n",
199
- "Also, pay attention to which attribute is in which model."),
200
- syst("Use the following format:\n",
213
+ "Also, pay attention to which attribute is in which model.\n\n",
214
+ "Use the following format:\n",
201
215
  "Question: ${{Question here}}\n",
202
- "ARCode: ${{Active Record code to run}} - make sure you use valid code\n",
203
216
  "ARChanges: ${{Active Record code to compute the number of records going to change}} - ",
204
- "Only add this line if the ARCode on the line before will make data changes.\n",
205
- "Answer: ${{Final answer here}}"),
206
- syst("Only use the following Active Record models: %<model_info>s\n",
207
- "Pay attention to use only the attribute names that you can see in the model description. ",
208
- "Be careful to not query for attributes that do not exist.\n"
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"
209
224
  ),
210
- assi("Question: %<question>s")
225
+ user("Question: %<question>s")
211
226
  ].freeze
212
227
 
213
228
  # The prompt to use for the engine.
@@ -65,7 +65,9 @@ module Boxcars
65
65
  # @param kwargs [Hash] A hash of input values to use for the prompt.
66
66
  # @return [String] The output value.
67
67
  def predict(current_conversation: nil, **kwargs)
68
- apply(current_conversation: current_conversation, input_list: [kwargs])[output_keys.first]
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
69
71
  end
70
72
 
71
73
  # check that there is exactly one output key
@@ -84,11 +86,12 @@ module Boxcars
84
86
  conversation = nil
85
87
  answer = nil
86
88
  4.times do
87
- t = predict(current_conversation: conversation, **prediction_variables(inputs)).strip
88
- answer = get_answer(t)
89
+ text = predict(current_conversation: conversation, **prediction_variables(inputs)).strip
90
+ answer = get_answer(text)
89
91
  if answer.status == :error
90
92
  Boxcars.debug "have error, trying again: #{answer.answer}", :red
91
93
  conversation ||= Conversation.new
94
+ conversation.add_assistant(text)
92
95
  conversation.add_user(answer.answer)
93
96
  else
94
97
  Boxcars.debug answer.to_json, :magenta
@@ -120,5 +123,19 @@ module Boxcars
120
123
  def prediction_variables(inputs)
121
124
  prediction_input(inputs).merge(prediction_additional)
122
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
123
140
  end
124
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,7 +90,8 @@ module Boxcars
87
90
  when /^Answer:/
88
91
  Result.from_text(text)
89
92
  else
90
- Result.from_error("Try answering again. Expected your answer to start with 'SQLQuery:'. You gave me:\n#{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
 
@@ -99,8 +103,8 @@ module Boxcars
99
103
  "to return the most interesting examples in the database.\n",
100
104
  "Never query for all the columns from a specific table, only ask for the elevant columns given the question.\n",
101
105
  "Pay attention to use only the column names that you can see in the schema description. Be careful to ",
102
- "not query for columns that do not exist. Also, pay attention to which column is in which table."),
103
- syst("Use the following format:\n",
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",
104
108
  "Question: 'Question here'\n",
105
109
  "SQLQuery: 'SQL Query to run'\n",
106
110
  "SQLResult: 'Result of the SQLQuery'\n",
@@ -49,11 +49,15 @@ module Boxcars
49
49
  else
50
50
  # the thought should be the frist line here if it doesn't start with "Action:"
51
51
  thought = engine_output.split(/\n+/).reject(&:empty?).first
52
- Boxcars.debug("Though: #{thought}", :cyan)
53
- regex = /Action: (?<action>.*)\nAction Input: (?<action_input>.*)/
52
+ Boxcars.debug("Thought: #{thought}", :yellow)
53
+ regex = /Action: (?<action>.*)\n+Action Input: (?<action_input>.*)/
54
54
  match = regex.match(engine_output)
55
- # TODO: this should return an error to the results that can be used for corrections
56
- 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
57
61
 
58
62
  action = match[:action].strip
59
63
  action_input = match[:action_input].strip.delete_prefix('"').delete_suffix('"')
@@ -63,24 +67,25 @@ module Boxcars
63
67
 
64
68
  CTEMPLATE = [
65
69
  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",
70
+ "%<boxcar_descriptions>s\n",
71
+ "Use the following format:\n",
68
72
  "Question: the input question you must answer\n",
69
73
  "Thought: you should always think about what to do\n",
70
- "Action: the action to take, should be one of [%<boxcar_names>s]\n",
74
+ "Action: the action to take, should be one from this list: %<boxcar_names>s\n",
71
75
  "Action Input: the input to the action\n",
72
76
  "Observation: the result of the action\n",
73
77
  "... (this Thought/Action/Action Input/Observation sequence can repeat N times)\n",
74
- "Thought: I now know the final answer\n",
78
+ "Thought: I know the final answer\n",
75
79
  "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",
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",
77
82
  "Begin!"),
78
83
  user("Question: %<input>s"),
79
84
  assi("Thought: %<agent_scratchpad>s")
80
85
  ].freeze
81
86
 
82
87
  def boxcar_names
83
- @boxcar_names ||= boxcars.map(&:name)
88
+ @boxcar_names ||= "[#{boxcars.map(&:name).join(', ')}]"
84
89
  end
85
90
 
86
91
  def boxcar_descriptions
data/lib/boxcars/train.rb CHANGED
@@ -27,6 +27,7 @@ module Boxcars
27
27
  # Extract the boxcar name and input from the text.
28
28
  # @param text [String] The text to extract from.
29
29
  def extract_boxcar_and_input(text)
30
+ Result.new(status: :ok, answer: text, explanation: engine_output)
30
31
  end
31
32
 
32
33
  # build the scratchpad for the engine
@@ -56,6 +57,7 @@ module Boxcars
56
57
  end
57
58
  if parsed_output.is_a?(Result)
58
59
  TrainAction.from_result(boxcar: "Final Answer", result: parsed_output, log: full_output)
60
+ # elsif parsed_output[0] == "Error"
59
61
  else
60
62
  TrainAction.new(boxcar: parsed_output[0], boxcar_input: parsed_output[1], log: full_output)
61
63
  end
@@ -198,6 +200,9 @@ module Boxcars
198
200
  Boxcars.error "Error in #{boxcar.name} boxcar#call: #{e}", :red
199
201
  observation = "Error - #{e}, correct and try again."
200
202
  end
203
+ elsif output.boxcar == :error
204
+ observation = output.log
205
+ return_direct = false
201
206
  else
202
207
  observation = "#{output.boxcar} is not a valid boxcar, try another one."
203
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.2"
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.
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.2
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-16 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