boxcars 0.1.6 → 0.1.8

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: 666f9b91451e8b6db61eb1162cb6508a566a46b676d646f1fb0000175714cede
4
- data.tar.gz: bee7b55be2288bf9361012bd562f88d91f64ce7972b0cf5c495a5fce87eea77c
3
+ metadata.gz: 3de375ad403bb62fb6af1045aff6a6115cbb598c3747fe48f882fb40483289e0
4
+ data.tar.gz: 343a3077d72ce21a56d8045701a8a3bd681c5fe91fd8ffbca2cc14270b92f3bb
5
5
  SHA512:
6
- metadata.gz: '0681f5bd00bbfe2afd67011d76c561640a0575a9656b20913d3c60193208ea33361003ee652d01802b49702b106bd4f3d5bc91c1c7f2023bc2e8224edca14a8d'
7
- data.tar.gz: 794fe20c2f9b8f14402db177bdee3d3e5318ab652e69c02929f9ae79d6daca096ae386a4e85730da4004927ab31aff53cc3a8d960ca661d6a5dd192432ffeabb
6
+ metadata.gz: efc72210fb52599b7c6278502a5b4143d995e671c62c7f6c090cbddf2a8926b231b0d7ab0aa28fa1325324ded1ebac5cac4e10e608ae59077a431a20f9d62f23
7
+ data.tar.gz: eeb00a993d7bbf64ac5c7461a1e31f241407c58a19a919c0853ade7e4a3bf717ab4f69331c5bc5d8c0049a89c53353d311d41616352131379b7e87b8ec3e491d
data/.env_sample ADDED
@@ -0,0 +1,2 @@
1
+ openai_access_token: ''
2
+ serpapi_api_key: ''
data/CHANGELOG.md CHANGED
@@ -1,8 +1,20 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased](https://github.com/BoxcarsAI/boxcars/tree/HEAD)
3
+ ## [v0.1.7](https://github.com/BoxcarsAI/boxcars/tree/v0.1.7) (2023-02-27)
4
4
 
5
- [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.5...HEAD)
5
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.6...v0.1.7)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - figure out logging [\#10](https://github.com/BoxcarsAI/boxcars/issues/10)
10
+
11
+ **Merged pull requests:**
12
+
13
+ - Fix typos in README concepts [\#26](https://github.com/BoxcarsAI/boxcars/pull/26) ([MasterOdin](https://github.com/MasterOdin))
14
+
15
+ ## [v0.1.6](https://github.com/BoxcarsAI/boxcars/tree/v0.1.6) (2023-02-24)
16
+
17
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.5...v0.1.6)
6
18
 
7
19
  **Implemented enhancements:**
8
20
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- boxcars (0.1.6)
4
+ boxcars (0.1.8)
5
5
  google_search_results (~> 2.2)
6
6
  ruby-openai (~> 3.0)
7
7
 
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,7 +7,7 @@ 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, :approval_callback
10
+ attr_accessor :connection, :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.
@@ -15,43 +15,25 @@ module Boxcars
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, :input_key, :output_key and :except_models
18
+ # :name, :description, :prompt, :except_models, :top_k, and :stop
19
19
  def initialize(engine: nil, models: nil, read_only: nil, approval_callback: nil, **kwargs)
20
20
  check_models(models)
21
21
  @except_models = LOCKED_OUT_MODELS + kwargs[:except_models].to_a
22
22
  @approval_callback = approval_callback
23
23
  @read_only = read_only.nil? ? !approval_callback : read_only
24
- @input_key = kwargs[:input_key] || :question
25
- @output_key = kwargs[:output_key] || :answer
26
24
  the_prompt = kwargs[prompt] || my_prompt
27
25
  name = kwargs[:name] || "Data"
26
+ kwargs[:stop] ||= ["Answer:"]
28
27
  super(name: name,
29
28
  description: kwargs[:description] || format(ARDESC, name: name),
30
29
  engine: engine,
31
30
  prompt: the_prompt,
32
- output_key: output_key)
31
+ **kwargs)
33
32
  end
34
33
 
