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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -3
  3. data/lib/dspy/callbacks.rb +222 -0
  4. data/lib/dspy/chain_of_thought.rb +2 -1
  5. data/lib/dspy/code_act.rb +14 -1
  6. data/lib/dspy/datasets/ade.rb +90 -0
  7. data/lib/dspy/datasets.rb +8 -0
  8. data/lib/dspy/lm.rb +9 -12
  9. data/lib/dspy/mixins/struct_builder.rb +17 -25
  10. data/lib/dspy/module.rb +45 -1
  11. data/lib/dspy/observability/async_span_processor.rb +67 -93
  12. data/lib/dspy/observability.rb +43 -1
  13. data/lib/dspy/predict.rb +17 -0
  14. data/lib/dspy/prompt.rb +90 -20
  15. data/lib/dspy/propose/dataset_summary_generator.rb +210 -0
  16. data/lib/dspy/propose/grounded_proposer.rb +320 -66
  17. data/lib/dspy/re_act.rb +13 -0
  18. data/lib/dspy/reflection_lm.rb +36 -0
  19. data/lib/dspy/teleprompt/bootstrap_strategy.rb +26 -0
  20. data/lib/dspy/teleprompt/gepa.rb +448 -2803
  21. data/lib/dspy/teleprompt/mipro_v2.rb +624 -100
  22. data/lib/dspy/teleprompt/utils.rb +349 -42
  23. data/lib/dspy/version.rb +2 -2
  24. data/lib/dspy.rb +4 -2
  25. data/lib/gepa/api.rb +61 -0
  26. data/lib/gepa/core/engine.rb +226 -0
  27. data/lib/gepa/core/evaluation_batch.rb +26 -0
  28. data/lib/gepa/core/result.rb +92 -0
  29. data/lib/gepa/core/state.rb +231 -0
  30. data/lib/gepa/logging/experiment_tracker.rb +54 -0
  31. data/lib/gepa/logging/logger.rb +57 -0
  32. data/lib/gepa/logging.rb +9 -0
  33. data/lib/gepa/proposer/base.rb +27 -0
  34. data/lib/gepa/proposer/merge_proposer.rb +424 -0
  35. data/lib/gepa/proposer/reflective_mutation/base.rb +48 -0
  36. data/lib/gepa/proposer/reflective_mutation/reflective_mutation.rb +188 -0
  37. data/lib/gepa/strategies/batch_sampler.rb +91 -0
  38. data/lib/gepa/strategies/candidate_selector.rb +97 -0
  39. data/lib/gepa/strategies/component_selector.rb +57 -0
  40. data/lib/gepa/strategies/instruction_proposal.rb +120 -0
  41. data/lib/gepa/telemetry.rb +122 -0
  42. data/lib/gepa/utils/pareto.rb +119 -0
  43. data/lib/gepa.rb +21 -0
  44. metadata +59 -4
  45. data/lib/dspy/teleprompt/simple_optimizer.rb +0 -497
@@ -11,40 +11,80 @@ module DSPy
11
11
  class GroundedProposer
12
12
  extend T::Sig
13
13
 
14
- # Configuration for instruction proposal
14
+ MAX_HISTORY_INSTRUCTIONS = 5
15
+
16
+ # Python-compatible TIPS dictionary for instruction generation
17
+ TIPS = {
18
+ "none" => "",
19
+ "creative" => "Don't be afraid to be creative when creating the new instruction!",
20
+ "simple" => "Keep the instruction clear and concise.",
21
+ "description" => "Make sure your instruction is very informative and descriptive.",
22
+ "high_stakes" => "The instruction should include a high stakes scenario in which the LM must solve the task!",
23
+ "persona" => 'Include a persona that is relevant to the task in the instruction (ie. "You are a ...")'
24
+ }.freeze
25
+
26
+ # Configuration for instruction proposal (Python-compatible)
15
27
  class Config
16
28
  extend T::Sig
17
29
 
30
+ # Core parameters
18
31
  sig { returns(Integer) }
19
32
  attr_accessor :num_instruction_candidates
20
33
 
34
+ # Python-compatible awareness flags (match Python defaults exactly)
35
+ sig { returns(T::Boolean) }
36
+ attr_accessor :program_aware
37
+
38
+ sig { returns(T::Boolean) }
39
+ attr_accessor :use_dataset_summary
40
+
41
+ sig { returns(T::Boolean) }
42
+ attr_accessor :use_task_demos
43
+
44
+ sig { returns(T::Boolean) }
45
+ attr_accessor :use_tip
46
+
47
+ sig { returns(T::Boolean) }
48
+ attr_accessor :use_instruct_history
49
+
50
+ # Additional parameters
21
51
  sig { returns(Integer) }
22
- attr_accessor :max_examples_for_analysis
52
+ attr_accessor :view_data_batch_size
23
53
 
24
54
  sig { returns(Integer) }
25
- attr_accessor :max_instruction_length
55
+ attr_accessor :num_demos_in_context
26
56
 
27
57
  sig { returns(T::Boolean) }
28
- attr_accessor :use_task_description
58
+ attr_accessor :set_tip_randomly
29
59
 
30
60
  sig { returns(T::Boolean) }
31
- attr_accessor :use_input_output_analysis
61
+ attr_accessor :set_history_randomly
32
62
 
33
- sig { returns(T::Boolean) }
34
- attr_accessor :use_few_shot_examples
63
+ sig { returns(Float) }
64
+ attr_accessor :init_temperature
35
65
 
36
- sig { returns(String) }
37
- attr_accessor :proposal_model
66
+ sig { returns(T::Boolean) }
67
+ attr_accessor :verbose
38
68
 
39
69
  sig { void }
40
70
  def initialize
71
+ # Core parameters
41
72
  @num_instruction_candidates = 5
42
- @max_examples_for_analysis = 10
43
- @max_instruction_length = 200
44
- @use_task_description = true
45
- @use_input_output_analysis = true
46
- @use_few_shot_examples = true
47
- @proposal_model = "gpt-4o-mini"
73
+
74
+ # Python-compatible awareness flags (match Python defaults)
75
+ @program_aware = true
76
+ @use_dataset_summary = true
77
+ @use_task_demos = true
78
+ @use_tip = true
79
+ @use_instruct_history = true
80
+
81
+ # Additional parameters
82
+ @view_data_batch_size = 10
83
+ @num_demos_in_context = 3
84
+ @set_tip_randomly = true
85
+ @set_history_randomly = true
86
+ @init_temperature = 1.0
87
+ @verbose = false
48
88
  end
49
89
  end
50
90
 
@@ -55,6 +95,9 @@ module DSPy
55
95
  sig { returns(T::Array[String]) }
56
96
  attr_reader :candidate_instructions
57
97
 
98
+ sig { returns(T::Hash[Integer, T::Array[String]]) }
99
+ attr_reader :predictor_instructions
100
+
58
101
  sig { returns(T::Hash[Symbol, T.untyped]) }
59
102
  attr_reader :analysis
60
103
 
@@ -65,11 +108,16 @@ module DSPy
65
108
  params(
66
109
  candidate_instructions: T::Array[String],
67
110
  analysis: T::Hash[Symbol, T.untyped],
68
- metadata: T::Hash[Symbol, T.untyped]
111
+ metadata: T::Hash[Symbol, T.untyped],
112
+ predictor_instructions: T.nilable(T::Hash[Integer, T::Array[String]])
69
113
  ).void
70
114
  end
71
- def initialize(candidate_instructions:, analysis:, metadata:)
115
+ def initialize(candidate_instructions:, analysis:, metadata:, predictor_instructions: nil)
72
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
73
121
  @analysis = analysis.freeze
74
122
  @metadata = metadata.freeze
75
123
  end
@@ -88,21 +136,77 @@ module DSPy
88
136
  sig { returns(Config) }
89
137
  attr_reader :config
90
138
 
91
- sig { params(config: T.nilable(Config)).void }
92
- def initialize(config: nil)
139
+ sig do
140
+ params(
141
+ config: T.nilable(Config),
142
+ program: T.nilable(T.untyped),
143
+ trainset: T.nilable(T::Array[DSPy::Example])
144
+ ).void
145
+ end
146
+ def initialize(config: nil, program: nil, trainset: nil)
93
147
  @config = config || Config.new
148
+ @program = program
149
+ @trainset = trainset
150
+ @dataset_summary = nil
151
+ @program_code_string = nil
152
+
153
+ # Generate dataset summary if data-aware mode enabled (Python: use_dataset_summary)
154
+ if @config.use_dataset_summary && trainset && !trainset.empty?
155
+ begin
156
+ require_relative 'dataset_summary_generator'
157
+ @dataset_summary = DatasetSummaryGenerator.create_dataset_summary(
158
+ trainset,
159
+ @config.view_data_batch_size,
160
+ DSPy.current_lm,
161
+ verbose: @config.verbose
162
+ )
163
+ rescue => e
164
+ DSPy.logger.warn("Failed to generate dataset summary: #{e.message}")
165
+ @dataset_summary = nil
166
+ end
167
+ end
168
+
169
+ # Extract program source code if program-aware mode enabled
170
+ if @config.program_aware && program
171
+ @program_code_string = extract_program_source(program)
172
+ end
173
+ end
174
+
175
+ private
176
+
177
+ # Extract source code from program for program-aware mode
178
+ sig { params(program: T.untyped).returns(T.nilable(String)) }
179
+ def extract_program_source(program)
180
+ # Get the program's class
181
+ klass = program.is_a?(Class) ? program : program.class
182
+
183
+ # Try to get source location
184
+ source_location = klass.instance_method(:forward).source_location rescue nil
185
+ return nil unless source_location
186
+
187
+ file, line = source_location
188
+ # Read the source file and extract the class definition
189
+ # This is a simplified version - could be enhanced with method_source gem
190
+ code = "Program: #{klass.name}\nSource: #{file}:#{line}"
191
+ code
192
+ rescue => e
193
+ DSPy.logger.warn("Could not extract program source: #{e.message}")
194
+ nil
94
195
  end
95
196
 
197
+ public
198
+
96
199
  # Generate instruction candidates for a signature and training examples
97
200
  sig do
98
201
  params(
99
202
  signature_class: T.class_of(DSPy::Signature),
100
203
  examples: T::Array[T.untyped],
101
204
  few_shot_examples: T.nilable(T::Array[T.untyped]),
102
- current_instruction: T.nilable(String)
205
+ current_instruction: T.nilable(String),
206
+ trial_logs: T.nilable(T::Hash[Integer, T::Hash[Symbol, T.untyped]])
103
207
  ).returns(ProposalResult)
104
208
  end
