asimov 0.1.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.
@@ -0,0 +1,28 @@
1
+ require_relative "utils/request_options_validator"
2
+
3
+ module Asimov
4
+ ##
5
+ # Application-wide configuration for the Asimov library. Should be
6
+ # configured once using the Asimov.configure method on application
7
+ # startup.
8
+ ##
9
+ class Configuration
10
+ attr_accessor :api_key, :organization_id
11
+
12
+ attr_reader :request_options
13
+
14
+ def initialize
15
+ reset
16
+ end
17
+
18
+ def reset
19
+ @api_key = nil
20
+ @organization_id = nil
21
+ @request_options = {}
22
+ end
23
+
24
+ def request_options=(val)
25
+ @request_options = Utils::RequestOptionsValidator.validate(val)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,129 @@
1
+ module Asimov
2
+ ##
3
+ # Parent error for all Asimov gem errors.
4
+ ##
5
+ class Error < StandardError; end
6
+
7
+ ##
8
+ # Errors that occur when initializing an Asimov::Client.
9
+ ##
10
+ class ConfigurationError < Error; end
11
+
12
+ class MissingApiKeyError < ConfigurationError; end
13
+
14
+ ##
15
+ # Errors that occur when making an API request. They
16
+ # can occur either through local validation or
17
+ # as a result of an error returned by the OpenAI API.
18
+ ##
19
+ class RequestError < Error; end
20
+
21
+ ##
22
+ # Errors that occur as a result of an error raised
23
+ # by the underlying network client. Examples
24
+ # include Net::OpenTimeout or Net::ReadTimeout
25
+ ##
26
+ class NetworkError < RequestError; end
27
+
28
+ ##
29
+ # Errors that occur as a result of a timeout raised
30
+ # by the underlying network client. Examples
31
+ # include Net::OpenTimeout or Net::ReadTimeout
32
+ ##
33
+ class TimeoutError < NetworkError; end
34
+
35
+ ##
36
+ # Error that occurs as a result of a Net::OpenTimeout raised
37
+ # by the underlying network client.
38
+ ##
39
+ class OpenTimeout < TimeoutError; end
40
+
41
+ ##
42
+ # Error that occurs as a result of a Net::ReadTimeout raised
43
+ # by the underlying network client.
44
+ ##
45
+ class ReadTimeout < TimeoutError; end
46
+
47
+ ##
48
+ # Error that occurs as a result of a Net::WriteTimeout raised
49
+ # by the underlying network client.
50
+ ##
51
+ class WriteTimeout < TimeoutError; end
52
+
53
+ ##
54
+ # Errors that occur as a result of an HTTP 401
55
+ # returned by the OpenAI API.
56
+ ##
57
+ class AuthorizationError < RequestError; end
58
+
59
+ ##
60
+ # Errors that occur because of issues with the
61
+ # parameters of a request. Typically these correspond
62
+ # to a 400 HTTP return code. Example causes include missing
63
+ # parameters, unexpected parameters, invalid parameter
64
+ # values.
65
+ #
66
+ # Errors of this type my be geenrated through local
67
+ # validation or as a result of an error returned by
68
+ # the OpenAI API.
69
+ ##
70
+ class ParameterError < RequestError; end
71
+
72
+ ##
73
+ # Errors that occur because of a problem with file data
74
+ # being provided as a part of a multipart form upload.
75
+ # This can include the file being unreadable, or
76
+ # formatting problems with the file.
77
+ ##
78
+ class FileDataError < RequestError; end
79
+
80
+ ##
81
+ # Error that occurs when a local file cannot be opened.
82
+ ##
83
+ class FileCannotBeOpenedError < FileDataError
84
+ def initialize(file_name, system_message)
85
+ super()
86
+ @file_name = file_name
87
+ @system_message = system_message
88
+ end
89
+
90
+ def message
91
+ "The file #{@file_name} could not be opened for upload because of the " \
92
+ "following error - #{@system_message}."
93
+ end
94
+ end
95
+
96
+ class JsonlFileCannotBeParsedError < FileDataError; end
97
+
98
+ class InvalidTrainingExampleError < FileDataError; end
99
+
100
+ class InvalidTextEntryError < FileDataError; end
101
+
102
+ class InvalidClassificationError < FileDataError; end
103
+
104
+ class InvalidParameterValueError < RequestError; end
105
+
106
+ class UnsupportedParameterError < RequestError; end
107
+
108
+ ##
109
+ #
110
+ ##
111
+ class MissingRequiredParameterError < RequestError
112
+ def initialize(parameter_name)
113
+ super()
114
+ @parameter_name = parameter_name
115
+ end
116
+
117
+ def message
118
+ "The parameter #{@parameter_name} is required."
119
+ end
120
+ end
121
+
122
+ ##
123
+ # Errors that occur because the OpenAI API returned
124
+ # an HTTP code 400. This typically occurs because
125
+ # one or more provided identifiers do not match
126
+ # an object in the OpenAI system.
127
+ ##
128
+ class NotFoundError < RequestError; end
129
+ end
@@ -0,0 +1,67 @@
1
+ module Asimov
2
+ ##
3
+ # Configures the set of HTTP headers being sent with requests to
4
+ # the OpenAI API. These header include authentication and content-type
5
+ # information.
6
+ #
7
+ # Not intended for client use.
8
+ ##
9
+ class HeadersFactory
10
+ ##
11
+ # Null object used to allow nil override of a default organization_id.
12
+ ##
13
+ NULL_ORGANIZATION_ID = Object.new.freeze
14
+
15
+ attr_reader :api_key, :organization_id
16
+
17
+ def initialize(api_key, organization_id)
18
+ @api_key = api_key || Asimov.configuration.api_key
19
+ initialize_organization_id(organization_id)
20
+
21
+ return if @api_key
22
+
23
+ raise Asimov::MissingApiKeyError,
24
+ "No OpenAI API key was provided for this client."
25
+ end
26
+
27
+ ##
28
+ # Returns the headers to use for a request.
29
+ #
30
+ # @param content_type The Content-Type header value for the request.
31
+ ##
32
+ def headers(content_type = "application/json")
33
+ {
34
+ "Content-Type" => content_type
35
+ }.merge(openai_headers)
36
+ end
37
+
38
+ private
39
+
40
+ def initialize_organization_id(organization_id)
41
+ @organization_id = if organization_id == NULL_ORGANIZATION_ID
42
+ Asimov.configuration.organization_id
43
+ else
44
+ organization_id
45
+ end
46
+ end
47
+
48
+ def openai_headers
49
+ @openai_headers ||=
50
+ if organization_id.nil?
51
+ auth_headers
52
+ else
53
+ auth_headers.merge(
54
+ { "OpenAI-Organization" => organization_id }
55
+ )
56
+ end
57
+ end
58
+
59
+ def auth_headers
60
+ @auth_headers ||= { "Authorization" => bearer_header(api_key) }
61
+ end
62
+
63
+ def bearer_header(api_key)
64
+ "Bearer #{api_key}"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,42 @@
1
+ require "json"
2
+
3
+ module Asimov
4
+ module Utils
5
+ ##
6
+ # Validates that a file is in the "classifications" format
7
+ # used by OpenAI. The file is a JSONL file, with
8
+ # "text" and "label" keys for each line that have string
9
+ # values and an optional "metadata" key that can have
10
+ # any value. No other keys are permitted.
11
+ ##
12
+ class ClassificationsFileValidator < JsonlValidator
13
+ def validate_line(line, idx)
14
+ parsed = JSON.parse(line)
15
+ validate_classification(parsed, idx)
16
+ end
17
+
18
+ def validate_classification(parsed, idx)
19
+ return if classification?(parsed)
20
+
21
+ raise InvalidClassificationError,
22
+ "Expected file to have JSONL format with text/label and (optional) metadata keys. " \
23
+ "Invalid format on line #{idx + 1}."
24
+ end
25
+
26
+ def classification?(parsed)
27
+ return false unless parsed.is_a?(Hash)
28
+ return false unless includes_required_key_value?("text", parsed)
29
+ return false unless includes_required_key_value?("label", parsed)
30
+
31
+ keys = parsed.keys
32
+ return false unless keys.size <= 3
33
+
34
+ keys.size == 2 ? true : keys.include?("metadata")
35
+ end
36
+
37
+ def includes_required_key_value?(key, parsed)
38
+ parsed[key]&.is_a?(String)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ require "json"
2
+
3
+ module Asimov
4
+ module Utils
5
+ ##
6
+ # File-related utility methods.
7
+ #
8
+ # Not intended for client use.
9
+ ##
10
+ class FileManager
11
+ def self.open(filename)
12
+ File.open(filename)
13
+ rescue SystemCallError => e
14
+ raise Asimov::FileCannotBeOpenedError.new(filename, e.message)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ require "json"
2
+
3
+ module Asimov
4
+ module Utils
5
+ ##
6
+ # Validates that a file is a JSONL file. Subclassed by
7
+ # more specific file validators.
8
+ ##
9
+ class JsonlValidator
10
+ def validate(file)
11
+ file.each_line.with_index { |line, idx| validate_line(line, idx) }
12
+ true
13
+ rescue JSON::ParserError
14
+ raise JsonlFileCannotBeParsedError, "Expected file to have the JSONL format."
15
+ end
16
+
17
+ def validate_line(line, idx)
18
+ JSON.parse(line)
19
+ rescue JSON::ParserError
20
+ raise JsonlFileCannotBeParsedError,
21
+ "Expected file to have the JSONL format. Error found on line #{idx + 1}."
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,56 @@
1
+ require "json"
2
+
3
+ module Asimov
4
+ module Utils
5
+ ##
6
+ # Validates a set of request options that are passed to the application
7
+ # configuration or on Asimov::Client initialization. Ensures that the
8
+ # value is a hash, that all keys are symbols, and that all keys correspond
9
+ # to legitimate options (typically relating to network behavior). Currently
10
+ # does not validate the option values.
11
+ ##
12
+ class RequestOptionsValidator
13
+ # This is taken from HTTParty
14
+ ALLOWED_OPTIONS = %i[timeout open_timeout read_timeout write_timeout local_host local_port
15
+ verify verify_peer ssl_ca_file ssl_ca_path ssl_version ciphers
16
+ http_proxyaddr http_proxyport http_proxyuser http_proxypass].freeze
17
+ def self.validate(options)
18
+ unless options.is_a?(Hash)
19
+ raise Asimov::ConfigurationError,
20
+ "Request options must be a hash"
21
+ end
22
+
23
+ check_unsupported_options(generate_unsupported_options(options))
24
+ options
25
+ end
26
+
27
+ def self.generate_unsupported_options(options)
28
+ unsupported_options = []
29
+ options.each_key do |k|
30
+ check_symbol(k)
31
+ unsupported_options << k unless supported_option?(k)
32
+ end
33
+ unsupported_options
34
+ end
35
+
36
+ def self.check_unsupported_options(unsupported_options)
37
+ return if unsupported_options.empty?
38
+
39
+ quoted_keys = unsupported_options.map { |k| "'#{k}'" }
40
+ raise Asimov::ConfigurationError,
41
+ "The options #{quoted_keys.join(',')} are not supported."
42
+ end
43
+
44
+ def self.supported_option?(key)
45
+ ALLOWED_OPTIONS.include?(key)
46
+ end
47
+
48
+ def self.check_symbol(key)
49
+ return if key.is_a?(Symbol)
50
+
51
+ raise Asimov::ConfigurationError,
52
+ "Request options keys must be symbols. The key '#{key}' is not a symbol."
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,38 @@
1
+ require "json"
2
+
3
+ module Asimov
4
+ module Utils
5
+ ##
6
+ # Validates that a file is in the "answers" or "search" format
7
+ # used by OpenAI. The file is a JSONL file, with
8
+ # a "text" key for each line that has a string
9
+ # value and an optional "metadata" key that can have
10
+ # any value. No other keys are permitted.
11
+ ##
12
+ class TextEntryFileValidator < JsonlValidator
13
+ def validate_line(line, idx)
14
+ parsed = JSON.parse(line)
15
+ validate_text_entry(parsed, idx)
16
+ end
17
+
18
+ def validate_text_entry(parsed, idx)
19
+ return if text_entry?(parsed)
20
+
21
+ raise InvalidTextEntryError,
22
+ "Expected file to have the JSONL format with 'text' key and (optional) " \
23
+ "'metadata' key. Invalid format on line #{idx + 1}."
24
+ end
25
+
26
+ def text_entry?(parsed)
27
+ return false unless parsed.is_a?(Hash)
28
+
29
+ keys = parsed.keys
30
+ return false unless keys.size >= 1 && keys.size <= 2
31
+ return false unless keys.include?("text")
32
+ return false unless parsed["text"].is_a?(String)
33
+
34
+ keys.size == 1 ? true : keys.include?("metadata")
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ require "json"
2
+
3
+ module Asimov
4
+ module Utils
5
+ ##
6
+ # Validates that a file is in the "fine-tune" format
7
+ # used by OpenAI. The file is a JSONL file, with
8
+ # "prompt" and "completion" keys for each line that have string
9
+ # values. No other keys are permitted.
10
+ ##
11
+ class TrainingFileValidator < JsonlValidator
12
+ def validate_line(line, idx)
13
+ parsed = JSON.parse(line)
14
+ validate_training_example(parsed, idx)
15
+ end
16
+
17
+ def validate_training_example(parsed, idx)
18
+ return if training_example?(parsed)
19
+
20
+ raise InvalidTrainingExampleError,
21
+ "Expected file to have JSONL format with prompt/completion keys. " \
22
+ "Invalid format on line #{idx + 1}."
23
+ end
24
+
25
+ def training_example?(parsed)
26
+ return false unless parsed.is_a?(Hash)
27
+
28
+ keys = parsed.keys
29
+ return false unless keys.size == 2
30
+ return false unless keys.include?("prompt")
31
+ return false unless keys.include?("completion")
32
+ return false unless parsed["prompt"].is_a?(String)
33
+ return false unless parsed["completion"].is_a?(String)
34
+
35
+ true
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module Asimov
2
+ VERSION = "0.1.0".freeze
3
+ end
data/lib/asimov.rb ADDED
@@ -0,0 +1,35 @@
1
+ require_relative "asimov/version"
2
+ require_relative "asimov/configuration"
3
+ require_relative "asimov/error"
4
+ require_relative "asimov/client"
5
+
6
+ ##
7
+ # Client library for using the OpenAI API.
8
+ ##
9
+ module Asimov
10
+ ##
11
+ # Method uses to initialize the application-wide configuration. Should be called with
12
+ # a block like so:
13
+ #
14
+ # Asimov.configure do |config|
15
+ # config.api_key = 'abcd'
16
+ # config.organization = 'def'
17
+ # end
18
+ #
19
+ # Attributes that can be set on the configuration include:
20
+ #
21
+ # api_key - The OpenAI API key that Asimov::Client instances should use by default.
22
+ # organization_id - The OpenAI organization identifier that Asimov::Client instances should
23
+ # use by default.
24
+ ##
25
+ def self.configure
26
+ yield(configuration)
27
+ end
28
+
29
+ ##
30
+ # Getter for the application-wide Asimove::Configuration singleton.
31
+ ##
32
+ def self.configuration
33
+ @configuration ||= Asimov::Configuration.new
34
+ end
35
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: asimov
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter M. Goldstein
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-01-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: httparty
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.18.1
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 0.22.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 0.18.1
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
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
132
+ email:
133
+ - peter.m.goldstein@gmail.com
134
+ executables: []
135
+ extensions: []
136
+ extra_rdoc_files: []
137
+ files:
138
+ - CHANGELOG.md
139
+ - Gemfile
140
+ - LICENSE.txt
141
+ - README.md
142
+ - lib/asimov.rb
143
+ - lib/asimov/api_v1/api_error_translator.rb
144
+ - lib/asimov/api_v1/base.rb
145
+ - lib/asimov/api_v1/completions.rb
146
+ - lib/asimov/api_v1/edits.rb
147
+ - lib/asimov/api_v1/embeddings.rb
148
+ - lib/asimov/api_v1/files.rb
149
+ - lib/asimov/api_v1/finetunes.rb
150
+ - lib/asimov/api_v1/images.rb
151
+ - lib/asimov/api_v1/models.rb
152
+ - lib/asimov/api_v1/moderations.rb
153
+ - lib/asimov/api_v1/network_error_translator.rb
154
+ - lib/asimov/client.rb
155
+ - lib/asimov/configuration.rb
156
+ - lib/asimov/error.rb
157
+ - lib/asimov/headers_factory.rb
158
+ - lib/asimov/utils/classifications_file_validator.rb
159
+ - lib/asimov/utils/file_manager.rb
160
+ - lib/asimov/utils/jsonl_validator.rb
161
+ - lib/asimov/utils/request_options_validator.rb
162
+ - lib/asimov/utils/text_entry_file_validator.rb
163
+ - lib/asimov/utils/training_file_validator.rb
164
+ - lib/asimov/version.rb
165
+ homepage: https://github.com/petergoldstein/asimov
166
+ licenses:
167
+ - MIT
168
+ metadata:
169
+ homepage_uri: https://github.com/petergoldstein/asimov
170
+ source_code_uri: https://github.com/petergoldstein/asimov
171
+ changelog_uri: https://github.com/petergoldstein/asimov/blob/main/CHANGELOG.md
172
+ rubygems_mfa_required: 'true'
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: 3.0.0
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubygems_version: 3.4.3
189
+ signing_key:
190
+ specification_version: 4
191
+ summary: A Ruby client for the OpenAI API
192
+ test_files: []