openai.rb 0.0.0

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.
data/lib/openai.rb ADDED
@@ -0,0 +1,381 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concord'
4
+ require 'anima'
5
+ require 'http'
6
+ require 'addressable'
7
+
8
+ require 'openai/version'
9
+
10
+ class OpenAI
11
+ include Concord.new(:api_key, :http)
12
+
13
+ ResponseError = Class.new(StandardError)
14
+
15
+ HOST = Addressable::URI.parse('https://api.openai.com/v1')
16
+
17
+ def initialize(api_key, http: HTTP)
18
+ super(api_key, http)
19
+ end
20
+
21
+ def create_completion(model:, **kwargs)
22
+ Response::Completion.from_json(
23
+ post('/v1/completions', model: model, **kwargs)
24
+ )
25
+ end
26
+
27
+ def create_chat_completion(model:, messages:, **kwargs)
28
+ Response::ChatCompletion.from_json(
29
+ post('/v1/chat/completions', model: model, messages: messages, **kwargs)
30
+ )
31
+ end
32
+
33
+ def create_embedding(model:, input:, **kwargs)
34
+ Response::Embedding.from_json(
35
+ post('/v1/embeddings', model: model, input: input, **kwargs)
36
+ )
37
+ end
38
+
39
+ def list_models
40
+ Response::ListModel.from_json(get('/v1/models'))
41
+ end
42
+
43
+ def get_model(model_id)
44
+ Response::Model.from_json(
45
+ get("/v1/models/#{model_id}")
46
+ )
47
+ end
48
+
49
+ def create_edit(model:, instruction:, **kwargs)
50
+ Response::Edit.from_json(
51
+ post('/v1/edits', model: model, instruction: instruction, **kwargs)
52
+ )
53
+ end
54
+
55
+ def create_image_generation(prompt:, **kwargs)
56
+ Response::ImageGeneration.from_json(
57
+ post('/v1/images/generations', prompt: prompt, **kwargs)
58
+ )
59
+ end
60
+
61
+ def create_file(file:, purpose:)
62
+ absolute_path = Pathname.new(file).expand_path.to_s
63
+ form_file = HTTP::FormData::File.new(absolute_path)
64
+ Response::File.from_json(
65
+ post_form_multipart('/v1/files', file: form_file, purpose: purpose)
66
+ )
67
+ end
68
+
69
+ def list_files
70
+ Response::FileList.from_json(
71
+ get('/v1/files')
72
+ )
73
+ end
74
+
75
+ def delete_file(file_id)
76
+ Response::File.from_json(
77
+ delete("/v1/files/#{file_id}")
78
+ )
79
+ end
80
+
81
+ def get_file(file_id)
82
+ Response::File.from_json(
83
+ get("/v1/files/#{file_id}")
84
+ )
85
+ end
86
+
87
+ def get_file_content(file_id)
88
+ get("/v1/files/#{file_id}/content")
89
+ end
90
+
91
+ def list_fine_tunes
92
+ Response::FineTuneList.from_json(
93
+ get('/v1/fine-tunes')
94
+ )
95
+ end
96
+
97
+ def create_fine_tune(training_file:, **kwargs)
98
+ Response::FineTune.from_json(
99
+ post('/v1/fine-tunes', training_file: training_file, **kwargs)
100
+ )
101
+ end
102
+
103
+ def get_fine_tune(fine_tune_id)
104
+ Response::FineTune.from_json(
105
+ get("/v1/fine-tunes/#{fine_tune_id}")
106
+ )
107
+ end
108
+
109
+ def cancel_fine_tune(fine_tune_id)
110
+ Response::FineTune.from_json(
111
+ post("/v1/fine-tunes/#{fine_tune_id}/cancel")
112
+ )
113
+ end
114
+
115
+ def transcribe_audio(file:, model:, **kwargs)
116
+ absolute_path = Pathname.new(file).expand_path.to_s
117
+ form_file = HTTP::FormData::File.new(absolute_path)
118
+ Response::Transcription.from_json(
119
+ post_form_multipart(
120
+ '/v1/audio/transcriptions',
121
+ file: form_file,
122
+ model: model,
123
+ **kwargs
124
+ )
125
+ )
126
+ end
127
+
128
+ def inspect
129
+ "#<#{self.class}>"
130
+ end
131
+
132
+ private
133
+
134
+ def get(route)
135
+ unwrap_response(json_http_client.get(url_for(route)))
136
+ end
137
+
138
+ def delete(route)
139
+ unwrap_response(json_http_client.delete(url_for(route)))
140
+ end
141
+
142
+ def post(route, **body)
143
+ unwrap_response(json_http_client.post(url_for(route), json: body))
144
+ end
145
+
146
+ def post_form_multipart(route, **body)
147
+ unwrap_response(http_client.post(url_for(route), form: body))
148
+ end
149
+
150
+ def url_for(route)
151
+ HOST.join(route).to_str
152
+ end
153
+
154
+ def unwrap_response(response)
155
+ unless response.status.success?
156
+ raise ResponseError, "Unexpected response #{response.status}\nBody:\n#{response.body}"
157
+ end
158
+
159
+ response.body.to_s
160
+ end
161
+
162
+ def json_http_client
163
+ http_client.headers('Content-Type' => 'application/json')
164
+ end
165
+
166
+ def http_client
167
+ http.headers('Authorization' => "Bearer #{api_key}")
168
+ end
169
+
170
+ class Response
171
+ class JSONPayload
172
+ include Concord.new(:internal_data)
173
+
174
+ def self.from_json(raw_json)
175
+ new(JSON.parse(raw_json, symbolize_names: true))
176
+ end
177
+
178
+ def self.field(name, path: [name], wrapper: nil)
179
+ given_wrapper = wrapper
180
+ define_method(name) do
181
+ field(path, wrapper: given_wrapper)
182
+ end
183
+ end
184
+
185
+ def self.optional_field(name, path: name)
186
+ define_method(name) do
187
+ optional_field(path)
188
+ end
189
+ end
190
+
191
+ def original_payload
192
+ internal_data
193
+ end
194
+
195
+ private
196
+
197
+ def optional_field(key_path)
198
+ *head, tail = key_path
199
+
200
+ field(head)[tail]
201
+ end
202
+
203
+ def field(key_path, wrapper: nil)
204
+ value = key_path.reduce(internal_data, :fetch)
205
+ return value unless wrapper
206
+
207
+ if value.is_a?(Array)
208
+ value.map { |item| wrapper.new(item) }
209
+ else
210
+ wrapper.new(value)
211
+ end
212
+ end
213
+ end
214
+
215
+ class Completion < JSONPayload
216
+ class Choice < JSONPayload
217
+ field :text
218
+ field :index
219
+ field :logprobs
220
+ field :finish_reason
221
+ end
222
+
223
+ class Usage < JSONPayload
224
+ field :prompt_tokens
225
+ field :completion_tokens
226
+ field :total_tokens
227
+ end
228
+
229
+ field :id
230
+ field :object
231
+ field :created
232
+ field :model
233
+ field :choices, wrapper: Choice
234
+ field :usage, wrapper: Usage
235
+ end
236
+
237
+ class ChatCompletion < JSONPayload
238
+ class Choice < JSONPayload
239
+ class Message < JSONPayload
240
+ field :role
241
+ field :content
242
+ end
243
+
244
+ field :index
245
+ field :message, wrapper: Message
246
+ field :finish_reason
247
+ end
248
+
249
+ class Usage < JSONPayload
250
+ field :prompt_tokens
251
+ field :completion_tokens
252
+ field :total_tokens
253
+ end
254
+
255
+ field :id
256
+ field :object
257
+ field :created
258
+ field :choices, wrapper: Choice
259
+ field :usage, wrapper: Usage
260
+ end
261
+
262
+ class Embedding < JSONPayload
263
+ class EmbeddingData < JSONPayload
264
+ field :object
265
+ field :embedding
266
+ field :index
267
+ end
268
+
269
+ class Usage < JSONPayload
270
+ field :prompt_tokens
271
+ field :total_tokens
272
+ end
273
+
274
+ field :object
275
+ field :data, wrapper: EmbeddingData
276
+ field :model
277
+ field :usage, wrapper: Usage
278
+ end
279
+
280
+ class Model < JSONPayload
281
+ field :id
282
+ field :object
283
+ field :owned_by
284
+ field :permission
285
+ end
286
+
287
+ class ListModel < JSONPayload
288
+ field :data, wrapper: Model
289
+ end
290
+
291
+ class Edit < JSONPayload
292
+ class Choice < JSONPayload
293
+ field :text
294
+ field :index
295
+ end
296
+
297
+ class Usage < JSONPayload
298
+ field :prompt_tokens
299
+ field :completion_tokens
300
+ field :total_tokens
301
+ end
302
+
303
+ field :object
304
+ field :created
305
+ field :choices, wrapper: Choice
306
+ field :usage, wrapper: Usage
307
+ end
308
+
309
+ class ImageGeneration < JSONPayload
310
+ class Image < JSONPayload
311
+ field :url
312
+ end
313
+
314
+ field :created
315
+ field :data, wrapper: Image
316
+ end
317
+
318
+ class File < JSONPayload
319
+ field :id
320
+ field :object
321
+ field :bytes
322
+ field :created_at
323
+ field :filename
324
+ field :purpose
325
+ optional_field :deleted?, path: :deleted
326
+ end
327
+
328
+ class FileList < JSONPayload
329
+ field :data, wrapper: File
330
+ field :object
331
+ end
332
+
333
+ class FineTune < JSONPayload
334
+ class Event < JSONPayload
335
+ field :object
336
+ field :created_at
337
+ field :level
338
+ field :message
339
+ end
340
+
341
+ class Hyperparams < JSONPayload
342
+ field :batch_size
343
+ field :learning_rate_multiplier
344
+ field :n_epochs
345
+ field :prompt_loss_weight
346
+ end
347
+
348
+ class File < JSONPayload
349
+ field :id
350
+ field :object
351
+ field :bytes
352
+ field :created_at
353
+ field :filename
354
+ field :purpose
355
+ end
356
+
357
+ field :id
358
+ field :object
359
+ field :model
360
+ field :created_at
361
+ field :events, wrapper: Event
362
+ field :fine_tuned_model
363
+ field :hyperparams, wrapper: Hyperparams
364
+ field :organization_id
365
+ field :result_files, wrapper: File
366
+ field :status
367
+ field :validation_files, wrapper: File
368
+ field :training_files, wrapper: File
369
+ field :updated_at
370
+ end
371
+
372
+ class FineTuneList < JSONPayload
373
+ field :object
374
+ field :data, wrapper: FineTune
375
+ end
376
+
377
+ class Transcription < JSONPayload
378
+ field :text
379
+ end
380
+ end
381
+ end
data/openai.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('lib/openai/version', __dir__)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'openai.rb'
7
+ spec.version = OpenAI::VERSION
8
+ spec.authors = %w[John Backus]
9
+ spec.email = %w[johncbackus@gmail.com]
10
+
11
+ spec.summary = 'OpenAI Ruby Wrapper'
12
+ spec.description = spec.summary
13
+ spec.homepage = 'https://github.com/backus/openai-ruby'
14
+
15
+ spec.files = `git ls-files`.split("\n")
16
+ spec.require_paths = %w[lib]
17
+ spec.executables = []
18
+
19
+ spec.add_dependency 'anima', '~> 0.3'
20
+ spec.add_dependency 'concord', '~> 0.1'
21
+ spec.add_dependency 'http', '~> 5.1'
22
+ end
@@ -0,0 +1 @@
1
+ {"prompt": "Say hello", "completion": "hello"}
Binary file
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openai'
4
+ require 'pry'
5
+ require 'pry-byebug'
6
+
7
+ module OpenAISpec
8
+ ROOT = Pathname.new(__dir__).parent
9
+ SPEC_ROOT = ROOT.join('spec')
10
+ end
11
+
12
+ RSpec.configure do |config|
13
+ # Enable focused tests and run all tests if nothing is focused
14
+ config.filter_run_when_matching(:focus)
15
+
16
+ # Forbid RSpec from monkey patching any of our objects
17
+ config.disable_monkey_patching!
18
+
19
+ # We should address configuration warnings when we upgrade
20
+ config.raise_errors_for_deprecations!
21
+
22
+ # RSpec gives helpful warnings when you are doing something wrong.
23
+ # We should take their advice!
24
+ config.raise_on_warning = true
25
+
26
+ config.mock_with(:rspec) do |mocks|
27
+ # Verifies stubbed methods on real objects.
28
+ # @see https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles/partial-doubles
29
+ mocks.verify_partial_doubles = true
30
+ end
31
+
32
+ config.expect_with(:rspec) do |expectations|
33
+ # Set a higher max length for RSpec's test failure output, so it doesn't as aggressively
34
+ # truncate test failure explanations, obscuring the test failure reason in the process
35
+ # @note The default output length is 200 characters
36
+ # @see https://www.rubydoc.info/github/rspec/rspec-expectations/RSpec%2FExpectations%2FConfiguration:max_formatted_output_length=
37
+ expectations.max_formatted_output_length = 600
38
+ end
39
+
40
+ # Write rspec results to a temporary file so that we can use `rspec --only-failures`
41
+ config.example_status_persistence_file_path =
42
+ OpenAISpec::ROOT.join('tmp', 'rspec.txt').to_s
43
+
44
+ # Always aggregate failures when there are multiple expectations. This makes debugging tests with
45
+ # multiple expectations much easier because we can see the failures of each expectation together.
46
+ config.define_derived_metadata do |metadata|
47
+ metadata[:aggregate_failures] = true
48
+ end
49
+
50
+ # Define metadata for all tests which live under spec/unit
51
+ config.define_derived_metadata(file_path: %r{\bspec/unit/}) do |metadata|
52
+ # Set the type of these tests as 'unit'
53
+ metadata[:type] = :unit
54
+ end
55
+ end