gepa 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 +7 -0
- data/LICENSE +45 -0
- data/README.md +247 -0
- data/lib/gepa/api.rb +61 -0
- data/lib/gepa/core/engine.rb +226 -0
- data/lib/gepa/core/evaluation_batch.rb +26 -0
- data/lib/gepa/core/result.rb +92 -0
- data/lib/gepa/core/state.rb +231 -0
- data/lib/gepa/logging/experiment_tracker.rb +54 -0
- data/lib/gepa/logging/logger.rb +57 -0
- data/lib/gepa/logging.rb +9 -0
- data/lib/gepa/proposer/base.rb +27 -0
- data/lib/gepa/proposer/merge_proposer.rb +424 -0
- data/lib/gepa/proposer/reflective_mutation/base.rb +48 -0
- data/lib/gepa/proposer/reflective_mutation/reflective_mutation.rb +188 -0
- data/lib/gepa/strategies/batch_sampler.rb +91 -0
- data/lib/gepa/strategies/candidate_selector.rb +97 -0
- data/lib/gepa/strategies/component_selector.rb +57 -0
- data/lib/gepa/strategies/instruction_proposal.rb +120 -0
- data/lib/gepa/telemetry.rb +122 -0
- data/lib/gepa/utils/pareto.rb +119 -0
- data/lib/gepa/version.rb +5 -0
- data/lib/gepa.rb +22 -0
- metadata +78 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'set'
|
|
5
|
+
require 'sorbet-runtime'
|
|
6
|
+
|
|
7
|
+
module GEPA
|
|
8
|
+
module Core
|
|
9
|
+
# Snapshot of GEPA optimization output with helpers for common queries.
|
|
10
|
+
class Result < T::Struct
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
const :candidates, T::Array[T::Hash[String, String]]
|
|
14
|
+
const :parents, T::Array[T::Array[T.nilable(Integer)]]
|
|
15
|
+
const :val_aggregate_scores, T::Array[Float]
|
|
16
|
+
const :val_subscores, T::Array[T::Array[Float]]
|
|
17
|
+
const :per_val_instance_best_candidates, T::Array[T::Array[Integer]]
|
|
18
|
+
const :discovery_eval_counts, T::Array[Integer]
|
|
19
|
+
const :best_outputs_valset, T.nilable(T::Array[T::Array[T::Array[T.untyped]]]), default: nil
|
|
20
|
+
const :total_metric_calls, T.nilable(Integer), default: nil
|
|
21
|
+
const :num_full_val_evals, T.nilable(Integer), default: nil
|
|
22
|
+
const :run_dir, T.nilable(String), default: nil
|
|
23
|
+
const :seed, T.nilable(Integer), default: nil
|
|
24
|
+
|
|
25
|
+
sig { returns(Integer) }
|
|
26
|
+
def num_candidates
|
|
27
|
+
candidates.length
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { returns(Integer) }
|
|
31
|
+
def num_val_instances
|
|
32
|
+
per_val_instance_best_candidates.length
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { returns(Integer) }
|
|
36
|
+
def best_idx
|
|
37
|
+
val_aggregate_scores.each_with_index.max_by { |score, _i| score }&.last || 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sig { returns(T::Hash[String, String]) }
|
|
41
|
+
def best_candidate
|
|
42
|
+
candidates.fetch(best_idx)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
46
|
+
def to_h
|
|
47
|
+
{
|
|
48
|
+
candidates: candidates.map(&:dup),
|
|
49
|
+
parents: parents.map(&:dup),
|
|
50
|
+
val_aggregate_scores: val_aggregate_scores.dup,
|
|
51
|
+
val_subscores: val_subscores.map(&:dup),
|
|
52
|
+
best_outputs_valset: best_outputs_valset&.map { |arr| arr.map(&:dup) },
|
|
53
|
+
per_val_instance_best_candidates: per_val_instance_best_candidates.map(&:dup),
|
|
54
|
+
discovery_eval_counts: discovery_eval_counts.dup,
|
|
55
|
+
total_metric_calls: total_metric_calls,
|
|
56
|
+
num_full_val_evals: num_full_val_evals,
|
|
57
|
+
run_dir: run_dir,
|
|
58
|
+
seed: seed,
|
|
59
|
+
best_idx: best_idx
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { returns(String) }
|
|
64
|
+
def to_json(*_args)
|
|
65
|
+
JSON.pretty_generate(to_h)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig do
|
|
69
|
+
params(
|
|
70
|
+
state: T.untyped,
|
|
71
|
+
run_dir: T.nilable(String),
|
|
72
|
+
seed: T.nilable(Integer)
|
|
73
|
+
).returns(Result)
|
|
74
|
+
end
|
|
75
|
+
def self.from_state(state, run_dir: nil, seed: nil)
|
|
76
|
+
new(
|
|
77
|
+
candidates: state.program_candidates.map(&:dup),
|
|
78
|
+
parents: state.parent_program_for_candidate.map(&:dup),
|
|
79
|
+
val_aggregate_scores: state.program_full_scores_val_set.map(&:to_f),
|
|
80
|
+
best_outputs_valset: state.respond_to?(:best_outputs_valset) ? state.best_outputs_valset&.map(&:dup) : nil,
|
|
81
|
+
val_subscores: state.prog_candidate_val_subscores.map { |scores| scores.map(&:to_f) },
|
|
82
|
+
per_val_instance_best_candidates: state.program_at_pareto_front_valset.map { |set| set.to_a },
|
|
83
|
+
discovery_eval_counts: state.num_metric_calls_by_discovery.map(&:to_i),
|
|
84
|
+
total_metric_calls: state.respond_to?(:total_num_evals) ? state.total_num_evals : nil,
|
|
85
|
+
num_full_val_evals: state.respond_to?(:num_full_ds_evals) ? state.num_full_ds_evals : nil,
|
|
86
|
+
run_dir: run_dir,
|
|
87
|
+
seed: seed
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'set'
|
|
6
|
+
require 'sorbet-runtime'
|
|
7
|
+
|
|
8
|
+
require_relative '../utils/pareto'
|
|
9
|
+
require_relative '../telemetry'
|
|
10
|
+
|
|
11
|
+
module GEPA
|
|
12
|
+
module Core
|
|
13
|
+
class State
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
attr_accessor :i, :num_full_ds_evals, :total_num_evals
|
|
17
|
+
attr_reader :program_candidates,
|
|
18
|
+
:parent_program_for_candidate,
|
|
19
|
+
:program_full_scores_val_set,
|
|
20
|
+
:program_at_pareto_front_valset,
|
|
21
|
+
:prog_candidate_val_subscores,
|
|
22
|
+
:list_of_named_predictors,
|
|
23
|
+
:named_predictor_id_to_update_next_for_program_candidate,
|
|
24
|
+
:num_metric_calls_by_discovery,
|
|
25
|
+
:full_program_trace,
|
|
26
|
+
:per_program_tracked_scores,
|
|
27
|
+
:pareto_front_valset,
|
|
28
|
+
:best_outputs_valset
|
|
29
|
+
|
|
30
|
+
sig do
|
|
31
|
+
params(
|
|
32
|
+
seed_candidate: T::Hash[String, String],
|
|
33
|
+
base_valset_eval_output: [T::Array[T.untyped], T::Array[Float]],
|
|
34
|
+
track_best_outputs: T::Boolean
|
|
35
|
+
).void
|
|
36
|
+
end
|
|
37
|
+
def initialize(seed_candidate, base_valset_eval_output, track_best_outputs: false)
|
|
38
|
+
outputs, scores = base_valset_eval_output
|
|
39
|
+
raise ArgumentError, 'validation scores must not be empty' if scores.empty?
|
|
40
|
+
|
|
41
|
+
valset_base_score = scores.sum / scores.length.to_f
|
|
42
|
+
|
|
43
|
+
@program_candidates = [seed_candidate.dup]
|
|
44
|
+
@program_full_scores_val_set = [valset_base_score]
|
|
45
|
+
@per_program_tracked_scores = [valset_base_score]
|
|
46
|
+
|
|
47
|
+
@pareto_front_valset = scores.dup
|
|
48
|
+
@parent_program_for_candidate = [[nil]]
|
|
49
|
+
@program_at_pareto_front_valset = Array.new(scores.length) { Set.new([0]) }
|
|
50
|
+
|
|
51
|
+
@list_of_named_predictors = seed_candidate.keys
|
|
52
|
+
@named_predictor_id_to_update_next_for_program_candidate = [0]
|
|
53
|
+
|
|
54
|
+
@prog_candidate_val_subscores = [scores.dup]
|
|
55
|
+
@num_metric_calls_by_discovery = [0]
|
|
56
|
+
|
|
57
|
+
@best_outputs_valset = if track_best_outputs
|
|
58
|
+
outputs.map { |output| [[0, output]] }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@full_program_trace = []
|
|
62
|
+
@i = -1
|
|
63
|
+
@num_full_ds_evals = 0
|
|
64
|
+
@total_num_evals = 0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { returns(T::Boolean) }
|
|
68
|
+
def consistent?
|
|
69
|
+
size = @program_candidates.length
|
|
70
|
+
raise 'program_full_scores_val_set mismatch' unless @program_full_scores_val_set.length == size
|
|
71
|
+
raise 'per_program_tracked_scores mismatch' unless @per_program_tracked_scores.length == size
|
|
72
|
+
raise 'parent_program_for_candidate mismatch' unless @parent_program_for_candidate.length == size
|
|
73
|
+
raise 'named_predictor_id_to_update mismatch' unless @named_predictor_id_to_update_next_for_program_candidate.length == size
|
|
74
|
+
raise 'prog_candidate_val_subscores mismatch' unless @prog_candidate_val_subscores.length == size
|
|
75
|
+
raise 'num_metric_calls mismatch' unless @num_metric_calls_by_discovery.length == size
|
|
76
|
+
raise 'pareto fronts length mismatch' unless @pareto_front_valset.length == @program_at_pareto_front_valset.length
|
|
77
|
+
|
|
78
|
+
@program_at_pareto_front_valset.each do |front|
|
|
79
|
+
front.each do |idx|
|
|
80
|
+
raise 'pareto index out of range' unless idx < size
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig { params(run_dir: T.nilable(String)).void }
|
|
87
|
+
def save(run_dir)
|
|
88
|
+
return if run_dir.nil?
|
|
89
|
+
|
|
90
|
+
FileUtils.mkdir_p(run_dir)
|
|
91
|
+
File.open(File.join(run_dir, 'gepa_state.bin'), 'wb') do |file|
|
|
92
|
+
data = instance_variables.each_with_object({}) do |ivar, acc|
|
|
93
|
+
acc[ivar.to_s.delete('@')] = instance_variable_get(ivar)
|
|
94
|
+
end
|
|
95
|
+
Marshal.dump(data, file)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sig { params(run_dir: String).returns(State) }
|
|
100
|
+
def self.load(run_dir)
|
|
101
|
+
File.open(File.join(run_dir, 'gepa_state.bin'), 'rb') do |file|
|
|
102
|
+
data = Marshal.load(file)
|
|
103
|
+
state = allocate
|
|
104
|
+
data.each { |key, value| state.instance_variable_set("@#{key}", value) }
|
|
105
|
+
state.consistent?
|
|
106
|
+
state
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
sig do
|
|
111
|
+
params(
|
|
112
|
+
parent_program_idx: T::Array[Integer],
|
|
113
|
+
new_program: T::Hash[String, String],
|
|
114
|
+
valset_score: Float,
|
|
115
|
+
valset_outputs: T::Array[T.untyped],
|
|
116
|
+
valset_subscores: T::Array[Float],
|
|
117
|
+
run_dir: T.nilable(String),
|
|
118
|
+
num_metric_calls: Integer
|
|
119
|
+
).returns([Integer, Integer])
|
|
120
|
+
end
|
|
121
|
+
def update_state_with_new_program(
|
|
122
|
+
parent_program_idx,
|
|
123
|
+
new_program,
|
|
124
|
+
valset_score,
|
|
125
|
+
valset_outputs,
|
|
126
|
+
valset_subscores,
|
|
127
|
+
run_dir,
|
|
128
|
+
num_metric_calls
|
|
129
|
+
)
|
|
130
|
+
new_program_idx = @program_candidates.length
|
|
131
|
+
@program_candidates << new_program.dup
|
|
132
|
+
@num_metric_calls_by_discovery << num_metric_calls
|
|
133
|
+
|
|
134
|
+
max_predictor_id = parent_program_idx.map { |idx| @named_predictor_id_to_update_next_for_program_candidate[idx] }.compact.max
|
|
135
|
+
@named_predictor_id_to_update_next_for_program_candidate << (max_predictor_id || 0)
|
|
136
|
+
@parent_program_for_candidate << parent_program_idx.dup
|
|
137
|
+
|
|
138
|
+
@prog_candidate_val_subscores << valset_subscores.dup
|
|
139
|
+
@program_full_scores_val_set << valset_score.to_f
|
|
140
|
+
|
|
141
|
+
valset_subscores.each_with_index do |new_score, task_idx|
|
|
142
|
+
old_score = @pareto_front_valset[task_idx]
|
|
143
|
+
if new_score > old_score
|
|
144
|
+
@pareto_front_valset[task_idx] = new_score
|
|
145
|
+
@program_at_pareto_front_valset[task_idx] = Set.new([new_program_idx])
|
|
146
|
+
if @best_outputs_valset
|
|
147
|
+
@best_outputs_valset[task_idx] = [[new_program_idx, valset_outputs[task_idx]]]
|
|
148
|
+
end
|
|
149
|
+
write_best_output(run_dir, task_idx, new_program_idx, valset_outputs[task_idx])
|
|
150
|
+
elsif new_score == old_score
|
|
151
|
+
@program_at_pareto_front_valset[task_idx].add(new_program_idx)
|
|
152
|
+
if @best_outputs_valset
|
|
153
|
+
@best_outputs_valset[task_idx] << [new_program_idx, valset_outputs[task_idx]]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
raise 'valset subscores length mismatch' unless valset_subscores.length == @program_at_pareto_front_valset.length
|
|
159
|
+
|
|
160
|
+
@per_program_tracked_scores = @program_full_scores_val_set.dup
|
|
161
|
+
linear_idx = GEPA::Utils::Pareto.idxmax(@per_program_tracked_scores)
|
|
162
|
+
|
|
163
|
+
[new_program_idx, linear_idx]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
sig do
|
|
167
|
+
params(
|
|
168
|
+
eval_output: [T::Array[T.untyped], T::Array[Float]],
|
|
169
|
+
output_dir: String
|
|
170
|
+
).void
|
|
171
|
+
end
|
|
172
|
+
def self.write_eval_output_to_directory(eval_output, output_dir)
|
|
173
|
+
_, scores = eval_output
|
|
174
|
+
scores.each_with_index do |_score, task_idx|
|
|
175
|
+
dir = File.join(output_dir, "task_#{task_idx}")
|
|
176
|
+
FileUtils.mkdir_p(dir)
|
|
177
|
+
path = File.join(dir, 'iter_0_prog_0.json')
|
|
178
|
+
File.write(path, JSON.pretty_generate(scores[task_idx]))
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
sig do
|
|
183
|
+
params(
|
|
184
|
+
run_dir: T.nilable(String),
|
|
185
|
+
logger: T.untyped,
|
|
186
|
+
seed_candidate: T::Hash[String, String],
|
|
187
|
+
valset_evaluator: T.proc.params(arg0: T::Hash[String, String]).returns([T::Array[T.untyped], T::Array[Float]]),
|
|
188
|
+
track_best_outputs: T::Boolean
|
|
189
|
+
).returns(State)
|
|
190
|
+
end
|
|
191
|
+
def self.initialize_gepa_state(run_dir:, logger:, seed_candidate:, valset_evaluator:, track_best_outputs: false)
|
|
192
|
+
if run_dir && File.exist?(File.join(run_dir, 'gepa_state.bin')) && File.exist?(File.join(run_dir, 'prog_candidates'))
|
|
193
|
+
logger.log('Loading gepa state from run dir')
|
|
194
|
+
return load(run_dir)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
valset_out = valset_evaluator.call(seed_candidate)
|
|
198
|
+
if run_dir
|
|
199
|
+
write_eval_output_to_directory(valset_out, File.join(run_dir, 'generated_best_outputs_valset'))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
state = new(seed_candidate, valset_out, track_best_outputs: track_best_outputs)
|
|
203
|
+
state.num_full_ds_evals = 1
|
|
204
|
+
state.total_num_evals = valset_out.last.length
|
|
205
|
+
state
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
sig do
|
|
211
|
+
params(run_dir: T.nilable(String), task_idx: Integer, program_idx: Integer, output: T.untyped).void
|
|
212
|
+
end
|
|
213
|
+
def write_best_output(run_dir, task_idx, program_idx, output)
|
|
214
|
+
return if run_dir.nil?
|
|
215
|
+
|
|
216
|
+
dir = File.join(run_dir, 'generated_best_outputs_valset', "task_#{task_idx}")
|
|
217
|
+
FileUtils.mkdir_p(dir)
|
|
218
|
+
payload = ensure_jsonable(output)
|
|
219
|
+
File.write(File.join(dir, "iter_#{@i + 1}_prog_#{program_idx}.json"), JSON.pretty_generate(payload))
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
sig { params(value: T.untyped).returns(T.untyped) }
|
|
223
|
+
def ensure_jsonable(value)
|
|
224
|
+
JSON.parse(JSON.generate(value))
|
|
225
|
+
rescue StandardError
|
|
226
|
+
GEPA::Utils::Pareto.json_default(value)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GEPA
|
|
4
|
+
module Logging
|
|
5
|
+
# Lightweight experiment tracker that records metrics locally and can fan out to user hooks.
|
|
6
|
+
class ExperimentTracker
|
|
7
|
+
attr_reader :events
|
|
8
|
+
|
|
9
|
+
def initialize(subscribers: [])
|
|
10
|
+
@subscribers = Array(subscribers)
|
|
11
|
+
@events = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def with_subscriber(proc = nil, &block)
|
|
15
|
+
@subscribers << (proc || block)
|
|
16
|
+
self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize_backends; end
|
|
20
|
+
|
|
21
|
+
def start_run; end
|
|
22
|
+
|
|
23
|
+
def log_metrics(metrics, step: nil)
|
|
24
|
+
entry = { metrics: symbolize_keys(metrics), step: step }
|
|
25
|
+
@events << entry
|
|
26
|
+
|
|
27
|
+
@subscribers.each do |subscriber|
|
|
28
|
+
subscriber.call(entry)
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
DSPy.log('gepa.experiment_tracker.error', error: e.message)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def end_run; end
|
|
35
|
+
|
|
36
|
+
def active?
|
|
37
|
+
!@events.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def each_event(&block)
|
|
41
|
+
@events.each(&block)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def symbolize_keys(hash)
|
|
47
|
+
hash.each_with_object({}) do |(k, v), memo|
|
|
48
|
+
memo[k.to_sym] = v
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
|
|
5
|
+
module GEPA
|
|
6
|
+
module Logging
|
|
7
|
+
# Minimal logger interface used across GEPA components.
|
|
8
|
+
class Logger
|
|
9
|
+
extend Forwardable
|
|
10
|
+
|
|
11
|
+
def initialize(io: $stdout)
|
|
12
|
+
@io = io
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def log(message)
|
|
16
|
+
write(message)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :io
|
|
22
|
+
|
|
23
|
+
def write(message)
|
|
24
|
+
io.puts(message)
|
|
25
|
+
io.flush if io.respond_to?(:flush)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Logger that fans out messages to multiple IO streams.
|
|
30
|
+
class CompositeLogger < Logger
|
|
31
|
+
def initialize(*ios)
|
|
32
|
+
@ios = ios.flatten
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def log(message)
|
|
36
|
+
@ios.each do |io|
|
|
37
|
+
io.puts(message)
|
|
38
|
+
io.flush if io.respond_to?(:flush)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Logger that captures messages into memory (handy for tests).
|
|
44
|
+
class BufferingLogger < Logger
|
|
45
|
+
attr_reader :messages
|
|
46
|
+
|
|
47
|
+
def initialize
|
|
48
|
+
@messages = []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def log(message)
|
|
52
|
+
@messages << message
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
data/lib/gepa/logging.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sorbet-runtime'
|
|
4
|
+
|
|
5
|
+
module GEPA
|
|
6
|
+
module Proposer
|
|
7
|
+
class CandidateProposal < T::Struct
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
const :candidate, T::Hash[String, String]
|
|
11
|
+
const :parent_program_ids, T::Array[Integer]
|
|
12
|
+
const :subsample_indices, T.nilable(T::Array[Integer]), default: nil
|
|
13
|
+
const :subsample_scores_before, T.nilable(T::Array[Float]), default: nil
|
|
14
|
+
const :subsample_scores_after, T.nilable(T::Array[Float]), default: nil
|
|
15
|
+
const :tag, String, default: 'reflective_mutation'
|
|
16
|
+
const :metadata, T::Hash[Symbol, T.untyped], default: {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module ProposeNewCandidate
|
|
20
|
+
extend T::Sig
|
|
21
|
+
|
|
22
|
+
sig { abstract.params(state: GEPA::Core::State).returns(T.nilable(CandidateProposal)) }
|
|
23
|
+
def propose(state); end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|