Class: LastLLM::Schema

Inherits:
Object
  • Object
show all
Defined in:
lib/last_llm/schema.rb

Overview

Schema utilities for structured data generation

Class Method Summary collapse

Class Method Details

.create(schema_def) ⇒ Dry::Schema::JSON

Create a dry-schema from a hash definition

Parameters:

  • schema_def (Hash)

    The schema definition in JSON Schema format

Returns:



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/last_llm/schema.rb', line 12

def self.create(schema_def)
  # Convert JSON Schema to dry-schema
  Dry::Schema.JSON do
    # Process properties
    if schema_def[:properties] || schema_def['properties']
      properties = schema_def[:properties] || schema_def['properties']
      required = schema_def[:required] || schema_def['required'] || []

      properties.each do |property_name, property_def|
        property_name = property_name.to_sym
        property_type = property_def[:type] || property_def['type']

        # Handle required properties
        if required.include?(property_name.to_s)
          # Handle different property types
          case property_type
          when 'string'
            required(property_name).filled(:string)
          when 'integer'
            required(property_name).filled(:integer)
          when 'number'
            required(property_name).filled(:float)
          when 'boolean'
            required(property_name).filled(:bool)
          when 'array'
            items_type = property_def.dig(:items, :type) || property_def.dig('items', 'type')
            case items_type
            when 'string'
              required(property_name).array(:string)
            when 'integer'
              required(property_name).array(:integer)
            when 'number'
              required(property_name).array(:float)
            when 'boolean'
              required(property_name).array(:bool)
            when 'object'
              # For complex nested objects, we'd need a more sophisticated approach
              # This is a simplified version
              required(property_name).array(:hash)
            else
              required(property_name).array
            end
          when 'object'
            # For nested objects, we can't recursively create a schema here
            # Instead, we'll just create a hash schema
            required(property_name).hash do
              # Add nested properties if available
              if property_def[:properties] || property_def['properties']
                nested_props = property_def[:properties] || property_def['properties']
                nested_required = property_def[:required] || property_def['required'] || []

                nested_props.each do |nested_name, nested_def|
                  nested_name = nested_name.to_sym
                  nested_type = nested_def[:type] || nested_def['type']

                  if nested_required.include?(nested_name.to_s)
                    case nested_type
                    when 'string'
                      required(nested_name).filled(:string)
                    when 'integer'
                      required(nested_name).filled(:integer)
                    when 'number'
                      required(nested_name).filled(:float)
                    when 'boolean'
                      required(nested_name).filled(:bool)
                    else
                      required(nested_name).filled
                    end
                  else
                    optional(nested_name)
                  end
                end
              end
            end
          else
            required(property_name).filled
          end
        else
          # Handle optional properties
          case property_type
          when 'string'
            optional(property_name).maybe(:string)
          when 'integer'
            optional(property_name).maybe(:integer)
          when 'number'
            optional(property_name).maybe(:float)
          when 'boolean'
            optional(property_name).maybe(:bool)
          when 'array'
            items_type = property_def.dig(:items, :type) || property_def.dig('items', 'type')
            case items_type
            when 'string'
              optional(property_name).maybe(:array, :string)
            when 'integer'
              optional(property_name).maybe(:array, :integer)
            when 'number'
              optional(property_name).maybe(:array, :float)
            when 'boolean'
              optional(property_name).maybe(:array, :bool)
            when 'object'
              optional(property_name).maybe(:array, :hash)
            else
              optional(property_name).maybe(:array)
            end
          when 'object'
            # For nested objects, we can't recursively create a schema here
            # Instead, we'll just create a hash schema
            optional(property_name).maybe(:hash) do
              # Add nested properties if available
              if property_def[:properties] || property_def['properties']
                nested_props = property_def[:properties] || property_def['properties']
                nested_required = property_def[:required] || property_def['required'] || []

                nested_props.each do |nested_name, nested_def|
                  nested_name = nested_name.to_sym
                  nested_type = nested_def[:type] || nested_def['type']

                  if nested_required.include?(nested_name.to_s)
                    case nested_type
                    when 'string'
                      required(nested_name).filled(:string)
                    when 'integer'
                      required(nested_name).filled(:integer)
                    when 'number'
                      required(nested_name).filled(:float)
                    when 'boolean'
                      required(nested_name).filled(:bool)
                    else
                      required(nested_name).filled
                    end
                  else
                    optional(nested_name)
                  end
                end
              end
            end
          else
            optional(property_name)
          end
        end
      end
    end
  end
end

.from_json_schema(json_schema) ⇒ Dry::Schema::JSON

Convert a JSON schema string to a dry-schema

Parameters:

  • json_schema (String)

    The JSON schema as a string

Returns:



160
161
162
163
# File 'lib/last_llm/schema.rb', line 160

def self.from_json_schema(json_schema)
  schema_def = JSON.parse(json_schema, symbolize_names: true)
  create(schema_def)
end

.to_json_schema(schema) ⇒ String

Convert a dry-schema to a JSON schema string

Parameters:

Returns:

  • (String)

    The JSON schema as a string



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/last_llm/schema.rb', line 168

def self.to_json_schema(schema)
  # If schema is already a Hash, assume it's a JSON schema
  return JSON.pretty_generate(schema) if schema.is_a?(Hash)

  # If schema has a json_schema method, use it
  return JSON.pretty_generate(schema.json_schema) if schema.respond_to?(:json_schema)

  # Otherwise, extract schema information from dry-schema
  json_schema = {
    type: 'object',
    properties: {},
    required: []
  }

  # Process each rule in the schema
  if schema.respond_to?(:rules) && schema.rules.respond_to?(:each_value)
    schema.rules.each_value do |rule|
      # Skip if rule is not a proper rule object
      next unless rule.respond_to?(:name)

      property_name = rule.name.to_s
      property_def = {}

      # Determine if the property is required
      json_schema[:required] << property_name if rule.is_a?(Dry::Schema::Rule::Required)

      # Determine the property type
      if rule.respond_to?(:type) && rule.type.is_a?(Dry::Types::Nominal)
        case rule.type.primitive
        when String
          property_def['type'] = 'string'
        when Integer
          property_def['type'] = 'integer'
        when Float
          property_def['type'] = 'number'
        when TrueClass, FalseClass
          property_def['type'] = 'boolean'
        when Array
          property_def['type'] = 'array'
          # Try to determine the item type
          property_def['items'] = if rule.type.respond_to?(:member) && rule.type.member.respond_to?(:primitive)
                                    case rule.type.member.primitive
                                    when String
                                      { 'type' => 'string' }
                                    when Integer
                                      { 'type' => 'integer' }
                                    when Float
                                      { 'type' => 'number' }
                                    when TrueClass, FalseClass
                                      { 'type' => 'boolean' }
                                    when Hash
                                      { 'type' => 'object' }
                                    else
                                      {}
                                    end
                                  else
                                    {}
                                  end
        when Hash
          property_def['type'] = 'object'
          # For nested objects, we'd need a more sophisticated approach
        else
          property_def['type'] = 'string' # Default to string
        end
      end

      json_schema[:properties][property_name] = property_def
    end
  end

  JSON.pretty_generate(json_schema)
end