lluminary 0.1.1 → 0.1.3
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 +6 -1
- data/lib/lluminary/models/base.rb +235 -0
- data/lib/lluminary/models/bedrock/amazon_nova_pro_v1.rb +19 -0
- data/lib/lluminary/models/bedrock/anthropic_claude_instant_v1.rb +17 -0
- data/lib/lluminary/models/bedrock/base.rb +29 -0
- data/lib/lluminary/models/openai/gpt35_turbo.rb +20 -0
- data/lib/lluminary/provider_error.rb +2 -1
- data/lib/lluminary/providers/base.rb +20 -3
- data/lib/lluminary/providers/bedrock.rb +52 -32
- data/lib/lluminary/providers/openai.rb +41 -24
- data/lib/lluminary/providers/test.rb +14 -13
- data/lib/lluminary/result.rb +5 -2
- data/lib/lluminary/schema.rb +59 -15
- data/lib/lluminary/schema_model.rb +67 -10
- data/lib/lluminary/task.rb +58 -99
- data/lib/lluminary/validation_error.rb +2 -1
- data/lib/lluminary/version.rb +3 -2
- data/lib/lluminary.rb +25 -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/meal_suggester_spec.rb +64 -0
- 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/models/base_spec.rb +581 -0
- data/spec/lluminary/models/bedrock/amazon_nova_pro_v1_spec.rb +30 -0
- data/spec/lluminary/models/bedrock/anthropic_claude_instant_v1_spec.rb +21 -0
- data/spec/lluminary/models/openai/gpt35_turbo_spec.rb +22 -0
- data/spec/lluminary/providers/bedrock_spec.rb +86 -57
- data/spec/lluminary/providers/openai_spec.rb +58 -34
- data/spec/lluminary/providers/test_spec.rb +46 -16
- data/spec/lluminary/result_spec.rb +17 -10
- data/spec/lluminary/schema_model_spec.rb +108 -22
- data/spec/lluminary/schema_spec.rb +241 -107
- data/spec/lluminary/task_spec.rb +118 -584
- data/spec/spec_helper.rb +8 -2
- metadata +73 -22
- data/lib/lluminary/field_description.rb +0 -148
- data/spec/lluminary/field_description_spec.rb +0 -36
- data/spec/lluminary/providers/base_spec.rb +0 -17
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
|
@@ -11,570 +12,79 @@ RSpec.describe Lluminary::Task do
|
|
11
12
|
string :summary, description: "A brief summary of the message"
|
12
13
|
end
|
13
14
|
|
14
|
-
private
|
15
|
-
|
16
15
|
def task_prompt
|
17
16
|
"Say: #{message}"
|
18
17
|
end
|
19
18
|
end
|
20
19
|
end
|
21
20
|
|
22
|
-
let(:task_with_test)
|
23
|
-
Class.new(described_class) do
|
24
|
-
use_provider :test
|
25
|
-
end
|
26
|
-
end
|
21
|
+
let(:task_with_test) { Class.new(described_class) { use_provider :test } }
|
27
22
|
|
28
|
-
describe
|
29
|
-
it
|
23
|
+
describe ".call" do
|
24
|
+
it "returns a result with a raw response from the provider" do
|
30
25
|
result = task_class.call(message: "hello")
|
31
|
-
expect(result.output.raw_response).to eq(
|
32
|
-
|
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"}')
|
26
|
+
expect(result.output.raw_response).to eq(
|
27
|
+
'{"summary": "Test string value"}'
|
28
|
+
)
|
37
29
|
end
|
38
30
|
|
39
|
-
it
|
31
|
+
it "string output returns the output in the result" do
|
40
32
|
result = task_class.call(message: "hello")
|
41
33
|
expect(result.output.summary).to eq("Test string value")
|
42
34
|
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
35
|
end
|
61
36
|
|
62
|
-
describe
|
63
|
-
|
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
|
37
|
+
describe ".provider" do
|
38
|
+
it "defaults to Test provider" do
|
101
39
|
expect(task_class.provider).to be_a(Lluminary::Providers::Test)
|
102
40
|
end
|
103
41
|
|
104
|
-
it
|
105
|
-
custom_provider = double(
|
42
|
+
it "allows setting a custom provider" do
|
43
|
+
custom_provider = double("CustomProvider")
|
106
44
|
task_class.provider = custom_provider
|
107
45
|
expect(task_class.provider).to eq(custom_provider)
|
108
46
|
end
|
109
47
|
end
|
110
48
|
|
111
|
-
describe
|
112
|
-
it
|
49
|
+
describe ".use_provider" do
|
50
|
+
it "with :test provider sets the test provider" do
|
113
51
|
expect(task_with_test.provider).to be_a(Lluminary::Providers::Test)
|
114
52
|
end
|
115
53
|
|
116
|
-
it
|
117
|
-
task_class.use_provider(:openai, api_key:
|
54
|
+
it "with :openai instantiates OpenAI provider with config" do
|
55
|
+
task_class.use_provider(:openai, api_key: "test")
|
118
56
|
expect(task_class.provider).to be_a(Lluminary::Providers::OpenAI)
|
119
|
-
expect(task_class.provider.config).to
|
120
|
-
|
121
|
-
|
122
|
-
|
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)
|
57
|
+
expect(task_class.provider.config).to include(
|
58
|
+
api_key: "test",
|
59
|
+
model: Lluminary::Models::OpenAi::Gpt35Turbo
|
60
|
+
)
|
244
61
|
end
|
245
62
|
|
246
|
-
it
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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)
|
63
|
+
it "with :bedrock instantiates Bedrock provider with config" do
|
64
|
+
task_class.use_provider(
|
65
|
+
:bedrock,
|
66
|
+
access_key_id: "test",
|
67
|
+
secret_access_key: "test",
|
68
|
+
region: "us-east-1"
|
69
|
+
)
|
70
|
+
expect(task_class.provider).to be_a(Lluminary::Providers::Bedrock)
|
71
|
+
expect(task_class.provider.config).to include(
|
72
|
+
access_key_id: "test",
|
73
|
+
secret_access_key: "test",
|
74
|
+
region: "us-east-1",
|
75
|
+
model: Lluminary::Models::Bedrock::AnthropicClaudeInstantV1
|
76
|
+
)
|
277
77
|
end
|
278
78
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
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
|
79
|
+
it "raises ArgumentError for unknown provider" do
|
80
|
+
expect { task_class.use_provider(:unknown) }.to raise_error(
|
81
|
+
ArgumentError,
|
82
|
+
"Unknown provider: unknown"
|
83
|
+
)
|
574
84
|
end
|
575
85
|
end
|
576
86
|
|
577
|
-
describe
|
87
|
+
describe "#validate_input" do
|
578
88
|
let(:task_with_types) do
|
579
89
|
Class.new(described_class) do
|
580
90
|
input_schema do
|
@@ -583,46 +93,58 @@ RSpec.describe Lluminary::Task do
|
|
583
93
|
datetime :start_time
|
584
94
|
end
|
585
95
|
|
586
|
-
private
|
587
|
-
|
588
96
|
def task_prompt
|
589
97
|
"Test prompt"
|
590
98
|
end
|
591
99
|
end
|
592
100
|
end
|
593
101
|
|
594
|
-
it
|
595
|
-
task =
|
102
|
+
it "validates string input type" do
|
103
|
+
task =
|
104
|
+
task_with_types.new(name: "John", age: 30, start_time: DateTime.now)
|
596
105
|
expect { task.send(:validate_input) }.not_to raise_error
|
597
106
|
end
|
598
107
|
|
599
|
-
it
|
108
|
+
it "raises error for invalid string input" do
|
600
109
|
task = task_with_types.new(name: 123, age: 30, start_time: DateTime.now)
|
601
|
-
expect { task.send(:validate_input) }.to raise_error(
|
110
|
+
expect { task.send(:validate_input) }.to raise_error(
|
111
|
+
Lluminary::ValidationError,
|
112
|
+
"Name must be a String"
|
113
|
+
)
|
602
114
|
end
|
603
115
|
|
604
|
-
it
|
605
|
-
task =
|
116
|
+
it "validates integer input type" do
|
117
|
+
task =
|
118
|
+
task_with_types.new(name: "John", age: 30, start_time: DateTime.now)
|
606
119
|
expect { task.send(:validate_input) }.not_to raise_error
|
607
120
|
end
|
608
121
|
|
609
|
-
it
|
610
|
-
task =
|
611
|
-
|
122
|
+
it "raises error for invalid integer input" do
|
123
|
+
task =
|
124
|
+
task_with_types.new(name: "John", age: "30", start_time: DateTime.now)
|
125
|
+
expect { task.send(:validate_input) }.to raise_error(
|
126
|
+
Lluminary::ValidationError,
|
127
|
+
"Age must be an Integer"
|
128
|
+
)
|
612
129
|
end
|
613
130
|
|
614
|
-
it
|
615
|
-
task =
|
131
|
+
it "validates datetime input type" do
|
132
|
+
task =
|
133
|
+
task_with_types.new(name: "John", age: 30, start_time: DateTime.now)
|
616
134
|
expect { task.send(:validate_input) }.not_to raise_error
|
617
135
|
end
|
618
136
|
|
619
|
-
it
|
620
|
-
task =
|
621
|
-
|
137
|
+
it "raises error for invalid datetime input" do
|
138
|
+
task =
|
139
|
+
task_with_types.new(name: "John", age: 30, start_time: "2024-01-01")
|
140
|
+
expect { task.send(:validate_input) }.to raise_error(
|
141
|
+
Lluminary::ValidationError,
|
142
|
+
"Start time must be a DateTime"
|
143
|
+
)
|
622
144
|
end
|
623
145
|
end
|
624
146
|
|
625
|
-
describe
|
147
|
+
describe "SchemaModel integration" do
|
626
148
|
let(:task_with_schema) do
|
627
149
|
Class.new(described_class) do
|
628
150
|
input_schema do
|
@@ -630,7 +152,11 @@ RSpec.describe Lluminary::Task do
|
|
630
152
|
integer :min_length
|
631
153
|
|
632
154
|
validates :text, presence: true
|
633
|
-
validates :min_length,
|
155
|
+
validates :min_length,
|
156
|
+
presence: true,
|
157
|
+
numericality: {
|
158
|
+
greater_than: 0
|
159
|
+
}
|
634
160
|
end
|
635
161
|
|
636
162
|
output_schema do
|
@@ -638,28 +164,26 @@ RSpec.describe Lluminary::Task do
|
|
638
164
|
integer :word_count
|
639
165
|
end
|
640
166
|
|
641
|
-
private
|
642
|
-
|
643
167
|
def task_prompt
|
644
168
|
"Test prompt"
|
645
169
|
end
|
646
170
|
end
|
647
171
|
end
|
648
172
|
|
649
|
-
it
|
173
|
+
it "wraps input in a SchemaModel instance" do
|
650
174
|
result = task_with_schema.call(text: "hello", min_length: 3)
|
651
175
|
expect(result.input).to be_a(Lluminary::SchemaModel)
|
652
176
|
expect(result.input.text).to eq("hello")
|
653
177
|
expect(result.input.min_length).to eq(3)
|
654
178
|
end
|
655
179
|
|
656
|
-
it
|
180
|
+
it "validates input using SchemaModel" do
|
657
181
|
result = task_with_schema.call(text: "hello", min_length: 3)
|
658
182
|
expect(result.input.valid?).to be true
|
659
183
|
expect(result.input.errors).to be_empty
|
660
184
|
end
|
661
185
|
|
662
|
-
it
|
186
|
+
it "returns validation errors for invalid input" do
|
663
187
|
result = task_with_schema.call(text: nil, min_length: nil)
|
664
188
|
expect(result.input.valid?).to be false
|
665
189
|
expect(result.input.errors.full_messages).to contain_exactly(
|
@@ -669,33 +193,37 @@ RSpec.describe Lluminary::Task do
|
|
669
193
|
)
|
670
194
|
end
|
671
195
|
|
672
|
-
it
|
196
|
+
it "does not execute task when input is invalid" do
|
673
197
|
result = task_with_schema.call(text: nil, min_length: nil)
|
674
198
|
expect(result.parsed_response).to be_nil
|
675
199
|
expect(result.output).to be_nil
|
676
200
|
end
|
677
201
|
|
678
|
-
it
|
679
|
-
expect
|
202
|
+
it "raises ValidationError for invalid input when using call!" do
|
203
|
+
expect do
|
680
204
|
task_with_schema.call!(text: nil, min_length: nil)
|
681
|
-
|
205
|
+
end.to raise_error(
|
206
|
+
Lluminary::ValidationError,
|
207
|
+
"Text can't be blank, Min length can't be blank, Min length is not a number"
|
208
|
+
)
|
682
209
|
end
|
683
210
|
|
684
|
-
it
|
211
|
+
it "validates that the response is valid JSON" do
|
685
212
|
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
|
-
})
|
213
|
+
allow(task.class.provider).to receive(:call).and_return(
|
214
|
+
{ raw: "not valid json at all", parsed: nil }
|
215
|
+
)
|
690
216
|
|
691
217
|
result = task.call
|
692
218
|
expect(result.input.valid?).to be true
|
693
219
|
expect(result.output.valid?).to be false
|
694
|
-
expect(result.output.errors.full_messages).to include(
|
220
|
+
expect(result.output.errors.full_messages).to include(
|
221
|
+
"Raw response must be valid JSON"
|
222
|
+
)
|
695
223
|
end
|
696
224
|
end
|
697
225
|
|
698
|
-
describe
|
226
|
+
describe "tasks without inputs" do
|
699
227
|
let(:quote_task) do
|
700
228
|
Class.new(described_class) do
|
701
229
|
use_provider :test
|
@@ -705,21 +233,19 @@ RSpec.describe Lluminary::Task do
|
|
705
233
|
string :author, description: "The person who said the quote"
|
706
234
|
end
|
707
235
|
|
708
|
-
private
|
709
|
-
|
710
236
|
def task_prompt
|
711
237
|
"Generate an inspirational quote and its author"
|
712
238
|
end
|
713
239
|
end
|
714
240
|
end
|
715
241
|
|
716
|
-
it
|
242
|
+
it "can be called without any input parameters" do
|
717
243
|
result = quote_task.call
|
718
244
|
expect(result.output.quote).to be_a(String)
|
719
245
|
expect(result.output.author).to be_a(String)
|
720
246
|
end
|
721
247
|
|
722
|
-
it
|
248
|
+
it "returns a valid result object" do
|
723
249
|
result = quote_task.call
|
724
250
|
expect(result).to be_a(Lluminary::Task)
|
725
251
|
expect(result.input).to be_a(Lluminary::SchemaModel)
|
@@ -727,7 +253,7 @@ RSpec.describe Lluminary::Task do
|
|
727
253
|
end
|
728
254
|
end
|
729
255
|
|
730
|
-
describe
|
256
|
+
describe "datetime handling" do
|
731
257
|
let(:task_with_datetime) do
|
732
258
|
Class.new(described_class) do
|
733
259
|
use_provider :test
|
@@ -736,20 +262,22 @@ RSpec.describe Lluminary::Task do
|
|
736
262
|
datetime :event_time, description: "When the event occurred"
|
737
263
|
end
|
738
264
|
|
739
|
-
private
|
740
|
-
|
741
265
|
def task_prompt
|
742
266
|
"Test prompt"
|
743
267
|
end
|
744
268
|
end
|
745
269
|
end
|
746
270
|
|
747
|
-
it
|
271
|
+
it "converts ISO8601 datetime strings to DateTime objects" do
|
748
272
|
task = task_with_datetime.new
|
749
|
-
allow(task.class.provider).to receive(:call).and_return(
|
750
|
-
|
751
|
-
|
752
|
-
|
273
|
+
allow(task.class.provider).to receive(:call).and_return(
|
274
|
+
{
|
275
|
+
raw: '{"event_time": "2024-01-01T12:00:00+00:00"}',
|
276
|
+
parsed: {
|
277
|
+
"event_time" => "2024-01-01T12:00:00+00:00"
|
278
|
+
}
|
279
|
+
}
|
280
|
+
)
|
753
281
|
|
754
282
|
result = task.call
|
755
283
|
expect(result.output.valid?).to be true
|
@@ -762,16 +290,22 @@ RSpec.describe Lluminary::Task do
|
|
762
290
|
expect(result.output.event_time.second).to eq(0)
|
763
291
|
end
|
764
292
|
|
765
|
-
it
|
293
|
+
it "handles invalid datetime strings" do
|
766
294
|
task = task_with_datetime.new
|
767
|
-
allow(task.class.provider).to receive(:call).and_return(
|
768
|
-
|
769
|
-
|
770
|
-
|
295
|
+
allow(task.class.provider).to receive(:call).and_return(
|
296
|
+
{
|
297
|
+
raw: '{"event_time": "not a valid datetime"}',
|
298
|
+
parsed: {
|
299
|
+
"event_time" => "not a valid datetime"
|
300
|
+
}
|
301
|
+
}
|
302
|
+
)
|
771
303
|
|
772
304
|
result = task.call
|
773
305
|
expect(result.output.valid?).to be false
|
774
|
-
expect(result.output.errors.full_messages).to include(
|
306
|
+
expect(result.output.errors.full_messages).to include(
|
307
|
+
"Event time must be a DateTime"
|
308
|
+
)
|
775
309
|
end
|
776
310
|
end
|
777
|
-
end
|
311
|
+
end
|