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,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
@@ -0,0 +1,4 @@
1
+ module Lluminary
2
+ class ValidationError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module Lluminary
2
+ VERSION = '0.1.0'
3
+ 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