openai.rb 0.0.0

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