dspy 0.28.1 → 0.28.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b377060443eeb9c3c5d975e76750d6c519d1b93cf6f20dc6ad20bcda08d1ca4
4
- data.tar.gz: 4580845b3fd9991b531c8c2bd809595cbd3328da57a23e58307cdbe52e3822bc
3
+ metadata.gz: f1cc0ac1e2e1dc27f6255b11ca70ff0ad0eb37b5ae50ff38b97ae7d35b7b69b8
4
+ data.tar.gz: 2399987757b4f037080632e646714e785328fbe3c1fd39138fe750336cdd7710
5
5
  SHA512:
6
- metadata.gz: 444a5e08364b2e996bf49d230cb9a94a1930e26d2ea796cd0104ea4888b45ade4099cec502b92c08752631adff47b1d9d1897da9209e9faabbb28fb6761935b0
7
- data.tar.gz: c1b3a83482c861923304c463d6f0b1c9042fbc31da920526ec18ed0f5148b4408a9023d312eded9263ca2d706e4779b78526e28dc50a17458cbbac9afa56b024
6
+ metadata.gz: ea48762186d3de89a005e8eac27f57ca90b294c4ab096ec1c7985d06396216d719ca3a2144406d1f43af472a560670e502329a785d33b8923b5c9c4c0f83dfd1
7
+ data.tar.gz: 98fee1468f2692a0f7cc87622722f945dbc344fb299e71935ef592bc1d59e68e48e1390208981b6263998627dacb38dc325727cc191f81e1db79fff57c1537a4
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ module DSPy
6
+ # Provides Rails-style callback hooks for DSPy modules
7
+ #
8
+ # @example Define callbacks in base class
9
+ # class DSPy::Module
10
+ # include DSPy::Callbacks
11
+ #
12
+ # create_before_callback :forward
13
+ # create_after_callback :forward
14
+ # create_around_callback :forward
15
+ # end
16
+ #
17
+ # @example Use callbacks in subclasses
18
+ # class MyAgent < DSPy::Module
19
+ # before :setup_context
20
+ # after :log_metrics
21
+ # around :manage_memory
22
+ #
23
+ # private
24
+ #
25
+ # def setup_context
26
+ # @start_time = Time.now
27
+ # end
28
+ #
29
+ # def log_metrics
30
+ # puts "Duration: #{Time.now - @start_time}"
31
+ # end
32
+ #
33
+ # def manage_memory
34
+ # load_context
35
+ # yield
36
+ # save_context
37
+ # end
38
+ # end
39
+ module Callbacks
40
+ def self.included(base)
41
+ base.extend(ClassMethods)
42
+ end
43
+
44
+ module ClassMethods
45
+ # Creates a before callback hook for the specified method
46
+ #
47
+ # @param method_name [Symbol] the method to add callback support to
48
+ def create_before_callback(method_name)
49
+ mark_method_has_callbacks(method_name)
50
+ ensure_callback_method_defined(:before, method_name)
51
+ wrap_method_with_callbacks(method_name)
52
+ end
53
+
54
+ # Creates an after callback hook for the specified method
55
+ #
56
+ # @param method_name [Symbol] the method to add callback support to
57
+ def create_after_callback(method_name)
58
+ mark_method_has_callbacks(method_name)
59
+ ensure_callback_method_defined(:after, method_name)
60
+ wrap_method_with_callbacks(method_name)
61
+ end
62
+
63
+ # Creates an around callback hook for the specified method
64
+ #
65
+ # @param method_name [Symbol] the method to add callback support to
66
+ def create_around_callback(method_name)
67
+ mark_method_has_callbacks(method_name)
68
+ ensure_callback_method_defined(:around, method_name)
69
+ wrap_method_with_callbacks(method_name)
70
+ end
71
+
72
+ private
73
+
74
+ # Ensures the callback registration method exists
75
+ def ensure_callback_method_defined(type, target_method_name)
76
+ return if singleton_class.method_defined?(type)
77
+
78
+ define_singleton_method(type) do |callback_method|
79
+ register_callback(type, target_method_name, callback_method)
80
+ end
81
+ end
82
+
83
+ # Registers a callback for execution
84
+ def register_callback(type, method_name, callback_method)
85
+ own_callbacks_for(method_name)[type] ||= []
86
+ own_callbacks_for(method_name)[type] << callback_method
87
+ end
88
+
89
+ # Returns own callbacks (not including parent)
90
+ def own_callbacks_for(method_name)
91
+ @callbacks ||= {}
92
+ @callbacks[method_name] ||= {}
93
+ end
94
+
95
+ # Marks that a method has callback support (even if no callbacks registered yet)
96
+ def mark_method_has_callbacks(method_name)
97
+ own_callbacks_for(method_name)
98
+ end
99
+
100
+ # Returns the callback registry for a method
101
+ # Includes callbacks from parent classes
102
+ def callbacks_for(method_name)
103
+ own_callbacks = own_callbacks_for(method_name)
104
+
105
+ # Merge parent callbacks if this is a subclass
106
+ if superclass.respond_to?(:callbacks_for, true)
107
+ parent_callbacks = superclass.send(:callbacks_for, method_name)
108
+
109
+ # Merge each callback type, with own callbacks coming after parent callbacks
110
+ merged_callbacks = {}
111
+ [:before, :after, :around].each do |type|
112
+ parent_list = parent_callbacks[type] || []
113
+ own_list = own_callbacks[type] || []
114
+ merged_callbacks[type] = parent_list + own_list if parent_list.any? || own_list.any?
115
+ end
116
+
117
+ merged_callbacks
118
+ else
119
+ own_callbacks
120
+ end
121
+ end
122
+
123
+ # Wraps a method with callback execution logic
124
+ def wrap_method_with_callbacks(method_name)
125
+ return if method_wrapped?(method_name)
126
+
127
+ # Defer wrapping if method doesn't exist yet
128
+ return unless method_defined?(method_name)
129
+
130
+ # Mark as wrapped BEFORE define_method to prevent infinite recursion
131
+ mark_method_wrapped(method_name)
132
+
133
+ original_method = instance_method(method_name)
134
+
135
+ define_method(method_name) do |*args, **kwargs, &block|
136
+ # Execute before callbacks
137
+ run_callbacks(:before, method_name)
138
+
139
+ # Execute around callbacks or original method
140
+ result = if self.class.send(:has_around_callbacks?, method_name)
141
+ execute_with_around_callbacks(method_name, original_method, *args, **kwargs, &block)
142
+ else
143
+ original_method.bind(self).call(*args, **kwargs, &block)
144
+ end
145
+
146
+ # Execute after callbacks
147
+ run_callbacks(:after, method_name)
148
+
149
+ result
150
+ end
151
+ end
152
+
153
+ # Checks if method has around callbacks
154
+ def has_around_callbacks?(method_name)
155
+ callbacks_for(method_name)[:around]&.any?
156
+ end
157
+
158
+ # Hook into method_added to wrap methods when they're defined
159
+ def method_added(method_name)
160
+ super
161
+
162
+ # Check if this method or any parent has callback support (even if no callbacks registered yet)
163
+ has_callback_support = method_has_callback_support?(method_name)
164
+
165
+ return unless has_callback_support
166
+ return if method_wrapped?(method_name)
167
+
168
+ wrap_method_with_callbacks(method_name)
169
+ end
170
+
171
+ # Checks if a method has callback support in this class or parents
172
+ def method_has_callback_support?(method_name)
173
+ # Check own callbacks registry
174
+ return true if @callbacks&.key?(method_name)
175
+
176
+ # Check parent class
177
+ if superclass.respond_to?(:method_has_callback_support?, true)
178
+ superclass.send(:method_has_callback_support?, method_name)
179
+ else
180
+ false
181
+ end
182
+ end
183
+
184
+ # Marks a method as wrapped
185
+ def mark_method_wrapped(method_name)
186
+ @wrapped_methods ||= []
187
+ @wrapped_methods << method_name
188
+ end
189
+
190
+ # Checks if method is already wrapped
191
+ def method_wrapped?(method_name)
192
+ @wrapped_methods&.include?(method_name)
193
+ end
194
+ end
195
+
196
+ private
197
+
198
+ # Executes callbacks of a specific type
199
+ def run_callbacks(type, method_name)
200
+ callbacks = self.class.send(:callbacks_for, method_name)[type]
201
+ return unless callbacks
202
+
203
+ callbacks.each do |callback_method|
204
+ send(callback_method)
205
+ end
206
+ end
207
+
208
+ # Executes method with around callbacks
209
+ def execute_with_around_callbacks(method_name, original_method, *args, **kwargs, &block)
210
+ callbacks = self.class.send(:callbacks_for, method_name)[:around]
211
+
212
+ # Build callback chain from innermost (original method) to outermost
213
+ chain = callbacks.reverse.inject(
214
+ -> { original_method.bind(self).call(*args, **kwargs, &block) }
215
+ ) do |inner, callback_method|
216
+ -> { send(callback_method) { inner.call } }
217
+ end
218
+
219
+ chain.call
220
+ end
221
+ end
222
+ end
@@ -46,7 +46,8 @@ module DSPy
46
46
  input_schema: @signature_class.input_json_schema,
