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 +4 -4
- data/CHANGELOG.md +27 -0
- data/Gemfile.lock +1 -1
- data/lib/boxcars/boxcar/active_record.rb +45 -28
- data/lib/boxcars/boxcar/calculator.rb +29 -40
- data/lib/boxcars/boxcar/engine_boxcar.rb +42 -46
- data/lib/boxcars/boxcar/sql.rb +22 -27
- data/lib/boxcars/boxcar.rb +16 -0
- data/lib/boxcars/conversation.rb +98 -0
- data/lib/boxcars/conversation_prompt.rb +40 -0
- data/lib/boxcars/engine/openai.rb +23 -15
- data/lib/boxcars/prompt.rb +25 -38
- data/lib/boxcars/train/zero_shot.rb +51 -46
- data/lib/boxcars/train.rb +16 -20
- data/lib/boxcars/version.rb +1 -1
- data/lib/boxcars.rb +7 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 967f04279864f1419fbe3e9f02817cc8f5594317c6f2b06dd49d1c5e1baa67a9
|
|
4
|
+
data.tar.gz: 156ca722488b91cefce4d03026490434004e1dcbd1f7c7cd714f1d3f6871cbc0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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
|
|
155
|
-
changes_code = text
|
|
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
|
|
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("
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
@
|
|
203
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
|
18
|
-
@stop = kwargs
|
|
19
|
-
super(
|
|
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
|
-
|
|
43
|
-
input_list.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
data/lib/boxcars/boxcar/sql.rb
CHANGED
|
@@ -87,41 +87,36 @@ module Boxcars
|
|
|
87
87
|
when /^Answer:/
|
|
88
88
|
Result.from_text(text)
|
|
89
89
|
else
|
|
90
|
-
Result.from_error("
|
|
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
|
-
|
|
95
|
-
Given an input question, first create a syntactically correct %<dialect>s SQL query to run,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
@
|
|
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
|
data/lib/boxcars/boxcar.rb
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
46
|
-
if
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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)
|
data/lib/boxcars/prompt.rb
CHANGED
|
@@ -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 ||=
|
|
17
|
+
prompt ||= my_prompt
|
|
42
18
|
super(engine: engine, boxcars: boxcars, prompt: prompt, name: name, description: description)
|
|
43
19
|
end
|
|
44
20
|
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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:"
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 :
|
|
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 [
|
|
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
|
-
@
|
|
17
|
+
@name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
|
|
17
18
|
@return_values = [:output]
|
|
18
|
-
@return_intermediate_steps = kwargs
|
|
19
|
-
@max_iterations = kwargs
|
|
20
|
-
@early_stopping_method = kwargs
|
|
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,
|
|
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 =
|
|
52
|
-
parsed_output =
|
|
53
|
-
|
|
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
|
-
|
|
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)
|
data/lib/boxcars/version.rb
CHANGED
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.
|
|
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-
|
|
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
|