dspy 0.29.0 → 0.29.1

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: 747119ce407283e4d8ed5f01014262f24a94418ad2cbef4305a28b21cb58c8bc
4
- data.tar.gz: 3693faccd1fca98015864fd4404491b619b2aa600ab83a78dd3fc7d9e3342ef1
3
+ metadata.gz: 42a6bfa4a1e0fe7b7dd38e3a2fe017959365f566248319e1deec6cadcd06efc3
4
+ data.tar.gz: ffceb724c96e7803ec7531bf52c12f502266e21d4ab9bcf534a8005e59835883
5
5
  SHA512:
6
- metadata.gz: 7fecac3bc3389e11bdb2328234455cbbbcbf6c7544518cbf94082e75a3f0bde489339b70f91dfd31a43bbd87b17451bfb5a78387c90f5b669aefa09e8af73500
7
- data.tar.gz: 01feee252179dd66016a8633658631dacc2262f5598ee07d4f78ec2947ebbb57cd15b19b433b8ea9a87b7120fbf5afa984fd929677de0fcfaf7a4368e5b29d85
6
+ metadata.gz: 2d81f0ab86954523f8b3511f3105f8005b9709f0a46fb08f0403dcac841c70efcfbff90dcdedccaf322554280f62bdbab6a36be34d0e787f01734d8ee2219bf6
7
+ data.tar.gz: 31d048ad3d7493be0c52a24729d033ce58a891039c180945aa80d54afb7a529b6e420ccfc49fdab32ef00803483ecbb63dd2c2bd9d15f569c44debfdfd6dea0c
data/README.md CHANGED
@@ -111,7 +111,7 @@ end
111
111
  - **Prompt Objects** - Manipulate prompts as first-class objects instead of strings
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
- - **MIPROv2 Optimization** - Advanced Bayesian optimization with Gaussian Processes, multiple optimization strategies, and storage persistence
114
+ - **MIPROv2 Optimization** - Advanced Bayesian optimization with Gaussian Processes, multiple optimization strategies, auto-config presets, and storage persistence
115
115
 
116
116
  **Production Features:**
117
117
  - **Reliable JSON Extraction** - Native structured outputs for OpenAI and Gemini, Anthropic tool-based extraction, and automatic strategy selection with fallback
@@ -2,9 +2,11 @@
2
2
 
3
3
  require 'digest'
4
4
  require 'time'
5
+ require 'json'
5
6
  require 'concurrent-ruby'
6
7
  require 'sorbet-runtime'
7
8
  require 'securerandom'
9
+ require 'set'
8
10
  require_relative 'teleprompter'
9
11
  require_relative 'utils'
10
12
  require_relative '../propose/grounded_proposer'
@@ -30,6 +32,58 @@ module DSPy
30
32
  Bayesian = new("bayesian")
31
33
  end
32
34
  end
35
+
36
+ class AutoPreset < T::Enum
37
+ enums do
38
+ None = new("none")
39
+ Light = new("light")
40
+ Medium = new("medium")
41
+ Heavy = new("heavy")
42
+ end
43
+ end
44
+
45
+ AUTO_PRESET_SETTINGS = {
46
+ AutoPreset::None => {},
47
+ AutoPreset::Light => {
48
+ candidate_budget: 6,
49
+ instruction_candidates: 3,
50
+ instruction_candidates_when_fewshot: 3,
51
+ bootstrap_sets: 3,
52
+ max_bootstrapped_examples: 2,
53
+ max_labeled_examples: 8,
54
+ optimization_strategy: OptimizationStrategy::Greedy,
55
+ early_stopping_patience: 2,
56
+ valset_target_size: 100,
57
+ minibatch_size: nil
58
+ },
59
+ AutoPreset::Medium => {
60
+ candidate_budget: 12,
61
+ instruction_candidates: 5,
62
+ instruction_candidates_when_fewshot: 5,
63
+ bootstrap_sets: 5,
64
+ max_bootstrapped_examples: 4,
65
+ max_labeled_examples: 16,
66
+ optimization_strategy: OptimizationStrategy::Adaptive,
67
+ early_stopping_patience: 3,
68
+ valset_target_size: 300,
69
+ minibatch_size: nil
70
+ },
71
+ AutoPreset::Heavy => {
72
+ candidate_budget: 18,
73
+ instruction_candidates: 8,
74
+ instruction_candidates_when_fewshot: 8,
75
+ bootstrap_sets: 8,
76
+ max_bootstrapped_examples: 6,
77
+ max_labeled_examples: 24,
78
+ optimization_strategy: OptimizationStrategy::Bayesian,
79
+ early_stopping_patience: 5,
80
+ valset_target_size: 1000,
81
+ minibatch_size: nil
82
+ }
83
+ }.freeze
84
+
85
+ DEFAULT_AUTO_SEED = 42
86
+
33
87
  # MIPROv2: Multi-prompt Instruction Proposal with Retrieval Optimization
34
88
  # State-of-the-art prompt optimization combining bootstrap sampling,
35
89
  # instruction generation, and Bayesian optimization
@@ -50,13 +104,7 @@ module DSPy
50
104
  def self.light(metric: nil, **kwargs)
51
105
  optimizer = MIPROv2.new(metric: metric, **kwargs)
52
106
  optimizer.configure do |config|
53
- config.num_trials = 6
54
- config.num_instruction_candidates = 3
55
- config.max_bootstrapped_examples = 2
56
- config.max_labeled_examples = 8
57
- config.bootstrap_sets = 3
58
- config.optimization_strategy = :greedy
59
- config.early_stopping_patience = 2
107
+ MIPROv2.apply_auto_defaults(config, AutoPreset::Light)
60
108
  end
61
109
  optimizer
62
110
  end
@@ -70,13 +118,7 @@ module DSPy
70
118
  def self.medium(metric: nil, **kwargs)
71
119
  optimizer = MIPROv2.new(metric: metric, **kwargs)
72
120
  optimizer.configure do |config|
73
- config.num_trials = 12
74
- config.num_instruction_candidates = 5
75
- config.max_bootstrapped_examples = 4
76
- config.max_labeled_examples = 16
77
- config.bootstrap_sets = 5
78
- config.optimization_strategy = :adaptive
79
- config.early_stopping_patience = 3
121
+ MIPROv2.apply_auto_defaults(config, AutoPreset::Medium)
80
122
  end
81
123
  optimizer
82
124
  end
@@ -90,19 +132,33 @@ module DSPy
90
132
  def self.heavy(metric: nil, **kwargs)
91
133
  optimizer = MIPROv2.new(metric: metric, **kwargs)
92
134
  optimizer.configure do |config|
93
- config.num_trials = 18
94
- config.num_instruction_candidates = 8
95
- config.max_bootstrapped_examples = 6
96
- config.max_labeled_examples = 24
97
- config.bootstrap_sets = 8
98
- config.optimization_strategy = :bayesian
99
- config.early_stopping_patience = 5
135
+ MIPROv2.apply_auto_defaults(config, AutoPreset::Heavy)
100
136
  end
101
137
  optimizer
102
138
  end
103
139
  end
104
140
 
105
141
  # Dry-configurable settings for MIPROv2
142
+ setting :auto_preset, default: AutoPreset::None, constructor: ->(value) {
143
+ case value
144
+ when AutoPreset
145
+ value
146
+ when String, Symbol
147
+ begin
148
+ AutoPreset.deserialize(value.to_s.downcase)
149
+ rescue ArgumentError
150
+ raise ArgumentError, "Invalid auto preset: #{value}. Must be one of :none, :light, :medium, :heavy"
151
+ end
152
+ when nil
153
+ AutoPreset::None
154
+ else
155
+ raise ArgumentError, "Invalid auto preset: #{value.inspect}"
156
+ end
157
+ }
158
+ setting :auto_seed, default: DEFAULT_AUTO_SEED, constructor: ->(value) {
159
+ value.nil? ? DEFAULT_AUTO_SEED : Integer(value)
160
+ }
161
+ setting :valset_target_size, default: nil
106
162
  setting :num_trials, default: 12
107
163
  setting :num_instruction_candidates, default: 5
108
164
  setting :bootstrap_sets, default: 5
@@ -142,6 +198,26 @@ module DSPy
142
198
  @default_config_block
143
199
  end
144
200
 
