boxcars 0.2.16 → 0.3.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 +31 -0
- data/Gemfile.lock +13 -2
- data/boxcars.gemspec +1 -0
- data/lib/boxcars/boxcar/active_record.rb +3 -2
- data/lib/boxcars/boxcar/calculator.rb +1 -1
- data/lib/boxcars/boxcar/url_text.rb +58 -0
- data/lib/boxcars/boxcar.rb +70 -8
- data/lib/boxcars/conversation.rb +21 -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/xml_train.rb +107 -0
- data/lib/boxcars/train/xml_zero_shot.rb +60 -0
- data/lib/boxcars/train/zero_shot.rb +8 -20
- data/lib/boxcars/train.rb +65 -23
- data/lib/boxcars/version.rb +1 -1
- data/lib/boxcars/x_node.rb +75 -0
- data/lib/boxcars.rb +4 -0
- metadata +21 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 463f57f1436cfea29e0a60bbfce071afd355a5d0cadbe1d69f96adc7393eacd6
|
|
4
|
+
data.tar.gz: 282e528fb8cb8b532b621db5c2e7ce2b82c4607057c01f0feb1eaacfe98ba09b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c76d2772db0925f71779c0bac1a2e92af20bcf398cc6e7cf943edeadb2014426554fba607b034fd68ac88bd26f2d42eef114de57332e83a4fe76558a9b2f0cea
|
|
7
|
+
data.tar.gz: '0489fffef87c32fbf6a7a9af4fbca77ef991437353211174617b6ea8e229e49accc85c39af93414ce1628a4472cc208d8075570abba5e8269b30cfb66000a40e'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v0.3.1](https://github.com/BoxcarsAI/boxcars/tree/v0.3.1) (2023-07-01)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.16...v0.3.1)
|
|
6
|
+
|
|
7
|
+
**Closed issues:**
|
|
8
|
+
|
|
9
|
+
- Add a running log of prompts for debugging [\#99](https://github.com/BoxcarsAI/boxcars/issues/99)
|
|
10
|
+
- Anyway to create conversation? [\#73](https://github.com/BoxcarsAI/boxcars/issues/73)
|
|
11
|
+
|
|
12
|
+
**Merged pull requests:**
|
|
13
|
+
|
|
14
|
+
- now, when you call run on a train multiple times, it remembers the ru… [\#101](https://github.com/BoxcarsAI/boxcars/pull/101) ([francis](https://github.com/francis))
|
|
15
|
+
|
|
16
|
+
## [v0.2.16](https://github.com/BoxcarsAI/boxcars/tree/v0.2.16) (2023-06-26)
|
|
17
|
+
|
|
18
|
+
[Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.15...v0.2.16)
|
|
19
|
+
|
|
20
|
+
**Implemented enhancements:**
|
|
21
|
+
|
|
22
|
+
- Support Sequel connection type [\#22](https://github.com/BoxcarsAI/boxcars/issues/22)
|
|
23
|
+
|
|
24
|
+
**Closed issues:**
|
|
25
|
+
|
|
26
|
+
- Using the SQL model This model's maximum context length is 4097 tokens [\#88](https://github.com/BoxcarsAI/boxcars/issues/88)
|
|
27
|
+
|
|
28
|
+
**Merged pull requests:**
|
|
29
|
+
|
|
30
|
+
- Add running logs [\#100](https://github.com/BoxcarsAI/boxcars/pull/100) ([francis](https://github.com/francis))
|
|
31
|
+
- create new Sequel boxcar, and refactor Active Record SQL boxcar [\#98](https://github.com/BoxcarsAI/boxcars/pull/98) ([francis](https://github.com/francis))
|
|
32
|
+
- Support for Sequel SQL connection types [\#97](https://github.com/BoxcarsAI/boxcars/pull/97) ([eltoob](https://github.com/eltoob))
|
|
33
|
+
|
|
3
34
|
## [v0.2.15](https://github.com/BoxcarsAI/boxcars/tree/v0.2.15) (2023-06-09)
|
|
4
35
|
|
|
5
36
|
[Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.14...v0.2.15)
|
data/Gemfile.lock
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
boxcars (0.2
|
|
4
|
+
boxcars (0.3.2)
|
|
5
5
|
google_search_results (~> 2.2)
|
|
6
6
|
gpt4all (~> 0.0.4)
|
|
7
7
|
hnswlib (~> 0.8)
|
|
8
|
+
nokogiri (~> 1.15)
|
|
8
9
|
pgvector (~> 0.2)
|
|
9
10
|
ruby-openai (~> 4.1)
|
|
10
11
|
|
|
@@ -55,7 +56,7 @@ GEM
|
|
|
55
56
|
domain_name (0.5.20190701)
|
|
56
57
|
unf (>= 0.0.5, < 1.0.0)
|
|
57
58
|
dotenv (2.8.1)
|
|
58
|
-
faraday (2.7.
|
|
59
|
+
faraday (2.7.9)
|
|
59
60
|
faraday-net_http (>= 2.0, < 3.1)
|
|
60
61
|
ruby2_keywords (>= 0.0.4)
|
|
61
62
|
faraday-http-cache (2.5.0)
|
|
@@ -103,6 +104,14 @@ GEM
|
|
|
103
104
|
netrc (0.11.0)
|
|
104
105
|
nio4r (2.5.9)
|
|
105
106
|
nio4r (2.5.9-java)
|
|
107
|
+
nokogiri (1.15.2-arm64-darwin)
|
|
108
|
+
racc (~> 1.4)
|
|
109
|
+
nokogiri (1.15.2-java)
|
|
110
|
+
racc (~> 1.4)
|
|
111
|
+
nokogiri (1.15.2-x86_64-darwin)
|
|
112
|
+
racc (~> 1.4)
|
|
113
|
+
nokogiri (1.15.2-x86_64-linux)
|
|
114
|
+
racc (~> 1.4)
|
|
106
115
|
octokit (4.25.1)
|
|
107
116
|
faraday (>= 1, < 3)
|
|
108
117
|
sawyer (~> 0.9)
|
|
@@ -120,6 +129,8 @@ GEM
|
|
|
120
129
|
protocol-hpack (~> 1.4)
|
|
121
130
|
protocol-http (~> 0.18)
|
|
122
131
|
public_suffix (5.0.1)
|
|
132
|
+
racc (1.7.1)
|
|
133
|
+
racc (1.7.1-java)
|
|
123
134
|
rainbow (3.1.1)
|
|
124
135
|
rake (13.0.6)
|
|
125
136
|
regexp_parser (2.8.0)
|
data/boxcars.gemspec
CHANGED
|
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
spec.add_dependency "google_search_results", "~> 2.2"
|
|
35
35
|
spec.add_dependency "gpt4all", "~> 0.0.4"
|
|
36
36
|
spec.add_dependency "hnswlib", "~> 0.8"
|
|
37
|
+
spec.add_dependency "nokogiri", "~> 1.15"
|
|
37
38
|
spec.add_dependency "pgvector", "~> 0.2"
|
|
38
39
|
spec.add_dependency "ruby-openai", "~> 4.1"
|
|
39
40
|
|
|
@@ -147,7 +147,7 @@ module Boxcars
|
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
def change_count(changes_code)
|
|
150
|
-
return 0
|
|
150
|
+
return 0 if changes_code.nil? || changes_code.empty? || changes_code =~ %r{^(None|N/A)$}i
|
|
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,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Boxcars
|
|
4
|
+
# A Boxcar that reads text from a URL.
|
|
5
|
+
class URLText < Boxcar
|
|
6
|
+
# the description of this boxcar
|
|
7
|
+
DESC = "useful when you want to get text from a URL."
|
|
8
|
+
|
|
9
|
+
# implements a boxcar that uses the Google SerpAPI to get answers to questions.
|
|
10
|
+
# @param name [String] The name of the boxcar. Defaults to classname.
|
|
11
|
+
# @param description [String] A description of the boxcar. Defaults to SERPDESC.
|
|
12
|
+
def initialize(name: "FetchURL", description: DESC)
|
|
13
|
+
super(name: name, description: description)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Get text from a url.
|
|
17
|
+
# @param url [String] The url
|
|
18
|
+
# @return [String] The text for the url.
|
|
19
|
+
def run(url)
|
|
20
|
+
url = URI.parse(url)
|
|
21
|
+
get_answer(url)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def html_to_text(url, response)
|
|
27
|
+
Nokogiri::HTML(response.body).css(%w[h1 h2 h3 h4 h5 h6 p a].join(",")).map do |e|
|
|
28
|
+
itxt = e.inner_text.strip
|
|
29
|
+
itxt = itxt.gsub(/[[:space:]]+/, " ") # remove extra spaces
|
|
30
|
+
# next if itxt.nil? || itxt.empty?
|
|
31
|
+
if e.name == "a"
|
|
32
|
+
href = e.attributes["href"]&.value
|
|
33
|
+
href = URI.join(url, href).to_s if href =~ %r{^/}
|
|
34
|
+
"[#{itxt}](#{href})" # if e.attributes["href"]&.value =~ /^http/
|
|
35
|
+
else
|
|
36
|
+
itxt
|
|
37
|
+
end
|
|
38
|
+
end.compact.join("\n\n")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get_answer(url)
|
|
42
|
+
response = Net::HTTP.get_response(url)
|
|
43
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
44
|
+
return Result.from_text(response.body) if response.content_type == "text/plain"
|
|
45
|
+
|
|
46
|
+
if response.content_type == "text/html"
|
|
47
|
+
# return only the top level text
|
|
48
|
+
txt = html_to_text(url, response)
|
|
49
|
+
Result.from_text(txt)
|
|
50
|
+
else
|
|
51
|
+
Result.from_text(response.body)
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
Result.new(status: :error, explanation: "Error with url: #{response.code} #{response.message}")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/boxcars/boxcar.rb
CHANGED
|
@@ -17,12 +17,12 @@ module Boxcars
|
|
|
17
17
|
|
|
18
18
|
# Input keys this chain expects.
|
|
19
19
|
def input_keys
|
|
20
|
-
|
|
20
|
+
[:question]
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# Output keys this chain expects.
|
|
24
24
|
def output_keys
|
|
25
|
-
|
|
25
|
+
[:answer]
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Check that all inputs are present.
|
|
@@ -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,65 @@ 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
|
+
|
|
119
|
+
def schema
|
|
120
|
+
params = input_keys.map do |key|
|
|
121
|
+
"<param name=\"#{key}\" data-type=\"String\" required=\"true\" description=\"#{key}\" />"
|
|
122
|
+
end.join("\n")
|
|
123
|
+
<<~SCHEMA.freeze
|
|
124
|
+
<tool>
|
|
125
|
+
<tool name="#{name}" version="0.1" description="#{description}">
|
|
126
|
+
<params>
|
|
127
|
+
#{params}
|
|
128
|
+
</params>
|
|
129
|
+
</tool>
|
|
130
|
+
SCHEMA
|
|
131
|
+
end
|
|
132
|
+
|
|
97
133
|
private
|
|
98
134
|
|
|
135
|
+
# remember the history of this boxcar. Take the current intermediate steps and
|
|
136
|
+
# create a history that can be used on the next run.
|
|
137
|
+
# @param current_results [Array<Hash>] The current results.
|
|
138
|
+
def remember_history(current_results)
|
|
139
|
+
return unless current_results[:intermediate_steps] && is_a?(Train)
|
|
140
|
+
|
|
141
|
+
# insert conversation history into the prompt
|
|
142
|
+
history = []
|
|
143
|
+
history << Boxcar.user(key_and_value_text(question_prefix, current_results[:input]))
|
|
144
|
+
current_results[:intermediate_steps].each do |action, obs|
|
|
145
|
+
if action.is_a?(TrainAction)
|
|
146
|
+
obs = Observation.new(status: :ok, note: obs) if obs.is_a?(String)
|
|
147
|
+
next if obs.status != :ok
|
|
148
|
+
|
|
149
|
+
history << Boxcar.assi("#{thought_prefix}#{action.log}", "\n",
|
|
150
|
+
key_and_value_text(observation_prefix, obs.note))
|
|
151
|
+
else
|
|
152
|
+
Boxcars.error "Unknown action: #{action}", :red
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
final_answer = key_and_value_text(final_answer_prefix, current_results[:output])
|
|
156
|
+
history << Boxcar.assi(
|
|
157
|
+
key_and_value_text(thought_prefix, "I know the final answer\n#{final_answer}\n"))
|
|
158
|
+
prompt.add_history(history)
|
|
159
|
+
end
|
|
160
|
+
|
|
99
161
|
# Get an answer from the boxcar.
|
|
100
162
|
def run_boxcar(inputs:, return_only_outputs: false)
|
|
101
163
|
inputs = our_inputs(inputs)
|
|
@@ -117,13 +179,11 @@ module Boxcars
|
|
|
117
179
|
if kwargs.empty?
|
|
118
180
|
raise Boxcars::ArgumentError, "run supports only one positional argument." if args.length != 1
|
|
119
181
|
|
|
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]
|
|
182
|
+
return run_boxcar(inputs: args[0])
|
|
125
183
|
end
|
|
126
184
|
|
|
185
|
+
return run_boxcar(inputs: kwargs) if args.empty?
|
|
186
|
+
|
|
127
187
|
raise Boxcars::ArgumentError, "run supported with either positional or keyword arguments but not both. Got args" \
|
|
128
188
|
": #{args} and kwargs: #{kwargs}."
|
|
129
189
|
end
|
|
@@ -148,10 +208,12 @@ module Boxcars
|
|
|
148
208
|
end
|
|
149
209
|
end
|
|
150
210
|
|
|
211
|
+
require "boxcars/observation"
|
|
151
212
|
require "boxcars/result"
|
|
152
213
|
require "boxcars/boxcar/engine_boxcar"
|
|
153
214
|
require "boxcars/boxcar/calculator"
|
|
154
215
|
require "boxcars/boxcar/google_search"
|
|
216
|
+
require "boxcars/boxcar/url_text"
|
|
155
217
|
require "boxcars/boxcar/wikipedia_search"
|
|
156
218
|
require "boxcars/boxcar/sql_base"
|
|
157
219
|
require "boxcars/boxcar/sql_active_record"
|
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,23 @@ module Boxcars
|
|
|
64
64
|
@lines += conversation.lines
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# insert converation above history line if it is present
|
|
68
|
+
# @param conversation [Conversation] The conversation to add
|
|
69
|
+
def add_history(conversation)
|
|
70
|
+
# find the history line
|
|
71
|
+
hi = lines.rindex { |ln| ln[0] == :history }
|
|
72
|
+
return unless hi
|
|
73
|
+
|
|
74
|
+
@lines = @lines.dup
|
|
75
|
+
|
|
76
|
+
# insert the conversation above the history line
|
|
77
|
+
@lines.insert(hi, *conversation.lines)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def no_history
|
|
81
|
+
@lines.reject { |ln| ln[0] == :history }
|
|
82
|
+
end
|
|
83
|
+
|
|
67
84
|
# return just the messages for the conversation
|
|
68
85
|
def message_text
|
|
69
86
|
lines.map(&:last).join("\n")
|
|
@@ -73,7 +90,7 @@ module Boxcars
|
|
|
73
90
|
# @param inputs [Hash] The inputs to use for the prompt.
|
|
74
91
|
# @return [Hash] The formatted prompt { messages: ...}
|
|
75
92
|
def as_messages(inputs = nil)
|
|
76
|
-
{ messages:
|
|
93
|
+
{ messages: no_history.map { |ln| { role: ln.first, content: ln.last % inputs } } }
|
|
77
94
|
rescue ::KeyError => e
|
|
78
95
|
first_line = e.message.to_s.split("\n").first
|
|
79
96
|
Boxcars.error "Missing prompt input key: #{first_line}"
|
|
@@ -85,9 +102,9 @@ module Boxcars
|
|
|
85
102
|
# @return [Hash] The formatted prompt { prompt: "..."}
|
|
86
103
|
def as_prompt(inputs = nil)
|
|
87
104
|
if show_roles
|
|
88
|
-
|
|
105
|
+
no_history.map { |ln| format("#{ln.first}: #{ln.last}", inputs) }.compact.join("\n\n")
|
|
89
106
|
else
|
|
90
|
-
|
|
107
|
+
no_history.map { |ln| format(ln.last, inputs) }.compact.join("\n\n")
|
|
91
108
|
end
|
|
92
109
|
rescue ::KeyError => e
|
|
93
110
|
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
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
# base class for all XML trains
|
|
6
|
+
module Boxcars
|
|
7
|
+
# A Train using XML for prompting and execution.
|
|
8
|
+
class XMLTrain < Train
|
|
9
|
+
# A Train will use a engine to run a series of boxcars.
|
|
10
|
+
# @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
|
|
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
|
|
14
|
+
# @abstract
|
|
15
|
+
def initialize(boxcars:, prompt:, engine: nil, **kwargs)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def init_prefixes
|
|
20
|
+
@thought_prefix ||= "<thought>"
|
|
21
|
+
@observation_prefix ||= "<observation>"
|
|
22
|
+
@final_answer_prefix ||= "<final_answer>"
|
|
23
|
+
@answer_prefix ||= "<answer>"
|
|
24
|
+
@question_prefix ||= "<question>"
|
|
25
|
+
@output_prefix ||= "<output>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def close_tag(tag)
|
|
29
|
+
tag.to_s.sub("<", "</") if tag.to_s[0] == "<"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# the xml to describe the boxcars
|
|
33
|
+
def boxcars_xml
|
|
34
|
+
schema = boxcars.map(&:schema).join("\n")
|
|
35
|
+
"<boxcars>\n#{schema}</boxcars>"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return Hash The additional variables for this boxcar.
|
|
39
|
+
def prediction_additional(_inputs)
|
|
40
|
+
{ boxcars_xml: boxcars_xml, next_actions: next_actions }.merge super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_output(text)
|
|
44
|
+
if text =~ /#{close_tag(thought_prefix)}/
|
|
45
|
+
"<data>#{engine_prefix}#{text}</data>"
|
|
46
|
+
else
|
|
47
|
+
"<data>#{text}</data>"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Extract the boxcar and input from the engine output.
|
|
52
|
+
# @param text [String] The output from the engine.
|
|
53
|
+
# @return [Array<Boxcars::Boxcar, String>] The boxcar and input.
|
|
54
|
+
def extract_boxcar_and_input(text)
|
|
55
|
+
get_action_and_input(engine_output: build_output(text))
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
Boxcars.debug("Error: #{e.message}", :red)
|
|
58
|
+
[:error, e.message]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def parse_output(engine_output)
|
|
64
|
+
doc = Nokogiri::XML("<data>#{engine_prefix}#{engine_output}\n</data>")
|
|
65
|
+
keys = doc.element_children.first.element_children.map(&:name).map(&:to_sym)
|
|
66
|
+
keys.to_h do |key|
|
|
67
|
+
[key, doc.at_xpath("//#{key}")&.text]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def child_keys(xnode)
|
|
72
|
+
xnode.children.map(&:name).map(&:to_sym)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# get next action and input using an XNode
|
|
76
|
+
# @param xnode [XNode] The XNode to use.
|
|
77
|
+
# @return [Array<String, String>] The action and input.
|
|
78
|
+
def xn_get_action_and_input(xnode)
|
|
79
|
+
action = xnode.xtext("//action")
|
|
80
|
+
action_input = xnode.xtext("//action_input")
|
|
81
|
+
thought = xnode.xtext("//thought")
|
|
82
|
+
final_answer = xnode.xtext("//final_answer")
|
|
83
|
+
|
|
84
|
+
# the thought should be the frist line here if it doesn't start with "Action:"
|
|
85
|
+
Boxcars.debug("Thought: #{thought}", :yellow)
|
|
86
|
+
|
|
87
|
+
if final_answer.present?
|
|
88
|
+
Result.new(status: :ok, answer: final_answer, explanation: final_answer)
|
|
89
|
+
else
|
|
90
|
+
# we have an unexpected output from the engine
|
|
91
|
+
unless action.present? && action_input.present?
|
|
92
|
+
return [:error, "You gave me an improperly formatted answer or didn't use tags."]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
Boxcars.debug("Action: #{action}\nAction Input: #{action_input}", :yellow)
|
|
96
|
+
[action, action_input]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Parse out the action and input from the engine output.
|
|
101
|
+
# @param engine_output [String] The output from the engine.
|
|
102
|
+
# @return [Array<String>] The action and input.
|
|
103
|
+
def get_action_and_input(engine_output:)
|
|
104
|
+
xn_get_action_and_input(XNode.from_xml(engine_output))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
# Agent for the MRKL chain
|
|
6
|
+
module Boxcars
|
|
7
|
+
# A Train using the zero-shot react method and only XML in the prompt.
|
|
8
|
+
class XMLZeroShot < XMLTrain
|
|
9
|
+
attr_reader :boxcars
|
|
10
|
+
attr_accessor :wants_next_actions
|
|
11
|
+
|
|
12
|
+
# @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
|
|
13
|
+
# @param engine [Boxcars::Engine] The engine to use for this train.
|
|
14
|
+
# @param name [String] The name of the train. Defaults to 'Zero Shot'.
|
|
15
|
+
# @param description [String] The description of the train. Defaults to 'Zero Shot Train'.
|
|
16
|
+
# @param prompt [Boxcars::Prompt] The prompt to use. Defaults to the built-in prompt.
|
|
17
|
+
# @param kwargs [Hash] Additional arguments to pass to the train. wants_next_actions: true
|
|
18
|
+
def initialize(boxcars:, engine: nil, name: 'Zero Shot XML', description: 'Zero Shot Train wiht XML', prompt: nil, **kwargs)
|
|
19
|
+
@engine_prefix = '<thought>'
|
|
20
|
+
@wants_next_actions = kwargs.fetch(:wants_next_actions, false)
|
|
21
|
+
prompt ||= my_prompt
|
|
22
|
+
super(engine: engine, boxcars: boxcars, prompt: prompt, name: name, description: description, **kwargs)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
CTEMPLATE = [
|
|
28
|
+
syst("<training>Answer the following questions as best you can. You have access to the following tools for actions:\n",
|
|
29
|
+
"%<boxcars_xml>s",
|
|
30
|
+
"Use the following format making sure all open tags have closing tags:\n",
|
|
31
|
+
" <question>the input question you must answer</question>\n",
|
|
32
|
+
" <thought>you should always think about what to do</thought>\n",
|
|
33
|
+
" <action>the action to take, from this action list above</action>\n",
|
|
34
|
+
" <action_input>input to the action</action_input>\n",
|
|
35
|
+
" <observation>the result of the action</observation>\n",
|
|
36
|
+
" ... (this Thought/Action/Action Input/Observation sequence can repeat N times)\n",
|
|
37
|
+
" <thought>I know the final answer</thought>\n",
|
|
38
|
+
" <final_answer>the final answer to the original input question</final_answer>\n",
|
|
39
|
+
"-- FORMAT END -\n",
|
|
40
|
+
"Your answer should always have begin and end tags for each element.\n",
|
|
41
|
+
"Also make sure to specify a question for the action_input.\n",
|
|
42
|
+
"Finally, if you can deduct the answer from the question or observation, you can ",
|
|
43
|
+
"jump to final_answer and give me the answer.\n",
|
|
44
|
+
"</training>"),
|
|
45
|
+
hist, # insert thoughts here from previous runs
|
|
46
|
+
user("<question>%<input>s</question>"),
|
|
47
|
+
assi("<thought>%<agent_scratchpad>s")
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
# The prompt to use for the train.
|
|
51
|
+
def my_prompt
|
|
52
|
+
@conversation ||= Conversation.new(lines: CTEMPLATE)
|
|
53
|
+
@my_prompt ||= ConversationPrompt.new(
|
|
54
|
+
conversation: @conversation,
|
|
55
|
+
input_variables: [:input],
|
|
56
|
+
other_inputs: [:boxcars_xml, :next_actions, :agent_scratchpad],
|
|
57
|
+
output_variables: [:answer])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
module Boxcars
|
|
5
5
|
# A Train using the zero-shot react method.
|
|
6
6
|
class ZeroShot < Train
|
|
7
|
-
attr_reader :boxcars, :observation_prefix
|
|
7
|
+
attr_reader :boxcars, :observation_prefix
|
|
8
8
|
attr_accessor :wants_next_actions
|
|
9
9
|
|
|
10
10
|
# @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
|
|
@@ -14,11 +14,9 @@ module Boxcars
|
|
|
14
14
|
# @param prompt [Boxcars::Prompt] The prompt to use. Defaults to the built-in prompt.
|
|
15
15
|
# @param kwargs [Hash] Additional arguments to pass to the train. wants_next_actions: true
|
|
16
16
|
def initialize(boxcars:, engine: nil, name: 'Zero Shot', description: 'Zero Shot Train', prompt: nil, **kwargs)
|
|
17
|
-
@observation_prefix = 'Observation: '
|
|
18
|
-
@engine_prefix = 'Thought:'
|
|
19
17
|
@wants_next_actions = kwargs.fetch(:wants_next_actions, false)
|
|
20
18
|
prompt ||= my_prompt
|
|
21
|
-
super(engine: engine, boxcars: boxcars, prompt: prompt, name: name, description: description)
|
|
19
|
+
super(engine: engine, boxcars: boxcars, prompt: prompt, name: name, description: description, **kwargs)
|
|
22
20
|
end
|
|
23
21
|
|
|
24
22
|
# @return Hash The additional variables for this boxcar.
|
|
@@ -31,6 +29,8 @@ module Boxcars
|
|
|
31
29
|
# @return [Array<Boxcars::Boxcar, String>] The boxcar and input.
|
|
32
30
|
def extract_boxcar_and_input(text)
|
|
33
31
|
get_action_and_input(engine_output: text)
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
[:error, e.message]
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
private
|
|
@@ -85,27 +85,15 @@ module Boxcars
|
|
|
85
85
|
"%<next_actions>s\n",
|
|
86
86
|
"Remember to start a line with \"Final Answer:\" to give me the final answer.\n",
|
|
87
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",
|
|
88
90
|
"Begin!"),
|
|
91
|
+
# insert thoughts here from previous runs
|
|
92
|
+
hist,
|
|
89
93
|
user("Question: %<input>s"),
|
|
90
94
|
assi("Thought: %<agent_scratchpad>s")
|
|
91
95
|
].freeze
|
|
92
96
|
|
|
93
|
-
def boxcar_names
|
|
94
|
-
@boxcar_names ||= "[#{boxcars.map(&:name).join(', ')}]"
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def boxcar_descriptions
|
|
98
|
-
@boxcar_descriptions ||= boxcars.map { |boxcar| "#{boxcar.name}: #{boxcar.description}" }.join("\n")
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def next_actions
|
|
102
|
-
if wants_next_actions
|
|
103
|
-
"Next Actions: Up to 3 logical suggested next questions for the user to ask after getting this answer.\n"
|
|
104
|
-
else
|
|
105
|
-
""
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
97
|
# The prompt to use for the train.
|
|
110
98
|
def my_prompt
|
|
111
99
|
@conversation ||= Conversation.new(lines: CTEMPLATE)
|
data/lib/boxcars/train.rb
CHANGED
|
@@ -4,7 +4,8 @@ module Boxcars
|
|
|
4
4
|
# @abstract
|
|
5
5
|
class Train < EngineBoxcar
|
|
6
6
|
attr_reader :boxcars, :return_values, :return_intermediate_steps,
|
|
7
|
-
:max_iterations, :early_stopping_method, :name_to_boxcar_map
|
|
7
|
+
:max_iterations, :early_stopping_method, :name_to_boxcar_map,
|
|
8
|
+
:observation_prefix, :thought_prefix, :final_answer_prefix, :answer_prefix, :question_prefix, :engine_prefix
|
|
8
9
|
|
|
9
10
|
# A Train will use a engine to run a series of boxcars.
|
|
10
11
|
# @param boxcars [Array<Boxcars::Boxcar>] The boxcars to run.
|
|
@@ -16,15 +17,25 @@ module Boxcars
|
|
|
16
17
|
@boxcars = boxcars
|
|
17
18
|
@name_to_boxcar_map = boxcars.to_h { |boxcar| [boxcar.name, boxcar] }
|
|
18
19
|
@return_values = [:output]
|
|
19
|
-
@return_intermediate_steps = kwargs.
|
|
20
|
+
@return_intermediate_steps = kwargs.fetch(:return_intermediate_steps, true)
|
|
21
|
+
kwargs.delete(:return_intermediate_steps)
|
|
20
22
|
@max_iterations = kwargs.delete(:max_iterations) || 25
|
|
21
23
|
@early_stopping_method = kwargs.delete(:early_stopping_method) || "force"
|
|
22
|
-
|
|
24
|
+
init_prefixes
|
|
25
|
+
kwargs[:stop] = ["\n#{observation_prefix}"] unless kwargs.key?(:stop)
|
|
23
26
|
|
|
24
27
|
super(prompt: prompt, engine: engine, **kwargs)
|
|
25
28
|
end
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
def init_prefixes
|
|
31
|
+
@thought_prefix ||= "Thought: "
|
|
32
|
+
@observation_prefix ||= "Observation: "
|
|
33
|
+
@final_answer_prefix ||= "Final Answer: "
|
|
34
|
+
@answer_prefix ||= "Answer:"
|
|
35
|
+
@question_prefix ||= "Question: "
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Callback to process the action/action input of a train.
|
|
28
39
|
# @param text [String] The text to extract from.
|
|
29
40
|
def extract_boxcar_and_input(text)
|
|
30
41
|
Result.new(status: :ok, answer: text, explanation: engine_output)
|
|
@@ -37,7 +48,7 @@ module Boxcars
|
|
|
37
48
|
thoughts = ""
|
|
38
49
|
intermediate_steps.each do |action, observation|
|
|
39
50
|
thoughts += action.is_a?(String) ? action : " #{action.log}"
|
|
40
|
-
thoughts += "\n#{
|
|
51
|
+
thoughts += "\n#{observation_text(observation)}\n#{engine_prefix}"
|
|
41
52
|
end
|
|
42
53
|
thoughts
|
|
43
54
|
end
|
|
@@ -51,7 +62,7 @@ module Boxcars
|
|
|
51
62
|
loop do
|
|
52
63
|
full_inputs[:agent_scratchpad] += full_output
|
|
53
64
|
output = predict(**full_inputs)
|
|
54
|
-
full_output += output
|
|
65
|
+
full_output += output.to_s
|
|
55
66
|
parsed_output = extract_boxcar_and_input(full_output)
|
|
56
67
|
break unless parsed_output.nil?
|
|
57
68
|
end
|
|
@@ -88,14 +99,12 @@ module Boxcars
|
|
|
88
99
|
# the input keys
|
|
89
100
|
# @return [Array<Symbol>] The input keys.
|
|
90
101
|
def input_keys
|
|
91
|
-
|
|
92
|
-
list.delete(:agent_scratchpad)
|
|
93
|
-
list
|
|
102
|
+
prompt.input_variables - [:agent_scratchpad]
|
|
94
103
|
end
|
|
95
104
|
|
|
96
105
|
# the output keys
|
|
97
106
|
def output_keys
|
|
98
|
-
return return_values + [
|
|
107
|
+
return return_values + [:intermediate_steps] if return_intermediate_steps
|
|
99
108
|
|
|
100
109
|
return_values
|
|
101
110
|
end
|
|
@@ -116,17 +125,10 @@ module Boxcars
|
|
|
116
125
|
def pre_return(output, intermediate_steps)
|
|
117
126
|
Boxcars.debug output.log, :yellow, style: :bold
|
|
118
127
|
final_output = output.return_values
|
|
119
|
-
final_output[
|
|
128
|
+
final_output[:intermediate_steps] = intermediate_steps if return_intermediate_steps
|
|
120
129
|
final_output
|
|
121
130
|
end
|
|
122
131
|
|
|
123
|
-
# the prefix for the engine
|
|
124
|
-
# @param return_direct [Boolean] Whether to return directly.
|
|
125
|
-
# @return [String] The prefix.
|
|
126
|
-
def engine_prefix(return_direct)
|
|
127
|
-
return_direct ? "" : engine_prefix
|
|
128
|
-
end
|
|
129
|
-
|
|
130
132
|
# validate the prompt
|
|
131
133
|
# @param values [Hash] The values to validate.
|
|
132
134
|
# @return [Hash] The validated values.
|
|
@@ -159,7 +161,7 @@ module Boxcars
|
|
|
159
161
|
thoughts = ""
|
|
160
162
|
intermediate_steps.each do |action, observation|
|
|
161
163
|
thoughts += action.log
|
|
162
|
-
thoughts += "\n#{
|
|
164
|
+
thoughts += "\n#{observation_text(observation)}\n#{engine_prefix}"
|
|
163
165
|
end
|
|
164
166
|
thoughts += "\n\nI now need to return a final answer based on the previous steps:"
|
|
165
167
|
new_inputs = { agent_scratchpad: thoughts, stop: _stop }
|
|
@@ -170,6 +172,7 @@ module Boxcars
|
|
|
170
172
|
TrainFinish.new({ output: full_output }, full_output)
|
|
171
173
|
else
|
|
172
174
|
boxcar, boxcar_input = parsed_output
|
|
175
|
+
Boxcars.debug "Got boxcar #{boxcar} and input #{boxcar_input}"
|
|
173
176
|
if boxcar == finish_boxcar_name
|
|
174
177
|
TrainFinish.new({ output: boxcar_input }, full_output)
|
|
175
178
|
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
205
|
Boxcars.error "Error in #{boxcar.name} boxcar#call: #{e}\nbt:#{caller[0..5].join("\n ")}", :red
|
|
203
|
-
observation = "Error - #{e}, correct and try again."
|
|
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 }, "")
|
|
@@ -220,9 +225,46 @@ module Boxcars
|
|
|
220
225
|
output = return_stopped_response(early_stopping_method, intermediate_steps, **inputs)
|
|
221
226
|
pre_return(output, intermediate_steps)
|
|
222
227
|
end
|
|
228
|
+
|
|
229
|
+
def key_and_value_text(key, value)
|
|
230
|
+
value = value.to_s
|
|
231
|
+
if key =~ /^<(?<tag_name>[[:word:]]+)>$/
|
|
232
|
+
# we need a close tag too
|
|
233
|
+
"#{key}#{value}</#{Regexp.last_match[:tag_name]}>"
|
|
234
|
+
else
|
|
235
|
+
"#{key}#{value}"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# this is for the scratchpad
|
|
240
|
+
def observation_text(observation)
|
|
241
|
+
key_and_value_text(observation_prefix, observation)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def question_text(question)
|
|
245
|
+
key_and_value_text(question_prefix, question)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def boxcar_names
|
|
249
|
+
@boxcar_names ||= boxcars.map(&:name).join(', ')
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def boxcar_descriptions
|
|
253
|
+
@boxcar_descriptions ||= boxcars.map { |boxcar| "#{boxcar.name}: #{boxcar.description}" }.join("\n")
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def next_actions
|
|
257
|
+
if wants_next_actions
|
|
258
|
+
"Next Actions: Up to 3 logical suggested next questions for the user to ask after getting this answer.\n"
|
|
259
|
+
else
|
|
260
|
+
""
|
|
261
|
+
end
|
|
262
|
+
end
|
|
223
263
|
end
|
|
224
264
|
end
|
|
225
265
|
|
|
226
266
|
require "boxcars/train/train_action"
|
|
227
267
|
require "boxcars/train/train_finish"
|
|
228
268
|
require "boxcars/train/zero_shot"
|
|
269
|
+
require "boxcars/train/xml_train"
|
|
270
|
+
require "boxcars/train/xml_zero_shot"
|
data/lib/boxcars/version.rb
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'nokogiri'
|
|
4
|
+
|
|
5
|
+
module Boxcars
|
|
6
|
+
class XNode
|
|
7
|
+
attr_accessor :node, :children, :attributes
|
|
8
|
+
|
|
9
|
+
def initialize(node)
|
|
10
|
+
@node = node
|
|
11
|
+
@valid_names = []
|
|
12
|
+
@children = {}
|
|
13
|
+
# @attributes = node.attributes.transform_values(&:value)
|
|
14
|
+
@attributes = node.attributes.values.to_h { |a| [a.name.to_sym, a.value] }
|
|
15
|
+
|
|
16
|
+
node.children.each do |child|
|
|
17
|
+
next if child.text?
|
|
18
|
+
|
|
19
|
+
child_node = XNode.new(child)
|
|
20
|
+
if @children[child.name].nil?
|
|
21
|
+
@valid_names << child.name.to_sym
|
|
22
|
+
@children[child.name] = child_node
|
|
23
|
+
elsif @children[child.name].is_a?(Array)
|
|
24
|
+
@children[child.name] << child_node
|
|
25
|
+
else
|
|
26
|
+
@children[child.name] = [@children[child.name], child_node]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.from_xml(xml)
|
|
32
|
+
doc = Nokogiri::XML.parse(xml)
|
|
33
|
+
raise XmlError, "XML is not valid: #{doc.errors.map { |e| "#{e.line}:#{e.column} #{e.message}" }}" if doc.errors.any?
|
|
34
|
+
|
|
35
|
+
XNode.new(doc.root)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def xml
|
|
39
|
+
@node.to_xml
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def text
|
|
43
|
+
@node.text
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def xpath(path)
|
|
47
|
+
@node.xpath(path)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def xtext(path)
|
|
51
|
+
rv = xpath(path)&.text&.gsub(/[[:space:]]+/, " ")&.strip
|
|
52
|
+
return nil if rv.empty?
|
|
53
|
+
|
|
54
|
+
rv
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def stext
|
|
58
|
+
@stext ||= text.gsub(/[[:space:]]+/, " ").strip # remove extra spaces
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def [](key)
|
|
62
|
+
@children[key.to_s]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def method_missing(name, *args)
|
|
66
|
+
return @children[name.to_s] if @children.key?(name.to_s)
|
|
67
|
+
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
72
|
+
@valid_names.include?(method) || super
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/boxcars.rb
CHANGED
|
@@ -22,6 +22,9 @@ module Boxcars
|
|
|
22
22
|
# Error class for all Boxcars key errors.
|
|
23
23
|
class KeyError < Error; end
|
|
24
24
|
|
|
25
|
+
# Error class for all Boxcars XML errors.
|
|
26
|
+
class XmlError < Error; end
|
|
27
|
+
|
|
25
28
|
# Configuration contains gem settings
|
|
26
29
|
class Configuration
|
|
27
30
|
attr_writer :openai_access_token, :serpapi_api_key
|
|
@@ -179,6 +182,7 @@ module Boxcars
|
|
|
179
182
|
end
|
|
180
183
|
|
|
181
184
|
require "boxcars/version"
|
|
185
|
+
require "boxcars/x_node"
|
|
182
186
|
require "boxcars/prompt"
|
|
183
187
|
require "boxcars/conversation_prompt"
|
|
184
188
|
require "boxcars/conversation"
|
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.3.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-
|
|
12
|
+
date: 2023-07-10 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: google_search_results
|
|
@@ -53,6 +53,20 @@ dependencies:
|
|
|
53
53
|
- - "~>"
|
|
54
54
|
- !ruby/object:Gem::Version
|
|
55
55
|
version: '0.8'
|
|
56
|
+
- !ruby/object:Gem::Dependency
|
|
57
|
+
name: nokogiri
|
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - "~>"
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '1.15'
|
|
63
|
+
type: :runtime
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - "~>"
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '1.15'
|
|
56
70
|
- !ruby/object:Gem::Dependency
|
|
57
71
|
name: pgvector
|
|
58
72
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -113,6 +127,7 @@ files:
|
|
|
113
127
|
- lib/boxcars/boxcar/sql_base.rb
|
|
114
128
|
- lib/boxcars/boxcar/sql_sequel.rb
|
|
115
129
|
- lib/boxcars/boxcar/swagger.rb
|
|
130
|
+
- lib/boxcars/boxcar/url_text.rb
|
|
116
131
|
- lib/boxcars/boxcar/vector_answer.rb
|
|
117
132
|
- lib/boxcars/boxcar/wikipedia_search.rb
|
|
118
133
|
- lib/boxcars/conversation.rb
|
|
@@ -122,12 +137,15 @@ files:
|
|
|
122
137
|
- lib/boxcars/engine/gpt4all_eng.rb
|
|
123
138
|
- lib/boxcars/engine/openai.rb
|
|
124
139
|
- lib/boxcars/generation.rb
|
|
140
|
+
- lib/boxcars/observation.rb
|
|
125
141
|
- lib/boxcars/prompt.rb
|
|
126
142
|
- lib/boxcars/result.rb
|
|
127
143
|
- lib/boxcars/ruby_repl.rb
|
|
128
144
|
- lib/boxcars/train.rb
|
|
129
145
|
- lib/boxcars/train/train_action.rb
|
|
130
146
|
- lib/boxcars/train/train_finish.rb
|
|
147
|
+
- lib/boxcars/train/xml_train.rb
|
|
148
|
+
- lib/boxcars/train/xml_zero_shot.rb
|
|
131
149
|
- lib/boxcars/train/zero_shot.rb
|
|
132
150
|
- lib/boxcars/vector_search.rb
|
|
133
151
|
- lib/boxcars/vector_store.rb
|
|
@@ -147,6 +165,7 @@ files:
|
|
|
147
165
|
- lib/boxcars/vector_store/pgvector/search.rb
|
|
148
166
|
- lib/boxcars/vector_store/split_text.rb
|
|
149
167
|
- lib/boxcars/version.rb
|
|
168
|
+
- lib/boxcars/x_node.rb
|
|
150
169
|
homepage: https://github.com/BoxcarsAI/boxcars
|
|
151
170
|
licenses:
|
|
152
171
|
- MIT
|