asimov 1.0.0 → 1.1.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: 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