boxcars 0.2.1 → 0.2.2

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: 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