boxcars 0.2.15 → 0.3.1

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