105
- 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)
106
210
  DSPy::Context.with_span(
107
211
  operation: 'optimization.instruction_proposal',
108
212
  'dspy.module' => 'GroundedProposer',
@@ -116,9 +220,11 @@ module DSPy
116
220
 
117
221
  # Generate instruction candidates
118
222
  candidates = generate_instruction_candidates(
119
- signature_class,
120
- analysis,
121
- current_instruction
223
+ signature_class,
224
+ analysis,
225
+ current_instruction,
226
+ few_shot_examples: few_shot_examples,
227
+ trial_logs: trial_logs
122
228
  )
123
229
 
124
230
  # Filter and rank candidates
@@ -126,8 +232,8 @@ module DSPy
126
232
 
127
233
  metadata = {
128
234
  generation_timestamp: Time.now.iso8601,
129
- model_used: @config.proposal_model,
130
- num_examples_analyzed: [examples.size, @config.max_examples_for_analysis].min,
235
+ model_used: DSPy.current_lm.model,
236
+ num_examples_analyzed: [examples.size, @config.view_data_batch_size].min,
131
237
  original_instruction: current_instruction
132
238
  }
133
239
 
@@ -142,6 +248,50 @@ module DSPy
142
248
  end
143
249
  end
144
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
+
145
295
  private
146
296
 
147
297
  # Analyze the task based on signature and training examples
@@ -204,7 +354,7 @@ module DSPy
204
354
  # Analyze patterns in training examples
205
355
  sig { params(examples: T::Array[T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
206
356
  def analyze_example_patterns(examples)
207
- analysis_examples = examples.take(@config.max_examples_for_analysis)
357
+ analysis_examples = examples.take(@config.view_data_batch_size)
208
358
 
209
359
  {
210
360
  total_examples: examples.size,
@@ -323,12 +473,20 @@ module DSPy
323
473
  params(
324
474
  signature_class: T.class_of(DSPy::Signature),
325
475
  analysis: T::Hash[Symbol, T.untyped],
326
- current_instruction: T.nilable(String)
476
+ current_instruction: T.nilable(String),
477
+ few_shot_examples: T.nilable(T::Array[T.untyped]),
478
+ trial_logs: T.nilable(T::Hash[Integer, T::Hash[Symbol, T.untyped]])
327
479
  ).returns(T::Array[String])
328
480
  end
329
- def generate_instruction_candidates(signature_class, analysis, current_instruction)
481
+ def generate_instruction_candidates(signature_class, analysis, current_instruction, few_shot_examples: nil, trial_logs: nil)
330
482
  # Build context for instruction generation
331
- context = build_generation_context(signature_class, analysis, current_instruction)
483
+ context = build_generation_context(
484
+ signature_class,
485
+ analysis,
486
+ current_instruction,
487
+ few_shot_examples: few_shot_examples,
488
+ trial_logs: trial_logs
489
+ )
332
490
 
333
491
  # Create instruction generation signature
334
492
  instruction_signature = create_instruction_generation_signature
@@ -346,16 +504,7 @@ module DSPy
346
504
  )
347
505
 
348
506
  instruction = result.instruction.strip
349
-
350
- # Truncate if too long
351
- if instruction.length > @config.max_instruction_length
352
- instruction = instruction[0, @config.max_instruction_length].strip
353
- # Try to end at a word boundary
354
- if instruction.include?(' ')
355
- instruction = instruction.rpartition(' ').first + '.'
356
- end
357
- end
358
-
507
+
359
508
  candidates << instruction if instruction.length > 0
360
509
  rescue => error
361
510
  DSPy.logger.warn("Failed to generate instruction candidate #{i + 1}: #{error.message}")
@@ -375,32 +524,64 @@ module DSPy
375
524
  params(
376
525
  signature_class: T.class_of(DSPy::Signature),
377
526
  analysis: T::Hash[Symbol, T.untyped],
378
- current_instruction: T.nilable(String)
527
+ current_instruction: T.nilable(String),
528
+ few_shot_examples: T.nilable(T::Array[T.untyped]),
529
+ trial_logs: T.nilable(T::Hash[Integer, T::Hash[Symbol, T.untyped]])
379
530
  ).returns(String)
380
531
  end
381
- def build_generation_context(signature_class, analysis, current_instruction)
532
+ def build_generation_context(signature_class, analysis, current_instruction, few_shot_examples: nil, trial_logs: nil)
382
533
  context_parts = []
383
-
384
- context_parts << "Task: #{signature_class.description}" if @config.use_task_description
385
-
386
- if @config.use_input_output_analysis
387
- # Build detailed field descriptions including enum values
388
- input_descriptions = analysis[:input_fields].map { |f| format_field_description(f) }
389
- output_descriptions = analysis[:output_fields].map { |f| format_field_description(f) }
390
-
391
- context_parts << "Input fields: #{input_descriptions.join(', ')}"
392
- context_parts << "Output fields: #{output_descriptions.join(', ')}"
534
+
535
+ # Include dataset summary if enabled and available
536
+ if @config.use_dataset_summary && @dataset_summary
537
+ context_parts << "Dataset Summary: #{@dataset_summary}"
393
538
  end
394
-
539
+
540
+ # Include program code if enabled and available
541
+ if @config.program_aware && @program_code_string
542
+ context_parts << "Program Code:\n#{@program_code_string}"
543
+ end
544
+
545
+ # Always include task description (fundamental to understanding the task)
546
+ context_parts << "Task: #{signature_class.description}"
547
+
548
+ # Always include field analysis (fundamental to understanding inputs/outputs)
549
+ input_descriptions = analysis[:input_fields].map { |f| format_field_description(f) }
550
+ output_descriptions = analysis[:output_fields].map { |f| format_field_description(f) }
551
+
552
+ context_parts << "Input fields: #{input_descriptions.join(', ')}"
553
+ context_parts << "Output fields: #{output_descriptions.join(', ')}"
554
+
555
+ # Include task demos if enabled and available
556
+ if @config.use_task_demos && few_shot_examples && !few_shot_examples.empty?
557
+ demo_strings = few_shot_examples.take(@config.num_demos_in_context).map do |example|
558
+ format_example_as_demo(example)
559
+ end
560
+ context_parts << "Task Demos:\n#{demo_strings.join("\n\n")}"
561
+ end
562
+
395
563
  if analysis[:common_themes] && analysis[:common_themes].any?
396
564
  context_parts << "Task themes: #{analysis[:common_themes].join(', ')}"
397
565
  end
398
-
566
+
399
567
  if current_instruction
400
568
  context_parts << "Current instruction: \"#{current_instruction}\""
401
569
  end
402
-
403
- context_parts.join("\n")
570
+
571
+ # Include tip if enabled
572
+ if @config.use_tip
573
+ tip = select_tip
574
+ context_parts << "Tip: #{tip}" if tip && !tip.empty?
575
+ end
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
+
584
+ context_parts.join("\n\n")
404
585
  end
405
586
 
406
587
  # Format field description with enum values if applicable
@@ -414,6 +595,83 @@ module DSPy
414
595
  end
415
596
  end
416
597
 
598
+ # Format an example as a demo for context
599
+ sig { params(example: T.untyped).returns(String) }
600
+ def format_example_as_demo(example)
601
+ return example.to_s unless example.respond_to?(:inputs) && example.respond_to?(:expected)
602
+
603
+ parts = []
604
+
605
+ # Format inputs
606
+ if example.inputs && !example.inputs.empty?
607
+ input_strs = example.inputs.map { |k, v| "#{k}: #{v.inspect}" }
608
+ parts << "Inputs: #{input_strs.join(', ')}"
609
+ end
610
+
611
+ # Format expected outputs
612
+ if example.expected && !example.expected.empty?
613
+ output_strs = example.expected.map { |k, v| "#{k}: #{v.inspect}" }
614
+ parts << "Expected: #{output_strs.join(', ')}"
615
+ end
616
+
617
+ parts.join(" | ")
618
+ end
619
+
620
+ # Select a tip based on configuration
621
+ sig { returns(T.nilable(String)) }
622
+ def select_tip
623
+ if @config.set_tip_randomly
624
+ # Randomly select a tip (excluding "none")
625
+ tip_keys = TIPS.keys.reject { |k| k == "none" }
626
+ selected_key = tip_keys.sample
627
+ TIPS[selected_key]
628
+ else
629
+ # Return empty string when not using random tips
630
+ ""
631
+ end
632
+ end
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
+
417
675
  # Build requirements text for instruction generation
418
676
  sig { params(analysis: T::Hash[Symbol, T.untyped]).returns(String) }
419
677
  def build_requirements_text(analysis)
@@ -478,25 +736,21 @@ module DSPy
478
736
  # Filter out duplicates and empty candidates
479
737
  filtered = candidates.uniq.reject(&:empty?)
480
738
 
481
- # Simple ranking based on length and content quality
739
+ # Simple ranking based on content quality (Python-compatible: no length scoring)
482
740
  filtered.sort_by do |instruction|
483
741
  score = 0
484
-
485
- # Prefer moderate length instructions
486
- length_score = [instruction.length, @config.max_instruction_length].min / @config.max_instruction_length.to_f
487
- score += length_score * 0.3
488
-
742
+
489
743
  # Prefer instructions with action words
490
744
  action_words = %w[analyze classify generate explain solve determine identify]
491
745
  action_score = action_words.count { |word| instruction.downcase.include?(word) }
492
746
  score += action_score * 0.4
493
-
747
+
494
748
  # Prefer instructions that mention reasoning for complex tasks
495
749
  if analysis[:complexity_indicators][:requires_reasoning]
496
750
  reasoning_score = instruction.downcase.match?(/\b(step|think|reason|explain)\b/) ? 1 : 0
497
751
  score += reasoning_score * 0.3
498
752
  end
499
-
753
+
500
754
  -score # Negative for descending sort
501
755
  end
502
756
  end
@@ -588,9 +842,9 @@ module DSPy
588
842
  'proposal.num_candidates' => result.num_candidates,
589
843
  'proposal.best_instruction_length' => result.best_instruction.length,
590
844
  'proposal.analysis_themes' => result.analysis[:common_themes] || [],
591
- 'proposal.model_used' => @config.proposal_model
845
+ 'proposal.model_used' => DSPy.current_lm.model
592
846
  })
593
847
  end
594
848
  end
595
849
  end
596
- 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
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ module DSPy
6
+ module Teleprompt
7
+ # Bootstrap strategy enum for create_n_fewshot_demo_sets
8
+ # Provides type-safe alternatives to Python's magic number seeds
9
+ class BootstrapStrategy < T::Enum
10
+ enums do
11
+ # No demonstrations - zero-shot learning (Python seed = -3)
12
+ ZeroShot = new
13
+
14
+ # Labeled examples only - no bootstrap generation (Python seed = -2)
15
+ LabeledOnly = new
16
+
17
+ # Bootstrapped demonstrations without shuffling (Python seed = -1)
18
+ Unshuffled = new
19
+
20
+ # Bootstrapped demonstrations with shuffling and random size (Python seed >= 0)
21
+ # Requires separate seed parameter for reproducibility
22
+ Shuffled = new
23
+ end
24
+ end
25
+ end
26
+ end