dspy 0.28.2 → 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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -3
  3. data/lib/dspy/code_act.rb +14 -1
  4. data/lib/dspy/datasets/ade.rb +90 -0
  5. data/lib/dspy/datasets.rb +8 -0
  6. data/lib/dspy/lm.rb +4 -8
  7. data/lib/dspy/mixins/struct_builder.rb +17 -25
  8. data/lib/dspy/module.rb +12 -1
  9. data/lib/dspy/observability/async_span_processor.rb +67 -93
  10. data/lib/dspy/observability.rb +43 -1
  11. data/lib/dspy/predict.rb +10 -0
  12. data/lib/dspy/propose/dataset_summary_generator.rb +36 -3
  13. data/lib/dspy/propose/grounded_proposer.rb +118 -11
  14. data/lib/dspy/re_act.rb +13 -0
  15. data/lib/dspy/reflection_lm.rb +36 -0
  16. data/lib/dspy/teleprompt/gepa.rb +448 -2803
  17. data/lib/dspy/teleprompt/mipro_v2.rb +564 -65
  18. data/lib/dspy/teleprompt/utils.rb +8 -3
  19. data/lib/dspy/version.rb +2 -2
  20. data/lib/dspy.rb +3 -2
  21. data/lib/gepa/api.rb +61 -0
  22. data/lib/gepa/core/engine.rb +226 -0
  23. data/lib/gepa/core/evaluation_batch.rb +26 -0
  24. data/lib/gepa/core/result.rb +92 -0
  25. data/lib/gepa/core/state.rb +231 -0
  26. data/lib/gepa/logging/experiment_tracker.rb +54 -0
  27. data/lib/gepa/logging/logger.rb +57 -0
  28. data/lib/gepa/logging.rb +9 -0
  29. data/lib/gepa/proposer/base.rb +27 -0
  30. data/lib/gepa/proposer/merge_proposer.rb +424 -0
  31. data/lib/gepa/proposer/reflective_mutation/base.rb +48 -0
  32. data/lib/gepa/proposer/reflective_mutation/reflective_mutation.rb +188 -0
  33. data/lib/gepa/strategies/batch_sampler.rb +91 -0
  34. data/lib/gepa/strategies/candidate_selector.rb +97 -0
  35. data/lib/gepa/strategies/component_selector.rb +57 -0
  36. data/lib/gepa/strategies/instruction_proposal.rb +120 -0
  37. data/lib/gepa/telemetry.rb +122 -0
  38. data/lib/gepa/utils/pareto.rb +119 -0
  39. data/lib/gepa.rb +21 -0
  40. metadata +42 -4
  41. data/lib/dspy/teleprompt/simple_optimizer.rb +0 -503
@@ -11,6 +11,8 @@ module DSPy
11
11
  class GroundedProposer
12
12
  extend T::Sig
13
13
 
14
+ MAX_HISTORY_INSTRUCTIONS = 5
15
+
14
16
  # Python-compatible TIPS dictionary for instruction generation
15
17
  TIPS = {
16
18
  "none" => "",
@@ -93,6 +95,9 @@ module DSPy
93
95
  sig { returns(T::Array[String]) }
94
96
  attr_reader :candidate_instructions
95
97
 
98
+ sig { returns(T::Hash[Integer, T::Array[String]]) }
99
+ attr_reader :predictor_instructions
100
+
96
101
  sig { returns(T::Hash[Symbol, T.untyped]) }
97
102
  attr_reader :analysis
98
103
 
@@ -103,11 +108,16 @@ module DSPy
103
108
  params(
104
109
  candidate_instructions: T::Array[String],
105
110
  analysis: T::Hash[Symbol, T.untyped],
106
- metadata: T::Hash[Symbol, T.untyped]
111
+ metadata: T::Hash[Symbol, T.untyped],
112
+ predictor_instructions: T.nilable(T::Hash[Integer, T::Array[String]])
107
113
  ).void
108
114
  end
109
- def initialize(candidate_instructions:, analysis:, metadata:)
115
+ def initialize(candidate_instructions:, analysis:, metadata:, predictor_instructions: nil)
110
116
  @candidate_instructions = candidate_instructions.freeze
117
+ normalized_predictor_instructions = (predictor_instructions || {}).each_with_object({}) do |(index, instructions), memo|
118
+ memo[index] = instructions.dup.freeze
119
+ end
120
+ @predictor_instructions = normalized_predictor_instructions.freeze
111
121
  @analysis = analysis.freeze
112
122
  @metadata = metadata.freeze
113
123
  end
@@ -192,10 +202,11 @@ module DSPy
192
202
  signature_class: T.class_of(DSPy::Signature),
193
203
  examples: T::Array[T.untyped],
194
204
  few_shot_examples: T.nilable(T::Array[T.untyped]),
195
- current_instruction: T.nilable(String)
205
+ current_instruction: T.nilable(String),
206
+ trial_logs: T.nilable(T::Hash[Integer, T::Hash[Symbol, T.untyped]])
196
207
  ).returns(ProposalResult)
197
208
  end
198
- def propose_instructions(signature_class, examples, few_shot_examples: nil, current_instruction: nil)
209
+ def propose_instructions(signature_class, examples, few_shot_examples: nil, current_instruction: nil, trial_logs: nil)
199
210
  DSPy::Context.with_span(
200
211
  operation: 'optimization.instruction_proposal',
201
212
  'dspy.module' => 'GroundedProposer',
@@ -212,7 +223,8 @@ module DSPy
212
223
  signature_class,
213
224
  analysis,
214
225
  current_instruction,
215
- few_shot_examples: few_shot_examples
226
+ few_shot_examples: few_shot_examples,
227
+ trial_logs: trial_logs
216
228
  )
217
229
 
218
230
  # Filter and rank candidates
@@ -236,6 +248,50 @@ module DSPy
236
248
  end
237
249
  end
238
250
 
251
+ sig do
252
+ params(
253
+ trainset: T::Array[T.untyped],
254
+ program: T.untyped,
255
+ demo_candidates: T::Hash[Integer, T::Array[T::Array[DSPy::FewShotExample]]],
256
+ trial_logs: T.nilable(T::Hash[Integer, T::Hash[Symbol, T.untyped]]),
257
+ num_instruction_candidates: T.nilable(Integer)
258
+ ).returns(ProposalResult)
259
+ end
260
+ def propose_instructions_for_program(trainset:, program:, demo_candidates:, trial_logs: nil, num_instruction_candidates: nil)
261
+ num_candidates = num_instruction_candidates || @config.num_instruction_candidates
262
+
263
+ current_instruction = if program.respond_to?(:prompt) && program.prompt.respond_to?(:instruction)
264
+ program.prompt.instruction
265
+ else
266
+ nil
267
+ end
268
+
269
+ few_shot_examples = demo_candidates[0]&.flatten&.take(@config.num_demos_in_context) || []
270
+
271
+ signature_class = if program.respond_to?(:signature_class)
272
+ program.signature_class
273
+ else
274
+ raise ArgumentError, "Program must expose signature_class for instruction proposal"
275
+ end
276
+
277
+ base_result = propose_instructions(
278
+ signature_class,
279
+ trainset,
280
+ few_shot_examples: few_shot_examples,
281
+ current_instruction: current_instruction,
282
+ trial_logs: trial_logs
283
+ )
284
+
285
+ predictor_instructions = { 0 => base_result.candidate_instructions.take(num_candidates) }
286
+
287
+ ProposalResult.new(
288
+ candidate_instructions: base_result.candidate_instructions,
289
+ analysis: base_result.analysis,
290
+ metadata: base_result.metadata,
291
+ predictor_instructions: predictor_instructions
292
+ )
293
+ end
294
+
239
295
  private
240
296
 
241
297
  # Analyze the task based on signature and training examples
@@ -418,16 +474,18 @@ module DSPy
418
474
  signature_class: T.class_of(DSPy::Signature),
419
475
  analysis: T::Hash[Symbol, T.untyped],
420
476
  current_instruction: T.nilable(String),
421
- few_shot_examples: T.nilable(T::Array[T.untyped])
477
+ few_shot_examples: T.nilable(T::Array[T.untyped]),
478
+ trial_logs: T.nilable(T::Hash[Integer, T::Hash[Symbol, T.untyped]])
422
479
  ).returns(T::Array[String])
423
480
  end
424
- def generate_instruction_candidates(signature_class, analysis, current_instruction, few_shot_examples: nil)
481
+ def generate_instruction_candidates(signature_class, analysis, current_instruction, few_shot_examples: nil, trial_logs: nil)
425
482
  # Build context for instruction generation
426
483
  context = build_generation_context(
427
484
  signature_class,
428
485
  analysis,
429
486
  current_instruction,
430
- few_shot_examples: few_shot_examples
487
+ few_shot_examples: few_shot_examples,
488
+ trial_logs: trial_logs
431
489
  )
432
490
 
433
491
  # Create instruction generation signature
@@ -467,10 +525,11 @@ module DSPy
467
525
  signature_class: T.class_of(DSPy::Signature),
468
526
  analysis: T::Hash[Symbol, T.untyped],
469
527
  current_instruction: T.nilable(String),
470
- few_shot_examples: T.nilable(T::Array[T.untyped])
528
+ few_shot_examples: T.nilable(T::Array[T.untyped]),
529
+ trial_logs: T.nilable(T::Hash[Integer, T::Hash[Symbol, T.untyped]])
471
530
  ).returns(String)
472
531
  end
473
- def build_generation_context(signature_class, analysis, current_instruction, few_shot_examples: nil)
532
+ def build_generation_context(signature_class, analysis, current_instruction, few_shot_examples: nil, trial_logs: nil)
474
533
  context_parts = []
