asimov 1.0.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b71726c0c67788a649dc1c01ca5763ab13d41f31118d6f8b093795a3a92e0ca8
4
- data.tar.gz: 5d5eb6dd51099aadf9418ff1c11c7ea6688a06a5e07b5a39bd376a48538ac79a
3
+ metadata.gz: 7d49b0bc867c3e318501548e7fcf45a3dd48bc469b05f959b5839144e3f9d472
4
+ data.tar.gz: 1193a9a5eea75c3d3b57d09ca10ccd5e50ee01d44ecce648c985ac2bcfd5cded
5
5
  SHA512:
6
- metadata.gz: b20449b89964c2b2b77de68726aef47bc09e65871ba04c80ad2f607e58bc5b6322ff6106c801f01ef68377056a55010d74e6481f883ad57793eca5366a438acd
7
- data.tar.gz: fdfe13acb265341e7c568874a8696ce520b6da8fbdc77ae694b3ba56deb97d44a65aac82935a3ad3f356258aa0375358349540c17cd54bfd0f9a3a746d40b807
6
+ metadata.gz: 349259c93c833021ebfc46f7912f8c36109dc51693c3828972d263282f514adee94b5416d9e89c2dc1cc1d868d01cabce20ef1df01e83279eac80d156b1ded61
7
+ data.tar.gz: 67deac4b3306ffdc5ab36aa49b7ab66093dc4938131b3264ce2ef2cd6a628840cd04d3474f39fe7bc1f70dc5c0d9dba7bd136c28c46c58b776ff2494ae825030
data/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## [1.1.0] - 2023-03-01
11
+
12
+ ### Added
13
+
14
+ - Made API base URI configurable to support services that proxy API calls (like Helicone)
15
+ - Added support for the chat and audio endpoints
16
+
10
17
  ## [1.0.0] - 2023-01-24
11
18
 
12
19
  This version has complete coverage of the OpenAI API (except for stream: true behavior), has
data/Gemfile CHANGED
@@ -3,9 +3,11 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  group :development, :test do
6
+ gem "faker"
6
7
  gem "rake", "~> 13.0"
7
8
  gem "rspec", "~> 3.12"
8
9
  gem "rubocop", "~> 1.44"
10
+ gem "rubocop-performance"
9
11
  gem "rubocop-rake"
10
12
  gem "rubocop-rspec"
11
13
  gem "simplecov", require: false
@@ -0,0 +1,52 @@
1
+ require_relative "../utils/chat_messages_validator"
2
+
3
+ module Asimov
4
+ module ApiV1
5
+ ##
6
+ # Class interface for API methods in the "/audio" URI subspace.
7
+ ##
8
+ class Audio < Base
9
+ RESOURCE = "audio".freeze
10
+
11
+ ##
12
+ # Creates a transcription request with the specified parameters.
13
+ #
14
+ # @param [String] model the model to use for the completion
15
+ # @param [Hash] parameters the set of parameters being passed to the API
16
+ ##
17
+ def create_transcription(file:, model:, parameters: {})
18
+ raise MissingRequiredParameterError.new(:model) unless model
19
+
20
+ rest_create_w_multipart_params(resource: [RESOURCE, "transcriptions"],
21
+ parameters:
22
+ open_file(parameters.merge({
23
+ file: file, model: model
24
+ })))
25
+ end
26
+
27
+ ##
28
+ # Creates a transcription request with the specified parameters.
29
+ #
30
+ # @param [String] model the model to use for the completion
31
+ # @param [Hash] parameters the set of parameters being passed to the API
32
+ ##
33
+ def create_translation(file:, model:, parameters: {})
34
+ raise MissingRequiredParameterError.new(:model) unless model
35
+
36
+ rest_create_w_multipart_params(resource: [RESOURCE, "translations"],
37
+ parameters:
38
+ open_file(parameters.merge({
39
+ file: file, model: model
40
+ })))
41
+ end
42
+
43
+ private
44
+
45
+ def open_file(parameters)
46
+ raise MissingRequiredParameterError.new(:file) unless parameters[:file]
47
+
48
+ parameters.merge(file: Utils::FileManager.open(parameters[:file]))
49
+ end
50
+ end
51
+ end
52
+ end
@@ -15,53 +15,67 @@ module Asimov
15
15
  extend Forwardable
