lluminary 0.1.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/lib/lluminary/config.rb +11 -1
- data/lib/lluminary/field_description.rb +34 -29
- data/lib/lluminary/provider_error.rb +2 -1
- data/lib/lluminary/providers/base.rb +5 -1
- data/lib/lluminary/providers/bedrock.rb +32 -30
- data/lib/lluminary/providers/openai.rb +28 -21
- data/lib/lluminary/providers/test.rb +9 -14
- data/lib/lluminary/result.rb +5 -2
- data/lib/lluminary/schema.rb +9 -16
- data/lib/lluminary/schema_model.rb +13 -10
- data/lib/lluminary/task.rb +84 -59
- data/lib/lluminary/validation_error.rb +2 -1
- data/lib/lluminary/version.rb +3 -2
- data/lib/lluminary.rb +23 -7
- data/spec/examples/analyze_text_spec.rb +7 -4
- data/spec/examples/color_analyzer_spec.rb +22 -22
- data/spec/examples/content_analyzer_spec.rb +27 -44
- data/spec/examples/historical_event_analyzer_spec.rb +18 -15
- data/spec/examples/price_analyzer_spec.rb +22 -28
- data/spec/examples/quote_task_spec.rb +9 -8
- data/spec/examples/sentiment_analysis_spec.rb +13 -10
- data/spec/examples/summarize_text_spec.rb +7 -4
- data/spec/lluminary/config_spec.rb +28 -26
- data/spec/lluminary/field_description_spec.rb +19 -21
- data/spec/lluminary/providers/base_spec.rb +11 -8
- data/spec/lluminary/providers/bedrock_spec.rb +47 -57
- data/spec/lluminary/providers/openai_spec.rb +27 -35
- data/spec/lluminary/providers/test_spec.rb +21 -16
- data/spec/lluminary/result_spec.rb +17 -10
- data/spec/lluminary/schema_model_spec.rb +31 -22
- data/spec/lluminary/schema_spec.rb +95 -107
- data/spec/lluminary/task_spec.rb +366 -300
- data/spec/spec_helper.rb +7 -2
- metadata +35 -19
data/spec/lluminary/task_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
2
3
|
|
3
4
|
RSpec.describe Lluminary::Task do
|
4
5
|
let(:task_class) do
|
@@ -19,29 +20,29 @@ RSpec.describe Lluminary::Task do
|
|
19
20
|
end
|
20
21
|
end
|
21
22
|
|
22
|
-
let(:task_with_test)
|
23
|
-
Class.new(described_class) do
|
24
|
-
use_provider :test
|
25
|
-
end
|
26
|
-
end
|
23
|
+
let(:task_with_test) { Class.new(described_class) { use_provider :test } }
|
27
24
|
|
28
|
-
describe
|
29
|
-
it
|
25
|
+
describe ".call" do
|
26
|
+
it "returns a result with a raw response from the provider" do
|
30
27
|
result = task_class.call(message: "hello")
|
31
|
-
expect(result.output.raw_response).to eq(
|
28
|
+
expect(result.output.raw_response).to eq(
|
29
|
+
'{"summary": "Test string value"}'
|
30
|
+
)
|
32
31
|
end
|
33
32
|
|
34
|
-
it
|
33
|
+
it "string input allows providing a string input" do
|
35
34
|
result = task_class.call(message: "hello")
|
36
|
-
expect(result.output.raw_response).to eq(
|
35
|
+
expect(result.output.raw_response).to eq(
|
36
|
+
'{"summary": "Test string value"}'
|
37
|
+
)
|
37
38
|
end
|
38
39
|
|
39
|
-
it
|
40
|
+
it "string output returns the output in the result" do
|
40
41
|
result = task_class.call(message: "hello")
|
41
42
|
expect(result.output.summary).to eq("Test string value")
|
42
43
|
end
|
43
44
|
|
44
|
-
it
|
45
|
+
it "includes schema descriptions in the prompt" do
|
45
46
|
result = task_class.call(message: "hello")
|
46
47
|
expected_schema = <<~SCHEMA
|
47
48
|
You must respond with ONLY a valid JSON object. Do not include any other text, explanations, or formatting.
|
@@ -59,16 +60,12 @@ RSpec.describe Lluminary::Task do
|
|
59
60
|
end
|
60
61
|
end
|
61
62
|
|
62
|
-
describe
|
63
|
+
describe ".call without descriptions" do
|
63
64
|
let(:task_without_descriptions) do
|
64
65
|
Class.new(described_class) do
|
65
|
-
input_schema
|
66
|
-
string :message
|
67
|
-
end
|
66
|
+
input_schema { string :message }
|
68
67
|
|
69
|
-
output_schema
|
70
|
-
string :summary
|
71
|
-
end
|
68
|
+
output_schema { string :summary }
|
72
69
|
|
73
70
|
private
|
74
71
|
|
@@ -78,7 +75,7 @@ RSpec.describe Lluminary::Task do
|
|
78
75
|
end
|
79
76
|
end
|
80
77
|
|
81
|
-
it
|
78
|
+
it "includes basic schema in the prompt" do
|
82
79
|
result = task_without_descriptions.call(message: "hello")
|
83
80
|
expected_schema = <<~SCHEMA
|
84
81
|
You must respond with ONLY a valid JSON object. Do not include any other text, explanations, or formatting.
|
@@ -96,38 +93,42 @@ RSpec.describe Lluminary::Task do
|
|
96
93
|
end
|
97
94
|
end
|
98
95
|
|
99
|
-
describe
|
100
|
-
it
|
96
|
+
describe ".provider" do
|
97
|
+
it "defaults to Test provider" do
|
101
98
|
expect(task_class.provider).to be_a(Lluminary::Providers::Test)
|
102
99
|
end
|
103
100
|
|
104
|
-
it
|
105
|
-
custom_provider = double(
|
101
|
+
it "allows setting a custom provider" do
|
102
|
+
custom_provider = double("CustomProvider")
|
106
103
|
task_class.provider = custom_provider
|
107
104
|
expect(task_class.provider).to eq(custom_provider)
|
108
105
|
end
|
109
106
|
end
|
110
107
|
|
111
|
-
describe
|
112
|
-
it
|
108
|
+
describe ".use_provider" do
|
109
|
+
it "with :test provider sets the test provider" do
|
113
110
|
expect(task_with_test.provider).to be_a(Lluminary::Providers::Test)
|
114
111
|
end
|
115
112
|
|
116
|
-
it
|
117
|
-
task_class.use_provider(:openai, api_key:
|
113
|
+
it "with :openai instantiates OpenAI provider with config" do
|
114
|
+
task_class.use_provider(:openai, api_key: "test")
|
118
115
|
expect(task_class.provider).to be_a(Lluminary::Providers::OpenAI)
|
119
|
-
expect(task_class.provider.config).to eq(
|
116
|
+
expect(task_class.provider.config).to eq(
|
117
|
+
api_key: "test",
|
118
|
+
model: "gpt-3.5-turbo"
|
119
|
+
)
|
120
120
|
end
|
121
121
|
|
122
|
-
it
|
123
|
-
expect {
|
124
|
-
|
125
|
-
|
122
|
+
it "raises ArgumentError for unknown provider" do
|
123
|
+
expect { task_class.use_provider(:unknown) }.to raise_error(
|
124
|
+
ArgumentError,
|
125
|
+
"Unknown provider: unknown"
|
126
|
+
)
|
126
127
|
end
|
127
128
|
end
|
128
129
|
|
129
|
-
describe
|
130
|
-
it
|
130
|
+
describe "#json_schema_example" do
|
131
|
+
it "generates a schema example with descriptions" do
|
131
132
|
task = task_class.new(message: "test")
|
132
133
|
expected_output = <<~SCHEMA
|
133
134
|
You must respond with ONLY a valid JSON object. Do not include any other text, explanations, or formatting.
|
@@ -144,22 +145,21 @@ RSpec.describe Lluminary::Task do
|
|
144
145
|
expect(task.send(:json_schema_example)).to eq(expected_output.chomp)
|
145
146
|
end
|
146
147
|
|
147
|
-
it
|
148
|
-
task_with_datetime =
|
149
|
-
|
150
|
-
string :message
|
151
|
-
end
|
148
|
+
it "generates a schema example with datetime field" do
|
149
|
+
task_with_datetime =
|
150
|
+
Class.new(described_class) do
|
151
|
+
input_schema { string :message }
|
152
152
|
|
153
|
-
|
154
|
-
|
155
|
-
|
153
|
+
output_schema do
|
154
|
+
datetime :start_time, description: "When the event starts"
|
155
|
+
end
|
156
156
|
|
157
|
-
|
157
|
+
private
|
158
158
|
|
159
|
-
|
160
|
-
|
159
|
+
def task_prompt
|
160
|
+
"Say: #{message}"
|
161
|
+
end
|
161
162
|
end
|
162
|
-
end
|
163
163
|
|
164
164
|
task = task_with_datetime.new(message: "test")
|
165
165
|
expected_output = <<~SCHEMA
|
@@ -177,22 +177,21 @@ RSpec.describe Lluminary::Task do
|
|
177
177
|
expect(task.send(:json_schema_example)).to eq(expected_output.chomp)
|
178
178
|
end
|
179
179
|
|
180
|
-
it
|
181
|
-
task_with_boolean =
|
182
|
-
|
183
|
-
string :message
|
184
|
-
end
|
180
|
+
it "generates a schema example with boolean field" do
|
181
|
+
task_with_boolean =
|
182
|
+
Class.new(described_class) do
|
183
|
+
input_schema { string :message }
|
185
184
|
|
186
|
-
|
187
|
-
|
188
|
-
|
185
|
+
output_schema do
|
186
|
+
boolean :is_valid, description: "Whether the input is valid"
|
187
|
+
end
|
189
188
|
|
190
|
-
|
189
|
+
private
|
191
190
|
|
192
|
-
|
193
|
-
|
191
|
+
def task_prompt
|
192
|
+
"Say: #{message}"
|
193
|
+
end
|
194
194
|
end
|
195
|
-
end
|
196
195
|
|
197
196
|
task = task_with_boolean.new(message: "test")
|
198
197
|
expected_output = <<~SCHEMA
|
@@ -210,22 +209,19 @@ RSpec.describe Lluminary::Task do
|
|
210
209
|
expect(task.send(:json_schema_example)).to eq(expected_output.chomp)
|
211
210
|
end
|
212
211
|
|
213
|
-
it
|
214
|
-
task_with_float =
|
215
|
-
|
216
|
-
string :message
|
217
|
-
end
|
212
|
+
it "generates a schema example with float field" do
|
213
|
+
task_with_float =
|
214
|
+
Class.new(described_class) do
|
215
|
+
input_schema { string :message }
|
218
216
|
|
219
|
-
|
220
|
-
float :score, description: "The confidence score"
|
221
|
-
end
|
217
|
+
output_schema { float :score, description: "The confidence score" }
|
222
218
|
|
223
|
-
|
219
|
+
private
|
224
220
|
|
225
|
-
|
226
|
-
|
221
|
+
def task_prompt
|
222
|
+
"Say: #{message}"
|
223
|
+
end
|
227
224
|
end
|
228
|
-
end
|
229
225
|
|
230
226
|
task = task_with_float.new(message: "test")
|
231
227
|
expected_output = <<~SCHEMA
|
@@ -243,22 +239,19 @@ RSpec.describe Lluminary::Task do
|
|
243
239
|
expect(task.send(:json_schema_example)).to eq(expected_output.chomp)
|
244
240
|
end
|
245
241
|
|
246
|
-
it
|
247
|
-
task_without_descriptions =
|
248
|
-
|
249
|
-
string :message
|
250
|
-
end
|
242
|
+
it "generates a schema example without descriptions" do
|
243
|
+
task_without_descriptions =
|
244
|
+
Class.new(described_class) do
|
245
|
+
input_schema { string :message }
|
251
246
|
|
252
|
-
|
253
|
-
string :summary
|
254
|
-
end
|
247
|
+
output_schema { string :summary }
|
255
248
|
|
256
|
-
|
249
|
+
private
|
257
250
|
|
258
|
-
|
259
|
-
|
251
|
+
def task_prompt
|
252
|
+
"Say: #{message}"
|
253
|
+
end
|
260
254
|
end
|
261
|
-
end
|
262
255
|
|
263
256
|
task = task_without_descriptions.new(message: "test")
|
264
257
|
expected_output = <<~SCHEMA
|
@@ -276,18 +269,20 @@ RSpec.describe Lluminary::Task do
|
|
276
269
|
expect(task.send(:json_schema_example)).to eq(expected_output.chomp)
|
277
270
|
end
|
278
271
|
|
279
|
-
context
|
280
|
-
context
|
281
|
-
it
|
282
|
-
task_class =
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
272
|
+
context "validation descriptions" do
|
273
|
+
context "presence validation" do
|
274
|
+
it "includes presence validation description" do
|
275
|
+
task_class =
|
276
|
+
Class.new(described_class) do
|
277
|
+
output_schema do
|
278
|
+
string :name, description: "The person's name"
|
279
|
+
validates :name, presence: true
|
280
|
+
end
|
287
281
|
|
288
|
-
|
289
|
-
|
290
|
-
|
282
|
+
private
|
283
|
+
|
284
|
+
def task_prompt = "Test prompt"
|
285
|
+
end
|
291
286
|
|
292
287
|
task = task_class.new
|
293
288
|
expect(task.send(:json_schema_example)).to include(
|
@@ -296,17 +291,19 @@ RSpec.describe Lluminary::Task do
|
|
296
291
|
end
|
297
292
|
end
|
298
293
|
|
299
|
-
context
|
300
|
-
it
|
301
|
-
task_class =
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
294
|
+
context "length validation" do
|
295
|
+
it "includes minimum length validation description" do
|
296
|
+
task_class =
|
297
|
+
Class.new(described_class) do
|
298
|
+
output_schema do
|
299
|
+
string :name, description: "The person's name"
|
300
|
+
validates :name, length: { minimum: 2 }
|
301
|
+
end
|
306
302
|
|
307
|
-
|
308
|
-
|
309
|
-
|
303
|
+
private
|
304
|
+
|
305
|
+
def task_prompt = "Test prompt"
|
306
|
+
end
|
310
307
|
|
311
308
|
task = task_class.new
|
312
309
|
expect(task.send(:json_schema_example)).to include(
|
@@ -314,16 +311,18 @@ RSpec.describe Lluminary::Task do
|
|
314
311
|
)
|
315
312
|
end
|
316
313
|
|
317
|
-
it
|
318
|
-
task_class =
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
314
|
+
it "includes maximum length validation description" do
|
315
|
+
task_class =
|
316
|
+
Class.new(described_class) do
|
317
|
+
output_schema do
|
318
|
+
string :name, description: "The person's name"
|
319
|
+
validates :name, length: { maximum: 20 }
|
320
|
+
end
|
323
321
|
|
324
|
-
|
325
|
-
|
326
|
-
|
322
|
+
private
|
323
|
+
|
324
|
+
def task_prompt = "Test prompt"
|
325
|
+
end
|
327
326
|
|
328
327
|
task = task_class.new
|
329
328
|
expect(task.send(:json_schema_example)).to include(
|
@@ -331,55 +330,62 @@ RSpec.describe Lluminary::Task do
|
|
331
330
|
)
|
332
331
|
end
|
333
332
|
|
334
|
-
it
|
335
|
-
task_class =
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
333
|
+
it "includes range length validation description" do
|
334
|
+
task_class =
|
335
|
+
Class.new(described_class) do
|
336
|
+
output_schema do
|
337
|
+
string :password, description: "The password"
|
338
|
+
validates :password, length: { in: 8..20 }
|
339
|
+
end
|
340
340
|
|
341
|
-
|
342
|
-
|
343
|
-
|
341
|
+
private
|
342
|
+
|
343
|
+
def task_prompt = "Test prompt"
|
344
|
+
end
|
344
345
|
|
345
346
|
task = task_class.new
|
346
|
-
expect(task.send(:json_schema_example)).to include(
|
347
|
-
|
348
|
-
|
347
|
+
expect(task.send(:json_schema_example)).to include(<<~DESCRIPTION)
|
348
|
+
password (string): The password
|
349
|
+
Validation: must be between 8 and 20 characters
|
350
|
+
Example: "your password here"
|
351
|
+
DESCRIPTION
|
349
352
|
end
|
350
353
|
|
351
|
-
it
|
352
|
-
task_class =
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
minimum: 3,
|
357
|
-
|
358
|
-
}
|
359
|
-
end
|
354
|
+
it "includes multiple length validations in description" do
|
355
|
+
task_class =
|
356
|
+
Class.new(described_class) do
|
357
|
+
output_schema do
|
358
|
+
string :username, description: "The username"
|
359
|
+
validates :username, length: { minimum: 3, maximum: 20 }
|
360
|
+
end
|
360
361
|
|
361
|
-
|
362
|
-
|
363
|
-
|
362
|
+
private
|
363
|
+
|
364
|
+
def task_prompt = "Test prompt"
|
365
|
+
end
|
364
366
|
|
365
367
|
task = task_class.new
|
366
|
-
expect(task.send(:json_schema_example)).to include(
|
367
|
-
|
368
|
-
|
368
|
+
expect(task.send(:json_schema_example)).to include(<<~DESCRIPTION)
|
369
|
+
username (string): The username
|
370
|
+
Validation: must be at least 3 characters, must be at most 20 characters
|
371
|
+
Example: "your username here"
|
372
|
+
DESCRIPTION
|
369
373
|
end
|
370
374
|
end
|
371
375
|
|
372
|
-
context
|
373
|
-
it
|
374
|
-
task_class =
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
376
|
+
context "numericality validation" do
|
377
|
+
it "includes odd validation description" do
|
378
|
+
task_class =
|
379
|
+
Class.new(described_class) do
|
380
|
+
output_schema do
|
381
|
+
integer :number, description: "Odd number"
|
382
|
+
validates :number, numericality: { odd: true }
|
383
|
+
end
|
379
384
|
|
380
|
-
|
381
|
-
|
382
|
-
|
385
|
+
private
|
386
|
+
|
387
|
+
def task_prompt = "Test prompt"
|
388
|
+
end
|
383
389
|
|
384
390
|
task = task_class.new
|
385
391
|
expect(task.send(:json_schema_example)).to include(
|
@@ -387,16 +393,18 @@ RSpec.describe Lluminary::Task do
|
|
387
393
|
)
|
388
394
|
end
|
389
395
|
|
390
|
-
it
|
391
|
-
task_class =
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
+
it "includes equal to validation description" do
|
397
|
+
task_class =
|
398
|
+
Class.new(described_class) do
|
399
|
+
output_schema do
|
400
|
+
integer :level, description: "Level"
|
401
|
+
validates :level, numericality: { equal_to: 5 }
|
402
|
+
end
|
396
403
|
|
397
|
-
|
398
|
-
|
399
|
-
|
404
|
+
private
|
405
|
+
|
406
|
+
def task_prompt = "Test prompt"
|
407
|
+
end
|
400
408
|
|
401
409
|
task = task_class.new
|
402
410
|
expect(task.send(:json_schema_example)).to include(
|
@@ -404,16 +412,18 @@ RSpec.describe Lluminary::Task do
|
|
404
412
|
)
|
405
413
|
end
|
406
414
|
|
407
|
-
it
|
408
|
-
task_class =
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
415
|
+
it "includes less than or equal to validation description" do
|
416
|
+
task_class =
|
417
|
+
Class.new(described_class) do
|
418
|
+
output_schema do
|
419
|
+
integer :score, description: "Score"
|
420
|
+
validates :score, numericality: { less_than_or_equal_to: 100 }
|
421
|
+
end
|
413
422
|
|
414
|
-
|
415
|
-
|
416
|
-
|
423
|
+
private
|
424
|
+
|
425
|
+
def task_prompt = "Test prompt"
|
426
|
+
end
|
417
427
|
|
418
428
|
task = task_class.new
|
419
429
|
expect(task.send(:json_schema_example)).to include(
|
@@ -421,16 +431,18 @@ RSpec.describe Lluminary::Task do
|
|
421
431
|
)
|
422
432
|
end
|
423
433
|
|
424
|
-
it
|
425
|
-
task_class =
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
434
|
+
it "includes other than validation description" do
|
435
|
+
task_class =
|
436
|
+
Class.new(described_class) do
|
437
|
+
output_schema do
|
438
|
+
integer :value, description: "Value"
|
439
|
+
validates :value, numericality: { other_than: 0 }
|
440
|
+
end
|
430
441
|
|
431
|
-
|
432
|
-
|
433
|
-
|
442
|
+
private
|
443
|
+
|
444
|
+
def task_prompt = "Test prompt"
|
445
|
+
end
|
434
446
|
|
435
447
|
task = task_class.new
|
436
448
|
expect(task.send(:json_schema_example)).to include(
|
@@ -438,16 +450,18 @@ RSpec.describe Lluminary::Task do
|
|
438
450
|
)
|
439
451
|
end
|
440
452
|
|
441
|
-
it
|
442
|
-
task_class =
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
453
|
+
it "includes in range validation description" do
|
454
|
+
task_class =
|
455
|
+
Class.new(described_class) do
|
456
|
+
output_schema do
|
457
|
+
integer :rating, description: "Rating"
|
458
|
+
validates :rating, numericality: { in: 1..5 }
|
459
|
+
end
|
447
460
|
|
448
|
-
|
449
|
-
|
450
|
-
|
461
|
+
private
|
462
|
+
|
463
|
+
def task_prompt = "Test prompt"
|
464
|
+
end
|
451
465
|
|
452
466
|
task = task_class.new
|
453
467
|
expect(task.send(:json_schema_example)).to include(
|
@@ -455,19 +469,22 @@ RSpec.describe Lluminary::Task do
|
|
455
469
|
)
|
456
470
|
end
|
457
471
|
|
458
|
-
it
|
459
|
-
task_class =
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
472
|
+
it "includes multiple numericality validations in description" do
|
473
|
+
task_class =
|
474
|
+
Class.new(described_class) do
|
475
|
+
output_schema do
|
476
|
+
integer :score, description: "Score"
|
477
|
+
validates :score,
|
478
|
+
numericality: {
|
479
|
+
greater_than: 0,
|
480
|
+
less_than_or_equal_to: 100
|
481
|
+
}
|
482
|
+
end
|
467
483
|
|
468
|
-
|
469
|
-
|
470
|
-
|
484
|
+
private
|
485
|
+
|
486
|
+
def task_prompt = "Test prompt"
|
487
|
+
end
|
471
488
|
|
472
489
|
task = task_class.new
|
473
490
|
expect(task.send(:json_schema_example)).to include(
|
@@ -475,19 +492,18 @@ RSpec.describe Lluminary::Task do
|
|
475
492
|
)
|
476
493
|
end
|
477
494
|
|
478
|
-
it
|
479
|
-
task_class =
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
greater_than: 0,
|
484
|
-
|
485
|
-
}
|
486
|
-
end
|
495
|
+
it "includes multiple comparison validations in description" do
|
496
|
+
task_class =
|
497
|
+
Class.new(described_class) do
|
498
|
+
output_schema do
|
499
|
+
integer :age, description: "Age"
|
500
|
+
validates :age, comparison: { greater_than: 0, less_than: 120 }
|
501
|
+
end
|
487
502
|
|
488
|
-
|
489
|
-
|
490
|
-
|
503
|
+
private
|
504
|
+
|
505
|
+
def task_prompt = "Test prompt"
|
506
|
+
end
|
491
507
|
|
492
508
|
task = task_class.new
|
493
509
|
expect(task.send(:json_schema_example)).to include(
|
@@ -496,36 +512,50 @@ RSpec.describe Lluminary::Task do
|
|
496
512
|
end
|
497
513
|
end
|
498
514
|
|
499
|
-
context
|
500
|
-
it
|
501
|
-
task_class =
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
515
|
+
context "format validation" do
|
516
|
+
it "includes format validation description" do
|
517
|
+
task_class =
|
518
|
+
Class.new(described_class) do
|
519
|
+
output_schema do
|
520
|
+
string :email, description: "Email address"
|
521
|
+
validates :email, format: { with: /\A[^@\s]+@[^@\s]+\z/ }
|
522
|
+
end
|
506
523
|
|
507
|
-
|
508
|
-
|
509
|
-
|
524
|
+
private
|
525
|
+
|
526
|
+
def task_prompt = "Test prompt"
|
527
|
+
end
|
510
528
|
|
511
529
|
task = task_class.new
|
512
|
-
expect(task.send(:json_schema_example)).to
|
513
|
-
|
514
|
-
|
530
|
+
expect(task.send(:json_schema_example)).to eq(<<~SCHEMA.chomp)
|
531
|
+
You must respond with ONLY a valid JSON object. Do not include any other text, explanations, or formatting.
|
532
|
+
The JSON object must contain the following fields:
|
533
|
+
|
534
|
+
email (string): Email address
|
535
|
+
Validation: must match format: (?-mix:\\A[^@\\s]+@[^@\\s]+\\z)
|
536
|
+
Example: "your email here"
|
537
|
+
|
538
|
+
Your response must be ONLY this JSON object:
|
539
|
+
{
|
540
|
+
"email": "your email here"
|
541
|
+
}
|
542
|
+
SCHEMA
|
515
543
|
end
|
516
544
|
end
|
517
545
|
|
518
|
-
context
|
519
|
-
it
|
520
|
-
task_class =
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
546
|
+
context "inclusion validation" do
|
547
|
+
it "includes inclusion validation description" do
|
548
|
+
task_class =
|
549
|
+
Class.new(described_class) do
|
550
|
+
output_schema do
|
551
|
+
string :role, description: "User role"
|
552
|
+
validates :role, inclusion: { in: %w[admin user guest] }
|
553
|
+
end
|
525
554
|
|
526
|
-
|
527
|
-
|
528
|
-
|
555
|
+
private
|
556
|
+
|
557
|
+
def task_prompt = "Test prompt"
|
558
|
+
end
|
529
559
|
|
530
560
|
task = task_class.new
|
531
561
|
expect(task.send(:json_schema_example)).to include(
|
@@ -534,17 +564,19 @@ RSpec.describe Lluminary::Task do
|
|
534
564
|
end
|
535
565
|
end
|
536
566
|
|
537
|
-
context
|
538
|
-
it
|
539
|
-
task_class =
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
567
|
+
context "exclusion validation" do
|
568
|
+
it "includes exclusion validation description" do
|
569
|
+
task_class =
|
570
|
+
Class.new(described_class) do
|
571
|
+
output_schema do
|
572
|
+
string :status, description: "Status"
|
573
|
+
validates :status, exclusion: { in: %w[banned blocked] }
|
574
|
+
end
|
544
575
|
|
545
|
-
|
546
|
-
|
547
|
-
|
576
|
+
private
|
577
|
+
|
578
|
+
def task_prompt = "Test prompt"
|
579
|
+
end
|
548
580
|
|
549
581
|
task = task_class.new
|
550
582
|
expect(task.send(:json_schema_example)).to include(
|
@@ -553,17 +585,19 @@ RSpec.describe Lluminary::Task do
|
|
553
585
|
end
|
554
586
|
end
|
555
587
|
|
556
|
-
context
|
557
|
-
it
|
558
|
-
task_class =
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
588
|
+
context "absence validation" do
|
589
|
+
it "includes absence validation description" do
|
590
|
+
task_class =
|
591
|
+
Class.new(described_class) do
|
592
|
+
output_schema do
|
593
|
+
string :deleted_at, description: "Deletion timestamp"
|
594
|
+
validates :deleted_at, absence: true
|
595
|
+
end
|
563
596
|
|
564
|
-
|
565
|
-
|
566
|
-
|
597
|
+
private
|
598
|
+
|
599
|
+
def task_prompt = "Test prompt"
|
600
|
+
end
|
567
601
|
|
568
602
|
task = task_class.new
|
569
603
|
expect(task.send(:json_schema_example)).to include(
|
@@ -574,7 +608,7 @@ RSpec.describe Lluminary::Task do
|
|
574
608
|
end
|
575
609
|
end
|
576
610
|
|
577
|
-
describe
|
611
|
+
describe "#validate_input" do
|
578
612
|
let(:task_with_types) do
|
579
613
|
Class.new(described_class) do
|
580
614
|
input_schema do
|
@@ -591,38 +625,52 @@ RSpec.describe Lluminary::Task do
|
|
591
625
|
end
|
592
626
|
end
|
593
627
|
|
594
|
-
it
|
595
|
-
task =
|
628
|
+
it "validates string input type" do
|
629
|
+
task =
|
630
|
+
task_with_types.new(name: "John", age: 30, start_time: DateTime.now)
|
596
631
|
expect { task.send(:validate_input) }.not_to raise_error
|
597
632
|
end
|
598
633
|
|
599
|
-
it
|
634
|
+
it "raises error for invalid string input" do
|
600
635
|
task = task_with_types.new(name: 123, age: 30, start_time: DateTime.now)
|
601
|
-
expect { task.send(:validate_input) }.to raise_error(
|
636
|
+
expect { task.send(:validate_input) }.to raise_error(
|
637
|
+
Lluminary::ValidationError,
|
638
|
+
"Name must be a String"
|
639
|
+
)
|
602
640
|
end
|
603
641
|
|
604
|
-
it
|
605
|
-
task =
|
642
|
+
it "validates integer input type" do
|
643
|
+
task =
|
644
|
+
task_with_types.new(name: "John", age: 30, start_time: DateTime.now)
|
606
645
|
expect { task.send(:validate_input) }.not_to raise_error
|
607
646
|
end
|
608
647
|
|
609
|
-
it
|
610
|
-
task =
|
611
|
-
|
648
|
+
it "raises error for invalid integer input" do
|
649
|
+
task =
|
650
|
+
task_with_types.new(name: "John", age: "30", start_time: DateTime.now)
|
651
|
+
expect { task.send(:validate_input) }.to raise_error(
|
652
|
+
Lluminary::ValidationError,
|
653
|
+
"Age must be an Integer"
|
654
|
+
)
|
612
655
|
end
|
613
656
|
|
614
|
-
it
|
615
|
-
task =
|
657
|
+
it "validates datetime input type" do
|
658
|
+
task =
|
659
|
+
task_with_types.new(name: "John", age: 30, start_time: DateTime.now)
|
616
660
|
expect { task.send(:validate_input) }.not_to raise_error
|
617
661
|
end
|
618
662
|
|
619
|
-
it
|
620
|
-
task =
|
621
|
-
|
663
|
+
it "raises error for invalid datetime input" do
|
664
|
+
task =
|
665
|
+
task_with_types.new(name: "John", age: 30, start_time: "2024-01-01")
|
666
|
+
expect { task.send(:validate_input) }.to raise_error(
|
667
|
+
Lluminary::ValidationError,
|
668
|
+
"Start time must be a DateTime"
|
669
|
+
)
|
622
670
|
end
|
623
671
|
end
|
624
672
|
|
625
|
-
describe
|
673
|
+
describe "SchemaModel integration" do
|
626
674
|
let(:task_with_schema) do
|
627
675
|
Class.new(described_class) do
|
628
676
|
input_schema do
|
@@ -630,7 +678,11 @@ RSpec.describe Lluminary::Task do
|
|
630
678
|
integer :min_length
|
631
679
|
|
632
680
|
validates :text, presence: true
|
633
|
-
validates :min_length,
|
681
|
+
validates :min_length,
|
682
|
+
presence: true,
|
683
|
+
numericality: {
|
684
|
+
greater_than: 0
|
685
|
+
}
|
634
686
|
end
|
635
687
|
|
636
688
|
output_schema do
|
@@ -646,20 +698,20 @@ RSpec.describe Lluminary::Task do
|
|
646
698
|
end
|
647
699
|
end
|
648
700
|
|
649
|
-
it
|
701
|
+
it "wraps input in a SchemaModel instance" do
|
650
702
|
result = task_with_schema.call(text: "hello", min_length: 3)
|
651
703
|
expect(result.input).to be_a(Lluminary::SchemaModel)
|
652
704
|
expect(result.input.text).to eq("hello")
|
653
705
|
expect(result.input.min_length).to eq(3)
|
654
706
|
end
|
655
707
|
|
656
|
-
it
|
708
|
+
it "validates input using SchemaModel" do
|
657
709
|
result = task_with_schema.call(text: "hello", min_length: 3)
|
658
710
|
expect(result.input.valid?).to be true
|
659
711
|
expect(result.input.errors).to be_empty
|
660
712
|
end
|
661
713
|
|
662
|
-
it
|
714
|
+
it "returns validation errors for invalid input" do
|
663
715
|
result = task_with_schema.call(text: nil, min_length: nil)
|
664
716
|
expect(result.input.valid?).to be false
|
665
717
|
expect(result.input.errors.full_messages).to contain_exactly(
|
@@ -669,33 +721,37 @@ RSpec.describe Lluminary::Task do
|
|
669
721
|
)
|
670
722
|
end
|
671
723
|
|
672
|
-
it
|
724
|
+
it "does not execute task when input is invalid" do
|
673
725
|
result = task_with_schema.call(text: nil, min_length: nil)
|
674
726
|
expect(result.parsed_response).to be_nil
|
675
727
|
expect(result.output).to be_nil
|
676
728
|
end
|
677
729
|
|
678
|
-
it
|
679
|
-
expect
|
730
|
+
it "raises ValidationError for invalid input when using call!" do
|
731
|
+
expect do
|
680
732
|
task_with_schema.call!(text: nil, min_length: nil)
|
681
|
-
|
733
|
+
end.to raise_error(
|
734
|
+
Lluminary::ValidationError,
|
735
|
+
"Text can't be blank, Min length can't be blank, Min length is not a number"
|
736
|
+
)
|
682
737
|
end
|
683
738
|
|
684
|
-
it
|
739
|
+
it "validates that the response is valid JSON" do
|
685
740
|
task = task_with_schema.new(text: "hello", min_length: 3)
|
686
|
-
allow(task.class.provider).to receive(:call).and_return(
|
687
|
-
raw: "not valid json at all",
|
688
|
-
|
689
|
-
})
|
741
|
+
allow(task.class.provider).to receive(:call).and_return(
|
742
|
+
{ raw: "not valid json at all", parsed: nil }
|
743
|
+
)
|
690
744
|
|
691
745
|
result = task.call
|
692
746
|
expect(result.input.valid?).to be true
|
693
747
|
expect(result.output.valid?).to be false
|
694
|
-
expect(result.output.errors.full_messages).to include(
|
748
|
+
expect(result.output.errors.full_messages).to include(
|
749
|
+
"Raw response must be valid JSON"
|
750
|
+
)
|
695
751
|
end
|
696
752
|
end
|
697
753
|
|
698
|
-
describe
|
754
|
+
describe "tasks without inputs" do
|
699
755
|
let(:quote_task) do
|
700
756
|
Class.new(described_class) do
|
701
757
|
use_provider :test
|
@@ -713,13 +769,13 @@ RSpec.describe Lluminary::Task do
|
|
713
769
|
end
|
714
770
|
end
|
715
771
|
|
716
|
-
it
|
772
|
+
it "can be called without any input parameters" do
|
717
773
|
result = quote_task.call
|
718
774
|
expect(result.output.quote).to be_a(String)
|
719
775
|
expect(result.output.author).to be_a(String)
|
720
776
|
end
|
721
777
|
|
722
|
-
it
|
778
|
+
it "returns a valid result object" do
|
723
779
|
result = quote_task.call
|
724
780
|
expect(result).to be_a(Lluminary::Task)
|
725
781
|
expect(result.input).to be_a(Lluminary::SchemaModel)
|
@@ -727,7 +783,7 @@ RSpec.describe Lluminary::Task do
|
|
727
783
|
end
|
728
784
|
end
|
729
785
|
|
730
|
-
describe
|
786
|
+
describe "datetime handling" do
|
731
787
|
let(:task_with_datetime) do
|
732
788
|
Class.new(described_class) do
|
733
789
|
use_provider :test
|
@@ -744,12 +800,16 @@ RSpec.describe Lluminary::Task do
|
|
744
800
|
end
|
745
801
|
end
|
746
802
|
|
747
|
-
it
|
803
|
+
it "converts ISO8601 datetime strings to DateTime objects" do
|
748
804
|
task = task_with_datetime.new
|
749
|
-
allow(task.class.provider).to receive(:call).and_return(
|
750
|
-
|
751
|
-
|
752
|
-
|
805
|
+
allow(task.class.provider).to receive(:call).and_return(
|
806
|
+
{
|
807
|
+
raw: '{"event_time": "2024-01-01T12:00:00+00:00"}',
|
808
|
+
parsed: {
|
809
|
+
"event_time" => "2024-01-01T12:00:00+00:00"
|
810
|
+
}
|
811
|
+
}
|
812
|
+
)
|
753
813
|
|
754
814
|
result = task.call
|
755
815
|
expect(result.output.valid?).to be true
|
@@ -762,16 +822,22 @@ RSpec.describe Lluminary::Task do
|
|
762
822
|
expect(result.output.event_time.second).to eq(0)
|
763
823
|
end
|
764
824
|
|
765
|
-
it
|
825
|
+
it "handles invalid datetime strings" do
|
766
826
|
task = task_with_datetime.new
|
767
|
-
allow(task.class.provider).to receive(:call).and_return(
|
768
|
-
|
769
|
-
|
770
|
-
|
827
|
+
allow(task.class.provider).to receive(:call).and_return(
|
828
|
+
{
|
829
|
+
raw: '{"event_time": "not a valid datetime"}',
|
830
|
+
parsed: {
|
831
|
+
"event_time" => "not a valid datetime"
|
832
|
+
}
|
833
|
+
}
|
834
|
+
)
|
771
835
|
|
772
836
|
result = task.call
|
773
837
|
expect(result.output.valid?).to be false
|
774
|
-
expect(result.output.errors.full_messages).to include(
|
838
|
+
expect(result.output.errors.full_messages).to include(
|
839
|
+
"Event time must be a DateTime"
|
840
|
+
)
|
775
841
|
end
|
776
842
|
end
|
777
|
-
end
|
843
|
+
end
|