boxcars 0.3.1 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2448419a2e348f8111fa19bd7b9bb1a05ab19b30d3dc9c0255ae0bd8673c156
4
- data.tar.gz: fd6577be64b72941a87cbdb8c1c1ca31a7c88383869fa67f86d21cf721749168
3
+ metadata.gz: 4c6fc81621333c6663dd04a49215f01a1fbd5fa247551b80b652676a4c06f155
4
+ data.tar.gz: a956727b4b986d42cc9f7b2b609dba561473f1976cce5f16ce2db641de778dfc
5
5
  SHA512:
6
- metadata.gz: fd21eb8cd3ea3cc2fdf29140a3dce7dec26a7e4ffbd791d7aa7dd63aaa267c34d4ead764f85d3f7dbb9d1038330e3614bbfaf7f8c34818544b0d282568ad66f5
7
- data.tar.gz: e12626132060202679533c3a101f4ec4fb1c9846f7d54beb9bc84ce7d71cb4f56be10f16a2c229480cc6c4378a074566f26a7ccbeeadc0c7ffc27ab81dfc6ee4
6
+ metadata.gz: cac0c355836d68387a0fe66e7c65efe341c4ec746627eb275411198a0ba0092291e7add9cb8fbd93aee00da1d61420e36eaa1c90d97621629f3db5f56c0b134d
7
+ data.tar.gz: 799149ab51920e7d95d136ad5b4e57ac98c7e344ce277bcbd6e0ee8d89e8cc0d8eea23f37d821712818b16eacf8f0b236bb8c5730633250dc02c209b647f7b8f
data/CHANGELOG.md CHANGED
@@ -1,12 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased](https://github.com/BoxcarsAI/boxcars/tree/HEAD)
3
+ ## [v0.3.1](https://github.com/BoxcarsAI/boxcars/tree/v0.3.1) (2023-07-01)
4
4
 
5
- [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.16...HEAD)
5
+ [Full Changelog](https://github.com/BoxcarsAI/boxcars/compare/v0.2.16...v0.3.1)
6
6
 
7
7
  **Closed issues:**
8
8
 
9
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))
10
15
 
11
16
  ## [v0.2.16](https://github.com/BoxcarsAI/boxcars/tree/v0.2.16) (2023-06-26)
12
17
 
data/Gemfile.lock CHANGED
@@ -1,10 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- boxcars (0.3.1)
4
+ boxcars (0.3.3)
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
 
@@ -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 && changes_code != "None"
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
@@ -0,0 +1,67 @@
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
+ do_encoding(get_answer(url))
22
+ end
23
+
24
+ private
25
+
26
+ def do_encoding(answer)
27
+ if answer.is_a?(Result)
28
+ answer.explanation = answer.explanation.encode(xml: :text)
29
+ answer
30
+ else
31
+ answer.encode(xml: :text)
32
+ end
33
+ end
34
+
35
+ def html_to_text(url, response)
36
+ Nokogiri::HTML(response.body).css(%w[h1 h2 h3 h4 h5 h6 p a].join(",")).map do |e|
37
+ itxt = e.inner_text.strip
38
+ itxt = itxt.gsub(/[[:space:]]+/, " ") # remove extra spaces
39
+ # next if itxt.nil? || itxt.empty?
40
+ if e.name == "a"
41
+ href = e.attributes["href"]&.value
42
+ href = URI.join(url, href).to_s if href =~ %r{^/}
43
+ "[#{itxt}](#{href})" # if e.attributes["href"]&.value =~ /^http/
44
+ else
45
+ itxt
46
+ end
47
+ end.compact.join("\n\n")
48
+ end
49
+
50
+ def get_answer(url)
51
+ response = Net::HTTP.get_response(url)
52
+ if response.is_a?(Net::HTTPSuccess)
53
+ return Result.from_text(response.body) if response.content_type == "text/plain"
54
+
55
+ if response.content_type == "text/html"
56
+ # return only the top level text
57
+ txt = html_to_text(url, response)
58
+ Result.from_text(txt)
59
+ else
60
+ Result.from_text(response.body)
61
+ end
62
+ else
63
+ Result.new(status: :error, explanation: "Error with url: #{response.code} #{response.message}")
64
+ end
65
+ end
66
+ end
67
+ 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.
@@ -116,6 +116,20 @@ module Boxcars
116
116
  end