35
- # the input keys for the prompt
36
- # @return [Array<Symbol>] The input keys for the prompt.
37
- def input_keys
38
- [input_key]
39
- end
40
-
41
- # the output keys for the prompt
42
- # @return [Array<Symbol>] The output keys for the prompt.
43
- def output_keys
44
- [output_key]
45
- end
46
-
47
- # call the boxcar
48
- # @param inputs [Hash] The inputs to the boxcar.
49
- # @return [Hash] The outputs from the boxcar.
50
- def call(inputs:)
51
- t = predict(question: inputs[input_key], top_k: 5, model_info: model_info, stop: ["Answer:"]).strip
52
- answer = get_answer(t)
53
- puts answer.colorize(:magenta)
54
- { output_key => answer }
34
+ # @return Hash The additional variables for this boxcar.
35
+ def prediction_additional
36
+ { model_info: model_info }.merge super
55
37
  end
56
38
 
57
39
  private
@@ -83,7 +65,7 @@ module Boxcars
83
65
 
84
66
  def model_info
85
67
  models = wanted_models
86
- models.pretty_inspect
68
+ models.inspect
87
69
  end
88
70
 
89
71
  # to be safe, we wrap the code in a transaction and rollback
@@ -108,7 +90,7 @@ module Boxcars
108
90
 
109
91
  bad_words.each do |w|
110
92
  if word_list.include?(w)
111
- puts "code included destructive instruction: #{w} #{code}".colorize(:red)
93
+ Boxcars.info "code included destructive instruction: #{w} #{code}", :red
112
94
  return false
113
95
  end
114
96
  end
@@ -128,7 +110,7 @@ module Boxcars
128
110
  return 0 unless changes_code
129
111
 
130
112
  rollback_after_running do
131
- puts "computing change count with: #{changes_code}".colorize(:yellow)
113
+ Boxcars.debug "computing change count with: #{changes_code}", :yellow
132
114
  evaluate_input changes_code
133
115
  end
134
116
  end
@@ -138,7 +120,7 @@ module Boxcars
138
120
  changes = change_count(changes_code)
139
121
  return true unless changes&.positive?
140
122
 
141
- puts "Pending Changes: #{changes}".colorize(:yellow, style: :bold)
123
+ Boxcars.debug "Pending Changes: #{changes}", :yellow, style: :bold
142
124
  change_str = "#{changes} change#{'s' if changes.to_i > 1}"
143
125
  raise SecurityError, "Can not run code that makes #{change_str} in read-only mode" if read_only?
144
126
 
@@ -148,7 +130,7 @@ module Boxcars
148
130
  end
149
131
 
150
132
  def run_active_record_code(code)
151
- puts code.colorize(:yellow)
133
+ Boxcars.debug code, :yellow
152
134
  if read_only?
153
135
  rollback_after_running do
154
136
  evaluate_input code
@@ -167,13 +149,12 @@ module Boxcars
167
149
  output = 0 if output.is_a?(Array) && output.empty?
168
150
  output = output.first if output.is_a?(Array) && output.length == 1
169
151
  output = output[output.keys.first] if output.is_a?(Hash) && output.length == 1
170
- "Answer: #{output.inspect}"
152
+ "Answer: #{output.to_json}"
171
153
  rescue StandardError => e
172
154
  "Error: #{e.message}"
173
155
  end
174
156
 
175
157
  def get_answer(text)
176
- # debugger
177
158
  case text
178
159
  when /^ARCode:/
179
160
  get_active_record_answer(text)
@@ -209,7 +190,8 @@ module Boxcars
209
190
 
210
191
  # The prompt to use for the engine.
211
192
  def my_prompt
212
- @my_prompt ||= Prompt.new(input_variables: [:question, :top_k, :model_info], template: TEMPLATE)
193
+ @my_prompt ||= Prompt.new(input_variables: [:question], other_inputs: [:top_k], output_variables: [:answer],
194
+ template: TEMPLATE)
213
195
  end
214
196
  end
215
197
  end
@@ -6,42 +6,19 @@ module Boxcars
6
6
  class Calculator < EngineBoxcar
7
7
  # the description of this engine boxcar
8
8
  CALCDESC = "useful for when you need to answer questions about math"
9
- attr_accessor :input_key
10
9
 
11
10
  # @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a train if nil.
12
11
  # @param prompt [Boxcars::Prompt] The prompt to use for this boxcar. Defaults to built-in prompt.
13
- # @param input_key [Symbol] The key to use for the input. Defaults to :question.
14
- # @param output_key [Symbol] The key to use for the output. Defaults to :answer.
15
12
  # @param kwargs [Hash] Any other keyword arguments to pass to the parent class.
16
- def initialize(engine: nil, prompt: nil, input_key: :question, output_key: :answer, **kwargs)
17
- # def initialize(engine:, prompt: my_prompt, input_key: :question, output_key: :answer, **kwargs)
18
- @input_key = input_key
13
+ def initialize(engine: nil, prompt: nil, **kwargs)
14
+ # def initialize(engine:, prompt: my_prompt, output_key: :answer, **kwargs)
19
15
  the_prompt = prompt || my_prompt
16
+ kwargs[:stop] ||= ["```output"]
20
17
  super(name: kwargs[:name] || "Calculator",
21
18
  description: kwargs[:description] || CALCDESC,
22
19
  engine: engine,
23
20
  prompt: the_prompt,
24
- output_key: output_key)
25
- end
26
-
27
- # the prompt input keys
28
- def input_keys
29
- [input_key]
30
- end
31
-
32
- # the output keys
33
- def output_keys
34
- [output_key]
35
- end
36
-
37
- # call the calculator
38
- # @param inputs [Hash] The inputs to the boxcar.
39
- # @return [Hash] The outputs from the boxcar.
40
- def call(inputs:)
41
- t = predict(question: inputs[input_key], stop: ["```output"]).strip
42
- answer = get_answer(t)
43
- puts answer.colorize(:magenta)
44
- { output_key => answer }
21
+ **kwargs)
45
22
  end
46
23
 
47
24
  private
@@ -104,7 +81,7 @@ module Boxcars
104
81
 
105
82
  # The prompt to use for the engine.
106
83
  def my_prompt
107
- @my_prompt ||= Prompt.new(input_variables: [:question], template: TEMPLATE)
84
+ @my_prompt ||= Prompt.new(input_variables: [:question], output_variables: [:answer], template: TEMPLATE)
108
85
  end
109
86
  end
110
87
  end
@@ -4,17 +4,18 @@
4
4
  module Boxcars
5
5
  # For Boxcars that use an engine to do their work.
6
6
  class EngineBoxcar < Boxcar
7
- attr_accessor :prompt, :engine, :output_key
7
+ attr_accessor :prompt, :engine, :top_k, :stop
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
11
  # @param name [String] The name of the boxcar. Defaults to classname.
12
12
  # @param description [String] A description of the boxcar.
13
13
  # @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, output_key: "text", name: nil, description: nil)
14
+ def initialize(prompt:, engine: nil, name: nil, description: nil, **kwargs)
15
15
  @prompt = prompt
16
16
  @engine = engine || Boxcars.engine.new
17
- @output_key = output_key
17
+ @top_k = kwargs[:top_k] || 5
18
+ @stop = kwargs[:stop] || ["Answer:"]
18
19
  super(name: name, description: description)
19
20
  end
20
21
 
@@ -23,9 +24,14 @@ module Boxcars
23
24
  prompt.input_variables
24
25
  end
25
26
 
27
+ # the first input key for the prompt
28
+ def input_key
29
+ input_keys.first
30
+ end
31
+
26
32
  # output keys
27
33
  def output_keys
28
- [output_key]
34
+ prompt.output_variables
29
35
  end
30
36
 
31
37
  # generate a response from the engine
@@ -35,8 +41,9 @@ module Boxcars
35
41
  stop = input_list[0][:stop]
36
42
  prompts = []
37
43
  input_list.each do |inputs|
44
+ # prompt.missing_variables?(inputs)
38
45
  new_prompt = prompt.format(**inputs)
39
- puts "Prompt after formatting:\n#{new_prompt.colorize(:cyan)}" if Boxcars.configuration.log_prompts
46
+ Boxcars.debug("Prompt after formatting:\n#{new_prompt}", :cyan) if Boxcars.configuration.log_prompts
40
47
  prompts.push(new_prompt)
41
48
  end
42
49
  engine.generate(prompts: prompts, stop: stop)
@@ -48,7 +55,7 @@ module Boxcars
48
55
  def apply(input_list:)
49
56
  response = generate(input_list: input_list)
50
57
  response.generations.to_h do |generation|
51
- [output_key, generation[0].text]
58
+ [output_keys.first, generation[0].text]
52
59
  end
53
60
  end
54
61
 
@@ -56,7 +63,7 @@ module Boxcars
56
63
  # @param kwargs [Hash] A hash of input values to use for the prompt.
57
64
  # @return [String] The output value.
58
65
  def predict(**kwargs)
59
- apply(input_list: [kwargs])[output_key]
66
+ apply(input_list: [kwargs])[output_keys.first]
60
67
  end
61
68
 
62
69
  # predict a response from the engine and parse it
@@ -77,7 +84,7 @@ module Boxcars
77
84
  def apply_and_parse(input_list:)
78
85
  result = apply(input_list: input_list)
79
86
  if prompt.output_parser
80
- result.map { |r| prompt.output_parser.parse(r[output_key]) }
87
+ result.map { |r| prompt.output_parser.parse(r[output_keys.first]) }
81
88
  else
82
89
  result
83
90
  end
@@ -90,5 +97,32 @@ module Boxcars
90
97
 
91
98
  raise Boxcars::ArgumentError, "run not supported when there is not exactly one output key. Got #{output_keys}."
92
99
  end
100
+
101
+ # call the boxcar
102
+ # @param inputs [Hash] The inputs to the boxcar.
103
+ # @return [Hash] The outputs from the boxcar.
104
+ def call(inputs:)
105
+ t = predict(**prediction_variables(inputs)).strip
106
+ answer = get_answer(t)
107
+ Boxcars.debug answer, :magenta
108
+ { output_keys.first => answer }
109
+ end
110
+
111
+ # @param inputs [Hash] The inputs to the boxcar.
112
+ # @return Hash The input variable for this boxcar.
113
+ def prediction_input(inputs)
114
+ { input_key => inputs[input_key] }
115
+ end
116
+
117
+ # @return Hash The additional variables for this boxcar.
118
+ def prediction_additional
119
+ { stop: stop, top_k: top_k }
120
+ end
121
+
122
+ # @param inputs [Hash] The inputs to the boxcar.
123
+ # @return Hash The variables for this boxcar.
124
+ def prediction_variables(inputs)
125
+ prediction_input(inputs).merge(prediction_additional)
126
+ end
93
127
  end
94
128
  end
@@ -6,46 +6,26 @@ 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
- attr_accessor :connection, :input_key
9
+ attr_accessor :connection
10
10
 
11
11
  # @param connection [ActiveRecord::Connection] The SQL connection to use for this boxcar.
12
12
  # @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a train if nil.
13
- # @param input_key [Symbol] The key to use for the input. Defaults to :question.
14
- # @param output_key [Symbol] The key to use for the output. Defaults to :answer.
15
13
  # @param kwargs [Hash] Any other keyword arguments to pass to the parent class. This can include
16
- # :name, :description and :prompt
17
- def initialize(connection: nil, engine: nil, input_key: :question, output_key: :answer, **kwargs)
14
+ # :name, :description, :prompt and :top_k
15
+ def initialize(connection: nil, engine: nil, **kwargs)
18
16
  @connection = connection || ::ActiveRecord::Base.connection
19
- @input_key = input_key
20
17
  the_prompt = kwargs[prompt] || my_prompt
21
- name = kwargs[:name] || "data"
18
+ kwargs[:stop] ||= ["Answer:"]
19
+ name = kwargs[:name] || "database"
22
20
  super(name: name,
23
21
  description: kwargs[:description] || format(SQLDESC, name: name),
24
22
  engine: engine,
25
- prompt: the_prompt,
26
- output_key: output_key)
23
+ prompt: the_prompt)
27
24
  end
28
25
 
29
- # the input keys for the prompt
30
- # @return [Array<Symbol>] The input keys for the prompt.
31
- def input_keys
32
- [input_key]
33
- end
34
-
35
- # the output keys for the prompt
36
- # @return [Array<Symbol>] The output keys for the prompt.
37
- def output_keys
38
- [output_key]
39
- end
40
-
41
- # call the boxcar
42
- # @param inputs [Hash] The inputs to the boxcar.
43
- # @return [Hash] The outputs from the boxcar.
44
- def call(inputs:)
45
- t = predict(question: inputs[input_key], dialect: dialect, top_k: 5, table_info: schema, stop: ["Answer:"]).strip
46
- answer = get_answer(t)
47
- puts answer.colorize(:magenta)
48
- { output_key => answer }
26
+ # @return Hash The additional variables for this boxcar.
27
+ def prediction_additional
28
+ { schema: schema, dialect: dialect }.merge super
49
29
  end
50
30
 
51
31
  private
@@ -66,13 +46,12 @@ module Boxcars
66
46
  end
67
47
 
68
48
  def dialect
69
- # connection.instance_variable_get "@config"[:adapter]
70
49
  connection.class.name.split("::").last.sub("Adapter", "")
71
50
  end
72
51
 
73
52
  def get_embedded_sql_answer(text)
74
53
  code = text[/^SQLQuery: (.*)/, 1]
75
- puts code.colorize(:yellow)
54
+ Boxcars.debug code, :yellow
76
55
  output = connection.exec_query(code).to_a
77
56
  "Answer: #{output}"
78
57
  end
@@ -107,14 +86,18 @@ module Boxcars
107
86
  Answer: "Final answer here"
108
87
 
109
88
  Only use the following tables:
110
- %<table_info>s
89
+ %<schema>s
111
90
 
112
91
  Question: %<question>s
113
92
  IPT
114
93
 
115
94
  # The prompt to use for the engine.
116
95
  def my_prompt
117
- @my_prompt ||= Prompt.new(input_variables: [:question, :dialect, :top_k], template: TEMPLATE)
96
+ @my_prompt ||= Prompt.new(
97
+ input_variables: [:question],
98
+ other_inputs: [:top_k, :dialect, :table_info],
99
+ output_variables: [:answer],
100
+ template: TEMPLATE)
118
101
  end
119
102
  end
120
103
  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
+ Boxcars.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 " \
@@ -83,6 +83,25 @@ module Boxcars
83
83
  end
84
84
  end
85
85
 
86
+ # make sure we got a valid response
87
+ # @param response [Hash] The response to check.
88
+ # @param must_haves [Array<String>] The keys that must be in the response. Defaults to %w[choices].
89
+ # @raise [KeyError] if there is an issue with the access token.
90
+ # @raise [ValueError] if the response is not valid.
91
+ def check_response(response, must_haves: %w[choices])
92
+ if response['error']
93
+ code = response.dig('error', 'code')
94
+ msg = response.dig('error', 'message') || 'unknown error'
95
+ raise KeyError, "OPENAI_ACCESS_TOKEN not valid" if code == 'invalid_api_key'
96
+
97
+ raise ValueError, "OpenAI error: #{msg}"
98
+ end
99
+
100
+ must_haves.each do |key|
101
+ raise ValueError, "Expecting key #{key} in response" unless response.key?(key)
102
+ end
103
+ end
104
+
86
105
  # Call out to OpenAI's endpoint with k unique prompts.
87
106
  # @param prompts [Array<String>] The prompts to pass into the model.
88
107
  # @param stop [Array<String>] Optional list of stop words to use when generating.
@@ -98,6 +117,7 @@ module Boxcars
98
117
  sub_prompts = prompts.each_slice(batch_size).to_a
99
118
  sub_prompts.each do |sprompts|
100
119
  response = client(prompt: sprompts, **params)
120
+ check_response(response)
101
121
  choices.concat(response["choices"])
102
122
  keys_to_use = inkeys & response["usage"].keys
103
123
  keys_to_use.each { |key| token_usage[key] = token_usage[key].to_i + response["usage"][key] }
@@ -3,25 +3,44 @@
3
3
  module Boxcars
4
4
  # used by Boxcars that have engine's to create a prompt.
5
5
  class Prompt
6
- attr_reader :template, :input_variables, :output_variables
6
+ attr_reader :template, :input_variables, :other_inputs, :output_variables
7
7
 
8
8
  # @param template [String] The template to use for the prompt.
9
- # @param input_variables [Array<Symbol>] The input vars to use for the prompt.
10
- # @param output_variables [Array<Symbol>] The output vars to use for the prompt. Defaults to [:agent_scratchpad]
11
- def initialize(template:, input_variables:, output_variables: [:agent_scratchpad])
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(template:, input_variables: nil, other_inputs: nil, output_variables: nil)
12
13
  @template = template
13
- @input_variables = input_variables
14
- @output_variables = output_variables
14
+ @input_variables = input_variables || [:input]
15
+ @other_inputs = other_inputs || []
16
+ @output_variables = output_variables || [:output]
15
17
  end
16
18
 
17
19
  # format the prompt with the input variables
20
+ # @param inputs [Hash] The inputs to use for the prompt.
21
+ # @return [String] The formatted prompt.
22
+ # @raise [Boxcars::KeyError] if the template has extra keys.
18
23
  def format(inputs)
19
24
  @template % inputs
25
+ rescue ::KeyError => e
26
+ first_line = e.message.to_s.split("\n").first
27
+ Boxcars.error "Missing prompt input key: #{first_line}"
28
+ raise KeyError, "Prompt format error: #{first_line}"
20
29
  end
21
30
 
22
31
  # check if the template is valid
23
32
  def template_is_valid?
24
- @template.include?("%<input>s") && @template.include?("%<agent_scratchpad>s")
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}"
25
44
  end
26
45
 
27
46
  # create a prompt template from examples
@@ -29,17 +48,22 @@ module Boxcars
29
48
  # @param input_variables [Array<Symbol>] The input variables to use for the prompt.
30
49
  # @param example_separator [String] The separator to use between the examples. Defaults to "\n\n"
31
50
  # @param prefix [String] The prefix to use for the template. Defaults to ""
32
- def self.from_examples(examples:, suffix:, input_variables:, example_separator: "\n\n", prefix: "")
51
+ def self.from_examples(examples:, suffix:, input_variables:, example_separator: "\n\n", prefix: "", **kwargs)
33
52
  template = [prefix, examples, suffix].join(example_separator)
34
- Prompt.new(template: template, input_variables: input_variables)
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)
35
57
  end
36
58
 
37
59
  # create a prompt template from a file
38
60
  # @param path [String] The path to the file to use for the template.
39
- # @param input_variables [Array<Symbol>] The input variables to use for the prompt.
40
- def self.from_file(path:, input_variables:)
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)
41
64
  template = File.read(path)
42
- Prompt.new(template: template, input_variables: input_variables)
65
+ Prompt.new(template: template, input_variables: input_variables, other_inputs: other_inputs,
66
+ output_variables: output_variables)
43
67
  end
44
68
  end
45
69
  end
@@ -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(:yellow)
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(:yellow, style: :bold)
16
+ Boxcars.debug "Answer: #{output}", :yellow, style: :bold
17
17
  output
18
18
  end
19
19
 
@@ -19,6 +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
23
  FINPUT
23
24
 
24
25
  # default prompt suffix
data/lib/boxcars/train.rb CHANGED
@@ -16,7 +16,7 @@ module Boxcars
16
16
  @name = name || self.class.name
17
17
  @return_values = [:output]
18
18
  @return_intermediate_steps = kwargs[:return_intermediate_steps] || false
19
- @max_iterations = kwargs[:max_iterations]
19
+ @max_iterations = kwargs[:max_iterations] || 25
20
20
  @early_stopping_method = kwargs[:early_stopping_method] || "force"
21
21
 
22
22
  super(prompt: prompt, engine: engine, name: kwargs[:name], description: kwargs[:description])
@@ -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, style: :bold)
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
@@ -137,8 +137,6 @@ module Boxcars
137
137
  case prompt
138
138
  when Prompt
139
139
  prompt.template += "\n%<agent_scratchpad>s"
140
- # when FewShotPromptTemplate
141
- # prompt.suffix += "\n%<agent_scratchpad>s"
142
140
  else
143
141
  raise ValueError, "Got unexpected prompt type #{type(prompt)}"
144
142
  end
@@ -198,14 +196,14 @@ module Boxcars
198
196
  observation = boxcar.run(output.boxcar_input)
199
197
  return_direct = boxcar.return_direct
200
198
  rescue StandardError => e
201
- puts "Error in #{boxcar.name} boxcar#call: #{e}".colorize(:red)
202
- raise e
199
+ error "Error in #{boxcar.name} boxcar#call: #{e}", :red
200
+ observation = "Error - #{e}, correct and try again."
203
201
  end
204
202
  else
205
203
  observation = "#{output.boxcar} is not a valid boxcar, try another one."
206
204
  return_direct = false
207
205
  end
208
- puts "Observation: #{observation}".colorize(:green)
206
+ Boxcars.debug "Observation: #{observation}", :green
209
207
  intermediate_steps.append([output, observation])
210
208
  if return_direct
211
209
  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.6"
5
+ VERSION = "0.1.8"
6
6
  end
data/lib/boxcars.rb CHANGED
@@ -19,22 +19,8 @@ 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
22
+ # Error class for all Boxcars key errors.
23
+ class KeyError < Error; end
38
24
 
39
25
  # Configuration contains gem settings
40
26
  class Configuration
@@ -44,7 +30,6 @@ module Boxcars
44
30
  def initialize
45
31
  @organization_id = nil
46
32
  @logger = Rails.logger if defined?(Rails)
47
- @logger ||= Logger.new($stdout)
48
33
  @log_prompts = false
49
34
  end
50
35
 
@@ -68,18 +53,18 @@ module Boxcars
68
53
  end
69
54
 
70
55
  def key_lookup(key, kwargs)
71
- rv = if kwargs.key?(key) && !kwargs[key].nil?
72
- # override with kwargs if present
73
- kwargs[key]
74
- elsif (set_val = instance_variable_get("@#{key}"))
75
- # use saved value if present
76
- set_val
77
- else
78
- # otherwise, dig out of the environment
79
- new_key = ENV.fetch(key.to_s.upcase, nil)
80
- new_key
81
- end
82
- check_key(key, rv)
56
+ val = if kwargs.key?(key) && !kwargs[key].nil?
57
+ # override with kwargs if present
58
+ kwargs[key]
59
+ elsif (provided_val = instance_variable_get("@#{key}"))
60
+ # use saved value if present. Set using Boxcars::configuration.the_key = "abcde"
61
+ provided_val
62
+ else
63
+ # otherwise, dig out of the environment
64
+ env_val = ENV.fetch(key.to_s.upcase, nil)
65
+ env_val
66
+ end
67
+ check_key(key, val)
83
68
  end
84
69
  end
85
70
 
@@ -115,6 +100,59 @@ module Boxcars
115
100
  answer.downcase == 'y'
116
101
  end
117
102
  end
103
+
104
+ # Return a logger, possibly if set.
105
+ def self.logger
106
+ Boxcars.configuration.logger
107
+ end
108
+
109
+ # Logging system
110
+ def self.debug(msg, color = nil, **options)
111
+ msg = colorize(msg.to_s, color, **options) if color
112
+ if logger
113
+ logger.debug(msg)
114
+ else
115
+ puts msg
116
+ end
117
+ end
118
+
119
+ def self.info(msg, color = nil, **options)
120
+ msg = colorize(msg.to_s, color, **options) if color
121
+ if logger
122
+ logger.info(msg)
123
+ else
124
+ puts msg
125
+ end
126
+ end
127
+
128
+ def self.warn(msg, color = nil, **options)
129
+ msg = colorize(msg.to_s, color, **options) if color
130
+ if logger
131
+ logger.warn(msg)
132
+ else
133
+ puts msg
134
+ end
135
+ end
136
+
137
+ def self.error(msg, color = nil, **options)
138
+ msg = colorize(msg.to_s, color, **options) if color
139
+ if logger
140
+ logger.error(msg)
141
+ else
142
+ puts msg
143
+ end
144
+ end
145
+
146
+ def self.colorize(str, color, **options)
147
+ background = options[:background] || options[:bg] || false
148
+ style = options[:style]
149
+ offsets = %i[gray red green yellow blue magenta cyan white]
150
+ styles = %i[normal bold dark italic underline xx xx underline xx strikethrough]
151
+ start = background ? 40 : 30
152
+ color_code = start + (offsets.index(color) || 8)
153
+ style_code = styles.index(style) || 0
154
+ "\e[#{style_code};#{color_code}m#{str}\e[0m"
155
+ end
118
156
  end
119
157
 
120
158
  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.6
4
+ version: 0.1.8
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-24 00:00:00.000000000 Z
12
+ date: 2023-03-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -89,6 +89,7 @@ executables: []
89
89
  extensions: []
90
90
  extra_rdoc_files: []
91
91
  files:
92
+ - ".env_sample"
92
93
  - ".rspec"
93
94
  - ".rubocop.yml"
94
95
  - CHANGELOG.md