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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/lluminary/config.rb +6 -1
  3. data/lib/lluminary/models/base.rb +235 -0
  4. data/lib/lluminary/models/bedrock/amazon_nova_pro_v1.rb +19 -0
  5. data/lib/lluminary/models/bedrock/anthropic_claude_instant_v1.rb +17 -0
  6. data/lib/lluminary/models/bedrock/base.rb +29 -0
  7. data/lib/lluminary/models/openai/gpt35_turbo.rb +20 -0
  8. data/lib/lluminary/provider_error.rb +2 -1
  9. data/lib/lluminary/providers/base.rb +20 -3
  10. data/lib/lluminary/providers/bedrock.rb +52 -32
  11. data/lib/lluminary/providers/openai.rb +41 -24
  12. data/lib/lluminary/providers/test.rb +14 -13
  13. data/lib/lluminary/result.rb +5 -2
  14. data/lib/lluminary/schema.rb +59 -15
  15. data/lib/lluminary/schema_model.rb +67 -10
  16. data/lib/lluminary/task.rb +58 -99
  17. data/lib/lluminary/validation_error.rb +2 -1
  18. data/lib/lluminary/version.rb +3 -2
  19. data/lib/lluminary.rb +25 -7
  20. data/spec/examples/analyze_text_spec.rb +7 -4
  21. data/spec/examples/color_analyzer_spec.rb +22 -22
  22. data/spec/examples/content_analyzer_spec.rb +27 -44
  23. data/spec/examples/historical_event_analyzer_spec.rb +18 -15
  24. data/spec/examples/meal_suggester_spec.rb +64 -0
  25. data/spec/examples/price_analyzer_spec.rb +22 -28
  26. data/spec/examples/quote_task_spec.rb +9 -8
  27. data/spec/examples/sentiment_analysis_spec.rb +13 -10
  28. data/spec/examples/summarize_text_spec.rb +7 -4
  29. data/spec/lluminary/config_spec.rb +28 -26
  30. data/spec/lluminary/models/base_spec.rb +581 -0
  31. data/spec/lluminary/models/bedrock/amazon_nova_pro_v1_spec.rb +30 -0
  32. data/spec/lluminary/models/bedrock/anthropic_claude_instant_v1_spec.rb +21 -0
  33. data/spec/lluminary/models/openai/gpt35_turbo_spec.rb +22 -0
  34. data/spec/lluminary/providers/bedrock_spec.rb +86 -57
  35. data/spec/lluminary/providers/openai_spec.rb +58 -34
  36. data/spec/lluminary/providers/test_spec.rb +46 -16
  37. data/spec/lluminary/result_spec.rb +17 -10
  38. data/spec/lluminary/schema_model_spec.rb +108 -22
  39. data/spec/lluminary/schema_spec.rb +241 -107
  40. data/spec/lluminary/task_spec.rb +118 -584
  41. data/spec/spec_helper.rb +8 -2
  42. metadata +73 -22
  43. data/lib/lluminary/field_description.rb +0 -148
  44. data/spec/lluminary/field_description_spec.rb +0 -36
  45. data/spec/lluminary/providers/base_spec.rb +0 -17
@@ -1,13 +1,10 @@
1
- require 'active_model'
2
- require_relative 'schema_model'
1
+ # frozen_string_literal: true
2
+ require "active_model"
3
+ require_relative "schema_model"
3
4
 
4
5
  module Lluminary
5
- class JsonValidator < ActiveModel::EachValidator
6
- def validate_each(record, attribute, value)
7
- record.errors.add(:base, "Response must be valid JSON") unless value.is_a?(Hash)
8
- end
9
- end
10
-
6
+ # Represents a JSON schema for validating task inputs and outputs.
7
+ # Provides methods for defining and validating schemas.
11
8
  class Schema
12
9
  def initialize
13
10
  @fields = {}
@@ -34,12 +31,30 @@ module Lluminary
34
31
  @fields[name] = { type: :datetime, description: description }
35
32
  end
36
33
 
37
- def fields
38
- @fields
34
+ def array(name, description: nil, &block)
35
+ field = { type: :array, description: description }
36
+
37
+ if block
38
+ element_schema = ArrayElementSchema.new
39
+ field[:element_type] = element_schema.instance_eval(&block)
40
+ end
41
+
42
+ @fields[name] = field
39
43
  end
40
44
 
45
+ attr_reader :fields
46
+
41
47
  def validates(*args, **options)
42
48
  @validations << [args, options]
49
+ # Attach the validation to each field it applies to
50
+ args.each do |field_name|
51
+ field = @fields[field_name]
52
+ next unless field # Skip if field doesn't exist yet
53
+
54
+ field[:validations] ||= []
55
+ # Store each validation option separately
56
+ options.each { |key, value| field[:validations] << { key => value } }
57
+ end
43
58
  end
44
59
 
45
60
  def validations_for(field_name)
@@ -47,15 +62,44 @@ module Lluminary
47
62
  end
48
63
 
49
64
  def schema_model
50
- @schema_model ||= SchemaModel.build(
51
- fields: @fields,
52
- validations: @validations
53
- )
65
+ @schema_model ||=
66
+ SchemaModel.build(fields: @fields, validations: @validations)
54
67
  end
55
68
 
56
69
  def validate(values)
57
70
  instance = schema_model.new(values)
58
71
  instance.valid? ? [] : instance.errors.full_messages
59
72
  end
73
+
74
+ # Internal class for defining array element types
75
+ class ArrayElementSchema
76
+ def string(description: nil)
77
+ { type: :string, description: description }
78
+ end
79
+
80
+ def integer(description: nil)
81
+ { type: :integer, description: description }
82
+ end
83
+
84
+ def boolean(description: nil)
85
+ { type: :boolean, description: description }
86
+ end
87
+
88
+ def float(description: nil)
89
+ { type: :float, description: description }
90
+ end
91
+
92
+ def datetime(description: nil)
93
+ { type: :datetime, description: description }
94
+ end
95
+
96
+ def array(description: nil, &block)
97
+ field = { type: :array, description: description }
98
+ field[:element_type] = ArrayElementSchema.new.instance_eval(
99
+ &block
100
+ ) if block
101
+ field
102
+ end
103
+ end
60
104
  end
61
- end
105
+ end
@@ -1,6 +1,9 @@
1
- require 'active_model'
1
+ # frozen_string_literal: true
2
+ require "active_model"
2
3
 
3
4
  module Lluminary
5
+ # Base class for models that use JSON schema validation.
6
+ # Provides ActiveModel integration and schema validation.
4
7
  class SchemaModel
5
8
  include ActiveModel::Validations
6
9
 
@@ -12,7 +15,7 @@ module Lluminary
12
15
 
13
16
  def to_s
14
17
  attrs = attributes.dup
15
- attrs.delete('raw_response')
18
+ attrs.delete("raw_response")
16
19
  "#<#{self.class.name} #{attrs.inspect}>"
17
20
  end
18
21
 
@@ -25,8 +28,10 @@ module Lluminary
25
28
  end
26
29
 
27
30
  # Add raw_response field and validation
28
- define_method(:raw_response) { @attributes['raw_response'] }
29
- define_method(:raw_response=) { |value| @attributes['raw_response'] = value }
31
+ define_method(:raw_response) { @attributes["raw_response"] }
32
+ define_method(:raw_response=) do |value|
33
+ @attributes["raw_response"] = value
34
+ end
30
35
 
31
36
  validate do |record|
32
37
  if record.raw_response
@@ -40,8 +45,60 @@ module Lluminary
40
45
 
41
46
  # Add type validations
42
47
  validate do |record|
