boxcars 0.2.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/Gemfile.lock +1 -1
- data/lib/boxcars/boxcar/active_record.rb +77 -51
- data/lib/boxcars/boxcar/calculator.rb +34 -47
- data/lib/boxcars/boxcar/engine_boxcar.rb +43 -47
- data/lib/boxcars/boxcar/sql.rb +59 -43
- data/lib/boxcars/boxcar.rb +34 -7
- 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/result.rb +68 -0
- data/lib/boxcars/ruby_repl.rb +7 -6
- data/lib/boxcars/train/train_action.rb +16 -2
- data/lib/boxcars/train/zero_shot.rb +52 -46
- data/lib/boxcars/train.rb +21 -22
- data/lib/boxcars/version.rb +1 -1
- data/lib/boxcars.rb +7 -0
- metadata +5 -2
data/lib/boxcars/boxcar/sql.rb
CHANGED
@@ -6,21 +6,21 @@ module Boxcars
|
|
6
6
|
class SQL < EngineBoxcar
|
7
7
|
# the description of this engine boxcar
|
8
8
|
SQLDESC = "useful for when you need to query a database for %<name>s."
|
9
|
+
LOCKED_OUT_TABLES = %w[schema_migrations ar_internal_metadata].freeze
|
9
10
|
attr_accessor :connection
|
10
11
|
|
11
12
|
# @param connection [ActiveRecord::Connection] The SQL connection to use for this boxcar.
|
12
|
-
# @param
|
13
|
+
# @param tables [Array<String>] The tables to use for this boxcar. Will use all if nil.
|
14
|
+
# @param except_tables [Array<String>] The tables to exclude from this boxcar. Will exclude none if nil.
|
13
15
|
# @param kwargs [Hash] Any other keyword arguments to pass to the parent class. This can include
|
14
|
-
# :name, :description, :prompt and :
|
15
|
-
def initialize(connection: nil,
|
16
|
+
# :name, :description, :prompt, :top_k, :stop, and :engine
|
17
|
+
def initialize(connection: nil, tables: nil, except_tables: nil, **kwargs)
|
16
18
|
@connection = connection || ::ActiveRecord::Base.connection
|
17
|
-
|
18
|
-
kwargs[:
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
engine: engine,
|
23
|
-
prompt: the_prompt)
|
19
|
+
check_tables(tables, except_tables)
|
20
|
+
kwargs[:name] ||= "Database"
|
21
|
+
kwargs[:description] ||= format(SQLDESC, name: name)
|
22
|
+
kwargs[:prompt] ||= my_prompt
|
23
|
+
super(**kwargs)
|
24
24
|
end
|
25
25
|
|
26
26
|
# @return Hash The additional variables for this boxcar.
|
@@ -30,6 +30,19 @@ module Boxcars
|
|
30
30
|
|
31
31
|
private
|
32
32
|
|
33
|
+
def check_tables(rtables, exceptions)
|
34
|
+
if rtables.is_a?(Array) && tables.length.positive?
|
35
|
+
@requested_tables = rtables
|
36
|
+
all_tables = tables
|
37
|
+
rtables.each do |t|
|
38
|
+
raise ArgumentError, "table #{t} needs to be an Active Record model" unless all_tables.include?(t)
|
39
|
+
end
|
40
|
+
elsif rtables
|
41
|
+
raise ArgumentError, "tables needs to be an array of Strings"
|
42
|
+
end
|
43
|
+
@except_models = LOCKED_OUT_TABLES + exceptions.to_a
|
44
|
+
end
|
45
|
+
|
33
46
|
def tables
|
34
47
|
connection&.tables
|
35
48
|
end
|
@@ -49,14 +62,22 @@ module Boxcars
|
|
49
62
|
connection.class.name.split("::").last.sub("Adapter", "")
|
50
63
|
end
|
51
64
|
|
52
|
-
def
|
53
|
-
|
54
|
-
Boxcars.debug code, :yellow
|
55
|
-
output = connection.exec_query(code)
|
65
|
+
def clean_up_output(output)
|
66
|
+
output = output.as_json if output.is_a?(::ActiveRecord::Result)
|
56
67
|
output = 0 if output.is_a?(Array) && output.empty?
|
57
68
|
output = output.first if output.is_a?(Array) && output.length == 1
|
58
69
|
output = output[output.keys.first] if output.is_a?(Hash) && output.length == 1
|
59
|
-
|
70
|
+
output = output.as_json if output.is_a?(::ActiveRecord::Relation)
|
71
|
+
output
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_embedded_sql_answer(text)
|
75
|
+
code = text[/^SQLQuery: (.*)/, 1]
|
76
|
+
Boxcars.debug code, :yellow
|
77
|
+
output = clean_up_output(connection.exec_query(code))
|
78
|
+
Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code: code)
|
79
|
+
rescue StandardError => e
|
80
|
+
Result.new(status: :error, answer: nil, explanation: "Error: #{e.message}", code: code)
|
60
81
|
end
|
61
82
|
|
62
83
|
def get_answer(text)
|
@@ -64,43 +85,38 @@ module Boxcars
|
|
64
85
|
when /^SQLQuery:/
|
65
86
|
get_embedded_sql_answer(text)
|
66
87
|
when /^Answer:/
|
67
|
-
text
|
88
|
+
Result.from_text(text)
|
68
89
|
else
|
69
|
-
|
90
|
+
Result.from_error("Try answering again. Expected your answer to start with 'SQLQuery:'. You gave me:\n#{text}")
|
70
91
|
end
|
71
92
|
end
|
72
93
|
|
73
|
-
|
74
|
-
Given an input question, first create a syntactically correct %<dialect>s query to run,
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
Only use the following tables:
|
92
|
-
%<schema>s
|
93
|
-
|
94
|
-
Question: %<question>s
|
95
|
-
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
|
96
111
|
|
97
112
|
# The prompt to use for the engine.
|
98
113
|
def my_prompt
|
99
|
-
@
|
114
|
+
@conversation ||= Conversation.new(lines: CTEMPLATE)
|
115
|
+
@my_prompt ||= ConversationPrompt.new(
|
116
|
+
conversation: @conversation,
|
100
117
|
input_variables: [:question],
|
101
118
|
other_inputs: [:top_k, :dialect, :table_info],
|
102
|
-
output_variables: [:answer]
|
103
|
-
template: TEMPLATE)
|
119
|
+
output_variables: [:answer])
|
104
120
|
end
|
105
121
|
end
|
106
122
|
end
|
data/lib/boxcars/boxcar.rb
CHANGED
@@ -62,16 +62,42 @@ module Boxcars
|
|
62
62
|
# you can pass one or the other, but not both.
|
63
63
|
# @return [String] The answer to the question.
|
64
64
|
def run(*args, **kwargs)
|
65
|
+
rv = conduct(*args, **kwargs)
|
66
|
+
rv.is_a?(Result) ? rv.to_answer : rv
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get an extended answer from the boxcar.
|
70
|
+
# @param args [Array] The positional arguments to pass to the boxcar.
|
71
|
+
# @param kwargs [Hash] The keyword arguments to pass to the boxcar.
|
72
|
+
# you can pass one or the other, but not both.
|
73
|
+
# @return [Boxcars::Result] The answer to the question.
|
74
|
+
def conduct(*args, **kwargs)
|
65
75
|
Boxcars.info "> Entering #{name}#run", :gray, style: :bold
|
66
|
-
rv =
|
76
|
+
rv = depart(*args, **kwargs)
|
67
77
|
Boxcars.info "< Exiting #{name}#run", :gray, style: :bold
|
68
78
|
rv
|
69
79
|
end
|
70
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
|
+
|
71
97
|
private
|
72
98
|
|
73
99
|
# Get an answer from the boxcar.
|
74
|
-
def
|
100
|
+
def run_boxcar(inputs:, return_only_outputs: false)
|
75
101
|
inputs = our_inputs(inputs)
|
76
102
|
output = nil
|
77
103
|
begin
|
@@ -81,19 +107,19 @@ module Boxcars
|
|
81
107
|
raise e
|
82
108
|
end
|
83
109
|
validate_outputs(outputs: output.keys)
|
84
|
-
# memory&.save_convext(inputs: inputs, outputs: outputs)
|
85
110
|
return output if return_only_outputs
|
86
111
|
|
87
112
|
inputs.merge(output)
|
88
113
|
end
|
89
114
|
|
90
|
-
|
115
|
+
# line up parameters and run boxcar
|
116
|
+
def depart(*args, **kwargs)
|
91
117
|
if kwargs.empty?
|
92
118
|
raise Boxcars::ArgumentError, "run supports only one positional argument." if args.length != 1
|
93
119
|
|
94
|
-
return
|
120
|
+
return run_boxcar(inputs: args[0])[output_keys.first]
|
95
121
|
end
|
96
|
-
return
|
122
|
+
return run_boxcar(**kwargs)[output_keys].first if args.empty?
|
97
123
|
|
98
124
|
raise Boxcars::ArgumentError, "run supported with either positional or keyword arguments but not both. Got args" \
|
99
125
|
": #{args} and kwargs: #{kwargs}."
|
@@ -114,11 +140,12 @@ module Boxcars
|
|
114
140
|
|
115
141
|
# the default answer is the text passed in
|
116
142
|
def get_answer(text)
|
117
|
-
text
|
143
|
+
Result.from_text(text)
|
118
144
|
end
|
119
145
|
end
|
120
146
|
end
|
121
147
|
|
148
|
+
require "boxcars/result"
|
122
149
|
require "boxcars/boxcar/engine_boxcar"
|
123
150
|
require "boxcars/boxcar/calculator"
|
124
151
|
require "boxcars/boxcar/google_search"
|
@@ -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
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Boxcars
|
4
|
+
# used by Boxcars to return structured result and additional context
|
5
|
+
class Result
|
6
|
+
attr_reader :status, :answer, :explanation, :suggestions, :added_context
|
7
|
+
|
8
|
+
# @param status [Symbol] :ok or :error
|
9
|
+
# @param answer [String] The answer to the question
|
10
|
+
# @param explanation [String] The explanation of the answer
|
11
|
+
# @param suggestions [Array<String>] The next suggestions for the user
|
12
|
+
# @param added_context [Hash] Any additional context to add to the result
|
13
|
+
def initialize(status:, answer: nil, explanation: nil, suggestions: nil, **added_context)
|
14
|
+
@status = status
|
15
|
+
@answer = answer || explanation
|
16
|
+
@explanation = explanation
|
17
|
+
@suggestions = suggestions
|
18
|
+
@added_context = added_context
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Hash] The result as a hash
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
status: status,
|
25
|
+
answer: answer,
|
26
|
+
explanation: explanation,
|
27
|
+
suggestions: suggestions
|
28
|
+
}.merge(added_context).compact
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [String] The result as a json string
|
32
|
+
def to_json(*args)
|
33
|
+
JSON.generate(to_h, *args)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [String] An explanation of the result
|
37
|
+
def to_s
|
38
|
+
explanation
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [String] The answer data to the question
|
42
|
+
def to_answer
|
43
|
+
answer
|
44
|
+
end
|
45
|
+
|
46
|
+
# create a new Result from a text string
|
47
|
+
# @param text [String] The text to use for the result
|
48
|
+
# @param kwargs [Hash] Any additional kwargs to pass to the result
|
49
|
+
# @return [Boxcars::Result] The result
|
50
|
+
def self.from_text(text, **kwargs)
|
51
|
+
answer = text.delete_prefix('"').delete_suffix('"').strip
|
52
|
+
answer = Regexp.last_match(:answer) if answer =~ /^Answer:\s*(?<answer>.*)$/
|
53
|
+
explanation = "Answer: #{answer}"
|
54
|
+
new(status: :ok, answer: answer, explanation: explanation, **kwargs)
|
55
|
+
end
|
56
|
+
|
57
|
+
# create a new Result from an error string
|
58
|
+
# @param error [String] The error to use for the result
|
59
|
+
# @param kwargs [Hash] Any additional kwargs to pass to the result
|
60
|
+
# @return [Boxcars::Result] The error result
|
61
|
+
def self.from_error(error, **kwargs)
|
62
|
+
answer = error
|
63
|
+
answer = Regexp.last_match(:answer) if answer =~ /^Error:\s*(?<answer>.*)$/
|
64
|
+
explanation = "Error: #{answer}"
|
65
|
+
new(status: :error, answer: answer, explanation: explanation, **kwargs)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/boxcars/ruby_repl.rb
CHANGED
@@ -9,19 +9,20 @@ module Boxcars
|
|
9
9
|
Boxcars.debug "RubyREPL: #{code}", :yellow
|
10
10
|
|
11
11
|
# wrap the code in an excption block so we can catch errors
|
12
|
-
|
12
|
+
wrapped = "begin\n#{code}\nrescue Exception => e\n puts 'Error: ' + e.message\nend"
|
13
13
|
output = ""
|
14
14
|
IO.popen("ruby", "r+") do |io|
|
15
|
-
io.puts
|
15
|
+
io.puts wrapped
|
16
16
|
io.close_write
|
17
17
|
output = io.read
|
18
18
|
end
|
19
19
|
if output =~ /^Error: /
|
20
|
-
Boxcars.
|
21
|
-
output
|
20
|
+
Boxcars.debug output, :red
|
21
|
+
Result.from_error(output, code: code)
|
22
|
+
else
|
23
|
+
Boxcars.debug "Answer: #{output}", :yellow, style: :bold
|
24
|
+
Result.from_text(output, code: code)
|
22
25
|
end
|
23
|
-
Boxcars.debug "Answer: #{output}", :yellow, style: :bold
|
24
|
-
output
|
25
26
|
end
|
26
27
|
|
27
28
|
# Execute ruby code
|
@@ -5,10 +5,24 @@ module Boxcars
|
|
5
5
|
class TrainAction
|
6
6
|
attr_accessor :boxcar, :boxcar_input, :log
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
# record for a train action
|
9
|
+
# @param boxcar [String] The boxcar to run.
|
10
|
+
# @param log [String] The log of the action.
|
11
|
+
# @param boxcar_input [String] The input to the boxcar.
|
12
|
+
# @return [Boxcars::TrainAction] The train action.
|
13
|
+
def initialize(boxcar:, log:, boxcar_input: nil)
|
10
14
|
@boxcar_input = boxcar_input
|
15
|
+
@boxcar = boxcar
|
11
16
|
@log = log
|
12
17
|
end
|
18
|
+
|
19
|
+
# build a train action from a result
|
20
|
+
# @param result [Boxcars::Result] The result to build from.
|
21
|
+
# @param boxcar [String] The boxcar to run.
|
22
|
+
# @param log [String] The log of the action.
|
23
|
+
# @return [Boxcars::TrainAction] The train action.
|
24
|
+
def self.from_result(result:, boxcar:, log:)
|
25
|
+
new(boxcar: boxcar, boxcar_input: result.to_answer, log: log)
|
26
|
+
end
|
13
27
|
end
|
14
28
|
end
|