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.
- checksums.yaml +4 -4
- data/README.md +2 -3
- data/lib/dspy/code_act.rb +14 -1
- data/lib/dspy/datasets/ade.rb +90 -0
- data/lib/dspy/datasets.rb +8 -0
- data/lib/dspy/lm.rb +4 -8
- data/lib/dspy/mixins/struct_builder.rb +17 -25
- data/lib/dspy/module.rb +12 -1
- data/lib/dspy/observability/async_span_processor.rb +67 -93
- data/lib/dspy/observability.rb +43 -1
- data/lib/dspy/predict.rb +10 -0
- data/lib/dspy/propose/dataset_summary_generator.rb +36 -3
- data/lib/dspy/propose/grounded_proposer.rb +118 -11
- data/lib/dspy/re_act.rb +13 -0
- data/lib/dspy/reflection_lm.rb +36 -0
- data/lib/dspy/teleprompt/gepa.rb +448 -2803
- data/lib/dspy/teleprompt/mipro_v2.rb +564 -65
- data/lib/dspy/teleprompt/utils.rb +8 -3
- data/lib/dspy/version.rb +2 -2
- data/lib/dspy.rb +3 -2
- 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.rb +21 -0
- metadata +42 -4
- 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
|