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
@@ -1,109 +1,138 @@
1
- require 'spec_helper'
2
- require 'lluminary/providers/bedrock'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+ require "lluminary/providers/bedrock"
3
4
 
4
5
  RSpec.describe Lluminary::Providers::Bedrock do
5
6
  let(:config) do
6
7
  {
7
- region: 'us-east-1',
8
- access_key_id: 'test-key',
9
- secret_access_key: 'test-secret'
8
+ region: "us-east-1",
9
+ access_key_id: "test-key",
10
+ secret_access_key: "test-secret"
10
11
  }
11
12
  end
12
13
  let(:provider) { described_class.new(**config) }
13
14
 
14
- describe '#client' do
15
- it 'returns the AWS Bedrock client instance' do
15
+ describe "#client" do
16
+ it "returns the AWS Bedrock client instance" do
16
17
  expect(provider.client).to be_a(Aws::BedrockRuntime::Client)
17
18
  end
18
19
  end
19
20
 
20
- describe '#call' do
21
- let(:prompt) { 'Test prompt' }
22
- let(:task) { 'Test task' }
21
+ describe "#models" do
22
+ let(:mock_models_response) do
23
+ OpenStruct.new(
24
+ foundation_models: [
25
+ OpenStruct.new(
26
+ model_id: "anthropic.claude-instant-v1",
27
+ model_name: "Claude Instant",
28
+ provider_name: "Anthropic",
29
+ input_modalities: ["TEXT"],
30
+ output_modalities: ["TEXT"],
31
+ customizations_supported: []
32
+ ),
33
+ OpenStruct.new(
34
+ model_id: "anthropic.claude-v2",
35
+ model_name: "Claude V2",
36
+ provider_name: "Anthropic",
37
+ input_modalities: ["TEXT"],
38
+ output_modalities: ["TEXT"],
39
+ customizations_supported: []
40
+ )
41
+ ]
42
+ )
43
+ end
44
+
45
+ before do
46
+ models_client = double("BedrockClient")
47
+ allow(Aws::Bedrock::Client).to receive(:new).and_return(models_client)
48
+ allow(models_client).to receive(:list_foundation_models).and_return(
49
+ mock_models_response
50
+ )
51
+ end
52
+
53
+ it "returns an array of model IDs as strings" do
54
+ expect(provider.models).to eq(
55
+ %w[anthropic.claude-instant-v1 anthropic.claude-v2]
56
+ )
57
+ end
58
+ end
59
+
60
+ describe "#call" do
61
+ let(:prompt) { "Test prompt" }
62
+ let(:task) { "Test task" }
23
63
  let(:mock_response) do
