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.
- checksums.yaml +4 -4
- data/README.md +2 -3
- data/lib/dspy/callbacks.rb +222 -0
- data/lib/dspy/chain_of_thought.rb +2 -1
- 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 +9 -12
- data/lib/dspy/mixins/struct_builder.rb +17 -25
- data/lib/dspy/module.rb +45 -1
- data/lib/dspy/observability/async_span_processor.rb +67 -93
- data/lib/dspy/observability.rb +43 -1
- data/lib/dspy/predict.rb +17 -0
- data/lib/dspy/prompt.rb +90 -20
- data/lib/dspy/propose/dataset_summary_generator.rb +210 -0
- data/lib/dspy/propose/grounded_proposer.rb +320 -66
- data/lib/dspy/re_act.rb +13 -0
- data/lib/dspy/reflection_lm.rb +36 -0
- data/lib/dspy/teleprompt/bootstrap_strategy.rb +26 -0
- data/lib/dspy/teleprompt/gepa.rb +448 -2803
- data/lib/dspy/teleprompt/mipro_v2.rb +624 -100
- data/lib/dspy/teleprompt/utils.rb +349 -42
- data/lib/dspy/version.rb +2 -2
- data/lib/dspy.rb +4 -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 +59 -4
- 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
|
-
|
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 :
|
52
|
+
attr_accessor :view_data_batch_size
|
23
53
|
|
24
54
|
sig { returns(Integer) }
|
25
|
-
attr_accessor :
|
55
|
+
attr_accessor :num_demos_in_context
|
26
56
|
|
27
57
|
sig { returns(T::Boolean) }
|
28
|
-
attr_accessor :
|
58
|
+
attr_accessor :set_tip_randomly
|
29
59
|
|
30
60
|
sig { returns(T::Boolean) }
|
31
|
-
attr_accessor :
|
61
|
+
attr_accessor :set_history_randomly
|
32
62
|
|
33
|
-
sig { returns(
|
34
|
-
attr_accessor :
|
63
|
+
sig { returns(Float) }
|
64
|
+
attr_accessor :init_temperature
|
35
65
|
|
36
|
-
sig { returns(
|
37
|
-
attr_accessor :
|
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
|
-
|
43
|
-
|
44
|
-
@
|
45
|
-
@
|
46
|
-
@
|
47
|
-
@
|
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
|
92
|
-
|
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:
|
130
|
-
num_examples_analyzed: [examples.size, @config.
|
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.
|
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(
|
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
|
-
|
385
|
-
|
386
|
-
|
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
|
-
|
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
|
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' =>
|
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
|