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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '02594d2f6a20444f1e6609a1582ca06c29ecddaf12042a42fdf65621eb16fce3'
4
- data.tar.gz: 20c476662fe4ab8dcc4fe403fd580b5d44323bc970f8c9830a7b32eb06589d4e
3
+ metadata.gz: 77729a2c4dc59717c0fd5c8fa8cf21df682bbaeff5c66e501f0e409cb8ac16c4
4
+ data.tar.gz: 46aa2188fcad140a7e7dfa2cc3d4bd5008d1bd5afbf70920bc3b211eb9018480
5
5
  SHA512:
6
- metadata.gz: 5b0f2590d708746f35958ea556d6b6bd6d7e9d594505fc5b70178af317a9b6570b1ac9f4ab8580eee0d16776358fa7be7b65f4a3abc88cf14ce364c5410442bf
7
- data.tar.gz: aad105c596b169ccb3ca0613d974b3883553d7296190c9906bda88f2b5b00b765a079364cb3b45ffaa94dbf9aa81e2b369aa3a236936eb756671785c32058d5d
6
+ metadata.gz: ed78898c62ab28d52136d470f5ccf398b3d5dcce8643af51348834877b3561ae3ca5b0ca1e7d12fbab6d0a8b677e7599c393dd9f678af970da5d3d5f01da9e24
7
+ data.tar.gz: 961371925c55006aedfe7b89d043aa10411f90b26fe1c67be356c2b81ebcc618417eb0fa1ff1cdf5dcd7344adb0e75b208d82e0ae256e0497c178c6f6d3569f9
@@ -1,4 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Lluminary
4
+ # Configuration class for Lluminary framework.
5
+ # Handles global settings and provider configurations.
6
+
2
7
  class Config
3
8
  def initialize
4
9
  @providers = {}
@@ -20,4 +25,4 @@ module Lluminary
20
25
  @providers = {}
21
26
  end
22
27
  end
23
- end
28
+ end
@@ -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
@@ -1,4 +1,5 @@
1
+ # frozen_string_literal: true
1
2
  module Lluminary
2
3
  class ProviderError < StandardError
3
4
  end
4
- end
5
+ end
@@ -1,15 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Lluminary
2
4
  module Providers
5
+ # Base class for all LLM providers.
6
+ # Defines the interface that all providers must implement.
3
7
  class Base
8
+ # The symbolic name of the provider. Must be overridden by subclasses.
9
+ NAME = :base
10
+
4
11
  attr_reader :config
5
12
 
6
- def initialize(**config)
7
- @config = config
13
+ def initialize(**config_overrides)
14
+ @config = default_provider_config.merge(config_overrides)
8
15
  end
9
16
 
10
17
  def call(prompt, task)
11
18
  raise NotImplementedError, "Subclasses must implement #call"
12
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
13
30
  end
14
31
  end
15
- end
32
+ end
@@ -1,51 +1,71 @@
1
- require 'aws-sdk-bedrockruntime'
2
- require 'json'
3
- require_relative '../provider_error'
4
-
5
- require 'pry-byebug'
1
+ # frozen_string_literal: true
2
+ require "aws-sdk-bedrockruntime"
3
+ require "aws-sdk-bedrock"
4
+ require "json"
5
+ require_relative "../provider_error"
6
6
 
7
7
  module Lluminary
8
8
  module Providers
9
+ # Provider for AWS Bedrock models.
10
+ # Implements the Base provider interface for AWS Bedrock's API.
9
11
  class Bedrock < Base
10
- DEFAULT_MODEL_ID = 'anthropic.claude-instant-v1'
12
+ NAME = :bedrock
13
+ DEFAULT_MODEL = Models::Bedrock::AnthropicClaudeInstantV1
11
14
 
12
15
  attr_reader :client, :config
13
16
 
14
- def initialize(**config)
17
+ def initialize(**config_overrides)
15
18
  super
16
- @config = config
19
+ @config = { model: DEFAULT_MODEL }.merge(config)
17
20
 
