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