47
47
  output_schema: @signature_class.output_json_schema,
48
48
  few_shot_examples: new_prompt.few_shot_examples,
49
- signature_class_name: @signature_class.name
49
+ signature_class_name: @signature_class.name,
50
+ schema_format: new_prompt.schema_format
50
51
  )
51
52
 
52
53
  instance.instance_variable_set(:@prompt, enhanced_prompt)
data/lib/dspy/lm.rb CHANGED
@@ -31,15 +31,16 @@ require_relative 'structured_outputs_prompt'
31
31
  module DSPy
32
32
  class LM
33
33
  extend T::Sig
34
- attr_reader :model_id, :api_key, :model, :provider, :adapter
34
+ attr_reader :model_id, :api_key, :model, :provider, :adapter, :schema_format
35
35
 
36
- def initialize(model_id, api_key: nil, **options)
36
+ def initialize(model_id, api_key: nil, schema_format: :json, **options)
37
37
  @model_id = model_id
38
38
  @api_key = api_key
39
-
39
+ @schema_format = schema_format
40
+
40
41
  # Parse provider and model from model_id
41
42
  @provider, @model = parse_model_id(model_id)
42
-
43
+
43
44
  # Create appropriate adapter with options
44
45
  @adapter = AdapterFactory.create(model_id, api_key: api_key, **options)
