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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'sorbet-runtime'
4
+ require 'fileutils'
4
5
  require_relative '../evaluate'
5
6
  require_relative '../example'
6
7
  require_relative 'data_handler'
@@ -12,6 +13,167 @@ module DSPy
12
13
  module Utils
13
14
  extend T::Sig
14
15
 
16
+ # Wrapper class that provides Python-compatible signature API
17
+ # Wraps a Predict instance to provide signature access and modification
18
+ class SignatureWrapper
19
+ extend T::Sig
20
+
21
+ sig { returns(T.untyped) }
22
+ attr_reader :predictor
23
+
24
+ sig { params(predictor: T.untyped).void }
25
+ def initialize(predictor)
26
+ @predictor = predictor
27
+ end
28
+
29
+ sig { returns(String) }
30
+ def instructions
31
+ # Get instructions from the predictor's prompt
32
+ @predictor.prompt.instruction
33
+ end
34
+
35
+ sig { params(new_instructions: String).returns(SignatureWrapper) }
36
+ def with_instructions(new_instructions)
37
+ # Return a new wrapper that will apply new instructions when set
38
+ updated_wrapper = SignatureWrapper.new(@predictor)
39
+ updated_wrapper.instance_variable_set(:@pending_instructions, new_instructions)
40
+ updated_wrapper
41
+ end
42
+
43
+ sig { returns(T.nilable(String)) }
44
+ def pending_instructions
45
+ @pending_instructions
46
+ end
47
+ end
48
+
49
+ # Get signature information from a predictor (Python compatibility)
50
+ # Returns a wrapper that provides Python-like signature API
51
+ #
52
+ # @param predictor [Predict] The predictor to get signature from
53
+ # @return [SignatureWrapper] Wrapper providing signature access
54
+ sig { params(predictor: T.untyped).returns(SignatureWrapper) }
55
+ def self.get_signature(predictor)
56
+ SignatureWrapper.new(predictor)
57
+ end
58
+
59
+ # Set signature on a predictor (Python compatibility)
60
+ # Updates the predictor's prompt with new instructions
61
+ #
62
+ # @param predictor [Predict] The predictor to update
63
+ # @param updated_signature [SignatureWrapper] The updated signature wrapper
64
+ sig { params(predictor: T.untyped, updated_signature: SignatureWrapper).void }
65
+ def self.set_signature(predictor, updated_signature)
66
+ # Extract pending instructions from the wrapper
67
+ new_instructions = updated_signature.pending_instructions
68
+
69
+ if new_instructions
70
+ # Update the predictor's prompt with new instructions
71
+ # We mutate the prompt's instruction directly for MIPROv2 compatibility
72
+ predictor.prompt.instance_variable_set(:@instruction, new_instructions)
73
+ end
74
+ end
75
+
76
+ # Create a minibatch from the trainset using random sampling
77
+ # This function is compatible with Python DSPy's MIPROv2 implementation
78
+ #
79
+ # @param trainset [Array] The training dataset to sample from
80
+ # @param batch_size [Integer] The desired size of the minibatch (default: 50)
81
+ # @param rng [Random, nil] Optional random number generator for reproducible sampling
82
+ # @return [Array] A randomly sampled subset of the trainset
83
+ sig do
84
+ params(
85
+ trainset: T::Array[T.untyped],
86
+ batch_size: Integer,
87
+ rng: T.nilable(Random)
88
+ ).returns(T::Array[T.untyped])
89
+ end
90
+ def self.create_minibatch(trainset, batch_size = 50, rng = nil)
91
+ # Ensure batch_size isn't larger than the size of the dataset
92
+ actual_batch_size = [batch_size, trainset.size].min
93
+
94
+ # Randomly sample from trainset
95
+ # If RNG is provided, use it for reproducible sampling
96
+ if rng
97
+ trainset.sample(actual_batch_size, random: rng)
98
+ else
99
+ trainset.sample(actual_batch_size)
100
+ end
101
+ end
102
+
103
+ # Get program with highest average score from minibatch trials
104
+ # Used as a helper function for Bayesian + minibatching optimizers
105
+ #
106
+ # @param param_score_dict [Hash] Maps combo keys to arrays of [score, program, params] tuples
107
+ # @param fully_evaled_param_combos [Array] List of combo keys that have been fully evaluated
108
+ # @return [Array] Returns [program, mean_score, combo_key, params]
109
+ sig do
110
+ params(
111
+ param_score_dict: T::Hash[String, T::Array[T::Array[T.untyped]]],
112
+ fully_evaled_param_combos: T::Array[String]
113
+ ).returns([T.untyped, Float, String, T::Hash[Symbol, T.untyped]])
114
+ end
115
+ def self.get_program_with_highest_avg_score(param_score_dict, fully_evaled_param_combos)
116
+ # Calculate the mean for each combination of categorical parameters, based on past trials
117
+ results = []
118
+ param_score_dict.each do |key, values|
119
+ scores = values.map { |v| v[0] }
120
+ mean = scores.sum.to_f / scores.size
121
+ program = values[0][1]
122
+ params = values[0][2]
123
+ results << [key, mean, program, params]
124
+ end
125
+
126
+ # Sort results by the mean in descending order
127
+ sorted_results = results.sort_by { |_key, mean, _program, _params| -mean }
128
+
129
+ # Find the combination with the highest mean, skip fully evaluated ones
130
+ sorted_results.each do |key, mean, program, params|
131
+ next if fully_evaled_param_combos.include?(key)
132
+ return [program, mean, key, params]
133
+ end
134
+
135
+ # If no valid program is found, return the last valid one
136
+ _key, mean, program, params = sorted_results.last
137
+ [program, mean, _key, params]
138
+ end
139
+
140
+ # Save a candidate program to the log directory
141
+ # Used during optimization to save intermediate trial results
142
+ #
143
+ # @param program [Module] The program to save
144
+ # @param log_dir [String, nil] The directory to save to (returns nil if nil)
145
+ # @param trial_num [Integer] The trial number for naming the file
146
+ # @param note [String, nil] Optional note to append to filename
147
+ # @return [String, nil] The path where program was saved, or nil if log_dir is nil
148
+ sig do
149
+ params(
150
+ program: T.untyped,
151
+ log_dir: T.nilable(String),
152
+ trial_num: Integer,
153
+ note: T.nilable(String)
154
+ ).returns(T.nilable(String))
155
+ end
156
+ def self.save_candidate_program(program, log_dir, trial_num, note: nil)
157
+ return nil if log_dir.nil?
158
+
159
+ # Ensure the directory exists
160
+ eval_programs_dir = File.join(log_dir, "evaluated_programs")
161
+ FileUtils.mkdir_p(eval_programs_dir) unless Dir.exist?(eval_programs_dir)
162
+
163
+ # Define the save path for the program
164
+ filename = if note
165
+ "program_#{trial_num}_#{note}.json"
166
+ else
167
+ "program_#{trial_num}.json"
168
+ end
169
+ save_path = File.join(eval_programs_dir, filename)
170
+
171
+ # Save the program
172
+ program.save(save_path)
173
+
174
+ save_path
175
+ end
176
+
15
177
  # Configuration for bootstrap operations
16
178
  class BootstrapConfig
17
179
  extend T::Sig
@@ -50,6 +212,9 @@ module DSPy
50
212
  end
51
213
 
52
214
  # Result of bootstrap operation
215
+ # @deprecated This class is deprecated and kept only for backward compatibility.
216
+ # The new create_n_fewshot_demo_sets returns a Hash{predictor_idx => [[demos]]}
217
+ # instead of this BootstrapResult object. Use the dict interface directly.
53
218
  class BootstrapResult
54
219
  extend T::Sig
55
220
 
@@ -93,58 +258,200 @@ module DSPy
93
258
  end
94
259
  end
95
260
 
96
- # Create multiple candidate sets of few-shot examples through bootstrapping
261
+ # Create multiple candidate sets of few-shot demonstrations using different bootstrap strategies
262
+ #
263
+ # This is the Python-compatible implementation that uses a seed-based loop to create
264
+ # demo sets using 4 strategies: ZeroShot (-3), LabeledOnly (-2), Unshuffled (-1), and Shuffled (>=0)
265
+ #
266
+ # @param student [DSPy::Module] The student program to bootstrap
267
+ # @param num_candidate_sets [Integer] Number of demo sets to create (accounts for special seeds)
268
+ # @param trainset [Array<DSPy::Example>] Training examples
269
+ # @param max_bootstrapped_demos [Integer] Maximum bootstrapped demonstrations per set
270
+ # @param max_labeled_demos [Integer] Maximum labeled demonstrations to prepend
271
+ # @param min_num_samples [Integer] Minimum number of samples for shuffled strategy
272
+ # @param metric [Proc] Optional metric to validate bootstrapped examples
273
+ # @param teacher_settings [Hash] Settings for teacher program (future use)
274
+ # @param seed [Integer] Random seed for reproducibility
275
+ # @param include_non_bootstrapped [Boolean] Include ZeroShot and LabeledOnly strategies
276
+ # @param labeled_sample [Boolean] Whether to sample labeled examples randomly
277
+ # @return [Hash{Integer => Array<Array<DSPy::FewShotExample>>}] Map of predictor index to demo sets
97
278
  sig do