16
16
  include HTTParty
17
17
 
18
- base_uri "https://api.openai.com/v1"
19
-
20
18
  def initialize(client: nil)
21
19
  @client = client
22
20
  end
23
- def_delegators :@client, :headers, :request_options
21
+ def_delegators :@client, :headers, :request_options, :openai_api_base
24
22
 
25
23
  ##
26
- # Executes an HTTP DELETE on the specified path.
24
+ # Executes a REST index for the specified resource
27
25
  #
28
- # @param [String] path the URI (when combined with the
29
- # base_uri) against which the DELETE is executed.
26
+ # @param [String] resource the pluralized resource name
30
27
  ##
31
- def http_delete(path:)
28
+ def rest_index(resource:)
29
+ wrap_response_with_error_handling do
30
+ self.class.get(
31
+ absolute_path("/#{Array(resource).join('/')}"),
32
+ { headers: headers }.merge!(request_options)
33
+ )
34
+ end
35
+ end
36
+
37
+ ##
38
+ # Executes a REST delete on the specified resource.
39
+ #
40
+ # @param [String] resource the pluralized resource name
41
+ # @param [String] id the id of the resource to delete
42
+ ##
43
+ def rest_delete(resource:, id:)
32
44
  wrap_response_with_error_handling do
33
45
  self.class.delete(
34
- path,
46
+ absolute_path("/#{resource}/#{CGI.escape(id)}"),
35
47
  { headers: headers }.merge!(request_options)
36
48
  )
37
49
  end
38
50
  end
39
51
 
40
52
  ##
41
- # Executes an HTTP GET on the specified path.
53
+ # Executes a REST get on the specified resource.
42
54
  #
43
- # @param [String] path the URI (when combined with the
44
- # base_uri) against which the GET is executed.
55
+ # @param [String] resource the pluralized resource name
56
+ # @param [String] id the id of the resource get
45
57
  ##
46
- def http_get(path:)
58
+ def rest_get(resource:, id:)
47
59
  wrap_response_with_error_handling do
48
60
  self.class.get(
49
- path,
61
+ absolute_path("/#{resource}/#{CGI.escape(id)}"),
50
62
  { headers: headers }.merge!(request_options)
51
63
  )
52
64
  end
53
65
  end
54
66
 
55
67
  ##
56
- # Executes an HTTP POST with JSON-encoded parameters on the specified path.
68
+ # Executes a REST create with JSON-encoded parameters for the specified
69
+ # resource.
57
70
  #
58
- # @param [String] path the URI (when combined with the
59
- # base_uri) against which the POST is executed.
71
+ # @param [String] the resource to be created.
72
+ # @param [Hash] parameters the parameters to include with the request
73
+ # to create the resource
60
74
  ##
61
- def json_post(path:, parameters:)
75
+ def rest_create_w_json_params(resource:, parameters:)
62
76
  wrap_response_with_error_handling do
63
77
  self.class.post(
64
- path,
78
+ absolute_path("/#{Array(resource).join('/')}"),
65
79
  { headers: headers,
66
80
  body: parameters&.to_json }.merge!(request_options)
67
81
  )
@@ -69,15 +83,17 @@ module Asimov
69
83
  end
70
84
 
71
85
  ##
72
- # Executes an HTTP POST with multipart encoded parameters on the specified path.
86
+ # Executes a REST create with multipart-encoded parameters for the specified
87
+ # resource.
73
88
  #
74
- # @param [String] path the URI (when combined with the
75
- # base_uri) against which the POST is executed.
89
+ # @param [String] the resource to be created.
90
+ # @param [Hash] parameters the optional parameters to include with the request
91
+ # to create the resource
76
92
  ##
