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