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.
- checksums.yaml +4 -4
- data/README.md +67 -385
- data/lib/dspy/chain_of_thought.rb +123 -86
- data/lib/dspy/evaluate.rb +554 -0
- data/lib/dspy/example.rb +203 -0
- data/lib/dspy/few_shot_example.rb +81 -0
- data/lib/dspy/instrumentation/token_tracker.rb +6 -6
- data/lib/dspy/instrumentation.rb +199 -18
- data/lib/dspy/lm/adapter_factory.rb +6 -8
- data/lib/dspy/lm.rb +79 -35
- data/lib/dspy/mixins/instrumentation_helpers.rb +133 -0
- data/lib/dspy/mixins/struct_builder.rb +133 -0
- data/lib/dspy/mixins/type_coercion.rb +67 -0
- data/lib/dspy/predict.rb +83 -128
- data/lib/dspy/prompt.rb +222 -0
- data/lib/dspy/propose/grounded_proposer.rb +560 -0
- data/lib/dspy/re_act.rb +242 -173
- data/lib/dspy/registry/registry_manager.rb +504 -0
- data/lib/dspy/registry/signature_registry.rb +725 -0
- data/lib/dspy/storage/program_storage.rb +442 -0
- data/lib/dspy/storage/storage_manager.rb +331 -0
- data/lib/dspy/subscribers/langfuse_subscriber.rb +669 -0
- data/lib/dspy/subscribers/logger_subscriber.rb +180 -5
- data/lib/dspy/subscribers/newrelic_subscriber.rb +686 -0
- data/lib/dspy/subscribers/otel_subscriber.rb +538 -0
- data/lib/dspy/teleprompt/data_handler.rb +107 -0
- data/lib/dspy/teleprompt/mipro_v2.rb +790 -0
- data/lib/dspy/teleprompt/simple_optimizer.rb +497 -0
- data/lib/dspy/teleprompt/teleprompter.rb +336 -0
- data/lib/dspy/teleprompt/utils.rb +380 -0
- data/lib/dspy/version.rb +5 -0
- data/lib/dspy.rb +105 -0
- metadata +32 -12
- data/lib/dspy/lm/adapters/ruby_llm_adapter.rb +0 -81
@@ -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
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
#
|
57
|
-
sig {
|
58
|
-
def
|
59
|
-
#
|
60
|
-
|
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
|
-
|
63
|
-
|
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
|
-
|
162
|
+
emit_reasoning_analysis(reasoning_content)
|
91
163
|
end
|
92
164
|
|
93
|
-
|
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
|