98
279
  params(
99
- program: T.untyped,
280
+ student: T.untyped,
281
+ num_candidate_sets: Integer,
100
282
  trainset: T::Array[T.untyped],
101
- config: BootstrapConfig,
102
- metric: T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T::Boolean))
103
- ).returns(BootstrapResult)
283
+ max_bootstrapped_demos: Integer,
284
+ max_labeled_demos: Integer,
285
+ min_num_samples: Integer,
286
+ metric: T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T::Boolean)),
287
+ teacher_settings: T::Hash[Symbol, T.untyped],
288
+ seed: T.nilable(Integer),
289
+ include_non_bootstrapped: T::Boolean,
290
+ labeled_sample: T::Boolean
291
+ ).returns(T::Hash[Integer, T::Array[T::Array[DSPy::FewShotExample]]])
104
292
  end
105
- def self.create_n_fewshot_demo_sets(program, trainset, config: BootstrapConfig.new, metric: nil)
106
- DSPy::Context.with_span(
107
- operation: 'optimization.bootstrap_start',
108
- 'dspy.module' => 'Bootstrap',
109
- 'bootstrap.trainset_size' => trainset.size,
110
- 'bootstrap.max_examples' => config.max_bootstrapped_examples,
111
- 'bootstrap.num_candidate_sets' => config.num_candidate_sets
112
- ) do
113
- # Convert to typed examples if needed
114
- typed_examples = ensure_typed_examples(trainset)
115
-
116
- # Generate successful examples through bootstrap
117
- successful_examples, failed_examples = generate_successful_examples(
118
- program,
119
- typed_examples,
120
- config,
121
- metric
122
- )
293
+ def self.create_n_fewshot_demo_sets(
294
+ student,
295
+ num_candidate_sets,
296
+ trainset,
297
+ max_bootstrapped_demos: 3,
298
+ max_labeled_demos: 3,
299
+ min_num_samples: 1,
300
+ metric: nil,
301
+ teacher_settings: {},
302
+ seed: nil,
303
+ include_non_bootstrapped: true,
304
+ labeled_sample: true
305
+ )
306
+ demo_candidates = Hash.new { |h, k| h[k] = [] }
307
+ rng = seed ? Random.new(seed) : Random.new
308
+
309
+ # Determine number of predictors exposed by the student module
310
+ num_predictors = if student.respond_to?(:predictors)
311
+ predictors = Array(student.predictors)
312
+ predictors.empty? ? 1 : predictors.size
313
+ else
314
+ 1
315
+ end
123
316
 
124
- # Create candidate sets from successful examples
125
- candidate_sets = create_candidate_sets(successful_examples, config)
126
-
127
- # Gather statistics
128
- statistics = {
129
- total_trainset: trainset.size,
130
- successful_count: successful_examples.size,
131
- failed_count: failed_examples.size,
132
- success_rate: successful_examples.size.to_f / (successful_examples.size + failed_examples.size),
133
- candidate_sets_created: candidate_sets.size,
134
- average_set_size: candidate_sets.empty? ? 0 : candidate_sets.map(&:size).sum.to_f / candidate_sets.size
135
- }
317
+ # Adjust for 3 special seeds (-3, -2, -1)
318
+ adjusted_num_sets = num_candidate_sets - 3
319
+
320
+ # Loop from -3 to adjusted_num_sets (exclusive)
321
+ (-3...adjusted_num_sets).each do |current_seed|
322
+ case current_seed
323
+ when -3 # ZeroShot strategy
324
+ next unless include_non_bootstrapped
325
+ # Empty demo sets for all predictors
326
+ num_predictors.times { |idx| demo_candidates[idx] << [] }
327
+
328
+ when -2 # LabeledOnly strategy
329
+ next unless include_non_bootstrapped && max_labeled_demos > 0
330
+ # Sample or take labeled examples
331
+ labeled_demos = create_labeled_demos(trainset, max_labeled_demos, labeled_sample, rng)
332
+ num_predictors.times { |idx| demo_candidates[idx] << labeled_demos }
333
+
334
+ when -1 # Unshuffled strategy
335
+ # Bootstrap without shuffle
336
+ bootstrapped_demos = create_bootstrapped_demos(
337
+ student, trainset, max_bootstrapped_demos, max_labeled_demos, metric
338
+ )
339
+ num_predictors.times { |idx| demo_candidates[idx] << bootstrapped_demos }
340
+
341
+ else # Shuffled strategies (seed >= 0)
342
+ # Shuffle trainset with current seed
343
+ seed_rng = Random.new(current_seed)
344
+ shuffled_trainset = trainset.shuffle(random: seed_rng)
345
+
346
+ # Random demo count between min and max
347
+ num_demos = seed_rng.rand(min_num_samples..max_bootstrapped_demos)
348
+
349
+ # Bootstrap with shuffled data
350
+ bootstrapped_demos = create_bootstrapped_demos(
351
+ student, shuffled_trainset, num_demos, max_labeled_demos, metric
352
+ )
353
+ num_predictors.times { |idx| demo_candidates[idx] << bootstrapped_demos }
354
+ end
355
+ end
136
356
 
137
- emit_bootstrap_complete_event(statistics)
357
+ demo_candidates
358
+ end
138
359
 
139
- BootstrapResult.new(
140
- candidate_sets: candidate_sets,
141
- successful_examples: successful_examples,
142
- failed_examples: failed_examples,
143
- statistics: statistics
360
+ # Create labeled demonstrations from trainset examples
361
+ sig do
362
+ params(
363
+ trainset: T::Array[T.untyped],
364
+ max_labeled: Integer,
365
+ labeled_sample: T::Boolean,
366
+ rng: Random
367
+ ).returns(T::Array[DSPy::FewShotExample])
368
+ end
369
+ def self.create_labeled_demos(trainset, max_labeled, labeled_sample, rng)
370
+ examples = if labeled_sample
371
+ trainset.sample([max_labeled, trainset.size].min, random: rng)
372
+ else
373
+ trainset.take(max_labeled)
374
+ end
375
+
376
+ examples.map do |ex|
377
+ DSPy::FewShotExample.new(
378
+ input: ex.input_values,
379
+ output: ex.expected_values
144
380
  )
145
381
  end
146
382
  end
147
383
 
384
+ # Create bootstrapped demonstrations by executing student on trainset
385
+ sig do
386
+ params(
387
+ student: T.untyped,
388
+ trainset: T::Array[T.untyped],
389
+ max_bootstrapped: Integer,
390
+ max_labeled: Integer,
391
+ metric: T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T::Boolean))
392
+ ).returns(T::Array[DSPy::FewShotExample])
393
+ end
394
+ def self.create_bootstrapped_demos(student, trainset, max_bootstrapped, max_labeled, metric)
395
+ successful_demos = []
396
+
397
+ # Execute student on trainset to bootstrap demonstrations
398
+ trainset.each do |example|
399
+ break if successful_demos.size >= max_bootstrapped
400
+
401
+ begin
402
+ # Call student with input
403
+ prediction = student.call(**example.input_values)
404
+ prediction_hash = prediction.respond_to?(:to_h) ? prediction.to_h : prediction
405
+
406
+ # Check if prediction matches expected output
407
+ success = if metric
408
+ metric.call(example, prediction_hash)
409
+ else
410
+ example.matches_prediction?(prediction_hash)
411
+ end
412
+
413
+ if success
414
+ # Extract only output fields from prediction
415
+ output_fields = extract_output_fields_for_demo(prediction_hash, example.signature_class)
416
+
417
+ demo = DSPy::FewShotExample.new(
418
+ input: example.input_values,
419
+ output: output_fields
420
+ )
421
+ successful_demos << demo
422
+ end
423
+ rescue => e
424
+ # Continue on errors
425
+ DSPy.logger.warn("Bootstrap error: #{e.message}") if DSPy.logger
426
+ end
427
+ end
428
+
429
+ # Prepend labeled examples if requested
430
+ if max_labeled > 0
431
+ labeled = trainset.take(max_labeled).map do |ex|
432
+ DSPy::FewShotExample.new(
433
+ input: ex.input_values,
434
+ output: ex.expected_values
435
+ )
436
+ end
437
+ successful_demos = labeled + successful_demos
438
+ end
439
+
440
+ successful_demos
441
+ end
442
+
443
+ # Extract only output fields from prediction hash
444
+ sig do
445
+ params(
446
+ prediction_hash: T::Hash[Symbol, T.untyped],
447
+ signature_class: T.class_of(DSPy::Signature)
448
+ ).returns(T::Hash[Symbol, T.untyped])
449
+ end
450
+ def self.extract_output_fields_for_demo(prediction_hash, signature_class)
451
+ output_field_names = signature_class.output_field_descriptors.keys
452
+ prediction_hash.slice(*output_field_names)
453
+ end
454
+
148
455
  # Evaluate a candidate program on examples with proper error handling
149
456
  sig do
150
457
  params(
@@ -404,4 +711,4 @@ module DSPy
404
711
  end
405
712
  end
406
713
  end
407
- end
714
+ 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.28.1"
5
- end
4
+ VERSION = "0.29.0"
5
+ end
data/lib/dspy.rb CHANGED
@@ -12,6 +12,7 @@ require_relative 'dspy/observability/observation_type'
12
12
  require_relative 'dspy/context'
13
13
  require_relative 'dspy/events'
14
14
  require_relative 'dspy/events/types'
15
+ require_relative 'dspy/reflection_lm'
15
16
 
16
17
  module DSPy
17
18
  extend Dry::Configurable
@@ -191,12 +192,14 @@ module DSPy
191
192
  end
192
193
  end
193
194
 
195
+ require_relative 'dspy/callbacks'
194
196
  require_relative 'dspy/module'
195
197
  require_relative 'dspy/field'
196
198
  require_relative 'dspy/signature'
197
199
  require_relative 'dspy/few_shot_example'
198
200
  require_relative 'dspy/prompt'
199
201
  require_relative 'dspy/example'
202
+ require_relative 'dspy/datasets'
200
203
  require_relative 'dspy/lm'
201
204
  require_relative 'dspy/image'
202
205
  require_relative 'dspy/prediction'
@@ -210,10 +213,9 @@ require_relative 'dspy/evaluate'
210
213
  require_relative 'dspy/teleprompt/teleprompter'
211
214
  require_relative 'dspy/teleprompt/utils'
212
215
  require_relative 'dspy/teleprompt/data_handler'
216
+ require_relative 'dspy/teleprompt/gepa'
213
217
  require_relative 'dspy/propose/grounded_proposer'
214
- require_relative 'dspy/teleprompt/simple_optimizer'
215
218
  require_relative 'dspy/teleprompt/mipro_v2'
216
- require_relative 'dspy/teleprompt/gepa'
217
219
  require_relative 'dspy/tools'
218
220
  require_relative 'dspy/memory'
219
221
  require_relative 'dspy/storage/program_storage'
data/lib/gepa/api.rb ADDED
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ require_relative 'core/engine'
6
+ require_relative 'core/result'
7
+
8
+ module GEPA
9
+ extend T::Sig
10
+ module_function
11
+
12
+ sig do
13
+ params(
14
+ seed_candidate: T::Hash[String, String],
15
+ trainset: T::Array[T.untyped],
16
+ valset: T::Array[T.untyped],
17
+ adapter: T.untyped,
18
+ reflective_proposer: T.untyped,
19
+ merge_proposer: T.nilable(T.untyped),
20
+ logger: T.untyped,
21
+ experiment_tracker: T.untyped,
22
+ max_metric_calls: Integer,
23
+ telemetry: T.nilable(T.untyped)
24
+ ).returns(GEPA::Core::Result)
25
+ end
26
+ def optimize(
27
+ seed_candidate:,
28
+ trainset:,
29
+ valset:,
30
+ adapter:,
31
+ reflective_proposer:,
32
+ merge_proposer: nil,
33
+ logger:,
34
+ experiment_tracker:,
35
+ max_metric_calls:,
36
+ telemetry: nil
37
+ )
38
+ evaluator = proc { |dataset, candidate| adapter.evaluate(dataset, candidate) }
39
+
40
+ engine = GEPA::Core::Engine.new(
41
+ run_dir: nil,
42
+ evaluator: evaluator,
43
+ valset: valset,
44
+ seed_candidate: seed_candidate,
45
+ max_metric_calls: max_metric_calls,
46
+ perfect_score: Float::INFINITY,
47
+ seed: 0,
48
+ reflective_proposer: reflective_proposer,
49
+ merge_proposer: merge_proposer,
50
+ logger: logger,
51
+ experiment_tracker: experiment_tracker,
52
+ telemetry: telemetry || GEPA::Telemetry,
53
+ track_best_outputs: false,
54
+ display_progress_bar: false,
55
+ raise_on_exception: true
56
+ )
57
+
58
+ state = engine.run
59
+ GEPA::Core::Result.from_state(state)
60
+ end
61
+ end