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.
@@ -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
- bootstrap_result = phase_1_bootstrap(program, typed_trainset)
298
- emit_event('phase_complete', {
299
- phase: 1,
300
- success_rate: bootstrap_result.statistics[:success_rate],
301
- candidate_sets: bootstrap_result.candidate_sets.size
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, bootstrap_result)
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
- bootstrap_result
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
- bootstrap_result,
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
- sig { params(program: T.untyped, trainset: T::Array[DSPy::Example]).returns(Utils::BootstrapResult) }
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
- bootstrap_config = Utils::BootstrapConfig.new
345
- bootstrap_config.max_bootstrapped_examples = config.max_bootstrapped_examples
346
- bootstrap_config.max_labeled_examples = config.max_labeled_examples
347
- bootstrap_config.num_candidate_sets = config.bootstrap_sets
348
- bootstrap_config.max_errors = config.max_errors
349
- bootstrap_config.num_threads = config.num_threads
350
-
351
- Utils.create_n_fewshot_demo_sets(program, trainset, config: bootstrap_config, metric: @metric)
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
- bootstrap_result: Utils::BootstrapResult
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, bootstrap_result)
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
- few_shot_examples = bootstrap_result.successful_examples.take(5)
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
- # Configure proposer for this optimization run
374
- @proposer.config.num_instruction_candidates = config.num_instruction_candidates
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
- bootstrap_result: Utils::BootstrapResult
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, bootstrap_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, bootstrap_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 bootstrap results
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
- bootstrap_result: Utils::BootstrapResult
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, bootstrap_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
- bootstrap_result.candidate_sets.each_with_index do |candidate_set, idx|
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: candidate_set,
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 = bootstrap_result.candidate_sets.take(3)
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 << [instruction.length.to_f / 100.0, 2.0].min # Instruction length, capped at 200 chars
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
- DSPy::FewShotExample.new(
735
- input: example.input_values,
736
- output: example.expected_values,
737
- reasoning: extract_reasoning_from_example(example)
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
- # Simple diversity metric based on instruction length and few-shot count
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
- [instruction_diversity + few_shot_diversity, 1.0].min
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
- bootstrap_result: Utils::BootstrapResult,
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, bootstrap_result, proposal_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: bootstrap_result.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
- bootstrap_result = nil
145
+ demo_candidates = nil
146
146
  if @optimizer_config.use_few_shot_optimization
147
- bootstrap_result = bootstrap_examples(program, typed_trainset)
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, bootstrap_result)
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
- bootstrap_result
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(Utils::BootstrapResult) }
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
- bootstrap_config = Utils::BootstrapConfig.new
181
- bootstrap_config.max_bootstrapped_examples = @optimizer_config.max_bootstrapped_examples
182
- bootstrap_config.max_labeled_examples = @optimizer_config.max_labeled_examples
183
- bootstrap_config.num_candidate_sets = [@optimizer_config.num_trials / 2, 5].max
184
- bootstrap_config.max_errors = @optimizer_config.max_errors
185
- bootstrap_config.num_threads = @optimizer_config.num_threads
186
-
187
- Utils.create_n_fewshot_demo_sets(program, trainset, config: bootstrap_config, metric: @metric)
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
- bootstrap_result: T.nilable(Utils::BootstrapResult)
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, bootstrap_result)
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
- few_shot_examples = bootstrap_result&.successful_examples&.take(5)
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
- bootstrap_result: T.nilable(Utils::BootstrapResult)
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, bootstrap_result)
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, bootstrap_result)
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
- bootstrap_result: T.nilable(Utils::BootstrapResult)
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, bootstrap_result)
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 bootstrap_result&.candidate_sets&.any?
289
- bootstrap_result.candidate_sets.each do |candidate_set|
290
- configs << { instruction: nil, few_shot_examples: candidate_set }
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? && bootstrap_result&.candidate_sets&.any?
301
+ if instruction_candidates.any? && demo_sets.any?
296
302
  instruction_candidates.take(3).each do |instruction|
297
- bootstrap_result.candidate_sets.take(2).each do |candidate_set|
298
- configs << { instruction: instruction, few_shot_examples: candidate_set }
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