lluminary 0.2.2 → 0.2.4
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/base.rb +45 -0
- data/lib/lluminary/models/google/gemini_20_flash.rb +19 -0
- data/lib/lluminary/providers/google.rb +65 -0
- data/lib/lluminary/schema.rb +30 -4
- data/lib/lluminary/schema_model.rb +86 -7
- data/lib/lluminary/task.rb +9 -9
- data/spec/examples/text_emotion_analyzer_spec.rb +75 -0
- data/spec/lluminary/models/base_spec.rb +141 -0
- data/spec/lluminary/models/google/gemini_20_flash_spec.rb +31 -0
- data/spec/lluminary/providers/google_spec.rb +84 -0
- data/spec/lluminary/schema_model_spec.rb +234 -0
- data/spec/lluminary/schema_spec.rb +223 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 43ca21be47208b82183b4156be99d1e9816ea1013b2ee0c5258df158b0f4f2c1
|
4
|
+
data.tar.gz: 4f835024abc67bcd6bb9dd108fd49431eec6a152f168e31592b2d5317b982889
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2cc7b7322ec931bf1d0c5783b3dfbd26bb3c7fff841d834cc120d271c6ecef3e22472ba278c5cee8932685cfe71c850950a0e6305b88c0d997df7b3ebc0fdca4
|
7
|
+
data.tar.gz: b9f95c68e583a7819bb4b37e36c0b40d6856ce9f4261aea7efe78018823e950471a306f223033747e32d25fb81790a64af1f9cec70ee102be4a2811ccfcdede4
|
@@ -175,6 +175,18 @@ module Lluminary
|
|
175
175
|
end
|
176
176
|
when :hash
|
177
177
|
"object"
|
178
|
+
when :dictionary
|
179
|
+
if field[:value_type].nil?
|
180
|
+
"object"
|
181
|
+
elsif field[:value_type][:type] == :array
|
182
|
+
"object with array values"
|
183
|
+
elsif field[:value_type][:type] == :datetime
|
184
|
+
"object with datetime values in ISO8601 format"
|
185
|
+
elsif field[:value_type][:type] == :hash
|
186
|
+
"object with object values"
|
187
|
+
else
|
188
|
+
"object with #{field[:value_type][:type]} values"
|
189
|
+
end
|
178
190
|
else
|
179
191
|
field[:type].to_s
|
180
192
|
end
|
@@ -294,6 +306,8 @@ module Lluminary
|
|
294
306
|
generate_array_example(name, field)
|
295
307
|
when :hash
|
296
308
|
generate_hash_example(name, field)
|
309
|
+
when :dictionary
|
310
|
+
generate_dictionary_example(name, field)
|
297
311
|
end
|
298
312
|
end
|
299
313
|
|
@@ -333,6 +347,37 @@ module Lluminary
|
|
333
347
|
end
|
334
348
|
end
|
335
349
|
|
350
|
+
def generate_dictionary_example(name, field)
|
351
|
+
return {} unless field[:value_type]
|
352
|
+
|
353
|
+
case field[:value_type][:type]
|
354
|
+
when :string
|
355
|
+
{ "some_key" => "first value", "other_key" => "second value" }
|
356
|
+
when :integer
|
357
|
+
{ "some_key" => 1, "other_key" => 2 }
|
358
|
+
when :float
|
359
|
+
{ "some_key" => 1.0, "other_key" => 2.0 }
|
360
|
+
when :boolean
|
361
|
+
{ "some_key" => true, "other_key" => false }
|
362
|
+
when :datetime
|
363
|
+
{
|
364
|
+
"some_key" => "2024-01-01T12:00:00+00:00",
|
365
|
+
"other_key" => "2024-01-02T12:00:00+00:00"
|
366
|
+
}
|
367
|
+
when :array
|
368
|
+
if field[:value_type][:element_type]
|
369
|
+
inner_example = generate_array_example("item", field[:value_type])
|
370
|
+
{ "some_key" => inner_example, "other_key" => inner_example }
|
371
|
+
else
|
372
|
+
{ "some_key" => [], "other_key" => [] }
|
373
|
+
end
|
374
|
+
when :hash
|
375
|
+
example =
|
376
|
+
generate_hash_example(name.to_s.singularize, field[:value_type])
|
377
|
+
{ "some_key" => example, "other_key" => example }
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
336
381
|
def format_additional_validations(custom_validations)
|
337
382
|
descriptions = custom_validations.map { |v| v[:description] }.compact
|
338
383
|
return "" if descriptions.empty?
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lluminary
|
4
|
+
module Models
|
5
|
+
module Google
|
6
|
+
class Gemini20Flash < Lluminary::Models::Base
|
7
|
+
NAME = "gemini-2.0-flash"
|
8
|
+
|
9
|
+
def compatible_with?(provider_name)
|
10
|
+
provider_name == :google
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
NAME
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openai"
|
4
|
+
require "json"
|
5
|
+
require_relative "../provider_error"
|
6
|
+
|
7
|
+
# This is a quick and dirty implementation of a provider that works with Google's AI studio.
|
8
|
+
# It does not currently support vertex. Plans are to eventually create a separate gem similar
|
9
|
+
# `gemini-ai` that can work with either AI studio or Vertex. For now, this just uses the
|
10
|
+
# OpenAI compatible endpoint.
|
11
|
+
module Lluminary
|
12
|
+
module Providers
|
13
|
+
class Google < Base
|
14
|
+
NAME = :google
|
15
|
+
DEFAULT_MODEL = Models::Google::Gemini20Flash
|
16
|
+
|
17
|
+
attr_reader :client, :config
|
18
|
+
|
19
|
+
def initialize(**config_overrides)
|
20
|
+
super
|
21
|
+
@config = { model: DEFAULT_MODEL }.merge(config)
|
22
|
+
@client =
|
23
|
+
::OpenAI::Client.new(
|
24
|
+
access_token: config[:api_key],
|
25
|
+
api_version: "",
|
26
|
+
uri_base: "https://generativelanguage.googleapis.com/v1beta/openai"
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def call(prompt, _task)
|
31
|
+
response =
|
32
|
+
client.chat(
|
33
|
+
parameters: {
|
34
|
+
model: model.class::NAME,
|
35
|
+
messages: [{ role: "user", content: prompt }],
|
36
|
+
response_format: {
|
37
|
+
type: "json_object"
|
38
|
+
}
|
39
|
+
}
|
40
|
+
)
|
41
|
+
|
42
|
+
content = response.dig("choices", 0, "message", "content")
|
43
|
+
|
44
|
+
{
|
45
|
+
raw: content,
|
46
|
+
parsed:
|
47
|
+
begin
|
48
|
+
JSON.parse(content) if content
|
49
|
+
rescue JSON::ParserError
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def model
|
56
|
+
@model ||= config[:model].new
|
57
|
+
end
|
58
|
+
|
59
|
+
def models
|
60
|
+
response = @client.models.list
|
61
|
+
response["data"].map { |model| model["id"].split("/").last }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/lluminary/schema.rb
CHANGED
@@ -36,7 +36,7 @@ module Lluminary
|
|
36
36
|
field = { type: :array, description: description }
|
37
37
|
|
38
38
|
if block
|
39
|
-
element_schema =
|
39
|
+
element_schema = ElementTypeSchema.new
|
40
40
|
field[:element_type] = element_schema.instance_eval(&block)
|
41
41
|
end
|
42
42
|
|
@@ -58,6 +58,21 @@ module Lluminary
|
|
58
58
|
}
|
59
59
|
end
|
60
60
|
|
61
|
+
def dictionary(name, description: nil, &block)
|
62
|
+
unless block
|
63
|
+
raise ArgumentError, "Dictionary fields must be defined with a block"
|
64
|
+
end
|
65
|
+
|
66
|
+
element_schema = ElementTypeSchema.new
|
67
|
+
value_type = element_schema.instance_eval(&block)
|
68
|
+
|
69
|
+
@fields[name] = {
|
70
|
+
type: :dictionary,
|
71
|
+
description: description,
|
72
|
+
value_type: value_type
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
61
76
|
attr_reader :fields, :custom_validations
|
62
77
|
|
63
78
|
def validates(*args, **options)
|
@@ -90,8 +105,8 @@ module Lluminary
|
|
90
105
|
)
|
91
106
|
end
|
92
107
|
|
93
|
-
# Internal class for defining
|
94
|
-
class
|
108
|
+
# Internal class for defining element types for arrays and dictionaries (or any other container-type objects)
|
109
|
+
class ElementTypeSchema
|
95
110
|
def string(description: nil)
|
96
111
|
{ type: :string, description: description }
|
97
112
|
end
|
@@ -114,7 +129,7 @@ module Lluminary
|
|
114
129
|
|
115
130
|
def array(description: nil, &block)
|
116
131
|
field = { type: :array, description: description }
|
117
|
-
field[:element_type] =
|
132
|
+
field[:element_type] = ElementTypeSchema.new.instance_eval(
|
118
133
|
&block
|
119
134
|
) if block
|
120
135
|
field
|
@@ -130,6 +145,17 @@ module Lluminary
|
|
130
145
|
|
131
146
|
{ type: :hash, description: description, fields: nested_schema.fields }
|
132
147
|
end
|
148
|
+
|
149
|
+
def dictionary(description: nil, &block)
|
150
|
+
unless block
|
151
|
+
raise ArgumentError, "Dictionary fields must be defined with a block"
|
152
|
+
end
|
153
|
+
|
154
|
+
element_schema = ElementTypeSchema.new
|
155
|
+
value_type = element_schema.instance_eval(&block)
|
156
|
+
|
157
|
+
{ type: :dictionary, description: description, value_type: value_type }
|
158
|
+
end
|
133
159
|
end
|
134
160
|
end
|
135
161
|
end
|
@@ -71,14 +71,11 @@ module Lluminary
|
|
71
71
|
|
72
72
|
case field[:type]
|
73
73
|
when :hash
|
74
|
-
validate_hash_field(record, name
|
74
|
+
validate_hash_field(record, name, value, field)
|
75
75
|
when :array
|
76
|
-
validate_array_field(
|
77
|
-
|
78
|
-
|
79
|
-
value,
|
80
|
-
field[:element_type]
|
81
|
-
)
|
76
|
+
validate_array_field(record, name, value, field[:element_type])
|
77
|
+
when :dictionary
|
78
|
+
validate_dictionary_field(record, name, value, field[:value_type])
|
82
79
|
when :string
|
83
80
|
unless value.is_a?(String)
|
84
81
|
record.errors.add(name.to_s.capitalize, "must be a String")
|
@@ -113,6 +110,72 @@ module Lluminary
|
|
113
110
|
|
114
111
|
private
|
115
112
|
|
113
|
+
def validate_dictionary_field(
|
114
|
+
record,
|
115
|
+
name,
|
116
|
+
value,
|
117
|
+
value_type,
|
118
|
+
path = nil
|
119
|
+
)
|
120
|
+
field_name = path || name
|
121
|
+
|
122
|
+
unless value.is_a?(Hash)
|
123
|
+
record.errors.add(field_name, "must be a Hash")
|
124
|
+
return
|
125
|
+
end
|
126
|
+
|
127
|
+
value.each do |key, val|
|
128
|
+
current_path = "#{field_name}[#{key}]"
|
129
|
+
|
130
|
+
case value_type[:type]
|
131
|
+
when :string
|
132
|
+
unless val.is_a?(String)
|
133
|
+
record.errors.add(current_path, "must be a String")
|
134
|
+
end
|
135
|
+
when :integer
|
136
|
+
unless val.is_a?(Integer)
|
137
|
+
record.errors.add(current_path, "must be an Integer")
|
138
|
+
end
|
139
|
+
when :boolean
|
140
|
+
unless [true, false].include?(val)
|
141
|
+
record.errors.add(current_path, "must be true or false")
|
142
|
+
end
|
143
|
+
when :float
|
144
|
+
unless val.is_a?(Float)
|
145
|
+
record.errors.add(current_path, "must be a float")
|
146
|
+
end
|
147
|
+
when :datetime
|
148
|
+
unless val.is_a?(DateTime)
|
149
|
+
record.errors.add(current_path, "must be a DateTime")
|
150
|
+
end
|
151
|
+
when :hash
|
152
|
+
validate_hash_field(
|
153
|
+
record,
|
154
|
+
current_path,
|
155
|
+
val,
|
156
|
+
value_type,
|
157
|
+
current_path
|
158
|
+
)
|
159
|
+
when :array
|
160
|
+
validate_array_field(
|
161
|
+
record,
|
162
|
+
current_path,
|
163
|
+
val,
|
164
|
+
value_type[:element_type],
|
165
|
+
current_path
|
166
|
+
)
|
167
|
+
when :dictionary
|
168
|
+
validate_dictionary_field(
|
169
|
+
record,
|
170
|
+
current_path,
|
171
|
+
val,
|
172
|
+
value_type[:value_type],
|
173
|
+
current_path
|
174
|
+
)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
116
179
|
def validate_hash_field(
|
117
180
|
record,
|
118
181
|
name,
|
@@ -145,6 +208,14 @@ module Lluminary
|
|
145
208
|
field[:element_type],
|
146
209
|
current_path
|
147
210
|
)
|
211
|
+
when :dictionary
|
212
|
+
validate_dictionary_field(
|
213
|
+
record,
|
214
|
+
key,
|
215
|
+
field_value,
|
216
|
+
field[:value_type],
|
217
|
+
current_path
|
218
|
+
)
|
148
219
|
when :string
|
149
220
|
unless field_value.is_a?(String)
|
150
221
|
record.errors.add(current_path, "must be a String")
|
@@ -199,6 +270,14 @@ module Lluminary
|
|
199
270
|
element_type[:element_type],
|
200
271
|
current_path
|
201
272
|
)
|
273
|
+
when :dictionary
|
274
|
+
validate_dictionary_field(
|
275
|
+
record,
|
276
|
+
name,
|
277
|
+
element,
|
278
|
+
element_type[:value_type],
|
279
|
+
current_path
|
280
|
+
)
|
202
281
|
when :string
|
203
282
|
unless element.is_a?(String)
|
204
283
|
record.errors.add(current_path, "must be a String")
|
data/lib/lluminary/task.rb
CHANGED
@@ -3,9 +3,6 @@ require "ostruct"
|
|
3
3
|
require "json"
|
4
4
|
require_relative "schema"
|
5
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"
|
9
6
|
|
10
7
|
module Lluminary
|
11
8
|
# Base class for all Lluminary tasks.
|
@@ -25,18 +22,21 @@ module Lluminary
|
|
25
22
|
def use_provider(provider_name, **config)
|
26
23
|
provider_class =
|
27
24
|
case provider_name
|
25
|
+
when :anthropic
|
26
|
+
require_relative "providers/anthropic"
|
27
|
+
Providers::Anthropic
|
28
|
+
when :bedrock
|
29
|
+
require_relative "providers/bedrock"
|
30
|
+
Providers::Bedrock
|
31
|
+
when :google
|
32
|
+
require_relative "providers/google"
|
33
|
+
Providers::Google
|
28
34
|
when :openai
|
29
35
|
require_relative "providers/openai"
|
30
36
|
Providers::OpenAI
|
31
37
|
when :test
|
32
38
|
require_relative "providers/test"
|
33
39
|
Providers::Test
|
34
|
-
when :bedrock
|
35
|
-
require_relative "providers/bedrock"
|
36
|
-
Providers::Bedrock
|
37
|
-
when :anthropic
|
38
|
-
require_relative "providers/anthropic"
|
39
|
-
Providers::Anthropic
|
40
40
|
else
|
41
41
|
raise ArgumentError, "Unknown provider: #{provider_name}"
|
42
42
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
3
|
+
require_relative "../../examples/text_emotion_analyzer"
|
4
|
+
|
5
|
+
RSpec.describe TextEmotionAnalyzer do
|
6
|
+
let(:sample_text) { <<~TEXT }
|
7
|
+
The sun was setting behind the mountains, casting long shadows across the valley.
|
8
|
+
Sarah felt a mix of emotions as she watched the last rays of light disappear.
|
9
|
+
There was a deep sense of peace, but also a tinge of sadness knowing this beautiful moment would soon be gone.
|
10
|
+
She smiled through her tears, grateful for the experience yet longing for it to last just a little longer.
|
11
|
+
TEXT
|
12
|
+
|
13
|
+
describe "input validation" do
|
14
|
+
it "accepts valid text input" do
|
15
|
+
expect { described_class.call!(text: sample_text) }.not_to raise_error
|
16
|
+
end
|
17
|
+
|
18
|
+
it "requires text to be present" do
|
19
|
+
expect { described_class.call!(text: "") }.to raise_error(
|
20
|
+
Lluminary::ValidationError
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "output validation" do
|
26
|
+
let(:result) { described_class.call(text: sample_text) }
|
27
|
+
|
28
|
+
it "returns a dictionary of emotion scores" do
|
29
|
+
expect(result.output.emotion_scores).to be_a(Hash)
|
30
|
+
expect(result.output.emotion_scores).not_to be_empty
|
31
|
+
end
|
32
|
+
|
33
|
+
it "returns float scores between 0.0 and 1.0" do
|
34
|
+
result.output.emotion_scores.each do |emotion, score|
|
35
|
+
expect(score).to be_a(Float)
|
36
|
+
expect(score).to be_between(0.0, 1.0)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns a dominant emotion" do
|
41
|
+
expect(result.output.dominant_emotion).to be_a(String)
|
42
|
+
expect(result.output.dominant_emotion).not_to be_empty
|
43
|
+
end
|
44
|
+
|
45
|
+
it "returns an analysis" do
|
46
|
+
expect(result.output.analysis).to be_a(String)
|
47
|
+
expect(result.output.analysis).not_to be_empty
|
48
|
+
end
|
49
|
+
|
50
|
+
it "returns valid JSON response" do
|
51
|
+
expect(result.output.raw_response).to be_a(String)
|
52
|
+
expect { JSON.parse(result.output.raw_response) }.not_to raise_error
|
53
|
+
json = JSON.parse(result.output.raw_response)
|
54
|
+
expect(json).to have_key("emotion_scores")
|
55
|
+
expect(json).to have_key("dominant_emotion")
|
56
|
+
expect(json).to have_key("analysis")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "emotion detection" do
|
61
|
+
it "detects multiple emotions in complex text" do
|
62
|
+
result = described_class.call(text: sample_text)
|
63
|
+
expect(result.output.emotion_scores.size).to be >= 2
|
64
|
+
end
|
65
|
+
|
66
|
+
it "identifies the highest scoring emotion as dominant" do
|
67
|
+
result = described_class.call(text: sample_text)
|
68
|
+
dominant_score =
|
69
|
+
result.output.emotion_scores[result.output.dominant_emotion]
|
70
|
+
result.output.emotion_scores.each do |emotion, score|
|
71
|
+
expect(score).to be <= dominant_score
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -1409,6 +1409,147 @@ RSpec.describe Lluminary::Models::Base do
|
|
1409
1409
|
expect(prompt).to include(expected_json)
|
1410
1410
|
end
|
1411
1411
|
end
|
1412
|
+
|
1413
|
+
context "with dictionary fields" do
|
1414
|
+
it "formats dictionary field description correctly" do
|
1415
|
+
task_class.output_schema do
|
1416
|
+
dictionary :emotion_scores,
|
1417
|
+
description: "Scores for each detected emotion" do
|
1418
|
+
float
|
1419
|
+
end
|
1420
|
+
end
|
1421
|
+
|
1422
|
+
prompt = model.format_prompt(task)
|
1423
|
+
|
1424
|
+
expected_description = <<~DESCRIPTION.chomp
|
1425
|
+
# emotion_scores
|
1426
|
+
Description: Scores for each detected emotion
|
1427
|
+
Type: object with float values
|
1428
|
+
Example: {"some_key":1.0,"other_key":2.0}
|
1429
|
+
DESCRIPTION
|
1430
|
+
|
1431
|
+
expect(prompt).to include(expected_description)
|
1432
|
+
end
|
1433
|
+
|
1434
|
+
it "formats dictionary of arrays field description correctly" do
|
1435
|
+
task_class.output_schema do
|
1436
|
+
dictionary :categories, description: "Categories and their items" do
|
1437
|
+
array { string }
|
1438
|
+
end
|
1439
|
+
end
|
1440
|
+
|
1441
|
+
prompt = model.format_prompt(task)
|
1442
|
+
|
1443
|
+
expected_description = <<~DESCRIPTION.chomp
|
1444
|
+
# categories
|
1445
|
+
Description: Categories and their items
|
1446
|
+
Type: object with array values
|
1447
|
+
Example: {"some_key":["first item","second item"],"other_key":["first item","second item"]}
|
1448
|
+
DESCRIPTION
|
1449
|
+
|
1450
|
+
expect(prompt).to include(expected_description)
|
1451
|
+
end
|
1452
|
+
|
1453
|
+
it "formats dictionary of objects field description correctly" do
|
1454
|
+
task_class.output_schema do
|
1455
|
+
dictionary :users, description: "User profiles" do
|
1456
|
+
hash do
|
1457
|
+
string :name
|
1458
|
+
integer :age
|
1459
|
+
end
|
1460
|
+
end
|
1461
|
+
end
|
1462
|
+
|
1463
|
+
prompt = model.format_prompt(task)
|
1464
|
+
|
1465
|
+
expected_description = <<~DESCRIPTION.chomp
|
1466
|
+
# users
|
1467
|
+
Description: User profiles
|
1468
|
+
Type: object with object values
|
1469
|
+
Example: {"some_key":{"name":"your name here","age":0},"other_key":{"name":"your name here","age":0}}
|
1470
|
+
DESCRIPTION
|
1471
|
+
|
1472
|
+
expect(prompt).to include(expected_description)
|
1473
|
+
end
|
1474
|
+
|
1475
|
+
it "generates correct JSON example for dictionary of floats" do
|
1476
|
+
task_class.output_schema do
|
1477
|
+
dictionary :scores, description: "Scores for each item" do
|
1478
|
+
float
|
1479
|
+
end
|
1480
|
+
end
|
1481
|
+
|
1482
|
+
prompt = model.format_prompt(task)
|
1483
|
+
|
1484
|
+
expected_json = <<~JSON.chomp
|
1485
|
+
{
|
1486
|
+
"scores": {
|
1487
|
+
"some_key": 1.0,
|
1488
|
+
"other_key": 2.0
|
1489
|
+
}
|
1490
|
+
}
|
1491
|
+
JSON
|
1492
|
+
|
1493
|
+
expect(prompt).to include(expected_json)
|
1494
|
+
end
|
1495
|
+
|
1496
|
+
it "generates correct JSON example for dictionary of arrays" do
|
1497
|
+
task_class.output_schema do
|
1498
|
+
dictionary :categories, description: "Categories and their items" do
|
1499
|
+
array { string }
|
1500
|
+
end
|
1501
|
+
end
|
1502
|
+
|
1503
|
+
prompt = model.format_prompt(task)
|
1504
|
+
|
1505
|
+
expected_json = <<~JSON.chomp
|
1506
|
+
{
|
1507
|
+
"categories": {
|
1508
|
+
"some_key": [
|
1509
|
+
"first item",
|
1510
|
+
"second item"
|
1511
|
+
],
|
1512
|
+
"other_key": [
|
1513
|
+
"first item",
|
1514
|
+
"second item"
|
1515
|
+
]
|
1516
|
+
}
|
1517
|
+
}
|
1518
|
+
JSON
|
1519
|
+
|
1520
|
+
expect(prompt).to include(expected_json)
|
1521
|
+
end
|
1522
|
+
|
1523
|
+
it "generates correct JSON example for dictionary of objects" do
|
1524
|
+
task_class.output_schema do
|
1525
|
+
dictionary :users, description: "User profiles" do
|
1526
|
+
hash do
|
1527
|
+
string :name
|
1528
|
+
integer :age
|
1529
|
+
end
|
1530
|
+
end
|
1531
|
+
end
|
1532
|
+
|
1533
|
+
prompt = model.format_prompt(task)
|
1534
|
+
|
1535
|
+
expected_json = <<~JSON.chomp
|
1536
|
+
{
|
1537
|
+
"users": {
|
1538
|
+
"some_key": {
|
1539
|
+
"name": "your name here",
|
1540
|
+
"age": 0
|
1541
|
+
},
|
1542
|
+
"other_key": {
|
1543
|
+
"name": "your name here",
|
1544
|
+
"age": 0
|
1545
|
+
}
|
1546
|
+
}
|
1547
|
+
}
|
1548
|
+
JSON
|
1549
|
+
|
1550
|
+
expect(prompt).to include(expected_json)
|
1551
|
+
end
|
1552
|
+
end
|
1412
1553
|
end
|
1413
1554
|
end
|
1414
1555
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe Lluminary::Models::Google::Gemini20Flash do
|
6
|
+
subject(:model) { described_class.new }
|
7
|
+
|
8
|
+
describe "#NAME" do
|
9
|
+
it "has the correct model name" do
|
10
|
+
expect(described_class::NAME).to eq("gemini-2.0-flash")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#compatible_with?" do
|
15
|
+
it "returns true for google provider" do
|
16
|
+
expect(model.compatible_with?(:google)).to be true
|
17
|
+
end
|
18
|
+
|
19
|
+
it "returns false for other providers" do
|
20
|
+
expect(model.compatible_with?(:openai)).to be false
|
21
|
+
expect(model.compatible_with?(:bedrock)).to be false
|
22
|
+
expect(model.compatible_with?(:anthropic)).to be false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#name" do
|
27
|
+
it "returns the model name" do
|
28
|
+
expect(model.name).to eq("gemini-2.0-flash")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
RSpec.describe Lluminary::Providers::Google do
|
5
|
+
let(:config) { { api_key: "test-key" } }
|
6
|
+
let(:provider) { described_class.new(**config) }
|
7
|
+
|
8
|
+
describe "#client" do
|
9
|
+
it "returns the OpenAI client instance" do
|
10
|
+
expect(provider.client).to be_a(OpenAI::Client)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#models" do
|
15
|
+
let(:mock_models_response) do
|
16
|
+
{
|
17
|
+
"object" => "list",
|
18
|
+
"data" => [
|
19
|
+
{
|
20
|
+
"id" => "models/gemini-2.0-flash",
|
21
|
+
"object" => "model",
|
22
|
+
"owned_by" => "google"
|
23
|
+
},
|
24
|
+
{
|
25
|
+
"id" => "models/gemini-1.5-pro",
|
26
|
+
"object" => "model",
|
27
|
+
"owned_by" => "google"
|
28
|
+
}
|
29
|
+
]
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
before do
|
34
|
+
allow_any_instance_of(OpenAI::Client).to receive(:models).and_return(
|
35
|
+
double("ModelsClient", list: mock_models_response)
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns an array of model IDs as strings with the 'models/' prefix removed" do
|
40
|
+
expect(provider.models).to eq(%w[gemini-2.0-flash gemini-1.5-pro])
|
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
|
+
{
|
49
|
+
"choices" => [
|
50
|
+
{ "message" => { "content" => '{"summary": "Test response"}' } }
|
51
|
+
]
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
before do
|
56
|
+
allow_any_instance_of(OpenAI::Client).to receive(:chat).and_return(
|
57
|
+
mock_response
|
58
|
+
)
|
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
|
+
{ "choices" => [{ "message" => { "content" => "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
|
+
end
|
@@ -428,4 +428,238 @@ RSpec.describe Lluminary::SchemaModel do
|
|
428
428
|
)
|
429
429
|
end
|
430
430
|
end
|
431
|
+
|
432
|
+
describe "dictionary type enforcement" do
|
433
|
+
let(:fields) do
|
434
|
+
{
|
435
|
+
tags: {
|
436
|
+
type: :dictionary,
|
437
|
+
description: nil,
|
438
|
+
value_type: {
|
439
|
+
type: :string,
|
440
|
+
description: nil
|
441
|
+
}
|
442
|
+
}
|
443
|
+
}
|
444
|
+
end
|
445
|
+
let(:model_class) { described_class.build(fields: fields, validations: []) }
|
446
|
+
|
447
|
+
it "validates that value is a hash" do
|
448
|
+
instance = model_class.new(tags: "not a hash")
|
449
|
+
expect(instance.valid?).to be false
|
450
|
+
expect(instance.errors.full_messages).to contain_exactly(
|
451
|
+
"Tags must be a Hash"
|
452
|
+
)
|
453
|
+
end
|
454
|
+
|
455
|
+
it "validates dictionary value types" do
|
456
|
+
instance = model_class.new(tags: { "tag1" => "valid", "tag2" => 123 })
|
457
|
+
expect(instance.valid?).to be false
|
458
|
+
expect(instance.errors.full_messages).to contain_exactly(
|
459
|
+
"Tags[tag2] must be a String"
|
460
|
+
)
|
461
|
+
end
|
462
|
+
|
463
|
+
it "accepts valid dictionary values" do
|
464
|
+
instance =
|
465
|
+
model_class.new(tags: { "tag1" => "valid", "tag2" => "also valid" })
|
466
|
+
expect(instance.valid?).to be true
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
describe "nested dictionary validation" do
|
471
|
+
let(:fields) do
|
472
|
+
{
|
473
|
+
config: {
|
474
|
+
type: :hash,
|
475
|
+
description: nil,
|
476
|
+
fields: {
|
477
|
+
settings: {
|
478
|
+
type: :dictionary,
|
479
|
+
description: nil,
|
480
|
+
value_type: {
|
481
|
+
type: :integer,
|
482
|
+
description: nil
|
483
|
+
}
|
484
|
+
}
|
485
|
+
}
|
486
|
+
}
|
487
|
+
}
|
488
|
+
end
|
489
|
+
let(:model_class) { described_class.build(fields: fields, validations: []) }
|
490
|
+
|
491
|
+
it "validates dictionaries inside hashes" do
|
492
|
+
instance =
|
493
|
+
model_class.new(
|
494
|
+
config: {
|
495
|
+
settings: {
|
496
|
+
"timeout" => 30,
|
497
|
+
"retries" => "3" # should be integer
|
498
|
+
}
|
499
|
+
}
|
500
|
+
)
|
501
|
+
expect(instance.valid?).to be false
|
502
|
+
expect(instance.errors.full_messages).to contain_exactly(
|
503
|
+
"Config[settings][retries] must be an Integer"
|
504
|
+
)
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
describe "dictionary of hashes validation" do
|
509
|
+
let(:fields) do
|
510
|
+
{
|
511
|
+
users: {
|
512
|
+
type: :dictionary,
|
513
|
+
description: nil,
|
514
|
+
value_type: {
|
515
|
+
type: :hash,
|
516
|
+
description: nil,
|
517
|
+
fields: {
|
518
|
+
name: {
|
519
|
+
type: :string,
|
520
|
+
description: nil
|
521
|
+
},
|
522
|
+
age: {
|
523
|
+
type: :integer,
|
524
|
+
description: nil
|
525
|
+
}
|
526
|
+
}
|
527
|
+
}
|
528
|
+
}
|
529
|
+
}
|
530
|
+
end
|
531
|
+
let(:model_class) { described_class.build(fields: fields, validations: []) }
|
532
|
+
|
533
|
+
it "validates hashes inside dictionaries" do
|
534
|
+
instance =
|
535
|
+
model_class.new(
|
536
|
+
users: {
|
537
|
+
"user1" => {
|
538
|
+
name: "Alice",
|
539
|
+
age: 30
|
540
|
+
},
|
541
|
+
"user2" => {
|
542
|
+
name: 123,
|
543
|
+
age: "invalid"
|
544
|
+
} # name should be string, age should be integer
|
545
|
+
}
|
546
|
+
)
|
547
|
+
expect(instance.valid?).to be false
|
548
|
+
expect(instance.errors.full_messages).to contain_exactly(
|
549
|
+
"Users[user2][name] must be a String",
|
550
|
+
"Users[user2][age] must be an Integer"
|
551
|
+
)
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
describe "dictionary of arrays validation" do
|
556
|
+
let(:fields) do
|
557
|
+
{
|
558
|
+
categories: {
|
559
|
+
type: :dictionary,
|
560
|
+
description: nil,
|
561
|
+
value_type: {
|
562
|
+
type: :array,
|
563
|
+
description: nil,
|
564
|
+
element_type: {
|
565
|
+
type: :string,
|
566
|
+
description: nil
|
567
|
+
}
|
568
|
+
}
|
569
|
+
}
|
570
|
+
}
|
571
|
+
end
|
572
|
+
let(:model_class) { described_class.build(fields: fields, validations: []) }
|
573
|
+
|
574
|
+
it "validates arrays inside dictionaries" do
|
575
|
+
instance =
|
576
|
+
model_class.new(
|
577
|
+
categories: {
|
578
|
+
"fruits" => %w[apple banana],
|
579
|
+
"numbers" => ["one", 2, "three"] # should all be strings
|
580
|
+
}
|
581
|
+
)
|
582
|
+
expect(instance.valid?).to be false
|
583
|
+
expect(instance.errors.full_messages).to contain_exactly(
|
584
|
+
"Categories[numbers][1] must be a String"
|
585
|
+
)
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
describe "array of dictionaries validation" do
|
590
|
+
let(:fields) do
|
591
|
+
{
|
592
|
+
configs: {
|
593
|
+
type: :array,
|
594
|
+
description: nil,
|
595
|
+
element_type: {
|
596
|
+
type: :dictionary,
|
597
|
+
description: nil,
|
598
|
+
value_type: {
|
599
|
+
type: :string,
|
600
|
+
description: nil
|
601
|
+
}
|
602
|
+
}
|
603
|
+
}
|
604
|
+
}
|
605
|
+
end
|
606
|
+
let(:model_class) { described_class.build(fields: fields, validations: []) }
|
607
|
+
|
608
|
+
it "validates dictionaries inside arrays" do
|
609
|
+
instance =
|
610
|
+
model_class.new(
|
611
|
+
configs: [
|
612
|
+
{ "key1" => "value1", "key2" => "value2" },
|
613
|
+
{ "key3" => "value3", "key4" => 123 } # should be string
|
614
|
+
]
|
615
|
+
)
|
616
|
+
expect(instance.valid?).to be false
|
617
|
+
expect(instance.errors.full_messages).to contain_exactly(
|
618
|
+
"Configs[1][key4] must be a String"
|
619
|
+
)
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
describe "hash with dictionary validation" do
|
624
|
+
let(:fields) do
|
625
|
+
{
|
626
|
+
config: {
|
627
|
+
type: :hash,
|
628
|
+
description: "Configuration",
|
629
|
+
fields: {
|
630
|
+
name: {
|
631
|
+
type: :string,
|
632
|
+
description: nil
|
633
|
+
},
|
634
|
+
settings: {
|
635
|
+
type: :dictionary,
|
636
|
+
description: nil,
|
637
|
+
value_type: {
|
638
|
+
type: :boolean,
|
639
|
+
description: nil
|
640
|
+
}
|
641
|
+
}
|
642
|
+
}
|
643
|
+
}
|
644
|
+
}
|
645
|
+
end
|
646
|
+
let(:model_class) { described_class.build(fields: fields, validations: []) }
|
647
|
+
|
648
|
+
it "validates dictionaries inside hashes" do
|
649
|
+
instance =
|
650
|
+
model_class.new(
|
651
|
+
config: {
|
652
|
+
name: "test",
|
653
|
+
settings: {
|
654
|
+
"enabled" => true,
|
655
|
+
"active" => "true" # should be boolean
|
656
|
+
}
|
657
|
+
}
|
658
|
+
)
|
659
|
+
expect(instance.valid?).to be false
|
660
|
+
expect(instance.errors.full_messages).to contain_exactly(
|
661
|
+
"Config[settings][active] must be true or false"
|
662
|
+
)
|
663
|
+
end
|
664
|
+
end
|
431
665
|
end
|
@@ -294,6 +294,25 @@ RSpec.describe Lluminary::Schema do
|
|
294
294
|
}
|
295
295
|
)
|
296
296
|
end
|
297
|
+
|
298
|
+
it "supports dictionaries inside arrays" do
|
299
|
+
schema.array(:configs) { dictionary { string } }
|
300
|
+
|
301
|
+
expect(schema.fields[:configs]).to eq(
|
302
|
+
{
|
303
|
+
type: :array,
|
304
|
+
description: nil,
|
305
|
+
element_type: {
|
306
|
+
type: :dictionary,
|
307
|
+
description: nil,
|
308
|
+
value_type: {
|
309
|
+
type: :string,
|
310
|
+
description: nil
|
311
|
+
}
|
312
|
+
}
|
313
|
+
}
|
314
|
+
)
|
315
|
+
end
|
297
316
|
end
|
298
317
|
|
299
318
|
describe "#hash" do
|
@@ -414,6 +433,210 @@ RSpec.describe Lluminary::Schema do
|
|
414
433
|
}
|
415
434
|
)
|
416
435
|
end
|
436
|
+
|
437
|
+
it "supports dictionaries inside hashes" do
|
438
|
+
schema.hash(:config) do
|
439
|
+
string :name
|
440
|
+
dictionary :settings do
|
441
|
+
string
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
expect(schema.fields[:config]).to eq(
|
446
|
+
{
|
447
|
+
type: :hash,
|
448
|
+
description: nil,
|
449
|
+
fields: {
|
450
|
+
name: {
|
451
|
+
type: :string,
|
452
|
+
description: nil
|
453
|
+
},
|
454
|
+
settings: {
|
455
|
+
type: :dictionary,
|
456
|
+
description: nil,
|
457
|
+
value_type: {
|
458
|
+
type: :string,
|
459
|
+
description: nil
|
460
|
+
}
|
461
|
+
}
|
462
|
+
}
|
463
|
+
}
|
464
|
+
)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
describe "#dictionary" do
|
469
|
+
it "adds a dictionary field to the schema" do
|
470
|
+
schema.dictionary(:tags) { string }
|
471
|
+
expect(schema.fields).to eq(
|
472
|
+
{
|
473
|
+
tags: {
|
474
|
+
type: :dictionary,
|
475
|
+
description: nil,
|
476
|
+
value_type: {
|
477
|
+
type: :string,
|
478
|
+
description: nil
|
479
|
+
}
|
480
|
+
}
|
481
|
+
}
|
482
|
+
)
|
483
|
+
end
|
484
|
+
|
485
|
+
it "adds a dictionary field with description" do
|
486
|
+
schema.dictionary(:tags, description: "A map of tags") { string }
|
487
|
+
expect(schema.fields).to eq(
|
488
|
+
{
|
489
|
+
tags: {
|
490
|
+
type: :dictionary,
|
491
|
+
description: "A map of tags",
|
492
|
+
value_type: {
|
493
|
+
type: :string,
|
494
|
+
description: nil
|
495
|
+
}
|
496
|
+
}
|
497
|
+
}
|
498
|
+
)
|
499
|
+
end
|
500
|
+
|
501
|
+
it "requires a name for the dictionary field" do
|
502
|
+
expect { schema.dictionary }.to raise_error(ArgumentError)
|
503
|
+
end
|
504
|
+
|
505
|
+
it "requires a block for dictionary fields" do
|
506
|
+
expect { schema.dictionary(:tags) }.to raise_error(
|
507
|
+
ArgumentError,
|
508
|
+
"Dictionary fields must be defined with a block"
|
509
|
+
)
|
510
|
+
end
|
511
|
+
|
512
|
+
it "accepts string type without a name" do
|
513
|
+
schema.dictionary(:tags) { string }
|
514
|
+
expect(schema.fields).to eq(
|
515
|
+
{
|
516
|
+
tags: {
|
517
|
+
type: :dictionary,
|
518
|
+
description: nil,
|
519
|
+
value_type: {
|
520
|
+
type: :string,
|
521
|
+
description: nil
|
522
|
+
}
|
523
|
+
}
|
524
|
+
}
|
525
|
+
)
|
526
|
+
end
|
527
|
+
|
528
|
+
it "accepts integer type without a name" do
|
529
|
+
schema.dictionary(:scores) { integer }
|
530
|
+
expect(schema.fields).to eq(
|
531
|
+
{
|
532
|
+
scores: {
|
533
|
+
type: :dictionary,
|
534
|
+
description: nil,
|
535
|
+
value_type: {
|
536
|
+
type: :integer,
|
537
|
+
description: nil
|
538
|
+
}
|
539
|
+
}
|
540
|
+
}
|
541
|
+
)
|
542
|
+
end
|
543
|
+
|
544
|
+
it "accepts boolean type without a name" do
|
545
|
+
schema.dictionary(:flags) { boolean }
|
546
|
+
expect(schema.fields).to eq(
|
547
|
+
{
|
548
|
+
flags: {
|
549
|
+
type: :dictionary,
|
550
|
+
description: nil,
|
551
|
+
value_type: {
|
552
|
+
type: :boolean,
|
553
|
+
description: nil
|
554
|
+
}
|
555
|
+
}
|
556
|
+
}
|
557
|
+
)
|
558
|
+
end
|
559
|
+
|
560
|
+
it "accepts float type without a name" do
|
561
|
+
schema.dictionary(:ratings) { float }
|
562
|
+
expect(schema.fields).to eq(
|
563
|
+
{
|
564
|
+
ratings: {
|
565
|
+
type: :dictionary,
|
566
|
+
description: nil,
|
567
|
+
value_type: {
|
568
|
+
type: :float,
|
569
|
+
description: nil
|
570
|
+
}
|
571
|
+
}
|
572
|
+
}
|
573
|
+
)
|
574
|
+
end
|
575
|
+
|
576
|
+
it "accepts datetime type without a name" do
|
577
|
+
schema.dictionary(:timestamps) { datetime }
|
578
|
+
expect(schema.fields).to eq(
|
579
|
+
{
|
580
|
+
timestamps: {
|
581
|
+
type: :dictionary,
|
582
|
+
description: nil,
|
583
|
+
value_type: {
|
584
|
+
type: :datetime,
|
585
|
+
description: nil
|
586
|
+
}
|
587
|
+
}
|
588
|
+
}
|
589
|
+
)
|
590
|
+
end
|
591
|
+
|
592
|
+
it "supports hashes as dictionary values" do
|
593
|
+
schema.dictionary(:users) do
|
594
|
+
hash do
|
595
|
+
string :name
|
596
|
+
integer :age
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
expect(schema.fields[:users]).to eq(
|
601
|
+
{
|
602
|
+
type: :dictionary,
|
603
|
+
description: nil,
|
604
|
+
value_type: {
|
605
|
+
type: :hash,
|
606
|
+
description: nil,
|
607
|
+
fields: {
|
608
|
+
name: {
|
609
|
+
type: :string,
|
610
|
+
description: nil
|
611
|
+
},
|
612
|
+
age: {
|
613
|
+
type: :integer,
|
614
|
+
description: nil
|
615
|
+
}
|
616
|
+
}
|
617
|
+
}
|
618
|
+
}
|
619
|
+
)
|
620
|
+
end
|
621
|
+
|
622
|
+
it "supports arrays as dictionary values" do
|
623
|
+
schema.dictionary(:categories) { array { string } }
|
624
|
+
|
625
|
+
expect(schema.fields[:categories]).to eq(
|
626
|
+
{
|
627
|
+
type: :dictionary,
|
628
|
+
description: nil,
|
629
|
+
value_type: {
|
630
|
+
type: :array,
|
631
|
+
description: nil,
|
632
|
+
element_type: {
|
633
|
+
type: :string,
|
634
|
+
description: nil
|
635
|
+
}
|
636
|
+
}
|
637
|
+
}
|
638
|
+
)
|
639
|
+
end
|
417
640
|
end
|
418
641
|
|
419
642
|
describe "primitive types" do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lluminary
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Doug Hughes
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-06-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -216,11 +216,13 @@ files:
|
|
216
216
|
- lib/lluminary/models/bedrock/amazon_nova_pro_v1.rb
|
217
217
|
- lib/lluminary/models/bedrock/anthropic_claude_instant_v1.rb
|
218
218
|
- lib/lluminary/models/bedrock/base.rb
|
219
|
+
- lib/lluminary/models/google/gemini_20_flash.rb
|
219
220
|
- lib/lluminary/models/openai/gpt35_turbo.rb
|
220
221
|
- lib/lluminary/provider_error.rb
|
221
222
|
- lib/lluminary/providers/anthropic.rb
|
222
223
|
- lib/lluminary/providers/base.rb
|
223
224
|
- lib/lluminary/providers/bedrock.rb
|
225
|
+
- lib/lluminary/providers/google.rb
|
224
226
|
- lib/lluminary/providers/openai.rb
|
225
227
|
- lib/lluminary/providers/test.rb
|
226
228
|
- lib/lluminary/result.rb
|
@@ -239,13 +241,16 @@ files:
|
|
239
241
|
- spec/examples/quote_task_spec.rb
|
240
242
|
- spec/examples/sentiment_analysis_spec.rb
|
241
243
|
- spec/examples/summarize_text_spec.rb
|
244
|
+
- spec/examples/text_emotion_analyzer_spec.rb
|
242
245
|
- spec/lluminary/config_spec.rb
|
243
246
|
- spec/lluminary/models/base_spec.rb
|
244
247
|
- spec/lluminary/models/bedrock/amazon_nova_pro_v1_spec.rb
|
245
248
|
- spec/lluminary/models/bedrock/anthropic_claude_instant_v1_spec.rb
|
249
|
+
- spec/lluminary/models/google/gemini_20_flash_spec.rb
|
246
250
|
- spec/lluminary/models/openai/gpt35_turbo_spec.rb
|
247
251
|
- spec/lluminary/providers/anthropic_spec.rb
|
248
252
|
- spec/lluminary/providers/bedrock_spec.rb
|
253
|
+
- spec/lluminary/providers/google_spec.rb
|
249
254
|
- spec/lluminary/providers/openai_spec.rb
|
250
255
|
- spec/lluminary/providers/test_spec.rb
|
251
256
|
- spec/lluminary/result_spec.rb
|