boxcars 0.4.5 → 0.4.6

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: 10f7d544a53712e622028ebb6dc8e52c9dec9497f6e4436ef0ab456b27885c1e
4
- data.tar.gz: 44e91be763215a67dec30899f9155b59b43397c225afe3a7e5677b888bc39056
3
+ metadata.gz: d6979baf9aa7c8dfb0c4f852b72ec597d92339531aa9d61055530ca73065965e
4
+ data.tar.gz: 8fdb73c699d0524d64a5f340adfd25f893402739e8598c16e8a5f1c402569d56
5
5
  SHA512:
6
- metadata.gz: ac6c9bd9ff37d2c7ead36371f26f968fdce01ee5f5cfdcc1c9f876d6a879680c10faef74a285ee0fc1e8a1c23656ebe7c650be7d6d04370884566cc09942aa32
7
- data.tar.gz: 58aaf3c88912de6cd2b6746a7a2b990f05227c9c9369e4fd3b64811f5ed479dc86608c7a60d8f9ce80cd1d26daefa565a6a3d925091190b4d36acfef4b344fc0
6
+ metadata.gz: c595f68c3e29cefe105a93e9e95889d8448d66229f03ae3ca3ddfa3bbb62325dcaea240929344c09b3b6e8911068c06d9d0367a07b78f574e2854019ac0291d0
7
+ data.tar.gz: 34a2ef8446c0c05179fd51de46ca6fe31d0629aed3115bef25d31f6e6285913315918f4872e2f06af7fbda6b07beaf0da1fd940a20d4a94f5a526a85b92f2204
@@ -43,7 +43,10 @@ module Boxcars
43
43
  # @param engine_output [String] The output from the engine.
44
44
  # @return [Result] The result.
45
45
  def get_answer(engine_output)
46
- extract_answer(JSON.parse(engine_output))
46
+ # sometimes the LLM adds text in front of the JSON output, so let's strip it here
47
+ json_start = engine_output.index("{")
48
+ json_end = engine_output.rindex("}")
49
+ extract_answer(JSON.parse(engine_output[json_start..json_end]))
47
50
  rescue StandardError => e
48
51
  Result.from_error("Error: #{e.message}:\n#{engine_output}")
49
52
  end
@@ -138,13 +138,6 @@ module Boxcars
138
138
  end
139
139
  # rubocop:enable Metrics/AbcSize
140
140
 
141
- # the identifying parameters for the engine
142
- def identifying_params
143
- params = { model_name: model_name }
144
- params.merge!(default_params)
145
- params
146
- end
147
-
148
141
  # the engine type
149
142
  def engine_type
150
143
  "claude"
@@ -83,13 +83,6 @@ module Boxcars
83
83
  answer
84
84
  end
85
85
 
86
- # Build extra kwargs from additional params that were passed in.
87
- # @param values [Hash] The values to build extra kwargs from.
88
- def build_extra(values:)
89
- values[:model_kw_args] = @open_ai_params.merge(values)
90
- values
91
- end
92
-
93
86
  # Get the default parameters for the engine.
94
87
  def default_params
95
88
  open_ai_params
@@ -163,13 +156,6 @@ module Boxcars
163
156
  # rubocop:enable Metrics/AbcSize
164
157
  end
165
158
 
166
- # the identifying parameters for the engine
167
- def identifying_params
168
- params = { model_name: model_name }
169
- params.merge!(default_params)
170
- params
171
- end
172
-
173
159
  # the engine type
174
160
  def engine_type
175
161
  "openai"
