boxcars 0.1.5 → 0.1.7

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: 8eb0adb30464688c5a68391ffb19a5d08188f87283d7cf3de28b12a30b927971
4
- data.tar.gz: 94605b14f7974159d21eb2cf219b8d4695533761084fe784419e47a04843c6c2
3
+ metadata.gz: f8cfde09f23575d2b54249bd2b6d2c15697092b7c52db4d5f0546bb0113a3949
4
+ data.tar.gz: d557f681bf8a26251608b17038650868563da9d54753789c568331328cfa456b
5
5
  SHA512:
6
- metadata.gz: 4b92e4e8c98ada38bb4acf5ed5f0bd563bcfe2ab29d9781002a585799aba35cfa82992086484886563585ad0b7a9795691f9804ef4807b58aaa29c8bea88a761
7
- data.tar.gz: e46f9a26fad0b0eb339868daef5bd4c05d750af08277f83b4edb15839a712553584223f056dc2aa403fccb8b5292a6ea57aadf12c04595f909d561f1fbbe2995
6
+ metadata.gz: db5af829a8a6e600cbb402e53aa7aaf1f9374725c5cf1d61ee908cb58ffda305b03ee00a4664e9f1134dc6887be9dab8c7877ad4c6b981e5a5665bfc1cfec2f3
7
+ data.tar.gz: 17930e46fa46b4d1a268367aff6d9b5851fac890f00d5a87e722ed3833552bb851614a5ab606cb9a08afce611a5882c9838094e31c1d34b9d17c05923656fc38
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
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.1.5...HEAD)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Add a callback function for Boxcars::ActiveRecord to approve changes [\#24](https://github.com/BoxcarsAI/boxcars/issues/24)
10
+
11
+ **Merged pull requests:**
12
+
13
+ - Add approval callback function for Boxcars::ActiveRecord for changes to the data [\#25](https://github.com/BoxcarsAI/boxcars/pull/25) ([francis](https://github.com/francis))
14
+ - \[fix\] Fixed specs which required a key [\#23](https://github.com/BoxcarsAI/boxcars/pull/23) ([AKovtunov](https://github.com/AKovtunov))
15
+
16
+ ## [v0.1.5](https://github.com/BoxcarsAI/boxcars/tree/v0.1.5) (2023-02-22)
17
+
18
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.4...v0.1.5)
19
+
20
+ **Implemented enhancements:**
21
+
22
+ - Make Boxcars::ActiveRecord read\_only by default [\#20](https://github.com/BoxcarsAI/boxcars/issues/20)
23
+
24
+ **Merged pull requests:**
25
+
26
+ - Active Record readonly [\#21](https://github.com/BoxcarsAI/boxcars/pull/21) ([francis](https://github.com/francis))
27
+
3
28
  ## [v0.1.4](https://github.com/BoxcarsAI/boxcars/tree/v0.1.4) (2023-02-22)
4
29
 
5
30
  [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.3...v0.1.4)
data/Gemfile CHANGED
@@ -24,3 +24,5 @@ gem "sqlite3", "~> 1.6"
24
24
  gem "activerecord", "~> 7.0"
25
25
 
26
26
  gem "github_changelog_generator", "~> 1.16"
27
+
28
+ gem "faraday-retry", "~> 2.0"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- boxcars (0.1.5)
4
+ boxcars (0.1.7)
5
5
  google_search_results (~> 2.2)
6
6
  ruby-openai (~> 3.0)
7
7
 
@@ -56,6 +56,8 @@ GEM
56
56
  faraday-http-cache (2.4.1)
57
57
  faraday (>= 0.8)
58
58
  faraday-net_http (3.0.2)
59
+ faraday-retry (2.0.0)
60
+ faraday (~> 2.0)
59
61
  fiber-local (1.0.0)
60
62
  github_changelog_generator (1.16.4)
61
63
  activesupport
@@ -171,6 +173,7 @@ DEPENDENCIES
171
173
  boxcars!
172
174
  debug (~> 1.1)
173
175
  dotenv (~> 2.8)
176
+ faraday-retry (~> 2.0)
174
177
  github_changelog_generator (~> 1.16)
175
178
  rake (~> 13.0)
176
179
  rspec (~> 3.2)
data/README.md CHANGED
@@ -17,10 +17,11 @@ This gem was inspired by the popular Python library Langchain. However, we wante
17
17
 
18
18
  ## Concepts
19
19
  All of these concepts are in a module named Boxcars:
20
- - Prompt: used by an Engine to generate text results.
21
- - Engine: an entity that generates text from a Prompt. OpenAI's LLM text generatory is the default Engine if no other is specified.
22
- - Boxcar: an encapsulation of a concept that performs something of interest (such as search, math, or SQL). A can use an Engine to do its work.
23
- - Train: Given a list of Boxcars and an optionally an Engine, a Train breaks down a problem into pieces for individual Boxcars to solve. The individual results are then combined until a final answer is found. ZeroShot is the only current implementation of Train, and you can either construct it directly or use `Boxcars::train` when you want to build a Train.
20
+
21
+ - Boxcar - an encapsulation that performs something of interest (such as search, math, SQL or an Active Record Query). A Boxcar can use an Engine (described below) to do its work.
22
+ - Train - Given a list of Boxcars and optionally an Engine, a Train breaks down a problem into pieces for individual Boxcars to solve. The individual results are then combined until a final answer is found. ZeroShot is the only current implementation of Train (but we are adding more soon), and you can either construct it directly or use `Boxcars::train` when you want to build a Train.
23
+ - Prompt - used by an Engine to generate text results. Most of the Boxcars have built-in prompts, so you only need to worry about these if you are extending the system.
24
+ - Engine - an entity that generates text from a Prompt. OpenAI's LLM text generator is the default Engine if no other is specified.
24
25
 
25
26
  ## Installation
26
27
 
@@ -86,6 +87,7 @@ Here is what we have so far, but please put up a PR with your new ideas.
86
87
  - GoogleSearch: uses the SERP API to do seaches
87
88
  - Calculator: uses an Engine to generate ruby code to do math
88
89
  - SQL: given an ActiveRecord connection, it will generate and run sql statments from a prompt.
90
+ - ActiveRecord: given an ActiveRecord connection, it will generate and run ActiveRecord statements from a prompt.
89
91
 
90
92
  ### Run a list of Boxcars
91
93
  ```ruby
@@ -114,7 +116,18 @@ Final Answer: 201.06
114
116
  < Exiting Zero Shot#run
115
117
  201.06
116
118
  ```
119
+ ### More Examples
120
+ See [this](https://github.com/BoxcarsAI/boxcars/blob/main/notebooks/boxcars_examples.ipynb) Jupyter Notebook for more examples.
121
+
122
+ Note, some folks that we talked to didn't know that you could run Ruby Jupyter notebooks. [You can](https://github.com/SciRuby/iruby).
123
+
124
+ ### Logging
125
+ If you use this in a Rails application, or configure `Boxcars.configuration.logger = your_logger`, logging will go to your log file.
126
+
127
+ Also, if you set this flag: `Boxcars.configuration.lop_prompts = true`
128
+ The actual prompts handed to the connected Engine will be logged. This is off by default because it is very wordy, but handy if you are debugging prompts.
117
129
 
130
+ Otherwise, we print to standard out.
118
131
  ## Development
119
132
 
120
133
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -123,7 +136,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
123
136
 
124
137
  ## Contributing
125
138
 
126
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/boxcars. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/boxcars/blob/main/CODE_OF_CONDUCT.md).
139
+ Bug reports and pull requests are welcome on GitHub at https://github.com/BoxcarsAI/boxcars. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/BoxcarsAI/boxcars/blob/main/CODE_OF_CONDUCT.md).
127
140
 
128
141
  ## License
129
142
 
@@ -131,4 +144,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
131
144
 
132
145
  ## Code of Conduct
133
146
 
134
- Everyone interacting in the Boxcars project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/boxcars/blob/main/CODE_OF_CONDUCT.md).
147
+ Everyone interacting in the Boxcars project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/BoxcarsAI/boxcars/blob/main/CODE_OF_CONDUCT.md).
@@ -7,18 +7,20 @@ 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, :input_key, :requested_models, :read_only
10
+ attr_accessor :connection, :input_key, :requested_models, :read_only, :approval_callback
11
11
  attr_reader :except_models
12
12
 
13
13
  # @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a train if nil.
14
14
  # @param models [Array<ActiveRecord::Model>] The models to use for this boxcar. Will use all if nil.
15
- # @param read_only [Boolean] Whether to use read only models. Defaults to true.
15
+ # @param read_only [Boolean] Whether to use read only models. Defaults to true unless you pass an approval function.
16
+ # @param approval_callback [Proc] A function to call to approve changes. Defaults to nil.
16
17
  # @param kwargs [Hash] Any other keyword arguments. These can include:
17
18
  # :name, :description, :prompt, :input_key, :output_key and :except_models
18
- def initialize(engine: nil, models: nil, read_only: true, **kwargs)
19
+ def initialize(engine: nil, models: nil, read_only: nil, approval_callback: nil, **kwargs)
19
20
  check_models(models)
20
21
  @except_models = LOCKED_OUT_MODELS + kwargs[:except_models].to_a
21
- @read_only = read_only
22
+ @approval_callback = approval_callback
23
+ @read_only = read_only.nil? ? !approval_callback : read_only
22
24
  @input_key = kwargs[:input_key] || :question
23
25
  @output_key = kwargs[:output_key] || :answer
24
26
  the_prompt = kwargs[prompt] || my_prompt
@@ -48,7 +50,7 @@ module Boxcars
48
50
  def call(inputs:)
49
51
  t = predict(question: inputs[input_key], top_k: 5, model_info: model_info, stop: ["Answer:"]).strip
50
52
  answer = get_answer(t)
51
- puts answer.colorize(:magenta)
53
+ Boxcars.info answer, :magenta
52
54
  { output_key => answer }
53
55
  end
54
56
 
@@ -85,28 +87,28 @@ module Boxcars
85
87
  end
86
88
 
87
89
  # to be safe, we wrap the code in a transaction and rollback
88
- # rubocop:disable Lint/SuppressedException
89
- def wrap_in_transaction
90
+ def rollback_after_running
91
+ rv = nil
90
92
  ::ActiveRecord::Base.transaction do
91
- yield
93
+ rv = yield
92
94
  ensure
93
95
  raise ::ActiveRecord::Rollback
94
96
  end
95
- rescue ::ActiveRecord::Rollback
97
+ rv
96
98
  end
97
- # rubocop:enable Lint/SuppressedException
98
99
 
100
+ # check for dangerous code that is outside of ActiveRecord
99
101
  def safe_to_run?(code)
100
- return true unless read_only?
101
-
102
- bad_words = %w[delete delete_all destroy destroy_all update update_all upsert upsert_all create save insert drop alter
103
- truncate revoke commit rollback reset execute].freeze
102
+ bad_words = %w[commit drop_constraint drop_constraint! drop_extension drop_extension! drop_foreign_key drop_foreign_key! \
103
+ drop_index drop_index! drop_join_table drop_join_table! drop_materialized_view drop_materialized_view! \
104
+ drop_partition drop_partition! drop_schema drop_schema! drop_table drop_table! drop_trigger drop_trigger! \
105
+ drop_view drop_view! eval execute reset revoke rollback truncate].freeze
104
106
  without_strings = code.gsub(/('([^'\\]*(\\.[^'\\]*)*)'|"([^"\\]*(\\.[^"\\]*)*"))/, 'XX')
105
107
  word_list = without_strings.split(/[.,()]/)
106
108
 
107
109
  bad_words.each do |w|
108
110
  if word_list.include?(w)
109
- puts "code included destructive instruction: #{w} #{code}"
111
+ Boxcars.info "code included destructive instruction: #{w} #{code}", :red
110
112
  return false
111
113
  end
112
114
  end
@@ -114,22 +116,64 @@ module Boxcars
114
116
  true
115
117
  end
116
118
 
117
- def get_active_record_answer(text)
118
- code = text[/^ARCode: (.*)/, 1]
119
- puts code.colorize(:yellow)
120
- raise SecurityError, "Can not run code that makes changes in read-only mode" unless safe_to_run?(code)
119
+ def evaluate_input(code)
120
+ raise SecurityError, "Found unsafe code while evaluating: #{code}" unless safe_to_run?(code)
121
121
 
122
122
  # rubocop:disable Security/Eval
123
- output = eval code
123
+ eval code
124
124
  # rubocop:enable Security/Eval
125
+ end
126
+
127
+ def change_count(changes_code)
128
+ return 0 unless changes_code
129
+
130
+ rollback_after_running do
131
+ Boxcars.debug "computing change count with: #{changes_code}", :yellow
132
+ evaluate_input changes_code
133
+ end
134
+ end
135
+
136
+ def approved?(changes_code, code)
137
+ # find out how many changes there are
138
+ changes = change_count(changes_code)
139
+ return true unless changes&.positive?
140
+
141
+ Boxcars.debug "Pending Changes: #{changes}", :yellow, style: :bold
142
+ change_str = "#{changes} change#{'s' if changes.to_i > 1}"
143
+ raise SecurityError, "Can not run code that makes #{change_str} in read-only mode" if read_only?
144
+
145
+ return approval_callback.call(changes, code) if approval_callback.is_a?(Proc)
146
+
147
+ true
148
+ end
149
+
150
+ def run_active_record_code(code)
151
+ Boxcars.debug code, :yellow
152
+ if read_only?
153
+ rollback_after_running do
154
+ evaluate_input code
155
+ end
156
+ else
157
+ evaluate_input code
158
+ end
159
+ end
160
+
161
+ def get_active_record_answer(text)
162
+ code = text[/^ARCode: (.*)/, 1]
163
+ changes_code = text[/^ARChanges: (.*)/, 1]
164
+ raise SecurityError, "Permission to run code that makes changes denied" unless approved?(changes_code, code)
165
+
166
+ output = run_active_record_code(code)
125
167
  output = 0 if output.is_a?(Array) && output.empty?
126
168
  output = output.first if output.is_a?(Array) && output.length == 1
169
+ output = output[output.keys.first] if output.is_a?(Hash) && output.length == 1
127
170
  "Answer: #{output.inspect}"
128
171
  rescue StandardError => e
129
172
  "Error: #{e.message}"
130
173
  end
131
174
 
132
175
  def get_answer(text)
176
+ # debugger
133
177
  case text
134
178
  when /^ARCode:/
135
179
  get_active_record_answer(text)
@@ -154,6 +198,7 @@ module Boxcars
154
198
  Use the following format:
155
199
  Question: "Question here"
156
200
  ARCode: "Active Record code to run"
201
+ 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
157
202
  Answer: "Final answer here"
158
203
 
159
204
  Only use the following Active Record models:
@@ -40,7 +40,7 @@ module Boxcars
40
40
  def call(inputs:)
41
41
  t = predict(question: inputs[input_key], stop: ["```output"]).strip
42
42
  answer = get_answer(t)
43
- puts answer.colorize(:magenta)
43
+ Boxcars.info answer, :magenta
44
44
  { output_key => answer }
45
45
  end
46
46
 
@@ -36,7 +36,7 @@ module Boxcars
36
36
  prompts = []
37
37
  input_list.each do |inputs|
38
38
  new_prompt = prompt.format(**inputs)
39
- puts "Prompt after formatting:\n#{new_prompt.colorize(:cyan)}" if Boxcars.configuration.log_prompts
39
+ Boxcars.debug("Prompt after formatting:\n#{new_prompt}", :cyan) if Boxcars.configuration.log_prompts
40
40
  prompts.push(new_prompt)
41
41
  end
42
42
  engine.generate(prompts: prompts, stop: stop)
@@ -13,7 +13,7 @@ module Boxcars
13
13
  # @param name [String] The name of the boxcar. Defaults to classname.
14
14
  # @param description [String] A description of the boxcar. Defaults to SERPDESC.
15
15
  # @param serpapi_api_key [String] The API key to use for the SerpAPI. Defaults to Boxcars.configuration.serpapi_api_key.
16
- def initialize(name: "Search", description: SERPDESC, serpapi_api_key: "not set")
16
+ def initialize(name: "Search", description: SERPDESC, serpapi_api_key: nil)
17
17
  super(name: name, description: description)
18
18
  api_key = Boxcars.configuration.serpapi_api_key(serpapi_api_key: serpapi_api_key)
19
19
  ::GoogleSearch.api_key = api_key
@@ -44,7 +44,7 @@ module Boxcars
44
44
  def call(inputs:)
45
45
  t = predict(question: inputs[input_key], dialect: dialect, top_k: 5, table_info: schema, stop: ["Answer:"]).strip
46
46
  answer = get_answer(t)
47
- puts answer.colorize(:magenta)
47
+ Boxcars.debug answer, :magenta
48
48
  { output_key => answer }
49
49
  end
50
50
 
@@ -72,7 +72,7 @@ module Boxcars
72
72
 
73
73
  def get_embedded_sql_answer(text)
74
74
  code = text[/^SQLQuery: (.*)/, 1]
75
- puts code.colorize(:yellow)
75
+ Boxcars.debug code, :yellow
76
76
  output = connection.exec_query(code).to_a
77
77
  "Answer: #{output}"
78
78
  end
@@ -62,9 +62,9 @@ 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
- puts "> Entering #{name}#run".colorize(:gray, style: :bold)
65
+ Boxcars.info "> Entering #{name}#run", :gray, style: :bold
66
66
  rv = do_run(*args, **kwargs)
67
- puts "< Exiting #{name}#run".colorize(:gray, style: :bold)
67
+ Boxcars.info "< Exiting #{name}#run", :gray, style: :bold
68
68
  rv
69
69
  end
70
70
 
@@ -77,7 +77,7 @@ module Boxcars
77
77
  begin
78
78
  output = call(inputs: inputs)
79
79
  rescue StandardError => e
80
- puts "Error in #{name} boxcar#call: #{e}".colorize(:red)
80
+ error "Error in #{name} boxcar#call: #{e}", :red
81
81
  raise e
82
82
  end
83
83
  validate_outputs(outputs: output.keys)
@@ -101,7 +101,7 @@ module Boxcars
101
101
 
102
102
  def our_inputs(inputs)
103
103
  if inputs.is_a?(String)
104
- puts inputs.colorize(:blue) # the question
104
+ Boxcars.info inputs, :blue # the question
105
105
  if input_keys.length != 1
106
106
  raise Boxcars::ArgumentError, "A single string input was passed in, but this boxcar expects " \
107
107
  "multiple inputs (#{input_keys}). When a boxcar expects " \
@@ -38,7 +38,7 @@ 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: 'not set', **kwargs)
41
+ def client(prompt:, 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)
@@ -6,14 +6,14 @@ module Boxcars
6
6
  # Execute ruby code
7
7
  # @param code [String] The code to run
8
8
  def call(code:)
9
- puts "RubyREPL: #{code}".colorize(:red)
9
+ Boxcars.debug "RubyREPL: #{code}", :yellow
10
10
  output = ""
11
11
  IO.popen("ruby", "r+") do |io|
12
12
  io.puts code
13
13
  io.close_write
14
14
  output = io.read
15
15
  end
16
- puts "Answer: #{output}".colorize(:red, style: :bold)
16
+ Boxcars.debug "Answer: #{output}", :yellow, style: :bold
17
17
  output
18
18
  end
19
19
 
data/lib/boxcars/train.rb CHANGED
@@ -112,7 +112,7 @@ module Boxcars
112
112
  # @param intermediate_steps [Array<Hash>] The intermediate steps.
113
113
  # @return [Hash] The final output.
114
114
  def pre_return(output, intermediate_steps)
115
- puts output.log.colorize(:yellow)
115
+ Boxcars.debug output.log, :yellow, style: :bold
116
116
  final_output = output.return_values
117
117
  final_output["intermediate_steps"] = intermediate_steps if return_intermediate_steps
118
118
  final_output
@@ -198,14 +198,14 @@ module Boxcars
198
198
  observation = boxcar.run(output.boxcar_input)
199
199
  return_direct = boxcar.return_direct
200
200
  rescue StandardError => e
201
- puts "Error in #{boxcar.name} boxcar#call: #{e}".colorize(:red)
201
+ error "Error in #{boxcar.name} boxcar#call: #{e}", :red
202
202
  raise e
203
203
  end
204
204
  else
205
205
  observation = "#{output.boxcar} is not a valid boxcar, try another one."
206
206
  return_direct = false
207
207
  end
208
- puts "Observation: #{observation}".colorize(:green)
208
+ Boxcars.debug "Observation: #{observation}", :green
209
209
  intermediate_steps.append([output, observation])
210
210
  if return_direct
211
211
  output = TrainFinish.new({ return_values[0] => observation }, "")
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Boxcars
4
4
  # The current version of the gem.
5
- VERSION = "0.1.5"
5
+ VERSION = "0.1.7"
6
6
  end
data/lib/boxcars.rb CHANGED
@@ -19,23 +19,6 @@ module Boxcars
19
19
  # Error class for all Boxcars security errors.
20
20
  class SecurityError < Error; end
21
21
 
22
- # simple string colorization
23
- class ::String
24
- # colorize a string
25
- # @param color [Symbol] The color to use.
26
- # @param options [Hash] The options to use.
27
- def colorize(color, options = {})
28
- background = options[:background] || options[:bg] || false
29
- style = options[:style]
30
- offsets = %i[gray red green yellow blue magenta cyan white]
31
- styles = %i[normal bold dark italic underline xx xx underline xx strikethrough]
32
- start = background ? 40 : 30
33
- color_code = start + (offsets.index(color) || 8)
34
- style_code = styles.index(style) || 0
35
- "\e[#{style_code};#{color_code}m#{self}\e[0m"
36
- end
37
- end
38
-
39
22
  # Configuration contains gem settings
40
23
  class Configuration
41
24
  attr_writer :openai_access_token, :serpapi_api_key
@@ -44,7 +27,6 @@ module Boxcars
44
27
  def initialize
45
28
  @organization_id = nil
46
29
  @logger = Rails.logger if defined?(Rails)
47
- @logger ||= Logger.new($stdout)
48
30
  @log_prompts = false
49
31
  end
50
32
 
@@ -68,7 +50,7 @@ module Boxcars
68
50
  end
69
51
 
70
52
  def key_lookup(key, kwargs)
71
- rv = if kwargs.key?(key) && kwargs[key] != "not set"
53
+ rv = if kwargs.key?(key) && !kwargs[key].nil?
72
54
  # override with kwargs if present
73
55
  kwargs[key]
74
56
  elsif (set_val = instance_variable_get("@#{key}"))
@@ -77,7 +59,6 @@ module Boxcars
77
59
  else
78
60
  # otherwise, dig out of the environment
79
61
  new_key = ENV.fetch(key.to_s.upcase, nil)
80
- send("#{key}=", new_key) if new_key
81
62
  new_key
82
63
  end
83
64
  check_key(key, rv)
@@ -107,6 +88,68 @@ module Boxcars
107
88
  def self.engine
108
89
  configuration.default_engine || Boxcars::Openai
109
90
  end
91
+
92
+ # return a proc that will ask the user for input
93
+ def self.ask_user
94
+ proc do |changes, _code|
95
+ puts "This request will make #{changes} changes. Are you sure you want to run it? (y/[n])"
96
+ answer = gets.chomp
97
+ answer.downcase == 'y'
98
+ end
99
+ end
100
+
101
+ # Return a logger, possibly if set.
102
+ def self.logger
103
+ Boxcars.configuration.logger
104
+ end
105
+
106
+ # Logging system
107
+ def self.debug(msg, color = nil, **options)
108
+ msg = colorize(msg.to_s, color, **options) if color
109
+ if logger
110
+ logger.debug(msg)
111
+ else
112
+ puts msg
113
+ end
114
+ end
115
+
116
+ def self.info(msg, color = nil, **options)
117
+ msg = colorize(msg.to_s, color, **options) if color
118
+ if logger
119
+ logger.info(msg)
120
+ else
121
+ puts msg
122
+ end
123
+ end
124
+
125
+ def self.warn(msg, color = nil, **options)
126
+ msg = colorize(msg.to_s, color, **options) if color
127
+ if logger
128
+ logger.warn(msg)
129
+ else
130
+ puts msg
131
+ end
132
+ end
133
+
134
+ def self.error(msg, color = nil, **options)
135
+ msg = colorize(msg.to_s, color, **options) if color
136
+ if logger
137
+ logger.error(msg)
138
+ else
139
+ puts msg
140
+ end
141
+ end
142
+
143
+ def self.colorize(str, color, **options)
144
+ background = options[:background] || options[:bg] || false
145
+ style = options[:style]
146
+ offsets = %i[gray red green yellow blue magenta cyan white]
147
+ styles = %i[normal bold dark italic underline xx xx underline xx strikethrough]
148
+ start = background ? 40 : 30
149
+ color_code = start + (offsets.index(color) || 8)
150
+ style_code = styles.index(style) || 0
151
+ "\e[#{style_code};#{color_code}m#{str}\e[0m"
152
+ end
110
153
  end
111
154
 
112
155
  require "boxcars/version"
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.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Francis Sullivan
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2023-02-22 00:00:00.000000000 Z
12
+ date: 2023-02-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug