boxcars 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ # @abstract
5
+ class Conductor
6
+ attr_reader :engine, :boxcars, :name, :description, :prompt, :engine_boxcar, :return_values
7
+
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.
10
+ # @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
11
+ # @abstract
12
+ def initialize(engine:, boxcars:, prompt:, name: nil, description: nil)
13
+ @engine = engine
14
+ @boxcars = boxcars
15
+ @prompt = prompt
16
+ @name = name || self.class.name
17
+ @description = description
18
+ @return_values = [:output]
19
+ @engine_boxcar = EngineBoxcar.new(prompt: prompt, engine: engine)
20
+ end
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
27
+ end
28
+
29
+ # Extract the boxcar name and input from the text.
30
+ def extract_boxcar_and_input(text)
31
+ end
32
+
33
+ def stop
34
+ ["\n#{observation_prefix}"]
35
+ end
36
+
37
+ def construct_scratchpad(intermediate_steps)
38
+ thoughts = ""
39
+ intermediate_steps.each do |action, observation|
40
+ thoughts += action.is_a?(String) ? action : action.log
41
+ thoughts += "\n#{observation_prefix}#{observation}\n#{engine_prefix}"
42
+ end
43
+ thoughts
44
+ end
45
+
46
+ def get_next_action(full_inputs)
47
+ full_output = engine_boxcar.predict(**full_inputs)
48
+ parsed_output = extract_boxcar_and_input(full_output)
49
+ while parsed_output.nil?
50
+ full_output = _fix_text(full_output)
51
+ full_inputs[:agent_scratchpad] += full_output
52
+ output = engine_boxcar.predict(**full_inputs)
53
+ full_output += output
54
+ parsed_output = extract_boxcar_and_input(full_output)
55
+ end
56
+ ConductorAction.new(boxcar: parsed_output[0], boxcar_input: parsed_output[1], log: full_output)
57
+ end
58
+
59
+ # Given input, decided what to do.
60
+ # @param intermediate_steps [Array<Hash>] The intermediate steps taken so far along with observations.
61
+ # @param kwargs [Hash] User inputs.
62
+ # @return [Boxcars::Action] Action specifying what boxcar to use.
63
+ def plan(intermediate_steps, **kwargs)
64
+ thoughts = construct_scratchpad(intermediate_steps)
65
+ new_inputs = { agent_scratchpad: thoughts, stop: stop }
66
+ full_inputs = kwargs.merge(new_inputs)
67
+ action = get_next_action(full_inputs)
68
+ return ConductorFinish.new({ output: action.boxcar_input }, log: action.log) if action.boxcar == finish_boxcar_name
69
+
70
+ action
71
+ end
72
+
73
+ # Prepare the agent for new call, if needed
74
+ def prepare_for_new_call
75
+ end
76
+
77
+ # Name of the boxcar to use to finish the chain
78
+ def finish_boxcar_name
79
+ "Final Answer"
80
+ end
81
+
82
+ def input_keys
83
+ # Return the input keys
84
+ list = prompt.input_variables
85
+ list.delete(:agent_scratchpad)
86
+ list
87
+ end
88
+
89
+ # Check that all inputs are present.
90
+ def validate_inputs(inputs:)
91
+ missing_keys = input_keys - inputs.keys
92
+ raise "Missing some input keys: #{missing_keys}" if missing_keys.any?
93
+ end
94
+
95
+ def validate_prompt(values: Dict)
96
+ prompt = values["engine_chain"].prompt
97
+ unless prompt.input_variables.include?(:agent_scratchpad)
98
+ logger.warning("`agent_scratchpad` should be a variable in prompt.input_variables. Not found, adding it at the end.")
99
+ prompt.input_variables.append(:agent_scratchpad)
100
+ case prompt
101
+ when PromptTemplate
102
+ prompt.template += "\n%<agent_scratchpad>s"
103
+ when FewShotPromptTemplate
104
+ prompt.suffix += "\n%<agent_scratchpad>s"
105
+ else
106
+ raise ValueError, "Got unexpected prompt type #{type(prompt)}"
107
+ end
108
+ end
109
+ values
110
+ end
111
+
112
+ def return_stopped_response(early_stopping_method, intermediate_steps, **kwargs)
113
+ case early_stopping_method
114
+ when "force"
115
+ ConductorFinish({ output: "Agent stopped due to max iterations." }, "")
116
+ when "generate"
117
+ thoughts = ""
118
+ intermediate_steps.each do |action, observation|
119
+ thoughts += action.log
120
+ thoughts += "\n#{observation_prefix}#{observation}\n#{engine_prefix}"
121
+ end
122
+ thoughts += "\n\nI now need to return a final answer based on the previous steps:"
123
+ new_inputs = { agent_scratchpad: thoughts, stop: _stop }
124
+ full_inputs = kwargs.merge(new_inputs)
125
+ full_output = engine_boxcar.predict(**full_inputs)
126
+ parsed_output = extract_boxcar_and_input(full_output)
127
+ if parsed_output.nil?
128
+ ConductorFinish({ output: full_output }, full_output)
129
+ else
130
+ boxcar, boxcar_input = parsed_output
131
+ if boxcar == finish_boxcar_name
132
+ ConductorFinish({ output: boxcar_input }, full_output)
133
+ else
134
+ ConductorFinish({ output: full_output }, full_output)
135
+ end
136
+ end
137
+ else
138
+ raise "early_stopping_method should be one of `force` or `generate`, got #{early_stopping_method}"
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ require "boxcars/conductor/conductor_action"
145
+ require "boxcars/conductor/conductor_finish"
146
+ require "boxcars/conductor/conductor_executer"
147
+ require "boxcars/conductor/zero_shot"
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ # Class that contains all the relevant information for a engine result
5
+ class EngineResult
6
+ attr_accessor :generations, :engine_output
7
+
8
+ def initialize(generations: nil, engine_output: nil)
9
+ @generations = generations
10
+ @engine_output = engine_output
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,156 @@
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 engine that uses OpenAI's API.
6
+ class Openai < Engine
7
+ attr_reader :prompts, :open_ai_params, :model_kwargs, :batch_size
8
+
9
+ DEFAULT_PARAMS = {
10
+ model: "text-davinci-003",
11
+ temperature: 0.7,
12
+ max_tokens: 256
13
+ }.freeze
14
+
15
+ DEFAULT_NAME = "OpenAI engine"
16
+ DEFAULT_DESCRIPTION = "useful for when you need to use AI to answer questions. " \
17
+ "You should ask targeted questions"
18
+
19
+ # A engine is a container for a single tool to run.
20
+ # @param name [String] The name of the engine. Defaults to "OpenAI engine".
21
+ # @param description [String] A description of the engine. Defaults to:
22
+ # useful for when you need to use AI to answer questions. You should ask targeted questions".
23
+ # @param prompts [Array<String>] The prompts to use when asking the engine. Defaults to [].
24
+ # @param batch_size [Integer] The number of prompts to send to the engine at once. Defaults to 20.
25
+ def initialize(name: DEFAULT_NAME, description: DEFAULT_DESCRIPTION, prompts: [], batch_size: 20, **kwargs)
26
+ @open_ai_params = DEFAULT_PARAMS.merge(kwargs)
27
+ @prompts = prompts
28
+ @batch_size = batch_size
29
+ super(description: description, name: name)
30
+ end
31
+
32
+ # Get an answer from the engine.
33
+ # @param question [String] The question to ask the engine.
34
+ # @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
35
+ def client(prompt:, openai_access_token: 'not set', **kwargs)
36
+ access_token = Boxcars.configuration.openai_access_token(openai_access_token: openai_access_token)
37
+ organization_id = Boxcars.configuration.organization_id
38
+ clnt = ::OpenAI::Client.new(access_token: access_token, organization_id: organization_id)
39
+ the_params = { prompt: prompt }.merge(open_ai_params).merge(kwargs)
40
+ clnt.completions(parameters: the_params)
41
+ end
42
+
43
+ # get an answer from the engine for a question.
44
+ # @param question [String] The question to ask the engine.
45
+ # @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
46
+ def run(question, **kwargs)
47
+ response = client(prompt: question, **kwargs)
48
+ answer = response["choices"].map { |c| c["text"] }.join("\n").strip
49
+ puts answer
50
+ answer
51
+ end
52
+
53
+ # Build extra kwargs from additional params that were passed in.
54
+ def build_extra(values:)
55
+ values[:model_kw_args] = @open_ai_params.merge(values)
56
+ values
57
+ end
58
+
59
+ def default_params
60
+ open_ai_params
61
+ end
62
+
63
+ def generation_info(sub_choices)
64
+ sub_choices.map do |choice|
65
+ Generation.new(
66
+ text: choice["text"],
67
+ generation_info: {
68
+ finish_reason: choice.fetch("finish_reason", nil),
69
+ logprobs: choice.fetch("logprobs", nil)
70
+ }
71
+ )
72
+ end
73
+ end
74
+
75
+ # Call out to OpenAI's endpoint with k unique prompts.
76
+
77
+ # Args:
78
+ # prompts: The prompts to pass into the model.
79
+ # stop: Optional list of stop words to use when generating.
80
+
81
+ # Returns:
82
+ # The full engine output.
83
+
84
+ # Example:
85
+ # .. code-block:: ruby
86
+
87
+ # response = openai.generate(["Tell me a joke."])
88
+ def generate(prompts:, stop: nil)
89
+ params = {}
90
+ params[:stop] = stop if stop
91
+ choices = []
92
+ token_usage = {}
93
+ # Get the token usage from the response.
94
+ # Includes prompt, completion, and total tokens used.
95
+ inkeys = %w[completion_tokens prompt_tokens total_tokens].freeze
96
+ sub_prompts = prompts.each_slice(batch_size).to_a
97
+ sub_prompts.each do |sprompts|
98
+ response = client(prompt: sprompts, **params)
99
+ choices.concat(response["choices"])
100
+ keys_to_use = inkeys & response["usage"].keys
101
+ keys_to_use.each { |key| token_usage[key] = token_usage[key].to_i + response["usage"][key] }
102
+ end
103
+
104
+ n = params.fetch(:n, 1)
105
+ generations = []
106
+ prompts.each_with_index do |_prompt, i|
107
+ sub_choices = choices[i * n, (i + 1) * n]
108
+ generations.push(generation_info(sub_choices))
109
+ end
110
+ EngineResult.new(generations: generations, engine_output: { token_usage: token_usage })
111
+ end
112
+ # rubocop:enable Metrics/AbcSize
113
+ end
114
+
115
+ def identifying_params
116
+ params = { model_name: model_name }
117
+ params.merge!(default_params)
118
+ params
119
+ end
120
+
121
+ def engine_type
122
+ "openai"
123
+ end
124
+
125
+ # calculate the number of tokens used
126
+ def get_num_tokens(text:)
127
+ text.split.length
128
+ end
129
+
130
+ def modelname_to_contextsize(modelname)
131
+ model_lookup = {
132
+ 'text-davinci-003': 4097,
133
+ 'text-curie-001': 2048,
134
+ 'text-babbage-001': 2048,
135
+ 'text-ada-001': 2048,
136
+ 'code-davinci-002': 8000,
137
+ 'code-cushman-001': 2048
138
+ }.freeze
139
+ model_lookup[modelname] || 4097
140
+ end
141
+
142
+ # Calculate the maximum number of tokens possible to generate for a prompt.
143
+
144
+ # Args:
145
+ # prompt: The prompt to use.
146
+
147
+ # Returns:
148
+ # The maximum number of tokens possible to generate for a prompt.
149
+ def max_tokens_for_prompt(prompt)
150
+ num_tokens = get_num_tokens(prompt)
151
+
152
+ # get max context size for model by name
153
+ max_size = modelname_to_contextsize(model_name)
154
+ max_size - num_tokens
155
+ end
156
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ # @abstract
5
+ class Engine
6
+ # An Engine is used by Boxcars to generate output from prompts
7
+ # @param name [String] The name of the Engine. Defaults to classname.
8
+ # @param description [String] A description of the Engine.
9
+ def initialize(description: 'Engine', name: nil)
10
+ @name = name || self.class.name
11
+ @description = description
12
+ end
13
+
14
+ # Get an answer from the Engine.
15
+ # @param question [String] The question to ask the Engine.
16
+ def run(question)
17
+ raise NotImplementedError
18
+ end
19
+ end
20
+ end
21
+
22
+ require "boxcars/engine/engine_result"
23
+ require "boxcars/engine/openai"
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ # Output of a single generation
5
+ class Generation
6
+ attr_accessor :text, :generation_info
7
+
8
+ def initialize(text: nil, generation_info: nil)
9
+ @text = text
10
+ @generation_info = generation_info
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ # used by Boxcars that have engine's to create a prompt.
5
+ class Prompt
6
+ attr_reader :template, :input_variables, :output_variables
7
+
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])
12
+ @template = template
13
+ @input_variables = input_variables
14
+ @output_variables = output_variables
15
+ end
16
+
17
+ # format the prompt with the input variables
18
+ def format(inputs)
19
+ @template % inputs
20
+ end
21
+
22
+ # check if the template is valid
23
+ def template_is_valid?
24
+ @template.include?("%<input>s") && @template.include?("%<agent_scratchpad>s")
25
+ end
26
+
27
+ # create a prompt template from examples
28
+ # @param examples [String] or [Array<String>] The example(s) to use for the template.
29
+ # @param input_variables [Array<Symbol>] The input variables to use for the prompt.
30
+ # @param example_separator [String] The separator to use between the examples. Defaults to "\n\n"
31
+ # @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: "")
33
+ template = [prefix, examples, suffix].join(example_separator)
34
+ Prompt.new(template: template, input_variables: input_variables)
35
+ end
36
+
37
+ # create a prompt template from a file
38
+ # @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:)
41
+ template = File.read(path)
42
+ Prompt.new(template: template, input_variables: input_variables)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ # used by Boxcars to run ruby code
5
+ class RubyREPL
6
+ def call(code:)
7
+ puts "RubyREPL: #{code}".colorize(:red)
8
+ output = ""
9
+ IO.popen("ruby", "r+") do |io|
10
+ io.puts code
11
+ io.close_write
12
+ output = io.read
13
+ end
14
+ puts "Answer: #{output}".colorize(:red, style: :bold)
15
+ output
16
+ end
17
+
18
+ def run(command)
19
+ call(code: command)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ VERSION = "0.1.0"
5
+ end
data/lib/boxcars.rb ADDED
@@ -0,0 +1,93 @@
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
+ # Error class for all Boxcars errors.
6
+ class Error < StandardError; end
7
+ class ConfigurationError < Error; end
8
+ class ArgumentError < Error; end
9
+ class ValueError < Error; end
10
+
11
+ # simple string colorization
12
+ class ::String
13
+ def colorize(color, options = {})
14
+ background = options[:background] || options[:bg] || false
15
+ style = options[:style]
16
+ offsets = %i[gray red green yellow blue magenta cyan white]
17
+ styles = %i[normal bold dark italic underline xx xx underline xx strikethrough]
18
+ start = background ? 40 : 30
19
+ color_code = start + (offsets.index(color) || 8)
20
+ style_code = styles.index(style) || 0
21
+ "\e[#{style_code};#{color_code}m#{self}\e[0m"
22
+ end
23
+ end
24
+
25
+ # Configuration contains gem settings
26
+ class Configuration
27
+ attr_writer :openai_access_token, :serpapi_api_key
28
+ attr_accessor :organization_id, :logger
29
+
30
+ def initialize
31
+ @organization_id = nil
32
+ @logger = Rails.logger if defined?(Rails)
33
+ @logger ||= Logger.new($stdout)
34
+ end
35
+
36
+ # @return [String] The OpenAI Access Token either from arg or env.
37
+ def openai_access_token(**kwargs)
38
+ key_lookup(:openai_access_token, kwargs)
39
+ end
40
+
41
+ # @return [String] The SerpAPI API key either from arg or env.
42
+ def serpapi_api_key(**kwargs)
43
+ key_lookup(:serpapi_api_key, kwargs)
44
+ end
45
+
46
+ private
47
+
48
+ def check_key(key, val)
49
+ return val unless val.nil? || val.empty?
50
+
51
+ error_text = ":#{key} missing! Please pass key, or set #{key.to_s.upcase} environment variable."
52
+ raise ConfigurationError, error_text
53
+ end
54
+
55
+ def key_lookup(key, kwargs)
56
+ rv = if kwargs.key?(key) && kwargs[key] != "not set"
57
+ # override with kwargs if present
58
+ kwargs[key]
59
+ elsif (set_val = instance_variable_get("@#{key}"))
60
+ # use saved value if present
61
+ set_val
62
+ else
63
+ # otherwise, dig out of the environment
64
+ new_key = ENV.fetch(key.to_s.upcase, nil)
65
+ send("#{key}=", new_key) if new_key
66
+ new_key
67
+ end
68
+ check_key(key, rv)
69
+ end
70
+ end
71
+
72
+ # @return [Boxcars::Configuration] The configuration object.
73
+ class << self
74
+ attr_writer :configuration
75
+ end
76
+
77
+ def self.configuration
78
+ @configuration ||= Boxcars::Configuration.new
79
+ end
80
+
81
+ # Configure the gem.
82
+ def self.configure
83
+ yield(configuration)
84
+ end
85
+ end
86
+
87
+ require "boxcars/version"
88
+ require "boxcars/prompt"
89
+ require "boxcars/generation"
90
+ require "boxcars/ruby_repl"
91
+ require "boxcars/engine"
92
+ require "boxcars/boxcar"
93
+ require "boxcars/conductor"
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: boxcars
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Francis Sullivan
8
+ - Tabrez Syed
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2023-02-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: debug
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.1'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.1'
28
+ - !ruby/object:Gem::Dependency
29
+ name: dotenv
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '2.8'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.8'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '3.2'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '3.2'
56
+ - !ruby/object:Gem::Dependency
57
+ name: google_search_results
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '2.2'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '2.2'
70
+ - !ruby/object:Gem::Dependency
71
+ name: ruby-openai
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '2.2'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '2.2'
84
+ description: You simply give a number of boxcars to a conductor, and it does the magic.
85
+ email:
86
+ - hi@boxcars.ai
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".rspec"
92
+ - ".rubocop.yml"
93
+ - CHANGELOG.md
94
+ - CODE_OF_CONDUCT.md
95
+ - Gemfile
96
+ - Gemfile.lock
97
+ - LICENSE.txt
98
+ - README.md
99
+ - Rakefile
100
+ - bin/console
101
+ - bin/setup
102
+ - lib/boxcars.rb
103
+ - lib/boxcars/boxcar.rb
104
+ - lib/boxcars/boxcar/calculator.rb
105
+ - lib/boxcars/boxcar/engine_boxcar.rb
106
+ - lib/boxcars/boxcar/serp.rb
107
+ - lib/boxcars/boxcar/sql.rb
108
+ - lib/boxcars/conductor.rb
109
+ - lib/boxcars/conductor/conductor_action.rb
110
+ - lib/boxcars/conductor/conductor_executer.rb
111
+ - lib/boxcars/conductor/conductor_finish.rb
112
+ - lib/boxcars/conductor/zero_shot.rb
113
+ - lib/boxcars/engine.rb
114
+ - lib/boxcars/engine/engine_result.rb
115
+ - lib/boxcars/engine/openai.rb
116
+ - lib/boxcars/generation.rb
117
+ - lib/boxcars/prompt.rb
118
+ - lib/boxcars/ruby_repl.rb
119
+ - lib/boxcars/version.rb
120
+ homepage: https://github.com/BoxcarsAI/boxcars
121
+ licenses:
122
+ - MIT
123
+ metadata:
124
+ homepage_uri: https://github.com/BoxcarsAI/boxcars
125
+ source_code_uri: https://github.com/BoxcarsAI/boxcars
126
+ changelog_uri: https://github.com/BoxcarsAI/boxcars/blob/main/CHANGELOG.md
127
+ rubygems_mfa_required: 'true'
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 2.6.0
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubygems_version: 3.2.32
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: Boxcars provide an API to connect together Boxcars and then conduct them.
147
+ Inspired by python langchain.
148
+ test_files: []