45
46
  end
data/lib/dspy/module.rb CHANGED
@@ -3,16 +3,23 @@
3
3
  require 'sorbet-runtime'
4
4
  require 'dry-configurable'
5
5
  require_relative 'context'
6
+ require_relative 'callbacks'
6
7
 
7
8
  module DSPy
8
9
  class Module
9
10
  extend T::Sig
10
11
  extend T::Generic
11
12
  include Dry::Configurable
13
+ include DSPy::Callbacks
12
14
 
13
15
  # Per-instance LM configuration
14
16
  setting :lm, default: nil
15
17
 
18
+ # Define callback hooks for forward method
19
+ create_before_callback :forward
20
+ create_after_callback :forward
21
+ create_around_callback :forward
22
+
16
23
  # The main forward method that users will call is generic and type parameterized
17
24
  sig do
18
25
  type_parameters(:I, :O)
@@ -72,5 +79,31 @@ module DSPy
72
79
  def lm
73
80
  config.lm || DSPy.current_lm
74
81
  end
82
+
83
+ # Save the module state to a JSON file
84
+ # Lightweight serialization for intermediate optimization trials
85
+ #
86
+ # @param path [String] Path to save the module state (JSON format)
87
+ sig { params(path: String).void }
88
+ def save(path)
89
+ require 'json'
90
+ require 'fileutils'
91
+
92
+ # Ensure parent directory exists
93
+ dir = File.dirname(path)
94
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
95
+
96
+ # Serialize module to JSON
97
+ File.write(path, JSON.pretty_generate(to_h))
98
+ end
99
+
100
+ # Default serialization method - subclasses can override
101
+ sig { returns(T::Hash[Symbol, T.untyped]) }
102
+ def to_h
103
+ {
104
+ class_name: self.class.name,
105
+ state: {}
106
+ }
107
+ end
75
108
  end
76
109
  end
data/lib/dspy/predict.rb CHANGED
@@ -53,11 +53,18 @@ module DSPy
53
53
  sig { returns(Prompt) }
54
54
  attr_reader :prompt
55
55
 
56
+ # Mutable demos attribute for MIPROv2 compatibility
57
+ sig { returns(T.nilable(T::Array[FewShotExample])) }
58
+ attr_accessor :demos
59
+
56
60
  sig { params(signature_class: T.class_of(Signature)).void }
57
61
  def initialize(signature_class)
58
62
  super()
59
63
  @signature_class = signature_class
64
+
65
+ # Prompt will read schema_format from config automatically
60
66
  @prompt = Prompt.from_signature(signature_class)
67
+ @demos = nil
61
68
  end
62
69
 
63
70
  # Reconstruct program from serialized hash
data/lib/dspy/prompt.rb CHANGED
@@ -22,21 +22,39 @@ module DSPy
22
22
  sig { returns(T.nilable(String)) }
23
23
  attr_reader :signature_class_name
24
24
 
25
+ # Returns the effective schema format
26
+ # Precedence: instance variable (if not :json default) > config.lm > :json
27
+ sig { returns(Symbol) }
28
+ def schema_format
29
+ # If @schema_format was explicitly set to something other than :json, respect it
30
+ return @schema_format if @schema_format && @schema_format != :json
31
+
32
+ # Otherwise, read from config if available
33
+ DSPy.config.lm&.schema_format || @schema_format || :json
34
+ end
35
+
36
+ sig { returns(T.nilable(T.class_of(Signature))) }
37
+ attr_reader :signature_class
38
+
25
39
  sig do
26
40
  params(
27
41
  instruction: String,
28
42
  input_schema: T::Hash[Symbol, T.untyped],
29
43
  output_schema: T::Hash[Symbol, T.untyped],
30
44
  few_shot_examples: T::Array[FewShotExample],
31
- signature_class_name: T.nilable(String)
45
+ signature_class_name: T.nilable(String),
46
+ schema_format: Symbol,
47
+ signature_class: T.nilable(T.class_of(Signature))
32
48
  ).void
33
49
  end
34
- def initialize(instruction:, input_schema:, output_schema:, few_shot_examples: [], signature_class_name: nil)
50
+ def initialize(instruction:, input_schema:, output_schema:, few_shot_examples: [], signature_class_name: nil, schema_format: :json, signature_class: nil)
35
51
  @instruction = instruction
36
52
  @few_shot_examples = few_shot_examples.freeze
37
53
  @input_schema = input_schema.freeze
38
54
  @output_schema = output_schema.freeze
39
55
  @signature_class_name = signature_class_name
56
+ @schema_format = schema_format
57
+ @signature_class = signature_class
40
58
  end
41
59
 
42
60
  # Immutable update methods for optimization
@@ -47,7 +65,9 @@ module DSPy
47
65
  input_schema: @input_schema,
48
66
  output_schema: @output_schema,
49
67
  few_shot_examples: @few_shot_examples,
50
- signature_class_name: @signature_class_name
68
+ signature_class_name: @signature_class_name,
69
+ schema_format: @schema_format,
70
+ signature_class: @signature_class
51
71
  )
52
72
  end
53
73
 
@@ -58,7 +78,9 @@ module DSPy
58
78
  input_schema: @input_schema,
59
79
  output_schema: @output_schema,
60
80
  few_shot_examples: new_examples,
61
- signature_class_name: @signature_class_name
81
+ signature_class_name: @signature_class_name,
82
+ schema_format: @schema_format,
83
+ signature_class: @signature_class
62
84
  )
63
85
  end
64
86
 
@@ -72,16 +94,29 @@ module DSPy
72
94
  sig { returns(String) }
73
95
  def render_system_prompt
74
96
  sections = []
75
-
76
- sections << "Your input schema fields are:"
77
- sections << "```json"
78
- sections << JSON.pretty_generate(@input_schema)
79
- sections << "```"
80
-
81
- sections << "Your output schema fields are:"
82
- sections << "```json"
83
- sections << JSON.pretty_generate(@output_schema)
84
- sections << "```"
97
+
98
+ case schema_format
99
+ when :baml
100
+ sections << "Your input schema fields are:"
101
+ sections << "```baml"
102
+ sections << render_baml_schema(@input_schema, :input)
103
+ sections << "```"
104
+
105
+ sections << "Your output schema fields are:"
106
+ sections << "```baml"
107
+ sections << render_baml_schema(@output_schema, :output)
108
+ sections << "```"
109
+ else # :json (default)
110
+ sections << "Your input schema fields are:"
111
+ sections << "```json"
112
+ sections << JSON.pretty_generate(@input_schema)
113
+ sections << "```"
114
+
115
+ sections << "Your output schema fields are:"
116
+ sections << "```json"
117
+ sections << JSON.pretty_generate(@output_schema)
118
+ sections << "```"
119
+ end
85
120
 
86
121
  sections << ""
87
122
  sections << "All interactions will be structured in the following way, with the appropriate values filled in."
@@ -148,32 +183,36 @@ module DSPy
148
183
  few_shot_examples: @few_shot_examples.map(&:to_h),
149
184
  input_schema: @input_schema,
150
185
  output_schema: @output_schema,
151
- signature_class_name: @signature_class_name
186
+ signature_class_name: @signature_class_name,
187
+ schema_format: @schema_format
152
188
  }
153
189
  end
154
190
 
155
191
  sig { params(hash: T::Hash[Symbol, T.untyped]).returns(Prompt) }
156
192
  def self.from_h(hash)
157
193
  examples = (hash[:few_shot_examples] || []).map { |ex| FewShotExample.from_h(ex) }
158
-
194
+
159
195
  new(
160
196
  instruction: hash[:instruction] || "",
161
197
  input_schema: hash[:input_schema] || {},
162
198
  output_schema: hash[:output_schema] || {},
163
199
  few_shot_examples: examples,
164
- signature_class_name: hash[:signature_class_name]
200
+ signature_class_name: hash[:signature_class_name],
201
+ schema_format: hash[:schema_format] || :json
165
202
  )
166
203
  end
167
204
 
168
205
  # Create prompt from signature class
169
- sig { params(signature_class: T.class_of(Signature)).returns(Prompt) }
170
- def self.from_signature(signature_class)
206
+ sig { params(signature_class: T.class_of(Signature), schema_format: Symbol).returns(Prompt) }
207
+ def self.from_signature(signature_class, schema_format: :json)
171
208
  new(
172
209
  instruction: signature_class.description || "Complete this task.",
173
210
  input_schema: signature_class.input_json_schema,
174
211
  output_schema: signature_class.output_json_schema,
175
212
  few_shot_examples: [],
176
- signature_class_name: signature_class.name
213
+ signature_class_name: signature_class.name,
214
+ schema_format: schema_format,
215
+ signature_class: signature_class
177
216
  )
178
217
  end
179
218
 
@@ -221,6 +260,37 @@ module DSPy
221
260
 
222
261
  private
223
262
 
263
+ # Render BAML schema for input or output
264
+ sig { params(schema: T::Hash[Symbol, T.untyped], type: Symbol).returns(String) }
265
+ def render_baml_schema(schema, type)
266
+ # If we have a signature_class, use sorbet-baml's to_baml method with custom name
267
+ if @signature_class
268
+ begin
269
+ require 'sorbet_baml'
270
+
271
+ struct_class = type == :input ? @signature_class.input_struct_class : @signature_class.output_struct_class
272
+ if struct_class
273
+ # Generate a proper class name from signature class name
274
+ base_name = @signature_class_name || @signature_class.name || "Schema"
275
+ class_name = type == :input ? "#{base_name}Input" : "#{base_name}Output"
276
+
277
+ # Get raw BAML and replace the ugly class name
278
+ raw_baml = struct_class.to_baml
279
+ # Replace the class definition line with a proper name
280
+ return raw_baml.sub(/^class #<Class:0x[0-9a-f]+>/, "class #{class_name}")
281
+ end
282
+ rescue LoadError
283
+ # Fall back to manual BAML generation if sorbet_baml is not available
284
+ end
285
+ end
286
+
287
+ # Fallback: generate BAML manually from schema
288
+ # This is a simple implementation that handles basic types
289
+ # For production use, sorbet-baml should be available
290
+ "# BAML schema generation requires sorbet-baml gem\n" \
291
+ "# Please install: gem install sorbet-baml"
292
+ end
293
+
224
294
  # Recursively serialize complex objects for JSON representation
225
295
  sig { params(obj: T.untyped).returns(T.untyped) }
226
296
  def serialize_for_json(obj)