dspy 0.3.1 → 0.5.0

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.
@@ -5,25 +5,110 @@ require 'sorbet-runtime'
5
5
  require_relative 'predict'
6
6
  require_relative 'signature'
7
7
  require_relative 'instrumentation'
8
+ require_relative 'mixins/struct_builder'
8
9
 
9
10
  module DSPy
10
11
  # Enhances prediction by encouraging step-by-step reasoning
11
12
  # before providing a final answer using Sorbet signatures.
12
13
  class ChainOfThought < Predict
13
14
  extend T::Sig
15
+ include Mixins::StructBuilder
14
16
 
15
17
  FieldDescriptor = DSPy::Signature::FieldDescriptor
16
18
 
17
19
  sig { params(signature_class: T.class_of(DSPy::Signature)).void }
18
20
  def initialize(signature_class)
19
21
  @original_signature = signature_class
22
+ enhanced_signature = build_enhanced_signature(signature_class)
23
+
24
+ # Call parent constructor with enhanced signature
25
+ super(enhanced_signature)
26
+ @signature_class = enhanced_signature
27
+ end
28
+
29
+ # Override prompt-based methods to maintain ChainOfThought behavior
30
+ sig { override.params(new_prompt: Prompt).returns(ChainOfThought) }
31
+ def with_prompt(new_prompt)
32
+ # Create a new ChainOfThought with the same original signature
33
+ instance = self.class.new(@original_signature)
34
+
35
+ # Ensure the instruction includes "Think step by step" if not already present
36
+ enhanced_instruction = if new_prompt.instruction.include?("Think step by step")
37
+ new_prompt.instruction
38
+ else
39
+ "#{new_prompt.instruction} Think step by step."
40
+ end
41
+
42
+ # Create enhanced prompt with ChainOfThought-specific schemas
43
+ enhanced_prompt = Prompt.new(
44
+ instruction: enhanced_instruction,
45
+ input_schema: @signature_class.input_json_schema,
46
+ output_schema: @signature_class.output_json_schema,
47
+ few_shot_examples: new_prompt.few_shot_examples,
48
+ signature_class_name: @signature_class.name
49
+ )
50
+
51
+ instance.instance_variable_set(:@prompt, enhanced_prompt)
52
+ instance
53
+ end
54
+
55
+ sig { override.params(instruction: String).returns(ChainOfThought) }
56
+ def with_instruction(instruction)
57
+ enhanced_instruction = ensure_chain_of_thought_instruction(instruction)
58
+ super(enhanced_instruction)
59
+ end
60
+
61
+ sig { override.params(examples: T::Array[FewShotExample]).returns(ChainOfThought) }
62
+ def with_examples(examples)
63
+ # Convert examples to include reasoning if they don't have it
64
+ enhanced_examples = examples.map do |example|
65
+ if example.reasoning.nil? || example.reasoning.empty?
66
+ # Try to extract reasoning from the output if it contains a reasoning field
67
+ reasoning = example.output[:reasoning] || "Step by step reasoning for this example."
68
+ DSPy::FewShotExample.new(
69
+ input: example.input,
70
+ output: example.output,
71
+ reasoning: reasoning
72
+ )
73
+ else
74
+ example
75
+ end
76
+ end
77
+
78
+ super(enhanced_examples)
79
+ end
80
+
81
+ # Access to the original signature for optimization
82
+ sig { returns(T.class_of(DSPy::Signature)) }
83
+ attr_reader :original_signature
20
84
 
21
- # Create enhanced output struct with reasoning
85
+ # Override forward_untyped to add ChainOfThought-specific instrumentation
86
+ sig { override.params(input_values: T.untyped).returns(T.untyped) }
87
+ def forward_untyped(**input_values)
88
+ instrument_prediction('dspy.chain_of_thought', @original_signature, input_values) do
89
+ # Call parent prediction logic
90
+ prediction_result = super(**input_values)
91
+
92
+ # Analyze reasoning if present
93
+ analyze_reasoning(prediction_result)
94
+
95
+ prediction_result
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ # Builds enhanced signature with reasoning capabilities
102
+ sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T.class_of(DSPy::Signature)) }
103
+ def build_enhanced_signature(signature_class)
22
104
  enhanced_output_struct = create_enhanced_output_struct(signature_class)
105
+ create_signature_class(signature_class, enhanced_output_struct)
106
+ end
23
107
 
24
- # Create enhanced signature class
25
- enhanced_signature = Class.new(DSPy::Signature) do
26
- # Set the description
108
+ # Creates signature class with enhanced description and reasoning field
109
+ sig { params(signature_class: T.class_of(DSPy::Signature), enhanced_output_struct: T.class_of(T::Struct)).returns(T.class_of(DSPy::Signature)) }
110
+ def create_signature_class(signature_class, enhanced_output_struct)
111
+ Class.new(DSPy::Signature) do
27
112
  description "#{signature_class.description} Think step by step."
28
113
 
29
114
  # Use the same input struct and copy field descriptors
@@ -47,52 +132,49 @@ module DSPy
47
132
  attr_reader :input_struct_class, :output_struct_class
48
133
  end
49
134
  end
135
+ end
50
136
 
51
- # Call parent constructor with enhanced signature
52
- super(enhanced_signature)
53
- @signature_class = enhanced_signature
137
+ # Creates enhanced output struct with reasoning field
138
+ sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T.class_of(T::Struct)) }
139
+ def create_enhanced_output_struct(signature_class)
140
+ output_props = signature_class.output_struct_class.props
141
+
142
+ build_enhanced_struct(
143
+ { output: output_props },
144
+ { reasoning: [String, "Step by step reasoning process"] }
145
+ )
54
146
  end
55
147
 
56
- # Override forward_untyped to add ChainOfThought-specific instrumentation
57
- sig { override.params(input_values: T.untyped).returns(T.untyped) }
58
- def forward_untyped(**input_values)
59
- # Prepare instrumentation payload
60
- input_fields = input_values.keys.map(&:to_s)
148
+ # Ensures instruction includes chain of thought prompt
149
+ sig { params(instruction: String).returns(String) }
150
+ def ensure_chain_of_thought_instruction(instruction)
151
+ instruction.include?("Think step by step") ? instruction : "#{instruction} Think step by step."
152
+ end
153
+
154
+ # Analyzes reasoning in prediction result and emits instrumentation events
155
+ sig { params(prediction_result: T.untyped).void }
156
+ def analyze_reasoning(prediction_result)
157
+ return unless prediction_result.respond_to?(:reasoning) && prediction_result.reasoning
61
158
 
62
- # Instrument ChainOfThought lifecycle
63
- result = Instrumentation.instrument('dspy.chain_of_thought', {
64
- signature_class: @original_signature.name,
65
- model: lm.model,
66
- provider: lm.provider,
67
- input_fields: input_fields
68
- }) do
69
- # Call parent prediction logic
70
- prediction_result = super(**input_values)
71
-
72
- # Analyze reasoning if present
73
- if prediction_result.respond_to?(:reasoning) && prediction_result.reasoning
74
- reasoning_content = prediction_result.reasoning.to_s
75
- reasoning_length = reasoning_content.length
76
- reasoning_steps = count_reasoning_steps(reasoning_content)
77
-
78
- # Emit reasoning analysis event
79
- Instrumentation.emit('dspy.chain_of_thought.reasoning_complete', {
80
- signature_class: @original_signature.name,
81
- reasoning_steps: reasoning_steps,
82
- reasoning_length: reasoning_length,
83
- has_reasoning: !reasoning_content.empty?
84
- })
85
- end
86
-
87
- prediction_result
88
- end
159
+ reasoning_content = prediction_result.reasoning.to_s
160
+ return if reasoning_content.empty?
89
161
 
90
- result
162
+ emit_reasoning_analysis(reasoning_content)
91
163
  end
92
164
 
93
- private
165
+ # Emits reasoning analysis instrumentation event
166
+ sig { params(reasoning_content: String).void }
167
+ def emit_reasoning_analysis(reasoning_content)
168
+ Instrumentation.emit('dspy.chain_of_thought.reasoning_complete', {
169
+ signature_class: @original_signature.name,
170
+ reasoning_steps: count_reasoning_steps(reasoning_content),
171
+ reasoning_length: reasoning_content.length,
172
+ has_reasoning: true
173
+ })
174
+ end
94
175
 
95
176
  # Count reasoning steps by looking for step indicators
177
+ sig { params(reasoning_text: String).returns(Integer) }
96
178
  def count_reasoning_steps(reasoning_text)
97
179
  return 0 if reasoning_text.nil? || reasoning_text.empty?
98
180
 
@@ -113,50 +195,5 @@ module DSPy
113
195
  # Fallback: count sentences if no clear steps
114
196
  max_count > 0 ? max_count : reasoning_text.split(/[.!?]+/).reject(&:empty?).length
115
197
  end
116
-
117
- sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T.class_of(T::Struct)) }
118
- def create_enhanced_output_struct(signature_class)
119
- # Get original output props
120
- original_props = signature_class.output_struct_class.props
121
-
122
- # Create new struct class with reasoning added
123
- Class.new(T::Struct) do
124
- # Add all original fields
125
- original_props.each do |name, prop|
126
- # Extract the type and other options
127
- type = prop[:type]
128
- options = prop.except(:type, :type_object, :accessor_key, :sensitivity, :redaction)
129
-
130
- # Handle default values
131
- if options[:default]
132
- const name, type, default: options[:default]
133
- elsif options[:factory]
134
- const name, type, factory: options[:factory]
135
- else
136
- const name, type
137
- end
138
- end
139
-
140
- # Add reasoning field (ChainOfThought always provides this)
141
- const :reasoning, String
142
-
143
- # Add to_h method to serialize the struct to a hash
144
- define_method :to_h do
145
- hash = {}
146
-
147
- # Start with input values if available
148
- if self.instance_variable_defined?(:@input_values)
149
- hash.merge!(self.instance_variable_get(:@input_values))
150
- end
151
-
152
- # Then add output properties
153
- self.class.props.keys.each do |key|
154
- hash[key] = self.send(key)
155
- end
156
-
157
- hash
158
- end
159
- end
160
- end
161
198
  end
162
199
  end