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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4a50b739f7e9ea0f069d17bc94d74f284e38546bd1a8abbda0a6084548803756
4
+ data.tar.gz: db0bb01c407511d3bb1e34796aae68c823c33e9da6d4b556a0e4d3a09ef16ee3
5
+ SHA512:
6
+ metadata.gz: 78891cbbe5940fe7685a33080c3cd5dd62afd3864c143a49a9ea4a1e63f5a390097ec1d863a93f47af44edded688b2c5b77068d5761198d3ae057dbcdb5d5c54
7
+ data.tar.gz: 6e0c48f4d4ce3c25756e611a3506e2bd44aa32ef3643be35f9c6312bb519c23dd1fc3ad2c273db6421eccb2c96895958f6a31d0d1ebf9bcd7cada876ab99b444
@@ -0,0 +1,23 @@
1
+ module Lluminary
2
+ class Config
3
+ def initialize
4
+ @providers = {}
5
+ end
6
+
7
+ def configure
8
+ yield self
9
+ end
10
+
11
+ def provider(name, **options)
12
+ @providers[name.to_sym] = options
13
+ end
14
+
15
+ def provider_config(provider_name)
16
+ @providers[provider_name.to_sym] || {}
17
+ end
18
+
19
+ def reset!
20
+ @providers = {}
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,148 @@
1
+ module Lluminary
2
+ class FieldDescription
3
+ def initialize(name, field)
4
+ @name = name
5
+ @type = field[:type]
6
+ @description = field[:description]
7
+ @validations = field[:validations] || []
8
+ end
9
+
10
+ def to_s
11
+ parts = []
12
+ parts << "#{@name} (#{type_description})"
13
+ parts << ": #{@description}" if @description
14
+ parts << " (#{validation_descriptions.join(', ')})" if validation_descriptions.any?
15
+ parts.join
16
+ end
17
+
18
+ def to_schema_s
19
+ parts = []
20
+ parts << "#{@name} (#{type_description})"
21
+ parts << ": #{@description}" if @description
22
+ parts << "\nValidation: #{validation_descriptions.join(', ')}" if validation_descriptions.any?
23
+ parts << "\nExample: #{example_value}"
24
+ parts.join
25
+ end
26
+
27
+ private
28
+
29
+ def type_description
30
+ case @type
31
+ when :datetime
32
+ "datetime in ISO8601 format"
33
+ else
34
+ @type.to_s
35
+ end
36
+ end
37
+
38
+ def validation_descriptions
39
+ @validations.map do |_, options|
40
+ case options.keys.first
41
+ when :absence
42
+ "must be absent"
43
+ when :comparison
44
+ comparison_descriptions(options[:comparison])
45
+ when :exclusion
46
+ "must not be one of: #{options[:exclusion][:in].join(', ')}"
47
+ when :format
48
+ "must match format: #{options[:format][:with]}"
49
+ when :inclusion
50
+ "must be one of: #{options[:inclusion][:in].join(', ')}"
51
+ when :length
52
+ length_descriptions(options[:length])
53
+ when :numericality
54
+ numericality_descriptions(options[:numericality])
55
+ when :presence
56
+ "must be present"
57
+ end
58
+ end.compact
59
+ end
60
+
61
+ def comparison_descriptions(options)
62
+ descriptions = []
63
+ if options[:greater_than]
64
+ descriptions << "must be greater than #{options[:greater_than]}"
65
+ end
66
+ if options[:greater_than_or_equal_to]
67
+ descriptions << "must be greater than or equal to #{options[:greater_than_or_equal_to]}"
68
+ end
69
+ if options[:equal_to]
70
+ descriptions << "must be equal to #{options[:equal_to]}"
71
+ end
72
+ if options[:less_than]
73
+ descriptions << "must be less than #{options[:less_than]}"
74
+ end
75
+ if options[:less_than_or_equal_to]
76
+ descriptions << "must be less than or equal to #{options[:less_than_or_equal_to]}"
77
+ end
78
+ if options[:other_than]
79
+ descriptions << "must be other than #{options[:other_than]}"
80
+ end
81
+ descriptions.join(", ")
82
+ end
83
+
84
+ def length_descriptions(options)
85
+ descriptions = []
86
+ if options[:minimum]
87
+ descriptions << "must be at least #{options[:minimum]} characters"
88
+ end
89
+ if options[:maximum]
90
+ descriptions << "must be at most #{options[:maximum]} characters"
91
+ end
92
+ if options[:is]
93
+ descriptions << "must be exactly #{options[:is]} characters"
94
+ end
95
+ if options[:in]
96
+ descriptions << "must be between #{options[:in].min} and #{options[:in].max} characters"
97
+ end
98
+ descriptions.join(", ")
99
+ end
100
+
101
+ def numericality_descriptions(options)
102
+ descriptions = []
103
+ if options[:greater_than]
104
+ descriptions << "must be greater than #{options[:greater_than]}"
105
+ end
106
+ if options[:greater_than_or_equal_to]
107
+ descriptions << "must be greater than or equal to #{options[:greater_than_or_equal_to]}"
108
+ end
109
+ if options[:equal_to]
110
+ descriptions << "must be equal to #{options[:equal_to]}"
111
+ end
112
+ if options[:less_than]
113
+ descriptions << "must be less than #{options[:less_than]}"
114
+ end
115
+ if options[:less_than_or_equal_to]
116
+ descriptions << "must be less than or equal to #{options[:less_than_or_equal_to]}"
117
+ end
118
+ if options[:other_than]
119
+ descriptions << "must be other than #{options[:other_than]}"
120
+ end
121
+ if options[:in]
122
+ descriptions << "must be in: #{options[:in].to_a.join(', ')}"
123
+ end
124
+ if options[:odd]
125
+ descriptions << "must be odd"
126
+ end
127
+ if options[:even]
128
+ descriptions << "must be even"
129
+ end
130
+ descriptions.join(", ")
131
+ end
132
+
133
+ def example_value
134
+ case @type
135
+ when :string
136
+ "\"your #{@name} here\""
137
+ when :integer
138
+ "0"
139
+ when :datetime
140
+ "\"2024-01-01T12:00:00+00:00\""
141
+ when :boolean
142
+ "true"
143
+ when :float
144
+ "0.0"
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,4 @@
1
+ module Lluminary
2
+ class ProviderError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,15 @@
1
+ module Lluminary
2
+ module Providers
3
+ class Base
4
+ attr_reader :config
5
+
6
+ def initialize(**config)
7
+ @config = config
8
+ end
9
+
10
+ def call(prompt, task)
11
+ raise NotImplementedError, "Subclasses must implement #call"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,51 @@
1
+ require 'aws-sdk-bedrockruntime'
2
+ require 'json'
3
+ require_relative '../provider_error'
4
+
5
+ require 'pry-byebug'
6
+
7
+ module Lluminary
8
+ module Providers
9
+ class Bedrock < Base
10
+ DEFAULT_MODEL_ID = 'anthropic.claude-instant-v1'
11
+
12
+ attr_reader :client, :config
13
+
14
+ def initialize(**config)
15
+ super
16
+ @config = config
17
+
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]
23
+ )
24
+ )
25
+ end
26
+
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
+ )
37
+
38
+ content = response.dig(:output, :message, :content, 0, :text)
39
+
40
+ {
41
+ raw: content,
42
+ parsed: begin
43
+ JSON.parse(content) if content
44
+ rescue JSON::ParserError
45
+ nil
46
+ end
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,40 @@
1
+ require 'openai'
2
+ require 'json'
3
+ require_relative '../provider_error'
4
+
5
+ module Lluminary
6
+ module Providers
7
+ class OpenAI < Base
8
+ DEFAULT_MODEL = "gpt-3.5-turbo"
9
+
10
+ attr_reader :client, :config
11
+
12
+ def initialize(**config)
13
+ super
14
+ @config = config
15
+ @client = ::OpenAI::Client.new(access_token: config[:api_key])
16
+ end
17
+
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
+ {
30
+ raw: content,
31
+ parsed: begin
32
+ JSON.parse(content) if content
33
+ rescue JSON::ParserError
34
+ nil
35
+ end
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,37 @@
1
+ module Lluminary
2
+ module Providers
3
+ class Test < Base
4
+ def initialize(**config)
5
+ super
6
+ end
7
+
8
+ def call(prompt, task)
9
+ 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
+ }
15
+ end
16
+
17
+ private
18
+
19
+ def generate_response(fields)
20
+ fields.each_with_object({}) do |(name, field), hash|
21
+ hash[name] = generate_value(field[:type])
22
+ end
23
+ end
24
+
25
+ def generate_value(type)
26
+ case type
27
+ when :string
28
+ "Test #{type} value"
29
+ when :integer
30
+ 0
31
+ else
32
+ raise "Unsupported type: #{type}"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,13 @@
1
+ require 'ostruct'
2
+
3
+ module Lluminary
4
+ class Result
5
+ attr_reader :raw_response, :output, :prompt
6
+
7
+ def initialize(raw_response:, output:, prompt:)
8
+ @raw_response = raw_response
9
+ @output = OpenStruct.new(output)
10
+ @prompt = prompt
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,61 @@
1
+ require 'active_model'
2
+ require_relative 'schema_model'
3
+
4
+ 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
+
11
+ class Schema
12
+ def initialize
13
+ @fields = {}
14
+ @validations = []
15
+ end
16
+
17
+ def string(name, description: nil)
18
+ @fields[name] = { type: :string, description: description }
19
+ end
20
+
21
+ def integer(name, description: nil)
22
+ @fields[name] = { type: :integer, description: description }
23
+ end
24
+
25
+ def boolean(name, description: nil)
26
+ @fields[name] = { type: :boolean, description: description }
27
+ end
28
+
29
+ def float(name, description: nil)
30
+ @fields[name] = { type: :float, description: description }
31
+ end
32
+
33
+ def datetime(name, description: nil)
34
+ @fields[name] = { type: :datetime, description: description }
35
+ end
36
+
37
+ def fields
38
+ @fields
39
+ end
40
+
41
+ def validates(*args, **options)
42
+ @validations << [args, options]
43
+ end
44
+
45
+ def validations_for(field_name)
46
+ @validations.select { |args, _| args.include?(field_name) }
47
+ end
48
+
49
+ def schema_model
50
+ @schema_model ||= SchemaModel.build(
51
+ fields: @fields,
52
+ validations: @validations
53
+ )
54
+ end
55
+
56
+ def validate(values)
57
+ instance = schema_model.new(values)
58
+ instance.valid? ? [] : instance.errors.full_messages
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,87 @@
1
+ require 'active_model'
2
+
3
+ module Lluminary
4
+ class SchemaModel
5
+ include ActiveModel::Validations
6
+
7
+ attr_reader :attributes
8
+
9
+ def initialize(attributes = {})
10
+ @attributes = attributes.transform_keys(&:to_s)
11
+ end
12
+
13
+ def to_s
14
+ attrs = attributes.dup
15
+ attrs.delete('raw_response')
16
+ "#<#{self.class.name} #{attrs.inspect}>"
17
+ end
18
+
19
+ def self.build(fields:, validations:)
20
+ Class.new(self) do
21
+ # Add accessors for each field
22
+ fields.each_key do |name|
23
+ define_method(name) { @attributes[name.to_s] }
24
+ define_method("#{name}=") { |value| @attributes[name.to_s] = value }
25
+ end
26
+
27
+ # Add raw_response field and validation
28
+ define_method(:raw_response) { @attributes['raw_response'] }
29
+ define_method(:raw_response=) { |value| @attributes['raw_response'] = value }
30
+
31
+ validate do |record|
32
+ if record.raw_response
33
+ begin
34
+ JSON.parse(record.raw_response)
35
+ rescue JSON::ParserError
36
+ record.errors.add(:raw_response, "must be valid JSON")
37
+ end
38
+ end
39
+ end
40
+
41
+ # Add type validations
42
+ validate do |record|
43
+ record.attributes.each do |name, value|
44
+ next if name == 'raw_response'
45
+ next if value.nil?
46
+
47
+ field = fields[name.to_sym]
48
+ next unless field
49
+
50
+ case field[:type]
51
+ when :string
52
+ unless value.is_a?(String)
53
+ record.errors.add(name, "must be a String")
54
+ end
55
+ when :integer
56
+ unless value.is_a?(Integer)
57
+ record.errors.add(name, "must be an Integer")
58
+ end
59
+ when :boolean
60
+ unless value == true || value == false
61
+ record.errors.add(name, "must be true or false")
62
+ end
63
+ when :float
64
+ unless value.is_a?(Float)
65
+ record.errors.add(name, "must be a float")
66
+ end
67
+ when :datetime
68
+ unless value.is_a?(DateTime)
69
+ record.errors.add(name, "must be a DateTime")
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ # Add ActiveModel validations
76
+ validations.each do |args, options|
77
+ validates(*args, **options)
78
+ end
79
+
80
+ # Set model name for error messages
81
+ define_singleton_method(:model_name) do
82
+ ActiveModel::Name.new(self, nil, "SchemaModel")
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end