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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92af5ad71c16886afc760bcc1b98a1d050486f8cf7010e0f05ee5015aa767710
4
- data.tar.gz: a0e5f39d40f79d4e4e5e337c00657764b5f9b329ec6bcc0f5a6e4ea62b0675b5
3
+ metadata.gz: 463f57f1436cfea29e0a60bbfce071afd355a5d0cadbe1d69f96adc7393eacd6
4
+ data.tar.gz: 282e528fb8cb8b532b621db5c2e7ce2b82c4607057c01f0feb1eaacfe98ba09b
5
5
  SHA512:
6
- metadata.gz: 06123e9f6ea4cb294b258056b6e50af7654b2c4866b1585fb32e520eee4e64dcdecc7cf8dc4d019a42a3193b01be6a02fba8190ef52add4eb70ad7f1a79d492c
7
- data.tar.gz: 3320ee8fdb68a4bf9601a6a584d64517242e0e80b14b812b6f04f3e0eaed05b5d520541e5547c72b7b3a8ebea044e7173163cafccc2cc8f9bb5c25b25a231ffe
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.16)
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.4)
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 unless changes_code
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
- "#{stage} Error: #{msg} - please fix \"#{stage}:\" to not have this error"
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
@@ -17,12 +17,12 @@ module Boxcars
17
17
 
18
18
  # Input keys this chain expects.
19
19
  def input_keys
20
- raise NotImplementedError
20
+ [:question]
21
21
  end
22
22
 
23
23
  # Output keys this chain expects.
24
24
  def output_keys
25
- raise NotImplementedError
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?(Result) ? rv.to_answer : rv
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])[output_keys.first]
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"
@@ -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: lines.map { |ln| { role: ln.first, content: ln.last % inputs } } }
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
- lines.map { |ln| format("#{ln.first}: #{ln.last}", inputs) }.join("\n\n")
105
+ no_history.map { |ln| format("#{ln.first}: #{ln.last}", inputs) }.compact.join("\n\n")
89
106
  else
90
- lines.map { |ln| format(ln.last, inputs) }.join("\n\n")
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
@@ -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 excption block so we can catch errors
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, :engine_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.delete(:return_intermediate_steps) || false
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
- kwargs[:stop] ||= ["\n#{observation_prefix}"]
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
- # Extract the boxcar name and input from the text.
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#{observation_prefix}#{observation}\n#{engine_prefix}"
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
- list = prompt.input_variables
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 + ["intermediate_steps"] if return_intermediate_steps
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["intermediate_steps"] = intermediate_steps if return_intermediate_steps
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#{observation_prefix}#{observation}\n#{engine_prefix}"
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 boxcar, try another one."
212
+ observation = Observation.err("Error - #{output.boxcar} is not a valid action, try again.")
210
213
  return_direct = false
211
214
  end
212
- Boxcars.debug "Observation: #{observation}", :green
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"
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Boxcars
4
4
  # The current version of the gem.
5
- VERSION = "0.2.16"
5
+ VERSION = "0.3.2"
6
6
  end
@@ -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.16
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-06-26 00:00:00.000000000 Z
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