Class: DSPy::LM

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/dspy/lm.rb,
lib/dspy/lm/usage.rb,
lib/dspy/lm/errors.rb,
lib/dspy/lm/adapter.rb,
lib/dspy/lm/message.rb,
lib/dspy/lm/response.rb,
lib/dspy/lm/chat_strategy.rb,
lib/dspy/lm/json_strategy.rb,
lib/dspy/lm/vision_models.rb,
lib/dspy/lm/adapter_factory.rb,
lib/dspy/lm/message_builder.rb

Defined Under Namespace

Modules: MessageFactory, ResponseMetadataFactory, UsageFactory, VisionModels Classes: Adapter, AdapterError, AdapterFactory, AnthropicResponseMetadata, ChatStrategy, ConfigurationError, Error, GeminiResponseMetadata, IncompatibleImageFeatureError, JSONStrategy, Message, MessageBuilder, MissingAPIKeyError, MissingAdapterError, MissingOfficialSDKError, OpenAIResponseMetadata, OpenAIUsage, Response, ResponseMetadata, UnsupportedProviderError, UnsupportedVersionError, Usage

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model_id, api_key: nil, schema_format: :json, data_format: :json, **options) ⇒ LM

Returns a new instance of LM.



30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/dspy/lm.rb', line 30

def initialize(model_id, api_key: nil, schema_format: :json, data_format: :json, **options)
  @model_id = model_id
  @api_key = api_key
  @schema_format = schema_format
  @data_format = data_format

  # Parse provider and model from model_id
  @provider, @model = parse_model_id(model_id)

  # Create appropriate adapter with options
  @adapter = AdapterFactory.create(model_id, api_key: api_key, **options)
end

Instance Attribute Details

#adapterObject (readonly)

Returns the value of attribute adapter.



28
29
30
# File 'lib/dspy/lm.rb', line 28

def adapter
  @adapter
end

#api_keyObject (readonly)

Returns the value of attribute api_key.



28
29
30
# File 'lib/dspy/lm.rb', line 28

def api_key
  @api_key
end

#data_formatObject (readonly)

Returns the value of attribute data_format.



28
29
30
# File 'lib/dspy/lm.rb', line 28

def data_format
  @data_format
end

#modelObject (readonly)

Returns the value of attribute model.



28
29
30
# File 'lib/dspy/lm.rb', line 28

def model
  @model
end

#model_idObject (readonly)

Returns the value of attribute model_id.



28
29
30
# File 'lib/dspy/lm.rb', line 28

def model_id
  @model_id
end

#providerObject (readonly)

Returns the value of attribute provider.



28
29
30
# File 'lib/dspy/lm.rb', line 28

def provider
  @provider
end

#schema_formatObject (readonly)

Returns the value of attribute schema_format.



28
29
30
# File 'lib/dspy/lm.rb', line 28

def schema_format
  @schema_format
end

Instance Method Details

#chat(inference_module, input_values, &block) ⇒ Object



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
# File 'lib/dspy/lm.rb', line 43

def chat(inference_module, input_values, &block)
  # Capture the current DSPy context before entering Sync block
  parent_context = DSPy::Context.current.dup
  
  Sync do
    # Properly restore the context in the new fiber created by Sync
    # We need to set both thread and fiber storage for the new context system
    thread_key = :"dspy_context_#{Thread.current.object_id}"
    Thread.current[thread_key] = parent_context
    Thread.current[:dspy_context] = parent_context  # Keep for backward compatibility
    Fiber[:dspy_context] = parent_context
    
    signature_class = inference_module.signature_class
    
    # Build messages from inference module
    messages = build_messages(inference_module, input_values)
    
    # Execute with instrumentation
    response = instrument_lm_request(messages, signature_class.name) do
      chat_with_strategy(messages, signature_class, &block)
    end

    # Parse response (no longer needs separate instrumentation)
    parsed_result = parse_response(response, input_values, signature_class)
    
    parsed_result
  end
end

#execute_raw_chat(messages, &streaming_block) ⇒ Object



438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/dspy/lm.rb', line 438

def execute_raw_chat(messages, &streaming_block)
  # Generate unique request ID for tracking
  request_id = SecureRandom.hex(8)
  start_time = Time.now
  
  # Store request context for correlation
  Thread.current[:dspy_request_id] = request_id
  Thread.current[:dspy_request_start_time] = start_time
  
  begin
    response = instrument_lm_request(messages, 'RawPrompt') do
      # Convert messages to hash format for adapter
      hash_messages = messages_to_hash_array(messages)
      # Direct adapter call, no strategies or JSON parsing
      adapter.chat(messages: hash_messages, signature: nil, &streaming_block)
    end
    
    # Return raw response content, not parsed JSON
    response.content
  ensure
    # Clean up thread-local storage
    Thread.current[:dspy_request_id] = nil
    Thread.current[:dspy_request_start_time] = nil
  end
end

#messages_to_hash_array(messages) ⇒ Object

Convert Message objects to hash array for adapters



505
506
507
508
509
510
511
512
513
# File 'lib/dspy/lm.rb', line 505

def messages_to_hash_array(messages)
  messages.map do |msg|
    if msg.is_a?(Message)
      msg.to_h
    else
      msg
    end
  end
end

#normalize_messages(messages) ⇒ Object

Convert messages to normalized Message objects



465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
# File 'lib/dspy/lm.rb', line 465

def normalize_messages(messages)
  # Validate array format first
  unless messages.is_a?(Array)
    raise ArgumentError, "messages must be an array"
  end
  
  return messages if messages.all? { |m| m.is_a?(Message) }
  
  # Convert hash messages to Message objects
  normalized = []
  messages.each_with_index do |msg, index|
    if msg.is_a?(Message)
      normalized << msg
    elsif msg.is_a?(Hash)
      # Validate hash has required fields
      unless msg.key?(:role) && msg.key?(:content)
        raise ArgumentError, "Message at index #{index} must have :role and :content"
      end
      
      # Validate role
      valid_roles = %w[system user assistant]
      unless valid_roles.include?(msg[:role])
        raise ArgumentError, "Invalid role at index #{index}: #{msg[:role]}. Must be one of: #{valid_roles.join(', ')}"
      end
      
      # Create Message object
      message = MessageFactory.create(msg)
      if message.nil?
        raise ArgumentError, "Failed to create Message from hash at index #{index}"
      end
      normalized << message
    else
      raise ArgumentError, "Message at index #{index} must be a Message object or hash with :role and :content"
    end
  end
  
  normalized
end

#raw_chat(messages = nil, &block) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/dspy/lm.rb', line 72

def raw_chat(messages = nil, &block)
  # Support both array format and builder DSL
  if block_given? && messages.nil?
    # DSL mode - block is for building messages
    builder = MessageBuilder.new
    yield builder
    messages = builder.messages
    streaming_block = nil
  else
    # Array mode - block is for streaming
    messages ||= []
    streaming_block = block
  end
  
  # Normalize and validate messages
  messages = normalize_messages(messages)
  
  # Execute with instrumentation
  execute_raw_chat(messages, &streaming_block)
end

#validate_messages!(messages) ⇒ Object



416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/dspy/lm.rb', line 416

def validate_messages!(messages)
  unless messages.is_a?(Array)
    raise ArgumentError, "messages must be an array"
  end
  
  messages.each_with_index do |message, index|
    # Accept both Message objects and hash format for backward compatibility
    if message.is_a?(Message)
      # Already validated by type system
      next
    elsif message.is_a?(Hash) && message.key?(:role) && message.key?(:content)
      # Legacy hash format - validate role
      valid_roles = %w[system user assistant]
      unless valid_roles.include?(message[:role])
        raise ArgumentError, "Invalid role at index #{index}: #{message[:role]}. Must be one of: #{valid_roles.join(', ')}"
      end
    else
      raise ArgumentError, "Message at index #{index} must be a Message object or hash with :role and :content"
    end
  end
end