boxcars 0.1.8 → 0.2.1

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: 3de375ad403bb62fb6af1045aff6a6115cbb598c3747fe48f882fb40483289e0
4
- data.tar.gz: 343a3077d72ce21a56d8045701a8a3bd681c5fe91fd8ffbca2cc14270b92f3bb
3
+ metadata.gz: 50a47cf449c063e7ca02cb36ad315008fa6a5813063a24d92ed4c0f94743c12e
4
+ data.tar.gz: fad47ad6fefc7d580251edf1675ae575a9f4eebf28fb903a1c82ba2868310bc4
5
5
  SHA512:
6
- metadata.gz: efc72210fb52599b7c6278502a5b4143d995e671c62c7f6c090cbddf2a8926b231b0d7ab0aa28fa1325324ded1ebac5cac4e10e608ae59077a431a20f9d62f23
7
- data.tar.gz: eeb00a993d7bbf64ac5c7461a1e31f241407c58a19a919c0853ade7e4a3bf717ab4f69331c5bc5d8c0049a89c53353d311d41616352131379b7e87b8ec3e491d
6
+ metadata.gz: '073919902e84d95158573400ebe593d1e5aa72cc1ab6efb0c4cfccaf9806a91a0c55913d9d9ec47dd8b56f7fb81660fdfd81753092e2a3bd590e4ef782c51ba8'
7
+ data.tar.gz: 87f1ec995889c7ca107ccf187f1b01874f136787861e5116abc13da5f204f4f3e7e8cf101692fed7285ac875f405e1f8e08701f3785e0c5e83f25d0e5a0b9b15
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.2.0](https://github.com/BoxcarsAI/boxcars/tree/v0.2.0) (2023-03-07)
4
+
5
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.8...v0.2.0)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - Default to chatgpt [\#35](https://github.com/BoxcarsAI/boxcars/pull/35) ([francis](https://github.com/francis))
10
+
11
+ ## [v0.1.8](https://github.com/BoxcarsAI/boxcars/tree/v0.1.8) (2023-03-02)
12
+
13
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.7...v0.1.8)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - return JSON from the Active Record boxcar [\#34](https://github.com/BoxcarsAI/boxcars/pull/34) ([francis](https://github.com/francis))
18
+ - validate return values from Open AI API [\#33](https://github.com/BoxcarsAI/boxcars/pull/33) ([francis](https://github.com/francis))
19
+ - simplify prompting and parameters used. refs \#29 [\#30](https://github.com/BoxcarsAI/boxcars/pull/30) ([francis](https://github.com/francis))
20
+ - \[infra\] Added sample .env file and updated the lookup to save the key [\#27](https://github.com/BoxcarsAI/boxcars/pull/27) ([AKovtunov](https://github.com/AKovtunov))
21
+
3
22
  ## [v0.1.7](https://github.com/BoxcarsAI/boxcars/tree/v0.1.7) (2023-02-27)
4
23
 
5
24
  [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.6...v0.1.7)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- boxcars (0.1.8)
4
+ boxcars (0.2.1)
5
5
  google_search_results (~> 2.2)
6
6
  ruby-openai (~> 3.0)
7
7
 
@@ -140,7 +140,7 @@ GEM
140
140
  rubocop-rspec (2.18.1)
141
141
  rubocop (~> 1.33)
142
142
  rubocop-capybara (~> 2.17)
143
- ruby-openai (3.3.0)
143
+ ruby-openai (3.5.0)
144
144
  httparty (>= 0.18.1)
145
145
  ruby-progressbar (1.11.0)
146
146
  ruby2_keywords (0.0.5)
@@ -7,28 +7,24 @@ module Boxcars
7
7
  # the description of this engine boxcar
8
8
  ARDESC = "useful for when you need to query a database for an application named %<name>s."
9
9
  LOCKED_OUT_MODELS = %w[ActiveRecord::SchemaMigration ActiveRecord::InternalMetadata ApplicationRecord].freeze
10
- attr_accessor :connection, :requested_models, :read_only, :approval_callback
10
+ attr_accessor :connection, :requested_models, :read_only, :approval_callback, :code_only
11
11
  attr_reader :except_models
12
12
 
13
- # @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a train if nil.
14
13
  # @param models [Array<ActiveRecord::Model>] The models to use for this boxcar. Will use all if nil.
14
+ # @param except_models [Array<ActiveRecord::Model>] The models to exclude from this boxcar. Will exclude none if nil.
15
15
  # @param read_only [Boolean] Whether to use read only models. Defaults to true unless you pass an approval function.
16
16
  # @param approval_callback [Proc] A function to call to approve changes. Defaults to nil.
17
17
  # @param kwargs [Hash] Any other keyword arguments. These can include:
18
- # :name, :description, :prompt, :except_models, :top_k, and :stop
19
- def initialize(engine: nil, models: nil, read_only: nil, approval_callback: nil, **kwargs)
20
- check_models(models)
21
- @except_models = LOCKED_OUT_MODELS + kwargs[:except_models].to_a
18
+ # :name, :description, :prompt, :except_models, :top_k, :stop, :code_only and :engine
19
+ def initialize(models: nil, except_models: nil, read_only: nil, approval_callback: nil, **kwargs)
20
+ check_models(models, except_models)
22
21
  @approval_callback = approval_callback
23
22
  @read_only = read_only.nil? ? !approval_callback : read_only
24
- the_prompt = kwargs[prompt] || my_prompt
25
- name = kwargs[:name] || "Data"
26
- kwargs[:stop] ||= ["Answer:"]
27
- super(name: name,
28
- description: kwargs[:description] || format(ARDESC, name: name),
29
- engine: engine,
30
- prompt: the_prompt,
31
- **kwargs)
23
+ @code_only = kwargs.delete(:code_only) || false
24
+ kwargs[:name] ||= "Data"
25
+ kwargs[:description] ||= format(ARDESC, name: name)
26
+ kwargs[:prompt] ||= my_prompt
27
+ super(**kwargs)
32
28
  end
33
29
 
34
30
  # @return Hash The additional variables for this boxcar.
@@ -42,7 +38,11 @@ module Boxcars
42
38
  read_only
43
39
  end
44
40
 
45
- def check_models(models)
41
+ def code_only?
42
+ code_only
43
+ end
44
+
45
+ def check_models(models, exceptions)
46
46
  if models.is_a?(Array) && models.length.positive?
47
47
  @requested_models = models
48
48
  models.each do |m|
@@ -51,6 +51,7 @@ module Boxcars
51
51
  elsif models
52
52
  raise ArgumentError, "models needs to be an array of Active Record models"
53
53
  end
54
+ @except_models = LOCKED_OUT_MODELS + exceptions.to_a
54
55
  end
55
56
 
56
57
  def wanted_models
@@ -120,7 +121,7 @@ module Boxcars
120
121
  changes = change_count(changes_code)
121
122
  return true unless changes&.positive?
122
123
 
123
- Boxcars.debug "Pending Changes: #{changes}", :yellow, style: :bold
124
+ Boxcars.debug "#{name}(Pending Changes): #{changes}", :yellow
124
125
  change_str = "#{changes} change#{'s' if changes.to_i > 1}"
125
126
  raise SecurityError, "Can not run code that makes #{change_str} in read-only mode" if read_only?
126
127
 
@@ -140,18 +141,26 @@ module Boxcars
140
141
  end
141
142
  end
142
143
 
144
+ def clean_up_output(output)
145
+ output = output.as_json if output.is_a?(::ActiveRecord::Result)
146
+ output = 0 if output.is_a?(Array) && output.empty?
147
+ output = output.first if output.is_a?(Array) && output.length == 1
148
+ output = output[output.keys.first] if output.is_a?(Hash) && output.length == 1
149
+ output = output.as_json if output.is_a?(::ActiveRecord::Relation)
150
+ output
151
+ end
152
+
143
153
  def get_active_record_answer(text)
144
154
  code = text[/^ARCode: (.*)/, 1]
145
155
  changes_code = text[/^ARChanges: (.*)/, 1]
156
+ return Result.new(status: :ok, explanation: "code to run", code: code, changes_code: changes_code) if code_only?
157
+
146
158
  raise SecurityError, "Permission to run code that makes changes denied" unless approved?(changes_code, code)
147
159
 
148
- output = run_active_record_code(code)
149
- output = 0 if output.is_a?(Array) && output.empty?
150
- output = output.first if output.is_a?(Array) && output.length == 1
151
- output = output[output.keys.first] if output.is_a?(Hash) && output.length == 1
152
- "Answer: #{output.to_json}"
160
+ output = clean_up_output(run_active_record_code(code))
161
+ Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code: code)
153
162
  rescue StandardError => e
154
- "Error: #{e.message}"
163
+ Result.new(status: :error, answer: nil, explanation: "Error: #{e.message}", code: code)
155
164
  end
156
165
 
157
166
  def get_answer(text)
@@ -159,9 +168,9 @@ module Boxcars
159
168
  when /^ARCode:/
160
169
  get_active_record_answer(text)
161
170
  when /^Answer:/
162
- text
171
+ Result.from_text(text)
163
172
  else
164
- raise Boxcars::Error "Unknown format from engine: #{text}"
173
+ Result.from_error("Unknown format from engine: #{text}")
165
174
  end
166
175
  end
167
176
 
@@ -11,14 +11,11 @@ module Boxcars
11
11
  # @param prompt [Boxcars::Prompt] The prompt to use for this boxcar. Defaults to built-in prompt.
12
12
  # @param kwargs [Hash] Any other keyword arguments to pass to the parent class.
13
13
  def initialize(engine: nil, prompt: nil, **kwargs)
14
- # def initialize(engine:, prompt: my_prompt, output_key: :answer, **kwargs)
15
14
  the_prompt = prompt || my_prompt
16
15
  kwargs[:stop] ||= ["```output"]
17
- super(name: kwargs[:name] || "Calculator",
18
- description: kwargs[:description] || CALCDESC,
19
- engine: engine,
20
- prompt: the_prompt,
21
- **kwargs)
16
+ kwargs[:name] ||= "Calculator"
17
+ kwargs[:description] ||= CALCDESC
18
+ super(engine: engine, prompt: the_prompt, **kwargs)
22
19
  end
23
20
 
24
21
  private
@@ -26,7 +23,7 @@ module Boxcars
26
23
  def get_embedded_ruby_answer(text)
27
24
  code = text[8..-4].split("```").first.strip
28
25
  ruby_executor = Boxcars::RubyREPL.new
29
- ruby_executor.call(code: code).strip
26
+ ruby_executor.call(code: code)
30
27
  end
31
28
 
32
29
  def get_answer(text)
@@ -34,9 +31,9 @@ module Boxcars
34
31
  when /^```ruby/
35
32
  get_embedded_ruby_answer(text)
36
33
  when /^Answer:/
37
- text
34
+ Result.from_text(text)
38
35
  else
39
- raise Boxcars::Error "Unknown format from engine: #{text}"
36
+ Result.new(status: :error, explanation: "Unknown format from engine: #{text}")
40
37
  end
41
38
  end
42
39
 
@@ -45,7 +42,7 @@ module Boxcars
45
42
  TEMPLATE = <<~'IPT'
46
43
  You are GPT-3, and you can't do math.
47
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.
48
- So we hooked you up to a Ruby 3 kernel, and now you can execute ruby code. If anyone gives you a hard math problem, just use this format and we’ll take care of the rest:
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:
49
46
 
50
47
  Question: ${{Question with hard calculation.}}
51
48
  ```ruby
@@ -16,7 +16,7 @@ module Boxcars
16
16
  @engine = engine || Boxcars.engine.new
17
17
  @top_k = kwargs[:top_k] || 5
18
18
  @stop = kwargs[:stop] || ["Answer:"]
19
- super(name: name, description: description)
19
+ super(name: name, description: description, return_direct: kwargs[:return_direct])
20
20
  end
21
21
 
22
22
  # input keys for the prompt
@@ -95,7 +95,7 @@ module Boxcars
95
95
  def check_output_keys
96
96
  return unless output_keys.length != 1
97
97
 
98
- raise Boxcars::ArgumentError, "run not supported when there is not exactly one output key. Got #{output_keys}."
98
+ raise Boxcars::ArgumentError, "not supported when there is not exactly one output key. Got #{output_keys}."
99
99
  end
100
100
 
101
101
  # call the boxcar
@@ -104,7 +104,7 @@ module Boxcars
104
104
  def call(inputs:)
105
105
  t = predict(**prediction_variables(inputs)).strip
106
106
  answer = get_answer(t)
107
- Boxcars.debug answer, :magenta
107
+ Boxcars.debug answer.to_json, :magenta
108
108
  { output_keys.first => answer }
109
109
  end
110
110
 
@@ -6,21 +6,21 @@ module Boxcars
6
6
  class SQL < EngineBoxcar
7
7
  # the description of this engine boxcar
8
8
  SQLDESC = "useful for when you need to query a database for %<name>s."
9
+ LOCKED_OUT_TABLES = %w[schema_migrations ar_internal_metadata].freeze
9
10
  attr_accessor :connection
10
11
 
11
12
  # @param connection [ActiveRecord::Connection] The SQL connection to use for this boxcar.
12
- # @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a train if nil.
13
+ # @param tables [Array<String>] The tables to use for this boxcar. Will use all if nil.
14
+ # @param except_tables [Array<String>] The tables to exclude from this boxcar. Will exclude none if nil.
13
15
  # @param kwargs [Hash] Any other keyword arguments to pass to the parent class. This can include
14
- # :name, :description, :prompt and :top_k
15
- def initialize(connection: nil, engine: nil, **kwargs)
16
+ # :name, :description, :prompt, :top_k, :stop, and :engine
17
+ def initialize(connection: nil, tables: nil, except_tables: nil, **kwargs)
16
18
  @connection = connection || ::ActiveRecord::Base.connection
17
- the_prompt = kwargs[prompt] || my_prompt
18
- kwargs[:stop] ||= ["Answer:"]
19
- name = kwargs[:name] || "database"
20
- super(name: name,
21
- description: kwargs[:description] || format(SQLDESC, name: name),
22
- engine: engine,
23
- prompt: the_prompt)
19
+ check_tables(tables, except_tables)
20
+ kwargs[:name] ||= "Database"
21
+ kwargs[:description] ||= format(SQLDESC, name: name)
22
+ kwargs[:prompt] ||= my_prompt
23
+ super(**kwargs)
24
24
  end
25
25
 
26
26
  # @return Hash The additional variables for this boxcar.
@@ -30,6 +30,19 @@ module Boxcars
30
30
 
31
31
  private
32
32
 
33
+ def check_tables(rtables, exceptions)
34
+ if rtables.is_a?(Array) && tables.length.positive?
35
+ @requested_tables = rtables
36
+ all_tables = tables
37
+ rtables.each do |t|
38
+ raise ArgumentError, "table #{t} needs to be an Active Record model" unless all_tables.include?(t)
39
+ end
40
+ elsif rtables
41
+ raise ArgumentError, "tables needs to be an array of Strings"
42
+ end
43
+ @except_models = LOCKED_OUT_TABLES + exceptions.to_a
44
+ end
45
+
33
46
  def tables
34
47
  connection&.tables
35
48
  end
@@ -49,11 +62,22 @@ module Boxcars
49
62
  connection.class.name.split("::").last.sub("Adapter", "")
50
63
  end
51
64
 
65
+ def clean_up_output(output)
66
+ output = output.as_json if output.is_a?(::ActiveRecord::Result)
67
+ output = 0 if output.is_a?(Array) && output.empty?
68
+ output = output.first if output.is_a?(Array) && output.length == 1
69
+ output = output[output.keys.first] if output.is_a?(Hash) && output.length == 1
70
+ output = output.as_json if output.is_a?(::ActiveRecord::Relation)
71
+ output
72
+ end
73
+
52
74
  def get_embedded_sql_answer(text)
53
75
  code = text[/^SQLQuery: (.*)/, 1]
54
76
  Boxcars.debug code, :yellow
55
- output = connection.exec_query(code).to_a
56
- "Answer: #{output}"
77
+ output = clean_up_output(connection.exec_query(code))
78
+ Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code: code)
79
+ rescue StandardError => e
80
+ Result.new(status: :error, answer: nil, explanation: "Error: #{e.message}", code: code)
57
81
  end
58
82
 
59
83
  def get_answer(text)
@@ -61,14 +85,14 @@ module Boxcars
61
85
  when /^SQLQuery:/
62
86
  get_embedded_sql_answer(text)
63
87
  when /^Answer:/
64
- text
88
+ Result.from_text(text)
65
89
  else
66
- raise Boxcars::Error "Unknown format from engine: #{text}"
90
+ Result.from_error("Unknown format from engine: #{text}")
67
91
  end
68
92
  end
69
93
 
70
94
  TEMPLATE = <<~IPT
71
- Given an input question, first create a syntactically correct %<dialect>s query to run,
95
+ Given an input question, first create a syntactically correct %<dialect>s SQL query to run,
72
96
  then look at the results of the query and return the answer. Unless the user specifies
73
97
  in his question a specific number of examples he wishes to obtain, always limit your query
74
98
  to at most %<top_k>s results using a LIMIT clause. You can order the results by a relevant column
@@ -62,8 +62,18 @@ module Boxcars
62
62
  # you can pass one or the other, but not both.
63
63
  # @return [String] The answer to the question.
64
64
  def run(*args, **kwargs)
65
+ rv = conduct(*args, **kwargs)
66
+ rv.is_a?(Result) ? rv.to_answer : rv
67
+ end
68
+
69
+ # Get an extended answer from the boxcar.
70
+ # @param args [Array] The positional arguments to pass to the boxcar.
71
+ # @param kwargs [Hash] The keyword arguments to pass to the boxcar.
72
+ # you can pass one or the other, but not both.
73
+ # @return [Boxcars::Result] The answer to the question.
74
+ def conduct(*args, **kwargs)
65
75
  Boxcars.info "> Entering #{name}#run", :gray, style: :bold
66
- rv = do_run(*args, **kwargs)
76
+ rv = depart(*args, **kwargs)
67
77
  Boxcars.info "< Exiting #{name}#run", :gray, style: :bold
68
78
  rv
69
79
  end
@@ -71,7 +81,7 @@ module Boxcars
71
81
  private
72
82
 
73
83
  # Get an answer from the boxcar.
74
- def do_call(inputs:, return_only_outputs: false)
84
+ def run_boxcar(inputs:, return_only_outputs: false)
75
85
  inputs = our_inputs(inputs)
76
86
  output = nil
77
87
  begin
@@ -81,19 +91,19 @@ module Boxcars
81
91
  raise e
82
92
  end
83
93
  validate_outputs(outputs: output.keys)
84
- # memory&.save_convext(inputs: inputs, outputs: outputs)
85
94
  return output if return_only_outputs
86
95
 
87
96
  inputs.merge(output)
88
97
  end
89
98
 
90
- def do_run(*args, **kwargs)
99
+ # line up parameters and run boxcar
100
+ def depart(*args, **kwargs)
91
101
  if kwargs.empty?
92
102
  raise Boxcars::ArgumentError, "run supports only one positional argument." if args.length != 1
93
103
 
94
- return do_call(inputs: args[0])[output_keys.first]
104
+ return run_boxcar(inputs: args[0])[output_keys.first]
95
105
  end
96
- return do_call(**kwargs)[output_keys].first if args.empty?
106
+ return run_boxcar(**kwargs)[output_keys].first if args.empty?
97
107
 
98
108
  raise Boxcars::ArgumentError, "run supported with either positional or keyword arguments but not both. Got args" \
99
109
  ": #{args} and kwargs: #{kwargs}."
@@ -111,9 +121,15 @@ module Boxcars
111
121
  end
112
122
  validate_inputs(inputs: inputs)
113
123
  end
124
+
125
+ # the default answer is the text passed in
126
+ def get_answer(text)
127
+ Result.from_text(text)
128
+ end
114
129
  end
115
130
  end
116
131
 
132
+ require "boxcars/result"
117
133
  require "boxcars/boxcar/engine_boxcar"
118
134
  require "boxcars/boxcar/calculator"
119
135
  require "boxcars/boxcar/google_search"
@@ -9,9 +9,9 @@ module Boxcars
9
9
 
10
10
  # The default parameters to use when asking the engine.
11
11
  DEFAULT_PARAMS = {
12
- model: "text-davinci-003",
12
+ model: "gpt-3.5-turbo",
13
13
  temperature: 0.7,
14
- max_tokens: 256
14
+ max_tokens: 512
15
15
  }.freeze
16
16
 
17
17
  # the default name of the engine
@@ -43,7 +43,13 @@ module Boxcars
43
43
  organization_id = Boxcars.configuration.organization_id
44
44
  clnt = ::OpenAI::Client.new(access_token: access_token, organization_id: organization_id)
45
45
  the_params = { prompt: prompt }.merge(open_ai_params).merge(kwargs)
46
- clnt.completions(parameters: the_params)
46
+ if the_params[:model] == "gpt-3.5-turbo"
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)
50
+ else
51
+ clnt.completions(parameters: the_params)
52
+ end
47
53
  end
