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.
data/bin/console CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'openai'
@@ -9,7 +10,12 @@ require 'pry-byebug'
9
10
  Dotenv.load
10
11
 
11
12
  def start_repl
12
- api = OpenAI.new(ENV.fetch('OPENAI_API_KEY'))
13
+ cache_dir = Pathname.new(__dir__).parent.join('tmp/console_cache')
14
+ cache_dir.mkpath unless cache_dir.exist?
15
+ openai = OpenAI.create(
16
+ ENV.fetch('OPENAI_API_KEY'),
17
+ cache: cache_dir
18
+ )
13
19
  binding.pry
14
20
  end
15
21
 
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OpenAI
4
+ class API
5
+ class Cache
6
+ include Concord.new(:client, :cache)
7
+
8
+ def get(route)
9
+ read_cache_or_apply(verb: :get, route: route) do
10
+ client.get(route)
11
+ end
12
+ end
13
+
14
+ # Caching is a no-op for delete requests since caching a delete does
15
+ # not really make sense
16
+ def delete(...)
17
+ client.delete(...)
18
+ end
19
+
20
+ def post(route, **body)
21
+ read_cache_or_apply(verb: :post, route: route, body: body, format: :json) do
22
+ client.post(route, **body)
23
+ end
24
+ end
25
+
26
+ def post_form_multipart(route, **body)
27
+ read_cache_or_apply(verb: :post, route: route, body: body, format: :form) do
28
+ client.post_form_multipart(route, **body)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def read_cache_or_apply(...)
35
+ target = cache_target(...).unique_id
36
+
37
+ if cache.cached?(target)
38
+ cache.read(target)
39
+ else
40
+ yield.tap do |result|
41
+ cache.write(target, result)
42
+ end
43
+ end
44
+ end
45
+
46
+ def cache_target(verb:, route:, body: nil, format: nil)
47
+ Target.new(
48
+ verb: verb,
49
+ api_key: client.api_key,
50
+ route: route,
51
+ body: body,
52
+ format: format
53
+ )
54
+ end
55
+
56
+ class Target
57
+ include Anima.new(:verb, :api_key, :route, :body, :format)
58
+ include Memoizable
59
+
60
+ def unique_id
61
+ serialized = JSON.dump(serialize_for_cache)
62
+ digest = Digest::SHA256.hexdigest(serialized)
63
+ bare_route = route.delete_prefix('/v1/')
64
+ prefix = "#{verb}_#{bare_route}".gsub('/', '_')
65
+ fingerprint = digest.slice(0, 8)
66
+
67
+ "#{prefix}_#{fingerprint}"
68
+ end
69
+ memoize :unique_id
70
+
71
+ def serialize_for_cache
72
+ data = to_h
73
+ if data[:body]
74
+ data[:body] = data[:body].transform_values do |value|
75
+ if value.instance_of?(HTTP::FormData::File)
76
+ Digest::SHA256.hexdigest(value.to_s)
77
+ else
78
+ value
79
+ end
80
+ end
81
+ end
82
+ data
83
+ end
84
+ end
85
+
86
+ class Strategy
87
+ include AbstractType
88
+
89
+ abstract_method :cached?
90
+ abstract_method :read
91
+ abstract_method :write
92
+
93
+ class Memory < self
94
+ include Concord.new(:cache)
95
+
96
+ def initialize
97
+ super({})
98
+ end
99
+
100
+ def cached?(target)
101
+ cache.key?(target)
102
+ end
103
+
104
+ def write(target, result)
105
+ cache[target] = result
106
+ end
107
+
108
+ def read(target)
109
+ cache.fetch(target)
110
+ end
111
+ end
112
+
113
+ class FileSystem < self
114
+ include Concord.new(:cache_dir)
115
+
116
+ def cached?(target)
117
+ cache_file(target).file?
118
+ end
119
+
120
+ def write(target, result)
121
+ cache_file(target).write(result)
122
+ end
123
+
124
+ def read(target)
125
+ cache_file(target).read
126
+ end
127
+
128
+ private
129
+
130
+ def cache_file(target)
131
+ cache_dir.join("#{target}.json")
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OpenAI
4
+ class API
5
+ class Client
6
+ include Concord.new(:api_key, :http)
7
+
8
+ public :api_key
9
+
10
+ HOST = Addressable::URI.parse('https://api.openai.com/v1')
11
+
12
+ def initialize(api_key, http: HTTP)
13
+ super(api_key, http)
14
+ end
15
+
16
+ def inspect
17
+ "#<#{self.class}>"
18
+ end
19
+
20
+ def get(route)
21
+ unwrap_response(json_http_client.get(url_for(route)))
22
+ end
23
+
24
+ def delete(route)
25
+ unwrap_response(json_http_client.delete(url_for(route)))
26
+ end
27
+
28
+ def post(route, **body)
29
+ url = url_for(route)
30
+ if block_given?
31
+ json_http_client.persistent(url) do |connection|
32
+ response = connection.post(url, json: body)
33
+
34
+ # Data comes in as JSON frames like so:
35
+ #
36
+ # data: {"choices": [{"text": "He"}]}
37
+ # data: {"choices": [{"text": "llo, "}]}
38
+ # data: {"choices": [{"text": "Wor"}]}
39
+ # data: {"choices": [{"text": "ld!"}]}
40
+ # data: [DONE]
41
+ #
42
+ # (The actual frames are fully formed JSON objects just like a
43
+ # non-streamed response, the examples above are just for brevity)
44
+ response.body.each do |chunk|
45
+ chunk.split("\n\n").each do |part|
46
+ frame = part.delete_prefix('data: ').strip
47
+
48
+ yield(frame) unless frame == '[DONE]' || frame.empty?
49
+ end
50
+ end
51
+ end
52
+
53
+ # Return nil since we aren't reconstructing what the API would have
54
+ # returned if we had not streamed the response
55
+ nil
56
+ else
57
+ unwrap_response(json_http_client.post(url, json: body))
58
+ end
59
+ end
60
+
61
+ def post_form_multipart(route, **body)
62
+ unwrap_response(http_client.post(url_for(route), form: body))
63
+ end
64
+
65
+ private
66
+
67
+ def url_for(route)
68
+ HOST.join(route).to_str
69
+ end
70
+
71
+ def unwrap_response(response)
72
+ raise API::Error, response unless response.status.success?
73
+
74
+ response.body.to_str
75
+ end
76
+
77
+ def json_http_client
78
+ http_client.headers('Content-Type' => 'application/json')
79
+ end
80
+
81
+ def http_client
82
+ http.headers('Authorization' => "Bearer #{api_key}")
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OpenAI
4
+ class API
5
+ class Resource
6
+ include Concord.new(:client)
7
+ include AbstractType
8
+
9
+ private
10
+
11
+ def post(...)
12
+ client.post(...)
13
+ end
14
+
15
+ def post_form_multipart(...)
16
+ client.post_form_multipart(...)
17
+ end
18
+
19
+ def get(...)
20
+ client.get(...)
21
+ end
22
+
23
+ def form_file(path)
24
+ absolute_path = Pathname.new(path).expand_path.to_s
25
+ HTTP::FormData::File.new(absolute_path)
26
+ end
27
+
28
+ def create_and_maybe_stream(
29
+ endpoint,
30
+ full_response_type:, stream: false,
31
+ chunk_response_type: full_response_type,
32
+ **kwargs
33
+ )
34
+ payload = kwargs.merge(stream: stream)
35
+
36
+ if stream && !block_given?
37
+ raise 'Streaming responses require a block'
38
+ elsif !stream && block_given?
39
+ raise 'Non-streaming responses do not support blocks'
40
+ end
41
+
42
+ if stream
43
+ post(endpoint, **payload) do |chunk|
44
+ yield(chunk_response_type.from_json(chunk))
45
+ end
46
+
47
+ nil
48
+ else
49
+ full_response_type.from_json(
50
+ post(endpoint, **payload)
51
+ )
52
+ end
53
+ end
54
+ end
55
+
56
+ class Completion < Resource
57
+ def create(model:, **kwargs, &block)
58
+ create_and_maybe_stream(
59
+ '/v1/completions',
60
+ model: model,
61
+ full_response_type: Response::Completion,
62
+ **kwargs,
63
+ &block
64
+ )
65
+ end
66
+ end
67
+
68
+ class ChatCompletion < Resource
69
+ def create(model:, messages:, **kwargs, &block)
70
+ create_and_maybe_stream(
71
+ '/v1/chat/completions',
72
+ model: model,
73
+ messages: messages,
74
+ full_response_type: Response::ChatCompletion,
75
+ chunk_response_type: Response::ChatCompletionChunk,
76
+ **kwargs,
77
+ &block
78
+ )
79
+ end
80
+ end
81
+
82
+ class Embedding < Resource
83
+ def create(model:, input:, **kwargs)
84
+ Response::Embedding.from_json(
85
+ post('/v1/embeddings', model: model, input: input, **kwargs)
86
+ )
87
+ end
88
+ end
89
+
90
+ class Model < Resource
91
+ def list
92
+ Response::ListModel.from_json(get('/v1/models'))
93
+ end
94
+
95
+ def fetch(model_id)
96
+ Response::Model.from_json(
97
+ get("/v1/models/#{model_id}")
98
+ )
99
+ end
100
+ end
101
+
102
+ class Moderation < Resource
103
+ def create(input:, model:)
104
+ Response::Moderation.from_json(
105
+ post('/v1/moderations', input: input, model: model)
106
+ )
107
+ end
108
+ end
109
+
110
+ class Edit < Resource
111
+ def create(model:, instruction:, **kwargs)
112
+ Response::Edit.from_json(
113
+ post('/v1/edits', model: model, instruction: instruction, **kwargs)
114
+ )
115
+ end
116
+ end
117
+
118
+ class File < Resource
119
+ def create(file:, purpose:)
120
+ Response::File.from_json(
121
+ post_form_multipart('/v1/files', file: form_file(file), purpose: purpose)
122
+ )
123
+ end
124
+
125
+ def list
126
+ Response::FileList.from_json(
127
+ get('/v1/files')
128
+ )
129
+ end
130
+
131
+ def delete(file_id)
132
+ Response::File.from_json(
133
+ client.delete("/v1/files/#{file_id}")
134
+ )
135
+ end
136
+
137
+ def fetch(file_id)
138
+ Response::File.from_json(
139
+ get("/v1/files/#{file_id}")
140
+ )
141
+ end
142
+
143
+ def get_content(file_id)
144
+ get("/v1/files/#{file_id}/content")
145
+ end
146
+ end
147
+
148
+ class FineTune < Resource
149
+ def list
150
+ Response::FineTuneList.from_json(
151
+ get('/v1/fine-tunes')
152
+ )
153
+ end
154
+
155
+ def create(training_file:, **kwargs)
156
+ Response::FineTune.from_json(
157
+ post('/v1/fine-tunes', training_file: training_file, **kwargs)
158
+ )
159
+ end
160
+
161
+ def fetch(fine_tune_id)
162
+ Response::FineTune.from_json(
163
+ get("/v1/fine-tunes/#{fine_tune_id}")
164
+ )
165
+ end
166
+
167
+ def cancel(fine_tune_id)
168
+ Response::FineTune.from_json(
169
+ post("/v1/fine-tunes/#{fine_tune_id}/cancel")
170
+ )
171
+ end
172
+
173
+ def list_events(fine_tune_id)
174
+ Response::FineTuneEventList.from_json(
175
+ get("/v1/fine-tunes/#{fine_tune_id}/events")
176
+ )
177
+ end
178
+ end
179
+
180
+ class Image < Resource
181
+ def create(prompt:, **kwargs)
182
+ Response::ImageGeneration.from_json(
183
+ post('/v1/images/generations', prompt: prompt, **kwargs)
184
+ )
185
+ end
186
+
187
+ def create_variation(image:, **kwargs)
188
+ Response::ImageVariation.from_json(
189
+ post_form_multipart('/v1/images/variations', {
190
+ image: form_file(image),
191
+ **kwargs
192
+ })
193
+ )
194
+ end
195
+
196
+ def edit(image:, prompt:, mask: nil, **kwargs)
197
+ params = {
198
+ image: form_file(image),
199
+ prompt: prompt,
200
+ **kwargs
201
+ }
202
+
203
+ params[:mask] = form_file(mask) if mask
204
+
205
+ Response::ImageEdit.from_json(
206
+ post_form_multipart('/v1/images/edits', **params)
207
+ )
208
+ end
209
+ end
210
+
211
+ class Audio < Resource
212
+ def transcribe(file:, model:, **kwargs)
213
+ Response::Transcription.from_json(
214
+ post_form_multipart(
215
+ '/v1/audio/transcriptions',
216
+ file: form_file(file),
217
+ model: model,
218
+ **kwargs
219
+ )
220
+ )
221
+ end
222
+
223
+ def translate(file:, model:, **kwargs)
224
+ Response::Transcription.from_json(
225
+ post_form_multipart(
226
+ '/v1/audio/translations',
227
+ file: form_file(file),
228
+ model: model,
229
+ **kwargs
230
+ )
231
+ )
232
+ end
233
+ end
234
+ end
235
+ end