boxcars 0.3.1 → 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: a2448419a2e348f8111fa19bd7b9bb1a05ab19b30d3dc9c0255ae0bd8673c156
4
- data.tar.gz: fd6577be64b72941a87cbdb8c1c1ca31a7c88383869fa67f86d21cf721749168
3
+ metadata.gz: 463f57f1436cfea29e0a60bbfce071afd355a5d0cadbe1d69f96adc7393eacd6
4
+ data.tar.gz: 282e528fb8cb8b532b621db5c2e7ce2b82c4607057c01f0feb1eaacfe98ba09b
5
5
  SHA512:
6
- metadata.gz: fd21eb8cd3ea3cc2fdf29140a3dce7dec26a7e4ffbd791d7aa7dd63aaa267c34d4ead764f85d3f7dbb9d1038330e3614bbfaf7f8c34818544b0d282568ad66f5
7
- data.tar.gz: e12626132060202679533c3a101f4ec4fb1c9846f7d54beb9bc84ce7d71cb4f56be10f16a2c229480cc6c4378a074566f26a7ccbeeadc0c7ffc27ab81dfc6ee4
6
+ metadata.gz: c76d2772db0925f71779c0bac1a2e92af20bcf398cc6e7cf943edeadb2014426554fba607b034fd68ac88bd26f2d42eef114de57332e83a4fe76558a9b2f0cea
7
+ data.tar.gz: '0489fffef87c32fbf6a7a9af4fbca77ef991437353211174617b6ea8e229e49accc85c39af93414ce1628a4472cc208d8075570abba5e8269b30cfb66000a40e'
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.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
 
@@ -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,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.
@@ -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
@@ -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,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
@@ -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.
@@ -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
@@ -225,9 +225,46 @@ module Boxcars
225
225
  output = return_stopped_response(early_stopping_method, intermediate_steps, **inputs)
226
226
  pre_return(output, intermediate_steps)
227
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
228
263
  end
229
264
  end
230
265
 
231
266
  require "boxcars/train/train_action"
232
267
  require "boxcars/train/train_finish"
233
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.3.1"
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.3.1
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-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