117
117
  # rubocop:enable Security/YAMLLoad
118
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
+
119
133
  private
120
134
 
121
135
  # remember the history of this boxcar. Take the current intermediate steps and
@@ -126,18 +140,21 @@ module Boxcars
126
140
 
127
141
  # insert conversation history into the prompt
128
142
  history = []
129
- history << Boxcar.user("Question: #{current_results[:input]}")
143
+ history << Boxcar.user(key_and_value_text(question_prefix, current_results[:input]))
130
144
  current_results[:intermediate_steps].each do |action, obs|
131
145
  if action.is_a?(TrainAction)
132
146
  obs = Observation.new(status: :ok, note: obs) if obs.is_a?(String)
133
147
  next if obs.status != :ok
134
148
 
135
- history << Boxcar.assi("Thought: #{action.log}\n", "Observation: #{obs.note}")
149
+ history << Boxcar.assi("#{thought_prefix}#{action.log}", "\n",
150
+ key_and_value_text(observation_prefix, obs.note))
136
151
  else
137
152
  Boxcars.error "Unknown action: #{action}", :red
138
153
  end
139
154
  end
140
- history << Boxcar.assi("Thought: I know the final answer\nFinal Answer: #{current_results[:output]}")
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"))
141
158
  prompt.add_history(history)
142
159
  end
143
160
 
@@ -196,6 +213,7 @@ require "boxcars/result"
196
213
  require "boxcars/boxcar/engine_boxcar"
197
214
  require "boxcars/boxcar/calculator"
198
215
  require "boxcars/boxcar/google_search"
216
+ require "boxcars/boxcar/url_text"
199
217
  require "boxcars/boxcar/wikipedia_search"
200
218
  require "boxcars/boxcar/sql_base"
201
219
  require "boxcars/boxcar/sql_active_record"
@@ -64,12 +64,15 @@ module Boxcars
64
64
  @lines += conversation.lines
65
65
  end
66
66
 
67
- # insert converation above history line
67
+ # insert converation above history line if it is present
68
68
  # @param conversation [Conversation] The conversation to add
69
69
  def add_history(conversation)
70
- @lines = @lines.dup
71
70
  # find the history line
72
71
  hi = lines.rindex { |ln| ln[0] == :history }
72
+ return unless hi
73
+
74
+ @lines = @lines.dup
75
+
73
76
  # insert the conversation above the history line
74
77
  @lines.insert(hi, *conversation.lines)
75
78
  end
@@ -29,7 +29,12 @@ module Boxcars
29
29
 
30
30
  # @return [String] An explanation of the result
31
31
  def to_s
32
- note
32
+ note.to_s
33
+ end
34
+
35
+ # @return [String] An explanation of the result
36
+ def to_text
37
+ to_s
33
38
  end
34
39
 
35
40
  # create a new Observaton from a text string with a status of :ok
@@ -3,7 +3,7 @@
3
3
  module Boxcars
4
4
  # used by Boxcars to return structured result and additional context
5
5
  class Result
6
- attr_reader :status, :answer, :explanation, :suggestions, :added_context
6
+ attr_accessor :status, :answer, :explanation, :suggestions, :added_context
7
7
 
8
8
  # @param status [Symbol] :ok or :error
9
9
  # @param answer [String] The answer to the question
