asimov 0.1.0 → 1.0.0

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: c50684e3e690496248b87a912334da4b3ace87c83dec1c05e2d8b33349c5d16d
4
- data.tar.gz: 6ab91367f32fa02d4582018fd6cad490ba4e78db92c86e231b64323a3389d88f
3
+ metadata.gz: b71726c0c67788a649dc1c01ca5763ab13d41f31118d6f8b093795a3a92e0ca8
4
+ data.tar.gz: 5d5eb6dd51099aadf9418ff1c11c7ea6688a06a5e07b5a39bd376a48538ac79a
5
5
  SHA512:
6
- metadata.gz: 859bbbfccff90fdddac6708846bc03b2a6f175bf83fd7ad9329a3de86d8830444e34d822eeeb65d4ba054678b1cda15312b85ca52b4dea2e43dc83cc2554e908
7
- data.tar.gz: 169621c5fe064acc1ba293a01da9473c75aae65f7a650139c377319990140f2295fdae39d5b231fae7f3df7a506224d99e9f7d64acfb46af6967841ec46072c5
6
+ metadata.gz: b20449b89964c2b2b77de68726aef47bc09e65871ba04c80ad2f607e58bc5b6322ff6106c801f01ef68377056a55010d74e6481f883ad57793eca5366a438acd
7
+ data.tar.gz: fdfe13acb265341e7c568874a8696ce520b6da8fbdc77ae694b3ba56deb97d44a65aac82935a3ad3f356258aa0375358349540c17cd54bfd0f9a3a746d40b807
data/CHANGELOG.md CHANGED
@@ -5,9 +5,35 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## Unreleased
9
+
10
+ ## [1.0.0] - 2023-01-24
11
+
12
+ This version has complete coverage of the OpenAI API (except for stream: true behavior), has
13
+ no known errors and has full test coverage. At this point there are no anticipated changes
14
+ to existing endpoints.
15
+
16
+ ### Fixed
17
+
18
+ - Fixed handling of authentication errors
19
+
20
+ ### Changed
21
+
22
+ - Properly distinguished public and private methods to ensure proper documentation.
23
+ - Renamed events to list_events for consistency
24
+ - Updated file arguments to take path strings or File-like objects
25
+ - Adjusted endpoints to make required parameters more explicit
26
+
27
+ ### Added
28
+
29
+ - Code level documentation for all public classes and methods.
30
+ - Error for the unsupported stream: true case
31
+ - Error mapping for 409 and 429 errors
32
+ - Specs for authentication
33
+
8
34
  ## [0.1.0] - 2023-01-14
9
35
 
10
- This initial version of the gem is now suitable for use. API endpoint method naming may shift slightly,
36
+ This initial version of the gem is now suitable for use. API endpoint method naming may shift slightly.
11
37
 
12
38
  ### Changed
13
39
 
data/Gemfile CHANGED
@@ -2,4 +2,13 @@ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
- gem "simplecov", require: false, group: :test
5
+ group :development, :test do
6
+ gem "rake", "~> 13.0"
7
+ gem "rspec", "~> 3.12"
8
+ gem "rubocop", "~> 1.44"
9
+ gem "rubocop-rake"
10
+ gem "rubocop-rspec"
11
+ gem "simplecov", require: false
12
+ gem "vcr", "~> 6.1.0"
13
+ gem "webmock", "~> 3.18.1"
14
+ end
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # Asimov
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/asimov.svg)](https://badge.fury.io/rb/asimov)
3
4
  [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/petergoldstein/asimov/blob/main/LICENSE.txt)
4
5
  [![Tests](https://github.com/petergoldstein/asimov/actions/workflows/ci.yml/badge.svg)](https://github.com/petergoldstein/asimov/actions/workflows/ci.yml)
5
6
 
@@ -8,26 +8,47 @@ module Asimov
8
8
  # fragile.
9
9
  ##
10
10
  class ApiErrorTranslator
11
+ ##
12
+ # This method raises an appropriate Asimov::RequestError
13
+ # subclass if the response corresponds to an HTTP error.
14
+ #
15
+ # @param [HTTParty::Response] resp the response (or fragment) object
16
+ # encapsulating the server response.
17
+ ##
11
18
  def self.translate(resp)
19
+ return if resp.code == 200
20
+
12
21
  match_400(resp)
13
22
  match_401(resp)
14
23
  match_404(resp)
24
+ match_409(resp)
25
+ match_429(resp)
26
+
27
+ raise Asimov::RequestError, error_message(resp)
15
28
  end
16
29
 
17
30
  # rubocop:disable Naming/VariableNumber
18
31
  # rubocop:disable Metrics/MethodLength
32
+
33
+ # Prefix for OpenAI error message when an invalid key is provided.
19
34
  INVALID_API_KEY_PREFIX = "Incorrect API key provided: ".freeze
35
+
36
+ # Prefix for OpenAI error message when an organization cannot be found.
20
37
  INVALID_ORGANIZATION_PREFIX = "No such organization: ".freeze
21
38
  def self.match_401(resp)
22
39
  return unless resp.code == 401
23
40
 
24
41
  msg = error_message(resp)
25
42
  raise Asimov::InvalidApiKeyError, msg if msg.start_with?(INVALID_API_KEY_PREFIX)
26
- raise Asimov::InvalidOrganizationError, msg if msg.start_with?(INVALID_API_KEY_PREFIX)
43
+ raise Asimov::InvalidOrganizationError, msg if msg.start_with?(INVALID_ORGANIZATION_PREFIX)
27
44
 
28
- raise Asimov::UnauthorizedError
45
+ raise Asimov::AuthorizationError
29
46
  end
47
+ private_class_method :match_401
48
+ private_constant :INVALID_API_KEY_PREFIX
49
+ private_constant :INVALID_ORGANIZATION_PREFIX
30
50
 
51
+ # Prefix for OpenAI error message when training file format cannot be validated.
31
52
  INVALID_TRAINING_EXAMPLE_PREFIX = "Expected file to have JSONL format with " \
32
53
  "prompt/completion keys. Missing".freeze
33
54
  ADDITIONAL_PROPERTIES_ERROR_PREFIX = "Additional properties are not allowed".freeze
@@ -39,28 +60,6 @@ module Asimov
39
60
  return unless resp.code == 400
40
61
 
41
62
  msg = error_message(resp)
42
- # 400
43
- # {"error"=>{"message"=>"'moose' is not one of ['fine-tune', 'answers', 'search', " \
44
- # "'classifications'] - 'purpose'", "type"=>"invalid_request_error",
45
- # "param"=>nil, "code"=>nil}}
46
- # {"error"=>{"code"=>nil, "message"=>"'8x8' is not one of ['256x256', '512x512', " \
47
- # "'1024x1024'] - 'size'", "param"=>nil,
48
- # "type"=>"invalid_request_error"}}
49
- # {"error"=>{"message"=>"Incorrect format for purpose=classifications. Please check " \
50
- # "the openai documentation and try again",
51
- # "type"=>"invalid_request_error", "param"=>nil, "code"=>nil}}
52
- # {"error"=>{"message"=>"Expected file to have the JSONL format with 'text' key " \
53
- # "and (optional) 'metadata' key.",
54
- # "type"=>"invalid_request_error", "param"=>nil, "code"=>nil}}
55
- # {"error"=>{"message"=>"Additional properties are not allowed ('moose' was unexpected)",
56
- # "type"=>"invalid_request_error", "param"=>nil, "code"=>nil}}
57
- # {"error"=>{"message"=>"Additional properties are not allowed ('moose', 'squirrel' were " \
58
- # "unexpected)",
59
- # "type"=>"invalid_request_error", "param"=>nil, "code"=>nil}}
60
- # {"error"=>{"code"=>nil, "message"=>"-1 is less than the minimum of 1 - 'n'",
61
- # "param"=>nil, "type"=>"invalid_request_error"}}
62
- # {"error"=>{"code"=>nil, "message"=>"20 is greater than the maximum of 10 - 'n'",
63
- # "param"=>nil, "type"=>"invalid_request_error"}}
64
63
 
65
64
  if msg.start_with?(INVALID_TRAINING_EXAMPLE_PREFIX)
66
65
  raise Asimov::InvalidTrainingExampleError,
@@ -78,6 +77,50 @@ module Asimov
78
77
 
79
78
  raise Asimov::RequestError, msg
80
79
  end
80
+ private_class_method :match_400
81
+ private_constant :INVALID_TRAINING_EXAMPLE_PREFIX
82
+ private_constant :ADDITIONAL_PROPERTIES_ERROR_PREFIX
83
+ private_constant :INVALID_PARAMETER_VALUE_STRING
84
+ private_constant :INVALID_PARAMETER_VALUE_PREFIX_2
85
+ private_constant :BELOW_MINIMUM_STRING
86
+ private_constant :ABOVE_MAXIMUM_STRING
87
+
88
+ def self.match_409(resp)
89
+ return unless resp.code == 409
90
+
91
+ raise Asimov::RequestError, error_message(resp)
92
+ end
93
+ private_class_method :match_409
94
+
95
+ QUOTA_EXCEEDED_MESSAGE = "You exceeded your current quota".freeze
96
+ RATE_LIMIT_REACHED_MESSAGE = "Rate limit reached".freeze
97
+ ENGINE_OVERLOADED_MESSAGE = "The engine is currently overloaded".freeze
98
+ def self.match_429(resp)
99
+ return unless resp.code == 429
100
+
101
+ msg = error_message(resp)
102
+
103
+ if msg.start_with?(QUOTA_EXCEEDED_MESSAGE)
104
+ raise Asimov::QuotaExceededError,
105
+ msg
106
+ end
107
+
108
+ if msg.start_with?(RATE_LIMIT_REACHED_MESSAGE)
109
+ raise Asimov::RateLimitError,
110
+ msg
111
+ end
112
+
113
+ if msg.start_with?(ENGINE_OVERLOADED_MESSAGE)
114
+ raise Asimov::ApiOverloadedError,
115
+ msg
116
+ end
117
+
118
+ raise Asimov::TooManyRequestsError, msg
119
+ end
120
+ private_class_method :match_429
121
+ private_constant :QUOTA_EXCEEDED_MESSAGE
122
+ private_constant :RATE_LIMIT_REACHED_MESSAGE
123
+ private_constant :ENGINE_OVERLOADED_MESSAGE
81
124
 
82
125
  def self.match_invalid_parameter_value?(msg)
83
126
  msg.include?(INVALID_PARAMETER_VALUE_STRING) ||
@@ -85,17 +128,15 @@ module Asimov
85
128
  msg.include?(ABOVE_MAXIMUM_STRING) ||
86
129
  msg.start_with?(INVALID_PARAMETER_VALUE_PREFIX_2)
87
130
  end
131
+ private_class_method :match_invalid_parameter_value?
88
132
 
89
133
  def self.match_404(resp)
90
134
  return unless resp.code == 404
91
135
 
92
136
  msg = error_message(resp)
93
- # {"error"=>{"message"=>"That model does not exist", "type"=>"invalid_request_error",
94
- # "param"=>"model", "code"=>nil}}
95
- # {"error"=>{"message"=>"No such File object: file-BWp1k9EVJRq5Ybjr3Mb0tDXW",
96
- # "type"=>"invalid_request_error", "param"=>"id", "code"=>nil}}
97
137
  raise Asimov::NotFoundError, msg
98
138
  end
139
+ private_class_method :match_404
99
140
 
100
141
  # rubocop:enable Naming/VariableNumber
101
142
  # rubocop:enable Metrics/MethodLength
@@ -110,6 +151,7 @@ module Asimov
110
151
 
111
152
  pr["error"]["message"] || ""
112
153
  end
154
+ private_class_method :error_message
113
155
  end
114
156
  end
115
157
  end
@@ -2,6 +2,10 @@ require_relative "./api_error_translator"
2
2
  require_relative "./network_error_translator"
3
3
 
4
4
  module Asimov
5
+ ##
6
+ # Classes and method associated with the requests, responses, and
7
+ # errors associated with v1 of the OpenAI API.
8
+ ##
5
9
  module ApiV1
6
10
  ##
7
11
  # Base class for API interface implementations. Currently
@@ -18,6 +22,12 @@ module Asimov
18
22
  end
19
23
  def_delegators :@client, :headers, :request_options
20
24
 
25
+ ##
26
+ # Executes an HTTP DELETE on the specified path.
27
+ #
28
+ # @param [String] path the URI (when combined with the
29
+ # base_uri) against which the DELETE is executed.
30
+ ##
21
31
  def http_delete(path:)
22
32
  wrap_response_with_error_handling do
23
33
  self.class.delete(
@@ -27,6 +37,12 @@ module Asimov
27
37
  end
28
38
  end
29
39
 
40
+ ##
41
+ # Executes an HTTP GET on the specified path.
42
+ #
43
+ # @param [String] path the URI (when combined with the
44
+ # base_uri) against which the GET is executed.
45
+ ##
30
46
  def http_get(path:)
31
47
  wrap_response_with_error_handling do
32
48
  self.class.get(
@@ -36,6 +52,12 @@ module Asimov
36
52
  end
37
53
  end
38
54
 
55
+ ##
56
+ # Executes an HTTP POST with JSON-encoded parameters on the specified path.
57
+ #
58
+ # @param [String] path the URI (when combined with the
59
+ # base_uri) against which the POST is executed.
60
+ ##
39
61
  def json_post(path:, parameters:)
40
62
  wrap_response_with_error_handling do
41
63
  self.class.post(
@@ -46,6 +68,12 @@ module Asimov
46
68
  end
47
69
  end
48
70
 
71
+ ##
72
+ # Executes an HTTP POST with multipart encoded parameters on the specified path.
73
+ #
74
+ # @param [String] path the URI (when combined with the
75
+ # base_uri) against which the POST is executed.
76
+ ##
49
77
  def multipart_post(path:, parameters: nil)
50
78
  wrap_response_with_error_handling do
51
79
  self.class.post(
@@ -56,20 +84,27 @@ module Asimov
56
84
  end
57
85
  end
58
86
 
87
+ ##
88
+ # Executes an HTTP GET on the specified path, streaming the resulting body
89
+ # to the writer in case of success.
90
+ #
91
+ # @param [String] path the URI (when combined with the
92
+ # base_uri) against which the POST is executed.
93
+ # @param [Writer] writer an object, typically a File, that responds to a `write` method
94
+ ##
59
95
  def http_streamed_download(path:, writer:)
60
96
  self.class.get(path,
61
97
  { headers: headers,
62
98
  stream_body: true }.merge!(request_options)) do |fragment|
63
99
  fragment.code == 200 ? writer.write(fragment) : check_for_api_error(fragment)
64
100
  end
65
- rescue Asimov::RequestError => e
66
- # Raise any translated API errors
67
- raise e
68
101
  rescue StandardError => e
69
- # Otherwise translate the error to a network error
102
+ # Any error raised by the HTTP call is a network error
70
103
  NetworkErrorTranslator.translate(e)
71
104
  end
72
105
 
106
+ private
107
+
73
108
  def wrap_response_with_error_handling
74
109
  resp = begin
75
110
  yield
@@ -84,7 +119,6 @@ module Asimov
84
119
  return if resp.code == 200
85
120
 
86
121
  ApiErrorTranslator.translate(resp)
87
- resp
88
122
  end
89
123
  end
90
124
  end
@@ -4,10 +4,17 @@ module Asimov
4
4
  # Class interface for API methods in the "/completions" URI subspace.
5
5
  ##
6
6
  class Completions < Base
7
- def create(parameters:)
8
- raise MissingRequiredParameterError.new(:model) unless parameters[:model]
7
+ ##
8
+ # Calls the /completions POST endpoint with the specified parameters.
9
+ #
10
+ # @param [String] model the model to use for the completion
11
+ # @param [Hash] parameters the set of parameters being passed to the API
12
+ ##
13
+ def create(model:, parameters: {})
14
+ raise MissingRequiredParameterError.new(:model) unless model
15
+ raise StreamingResponseNotSupportedError if parameters[:stream]
9
16
 
10
- json_post(path: "/completions", parameters: parameters)
17
+ json_post(path: "/completions", parameters: parameters.merge({ model: model }))
11
18
  end
12
19
  end
13
20
  end
@@ -4,11 +4,18 @@ module Asimov
4
4
  # Class interface for API methods in the "/edits" URI subspace.
5
5
  ##
6
6
  class Edits < Base
7
- def create(parameters:)
8
- raise MissingRequiredParameterError.new(:model) unless parameters[:model]
9
- raise MissingRequiredParameterError.new(:instruction) unless parameters[:instruction]
7
+ ##
8
+ # Calls the /edits POST endpoint with the specified parameters.
9
+ #
10
+ # @param [Hash] parameters the set of parameters being passed to the API
11
+ ##
12
+ def create(model:, instruction:, parameters: {})
13
+ raise MissingRequiredParameterError.new(:model) unless model
14
+ raise MissingRequiredParameterError.new(:instruction) unless instruction
10
15
 
11
- json_post(path: "/edits", parameters: parameters)
16
+ json_post(path: "/edits",
17
+ parameters: parameters.merge({ model: model,
18
+ instruction: instruction }))
12
19
  end
13
20
  end
14
21
  end
@@ -4,11 +4,16 @@ module Asimov
4
4
  # Class interface for API methods in the "/embeddings" URI subspace.
5
5
  ##
6
6
  class Embeddings < Base
7
- def create(parameters:)
8
- raise MissingRequiredParameterError.new(:model) unless parameters[:model]
9
- raise MissingRequiredParameterError.new(:input) unless parameters[:input]
7
+ ##
8
+ # Calls the /embeddings POST endpoint with the specified parameters.
9
+ #
10
+ # @param [Hash] parameters the set of parameters being passed to the API
11
+ ##
12
+ def create(model:, input:, parameters: {})
13
+ raise MissingRequiredParameterError.new(:model) unless model
14
+ raise MissingRequiredParameterError.new(:input) unless input
10
15
 
11
- json_post(path: "/embeddings", parameters: parameters)
16
+ json_post(path: "/embeddings", parameters: parameters.merge({ model: model, input: input }))
12
17
  end
13
18
  end
14
19
  end
@@ -11,6 +11,7 @@ module Asimov
11
11
  ##
12
12
  class Files < Base
13
13
  URI_PREFIX = "/files".freeze
14
+ private_constant :URI_PREFIX
14
15
 
15
16
  ##
16
17
  # Lists files that have been uploaded to OpenAI
@@ -19,26 +20,50 @@ module Asimov
19
20
  http_get(path: URI_PREFIX)
20
21
  end
21
22
 
22
- def upload(parameters:)
23
- raise MissingRequiredParameterError.new(:file) unless parameters[:file]
24
- raise MissingRequiredParameterError.new(:purpose) unless parameters[:purpose]
23
+ ##
24
+ # Uploads a file to the /files endpoint with the specified parameters.
25
+ #
26
+ # @param [String] file file name or a File-like object to be uploaded
27
+ # @param [Hash] parameters the set of parameters being passed to the API
28
+ ##
29
+ def upload(file:, purpose:, parameters: {})
30
+ raise MissingRequiredParameterError.new(:file) unless file
31
+ raise MissingRequiredParameterError.new(:purpose) unless purpose
25
32
 
26
- validate(parameters[:file], parameters[:purpose])
33
+ validate(file, purpose)
27
34
 
28
35
  multipart_post(
29
36
  path: URI_PREFIX,
30
- parameters: parameters.merge(file: Utils::FileManager.open(parameters[:file]))
37
+ parameters: parameters.merge(file: Utils::FileManager.open(file), purpose: purpose)
31
38
  )
32
39
  end
33
40
 
41
+ ##
42
+ # Retrieves the file with the specified file_id from OpenAI.
43
+ #
44
+ # @param [String] file_id the id of the file to be retrieved
45
+ ##
34
46
  def retrieve(file_id:)
35
47
  http_get(path: "#{URI_PREFIX}/#{file_id}")
36
48
  end
37
49
 
50
+ ##
51
+ # Deletes the file with the specified file_id from OpenAI.
52
+ #
53
+ # @param [String] file_id the id of the file to be deleted
54
+ ##
38
55
  def delete(file_id:)
39
56
  http_delete(path: "#{URI_PREFIX}/#{file_id}")
40
57
  end
41
58
 
59
+ ##
60
+ # Retrieves the contents of the file with the specified file_id from OpenAI
61
+ # and passes those contents to the writer in a chunked manner.
62
+ #
63
+ # @param [String] file_id the id of the file to be retrieved
64
+ # @param [Writer] writer the Writer that will process the chunked content
65
+ # as it is received from the API
66
+ ##
42
67
  def content(file_id:, writer:)
43
68
  http_streamed_download(path: "#{URI_PREFIX}/#{file_id}/content", writer: writer)
44
69
  end
@@ -5,26 +5,51 @@ module Asimov
5
5
  ##
6
6
  class Finetunes < Base
7
7
  URI_PREFIX = "/fine-tunes".freeze
8
+ private_constant :URI_PREFIX
8
9
 
10
+ ##
11
+ # Lists the set of fine-tuning jobs for this API key and (optionally) organization.
12
+ ##
9
13
  def list
10
14
  http_get(path: URI_PREFIX)
11
15
  end
12
16
 
13
- def create(parameters:)
14
- raise MissingRequiredParameterError.new(:training_file) unless parameters[:training_file]
17
+ ##
18
+ # Creates a new fine-tuning job with the specified parameters.
19
+ #
20
+ # @param [String] training_file the id of the training file to use for fine tuning
21
+ # @param [Hash] parameters the parameters passed with the fine tuning job
22
+ ##
23
+ def create(training_file:, parameters: {})
24
+ raise MissingRequiredParameterError.new(:training_file) unless training_file
15
25
 
16
- json_post(path: URI_PREFIX, parameters: parameters)
26
+ json_post(path: URI_PREFIX, parameters: parameters.merge(training_file: training_file))
17
27
  end
18
28
 
29
+ ##
30
+ # Retrieves the details of a fine-tuning job with the specified id.
31
+ #
32
+ # @param [String] fine_tune_id the id of fine tuning job
33
+ ##
19
34
  def retrieve(fine_tune_id:)
20
35
  http_get(path: "#{URI_PREFIX}/#{fine_tune_id}")
21
36
  end
22
37
 
38
+ ##
39
+ # Cancels the details of a fine-tuning job with the specified id.
40
+ #
41
+ # @param [String] fine_tune_id the id of fine tuning job
42
+ ##
23
43
  def cancel(fine_tune_id:)
24
44
  multipart_post(path: "#{URI_PREFIX}/#{fine_tune_id}/cancel")
25
45
  end
26
46
 
27
- def events(fine_tune_id:)
47
+ ##
48
+ # Lists the events associated with a fine-tuning job with the specified id.
49
+ #
50
+ # @param [String] fine_tune_id the id of fine tuning job
51
+ ##
52
+ def list_events(fine_tune_id:)
28
53
  http_get(path: "#{URI_PREFIX}/#{fine_tune_id}/events")
29
54
  end
30
55
  end
@@ -7,21 +7,45 @@ module Asimov
7
7
  ##
8
8
  class Images < Base
9
9
  URI_PREFIX = "/images".freeze
10
-
11
- def create(parameters:)
12
- raise MissingRequiredParameterError.new(:prompt) unless parameters[:prompt]
13
-
14
- json_post(path: "#{URI_PREFIX}/generations", parameters: parameters)
10
+ private_constant :URI_PREFIX
11
+
12
+ ##
13
+ # Creates an image using the specified prompt.
14
+ #
15
+ # @param [String] prompt the prompt used to create the image
16
+ # @param [Hash] parameters additional parameters passed to the API
17
+ ##
18
+ def create(prompt:, parameters: {})
19
+ raise MissingRequiredParameterError.new(:prompt) unless prompt
20
+
21
+ json_post(path: "#{URI_PREFIX}/generations",
22
+ parameters: parameters.merge({ prompt: prompt }))
15
23
  end
16
24
 
17
- def create_edit(parameters:)
18
- raise MissingRequiredParameterError.new(:prompt) unless parameters[:prompt]
19
-
20
- multipart_post(path: "#{URI_PREFIX}/edits", parameters: open_files(parameters))
25
+ ##
26
+ # Creates edits of the specified image based on the prompt.
27
+ #
28
+ # @param [String] image file name or a File-like object of the base image
29
+ # @param [String] prompt the prompt used to guide the edit
30
+ # @param [Hash] parameters additional parameters passed to the API
31
+ ##
32
+ def create_edit(image:, prompt:, parameters: {})
33
+ raise MissingRequiredParameterError.new(:prompt) unless prompt
34
+
35
+ multipart_post(path: "#{URI_PREFIX}/edits",
36
+ parameters: open_files(parameters.merge({ image: image, prompt: prompt })))
21
37
  end
22
38
 
23
- def create_variation(parameters:)
24
- multipart_post(path: "#{URI_PREFIX}/variations", parameters: open_files(parameters))
39
+ ##
40
+ # Creates variations of the specified image.
41
+ #
42
+ # @param [String] image file name or a File-like object of the base image
43
+ # @param [Hash] parameters additional parameters passed to the API
44
+ # @option parameters [String] :mask mask file name or a File-like object
45
+ ##
46
+ def create_variation(image:, parameters: {})
47
+ multipart_post(path: "#{URI_PREFIX}/variations",
48
+ parameters: open_files(parameters.merge({ image: image })))
25
49
  end
26
50
 
27
51
  private
@@ -5,15 +5,32 @@ module Asimov
5
5
  ##
6
6
  class Models < Base
7
7
  URI_PREFIX = "/models".freeze
8
+ private_constant :URI_PREFIX
8
9
 
10
+ ##
11
+ # Lists the models accessible to this combination of OpenAI API
12
+ # key and organization id.
13
+ ##
9
14
  def list
10
15
  http_get(path: URI_PREFIX)
11
16
  end
12
17
 
18
+ ##
19
+ # Retrieve information about the model with the specified
20
+ # model_id.
21
+ #
22
+ # @param [String] model_id the id of the model to be retrieved
23
+ ##
13
24
  def retrieve(model_id:)
14
25
  http_get(path: "#{URI_PREFIX}/#{model_id}")
15
26
  end
16
27
 
28
+ ##
29
+ # Deletes the model with the specified model_id. Only
30
+ # works on models created via fine tuning.
31
+ #
32
+ # @param [String] model_id the id of the model to be deleted
33
+ ##
17
34
  def delete(model_id:)
18
35
  http_delete(path: "#{URI_PREFIX}/#{model_id}")
19
36
  end
@@ -4,10 +4,16 @@ module Asimov
4
4
  # Class interface for API methods in the "/moderations" URI subspace.
5
5
  ##
6
6
  class Moderations < Base
7
- def create(parameters:)
8
- raise MissingRequiredParameterError.new(:input) unless parameters[:input]
7
+ ##
8
+ # Calls the /moderations POST endpoint with the specified parameters.
9
+ #
10
+ # @param [String] input the text being evaluated by the API
11
+ # @param [Hash] parameters the set of parameters being passed to the API
12
+ ##
13
+ def create(input:, parameters: {})
14
+ raise MissingRequiredParameterError.new(:input) unless input
9
15
 
10
- json_post(path: "/moderations", parameters: parameters)
16
+ json_post(path: "/moderations", parameters: parameters.merge({ input: input }))
11
17
  end
12
18
  end
13
19
  end
@@ -11,16 +11,26 @@ module Asimov
11
11
 
12
12
  attr_reader :request_options
13
13
 
14
+ ##
15
+ # Initializes the Configuration object and resets it to default values.
16
+ ##
14
17
  def initialize
15
18
  reset
16
19
  end
17
20
 
21
+ ##
22
+ # Reset the configuration to default values. Mostly used for testing.
23
+ ##
18
24
  def reset
19
25
  @api_key = nil
20
26
  @organization_id = nil
21
27
  @request_options = {}
22
28
  end
23
29
 
30
+ ##
31
+ # Sets the request_options on the Configuration. Typically not invoked
32
+ # directly, but rather through use of `Asimov.configure`.
33
+ ##
24
34
  def request_options=(val)
25
35
  @request_options = Utils::RequestOptionsValidator.validate(val)
26
36
  end
data/lib/asimov/error.rb CHANGED
@@ -9,6 +9,10 @@ module Asimov
9
9
  ##
10
10
  class ConfigurationError < Error; end
11
11
 
12
+ ##
13
+ # Error that occurs when there is no configured
14
+ # API key for a newly created Asimov::Client.
15
+ ##
12
16
  class MissingApiKeyError < ConfigurationError; end
13
17
 
14
18
  ##
@@ -56,6 +60,18 @@ module Asimov
56
60
  ##
57
61
  class AuthorizationError < RequestError; end
58
62
 
63
+ ##
64
+ # Error that occurs because the provided API key is not
65
+ # valid.
66
+ ##
67
+ class InvalidApiKeyError < AuthorizationError; end
68
+
69
+ ##
70
+ # Error that occurs because the provided API key is not
71
+ # valid.
72
+ ##
73
+ class InvalidOrganizationError < AuthorizationError; end
74
+
59
75
  ##
60
76
  # Errors that occur because of issues with the
61
77
  # parameters of a request. Typically these correspond
@@ -87,26 +103,54 @@ module Asimov
87
103
  @system_message = system_message
88
104
  end
89
105
 
106
+ ##
107
+ # Returns the error message based on the file name and the wrapped error.
108
+ ##
90
109
  def message
91
110
  "The file #{@file_name} could not be opened for upload because of the " \
92
111
  "following error - #{@system_message}."
93
112
  end
94
113
  end
95
114
 
115
+ ##
116
+ # Error that occurs when a JSONL file is expected
117
+ # and it cannot be parsed.
118
+ ##
96
119
  class JsonlFileCannotBeParsedError < FileDataError; end
97
120
 
121
+ ##
122
+ # Error that occurs when an invalid training example
123
+ # is found in a training file.
124
+ ##
98
125
  class InvalidTrainingExampleError < FileDataError; end
99
126
 
127
+ ##
128
+ # Error that occurs when an invalid text entry
129
+ # is found in a text entry file.
130
+ ##
100
131
  class InvalidTextEntryError < FileDataError; end
101
132
 
133
+ ##
134
+ # Error that occurs when an invalid classification
135
+ # is found in a classifications file.
136
+ ##
102
137
  class InvalidClassificationError < FileDataError; end
103
138
 
139
+ ##
140
+ # Error that occurs when an invalid value is provided
141
+ # for an expected parameter in a request.
142
+ ##
104
143
  class InvalidParameterValueError < RequestError; end
105
144
 
145
+ ##
146
+ # Error that occurs when an unexpected parameter is
147
+ # provided in a request.
148
+ ##
106
149
  class UnsupportedParameterError < RequestError; end
107
150
 
108
151
  ##
109
- #
152
+ # Error raised when a required parameter is not included
153
+ # in the request.
110
154
  ##
111
155
  class MissingRequiredParameterError < RequestError
112
156
  def initialize(parameter_name)
@@ -114,6 +158,9 @@ module Asimov
114
158
  @parameter_name = parameter_name
115
159
  end
116
160
 
161
+ ##
162
+ # Returns the error message based on the missing parameter name.
163
+ ##
117
164
  def message
118
165
  "The parameter #{@parameter_name} is required."
119
166
  end
@@ -126,4 +173,39 @@ module Asimov
126
173
  # an object in the OpenAI system.
127
174
  ##
128
175
  class NotFoundError < RequestError; end
176
+
177
+ ##
178
+ # Errors that occur because the OpenAI API returned
179
+ # an HTTP code 429. This typically occurs because
180
+ # you have hit a rate limit or quota limit, or
181
+ # because the engine is overloaded.
182
+ ##
183
+ class TooManyRequestsError < RequestError; end
184
+
185
+ ##
186
+ # Error that occurs when the quota for an API key
187
+ # is exceeded.
188
+ ##
189
+ class QuotaExceededError < TooManyRequestsError; end
190
+
191
+ ##
192
+ # Error that occurs when the rate limit for requests
193
+ # is exceeded.
194
+ ##
195
+ class RateLimitError < TooManyRequestsError; end
196
+
197
+ ##
198
+ # Error that occurs when the API itself is
199
+ # overloaded and temporarily cannot accept additional
200
+ # requests.
201
+ ##
202
+ class ApiOverloadedError < TooManyRequestsError; end
203
+
204
+ ##
205
+ # Raised when a non-false stream parameter is passed
206
+ # to certain API methods. Processing of server-side
207
+ # events using the stream parameter is currently not
208
+ # supported.
209
+ ##
210
+ class StreamingResponseNotSupportedError < RequestError; end
129
211
  end
@@ -1,6 +1,9 @@
1
1
  require "json"
2
2
 
3
3
  module Asimov
4
+ ##
5
+ # Set of utilities, primarily intended for internal library use.
6
+ ##
4
7
  module Utils
5
8
  ##
6
9
  # Validates that a file is in the "classifications" format
@@ -8,8 +11,13 @@ module Asimov
8
11
  # "text" and "label" keys for each line that have string
9
12
  # values and an optional "metadata" key that can have
10
13
  # any value. No other keys are permitted.
14
+ #
15
+ # The only method that clients should call on instances
16
+ # of this class is `validate`
11
17
  ##
12
18
  class ClassificationsFileValidator < JsonlValidator
19
+ private
20
+
13
21
  def validate_line(line, idx)
14
22
  parsed = JSON.parse(line)
15
23
  validate_classification(parsed, idx)
@@ -8,11 +8,23 @@ module Asimov
8
8
  # Not intended for client use.
9
9
  ##
10
10
  class FileManager
11
- def self.open(filename)
12
- File.open(filename)
11
+ ##
12
+ # Returns the file corresponding to the file_or_path. If the argument is a
13
+ # file, then just returns the argument. Otherwise calls File.open with the
14
+ # argument. Raises an error if the file cannot be opened.
15
+ #
16
+ # @param [File/String] file_or_path the path to the file to be opened
17
+ ##
18
+ def self.open(file_or_path)
19
+ file?(file_or_path) ? file_or_path : File.open(file_or_path)
13
20
  rescue SystemCallError => e
14
- raise Asimov::FileCannotBeOpenedError.new(filename, e.message)
21
+ raise Asimov::FileCannotBeOpenedError.new(file_or_path, e.message)
15
22
  end
23
+
24
+ def self.file?(object)
25
+ object.respond_to?(:path) && object.respond_to?(:read)
26
+ end
27
+ private_class_method :file?
16
28
  end
17
29
  end
18
30
  end
@@ -7,13 +7,23 @@ module Asimov
7
7
  # more specific file validators.
8
8
  ##
9
9
  class JsonlValidator
10
- def validate(file)
11
- file.each_line.with_index { |line, idx| validate_line(line, idx) }
10
+ ##
11
+ # Validate that the IO object (typically a File) is properly
12
+ # formatted. Entry method for this class and its subclasses.
13
+ # Required format will depend on the class.
14
+ #
15
+ # @param [IO] io IO object, typically a file, whose contents
16
+ # are to be format checked.
17
+ ##
18
+ def validate(io)
19
+ io.each_line.with_index { |line, idx| validate_line(line, idx) }
12
20
  true
13
21
  rescue JSON::ParserError
14
22
  raise JsonlFileCannotBeParsedError, "Expected file to have the JSONL format."
15
23
  end
16
24
 
25
+ private
26
+
17
27
  def validate_line(line, idx)
18
28
  JSON.parse(line)
19
29
  rescue JSON::ParserError
@@ -14,6 +14,16 @@ module Asimov
14
14
  ALLOWED_OPTIONS = %i[timeout open_timeout read_timeout write_timeout local_host local_port
15
15
  verify verify_peer ssl_ca_file ssl_ca_path ssl_version ciphers
16
16
  http_proxyaddr http_proxyport http_proxyuser http_proxypass].freeze
17
+
18
+ ##
19
+ # Validates that the options are allowed request
20
+ # options. Currently checks the keys - both that they are symbols and that
21
+ # they are allowed options. Does not validate values.
22
+ #
23
+ # Only entry point for this class.
24
+ #
25
+ # @param [Hash] options the set of request options to validate
26
+ ##
17
27
  def self.validate(options)
18
28
  unless options.is_a?(Hash)
19
29
  raise Asimov::ConfigurationError,
@@ -32,6 +42,7 @@ module Asimov
32
42
  end
33
43
  unsupported_options
34
44
  end
45
+ private_class_method :generate_unsupported_options
35
46
 
36
47
  def self.check_unsupported_options(unsupported_options)
37
48
  return if unsupported_options.empty?
@@ -40,6 +51,7 @@ module Asimov
40
51
  raise Asimov::ConfigurationError,
41
52
  "The options #{quoted_keys.join(',')} are not supported."
42
53
  end
54
+ private_class_method :check_unsupported_options
43
55
 
44
56
  def self.supported_option?(key)
45
57
  ALLOWED_OPTIONS.include?(key)
@@ -51,6 +63,7 @@ module Asimov
51
63
  raise Asimov::ConfigurationError,
52
64
  "Request options keys must be symbols. The key '#{key}' is not a symbol."
53
65
  end
66
+ private_class_method :check_symbol
54
67
  end
55
68
  end
56
69
  end
@@ -10,6 +10,8 @@ module Asimov
10
10
  # any value. No other keys are permitted.
11
11
  ##
12
12
  class TextEntryFileValidator < JsonlValidator
13
+ private
14
+
13
15
  def validate_line(line, idx)
14
16
  parsed = JSON.parse(line)
15
17
  validate_text_entry(parsed, idx)
@@ -9,6 +9,8 @@ module Asimov
9
9
  # values. No other keys are permitted.
10
10
  ##
11
11
  class TrainingFileValidator < JsonlValidator
12
+ private
13
+
12
14
  def validate_line(line, idx)
13
15
  parsed = JSON.parse(line)
14
16
  validate_training_example(parsed, idx)
@@ -1,3 +1,4 @@
1
1
  module Asimov
2
- VERSION = "0.1.0".freeze
2
+ # Current gem version
3
+ VERSION = "1.0.0".freeze
3
4
  end
data/lib/asimov.rb CHANGED
@@ -4,7 +4,7 @@ require_relative "asimov/error"
4
4
  require_relative "asimov/client"
5
5
 
6
6
  ##
7
- # Client library for using the OpenAI API.
7
+ # Top level module for the Asimov client library for using the OpenAI API.
8
8
  ##
9
9
  module Asimov
10
10
  ##
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asimov
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-14 00:00:00.000000000 Z
11
+ date: 2023-01-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -30,105 +30,8 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 0.22.0
33
- - !ruby/object:Gem::Dependency
34
- name: rake
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '13.0'
40
- type: :development
41
- prerelease: false
42
- version_requirements: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: '13.0'
47
- - !ruby/object:Gem::Dependency
48
- name: rspec
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '3.12'
54
- type: :development
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '3.12'
61
- - !ruby/object:Gem::Dependency
62
- name: rubocop
63
- requirement: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: 1.43.0
68
- type: :development
69
- prerelease: false
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: 1.43.0
75
- - !ruby/object:Gem::Dependency
76
- name: rubocop-rake
77
- requirement: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - ">="
80
- - !ruby/object:Gem::Version
81
- version: '0'
82
- type: :development
83
- prerelease: false
84
- version_requirements: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - ">="
87
- - !ruby/object:Gem::Version
88
- version: '0'
89
- - !ruby/object:Gem::Dependency
90
- name: rubocop-rspec
91
- requirement: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - ">="
94
- - !ruby/object:Gem::Version
95
- version: '0'
96
- type: :development
97
- prerelease: false
98
- version_requirements: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - ">="
101
- - !ruby/object:Gem::Version
102
- version: '0'
103
- - !ruby/object:Gem::Dependency
104
- name: vcr
105
- requirement: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - "~>"
108
- - !ruby/object:Gem::Version
109
- version: 6.1.0
110
- type: :development
111
- prerelease: false
112
- version_requirements: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - "~>"
115
- - !ruby/object:Gem::Version
116
- version: 6.1.0
117
- - !ruby/object:Gem::Dependency
118
- name: webmock
119
- requirement: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - "~>"
122
- - !ruby/object:Gem::Version
123
- version: 3.18.1
124
- type: :development
125
- prerelease: false
126
- version_requirements: !ruby/object:Gem::Requirement
127
- requirements:
128
- - - "~>"
129
- - !ruby/object:Gem::Version
130
- version: 3.18.1
131
- description: A Ruby client for the OpenAI API
33
+ description: A Ruby client for the OpenAI API support for multiple API configurations
34
+ in a single app, robust and simple error handling, and network-level configuration.
132
35
  email:
133
36
  - peter.m.goldstein@gmail.com
134
37
  executables: []
@@ -185,8 +88,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
88
  - !ruby/object:Gem::Version
186
89
  version: '0'
187
90
  requirements: []
188
- rubygems_version: 3.4.3
91
+ rubygems_version: 3.4.5
189
92
  signing_key:
190
93
  specification_version: 4
191
- summary: A Ruby client for the OpenAI API
94
+ summary: A Ruby client for the OpenAI API support for multiple API configurations
95
+ in a single app, robust and simple error handling, and network-level configuration.
192
96
  test_files: []