asimov 0.1.0 → 1.0.0

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