@@ -0,0 +1,196 @@
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 engine that uses OpenAI's API.
6
+ class Perplexityai < Engine
7
+ attr_reader :prompts, :perplexity_params, :model_kwargs, :batch_size
8
+
9
+ # The default parameters to use when asking the engine.
10
+ DEFAULT_PER_PARAMS = {
11
+ model: "llama-2-70b-chat",
12
+ temperature: 0.1,
13
+ max_tokens: 3200
14
+ }.freeze
15
+
16
+ # the default name of the engine
17
+ DEFAULT_PER_NAME = "PerplexityAI engine"
18
+ # the default description of the engine
19
+ DEFAULT_PER_DESCRIPTION = "useful for when you need to use AI to answer questions. " \
20
+ "You should ask targeted questions"
21
+
22
+ # A engine is a container for a single tool to run.
23
+ # @param name [String] The name of the engine. Defaults to "PerplexityAI engine".
24
+ # @param description [String] A description of the engine. Defaults to:
25
+ # useful for when you need to use AI to answer questions. You should ask targeted questions".
26
+ # @param prompts [Array<String>] The prompts to use when asking the engine. Defaults to [].
27
+ # @param batch_size [Integer] The number of prompts to send to the engine at once. Defaults to 20.
28
+ def initialize(name: DEFAULT_PER_NAME, description: DEFAULT_PER_DESCRIPTION, prompts: [], batch_size: 20, **kwargs)
29
+ @perplexity_params = DEFAULT_PER_PARAMS.merge(kwargs)
30
+ @prompts = prompts
31
+ @batch_size = batch_size
32
+ super(description: description, name: name)
33
+ end
34
+
35
+ def conversation_model?(model)
36
+ ["mistral-7b-instruct", "llama-2-13b-chat", "llama-2-70b-chat", "openhermes-2-mistral-7b"].include?(model)
37
+ end
38
+
39
+ def chat(parameters:)
40
+ url = URI("https://api.perplexity.ai/chat/completions")
41
+
42
+ http = Net::HTTP.new(url.host, url.port)
43
+ http.use_ssl = true
44
+
45
+ request = Net::HTTP::Post.new(url)
46
+ request["accept"] = 'application/json'
47
+ request["authorization"] = "Bearer #{ENV.fetch('PERPLEXITY_API_KEY')}"
48
+ request["content-type"] = 'application/json'
49
+ the_body = {
50
+ model: (parameters[:model] || "mistral-7b-instruct"),
51
+ messages: parameters[:messages]
52
+ }
53
+ request.body = the_body.to_json
54
+
55
+ response = http.request(request)
56
+ JSON.parse(response.read_body)
57
+ end
58
+
59
+ # Get an answer from the engine.
60
+ # @param prompt [String] The prompt to use when asking the engine.
61
+ # @param openai_access_token [String] The access token to use when asking the engine.
62
+ # Defaults to Boxcars.configuration.openai_access_token.
63
+ # @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
64
+ def client(prompt:, inputs: {}, **kwargs)
65
+ prompt = prompt.first if prompt.is_a?(Array)
66
+ params = prompt.as_messages(inputs).merge(default_params).merge(kwargs)
67
+ params[:model] ||= "llama-2-70b-chat"
68
+ if Boxcars.configuration.log_prompts
69
+ Boxcars.debug(params[:messages].last(2).map { |p| ">>>>>> Role: #{p[:role]} <<<<<<\n#{p[:content]}" }.join("\n"), :cyan)
70
+ end
71
+ chat(parameters: params)
72
+ end
73
+
74
+ # get an answer from the engine for a question.
75
+ # @param question [String] The question to ask the engine.
76
+ # @param kwargs [Hash] Additional parameters to pass to the engine if wanted.
77
+ def run(question, **kwargs)
78
+ prompt = Prompt.new(template: question)
79
+ response = client(prompt: prompt, **kwargs)
80
+ raise Error, "PerplexityAI: No response from API" unless response
81
+ raise Error, "PerplexityAI: #{response['error']}" if response["error"]
82
+
83
+ answer = response["choices"].map { |c| c.dig("message", "content") || c["text"] }.join("\n").strip
84
+ puts answer
85
+ answer
86
+ end
87
+
88
+ # Get the default parameters for the engine.
89
+ def default_params
90
+ perplexity_params
91
+ end
92
+
93
+ # Get generation informaton
94
+ # @param sub_choices [Array<Hash>] The choices to get generation info for.
95
+ # @return [Array<Generation>] The generation information.
96
+ def generation_info(sub_choices)
97
+ sub_choices.map do |choice|
98
+ Generation.new(
99
+ text: choice.dig("message", "content") || choice["text"],
100
+ generation_info: {
101
+ finish_reason: choice.fetch("finish_reason", nil),
102
+ logprobs: choice.fetch("logprobs", nil)
103
+ }
104
+ )
105
+ end
106
+ end
107
+
108
+ # make sure we got a valid response
109
+ # @param response [Hash] The response to check.
110
+ # @param must_haves [Array<String>] The keys that must be in the response. Defaults to %w[choices].
111
+ # @raise [KeyError] if there is an issue with the access token.
112
+ # @raise [ValueError] if the response is not valid.
113
+ def check_response(response, must_haves: %w[choices])
114
+ if response['error']
115
+ code = response.dig('error', 'code')
116
+ msg = response.dig('error', 'message') || 'unknown error'
117
+ raise KeyError, "PERPLEXITY_API_KEY not valid" if code == 'invalid_api_key'
118
+
119
+ raise ValueError, "PerplexityAI error: #{msg}"
120
+ end
121
+
122
+ must_haves.each do |key|
123
+ raise ValueError, "Expecting key #{key} in response" unless response.key?(key)
124
+ end
125
+ end
126
+
127
+ # Call out to OpenAI's endpoint with k unique prompts.
128
+ # @param prompts [Array<String>] The prompts to pass into the model.
129
+ # @param inputs [Array<String>] The inputs to subsitite into the prompt.
130
+ # @param stop [Array<String>] Optional list of stop words to use when generating.
131
+ # @return [EngineResult] The full engine output.
132
+ def generate(prompts:, stop: nil)
133
+ params = {}
134
+ params[:stop] = stop if stop
135
+ choices = []
136
+ token_usage = {}
137
+ # Get the token usage from the response.
138
+ # Includes prompt, completion, and total tokens used.
139
+ inkeys = %w[completion_tokens prompt_tokens total_tokens].freeze
140
+ prompts.each_slice(batch_size) do |sub_prompts|
141
+ sub_prompts.each do |sprompts, inputs|
142
+ response = client(prompt: sprompts, inputs: inputs, **params)
143
+ check_response(response)
144
+ choices.concat(response["choices"])
145
+ usage_keys = inkeys & response["usage"].keys
146
+ usage_keys.each { |key| token_usage[key] = token_usage[key].to_i + response["usage"][key] }
147
+ end
148
+ end
149
+
150
+ n = params.fetch(:n, 1)
151
+ generations = []
152
+ prompts.each_with_index do |_prompt, i|
153
+ sub_choices = choices[i * n, (i + 1) * n]
154
+ generations.push(generation_info(sub_choices))
155
+ end
156
+ EngineResult.new(generations: generations, engine_output: { token_usage: token_usage })
157
+ end
158
+ # rubocop:enable Metrics/AbcSize
159
+ end
160
+
161
+ # the engine type
162
+ def engine_type
163
+ "perplexityai"
164
+ end
165
+
166
+ # calculate the number of tokens used
167
+ def get_num_tokens(text:)
168
+ text.split.length # TODO: hook up to token counting gem
169
+ end
170
+
171
+ # lookup the context size for a model by name
172
+ # @param modelname [String] The name of the model to lookup.
173
+ def modelname_to_contextsize(modelname)
174
+ model_lookup = {
175
+ 'text-davinci-003': 4097,
176
+ 'text-curie-001': 2048,
177
+ 'text-babbage-001': 2048,
178
+ 'text-ada-001': 2048,
179
+ 'code-davinci-002': 8000,
180
+ 'code-cushman-001': 2048,
181
+ 'gpt-3.5-turbo-1': 4096
182
+ }.freeze
183
+ model_lookup[modelname] || 4097
184
+ end
185
+
186
+ # Calculate the maximum number of tokens possible to generate for a prompt.
187
+ # @param prompt_text [String] The prompt text to use.
188
+ # @return [Integer] the number of tokens possible to generate.
189
+ def max_tokens_for_prompt(prompt_text)
190
+ num_tokens = get_num_tokens(prompt_text)
191
+
192
+ # get max context size for model by name
193
+ max_size = modelname_to_contextsize(model_name)
194
+ max_size - num_tokens
195
+ end
196
+ end
@@ -22,4 +22,5 @@ end
22
22
  require "boxcars/engine/engine_result"
