dspy 0.28.0 → 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 +4 -4
- data/lib/dspy/callbacks.rb +222 -0
- data/lib/dspy/chain_of_thought.rb +2 -1
- data/lib/dspy/lm/adapters/gemini/schema_converter.rb +25 -16
- data/lib/dspy/lm/json_strategy.rb +0 -5
- data/lib/dspy/lm.rb +38 -9
- data/lib/dspy/mixins/type_coercion.rb +7 -7
- data/lib/dspy/module.rb +33 -0
- data/lib/dspy/predict.rb +7 -0
- data/lib/dspy/prompt.rb +90 -20
- data/lib/dspy/propose/dataset_summary_generator.rb +177 -0
- data/lib/dspy/propose/grounded_proposer.rb +208 -61
- data/lib/dspy/structured_outputs_prompt.rb +53 -0
- data/lib/dspy/teleprompt/bootstrap_strategy.rb +26 -0
- data/lib/dspy/teleprompt/mipro_v2.rb +81 -56
- data/lib/dspy/teleprompt/simple_optimizer.rb +40 -34
- data/lib/dspy/teleprompt/utils.rb +343 -41
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +1 -0
- metadata +20 -2
@@ -294,18 +294,18 @@ module DSPy
|
|
294
294
|
|
295
295
|
# Phase 1: Bootstrap few-shot examples
|
296
296
|
emit_event('phase_start', { phase: 1, name: 'bootstrap' })
|
297
|
-
|
298
|
-
emit_event('phase_complete', {
|
299
|
-
phase: 1,
|
300
|
-
|
301
|
-
|
297
|
+
demo_candidates = phase_1_bootstrap(program, typed_trainset)
|
298
|
+
emit_event('phase_complete', {
|
299
|
+
phase: 1,
|
300
|
+
num_predictors: demo_candidates.keys.size,
|
301
|
+
demo_sets_per_predictor: demo_candidates[0]&.size || 0
|
302
302
|
})
|
303
303
|
|
304
304
|
# Phase 2: Generate instruction candidates
|
305
305
|
emit_event('phase_start', { phase: 2, name: 'instruction_proposal' })
|
306
|
-
proposal_result = phase_2_propose_instructions(program, typed_trainset,
|
307
|
-
emit_event('phase_complete', {
|
308
|
-
phase: 2,
|
306
|
+
proposal_result = phase_2_propose_instructions(program, typed_trainset, demo_candidates)
|
307
|
+
emit_event('phase_complete', {
|
308
|
+
phase: 2,
|
309
309
|
num_candidates: proposal_result.num_candidates,
|
310
310
|
best_instruction_preview: proposal_result.best_instruction[0, 50]
|
311
311
|
})
|
@@ -316,7 +316,7 @@ module DSPy
|
|
316
316
|
program,
|
317
317
|
evaluation_set,
|
318
318
|
proposal_result,
|
319
|
-
|
319
|
+
demo_candidates
|
320
320
|
)
|
321
321
|
emit_event('phase_complete', {
|
322
322
|
phase: 3,
|
@@ -327,7 +327,7 @@ module DSPy
|
|
327
327
|
# Build final result
|
328
328
|
final_result = build_miprov2_result(
|
329
329
|
optimization_result,
|
330
|
-
|
330
|
+
demo_candidates,
|
331
331
|
proposal_result
|
332
332
|
)
|
333
333
|
|
@@ -339,16 +339,17 @@ module DSPy
|
|
339
339
|
private
|
340
340
|
|
341
341
|
# Phase 1: Bootstrap few-shot examples from training data
|
342
|
-
|
342
|
+
# Returns a hash mapping predictor indices to arrays of demo sets
|
343
|
+
sig { params(program: T.untyped, trainset: T::Array[DSPy::Example]).returns(T::Hash[Integer, T::Array[T::Array[DSPy::FewShotExample]]]) }
|
343
344
|
def phase_1_bootstrap(program, trainset)
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
345
|
+
Utils.create_n_fewshot_demo_sets(
|
346
|
+
program,
|
347
|
+
config.bootstrap_sets, # num_candidate_sets
|
348
|
+
trainset,
|
349
|
+
max_bootstrapped_demos: config.max_bootstrapped_examples,
|
350
|
+
max_labeled_demos: config.max_labeled_examples,
|
351
|
+
metric: @metric
|
352
|
+
)
|
352
353
|
end
|
353
354
|
|
354
355
|
# Phase 2: Generate instruction candidates using grounded proposer
|
@@ -356,22 +357,31 @@ module DSPy
|
|
356
357
|
params(
|
357
358
|
program: T.untyped,
|
358
359
|
trainset: T::Array[DSPy::Example],
|
359
|
-
|
360
|
+
demo_candidates: T::Hash[Integer, T::Array[T::Array[DSPy::FewShotExample]]]
|
360
361
|
).returns(DSPy::Propose::GroundedProposer::ProposalResult)
|
361
362
|
end
|
362
|
-
def phase_2_propose_instructions(program, trainset,
|
363
|
+
def phase_2_propose_instructions(program, trainset, demo_candidates)
|
363
364
|
# Get current instruction if available
|
364
365
|
current_instruction = extract_current_instruction(program)
|
365
|
-
|
366
|
+
|
366
367
|
# Use few-shot examples from bootstrap if available
|
367
|
-
|
368
|
+
# Flatten demo sets from first predictor and take first 5 examples
|
369
|
+
few_shot_examples = demo_candidates[0]&.flatten&.take(5) || []
|
368
370
|
|
369
371
|
# Get signature class from program
|
370
372
|
signature_class = extract_signature_class(program)
|
371
373
|
raise ArgumentError, "Cannot extract signature class from program" unless signature_class
|
372
374
|
|
373
|
-
#
|
374
|
-
|
375
|
+
# Re-initialize proposer with program and trainset for awareness features
|
376
|
+
# This enables program_aware and use_dataset_summary flags to work correctly
|
377
|
+
proposer_config = DSPy::Propose::GroundedProposer::Config.new
|
378
|
+
proposer_config.num_instruction_candidates = config.num_instruction_candidates
|
379
|
+
|
380
|
+
@proposer = DSPy::Propose::GroundedProposer.new(
|
381
|
+
config: proposer_config,
|
382
|
+
program: program,
|
383
|
+
trainset: trainset
|
384
|
+
)
|
375
385
|
|
376
386
|
@proposer.propose_instructions(
|
377
387
|
signature_class,
|
@@ -387,12 +397,12 @@ module DSPy
|
|
387
397
|
program: T.untyped,
|
388
398
|
evaluation_set: T::Array[DSPy::Example],
|
389
399
|
proposal_result: DSPy::Propose::GroundedProposer::ProposalResult,
|
390
|
-
|
400
|
+
demo_candidates: T::Hash[Integer, T::Array[T::Array[DSPy::FewShotExample]]]
|
391
401
|
).returns(T::Hash[Symbol, T.untyped])
|
392
402
|
end
|
393
|
-
def phase_3_optimize(program, evaluation_set, proposal_result,
|
403
|
+
def phase_3_optimize(program, evaluation_set, proposal_result, demo_candidates)
|
394
404
|
# Generate candidate configurations
|
395
|
-
candidates = generate_candidate_configurations(proposal_result,
|
405
|
+
candidates = generate_candidate_configurations(proposal_result, demo_candidates)
|
396
406
|
|
397
407
|
# Initialize optimization state
|
398
408
|
optimization_state = initialize_optimization_state(candidates)
|
@@ -468,16 +478,16 @@ module DSPy
|
|
468
478
|
}
|
469
479
|
end
|
470
480
|
|
471
|
-
# Generate candidate configurations from proposals and
|
481
|
+
# Generate candidate configurations from proposals and demo candidates
|
472
482
|
sig do
|
473
483
|
params(
|
474
484
|
proposal_result: DSPy::Propose::GroundedProposer::ProposalResult,
|
475
|
-
|
485
|
+
demo_candidates: T::Hash[Integer, T::Array[T::Array[DSPy::FewShotExample]]]
|
476
486
|
).returns(T::Array[EvaluatedCandidate])
|
477
487
|
end
|
478
|
-
def generate_candidate_configurations(proposal_result,
|
488
|
+
def generate_candidate_configurations(proposal_result, demo_candidates)
|
479
489
|
candidates = []
|
480
|
-
|
490
|
+
|
481
491
|
# Base configuration (no modifications)
|
482
492
|
candidates << EvaluatedCandidate.new(
|
483
493
|
instruction: "",
|
@@ -486,7 +496,7 @@ module DSPy
|
|
486
496
|
metadata: {},
|
487
497
|
config_id: SecureRandom.hex(6)
|
488
498
|
)
|
489
|
-
|
499
|
+
|
490
500
|
# Instruction-only candidates
|
491
501
|
proposal_result.candidate_instructions.each_with_index do |instruction, idx|
|
492
502
|
candidates << EvaluatedCandidate.new(
|
@@ -497,12 +507,14 @@ module DSPy
|
|
497
507
|
config_id: SecureRandom.hex(6)
|
498
508
|
)
|
499
509
|
end
|
500
|
-
|
510
|
+
|
501
511
|
# Few-shot only candidates
|
502
|
-
|
512
|
+
# Extract demo sets from first predictor (predictor index 0)
|
513
|
+
demo_sets = demo_candidates[0] || []
|
514
|
+
demo_sets.each_with_index do |demo_set, idx|
|
503
515
|
candidates << EvaluatedCandidate.new(
|
504
516
|
instruction: "",
|
505
|
-
few_shot_examples:
|
517
|
+
few_shot_examples: demo_set,
|
506
518
|
type: CandidateType::FewShotOnly,
|
507
519
|
metadata: { bootstrap_rank: idx },
|
508
520
|
config_id: SecureRandom.hex(6)
|
@@ -511,7 +523,7 @@ module DSPy
|
|
511
523
|
|
512
524
|
# Combined candidates (instruction + few-shot)
|
513
525
|
top_instructions = proposal_result.candidate_instructions.take(3)
|
514
|
-
top_bootstrap_sets =
|
526
|
+
top_bootstrap_sets = demo_sets.take(3)
|
515
527
|
|
516
528
|
top_instructions.each_with_index do |instruction, i_idx|
|
517
529
|
top_bootstrap_sets.each_with_index do |candidate_set, b_idx|
|
@@ -685,10 +697,10 @@ module DSPy
|
|
685
697
|
features << ((config_hash / 1000) % 1000).to_f / 1000.0 # Feature 2: different part of hash
|
686
698
|
features << ((config_hash / 1_000_000) % 1000).to_f / 1000.0 # Feature 3: high bits
|
687
699
|
|
688
|
-
# Add instruction length if available
|
700
|
+
# Add instruction length if available (Python-compatible: no cap)
|
689
701
|
instruction = candidate.instruction
|
690
702
|
if instruction && !instruction.empty?
|
691
|
-
features <<
|
703
|
+
features << instruction.length.to_f / 100.0 # Instruction length, uncapped
|
692
704
|
else
|
693
705
|
features << 0.5 # Default value
|
694
706
|
end
|
@@ -731,11 +743,17 @@ module DSPy
|
|
731
743
|
# Apply few-shot examples if provided
|
732
744
|
if candidate.few_shot_examples.any? && program.respond_to?(:with_examples)
|
733
745
|
few_shot_examples = candidate.few_shot_examples.map do |example|
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
746
|
+
# If already a FewShotExample, use it directly
|
747
|
+
if example.is_a?(DSPy::FewShotExample)
|
748
|
+
example
|
749
|
+
else
|
750
|
+
# Convert from DSPy::Example
|
751
|
+
DSPy::FewShotExample.new(
|
752
|
+
input: example.input_values,
|
753
|
+
output: example.expected_values,
|
754
|
+
reasoning: extract_reasoning_from_example(example)
|
755
|
+
)
|
756
|
+
end
|
739
757
|
end
|
740
758
|
modified_program = modified_program.with_examples(few_shot_examples)
|
741
759
|
end
|
@@ -779,39 +797,38 @@ module DSPy
|
|
779
797
|
state[:no_improvement_count] >= config.early_stopping_patience
|
780
798
|
end
|
781
799
|
|
782
|
-
# Calculate diversity score for candidate
|
800
|
+
# Calculate diversity score for candidate (Python-compatible: only few-shot count)
|
783
801
|
sig { params(candidate: EvaluatedCandidate).returns(Float) }
|
784
802
|
def calculate_diversity_score(candidate)
|
785
|
-
#
|
786
|
-
instruction_diversity = candidate.instruction.length / 200.0
|
803
|
+
# Python DSPy doesn't use instruction length for diversity, only few-shot count
|
787
804
|
few_shot_diversity = candidate.few_shot_examples.size / 10.0
|
788
|
-
|
789
|
-
[
|
805
|
+
|
806
|
+
[few_shot_diversity, 1.0].min
|
790
807
|
end
|
791
808
|
|
792
809
|
# Build final MIPROv2 result
|
793
810
|
sig do
|
794
811
|
params(
|
795
812
|
optimization_result: T::Hash[Symbol, T.untyped],
|
796
|
-
|
813
|
+
demo_candidates: T::Hash[Integer, T::Array[T::Array[DSPy::FewShotExample]]],
|
797
814
|
proposal_result: DSPy::Propose::GroundedProposer::ProposalResult
|
798
815
|
).returns(MIPROv2Result)
|
799
816
|
end
|
800
|
-
def build_miprov2_result(optimization_result,
|
817
|
+
def build_miprov2_result(optimization_result, demo_candidates, proposal_result)
|
801
818
|
best_candidate = optimization_result[:best_candidate]
|
802
819
|
best_program = optimization_result[:best_program]
|
803
820
|
best_score = optimization_result[:best_score]
|
804
821
|
best_evaluation_result = optimization_result[:best_evaluation_result]
|
805
|
-
|
822
|
+
|
806
823
|
scores = { pass_rate: best_score }
|
807
|
-
|
824
|
+
|
808
825
|
history = {
|
809
826
|
total_trials: optimization_result[:trials_completed],
|
810
827
|
optimization_strategy: config.optimization_strategy,
|
811
828
|
early_stopped: optimization_result[:trials_completed] < config.num_trials,
|
812
829
|
score_history: optimization_result[:optimization_state][:best_score_history]
|
813
830
|
}
|
814
|
-
|
831
|
+
|
815
832
|
metadata = {
|
816
833
|
optimizer: "MIPROv2",
|
817
834
|
auto_mode: infer_auto_mode,
|
@@ -820,7 +837,15 @@ module DSPy
|
|
820
837
|
best_candidate_type: best_candidate&.type&.serialize || "unknown",
|
821
838
|
optimization_timestamp: Time.now.iso8601
|
822
839
|
}
|
823
|
-
|
840
|
+
|
841
|
+
# Create bootstrap statistics from demo_candidates
|
842
|
+
demo_sets = demo_candidates[0] || []
|
843
|
+
bootstrap_statistics = {
|
844
|
+
num_predictors: demo_candidates.keys.size,
|
845
|
+
demo_sets_per_predictor: demo_sets.size,
|
846
|
+
avg_demos_per_set: demo_sets.empty? ? 0 : demo_sets.map(&:size).sum.to_f / demo_sets.size
|
847
|
+
}
|
848
|
+
|
824
849
|
MIPROv2Result.new(
|
825
850
|
optimized_program: best_program,
|
826
851
|
scores: scores,
|
@@ -830,7 +855,7 @@ module DSPy
|
|
830
855
|
metadata: metadata,
|
831
856
|
evaluated_candidates: @evaluated_candidates,
|
832
857
|
optimization_trace: serialize_optimization_trace(optimization_result[:optimization_state]),
|
833
|
-
bootstrap_statistics:
|
858
|
+
bootstrap_statistics: bootstrap_statistics,
|
834
859
|
proposal_statistics: proposal_result.analysis,
|
835
860
|
best_evaluation_result: best_evaluation_result
|
836
861
|
)
|
@@ -142,15 +142,15 @@ module DSPy
|
|
142
142
|
evaluation_set = typed_valset || typed_trainset.take(10)
|
143
143
|
|
144
144
|
# Bootstrap few-shot examples if enabled
|
145
|
-
|
145
|
+
demo_candidates = nil
|
146
146
|
if @optimizer_config.use_few_shot_optimization
|
147
|
-
|
147
|
+
demo_candidates = bootstrap_examples(program, typed_trainset)
|
148
148
|
end
|
149
149
|
|
150
150
|
# Generate instruction candidates if enabled
|
151
151
|
instruction_candidates = []
|
152
152
|
if @optimizer_config.use_instruction_optimization && @proposer
|
153
|
-
instruction_candidates = generate_instruction_candidates(program, typed_trainset,
|
153
|
+
instruction_candidates = generate_instruction_candidates(program, typed_trainset, demo_candidates)
|
154
154
|
end
|
155
155
|
|
156
156
|
# Run optimization trials
|
@@ -158,7 +158,7 @@ module DSPy
|
|
158
158
|
program,
|
159
159
|
evaluation_set,
|
160
160
|
instruction_candidates,
|
161
|
-
|
161
|
+
demo_candidates
|
162
162
|
)
|
163
163
|
|
164
164
|
# Find best trial
|
@@ -175,16 +175,18 @@ module DSPy
|
|
175
175
|
private
|
176
176
|
|
177
177
|
# Bootstrap few-shot examples from training set
|
178
|
-
sig { params(program: T.untyped, trainset: T::Array[DSPy::Example]).returns(
|
178
|
+
sig { params(program: T.untyped, trainset: T::Array[DSPy::Example]).returns(T::Hash[Integer, T::Array[T::Array[DSPy::FewShotExample]]]) }
|
179
179
|
def bootstrap_examples(program, trainset)
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
180
|
+
num_candidate_sets = [@optimizer_config.num_trials / 2, 5].max
|
181
|
+
|
182
|
+
Utils.create_n_fewshot_demo_sets(
|
183
|
+
program,
|
184
|
+
num_candidate_sets,
|
185
|
+
trainset,
|
186
|
+
max_bootstrapped_demos: @optimizer_config.max_bootstrapped_examples,
|
187
|
+
max_labeled_demos: @optimizer_config.max_labeled_examples,
|
188
|
+
metric: @metric
|
189
|
+
)
|
188
190
|
end
|
189
191
|
|
190
192
|
# Generate instruction candidates using the proposer
|
@@ -192,17 +194,18 @@ module DSPy
|
|
192
194
|
params(
|
193
195
|
program: T.untyped,
|
194
196
|
trainset: T::Array[DSPy::Example],
|
195
|
-
|
197
|
+
demo_candidates: T.nilable(T::Hash[Integer, T::Array[T::Array[DSPy::FewShotExample]]])
|
196
198
|
).returns(T::Array[String])
|
197
199
|
end
|
198
|
-
def generate_instruction_candidates(program, trainset,
|
200
|
+
def generate_instruction_candidates(program, trainset, demo_candidates)
|
199
201
|
return [] unless @proposer
|
200
202
|
|
201
203
|
# Get current instruction if available
|
202
204
|
current_instruction = extract_current_instruction(program)
|
203
|
-
|
205
|
+
|
204
206
|
# Use few-shot examples from bootstrap if available
|
205
|
-
|
207
|
+
# Flatten demo sets from first predictor and take first 5 examples
|
208
|
+
few_shot_examples = demo_candidates&.dig(0)&.flatten&.take(5) || []
|
206
209
|
|
207
210
|
# Get signature class from program
|
208
211
|
signature_class = extract_signature_class(program)
|
@@ -224,14 +227,14 @@ module DSPy
|
|
224
227
|
program: T.untyped,
|
225
228
|
evaluation_set: T::Array[DSPy::Example],
|
226
229
|
instruction_candidates: T::Array[String],
|
227
|
-
|
230
|
+
demo_candidates: T.nilable(T::Hash[Integer, T::Array[T::Array[DSPy::FewShotExample]]])
|
228
231
|
).returns(T::Array[TrialResult])
|
229
232
|
end
|
230
|
-
def run_optimization_trials(program, evaluation_set, instruction_candidates,
|
233
|
+
def run_optimization_trials(program, evaluation_set, instruction_candidates, demo_candidates)
|
231
234
|
trials = []
|
232
|
-
|
235
|
+
|
233
236
|
# Generate trial configurations
|
234
|
-
trial_configs = generate_trial_configurations(instruction_candidates,
|
237
|
+
trial_configs = generate_trial_configurations(instruction_candidates, demo_candidates)
|
235
238
|
|
236
239
|
trial_configs.take(@optimizer_config.num_trials).each_with_index do |config, index|
|
237
240
|
trial_number = index + 1
|
@@ -270,36 +273,39 @@ module DSPy
|
|
270
273
|
sig do
|
271
274
|
params(
|
272
275
|
instruction_candidates: T::Array[String],
|
273
|
-
|
276
|
+
demo_candidates: T.nilable(T::Hash[Integer, T::Array[T::Array[DSPy::FewShotExample]]])
|
274
277
|
).returns(T::Array[T::Hash[Symbol, T.untyped]])
|
275
278
|
end
|
276
|
-
def generate_trial_configurations(instruction_candidates,
|
279
|
+
def generate_trial_configurations(instruction_candidates, demo_candidates)
|
277
280
|
configs = []
|
278
|
-
|
281
|
+
|
282
|
+
# Extract demo sets from first predictor
|
283
|
+
demo_sets = demo_candidates&.dig(0) || []
|
284
|
+
|
279
285
|
# Base configuration (no changes)
|
280
286
|
configs << { instruction: nil, few_shot_examples: [] }
|
281
|
-
|
287
|
+
|
282
288
|
# Instruction-only trials
|
283
289
|
instruction_candidates.each do |instruction|
|
284
290
|
configs << { instruction: instruction, few_shot_examples: [] }
|
285
291
|
end
|
286
|
-
|
292
|
+
|
287
293
|
# Few-shot only trials
|
288
|
-
if
|
289
|
-
|
290
|
-
configs << { instruction: nil, few_shot_examples:
|
294
|
+
if demo_sets.any?
|
295
|
+
demo_sets.each do |demo_set|
|
296
|
+
configs << { instruction: nil, few_shot_examples: demo_set }
|
291
297
|
end
|
292
298
|
end
|
293
|
-
|
299
|
+
|
294
300
|
# Combined instruction + few-shot trials
|
295
|
-
if instruction_candidates.any? &&
|
301
|
+
if instruction_candidates.any? && demo_sets.any?
|
296
302
|
instruction_candidates.take(3).each do |instruction|
|
297
|
-
|
298
|
-
configs << { instruction: instruction, few_shot_examples:
|
303
|
+
demo_sets.take(2).each do |demo_set|
|
304
|
+
configs << { instruction: instruction, few_shot_examples: demo_set }
|
299
305
|
end
|
300
306
|
end
|
301
307
|
end
|
302
|
-
|
308
|
+
|
303
309
|
# Shuffle for random strategy
|
304
310
|
if @optimizer_config.search_strategy == "random"
|
305
311
|
configs.shuffle
|