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.
@@ -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