boxcars 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50a47cf449c063e7ca02cb36ad315008fa6a5813063a24d92ed4c0f94743c12e
4
- data.tar.gz: fad47ad6fefc7d580251edf1675ae575a9f4eebf28fb903a1c82ba2868310bc4
3
+ metadata.gz: 967f04279864f1419fbe3e9f02817cc8f5594317c6f2b06dd49d1c5e1baa67a9
4
+ data.tar.gz: 156ca722488b91cefce4d03026490434004e1dcbd1f7c7cd714f1d3f6871cbc0
5
5
  SHA512:
6
- metadata.gz: '073919902e84d95158573400ebe593d1e5aa72cc1ab6efb0c4cfccaf9806a91a0c55913d9d9ec47dd8b56f7fb81660fdfd81753092e2a3bd590e4ef782c51ba8'
7
- data.tar.gz: 87f1ec995889c7ca107ccf187f1b01874f136787861e5116abc13da5f204f4f3e7e8cf101692fed7285ac875f405e1f8e08701f3785e0c5e83f25d0e5a0b9b15
6
+ metadata.gz: 14158dbd32be6d95e35c2331e26d2e8883bc7621585d5f24b54d1fce6ba78b15e3d67df73b9ed13faf1fd5c306b9341cd4705a3ecf9e5beaf9d47f05e7c1f438
7
+ data.tar.gz: 90722b9f3b7f34607cd4ee4afe5888c625c5724904943a5160021b48ca1944af5a87269613aca554b595c73c32c923ee9fc442eb8b0271a712fe6aac5702f2ac
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased](https://github.com/BoxcarsAI/boxcars/tree/HEAD)
4
+
5
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.1...HEAD)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - return a structure from boxcars instead of just a string [\#31](https://github.com/BoxcarsAI/boxcars/issues/31)
10
+ - modify SQL boxcar to take a list of Tables. Handy if you have a large database with many tables. [\#19](https://github.com/BoxcarsAI/boxcars/issues/19)
11
+
12
+ **Closed issues:**
13
+
14
+ - make a new type of prompt that uses conversations [\#38](https://github.com/BoxcarsAI/boxcars/issues/38)
15
+ - Add test coverage [\#8](https://github.com/BoxcarsAI/boxcars/issues/8)
16
+
17
+ ## [v0.2.1](https://github.com/BoxcarsAI/boxcars/tree/v0.2.1) (2023-03-08)
18
+
19
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.0...v0.2.1)
20
+
21
+ **Implemented enhancements:**
22
+
23
+ - add the ability to use the ChatGPT API - a ChatGPT Boxcar::Engine [\#32](https://github.com/BoxcarsAI/boxcars/issues/32)
24
+ - Prompt simplification [\#29](https://github.com/BoxcarsAI/boxcars/issues/29)
25
+
26
+ **Merged pull requests:**
27
+
28
+ - use structured results internally and bring SQL up to parity with ActiveRecord boxcar. [\#36](https://github.com/BoxcarsAI/boxcars/pull/36) ([francis](https://github.com/francis))
29
+
3
30
  ## [v0.2.0](https://github.com/BoxcarsAI/boxcars/tree/v0.2.0) (2023-03-07)
4
31
 
5
32
  [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.1.8...v0.2.0)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- boxcars (0.2.1)
4
+ boxcars (0.2.2)
5
5
  google_search_results (~> 2.2)
6
6
  ruby-openai (~> 3.0)
7
7
 
@@ -131,6 +131,7 @@ module Boxcars
131
131
  end
132
132
 
133
133
  def run_active_record_code(code)
134
+ code = ::Regexp.last_match(1) if code =~ /`(.+)`/
134
135
  Boxcars.debug code, :yellow
135
136
  if read_only?
136
137
  rollback_after_running do
@@ -150,16 +151,29 @@ module Boxcars
150
151
  output
151
152
  end
152
153
 
154
+ def extract_code(code)
155
+ case code
156
+ when /^```ruby/
157
+ code.split('```ruby').last.split('```').first.strip
158
+ when /^`(.+)`/
159
+ ::Regexp.last_match(1)
160
+ else
161
+ code
162
+ end
163
+ end
164
+
153
165
  def get_active_record_answer(text)
154
- code = text[/^ARCode: (.*)/, 1]
155
- changes_code = text[/^ARChanges: (.*)/, 1]
166
+ code = extract_code text.split('ARChanges:').first.strip.split('ARCode:').last.strip
167
+ changes_code = extract_code text.split('ARChanges:').last.strip if text =~ /^ARChanges:/
156
168
  return Result.new(status: :ok, explanation: "code to run", code: code, changes_code: changes_code) if code_only?
157
169
 
158
170
  raise SecurityError, "Permission to run code that makes changes denied" unless approved?(changes_code, code)
159
171
 
160
172
  output = clean_up_output(run_active_record_code(code))
161
173
  Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code: code)
162
- rescue StandardError => e
174
+ rescue SecurityError, ConfigurationError => e
175
+ raise e
176
+ rescue Boxcars::Error => e
163
177
  Result.new(status: :error, answer: nil, explanation: "Error: #{e.message}", code: code)
164
178
  end
165
179
 
@@ -170,37 +184,40 @@ module Boxcars
170
184
  when /^Answer:/
171
185
  Result.from_text(text)
172
186
  else
173
- Result.from_error("Unknown format from engine: #{text}")
187
+ Result.from_error("Try answering again. Expected your answer to start with 'ARCode:'. You gave me:\n#{text}")
174
188
  end
175
189
  end
176
190
 
177
- TEMPLATE = <<~IPT
178
- Given an input question, first create a syntactically correct Rails Active Record code to run,
179
- then look at the results of the code and return the answer. Unless the user specifies
180
- in her question a specific number of examples she wishes to obtain, limit your code
181
- to at most %<top_k>s results.
182
-
183
- Never query for all the columns from a specific model, only ask for a the few relevant attributes given the question.
184
-
185
- 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.
186
- Also, pay attention to which attribute is in which model.
187
-
188
- Use the following format:
189
- Question: "Question here"
190
- ARCode: "Active Record code to run"
191
- ARChanges: "Active Record code to compute the number of records going to change" - Only add this line if the ARCode on the line before will make data changes
192
- Answer: "Final answer here"
193
-
194
- Only use the following Active Record models:
195
- %<model_info>s
196
-
197
- Question: %<question>s
198
- IPT
191
+ CTEMPLATE = [
192
+ syst("You are a Ruby on Rails Active Record code generator"),
193
+ syst("Given an input question, first create a syntactically correct Rails Active Record code to run, ",
194
+ "then look at the results of the code and return the answer. Unless the user specifies ",
195
+ "in her question a specific number of examples she wishes to obtain, limit your code ",
196
+ "to at most %<top_k>s results.\n",
197
+ "Never query for all the columns from a specific model, ",
198
+ "only ask for the relevant attributes given the question.\n",
199
+ "Also, pay attention to which attribute is in which model."),
200
+ syst("Use the following format:\n",
201
+ "Question: ${{Question here}}\n",
202
+ "ARCode: ${{Active Record code to run}} - make sure you use valid code\n",
203
+ "ARChanges: ${{Active Record code to compute the number of records going to change}} - ",
204
+ "Only add this line if the ARCode on the line before will make data changes.\n",
205
+ "Answer: ${{Final answer here}}"),
206
+ syst("Only use the following Active Record models: %<model_info>s\n",
207
+ "Pay attention to use only the attribute names that you can see in the model description. ",
208
+ "Be careful to not query for attributes that do not exist.\n"
209
+ ),
210
+ assi("Question: %<question>s")
211
+ ].freeze
199
212
 
200
213
  # The prompt to use for the engine.
201
214
  def my_prompt
202
- @my_prompt ||= Prompt.new(input_variables: [:question], other_inputs: [:top_k], output_variables: [:answer],
203
- template: TEMPLATE)
215
+ @conversation ||= Conversation.new(lines: CTEMPLATE)
216
+ @my_prompt ||= ConversationPrompt.new(
217
+ conversation: @conversation,
218
+ input_variables: [:question],
219
+ other_inputs: [:top_k],
220
+ output_variables: [:answer])
204
221
  end
205
222
  end
206
223
  end
@@ -21,7 +21,8 @@ module Boxcars
21
21
  private
22
22
 
23
23
  def get_embedded_ruby_answer(text)
24
- code = text[8..-4].split("```").first.strip
24
+ code = text.split("```ruby\n").last.split("```").first.strip
25
+ # code = text[8..-4].split("```").first.strip
25
26
  ruby_executor = Boxcars::RubyREPL.new
26
27
  ruby_executor.call(code: code)
27
28
  end
@@ -33,52 +34,40 @@ module Boxcars
33
34
  when /^Answer:/
34
35
  Result.from_text(text)
35
36
  else
36
- Result.new(status: :error, explanation: "Unknown format from engine: #{text}")
37
+ Result.new(status: :error,
38
+ explanation: "Error: expecting your response to begin with '```ruby'. Try answering the question again.")
37
39
  end
38
40
  end
39
41
 
40
42
  # our template
41
- # rubocop:disable Style/RedundantHeredocDelimiterQuotes
42
- TEMPLATE = <<~'IPT'
43
- You are GPT-3, and you can't do math.
44
- 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.
45
- So we hooked you up to a Ruby 3 kernel, and now you can execute code written in the Ruby programming language. If anyone gives you a hard math problem, just use this format and we’ll take care of the rest:
46
-
47
- Question: ${{Question with hard calculation.}}
48
- ```ruby
49
- ${{Code that prints what you need to know}}
50
- ```
51
- ```output
52
- ${{Output of your code}}
53
- ```
54
- Answer: ${{Answer}}
55
-
56
- Otherwise, use this simpler format:
57
-
58
- Question: ${{Question without hard calculation}}
59
- Answer: ${{Answer}}
60
-
61
- Begin.
62
-
63
- Question: What is 37593 * 67?
64
- ```ruby
65
- puts(37593 * 67)
66
- ```
67
- ```output
68
- 2518731
69
- ```
70
- Answer: 2518731
71
-
72
- Question: what is 2518731 + 0?
73
- Answer: 2518731
74
-
75
- Question: %<question>s
76
- IPT
77
- # rubocop:enable Style/RedundantHeredocDelimiterQuotes
43
+ CTEMPLATE = [
44
+ syst("You can do basic math, but for any hard calculations that a human could not do ",
45
+ "in their head, use the following approach instead. ",
46
+ "Return code written in the Ruby programming language that prints the results. ",
47
+ "If anyone gives you a hard math problem, just ",
48
+ "use the following format and we’ll take care of the rest:\n",
49
+ "${{Question with hard calculation.}}\n",
50
+ "reply only with the following format:\n",
51
+ "```ruby\n${{only Ruby code that prints the answer}}\n```\n",
52
+ "```output\n${{Output of your code}}\n```\n\n",
53
+ "Otherwise, you should use this simpler format:\n",
54
+ "${{Question without hard calculation}}\n",
55
+ "Answer: ${{Answer}}\n\n",
56
+ "Do not give an explanation of the answer and make sure your answer starts with either 'Answer:' or '```ruby'."),
57
+ syst("here is a hard example:\n", "the user asks: What is 37593 * 67?\n",
58
+ "your answer: ```ruby\nputs(37593 * 67)\n```\n```output\n2518731\n```\nAnswer: 2518731"),
59
+ syst("basic example:\n", "user asks: What is 2518731 + 0?\n", "you answer: Answer: 2518731"),
60
+ syst("Begin."),
61
+ user("%<question>s")
62
+ ].freeze
78
63
 
79
64
  # The prompt to use for the engine.
80
65
  def my_prompt
81
- @my_prompt ||= Prompt.new(input_variables: [:question], output_variables: [:answer], template: TEMPLATE)
66
+ @conversation ||= Conversation.new(lines: CTEMPLATE)
67
+ @my_prompt ||= ConversationPrompt.new(
68
+ conversation: @conversation,
69
+ input_variables: [:question],
70
+ output_variables: [:answer])
82
71
  end
83
72
  end
84
73
  end
@@ -8,15 +8,14 @@ module Boxcars
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
- # @param name [String] The name of the boxcar. Defaults to classname.
12
- # @param description [String] A description of the boxcar.
13
11
  # @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, name: nil, description: nil, **kwargs)
12
+ # @param kwargs [Hash] Additional arguments including: name, description, top_k, return_direct, and stop
13
+ def initialize(prompt:, engine: nil, **kwargs)
15
14
  @prompt = prompt
16
15
  @engine = engine || Boxcars.engine.new
17
- @top_k = kwargs[:top_k] || 5
18
- @stop = kwargs[:stop] || ["Answer:"]
19
- super(name: name, description: description, return_direct: kwargs[:return_direct])
16
+ @top_k = kwargs.delete(:top_k) || 5
17
+ @stop = kwargs.delete(:stop) || ["Answer:"]
18
+ super(**kwargs)
20
19
  end
21
20
 
22
21
  # input keys for the prompt
@@ -34,60 +33,39 @@ module Boxcars
34
33
  prompt.output_variables
35
34
  end
36
35
 
36
+ # the first output key
37
+ def output_key
38
+ output_keys.first
39
+ end
40
+
37
41
  # generate a response from the engine
38
42
  # @param input_list [Array<Hash>] A list of hashes of input values to use for the prompt.
43
+ # @param current_conversation [Boxcars::Conversation] Optional ongoing conversation to use for the prompt.
39
44
  # @return [Boxcars::EngineResult] The result from the engine.
40
- def generate(input_list:)
45
+ def generate(input_list:, current_conversation: nil)
41
46
  stop = input_list[0][:stop]
42
- prompts = []
43
- input_list.each do |inputs|
44
- # prompt.missing_variables?(inputs)
45
- new_prompt = prompt.format(**inputs)
46
- Boxcars.debug("Prompt after formatting:\n#{new_prompt}", :cyan) if Boxcars.configuration.log_prompts
47
- prompts.push(new_prompt)
48
- end
47
+ the_prompt = current_conversation ? prompt.with_conversation(current_conversation) : prompt
48
+ prompts = input_list.map { |inputs| [the_prompt, inputs] }
49
49
  engine.generate(prompts: prompts, stop: stop)
50
50
  end
51
51
 
52
52
  # apply a response from the engine
53
53
  # @param input_list [Array<Hash>] A list of hashes of input values to use for the prompt.
54
+ # @param current_conversation [Boxcars::Conversation] Optional ongoing conversation to use for the prompt.
54
55
  # @return [Hash] A hash of the output key and the output value.
55
- def apply(input_list:)
56
- response = generate(input_list: input_list)
56
+ def apply(input_list:, current_conversation: nil)
57
+ response = generate(input_list: input_list, current_conversation: current_conversation)
57
58
  response.generations.to_h do |generation|
58
59
  [output_keys.first, generation[0].text]
59
60
  end
60
61
  end
61
62
 
62
63
  # predict a response from the engine
64
+ # @param current_conversation [Boxcars::Conversation] Optional ongoing conversation to use for the prompt.
63
65
  # @param kwargs [Hash] A hash of input values to use for the prompt.
64
66
  # @return [String] The output value.
65
- def predict(**kwargs)
66
- apply(input_list: [kwargs])[output_keys.first]
67
- end
68
-
69
- # predict a response from the engine and parse it
70
- # @param kwargs [Hash] A hash of input values to use for the prompt.
71
- # @return [String] The output value.
72
- def predict_and_parse(**kwargs)
73
- result = predict(**kwargs)
74
- if prompt.output_parser
75
- prompt.output_parser.parse(result)
76
- else
77
- result
78
- end
79
- end
80
-
81
- # apply a response from the engine and parse it
82
- # @param input_list [Array<Hash>] A list of hashes of input values to use for the prompt.
83
- # @return [Array<String>] The output values.
84
- def apply_and_parse(input_list:)
85
- result = apply(input_list: input_list)
86
- if prompt.output_parser
87
- result.map { |r| prompt.output_parser.parse(r[output_keys.first]) }
88
- else
89
- result
90
- end
67
+ def predict(current_conversation: nil, **kwargs)
68
+ apply(current_conversation: current_conversation, input_list: [kwargs])[output_keys.first]
91
69
  end
92
70
 
93
71
  # check that there is exactly one output key
@@ -102,10 +80,28 @@ module Boxcars
102
80
  # @param inputs [Hash] The inputs to the boxcar.
103
81
  # @return [Hash] The outputs from the boxcar.
104
82
  def call(inputs:)
105
- t = predict(**prediction_variables(inputs)).strip
106
- answer = get_answer(t)
107
- Boxcars.debug answer.to_json, :magenta
108
- { output_keys.first => answer }
83
+ # if we get errors back, try predicting again giving the errors with the inputs
84
+ conversation = nil
85
+ answer = nil
86
+ 4.times do
87
+ t = predict(current_conversation: conversation, **prediction_variables(inputs)).strip
88
+ answer = get_answer(t)
89
+ if answer.status == :error
90
+ Boxcars.debug "have error, trying again: #{answer.answer}", :red
91
+ conversation ||= Conversation.new
92
+ conversation.add_user(answer.answer)
93
+ else
94
+ Boxcars.debug answer.to_json, :magenta
95
+ return { output_keys.first => answer }
96
+ end
97
+ end
98
+ Boxcars.error answer.to_json, :red
99
+ { output_key => "Error: #{answer}" }
100
+ rescue Boxcars::ConfigurationError => e
101
+ raise e
102
+ rescue Boxcars::Error => e
103
+ Boxcars.error e.message, :red
104
+ { output_key => "Error: #{e.message}" }
109
105
  end
110
106
 
111
107
  # @param inputs [Hash] The inputs to the boxcar.
@@ -87,41 +87,36 @@ module Boxcars
87
87
  when /^Answer:/
88
88
  Result.from_text(text)
89
89
  else
90
- Result.from_error("Unknown format from engine: #{text}")
90
+ Result.from_error("Try answering again. Expected your answer to start with 'SQLQuery:'. You gave me:\n#{text}")
91
91
  end
92
92
  end
93
93
 
94
- TEMPLATE = <<~IPT
95
- Given an input question, first create a syntactically correct %<dialect>s SQL query to run,
96
- then look at the results of the query and return the answer. Unless the user specifies
97
- in his question a specific number of examples he wishes to obtain, always limit your query
98
- to at most %<top_k>s results using a LIMIT clause. You can order the results by a relevant column
99
- to return the most interesting examples in the database.
100
-
101
- Never query for all the columns from a specific table, only ask for a the few relevant columns given the question.
102
-
103
- Pay attention to use only the column names that you can see in the schema description. Be careful to not query for columns that do not exist.
104
- Also, pay attention to which column is in which table.
105
-
106
- Use the following format:
107
- Question: "Question here"
108
- SQLQuery: "SQL Query to run"
109
- SQLResult: "Result of the SQLQuery"
110
- Answer: "Final answer here"
111
-
112
- Only use the following tables:
113
- %<schema>s
114
-
115
- Question: %<question>s
116
- IPT
94
+ CTEMPLATE = [
95
+ syst("Given an input question, first create a syntactically correct %<dialect>s SQL query to run, ",
96
+ "then look at the results of the query and return the answer. Unless the user specifies ",
97
+ "in her question a specific number of examples he wishes to obtain, always limit your query ",
98
+ "to at most %<top_k>s results using a LIMIT clause. You can order the results by a relevant column ",
99
+ "to return the most interesting examples in the database.\n",
100
+ "Never query for all the columns from a specific table, only ask for the elevant columns given the question.\n",
101
+ "Pay attention to use only the column names that you can see in the schema description. Be careful to ",
102
+ "not query for columns that do not exist. Also, pay attention to which column is in which table."),
103
+ syst("Use the following format:\n",
104
+ "Question: 'Question here'\n",
105
+ "SQLQuery: 'SQL Query to run'\n",
106
+ "SQLResult: 'Result of the SQLQuery'\n",
107
+ "Answer: 'Final answer here'"),
108
+ syst("Only use the following tables:\n%<schema>s"),
109
+ user("Question: %<question>s")
110
+ ].freeze
117
111
 
118
112
  # The prompt to use for the engine.
119
113
  def my_prompt
120
- @my_prompt ||= Prompt.new(
114
+ @conversation ||= Conversation.new(lines: CTEMPLATE)
115
+ @my_prompt ||= ConversationPrompt.new(
116
+ conversation: @conversation,
121
117
  input_variables: [:question],
122
118
  other_inputs: [:top_k, :dialect, :table_info],
123
- output_variables: [:answer],
124
- template: TEMPLATE)
119
+ output_variables: [:answer])
125
120
  end
126
121
  end
127
122
  end
@@ -78,6 +78,22 @@ module Boxcars
78
78
  rv
79
79
  end
80
80
 
81
+ # helpers for conversation prompt building
82
+ # assistant message
83
+ def self.assi(*strs)
84
+ [:assistant, strs.join]
85
+ end
86
+
87
+ # system message
88
+ def self.syst(*strs)
89
+ [:system, strs.join]
90
+ end
91
+
92
+ # user message
93
+ def self.user(*strs)
94
+ [:user, strs.join]
95
+ end
96
+
81
97
  private
82
98
 
83
99
  # Get an answer from the boxcar.
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ # used to keep track of the conversation
5
+ class Conversation
6
+ attr_reader :lines, :show_roles
7
+
8
+ PEOPLE = %i[system user assistant].freeze
9
+
10
+ def initialize(lines: [], show_roles: false)
11
+ @lines = lines
12
+ check_lines(@lines)
13
+ @show_roles = show_roles
14
+ end
15
+
16
+ # check the lines
17
+ def check_lines(lines)
18
+ raise ArgumentError, "Lines must be an array" unless lines.is_a?(Array)
19
+
20
+ lines.each do |ln|
21
+ raise ArgumentError, "Conversation item must be a array" unless ln.is_a?(Array)
22
+ raise ArgumentError, "Conversation item must have 2 items, role and text" unless ln.size == 2
23
+ raise ArgumentError, "Conversation item must have a role #{ln} in (#{PEOPLE})" unless PEOPLE.include? ln[0]
24
+ raise ArgumentError, "Conversation value must be a string" unless ln[1].is_a?(String)
25
+ end
26
+ end
27
+
28
+ # @return [Array] The result as a convesation array
29
+ def to_a
30
+ lines
31
+ end
32
+
33
+ # @return [String] A conversation string
34
+ def to_s
35
+ lines.map { |ln| "#{ln[0]}: #{ln[1]}" }.join("\n")
36
+ end
37
+
38
+ # add assistant text to the conversation at the end
39
+ # @param text [String] The text to add
40
+ def add_assistant(text)
41
+ @lines << [:assistant, text]
42
+ end
43
+
44
+ # add user text to the conversation at the end
45
+ # @param text [String] The text to add
46
+ def add_user(text)
47
+ @lines << [:user, text]
48
+ end
49
+
50
+ # add system text to the conversation at the end
51
+ # @param text [String] The text to add
52
+ def add_system(text)
53
+ @lines << [:system, text]
54
+ end
55
+
56
+ # add multiple lines to the conversation
57
+ def add_lines(lines)
58
+ check_lines(lines)
59
+ @lines += lines
60
+ end
61
+
62
+ # add a conversation to the conversation
63
+ def add_conversation(conversation)
64
+ @lines += conversation.lines
65
+ end
66
+
67
+ # return just the messages for the conversation
68
+ def message_text
69
+ lines.map(&:last).join("\n")
70
+ end
71
+
72
+ # compute the prompt parameters with input substitutions (used for chatGPT)
73
+ # @param inputs [Hash] The inputs to use for the prompt.
74
+ # @return [Hash] The formatted prompt { messages: ...}
75
+ def as_messages(inputs = nil)
76
+ { messages: lines.map { |ln| { role: ln.first, content: ln.last % inputs } } }
77
+ rescue ::KeyError => e
78
+ first_line = e.message.to_s.split("\n").first
79
+ Boxcars.error "Missing prompt input key: #{first_line}"
80
+ raise KeyError, "Prompt format error: #{first_line}"
81
+ end
82
+
83
+ # compute the prompt parameters with input substitutions
84
+ # @param inputs [Hash] The inputs to use for the prompt.
85
+ # @return [Hash] The formatted prompt { prompt: "..."}
86
+ def as_prompt(inputs = nil)
87
+ if show_roles
88
+ lines.map { |ln| format("#{ln.first}: #{ln.last}", inputs) }.join("\n\n")
89
+ else
90
+ lines.map { |ln| format(ln.last, inputs) }.join("\n\n")
91
+ end
92
+ rescue ::KeyError => e
93
+ first_line = e.message.to_s.split("\n").first
94
+ Boxcars.error "Missing prompt input key: #{first_line}"
95
+ raise KeyError, "Prompt format error: #{first_line}"
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxcars
4
+ # used by Boxcars that have engine's to create a conversation prompt.
5
+ class ConversationPrompt < Prompt
6
+ attr_reader :conversation
7
+
8
+ # @param conversation [Boxcars::Conversation] The conversation to use for the prompt.
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(conversation:, input_variables: nil, other_inputs: nil, output_variables: nil)
13
+ @conversation = conversation
14
+ super(template: template, input_variables: input_variables, other_inputs: other_inputs, output_variables: output_variables)
15
+ end
16
+
17
+ # prompt for chatGPT params
18
+ # @param inputs [Hash] The inputs to use for the prompt.
19
+ # @return [Hash] The formatted prompt.
20
+ def as_messages(inputs)
21
+ conversation.as_messages(inputs)
22
+ end
23
+
24
+ # prompt for non chatGPT params
25
+ # @param inputs [Hash] The inputs to use for the prompt.
26
+ # @return [Hash] The formatted prompt.
27
+ def as_prompt(inputs)
28
+ { prompt: conversation.as_prompt(inputs) }
29
+ end
30
+
31
+ # tack on the ongoing conversation if present to the prompt
32
+ def with_conversation(conversation)
33
+ return self unless conversation
34
+
35
+ new_prompt = dup
36
+ new_prompt.conversation.add_conversation(conversation)
37
+ new_prompt
38
+ end
39
+ end
40
+ end
@@ -10,7 +10,7 @@ module Boxcars
10
10
  # The default parameters to use when asking the engine.
11
11
  DEFAULT_PARAMS = {
12
12
  model: "gpt-3.5-turbo",
13
- temperature: 0.7,
13
+ temperature: 0.2,
14
14
  max_tokens: 512
15
15
  }.freeze
16
16
 
@@ -38,17 +38,22 @@ module Boxcars
38
38
  # @param openai_access_token [String] The access token to use when asking the engine.
39
39
  # Defaults to Boxcars.configuration.openai_access_token.
40
40
  # @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
41
- def client(prompt:, openai_access_token: nil, **kwargs)
41
+ def client(prompt:, inputs: {}, openai_access_token: nil, **kwargs)
42
42
  access_token = Boxcars.configuration.openai_access_token(openai_access_token: openai_access_token)
43
43
  organization_id = Boxcars.configuration.organization_id
44
44
  clnt = ::OpenAI::Client.new(access_token: access_token, organization_id: organization_id)
45
- the_params = { prompt: prompt }.merge(open_ai_params).merge(kwargs)
46
- if the_params[:model] == "gpt-3.5-turbo"
45
+ params = open_ai_params.merge(kwargs)
46
+ if params[:model] == "gpt-3.5-turbo"
47
47
  prompt = prompt.first if prompt.is_a?(Array)
48
- the_params = { messages: [{ role: "user", content: prompt }] }.merge(open_ai_params).merge(kwargs)
49
- clnt.chat(parameters: the_params)
48
+ params = prompt.as_messages(inputs).merge(params)
49
+ if Boxcars.configuration.log_prompts
50
+ Boxcars.debug(params[:messages].map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
51
+ end
52
+ clnt.chat(parameters: params)
50
53
  else
51
- clnt.completions(parameters: the_params)
54
+ params = prompt.as_prompt(inputs).merge(params)
55
+ Boxcars.debug("Prompt after formatting:\n#{params[:prompt]}", :cyan) if Boxcars.configuration.log_prompts
56
+ clnt.completions(parameters: params)
52
57
  end
53
58
  end
54
59
 
@@ -56,7 +61,8 @@ module Boxcars
56
61
  # @param question [String] The question to ask the engine.
57
62
  # @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
58
63
  def run(question, **kwargs)
59
- response = client(prompt: question, **kwargs)
64
+ prompt = Prompt.new(template: question)
65
+ response = client(prompt: prompt, **kwargs)
60
66
  answer = response["choices"].map { |c| c.dig("message", "content") || c["text"] }.join("\n").strip
61
67
  puts answer
62
68
  answer
@@ -110,6 +116,7 @@ module Boxcars
110
116
 
111
117
  # Call out to OpenAI's endpoint with k unique prompts.
112
118
  # @param prompts [Array<String>] The prompts to pass into the model.
119
+ # @param inputs [Array<String>] The inputs to subsitite into the prompt.
113
120
  # @param stop [Array<String>] Optional list of stop words to use when generating.
114
121
  # @return [EngineResult] The full engine output.
115
122
  def generate(prompts:, stop: nil)
@@ -120,13 +127,14 @@ module Boxcars
120
127
  # Get the token usage from the response.
121
128
  # Includes prompt, completion, and total tokens used.
122
129
  inkeys = %w[completion_tokens prompt_tokens total_tokens].freeze
123
- sub_prompts = prompts.each_slice(batch_size).to_a
124
- sub_prompts.each do |sprompts|
125
- response = client(prompt: sprompts, **params)
126
- check_response(response)
127
- choices.concat(response["choices"])
128
- keys_to_use = inkeys & response["usage"].keys
129
- keys_to_use.each { |key| token_usage[key] = token_usage[key].to_i + response["usage"][key] }
130
+ prompts.each_slice(batch_size) do |sub_prompts|
131
+ sub_prompts.each do |sprompts, inputs|
132
+ response = client(prompt: sprompts, inputs: inputs, **params)
133
+ check_response(response)
134
+ choices.concat(response["choices"])
135
+ usage_keys = inkeys & response["usage"].keys
136
+ usage_keys.each { |key| token_usage[key] = token_usage[key].to_i + response["usage"][key] }
137
+ end
130
138
  end
131
139
 
132
140
  n = params.fetch(:n, 1)
@@ -16,6 +16,31 @@ module Boxcars
16
16
  @output_variables = output_variables || [:output]
17
17
  end
18
18
 
19
+ # compute the prompt parameters with input substitutions (used for chatGPT)
20
+ # @param inputs [Hash] The inputs to use for the prompt.
21
+ # @return [Hash] The formatted prompt { messages: ...}
22
+ def as_prompt(inputs)
23
+ { prompt: format(inputs) }
24
+ end
25
+
26
+ # compute the prompt parameters with input substitutions
27
+ # @param inputs [Hash] The inputs to use for the prompt.
28
+ # @return [Hash] The formatted prompt { prompt: "..."}
29
+ def as_messages(inputs)
30
+ { messages: [{ role: :assistant, content: format(inputs) }] }
31
+ end
32
+
33
+ # tack on the ongoing conversation if present to the prompt
34
+ def with_conversation(conversation)
35
+ return self unless conversation
36
+
37
+ new_prompt = dup
38
+ new_prompt.template += "\n\n#{conversation.message_text}"
39
+ new_prompt
40
+ end
41
+
42
+ private
43
+
19
44
  # format the prompt with the input variables
20
45
  # @param inputs [Hash] The inputs to use for the prompt.
21
46
  # @return [String] The formatted prompt.
@@ -27,43 +52,5 @@ module Boxcars
27
52
  Boxcars.error "Missing prompt input key: #{first_line}"
28
53
  raise KeyError, "Prompt format error: #{first_line}"
29
54
  end
30
-
31
- # check if the template is valid
32
- def template_is_valid?
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}"
44
- end
45
-
46
- # create a prompt template from examples
47
- # @param examples [String] or [Array<String>] The example(s) to use for the template.
48
- # @param input_variables [Array<Symbol>] The input variables to use for the prompt.
49
- # @param example_separator [String] The separator to use between the examples. Defaults to "\n\n"
50
- # @param prefix [String] The prefix to use for the template. Defaults to ""
51
- def self.from_examples(examples:, suffix:, input_variables:, example_separator: "\n\n", prefix: "", **kwargs)
52
- template = [prefix, examples, suffix].join(example_separator)
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)
57
- end
58
-
59
- # create a prompt template from a file
60
- # @param path [String] The path to the file to use for the template.
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)
64
- template = File.read(path)
65
- Prompt.new(template: template, input_variables: input_variables, other_inputs: other_inputs,
66
- output_variables: output_variables)
67
- end
68
55
  end
69
56
  end
@@ -1,35 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Agent for the MRKL chain
2
4
  module Boxcars
3
5
  # A Train using the zero-shot react method.
4
6
  class ZeroShot < Train
5
7
  attr_reader :boxcars, :observation_prefix, :engine_prefix
6
8
 
7
- # default prompt prefix
8
- PREFIX = "Answer the following questions as best you can. You have access to the following actions:".freeze
9
-
10
- # default prompt instructions
11
- FORMAT_INSTRUCTIONS = <<~FINPUT.freeze
12
- Use the following format:
13
-
14
- Question: the input question you must answer
15
- Thought: you should always think about what to do
16
- Action: the action to take, should be one of [%<boxcar_names>s]
17
- Action Input: the input to the action
18
- Observation: the result of the action
19
- ... (this Thought/Action/Action Input/Observation sequence can repeat N times)
20
- Thought: I now know the final answer
21
- Final Answer: the final answer to the original input question
22
- Next Actions: If you have them, up to three suggested actions for the user to take after getting this answer.
23
- FINPUT
24
-
25
- # default prompt suffix
26
- SUFFIX = <<~SINPUT.freeze
27
- Begin!
28
-
29
- Question: %<input>s
30
- Thought:%<agent_scratchpad>s
31
- SINPUT
32
-
33
9
  # @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
34
10
  # @param engine [Boxcars::Engine] The engine to use for this train.
35
11
  # @param name [String] The name of the train. Defaults to 'Zero Shot'.
@@ -38,26 +14,26 @@ module Boxcars
38
14
  def initialize(boxcars:, engine: nil, name: 'Zero Shot', description: 'Zero Shot Train', prompt: nil)
39
15
  @observation_prefix = 'Observation: '
40
16
  @engine_prefix = 'Thought:'
41
- prompt ||= self.class.create_prompt(boxcars: boxcars)
17
+ prompt ||= my_prompt
42
18
  super(engine: engine, boxcars: boxcars, prompt: prompt, name: name, description: description)
43
19
  end
44
20
 
45
- # Create prompt in the style of the zero shot agent. Without arguments, returns the default prompt.
46
- # @param boxcars [Array<Boxcars::Boxcar>] List of boxcars the agent will have access to, used to format the prompt.
47
- # @param prefix [String] String to put before the main prompt.
48
- # @param suffix [String] String to put after the main prompt.
49
- # @param input_variables [Array<Symbol>] List of input variables the final prompt will expect.
50
- # @return [Boxcars::Prompt] A Prompt with the template assembled from the pieces here.
51
- def self.create_prompt(boxcars:, prefix: PREFIX, suffix: SUFFIX, input_variables: [:input, :agent_scratchpad])
52
- boxcar_strings = boxcars.map { |boxcar| "#{boxcar.name}: #{boxcar.description}" }.join("\n")
53
- boxcar_names = boxcars.map(&:name)
54
- format_instructions = format(FORMAT_INSTRUCTIONS, boxcar_names: boxcar_names.join(", "))
55
- template = [prefix, boxcar_strings, format_instructions, suffix].join("\n\n")
56
- Prompt.new(template: template, input_variables: input_variables)
21
+ # @return Hash The additional variables for this boxcar.
22
+ def prediction_additional
23
+ { boxcar_names: boxcar_names, boxcar_descriptions: boxcar_descriptions }.merge super
24
+ end
25
+
26
+ # Extract the boxcar and input from the engine output.
27
+ # @param text [String] The output from the engine.
28
+ # @return [Array<Boxcars::Boxcar, String>] The boxcar and input.
29
+ def extract_boxcar_and_input(text)
30
+ get_action_and_input(engine_output: text)
57
31
  end
58
32
 
33
+ private
34
+
59
35
  # the final answer action string
60
- FINAL_ANSWER_ACTION = "Final Answer:".freeze
36
+ FINAL_ANSWER_ACTION = "Final Answer:"
61
37
 
62
38
  # Parse out the action and input from the engine output.
63
39
  # @param engine_output [String] The output from the engine.
@@ -70,13 +46,13 @@ module Boxcars
70
46
  if engine_output.include?(FINAL_ANSWER_ACTION)
71
47
  answer = engine_output.split(FINAL_ANSWER_ACTION).last.strip
72
48
  Result.new(status: :ok, answer: answer, explanation: engine_output)
73
- # ['Final Answer', answer]
74
49
  else
75
50
  # the thought should be the frist line here if it doesn't start with "Action:"
76
51
  thought = engine_output.split(/\n+/).reject(&:empty?).first
77
52
  Boxcars.debug("Though: #{thought}", :cyan)
78
53
  regex = /Action: (?<action>.*)\nAction Input: (?<action_input>.*)/
79
54
  match = regex.match(engine_output)
55
+ # TODO: this should return an error to the results that can be used for corrections
80
56
  raise ValueError, "Could not parse engine output: #{engine_output}" unless match
81
57
 
82
58
  action = match[:action].strip
@@ -85,11 +61,40 @@ module Boxcars
85
61
  end
86
62
  end
87
63
 
88
- # Extract the boxcar and input from the engine output.
89
- # @param text [String] The output from the engine.
90
- # @return [Array<Boxcars::Boxcar, String>] The boxcar and input.
91
- def extract_boxcar_and_input(text)
92
- get_action_and_input(engine_output: text)
64
+ CTEMPLATE = [
65
+ syst("Answer the following questions as best you can. You have access to the following actions:\n",
66
+ "%<boxcar_descriptions>s"),
67
+ syst("Use the following format:\n",
68
+ "Question: the input question you must answer\n",
69
+ "Thought: you should always think about what to do\n",
70
+ "Action: the action to take, should be one of [%<boxcar_names>s]\n",
71
+ "Action Input: the input to the action\n",
72
+ "Observation: the result of the action\n",
73
+ "... (this Thought/Action/Action Input/Observation sequence can repeat N times)\n",
74
+ "Thought: I now know the final answer\n",
75
+ "Final Answer: the final answer to the original input question\n",
76
+ "Next Actions: If you have them, up to 3 suggested actions for the user to take after getting this answer.\n",
77
+ "Begin!"),
78
+ user("Question: %<input>s"),
79
+ assi("Thought: %<agent_scratchpad>s")
80
+ ].freeze
81
+
82
+ def boxcar_names
83
+ @boxcar_names ||= boxcars.map(&:name)
84
+ end
85
+
86
+ def boxcar_descriptions
87
+ @boxcar_descriptions ||= boxcars.map { |boxcar| "#{boxcar.name}: #{boxcar.description}" }.join("\n")
88
+ end
89
+
90
+ # The prompt to use for the train.
91
+ def my_prompt
92
+ @conversation ||= Conversation.new(lines: CTEMPLATE)
93
+ @my_prompt ||= ConversationPrompt.new(
94
+ conversation: @conversation,
95
+ input_variables: [:input],
96
+ other_inputs: [:boxcar_names, :boxcar_descriptions, :agent_scratchpad],
97
+ output_variables: [:answer])
93
98
  end
94
99
  end
95
100
  end
data/lib/boxcars/train.rb CHANGED
@@ -3,23 +3,25 @@
3
3
  module Boxcars
4
4
  # @abstract
5
5
  class Train < EngineBoxcar
6
- attr_reader :engine, :boxcars, :name, :description, :prompt, :return_values, :return_intermediate_steps,
7
- :max_iterations, :early_stopping_method
6
+ attr_reader :boxcars, :return_values, :return_intermediate_steps,
7
+ :max_iterations, :early_stopping_method, :name_to_boxcar_map
8
8
 
9
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.
11
10
  # @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
12
- # @param prompt [String] The prompt to use.
11
+ # @param prompt [Boxcars::Prompt] The prompt to use.
12
+ # @param engine [Boxcars::Engine] The engine to use for this train.
13
+ # @param kwargs [Hash] Additional arguments including: name, description, top_k, return_direct, and stop
13
14
  # @abstract
14
15
  def initialize(boxcars:, prompt:, engine: nil, **kwargs)
15
16
  @boxcars = boxcars
16
- @name = name || self.class.name
17
+ @name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
17
18
  @return_values = [:output]
18
- @return_intermediate_steps = kwargs[:return_intermediate_steps] || false
19
- @max_iterations = kwargs[:max_iterations] || 25
20
- @early_stopping_method = kwargs[:early_stopping_method] || "force"
19
+ @return_intermediate_steps = kwargs.delete(:return_intermediate_steps) || false
20
+ @max_iterations = kwargs.delete(:max_iterations) || 25
21
+ @early_stopping_method = kwargs.delete(:early_stopping_method) || "force"
22
+ kwargs[:stop] ||= ["\n#{observation_prefix}"]
21
23
 
22
- super(prompt: prompt, engine: engine, name: kwargs[:name], description: kwargs[:description])
24
+ super(prompt: prompt, engine: engine, **kwargs)
23
25
  end
24
26
 
25
27
  # Extract the boxcar name and input from the text.
@@ -27,11 +29,6 @@ module Boxcars
27
29
  def extract_boxcar_and_input(text)
28
30
  end
29
31
 
30
- # the stop strings list
31
- def stop
32
- ["\n#{observation_prefix}"]
33
- end
34
-
35
32
  # build the scratchpad for the engine
36
33
  # @param intermediate_steps [Array] The intermediate steps to build the scratchpad from.
37
34
  # @return [String] The scratchpad.
@@ -48,13 +45,14 @@ module Boxcars
48
45
  # @param full_inputs [Hash] The inputs to the engine.
49
46
  # @return [Boxcars::Action] The next action.
50
47
  def get_next_action(full_inputs)
51
- full_output = predict(**full_inputs)
52
- parsed_output = extract_boxcar_and_input(full_output)
53
- while parsed_output.nil?
48
+ full_output = ""
49
+ parsed_output = nil
50
+ loop do
54
51
  full_inputs[:agent_scratchpad] += full_output
55
52
  output = predict(**full_inputs)
56
53
  full_output += output
57
54
  parsed_output = extract_boxcar_and_input(full_output)
55
+ break unless parsed_output.nil?
58
56
  end
59
57
  if parsed_output.is_a?(Result)
60
58
  TrainAction.from_result(boxcar: "Final Answer", result: parsed_output, log: full_output)
@@ -69,8 +67,7 @@ module Boxcars
69
67
  # @return [Boxcars::Action] Action specifying what boxcar to use.
70
68
  def plan(intermediate_steps, **kwargs)
71
69
  thoughts = construct_scratchpad(intermediate_steps)
72
- new_inputs = { agent_scratchpad: thoughts, stop: stop }
73
- full_inputs = kwargs.merge(new_inputs)
70
+ full_inputs = prediction_additional.merge(kwargs).merge(agent_scratchpad: thoughts)
74
71
  action = get_next_action(full_inputs)
75
72
  return TrainFinish.new({ output: action.boxcar_input }, log: action.log) if action.boxcar == finish_boxcar_name
76
73
 
@@ -187,7 +184,6 @@ module Boxcars
187
184
  # @return [Hash] The output.
188
185
  def call(inputs:)
189
186
  prepare_for_new_call
190
- name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
191
187
  intermediate_steps = []
192
188
  iterations = 0
193
189
  while should_continue?(iterations)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Boxcars
4
4
  # The current version of the gem.
5
- VERSION = "0.2.1"
5
+ VERSION = "0.2.2"
6
6
  end
data/lib/boxcars.rb CHANGED
@@ -107,6 +107,7 @@ module Boxcars
107
107
  end
108
108
 
109
109
  # Logging system
110
+ # debug log
110
111
  def self.debug(msg, color = nil, **options)
111
112
  msg = colorize(msg.to_s, color, **options) if color
112
113
  if logger
@@ -116,6 +117,7 @@ module Boxcars
116
117
  end
117
118
  end
118
119
 
120
+ # info log
119
121
  def self.info(msg, color = nil, **options)
120
122
  msg = colorize(msg.to_s, color, **options) if color
121
123
  if logger
@@ -125,6 +127,7 @@ module Boxcars
125
127
  end
126
128
  end
127
129
 
130
+ # warn log
128
131
  def self.warn(msg, color = nil, **options)
129
132
  msg = colorize(msg.to_s, color, **options) if color
130
133
  if logger
@@ -134,6 +137,7 @@ module Boxcars
134
137
  end
135
138
  end
136
139
 
140
+ # error log
137
141
  def self.error(msg, color = nil, **options)
138
142
  msg = colorize(msg.to_s, color, **options) if color
139
143
  if logger
@@ -143,6 +147,7 @@ module Boxcars
143
147
  end
144
148
  end
145
149
 
150
+ # simple colorization
146
151
  def self.colorize(str, color, **options)
147
152
  background = options[:background] || options[:bg] || false
148
153
  style = options[:style]
@@ -157,6 +162,8 @@ end
157
162
 
158
163
  require "boxcars/version"
159
164
  require "boxcars/prompt"
165
+ require "boxcars/conversation_prompt"
166
+ require "boxcars/conversation"
160
167
  require "boxcars/generation"
161
168
  require "boxcars/ruby_repl"
162
169
  require "boxcars/engine"
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.2.1
4
+ version: 0.2.2
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-03-08 00:00:00.000000000 Z
12
+ date: 2023-03-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -109,6 +109,8 @@ files:
109
109
  - lib/boxcars/boxcar/engine_boxcar.rb
110
110
  - lib/boxcars/boxcar/google_search.rb
111
111
  - lib/boxcars/boxcar/sql.rb
112
+ - lib/boxcars/conversation.rb
113
+ - lib/boxcars/conversation_prompt.rb
112
114
  - lib/boxcars/engine.rb
113
115
  - lib/boxcars/engine/engine_result.rb
114
116
  - lib/boxcars/engine/openai.rb