48
+ def validate_array_field(
49
+ record,
50
+ name,
51
+ value,
52
+ element_type,
53
+ path = nil
54
+ )
55
+ field_name = path || name
56
+
57
+ unless value.is_a?(Array)
58
+ record.errors.add(field_name, "must be an Array")
59
+ return
60
+ end
61
+
62
+ return unless element_type # untyped array
63
+
64
+ value.each_with_index do |element, index|
65
+ current_path = "#{field_name}[#{index}]"
66
+
67
+ case element_type[:type]
68
+ when :array
69
+ validate_array_field(
70
+ record,
71
+ name,
72
+ element,
73
+ element_type[:element_type],
74
+ current_path
75
+ )
76
+ when :string
77
+ unless element.is_a?(String)
78
+ record.errors.add(current_path, "must be a String")
79
+ end
80
+ when :integer
81
+ unless element.is_a?(Integer)
82
+ record.errors.add(current_path, "must be an Integer")
83
+ end
84
+ when :boolean
85
+ unless [true, false].include?(element)
86
+ record.errors.add(current_path, "must be true or false")
87
+ end
88
+ when :float
89
+ unless element.is_a?(Float)
90
+ record.errors.add(current_path, "must be a float")
91
+ end
92
+ when :datetime
93
+ unless element.is_a?(DateTime)
94
+ record.errors.add(current_path, "must be a DateTime")
95
+ end
96
+ end
97
+ end
98
+ end
99
+
43
100
  record.attributes.each do |name, value|
44
- next if name == 'raw_response'
101
+ next if name == "raw_response"
45
102
  next if value.nil?
46
103
 
47
104
  field = fields[name.to_sym]
@@ -57,7 +114,7 @@ module Lluminary
57
114
  record.errors.add(name, "must be an Integer")
58
115
  end
59
116
  when :boolean
60
- unless value == true || value == false
117
+ unless [true, false].include?(value)
61
118
  record.errors.add(name, "must be true or false")
62
119
  end
63
120
  when :float
@@ -68,14 +125,14 @@ module Lluminary
68
125
  unless value.is_a?(DateTime)
69
126
  record.errors.add(name, "must be a DateTime")
70
127
  end
128
+ when :array
129
+ validate_array_field(record, name, value, field[:element_type])
71
130
  end
72
131
  end
73
132
  end
74
133
 
75
134
  # Add ActiveModel validations
76
- validations.each do |args, options|
77
- validates(*args, **options)
78
- end
135
+ validations.each { |args, options| validates(*args, **options) }
79
136
 
80
137
  # Set model name for error messages
81
138
  define_singleton_method(:model_name) do
@@ -84,4 +141,4 @@ module Lluminary
84
141
  end
85
142
  end
86
143
  end
87
- end
144
+ end
@@ -1,10 +1,15 @@
1
- require 'ostruct'
2
- require_relative 'schema'
3
- require_relative 'validation_error'
4
- require_relative 'field_description'
5
- require 'json'
1
+ # frozen_string_literal: true
2
+ require "ostruct"
3
+ require "json"
4
+ require_relative "schema"
5
+ require_relative "validation_error"
6
+ require_relative "models/base"
7
+ require_relative "models/openai/gpt35_turbo"
8
+ require_relative "models/bedrock/anthropic_claude_instant_v1"
6
9
 
7
10
  module Lluminary
11
+ # Base class for all Lluminary tasks.
12
+ # Provides the core functionality for defining and running LLM-powered tasks.
8
13
  class Task
9
14
  class << self
10
15
  def input_schema(&block)
@@ -18,25 +23,22 @@ module Lluminary
18
23
  end
19
24
 
20
25
  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)
26
+ provider_class =
27
+ case provider_name
28
+ when :openai
29
+ require_relative "providers/openai"
30
+ Providers::OpenAI
31
+ when :test
32
+ require_relative "providers/test"
33
+ Providers::Test
34
+ when :bedrock
35
+ require_relative "providers/bedrock"
36
+ Providers::Bedrock
37
+ else
38
+ raise ArgumentError, "Unknown provider: #{provider_name}"
39
+ end
38
40
 