@@ -0,0 +1,110 @@
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
+ @using_xml = true
17
+ super
18
+ end
19
+
20
+ def init_prefixes
21
+ @thought_prefix ||= "<thought>"
22
+ @observation_prefix ||= "<observation>"
23
+ @final_answer_prefix ||= "<final_answer>"
24
+ @answer_prefix ||= "<answer>"
25
+ @question_prefix ||= "<question>"
26
+ @output_prefix ||= "<output>"
27
+ end
28
+
29
+ def close_tag(tag)
30
+ tag.to_s.sub("<", "</") if tag.to_s[0] == "<"
31
+ end
32
+
33
+ # the xml to describe the boxcars
34
+ def boxcars_xml
35
+ schema = boxcars.map(&:schema).join("\n")
36
+ "<boxcars>\n#{schema}</boxcars>"
37
+ end
38
+
39
+ # @return Hash The additional variables for this boxcar.
40
+ def prediction_additional(_inputs)
41
+ { boxcars_xml: boxcars_xml, next_actions: next_actions }.merge super
42
+ end
43
+
44
+ def build_output(text)
45
+ if text.end_with?("</usetool>")
46
+ "<data>#{engine_prefix}#{text}</output></data>"
47
+ elsif text =~ /#{close_tag(thought_prefix)}/
48
+ "<data>#{engine_prefix}#{text}</data>"
49
+ else
50
+ "<data>#{text}</data>"
51
+ end
52
+ end
53
+
54
+ # Extract the boxcar and input from the engine output.
55
+ # @param text [String] The output from the engine.
56
+ # @return [Array<Boxcars::Boxcar, String>] The boxcar and input.
57
+ def extract_boxcar_and_input(text)
58
+ get_action_and_input(engine_output: build_output(text))
59
+ rescue StandardError => e
60
+ Boxcars.debug("Error: #{e.message}", :red)
61
+ [:error, e.message]
62
+ end
63
+
64
+ private
65
+
66
+ def parse_output(engine_output)
67
+ doc = Nokogiri::XML("<data>#{engine_prefix}#{engine_output}\n</data>")
68
+ keys = doc.element_children.first.element_children.map(&:name).map(&:to_sym)
69
+ keys.to_h do |key|
70
+ [key, doc.at_xpath("//#{key}")&.text]
71
+ end
72
+ end
73
+
74
+ def child_keys(xnode)
75
+ xnode.children.map(&:name).map(&:to_sym)
76
+ end
77
+
78
+ # get next action and input using an XNode
79
+ # @param xnode [XNode] The XNode to use.
80
+ # @return [Array<String, String>] The action and input.
81
+ def xn_get_action_and_input(xnode)
82
+ action = xnode.xtext("//action")
83
+ action_input = xnode.xtext("//action_input")
84
+ thought = xnode.xtext("//thought")
85
+ final_answer = xnode.xtext("//final_answer")
86
+
87
+ # the thought should be the frist line here if it doesn't start with "Action:"
88
+ Boxcars.debug("Thought: #{thought}", :yellow)
89
+
90
+ if final_answer.present?
91
+ Result.new(status: :ok, answer: final_answer, explanation: final_answer)
92
+ else
93
+ # we have an unexpected output from the engine
94
+ unless action.present? && action_input.present?
95
+ return [:error, "You gave me an improperly formatted answer or didn't use tags."]
96
+ end
97
+
98
+ Boxcars.debug("Action: #{action}\nAction Input: #{action_input}", :yellow)
99
+ [action, action_input]
100
+ end
101
+ end
102
+
103
+ # Parse out the action and input from the engine output.
104
+ # @param engine_output [String] The output from the engine.
105
+ # @return [Array<String>] The action and input.
106
+ def get_action_and_input(engine_output:)
107
+ xn_get_action_and_input(XNode.from_xml(engine_output))
108
+ end
109
+ end
110
+ 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,8 +14,6 @@ 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
19
  super(engine: engine, boxcars: boxcars, prompt: prompt, name: name, description: description, **kwargs)
@@ -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
@@ -94,22 +94,6 @@ module Boxcars
94
94
  assi("Thought: %<agent_scratchpad>s")
