lluminary 0.1.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd0178afb0e59f3bf23029641e37f9ed149a7a3fadcdf364a88ad9079c8adc7d
4
- data.tar.gz: 4fe96f29859d3f55104d977ca81df4e41ff450c1e37a3a880f7af3f574bf8037
3
+ metadata.gz: 77729a2c4dc59717c0fd5c8fa8cf21df682bbaeff5c66e501f0e409cb8ac16c4
4
+ data.tar.gz: 46aa2188fcad140a7e7dfa2cc3d4bd5008d1bd5afbf70920bc3b211eb9018480
5
5
  SHA512:
6
- metadata.gz: 33ec5d4bd8e00d43dca677aafd076f6de386859c14fb0b001b962032ca0a455fdd2c36e5755e6d898ee54a1bbc4f64714c1ea7b9d7774330a25478c4698c8759
7
- data.tar.gz: 99c318f93c98a31b1c171436db63609b3258ed183c7e9fc73f16c7f0eb8c09a5eb3d5d1c4e548232e10c2b40956d13b31efae5a0582bb35d91b19d315d667a06
6
+ metadata.gz: ed78898c62ab28d52136d470f5ccf398b3d5dcce8643af51348834877b3561ae3ca5b0ca1e7d12fbab6d0a8b677e7599c393dd9f678af970da5d3d5f01da9e24
7
+ data.tar.gz: 961371925c55006aedfe7b89d043aa10411f90b26fe1c67be356c2b81ebcc618417eb0fa1ff1cdf5dcd7344adb0e75b208d82e0ae256e0497c178c6f6d3569f9
@@ -3,12 +3,7 @@
3
3
  module Lluminary
4
4
  # Configuration class for Lluminary framework.
5
5
  # Handles global settings and provider configurations.
6
- #
7
- # @example Setting up configuration
8
- # Lluminary.configure do |config|
9
- # config.provider = :openai
10
- # config.api_key = "your-api-key"
11
- # end
6
+
12
7
  class Config
13
8
  def initialize
14
9
  @providers = {}
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lluminary
4
+ module Models
5
+ # Base class for all LLM models.
6
+ # Defines the interface that all model classes must implement and provides
7
+ # default prompt formatting behavior.
8
+ class Base
9
+ # Checks if this model is compatible with a given provider
10
+ # @param provider_name [Symbol] The name of the provider to check
11
+ # @return [Boolean]
12
+ def compatible_with?(provider_name)
13
+ raise NotImplementedError, "Subclasses must implement #compatible_with?"
14
+ end
15
+
16
+ # Returns the name of the model
17
+ # @return [String]
18
+ def name
19
+ raise NotImplementedError, "Subclasses must implement #name"
20
+ end
21
+
22
+ def format_prompt(task)
23
+ <<~PROMPT
24
+ #{task.task_prompt.chomp}
25
+
26
+ #{output_preamble}
27
+
28
+ #{format_field_descriptions(task.class.output_fields)}
29
+
30
+ #{json_preamble}
31
+
32
+ #{format_json_example(task.class.output_fields)}
33
+ PROMPT
34
+ end
35
+
36
+ private
37
+
38
+ def output_preamble
39
+ <<~PREAMBLE.chomp
40
+ You must respond with ONLY a valid JSON object. Do not include any other text, explanations, or formatting.
41
+ The JSON object must contain the following fields:
42
+ PREAMBLE
43
+ end
44
+
45
+ def json_preamble
46
+ "Your response must be ONLY this JSON object:"
47
+ end
48
+
49
+ def format_field_descriptions(fields)
50
+ fields
51
+ .map do |name, field|
52
+ desc = "# #{name}"
53
+ desc += "\nType: #{format_type(field)}"
54
+
55
+ desc += "\nDescription: #{field[:description].chomp}" if field[
56
+ :description
57
+ ]
58
+
59
+ if (validations = describe_validations(field[:validations]))
60
+ desc += "\nValidations: #{validations}"
61
+ end
62
+
63
+ desc += "\nExample: #{generate_example_value(name, field)}"
64
+ desc
65
+ end
66
+ .join("\n\n")
67
+ end
68
+
69
+ def describe_validations(validations)
70
+ return unless validations&.any?
71
+
72
+ validations
73
+ .map do |options|
74
+ case options.keys.first
75
+ when :presence
76
+ "must be present"
77
+ when :inclusion
78
+ "must be one of: #{options[:inclusion][:in].join(", ")}"
79
+ when :exclusion
80
+ "must not be one of: #{options[:exclusion][:in].join(", ")}"
81
+ when :format
82
+ "must match format: #{options[:format][:with]}"
83
+ when :length
84
+ describe_length_validation(options[:length])
85
+ when :numericality
86
+ describe_numericality_validation(options[:numericality])
87
+ when :comparison
88
+ describe_comparison_validation(options[:comparison])
89
+ when :absence
90
+ "must be absent"
91
+ end
92
+ end
93
+ .compact
94
+ .join(", ")
95
+ end
96
+
97
+ def describe_length_validation(options)
98
+ descriptions = []
99
+ if options[:minimum]
100
+ descriptions << "must be at least #{options[:minimum]} characters"
101
+ end
102
+ if options[:maximum]
103
+ descriptions << "must be at most #{options[:maximum]} characters"
104
+ end
105
+ if options[:is]
106
+ descriptions << "must be exactly #{options[:is]} characters"
107
+ end
108
+ if options[:in]
109
+ descriptions << "must be between #{options[:in].min} and #{options[:in].max} characters"
110
+ end
111
+ descriptions.join(", ")
112
+ end
113
+
114
+ def describe_numericality_validation(options)
115
+ descriptions = []
116
+ if options[:greater_than]
117
+ descriptions << "must be greater than #{options[:greater_than]}"
118
+ end
119
+ if options[:greater_than_or_equal_to]
120
+ descriptions << "must be greater than or equal to #{options[:greater_than_or_equal_to]}"
121
+ end
122
+ if options[:equal_to]
123
+ descriptions << "must be equal to #{options[:equal_to]}"
124
+ end
125
+ if options[:less_than]
126
+ descriptions << "must be less than #{options[:less_than]}"
127
+ end
128
+ if options[:less_than_or_equal_to]
129
+ descriptions << "must be less than or equal to #{options[:less_than_or_equal_to]}"
130
+ end
131
+ if options[:other_than]
132
+ descriptions << "must be other than #{options[:other_than]}"
133
+ end
134
+ if options[:in]
135
+ descriptions << "must be in: #{options[:in].to_a.join(", ")}"
136
+ end
137
+ descriptions << "must be odd" if options[:odd]
138
+ descriptions << "must be even" if options[:even]
139
+ descriptions.join(", ")
140
+ end
141
+
142
+ def describe_comparison_validation(options)
143
+ descriptions = []
144
+ if options[:greater_than]
145
+ descriptions << "must be greater than #{options[:greater_than]}"
146
+ end
147
+ if options[:greater_than_or_equal_to]
148
+ descriptions << "must be greater than or equal to #{options[:greater_than_or_equal_to]}"
149
+ end
150
+ if options[:equal_to]
151
+ descriptions << "must be equal to #{options[:equal_to]}"
152
+ end
153
+ if options[:less_than]
154
+ descriptions << "must be less than #{options[:less_than]}"
155
+ end
156
+ if options[:less_than_or_equal_to]
157
+ descriptions << "must be less than or equal to #{options[:less_than_or_equal_to]}"
158
+ end
159
+ if options[:other_than]
160
+ descriptions << "must be other than #{options[:other_than]}"
161
+ end
162
+ descriptions.join(", ")
163
+ end
164
+
165
+ def format_json_example(fields)
166
+ example =
167
+ fields.each_with_object({}) do |(name, field), hash|
168
+ hash[name] = generate_example_value(name, field)
169
+ end
170
+ JSON.pretty_generate(example)
171
+ end
172
+
173
+ def format_type(field)
174
+ type = field[:type]
175
+ case type
176
+ when :datetime
177
+ "datetime in ISO8601 format"
178
+ when :array
179
+ if field[:element_type]
180
+ "array of #{format_type(field[:element_type])}"
181
+ else
182
+ "array"
183
+ end
184
+ else
185
+ type.to_s
186
+ end
187
+ end
188
+
189
+ def generate_example_value(name, field)
190
+ case field[:type]
191
+ when :string
192
+ "your #{name} here"
193
+ when :integer
194
+ 0
195
+ when :datetime
196
+ "2024-01-01T12:00:00+00:00"
197
+ when :boolean
198
+ true
199
+ when :float
200
+ 0.0
201
+ when :array
202
+ generate_array_example(name, field)
203
+ end
204
+ end
205
+
206
+ def generate_array_example(name, field)
207
+ return [] unless field[:element_type]
208
+
209
+ case field[:element_type][:type]
210
+ when :string
211
+ [
212
+ "first #{name.to_s.singularize}",
213
+ "second #{name.to_s.singularize}",
214
+ "..."
215
+ ]
216
+ when :integer
217
+ [1, 2, 3]
218
+ when :float
219
+ [1.0, 2.0, 3.0]
220
+ when :boolean
221
+ [true, false, true]
222
+ when :datetime
223
+ %w[2024-01-01T12:00:00+00:00 2024-01-02T12:00:00+00:00]
224
+ when :array
225
+ if field[:element_type][:element_type]
226
+ inner_example = generate_array_example("item", field[:element_type])
227
+ [inner_example, inner_example]
228
+ else
229
+ [["..."], ["..."]]
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Lluminary
6
+ module Models
7
+ module Bedrock
8
+ class AmazonNovaProV1 < Lluminary::Models::Bedrock::Base
9
+ NAME = "amazon.nova-pro-v1"
10
+ VERSIONS = %w[0].freeze
11
+ CONTEXT_WINDOWS = %w[24k 300k].freeze
12
+
13
+ def compatible_with?(provider_name)
14
+ provider_name == :bedrock
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Lluminary
6
+ module Models
7
+ module Bedrock
8
+ class AnthropicClaudeInstantV1 < Lluminary::Models::Bedrock::Base
9
+ NAME = "anthropic.claude-instant-v1"
10
+
11
+ def compatible_with?(provider_name)
12
+ provider_name == :bedrock
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lluminary
4
+ module Models
5
+ module Bedrock
6
+ # TODO: test me
7
+ class Base < Lluminary::Models::Base
8
+ VERSIONS = [].freeze
9
+ CONTEXT_WINDOWS = [].freeze
10
+
11
+ def default_version
12
+ self.class::VERSIONS.last
13
+ end
14
+
15
+ def default_context_window
16
+ nil
17
+ end
18
+
19
+ def name
20
+ [
21
+ self.class::NAME,
22
+ default_version,
23
+ default_context_window
24
+ ].compact.join(":")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lluminary
4
+ module Models
5
+ module OpenAi
6
+ # Model class for OpenAI's GPT-3.5 Turbo
7
+ class Gpt35Turbo < Base
8
+ NAME = "gpt-3.5-turbo"
9
+
10
+ def compatible_with?(provider_name)
11
+ provider_name == :openai
12
+ end
13
+
14
+ def name
15
+ NAME
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -5,15 +5,28 @@ module Lluminary
5
5
  # Base class for all LLM providers.
6
6
  # Defines the interface that all providers must implement.
7
7
  class Base
8
+ # The symbolic name of the provider. Must be overridden by subclasses.
9
+ NAME = :base
10
+
8
11
  attr_reader :config
9
12
 
10
- def initialize(**config)
11
- @config = config
13
+ def initialize(**config_overrides)
14
+ @config = default_provider_config.merge(config_overrides)
12
15
  end
13
16
 
14
17
  def call(prompt, task)
15
18
  raise NotImplementedError, "Subclasses must implement #call"
16
19
  end
20
+
21
+ def models
22
+ raise NotImplementedError, "Subclasses must implement #models"
23
+ end
24
+
25
+ private
26
+
27
+ def default_provider_config
28
+ Lluminary.config.provider_config(self.class::NAME)
29
+ end
17
30
  end
18
31
  end
19
32
  end
@@ -1,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
  require "aws-sdk-bedrockruntime"
3
+ require "aws-sdk-bedrock"
3
4
  require "json"
4
5
  require_relative "../provider_error"
5
6
 
6
- require "pry-byebug"
7
-
8
7
  module Lluminary
9
8
  module Providers
10
9
  # Provider for AWS Bedrock models.
11
10
  # Implements the Base provider interface for AWS Bedrock's API.
12
11
  class Bedrock < Base
13
- DEFAULT_MODEL_ID = "anthropic.claude-instant-v1"
12
+ NAME = :bedrock
13
+ DEFAULT_MODEL = Models::Bedrock::AnthropicClaudeInstantV1
14
14
 
15
15
  attr_reader :client, :config
16
16
 
17
- def initialize(**config)
17
+ def initialize(**config_overrides)
18
18
  super
19
- @config = { model_id: DEFAULT_MODEL_ID }.merge(config)
19
+ @config = { model: DEFAULT_MODEL }.merge(config)
20
20
 
21
21
  @client =
22
22
  Aws::BedrockRuntime::Client.new(
@@ -31,8 +31,8 @@ module Lluminary
31
31
 
32
32
  def call(prompt, _task)
33
33
  response =
34
- @client.converse(
35
- model_id: config[:model_id],
34
+ client.converse(
35
+ model_id: model.name,
36
36
  messages: [{ role: "user", content: [{ text: prompt }] }]
37
37
  )
38
38
 
@@ -48,6 +48,24 @@ module Lluminary
48
48
  end
49
49
  }
50
50
  end
51
+
52
+ def model
53
+ @model ||= config[:model].new
54
+ end
55
+
56
+ def models
57
+ models_client =
58
+ Aws::Bedrock::Client.new(
59
+ region: config[:region],
60
+ credentials:
61
+ Aws::Credentials.new(
62
+ config[:access_key_id],
63
+ config[:secret_access_key]
64
+ )
65
+ )
66
+ response = models_client.list_foundation_models
67
+ response.foundation_models.map(&:model_id)
68
+ end
51
69
  end
52
70
  end
53
71
  end
@@ -8,11 +8,12 @@ module Lluminary
8
8
  # Provider for OpenAI's GPT models.
9
9
  # Implements the Base provider interface for OpenAI's API.
10
10
  class OpenAI < Base
11
- DEFAULT_MODEL = "gpt-3.5-turbo"
11
+ NAME = :openai
12
+ DEFAULT_MODEL = Models::OpenAi::Gpt35Turbo
12
13
 
13
14
  attr_reader :client, :config
14
15
 
15
- def initialize(**config)
16
+ def initialize(**config_overrides)
16
17
  super
17
18
  @config = { model: DEFAULT_MODEL }.merge(config)
18
19
  @client = ::OpenAI::Client.new(access_token: config[:api_key])
@@ -20,9 +21,9 @@ module Lluminary
20
21
 
21
22
  def call(prompt, _task)
22
23
  response =
23
- @client.chat(
24
+ client.chat(
24
25
  parameters: {
25
- model: config[:model],
26
+ model: model.class::NAME,
26
27
  messages: [{ role: "user", content: prompt }],
27
28
  response_format: {
28
29
  type: "json_object"
@@ -42,6 +43,15 @@ module Lluminary
42
43
  end
43
44
  }
44
45
  end
46
+
47
+ def model
48
+ @model ||= config[:model].new
49
+ end
50
+
51
+ def models
52
+ response = @client.models.list
53
+ response["data"].map { |model| model["id"] }
54
+ end
45
55
  end
46
56
  end
47
57
  end
@@ -5,12 +5,18 @@ module Lluminary
5
5
  # Test provider for development and testing.
6
6
  # Returns predefined responses for testing purposes.
7
7
  class Test < Base
8
+ NAME = :test
9
+
8
10
  def call(_prompt, task)
9
11
  response = generate_response(task.class.output_fields)
10
12
  raw_response = JSON.pretty_generate(response).gsub(/\n\s*/, "")
11
13
  { raw: raw_response, parsed: JSON.parse(raw_response) }
12
14
  end
13
15
 
16
+ def model
17
+ @model ||= Lluminary::Models::Base.new
18
+ end
19
+
14
20
  private
15
21
 
16
22
  def generate_response(fields)
@@ -31,10 +31,30 @@ module Lluminary
31
31
  @fields[name] = { type: :datetime, description: description }
32
32
  end
33
33
 
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
43
+ end
44
+
34
45
  attr_reader :fields
35
46
 
36
47
  def validates(*args, **options)
37
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
38
58
  end
39
59
 
40
60
  def validations_for(field_name)
@@ -50,5 +70,36 @@ module Lluminary
50
70
  instance = schema_model.new(values)
51
71
  instance.valid? ? [] : instance.errors.full_messages
52
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
53
104
  end
54
105
  end
@@ -45,6 +45,58 @@ module Lluminary
45
45
 
46
46
  # Add type validations
47
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
+
48
100
  record.attributes.each do |name, value|
49
101
  next if name == "raw_response"
50
102
  next if value.nil?
@@ -73,6 +125,8 @@ module Lluminary
73
125
  unless value.is_a?(DateTime)
74
126
  record.errors.add(name, "must be a DateTime")
75
127
  end
128
+ when :array
129
+ validate_array_field(record, name, value, field[:element_type])
76
130
  end
77
131
  end
78
132
  end