23
23
  require "boxcars/engine/anthropic"
24
24
  require "boxcars/engine/openai"
25
+ require "boxcars/engine/perplexityai"
25
26
  require "boxcars/engine/gpt4all_eng"
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Boxcars
4
4
  # The current version of the gem.
5
- VERSION = "0.4.5"
5
+ VERSION = "0.4.6"
6
6
  end
@@ -29,6 +29,7 @@ module Boxcars
29
29
 
30
30
  def self.from_xml(xml)
31
31
  xml = xml[xml.index("<")..-1] unless xml.start_with?("<")
32
+ xml = xml[0..xml.rindex(">")] unless xml.end_with?(">")
32
33
  doc = Nokogiri::XML.parse(xml)
33
34
  if doc.errors.any?
34
35
  Boxcars.debug("XML: #{xml}", :yellow)
@@ -0,0 +1,28 @@
1
+ require "debug"
2
+ require "dotenv/load"
3
+ require "boxcars"
4
+
5
+ # Boxcars.configuration.logger = Logger.new($stdout)
6
+
7
+ eng = Boxcars::Perplexityai.new
8
+ # eng = Boxcars::Openai.new(model: "gpt-4")
9
+ ctemplate = [
10
+ Boxcars::Boxcar.syst("The user will type in a city name. Your job is to evaluate if the given city is a good place to live. " \
11
+ "Build a comprehensive report about livability, weather, cost of living, crime rate, drivability, " \
12
+ "walkability, and bike ability, and direct flights. In the final answer, for the first paragraph, " \
13
+ "summarize the pros and cons of living in the city followed by the background information and links " \
14
+ "for the research. Finalize your answer with an overall grade from A to F on the city."),
15
+ Boxcars::Boxcar.user("%<input>s")
16
+ ]
17
+ conv = Boxcars::Conversation.new(lines: ctemplate)
18
+
19
+ conversation_prompt = Boxcars::ConversationPrompt.new(conversation: conv, input_variables: [:input], other_inputs: [],
20
+ output_variables: [:answer])
21
+
22
+ boxcar = Boxcars::EngineBoxcar.new(engine: eng, name: "City Helper", prompt: conversation_prompt,
23
+ description: "Evaluate if a city is a good place to live.")
24
+ data = boxcar.run(ARGV.fetch(0, "San Francisco"))
25
+ # train = Boxcars.train.new(boxcars: [boxcar])
26
+ # data = train.run()
27
+ # debugger
28
+ puts data
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.4.5
4
+ version: 0.4.6
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-10-06 00:00:00.000000000 Z
12
+ date: 2023-11-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: anthropic
@@ -154,6 +154,7 @@ files:
154
154
  - lib/boxcars/engine/engine_result.rb
155
155
  - lib/boxcars/engine/gpt4all_eng.rb
156
156
  - lib/boxcars/engine/openai.rb
157
+ - lib/boxcars/engine/perplexityai.rb
157
158
  - lib/boxcars/generation.rb
158
159
  - lib/boxcars/observation.rb
159
160
  - lib/boxcars/prompt.rb
@@ -184,6 +185,7 @@ files:
184
185
  - lib/boxcars/vector_store/split_text.rb
185
186
  - lib/boxcars/version.rb
186
187
  - lib/boxcars/x_node.rb
188
+ - perplexity_example.rb
187
189
  - run.json
188
190
  homepage: https://github.com/BoxcarsAI/boxcars
189
191
  licenses: