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.
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OpenAI
4
- VERSION = '0.0.0'
4
+ VERSION = '0.0.1'
5
5
  end