openai.rb 0.0.0 → 0.0.1
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 +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +4 -4
- data/Gemfile.lock +17 -14
- data/README.md +401 -0
- data/bin/codegen +64 -55
- data/bin/console +7 -1
- data/lib/openai/api/cache.rb +137 -0
- data/lib/openai/api/client.rb +86 -0
- data/lib/openai/api/resource.rb +235 -0
- data/lib/openai/api/response.rb +352 -0
- data/lib/openai/api.rb +61 -0
- data/lib/openai/chat.rb +75 -0
- data/lib/openai/tokenizer.rb +50 -0
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +29 -358
- data/openai.gemspec +7 -3
- data/spec/data/sample_french.mp3 +0 -0
- data/spec/data/sample_image.png +0 -0
- data/spec/data/sample_image_mask.png +0 -0
- data/spec/shared/api_resource_context.rb +22 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/unit/openai/api/audio_spec.rb +78 -0
- data/spec/unit/openai/api/cache_spec.rb +115 -0
- data/spec/unit/openai/api/chat_completions_spec.rb +116 -0
- data/spec/unit/openai/api/completions_spec.rb +119 -0
- data/spec/unit/openai/api/edits_spec.rb +40 -0
- data/spec/unit/openai/api/embeddings_spec.rb +45 -0
- data/spec/unit/openai/api/files_spec.rb +163 -0
- data/spec/unit/openai/api/fine_tunes_spec.rb +322 -0
- data/spec/unit/openai/api/images_spec.rb +137 -0
- data/spec/unit/openai/api/models_spec.rb +98 -0
- data/spec/unit/openai/api/moderations_spec.rb +61 -0
- data/spec/unit/openai/api/response_spec.rb +203 -0
- data/spec/unit/openai/tokenizer_spec.rb +45 -0
- data/spec/unit/openai_spec.rb +47 -736
- metadata +83 -2
@@ -0,0 +1,352 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class OpenAI
|
4
|
+
class API
|
5
|
+
class Response
|
6
|
+
include Concord.new(:internal_data)
|
7
|
+
include AbstractType
|
8
|
+
|
9
|
+
class MissingFieldError < StandardError
|
10
|
+
include Anima.new(:path, :missing_key, :actual_payload)
|
11
|
+
|
12
|
+
def message
|
13
|
+
<<~ERROR
|
14
|
+
Missing field #{missing_key.inspect} in response payload!
|
15
|
+
Was attempting to access value at path `#{path}`.
|
16
|
+
Payload: #{JSON.pretty_generate(actual_payload)}
|
17
|
+
ERROR
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_accessor :field_registry
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.register_field(field_name)
|
28
|
+
self.field_registry ||= []
|
29
|
+
field_registry << field_name
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.from_json(raw_json)
|
33
|
+
new(JSON.parse(raw_json, symbolize_names: true))
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(internal_data)
|
37
|
+
super(IceNine.deep_freeze(internal_data))
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.field(name, path: [name], wrapper: nil)
|
41
|
+
register_field(name)
|
42
|
+
|
43
|
+
define_method(name) do
|
44
|
+
field(path, wrapper: wrapper)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.optional_field(name, path: name, wrapper: nil)
|
49
|
+
register_field(name)
|
50
|
+
|
51
|
+
define_method(name) do
|
52
|
+
optional_field(path, wrapper: wrapper)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def original_payload
|
57
|
+
internal_data
|
58
|
+
end
|
59
|
+
|
60
|
+
def inspect
|
61
|
+
attr_list = field_list.map do |field_name|
|
62
|
+
"#{field_name}=#{__send__(field_name).inspect}"
|
63
|
+
end.join(' ')
|
64
|
+
"#<#{self.class} #{attr_list}>"
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# We need to access the registry list from the instance for `#inspect`.
|
70
|
+
# It is just private in terms of the public API which is why we do this
|
71
|
+
# weird private dispatch on our own class.
|
72
|
+
def field_list
|
73
|
+
self.class.__send__(:field_registry)
|
74
|
+
end
|
75
|
+
|
76
|
+
def optional_field(key_path, wrapper: nil)
|
77
|
+
*head, tail = key_path
|
78
|
+
|
79
|
+
parent = field(head)
|
80
|
+
return unless parent.key?(tail)
|
81
|
+
|
82
|
+
wrap_value(parent.fetch(tail), wrapper)
|
83
|
+
end
|
84
|
+
|
85
|
+
def field(key_path, wrapper: nil)
|
86
|
+
value = key_path.reduce(internal_data) do |object, key|
|
87
|
+
object.fetch(key) do
|
88
|
+
raise MissingFieldError.new(
|
89
|
+
path: key_path,
|
90
|
+
missing_key: key,
|
91
|
+
actual_payload: internal_data
|
92
|
+
)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
wrap_value(value, wrapper)
|
97
|
+
end
|
98
|
+
|
99
|
+
def wrap_value(value, wrapper)
|
100
|
+
return value unless wrapper
|
101
|
+
|
102
|
+
if value.instance_of?(Array)
|
103
|
+
value.map { |item| wrapper.new(item) }
|
104
|
+
else
|
105
|
+
wrapper.new(value)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class Completion < Response
|
110
|
+
class Choice < Response
|
111
|
+
field :text
|
112
|
+
field :index
|
113
|
+
field :logprobs
|
114
|
+
field :finish_reason
|
115
|
+
end
|
116
|
+
|
117
|
+
class Usage < Response
|
118
|
+
field :prompt_tokens
|
119
|
+
field :completion_tokens
|
120
|
+
field :total_tokens
|
121
|
+
end
|
122
|
+
|
123
|
+
field :id
|
124
|
+
field :object
|
125
|
+
field :created
|
126
|
+
field :model
|
127
|
+
field :choices, wrapper: Choice
|
128
|
+
optional_field :usage, wrapper: Usage
|
129
|
+
end
|
130
|
+
|
131
|
+
class ChatCompletion < Response
|
132
|
+
class Choice < Response
|
133
|
+
class Message < Response
|
134
|
+
field :role
|
135
|
+
field :content
|
136
|
+
end
|
137
|
+
|
138
|
+
field :index
|
139
|
+
field :message, wrapper: Message
|
140
|
+
field :finish_reason
|
141
|
+
end
|
142
|
+
|
143
|
+
class Usage < Response
|
144
|
+
field :prompt_tokens
|
145
|
+
field :completion_tokens
|
146
|
+
field :total_tokens
|
147
|
+
end
|
148
|
+
|
149
|
+
field :id
|
150
|
+
field :object
|
151
|
+
field :created
|
152
|
+
field :choices, wrapper: Choice
|
153
|
+
field :usage, wrapper: Usage
|
154
|
+
end
|
155
|
+
|
156
|
+
class ChatCompletionChunk < Response
|
157
|
+
class Delta < Response
|
158
|
+
optional_field :role
|
159
|
+
optional_field :_content, path: %i[content]
|
160
|
+
|
161
|
+
def content
|
162
|
+
_content.to_s
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
class Choice < Response
|
167
|
+
field :delta, wrapper: Delta
|
168
|
+
end
|
169
|
+
|
170
|
+
field :id
|
171
|
+
field :object
|
172
|
+
field :created
|
173
|
+
field :model
|
174
|
+
field :choices, wrapper: Choice
|
175
|
+
end
|
176
|
+
|
177
|
+
class Embedding < Response
|
178
|
+
class EmbeddingData < Response
|
179
|
+
field :object
|
180
|
+
field :embedding
|
181
|
+
field :index
|
182
|
+
end
|
183
|
+
|
184
|
+
class Usage < Response
|
185
|
+
field :prompt_tokens
|
186
|
+
field :total_tokens
|
187
|
+
end
|
188
|
+
|
189
|
+
field :object
|
190
|
+
field :data, wrapper: EmbeddingData
|
191
|
+
field :model
|
192
|
+
field :usage, wrapper: Usage
|
193
|
+
end
|
194
|
+
|
195
|
+
class Model < Response
|
196
|
+
field :id
|
197
|
+
field :object
|
198
|
+
field :owned_by
|
199
|
+
field :permission
|
200
|
+
end
|
201
|
+
|
202
|
+
class Moderation < Response
|
203
|
+
class Category < Response
|
204
|
+
field :hate
|
205
|
+
field :hate_threatening, path: %i[hate/threatening]
|
206
|
+
field :self_harm, path: %i[self-harm]
|
207
|
+
field :sexual
|
208
|
+
field :sexual_minors, path: %i[sexual/minors]
|
209
|
+
field :violence
|
210
|
+
field :violence_graphic, path: %i[violence/graphic]
|
211
|
+
end
|
212
|
+
|
213
|
+
class CategoryScore < Response
|
214
|
+
field :hate
|
215
|
+
field :hate_threatening, path: %i[hate/threatening]
|
216
|
+
field :self_harm, path: %i[self-harm]
|
217
|
+
field :sexual
|
218
|
+
field :sexual_minors, path: %i[sexual/minors]
|
219
|
+
field :violence
|
220
|
+
field :violence_graphic, path: %i[violence/graphic]
|
221
|
+
end
|
222
|
+
|
223
|
+
class Result < Response
|
224
|
+
field :categories, wrapper: Category
|
225
|
+
field :category_scores, wrapper: CategoryScore
|
226
|
+
field :flagged
|
227
|
+
end
|
228
|
+
|
229
|
+
field :id
|
230
|
+
field :model
|
231
|
+
field :results, wrapper: Result
|
232
|
+
end
|
233
|
+
|
234
|
+
class ListModel < Response
|
235
|
+
field :data, wrapper: Model
|
236
|
+
end
|
237
|
+
|
238
|
+
class Edit < Response
|
239
|
+
class Choice < Response
|
240
|
+
field :text
|
241
|
+
field :index
|
242
|
+
end
|
243
|
+
|
244
|
+
class Usage < Response
|
245
|
+
field :prompt_tokens
|
246
|
+
field :completion_tokens
|
247
|
+
field :total_tokens
|
248
|
+
end
|
249
|
+
|
250
|
+
field :object
|
251
|
+
field :created
|
252
|
+
field :choices, wrapper: Choice
|
253
|
+
field :usage, wrapper: Usage
|
254
|
+
end
|
255
|
+
|
256
|
+
class ImageGeneration < Response
|
257
|
+
class Image < Response
|
258
|
+
field :url
|
259
|
+
end
|
260
|
+
|
261
|
+
field :created
|
262
|
+
field :data, wrapper: Image
|
263
|
+
end
|
264
|
+
|
265
|
+
class ImageEdit < Response
|
266
|
+
class ImageEditData < Response
|
267
|
+
field :url
|
268
|
+
end
|
269
|
+
|
270
|
+
field :created
|
271
|
+
field :data, wrapper: ImageEditData
|
272
|
+
end
|
273
|
+
|
274
|
+
class ImageVariation < Response
|
275
|
+
class ImageVariationData < Response
|
276
|
+
field :url
|
277
|
+
end
|
278
|
+
|
279
|
+
field :created
|
280
|
+
field :data, wrapper: ImageVariationData
|
281
|
+
end
|
282
|
+
|
283
|
+
class File < Response
|
284
|
+
field :id
|
285
|
+
field :object
|
286
|
+
field :bytes
|
287
|
+
field :created_at
|
288
|
+
field :filename
|
289
|
+
field :purpose
|
290
|
+
optional_field :deleted?, path: :deleted
|
291
|
+
end
|
292
|
+
|
293
|
+
class FileList < Response
|
294
|
+
field :data, wrapper: File
|
295
|
+
field :object
|
296
|
+
end
|
297
|
+
|
298
|
+
class FineTune < Response
|
299
|
+
class Event < Response
|
300
|
+
field :object
|
301
|
+
field :created_at
|
302
|
+
field :level
|
303
|
+
field :message
|
304
|
+
end
|
305
|
+
|
306
|
+
class Hyperparams < Response
|
307
|
+
field :batch_size
|
308
|
+
field :learning_rate_multiplier
|
309
|
+
field :n_epochs
|
310
|
+
field :prompt_loss_weight
|
311
|
+
end
|
312
|
+
|
313
|
+
class File < Response
|
314
|
+
field :id
|
315
|
+
field :object
|
316
|
+
field :bytes
|
317
|
+
field :created_at
|
318
|
+
field :filename
|
319
|
+
field :purpose
|
320
|
+
end
|
321
|
+
|
322
|
+
field :id
|
323
|
+
field :object
|
324
|
+
field :model
|
325
|
+
field :created_at
|
326
|
+
field :events, wrapper: Event
|
327
|
+
field :fine_tuned_model
|
328
|
+
field :hyperparams, wrapper: Hyperparams
|
329
|
+
field :organization_id
|
330
|
+
field :result_files, wrapper: File
|
331
|
+
field :status
|
332
|
+
field :validation_files, wrapper: File
|
333
|
+
field :training_files, wrapper: File
|
334
|
+
field :updated_at
|
335
|
+
end
|
336
|
+
|
337
|
+
class FineTuneList < Response
|
338
|
+
field :object
|
339
|
+
field :data, wrapper: FineTune
|
340
|
+
end
|
341
|
+
|
342
|
+
class FineTuneEventList < Response
|
343
|
+
field :data, wrapper: FineTune::Event
|
344
|
+
field :object
|
345
|
+
end
|
346
|
+
|
347
|
+
class Transcription < Response
|
348
|
+
field :text
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
data/lib/openai/api.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class OpenAI
|
4
|
+
class API
|
5
|
+
include Concord.new(:client)
|
6
|
+
|
7
|
+
class Error < StandardError
|
8
|
+
include Concord::Public.new(:http_response)
|
9
|
+
|
10
|
+
def message
|
11
|
+
<<~ERROR
|
12
|
+
Unexpected response status! Expected 2xx but got: #{http_response.status}
|
13
|
+
|
14
|
+
Body:
|
15
|
+
|
16
|
+
#{http_response.body}
|
17
|
+
ERROR
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def completions
|
22
|
+
API::Completion.new(client)
|
23
|
+
end
|
24
|
+
|
25
|
+
def chat_completions
|
26
|
+
API::ChatCompletion.new(client)
|
27
|
+
end
|
28
|
+
|
29
|
+
def embeddings
|
30
|
+
API::Embedding.new(client)
|
31
|
+
end
|
32
|
+
|
33
|
+
def models
|
34
|
+
API::Model.new(client)
|
35
|
+
end
|
36
|
+
|
37
|
+
def edits
|
38
|
+
API::Edit.new(client)
|
39
|
+
end
|
40
|
+
|
41
|
+
def files
|
42
|
+
API::File.new(client)
|
43
|
+
end
|
44
|
+
|
45
|
+
def fine_tunes
|
46
|
+
API::FineTune.new(client)
|
47
|
+
end
|
48
|
+
|
49
|
+
def images
|
50
|
+
API::Image.new(client)
|
51
|
+
end
|
52
|
+
|
53
|
+
def audio
|
54
|
+
API::Audio.new(client)
|
55
|
+
end
|
56
|
+
|
57
|
+
def moderations
|
58
|
+
API::Moderation.new(client)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/openai/chat.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class OpenAI
|
4
|
+
class Chat
|
5
|
+
include Anima.new(:messages, :settings, :api)
|
6
|
+
|
7
|
+
def initialize(messages:, **kwargs)
|
8
|
+
messages = messages.map do |msg|
|
9
|
+
if msg.is_a?(Hash)
|
10
|
+
Message.new(msg)
|
11
|
+
else
|
12
|
+
msg
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
super(messages: messages, **kwargs)
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_user_message(message)
|
20
|
+
add_message('user', message)
|
21
|
+
end
|
22
|
+
alias user add_user_message
|
23
|
+
|
24
|
+
def add_system_message(message)
|
25
|
+
add_message('system', message)
|
26
|
+
end
|
27
|
+
alias system add_system_message
|
28
|
+
|
29
|
+
def add_assistant_message(message)
|
30
|
+
add_message('assistant', message)
|
31
|
+
end
|
32
|
+
alias assistant add_assistant_message
|
33
|
+
|
34
|
+
def submit
|
35
|
+
response = api.chat_completions.create(
|
36
|
+
**settings,
|
37
|
+
messages: raw_messages
|
38
|
+
)
|
39
|
+
|
40
|
+
msg = response.choices.first.message
|
41
|
+
|
42
|
+
add_message(msg.role, msg.content)
|
43
|
+
end
|
44
|
+
|
45
|
+
def last_message
|
46
|
+
API::Response::ChatCompletion::Choice::Message.new(messages.last)
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_log_format
|
50
|
+
messages.map(&:to_log_format).join("\n\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def raw_messages
|
56
|
+
messages.map(&:to_h)
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_message(role, content)
|
60
|
+
with_message(role: role, content: content)
|
61
|
+
end
|
62
|
+
|
63
|
+
def with_message(message)
|
64
|
+
with(messages: messages + [message])
|
65
|
+
end
|
66
|
+
|
67
|
+
class Message
|
68
|
+
include Anima.new(:role, :content)
|
69
|
+
|
70
|
+
def to_log_format
|
71
|
+
"#{role.upcase}: #{content}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class OpenAI
|
4
|
+
class Tokenizer
|
5
|
+
include Equalizer.new
|
6
|
+
|
7
|
+
UnknownModel = Class.new(StandardError)
|
8
|
+
UnknownEncoding = Class.new(StandardError)
|
9
|
+
|
10
|
+
def for_model(model)
|
11
|
+
encoding = Tiktoken.encoding_for_model(model)
|
12
|
+
raise UnknownModel, "Invalid model name or not recognized by Tiktoken: #{model.inspect}" if encoding.nil?
|
13
|
+
|
14
|
+
Encoding.new(encoding.name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(encoding_name)
|
18
|
+
encoding = Tiktoken.get_encoding(encoding_name)
|
19
|
+
if encoding.nil?
|
20
|
+
raise UnknownEncoding,
|
21
|
+
"Invalid encoding name or not recognized by Tiktoken: #{encoding_name.inspect}"
|
22
|
+
end
|
23
|
+
|
24
|
+
Encoding.new(encoding.name)
|
25
|
+
end
|
26
|
+
|
27
|
+
class Encoding
|
28
|
+
include Concord.new(:name)
|
29
|
+
|
30
|
+
def encode(text)
|
31
|
+
encoder.encode(text)
|
32
|
+
end
|
33
|
+
alias tokenize encode
|
34
|
+
|
35
|
+
def decode(tokens)
|
36
|
+
encoder.decode(tokens)
|
37
|
+
end
|
38
|
+
|
39
|
+
def num_tokens(text)
|
40
|
+
encode(text).size
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def encoder
|
46
|
+
Tiktoken.get_encoding(name)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/openai/version.rb
CHANGED