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.
@@ -0,0 +1,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe OpenAI::API, '#fine_tunes' do
4
+ include_context 'an API Resource'
5
+
6
+ let(:resource) { api.fine_tunes }
7
+ let(:response_body) do
8
+ {
9
+ "object": 'list',
10
+ "data": [
11
+ {
12
+ "id": 'ft-AF1WoRqd3aJAHsqc9NY7iL8F',
13
+ "object": 'fine-tune',
14
+ "model": 'curie',
15
+ "created_at": 1_614_807_352,
16
+ "fine_tuned_model": nil,
17
+ "hyperparams": {},
18
+ "organization_id": 'org-...',
19
+ "result_files": [],
20
+ "status": 'pending',
21
+ "validation_files": [],
22
+ "training_files": [{}],
23
+ "updated_at": 1_614_807_352
24
+ },
25
+ {},
26
+ {}
27
+ ]
28
+ }
29
+ end
30
+
31
+ context 'when listing fine-tunes' do
32
+ it 'can get a list of fine-tunes' do
33
+ fine_tunes = resource.list
34
+
35
+ expect(http)
36
+ .to have_received(:get)
37
+ .with('https://api.openai.com/v1/fine-tunes')
38
+
39
+ expect(fine_tunes.object).to eql('list')
40
+ expect(fine_tunes.data.size).to eql(3)
41
+ expect(fine_tunes.data.first.id).to eql('ft-AF1WoRqd3aJAHsqc9NY7iL8F')
42
+ expect(fine_tunes.data.first.object).to eql('fine-tune')
43
+ expect(fine_tunes.data.first.model).to eql('curie')
44
+ expect(fine_tunes.data.first.created_at).to eql(1_614_807_352)
45
+ expect(fine_tunes.data.first.fine_tuned_model).to be_nil
46
+ expect(fine_tunes.data.first.hyperparams).to eql(
47
+ described_class::Response::FineTune::Hyperparams.new({})
48
+ )
49
+ expect(fine_tunes.data.first.organization_id).to eql('org-...')
50
+ expect(fine_tunes.data.first.result_files).to eql([])
51
+ expect(fine_tunes.data.first.status).to eql('pending')
52
+ expect(fine_tunes.data.first.validation_files).to eql([])
53
+ expect(fine_tunes.data.first.training_files).to eql(
54
+ [
55
+ described_class::Response::FineTune::File.new({})
56
+ ]
57
+ )
58
+ expect(fine_tunes.data.first.updated_at).to eql(1_614_807_352)
59
+ end
60
+ end
61
+
62
+ context 'when creating a fine-tune' do
63
+ let(:response_body) do
64
+ {
65
+ "id": 'ft-AF1WoRqd3aJAHsqc9NY7iL8F',
66
+ "object": 'fine-tune',
67
+ "model": 'curie',
68
+ "created_at": 1_614_807_352,
69
+ "events": [
70
+ {
71
+ "object": 'fine-tune-event',
72
+ "created_at": 1_614_807_352,
73
+ "level": 'info',
74
+ "message": 'Job enqueued. Waiting for jobs ahead to complete. Queue number: 0.'
75
+ }
76
+ ],
77
+ "fine_tuned_model": nil,
78
+ "hyperparams": {
79
+ "batch_size": 4,
80
+ "learning_rate_multiplier": 0.1,
81
+ "n_epochs": 4,
82
+ "prompt_loss_weight": 0.1
83
+ },
84
+ "organization_id": 'org-...',
85
+ "result_files": [],
86
+ "status": 'pending',
87
+ "validation_files": [],
88
+ "training_files": [
89
+ {
90
+ "id": 'file-XGinujblHPwGLSztz8cPS8XY',
91
+ "object": 'file',
92
+ "bytes": 1_547_276,
93
+ "created_at": 1_610_062_281,
94
+ "filename": 'my-data-train.jsonl',
95
+ "purpose": 'fine-tune-train'
96
+ }
97
+ ],
98
+ "updated_at": 1_614_807_352
99
+ }
100
+ end
101
+
102
+ it 'can create a fine-tune' do
103
+ fine_tune = resource.create(training_file: 'my-data-train.jsonl', model: 'curie')
104
+
105
+ expect(http)
106
+ .to have_received(:post)
107
+ .with('https://api.openai.com/v1/fine-tunes', hash_including(:json))
108
+
109
+ expect(fine_tune.id).to eql('ft-AF1WoRqd3aJAHsqc9NY7iL8F')
110
+ expect(fine_tune.model).to eql('curie')
111
+ expect(fine_tune.created_at).to eql(1_614_807_352)
112
+ expect(fine_tune.events.first.object).to eql('fine-tune-event')
113
+ expect(fine_tune.events.first.created_at).to eql(1_614_807_352)
114
+ expect(fine_tune.events.first.level).to eql('info')
115
+ expect(fine_tune.events.first.message).to eql('Job enqueued. Waiting for jobs ahead to complete. Queue number: 0.')
116
+ expect(fine_tune.fine_tuned_model).to be_nil
117
+ expect(fine_tune.hyperparams.batch_size).to eql(4)
118
+ expect(fine_tune.hyperparams.learning_rate_multiplier).to eql(0.1)
119
+ expect(fine_tune.hyperparams.n_epochs).to eql(4)
120
+ expect(fine_tune.hyperparams.prompt_loss_weight).to eql(0.1)
121
+ expect(fine_tune.organization_id).to eql('org-...')
122
+ expect(fine_tune.result_files).to be_empty
123
+ expect(fine_tune.status).to eql('pending')
124
+ expect(fine_tune.validation_files).to be_empty
125
+ expect(fine_tune.training_files.first.id).to eql('file-XGinujblHPwGLSztz8cPS8XY')
126
+ expect(fine_tune.training_files.first.object).to eql('file')
127
+ expect(fine_tune.training_files.first.bytes).to eql(1_547_276)
128
+ expect(fine_tune.training_files.first.created_at).to eql(1_610_062_281)
129
+ expect(fine_tune.training_files.first.filename).to eql('my-data-train.jsonl')
130
+ expect(fine_tune.training_files.first.purpose).to eql('fine-tune-train')
131
+ expect(fine_tune.updated_at).to eql(1_614_807_352)
132
+ end
133
+ end
134
+
135
+ context 'when fetching a fine tune' do
136
+ let(:response_body) do
137
+ {
138
+ "id": 'ft-AF1WoRqd3aJAHsqc9NY7iL8F',
139
+ "object": 'fine-tune',
140
+ "model": 'curie',
141
+ "created_at": 1_614_807_352,
142
+ "events": [
143
+ {
144
+ "object": 'fine-tune-event',
145
+ "created_at": 1_614_807_352,
146
+ "level": 'info',
147
+ "message": 'Job enqueued. Waiting for jobs ahead to complete. Queue number: 0.'
148
+ },
149
+ {
150
+ "object": 'fine-tune-event',
151
+ "created_at": 1_614_807_356,
152
+ "level": 'info',
153
+ "message": 'Job started.'
154
+ },
155
+ {
156
+ "object": 'fine-tune-event',
157
+ "created_at": 1_614_807_861,
158
+ "level": 'info',
159
+ "message": 'Uploaded snapshot: curie:ft-acmeco-2021-03-03-21-44-20.'
160
+ },
161
+ {
162
+ "object": 'fine-tune-event',
163
+ "created_at": 1_614_807_864,
164
+ "level": 'info',
165
+ "message": 'Uploaded result files: file-QQm6ZpqdNwAaVC3aSz5sWwLT.'
166
+ },
167
+ {
168
+ "object": 'fine-tune-event',
169
+ "created_at": 1_614_807_864,
170
+ "level": 'info',
171
+ "message": 'Job succeeded.'
172
+ }
173
+ ],
174
+ "fine_tuned_model": 'curie:ft-acmeco-2021-03-03-21-44-20',
175
+ "hyperparams": {
176
+ "batch_size": 4,
177
+ "learning_rate_multiplier": 0.1,
178
+ "n_epochs": 4,
179
+ "prompt_loss_weight": 0.1
180
+ },
181
+ "organization_id": 'org-...',
182
+ "result_files": [
183
+ {
184
+ "id": 'file-QQm6ZpqdNwAaVC3aSz5sWwLT',
185
+ "object": 'file',
186
+ "bytes": 81_509,
187
+ "created_at": 1_614_807_863,
188
+ "filename": 'compiled_results.csv',
189
+ "purpose": 'fine-tune-results'
190
+ }
191
+ ],
192
+ "status": 'succeeded',
193
+ "validation_files": [],
194
+ "training_files": [
195
+ {
196
+ "id": 'file-XGinujblHPwGLSztz8cPS8XY',
197
+ "object": 'file',
198
+ "bytes": 1_547_276,
199
+ "created_at": 1_610_062_281,
200
+ "filename": 'my-data-train.jsonl',
201
+ "purpose": 'fine-tune-train'
202
+ }
203
+ ],
204
+ "updated_at": 1_614_807_865
205
+ }
206
+ end
207
+
208
+ it 'can get a fine-tune' do
209
+ fine_tune = resource.fetch('ft-AF1WoRqd3aJAHsqc9NY7iL8F')
210
+
211
+ expect(http)
212
+ .to have_received(:get)
213
+ .with('https://api.openai.com/v1/fine-tunes/ft-AF1WoRqd3aJAHsqc9NY7iL8F')
214
+
215
+ expect(fine_tune.id).to eql('ft-AF1WoRqd3aJAHsqc9NY7iL8F')
216
+ expect(fine_tune.model).to eql('curie')
217
+ expect(fine_tune.created_at).to eql(1_614_807_352)
218
+ expect(fine_tune.events.first.object).to eql('fine-tune-event')
219
+ expect(fine_tune.events.first.created_at).to eql(1_614_807_352)
220
+ expect(fine_tune.events.first.level).to eql('info')
221
+ expect(fine_tune.events.first.message).to eql('Job enqueued. Waiting for jobs ahead to complete. Queue number: 0.')
222
+ expect(fine_tune.fine_tuned_model).to eql('curie:ft-acmeco-2021-03-03-21-44-20')
223
+ expect(fine_tune.hyperparams.batch_size).to eql(4)
224
+ expect(fine_tune.hyperparams.learning_rate_multiplier).to eql(0.1)
225
+ expect(fine_tune.hyperparams.n_epochs).to eql(4)
226
+ expect(fine_tune.hyperparams.prompt_loss_weight).to eql(0.1)
227
+ expect(fine_tune.organization_id).to eql('org-...')
228
+ expect(fine_tune.result_files.first.id).to eql('file-QQm6ZpqdNwAaVC3aSz5sWwLT')
229
+ expect(fine_tune.result_files.first.object).to eql('file')
230
+ expect(fine_tune.result_files.first.bytes).to eql(81_509)
231
+ expect(fine_tune.result_files.first.created_at).to eql(1_614_807_863)
232
+ expect(fine_tune.result_files.first.filename).to eql('compiled_results.csv')
233
+ expect(fine_tune.result_files.first.purpose).to eql('fine-tune-results')
234
+ expect(fine_tune.status).to eql('succeeded')
235
+ expect(fine_tune.validation_files).to be_empty
236
+ expect(fine_tune.training_files.first.id).to eql('file-XGinujblHPwGLSztz8cPS8XY')
237
+ expect(fine_tune.training_files.first.object).to eql('file')
238
+ expect(fine_tune.training_files.first.bytes).to eql(1_547_276)
239
+ expect(fine_tune.training_files.first.created_at).to eql(1_610_062_281)
240
+ expect(fine_tune.training_files.first.filename).to eql('my-data-train.jsonl')
241
+ expect(fine_tune.training_files.first.purpose).to eql('fine-tune-train')
242
+ expect(fine_tune.updated_at).to eql(1_614_807_865)
243
+ end
244
+ end
245
+
246
+ context 'when canceling a fine-tune' do
247
+ let(:response_body) do
248
+ {
249
+ "id": 'ft-xhrpBbvVUzYGo8oUO1FY4nI7',
250
+ "status": 'cancelled'
251
+ }
252
+ end
253
+
254
+ it 'can cancel a fine-tune' do
255
+ fine_tune = resource.cancel('ft-xhrpBbvVUzYGo8oUO1FY4nI7')
256
+
257
+ expect(http)
258
+ .to have_received(:post)
259
+ .with('https://api.openai.com/v1/fine-tunes/ft-xhrpBbvVUzYGo8oUO1FY4nI7/cancel', json: {})
260
+
261
+ expect(fine_tune.id).to eql('ft-xhrpBbvVUzYGo8oUO1FY4nI7')
262
+ expect(fine_tune.status).to eql('cancelled')
263
+ end
264
+ end
265
+
266
+ context 'when listing fine-tune events' do
267
+ let(:response_body) do
268
+ {
269
+ "object": 'list',
270
+ "data": [
271
+ {
272
+ "object": 'fine-tune-event',
273
+ "created_at": 1_614_807_352,
274
+ "level": 'info',
275
+ "message": 'Job enqueued. Waiting for jobs ahead to complete. Queue number: 0.'
276
+ },
277
+ {
278
+ "object": 'fine-tune-event',
279
+ "created_at": 1_614_807_356,
280
+ "level": 'info',
281
+ "message": 'Job started.'
282
+ },
283
+ {
284
+ "object": 'fine-tune-event',
285
+ "created_at": 1_614_807_861,
286
+ "level": 'info',
287
+ "message": 'Uploaded snapshot: curie:ft-acmeco-2021-03-03-21-44-20.'
288
+ },
289
+ {
290
+ "object": 'fine-tune-event',
291
+ "created_at": 1_614_807_864,
292
+ "level": 'info',
293
+ "message": 'Uploaded result files: file-QQm6ZpqdNwAaVC3aSz5sWwLT.'
294
+ },
295
+ {
296
+ "object": 'fine-tune-event',
297
+ "created_at": 1_614_807_864,
298
+ "level": 'info',
299
+ "message": 'Job succeeded.'
300
+ }
301
+ ]
302
+ }
303
+ end
304
+
305
+ it 'can list fine-tune events' do
306
+ events = resource.list_events('fine-tune-id')
307
+
308
+ expect(http)
309
+ .to have_received(:get)
310
+ .with('https://api.openai.com/v1/fine-tunes/fine-tune-id/events')
311
+
312
+ expect(events.object).to eql('list')
313
+ expect(events.data.size).to eql(5)
314
+
315
+ event = events.data.first
316
+ expect(event.object).to eql('fine-tune-event')
317
+ expect(event.created_at).to eql(1_614_807_352)
318
+ expect(event.level).to eql('info')
319
+ expect(event.message).to eql('Job enqueued. Waiting for jobs ahead to complete. Queue number: 0.')
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe OpenAI::API, '#images' do
4
+ include_context 'an API Resource'
5
+
6
+ let(:resource) { api.images }
7
+
8
+ let(:sample_image) { OpenAISpec::SPEC_ROOT.join('data/sample_image.png') }
9
+
10
+ context 'when creating an image' do
11
+ let(:response_body) do
12
+ {
13
+ created: Time.now.to_i,
14
+ data: [
15
+ { url: 'https://example.com/image1.png' },
16
+ { url: 'https://example.com/image2.png' }
17
+ ]
18
+ }
19
+ end
20
+
21
+ it 'can create an image generation' do
22
+ image_generation = resource.create(prompt: 'a bird in the forest', size: 512)
23
+
24
+ expect(http)
25
+ .to have_received(:post)
26
+ .with(
27
+ 'https://api.openai.com/v1/images/generations',
28
+ hash_including(
29
+ json: hash_including(prompt: 'a bird in the forest', size: 512)
30
+ )
31
+ )
32
+
33
+ expect(image_generation.created).to be_within(1).of(Time.now.to_i)
34
+ expect(image_generation.data.map(&:url)).to contain_exactly('https://example.com/image1.png', 'https://example.com/image2.png')
35
+ end
36
+ end
37
+
38
+ context 'when editing an image' do
39
+ let(:sample_mask) { OpenAISpec::SPEC_ROOT.join('data/sample_image_mask.png') }
40
+
41
+ let(:response_body) do
42
+ {
43
+ "created": 1_589_478_378,
44
+ "data": [
45
+ {
46
+ "url": 'https://...'
47
+ },
48
+ {
49
+ "url": 'https://...'
50
+ }
51
+ ]
52
+ }
53
+ end
54
+
55
+ it 'can edit an image' do
56
+ image_edit = resource.edit(
57
+ image: sample_image,
58
+ mask: sample_mask,
59
+ prompt: 'Draw a red hat on the person in the image',
60
+ n: 1,
61
+ size: '512x512',
62
+ response_format: 'url',
63
+ user: 'user-123'
64
+ )
65
+
66
+ expect(http)
67
+ .to have_received(:post)
68
+ .with(
69
+ 'https://api.openai.com/v1/images/edits',
70
+ hash_including(
71
+ form: hash_including(
72
+ {
73
+ image: instance_of(HTTP::FormData::File),
74
+ mask: instance_of(HTTP::FormData::File),
75
+ prompt: 'Draw a red hat on the person in the image',
76
+ n: 1,
77
+ size: '512x512',
78
+ response_format: 'url',
79
+ user: 'user-123'
80
+ }
81
+ )
82
+ )
83
+ )
84
+
85
+ expect(image_edit.created).to eql(1_589_478_378)
86
+ expect(image_edit.data.first.url).to eql('https://...')
87
+ end
88
+ end
89
+
90
+ context 'when creating image variations' do
91
+ let(:response_body) do
92
+ {
93
+ "created": 1_589_478_378,
94
+ "data": [
95
+ {
96
+ "url": 'https://...'
97
+ },
98
+ {
99
+ "url": 'https://...'
100
+ }
101
+ ]
102
+ }
103
+ end
104
+
105
+ it 'can create image variations' do
106
+ image_variations = resource.create_variation(
107
+ image: sample_image,
108
+ n: 2,
109
+ size: '512x512',
110
+ response_format: 'url',
111
+ user: 'user123'
112
+ )
113
+
114
+ expect(http)
115
+ .to have_received(:post)
116
+ .with(
117
+ 'https://api.openai.com/v1/images/variations',
118
+ hash_including(
119
+ form: hash_including(
120
+ {
121
+ image: instance_of(HTTP::FormData::File),
122
+ n: 2,
123
+ size: '512x512',
124
+ response_format: 'url',
125
+ user: 'user123'
126
+ }
127
+ )
128
+ )
129
+ )
130
+
131
+ expect(image_variations.created).to eql(1_589_478_378)
132
+ expect(image_variations.data.size).to eql(2)
133
+ expect(image_variations.data.first.url).to eql('https://...')
134
+ expect(image_variations.data.last.url).to eql('https://...')
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe OpenAI::API, '#models' do
4
+ include_context 'an API Resource'
5
+
6
+ let(:resource) { api.models }
7
+
8
+ context 'when listing models' do
9
+ let(:response_body) do
10
+ {
11
+ data: [
12
+ {
13
+ id: 'model-id-0',
14
+ object: 'model',
15
+ owned_by: 'organization-owner',
16
+ permission: [1, 2, 3]
17
+ },
18
+ {
19
+ id: 'model-id-1',
20
+ object: 'model',
21
+ owned_by: 'organization-owner',
22
+ permission: [4, 5, 6]
23
+ },
24
+ {
25
+ id: 'model-id-2',
26
+ object: 'model',
27
+ owned_by: 'openai',
28
+ permission: [7, 8, 9]
29
+ }
30
+ ],
31
+ object: 'list'
32
+ }
33
+ end
34
+
35
+ it 'can list all models' do
36
+ models = resource.list
37
+
38
+ expect(http)
39
+ .to have_received(:get)
40
+ .with('https://api.openai.com/v1/models')
41
+
42
+ expect(models.data.length).to eql(3)
43
+
44
+ expect(models.data[0].id).to eql('model-id-0')
45
+ expect(models.data[0].object).to eql('model')
46
+ expect(models.data[0].owned_by).to eql('organization-owner')
47
+ expect(models.data[0].permission).to eql([1, 2, 3])
48
+
49
+ expect(models.data[1].id).to eql('model-id-1')
50
+ expect(models.data[1].object).to eql('model')
51
+ expect(models.data[1].owned_by).to eql('organization-owner')
52
+ expect(models.data[1].permission).to eql([4, 5, 6])
53
+
54
+ expect(models.data[2].id).to eql('model-id-2')
55
+ expect(models.data[2].object).to eql('model')
56
+ expect(models.data[2].owned_by).to eql('openai')
57
+ expect(models.data[2].permission).to eql([7, 8, 9])
58
+ end
59
+ end
60
+
61
+ context 'when retrieving a model' do
62
+ let(:response_body) do
63
+ {
64
+ "id": 'text-davinci-002',
65
+ "object": 'model',
66
+ "owned_by": 'openai',
67
+ "permission": %w[
68
+ query
69
+ completions
70
+ models:read
71
+ models:write
72
+ engine:read
73
+ engine:write
74
+ ]
75
+ }
76
+ end
77
+
78
+ it 'can retrieve a model' do
79
+ model = resource.fetch('text-davinci-002')
80
+
81
+ expect(http)
82
+ .to have_received(:get)
83
+ .with('https://api.openai.com/v1/models/text-davinci-002')
84
+
85
+ expect(model.id).to eql('text-davinci-002')
86
+ expect(model.object).to eql('model')
87
+ expect(model.owned_by).to eql('openai')
88
+ expect(model.permission).to eql(%w[
89
+ query
90
+ completions
91
+ models:read
92
+ models:write
93
+ engine:read
94
+ engine:write
95
+ ])
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,61 @@
1
+ RSpec.describe OpenAI::API, '#moderations' do
2
+ include_context 'an API Resource'
3
+
4
+ let(:resource) { api.moderations }
5
+
6
+ let(:response_body) do
7
+ {
8
+ "id": 'modr-5MWoLO',
9
+ "model": 'text-moderation-001',
10
+ "results": [
11
+ {
12
+ "categories": {
13
+ "hate": false,
14
+ "hate/threatening": true,
15
+ "self-harm": false,
16
+ "sexual": false,
17
+ "sexual/minors": false,
18
+ "violence": true,
19
+ "violence/graphic": false
20
+ },
21
+ "category_scores": {
22
+ "hate": 0.22714105248451233,
23
+ "hate/threatening": 0.4132447838783264,
24
+ "self-harm": 0.005232391878962517,
25
+ "sexual": 0.01407341007143259,
26
+ "sexual/minors": 0.0038522258400917053,
27
+ "violence": 0.9223177433013916,
28
+ "violence/graphic": 0.036865197122097015
29
+ },
30
+ "flagged": true
31
+ }
32
+ ]
33
+ }
34
+ end
35
+
36
+ it 'can create a moderation' do
37
+ moderation = resource.create(input: 'This is a test', model: 'text-moderation-001')
38
+
39
+ expect(http)
40
+ .to have_received(:post)
41
+ .with('https://api.openai.com/v1/moderations', hash_including(:json))
42
+
43
+ expect(moderation.id).to eql('modr-5MWoLO')
44
+ expect(moderation.model).to eql('text-moderation-001')
45
+ expect(moderation.results.first.categories.hate).to be_falsey
46
+ expect(moderation.results.first.categories.hate_threatening).to be_truthy
47
+ expect(moderation.results.first.categories.self_harm).to be_falsey
48
+ expect(moderation.results.first.categories.sexual).to be_falsey
49
+ expect(moderation.results.first.categories.sexual_minors).to be_falsey
50
+ expect(moderation.results.first.categories.violence).to be_truthy
51
+ expect(moderation.results.first.categories.violence_graphic).to be_falsey
52
+ expect(moderation.results.first.category_scores.hate).to eql(0.22714105248451233)
53
+ expect(moderation.results.first.category_scores.hate_threatening).to eql(0.4132447838783264)
54
+ expect(moderation.results.first.category_scores.self_harm).to eql(0.005232391878962517)
55
+ expect(moderation.results.first.category_scores.sexual).to eql(0.01407341007143259)
56
+ expect(moderation.results.first.category_scores.sexual_minors).to eql(0.0038522258400917053)
57
+ expect(moderation.results.first.category_scores.violence).to eql(0.9223177433013916)
58
+ expect(moderation.results.first.category_scores.violence_graphic).to eql(0.036865197122097015)
59
+ expect(moderation.results.first.flagged).to be_truthy
60
+ end
61
+ end