dspy 0.28.1 → 0.29.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 +2 -3
- data/lib/dspy/callbacks.rb +222 -0
- data/lib/dspy/chain_of_thought.rb +2 -1
- data/lib/dspy/code_act.rb +14 -1
- data/lib/dspy/datasets/ade.rb +90 -0
- data/lib/dspy/datasets.rb +8 -0
- data/lib/dspy/lm.rb +9 -12
- data/lib/dspy/mixins/struct_builder.rb +17 -25
- data/lib/dspy/module.rb +45 -1
- data/lib/dspy/observability/async_span_processor.rb +67 -93
- data/lib/dspy/observability.rb +43 -1
- data/lib/dspy/predict.rb +17 -0
- data/lib/dspy/prompt.rb +90 -20
- data/lib/dspy/propose/dataset_summary_generator.rb +210 -0
- data/lib/dspy/propose/grounded_proposer.rb +320 -66
- data/lib/dspy/re_act.rb +13 -0
- data/lib/dspy/reflection_lm.rb +36 -0
- data/lib/dspy/teleprompt/bootstrap_strategy.rb +26 -0
- data/lib/dspy/teleprompt/gepa.rb +448 -2803
- data/lib/dspy/teleprompt/mipro_v2.rb +624 -100
- data/lib/dspy/teleprompt/utils.rb +349 -42
- data/lib/dspy/version.rb +2 -2
- data/lib/dspy.rb +4 -2
- data/lib/gepa/api.rb +61 -0
- data/lib/gepa/core/engine.rb +226 -0
- data/lib/gepa/core/evaluation_batch.rb +26 -0
- data/lib/gepa/core/result.rb +92 -0
- data/lib/gepa/core/state.rb +231 -0
- data/lib/gepa/logging/experiment_tracker.rb +54 -0
- data/lib/gepa/logging/logger.rb +57 -0
- data/lib/gepa/logging.rb +9 -0
- data/lib/gepa/proposer/base.rb +27 -0
- data/lib/gepa/proposer/merge_proposer.rb +424 -0
- data/lib/gepa/proposer/reflective_mutation/base.rb +48 -0
- data/lib/gepa/proposer/reflective_mutation/reflective_mutation.rb +188 -0
- data/lib/gepa/strategies/batch_sampler.rb +91 -0
- data/lib/gepa/strategies/candidate_selector.rb +97 -0
- data/lib/gepa/strategies/component_selector.rb +57 -0
- data/lib/gepa/strategies/instruction_proposal.rb +120 -0
- data/lib/gepa/telemetry.rb +122 -0
- data/lib/gepa/utils/pareto.rb +119 -0
- data/lib/gepa.rb +21 -0
- metadata +59 -4
- data/lib/dspy/teleprompt/simple_optimizer.rb +0 -497
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 747119ce407283e4d8ed5f01014262f24a94418ad2cbef4305a28b21cb58c8bc
|
4
|
+
data.tar.gz: 3693faccd1fca98015864fd4404491b619b2aa600ab83a78dd3fc7d9e3342ef1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7fecac3bc3389e11bdb2328234455cbbbcbf6c7544518cbf94082e75a3f0bde489339b70f91dfd31a43bbd87b17451bfb5a78387c90f5b669aefa09e8af73500
|
7
|
+
data.tar.gz: 01feee252179dd66016a8633658631dacc2262f5598ee07d4f78ec2947ebbb57cd15b19b433b8ea9a87b7120fbf5afa984fd929677de0fcfaf7a4368e5b29d85
|
data/README.md
CHANGED
@@ -112,7 +112,6 @@ end
|
|
112
112
|
- **Typed Examples** - Type-safe training data with automatic validation
|
113
113
|
- **Evaluation Framework** - Advanced metrics beyond simple accuracy with error-resilient pipelines
|
114
114
|
- **MIPROv2 Optimization** - Advanced Bayesian optimization with Gaussian Processes, multiple optimization strategies, and storage persistence
|
115
|
-
- **GEPA Optimization** - Genetic-Pareto optimization for multi-objective prompt improvement
|
116
115
|
|
117
116
|
**Production Features:**
|
118
117
|
- **Reliable JSON Extraction** - Native structured outputs for OpenAI and Gemini, Anthropic tool-based extraction, and automatic strategy selection with fallback
|
@@ -168,11 +167,11 @@ For LLMs and AI assistants working with DSPy.rb:
|
|
168
167
|
- **[Evaluation Framework](docs/src/optimization/evaluation.md)** - Advanced metrics beyond simple accuracy
|
169
168
|
- **[Prompt Optimization](docs/src/optimization/prompt-optimization.md)** - Manipulate prompts as objects
|
170
169
|
- **[MIPROv2 Optimizer](docs/src/optimization/miprov2.md)** - Advanced Bayesian optimization with Gaussian Processes
|
171
|
-
- **[GEPA Optimizer](docs/src/optimization/gepa.md)**
|
170
|
+
- **[GEPA Optimizer](docs/src/optimization/gepa.md)** *(beta)* - Reflective mutation with optional reflection LMs
|
172
171
|
|
173
172
|
### Production Features
|
174
173
|
- **[Storage System](docs/src/production/storage.md)** - Persistence and optimization result storage
|
175
|
-
- **[Observability](docs/src/production/observability.md)** - Zero-config Langfuse integration
|
174
|
+
- **[Observability](docs/src/production/observability.md)** - Zero-config Langfuse integration with a dedicated export worker that never blocks your LLMs
|
176
175
|
|
177
176
|
### Advanced Usage
|
178
177
|
- **[Complex Types](docs/src/advanced/complex-types.md)** - Sorbet type integration with automatic coercion for structs, enums, and arrays
|
@@ -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/code_act.rb
CHANGED
@@ -146,6 +146,19 @@ module DSPy
|
|
146
146
|
super(enhanced_signature)
|
147
147
|
end
|
148
148
|
|
149
|
+
sig { override.returns(T::Array[[String, DSPy::Module]]) }
|
150
|
+
def named_predictors
|
151
|
+
pairs = T.let([], T::Array[[String, DSPy::Module]])
|
152
|
+
pairs << ["code_generator", @code_generator]
|
153
|
+
pairs << ["observation_processor", @observation_processor]
|
154
|
+
pairs
|
155
|
+
end
|
156
|
+
|
157
|
+
sig { override.returns(T::Array[DSPy::Module]) }
|
158
|
+
def predictors
|
159
|
+
named_predictors.map { |(_, predictor)| predictor }
|
160
|
+
end
|
161
|
+
|
149
162
|
sig { params(kwargs: T.untyped).returns(T.untyped).override }
|
150
163
|
def forward(**kwargs)
|
151
164
|
# Validate input and serialize all fields as task context
|
@@ -461,4 +474,4 @@ module DSPy
|
|
461
474
|
example
|
462
475
|
end
|
463
476
|
end
|
464
|
-
end
|
477
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'net/http'
|
5
|
+
require 'uri'
|
6
|
+
require 'cgi'
|
7
|
+
require 'fileutils'
|
8
|
+
|
9
|
+
module DSPy
|
10
|
+
module Datasets
|
11
|
+
module ADE
|
12
|
+
extend self
|
13
|
+
|
14
|
+
DATASET = 'ade-benchmark-corpus/ade_corpus_v2'
|
15
|
+
CLASSIFICATION_CONFIG = 'Ade_corpus_v2_classification'
|
16
|
+
BASE_URL = 'https://datasets-server.huggingface.co'
|
17
|
+
|
18
|
+
DEFAULT_CACHE_DIR = File.expand_path('../../../tmp/dspy_datasets/ade', __dir__)
|
19
|
+
|
20
|
+
MAX_BATCH_SIZE = 100
|
21
|
+
|
22
|
+
def examples(split: 'train', limit: 200, offset: 0, cache_dir: default_cache_dir)
|
23
|
+
remaining = limit
|
24
|
+
current_offset = offset
|
25
|
+
collected = []
|
26
|
+
|
27
|
+
while remaining.positive?
|
28
|
+
batch_size = [remaining, MAX_BATCH_SIZE].min
|
29
|
+
rows = fetch_rows(
|
30
|
+
split: split,
|
31
|
+
limit: batch_size,
|
32
|
+
offset: current_offset,
|
33
|
+
cache_dir: cache_dir
|
34
|
+
)
|
35
|
+
|
36
|
+
break if rows.empty?
|
37
|
+
|
38
|
+
collected.concat(rows.map do |row|
|
39
|
+
{
|
40
|
+
'text' => row.fetch('text', ''),
|
41
|
+
'label' => row.fetch('label', 0).to_i
|
42
|
+
}
|
43
|
+
end)
|
44
|
+
|
45
|
+
current_offset += batch_size
|
46
|
+
remaining -= batch_size
|
47
|
+
end
|
48
|
+
|
49
|
+
collected
|
50
|
+
end
|
51
|
+
|
52
|
+
def fetch_rows(split:, limit:, offset:, cache_dir:)
|
53
|
+
FileUtils.mkdir_p(cache_dir)
|
54
|
+
cache_path = File.join(cache_dir, "#{CLASSIFICATION_CONFIG}_#{split}_#{offset}_#{limit}.json")
|
55
|
+
|
56
|
+
if File.exist?(cache_path)
|
57
|
+
return JSON.parse(File.read(cache_path))
|
58
|
+
end
|
59
|
+
|
60
|
+
rows = request_rows(split: split, limit: limit, offset: offset)
|
61
|
+
File.write(cache_path, JSON.pretty_generate(rows))
|
62
|
+
rows
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def request_rows(split:, limit:, offset:)
|
68
|
+
uri = URI("#{BASE_URL}/rows")
|
69
|
+
params = {
|
70
|
+
dataset: DATASET,
|
71
|
+
config: CLASSIFICATION_CONFIG,
|
72
|
+
split: split,
|
73
|
+
offset: offset,
|
74
|
+
length: limit
|
75
|
+
}
|
76
|
+
uri.query = URI.encode_www_form(params)
|
77
|
+
|
78
|
+
response = Net::HTTP.get_response(uri)
|
79
|
+
raise "ADE dataset request failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
80
|
+
|
81
|
+
body = JSON.parse(response.body)
|
82
|
+
body.fetch('rows', []).map { |row| row.fetch('row', {}) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def default_cache_dir
|
86
|
+
ENV['DSPY_DATASETS_CACHE'] ? File.expand_path('ade', ENV['DSPY_DATASETS_CACHE']) : DEFAULT_CACHE_DIR
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
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
|
@@ -66,9 +67,6 @@ module DSPy
|
|
66
67
|
chat_with_strategy(messages, signature_class, &block)
|
67
68
|
end
|
68
69
|
|
69
|
-
# Emit the standard lm.tokens event (consistent with raw_chat)
|
70
|
-
emit_token_usage(response, signature_class.name)
|
71
|
-
|
72
70
|
# Parse response (no longer needs separate instrumentation)
|
73
71
|
parsed_result = parse_response(response, input_values, signature_class)
|
74
72
|
|
@@ -270,7 +268,7 @@ module DSPy
|
|
270
268
|
'dspy.signature' => signature_class_name
|
271
269
|
) do |span|
|
272
270
|
result = execution_block.call
|
273
|
-
|
271
|
+
|
274
272
|
# Add output and usage data directly to span
|
275
273
|
if span && result
|
276
274
|
# Add completion output
|
@@ -292,7 +290,9 @@ module DSPy
|
|
292
290
|
span.set_attribute('gen_ai.usage.total_tokens', usage.total_tokens) if usage.total_tokens
|
293
291
|
end
|
294
292
|
end
|
295
|
-
|
293
|
+
|
294
|
+
emit_token_usage(result, signature_class_name)
|
295
|
+
|
296
296
|
result
|
297
297
|
end
|
298
298
|
|
@@ -409,9 +409,6 @@ module DSPy
|
|
409
409
|
adapter.chat(messages: hash_messages, signature: nil, &streaming_block)
|
410
410
|
end
|
411
411
|
|
412
|
-
# Emit the standard lm.tokens event (consistent with other LM calls)
|
413
|
-
emit_token_usage(response, 'RawPrompt')
|
414
|
-
|
415
412
|
# Return raw response content, not parsed JSON
|
416
413
|
response.content
|
417
414
|
ensure
|
@@ -19,20 +19,20 @@ module DSPy
|
|
19
19
|
|
20
20
|
Class.new(T::Struct) do
|
21
21
|
extend T::Sig
|
22
|
+
define_field = lambda do |name, type, options|
|
23
|
+
const_kwargs = {}
|
24
|
+
const_kwargs[:default] = options[:default] if options.key?(:default)
|
25
|
+
const_kwargs[:factory] = options[:factory] if options.key?(:factory)
|
26
|
+
const_kwargs[:override] = true if props.key?(name)
|
27
|
+
const name, type, **const_kwargs
|
28
|
+
end
|
22
29
|
|
23
30
|
# Add properties from each source
|
24
31
|
property_sources.each do |_source_name, props|
|
25
32
|
props.each do |name, prop|
|
26
33
|
type = builder.send(:extract_type_from_prop, prop)
|
27
34
|
options = builder.send(:extract_options_from_prop, prop)
|
28
|
-
|
29
|
-
if options[:default]
|
30
|
-
const name, type, default: options[:default]
|
31
|
-
elsif options[:factory]
|
32
|
-
const name, type, factory: options[:factory]
|
33
|
-
else
|
34
|
-
const name, type
|
35
|
-
end
|
35
|
+
define_field.call(name, type, options)
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
@@ -40,14 +40,7 @@ module DSPy
|
|
40
40
|
additional_fields.each do |name, field_config|
|
41
41
|
type = builder.send(:extract_type_from_prop, field_config)
|
42
42
|
options = builder.send(:extract_options_from_prop, field_config)
|
43
|
-
|
44
|
-
if options[:default]
|
45
|
-
const name, type, default: options[:default]
|
46
|
-
elsif options[:factory]
|
47
|
-
const name, type, factory: options[:factory]
|
48
|
-
else
|
49
|
-
const name, type
|
50
|
-
end
|
43
|
+
define_field.call(name, type, options)
|
51
44
|
end
|
52
45
|
|
53
46
|
include StructSerialization
|
@@ -65,14 +58,13 @@ module DSPy
|
|
65
58
|
def build_single_property(name, prop)
|
66
59
|
type = extract_type_from_prop(prop)
|
67
60
|
options = extract_options_from_prop(prop)
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
end
|
61
|
+
|
62
|
+
const_kwargs = {}
|
63
|
+
const_kwargs[:default] = options[:default] if options.key?(:default)
|
64
|
+
const_kwargs[:factory] = options[:factory] if options.key?(:factory)
|
65
|
+
const_kwargs[:override] = true if respond_to?(:props) && props.key?(name)
|
66
|
+
|
67
|
+
const name, type, **const_kwargs
|
76
68
|
end
|
77
69
|
|
78
70
|
# Extracts type from property configuration
|
@@ -142,4 +134,4 @@ module DSPy
|
|
142
134
|
end
|
143
135
|
end
|
144
136
|
end
|
145
|
-
end
|
137
|
+
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,42 @@ 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
|
108
|
+
|
109
|
+
# Discover nested predictor modules (Python parity helper)
|
110
|
+
sig { returns(T::Array[[String, DSPy::Module]]) }
|
111
|
+
def named_predictors
|
112
|
+
[]
|
113
|
+
end
|
114
|
+
|
115
|
+
sig { returns(T::Array[DSPy::Module]) }
|
116
|
+
def predictors
|
117
|
+
named_predictors.map { |(_, predictor)| predictor }
|
118
|
+
end
|
75
119
|
end
|
76
|
-
end
|
120
|
+
end
|