201
+ class << self
202
+ extend T::Sig
203
+
204
+ sig { params(config: T.untyped, preset: AutoPreset).void }
205
+ def apply_auto_defaults(config, preset)
206
+ settings = AUTO_PRESET_SETTINGS.fetch(preset) { {} }
207
+
208
+ config.auto_preset = preset
209
+ config.num_trials = settings[:candidate_budget] if settings[:candidate_budget]
210
+ config.num_instruction_candidates = settings[:instruction_candidates] if settings[:instruction_candidates]
211
+ config.bootstrap_sets = settings[:bootstrap_sets] if settings[:bootstrap_sets]
212
+ config.max_bootstrapped_examples = settings[:max_bootstrapped_examples] if settings.key?(:max_bootstrapped_examples)
213
+ config.max_labeled_examples = settings[:max_labeled_examples] if settings.key?(:max_labeled_examples)
214
+ config.optimization_strategy = settings[:optimization_strategy] if settings[:optimization_strategy]
215
+ config.early_stopping_patience = settings[:early_stopping_patience] if settings[:early_stopping_patience]
216
+ config.minibatch_size = settings[:minibatch_size] if settings.key?(:minibatch_size)
217
+ config.valset_target_size = settings[:valset_target_size] if settings[:valset_target_size]
218
+ end
219
+ end
220
+
145
221
 
146
222
  # Simple data structure for evaluated candidate configurations (immutable)
