openai.rb 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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