boxcars 0.4.5 → 0.4.6

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: 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: