boxcars 0.1.2 → 0.1.4

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: d5ecef1207a69d835d18b96f8d3412c58c84faaf15c5f783c5a8722aafd88ba0
4
- data.tar.gz: ac308f23b0b98a733ac24f364caac8e724d3c319e550e35d1b7eef2b8477fd03
3
+ metadata.gz: 0fc42578d07a962cd91fbab547ba657842c131729070e57bcf7789aa1648aaee
4
+ data.tar.gz: 3e3009663c45d58ee616d2d705c74f939b12922b24bceff74d231f5aeaff573d
5
5
  SHA512:
6
- metadata.gz: 132bddb5d37af596a9da1e91902fb64d3df55e626806ab56b76d958d7a83ef65625a3ed2fa974dcb24a2b2cba31b858d7830b4b02fe07d27cf31f264e8ae78c8
7
- data.tar.gz: 64a536a7d5e8c3f93e186bfab8d638d27c4077f6f67851461d6d01fe6d9d166e7b648d7d83ba205fbd8d45e60bd3312202632a12fe1e378341d95f3b4571c54c
6
+ metadata.gz: d5a2664fb2b4973b57e41f682a9b194394bc196ada00b9c8cf9106a7876b92065cd9f40ff270650219c282143f812241e16fc450a24f61567acaaf413f44150f
7
+ data.tar.gz: 1d73a0c1519a3834852ffe7108e43e03460cc6e1b99dbcc9c16bf73c7824016d71628878b32394a33b548a0c84de7f9a41feefa1dc922628957eb0251269cd7a
data/CHANGELOG.md CHANGED
@@ -1,15 +1,38 @@
1
- ## [0.1.2] - 2023-02-16
1
+ # Changelog
2
2
 
3
- Changes
4
- - cleaned up and added more yard docs
5
- - required open ai gem and logger inside gem to simplify use
6
- - updated README with examples
3
+ ## [v0.1.3](https://github.com/BoxcarsAI/boxcars/tree/v0.1.3) (2023-02-17)
7
4
 
8
- ## [0.1.1] - 2023-02-16
5
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.2...v0.1.3)
9
6
 
10
- Changes
11
- - updated to OpenAI gem 3.0
7
+ **Closed issues:**
12
8
 
13
- ## [0.1.0] - 2023-02-15
9
+ - generate changelog automatically [\#12](https://github.com/BoxcarsAI/boxcars/issues/12)
10
+ - Make sure the yard docs are up to date and have coverage [\#7](https://github.com/BoxcarsAI/boxcars/issues/7)
11
+ - Name changes and code movement. [\#6](https://github.com/BoxcarsAI/boxcars/issues/6)
12
+ - Specs need environment variables to be set to run green [\#4](https://github.com/BoxcarsAI/boxcars/issues/4)
14
13
 
15
- - Initial release
14
+ **Merged pull requests:**
15
+
16
+ - Get GitHub Actions to green [\#5](https://github.com/BoxcarsAI/boxcars/pull/5) ([petergoldstein](https://github.com/petergoldstein))
17
+ - Fix typo introduced by merge. Pull publish-rubygem into its own job [\#3](https://github.com/BoxcarsAI/boxcars/pull/3) ([petergoldstein](https://github.com/petergoldstein))
18
+
19
+ ## [v0.1.2](https://github.com/BoxcarsAI/boxcars/tree/v0.1.2) (2023-02-17)
20
+
21
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.1...v0.1.2)
22
+
23
+ **Merged pull requests:**
24
+
25
+ - Run GitHub Actions against multiple Rubies [\#2](https://github.com/BoxcarsAI/boxcars/pull/2) ([petergoldstein](https://github.com/petergoldstein))
26
+ - \[infra\] Added deployment step for the RubyGems [\#1](https://github.com/BoxcarsAI/boxcars/pull/1) ([AKovtunov](https://github.com/AKovtunov))
27
+
28
+ ## [v0.1.1](https://github.com/BoxcarsAI/boxcars/tree/v0.1.1) (2023-02-16)
29
+
30
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.0...v0.1.1)
31
+
32
+ ## [v0.1.0](https://github.com/BoxcarsAI/boxcars/tree/v0.1.0) (2023-02-15)
33
+
34
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/e3c50bdc76f71c6d2abb012c38174633a5847028...v0.1.0)
35
+
36
+
37
+
38
+ \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
data/Gemfile CHANGED
@@ -22,3 +22,5 @@ gem "rubocop-rspec", "~> 2.17"
22
22
  gem "sqlite3", "~> 1.6"
23
23
 
24
24
  gem "activerecord", "~> 7.0"
25
+
26
+ gem "github_changelog_generator", "~> 1.16"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- boxcars (0.1.2)
4
+ boxcars (0.1.4)
5
5
  google_search_results (~> 2.2)
6
6
  ruby-openai (~> 3.0)
7
7
 
@@ -21,7 +21,28 @@ GEM
21
21
  addressable (2.8.1)
22
22
  public_suffix (>= 2.0.2, < 6.0)
23
23
  ast (2.4.2)
24
+ async (1.30.3)
25
+ console (~> 1.10)
26
+ nio4r (~> 2.3)
27
+ timers (~> 4.1)
28
+ async-http (0.60.1)
29
+ async (>= 1.25)
30
+ async-io (>= 1.28)
31
+ async-pool (>= 0.2)
32
+ protocol-http (~> 0.24.0)
33
+ protocol-http1 (~> 0.15.0)
34
+ protocol-http2 (~> 0.15.0)
35
+ traces (>= 0.8.0)
36
+ async-http-faraday (0.11.0)
37
+ async-http (~> 0.42)
38
+ faraday
39
+ async-io (1.34.3)
40
+ async
41
+ async-pool (0.3.12)
42
+ async (>= 1.25)
24
43
  concurrent-ruby (1.2.0)
44
+ console (1.16.2)
45
+ fiber-local
25
46
  crack (0.4.5)
26
47
  rexml
27
48
  debug (1.7.1)
@@ -29,6 +50,22 @@ GEM
29
50
  reline (>= 0.3.1)
30
51
  diff-lcs (1.5.0)
31
52
  dotenv (2.8.1)
53
+ faraday (2.7.4)
54
+ faraday-net_http (>= 2.0, < 3.1)
55
+ ruby2_keywords (>= 0.0.4)
56
+ faraday-http-cache (2.4.1)
57
+ faraday (>= 0.8)
58
+ faraday-net_http (3.0.2)
59
+ fiber-local (1.0.0)
60
+ github_changelog_generator (1.16.4)
61
+ activesupport
62
+ async (>= 1.25.0)
63
+ async-http-faraday
64
+ faraday-http-cache
65
+ multi_json
66
+ octokit (~> 4.6)
67
+ rainbow (>= 2.2.1)
68
+ rake (>= 10.0)
32
69
  google_search_results (2.2.0)
33
70
  hashdiff (1.0.1)
34
71
  httparty (0.21.0)
@@ -37,15 +74,31 @@ GEM
37
74
  i18n (1.12.0)
38
75
  concurrent-ruby (~> 1.0)
39
76
  io-console (0.6.0)
77
+ io-console (0.6.0-java)
40
78
  irb (1.6.2)
41
79
  reline (>= 0.3.0)
42
80
  json (2.6.3)
81
+ json (2.6.3-java)
43
82
  mini_mime (1.1.2)
83
+ mini_portile2 (2.8.1)
44
84
  minitest (5.17.0)
85
+ multi_json (1.15.0)
45
86
  multi_xml (0.6.0)
87
+ nio4r (2.5.8)
88
+ nio4r (2.5.8-java)
89
+ octokit (4.25.1)
90
+ faraday (>= 1, < 3)
91
+ sawyer (~> 0.9)
46
92
  parallel (1.22.1)
47
93
  parser (3.2.1.0)
48
94
  ast (~> 2.4.1)
95
+ protocol-hpack (1.4.2)
96
+ protocol-http (0.24.1)
97
+ protocol-http1 (0.15.0)
98
+ protocol-http (~> 0.22)
99
+ protocol-http2 (0.15.1)
100
+ protocol-hpack (~> 1.4)
101
+ protocol-http (~> 0.18)
49
102
  public_suffix (5.0.1)
50
103
  rainbow (3.1.1)
51
104
  rake (13.0.6)
@@ -88,8 +141,16 @@ GEM
88
141
  ruby-openai (3.3.0)
89
142
  httparty (>= 0.18.1)
90
143
  ruby-progressbar (1.11.0)
144
+ ruby2_keywords (0.0.5)
145
+ sawyer (0.9.2)
146
+ addressable (>= 2.3.5)
147
+ faraday (>= 0.17.3, < 3)
148
+ sqlite3 (1.6.0)
149
+ mini_portile2 (~> 2.8.0)
91
150
  sqlite3 (1.6.0-x86_64-darwin)
92
151
  sqlite3 (1.6.0-x86_64-linux)
152
+ timers (4.3.5)
153
+ traces (0.8.0)
93
154
  tzinfo (2.0.6)
94
155
  concurrent-ruby (~> 1.0)
95
156
  unicode-display_width (2.4.2)
@@ -100,6 +161,7 @@ GEM
100
161
  hashdiff (>= 0.4.0, < 2.0.0)
101
162
 
102
163
  PLATFORMS
164
+ universal-java-11
103
165
  x86_64-darwin-21
104
166
  x86_64-darwin-22
105
167
  x86_64-linux
@@ -109,6 +171,7 @@ DEPENDENCIES
109
171
  boxcars!
110
172
  debug (~> 1.1)
111
173
  dotenv (~> 2.8)
174
+ github_changelog_generator (~> 1.16)
112
175
  rake (~> 13.0)
113
176
  rspec (~> 3.2)
114
177
  rubocop (~> 1.21)
data/README.md CHANGED
@@ -1,16 +1,26 @@
1
- # Boxcars
1
+ <h2 align="center">Boxcars</h2>
2
2
 
3
- This gem lets you compose new systems with composability using systems like OpenAI, Search, and SQL (and many more).
3
+ <h4 align="center">
4
+ <a href="https://www.boxcars.ai">Website</a> |
5
+ <a href="https://www.boxcars.ai/roadmap">Roadmap</a> |
6
+ <a href="https://www.boxcars.ai/blog">Blog</a> |
7
+ <a href="https://www.boxcars.ai/en/introduction/">Documentation</a>
8
+ </h4>
4
9
 
5
- The popular python library langchain is a precursor to this work, but we wanted to put a ruby spin on things and make it easier to get started.
10
+ <p align="center">
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
+ </p>
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).
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.
6
17
 
7
18
  ## Concepts
8
19
  All of these concepts are in a module named Boxcars:
9
- - Prompt: a prompt is used by an Engine to generate text results.
10
- - Engine: an entity that generates text from a Prompt.
11
- - Boxcar: an encapsulation that does one thing (search, math, SQL, etc) and a many use an Engine to get their work accomplished.
12
- - Conductor: given a list of Boxcars and an Engine, an answer if found by breaking the promlem into peices for an indvidual Boxcar to solve. This is all joined back together to yield a final result. There is currently only one implementation - ZeroShot.
13
- - ConductorExecutor: manages the running of a Conductor.
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.
14
24
 
15
25
  ## Installation
16
26
 
@@ -30,25 +40,45 @@ Or install it yourself as:
30
40
 
31
41
  ## Usage
32
42
 
33
- We will document many more examples soon, but here are a couple. The first step is to setup your environment variables for OpenAI and Serp (OPENAI_ACCESS_TOKEN, SERPAPI_API_KEY). If you don't want to set them in your environment, they can be passed in as well to the API.
43
+ We will be adding more examples soon, but here are a couple to get you started. First, you'll need to set up your environment variables for OpenAI and Google SERP (OPENAI_ACCESS_TOKEN, SERPAPI_API_KEY). If you prefer not to set these variables in your environment, you can pass them directly into the API.
34
44
 
35
45
  In the examples below, we added one rubygem to load the environment at the first line, but depending on what you want, you might not need this.
36
46
  ```ruby
37
47
  require "dotenv/load"
48
+ require "boxcars"
49
+ ```
50
+
51
+ Note: if you want to try out the examples below, run this command and then paste in the code segments of interest:
52
+ ```bash
53
+ irb -r dotenv/load -r boxcars
38
54
  ```
39
55
 
40
56
  ### Direct Boxcar Use
41
57
 
42
58
  ```ruby
43
59
  # run the calculator
44
- require "dotenv/load"
45
- require "boxcars"
46
-
47
- engine = Boxcars::Openai.new
60
+ engine = Boxcars::Openai.new(max_tokens: 256)
48
61
  calc = Boxcars::Calculator.new(engine: engine)
49
62
  puts calc.run "what is pi to the forth power divided by 22.1?"
50
63
  ```
64
+ Produces:
65
+ ```text
66
+ > Entering Calculator#run
67
+ what is pi to the forth power divided by 22.1?
68
+ RubyREPL: puts(Math::PI**4 / 22.1)
69
+ Answer: 4.407651178009159
70
+
71
+ 4.407651178009159
72
+ < Exiting Calculator#run
73
+ 4.407651178009159
74
+ ```
51
75
 
76
+ Note that since Openai is currently the most used Engine, if you do not pass in an engine, it will default as expected. So, this is the equialent shorter version of the above script:
77
+ ```ruby
78
+ # run the calculator
79
+ calc = Boxcars::Calculator.new # just use the default Engine
80
+ puts calc.run "what is pi to the forth power divided by 22.1?"
81
+ ```
52
82
  ### Boxcars currently implemmented
53
83
 
54
84
  Here is what we have so far, but please put up a PR with your new ideas.
@@ -58,16 +88,30 @@ Here is what we have so far, but please put up a PR with your new ideas.
58
88
 
59
89
  ### Run a list of Boxcars
60
90
  ```ruby
61
- # run a Conductor for a calculator, and search
62
- require "dotenv/load"
63
- require "boxcars"
64
-
65
- engine = Boxcars::Openai.new
66
- calc = Boxcars::Calculator.new(engine: engine)
67
- search = Boxcars::Serp.new
68
- cond = Boxcars::ZeroShot.new(engine: engine, boxcars: [calc, search])
69
- cexe = Boxcars::ConductorExecuter.new(conductor: cond)
70
- puts cexe.run "What is pi times the square root of the average temperature in Austin TX in January?"
91
+ # run a Train for a calculator, and search using default Engine
92
+ boxcars = [Boxcars::Calculator.new, Boxcars::Serp.new]
93
+ train = Boxcars.train.new(boxcars: boxcars)
94
+ puts train.run "What is pi times the square root of the average temperature in Austin TX in January?"
95
+ ```
96
+ Produces:
97
+ ```text
98
+ > Entering Zero Shot#run
99
+ What is pi times the square root of the average temperature in Austin TX in January?
100
+ Question: Average temperature in Austin TX in January
101
+ Answer: increase from 62°F to 64°F
102
+ #Observation: increase from 62°F to 64°F
103
+ > Entering Calculator#run
104
+ 64°F x pi
105
+ RubyREPL: puts (64 * Math::PI).round(2)
106
+ Answer: 201.06
107
+
108
+ 201.06
109
+ < Exiting Calculator#run
110
+ #Observation: 201.06
111
+ I now know the final answer
112
+ Final Answer: 201.06
113
+ < Exiting Zero Shot#run
114
+ 201.06
71
115
  ```
72
116
 
73
117
  ## Development
data/Rakefile CHANGED
@@ -10,3 +10,10 @@ require "rubocop/rake_task"
10
10
  RuboCop::RakeTask.new
11
11
 
12
12
  task default: %i[spec rubocop]
13
+
14
+ require 'github_changelog_generator/task'
15
+
16
+ GitHubChangelogGenerator::RakeTask.new :changelog do |config|
17
+ config.user = 'BoxcarsAI'
18
+ config.project = 'boxcars'
19
+ end
data/boxcars.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ["hi@boxcars.ai"]
10
10
 
11
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 conductor, and it does the magic."
12
+ spec.description = "You simply give a number of boxcars to a train, and it does the magic."
13
13
  spec.homepage = "https://github.com/BoxcarsAI/boxcars"
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = ">= 2.6.0"
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
24
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
25
  `git ls-files -z`.split("\x0").reject do |f|
26
- (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features|notebooks)/|\.(?:git|travis|circleci)|appveyor)})
27
27
  end
28
28
  end
29
29
  spec.bindir = "exe"
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Boxcars is a framework for running a series of tools to get an answer to a question.
4
+ module Boxcars
5
+ # A Boxcar that interprets a prompt and executes SQL code to get answers
6
+ class ActiveRecord < EngineBoxcar
7
+ # the description of this engine boxcar
8
+ ARDESC = "useful for when you need to query a database for an application named %<name>s."
9
+ LOCKED_OUT_MODELS = %w[ActiveRecord::SchemaMigration ActiveRecord::InternalMetadata ApplicationRecord].freeze
10
+ attr_accessor :connection, :input_key, :requested_models
11
+ attr_reader :except_models
12
+
13
+ # @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a train if nil.
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
28
+ @except_models = LOCKED_OUT_MODELS + kwargs[:except_models].to_a
29
+ @input_key = input_key
30
+ the_prompt = kwargs[prompt] || my_prompt
31
+ name = kwargs[:name] || "Data"
32
+ super(name: name,
33
+ description: kwargs[:description] || format(ARDESC, name: name),
34
+ engine: engine,
35
+ prompt: the_prompt,
36
+ output_key: output_key)
37
+ end
38
+
39
+ # the input keys for the prompt
40
+ # @return [Array<Symbol>] The input keys for the prompt.
41
+ def input_keys
42
+ [input_key]
43
+ end
44
+
45
+ # the output keys for the prompt
46
+ # @return [Array<Symbol>] The output keys for the prompt.
47
+ def output_keys
48
+ [output_key]
49
+ end
50
+
51
+ # call the boxcar
52
+ # @param inputs [Hash] The inputs to the boxcar.
53
+ # @return [Hash] The outputs from the boxcar.
54
+ def call(inputs:)
55
+ t = predict(question: inputs[input_key], top_k: 5, model_info: model_info, stop: ["Answer:"]).strip
56
+ answer = get_answer(t)
57
+ puts answer.colorize(:magenta)
58
+ { output_key => answer }
59
+ end
60
+
61
+ private
62
+
63
+ def wanted_models
64
+ the_models = requested_models || ::ActiveRecord::Base.descendants
65
+ the_models.reject { |m| except_models.include?(m.name) }
66
+ end
67
+
68
+ def models
69
+ models = wanted_models.map(&:name)
70
+ models.join(", ")
71
+ end
72
+
73
+ def model_info
74
+ models = wanted_models
75
+ models.pretty_inspect
76
+ end
77
+
78
+ def get_active_record_answer(text)
79
+ code = text[/^ARCode: (.*)/, 1]
80
+ 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}"
89
+ end
90
+ end
91
+
92
+ def get_answer(text)
93
+ case text
94
+ when /^ARCode:/
95
+ get_active_record_answer(text)
96
+ when /^Answer:/
97
+ text
98
+ else
99
+ raise Boxcars::Error "Unknown format from engine: #{text}"
100
+ end
101
+ end
102
+
103
+ TEMPLATE = <<~IPT
104
+ Given an input question, first create a syntactically correct Rails Active Record code to run,
105
+ then look at the results of the code and return the answer. Unless the user specifies
106
+ in her question a specific number of examples she wishes to obtain, limit your code
107
+ to at most %<top_k>s results.
108
+
109
+ Never query for all the columns from a specific model, only ask for a the few relevant attributes given the question.
110
+
111
+ Pay attention to use only the attribute names that you can see in the model description. Be careful to not query for attributes that do not exist.
112
+ Also, pay attention to which attribute is in which model.
113
+
114
+ Use the following format:
115
+ Question: "Question here"
116
+ ARCode: "Active Record code to run"
117
+ Answer: "Final answer here"
118
+
119
+ Only use the following Active Record models:
120
+ %<model_info>s
121
+
122
+ Question: %<question>s
123
+ IPT
124
+
125
+ # The prompt to use for the engine.
126
+ def my_prompt
127
+ @my_prompt ||= Prompt.new(input_variables: [:question, :top_k, :model_info], template: TEMPLATE)
128
+ end
129
+ end
130
+ end
@@ -8,7 +8,7 @@ module Boxcars
8
8
  CALCDESC = "useful for when you need to answer questions about math"
9
9
  attr_accessor :input_key
10
10
 
11
- # @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a conductor if nil.
11
+ # @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a train if nil.
12
12
  # @param prompt [Boxcars::Prompt] The prompt to use for this boxcar. Defaults to built-in prompt.
13
13
  # @param input_key [Symbol] The key to use for the input. Defaults to :question.
14
14
  # @param output_key [Symbol] The key to use for the output. Defaults to :answer.
@@ -63,7 +63,9 @@ module Boxcars
63
63
  end
64
64
  end
65
65
 
66
- TEMPLATE = <<~IPT
66
+ # our template
67
+ # rubocop:disable Style/RedundantHeredocDelimiterQuotes
68
+ TEMPLATE = <<~'IPT'
67
69
  You are GPT-3, and you can't do math.
68
70
  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.
69
71
  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:
@@ -98,6 +100,7 @@ module Boxcars
98
100
 
99
101
  Question: %<question>s
100
102
  IPT
103
+ # rubocop:enable Style/RedundantHeredocDelimiterQuotes
101
104
 
102
105
  # The prompt to use for the engine.
103
106
  def my_prompt
@@ -3,17 +3,17 @@
3
3
  # Boxcars is a framework for running a series of tools to get an answer to a question.
4
4
  module Boxcars
5
5
  # For Boxcars that use an engine to do their work.
6
- class EngineBoxcar < Boxcars::Boxcar
6
+ class EngineBoxcar < Boxcar
7
7
  attr_accessor :prompt, :engine, :output_key
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
- # @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a conductor if nil.
14
- def initialize(prompt:, engine:, output_key: "text", name: nil, description: nil)
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)
15
15
  @prompt = prompt
16
- @engine = engine
16
+ @engine = engine || Boxcars.engine.new
17
17
  @output_key = output_key
18
18
  super(name: name, description: description)
19
19
  end
@@ -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)}"
39
+ puts "Prompt after formatting:\n#{new_prompt.colorize(:cyan)}" if Boxcars.configuration.log_prompts
40
40
  prompts.push(new_prompt)
41
41
  end
42
42
  engine.generate(prompts: prompts, stop: stop)
@@ -2,8 +2,9 @@
2
2
 
3
3
  require 'google_search_results'
4
4
  module Boxcars
5
- # A Boxcar that uses the Google SerpAPI to get answers to questions.
6
- class Serp < Boxcar
5
+ # A Boxcar that uses the Google SERP API to get answers to questions.
6
+ # It looks through SERP (search engine results page) results to find the answer.
7
+ class GoogleSearch < Boxcar
7
8
  # the description of this boxcar
8
9
  SERPDESC = "useful for when you need to answer questions about current events." \
9
10
  "You should ask targeted questions"
@@ -15,14 +16,14 @@ module Boxcars
15
16
  def initialize(name: "Search", description: SERPDESC, serpapi_api_key: "not set")
16
17
  super(name: name, description: description)
17
18
  api_key = Boxcars.configuration.serpapi_api_key(serpapi_api_key: serpapi_api_key)
18
- GoogleSearch.api_key = api_key
19
+ ::GoogleSearch.api_key = api_key
19
20
  end
20
21
 
21
22
  # Get an answer from Google using the SerpAPI.
22
23
  # @param question [String] The question to ask Google.
23
24
  # @return [String] The answer to the question.
24
25
  def run(question)
25
- search = GoogleSearch.new(q: question)
26
+ search = ::GoogleSearch.new(q: question)
26
27
  rv = find_answer(search.get_hash)
27
28
  puts "Question: #{question}"
28
29
  puts "Answer: #{rv}"
@@ -33,7 +34,7 @@ module Boxcars
33
34
  # @param question [String] The question to ask Google.
34
35
  # @return [String] The location found.
35
36
  def get_location(question)
36
- search = GoogleSearch.new(q: question, limit: 3)
37
+ search = ::GoogleSearch.new(q: question, limit: 3)
37
38
  rv = search.get_location
38
39
  puts "Question: #{question}"
39
40
  puts "Answer: #{rv}"
@@ -5,21 +5,22 @@ module Boxcars
5
5
  # A Boxcar that interprets a prompt and executes SQL code to get answers
6
6
  class SQL < EngineBoxcar
7
7
  # the description of this engine boxcar
8
- SQLDESC = "useful for when you need to query a SQL database"
8
+ SQLDESC = "useful for when you need to query a database for %<name>s."
9
9
  attr_accessor :connection, :input_key
10
10
 
11
11
  # @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 conductor if nil.
12
+ # @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a train if nil.
13
13
  # @param input_key [Symbol] The key to use for the input. Defaults to :question.
14
14
  # @param output_key [Symbol] The key to use for the output. Defaults to :answer.
15
15
  # @param kwargs [Hash] Any other keyword arguments to pass to the parent class. This can include
16
16
  # :name, :description and :prompt
17
- def initialize(connection:, engine: nil, input_key: :question, output_key: :answer, **kwargs)
18
- @connection = connection
17
+ def initialize(connection: nil, engine: nil, input_key: :question, output_key: :answer, **kwargs)
18
+ @connection = connection || ::ActiveRecord::Base.connection
19
19
  @input_key = input_key
20
20
  the_prompt = kwargs[prompt] || my_prompt
21
- super(name: kwargs[:name] || "SQLdatabase",
22
- description: kwargs[:description] || SQLDESC,
21
+ name = kwargs[:name] || "data"
22
+ super(name: name,
23
+ description: kwargs[:description] || format(SQLDESC, name: name),
23
24
  engine: engine,
24
25
  prompt: the_prompt,
25
26
  output_key: output_key)
@@ -73,7 +74,6 @@ module Boxcars
73
74
  code = text[/^SQLQuery: (.*)/, 1]
74
75
  puts code.colorize(:yellow)
75
76
  output = connection.exec_query(code).to_a
76
- puts "Answer: #{output}"
77
77
  "Answer: #{output}"
78
78
  end
79
79
 
@@ -116,18 +116,5 @@ module Boxcars
116
116
  def my_prompt
117
117
  @my_prompt ||= Prompt.new(input_variables: [:question, :dialect, :top_k], template: TEMPLATE)
118
118
  end
119
-
120
- # DECIDER_TEMPLATE = <<~DPT
121
- # Given the below input question and list of potential tables, output a comma separated list of the table names that may
122
- # be necessary to answer this question.
123
- # Question: %<query>s
124
- # Table Names: %<table_names>s
125
- # Relevant Table Names:
126
- # DPT
127
- # DECIDER_PROMPT = Prompt.new(
128
- # input_variables: %i[query table_names],
129
- # template: DECIDER_TEMPLATE,
130
- # output_parser: CommaSeparatedListOutputParser
131
- # )
132
119
  end
133
120
  end
@@ -11,7 +11,7 @@ module Boxcars
11
11
  # @param return_direct [Boolean] If true, return the output of this boxcar directly, without merging it with the inputs.
12
12
  def initialize(description:, name: nil, return_direct: false)
13
13
  @name = name || self.class.name
14
- @description = description
14
+ @description = description || @name
15
15
  @return_direct = return_direct
16
16
  end
17
17
 
@@ -53,7 +53,7 @@ module Boxcars
53
53
  # @param input_list [Array<Hash>] The list of inputs.
54
54
  # @return [Array<Boxcars::Boxcar>] The list of outputs.
55
55
  def apply(input_list:)
56
- input_list.map { |inputs| new(**inputs) }
56
+ raise NotImplementedError
57
57
  end
58
58
 
59
59
  # Get an answer from the boxcar.
@@ -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 "> Enterning #{name} boxcar#run".colorize(:gray, style: :bold)
65
+ puts "> Entering #{name}#run".colorize(:gray, style: :bold)
66
66
  rv = do_run(*args, **kwargs)
67
- puts "< Exiting #{name} boxcar#run".colorize(:gray, style: :bold)
67
+ puts "< Exiting #{name}#run".colorize(:gray, style: :bold)
68
68
  rv
69
69
  end
70
70
 
@@ -116,5 +116,6 @@ end
116
116
 
117
117
  require "boxcars/boxcar/engine_boxcar"
118
118
  require "boxcars/boxcar/calculator"
119
- require "boxcars/boxcar/serp"
119
+ require "boxcars/boxcar/google_search"
120
120
  require "boxcars/boxcar/sql"
121
+ require "boxcars/boxcar/active_record"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Boxcars
4
- # Conductor's action to take.
5
- class ConductorAction
4
+ # Train's action to take.
5
+ class TrainAction
6
6
  attr_accessor :boxcar, :boxcar_input, :log
7
7
 
8
8
  def initialize(boxcar: nil, boxcar_input: nil, log: nil)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Boxcars
4
- # Conductor's return value
5
- class ConductorFinish
4
+ # Train's return value
5
+ class TrainFinish
6
6
  attr_accessor :return_values, :log
7
7
 
8
8
  def initialize(return_values, log:)
@@ -1,7 +1,7 @@
1
1
  # Agent for the MRKL chain
2
2
  module Boxcars
3
- # A Conductor using the zero-shot react method.
4
- class ZeroShot < Conductor
3
+ # A Train using the zero-shot react method.
4
+ class ZeroShot < Train
5
5
  attr_reader :boxcars, :observation_prefix, :engine_prefix
6
6
 
7
7
  # default prompt prefix
@@ -30,10 +30,10 @@ module Boxcars
30
30
  SINPUT
31
31
 
32
32
  # @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
33
- # @param engine [Boxcars::Engine] The engine to use for this conductor.
34
- # @param name [String] The name of the conductor. Defaults to 'Zero Shot'.
35
- # @param description [String] The description of the conductor. Defaults to 'Zero Shot Conductor'.
36
- def initialize(boxcars:, engine:, name: 'Zero Shot', description: 'Zero Shot Conductor')
33
+ # @param engine [Boxcars::Engine] The engine to use for this train.
34
+ # @param name [String] The name of the train. Defaults to 'Zero Shot'.
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')
37
37
  @observation_prefix = 'Observation: '
38
38
  @engine_prefix = 'Thought:'
39
39
  prompt = self.class.create_prompt(boxcars: boxcars)
@@ -2,28 +2,24 @@
2
2
 
3
3
  module Boxcars
4
4
  # @abstract
5
- class Conductor
6
- attr_reader :engine, :boxcars, :name, :description, :prompt, :engine_boxcar, :return_values
5
+ class Train < EngineBoxcar
6
+ attr_reader :engine, :boxcars, :name, :description, :prompt, :return_values, :return_intermediate_steps,
7
+ :max_iterations, :early_stopping_method
7
8
 
8
- # A Conductor will use a engine to run a series of boxcars.
9
- # @param engine [Boxcars::Engine] The engine to use for this conductor.
9
+ # A Train will use a engine to run a series of boxcars.
10
+ # @param engine [Boxcars::Engine] The engine to use for this train.
10
11
  # @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
12
+ # @param prompt [String] The prompt to use.
11
13
  # @abstract
12
- def initialize(engine:, boxcars:, prompt:, name: nil, description: nil)
13
- @engine = engine
14
+ def initialize(boxcars:, prompt:, engine: nil, **kwargs)
14
15
  @boxcars = boxcars
15
- @prompt = prompt
16
16
  @name = name || self.class.name
17
- @description = description
18
17
  @return_values = [:output]
19
- @engine_boxcar = EngineBoxcar.new(prompt: prompt, engine: engine)
20
- end
18
+ @return_intermediate_steps = kwargs[:return_intermediate_steps] || false
19
+ @max_iterations = kwargs[:max_iterations]
20
+ @early_stopping_method = kwargs[:early_stopping_method] || "force"
21
21
 
22
- # Get an answer from the conductor.
23
- # @param question [String] The question to ask the conductor.
24
- # @return [String] The answer to the question.
25
- def run(question)
26
- raise NotImplementedError
22
+ super(prompt: prompt, engine: engine, name: kwargs[:name], description: kwargs[:description])
27
23
  end
28
24
 
29
25
  # Extract the boxcar name and input from the text.
@@ -52,16 +48,16 @@ module Boxcars
52
48
  # @param full_inputs [Hash] The inputs to the engine.
53
49
  # @return [Boxcars::Action] The next action.
54
50
  def get_next_action(full_inputs)
55
- full_output = engine_boxcar.predict(**full_inputs)
51
+ full_output = predict(**full_inputs)
56
52
  parsed_output = extract_boxcar_and_input(full_output)
57
53
  while parsed_output.nil?
58
54
  full_output = _fix_text(full_output)
59
55
  full_inputs[:agent_scratchpad] += full_output
60
- output = engine_boxcar.predict(**full_inputs)
56
+ output = predict(**full_inputs)
61
57
  full_output += output
62
58
  parsed_output = extract_boxcar_and_input(full_output)
63
59
  end
64
- ConductorAction.new(boxcar: parsed_output[0], boxcar_input: parsed_output[1], log: full_output)
60
+ TrainAction.new(boxcar: parsed_output[0], boxcar_input: parsed_output[1], log: full_output)
65
61
  end
66
62
 
67
63
  # Given input, decided what to do.
@@ -73,7 +69,7 @@ module Boxcars
73
69
  new_inputs = { agent_scratchpad: thoughts, stop: stop }
74
70
  full_inputs = kwargs.merge(new_inputs)
75
71
  action = get_next_action(full_inputs)
76
- return ConductorFinish.new({ output: action.boxcar_input }, log: action.log) if action.boxcar == finish_boxcar_name
72
+ return TrainFinish.new({ output: action.boxcar_input }, log: action.log) if action.boxcar == finish_boxcar_name
77
73
 
78
74
  action
79
75
  end
@@ -95,12 +91,38 @@ module Boxcars
95
91
  list
96
92
  end
97
93
 
98
- # Check that all inputs are present.
99
- # @param inputs [Hash] The inputs to check.
100
- # @raise [RuntimeError] If any inputs are missing.
101
- def validate_inputs(inputs:)
102
- missing_keys = input_keys - inputs.keys
103
- raise "Missing some input keys: #{missing_keys}" if missing_keys.any?
94
+ # the output keys
95
+ def output_keys
96
+ return return_values + ["intermediate_steps"] if return_intermediate_steps
97
+
98
+ return_values
99
+ end
100
+
101
+ # should we continue to run?
102
+ # @param iterations [Integer] The number of iterations.
103
+ # @return [Boolean] Whether to continue.
104
+ def should_continue?(iterations)
105
+ return true if max_iterations.nil?
106
+
107
+ iterations < max_iterations
108
+ end
109
+
110
+ # handler before returning
111
+ # @param output [Boxcars::TrainFinish] The output.
112
+ # @param intermediate_steps [Array<Hash>] The intermediate steps.
113
+ # @return [Hash] The final output.
114
+ def pre_return(output, intermediate_steps)
115
+ puts output.log.colorize(:yellow)
116
+ final_output = output.return_values
117
+ final_output["intermediate_steps"] = intermediate_steps if return_intermediate_steps
118
+ final_output
119
+ end
120
+
121
+ # the prefix for the engine
122
+ # @param return_direct [Boolean] Whether to return directly.
123
+ # @return [String] The prefix.
124
+ def engine_prefix(return_direct)
125
+ return_direct ? "" : engine_prefix
104
126
  end
105
127
 
106
128
  # validate the prompt
@@ -113,10 +135,10 @@ module Boxcars
113
135
  logger.warning("`agent_scratchpad` should be a variable in prompt.input_variables. Not found, adding it at the end.")
114
136
  prompt.input_variables.append(:agent_scratchpad)
115
137
  case prompt
116
- when PromptTemplate
138
+ when Prompt
117
139
  prompt.template += "\n%<agent_scratchpad>s"
118
- when FewShotPromptTemplate
119
- prompt.suffix += "\n%<agent_scratchpad>s"
140
+ # when FewShotPromptTemplate
141
+ # prompt.suffix += "\n%<agent_scratchpad>s"
120
142
  else
121
143
  raise ValueError, "Got unexpected prompt type #{type(prompt)}"
122
144
  end
@@ -132,7 +154,7 @@ module Boxcars
132
154
  def return_stopped_response(early_stopping_method, intermediate_steps, **kwargs)
133
155
  case early_stopping_method
134
156
  when "force"
135
- ConductorFinish({ output: "Agent stopped due to max iterations." }, "")
157
+ TrainFinish({ output: "Agent stopped due to max iterations." }, "")
136
158
  when "generate"
137
159
  thoughts = ""
138
160
  intermediate_steps.each do |action, observation|
@@ -142,26 +164,61 @@ module Boxcars
142
164
  thoughts += "\n\nI now need to return a final answer based on the previous steps:"
143
165
  new_inputs = { agent_scratchpad: thoughts, stop: _stop }
144
166
  full_inputs = kwargs.merge(new_inputs)
145
- full_output = engine_boxcar.predict(**full_inputs)
167
+ full_output = predict(**full_inputs)
146
168
  parsed_output = extract_boxcar_and_input(full_output)
147
169
  if parsed_output.nil?
148
- ConductorFinish({ output: full_output }, full_output)
170
+ TrainFinish({ output: full_output }, full_output)
149
171
  else
150
172
  boxcar, boxcar_input = parsed_output
151
173
  if boxcar == finish_boxcar_name
152
- ConductorFinish({ output: boxcar_input }, full_output)
174
+ TrainFinish({ output: boxcar_input }, full_output)
153
175
  else
154
- ConductorFinish({ output: full_output }, full_output)
176
+ TrainFinish({ output: full_output }, full_output)
155
177
  end
156
178
  end
157
179
  else
158
180
  raise "early_stopping_method should be one of `force` or `generate`, got #{early_stopping_method}"
159
181
  end
160
182
  end
183
+
184
+ # execute the train train
185
+ # @param inputs [Hash] The inputs.
186
+ # @return [Hash] The output.
187
+ def call(inputs:)
188
+ prepare_for_new_call
189
+ name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
190
+ intermediate_steps = []
191
+ iterations = 0
192
+ while should_continue?(iterations)
193
+ output = plan(intermediate_steps, **inputs)
194
+ return pre_return(output, intermediate_steps) if output.is_a?(TrainFinish)
195
+
196
+ if (boxcar = name_to_boxcar_map[output.boxcar])
197
+ begin
198
+ observation = boxcar.run(output.boxcar_input)
199
+ return_direct = boxcar.return_direct
200
+ rescue StandardError => e
201
+ puts "Error in #{boxcar.name} boxcar#call: #{e}".colorize(:red)
202
+ raise e
203
+ end
204
+ else
205
+ observation = "#{output.boxcar} is not a valid boxcar, try another one."
206
+ return_direct = false
207
+ end
208
+ puts "Observation: #{observation}".colorize(:green)
209
+ intermediate_steps.append([output, observation])
210
+ if return_direct
211
+ output = TrainFinish.new({ return_values[0] => observation }, "")
212
+ return pre_return(output, intermediate_steps)
213
+ end
214
+ iterations += 1
215
+ end
216
+ output = return_stopped_response(early_stopping_method, intermediate_steps, **inputs)
217
+ pre_return(output, intermediate_steps)
218
+ end
161
219
  end
162
220
  end
163
221
 
164
- require "boxcars/conductor/conductor_action"
165
- require "boxcars/conductor/conductor_finish"
166
- require "boxcars/conductor/conductor_executer"
167
- require "boxcars/conductor/zero_shot"
222
+ require "boxcars/train/train_action"
223
+ require "boxcars/train/train_finish"
224
+ require "boxcars/train/zero_shot"
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Boxcars
4
4
  # The current version of the gem.
5
- VERSION = "0.1.2"
5
+ VERSION = "0.1.4"
6
6
  end
data/lib/boxcars.rb CHANGED
@@ -36,12 +36,13 @@ module Boxcars
36
36
  # Configuration contains gem settings
37
37
  class Configuration
38
38
  attr_writer :openai_access_token, :serpapi_api_key
39
- attr_accessor :organization_id, :logger
39
+ attr_accessor :organization_id, :logger, :log_prompts, :default_train, :default_engine
40
40
 
41
41
  def initialize
42
42
  @organization_id = nil
43
43
  @logger = Rails.logger if defined?(Rails)
44
44
  @logger ||= Logger.new($stdout)
45
+ @log_prompts = false
45
46
  end
46
47
 
47
48
  # @return [String] The OpenAI Access Token either from arg or env.
@@ -93,6 +94,16 @@ module Boxcars
93
94
  def self.configure
94
95
  yield(configuration)
95
96
  end
97
+
98
+ # Return the default Train class.
99
+ def self.train
100
+ configuration.default_train || Boxcars::ZeroShot
101
+ end
102
+
103
+ # Return the default Engine class.
104
+ def self.engine
105
+ configuration.default_engine || Boxcars::Openai
106
+ end
96
107
  end
97
108
 
98
109
  require "boxcars/version"
@@ -101,4 +112,4 @@ require "boxcars/generation"
101
112
  require "boxcars/ruby_repl"
102
113
  require "boxcars/engine"
103
114
  require "boxcars/boxcar"
104
- require "boxcars/conductor"
115
+ require "boxcars/train"
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.2
4
+ version: 0.1.4
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-17 00:00:00.000000000 Z
12
+ date: 2023-02-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -81,7 +81,7 @@ 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 conductor, and it does the magic.
84
+ description: You simply give a number of boxcars to a train, and it does the magic.
85
85
  email:
86
86
  - hi@boxcars.ai
87
87
  executables: []
@@ -102,21 +102,21 @@ files:
102
102
  - boxcars.gemspec
103
103
  - lib/boxcars.rb
104
104
  - lib/boxcars/boxcar.rb
105
+ - lib/boxcars/boxcar/active_record.rb
105
106
  - lib/boxcars/boxcar/calculator.rb
106
107
  - lib/boxcars/boxcar/engine_boxcar.rb
107
- - lib/boxcars/boxcar/serp.rb
108
+ - lib/boxcars/boxcar/google_search.rb
108
109
  - lib/boxcars/boxcar/sql.rb
109
- - lib/boxcars/conductor.rb
110
- - lib/boxcars/conductor/conductor_action.rb
111
- - lib/boxcars/conductor/conductor_executer.rb
112
- - lib/boxcars/conductor/conductor_finish.rb
113
- - lib/boxcars/conductor/zero_shot.rb
114
110
  - lib/boxcars/engine.rb
115
111
  - lib/boxcars/engine/engine_result.rb
116
112
  - lib/boxcars/engine/openai.rb
117
113
  - lib/boxcars/generation.rb
118
114
  - lib/boxcars/prompt.rb
119
115
  - lib/boxcars/ruby_repl.rb
116
+ - lib/boxcars/train.rb
117
+ - lib/boxcars/train/train_action.rb
118
+ - lib/boxcars/train/train_finish.rb
119
+ - lib/boxcars/train/zero_shot.rb
120
120
  - lib/boxcars/version.rb
121
121
  homepage: https://github.com/BoxcarsAI/boxcars
122
122
  licenses:
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Boxcars
4
- # Consists of an conductor using boxcars.
5
- class ConductorExecuter < EngineBoxcar
6
- attr_accessor :conductor, :boxcars, :return_intermediate_steps, :max_iterations, :early_stopping_method
7
-
8
- # @param conductor [Boxcars::Conductor] The conductor to use.
9
- # @param boxcars [Array<Boxcars::Boxcar>] The boxcars to use.
10
- # @param return_intermediate_steps [Boolean] Whether to return the intermediate steps. Defaults to false.
11
- # @param max_iterations [Integer] The maximum number of iterations to run. Defaults to nil.
12
- # @param early_stopping_method [String] The early stopping method to use. Defaults to "force".
13
- def initialize(conductor:, boxcars: nil, return_intermediate_steps: false, max_iterations: nil,
14
- early_stopping_method: "force")
15
- @conductor = conductor
16
- @boxcars = boxcars || conductor.boxcars
17
- @return_intermediate_steps = return_intermediate_steps
18
- @max_iterations = max_iterations
19
- @early_stopping_method = early_stopping_method
20
- # def initialize(prompt:, engine:, output_key: "text", name: nil, description: nil)
21
- super(prompt: conductor.prompt, engine: conductor.engine, name: conductor.name, description: conductor.description)
22
- end
23
-
24
- # is this the same list of boxcars?
25
- # @param boxcar_names [Array<String>] The boxcar names to compare.
26
- # @return [Boolean] Whether the boxcars are the same.
27
- def same_boxcars?(boxcar_names)
28
- conductor.allowed_boxcars.sort == boxcar_names
29
- end
30
-
31
- # validate the boxcars
32
- # @raise [RuntimeError] If the boxcars are not the same.
33
- def validate_boxcars
34
- boxcar_names = boxcars.map(&:name).sort
35
- return if same_boxcars?(boxcar_names)
36
-
37
- raise "Allowed boxcars (#{conductor.allowed_boxcars}) different than provided boxcars (#{boxcar_names})"
38
- end
39
-
40
- # the input keys
41
- def input_keys
42
- conductor.input_keys
43
- end
44
-
45
- # the output keys
46
- def output_keys
47
- return conductor.return_values + ["intermediate_steps"] if return_intermediate_steps
48
-
49
- conductor.return_values
50
- end
51
-
52
- # should we continue to run?
53
- # @param iterations [Integer] The number of iterations.
54
- # @return [Boolean] Whether to continue.
55
- def should_continue?(iterations)
56
- return true if max_iterations.nil?
57
-
58
- iterations < max_iterations
59
- end
60
-
61
- # handler before returning
62
- # @param output [Boxcars::ConductorFinish] The output.
63
- # @param intermediate_steps [Array<Hash>] The intermediate steps.
64
- # @return [Hash] The final output.
65
- def pre_return(output, intermediate_steps)
66
- puts output.log.colorize(:yellow)
67
- final_output = output.return_values
68
- final_output["intermediate_steps"] = intermediate_steps if return_intermediate_steps
69
- final_output
70
- end
71
-
72
- # the prefix for the engine
73
- # @param return_direct [Boolean] Whether to return directly.
74
- # @return [String] The prefix.
75
- def engine_prefix(return_direct)
76
- return_direct ? "" : conductor.engine_prefix
77
- end
78
-
79
- # call the conductor
80
- # @param inputs [Hash] The inputs.
81
- # @return [Hash] The output.
82
- def call(inputs:)
83
- conductor.prepare_for_new_call
84
- name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
85
- intermediate_steps = []
86
- iterations = 0
87
- while should_continue?(iterations)
88
- output = conductor.plan(intermediate_steps, **inputs)
89
- return pre_return(output, intermediate_steps) if output.is_a?(ConductorFinish)
90
-
91
- if (boxcar = name_to_boxcar_map[output.boxcar])
92
- begin
93
- observation = boxcar.run(output.boxcar_input)
94
- return_direct = boxcar.return_direct
95
- rescue StandardError => e
96
- puts "Error in #{boxcar.name} boxcar#call: #{e}".colorize(:red)
97
- raise e
98
- end
99
- else
100
- observation = "#{output.boxcar} is not a valid boxcar, try another one."
101
- return_direct = false
102
- end
103
- puts "#Observation: #{observation}".colorize(:green)
104
- intermediate_steps.append([output, observation])
105
- if return_direct
106
- output = ConductorFinish.new({ conductor.return_values[0] => observation }, "")
107
- return pre_return(output, intermediate_steps)
108
- end
109
- iterations += 1
110
- end
111
- output = conductor.return_stopped_response(early_stopping_method, intermediate_steps, **inputs)
112
- pre_return(output, intermediate_steps)
113
- end
114
- end
115
- end