39
- @provider = provider_class.new(**merged_config)
41
+ @provider = provider_class.new(**config)
40
42
  end
41
43
 
42
44
  def call(input = {})
@@ -48,15 +50,14 @@ module Lluminary
48
50
  end
49
51
 
50
52
  def provider
51
- @provider ||= begin
52
- require_relative 'providers/test'
53
- Providers::Test.new
54
- end
53
+ @provider ||=
54
+ begin
55
+ require_relative "providers/test"
56
+ Providers::Test.new
57
+ end
55
58
  end
56
59
 
57
- def provider=(provider)
58
- @provider = provider
59
- end
60
+ attr_writer :provider
60
61
 
61
62
  def input_fields
62
63
  @input_schema&.fields || {}
@@ -98,7 +99,7 @@ module Lluminary
98
99
  validate_input!
99
100
  response = self.class.provider.call(prompt, self)
100
101
  process_response(response)
101
-
102
+
102
103
  self
103
104
  end
104
105
 
@@ -107,17 +108,16 @@ module Lluminary
107
108
  end
108
109
 
109
110
  def validate_input!
110
- unless @input.valid?
111
- raise ValidationError, @input.errors.full_messages.join(", ")
112
- end
111
+ return if @input.valid?
112
+ raise ValidationError, @input.errors.full_messages.join(", ")
113
113
  end
114
114
 
115
115
  def prompt
116
- base_prompt = <<~PROMPT
117
- #{task_prompt}
116
+ @prompt ||= self.class.provider.model.format_prompt(self)
117
+ end
118
118
 
119
- #{json_schema_example}
120
- PROMPT
119
+ def task_prompt
120
+ raise NotImplementedError, "Subclasses must implement task_prompt"
121
121
  end
122
122
 
123
123
  private
@@ -134,27 +134,36 @@ module Lluminary
134
134
  # Merge the parsed response first, then validate
135
135
  if @parsed_response.is_a?(Hash)
136
136
  # Get datetime fields from schema
137
- datetime_fields = self.class.output_fields.select { |_, field| field[:type] == :datetime }.keys
137
+ datetime_fields =
138
+ self
139
+ .class
140
+ .output_fields
141
+ .select { |_, field| field[:type] == :datetime }
142
+ .keys
138
143
 
139
144
  # Convert datetime fields
140
145
  converted_response = @parsed_response.dup
141
146
  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
147
+ unless converted_response.key?(field_name.to_s) &&
148
+ converted_response[field_name.to_s].is_a?(String)
149
+ next
150
+ end
151
+ begin
152
+ converted_response[field_name.to_s] = DateTime.parse(
153
+ converted_response[field_name.to_s]
154
+ )
155
+ rescue ArgumentError
156
+ # Leave as string, validation will fail
148
157
  end
149
158
  end
150
159
 
151
160
  @output.attributes.merge!(converted_response)
152
161
  end
153
-
162
+
154
163
  # Validate after merging
155
164
  @output.valid?
156
165
 
157
- @prompt = prompt
166
+ prompt
158
167
  end
159
168
 
160
169
  def define_input_methods
@@ -163,62 +172,12 @@ module Lluminary
163
172
  end
164
173
  end
165
174
 
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
175
  def to_result
217
176
  Result.new(
218
177
  raw_response: @output&.raw_response,
219
178
  output: @parsed_response,
220
- prompt: @prompt
179
+ prompt: prompt
221
180
  )
222
181
  end
223
182
  end
224
- end
183
+ end
@@ -1,4 +1,5 @@
1
+ # frozen_string_literal: true
1
2
  module Lluminary
2
3
  class ValidationError < StandardError
3
4
  end
4
- end
5
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Lluminary
2
- VERSION = '0.1.0'
3
- end
3
+ VERSION = "0.1.0"
4
+ end
data/lib/lluminary.rb CHANGED
@@ -1,10 +1,24 @@
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'
1
+ # frozen_string_literal: true
7
2
 
3
+ require_relative "lluminary/version"
4
+ require_relative "lluminary/result"
5
+ require_relative "lluminary/task"
6
+ # automatically require all providers
7
+ Dir[File.join(__dir__, "lluminary/providers/*.rb")].each { |file| require file }
8
+ # automatically require all models
9
+ Dir[File.join(__dir__, "lluminary/models/**/*.rb")].each { |file| require file }
10
+ require_relative "lluminary/config"
11
+
12
+ # Lluminary is a framework for building and running LLM-powered tasks.
13
+ # It provides a structured way to define tasks, their inputs and outputs,
14
+ # and handles the interaction with various LLM providers.
15
+ #
16
+ # @example Creating a simple task
17
+ # class MyTask < Lluminary::Task
18
+ # def run
19
+ # # Task implementation
20
+ # end
21
+ # end
8
22
  module Lluminary
9
23
  class << self
10
24
  def config
@@ -14,5 +28,9 @@ module Lluminary
14
28
  def configure
15
29
  yield config
16
30
  end
31
+
32
+ def reset_configuration
33
+ @config = Config.new
34
+ end
17
35
  end
18
- end
36
+ end
@@ -1,8 +1,11 @@
1
- require 'spec_helper'
2
- require_relative '../../examples/analyze_text'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+ require_relative "../../examples/analyze_text"
3
4
 
4
5
  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
+ let(:text) { <<~TEXT }
7
+ 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.
8
+ TEXT
6
9
 
7
10
  it "analyzes text" do
8
11
  result = described_class.call(text: text)
@@ -18,4 +21,4 @@ RSpec.describe AnalyzeText do
18
21
  expect(json).to have_key("analysis")
19
22
  expect(json["analysis"]).to eq(result.output.analysis)
20
23
  end
21
- end
24
+ end
@@ -1,42 +1,42 @@
1
- require 'spec_helper'
2
- require_relative '../../examples/color_analyzer'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+ require_relative "../../examples/color_analyzer"
3
4
 
4
5
  RSpec.describe ColorAnalyzer do
5
- describe '#call' do
6
+ describe "#call" do
6
7
  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
- )
8
+ result = described_class.call(image_description: <<~DESCRIPTION)
9
+ 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.
10
+ DESCRIPTION
10
11
 
11
12
  expect(result.output.color_name).to eq("red")
12
13
  expect(result.output.valid?).to be true
13
14
  end
14
15
 
15
- it 'returns invalid output when description strongly suggests orange' do
16
+ it "returns invalid output when description strongly suggests orange" do
16
17
  # 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
- )
18
+ result = described_class.call(image_description: <<~DESCRIPTION)
19
+ 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.
20
+ DESCRIPTION
20
21
 
21
22
  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
+ expect(result.output.errors.full_messages).to include(
24
+ "Color name must be a valid CSS level 1 color name"
25
+ )
23
26
  end
24
27
 
25
- it 'validates presence of image_description' do
26
- expect {
27
- described_class.call!(
28
- image_description: ""
29
- )
30
- }.to raise_error(Lluminary::ValidationError)
28
+ it "validates presence of image_description" do
29
+ expect { described_class.call!(image_description: "") }.to raise_error(
30
+ Lluminary::ValidationError
31
+ )
31
32
  end
32
33
 
33
- it 'validates that color_name is lowercase' do
34
- result = described_class.call(
35
- image_description: "A bright red sports car"
36
- )
34
+ it "validates that color_name is lowercase" do
35
+ result =
36
+ described_class.call(image_description: "A bright red sports car")
37
37
 
38
38
  expect(result.output.valid?).to be true
39
39
  expect(result.output.color_name).to eq(result.output.color_name.downcase)
40
40
  end
41
41
  end
42
- end
42
+ end