Skip to content

Implement Typed Predictors #15

@obie

Description

@obie

Overview

Implement typed predictors that provide type-safe field handling with automatic validation and parsing, similar to Pydantic in Python.

Description

Typed predictors ensure type safety at module boundaries by automatically validating and coercing inputs/outputs according to specified types. This reduces runtime errors and improves developer experience with better IDE support.

Key Features to Implement

  • Type definitions for signature fields
  • Automatic validation on input/output
  • Type coercion with clear rules
  • Custom type validators
  • IDE-friendly type annotations
  • Integration with existing Field system

Implementation Requirements

1. Type System Enhancement

module Desiru
  module Types
    class TypedField < Field
      attr_reader :type_class, :validator
      
      def initialize(name:, type:, description: nil, optional: false, validator: nil)
        super(name: name, type: type, description: description, optional: optional)
        @type_class = resolve_type_class(type)
        @validator = validator
      end
      
      def validate_and_coerce(value)
        # Skip nil for optional fields
        return nil if value.nil? && optional
        
        # Type coercion
        coerced = coerce_value(value)
        
        # Type validation
        unless valid_type?(coerced)
          raise ValidationError, "Expected #{type}, got #{coerced.class}"
        end
        
        # Custom validation
        if @validator && \!@validator.call(coerced)
          raise ValidationError, "Custom validation failed for #{name}"
        end
        
        coerced
      end
      
      private
      
      def coerce_value(value)
        case @type
        when :integer
          Integer(value) rescue raise ValidationError, "Cannot convert to integer"
        when :float
          Float(value) rescue raise ValidationError, "Cannot convert to float"
        when :boolean
          to_boolean(value)
        when :array
          Array(value)
        when :hash
          value.to_h rescue raise ValidationError, "Cannot convert to hash"
        else
          value
        end
      end
    end
  end
end

2. Typed Predictor Module

module Desiru
  class TypedPredictor < Module
    def initialize(signature:, **options)
      super
      setup_typed_fields
    end
    
    def forward(**inputs)
      # Validate and coerce inputs
      validated_inputs = validate_inputs(inputs)
      
      # Execute prediction
      result = super(**validated_inputs)
      
      # Validate and coerce outputs
      validate_outputs(result)
    end
    
    private
    
    def setup_typed_fields
      @typed_input_fields = {}
      @typed_output_fields = {}
      
      @signature.input_fields.each do |name, field|
        @typed_input_fields[name] = Types::TypedField.new(
          name: name,
          type: field.type,
          description: field.description,
          optional: field.optional
        )
      end
      
      @signature.output_fields.each do |name, field|
        @typed_output_fields[name] = Types::TypedField.new(
          name: name,
          type: field.type,
          description: field.description,
          optional: field.optional
        )
      end
    end
    
    def validate_inputs(inputs)
      validated = {}
      
      @typed_input_fields.each do |name, field|
        value = inputs[name] || inputs[name.to_s]
        validated[name] = field.validate_and_coerce(value)
      end
      
      validated
    end
  end
end

3. Advanced Type Definitions

# Support for complex types
module Desiru::Types
  # Enum type
  class Enum < TypedField
    def initialize(name:, values:, **options)
      super(name: name, type: :enum, **options)
      @allowed_values = values
    end
    
    def validate_and_coerce(value)
      unless @allowed_values.include?(value)
        raise ValidationError, "Value must be one of: #{@allowed_values.join(', ')}"
      end
      value
    end
  end
  
  # Structured type
  class Structured < TypedField
    def initialize(name:, schema:, **options)
      super(name: name, type: :structured, **options)
      @schema = schema
    end
    
    def validate_and_coerce(value)
      validate_against_schema(value, @schema)
    end
  end
  
  # List with element type
  class TypedList < TypedField
    def initialize(name:, element_type:, **options)
      super(name: name, type: :array, **options)
      @element_type = element_type
    end
    
    def validate_and_coerce(value)
      array = super(value)
      array.map { |elem| 
        @element_type.validate_and_coerce(elem)
      }
    end
  end
end

4. DSL for Typed Signatures

class TypedSignature < Signature
  def self.define(&block)
    builder = SignatureBuilder.new
    builder.instance_eval(&block)
    builder.build
  end
  
  class SignatureBuilder
    def initialize
      @input_fields = {}
      @output_fields = {}
    end
    
    # DSL methods
    def input(name, type, **options)
      @input_fields[name] = create_typed_field(name, type, **options)
    end
    
    def output(name, type, **options)
      @output_fields[name] = create_typed_field(name, type, **options)
    end
    
    def enum(name, values:, **options)
      Types::Enum.new(name: name, values: values, **options)
    end
    
    def list_of(type)
      Types::TypedList.new(element_type: type)
    end
  end
end

Example Usage

# Define typed signature
signature = Desiru::TypedSignature.define do
  input :age, :integer, validator: ->(v) { v >= 0 && v <= 150 }
  input :name, :string, description: "Person's full name"
  input :interests, list_of(:string), optional: true
  
  output :category, enum(values: ["child", "teen", "adult", "senior"])
  output :summary, :string
end

# Create typed predictor
predictor = Desiru::TypedPredictor.new(
  signature: signature,
  model: "gpt-4"
)

# Type-safe usage
result = predictor.forward(
  age: "25",  # Automatically converted to integer
  name: "Alice Smith"
)
# Result has validated category and summary

# This would raise ValidationError
result = predictor.forward(
  age: "invalid",  # Cannot convert to integer
  name: "Bob"
)

Integration with IDE

# Generate RBS type signatures
class TypedPredictor
  def generate_rbs
    <<~RBS
      class #{self.class.name}
        def forward: (#{format_input_types}) -> { #{format_output_types} }
      end
    RBS
  end
end

Testing Requirements

  • Unit tests for each type coercion
  • Validation error cases
  • Complex type scenarios
  • Performance impact measurement
  • IDE integration tests

Priority

High - Significantly improves reliability and developer experience

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions