lluminary 0.1.1 → 0.1.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/config.rb +11 -1
- data/lib/lluminary/field_description.rb +34 -29
- data/lib/lluminary/provider_error.rb +2 -1
- data/lib/lluminary/providers/base.rb +5 -1
- data/lib/lluminary/providers/bedrock.rb +32 -30
- data/lib/lluminary/providers/openai.rb +28 -21
- data/lib/lluminary/providers/test.rb +9 -14
- data/lib/lluminary/result.rb +5 -2
- data/lib/lluminary/schema.rb +9 -16
- data/lib/lluminary/schema_model.rb +13 -10
- data/lib/lluminary/task.rb +84 -59
- data/lib/lluminary/validation_error.rb +2 -1
- data/lib/lluminary/version.rb +3 -2
- data/lib/lluminary.rb +23 -7
- data/spec/examples/analyze_text_spec.rb +7 -4
- data/spec/examples/color_analyzer_spec.rb +22 -22
- data/spec/examples/content_analyzer_spec.rb +27 -44
- data/spec/examples/historical_event_analyzer_spec.rb +18 -15
- data/spec/examples/price_analyzer_spec.rb +22 -28
- data/spec/examples/quote_task_spec.rb +9 -8
- data/spec/examples/sentiment_analysis_spec.rb +13 -10
- data/spec/examples/summarize_text_spec.rb +7 -4
- data/spec/lluminary/config_spec.rb +28 -26
- data/spec/lluminary/field_description_spec.rb +19 -21
- data/spec/lluminary/providers/base_spec.rb +11 -8
- data/spec/lluminary/providers/bedrock_spec.rb +47 -57
- data/spec/lluminary/providers/openai_spec.rb +27 -35
- data/spec/lluminary/providers/test_spec.rb +21 -16
- data/spec/lluminary/result_spec.rb +17 -10
- data/spec/lluminary/schema_model_spec.rb +31 -22
- data/spec/lluminary/schema_spec.rb +95 -107
- data/spec/lluminary/task_spec.rb +366 -300
- data/spec/spec_helper.rb +7 -2
- metadata +35 -19
| @@ -1,8 +1,11 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "spec_helper"
         | 
| 3 | 
            +
            require_relative "../../examples/summarize_text"
         | 
| 3 4 |  | 
| 4 5 | 
             
            RSpec.describe SummarizeText do
         | 
| 5 | 
            -
              let(:text) {  | 
| 6 | 
            +
              let(:text) { <<~TEXT }
         | 
| 7 | 
            +
                  Ruby is a dynamic, open source programming language with a focus on simplicity and productivity. It has an elegant syntax that is natural to read and easy to write.
         | 
| 8 | 
            +
                TEXT
         | 
| 6 9 |  | 
| 7 10 | 
             
              it "summarizes text" do
         | 
| 8 11 | 
             
                result = described_class.call(text: text)
         | 
| @@ -18,4 +21,4 @@ RSpec.describe SummarizeText do | |
| 18 21 | 
             
                expect(json).to have_key("summary")
         | 
| 19 22 | 
             
                expect(json["summary"]).to eq(result.output.summary)
         | 
| 20 23 | 
             
              end
         | 
| 21 | 
            -
            end | 
| 24 | 
            +
            end
         | 
| @@ -1,47 +1,49 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "spec_helper"
         | 
| 2 3 |  | 
| 3 4 | 
             
            RSpec.describe Lluminary::Config do
         | 
| 4 5 | 
             
              let(:config) { described_class.new }
         | 
| 5 6 |  | 
| 6 | 
            -
              describe  | 
| 7 | 
            -
                it  | 
| 7 | 
            +
              describe "#configure" do
         | 
| 8 | 
            +
                it "allows setting provider configurations" do
         | 
| 8 9 | 
             
                  config.configure do |c|
         | 
| 9 | 
            -
                    c.provider(:openai, api_key:  | 
| 10 | 
            -
                    c.provider( | 
| 11 | 
            -
                       | 
| 12 | 
            -
                       | 
| 13 | 
            -
                       | 
| 10 | 
            +
                    c.provider(:openai, api_key: "global-key")
         | 
| 11 | 
            +
                    c.provider(
         | 
| 12 | 
            +
                      :bedrock,
         | 
| 13 | 
            +
                      access_key_id: "global-access-key",
         | 
| 14 | 
            +
                      secret_access_key: "global-secret-key",
         | 
| 15 | 
            +
                      region: "us-east-1"
         | 
| 14 16 | 
             
                    )
         | 
| 15 17 | 
             
                  end
         | 
| 16 18 |  | 
| 17 | 
            -
                  expect(config.provider_config(:openai)).to eq({ api_key:  | 
| 18 | 
            -
                  expect(config.provider_config(:bedrock)).to eq( | 
| 19 | 
            -
                     | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 19 | 
            +
                  expect(config.provider_config(:openai)).to eq({ api_key: "global-key" })
         | 
| 20 | 
            +
                  expect(config.provider_config(:bedrock)).to eq(
         | 
| 21 | 
            +
                    {
         | 
| 22 | 
            +
                      access_key_id: "global-access-key",
         | 
| 23 | 
            +
                      secret_access_key: "global-secret-key",
         | 
| 24 | 
            +
                      region: "us-east-1"
         | 
| 25 | 
            +
                    }
         | 
| 26 | 
            +
                  )
         | 
| 23 27 | 
             
                end
         | 
| 24 28 | 
             
              end
         | 
| 25 29 |  | 
| 26 | 
            -
              describe  | 
| 27 | 
            -
                it  | 
| 30 | 
            +
              describe "#provider_config" do
         | 
| 31 | 
            +
                it "returns empty hash for unconfigured providers" do
         | 
| 28 32 | 
             
                  expect(config.provider_config(:unknown)).to eq({})
         | 
| 29 33 | 
             
                end
         | 
| 30 34 |  | 
| 31 | 
            -
                it  | 
| 32 | 
            -
                  config.configure  | 
| 33 | 
            -
                    c.provider(:openai, api_key: 'test-key')
         | 
| 34 | 
            -
                  end
         | 
| 35 | 
            +
                it "returns the configuration for a configured provider" do
         | 
| 36 | 
            +
                  config.configure { |c| c.provider(:openai, api_key: "test-key") }
         | 
| 35 37 |  | 
| 36 | 
            -
                  expect(config.provider_config(:openai)).to eq({ api_key:  | 
| 38 | 
            +
                  expect(config.provider_config(:openai)).to eq({ api_key: "test-key" })
         | 
| 37 39 | 
             
                end
         | 
| 38 40 | 
             
              end
         | 
| 39 41 |  | 
| 40 | 
            -
              describe  | 
| 41 | 
            -
                it  | 
| 42 | 
            +
              describe "#reset!" do
         | 
| 43 | 
            +
                it "clears all provider configurations" do
         | 
| 42 44 | 
             
                  config.configure do |c|
         | 
| 43 | 
            -
                    c.provider(:openai, api_key:  | 
| 44 | 
            -
                    c.provider(:bedrock, access_key_id:  | 
| 45 | 
            +
                    c.provider(:openai, api_key: "test-key")
         | 
| 46 | 
            +
                    c.provider(:bedrock, access_key_id: "test-access-key")
         | 
| 45 47 | 
             
                  end
         | 
| 46 48 |  | 
| 47 49 | 
             
                  config.reset!
         | 
| @@ -50,4 +52,4 @@ RSpec.describe Lluminary::Config do | |
| 50 52 | 
             
                  expect(config.provider_config(:bedrock)).to eq({})
         | 
| 51 53 | 
             
                end
         | 
| 52 54 | 
             
              end
         | 
| 53 | 
            -
            end | 
| 55 | 
            +
            end
         | 
| @@ -1,36 +1,34 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "spec_helper"
         | 
| 2 3 |  | 
| 3 4 | 
             
            RSpec.describe Lluminary::FieldDescription do
         | 
| 4 | 
            -
              describe  | 
| 5 | 
            -
                it  | 
| 6 | 
            -
                  field = {
         | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
                  }
         | 
| 10 | 
            -
                  description = described_class.new('test_field', field)
         | 
| 11 | 
            -
                  expect(description.to_s).to eq('test_field (string): A test field')
         | 
| 5 | 
            +
              describe "#to_s" do
         | 
| 6 | 
            +
                it "generates a description for a string field" do
         | 
| 7 | 
            +
                  field = { type: :string, description: "A test field" }
         | 
| 8 | 
            +
                  description = described_class.new("test_field", field)
         | 
| 9 | 
            +
                  expect(description.to_s).to eq("test_field (string): A test field")
         | 
| 12 10 | 
             
                end
         | 
| 13 11 |  | 
| 14 | 
            -
                it  | 
| 15 | 
            -
                  field = {
         | 
| 16 | 
            -
             | 
| 17 | 
            -
                   | 
| 18 | 
            -
                  description = described_class.new('count', field)
         | 
| 19 | 
            -
                  expect(description.to_s).to eq('count (integer)')
         | 
| 12 | 
            +
                it "generates a description for a field without a description" do
         | 
| 13 | 
            +
                  field = { type: :integer }
         | 
| 14 | 
            +
                  description = described_class.new("count", field)
         | 
| 15 | 
            +
                  expect(description.to_s).to eq("count (integer)")
         | 
| 20 16 | 
             
                end
         | 
| 21 17 |  | 
| 22 | 
            -
                it  | 
| 18 | 
            +
                it "includes validation descriptions when present" do
         | 
| 23 19 | 
             
                  field = {
         | 
| 24 20 | 
             
                    type: :string,
         | 
| 25 | 
            -
                    description:  | 
| 21 | 
            +
                    description: "A test field",
         | 
| 26 22 | 
             
                    validations: [
         | 
| 27 23 | 
             
                      [{}, { length: { minimum: 5, maximum: 10 } }],
         | 
| 28 | 
            -
                      [{}, { format: { with:  | 
| 24 | 
            +
                      [{}, { format: { with: "/^[A-Z]+$/" } }]
         | 
| 29 25 | 
             
                    ]
         | 
| 30 26 | 
             
                  }
         | 
| 31 | 
            -
                  description = described_class.new( | 
| 32 | 
            -
                  expected =  | 
| 27 | 
            +
                  description = described_class.new("test_field", field)
         | 
| 28 | 
            +
                  expected = <<~DESCRIPTION.chomp
         | 
| 29 | 
            +
                      test_field (string): A test field (must be at least 5 characters, must be at most 10 characters, must match format: /^[A-Z]+$/)
         | 
| 30 | 
            +
                    DESCRIPTION
         | 
| 33 31 | 
             
                  expect(description.to_s).to eq(expected)
         | 
| 34 32 | 
             
                end
         | 
| 35 33 | 
             
              end
         | 
| 36 | 
            -
            end | 
| 34 | 
            +
            end
         | 
| @@ -1,17 +1,20 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "lluminary"
         | 
| 2 3 |  | 
| 3 4 | 
             
            RSpec.describe Lluminary::Providers::Base do
         | 
| 4 | 
            -
              describe  | 
| 5 | 
            -
                it  | 
| 6 | 
            -
                  config = { api_key:  | 
| 5 | 
            +
              describe "#initialize" do
         | 
| 6 | 
            +
                it "accepts configuration options" do
         | 
| 7 | 
            +
                  config = { api_key: "test_key", model: "test_model" }
         | 
| 7 8 | 
             
                  provider = described_class.new(**config)
         | 
| 8 9 | 
             
                  expect(provider.config).to eq(config)
         | 
| 9 10 | 
             
                end
         | 
| 10 11 | 
             
              end
         | 
| 11 12 |  | 
| 12 | 
            -
              describe  | 
| 13 | 
            -
                it  | 
| 14 | 
            -
                  expect  | 
| 13 | 
            +
              describe "#call" do
         | 
| 14 | 
            +
                it "raises NotImplementedError" do
         | 
| 15 | 
            +
                  expect do
         | 
| 16 | 
            +
                    described_class.new.call("test", double("Task"))
         | 
| 17 | 
            +
                  end.to raise_error(NotImplementedError)
         | 
| 15 18 | 
             
                end
         | 
| 16 19 | 
             
              end
         | 
| 17 | 
            -
            end | 
| 20 | 
            +
            end
         | 
| @@ -1,109 +1,99 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
            require  | 
| 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:  | 
| 8 | 
            -
                  access_key_id:  | 
| 9 | 
            -
                  secret_access_key:  | 
| 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  | 
| 15 | 
            -
                it  | 
| 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  | 
| 21 | 
            -
                let(:prompt) {  | 
| 22 | 
            -
                let(:task) {  | 
| 21 | 
            +
              describe "#call" do
         | 
| 22 | 
            +
                let(:prompt) { "Test prompt" }
         | 
| 23 | 
            +
                let(:task) { "Test task" }
         | 
| 23 24 | 
             
                let(:mock_response) do
         | 
| 24 25 | 
             
                  OpenStruct.new(
         | 
| 25 | 
            -
                    output: | 
| 26 | 
            -
                       | 
| 27 | 
            -
                         | 
| 28 | 
            -
                          OpenStruct.new( | 
| 29 | 
            -
             | 
| 26 | 
            +
                    output:
         | 
| 27 | 
            +
                      OpenStruct.new(
         | 
| 28 | 
            +
                        message:
         | 
| 29 | 
            +
                          OpenStruct.new(
         | 
| 30 | 
            +
                            content: [OpenStruct.new(text: '{"sentiment": "positive"}')]
         | 
| 31 | 
            +
                          )
         | 
| 30 32 | 
             
                      )
         | 
| 31 | 
            -
                    )
         | 
| 32 33 | 
             
                  )
         | 
| 33 34 | 
             
                end
         | 
| 34 35 |  | 
| 35 36 | 
             
                before do
         | 
| 36 | 
            -
                  allow_any_instance_of(Aws::BedrockRuntime::Client).to receive( | 
| 37 | 
            +
                  allow_any_instance_of(Aws::BedrockRuntime::Client).to receive(
         | 
| 38 | 
            +
                    :converse
         | 
| 39 | 
            +
                  ).and_return(mock_response)
         | 
| 37 40 | 
             
                end
         | 
| 38 41 |  | 
| 39 | 
            -
                it  | 
| 42 | 
            +
                it "returns a hash with raw and parsed response" do
         | 
| 40 43 | 
             
                  response = provider.call(prompt, task)
         | 
| 41 | 
            -
                  expect(response).to eq( | 
| 42 | 
            -
                     | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 44 | 
            +
                  expect(response).to eq(
         | 
| 45 | 
            +
                    {
         | 
| 46 | 
            +
                      raw: '{"sentiment": "positive"}',
         | 
| 47 | 
            +
                      parsed: {
         | 
| 48 | 
            +
                        "sentiment" => "positive"
         | 
| 49 | 
            +
                      }
         | 
| 50 | 
            +
                    }
         | 
| 51 | 
            +
                  )
         | 
| 45 52 | 
             
                end
         | 
| 46 53 |  | 
| 47 | 
            -
                context  | 
| 54 | 
            +
                context "when the response is not valid JSON" do
         | 
| 48 55 | 
             
                  let(:mock_response) do
         | 
| 49 56 | 
             
                    OpenStruct.new(
         | 
| 50 | 
            -
                      output: | 
| 51 | 
            -
                         | 
| 52 | 
            -
                           | 
| 53 | 
            -
                            OpenStruct.new( | 
| 54 | 
            -
             | 
| 57 | 
            +
                      output:
         | 
| 58 | 
            +
                        OpenStruct.new(
         | 
| 59 | 
            +
                          message:
         | 
| 60 | 
            +
                            OpenStruct.new(
         | 
| 61 | 
            +
                              content: [OpenStruct.new(text: "not valid json")]
         | 
| 62 | 
            +
                            )
         | 
| 55 63 | 
             
                        )
         | 
| 56 | 
            -
                      )
         | 
| 57 64 | 
             
                    )
         | 
| 58 65 | 
             
                  end
         | 
| 59 66 |  | 
| 60 | 
            -
                  it  | 
| 67 | 
            +
                  it "returns raw response with nil parsed value" do
         | 
| 61 68 | 
             
                    response = provider.call(prompt, task)
         | 
| 62 | 
            -
                    expect(response).to eq({
         | 
| 63 | 
            -
                      raw: 'not valid json',
         | 
| 64 | 
            -
                      parsed: nil
         | 
| 65 | 
            -
                    })
         | 
| 69 | 
            +
                    expect(response).to eq({ raw: "not valid json", parsed: nil })
         | 
| 66 70 | 
             
                  end
         | 
| 67 71 | 
             
                end
         | 
| 68 72 |  | 
| 69 | 
            -
                context  | 
| 73 | 
            +
                context "when the response content is nil" do
         | 
| 70 74 | 
             
                  let(:mock_response) do
         | 
| 71 75 | 
             
                    OpenStruct.new(
         | 
| 72 | 
            -
                      output: OpenStruct.new(
         | 
| 73 | 
            -
                        message: OpenStruct.new(
         | 
| 74 | 
            -
                          content: nil
         | 
| 75 | 
            -
                        )
         | 
| 76 | 
            -
                      )
         | 
| 76 | 
            +
                      output: OpenStruct.new(message: OpenStruct.new(content: nil))
         | 
| 77 77 | 
             
                    )
         | 
| 78 78 | 
             
                  end
         | 
| 79 79 |  | 
| 80 | 
            -
                  it  | 
| 80 | 
            +
                  it "returns nil for both raw and parsed values" do
         | 
| 81 81 | 
             
                    response = provider.call(prompt, task)
         | 
| 82 | 
            -
                    expect(response).to eq({
         | 
| 83 | 
            -
                      raw: nil,
         | 
| 84 | 
            -
                      parsed: nil
         | 
| 85 | 
            -
                    })
         | 
| 82 | 
            +
                    expect(response).to eq({ raw: nil, parsed: nil })
         | 
| 86 83 | 
             
                  end
         | 
| 87 84 | 
             
                end
         | 
| 88 85 |  | 
| 89 | 
            -
                context  | 
| 86 | 
            +
                context "when the response content array is empty" do
         | 
| 90 87 | 
             
                  let(:mock_response) do
         | 
| 91 88 | 
             
                    OpenStruct.new(
         | 
| 92 | 
            -
                      output: OpenStruct.new(
         | 
| 93 | 
            -
                        message: OpenStruct.new(
         | 
| 94 | 
            -
                          content: []
         | 
| 95 | 
            -
                        )
         | 
| 96 | 
            -
                      )
         | 
| 89 | 
            +
                      output: OpenStruct.new(message: OpenStruct.new(content: []))
         | 
| 97 90 | 
             
                    )
         | 
| 98 91 | 
             
                  end
         | 
| 99 92 |  | 
| 100 | 
            -
                  it  | 
| 93 | 
            +
                  it "returns nil for both raw and parsed values" do
         | 
| 101 94 | 
             
                    response = provider.call(prompt, task)
         | 
| 102 | 
            -
                    expect(response).to eq({
         | 
| 103 | 
            -
                      raw: nil,
         | 
| 104 | 
            -
                      parsed: nil
         | 
| 105 | 
            -
                    })
         | 
| 95 | 
            +
                    expect(response).to eq({ raw: nil, parsed: nil })
         | 
| 106 96 | 
             
                  end
         | 
| 107 97 | 
             
                end
         | 
| 108 98 | 
             
              end
         | 
| 109 | 
            -
            end | 
| 99 | 
            +
            end
         | 
| @@ -1,62 +1,54 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "spec_helper"
         | 
| 2 3 |  | 
| 3 4 | 
             
            RSpec.describe Lluminary::Providers::OpenAI do
         | 
| 4 | 
            -
              let(:config) { { api_key:  | 
| 5 | 
            +
              let(:config) { { api_key: "test-key" } }
         | 
| 5 6 | 
             
              let(:provider) { described_class.new(**config) }
         | 
| 6 7 |  | 
| 7 | 
            -
              describe  | 
| 8 | 
            -
                it  | 
| 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  | 
| 14 | 
            -
                let(:prompt) {  | 
| 15 | 
            -
                let(:task) {  | 
| 14 | 
            +
              describe "#call" do
         | 
| 15 | 
            +
                let(:prompt) { "Test prompt" }
         | 
| 16 | 
            +
                let(:task) { "Test task" }
         | 
| 16 17 | 
             
                let(:mock_response) do
         | 
| 17 18 | 
             
                  {
         | 
| 18 | 
            -
                     | 
| 19 | 
            -
                      {
         | 
| 20 | 
            -
                        'message' => {
         | 
| 21 | 
            -
                          'content' => '{"summary": "Test response"}'
         | 
| 22 | 
            -
                        }
         | 
| 23 | 
            -
                      }
         | 
| 19 | 
            +
                    "choices" => [
         | 
| 20 | 
            +
                      { "message" => { "content" => '{"summary": "Test response"}' } }
         | 
| 24 21 | 
             
                    ]
         | 
| 25 22 | 
             
                  }
         | 
| 26 23 | 
             
                end
         | 
| 27 24 |  | 
| 28 25 | 
             
                before do
         | 
| 29 | 
            -
                  allow_any_instance_of(OpenAI::Client).to receive(:chat).and_return( | 
| 26 | 
            +
                  allow_any_instance_of(OpenAI::Client).to receive(:chat).and_return(
         | 
| 27 | 
            +
                    mock_response
         | 
| 28 | 
            +
                  )
         | 
| 30 29 | 
             
                end
         | 
| 31 30 |  | 
| 32 | 
            -
                it  | 
| 31 | 
            +
                it "returns a hash with raw and parsed response" do
         | 
| 33 32 | 
             
                  response = provider.call(prompt, task)
         | 
| 34 | 
            -
                  expect(response).to eq( | 
| 35 | 
            -
                     | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 33 | 
            +
                  expect(response).to eq(
         | 
| 34 | 
            +
                    {
         | 
| 35 | 
            +
                      raw: '{"summary": "Test response"}',
         | 
| 36 | 
            +
                      parsed: {
         | 
| 37 | 
            +
                        "summary" => "Test response"
         | 
| 38 | 
            +
                      }
         | 
| 39 | 
            +
                    }
         | 
| 40 | 
            +
                  )
         | 
| 38 41 | 
             
                end
         | 
| 39 42 |  | 
| 40 | 
            -
                context  | 
| 43 | 
            +
                context "when the response is not valid JSON" do
         | 
| 41 44 | 
             
                  let(:mock_response) do
         | 
| 42 | 
            -
                    {
         | 
| 43 | 
            -
                      'choices' => [
         | 
| 44 | 
            -
                        {
         | 
| 45 | 
            -
                          'message' => {
         | 
| 46 | 
            -
                            'content' => 'not valid json'
         | 
| 47 | 
            -
                          }
         | 
| 48 | 
            -
                        }
         | 
| 49 | 
            -
                      ]
         | 
| 50 | 
            -
                    }
         | 
| 45 | 
            +
                    { "choices" => [{ "message" => { "content" => "not valid json" } }] }
         | 
| 51 46 | 
             
                  end
         | 
| 52 47 |  | 
| 53 | 
            -
                  it  | 
| 48 | 
            +
                  it "returns raw response with nil parsed value" do
         | 
| 54 49 | 
             
                    response = provider.call(prompt, task)
         | 
| 55 | 
            -
                    expect(response).to eq({
         | 
| 56 | 
            -
                      raw: 'not valid json',
         | 
| 57 | 
            -
                      parsed: nil
         | 
| 58 | 
            -
                    })
         | 
| 50 | 
            +
                    expect(response).to eq({ raw: "not valid json", parsed: nil })
         | 
| 59 51 | 
             
                  end
         | 
| 60 52 | 
             
                end
         | 
| 61 53 | 
             
              end
         | 
| 62 | 
            -
            end | 
| 54 | 
            +
            end
         | 
| @@ -1,14 +1,17 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
            require  | 
| 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)  | 
| 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  | 
| 11 | 
            -
                it  | 
| 13 | 
            +
              describe "#call" do
         | 
| 14 | 
            +
                it "returns a hash with raw and parsed response" do
         | 
| 12 15 | 
             
                  response = provider.call(prompt, task)
         | 
| 13 16 |  | 
| 14 17 | 
             
                  expect(response).to be_a(Hash)
         | 
| @@ -16,7 +19,7 @@ RSpec.describe Lluminary::Providers::Test do | |
| 16 19 | 
             
                  expect(response[:parsed]).to eq({ "summary" => "Test string value" })
         | 
| 17 20 | 
             
                end
         | 
| 18 21 |  | 
| 19 | 
            -
                it  | 
| 22 | 
            +
                it "handles prompts with schema descriptions" do
         | 
| 20 23 | 
             
                  prompt_with_schema = <<~PROMPT
         | 
| 21 24 | 
             
                    Test prompt
         | 
| 22 25 |  | 
| @@ -36,22 +39,24 @@ RSpec.describe Lluminary::Providers::Test do | |
| 36 39 | 
             
                  expect(response[:parsed]).to eq({ "summary" => "Test string value" })
         | 
| 37 40 | 
             
                end
         | 
| 38 41 |  | 
| 39 | 
            -
                it  | 
| 40 | 
            -
                  task_class = | 
| 42 | 
            +
                it "generates integer values for integer fields" do
         | 
| 43 | 
            +
                  task_class =
         | 
| 44 | 
            +
                    double("TaskClass", output_fields: { count: { type: :integer } })
         | 
| 41 45 | 
             
                  task = double("Task", class: task_class)
         | 
| 42 | 
            -
             | 
| 46 | 
            +
             | 
| 43 47 | 
             
                  response = provider.call(prompt, task)
         | 
| 44 48 | 
             
                  expect(response[:raw]).to eq('{"count": 0}')
         | 
| 45 49 | 
             
                  expect(response[:parsed]).to eq({ "count" => 0 })
         | 
| 46 50 | 
             
                end
         | 
| 47 51 |  | 
| 48 | 
            -
                it  | 
| 49 | 
            -
                  task_class = | 
| 52 | 
            +
                it "raises error for unsupported types" do
         | 
| 53 | 
            +
                  task_class =
         | 
| 54 | 
            +
                    double("TaskClass", output_fields: { value: { type: :unsupported } })
         | 
| 50 55 | 
             
                  task = double("Task", class: task_class)
         | 
| 51 | 
            -
             | 
| 52 | 
            -
                  expect {
         | 
| 53 | 
            -
                     | 
| 54 | 
            -
                   | 
| 56 | 
            +
             | 
| 57 | 
            +
                  expect { provider.call(prompt, task) }.to raise_error(
         | 
| 58 | 
            +
                    "Unsupported type: unsupported"
         | 
| 59 | 
            +
                  )
         | 
| 55 60 | 
             
                end
         | 
| 56 61 | 
             
              end
         | 
| 57 | 
            -
            end | 
| 62 | 
            +
            end
         | 
| @@ -1,31 +1,38 @@ | |
| 1 | 
            -
             | 
| 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)  | 
| 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  | 
| 10 | 
            -
                it  | 
| 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  | 
| 16 | 
            -
                it  | 
| 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  | 
| 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  | 
| 27 | 
            -
                it  | 
| 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 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "spec_helper"
         | 
| 2 3 |  | 
| 3 4 | 
             
            RSpec.describe Lluminary::SchemaModel do
         | 
| 4 | 
            -
              describe  | 
| 5 | 
            +
              describe ".build" do
         | 
| 5 6 | 
             
                let(:fields) do
         | 
| 6 7 | 
             
                  {
         | 
| 7 | 
            -
                    name: { | 
| 8 | 
            -
             | 
| 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)  | 
| 26 | 
            +
                let(:model_class) do
         | 
| 27 | 
            +
                  described_class.build(fields: fields, validations: validations)
         | 
| 28 | 
            +
                end
         | 
| 20 29 |  | 
| 21 | 
            -
                it  | 
| 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  | 
| 34 | 
            +
                it "includes ActiveModel::Validations" do
         | 
| 26 35 | 
             
                  expect(model_class.ancestors).to include(ActiveModel::Validations)
         | 
| 27 36 | 
             
                end
         | 
| 28 37 |  | 
| 29 | 
            -
                it  | 
| 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  | 
| 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  | 
| 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( | 
| 53 | 
            +
                  expect(instance.errors.full_messages).to include(
         | 
| 54 | 
            +
                    "Age must be greater than 0"
         | 
| 55 | 
            +
                  )
         | 
| 45 56 | 
             
                end
         | 
| 46 57 |  | 
| 47 | 
            -
                it  | 
| 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,32 @@ RSpec.describe Lluminary::SchemaModel do | |
| 53 64 | 
             
                  )
         | 
| 54 65 | 
             
                end
         | 
| 55 66 |  | 
| 56 | 
            -
                it  | 
| 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  | 
| 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  | 
| 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
         | 
| 85 94 | 
             
              end
         | 
| 86 | 
            -
            end | 
| 95 | 
            +
            end
         |