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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/lib/lluminary/config.rb +23 -0
  3. data/lib/lluminary/field_description.rb +148 -0
  4. data/lib/lluminary/provider_error.rb +4 -0
  5. data/lib/lluminary/providers/base.rb +15 -0
  6. data/lib/lluminary/providers/bedrock.rb +51 -0
  7. data/lib/lluminary/providers/openai.rb +40 -0
  8. data/lib/lluminary/providers/test.rb +37 -0
  9. data/lib/lluminary/result.rb +13 -0
  10. data/lib/lluminary/schema.rb +61 -0
  11. data/lib/lluminary/schema_model.rb +87 -0
  12. data/lib/lluminary/task.rb +224 -0
  13. data/lib/lluminary/validation_error.rb +4 -0
  14. data/lib/lluminary/version.rb +3 -0
  15. data/lib/lluminary.rb +18 -0
  16. data/spec/examples/analyze_text_spec.rb +21 -0
  17. data/spec/examples/color_analyzer_spec.rb +42 -0
  18. data/spec/examples/content_analyzer_spec.rb +75 -0
  19. data/spec/examples/historical_event_analyzer_spec.rb +37 -0
  20. data/spec/examples/price_analyzer_spec.rb +46 -0
  21. data/spec/examples/quote_task_spec.rb +27 -0
  22. data/spec/examples/sentiment_analysis_spec.rb +45 -0
  23. data/spec/examples/summarize_text_spec.rb +21 -0
  24. data/spec/lluminary/config_spec.rb +53 -0
  25. data/spec/lluminary/field_description_spec.rb +36 -0
  26. data/spec/lluminary/providers/base_spec.rb +17 -0
  27. data/spec/lluminary/providers/bedrock_spec.rb +109 -0
  28. data/spec/lluminary/providers/openai_spec.rb +62 -0
  29. data/spec/lluminary/providers/test_spec.rb +57 -0
  30. data/spec/lluminary/result_spec.rb +31 -0
  31. data/spec/lluminary/schema_model_spec.rb +86 -0
  32. data/spec/lluminary/schema_spec.rb +302 -0
  33. data/spec/lluminary/task_spec.rb +777 -0
  34. data/spec/spec_helper.rb +4 -0
  35. 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