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/retry_handler.rb,
lib/dspy/lm/vision_models.rb,
lib/dspy/lm/adapter_factory.rb,
lib/dspy/lm/message_builder.rb,
lib/dspy/lm/strategy_selector.rb,
lib/dspy/lm/adapters/gemini_adapter.rb,
lib/dspy/lm/adapters/ollama_adapter.rb,
lib/dspy/lm/adapters/openai_adapter.rb,
lib/dspy/lm/strategies/base_strategy.rb,
lib/dspy/lm/adapters/anthropic_adapter.rb,
lib/dspy/lm/structured_output_strategy.rb,
lib/dspy/lm/adapters/gemini/schema_converter.rb,
lib/dspy/lm/adapters/openai/schema_converter.rb,
lib/dspy/lm/strategies/anthropic_tool_use_strategy.rb,
lib/dspy/lm/strategies/enhanced_prompting_strategy.rb,
lib/dspy/lm/strategies/anthropic_extraction_strategy.rb,
lib/dspy/lm/strategies/gemini_structured_output_strategy.rb,
lib/dspy/lm/strategies/openai_structured_output_strategy.rb

Defined Under Namespace

Modules: Adapters, MessageFactory, ResponseMetadataFactory, Strategies, UsageFactory, VisionModels Classes: Adapter, AdapterError, AdapterFactory, AnthropicAdapter, AnthropicResponseMetadata, ConfigurationError, Error, GeminiAdapter, GeminiResponseMetadata, IncompatibleImageFeatureError, Message, MessageBuilder, MissingAPIKeyError, OllamaAdapter, OpenAIAdapter, OpenAIResponseMetadata, OpenAIUsage, Response, ResponseMetadata, RetryHandler, StrategySelector, StructuredOutputStrategy, UnsupportedProviderError, Usage

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

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



34
35
36
37
38
39
40
41
42
43
# File 'lib/dspy/lm.rb', line 34

def initialize(model_id, api_key: nil, **options)
  @model_id = model_id
  @api_key = api_key
  
  # 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.



32
33
34
# File 'lib/dspy/lm.rb', line 32

def adapter
  @adapter
end

#api_keyObject (readonly)

Returns the value of attribute api_key.



32
33
34
# File 'lib/dspy/lm.rb', line 32

def api_key
  @api_key
end

#modelObject (readonly)

Returns the value of attribute model.



32
33
34
# File 'lib/dspy/lm.rb', line 32

def model
  @model
end

#model_idObject (readonly)

Returns the value of attribute model_id.



32
33
34
# File 'lib/dspy/lm.rb', line 32

def model_id
  @model_id
end

#providerObject (readonly)

Returns the value of attribute provider.



32
33
34
# File 'lib/dspy/lm.rb', line 32

def provider
  @provider
end

Instance Method Details

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



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

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



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/dspy/lm.rb', line 369

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
    
    # Emit the standard lm.tokens event (consistent with other LM calls)
    emit_token_usage(response, 'RawPrompt')
    
    # 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



439
440
441
442
443
444
445
446
447
# File 'lib/dspy/lm.rb', line 439

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



399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
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 399

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



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

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



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/dspy/lm.rb', line 347

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