18
- @client = Aws::BedrockRuntime::Client.new(
19
- region: config[:region],
20
- credentials: Aws::Credentials.new(
21
- config[:access_key_id],
22
- config[:secret_access_key]
21
+ @client =
22
+ Aws::BedrockRuntime::Client.new(
23
+ region: config[:region],
24
+ credentials:
25
+ Aws::Credentials.new(
26
+ config[:access_key_id],
27
+ config[:secret_access_key]
28
+ )
23
29
  )
24
- )
25
30
  end
26
31
 
27
- def call(prompt, task)
28
- response = @client.converse(
29
- model_id: config[:model_id] || DEFAULT_MODEL_ID,
30
- messages: [
31
- {
32
- role: 'user',
33
- content: [{text: prompt}]
34
- }
35
- ]
36
- )
32
+ def call(prompt, _task)
33
+ response =
34
+ client.converse(
35
+ model_id: model.name,
36
+ messages: [{ role: "user", content: [{ text: prompt }] }]
37
+ )
37
38
 
38
39
  content = response.dig(:output, :message, :content, 0, :text)
39
-
40
- {
40
+
41
+ {
41
42
  raw: content,
42
- parsed: begin
43
- JSON.parse(content) if content
44
- rescue JSON::ParserError
45
- nil
46
- end
43
+ parsed:
44
+ begin
45
+ JSON.parse(content) if content
46
+ rescue JSON::ParserError
47
+ nil
48
+ end
47
49
  }
48
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
49
69
  end
50
70
  end
51
- end
71
+ end
@@ -1,40 +1,57 @@
1
- require 'openai'
2
- require 'json'
3
- require_relative '../provider_error'
1
+ # frozen_string_literal: true
2
+ require "openai"
3
+ require "json"
4
+ require_relative "../provider_error"
4
5
 
5
6
  module Lluminary
6
7
  module Providers
8
+ # Provider for OpenAI's GPT models.
9
+ # Implements the Base provider interface for OpenAI's API.
7
10
  class OpenAI < Base
8
- DEFAULT_MODEL = "gpt-3.5-turbo"
11
+ NAME = :openai
12
+ DEFAULT_MODEL = Models::OpenAi::Gpt35Turbo
9
13
 
10
14
  attr_reader :client, :config
11
15
 
12
- def initialize(**config)
16
+ def initialize(**config_overrides)
13
17
  super
14
- @config = config
18
+ @config = { model: DEFAULT_MODEL }.merge(config)
15
19
  @client = ::OpenAI::Client.new(access_token: config[:api_key])
16
20
  end
17
21
 
18
- def call(prompt, task)
19
- response = @client.chat(
20
- parameters: {
21
- model: config[:model] || DEFAULT_MODEL,
22
- messages: [{ role: "user", content: prompt }],
23
- response_format: { type: "json_object" }
24
- }
25
- )
26
-
27
- content = response.dig('choices', 0, 'message', 'content')
28
-
29
- {
22
+ def call(prompt, _task)
23
+ response =
24
+ client.chat(
25
+ parameters: {
26
+ model: model.class::NAME,
27
+ messages: [{ role: "user", content: prompt }],
28
+ response_format: {
29
+ type: "json_object"
30
+ }
31
+ }
32
+ )
33
+
34
+ content = response.dig("choices", 0, "message", "content")
35
+
36
+ {
30
37
  raw: content,
31
- parsed: begin
32
- JSON.parse(content) if content
33
- rescue JSON::ParserError
34
- nil
35
- end
38
+ parsed:
39
+ begin
40
+ JSON.parse(content) if content
41
+ rescue JSON::ParserError
42
+ nil
43
+ end
36
44
  }
37
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
38
55
  end
39
56
  end
40
- end
57
+ end
@@ -1,25 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Lluminary
2
4
  module Providers
5
+ # Test provider for development and testing.
6
+ # Returns predefined responses for testing purposes.
3
7
  class Test < Base
4
- def initialize(**config)
5
- super
6
- end
8
+ NAME = :test
7
9
 
8
- def call(prompt, task)
10
+ def call(_prompt, task)
9
11
  response = generate_response(task.class.output_fields)
10
- raw_response = JSON.pretty_generate(response).gsub(/\n\s*/, '')
11
- {
12
- raw: raw_response,
13
- parsed: JSON.parse(raw_response)
14
- }
12
+ raw_response = JSON.pretty_generate(response).gsub(/\n\s*/, "")
13
+ { raw: raw_response, parsed: JSON.parse(raw_response) }
14
+ end
15
+
16
+ def model
17
+ @model ||= Lluminary::Models::Base.new
15
18
  end
16
19
 
17
20
  private
18
21
 
19
22
  def generate_response(fields)
20
- fields.each_with_object({}) do |(name, field), hash|
21
- hash[name] = generate_value(field[:type])
22
- end
23
+ fields.transform_values { |field| generate_value(field[:type]) }
23
24
  end
24
25
 
25
26
  def generate_value(type)
@@ -34,4 +35,4 @@ module Lluminary
34
35
  end
35
36
  end
36
37
  end
37
- end
38
+ end
@@ -1,6 +1,9 @@
1
- require 'ostruct'
1
+ # frozen_string_literal: true
2
+ require "ostruct"
2
3
 
3
4
  module Lluminary
5
+ # Represents the result of a task execution.
6
+ # Contains the output data and any metadata about the execution.
4
7
  class Result
5
8
  attr_reader :raw_response, :output, :prompt
6
9
 
@@ -10,4 +13,4 @@ module Lluminary
10
13
  @prompt = prompt
11
14
  end
12
15
  end
13
- end
16
+ end