77
- def multipart_post(path:, parameters: nil)
93
+ def rest_create_w_multipart_params(resource:, parameters: nil)
78
94
  wrap_response_with_error_handling do
79
95
  self.class.post(
80
- path,
96
+ absolute_path("/#{Array(resource).join('/')}"),
81
97
  { headers: headers("multipart/form-data"),
82
98
  body: parameters }.merge!(request_options)
83
99
  )
@@ -85,15 +101,14 @@ module Asimov
85
101
  end
86
102
 
87
103
  ##
88
- # Executes an HTTP GET on the specified path, streaming the resulting body
104
+ # Executes an REST get on the specified path, streaming the resulting body
89
105
  # to the writer in case of success.
90
106
  #
91
- # @param [String] path the URI (when combined with the
92
- # base_uri) against which the POST is executed.
107
+ # @param [Array] resource the resource path elements as an array
93
108
  # @param [Writer] writer an object, typically a File, that responds to a `write` method
94
109
  ##
95
- def http_streamed_download(path:, writer:)
96
- self.class.get(path,
110
+ def rest_get_streamed_download(resource:, writer:)
111
+ self.class.get(absolute_path("/#{Array(resource).join('/')}"),
97
112
  { headers: headers,
98
113
  stream_body: true }.merge!(request_options)) do |fragment|
99
114
  fragment.code == 200 ? writer.write(fragment) : check_for_api_error(fragment)
@@ -105,6 +120,10 @@ module Asimov
105
120
 
106
121
  private
107
122
 
123
+ def absolute_path(path)
124
+ "#{openai_api_base}#{path}"
125
+ end
126
+
108
127
  def wrap_response_with_error_handling
109
128
  resp = begin
110
129
  yield
@@ -0,0 +1,29 @@
1
+ require_relative "../utils/chat_messages_validator"
2
+
3
+ module Asimov
4
+ module ApiV1
5
+ ##
6
+ # Class interface for API methods in the "/chat" URI subspace.
7
+ ##
8
+ class Chat < Base
9
+ RESOURCE = "chat".freeze
10
+
11
+ ##
12
+ # Creates a completion request with the specified parameters.
13
+ #
14
+ # @param [String] model the model to use for the completion
15
+ # @param [Hash] parameters the set of parameters being passed to the API
16
+ ##
17
+ def create_completions(model:, messages:, parameters: {})
18
+ raise MissingRequiredParameterError.new(:model) unless model
19
+ raise MissingRequiredParameterError.new(:messages) unless messages
20
+ raise StreamingResponseNotSupportedError if parameters[:stream]
21
+
22
+ messages = Utils::ChatMessagesValidator.validate_and_normalize(messages)
23
+ rest_create_w_json_params(resource: [RESOURCE, "completions"],
24
+ parameters: parameters.merge({ model: model,
25
+ messages: messages }))
26
+ end
27
+ end
28
+ end
29
+ end
@@ -5,7 +5,7 @@ module Asimov
5
5
  ##
6
6
  class Completions < Base
7
7
  ##
8
- # Calls the /completions POST endpoint with the specified parameters.
8
+ # Creates a completion request with the specified parameters.
9
9
  #
10
10
  # @param [String] model the model to use for the completion
11
11
  # @param [Hash] parameters the set of parameters being passed to the API
@@ -14,7 +14,8 @@ module Asimov
14
14
  raise MissingRequiredParameterError.new(:model) unless model
15
15
  raise StreamingResponseNotSupportedError if parameters[:stream]
16
16
 
17
- json_post(path: "/completions", parameters: parameters.merge({ model: model }))
17
+ rest_create_w_json_params(resource: "completions",
18
+ parameters: parameters.merge({ model: model }))
18
19
  end
19
20
  end
20
21
  end
@@ -5,7 +5,7 @@ module Asimov
5
5
  ##
6
6
  class Edits < Base
7
7
  ##
8
- # Calls the /edits POST endpoint with the specified parameters.
8
+ # Creates an edit resource with the specified parameters.
9
9
  #
10
10
  # @param [Hash] parameters the set of parameters being passed to the API
11
11
  ##
@@ -13,9 +13,9 @@ module Asimov
13
13
  raise MissingRequiredParameterError.new(:model) unless model
14
14
  raise MissingRequiredParameterError.new(:instruction) unless instruction
15
15
 
16
- json_post(path: "/edits",
17
- parameters: parameters.merge({ model: model,
18
- instruction: instruction }))
16
+ rest_create_w_json_params(resource: "edits",
17
+ parameters: parameters.merge({ model: model,
18
+ instruction: instruction }))
19
19
  end
20
20
  end
21
21
  end
@@ -5,15 +5,21 @@ module Asimov
5
5
  ##
6
6
  class Embeddings < Base
7
7
  ##
8
- # Calls the /embeddings POST endpoint with the specified parameters.
8
+ # Creates an embedding resource with the specified parameters.
9
9
  #
10
+ # @param [String] model the id for the model used to create the embedding
11
+ # @param [String] parameters the (optional) additional parameters being
12
+ # provided to inform embedding creation.
10
13
  # @param [Hash] parameters the set of parameters being passed to the API
11
14
  ##
12
15
  def create(model:, input:, parameters: {})
13
16
  raise MissingRequiredParameterError.new(:model) unless model
14
17
  raise MissingRequiredParameterError.new(:input) unless input
15
18
 
16
- json_post(path: "/embeddings", parameters: parameters.merge({ model: model, input: input }))
19
+ rest_create_w_json_params(resource: "embeddings",
20
+ parameters: parameters.merge({
21
+ model: model, input: input
22
+ }))
17
23
  end
18
24
  end
19
25
  end
@@ -10,14 +10,14 @@ module Asimov
10
10
  # Class interface for API methods in the "/files" URI subspace.
11
11
  ##
12
12
  class Files < Base
13
- URI_PREFIX = "/files".freeze
14
- private_constant :URI_PREFIX
13
+ RESOURCE = "files".freeze
14
+ private_constant :RESOURCE
15
15
 
16
16
  ##
17
17
  # Lists files that have been uploaded to OpenAI
18
18
  ##
19
19
  def list
20
- http_get(path: URI_PREFIX)
20
+ rest_index(resource: RESOURCE)
21
21
  end
22
22
 
23
23
  ##
@@ -32,8 +32,8 @@ module Asimov
32
32
 
33
33
  validate(file, purpose)
34
34
 
35
- multipart_post(
36
- path: URI_PREFIX,
35
+ rest_create_w_multipart_params(
36
+ resource: RESOURCE,
37
37
  parameters: parameters.merge(file: Utils::FileManager.open(file), purpose: purpose)
38
38
  )
39
39
  end
@@ -44,7 +44,7 @@ module Asimov
44
44
  # @param [String] file_id the id of the file to be retrieved
45
45
  ##
46
46
  def retrieve(file_id:)
47
- http_get(path: "#{URI_PREFIX}/#{file_id}")
47
+ rest_get(resource: RESOURCE, id: file_id)
48
48
  end
49
49
 
50
50
  ##
@@ -53,7 +53,7 @@ module Asimov
53
53
  # @param [String] file_id the id of the file to be deleted
54
54
  ##
55
55
  def delete(file_id:)
56
- http_delete(path: "#{URI_PREFIX}/#{file_id}")
56
+ rest_delete(resource: RESOURCE, id: file_id)
57
57
  end
58
58
 
59
59
  ##
@@ -65,7 +65,7 @@ module Asimov
65
65
  # as it is received from the API
66
66
  ##
67
67
  def content(file_id:, writer:)
68
- http_streamed_download(path: "#{URI_PREFIX}/#{file_id}/content", writer: writer)
68
+ rest_get_streamed_download(resource: [RESOURCE, file_id, "content"], writer: writer)
69
69
  end
70
70
 
71
71
  private
@@ -4,14 +4,14 @@ module Asimov
4
4
  # Class interface for API methods in the "/fine-tunes" URI subspace.
5
5
  ##
6
6
  class Finetunes < Base
7
- URI_PREFIX = "/fine-tunes".freeze
8
- private_constant :URI_PREFIX
7
+ RESOURCE = "fine-tunes".freeze
8
+ private_constant :RESOURCE
9
9
 
10
10
  ##
11
11
  # Lists the set of fine-tuning jobs for this API key and (optionally) organization.
12
12
  ##
13
13
  def list
14
- http_get(path: URI_PREFIX)
14
+ rest_index(resource: RESOURCE)
15
15
  end
16
16
 
17
17
  ##
@@ -23,7 +23,8 @@ module Asimov
23
23
  def create(training_file:, parameters: {})
24
24
  raise MissingRequiredParameterError.new(:training_file) unless training_file
25
25
 
26
- json_post(path: URI_PREFIX, parameters: parameters.merge(training_file: training_file))
26
+ rest_create_w_json_params(resource: RESOURCE,
27
+ parameters: parameters.merge(training_file: training_file))
27
28
  end
28
29
 
29
30
  ##
@@ -32,7 +33,7 @@ module Asimov
32
33
  # @param [String] fine_tune_id the id of fine tuning job
33
34
  ##
34
35
  def retrieve(fine_tune_id:)
35
- http_get(path: "#{URI_PREFIX}/#{fine_tune_id}")
36
+ rest_get(resource: RESOURCE, id: fine_tune_id)
36
37
  end
37
38
 
38
39
  ##
@@ -41,7 +42,7 @@ module Asimov
41
42
  # @param [String] fine_tune_id the id of fine tuning job
42
43
  ##
43
44
  def cancel(fine_tune_id:)
44
- multipart_post(path: "#{URI_PREFIX}/#{fine_tune_id}/cancel")
45
+ rest_create_w_multipart_params(resource: [RESOURCE, fine_tune_id, "cancel"])
45
46
  end
46
47
 
47
48
  ##
@@ -50,7 +51,7 @@ module Asimov
50
51
  # @param [String] fine_tune_id the id of fine tuning job
51
52
  ##
52
53
  def list_events(fine_tune_id:)
53
- http_get(path: "#{URI_PREFIX}/#{fine_tune_id}/events")
54
+ rest_index(resource: [RESOURCE, fine_tune_id, "events"])
54
55
  end
55
56
  end
56
57
  end
@@ -6,8 +6,8 @@ module Asimov
6
6
  # Class interface for API methods in the "/images" URI subspace.
7
7
  ##
8
8
  class Images < Base
9
- URI_PREFIX = "/images".freeze
10
- private_constant :URI_PREFIX
9
+ RESOURCE = "images".freeze
10
+ private_constant :RESOURCE
11
11
 
12
12
  ##
13
13
  # Creates an image using the specified prompt.
@@ -18,8 +18,8 @@ module Asimov
18
18
  def create(prompt:, parameters: {})
19
19
  raise MissingRequiredParameterError.new(:prompt) unless prompt
20
20
 
21
- json_post(path: "#{URI_PREFIX}/generations",
22
- parameters: parameters.merge({ prompt: prompt }))
21
+ rest_create_w_json_params(resource: [RESOURCE, "generations"],
22
+ parameters: parameters.merge({ prompt: prompt }))
23
23
  end
24
24
 
25
25
  ##
@@ -32,8 +32,11 @@ module Asimov
32
32
  def create_edit(image:, prompt:, parameters: {})
33
33
  raise MissingRequiredParameterError.new(:prompt) unless prompt
34
34
 
35
- multipart_post(path: "#{URI_PREFIX}/edits",
36
- parameters: open_files(parameters.merge({ image: image, prompt: prompt })))
35
+ rest_create_w_multipart_params(resource: [RESOURCE, "edits"],
36
+ parameters: open_files(parameters.merge({
37
+ image: image,
38
+ prompt: prompt
39
+ })))
37
40
  end
38
41
 
39
42
  ##
@@ -44,8 +47,8 @@ module Asimov
44
47
  # @option parameters [String] :mask mask file name or a File-like object
45
48
  ##
46
49
  def create_variation(image:, parameters: {})
47
- multipart_post(path: "#{URI_PREFIX}/variations",
48
- parameters: open_files(parameters.merge({ image: image })))
50
+ rest_create_w_multipart_params(resource: [RESOURCE, "variations"],
51
+ parameters: open_files(parameters.merge({ image: image })))
49
52
  end
50
53
 
51
54
  private
@@ -54,7 +57,7 @@ module Asimov
54
57
  raise MissingRequiredParameterError.new(:image) unless parameters[:image]
55
58
 
56
59
  parameters = parameters.merge(image: Utils::FileManager.open(parameters[:image]))
57
- parameters.merge!(mask: Utils::FileManager.open(parameters[:mask])) if parameters[:mask]
60
+ parameters[:mask] = Utils::FileManager.open(parameters[:mask]) if parameters[:mask]
58
61
  parameters
59
62
  end
60
63
  end
@@ -4,15 +4,15 @@ module Asimov
4
4
  # Class interface for API methods in the "/models" URI subspace.
5
5
  ##
6
6
  class Models < Base
7
- URI_PREFIX = "/models".freeze
8
- private_constant :URI_PREFIX
7
+ RESOURCE = "models".freeze
8
+ private_constant :RESOURCE
9
9
 
10
10
  ##
11
11
  # Lists the models accessible to this combination of OpenAI API
12
12
  # key and organization id.
13
13
  ##
14
14
  def list
15
- http_get(path: URI_PREFIX)
15
+ rest_index(resource: RESOURCE)
16
16
  end
17
17
 
18
18
  ##
@@ -22,7 +22,7 @@ module Asimov
22
22
  # @param [String] model_id the id of the model to be retrieved
23
23
  ##
24
24
  def retrieve(model_id:)
25
- http_get(path: "#{URI_PREFIX}/#{model_id}")
25
+ rest_get(resource: RESOURCE, id: model_id)
26
26
  end
27
27
 
28
28
  ##
@@ -32,7 +32,7 @@ module Asimov
32
32
  # @param [String] model_id the id of the model to be deleted
33
33
  ##
34
34
  def delete(model_id:)
35
- http_delete(path: "#{URI_PREFIX}/#{model_id}")
35
+ rest_delete(resource: RESOURCE, id: model_id)
36
36
  end
37
37
  end
38
38
  end
@@ -13,7 +13,8 @@ module Asimov
13
13
  def create(input:, parameters: {})
14
14
  raise MissingRequiredParameterError.new(:input) unless input
15
15
 
16
- json_post(path: "/moderations", parameters: parameters.merge({ input: input }))
16
+ rest_create_w_json_params(resource: "moderations",
17
+ parameters: parameters.merge({ input: input }))
17
18
  end
18
19
  end
19
20
  end
data/lib/asimov/client.rb CHANGED
@@ -3,6 +3,8 @@ require "httparty"
3
3
  require_relative "headers_factory"
4
4
  require_relative "utils/request_options_validator"
5
5
  require_relative "api_v1/base"
6
+ require_relative "api_v1/audio"
7
+ require_relative "api_v1/chat"
6
8
  require_relative "api_v1/completions"
7
9
  require_relative "api_v1/edits"
8
10
  require_relative "api_v1/embeddings"
@@ -20,7 +22,7 @@ module Asimov
20
22
  class Client
21
23
  extend Forwardable
22
24
 
23
- attr_reader :api_key, :organization_id, :api_version, :request_options
25
+ attr_reader :api_key, :organization_id, :api_version, :request_options, :openai_api_base
24
26
 
25
27
  ##
26
28
  # Creates a new Asimov::Client. Includes several optional named parameters:
@@ -29,17 +31,36 @@ module Asimov
29
31
  # defaults to the application-wide configuration
30
32
  # organization_id - The OpenAI organization identifier that this Asimov::Client instance
31
33
  # will use. If unspecified, defaults to the application-wide configuration.
34
+ # request_options - HTTParty request options that will be passed to the underlying network
35
+ # client. Merges (and overrides) global configuration value.
36
+ # openai_api_base - Custom base URI for the API calls made by this client. Defaults to global
37
+ # configuration value.
32
38
  ##
33
39
  def initialize(api_key: nil, organization_id: HeadersFactory::NULL_ORGANIZATION_ID,
34
- request_options: {})
40
+ request_options: {}, openai_api_base: nil)
35
41
  @headers_factory = HeadersFactory.new(api_key,
36
42
  organization_id)
37
43
  @request_options = Asimov.configuration.request_options
38
44
  .merge(Utils::RequestOptionsValidator.validate(request_options))
39
45
  .freeze
46
+ initialize_openai_api_base(openai_api_base)
40
47
  end
41
48
  def_delegators :@headers_factory, :api_key, :organization_id, :headers
42
49
 
50
+ ##
51
+ # Use the audio method to access API calls in the /audio URI space.
52
+ ##
53
+ def audio
54
+ @audio ||= Asimov::ApiV1::Audio.new(client: self)
55
+ end
56
+
57
+ ##
58
+ # Use the chat method to access API calls in the /chat URI space.
59
+ ##
60
+ def chat
61
+ @chat ||= Asimov::ApiV1::Chat.new(client: self)
62
+ end
63
+
43
64
  ##
44
65
  # Use the completions method to access API calls in the /completions URI space.
45
66
  ##
@@ -95,5 +116,17 @@ module Asimov
95
116
  def moderations
96
117
  @moderations ||= Asimov::ApiV1::Moderations.new(client: self)
97
118
  end
119
+
120
+ private
121
+
122
+ def initialize_openai_api_base(openai_api_base)
123
+ @openai_api_base = openai_api_base || Asimov.configuration.openai_api_base
124
+ if @openai_api_base
125
+ @openai_api_base = HTTParty.normalize_base_uri(@openai_api_base)
126
+ else
127
+ raise Asimov::MissingBaseUriError,
128
+ "No API Base URI was provided for this client."
129
+ end
130
+ end
98
131
  end
99
132
  end
@@ -9,7 +9,9 @@ module Asimov
9
9
  class Configuration
10
10
  attr_accessor :api_key, :organization_id
11
11
 
12
- attr_reader :request_options
12
+ attr_reader :request_options, :openai_api_base
13
+
14
+ DEFAULT_OPENAI_BASE_URI = "https://api.openai.com/v1".freeze
13
15
 
14
16
  ##
15
17
  # Initializes the Configuration object and resets it to default values.
@@ -25,6 +27,15 @@ module Asimov
25
27
  @api_key = nil
26
28
  @organization_id = nil
27
29
  @request_options = {}
30
+ @openai_api_base = DEFAULT_OPENAI_BASE_URI
31
+ end
32
+
33
+ ##
34
+ # Sets the openai_api_base on the Configuration. Typically not invoked
35
+ # directly, but rather through use of `Asimov.configure`.
36
+ ##
37
+ def openai_api_base=(val)
38
+ @openai_api_base = val ? HTTParty.normalize_base_uri(val) : nil
28
39
  end
29
40
 
30
41
  ##
data/lib/asimov/error.rb CHANGED
@@ -15,6 +15,12 @@ module Asimov
15
15
  ##
16
16
  class MissingApiKeyError < ConfigurationError; end
17
17
 
18
+ ##
19
+ # Error that occurs when there is no configured
20
+ # base URI for a newly created Asimov::Client.
21
+ ##
22
+ class MissingBaseUriError < ConfigurationError; end
23
+
18
24
  ##
19
25
  # Errors that occur when making an API request. They
20
26
  # can occur either through local validation or
@@ -136,6 +142,12 @@ module Asimov
136
142
  ##
137
143
  class InvalidClassificationError < FileDataError; end
138
144
 
145
+ ##
146
+ # Error that occurs when the provided chat messages
147
+ # parameter is not valid.
148
+ ##
149
+ class InvalidChatMessagesError < RequestError; end
150
+
139
151
  ##
140
152
  # Error that occurs when an invalid value is provided
141
153
  # for an expected parameter in a request.
@@ -0,0 +1,63 @@
1
+ module Asimov
2
+ module Utils
3
+ ##
4
+ # Validator for OpenAI's chat message format
5
+ ##
6
+ class ChatMessagesValidator
7
+ def self.validate_and_normalize(messages)
8
+ new.validate(messages)
9
+ end
10
+
11
+ def validate(messages)
12
+ raise InvalidChatMessagesError, "Chat messages cannot be nil." if messages.nil?
13
+
14
+ unless messages.is_a?(Array)
15
+ raise InvalidChatMessagesError,
16
+ "Chat messages must be an array."
17
+ end
18
+
19
+ messages.map do |message|
20
+ validate_message(normalize_parsed(message))
21
+ end
22
+ rescue JSON::ParserError
23
+ raise InvalidChatMessagesError, "Chat messages must be valid JSON."
24
+ end
25
+
26
+ def validate_message(message)
27
+ raise InvalidChatMessagesError, "Chat messages must be hashes." unless message.is_a?(Hash)
28
+
29
+ validate_role(message["role"])
30
+
31
+ content = message["content"]
32
+ raise InvalidChatMessagesError, "Chat messages must have content." if content.nil?
33
+
34
+ validate_keys(message)
35
+ message
36
+ end
37
+
38
+ def validate_keys(json)
39
+ additional_keys = json.keys - %w[role content]
40
+ return if additional_keys.empty?
41
+
42
+ raise InvalidChatMessagesError,
43
+ "Chat messages must not have additional keys - #{additional_keys.join(', ')}."
44
+ end
45
+
46
+ ALLOWED_ROLES = %w[assistant system user].freeze
47
+ def validate_role(role)
48
+ raise InvalidChatMessagesError, "Chat messages must have a role." if role.nil?
49
+
50
+ return true if ALLOWED_ROLES.include?(role)
51
+
52
+ raise InvalidChatMessagesError,
53
+ "The value '#{role}' is not a valid role for a chat message."
54
+ end
55
+
56
+ def normalize_parsed(message)
57
+ return message if message.is_a?(String)
58
+
59
+ JSON.parse(message.respond_to?(:to_json) ? message.to_json : message)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,4 +1,4 @@
1
1
  module Asimov
2
2
  # Current gem version
3
- VERSION = "1.0.0".freeze
3
+ VERSION = "1.1.0".freeze
4
4
  end
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: 1.0.0
4
+ version: 1.1.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-25 00:00:00.000000000 Z
11
+ date: 2023-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -44,7 +44,9 @@ files:
44
44
  - README.md
45
45
  - lib/asimov.rb
46
46
  - lib/asimov/api_v1/api_error_translator.rb
47
+ - lib/asimov/api_v1/audio.rb
47
48
  - lib/asimov/api_v1/base.rb
49
+ - lib/asimov/api_v1/chat.rb
48
50
  - lib/asimov/api_v1/completions.rb
49
51
  - lib/asimov/api_v1/edits.rb
50
52
  - lib/asimov/api_v1/embeddings.rb
@@ -58,6 +60,7 @@ files:
58
60
  - lib/asimov/configuration.rb
59
61
  - lib/asimov/error.rb
60
62
  - lib/asimov/headers_factory.rb
63
+ - lib/asimov/utils/chat_messages_validator.rb
61
64
  - lib/asimov/utils/classifications_file_validator.rb
62
65
  - lib/asimov/utils/file_manager.rb
63
66
  - lib/asimov/utils/jsonl_validator.rb
@@ -88,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
91
  - !ruby/object:Gem::Version
89
92
  version: '0'
90
93
  requirements: []
91
- rubygems_version: 3.4.5
94
+ rubygems_version: 3.4.7
92
95
  signing_key:
93
96
  specification_version: 4
94
97
  summary: A Ruby client for the OpenAI API support for multiple API configurations