boxcars 0.2.15 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/Gemfile.lock +2 -2
- data/README.md +1 -1
- data/lib/boxcars/boxcar/active_record.rb +3 -2
- data/lib/boxcars/boxcar/calculator.rb +1 -1
- data/lib/boxcars/boxcar/sql_active_record.rb +33 -0
- data/lib/boxcars/boxcar/{sql.rb → sql_base.rb} +25 -16
- data/lib/boxcars/boxcar/sql_sequel.rb +32 -0
- data/lib/boxcars/boxcar.rb +54 -8
- data/lib/boxcars/conversation.rb +18 -4
- data/lib/boxcars/conversation_prompt.rb +11 -0
- data/lib/boxcars/observation.rb +51 -0
- data/lib/boxcars/ruby_repl.rb +3 -1
- data/lib/boxcars/train/zero_shot.rb +7 -1
- data/lib/boxcars/train.rb +19 -14
- data/lib/boxcars/version.rb +1 -1
- data/lib/boxcars.rb +17 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a2448419a2e348f8111fa19bd7b9bb1a05ab19b30d3dc9c0255ae0bd8673c156
|
4
|
+
data.tar.gz: fd6577be64b72941a87cbdb8c1c1ca31a7c88383869fa67f86d21cf721749168
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fd21eb8cd3ea3cc2fdf29140a3dce7dec26a7e4ffbd791d7aa7dd63aaa267c34d4ead764f85d3f7dbb9d1038330e3614bbfaf7f8c34818544b0d282568ad66f5
|
7
|
+
data.tar.gz: e12626132060202679533c3a101f4ec4fb1c9846f7d54beb9bc84ce7d71cb4f56be10f16a2c229480cc6c4378a074566f26a7ccbeeadc0c7ffc27ab81dfc6ee4
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,39 @@
|
|
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.16...HEAD)
|
6
|
+
|
7
|
+
**Closed issues:**
|
8
|
+
|
9
|
+
- Add a running log of prompts for debugging [\#99](https://github.com/BoxcarsAI/boxcars/issues/99)
|
10
|
+
|
11
|
+
## [v0.2.16](https://github.com/BoxcarsAI/boxcars/tree/v0.2.16) (2023-06-26)
|
12
|
+
|
13
|
+
[Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.15...v0.2.16)
|
14
|
+
|
15
|
+
**Implemented enhancements:**
|
16
|
+
|
17
|
+
- Support Sequel connection type [\#22](https://github.com/BoxcarsAI/boxcars/issues/22)
|
18
|
+
|
19
|
+
**Closed issues:**
|
20
|
+
|
21
|
+
- Using the SQL model This model's maximum context length is 4097 tokens [\#88](https://github.com/BoxcarsAI/boxcars/issues/88)
|
22
|
+
|
23
|
+
**Merged pull requests:**
|
24
|
+
|
25
|
+
- Add running logs [\#100](https://github.com/BoxcarsAI/boxcars/pull/100) ([francis](https://github.com/francis))
|
26
|
+
- create new Sequel boxcar, and refactor Active Record SQL boxcar [\#98](https://github.com/BoxcarsAI/boxcars/pull/98) ([francis](https://github.com/francis))
|
27
|
+
- Support for Sequel SQL connection types [\#97](https://github.com/BoxcarsAI/boxcars/pull/97) ([eltoob](https://github.com/eltoob))
|
28
|
+
|
29
|
+
## [v0.2.15](https://github.com/BoxcarsAI/boxcars/tree/v0.2.15) (2023-06-09)
|
30
|
+
|
31
|
+
[Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.14...v0.2.15)
|
32
|
+
|
33
|
+
**Merged pull requests:**
|
34
|
+
|
35
|
+
- make suggested next actions optional [\#94](https://github.com/BoxcarsAI/boxcars/pull/94) ([francis](https://github.com/francis))
|
36
|
+
|
3
37
|
## [v0.2.14](https://github.com/BoxcarsAI/boxcars/tree/v0.2.14) (2023-06-06)
|
4
38
|
|
5
39
|
[Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.13...v0.2.14)
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
boxcars (0.
|
4
|
+
boxcars (0.3.1)
|
5
5
|
google_search_results (~> 2.2)
|
6
6
|
gpt4all (~> 0.0.4)
|
7
7
|
hnswlib (~> 0.8)
|
@@ -55,7 +55,7 @@ GEM
|
|
55
55
|
domain_name (0.5.20190701)
|
56
56
|
unf (>= 0.0.5, < 1.0.0)
|
57
57
|
dotenv (2.8.1)
|
58
|
-
faraday (2.7.
|
58
|
+
faraday (2.7.9)
|
59
59
|
faraday-net_http (>= 2.0, < 3.1)
|
60
60
|
ruby2_keywords (>= 0.0.4)
|
61
61
|
faraday-http-cache (2.5.0)
|
data/README.md
CHANGED
@@ -10,7 +10,7 @@
|
|
10
10
|
<a href="https://github.com/BoxcarsAI/boxcars/blob/main/LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-informational" alt="License"></a>
|
11
11
|
</p>
|
12
12
|
|
13
|
-
Boxcars is a gem that enables you to create new systems with AI composability, using various concepts such as OpenAI, Search, SQL, Rails Active Record, Vector Search and more. This can even be extended with your concepts as well (including your concepts).
|
13
|
+
Boxcars is a gem that enables you to create new systems with AI composability, using various concepts such as OpenAI, Search, SQL (with both Sequel an Active Record support), Rails Active Record, Vector Search and more. This can even be extended with your concepts as well (including your concepts).
|
14
14
|
|
15
15
|
This gem was inspired by the popular Python library Langchain. However, we wanted to give it a Ruby spin and make it more user-friendly for beginners to get started.
|
16
16
|
|
@@ -147,7 +147,7 @@ module Boxcars
|
|
147
147
|
end
|
148
148
|
|
149
149
|
def change_count(changes_code)
|
150
|
-
return 0 unless changes_code
|
150
|
+
return 0 unless changes_code && changes_code != "None"
|
151
151
|
|
152
152
|
rollback_after_running do
|
153
153
|
Boxcars.debug "computing change count with: #{changes_code}", :yellow
|
@@ -200,7 +200,8 @@ module Boxcars
|
|
200
200
|
def error_message(err, stage)
|
201
201
|
msg = err.message
|
202
202
|
msg = ::Regexp.last_match(1) if msg =~ /^(.+)' for #<Boxcars::ActiveRecord/
|
203
|
-
|
203
|
+
msg.gsub!(/Boxcars::ActiveRecord::/, '')
|
204
|
+
"For the value you gave for #{stage}, fix this error: #{msg}"
|
204
205
|
end
|
205
206
|
|
206
207
|
def get_active_record_answer(text)
|
@@ -43,7 +43,7 @@ module Boxcars
|
|
43
43
|
CTEMPLATE = [
|
44
44
|
syst("You can do basic math, but for any hard calculations that a human could not do ",
|
45
45
|
"in their head, use the following approach instead. ",
|
46
|
-
"Return code written in the Ruby programming language that prints the results. ",
|
46
|
+
"Return code written in the Ruby programming language that prints the results to the console. ",
|
47
47
|
"If anyone gives you a hard math problem, just ",
|
48
48
|
"use the following format and we’ll take care of the rest:\n",
|
49
49
|
"${{Question with hard calculation.}}\n",
|
@@ -0,0 +1,33 @@
|
|
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 using Active Record to get answers
|
6
|
+
class SQLActiveRecord < SQLBase
|
7
|
+
# @param connection [ActiveRecord::Connection] The SQL connection to use for this boxcar.
|
8
|
+
# @param tables [Array<String>] The tables to use for this boxcar. Will use all if nil.
|
9
|
+
# @param except_tables [Array<String>] The tables to exclude from this boxcar. Will exclude none if nil.
|
10
|
+
# @param kwargs [Hash] Any other keyword arguments to pass to the parent class. This can include
|
11
|
+
# :name, :description, :prompt, :top_k, :stop, and :engine
|
12
|
+
def initialize(connection: nil, tables: nil, except_tables: nil, **kwargs)
|
13
|
+
connection ||= ::ActiveRecord::Base.connection
|
14
|
+
super(connection: connection, tables: tables, except_tables: except_tables, **kwargs)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def table_schema(table)
|
20
|
+
["CREATE TABLE #{table} (",
|
21
|
+
connection&.columns(table)&.map { |c| " #{c.name} #{c.sql_type} #{c.null ? "NULL" : "NOT NULL"}" }&.join(",\n"),
|
22
|
+
");"].join("\n")
|
23
|
+
end
|
24
|
+
|
25
|
+
def dialect
|
26
|
+
connection.class.name.split("::").last.sub("Adapter", "")
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_output(code)
|
30
|
+
connection&.exec_query(code)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -3,19 +3,21 @@
|
|
3
3
|
# Boxcars is a framework for running a series of tools to get an answer to a question.
|
4
4
|
module Boxcars
|
5
5
|
# A Boxcar that interprets a prompt and executes SQL code to get answers
|
6
|
-
|
6
|
+
# Use one of the subclasses for ActiveRecord or Sequel
|
7
|
+
# @abstract
|
8
|
+
class SQLBase < EngineBoxcar
|
7
9
|
# the description of this engine boxcar
|
8
10
|
SQLDESC = "useful for when you need to query a database for %<name>s."
|
9
11
|
LOCKED_OUT_TABLES = %w[schema_migrations ar_internal_metadata].freeze
|
10
|
-
attr_accessor :connection
|
12
|
+
attr_accessor :connection, :the_tables
|
11
13
|
|
12
|
-
# @param connection [ActiveRecord::Connection] The SQL connection to use for this boxcar.
|
14
|
+
# @param connection [ActiveRecord::Connection] or [Sequel Object] The SQL connection to use for this boxcar.
|
13
15
|
# @param tables [Array<String>] The tables to use for this boxcar. Will use all if nil.
|
14
16
|
# @param except_tables [Array<String>] The tables to exclude from this boxcar. Will exclude none if nil.
|
15
17
|
# @param kwargs [Hash] Any other keyword arguments to pass to the parent class. This can include
|
16
18
|
# :name, :description, :prompt, :top_k, :stop, and :engine
|
17
19
|
def initialize(connection: nil, tables: nil, except_tables: nil, **kwargs)
|
18
|
-
@connection = connection
|
20
|
+
@connection = connection
|
19
21
|
check_tables(tables, except_tables)
|
20
22
|
kwargs[:name] ||= "Database"
|
21
23
|
kwargs[:description] ||= format(SQLDESC, name: name)
|
@@ -33,40 +35,47 @@ module Boxcars
|
|
33
35
|
private
|
34
36
|
|
35
37
|
def check_tables(rtables, exceptions)
|
38
|
+
requested_tables = nil
|
36
39
|
if rtables.is_a?(Array) && tables.length.positive?
|
37
|
-
|
40
|
+
requested_tables = rtables
|
38
41
|
all_tables = tables
|
39
42
|
rtables.each do |t|
|
40
|
-
raise ArgumentError, "table #{t}
|
43
|
+
raise ArgumentError, "table #{t} not found in database" unless all_tables.include?(t)
|
41
44
|
end
|
42
45
|
elsif rtables
|
43
46
|
raise ArgumentError, "tables needs to be an array of Strings"
|
44
47
|
else
|
45
|
-
|
48
|
+
requested_tables = tables.to_a
|
46
49
|
end
|
47
|
-
|
50
|
+
except_tables = LOCKED_OUT_TABLES + exceptions.to_a
|
51
|
+
@the_tables = requested_tables - except_tables
|
48
52
|
end
|
49
53
|
|
50
54
|
def tables
|
51
55
|
connection&.tables
|
52
56
|
end
|
53
57
|
|
58
|
+
# abstract method to get the prompt for this boxcar
|
54
59
|
def table_schema(table)
|
55
|
-
|
56
|
-
connection&.columns(table)&.map { |c| " #{c.name} #{c.sql_type} #{c.null ? "NULL" : "NOT NULL"}" }&.join(",\n"),
|
57
|
-
");"].join("\n")
|
60
|
+
raise NotImplementedError
|
58
61
|
end
|
59
62
|
|
60
63
|
def schema
|
61
|
-
|
62
|
-
wanted_tables.map(&method(:table_schema)).join("\n")
|
64
|
+
the_tables.map(&method(:table_schema)).join("\n")
|
63
65
|
end
|
64
66
|
|
67
|
+
# abstract method to get the prompt for this boxcar
|
65
68
|
def dialect
|
66
|
-
|
69
|
+
raise NotImplementedError
|
67
70
|
end
|
68
71
|
|
69
|
-
|
72
|
+
# abstract method to get the output for the last query
|
73
|
+
def get_output(code)
|
74
|
+
raise NotImplementedError
|
75
|
+
end
|
76
|
+
|
77
|
+
def clean_up_output(code)
|
78
|
+
output = get_output(code)
|
70
79
|
output = output.as_json if output.is_a?(::ActiveRecord::Result)
|
71
80
|
output = 0 if output.is_a?(Array) && output.empty?
|
72
81
|
output = output.first if output.is_a?(Array) && output.length == 1
|
@@ -79,7 +88,7 @@ module Boxcars
|
|
79
88
|
code = text[/^SQLQuery: (.*)/, 1]
|
80
89
|
code = extract_code text.split('SQLQuery:').last.strip
|
81
90
|
Boxcars.debug code, :yellow
|
82
|
-
output = clean_up_output(
|
91
|
+
output = clean_up_output(code)
|
83
92
|
Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code: code)
|
84
93
|
rescue StandardError => e
|
85
94
|
Result.new(status: :error, answer: nil, explanation: "Error: #{e.message}", code: code)
|
@@ -0,0 +1,32 @@
|
|
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 Sequel SQL code to get answers
|
6
|
+
class SQLSequel < SQLBase
|
7
|
+
# @param connection [SEQUEL Database object] The Sequel connection to use for this boxcar.
|
8
|
+
# @param tables [Array<String>] The tables to use for this boxcar. Will use all if nil.
|
9
|
+
# @param except_tables [Array<String>] The tables to exclude from this boxcar. Will exclude none if nil.
|
10
|
+
# @param kwargs [Hash] Any other keyword arguments to pass to the parent class. This can include
|
11
|
+
# :name, :description, :prompt, :top_k, :stop, and :engine
|
12
|
+
def initialize(connection: nil, tables: nil, except_tables: nil, **kwargs)
|
13
|
+
super(connection: connection, tables: tables, except_tables: except_tables, **kwargs)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def table_schema(table)
|
19
|
+
["CREATE TABLE #{table} (",
|
20
|
+
connection&.schema(table)&.map { |c| " #{c[0]} #{c[1][:type]} #{c[1][:allow_null] ? "NULL" : "NOT NULL"}" }&.join(",\n"),
|
21
|
+
");"].join("\n")
|
22
|
+
end
|
23
|
+
|
24
|
+
def dialect
|
25
|
+
connection.database_type
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_output(code)
|
29
|
+
connection[code].all
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/boxcars/boxcar.rb
CHANGED
@@ -39,7 +39,7 @@ module Boxcars
|
|
39
39
|
# @param outputs [Array<String>] The output keys.
|
40
40
|
# @raise [RuntimeError] If the outputs are not the same.
|
41
41
|
def validate_outputs(outputs:)
|
42
|
-
return if outputs
|
42
|
+
return if (outputs - output_keys - ['log']).empty?
|
43
43
|
|
44
44
|
raise "Did not get output keys that were expected, got: #{outputs}. Expected: #{output_keys}"
|
45
45
|
end
|
@@ -63,7 +63,11 @@ module Boxcars
|
|
63
63
|
# @return [String] The answer to the question.
|
64
64
|
def run(*args, **kwargs)
|
65
65
|
rv = conduct(*args, **kwargs)
|
66
|
-
rv.is_a?(
|
66
|
+
rv = rv[:answer] if rv.is_a?(Hash) && rv.key?(:answer)
|
67
|
+
return rv.answer if rv.is_a?(Result)
|
68
|
+
return rv[output_keys[0]] if rv.is_a?(Hash)
|
69
|
+
|
70
|
+
rv
|
67
71
|
end
|
68
72
|
|
69
73
|
# Get an extended answer from the boxcar.
|
@@ -74,6 +78,7 @@ module Boxcars
|
|
74
78
|
def conduct(*args, **kwargs)
|
75
79
|
Boxcars.info "> Entering #{name}#run", :gray, style: :bold
|
76
80
|
rv = depart(*args, **kwargs)
|
81
|
+
remember_history(rv)
|
77
82
|
Boxcars.info "< Exiting #{name}#run", :gray, style: :bold
|
78
83
|
rv
|
79
84
|
end
|
@@ -94,8 +99,48 @@ module Boxcars
|
|
94
99
|
[:user, strs.join]
|
95
100
|
end
|
96
101
|
|
102
|
+
# history entries
|
103
|
+
def self.hist
|
104
|
+
[:history, ""]
|
105
|
+
end
|
106
|
+
|
107
|
+
# save this boxcar to a file
|
108
|
+
def save(path:)
|
109
|
+
File.write(path, YAML.dump(self))
|
110
|
+
end
|
111
|
+
|
112
|
+
# load this boxcar from a file
|
113
|
+
# rubocop:disable Security/YAMLLoad
|
114
|
+
def load(path:)
|
115
|
+
YAML.load(File.read(path))
|
116
|
+
end
|
117
|
+
# rubocop:enable Security/YAMLLoad
|
118
|
+
|
97
119
|
private
|
98
120
|
|
121
|
+
# remember the history of this boxcar. Take the current intermediate steps and
|
122
|
+
# create a history that can be used on the next run.
|
123
|
+
# @param current_results [Array<Hash>] The current results.
|
124
|
+
def remember_history(current_results)
|
125
|
+
return unless current_results[:intermediate_steps] && is_a?(Train)
|
126
|
+
|
127
|
+
# insert conversation history into the prompt
|
128
|
+
history = []
|
129
|
+
history << Boxcar.user("Question: #{current_results[:input]}")
|
130
|
+
current_results[:intermediate_steps].each do |action, obs|
|
131
|
+
if action.is_a?(TrainAction)
|
132
|
+
obs = Observation.new(status: :ok, note: obs) if obs.is_a?(String)
|
133
|
+
next if obs.status != :ok
|
134
|
+
|
135
|
+
history << Boxcar.assi("Thought: #{action.log}\n", "Observation: #{obs.note}")
|
136
|
+
else
|
137
|
+
Boxcars.error "Unknown action: #{action}", :red
|
138
|
+
end
|
139
|
+
end
|
140
|
+
history << Boxcar.assi("Thought: I know the final answer\nFinal Answer: #{current_results[:output]}")
|
141
|
+
prompt.add_history(history)
|
142
|
+
end
|
143
|
+
|
99
144
|
# Get an answer from the boxcar.
|
100
145
|
def run_boxcar(inputs:, return_only_outputs: false)
|
101
146
|
inputs = our_inputs(inputs)
|
@@ -117,13 +162,11 @@ module Boxcars
|
|
117
162
|
if kwargs.empty?
|
118
163
|
raise Boxcars::ArgumentError, "run supports only one positional argument." if args.length != 1
|
119
164
|
|
120
|
-
return run_boxcar(inputs: args[0])
|
121
|
-
end
|
122
|
-
if args.empty?
|
123
|
-
ans = run_boxcar(inputs: kwargs)
|
124
|
-
return ans[output_keys.first]
|
165
|
+
return run_boxcar(inputs: args[0])
|
125
166
|
end
|
126
167
|
|
168
|
+
return run_boxcar(inputs: kwargs) if args.empty?
|
169
|
+
|
127
170
|
raise Boxcars::ArgumentError, "run supported with either positional or keyword arguments but not both. Got args" \
|
128
171
|
": #{args} and kwargs: #{kwargs}."
|
129
172
|
end
|
@@ -148,12 +191,15 @@ module Boxcars
|
|
148
191
|
end
|
149
192
|
end
|
150
193
|
|
194
|
+
require "boxcars/observation"
|
151
195
|
require "boxcars/result"
|
152
196
|
require "boxcars/boxcar/engine_boxcar"
|
153
197
|
require "boxcars/boxcar/calculator"
|
154
198
|
require "boxcars/boxcar/google_search"
|
155
199
|
require "boxcars/boxcar/wikipedia_search"
|
156
|
-
require "boxcars/boxcar/
|
200
|
+
require "boxcars/boxcar/sql_base"
|
201
|
+
require "boxcars/boxcar/sql_active_record"
|
202
|
+
require "boxcars/boxcar/sql_sequel"
|
157
203
|
require "boxcars/boxcar/swagger"
|
158
204
|
require "boxcars/boxcar/active_record"
|
159
205
|
require "boxcars/vector_store"
|
data/lib/boxcars/conversation.rb
CHANGED
@@ -5,7 +5,7 @@ module Boxcars
|
|
5
5
|
class Conversation
|
6
6
|
attr_reader :lines, :show_roles
|
7
7
|
|
8
|
-
PEOPLE = %i[system user assistant].freeze
|
8
|
+
PEOPLE = %i[system user assistant history].freeze
|
9
9
|
|
10
10
|
def initialize(lines: [], show_roles: false)
|
11
11
|
@lines = lines
|
@@ -64,6 +64,20 @@ module Boxcars
|
|
64
64
|
@lines += conversation.lines
|
65
65
|
end
|
66
66
|
|
67
|
+
# insert converation above history line
|
68
|
+
# @param conversation [Conversation] The conversation to add
|
69
|
+
def add_history(conversation)
|
70
|
+
@lines = @lines.dup
|
71
|
+
# find the history line
|
72
|
+
hi = lines.rindex { |ln| ln[0] == :history }
|
73
|
+
# insert the conversation above the history line
|
74
|
+
@lines.insert(hi, *conversation.lines)
|
75
|
+
end
|
76
|
+
|
77
|
+
def no_history
|
78
|
+
@lines.reject { |ln| ln[0] == :history }
|
79
|
+
end
|
80
|
+
|
67
81
|
# return just the messages for the conversation
|
68
82
|
def message_text
|
69
83
|
lines.map(&:last).join("\n")
|
@@ -73,7 +87,7 @@ module Boxcars
|
|
73
87
|
# @param inputs [Hash] The inputs to use for the prompt.
|
74
88
|
# @return [Hash] The formatted prompt { messages: ...}
|
75
89
|
def as_messages(inputs = nil)
|
76
|
-
{ messages:
|
90
|
+
{ messages: no_history.map { |ln| { role: ln.first, content: ln.last % inputs } } }
|
77
91
|
rescue ::KeyError => e
|
78
92
|
first_line = e.message.to_s.split("\n").first
|
79
93
|
Boxcars.error "Missing prompt input key: #{first_line}"
|
@@ -85,9 +99,9 @@ module Boxcars
|
|
85
99
|
# @return [Hash] The formatted prompt { prompt: "..."}
|
86
100
|
def as_prompt(inputs = nil)
|
87
101
|
if show_roles
|
88
|
-
|
102
|
+
no_history.map { |ln| format("#{ln.first}: #{ln.last}", inputs) }.compact.join("\n\n")
|
89
103
|
else
|
90
|
-
|
104
|
+
no_history.map { |ln| format(ln.last, inputs) }.compact.join("\n\n")
|
91
105
|
end
|
92
106
|
rescue ::KeyError => e
|
93
107
|
first_line = e.message.to_s.split("\n").first
|
@@ -36,5 +36,16 @@ module Boxcars
|
|
36
36
|
new_prompt.conversation.add_conversation(conversation)
|
37
37
|
new_prompt
|
38
38
|
end
|
39
|
+
|
40
|
+
# add conversation history to the prompt
|
41
|
+
# @param history [Hash] The history to add to the prompt.
|
42
|
+
def add_history(history)
|
43
|
+
conversation.add_history(Conversation.new(lines: history))
|
44
|
+
end
|
45
|
+
|
46
|
+
# print the prompt
|
47
|
+
def to_s
|
48
|
+
conversation.to_s
|
49
|
+
end
|
39
50
|
end
|
40
51
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Boxcars
|
4
|
+
# used by Boxcars to return structured result and additional context
|
5
|
+
class Observation
|
6
|
+
attr_reader :note, :status, :added_context
|
7
|
+
|
8
|
+
# @param note [String] The note to use for the result
|
9
|
+
# @param status [Symbol] :ok or :error
|
10
|
+
# @param added_context [Hash] Any additional context to add to the result
|
11
|
+
def initialize(note:, status: :ok, **added_context)
|
12
|
+
@note = note
|
13
|
+
@status = status
|
14
|
+
@added_context = added_context
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Hash] The result as a hash
|
18
|
+
def to_h
|
19
|
+
{
|
20
|
+
note: note,
|
21
|
+
status: status
|
22
|
+
}.merge(added_context).compact
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String] The result as a json string
|
26
|
+
def to_json(*args)
|
27
|
+
JSON.generate(to_h, *args)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [String] An explanation of the result
|
31
|
+
def to_s
|
32
|
+
note
|
33
|
+
end
|
34
|
+
|
35
|
+
# create a new Observaton from a text string with a status of :ok
|
36
|
+
# @param note [String] The text to use for the observation
|
37
|
+
# @param added_context [Hash] Any additional context to add to the result
|
38
|
+
# @return [Boxcars::Observation] The observation
|
39
|
+
def self.ok(note, **kwargs)
|
40
|
+
new(note: note, status: :ok, **kwargs)
|
41
|
+
end
|
42
|
+
|
43
|
+
# create a new Observaton from a text string with a status of :error
|
44
|
+
# @param note [String] The text to use for the observation
|
45
|
+
# @param added_context [Hash] Any additional context to add to the result
|
46
|
+
# @return [Boxcars::Observation] The observation
|
47
|
+
def self.err(note, **kwargs)
|
48
|
+
new(note: note, status: :error, **kwargs)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/boxcars/ruby_repl.rb
CHANGED
@@ -8,7 +8,7 @@ module Boxcars
|
|
8
8
|
def call(code:)
|
9
9
|
Boxcars.debug "RubyREPL: #{code}", :yellow
|
10
10
|
|
11
|
-
# wrap the code in an
|
11
|
+
# wrap the code in an exception 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|
|
@@ -19,6 +19,8 @@ module Boxcars
|
|
19
19
|
if output =~ /^Error: /
|
20
20
|
Boxcars.debug output, :red
|
21
21
|
Result.from_error(output, code: code)
|
22
|
+
elsif output.blank?
|
23
|
+
Result.from_error("The code you gave me did not print a result", code: code)
|
22
24
|
else
|
23
25
|
output = ::Regexp.last_match(1) if output =~ /^\s*Answer:\s*(.*)$/m
|
24
26
|
Boxcars.debug "Answer: #{output}", :yellow, style: :bold
|
@@ -18,7 +18,7 @@ module Boxcars
|
|
18
18
|
@engine_prefix = 'Thought:'
|
19
19
|
@wants_next_actions = kwargs.fetch(:wants_next_actions, false)
|
20
20
|
prompt ||= my_prompt
|
21
|
-
super(engine: engine, boxcars: boxcars, prompt: prompt, name: name, description: description)
|
21
|
+
super(engine: engine, boxcars: boxcars, prompt: prompt, name: name, description: description, **kwargs)
|
22
22
|
end
|
23
23
|
|
24
24
|
# @return Hash The additional variables for this boxcar.
|
@@ -63,7 +63,9 @@ module Boxcars
|
|
63
63
|
end
|
64
64
|
|
65
65
|
action = match[:action].strip
|
66
|
+
Boxcars.debug("Action: #{action}", :yellow)
|
66
67
|
action_input = match[:action_input].strip.delete_prefix('"').delete_suffix('"')
|
68
|
+
Boxcars.debug("Action Input: #{action_input}", :yellow)
|
67
69
|
[action, action_input]
|
68
70
|
end
|
69
71
|
end
|
@@ -83,7 +85,11 @@ module Boxcars
|
|
83
85
|
"%<next_actions>s\n",
|
84
86
|
"Remember to start a line with \"Final Answer:\" to give me the final answer.\n",
|
85
87
|
"Also make sure to specify a question for the Action Input.\n",
|
88
|
+
"Finally, if you can deduct the answer from the question or observation, you can ",
|
89
|
+
"start with \"Final Answer:\" and give me the answer.\n",
|
86
90
|
"Begin!"),
|
91
|
+
# insert thoughts here from previous runs
|
92
|
+
hist,
|
87
93
|
user("Question: %<input>s"),
|
88
94
|
assi("Thought: %<agent_scratchpad>s")
|
89
95
|
].freeze
|
data/lib/boxcars/train.rb
CHANGED
@@ -16,7 +16,8 @@ module Boxcars
|
|
16
16
|
@boxcars = boxcars
|
17
17
|
@name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
|
18
18
|
@return_values = [:output]
|
19
|
-
@return_intermediate_steps = kwargs.
|
19
|
+
@return_intermediate_steps = kwargs.fetch(:return_intermediate_steps, true)
|
20
|
+
kwargs.delete(:return_intermediate_steps)
|
20
21
|
@max_iterations = kwargs.delete(:max_iterations) || 25
|
21
22
|
@early_stopping_method = kwargs.delete(:early_stopping_method) || "force"
|
22
23
|
kwargs[:stop] ||= ["\n#{observation_prefix}"]
|
@@ -33,14 +34,16 @@ module Boxcars
|
|
33
34
|
# build the scratchpad for the engine
|
34
35
|
# @param intermediate_steps [Array] The intermediate steps to build the scratchpad from.
|
35
36
|
# @return [String] The scratchpad.
|
37
|
+
# rubocop:disable Lint/RedundantStringCoercion
|
36
38
|
def construct_scratchpad(intermediate_steps)
|
37
39
|
thoughts = ""
|
38
40
|
intermediate_steps.each do |action, observation|
|
39
41
|
thoughts += action.is_a?(String) ? action : " #{action.log}"
|
40
|
-
thoughts += "\n#{observation_prefix}#{observation}\n#{engine_prefix}"
|
42
|
+
thoughts += "\n#{observation_prefix}#{observation.to_s}\n#{engine_prefix}"
|
41
43
|
end
|
42
44
|
thoughts
|
43
45
|
end
|
46
|
+
# rubocop:enable Lint/RedundantStringCoercion
|
44
47
|
|
45
48
|
# determine the next action
|
46
49
|
# @param full_inputs [Hash] The inputs to the engine.
|
@@ -51,7 +54,7 @@ module Boxcars
|
|
51
54
|
loop do
|
52
55
|
full_inputs[:agent_scratchpad] += full_output
|
53
56
|
output = predict(**full_inputs)
|
54
|
-
full_output += output
|
57
|
+
full_output += output.to_s
|
55
58
|
parsed_output = extract_boxcar_and_input(full_output)
|
56
59
|
break unless parsed_output.nil?
|
57
60
|
end
|
@@ -95,7 +98,7 @@ module Boxcars
|
|
95
98
|
|
96
99
|
# the output keys
|
97
100
|
def output_keys
|
98
|
-
return return_values + [
|
101
|
+
return return_values + [:intermediate_steps] if return_intermediate_steps
|
99
102
|
|
100
103
|
return_values
|
101
104
|
end
|
@@ -116,7 +119,7 @@ module Boxcars
|
|
116
119
|
def pre_return(output, intermediate_steps)
|
117
120
|
Boxcars.debug output.log, :yellow, style: :bold
|
118
121
|
final_output = output.return_values
|
119
|
-
final_output[
|
122
|
+
final_output[:intermediate_steps] = intermediate_steps if return_intermediate_steps
|
120
123
|
final_output
|
121
124
|
end
|
122
125
|
|
@@ -154,7 +157,7 @@ module Boxcars
|
|
154
157
|
def return_stopped_response(early_stopping_method, intermediate_steps, **kwargs)
|
155
158
|
case early_stopping_method
|
156
159
|
when "force"
|
157
|
-
TrainFinish({ output: "Agent stopped due to max iterations." }, "")
|
160
|
+
TrainFinish.new({ output: "Agent stopped due to max iterations." }, "")
|
158
161
|
when "generate"
|
159
162
|
thoughts = ""
|
160
163
|
intermediate_steps.each do |action, observation|
|
@@ -167,13 +170,13 @@ module Boxcars
|
|
167
170
|
full_output = predict(**full_inputs)
|
168
171
|
parsed_output = extract_boxcar_and_input(full_output)
|
169
172
|
if parsed_output.nil?
|
170
|
-
TrainFinish({ output: full_output }, full_output)
|
173
|
+
TrainFinish.new({ output: full_output }, full_output)
|
171
174
|
else
|
172
175
|
boxcar, boxcar_input = parsed_output
|
173
176
|
if boxcar == finish_boxcar_name
|
174
|
-
TrainFinish({ output: boxcar_input }, full_output)
|
177
|
+
TrainFinish.new({ output: boxcar_input }, full_output)
|
175
178
|
else
|
176
|
-
TrainFinish({ output: full_output }, full_output)
|
179
|
+
TrainFinish.new({ output: full_output }, full_output)
|
177
180
|
end
|
178
181
|
end
|
179
182
|
else
|
@@ -194,22 +197,24 @@ module Boxcars
|
|
194
197
|
|
195
198
|
if (boxcar = name_to_boxcar_map[output.boxcar])
|
196
199
|
begin
|
197
|
-
observation = boxcar.run(output.boxcar_input)
|
200
|
+
observation = Observation.ok(boxcar.run(output.boxcar_input))
|
198
201
|
return_direct = boxcar.return_direct
|
199
202
|
rescue Boxcars::ConfigurationError, Boxcars::SecurityError => e
|
200
203
|
raise e
|
201
204
|
rescue StandardError => e
|
202
|
-
Boxcars.error "Error in #{boxcar.name} boxcar#call: #{e}", :red
|
203
|
-
observation = "Error - #{e}, correct and try again."
|
205
|
+
Boxcars.error "Error in #{boxcar.name} boxcar#call: #{e}\nbt:#{caller[0..5].join("\n ")}", :red
|
206
|
+
observation = Observation.err("Error - #{e}, correct and try again.")
|
204
207
|
end
|
205
208
|
elsif output.boxcar == :error
|
206
209
|
observation = output.log
|
207
210
|
return_direct = false
|
208
211
|
else
|
209
|
-
observation = "#{output.boxcar} is not a valid
|
212
|
+
observation = Observation.err("Error - #{output.boxcar} is not a valid action, try again.")
|
210
213
|
return_direct = false
|
211
214
|
end
|
212
|
-
|
215
|
+
# rubocop:disable Lint/RedundantStringCoercion
|
216
|
+
Boxcars.debug "Observation: #{observation.to_s}", :green
|
217
|
+
# rubocop:enable Lint/RedundantStringCoercion
|
213
218
|
intermediate_steps.append([output, observation])
|
214
219
|
if return_direct
|
215
220
|
output = TrainFinish.new({ return_values[0] => observation }, "")
|
data/lib/boxcars/version.rb
CHANGED
data/lib/boxcars.rb
CHANGED
@@ -107,10 +107,24 @@ module Boxcars
|
|
107
107
|
Boxcars.configuration.logger
|
108
108
|
end
|
109
109
|
|
110
|
+
# Keep a running log of log messages
|
111
|
+
def self.log
|
112
|
+
@log ||= []
|
113
|
+
@log
|
114
|
+
end
|
115
|
+
|
116
|
+
# Resets the log and return the old log
|
117
|
+
def self.take_log
|
118
|
+
logs = @log
|
119
|
+
@log = []
|
120
|
+
logs
|
121
|
+
end
|
122
|
+
|
110
123
|
# Logging system
|
111
124
|
# debug log
|
112
125
|
def self.debug(msg, color = nil, **options)
|
113
126
|
msg = colorize(msg.to_s, color, **options) if color
|
127
|
+
log << msg
|
114
128
|
if logger
|
115
129
|
logger.debug(msg)
|
116
130
|
else
|
@@ -121,6 +135,7 @@ module Boxcars
|
|
121
135
|
# info log
|
122
136
|
def self.info(msg, color = nil, **options)
|
123
137
|
msg = colorize(msg.to_s, color, **options) if color
|
138
|
+
log << msg
|
124
139
|
if logger
|
125
140
|
logger.info(msg)
|
126
141
|
else
|
@@ -131,6 +146,7 @@ module Boxcars
|
|
131
146
|
# warn log
|
132
147
|
def self.warn(msg, color = nil, **options)
|
133
148
|
msg = colorize(msg.to_s, color, **options) if color
|
149
|
+
log << msg
|
134
150
|
if logger
|
135
151
|
logger.warn(msg)
|
136
152
|
else
|
@@ -141,6 +157,7 @@ module Boxcars
|
|
141
157
|
# error log
|
142
158
|
def self.error(msg, color = nil, **options)
|
143
159
|
msg = colorize(msg.to_s, color, **options) if color
|
160
|
+
log << msg
|
144
161
|
if logger
|
145
162
|
logger.error(msg)
|
146
163
|
else
|
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.
|
4
|
+
version: 0.3.1
|
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-
|
12
|
+
date: 2023-07-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: google_search_results
|
@@ -109,7 +109,9 @@ files:
|
|
109
109
|
- lib/boxcars/boxcar/calculator.rb
|
110
110
|
- lib/boxcars/boxcar/engine_boxcar.rb
|
111
111
|
- lib/boxcars/boxcar/google_search.rb
|
112
|
-
- lib/boxcars/boxcar/
|
112
|
+
- lib/boxcars/boxcar/sql_active_record.rb
|
113
|
+
- lib/boxcars/boxcar/sql_base.rb
|
114
|
+
- lib/boxcars/boxcar/sql_sequel.rb
|
113
115
|
- lib/boxcars/boxcar/swagger.rb
|
114
116
|
- lib/boxcars/boxcar/vector_answer.rb
|
115
117
|
- lib/boxcars/boxcar/wikipedia_search.rb
|
@@ -120,6 +122,7 @@ files:
|
|
120
122
|
- lib/boxcars/engine/gpt4all_eng.rb
|
121
123
|
- lib/boxcars/engine/openai.rb
|
122
124
|
- lib/boxcars/generation.rb
|
125
|
+
- lib/boxcars/observation.rb
|
123
126
|
- lib/boxcars/prompt.rb
|
124
127
|
- lib/boxcars/result.rb
|
125
128
|
- lib/boxcars/ruby_repl.rb
|