boxcars 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +154 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +119 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/boxcars/boxcar/calculator.rb +102 -0
- data/lib/boxcars/boxcar/engine_boxcar.rb +90 -0
- data/lib/boxcars/boxcar/serp.rb +64 -0
- data/lib/boxcars/boxcar/sql.rb +126 -0
- data/lib/boxcars/boxcar.rb +108 -0
- data/lib/boxcars/conductor/conductor_action.rb +14 -0
- data/lib/boxcars/conductor/conductor_executer.rb +95 -0
- data/lib/boxcars/conductor/conductor_finish.rb +13 -0
- data/lib/boxcars/conductor/zero_shot.rb +81 -0
- data/lib/boxcars/conductor.rb +147 -0
- data/lib/boxcars/engine/engine_result.rb +13 -0
- data/lib/boxcars/engine/openai.rb +156 -0
- data/lib/boxcars/engine.rb +23 -0
- data/lib/boxcars/generation.rb +13 -0
- data/lib/boxcars/prompt.rb +45 -0
- data/lib/boxcars/ruby_repl.rb +22 -0
- data/lib/boxcars/version.rb +5 -0
- data/lib/boxcars.rb +93 -0
- metadata +148 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Boxcars is a framework for running a series of tools to get an answer to a question.
|
4
|
+
module Boxcars
|
5
|
+
# For Boxcars that use an engine to do their work.
|
6
|
+
# @abstract
|
7
|
+
class EngineBoxcar < Boxcars::Boxcar
|
8
|
+
attr_accessor :prompt, :engine, :output_key
|
9
|
+
|
10
|
+
# A Boxcar is a container for a single tool to run.
|
11
|
+
# @param prompt [Boxcars::Prompt] The prompt to use for this boxcar with sane defaults.
|
12
|
+
# @param name [String] The name of the boxcar. Defaults to classname.
|
13
|
+
# @param description [String] A description of the boxcar.
|
14
|
+
# @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a conductor if nil.
|
15
|
+
def initialize(prompt:, engine:, output_key: "text", name: nil, description: nil)
|
16
|
+
@prompt = prompt
|
17
|
+
@engine = engine
|
18
|
+
@output_key = output_key
|
19
|
+
super(name: name, description: description)
|
20
|
+
end
|
21
|
+
|
22
|
+
def input_keys
|
23
|
+
prompt.input_variables
|
24
|
+
end
|
25
|
+
|
26
|
+
def output_keys
|
27
|
+
[output_key]
|
28
|
+
end
|
29
|
+
|
30
|
+
# # Check that all inputs are present.
|
31
|
+
# def validate_inputs(inputs:)
|
32
|
+
# missing_keys = input_keys - inputs.keys
|
33
|
+
# raise Boxcars::ArgumentError, "Missing some input keys: #{missing_keys}" if missing_keys.any?
|
34
|
+
|
35
|
+
# inputs
|
36
|
+
# end
|
37
|
+
|
38
|
+
# def validate_outputs(outputs:)
|
39
|
+
# return if outputs.sort == output_keys.sort
|
40
|
+
|
41
|
+
# raise Boxcars::ArgumentError, "Did not get out keys that were expected, got: #{outputs}. Expected: #{output_keys}"
|
42
|
+
# end
|
43
|
+
|
44
|
+
def generate(input_list:)
|
45
|
+
stop = input_list[0][:stop]
|
46
|
+
prompts = []
|
47
|
+
input_list.each do |inputs|
|
48
|
+
new_prompt = prompt.format(**inputs)
|
49
|
+
# puts "Prompt after formatting:\n#{new_prompt.colorize(:cyan)}"
|
50
|
+
prompts.push(new_prompt)
|
51
|
+
end
|
52
|
+
engine.generate(prompts: prompts, stop: stop)
|
53
|
+
end
|
54
|
+
|
55
|
+
def apply(input_list:)
|
56
|
+
response = generate(input_list: input_list)
|
57
|
+
response.generations.to_h do |generation|
|
58
|
+
[output_key, generation[0].text]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def predict(**kwargs)
|
63
|
+
apply(input_list: [kwargs])[output_key]
|
64
|
+
end
|
65
|
+
|
66
|
+
def predict_and_parse(**kwargs)
|
67
|
+
result = predict(**kwargs)
|
68
|
+
if prompt.output_parser
|
69
|
+
prompt.output_parser.parse(result)
|
70
|
+
else
|
71
|
+
result
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def apply_and_parse(input_list:)
|
76
|
+
result = apply(input_list: input_list)
|
77
|
+
if prompt.output_parser
|
78
|
+
result.map { |r| prompt.output_parser.parse(r[output_key]) }
|
79
|
+
else
|
80
|
+
result
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def check_output_keys
|
85
|
+
return unless output_keys.length != 1
|
86
|
+
|
87
|
+
raise Boxcars::ArgumentError, "run not supported when there is not exactly one output key. Got #{output_keys}."
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'google_search_results'
|
4
|
+
module Boxcars
|
5
|
+
# A Boxcar that uses the Google SerpAPI to get answers to questions.
|
6
|
+
class Serp < Boxcar
|
7
|
+
SERPDESC = "useful for when you need to answer questions about current events." \
|
8
|
+
"You should ask targeted questions"
|
9
|
+
|
10
|
+
# implements a boxcar that uses the Google SerpAPI to get answers to questions.
|
11
|
+
# @param name [String] The name of the boxcar. Defaults to classname.
|
12
|
+
# @param description [String] A description of the boxcar. Defaults to SERPDESC.
|
13
|
+
# @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a Conductor if nil.
|
14
|
+
#
|
15
|
+
def initialize(name: "Search", description: SERPDESC, serpapi_api_key: "not set")
|
16
|
+
super(name: name, description: description)
|
17
|
+
api_key = Boxcars.configuration.serpapi_api_key(serpapi_api_key: serpapi_api_key)
|
18
|
+
GoogleSearch.api_key = api_key
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get an answer from Google using the SerpAPI.
|
22
|
+
# @param question [String] The question to ask Google.
|
23
|
+
# @return [String] The answer to the question.
|
24
|
+
def run(question)
|
25
|
+
search = GoogleSearch.new(q: question)
|
26
|
+
rv = find_answer(search.get_hash)
|
27
|
+
puts "Question: #{question}"
|
28
|
+
puts "Answer: #{rv}"
|
29
|
+
rv
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get the location of an answer from Google using the SerpAPI.
|
33
|
+
# @param question [String] The question to ask Google.
|
34
|
+
# @return [String] The location found.
|
35
|
+
def get_location(question)
|
36
|
+
search = GoogleSearch.new(q: question, limit: 3)
|
37
|
+
rv = search.get_location
|
38
|
+
puts "Question: #{question}"
|
39
|
+
puts "Answer: #{rv}"
|
40
|
+
rv
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
ANSWER_LOCATIONS = [
|
46
|
+
%i[answer_box answer],
|
47
|
+
%i[answer_box snippet],
|
48
|
+
[:answer_box, :snippet_highlighted_words, 0],
|
49
|
+
%i[sports_results game_spotlight],
|
50
|
+
%i[knowledge_graph description],
|
51
|
+
[:organic_results, 0, :snippet_highlighted_words, 0],
|
52
|
+
[:organic_results, 0, :snippet]
|
53
|
+
].freeze
|
54
|
+
|
55
|
+
def find_answer(res)
|
56
|
+
raise Error, "Got error from SerpAPI: {res[:error]}" if res[:error]
|
57
|
+
|
58
|
+
ANSWER_LOCATIONS.each do |path|
|
59
|
+
return res.dig(*path) if res.dig(*path)
|
60
|
+
end
|
61
|
+
"No good search result found"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Boxcars is a framework for running a series of tools to get an answer to a question.
|
4
|
+
module Boxcars
|
5
|
+
# A Boxcar that interprets a prompt and executes SQL code to get answers
|
6
|
+
class SQL < EngineBoxcar
|
7
|
+
SQLDESC = "useful for when you need to query a SQL database"
|
8
|
+
attr_accessor :connection, :input_key
|
9
|
+
|
10
|
+
# @param connection [ActiveRecord::Connection] The SQL connection to use for this boxcar.
|
11
|
+
# @param prompt [Boxcars::Prompt] The prompt to use for this boxcar.
|
12
|
+
# @param name [String] The name of the boxcar. Defaults to classname.
|
13
|
+
# @param description [String] A description of the boxcar.
|
14
|
+
# @param engine [Boxcars::Engine] The engine to user for this boxcar. Can be inherited from a conductor if nil.
|
15
|
+
# @param input_key [Symbol] The key to use for the input. Defaults to :question.
|
16
|
+
# @param output_key [Symbol] The key to use for the output. Defaults to :answer.
|
17
|
+
def initialize(connection:, engine: nil, input_key: :question, output_key: :answer, **kwargs)
|
18
|
+
@connection = connection
|
19
|
+
@input_key = input_key
|
20
|
+
the_prompt = kwargs[prompt] || my_prompt
|
21
|
+
super(name: kwargs[:name] || "SQLdatabase",
|
22
|
+
description: kwargs[:description] || SQLDESC,
|
23
|
+
engine: engine,
|
24
|
+
prompt: the_prompt,
|
25
|
+
output_key: output_key)
|
26
|
+
end
|
27
|
+
|
28
|
+
def input_keys
|
29
|
+
[input_key]
|
30
|
+
end
|
31
|
+
|
32
|
+
def output_keys
|
33
|
+
[output_key]
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(inputs:)
|
37
|
+
t = predict(question: inputs[input_key], dialect: dialect, top_k: 5, table_info: schema, stop: ["SQLQuery:"]).strip
|
38
|
+
answer = get_answer(t)
|
39
|
+
puts answer.colorize(:magenta)
|
40
|
+
{ output_key => answer }
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def tables
|
46
|
+
connection&.tables
|
47
|
+
end
|
48
|
+
|
49
|
+
def table_schema(table)
|
50
|
+
["CREATE TABLE #{table} (",
|
51
|
+
connection&.columns(table)&.map { |c| " #{c.name} #{c.sql_type} #{c.null ? "NULL" : "NOT NULL"}" }&.join(",\n"),
|
52
|
+
");"].join("\n")
|
53
|
+
end
|
54
|
+
|
55
|
+
def schema(except_tables: ['ar_internal_metadata'])
|
56
|
+
wanted_tables = tables.to_a - except_tables
|
57
|
+
wanted_tables.map(&method(:table_schema)).join("\n")
|
58
|
+
end
|
59
|
+
|
60
|
+
def dialect
|
61
|
+
# connection.instance_variable_get "@config"[:adapter]
|
62
|
+
connection.class.name.split("::").last.sub("Adapter", "")
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_embedded_sql_answer(text)
|
66
|
+
code = text[/^SQLQuery: (.*)/, 1]
|
67
|
+
puts code.colorize(:yellow)
|
68
|
+
output = connection.exec_query(code).to_a
|
69
|
+
puts "Answer: #{output}"
|
70
|
+
"Answer: #{output}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_answer(text)
|
74
|
+
case text
|
75
|
+
when /^SQLQuery:/
|
76
|
+
get_embedded_sql_answer(text)
|
77
|
+
when /^Answer:/
|
78
|
+
text
|
79
|
+
else
|
80
|
+
raise Boxcars::Error "Unknown format from engine: #{text}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
TEMPLATE = <<~IPT
|
85
|
+
Given an input question, first create a syntactically correct %<dialect>s query to run,
|
86
|
+
then look at the results of the query and return the answer. Unless the user specifies
|
87
|
+
in his question a specific number of examples he wishes to obtain, always limit your query
|
88
|
+
to at most %<top_k>s results using a LIMIT clause. You can order the results by a relevant column
|
89
|
+
to return the most interesting examples in the database.
|
90
|
+
|
91
|
+
Never query for all the columns from a specific table, only ask for a the few relevant columns given the question.
|
92
|
+
|
93
|
+
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.
|
94
|
+
Also, pay attention to which column is in which table.
|
95
|
+
|
96
|
+
Use the following format:
|
97
|
+
Question: "Question here"
|
98
|
+
SQLQuery: "SQL Query to run"
|
99
|
+
SQLResult: "Result of the SQLQuery"
|
100
|
+
Answer: "Final answer here"
|
101
|
+
|
102
|
+
Only use the following tables:
|
103
|
+
%<table_info>s
|
104
|
+
|
105
|
+
Question: %<question>s
|
106
|
+
IPT
|
107
|
+
|
108
|
+
# The prompt to use for the engine.
|
109
|
+
def my_prompt
|
110
|
+
@my_prompt ||= Prompt.new(input_variables: [:question, :dialect, :top_k], template: TEMPLATE)
|
111
|
+
end
|
112
|
+
|
113
|
+
# DECIDER_TEMPLATE = <<~DPT
|
114
|
+
# Given the below input question and list of potential tables, output a comma separated list of the table names that may
|
115
|
+
# be necessary to answer this question.
|
116
|
+
# Question: %<query>s
|
117
|
+
# Table Names: %<table_names>s
|
118
|
+
# Relevant Table Names:
|
119
|
+
# DPT
|
120
|
+
# DECIDER_PROMPT = Prompt.new(
|
121
|
+
# input_variables: %i[query table_names],
|
122
|
+
# template: DECIDER_TEMPLATE,
|
123
|
+
# output_parser: CommaSeparatedListOutputParser
|
124
|
+
# )
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Boxcars
|
4
|
+
# @abstract
|
5
|
+
class Boxcar
|
6
|
+
attr_reader :name, :description, :return_direct
|
7
|
+
|
8
|
+
# A Boxcar is a container for a single tool to run.
|
9
|
+
# @param name [String] The name of the boxcar. Defaults to classname.
|
10
|
+
# @param description [String] A description of the boxcar.
|
11
|
+
# @param return_direct [Boolean] If true, return the output of this boxcar directly, without merging it with the inputs.
|
12
|
+
def initialize(description:, name: nil, return_direct: false)
|
13
|
+
@name = name || self.class.name
|
14
|
+
@description = description
|
15
|
+
@return_direct = return_direct
|
16
|
+
end
|
17
|
+
|
18
|
+
# Input keys this chain expects.
|
19
|
+
def input_keys
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
# Output keys this chain expects.
|
24
|
+
def output_keys
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
# Check that all inputs are present.
|
29
|
+
def validate_inputs(inputs:)
|
30
|
+
missing_keys = input_keys - inputs.keys
|
31
|
+
raise "Missing some input keys: #{missing_keys}" if missing_keys.any?
|
32
|
+
|
33
|
+
inputs
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate_outputs(outputs:)
|
37
|
+
return if outputs.sort == output_keys.sort
|
38
|
+
|
39
|
+
raise "Did not get output keys that were expected, got: #{outputs}. Expected: #{output_keys}"
|
40
|
+
end
|
41
|
+
|
42
|
+
# Run the logic of this chain and return the output.
|
43
|
+
def call(inputs:)
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
def do_call(inputs:, return_only_outputs: false)
|
48
|
+
inputs = our_inputs(inputs)
|
49
|
+
output = nil
|
50
|
+
begin
|
51
|
+
output = call(inputs: inputs)
|
52
|
+
rescue StandardError => e
|
53
|
+
raise e
|
54
|
+
end
|
55
|
+
validate_outputs(outputs: output.keys)
|
56
|
+
# memory&.save_convext(inputs: inputs, outputs: outputs)
|
57
|
+
return output if return_only_outputs
|
58
|
+
|
59
|
+
inputs.merge(output)
|
60
|
+
end
|
61
|
+
|
62
|
+
def apply(input_list:)
|
63
|
+
input_list.map { |inputs| new(**inputs) }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get an answer from the boxcar.
|
67
|
+
# @param question [String] The question to ask the boxcar.
|
68
|
+
# @return [String] The answer to the question.
|
69
|
+
def run(*args, **kwargs)
|
70
|
+
puts "> Enterning #{name} boxcar#run".colorize(:gray, style: :bold)
|
71
|
+
rv = do_run(*args, **kwargs)
|
72
|
+
puts "< Exiting #{name} boxcar#run".colorize(:gray, style: :bold)
|
73
|
+
rv
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def do_run(*args, **kwargs)
|
79
|
+
if kwargs.empty?
|
80
|
+
raise Boxcars::ArgumentError, "run supports only one positional argument." if args.length != 1
|
81
|
+
|
82
|
+
return do_call(inputs: args[0])[output_keys.first]
|
83
|
+
end
|
84
|
+
return do_call(**kwargs)[output_keys].first if args.empty?
|
85
|
+
|
86
|
+
raise Boxcars::ArgumentError, "run supported with either positional or keyword arguments but not both. Got args" \
|
87
|
+
": #{args} and kwargs: #{kwargs}."
|
88
|
+
end
|
89
|
+
|
90
|
+
def our_inputs(inputs)
|
91
|
+
if inputs.is_a?(String)
|
92
|
+
puts inputs.colorize(:blue) # the question
|
93
|
+
if input_keys.length != 1
|
94
|
+
raise Boxcars::ArgumentError, "A single string input was passed in, but this boxcar expects " \
|
95
|
+
"multiple inputs (#{input_keys}). When a boxcar expects " \
|
96
|
+
"multiple inputs, please call it by passing in a hash, eg: `boxcar({'foo': 1, 'bar': 2})`"
|
97
|
+
end
|
98
|
+
inputs = { input_keys.first => inputs }
|
99
|
+
end
|
100
|
+
validate_inputs(inputs: inputs)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
require "boxcars/boxcar/engine_boxcar"
|
106
|
+
require "boxcars/boxcar/calculator"
|
107
|
+
require "boxcars/boxcar/serp"
|
108
|
+
require "boxcars/boxcar/sql"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Boxcars
|
4
|
+
# Conductor's action to take.
|
5
|
+
class ConductorAction
|
6
|
+
attr_accessor :boxcar, :boxcar_input, :log
|
7
|
+
|
8
|
+
def initialize(boxcar: nil, boxcar_input: nil, log: nil)
|
9
|
+
@boxcar = boxcar
|
10
|
+
@boxcar_input = boxcar_input
|
11
|
+
@log = log
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Boxcars
|
4
|
+
# Consists of an conductor using boxcars.
|
5
|
+
class ConductorExecuter < EngineBoxcar
|
6
|
+
attr_accessor :conductor, :boxcars, :return_intermediate_steps, :max_iterations, :early_stopping_method
|
7
|
+
|
8
|
+
# @param conductor [Boxcars::Conductor] The conductor to use.
|
9
|
+
# @param boxcars [Array<Boxcars::Boxcar>] The boxcars to use.
|
10
|
+
# @param return_intermediate_steps [Boolean] Whether to return the intermediate steps. Defaults to false.
|
11
|
+
# @param max_iterations [Integer] The maximum number of iterations to run. Defaults to nil.
|
12
|
+
# @param early_stopping_method [String] The early stopping method to use. Defaults to "force".
|
13
|
+
def initialize(conductor:, boxcars:, return_intermediate_steps: false, max_iterations: nil,
|
14
|
+
early_stopping_method: "force")
|
15
|
+
@conductor = conductor
|
16
|
+
@boxcars = boxcars
|
17
|
+
@return_intermediate_steps = return_intermediate_steps
|
18
|
+
@max_iterations = max_iterations
|
19
|
+
@early_stopping_method = early_stopping_method
|
20
|
+
# def initialize(prompt:, engine:, output_key: "text", name: nil, description: nil)
|
21
|
+
super(prompt: conductor.prompt, engine: conductor.engine, name: conductor.name, description: conductor.description)
|
22
|
+
end
|
23
|
+
|
24
|
+
def same_boxcars?(boxcar_names)
|
25
|
+
conductor.allowed_boxcars.sort == boxcar_names
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate_boxcars
|
29
|
+
boxcar_names = boxcars.map(&:name).sort
|
30
|
+
return if same_boxcars?(boxcar_names)
|
31
|
+
|
32
|
+
raise "Allowed boxcars (#{conductor.allowed_boxcars}) different than provided boxcars (#{boxcar_names})"
|
33
|
+
end
|
34
|
+
|
35
|
+
def input_keys
|
36
|
+
conductor.input_keys
|
37
|
+
end
|
38
|
+
|
39
|
+
def output_keys
|
40
|
+
return conductor.return_values + ["intermediate_steps"] if return_intermediate_steps
|
41
|
+
|
42
|
+
conductor.return_values
|
43
|
+
end
|
44
|
+
|
45
|
+
def should_continue?(iterations)
|
46
|
+
return true if max_iterations.nil?
|
47
|
+
|
48
|
+
iterations < max_iterations
|
49
|
+
end
|
50
|
+
|
51
|
+
# handler before returning
|
52
|
+
def pre_return(output, intermediate_steps)
|
53
|
+
puts output.log.colorize(:yellow)
|
54
|
+
final_output = output.return_values
|
55
|
+
final_output["intermediate_steps"] = intermediate_steps if return_intermediate_steps
|
56
|
+
final_output
|
57
|
+
end
|
58
|
+
|
59
|
+
def engine_prefix(return_direct)
|
60
|
+
return_direct ? "" : conductor.engine_prefix
|
61
|
+
end
|
62
|
+
|
63
|
+
def call(inputs:)
|
64
|
+
conductor.prepare_for_new_call
|
65
|
+
name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
|
66
|
+
intermediate_steps = []
|
67
|
+
iterations = 0
|
68
|
+
while should_continue?(iterations)
|
69
|
+
output = conductor.plan(intermediate_steps, **inputs)
|
70
|
+
return pre_return(output, intermediate_steps) if output.is_a?(ConductorFinish)
|
71
|
+
|
72
|
+
if (boxcar = name_to_boxcar_map[output.boxcar])
|
73
|
+
begin
|
74
|
+
observation = boxcar.run(output.boxcar_input)
|
75
|
+
return_direct = boxcar.return_direct
|
76
|
+
rescue StandardError => e
|
77
|
+
raise e
|
78
|
+
end
|
79
|
+
else
|
80
|
+
observation = "#{output.boxcar} is not a valid boxcar, try another one."
|
81
|
+
return_direct = false
|
82
|
+
end
|
83
|
+
puts "#Observation: #{observation}".colorize(:green)
|
84
|
+
intermediate_steps.append([output, observation])
|
85
|
+
if return_direct
|
86
|
+
output = ConductorFinish.new({ conductor.return_values[0] => observation }, "")
|
87
|
+
return pre_return(output, intermediate_steps)
|
88
|
+
end
|
89
|
+
iterations += 1
|
90
|
+
end
|
91
|
+
output = conductor.return_stopped_response(early_stopping_method, intermediate_steps, **inputs)
|
92
|
+
pre_return(output, intermediate_steps)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# Agent for the MRKL chain
|
2
|
+
module Boxcars
|
3
|
+
# A Conductor using the zero-shot react method.
|
4
|
+
class ZeroShot < Conductor
|
5
|
+
attr_reader :boxcars, :observation_prefix, :engine_prefix
|
6
|
+
|
7
|
+
PREFIX = "Answer the following questions as best you can. You have access to the following actions:".freeze
|
8
|
+
FORMAT_INSTRUCTIONS = <<~FINPUT.freeze
|
9
|
+
Use the following format:
|
10
|
+
|
11
|
+
Question: the input question you must answer
|
12
|
+
Thought: you should always think about what to do
|
13
|
+
Action: the action to take, should be one of [%<boxcar_names>s]
|
14
|
+
Action Input: the input to the action
|
15
|
+
Observation: the result of the action
|
16
|
+
... (this Thought/Action/Action Input/Observation sequence can repeat N times)
|
17
|
+
Thought: I now know the final answer
|
18
|
+
Final Answer: the final answer to the original input question
|
19
|
+
FINPUT
|
20
|
+
|
21
|
+
SUFFIX = <<~SINPUT.freeze
|
22
|
+
Begin!
|
23
|
+
|
24
|
+
Question: %<input>s
|
25
|
+
Thought:%<agent_scratchpad>s
|
26
|
+
SINPUT
|
27
|
+
|
28
|
+
def initialize(boxcars:, engine:, name: 'Zero Shot', description: 'Zero Shot Conductor')
|
29
|
+
@observation_prefix = 'Observation: '
|
30
|
+
@engine_prefix = 'Thought:'
|
31
|
+
prompt = self.class.create_prompt(boxcars: boxcars)
|
32
|
+
super(engine: engine, boxcars: boxcars, prompt: prompt, name: name, description: description)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Create prompt in the style of the zero shot agent.
|
36
|
+
|
37
|
+
# Args:
|
38
|
+
# boxcars: List of boxcars the agent will have access to, used to format the prompt.
|
39
|
+
# prefix: String to put before the list of boxcars.
|
40
|
+
# suffix: String to put after the list of boxcars.
|
41
|
+
# input_variables: List of input variables the final prompt will expect.
|
42
|
+
|
43
|
+
# Returns:
|
44
|
+
# A Prompt with the template assembled from the pieces here.
|
45
|
+
|
46
|
+
def self.create_prompt(boxcars:, prefix: PREFIX, suffix: SUFFIX, input_variables: [:input, :agent_scratchpad])
|
47
|
+
boxcar_strings = boxcars.map { |boxcar| "#{boxcar.name}: #{boxcar.description}" }.join("\n")
|
48
|
+
boxcar_names = boxcars.map(&:name)
|
49
|
+
format_instructions = format(FORMAT_INSTRUCTIONS, boxcar_names: boxcar_names.join(", "))
|
50
|
+
template = [prefix, boxcar_strings, format_instructions, suffix].join("\n\n")
|
51
|
+
Prompt.new(template: template, input_variables: input_variables)
|
52
|
+
end
|
53
|
+
|
54
|
+
FINAL_ANSWER_ACTION = "Final Answer:".freeze
|
55
|
+
|
56
|
+
# Parse out the action and input from the engine output.
|
57
|
+
def get_action_and_input(engine_output:)
|
58
|
+
# NOTE: if you're specifying a custom prompt for the ZeroShotAgent,
|
59
|
+
# you will need to ensure that it meets the following Regex requirements.
|
60
|
+
# The string starting with "Action:" and the following string starting
|
61
|
+
# with "Action Input:" should be separated by a newline.
|
62
|
+
if engine_output.include?(FINAL_ANSWER_ACTION)
|
63
|
+
answer = engine_output.split(FINAL_ANSWER_ACTION).last.strip
|
64
|
+
['Final Answer', answer]
|
65
|
+
else
|
66
|
+
regex = /Action: (?<action>.*)\nAction Input: (?<action_input>.*)/
|
67
|
+
match = regex.match(engine_output)
|
68
|
+
raise ValueError, "Could not parse engine output: #{engine_output}" unless match
|
69
|
+
|
70
|
+
action = match[:action].strip
|
71
|
+
action_input = match[:action_input].strip
|
72
|
+
# [action, action_input.strip(" ").strip('"')]
|
73
|
+
[action, action_input]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def extract_boxcar_and_input(text)
|
78
|
+
get_action_and_input(engine_output: text)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|