boxcars 0.1.4 → 0.1.6

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: 0fc42578d07a962cd91fbab547ba657842c131729070e57bcf7789aa1648aaee
4
- data.tar.gz: 3e3009663c45d58ee616d2d705c74f939b12922b24bceff74d231f5aeaff573d
3
+ metadata.gz: 666f9b91451e8b6db61eb1162cb6508a566a46b676d646f1fb0000175714cede
4
+ data.tar.gz: bee7b55be2288bf9361012bd562f88d91f64ce7972b0cf5c495a5fce87eea77c
5
5
  SHA512:
6
- metadata.gz: d5a2664fb2b4973b57e41f682a9b194394bc196ada00b9c8cf9106a7876b92065cd9f40ff270650219c282143f812241e16fc450a24f61567acaaf413f44150f
7
- data.tar.gz: 1d73a0c1519a3834852ffe7108e43e03460cc6e1b99dbcc9c16bf73c7824016d71628878b32394a33b548a0c84de7f9a41feefa1dc922628957eb0251269cd7a
6
+ metadata.gz: '0681f5bd00bbfe2afd67011d76c561640a0575a9656b20913d3c60193208ea33361003ee652d01802b49702b106bd4f3d5bc91c1c7f2023bc2e8224edca14a8d'
7
+ data.tar.gz: 794fe20c2f9b8f14402db177bdee3d3e5318ab652e69c02929f9ae79d6daca096ae386a4e85730da4004927ab31aff53cc3a8d960ca661d6a5dd192432ffeabb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
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
+
28
+ ## [v0.1.4](https://github.com/BoxcarsAI/boxcars/tree/v0.1.4) (2023-02-22)
29
+
30
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.3...v0.1.4)
31
+
32
+ **Implemented enhancements:**
33
+
34
+ - Extend Sql concept to produce and run ActiveRecord code instead of SQL [\#9](https://github.com/BoxcarsAI/boxcars/issues/9)
35
+
36
+ **Merged pull requests:**
37
+
38
+ - first pass at an ActiveRecord boxcar [\#18](https://github.com/BoxcarsAI/boxcars/pull/18) ([francis](https://github.com/francis))
39
+ - change Boxcars::default\_train to Boxcars::train to improve code reada… [\#17](https://github.com/BoxcarsAI/boxcars/pull/17) ([francis](https://github.com/francis))
40
+ - rename class Serp to GoogleSearch [\#16](https://github.com/BoxcarsAI/boxcars/pull/16) ([francis](https://github.com/francis))
41
+ - Update README.md [\#15](https://github.com/BoxcarsAI/boxcars/pull/15) ([tabrez-syed](https://github.com/tabrez-syed))
42
+
3
43
  ## [v0.1.3](https://github.com/BoxcarsAI/boxcars/tree/v0.1.3) (2023-02-17)
4
44
 
5
45
  [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.2...v0.1.3)
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.4)
4
+ boxcars (0.1.6)
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
@@ -4,16 +4,16 @@
4
4
  <a href="https://www.boxcars.ai">Website</a> |
5
5
  <a href="https://www.boxcars.ai/roadmap">Roadmap</a> |
6
6
  <a href="https://www.boxcars.ai/blog">Blog</a> |
7
- <a href="https://www.boxcars.ai/en/introduction/">Documentation</a>
7
+ <a href="https://github.com/BoxcarsAI/boxcars/wiki">Documentation</a>
8
8
  </h4>
9
9
 
10
10
  <p align="center">
11
11
  <a href="https://github.com/BoxcarsAI/boxcars/blob/main/LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-informational" alt="License"></a>
12
12
  </p>
13
13
 
14
- Boxcars is a gem that enables you to create new systems with AI composability, using various concepts such as OpenAI, Search, SQL, Rails Active Record and more (including your concepts).
14
+ Boxcars is a gem that enables you to create new systems with AI composability, using various concepts such as OpenAI, Search, SQL, Rails Active Record and more. This can even be extended with your concepts as well.(including your concepts).
15
15
 
16
- This work was inspired by the popular Python library Langchain. However, we wanted to give it a Ruby spin and make it more user-friendly for beginners to get started.
16
+ This gem was inspired by the popular Python library Langchain. However, we wanted to give it a Ruby spin and make it more user-friendly for beginners to get started.
17
17
 
18
18
  ## Concepts
19
19
  All of these concepts are in a module named Boxcars:
@@ -79,10 +79,11 @@ Note that since Openai is currently the most used Engine, if you do not pass in
79
79
  calc = Boxcars::Calculator.new # just use the default Engine
80
80
  puts calc.run "what is pi to the forth power divided by 22.1?"
81
81
  ```
82
+ You can change the default_engine with `Boxcars::configuration.default_engine = NewDefaultEngine`
82
83
  ### Boxcars currently implemmented
83
84
 
84
85
  Here is what we have so far, but please put up a PR with your new ideas.
85
- - Search: uses the SERP API to do seaches
86
+ - GoogleSearch: uses the SERP API to do seaches
86
87
  - Calculator: uses an Engine to generate ruby code to do math
87
88
  - SQL: given an ActiveRecord connection, it will generate and run sql statments from a prompt.
88
89
 
data/boxcars.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Francis Sullivan", "Tabrez Syed"]
9
9
  spec.email = ["hi@boxcars.ai"]
10
10
 
11
- spec.summary = "Boxcars provide an API to connect together Boxcars and then conduct them. Inspired by python langchain."
12
- spec.description = "You simply give a number of boxcars to a train, and it does the magic."
11
+ spec.summary = "Boxcars is a gem that enables you to create new systems with AI composability. Inspired by python langchain."
12
+ spec.description = "You simply set an OpenAI key, give a number of Boxcars to a Train, and magic ensues when you run it."
13
13
  spec.homepage = "https://github.com/BoxcarsAI/boxcars"
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = ">= 2.6.0"
@@ -7,26 +7,22 @@ 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
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 input_key [Symbol] The key to use for the input. Defaults to :question.
16
- # @param output_key [Symbol] The key to use for the output. Defaults to :answer.
17
- # @param kwargs [Hash] Any other keyword arguments to pass to the parent class. This can include
18
- # :name, :description and :prompt
19
- def initialize(engine: nil, models: nil, input_key: :question, output_key: :answer, **kwargs)
20
- if models.is_a?(Array) && models.length.positive?
21
- @requested_models = models
22
- models.each do |m|
23
- raise ArgumentError, "model #{m} needs to be an Active Record model" unless m.ancestors.include?(::ActiveRecord::Base)
24
- end
25
- elsif models
26
- raise ArgumentError, "models needs to be an array of Active Record models"
27
- end
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.
17
+ # @param kwargs [Hash] Any other keyword arguments. These can include:
18
+ # :name, :description, :prompt, :input_key, :output_key and :except_models
19
+ def initialize(engine: nil, models: nil, read_only: nil, approval_callback: nil, **kwargs)
20
+ check_models(models)
28
21
  @except_models = LOCKED_OUT_MODELS + kwargs[:except_models].to_a
29
- @input_key = input_key
22
+ @approval_callback = approval_callback
23
+ @read_only = read_only.nil? ? !approval_callback : read_only
24
+ @input_key = kwargs[:input_key] || :question
25
+ @output_key = kwargs[:output_key] || :answer
30
26
  the_prompt = kwargs[prompt] || my_prompt
31
27
  name = kwargs[:name] || "Data"
32
28
  super(name: name,
@@ -60,6 +56,21 @@ module Boxcars
60
56
 
61
57
  private
62
58
 
59
+ def read_only?
60
+ read_only
61
+ end
62
+
63
+ def check_models(models)
64
+ if models.is_a?(Array) && models.length.positive?
65
+ @requested_models = models
66
+ models.each do |m|
67
+ raise ArgumentError, "model #{m} needs to be an Active Record model" unless m.ancestors.include?(::ActiveRecord::Base)
68
+ end
69
+ elsif models
70
+ raise ArgumentError, "models needs to be an array of Active Record models"
71
+ end
72
+ end
73
+
63
74
  def wanted_models
64
75
  the_models = requested_models || ::ActiveRecord::Base.descendants
65
76
  the_models.reject { |m| except_models.include?(m.name) }
@@ -75,21 +86,94 @@ module Boxcars
75
86
  models.pretty_inspect
76
87
  end
77
88
 
78
- def get_active_record_answer(text)
79
- code = text[/^ARCode: (.*)/, 1]
89
+ # to be safe, we wrap the code in a transaction and rollback
90
+ def rollback_after_running
91
+ rv = nil
92
+ ::ActiveRecord::Base.transaction do
93
+ rv = yield
94
+ ensure
95
+ raise ::ActiveRecord::Rollback
96
+ end
97
+ rv
98
+ end
99
+
100
+ # check for dangerous code that is outside of ActiveRecord
101
+ def safe_to_run?(code)
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
106
+ without_strings = code.gsub(/('([^'\\]*(\\.[^'\\]*)*)'|"([^"\\]*(\\.[^"\\]*)*"))/, 'XX')
107
+ word_list = without_strings.split(/[.,()]/)
108
+
109
+ bad_words.each do |w|
110
+ if word_list.include?(w)
111
+ puts "code included destructive instruction: #{w} #{code}".colorize(:red)
112
+ return false
113
+ end
114
+ end
115
+
116
+ true
117
+ end
118
+
119
+ def evaluate_input(code)
120
+ raise SecurityError, "Found unsafe code while evaluating: #{code}" unless safe_to_run?(code)
121
+
122
+ # rubocop:disable Security/Eval
123
+ eval code
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
+ puts "computing change count with: #{changes_code}".colorize(: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
+ puts "Pending Changes: #{changes}".colorize(: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)
80
151
  puts code.colorize(:yellow)
81
- begin
82
- # rubocop:disable Security/Eval
83
- output = eval code
84
- # rubocop:enable Security/Eval
85
- output = output.first if output.is_a?(Array) && output.length == 1
86
- "Answer: #{output.inspect}"
87
- rescue StandardError => e
88
- "Error: #{e.message}"
152
+ if read_only?
153
+ rollback_after_running do
154
+ evaluate_input code
155
+ end
156
+ else
157
+ evaluate_input code
89
158
  end
90
159
  end
91
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)
167
+ output = 0 if output.is_a?(Array) && output.empty?
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
170
+ "Answer: #{output.inspect}"
171
+ rescue StandardError => e
172
+ "Error: #{e.message}"
173
+ end
174
+
92
175
  def get_answer(text)
176
+ # debugger
93
177
  case text
94
178
  when /^ARCode:/
95
179
  get_active_record_answer(text)
@@ -114,6 +198,7 @@ module Boxcars
114
198
  Use the following format:
115
199
  Question: "Question here"
116
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
117
202
  Answer: "Final answer here"
118
203
 
119
204
  Only use the following Active Record models:
@@ -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
@@ -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
+ puts "RubyREPL: #{code}".colorize(: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
+ puts "Answer: #{output}".colorize(:yellow, style: :bold)
17
17
  output
18
18
  end
19
19
 
@@ -33,14 +33,15 @@ module Boxcars
33
33
  # @param engine [Boxcars::Engine] The engine to use for this train.
34
34
  # @param name [String] The name of the train. Defaults to 'Zero Shot'.
35
35
  # @param description [String] The description of the train. Defaults to 'Zero Shot Train'.
36
- def initialize(boxcars:, engine: nil, name: 'Zero Shot', description: 'Zero Shot Train')
36
+ # @param prompt [Boxcars::Prompt] The prompt to use. Defaults to the built-in prompt.
37
+ def initialize(boxcars:, engine: nil, name: 'Zero Shot', description: 'Zero Shot Train', prompt: nil)
37
38
  @observation_prefix = 'Observation: '
38
39
  @engine_prefix = 'Thought:'
39
- prompt = self.class.create_prompt(boxcars: boxcars)
40
+ prompt ||= self.class.create_prompt(boxcars: boxcars)
40
41
  super(engine: engine, boxcars: boxcars, prompt: prompt, name: name, description: description)
41
42
  end
42
43
 
43
- # Create prompt in the style of the zero shot agent.
44
+ # Create prompt in the style of the zero shot agent. Without arguments, returns the default prompt.
44
45
  # @param boxcars [Array<Boxcars::Boxcar>] List of boxcars the agent will have access to, used to format the prompt.
45
46
  # @param prefix [String] String to put before the main prompt.
46
47
  # @param suffix [String] String to put after the main prompt.
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
+ puts output.log.colorize(: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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Boxcars
4
4
  # The current version of the gem.
5
- VERSION = "0.1.4"
5
+ VERSION = "0.1.6"
6
6
  end
data/lib/boxcars.rb CHANGED
@@ -16,6 +16,9 @@ module Boxcars
16
16
  # Error class for all Boxcars value errors.
17
17
  class ValueError < Error; end
18
18
 
19
+ # Error class for all Boxcars security errors.
20
+ class SecurityError < Error; end
21
+
19
22
  # simple string colorization
20
23
  class ::String
21
24
  # colorize a string
@@ -65,7 +68,7 @@ module Boxcars
65
68
  end
66
69
 
67
70
  def key_lookup(key, kwargs)
68
- rv = if kwargs.key?(key) && kwargs[key] != "not set"
71
+ rv = if kwargs.key?(key) && !kwargs[key].nil?
69
72
  # override with kwargs if present
70
73
  kwargs[key]
71
74
  elsif (set_val = instance_variable_get("@#{key}"))
@@ -74,7 +77,6 @@ module Boxcars
74
77
  else
75
78
  # otherwise, dig out of the environment
76
79
  new_key = ENV.fetch(key.to_s.upcase, nil)
77
- send("#{key}=", new_key) if new_key
78
80
  new_key
79
81
  end
80
82
  check_key(key, rv)
@@ -104,6 +106,15 @@ module Boxcars
104
106
  def self.engine
105
107
  configuration.default_engine || Boxcars::Openai
106
108
  end
109
+
110
+ # return a proc that will ask the user for input
111
+ def self.ask_user
112
+ proc do |changes, _code|
113
+ puts "This request will make #{changes} changes. Are you sure you want to run it? (y/[n])"
114
+ answer = gets.chomp
115
+ answer.downcase == 'y'
116
+ end
117
+ end
107
118
  end
108
119
 
109
120
  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.4
4
+ version: 0.1.6
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-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -81,7 +81,8 @@ dependencies:
81
81
  - - "~>"
82
82
  - !ruby/object:Gem::Version
83
83
  version: '3.0'
84
- description: You simply give a number of boxcars to a train, and it does the magic.
84
+ description: You simply set an OpenAI key, give a number of Boxcars to a Train, and
85
+ magic ensues when you run it.
85
86
  email:
86
87
  - hi@boxcars.ai
87
88
  executables: []
@@ -144,6 +145,6 @@ requirements: []
144
145
  rubygems_version: 3.2.32
145
146
  signing_key:
146
147
  specification_version: 4
147
- summary: Boxcars provide an API to connect together Boxcars and then conduct them.
148
+ summary: Boxcars is a gem that enables you to create new systems with AI composability.
148
149
  Inspired by python langchain.
149
150
  test_files: []