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,224 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require_relative 'schema'
|
3
|
+
require_relative 'validation_error'
|
4
|
+
require_relative 'field_description'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Lluminary
|
8
|
+
class Task
|
9
|
+
class << self
|
10
|
+
def input_schema(&block)
|
11
|
+
@input_schema = Schema.new
|
12
|
+
@input_schema.instance_eval(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def output_schema(&block)
|
16
|
+
@output_schema = Schema.new
|
17
|
+
@output_schema.instance_eval(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def use_provider(provider_name, **config)
|
21
|
+
provider_class = case provider_name
|
22
|
+
when :openai
|
23
|
+
require_relative 'providers/openai'
|
24
|
+
Providers::OpenAI
|
25
|
+
when :test
|
26
|
+
require_relative 'providers/test'
|
27
|
+
Providers::Test
|
28
|
+
when :bedrock
|
29
|
+
require_relative 'providers/bedrock'
|
30
|
+
Providers::Bedrock
|
31
|
+
else
|
32
|
+
raise ArgumentError, "Unknown provider: #{provider_name}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Merge global config with task-specific config
|
36
|
+
global_config = Lluminary.config.provider_config(provider_name)
|
37
|
+
merged_config = global_config.merge(config)
|
38
|
+
|
39
|
+
@provider = provider_class.new(**merged_config)
|
40
|
+
end
|
41
|
+
|
42
|
+
def call(input = {})
|
43
|
+
new(input).call
|
44
|
+
end
|
45
|
+
|
46
|
+
def call!(input = {})
|
47
|
+
new(input).call!
|
48
|
+
end
|
49
|
+
|
50
|
+
def provider
|
51
|
+
@provider ||= begin
|
52
|
+
require_relative 'providers/test'
|
53
|
+
Providers::Test.new
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def provider=(provider)
|
58
|
+
@provider = provider
|
59
|
+
end
|
60
|
+
|
61
|
+
def input_fields
|
62
|
+
@input_schema&.fields || {}
|
63
|
+
end
|
64
|
+
|
65
|
+
def output_fields
|
66
|
+
@output_schema&.fields || {}
|
67
|
+
end
|
68
|
+
|
69
|
+
def input_schema_model
|
70
|
+
@input_schema&.schema_model || Schema.new.schema_model
|
71
|
+
end
|
72
|
+
|
73
|
+
def output_schema_model
|
74
|
+
@output_schema&.schema_model || Schema.new.schema_model
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
attr_reader :input, :output, :parsed_response
|
79
|
+
|
80
|
+
def initialize(input = {})
|
81
|
+
@input = self.class.input_schema_model.new(input)
|
82
|
+
define_input_methods
|
83
|
+
end
|
84
|
+
|
85
|
+
def call
|
86
|
+
if valid?
|
87
|
+
response = self.class.provider.call(prompt, self)
|
88
|
+
process_response(response)
|
89
|
+
else
|
90
|
+
@parsed_response = nil
|
91
|
+
@output = nil
|
92
|
+
end
|
93
|
+
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
def call!
|
98
|
+
validate_input!
|
99
|
+
response = self.class.provider.call(prompt, self)
|
100
|
+
process_response(response)
|
101
|
+
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
def valid?
|
106
|
+
@input.valid?
|
107
|
+
end
|
108
|
+
|
109
|
+
def validate_input!
|
110
|
+
unless @input.valid?
|
111
|
+
raise ValidationError, @input.errors.full_messages.join(", ")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def prompt
|
116
|
+
base_prompt = <<~PROMPT
|
117
|
+
#{task_prompt}
|
118
|
+
|
119
|
+
#{json_schema_example}
|
120
|
+
PROMPT
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def validate_input
|
126
|
+
validate_input!
|
127
|
+
end
|
128
|
+
|
129
|
+
def process_response(response)
|
130
|
+
@parsed_response = response[:parsed]
|
131
|
+
@output = self.class.output_schema_model.new
|
132
|
+
@output.raw_response = response[:raw]
|
133
|
+
|
134
|
+
# Merge the parsed response first, then validate
|
135
|
+
if @parsed_response.is_a?(Hash)
|
136
|
+
# Get datetime fields from schema
|
137
|
+
datetime_fields = self.class.output_fields.select { |_, field| field[:type] == :datetime }.keys
|
138
|
+
|
139
|
+
# Convert datetime fields
|
140
|
+
converted_response = @parsed_response.dup
|
141
|
+
datetime_fields.each do |field_name|
|
142
|
+
if converted_response.key?(field_name.to_s) && converted_response[field_name.to_s].is_a?(String)
|
143
|
+
begin
|
144
|
+
converted_response[field_name.to_s] = DateTime.parse(converted_response[field_name.to_s])
|
145
|
+
rescue ArgumentError
|
146
|
+
# Leave as string, validation will fail
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
@output.attributes.merge!(converted_response)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Validate after merging
|
155
|
+
@output.valid?
|
156
|
+
|
157
|
+
@prompt = prompt
|
158
|
+
end
|
159
|
+
|
160
|
+
def define_input_methods
|
161
|
+
self.class.input_fields.each_key do |name|
|
162
|
+
define_singleton_method(name) { @input.attributes[name.to_s] }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def task_prompt
|
167
|
+
raise NotImplementedError, "Subclasses must implement task_prompt"
|
168
|
+
end
|
169
|
+
|
170
|
+
def json_schema_example
|
171
|
+
return "{}" if fields.empty?
|
172
|
+
|
173
|
+
<<~SCHEMA.chomp
|
174
|
+
You must respond with ONLY a valid JSON object. Do not include any other text, explanations, or formatting.
|
175
|
+
The JSON object must contain the following fields:
|
176
|
+
|
177
|
+
#{generate_field_descriptions}
|
178
|
+
|
179
|
+
Your response must be ONLY this JSON object:
|
180
|
+
#{example_json}
|
181
|
+
SCHEMA
|
182
|
+
end
|
183
|
+
|
184
|
+
def fields
|
185
|
+
@fields ||= self.class.output_fields
|
186
|
+
end
|
187
|
+
|
188
|
+
def generate_field_descriptions
|
189
|
+
fields.map do |name, field|
|
190
|
+
# Get validations for this field
|
191
|
+
validations = self.class.instance_variable_get(:@output_schema)&.validations_for(name) || []
|
192
|
+
field_with_validations = field.merge(validations: validations)
|
193
|
+
FieldDescription.new(name, field_with_validations).to_schema_s
|
194
|
+
end.join("\n\n")
|
195
|
+
end
|
196
|
+
|
197
|
+
def example_json
|
198
|
+
json = fields.each_with_object({}) do |(name, field), hash|
|
199
|
+
hash[name] = case field[:type]
|
200
|
+
when :string
|
201
|
+
"your #{name} here"
|
202
|
+
when :integer
|
203
|
+
0
|
204
|
+
when :datetime
|
205
|
+
"2024-01-01T12:00:00+00:00"
|
206
|
+
when :boolean
|
207
|
+
true
|
208
|
+
when :float
|
209
|
+
0.0
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
JSON.pretty_generate(json)
|
214
|
+
end
|
215
|
+
|
216
|
+
def to_result
|
217
|
+
Result.new(
|
218
|
+
raw_response: @output&.raw_response,
|
219
|
+
output: @parsed_response,
|
220
|
+
prompt: @prompt
|
221
|
+
)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
data/lib/lluminary.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'lluminary/version'
|
2
|
+
require_relative 'lluminary/result'
|
3
|
+
require_relative 'lluminary/task'
|
4
|
+
require_relative 'lluminary/providers/base'
|
5
|
+
require_relative 'lluminary/providers/openai'
|
6
|
+
require_relative 'lluminary/config'
|
7
|
+
|
8
|
+
module Lluminary
|
9
|
+
class << self
|
10
|
+
def config
|
11
|
+
@config ||= Config.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def configure
|
15
|
+
yield config
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../examples/analyze_text'
|
3
|
+
|
4
|
+
RSpec.describe AnalyzeText do
|
5
|
+
let(:text) { "Ruby is a dynamic, open source programming language with a focus on simplicity and productivity. It has an elegant syntax that is natural to read and easy to write." }
|
6
|
+
|
7
|
+
it "analyzes text" do
|
8
|
+
result = described_class.call(text: text)
|
9
|
+
expect(result.output.analysis).to be_a(String)
|
10
|
+
expect(result.output.analysis).not_to be_empty
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns JSON response" do
|
14
|
+
result = described_class.call(text: text)
|
15
|
+
expect(result.output.raw_response).to be_a(String)
|
16
|
+
expect { JSON.parse(result.output.raw_response) }.not_to raise_error
|
17
|
+
json = JSON.parse(result.output.raw_response)
|
18
|
+
expect(json).to have_key("analysis")
|
19
|
+
expect(json["analysis"]).to eq(result.output.analysis)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../examples/color_analyzer'
|
3
|
+
|
4
|
+
RSpec.describe ColorAnalyzer do
|
5
|
+
describe '#call' do
|
6
|
+
it 'returns "red" for a description strongly suggesting the color red' do
|
7
|
+
result = described_class.call(
|
8
|
+
image_description: "A bright red sports car parked in front of a red brick building at sunset. The car's glossy red paint reflects the warm light, making it appear even more vibrant. A red stop sign stands nearby, and red roses bloom in a garden beside the building."
|
9
|
+
)
|
10
|
+
|
11
|
+
expect(result.output.color_name).to eq("red")
|
12
|
+
expect(result.output.valid?).to be true
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'returns invalid output when description strongly suggests orange' do
|
16
|
+
# This should fail validation because "orange" is not a CSS Level 1 color
|
17
|
+
result = described_class.call(
|
18
|
+
image_description: "A field of ripe oranges under a bright orange sunset. The fruit glows with a warm orange hue, and the sky is painted in shades of orange and gold. Orange butterflies flutter among the trees, and orange flowers bloom throughout the scene."
|
19
|
+
)
|
20
|
+
|
21
|
+
expect(result.output.valid?).to be false
|
22
|
+
expect(result.output.errors.full_messages).to include("Color name must be a valid CSS level 1 color name")
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'validates presence of image_description' do
|
26
|
+
expect {
|
27
|
+
described_class.call!(
|
28
|
+
image_description: ""
|
29
|
+
)
|
30
|
+
}.to raise_error(Lluminary::ValidationError)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'validates that color_name is lowercase' do
|
34
|
+
result = described_class.call(
|
35
|
+
image_description: "A bright red sports car"
|
36
|
+
)
|
37
|
+
|
38
|
+
expect(result.output.valid?).to be true
|
39
|
+
expect(result.output.color_name).to eq(result.output.color_name.downcase)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../examples/content_analyzer'
|
3
|
+
|
4
|
+
RSpec.describe ContentAnalyzer do
|
5
|
+
let(:technical_text) do
|
6
|
+
<<~TEXT
|
7
|
+
The revolutionary new quantum processor leverages advanced photonic circuits to achieve unprecedented computational speeds.
|
8
|
+
By utilizing entangled photon pairs, it can perform complex calculations in parallel, significantly reducing processing time.
|
9
|
+
This breakthrough technology represents a major advancement in quantum computing.
|
10
|
+
TEXT
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:emotional_text) do
|
14
|
+
<<~TEXT
|
15
|
+
I can't believe how amazing this experience was! My heart was racing with excitement as I watched the performance.
|
16
|
+
The raw emotion and passion in every movement brought tears to my eyes. It was truly a transformative moment that I'll never forget.
|
17
|
+
TEXT
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#call' do
|
21
|
+
it 'correctly identifies technical content' do
|
22
|
+
result = described_class.call(
|
23
|
+
text: technical_text,
|
24
|
+
content_type: "technical"
|
25
|
+
)
|
26
|
+
|
27
|
+
expect(result.output.contains_type).to be true
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'correctly identifies non-technical content' do
|
31
|
+
result = described_class.call(
|
32
|
+
text: emotional_text,
|
33
|
+
content_type: "technical"
|
34
|
+
)
|
35
|
+
|
36
|
+
expect(result.output.contains_type).to be false
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'correctly identifies emotional content' do
|
40
|
+
result = described_class.call(
|
41
|
+
text: emotional_text,
|
42
|
+
content_type: "emotional"
|
43
|
+
)
|
44
|
+
|
45
|
+
expect(result.output.contains_type).to be true
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'correctly identifies non-emotional content' do
|
49
|
+
result = described_class.call(
|
50
|
+
text: technical_text,
|
51
|
+
content_type: "emotional"
|
52
|
+
)
|
53
|
+
|
54
|
+
expect(result.output.contains_type).to be false
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'validates presence of text' do
|
58
|
+
expect {
|
59
|
+
described_class.call!(
|
60
|
+
text: "",
|
61
|
+
content_type: "technical"
|
62
|
+
)
|
63
|
+
}.to raise_error(Lluminary::ValidationError)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'validates presence of content_type' do
|
67
|
+
expect {
|
68
|
+
described_class.call!(
|
69
|
+
text: technical_text,
|
70
|
+
content_type: ""
|
71
|
+
)
|
72
|
+
}.to raise_error(Lluminary::ValidationError)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../examples/historical_event_analyzer'
|
3
|
+
require 'pry-byebug'
|
4
|
+
|
5
|
+
RSpec.describe HistoricalEventAnalyzer do
|
6
|
+
describe '#call' do
|
7
|
+
context 'with events that have known exact times' do
|
8
|
+
it 'returns the exact time of the human step on the moon' do
|
9
|
+
result = described_class.call(
|
10
|
+
event_description: "Neil Armstrong's first step onto the Moon"
|
11
|
+
)
|
12
|
+
|
13
|
+
expect(result.output.valid?).to be true
|
14
|
+
expect(result.output.event_datetime).to be_a(DateTime)
|
15
|
+
# LLMs are too flaky to get the exact date and time correct, at least with my prompt.
|
16
|
+
expect(result.output.event_datetime.year).to eq(1969)
|
17
|
+
expect(result.output.event_datetime.month).to eq(7)
|
18
|
+
expect(result.output.exact_time_is_known).to be true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'with events that have approximate times' do
|
23
|
+
it 'returns midnight for the fall of the Roman Empire' do
|
24
|
+
result = described_class.call(
|
25
|
+
event_description: "Assassination of Julius Caesar"
|
26
|
+
)
|
27
|
+
|
28
|
+
expect(result.output.valid?).to be true
|
29
|
+
expect(result.output.event_datetime).to be_a(DateTime)
|
30
|
+
expect(result.output.event_datetime.year).to eq(44)
|
31
|
+
expect(result.output.event_datetime.month).to eq(3)
|
32
|
+
expect(result.output.event_datetime.day).to eq(15)
|
33
|
+
expect(result.output.exact_time_is_known).to be false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../examples/price_analyzer'
|
3
|
+
|
4
|
+
RSpec.describe PriceAnalyzer do
|
5
|
+
describe '#call' do
|
6
|
+
it 'returns a competitiveness score between 0.0 and 1.0 for a high-priced item' do
|
7
|
+
result = described_class.call(
|
8
|
+
product_name: "Entry LevelLuxury Watch",
|
9
|
+
price: 999.99
|
10
|
+
)
|
11
|
+
|
12
|
+
expect(result.output.competitiveness_score).to be_a(Float)
|
13
|
+
expect(result.output.competitiveness_score).to be_between(0.0, 1.0)
|
14
|
+
expect(result.output.competitiveness_score).to be >= 0.5 # Lower priced luxury watch is more competitive
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'returns a higher competitiveness score for a reasonably priced item' do
|
18
|
+
result = described_class.call(
|
19
|
+
product_name: "Basic Watch",
|
20
|
+
price: 10049.99
|
21
|
+
)
|
22
|
+
|
23
|
+
expect(result.output.competitiveness_score).to be_a(Float)
|
24
|
+
expect(result.output.competitiveness_score).to be_between(0.0, 1.0)
|
25
|
+
expect(result.output.competitiveness_score).to be <= 0.5 # Higher priced basic watch is less competitive
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'validates presence of product_name' do
|
29
|
+
expect {
|
30
|
+
described_class.call!(
|
31
|
+
product_name: "",
|
32
|
+
price: 49.99
|
33
|
+
)
|
34
|
+
}.to raise_error(Lluminary::ValidationError)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'validates presence of price' do
|
38
|
+
expect {
|
39
|
+
described_class.call!(
|
40
|
+
product_name: "Basic Watch",
|
41
|
+
price: nil
|
42
|
+
)
|
43
|
+
}.to raise_error(Lluminary::ValidationError)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../examples/quote_task'
|
3
|
+
|
4
|
+
RSpec.describe QuoteTask do
|
5
|
+
describe '.call' do
|
6
|
+
it 'returns a quote and its author' do
|
7
|
+
result = described_class.call
|
8
|
+
|
9
|
+
expect(result.output.quote).to be_a(String)
|
10
|
+
expect(result.output.quote).not_to be_empty
|
11
|
+
|
12
|
+
expect(result.output.author).to be_a(String)
|
13
|
+
expect(result.output.author).not_to be_empty
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'can be called without any input parameters' do
|
17
|
+
expect { described_class.call }.not_to raise_error
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns a valid result object' do
|
21
|
+
result = described_class.call
|
22
|
+
expect(result).to be_a(Lluminary::Task)
|
23
|
+
expect(result.input).to be_a(Lluminary::SchemaModel)
|
24
|
+
expect(result.input.valid?).to be true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../examples/sentiment_analysis'
|
3
|
+
|
4
|
+
RSpec.describe SentimentAnalysis do
|
5
|
+
let(:text) { "I absolutely love this new feature!" }
|
6
|
+
|
7
|
+
describe '.call' do
|
8
|
+
it 'analyzes sentiment of given text' do
|
9
|
+
result = described_class.call(text: text)
|
10
|
+
|
11
|
+
expect(result.output.sentiment).to be_a(String)
|
12
|
+
expect(result.output.sentiment).not_to be_empty
|
13
|
+
expect(result.output.explanation).to be_a(String)
|
14
|
+
expect(result.output.explanation).not_to be_empty
|
15
|
+
expect(result.output.confidence).to be_a(Integer)
|
16
|
+
expect(result.output.confidence).to be_between(0, 100)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'returns valid JSON response' do
|
20
|
+
result = described_class.call(text: text)
|
21
|
+
expect(result.output.raw_response).to be_a(String)
|
22
|
+
expect { JSON.parse(result.output.raw_response) }.not_to raise_error
|
23
|
+
json = JSON.parse(result.output.raw_response)
|
24
|
+
expect(json).to have_key("sentiment")
|
25
|
+
expect(json).to have_key("explanation")
|
26
|
+
expect(json).to have_key("confidence")
|
27
|
+
expect(json["sentiment"]).to eq(result.output.sentiment)
|
28
|
+
expect(json["explanation"]).to eq(result.output.explanation)
|
29
|
+
expect(json["confidence"]).to eq(result.output.confidence)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'requires text input' do
|
33
|
+
result = described_class.call({})
|
34
|
+
expect(result.valid?).to be false
|
35
|
+
expect(result.input.errors.full_messages).to include("Text can't be blank")
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'returns a valid result object' do
|
39
|
+
result = described_class.call(text: text)
|
40
|
+
expect(result).to be_a(Lluminary::Task)
|
41
|
+
expect(result.input).to be_a(Lluminary::SchemaModel)
|
42
|
+
expect(result.input.valid?).to be true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../examples/summarize_text'
|
3
|
+
|
4
|
+
RSpec.describe SummarizeText do
|
5
|
+
let(:text) { "Ruby is a dynamic, open source programming language with a focus on simplicity and productivity. It has an elegant syntax that is natural to read and easy to write." }
|
6
|
+
|
7
|
+
it "summarizes text" do
|
8
|
+
result = described_class.call(text: text)
|
9
|
+
expect(result.output.summary).to be_a(String)
|
10
|
+
expect(result.output.summary).not_to be_empty
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns JSON response" do
|
14
|
+
result = described_class.call(text: text)
|
15
|
+
expect(result.output.raw_response).to be_a(String)
|
16
|
+
expect { JSON.parse(result.output.raw_response) }.not_to raise_error
|
17
|
+
json = JSON.parse(result.output.raw_response)
|
18
|
+
expect(json).to have_key("summary")
|
19
|
+
expect(json["summary"]).to eq(result.output.summary)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lluminary::Config do
|
4
|
+
let(:config) { described_class.new }
|
5
|
+
|
6
|
+
describe '#configure' do
|
7
|
+
it 'allows setting provider configurations' do
|
8
|
+
config.configure do |c|
|
9
|
+
c.provider(:openai, api_key: 'global-key')
|
10
|
+
c.provider(:bedrock,
|
11
|
+
access_key_id: 'global-access-key',
|
12
|
+
secret_access_key: 'global-secret-key',
|
13
|
+
region: 'us-east-1'
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
expect(config.provider_config(:openai)).to eq({ api_key: 'global-key' })
|
18
|
+
expect(config.provider_config(:bedrock)).to eq({
|
19
|
+
access_key_id: 'global-access-key',
|
20
|
+
secret_access_key: 'global-secret-key',
|
21
|
+
region: 'us-east-1'
|
22
|
+
})
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#provider_config' do
|
27
|
+
it 'returns empty hash for unconfigured providers' do
|
28
|
+
expect(config.provider_config(:unknown)).to eq({})
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'returns the configuration for a configured provider' do
|
32
|
+
config.configure do |c|
|
33
|
+
c.provider(:openai, api_key: 'test-key')
|
34
|
+
end
|
35
|
+
|
36
|
+
expect(config.provider_config(:openai)).to eq({ api_key: 'test-key' })
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#reset!' do
|
41
|
+
it 'clears all provider configurations' do
|
42
|
+
config.configure do |c|
|
43
|
+
c.provider(:openai, api_key: 'test-key')
|
44
|
+
c.provider(:bedrock, access_key_id: 'test-access-key')
|
45
|
+
end
|
46
|
+
|
47
|
+
config.reset!
|
48
|
+
|
49
|
+
expect(config.provider_config(:openai)).to eq({})
|
50
|
+
expect(config.provider_config(:bedrock)).to eq({})
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lluminary::FieldDescription do
|
4
|
+
describe '#to_s' do
|
5
|
+
it 'generates a description for a string field' do
|
6
|
+
field = {
|
7
|
+
type: :string,
|
8
|
+
description: 'A test field'
|
9
|
+
}
|
10
|
+
description = described_class.new('test_field', field)
|
11
|
+
expect(description.to_s).to eq('test_field (string): A test field')
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'generates a description for a field without a description' do
|
15
|
+
field = {
|
16
|
+
type: :integer
|
17
|
+
}
|
18
|
+
description = described_class.new('count', field)
|
19
|
+
expect(description.to_s).to eq('count (integer)')
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'includes validation descriptions when present' do
|
23
|
+
field = {
|
24
|
+
type: :string,
|
25
|
+
description: 'A test field',
|
26
|
+
validations: [
|
27
|
+
[{}, { length: { minimum: 5, maximum: 10 } }],
|
28
|
+
[{}, { format: { with: '/^[A-Z]+$/' } }]
|
29
|
+
]
|
30
|
+
}
|
31
|
+
description = described_class.new('test_field', field)
|
32
|
+
expected = 'test_field (string): A test field (must be at least 5 characters, must be at most 10 characters, must match format: /^[A-Z]+$/)'
|
33
|
+
expect(description.to_s).to eq(expected)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'lluminary'
|
2
|
+
|
3
|
+
RSpec.describe Lluminary::Providers::Base do
|
4
|
+
describe '#initialize' do
|
5
|
+
it 'accepts configuration options' do
|
6
|
+
config = { api_key: 'test_key', model: 'test_model' }
|
7
|
+
provider = described_class.new(**config)
|
8
|
+
expect(provider.config).to eq(config)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#call' do
|
13
|
+
it 'raises NotImplementedError' do
|
14
|
+
expect { described_class.new.call('test', double('Task')) }.to raise_error(NotImplementedError)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|