48
54
 
49
55
  # get an answer from the engine for a question.
@@ -51,7 +57,7 @@ module Boxcars
51
57
  # @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
52
58
  def run(question, **kwargs)
53
59
  response = client(prompt: question, **kwargs)
54
- answer = response["choices"].map { |c| c["text"] }.join("\n").strip
60
+ answer = response["choices"].map { |c| c.dig("message", "content") || c["text"] }.join("\n").strip
55
61
  puts answer
56
62
  answer
57
63
  end
@@ -74,7 +80,7 @@ module Boxcars
74
80
  def generation_info(sub_choices)
75
81
  sub_choices.map do |choice|
76
82
  Generation.new(
77
- text: choice["text"],
83
+ text: choice.dig("message", "content") || choice["text"],
78
84
  generation_info: {
79
85
  finish_reason: choice.fetch("finish_reason", nil),
80
86
  logprobs: choice.fetch("logprobs", nil)
@@ -160,7 +166,8 @@ module Boxcars
160
166
  'text-babbage-001': 2048,
161
167
  'text-ada-001': 2048,
162
168
  'code-davinci-002': 8000,
163
- 'code-cushman-001': 2048
169
+ 'code-cushman-001': 2048,
170
+ 'gpt-3.5-turbo-1': 4096
164
171
  }.freeze
165
172
  model_lookup[modelname] || 4097
166
173
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ # used by Boxcars to return structured result and additional context
5
+ class Result
6
+ attr_reader :status, :answer, :explanation, :suggestions, :added_context
7
+
8
+ # @param status [Symbol] :ok or :error
9
+ # @param answer [String] The answer to the question
10
+ # @param explanation [String] The explanation of the answer
11
+ # @param suggestions [Array<String>] The next suggestions for the user
12
+ # @param added_context [Hash] Any additional context to add to the result
13
+ def initialize(status:, answer: nil, explanation: nil, suggestions: nil, **added_context)
14
+ @status = status
15
+ @answer = answer || explanation
16
+ @explanation = explanation
17
+ @suggestions = suggestions
18
+ @added_context = added_context
19
+ end
20
+
21
+ # @return [Hash] The result as a hash
22
+ def to_h
23
+ {
24
+ status: status,
25
+ answer: answer,
26
+ explanation: explanation,
27
+ suggestions: suggestions
28
+ }.merge(added_context).compact
29
+ end
30
+
31
+ # @return [String] The result as a json string
32
+ def to_json(*args)
33
+ JSON.generate(to_h, *args)
34
+ end
35
+
36
+ # @return [String] An explanation of the result
37
+ def to_s
38
+ explanation
39
+ end
40
+
41
+ # @return [String] The answer data to the question
42
+ def to_answer
43
+ answer
44
+ end
45
+
46
+ # create a new Result from a text string
47
+ # @param text [String] The text to use for the result
48
+ # @param kwargs [Hash] Any additional kwargs to pass to the result
49
+ # @return [Boxcars::Result] The result
50
+ def self.from_text(text, **kwargs)
51
+ answer = text.delete_prefix('"').delete_suffix('"').strip
52
+ answer = Regexp.last_match(:answer) if answer =~ /^Answer:\s*(?<answer>.*)$/
53
+ explanation = "Answer: #{answer}"
54
+ new(status: :ok, answer: answer, explanation: explanation, **kwargs)
55
+ end
56
+
57
+ # create a new Result from an error string
58
+ # @param error [String] The error to use for the result
59
+ # @param kwargs [Hash] Any additional kwargs to pass to the result
60
+ # @return [Boxcars::Result] The error result
61
+ def self.from_error(error, **kwargs)
62
+ answer = error
63
+ answer = Regexp.last_match(:answer) if answer =~ /^Error:\s*(?<answer>.*)$/
64
+ explanation = "Error: #{answer}"
65
+ new(status: :error, answer: answer, explanation: explanation, **kwargs)
66
+ end
67
+ end
68
+ end
@@ -7,14 +7,22 @@ module Boxcars
7
7
  # @param code [String] The code to run
8
8
  def call(code:)
9
9
  Boxcars.debug "RubyREPL: #{code}", :yellow
10
+
11
+ # wrap the code in an excption block so we can catch errors
12
+ wrapped = "begin\n#{code}\nrescue Exception => e\n puts 'Error: ' + e.message\nend"
10
13
  output = ""
11
14
  IO.popen("ruby", "r+") do |io|
12
- io.puts code
15
+ io.puts wrapped
13
16
  io.close_write
14
17
  output = io.read
15
18
  end
16
- Boxcars.debug "Answer: #{output}", :yellow, style: :bold
17
- output
19
+ if output =~ /^Error: /
20
+ Boxcars.debug output, :red
21
+ Result.from_error(output, code: code)
22
+ else
23
+ Boxcars.debug "Answer: #{output}", :yellow, style: :bold
24
+ Result.from_text(output, code: code)
25
+ end
18
26
  end
19
27
 
20
28
  # Execute ruby code
@@ -5,10 +5,24 @@ module Boxcars
5
5
  class TrainAction
6
6
  attr_accessor :boxcar, :boxcar_input, :log
7
7
 
8
- def initialize(boxcar: nil, boxcar_input: nil, log: nil)
9
- @boxcar = boxcar
8
+ # record for a train action
9
+ # @param boxcar [String] The boxcar to run.
10
+ # @param log [String] The log of the action.
11
+ # @param boxcar_input [String] The input to the boxcar.
12
+ # @return [Boxcars::TrainAction] The train action.
13
+ def initialize(boxcar:, log:, boxcar_input: nil)
10
14
  @boxcar_input = boxcar_input
15
+ @boxcar = boxcar
11
16
  @log = log
12
17
  end
18
+
19
+ # build a train action from a result
20
+ # @param result [Boxcars::Result] The result to build from.
21
+ # @param boxcar [String] The boxcar to run.
22
+ # @param log [String] The log of the action.
23
+ # @return [Boxcars::TrainAction] The train action.
24
+ def self.from_result(result:, boxcar:, log:)
25
+ new(boxcar: boxcar, boxcar_input: result.to_answer, log: log)
26
+ end
13
27
  end
14
28
  end
@@ -19,7 +19,7 @@ module Boxcars
19
19
  ... (this Thought/Action/Action Input/Observation sequence can repeat N times)
20
20
  Thought: I now know the final answer
21
21
  Final Answer: the final answer to the original input question
22
- Next Actions: up to three suggested actions for the user to take next
22
+ Next Actions: If you have them, up to three suggested actions for the user to take after getting this answer.
23
23
  FINPUT
24
24
 
25
25
  # default prompt suffix
@@ -69,15 +69,18 @@ module Boxcars
69
69
  # with "Action Input:" should be separated by a newline.
70
70
  if engine_output.include?(FINAL_ANSWER_ACTION)
71
71
  answer = engine_output.split(FINAL_ANSWER_ACTION).last.strip
72
- ['Final Answer', answer]
72
+ Result.new(status: :ok, answer: answer, explanation: engine_output)
73
+ # ['Final Answer', answer]
73
74
  else
75
+ # the thought should be the frist line here if it doesn't start with "Action:"
76
+ thought = engine_output.split(/\n+/).reject(&:empty?).first
77
+ Boxcars.debug("Though: #{thought}", :cyan)
74
78
  regex = /Action: (?<action>.*)\nAction Input: (?<action_input>.*)/
75
79
  match = regex.match(engine_output)
76
80
  raise ValueError, "Could not parse engine output: #{engine_output}" unless match
77
81
 
78
82
  action = match[:action].strip
79
- action_input = match[:action_input].strip
80
- # [action, action_input.strip(" ").strip('"')]
83
+ action_input = match[:action_input].strip.delete_prefix('"').delete_suffix('"')
81
84
  [action, action_input]
82
85
  end
83
86
  end
data/lib/boxcars/train.rb CHANGED
@@ -38,7 +38,7 @@ module Boxcars
38
38
  def construct_scratchpad(intermediate_steps)
39
39
  thoughts = ""
40
40
  intermediate_steps.each do |action, observation|
41
- thoughts += action.is_a?(String) ? action : action.log
41
+ thoughts += action.is_a?(String) ? action : " #{action.log}"
42
42
  thoughts += "\n#{observation_prefix}#{observation}\n#{engine_prefix}"
43
43
  end
44
44
  thoughts
@@ -51,13 +51,16 @@ module Boxcars
51
51
  full_output = predict(**full_inputs)
52
52
  parsed_output = extract_boxcar_and_input(full_output)
53
53
  while parsed_output.nil?
54
- full_output = _fix_text(full_output)
55
54
  full_inputs[:agent_scratchpad] += full_output
56
55
  output = predict(**full_inputs)
57
56
  full_output += output
58
57
  parsed_output = extract_boxcar_and_input(full_output)
59
58
  end
60
- TrainAction.new(boxcar: parsed_output[0], boxcar_input: parsed_output[1], log: full_output)
59
+ if parsed_output.is_a?(Result)
60
+ TrainAction.from_result(boxcar: "Final Answer", result: parsed_output, log: full_output)
61
+ else
62
+ TrainAction.new(boxcar: parsed_output[0], boxcar_input: parsed_output[1], log: full_output)
63
+ end
61
64
  end
62
65
 
63
66
  # Given input, decided what to do.
@@ -196,7 +199,7 @@ module Boxcars
196
199
  observation = boxcar.run(output.boxcar_input)
197
200
  return_direct = boxcar.return_direct
198
201
  rescue StandardError => e
199
- error "Error in #{boxcar.name} boxcar#call: #{e}", :red
202
+ Boxcars.error "Error in #{boxcar.name} boxcar#call: #{e}", :red
200
203
  observation = "Error - #{e}, correct and try again."
201
204
  end
202
205
  else
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Boxcars
4
4
  # The current version of the gem.
5
- VERSION = "0.1.8"
5
+ VERSION = "0.2.1"
6
6
  end
data/lib/boxcars.rb CHANGED
@@ -30,7 +30,7 @@ module Boxcars
30
30
  def initialize
31
31
  @organization_id = nil
32
32
  @logger = Rails.logger if defined?(Rails)
33
- @log_prompts = false
33
+ @log_prompts = ENV.fetch("LOG_PROMPTS", false)
34
34
  end
35
35
 
36
36
  # @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.1.8
4
+ version: 0.2.1
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-02 00:00:00.000000000 Z
12
+ date: 2023-03-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -114,6 +114,7 @@ files:
114
114
  - lib/boxcars/engine/openai.rb
115
115
  - lib/boxcars/generation.rb
116
116
  - lib/boxcars/prompt.rb
117
+ - lib/boxcars/result.rb
117
118
  - lib/boxcars/ruby_repl.rb
118
119
  - lib/boxcars/train.rb
119
120
  - lib/boxcars/train/train_action.rb