lluminary 0.2.0 → 0.2.2
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 +4 -4
- data/lib/lluminary/models/anthropic/claude_3_5_sonnet.rb +17 -0
- data/lib/lluminary/models/base.rb +11 -0
- data/lib/lluminary/providers/anthropic.rb +54 -0
- data/lib/lluminary/schema.rb +11 -7
- data/lib/lluminary/schema_model.rb +16 -2
- data/lib/lluminary/task.rb +40 -0
- data/lib/lluminary.rb +9 -3
- data/spec/lluminary/models/base_spec.rb +32 -0
- data/spec/lluminary/providers/anthropic_spec.rb +104 -0
- data/spec/lluminary/schema_model_spec.rb +259 -0
- data/spec/lluminary/schema_spec.rb +80 -241
- data/spec/lluminary/task_custom_validation_spec.rb +262 -0
- data/spec/lluminary/task_spec.rb +18 -0
- data/spec/spec_helper.rb +3 -0
- metadata +34 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '015093c0cffd9d6e752bfd3bd6b03b16fb3740f33f1e7b11285908a4be4f3317'
|
4
|
+
data.tar.gz: 4120b07419eee36bfe7d9b00cf2224ae35739ac21be9e31f3b45cb531e88b2f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d7424d0cdfbace6684373aa11dbc3d73d5ae2e4b7dddcb2f28eff13d5ac07e5a69672fca7da7fed0ad02be92faceb45d80bc5d339906e584e5d2732584e6a0c9
|
7
|
+
data.tar.gz: e529bd7a10437c74240b8849880108268c43b6ff6312c047b0b4809b75c80d0e800cbe2d7dc51b7fea09bdf6acf1e4af716defdd9937bf81cce8eb1b4b3bb63b
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# require_relative "../base"
|
4
|
+
|
5
|
+
module Lluminary
|
6
|
+
module Models
|
7
|
+
module Anthropic
|
8
|
+
class Claude35Sonnet < Lluminary::Models::Base
|
9
|
+
NAME = "claude-3-5-sonnet-latest"
|
10
|
+
|
11
|
+
def compatible_with?(provider_name)
|
12
|
+
provider_name == :anthropic
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -27,6 +27,8 @@ module Lluminary
|
|
27
27
|
|
28
28
|
#{format_fields_descriptions(task.class.output_fields)}
|
29
29
|
|
30
|
+
#{format_additional_validations(task.class.output_custom_validations)}
|
31
|
+
|
30
32
|
#{json_preamble}
|
31
33
|
|
32
34
|
#{generate_example_json_object(task.class.output_fields)}
|
@@ -330,6 +332,15 @@ module Lluminary
|
|
330
332
|
hash[subname] = generate_example_value(subname, subfield)
|
331
333
|
end
|
332
334
|
end
|
335
|
+
|
336
|
+
def format_additional_validations(custom_validations)
|
337
|
+
descriptions = custom_validations.map { |v| v[:description] }.compact
|
338
|
+
return "" if descriptions.empty?
|
339
|
+
|
340
|
+
section = ["Additional Validations:"]
|
341
|
+
descriptions.each { |desc| section << "- #{desc}" }
|
342
|
+
"#{section.join("\n")}\n"
|
343
|
+
end
|
333
344
|
end
|
334
345
|
end
|
335
346
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anthropic"
|
4
|
+
require "json"
|
5
|
+
require_relative "../provider_error"
|
6
|
+
|
7
|
+
module Lluminary
|
8
|
+
module Providers
|
9
|
+
# Provider for Anthropic's models.
|
10
|
+
# Implements the Base provider interface for Anthropic's API.
|
11
|
+
class Anthropic < Base
|
12
|
+
NAME = :anthropic
|
13
|
+
DEFAULT_MODEL = Models::Anthropic::Claude35Sonnet
|
14
|
+
|
15
|
+
attr_reader :client, :config
|
16
|
+
|
17
|
+
def initialize(**config_overrides)
|
18
|
+
super
|
19
|
+
@config = { model: DEFAULT_MODEL }.merge(config)
|
20
|
+
@client = ::Anthropic::Client.new(api_key: config[:api_key])
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(prompt, _task)
|
24
|
+
message =
|
25
|
+
client.messages.create(
|
26
|
+
max_tokens: 1024, # TODO: make this configurable
|
27
|
+
messages: [{ role: "user", content: prompt }],
|
28
|
+
model: model.class::NAME
|
29
|
+
)
|
30
|
+
|
31
|
+
content = message.content.first.text
|
32
|
+
|
33
|
+
{
|
34
|
+
raw: content,
|
35
|
+
parsed:
|
36
|
+
begin
|
37
|
+
JSON.parse(content) if content
|
38
|
+
rescue JSON::ParserError
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def model
|
45
|
+
@model ||= config[:model].new
|
46
|
+
end
|
47
|
+
|
48
|
+
def models
|
49
|
+
response = @client.models.list
|
50
|
+
response.data.map { |model| model.id }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/lluminary/schema.rb
CHANGED
@@ -9,6 +9,7 @@ module Lluminary
|
|
9
9
|
def initialize
|
10
10
|
@fields = {}
|
11
11
|
@validations = []
|
12
|
+
@custom_validations = []
|
12
13
|
end
|
13
14
|
|
14
15
|
def string(name, description: nil)
|
@@ -57,7 +58,7 @@ module Lluminary
|
|
57
58
|
}
|
58
59
|
end
|
59
60
|
|
60
|
-
attr_reader :fields
|
61
|
+
attr_reader :fields, :custom_validations
|
61
62
|
|
62
63
|
def validates(*args, **options)
|
63
64
|
@validations << [args, options]
|
@@ -72,18 +73,21 @@ module Lluminary
|
|
72
73
|
end
|
73
74
|
end
|
74
75
|
|
76
|
+
def validate(method_name, description: nil)
|
77
|
+
@custom_validations << { method: method_name, description: description }
|
78
|
+
end
|
79
|
+
|
75
80
|
def validations_for(field_name)
|
76
81
|
@validations.select { |args, _| args.include?(field_name) }
|
77
82
|
end
|
78
83
|
|
79
84
|
def schema_model
|
80
85
|
@schema_model ||=
|
81
|
-
SchemaModel.build(
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
instance.valid? ? [] : instance.errors.full_messages
|
86
|
+
SchemaModel.build(
|
87
|
+
fields: @fields,
|
88
|
+
validations: @validations,
|
89
|
+
custom_validations: @custom_validations
|
90
|
+
)
|
87
91
|
end
|
88
92
|
|
89
93
|
# Internal class for defining array element types
|
@@ -8,6 +8,7 @@ module Lluminary
|
|
8
8
|
include ActiveModel::Validations
|
9
9
|
|
10
10
|
attr_reader :attributes
|
11
|
+
attr_accessor :task_instance
|
11
12
|
|
12
13
|
def initialize(attributes = {})
|
13
14
|
@attributes = attributes.transform_keys(&:to_s)
|
@@ -19,13 +20,14 @@ module Lluminary
|
|
19
20
|
"#<#{self.class.name} #{attrs.inspect}>"
|
20
21
|
end
|
21
22
|
|
22
|
-
def self.build(fields:, validations:)
|
23
|
+
def self.build(fields:, validations:, custom_validations: [])
|
23
24
|
Class.new(self) do
|
24
25
|
class << self
|
25
|
-
attr_accessor :schema_fields
|
26
|
+
attr_accessor :schema_fields, :custom_validation_methods
|
26
27
|
end
|
27
28
|
|
28
29
|
self.schema_fields = fields
|
30
|
+
self.custom_validation_methods = custom_validations
|
29
31
|
|
30
32
|
# Add accessors for each field
|
31
33
|
fields.each_key do |name|
|
@@ -39,7 +41,19 @@ module Lluminary
|
|
39
41
|
@attributes["raw_response"] = value
|
40
42
|
end
|
41
43
|
|
44
|
+
# Add custom validation hook
|
42
45
|
validate do |record|
|
46
|
+
# Run custom validations from the task if present
|
47
|
+
if record.task_instance &&
|
48
|
+
!record.class.custom_validation_methods.empty?
|
49
|
+
record.class.custom_validation_methods.each do |validation|
|
50
|
+
method_name = validation[:method]
|
51
|
+
if record.task_instance.respond_to?(method_name)
|
52
|
+
record.task_instance.send(method_name)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
43
57
|
if record.raw_response
|
44
58
|
begin
|
45
59
|
JSON.parse(record.raw_response)
|
data/lib/lluminary/task.rb
CHANGED
@@ -34,6 +34,9 @@ module Lluminary
|
|
34
34
|
when :bedrock
|
35
35
|
require_relative "providers/bedrock"
|
36
36
|
Providers::Bedrock
|
37
|
+
when :anthropic
|
38
|
+
require_relative "providers/anthropic"
|
39
|
+
Providers::Anthropic
|
37
40
|
else
|
38
41
|
raise ArgumentError, "Unknown provider: #{provider_name}"
|
39
42
|
end
|
@@ -74,12 +77,23 @@ module Lluminary
|
|
74
77
|
def output_schema_model
|
75
78
|
@output_schema&.schema_model || Schema.new.schema_model
|
76
79
|
end
|
80
|
+
|
81
|
+
def output_custom_validations
|
82
|
+
@output_schema&.custom_validations || []
|
83
|
+
end
|
84
|
+
|
85
|
+
def input_custom_validations
|
86
|
+
@input_schema&.custom_validations || []
|
87
|
+
end
|
77
88
|
end
|
78
89
|
|
79
90
|
attr_reader :input, :output, :parsed_response
|
91
|
+
attr_accessor :validation_failed
|
80
92
|
|
81
93
|
def initialize(input = {})
|
82
94
|
@input = self.class.input_schema_model.new(input)
|
95
|
+
@input.task_instance = self
|
96
|
+
@validation_failed = false
|
83
97
|
define_input_methods
|
84
98
|
end
|
85
99
|
|
@@ -120,8 +134,30 @@ module Lluminary
|
|
120
134
|
raise NotImplementedError, "Subclasses must implement task_prompt"
|
121
135
|
end
|
122
136
|
|
137
|
+
# Helper for validation methods to add errors
|
138
|
+
def errors
|
139
|
+
# Points to the current model being validated - used by custom validation methods
|
140
|
+
if @current_model == :output && @output
|
141
|
+
@output.errors
|
142
|
+
else
|
143
|
+
@input.errors
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
123
147
|
private
|
124
148
|
|
149
|
+
def define_output_accessor_methods
|
150
|
+
return unless @output
|
151
|
+
|
152
|
+
# Define accessor methods for each output field
|
153
|
+
@output.attributes.each_key do |name|
|
154
|
+
next if name == "raw_response"
|
155
|
+
singleton_class.class_eval do
|
156
|
+
define_method(name) { @output.attributes[name.to_s] }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
125
161
|
def validate_input
|
126
162
|
validate_input!
|
127
163
|
end
|
@@ -129,6 +165,7 @@ module Lluminary
|
|
129
165
|
def process_response(response)
|
130
166
|
@parsed_response = response[:parsed]
|
131
167
|
@output = self.class.output_schema_model.new
|
168
|
+
@output.task_instance = self
|
132
169
|
@output.raw_response = response[:raw]
|
133
170
|
|
134
171
|
# Merge the parsed response first, then validate
|
@@ -160,6 +197,9 @@ module Lluminary
|
|
160
197
|
@output.attributes.merge!(converted_response)
|
161
198
|
end
|
162
199
|
|
200
|
+
# Define methods to access output attributes directly in validation methods
|
201
|
+
define_output_accessor_methods
|
202
|
+
|
163
203
|
# Validate after merging
|
164
204
|
@output.valid?
|
165
205
|
|
data/lib/lluminary.rb
CHANGED
@@ -3,10 +3,16 @@
|
|
3
3
|
require_relative "lluminary/version"
|
4
4
|
require_relative "lluminary/result"
|
5
5
|
require_relative "lluminary/task"
|
6
|
-
#
|
7
|
-
|
8
|
-
# automatically require all models
|
6
|
+
# require base model first
|
7
|
+
require_relative "lluminary/models/base"
|
8
|
+
# automatically require all models first
|
9
9
|
Dir[File.join(__dir__, "lluminary/models/**/*.rb")].each { |file| require file }
|
10
|
+
# require base provider first
|
11
|
+
require_relative "lluminary/providers/base"
|
12
|
+
# then require all other providers
|
13
|
+
Dir[File.join(__dir__, "lluminary/providers/*.rb")].each do |file|
|
14
|
+
require file unless file.end_with?("base.rb")
|
15
|
+
end
|
10
16
|
require_relative "lluminary/config"
|
11
17
|
|
12
18
|
# Lluminary is a framework for building and running LLM-powered tasks.
|
@@ -1035,6 +1035,38 @@ RSpec.describe Lluminary::Models::Base do
|
|
1035
1035
|
end
|
1036
1036
|
end
|
1037
1037
|
|
1038
|
+
context "with custom validation descriptions" do
|
1039
|
+
before do
|
1040
|
+
task_class.output_schema do
|
1041
|
+
string :name, description: "The person's name"
|
1042
|
+
integer :confidence, description: "Confidence score from 0-100"
|
1043
|
+
validate :validate_confidence_score,
|
1044
|
+
description: "Confidence score must be between 0 and 100"
|
1045
|
+
validate :validate_other_thing, description: nil
|
1046
|
+
end
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
it "includes an Additional Validations section with non-nil descriptions" do
|
1050
|
+
prompt = model.format_prompt(task)
|
1051
|
+
expect(prompt).to include("Additional Validations:")
|
1052
|
+
expect(prompt).to include(
|
1053
|
+
"- Confidence score must be between 0 and 100"
|
1054
|
+
)
|
1055
|
+
expect(prompt).not_to include("- \n") # Should not include a blank bullet for nil
|
1056
|
+
end
|
1057
|
+
|
1058
|
+
it "omits Additional Validations section if all descriptions are nil" do
|
1059
|
+
# Redefine schema with only nil descriptions
|
1060
|
+
task_class.output_schema do
|
1061
|
+
string :name, description: "The person's name"
|
1062
|
+
validate :validate_confidence_score, description: nil
|
1063
|
+
validate :validate_other_thing, description: nil
|
1064
|
+
end
|
1065
|
+
prompt = model.format_prompt(task)
|
1066
|
+
expect(prompt).not_to include("Additional Validations:")
|
1067
|
+
end
|
1068
|
+
end
|
1069
|
+
|
1038
1070
|
context "JSON example generation" do
|
1039
1071
|
context "with simple field types" do
|
1040
1072
|
it "generates correct JSON example for string field" do
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
3
|
+
require "lluminary/providers/anthropic"
|
4
|
+
|
5
|
+
RSpec.describe Lluminary::Providers::Anthropic do
|
6
|
+
let(:config) { { api_key: "test-key" } }
|
7
|
+
let(:provider) { described_class.new(**config) }
|
8
|
+
|
9
|
+
describe "#client" do
|
10
|
+
it "returns the Anthropic client instance" do
|
11
|
+
expect(provider.client).to be_a(Anthropic::Client)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#models" do
|
16
|
+
let(:mock_models_response) do
|
17
|
+
mock_model_info_1 = double("ModelInfo", id: "claude-3-5-sonnet-latest")
|
18
|
+
mock_model_info_2 = double("ModelInfo", id: "claude-3-haiku-20240307")
|
19
|
+
|
20
|
+
double(
|
21
|
+
"Page",
|
22
|
+
data: [mock_model_info_1, mock_model_info_2],
|
23
|
+
has_more: false,
|
24
|
+
first_id: "claude-3-5-sonnet-latest",
|
25
|
+
last_id: "claude-3-haiku-20240307"
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
before do
|
30
|
+
models_client = double("ModelsClient")
|
31
|
+
allow_any_instance_of(Anthropic::Client).to receive(:models).and_return(
|
32
|
+
models_client
|
33
|
+
)
|
34
|
+
allow(models_client).to receive(:list).and_return(mock_models_response)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "returns an array of model IDs as strings" do
|
38
|
+
expect(provider.models).to eq(
|
39
|
+
%w[claude-3-5-sonnet-latest claude-3-haiku-20240307]
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#call" do
|
45
|
+
let(:prompt) { "Test prompt" }
|
46
|
+
let(:task) { "Test task" }
|
47
|
+
let(:mock_response) do
|
48
|
+
OpenStruct.new(
|
49
|
+
content: [OpenStruct.new(text: '{"summary": "Test response"}')]
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
before do
|
54
|
+
messages_client = double("MessagesClient")
|
55
|
+
allow_any_instance_of(Anthropic::Client).to receive(:messages).and_return(
|
56
|
+
messages_client
|
57
|
+
)
|
58
|
+
allow(messages_client).to receive(:create).and_return(mock_response)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "returns a hash with raw and parsed response" do
|
62
|
+
response = provider.call(prompt, task)
|
63
|
+
expect(response).to eq(
|
64
|
+
{
|
65
|
+
raw: '{"summary": "Test response"}',
|
66
|
+
parsed: {
|
67
|
+
"summary" => "Test response"
|
68
|
+
}
|
69
|
+
}
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
context "when the response is not valid JSON" do
|
74
|
+
let(:mock_response) do
|
75
|
+
OpenStruct.new(content: [OpenStruct.new(text: "not valid json")])
|
76
|
+
end
|
77
|
+
|
78
|
+
it "returns raw response with nil parsed value" do
|
79
|
+
response = provider.call(prompt, task)
|
80
|
+
expect(response).to eq({ raw: "not valid json", parsed: nil })
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "#model" do
|
86
|
+
it "returns the default model when not specified" do
|
87
|
+
expect(provider.model).to be_a(
|
88
|
+
Lluminary::Models::Anthropic::Claude35Sonnet
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "returns the specified model when provided in config" do
|
93
|
+
model_class = double("ModelClass")
|
94
|
+
model_instance = double("ModelInstance")
|
95
|
+
|
96
|
+
allow(model_class).to receive(:new).and_return(model_instance)
|
97
|
+
|
98
|
+
custom_provider =
|
99
|
+
described_class.new(model: model_class, api_key: "test-key")
|
100
|
+
|
101
|
+
expect(custom_provider.model).to eq(model_instance)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|