475
534
 
476
535
  # Include dataset summary if enabled and available
@@ -515,6 +574,13 @@ module DSPy
515
574
  context_parts << "Tip: #{tip}" if tip && !tip.empty?
516
575
  end
517
576
 
577
+ if @config.use_instruct_history
578
+ history_summary = build_instruction_history_summary(trial_logs, predictor_index: 0, top_n: MAX_HISTORY_INSTRUCTIONS)
579
+ unless history_summary.empty?
580
+ context_parts << "Previous instructions:\n#{history_summary}"
581
+ end
582
+ end
583
+
518
584
  context_parts.join("\n\n")
519
585
  end
520
586
 
@@ -565,6 +631,47 @@ module DSPy
565
631
  end
566
632
  end
567
633
 
634
+ sig do
635
+ params(
636
+ trial_logs: T.nilable(T::Hash[Integer, T::Hash[Symbol, T.untyped]]),
637
+ predictor_index: Integer,
638
+ top_n: Integer
639
+ ).returns(String)
640
+ end
641
+ def build_instruction_history_summary(trial_logs, predictor_index:, top_n:)
642
+ return "" unless @config.use_instruct_history
643
+
644
+ logs = trial_logs || {}
645
+ aggregate = Hash.new { |hash, key| hash[key] = { total: 0.0, count: 0 } }
646
+
647
+ logs.each_value do |entry|
648
+ score = entry[:score]
649
+ next unless score.respond_to?(:to_f)
650
+
651
+ instructions = entry[:instructions]
652
+ instruction = nil
653
+ if instructions.respond_to?(:[])
654
+ instruction = instructions[predictor_index] || instructions[:default]
655
+ end
656
+ instruction ||= entry[:instruction]
657
+
658
+ next unless instruction.is_a?(String) && !instruction.empty?
659
+
660
+ aggregate[instruction][:total] += score.to_f
661
+ aggregate[instruction][:count] += 1
662
+ end
663
+
664
+ return "" if aggregate.empty?
665
+
666
+ ranked = aggregate.map do |instruction, stats|
667
+ average = stats[:total] / stats[:count]
668
+ [instruction, average]
669
+ end
670
+
671
+ top_entries = ranked.sort_by { |(_, avg)| -avg }.take(top_n).reverse
672
+ top_entries.map { |instruction, avg| format("%s | Score: %.4f", instruction, avg) }.join("\n\n")
673
+ end
674
+
568
675
  # Build requirements text for instruction generation
569
676
  sig { params(analysis: T::Hash[Symbol, T.untyped]).returns(String) }
570
677
  def build_requirements_text(analysis)
@@ -740,4 +847,4 @@ module DSPy
740
847
  end
741
848
  end
742
849
  end
743
- end
850
+ end
data/lib/dspy/re_act.rb CHANGED
@@ -144,6 +144,19 @@ module DSPy
144
144
  super(enhanced_signature)
145
145
  end
146
146
 
147
+ sig { override.returns(T::Array[[String, DSPy::Module]]) }
148
+ def named_predictors
149
+ pairs = T.let([], T::Array[[String, DSPy::Module]])
150
+ pairs << ["thought_generator", @thought_generator]
151
+ pairs << ["observation_processor", @observation_processor]
152
+ pairs
153
+ end
154
+
155
+ sig { override.returns(T::Array[DSPy::Module]) }
156
+ def predictors
157
+ named_predictors.map { |(_, predictor)| predictor }
158
+ end
159
+
147
160
  sig { params(kwargs: T.untyped).returns(T.untyped).override }
148
161
  def forward(**kwargs)
149
162
  # Validate input
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ module DSPy
6
+ # Lightweight wrapper for running reflection prompts without structured outputs.
7
+ class ReflectionLM
8
+ extend T::Sig
9
+
10
+ sig do
11
+ params(
12
+ model_id: String,
13
+ api_key: T.nilable(String),
14
+ options: T.untyped
15
+ ).void
16
+ end
17
+ def initialize(model_id, api_key: nil, **options)
18
+ opts = options.each_with_object({}) do |(key, value), memo|
19
+ memo[key.to_sym] = value
20
+ end
21
+ opts[:api_key] = api_key if api_key
22
+ @lm = DSPy::LM.new(model_id, structured_outputs: false, schema_format: :json, **opts)
23
+ end
24
+
25
+ sig { params(prompt: String).returns(String) }
26
+ def call(prompt)
27
+ response = @lm.raw_chat([{ role: 'user', content: prompt }])
28
+ response.respond_to?(:content) ? response.content : response.to_s
29
+ end
30
+
31
+ sig { params(messages: T.nilable(T::Array[T::Hash[Symbol, String]]), block: T.nilable(T.proc.params(arg0: T.untyped).void)).returns(T.untyped) }
32
+ def raw_chat(messages = nil, &block)
33
+ @lm.raw_chat(messages, &block)
34
+ end
35
+ end
36
+ end