147
223
  EvaluatedCandidate = Data.define(
@@ -294,6 +370,13 @@ module DSPy
294
370
  typed_trainset = ensure_typed_examples(trainset)
295
371
  typed_valset = valset ? ensure_typed_examples(valset) : nil
296
372
 
373
+ if auto_preset_active?
374
+ typed_trainset, typed_valset = prepare_datasets_for_auto(typed_trainset, typed_valset)
375
+ typed_valset = apply_auto_preset!(program, typed_valset)
376
+ else
377
+ typed_valset = limit_validation_set(typed_valset, config.valset_target_size)
378
+ end
379
+
297
380
  # Use validation set if available, otherwise use part of training set
298
381
  evaluation_set = typed_valset || typed_trainset.take([typed_trainset.size / 3, 10].max)
299
382
 
@@ -345,6 +428,105 @@ module DSPy
345
428
 
346
429
  private
347
430
 
431
+ sig { returns(T::Boolean) }
432
+ def auto_preset_active?
433
+ config.auto_preset != AutoPreset::None
434
+ end
435
+
436
+ sig { params(trainset: T::Array[DSPy::Example], valset: T.nilable(T::Array[DSPy::Example])).returns([T::Array[DSPy::Example], T::Array[DSPy::Example]]) }
437
+ def prepare_datasets_for_auto(trainset, valset)
438
+ settings = auto_settings_for(config.auto_preset)
439
+ target_size = settings[:valset_target_size]
440
+ config.valset_target_size = target_size
441
+
442
+ if valset && valset.any?
443
+ [trainset, limit_validation_set(valset, target_size)]
444
+ else
445
+ raise ArgumentError, "Training set must contain at least 2 examples when auto presets are enabled" if trainset.size < 2
446
+
447
+ shuffled = trainset.shuffle(random: Random.new(config.auto_seed))
448
+ default_val_size = [
449
+ [(trainset.size * 0.8).ceil, 1].max,
450
+ trainset.size - 1
451
+ ].min
452
+
453
+ desired_val_size = target_size ? [default_val_size, target_size].min : default_val_size
454
+ desired_val_size = [[desired_val_size, 1].max, trainset.size - 1].min
455
+
456
+ validation_examples = shuffled.take(desired_val_size)
457
+ training_examples = shuffled.drop(desired_val_size)
458
+
459
+ [training_examples, limit_validation_set(validation_examples, target_size)]
460
+ end
461
+ end
462
+
463
+ sig { params(program: T.untyped, valset: T::Array[DSPy::Example]).returns(T::Array[DSPy::Example]) }
464
+ def apply_auto_preset!(program, valset)
465
+ settings = auto_settings_for(config.auto_preset)
466
+ zeroshot = zero_shot_for_settings?(settings)
467
+ candidate_budget = settings[:candidate_budget]
468
+
469
+ if candidate_budget && candidate_budget.positive?
470
+ config.num_trials = compute_trials_from_candidate_budget(program, candidate_budget, zeroshot)
471
+ instruction_candidates = if zeroshot
472
+ candidate_budget
473
+ else
474
+ settings[:instruction_candidates_when_fewshot] || (candidate_budget / 2.0).ceil
475
+ end
476
+ config.num_instruction_candidates = [instruction_candidates, 1].max
477
+ end
478
+
479
+ config.bootstrap_sets = settings[:bootstrap_sets] if settings[:bootstrap_sets]
480
+ config.max_bootstrapped_examples = settings[:max_bootstrapped_examples] if settings.key?(:max_bootstrapped_examples)
481
+ config.max_labeled_examples = settings[:max_labeled_examples] if settings.key?(:max_labeled_examples)
482
+ config.optimization_strategy = settings[:optimization_strategy] if settings[:optimization_strategy]
483
+ config.early_stopping_patience = settings[:early_stopping_patience] if settings[:early_stopping_patience]
484
+ config.minibatch_size = settings[:minibatch_size] if settings.key?(:minibatch_size)
485
+
486
+ config.valset_target_size = settings[:valset_target_size]
487
+ limit_validation_set(valset, config.valset_target_size)
488
+ end
489
+
490
+ sig { params(valset: T.nilable(T::Array[DSPy::Example]), target_size: T.nilable(Integer)).returns(T.nilable(T::Array[DSPy::Example])) }
491
+ def limit_validation_set(valset, target_size)
492
+ return valset unless valset && target_size && target_size.positive?
493
+ return valset if valset.size <= target_size
494
+
495
+ valset.shuffle(random: Random.new(config.auto_seed)).take(target_size)
496
+ end
497
+
498
+ sig { params(program: T.untyped, num_candidates: Integer, zeroshot: T::Boolean).returns(Integer) }
499
+ def compute_trials_from_candidate_budget(program, num_candidates, zeroshot)
500
+ predictor_count =
501
+ if program.respond_to?(:predictors)
502
+ Array(program.predictors).size
503
+ else
504
+ 1
505
+ end
506
+
507
+ predictor_count = 1 if predictor_count.zero?
508
+ variable_count = zeroshot ? predictor_count : predictor_count * 2
509
+ log_term = Math.log2([num_candidates, 2].max)
510
+
511
+ [
512
+ (2 * variable_count * log_term).ceil,
513
+ (1.5 * num_candidates).ceil
514
+ ].max
515
+ end
516
+
517
+ sig { params(settings: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
518
+ def zero_shot_for_settings?(settings)
519
+ settings.fetch(:max_bootstrapped_examples, 0).to_i.zero? &&
520
+ settings.fetch(:max_labeled_examples, 0).to_i.zero?
521
+ end
522
+
523
+ sig { params(preset: AutoPreset).returns(T::Hash[Symbol, T.untyped]) }
524
+ def auto_settings_for(preset)
525
+ AUTO_PRESET_SETTINGS.fetch(preset) do
526
+ raise ArgumentError, "Unknown auto preset: #{preset.inspect}"
527
+ end
528
+ end
529
+
348
530
  # Phase 1: Bootstrap few-shot examples from training data
349
531
  # Returns a hash mapping predictor indices to arrays of demo sets
350
532
  sig { params(program: T.untyped, trainset: T::Array[DSPy::Example]).returns(T::Hash[Integer, T::Array[T::Array[DSPy::FewShotExample]]]) }
@@ -546,6 +728,21 @@ module DSPy
546
728
  end
547
729
  def generate_candidate_configurations(proposal_result, demo_candidates)
548
730
  candidates = []
731
+ seen_signatures = Set.new
732
+
733
+ add_candidate = lambda do |instruction:, few_shot_examples:, type:, metadata:, config_id:|
734
+ signature = candidate_signature(type, instruction, metadata, few_shot_examples)
735
+ next if seen_signatures.include?(signature)
736
+
737
+ seen_signatures << signature
738
+ candidates << EvaluatedCandidate.new(
739
+ instruction: instruction,
740
+ few_shot_examples: few_shot_examples,
741
+ type: type,
742
+ metadata: metadata,
743
+ config_id: config_id
744
+ )
745
+ end
549
746
 
550
747
  predictor_instruction_map = if proposal_result.respond_to?(:predictor_instructions) && proposal_result.predictor_instructions.any?
551
748
  proposal_result.predictor_instructions
@@ -557,7 +754,7 @@ module DSPy
557
754
  demo_maps = build_demo_maps(demo_candidates)
558
755
 
559
756
  # Base configuration (no modifications)
560
- candidates << EvaluatedCandidate.new(
757
+ add_candidate.call(
561
758
  instruction: "",
562
759
  few_shot_examples: [],
563
760
  type: CandidateType::Baseline,
@@ -570,7 +767,7 @@ module DSPy
570
767
 
571
768
  instruction_maps.each_with_index do |instruction_map, combo_idx|
572
769
  primary_instruction = instruction_map[0] || instruction_map.values.first || ""
573
- candidates << EvaluatedCandidate.new(
770
+ add_candidate.call(
574
771
  instruction: primary_instruction,
575
772
  few_shot_examples: [],
576
773
  type: CandidateType::InstructionOnly,
@@ -587,7 +784,7 @@ module DSPy
587
784
  next if demo_map.empty?
588
785
 
589
786
  flattened_examples = demo_map.values.flatten
590
- candidates << EvaluatedCandidate.new(
787
+ add_candidate.call(
591
788
  instruction: "",
592
789
  few_shot_examples: flattened_examples,
593
790
  type: CandidateType::FewShotOnly,
@@ -607,7 +804,7 @@ module DSPy
607
804
  next if demo_map.empty?
608
805
 
609
806
  flattened_examples = demo_map.values.flatten
610
- candidates << EvaluatedCandidate.new(
807
+ add_candidate.call(
611
808
  instruction: primary_instruction,
612
809
  few_shot_examples: flattened_examples,
613
810
  type: CandidateType::Combined,
@@ -687,6 +884,55 @@ module DSPy
687
884
  end
688
885
  end
689
886
 
887
+ sig do
888
+ params(
889
+ type: CandidateType,
890
+ instruction: String,
891
+ metadata: T::Hash[Symbol, T.untyped],
892
+ few_shot_examples: T::Array[T.untyped]
893
+ ).returns(String)
894
+ end
895
+ def candidate_signature(type, instruction, metadata, few_shot_examples)
896
+ JSON.generate(
897
+ type: type.serialize,
898
+ instruction: instruction,
899
+ instructions_map: normalize_instruction_map(metadata[:instructions_map] || {}),
900
+ demos_map: normalize_demo_map(metadata[:demos_map] || {}),
901
+ few_shot_examples: few_shot_examples.map { |example| serialize_few_shot_example(example) }
902
+ )
903
+ end
904
+
905
+ sig { params(map: T::Hash[Integer, T.untyped]).returns(T::Hash[Integer, String]) }
906
+ def normalize_instruction_map(map)
907
+ map.sort_by { |index, _| index }.each_with_object({}) do |(index, value), memo|
908
+ memo[index] = value.to_s
909
+ end
910
+ end
911
+
912
+ sig { params(map: T::Hash[Integer, T::Array[T.untyped]]).returns(T::Hash[Integer, T::Array[T.untyped]]) }
913
+ def normalize_demo_map(map)
914
+ map.sort_by { |index, _| index }.each_with_object({}) do |(index, demos), memo|
915
+ memo[index] = Array(demos).map { |demo| serialize_few_shot_example(demo) }
916
+ end
917
+ end
918
+
919
+ sig { params(example: T.untyped).returns(T.untyped) }
920
+ def serialize_few_shot_example(example)
921
+ case example
922
+ when DSPy::FewShotExample
923
+ deep_dup(example.to_h)
924
+ when DSPy::Example
925
+ {
926
+ input: deep_dup(example.input_values),
927
+ expected: deep_dup(example.expected_values)
928
+ }
929
+ when Hash
930
+ deep_dup(example)
931
+ else
932
+ example
933
+ end
934
+ end
935
+
690
936
  sig { params(examples: T::Array[T.untyped]).returns(T::Array[DSPy::FewShotExample]) }
691
937
  def normalize_few_shot_examples(examples)
692
938
  examples.map do |example|
@@ -1412,10 +1658,13 @@ module DSPy
1412
1658
  # Infer auto mode based on configuration
1413
1659
  sig { returns(String) }
1414
1660
  def infer_auto_mode
1661
+ return config.auto_preset.serialize unless config.auto_preset == AutoPreset::None
1662
+
1415
1663
  case config.num_trials
1416
1664
  when 0..6 then "light"
1417
1665
  when 7..12 then "medium"
1418
- else "heavy"
1666
+ when 13..Float::INFINITY then "heavy"
1667
+ else "manual"
1419
1668
  end
1420
1669
  end
1421
1670
  end
data/lib/dspy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSPy
4
- VERSION = "0.29.0"
4
+ VERSION = "0.29.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.0
4
+ version: 0.29.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-10-19 00:00:00.000000000 Z
10
+ date: 2025-10-20 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: dry-configurable
@@ -321,7 +320,6 @@ homepage: https://github.com/vicentereig/dspy.rb
321
320
  licenses:
322
321
  - MIT
323
322
  metadata: {}
324
- post_install_message:
325
323
  rdoc_options: []
326
324
  require_paths:
327
325
  - lib
@@ -336,8 +334,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
336
334
  - !ruby/object:Gem::Version
337
335
  version: '0'
338
336
  requirements: []
339
- rubygems_version: 3.0.3.1
340
- signing_key:
337
+ rubygems_version: 3.6.5
341
338
  specification_version: 4
342
339
  summary: The Ruby framework for programming—rather than prompting—language models.
343
340
  test_files: []