24
64
  OpenStruct.new(
25
- output: OpenStruct.new(
26
- message: OpenStruct.new(
27
- content: [
28
- OpenStruct.new(text: '{"sentiment": "positive"}')
29
- ]
65
+ output:
66
+ OpenStruct.new(
67
+ message:
68
+ OpenStruct.new(
69
+ content: [OpenStruct.new(text: '{"sentiment": "positive"}')]
70
+ )
30
71
  )
31
- )
32
72
  )
33
73
  end
34
74
 
35
75
  before do
36
- allow_any_instance_of(Aws::BedrockRuntime::Client).to receive(:converse).and_return(mock_response)
76
+ allow_any_instance_of(Aws::BedrockRuntime::Client).to receive(
77
+ :converse
78
+ ).and_return(mock_response)
37
79
  end
38
80
 
39
- it 'returns a hash with raw and parsed response' do
81
+ it "returns a hash with raw and parsed response" do
40
82
  response = provider.call(prompt, task)
41
- expect(response).to eq({
42
- raw: '{"sentiment": "positive"}',
43
- parsed: { 'sentiment' => 'positive' }
44
- })
83
+ expect(response).to eq(
84
+ {
85
+ raw: '{"sentiment": "positive"}',
86
+ parsed: {
87
+ "sentiment" => "positive"
88
+ }
89
+ }
90
+ )
45
91
  end
46
92
 
47
- context 'when the response is not valid JSON' do
93
+ context "when the response is not valid JSON" do
48
94
  let(:mock_response) do
49
95
  OpenStruct.new(
50
- output: OpenStruct.new(
51
- message: OpenStruct.new(
52
- content: [
53
- OpenStruct.new(text: 'not valid json')
54
- ]
96
+ output:
97
+ OpenStruct.new(
98
+ message:
99
+ OpenStruct.new(
100
+ content: [OpenStruct.new(text: "not valid json")]
101
+ )
55
102
  )
56
- )
57
103
  )
58
104
  end
59
105
 
60
- it 'returns raw response with nil parsed value' do
106
+ it "returns raw response with nil parsed value" do
61
107
  response = provider.call(prompt, task)
62
- expect(response).to eq({
63
- raw: 'not valid json',
64
- parsed: nil
65
- })
108
+ expect(response).to eq({ raw: "not valid json", parsed: nil })
66
109
  end
67
110
  end
68
111
 
69
- context 'when the response content is nil' do
112
+ context "when the response content is nil" do
70
113
  let(:mock_response) do
71
114
  OpenStruct.new(
72
- output: OpenStruct.new(
73
- message: OpenStruct.new(
74
- content: nil
75
- )
76
- )
115
+ output: OpenStruct.new(message: OpenStruct.new(content: nil))
77
116
  )
78
117
  end
79
118
 
80
- it 'returns nil for both raw and parsed values' do
119
+ it "returns nil for both raw and parsed values" do
81
120
  response = provider.call(prompt, task)
82
- expect(response).to eq({
83
- raw: nil,
84
- parsed: nil
85
- })
121
+ expect(response).to eq({ raw: nil, parsed: nil })
86
122
  end
87
123
  end
88
124
 
89
- context 'when the response content array is empty' do
125
+ context "when the response content array is empty" do
90
126
  let(:mock_response) do
91
127
  OpenStruct.new(
92
- output: OpenStruct.new(
93
- message: OpenStruct.new(
94
- content: []
95
- )
96
- )
128
+ output: OpenStruct.new(message: OpenStruct.new(content: []))
97
129
  )
98
130
  end
99
131
 
100
- it 'returns nil for both raw and parsed values' do
132
+ it "returns nil for both raw and parsed values" do
101
133
  response = provider.call(prompt, task)
102
- expect(response).to eq({
103
- raw: nil,
104
- parsed: nil
105
- })
134
+ expect(response).to eq({ raw: nil, parsed: nil })
106
135
  end
107
136
  end
108
137
  end
109
- end
138
+ end
@@ -1,62 +1,86 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  RSpec.describe Lluminary::Providers::OpenAI do
4
- let(:config) { { api_key: 'test-key' } }
5
+ let(:config) { { api_key: "test-key" } }
5
6
  let(:provider) { described_class.new(**config) }
6
7
 
7
- describe '#client' do
8
- it 'returns the OpenAI client instance' do
8
+ describe "#client" do
9
+ it "returns the OpenAI client instance" do
9
10
  expect(provider.client).to be_a(OpenAI::Client)
10
11
  end
11
12
  end
12
13
 
13
- describe '#call' do
14
- let(:prompt) { 'Test prompt' }
15
- let(:task) { 'Test task' }
16
- let(:mock_response) do
14
+ describe "#models" do
15
+ let(:mock_models_response) do
17
16
  {
18
- 'choices' => [
17
+ "object" => "list",
18
+ "data" => [
19
+ {
20
+ "id" => "gpt-4",
21
+ "object" => "model",
22
+ "created" => 1_687_882_411,
23
+ "owned_by" => "openai"
24
+ },
19
25
  {
20
- 'message' => {
21
- 'content' => '{"summary": "Test response"}'
22
- }
26
+ "id" => "gpt-3.5-turbo",
27
+ "object" => "model",
28
+ "created" => 1_677_610_602,
29
+ "owned_by" => "openai"
23
30
  }
24
31
  ]
25
32
  }
26
33
  end
27
34
 
28
35
  before do
29
- allow_any_instance_of(OpenAI::Client).to receive(:chat).and_return(mock_response)
36
+ allow_any_instance_of(OpenAI::Client).to receive(:models).and_return(
37
+ double("ModelsClient", list: mock_models_response)
38
+ )
30
39
  end
31
40
 
32
- it 'returns a hash with raw and parsed response' do
33
- response = provider.call(prompt, task)
34
- expect(response).to eq({
35
- raw: '{"summary": "Test response"}',
36
- parsed: { 'summary' => 'Test response' }
37
- })
41
+ it "returns an array of model IDs as strings" do
42
+ expect(provider.models).to eq(%w[gpt-4 gpt-3.5-turbo])
38
43
  end
44
+ end
39
45
 
40
- context 'when the response is not valid JSON' do
41
- let(:mock_response) do
46
+ describe "#call" do
47
+ let(:prompt) { "Test prompt" }
48
+ let(:task) { "Test task" }
49
+ let(:mock_response) do
50
+ {
51
+ "choices" => [
52
+ { "message" => { "content" => '{"summary": "Test response"}' } }
53
+ ]
54
+ }
55
+ end
56
+
57
+ before do
58
+ allow_any_instance_of(OpenAI::Client).to receive(:chat).and_return(
59
+ mock_response
60
+ )
61
+ end
62
+
63
+ it "returns a hash with raw and parsed response" do
64
+ response = provider.call(prompt, task)
65
+ expect(response).to eq(
42
66
  {
43
- 'choices' => [
44
- {
45
- 'message' => {
46
- 'content' => 'not valid json'
47
- }
48
- }
49
- ]
67
+ raw: '{"summary": "Test response"}',
68
+ parsed: {
69
+ "summary" => "Test response"
70
+ }
50
71
  }
72
+ )
73
+ end
74
+
75
+ context "when the response is not valid JSON" do
76
+ let(:mock_response) do
77
+ { "choices" => [{ "message" => { "content" => "not valid json" } }] }
51
78
  end
52
79
 
53
- it 'returns raw response with nil parsed value' do
80
+ it "returns raw response with nil parsed value" do
54
81
  response = provider.call(prompt, task)
55
- expect(response).to eq({
56
- raw: 'not valid json',
57
- parsed: nil
58
- })
82
+ expect(response).to eq({ raw: "not valid json", parsed: nil })
59
83
  end
60
84
  end
61
85
  end
62
- end
86
+ end
@@ -1,14 +1,42 @@
1
- require 'spec_helper'
2
- require 'lluminary/providers/test'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+ require "lluminary/providers/test"
3
4
 
4
5
  RSpec.describe Lluminary::Providers::Test do
5
6
  let(:provider) { described_class.new }
6
7
  let(:prompt) { "Test prompt" }
7
- let(:task_class) { double("TaskClass", output_fields: { summary: { type: :string } }) }
8
+ let(:task_class) do
9
+ double("TaskClass", output_fields: { summary: { type: :string } })
10
+ end
8
11
  let(:task) { double("Task", class: task_class) }
9
12
 
10
- describe '#call' do
11
- it 'returns a hash with raw and parsed response' do
13
+ describe "#initialize" do
14
+ it "accepts configuration options" do
15
+ config = { api_key: "test_key", model: "test_model" }
16
+ provider = described_class.new(**config)
17
+ expect(provider.config).to eq(config)
18
+ end
19
+
20
+ it "merges default provider configuration with instance configuration" do
21
+ # Set up default provider configuration
22
+ Lluminary.configure do |config|
23
+ config.provider(:test, api_key: "global_key", model: "global_model")
24
+ end
25
+
26
+ # Create provider with instance-specific config
27
+ provider = described_class.new(model: "instance_model", temperature: 0.7)
28
+
29
+ # Should merge default and instance config, with instance taking precedence
30
+ expect(provider.config).to eq(
31
+ api_key: "global_key",
32
+ model: "instance_model",
33
+ temperature: 0.7
34
+ )
35
+ end
36
+ end
37
+
38
+ describe "#call" do
39
+ it "returns a hash with raw and parsed response" do
12
40
  response = provider.call(prompt, task)
13
41
 
14
42
  expect(response).to be_a(Hash)
@@ -16,7 +44,7 @@ RSpec.describe Lluminary::Providers::Test do
16
44
  expect(response[:parsed]).to eq({ "summary" => "Test string value" })
17
45
  end
18
46
 
19
- it 'handles prompts with schema descriptions' do
47
+ it "handles prompts with schema descriptions" do
20
48
  prompt_with_schema = <<~PROMPT
21
49
  Test prompt
22
50
 
@@ -36,22 +64,24 @@ RSpec.describe Lluminary::Providers::Test do
36
64
  expect(response[:parsed]).to eq({ "summary" => "Test string value" })
37
65
  end
38
66
 
39
- it 'generates integer values for integer fields' do
40
- task_class = double("TaskClass", output_fields: { count: { type: :integer } })
67
+ it "generates integer values for integer fields" do
68
+ task_class =
69
+ double("TaskClass", output_fields: { count: { type: :integer } })
41
70
  task = double("Task", class: task_class)
42
-
71
+
43
72
  response = provider.call(prompt, task)
44
73
  expect(response[:raw]).to eq('{"count": 0}')
45
74
  expect(response[:parsed]).to eq({ "count" => 0 })
46
75
  end
47
76
 
48
- it 'raises error for unsupported types' do
49
- task_class = double("TaskClass", output_fields: { value: { type: :unsupported } })
77
+ it "raises error for unsupported types" do
78
+ task_class =
79
+ double("TaskClass", output_fields: { value: { type: :unsupported } })
50
80
  task = double("Task", class: task_class)
51
-
52
- expect {
53
- provider.call(prompt, task)
54
- }.to raise_error("Unsupported type: unsupported")
81
+
82
+ expect { provider.call(prompt, task) }.to raise_error(
83
+ "Unsupported type: unsupported"
84
+ )
55
85
  end
56
86
  end
57
- end
87
+ end
@@ -1,31 +1,38 @@
1
- require 'lluminary'
1
+ # frozen_string_literal: true
2
+ require "lluminary"
2
3
 
3
4
  RSpec.describe Lluminary::Result do
4
5
  let(:raw_response) { "Test response" }
5
6
  let(:output) { { summary: "Test summary" } }
6
7
  let(:prompt) { "Test prompt" }
7
- let(:result) { described_class.new(raw_response: raw_response, output: output, prompt: prompt) }
8
+ let(:result) do
9
+ described_class.new(
10
+ raw_response: raw_response,
11
+ output: output,
12
+ prompt: prompt
13
+ )
14
+ end
8
15
 
9
- describe '#raw_response' do
10
- it 'returns the raw response' do
16
+ describe "#raw_response" do
17
+ it "returns the raw response" do
11
18
  expect(result.raw_response).to eq(raw_response)
12
19
  end
13
20
  end
14
21
 
15
- describe '#output' do
16
- it 'returns an OpenStruct with the output data' do
22
+ describe "#output" do
23
+ it "returns an OpenStruct with the output data" do
17
24
  expect(result.output).to be_a(OpenStruct)
18
25
  expect(result.output.summary).to eq(output[:summary])
19
26
  end
20
27
 
21
- it 'allows accessing output fields as methods' do
28
+ it "allows accessing output fields as methods" do
22
29
  expect(result.output.summary).to eq(output[:summary])
23
30
  end
24
31
  end
25
32
 
26
- describe '#prompt' do
27
- it 'returns the prompt' do
33
+ describe "#prompt" do
34
+ it "returns the prompt" do
28
35
  expect(result.prompt).to eq(prompt)
29
36
  end
30
37
  end
31
- end
38
+ end
@@ -1,11 +1,18 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  RSpec.describe Lluminary::SchemaModel do
4
- describe '.build' do
5
+ describe ".build" do
5
6
  let(:fields) do
6
7
  {
7
- name: { type: :string, description: "The user's name" },
8
- age: { type: :integer, description: "The user's age" }
8
+ name: {
9
+ type: :string,
10
+ description: "The user's name"
11
+ },
12
+ age: {
13
+ type: :integer,
14
+ description: "The user's age"
15
+ }
9
16
  }
10
17
  end
11
18
 
@@ -16,35 +23,39 @@ RSpec.describe Lluminary::SchemaModel do
16
23
  ]
17
24
  end
18
25
 
19
- let(:model_class) { described_class.build(fields: fields, validations: validations) }
26
+ let(:model_class) do
27
+ described_class.build(fields: fields, validations: validations)
28
+ end
20
29
 
21
- it 'creates a class that inherits from SchemaModel' do
30
+ it "creates a class that inherits from SchemaModel" do
22
31
  expect(model_class.ancestors).to include(described_class)
23
32
  end
24
33
 
25
- it 'includes ActiveModel::Validations' do
34
+ it "includes ActiveModel::Validations" do
26
35
  expect(model_class.ancestors).to include(ActiveModel::Validations)
27
36
  end
28
37
 
29
- it 'adds accessors for fields' do
38
+ it "adds accessors for fields" do
30
39
  instance = model_class.new(name: "John", age: 30)
31
40
  expect(instance.name).to eq("John")
32
41
  expect(instance.age).to eq(30)
33
42
  end
34
43
 
35
- it 'validates presence' do
44
+ it "validates presence" do
36
45
  instance = model_class.new(age: 30)
37
46
  expect(instance.valid?).to be false
38
47
  expect(instance.errors.full_messages).to include("Name can't be blank")
39
48
  end
40
49
 
41
- it 'validates numericality' do
50
+ it "validates numericality" do
42
51
  instance = model_class.new(name: "John", age: 0)
43
52
  expect(instance.valid?).to be false
44
- expect(instance.errors.full_messages).to include("Age must be greater than 0")
53
+ expect(instance.errors.full_messages).to include(
54
+ "Age must be greater than 0"
55
+ )
45
56
  end
46
57
 
47
- it 'validates types' do
58
+ it "validates types" do
48
59
  instance = model_class.new(name: 123, age: "30")
49
60
  expect(instance.valid?).to be false
50
61
  expect(instance.errors.full_messages).to include(
@@ -53,34 +64,109 @@ RSpec.describe Lluminary::SchemaModel do
53
64
  )
54
65
  end
55
66
 
56
- it 'validates float types' do
57
- fields = {
58
- price: { type: :float, description: "The price" }
59
- }
67
+ it "validates float types" do
68
+ fields = { price: { type: :float, description: "The price" } }
60
69
  model_class = described_class.build(fields: fields, validations: [])
61
-
70
+
62
71
  # Test that nil is allowed
63
72
  instance = model_class.new(price: nil)
64
73
  expect(instance.valid?).to be true
65
-
74
+
66
75
  # Test invalid float value
67
76
  instance = model_class.new(price: "not a float")
68
77
  expect(instance.valid?).to be false
69
78
  expect(instance.errors.full_messages).to include("Price must be a float")
70
-
79
+
71
80
  # Test valid float value
72
81
  instance = model_class.new(price: 12.34)
73
82
  expect(instance.valid?).to be true
74
83
  end
75
84
 
76
- it 'accepts valid attributes' do
85
+ it "accepts valid attributes" do
77
86
  instance = model_class.new(name: "John", age: 30)
78
87
  expect(instance.valid?).to be true
79
88
  end
80
89
 
81
- it 'provides access to raw attributes' do
90
+ it "provides access to raw attributes" do
82
91
  instance = model_class.new(name: "John", age: 30)
83
92
  expect(instance.attributes).to eq({ "name" => "John", "age" => 30 })
84
93
  end
94
+
95
+ it "validates array types" do
96
+ fields = {
97
+ items: {
98
+ type: :array,
99
+ element_type: {
100
+ type: :string,
101
+ description: nil
102
+ },
103
+ description: "A list of strings"
104
+ }
105
+ }
106
+ model_class = described_class.build(fields: fields, validations: [])
107
+
108
+ # Test that nil is allowed
109
+ instance = model_class.new(items: nil)
110
+ expect(instance.valid?).to be true
111
+
112
+ # Test valid array of strings
113
+ instance = model_class.new(items: %w[one two])
114
+ expect(instance.valid?).to be true
115
+
116
+ # Test invalid array (not an array)
117
+ instance = model_class.new(items: "not an array")
118
+ expect(instance.valid?).to be false
119
+ expect(instance.errors.full_messages).to include("Items must be an Array")
120
+
121
+ # Test invalid array elements
122
+ instance = model_class.new(items: ["one", 2, "three"])
123
+ expect(instance.valid?).to be false
124
+ expect(instance.errors.full_messages).to include(
125
+ "Items[1] must be a String"
126
+ )
127
+ end
128
+
129
+ it "validates nested array types" do
130
+ fields = {
131
+ matrix: {
132
+ type: :array,
133
+ element_type: {
134
+ type: :array,
135
+ element_type: {
136
+ type: :integer,
137
+ description: nil
138
+ },
139
+ description: nil
140
+ },
141
+ description: "A matrix of integers"
142
+ }
143
+ }
144
+ model_class = described_class.build(fields: fields, validations: [])
145
+
146
+ # Test valid nested arrays
147
+ instance = model_class.new(matrix: [[1, 2], [3, 4]])
148
+ expect(instance.valid?).to be true
149
+
150
+ # Test invalid outer array
151
+ instance = model_class.new(matrix: "not an array")
152
+ expect(instance.valid?).to be false
153
+ expect(instance.errors.full_messages).to include(
154
+ "Matrix must be an Array"
155
+ )
156
+
157
+ # Test invalid inner array
158
+ instance = model_class.new(matrix: ["not an array"])
159
+ expect(instance.valid?).to be false
160
+ expect(instance.errors.full_messages).to include(
161
+ "Matrix[0] must be an Array"
162
+ )
163
+
164
+ # Test invalid inner array elements
165
+ instance = model_class.new(matrix: [[1, "2"], [3, 4]])
166
+ expect(instance.valid?).to be false
167
+ expect(instance.errors.full_messages).to include(
168
+ "Matrix[0][1] must be an Integer"
169
+ )
170
+ end
85
171
  end
86
- end
172
+ end