95
95
  ].freeze
96
96
 
97
- def boxcar_names
98
- @boxcar_names ||= "[#{boxcars.map(&:name).join(', ')}]"
99
- end
100
-
101
- def boxcar_descriptions
102
- @boxcar_descriptions ||= boxcars.map { |boxcar| "#{boxcar.name}: #{boxcar.description}" }.join("\n")
103
- end
104
-
105
- def next_actions
106
- if wants_next_actions
107
- "Next Actions: Up to 3 logical suggested next questions for the user to ask after getting this answer.\n"
108
- else
109
- ""
110
- end
111
- end
112
-
113
97
  # The prompt to use for the train.
114
98
  def my_prompt
115
99
  @conversation ||= Conversation.new(lines: CTEMPLATE)
data/lib/boxcars/train.rb CHANGED
@@ -3,8 +3,9 @@
3
3
  module Boxcars
4
4
  # @abstract
5
5
  class Train < EngineBoxcar
6
- attr_reader :boxcars, :return_values, :return_intermediate_steps,
7
- :max_iterations, :early_stopping_method, :name_to_boxcar_map
6
+ attr_reader :boxcars, :return_values, :return_intermediate_steps, :using_xml,
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.
@@ -20,12 +21,21 @@ module Boxcars
20
21
  kwargs.delete(:return_intermediate_steps)
21
22
  @max_iterations = kwargs.delete(:max_iterations) || 25
22
23
  @early_stopping_method = kwargs.delete(:early_stopping_method) || "force"
23
- kwargs[:stop] ||= ["\n#{observation_prefix}"]
24
+ init_prefixes
25
+ kwargs[:stop] = ["\n#{observation_prefix}"] unless kwargs.key?(:stop)
24
26
 
25
27
  super(prompt: prompt, engine: engine, **kwargs)
26
28
  end
27
29
 
28
- # 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.
29
39
  # @param text [String] The text to extract from.
30
40
  def extract_boxcar_and_input(text)
31
41
  Result.new(status: :ok, answer: text, explanation: engine_output)
@@ -34,16 +44,14 @@ module Boxcars
34
44
  # build the scratchpad for the engine
35
45
  # @param intermediate_steps [Array] The intermediate steps to build the scratchpad from.
36
46
  # @return [String] The scratchpad.
37
- # rubocop:disable Lint/RedundantStringCoercion
38
47
  def construct_scratchpad(intermediate_steps)
39
48
  thoughts = ""
40
49
  intermediate_steps.each do |action, observation|
41
50
  thoughts += action.is_a?(String) ? action : " #{action.log}"
42
- thoughts += "\n#{observation_prefix}#{observation.to_s}\n#{engine_prefix}"
51
+ thoughts += "\n#{observation_text(observation)}\n#{engine_prefix}"
43
52
  end
44
53
  thoughts
45
54
  end
46
- # rubocop:enable Lint/RedundantStringCoercion
47
55
 
48
56
  # determine the next action
49
57
  # @param full_inputs [Hash] The inputs to the engine.
@@ -91,9 +99,7 @@ module Boxcars
91
99
  # the input keys
92
100
  # @return [Array<Symbol>] The input keys.
93
101
  def input_keys
94
- list = prompt.input_variables
95
- list.delete(:agent_scratchpad)
96
- list
102
+ prompt.input_variables - [:agent_scratchpad]
97
103
  end
98
104
 
99
105
  # the output keys
@@ -123,13 +129,6 @@ module Boxcars
123
129
  final_output
124
130
  end
125
131
 
126
- # the prefix for the engine
127
- # @param return_direct [Boolean] Whether to return directly.
128
- # @return [String] The prefix.
129
- def engine_prefix(return_direct)
130
- return_direct ? "" : engine_prefix
131
- end
132
-
133
132
  # validate the prompt
134
133
  # @param values [Hash] The values to validate.
135
134
  # @return [Hash] The validated values.
@@ -162,7 +161,7 @@ module Boxcars
162
161
  thoughts = ""
163
162
  intermediate_steps.each do |action, observation|
164
163
  thoughts += action.log
165
- thoughts += "\n#{observation_prefix}#{observation}\n#{engine_prefix}"
164
+ thoughts += "\n#{observation_text(observation)}\n#{engine_prefix}"
166
165
  end
167
166
  thoughts += "\n\nI now need to return a final answer based on the previous steps:"
168
167
  new_inputs = { agent_scratchpad: thoughts, stop: _stop }
@@ -173,6 +172,7 @@ module Boxcars
173
172
  TrainFinish.new({ output: full_output }, full_output)
174
173
  else
175
174
  boxcar, boxcar_input = parsed_output
175
+ Boxcars.debug "Got boxcar #{boxcar} and input #{boxcar_input}"
176
176
  if boxcar == finish_boxcar_name
177
177
  TrainFinish.new({ output: boxcar_input }, full_output)
178
178
  else
@@ -184,6 +184,18 @@ module Boxcars
184
184
  end
185
185
  end
186
186
 
187
+ def get_boxcar_result(boxcar, boxcar_input)
188
+ boxcar_result = boxcar.run(boxcar_input)
189
+ return boxcar_result unless using_xml
190
+
191
+ if boxcar_result.is_a?(Result)
192
+ boxcar_result.answer = boxcar_result.answer.encode(xml: :text)
193
+ else
194
+ boxcar_result = boxcar_result.encode(xml: :text)
195
+ end
196
+ boxcar_result
197
+ end
198
+
187
199
  # execute the train train
188
200
  # @param inputs [Hash] The inputs.
189
201
  # @return [Hash] The output.
@@ -197,7 +209,7 @@ module Boxcars
197
209
 
198
210
  if (boxcar = name_to_boxcar_map[output.boxcar])
199
211
  begin
200
- observation = Observation.ok(boxcar.run(output.boxcar_input))
212
+ observation = Observation.ok(get_boxcar_result(boxcar, output.boxcar_input))
201
213
  return_direct = boxcar.return_direct
202
214
  rescue Boxcars::ConfigurationError, Boxcars::SecurityError => e
203
215
  raise e
@@ -212,9 +224,7 @@ module Boxcars
212
224
  observation = Observation.err("Error - #{output.boxcar} is not a valid action, try again.")
213
225
  return_direct = false
214
226
  end
215
- # rubocop:disable Lint/RedundantStringCoercion
216
- Boxcars.debug "Observation: #{observation.to_s}", :green
217
- # rubocop:enable Lint/RedundantStringCoercion
227
+ Boxcars.debug "Observation: #{observation}", :green
218
228
  intermediate_steps.append([output, observation])
219
229
  if return_direct
220
230
  output = TrainFinish.new({ return_values[0] => observation }, "")
@@ -225,9 +235,46 @@ module Boxcars
225
235
  output = return_stopped_response(early_stopping_method, intermediate_steps, **inputs)
226
236
  pre_return(output, intermediate_steps)
227
237
  end
238
+
239
+ def key_and_value_text(key, value)
240
+ value = value.to_s
241
+ if key =~ /^<(?<tag_name>[[:word:]]+)>$/
242
+ # we need a close tag too
243
+ "#{key}#{value}</#{Regexp.last_match[:tag_name]}>"
244
+ else
245
+ "#{key}#{value}"
246
+ end
247
+ end
248
+
249
+ # this is for the scratchpad
250
+ def observation_text(observation)
251
+ key_and_value_text(observation_prefix, observation)
252
+ end
253
+
254
+ def question_text(question)
255
+ key_and_value_text(question_prefix, question)
256
+ end
257
+
258
+ def boxcar_names
259
+ @boxcar_names ||= boxcars.map(&:name).join(', ')
260
+ end
261
+
262
+ def boxcar_descriptions
263
+ @boxcar_descriptions ||= boxcars.map { |boxcar| "#{boxcar.name}: #{boxcar.description}" }.join("\n")
264
+ end
265
+
266
+ def next_actions
267
+ if wants_next_actions
268
+ "Next Actions: Up to 3 logical suggested next questions for the user to ask after getting this answer.\n"
269
+ else
270
+ ""
271
+ end
272
+ end
228
273
  end
229
274
  end
230
275
 
231
276
  require "boxcars/train/train_action"
232
277
  require "boxcars/train/train_finish"
233
278
  require "boxcars/train/zero_shot"
279
+ require "boxcars/train/xml_train"
280
+ 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.3.1"
5
+ VERSION = "0.3.3"
6
6
  end
@@ -0,0 +1,79 @@
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.values.to_h { |a| [a.name.to_sym, a.value] }
14
+
15
+ node.children.each do |child|
16
+ next if child.text?
17
+
18
+ child_node = XNode.new(child)
19
+ if @children[child.name].nil?
20
+ @valid_names << child.name.to_sym
21
+ @children[child.name] = child_node
22
+ elsif @children[child.name].is_a?(Array)
23
+ @children[child.name] << child_node
24
+ else
25
+ @children[child.name] = [@children[child.name], child_node]
26
+ end
27
+ end
28
+ end
29
+
30
+ def self.from_xml(xml)
31
+ doc = Nokogiri::XML.parse(xml)
32
+ if doc.errors.any?
33
+ Boxcars.debug("XML: #{xml}", :yellow)
34
+ # rubocop:disable Lint/Debugger
35
+ debugger if ENV.fetch("DEBUG_XML", false)
36
+ # rubocop:enable Lint/Debugger
37
+ raise XmlError, "XML is not valid: #{doc.errors.map { |e| "#{e.line}:#{e.column} #{e.message}" }}"
38
+ end
39
+ XNode.new(doc.root)
40
+ end
41
+
42
+ def xml
43
+ @node.to_xml
44
+ end
45
+
46
+ def text
47
+ @node.text
48
+ end
49
+
50
+ def xpath(path)
51
+ @node.xpath(path)
52
+ end
53
+
54
+ def xtext(path)
55
+ rv = xpath(path)&.text&.gsub(/[[:space:]]+/, " ")&.strip
56
+ return nil if rv.empty?
57
+
58
+ rv
59
+ end
60
+
61
+ def stext
62
+ @stext ||= text.gsub(/[[:space:]]+/, " ").strip # remove extra spaces
63
+ end
64
+
65
+ def [](key)
66
+ @children[key.to_s]
67
+ end
68
+
69
+ def method_missing(name, *args)
70
+ return @children[name.to_s] if @children.key?(name.to_s)
71
+
72
+ super
73
+ end
74
+
75
+ def respond_to_missing?(method_name, include_private = false)
76
+ @valid_names.include?(method) || super
77
+ end
78
+ end
79
+ 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.3.1
4
+ version: 0.3.3
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-07-01 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
@@ -129,6 +144,8 @@ files:
129
144
  - lib/boxcars/train.rb
130
145
  - lib/boxcars/train/train_action.rb
131
146
  - lib/boxcars/train/train_finish.rb
147
+ - lib/boxcars/train/xml_train.rb
148
+ - lib/boxcars/train/xml_zero_shot.rb
132
149
  - lib/boxcars/train/zero_shot.rb
133
150
  - lib/boxcars/vector_search.rb
134
151
  - lib/boxcars/vector_store.rb
@@ -148,6 +165,7 @@ files:
148
165
  - lib/boxcars/vector_store/pgvector/search.rb
149
166
  - lib/boxcars/vector_store/split_text.rb
150
167
  - lib/boxcars/version.rb
168
+ - lib/boxcars/x_node.rb
151
169
  homepage: https://github.com/BoxcarsAI/boxcars
152
170
  licenses:
153
171
  - MIT