dspy 0.3.1 → 0.4.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 +69 -382
- data/lib/dspy/chain_of_thought.rb +57 -0
- data/lib/dspy/evaluate.rb +554 -0
- data/lib/dspy/example.rb +203 -0
- data/lib/dspy/few_shot_example.rb +81 -0
- data/lib/dspy/instrumentation.rb +97 -8
- data/lib/dspy/lm/adapter_factory.rb +6 -8
- data/lib/dspy/lm.rb +5 -7
- data/lib/dspy/predict.rb +32 -34
- data/lib/dspy/prompt.rb +222 -0
- data/lib/dspy/propose/grounded_proposer.rb +560 -0
- data/lib/dspy/registry/registry_manager.rb +504 -0
- data/lib/dspy/registry/signature_registry.rb +725 -0
- data/lib/dspy/storage/program_storage.rb +442 -0
- data/lib/dspy/storage/storage_manager.rb +331 -0
- data/lib/dspy/subscribers/langfuse_subscriber.rb +669 -0
- data/lib/dspy/subscribers/logger_subscriber.rb +120 -0
- data/lib/dspy/subscribers/newrelic_subscriber.rb +686 -0
- data/lib/dspy/subscribers/otel_subscriber.rb +538 -0
- data/lib/dspy/teleprompt/data_handler.rb +107 -0
- data/lib/dspy/teleprompt/mipro_v2.rb +790 -0
- data/lib/dspy/teleprompt/simple_optimizer.rb +497 -0
- data/lib/dspy/teleprompt/teleprompter.rb +336 -0
- data/lib/dspy/teleprompt/utils.rb +380 -0
- data/lib/dspy/version.rb +5 -0
- data/lib/dspy.rb +16 -0
- metadata +29 -12
- data/lib/dspy/lm/adapters/ruby_llm_adapter.rb +0 -81
@@ -0,0 +1,560 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
require_relative '../instrumentation'
|
5
|
+
require_relative '../signature'
|
6
|
+
require_relative '../predict'
|
7
|
+
|
8
|
+
module DSPy
|
9
|
+
module Propose
|
10
|
+
# Grounded Proposer for generating better instructions based on training data
|
11
|
+
# Analyzes task patterns and creates contextually appropriate instructions
|
12
|
+
class GroundedProposer
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
# Configuration for instruction proposal
|
16
|
+
class Config
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
sig { returns(Integer) }
|
20
|
+
attr_accessor :num_instruction_candidates
|
21
|
+
|
22
|
+
sig { returns(Integer) }
|
23
|
+
attr_accessor :max_examples_for_analysis
|
24
|
+
|
25
|
+
sig { returns(Integer) }
|
26
|
+
attr_accessor :max_instruction_length
|
27
|
+
|
28
|
+
sig { returns(T::Boolean) }
|
29
|
+
attr_accessor :use_task_description
|
30
|
+
|
31
|
+
sig { returns(T::Boolean) }
|
32
|
+
attr_accessor :use_input_output_analysis
|
33
|
+
|
34
|
+
sig { returns(T::Boolean) }
|
35
|
+
attr_accessor :use_few_shot_examples
|
36
|
+
|
37
|
+
sig { returns(String) }
|
38
|
+
attr_accessor :proposal_model
|
39
|
+
|
40
|
+
sig { void }
|
41
|
+
def initialize
|
42
|
+
@num_instruction_candidates = 5
|
43
|
+
@max_examples_for_analysis = 10
|
44
|
+
@max_instruction_length = 200
|
45
|
+
@use_task_description = true
|
46
|
+
@use_input_output_analysis = true
|
47
|
+
@use_few_shot_examples = true
|
48
|
+
@proposal_model = "gpt-4o-mini"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Result of instruction proposal
|
53
|
+
class ProposalResult
|
54
|
+
extend T::Sig
|
55
|
+
|
56
|
+
sig { returns(T::Array[String]) }
|
57
|
+
attr_reader :candidate_instructions
|
58
|
+
|
59
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
60
|
+
attr_reader :analysis
|
61
|
+
|
62
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
63
|
+
attr_reader :metadata
|
64
|
+
|
65
|
+
sig do
|
66
|
+
params(
|
67
|
+
candidate_instructions: T::Array[String],
|
68
|
+
analysis: T::Hash[Symbol, T.untyped],
|
69
|
+
metadata: T::Hash[Symbol, T.untyped]
|
70
|
+
).void
|
71
|
+
end
|
72
|
+
def initialize(candidate_instructions:, analysis:, metadata:)
|
73
|
+
@candidate_instructions = candidate_instructions.freeze
|
74
|
+
@analysis = analysis.freeze
|
75
|
+
@metadata = metadata.freeze
|
76
|
+
end
|
77
|
+
|
78
|
+
sig { returns(String) }
|
79
|
+
def best_instruction
|
80
|
+
@candidate_instructions.first || ""
|
81
|
+
end
|
82
|
+
|
83
|
+
sig { returns(Integer) }
|
84
|
+
def num_candidates
|
85
|
+
@candidate_instructions.size
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
sig { returns(Config) }
|
90
|
+
attr_reader :config
|
91
|
+
|
92
|
+
sig { params(config: T.nilable(Config)).void }
|
93
|
+
def initialize(config: nil)
|
94
|
+
@config = config || Config.new
|
95
|
+
end
|
96
|
+
|
97
|
+
# Generate instruction candidates for a signature and training examples
|
98
|
+
sig do
|
99
|
+
params(
|
100
|
+
signature_class: T.class_of(DSPy::Signature),
|
101
|
+
examples: T::Array[T.untyped],
|
102
|
+
few_shot_examples: T.nilable(T::Array[T.untyped]),
|
103
|
+
current_instruction: T.nilable(String)
|
104
|
+
).returns(ProposalResult)
|
105
|
+
end
|
106
|
+
def propose_instructions(signature_class, examples, few_shot_examples: nil, current_instruction: nil)
|
107
|
+
Instrumentation.instrument('dspy.optimization.instruction_proposal_start', {
|
108
|
+
signature_class: signature_class.name,
|
109
|
+
num_examples: examples.size,
|
110
|
+
has_few_shot: !few_shot_examples.nil?,
|
111
|
+
has_current_instruction: !current_instruction.nil?
|
112
|
+
}) do
|
113
|
+
# Analyze the task and training data
|
114
|
+
analysis = analyze_task(signature_class, examples, few_shot_examples)
|
115
|
+
|
116
|
+
# Generate instruction candidates
|
117
|
+
candidates = generate_instruction_candidates(
|
118
|
+
signature_class,
|
119
|
+
analysis,
|
120
|
+
current_instruction
|
121
|
+
)
|
122
|
+
|
123
|
+
# Filter and rank candidates
|
124
|
+
filtered_candidates = filter_and_rank_candidates(candidates, analysis)
|
125
|
+
|
126
|
+
metadata = {
|
127
|
+
generation_timestamp: Time.now.iso8601,
|
128
|
+
model_used: @config.proposal_model,
|
129
|
+
num_examples_analyzed: [examples.size, @config.max_examples_for_analysis].min,
|
130
|
+
original_instruction: current_instruction
|
131
|
+
}
|
132
|
+
|
133
|
+
result = ProposalResult.new(
|
134
|
+
candidate_instructions: filtered_candidates,
|
135
|
+
analysis: analysis,
|
136
|
+
metadata: metadata
|
137
|
+
)
|
138
|
+
|
139
|
+
emit_proposal_complete_event(result)
|
140
|
+
result
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# Analyze the task based on signature and training examples
|
147
|
+
sig do
|
148
|
+
params(
|
149
|
+
signature_class: T.class_of(DSPy::Signature),
|
150
|
+
examples: T::Array[T.untyped],
|
151
|
+
few_shot_examples: T.nilable(T::Array[T.untyped])
|
152
|
+
).returns(T::Hash[Symbol, T.untyped])
|
153
|
+
end
|
154
|
+
def analyze_task(signature_class, examples, few_shot_examples)
|
155
|
+
analysis = {
|
156
|
+
task_description: signature_class.description,
|
157
|
+
input_fields: extract_field_info(signature_class.input_struct_class),
|
158
|
+
output_fields: extract_field_info(signature_class.output_struct_class),
|
159
|
+
example_patterns: analyze_example_patterns(examples),
|
160
|
+
complexity_indicators: assess_task_complexity(signature_class, examples)
|
161
|
+
}
|
162
|
+
|
163
|
+
if few_shot_examples && few_shot_examples.any?
|
164
|
+
analysis[:few_shot_patterns] = analyze_few_shot_patterns(few_shot_examples)
|
165
|
+
end
|
166
|
+
|
167
|
+
analysis
|
168
|
+
end
|
169
|
+
|
170
|
+
# Extract field information from struct classes
|
171
|
+
sig { params(struct_class: T.class_of(T::Struct)).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
172
|
+
def extract_field_info(struct_class)
|
173
|
+
struct_class.props.map do |name, prop_info|
|
174
|
+
{
|
175
|
+
name: name,
|
176
|
+
type: prop_info[:type].to_s,
|
177
|
+
description: prop_info[:description] || "",
|
178
|
+
required: !prop_info[:rules]&.any? { |rule| rule.is_a?(T::Props::NilableRules) }
|
179
|
+
}
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Analyze patterns in training examples
|
184
|
+
sig { params(examples: T::Array[T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
185
|
+
def analyze_example_patterns(examples)
|
186
|
+
analysis_examples = examples.take(@config.max_examples_for_analysis)
|
187
|
+
|
188
|
+
{
|
189
|
+
total_examples: examples.size,
|
190
|
+
analyzed_examples: analysis_examples.size,
|
191
|
+
input_patterns: analyze_input_patterns(analysis_examples),
|
192
|
+
output_patterns: analyze_output_patterns(analysis_examples),
|
193
|
+
common_themes: extract_common_themes(analysis_examples)
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
# Analyze input patterns in examples
|
198
|
+
sig { params(examples: T::Array[T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
199
|
+
def analyze_input_patterns(examples)
|
200
|
+
input_lengths = []
|
201
|
+
input_types = []
|
202
|
+
common_keywords = Hash.new(0)
|
203
|
+
|
204
|
+
examples.each do |example|
|
205
|
+
input_values = extract_input_values(example)
|
206
|
+
|
207
|
+
input_values.each do |key, value|
|
208
|
+
if value.is_a?(String)
|
209
|
+
input_lengths << value.length
|
210
|
+
# Extract potential keywords
|
211
|
+
value.downcase.split(/\W+/).each { |word| common_keywords[word] += 1 if word.length > 3 }
|
212
|
+
end
|
213
|
+
input_types << value.class.name
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
{
|
218
|
+
avg_input_length: input_lengths.empty? ? 0 : input_lengths.sum.to_f / input_lengths.size,
|
219
|
+
common_input_types: input_types.tally,
|
220
|
+
frequent_keywords: common_keywords.sort_by { |_, count| -count }.take(10).to_h
|
221
|
+
}
|
222
|
+
end
|
223
|
+
|
224
|
+
# Analyze output patterns in examples
|
225
|
+
sig { params(examples: T::Array[T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
226
|
+
def analyze_output_patterns(examples)
|
227
|
+
output_lengths = []
|
228
|
+
output_types = []
|
229
|
+
|
230
|
+
examples.each do |example|
|
231
|
+
expected_values = extract_expected_values(example)
|
232
|
+
|
233
|
+
expected_values.each do |key, value|
|
234
|
+
if value.is_a?(String)
|
235
|
+
output_lengths << value.length
|
236
|
+
end
|
237
|
+
output_types << value.class.name
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
{
|
242
|
+
avg_output_length: output_lengths.empty? ? 0 : output_lengths.sum.to_f / output_lengths.size,
|
243
|
+
common_output_types: output_types.tally
|
244
|
+
}
|
245
|
+
end
|
246
|
+
|
247
|
+
# Extract common themes from examples
|
248
|
+
sig { params(examples: T::Array[T.untyped]).returns(T::Array[String]) }
|
249
|
+
def extract_common_themes(examples)
|
250
|
+
themes = []
|
251
|
+
|
252
|
+
# Simple heuristics for theme detection
|
253
|
+
input_texts = examples.map { |ex| extract_input_values(ex).values.select { |v| v.is_a?(String) } }.flatten
|
254
|
+
|
255
|
+
if input_texts.any? { |text| text.downcase.include?("question") || text.include?("?") }
|
256
|
+
themes << "question_answering"
|
257
|
+
end
|
258
|
+
|
259
|
+
if input_texts.any? { |text| text.downcase.match?(/\b(classify|category|type)\b/) }
|
260
|
+
themes << "classification"
|
261
|
+
end
|
262
|
+
|
263
|
+
if input_texts.any? { |text| text.match?(/\d+.*[+\-*\/].*\d+/) }
|
264
|
+
themes << "mathematical_reasoning"
|
265
|
+
end
|
266
|
+
|
267
|
+
if input_texts.any? { |text| text.downcase.match?(/\b(analyze|explain|reason)\b/) }
|
268
|
+
themes << "analytical_reasoning"
|
269
|
+
end
|
270
|
+
|
271
|
+
themes
|
272
|
+
end
|
273
|
+
|
274
|
+
# Analyze few-shot example patterns
|
275
|
+
sig { params(few_shot_examples: T::Array[T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
276
|
+
def analyze_few_shot_patterns(few_shot_examples)
|
277
|
+
{
|
278
|
+
num_examples: few_shot_examples.size,
|
279
|
+
demonstrates_reasoning: few_shot_examples.any? { |ex| has_reasoning_field?(ex) },
|
280
|
+
example_variety: assess_example_variety(few_shot_examples)
|
281
|
+
}
|
282
|
+
end
|
283
|
+
|
284
|
+
# Assess task complexity indicators
|
285
|
+
sig do
|
286
|
+
params(
|
287
|
+
signature_class: T.class_of(DSPy::Signature),
|
288
|
+
examples: T::Array[T.untyped]
|
289
|
+
).returns(T::Hash[Symbol, T.untyped])
|
290
|
+
end
|
291
|
+
def assess_task_complexity(signature_class, examples)
|
292
|
+
{
|
293
|
+
num_input_fields: signature_class.input_struct_class.props.size,
|
294
|
+
num_output_fields: signature_class.output_struct_class.props.size,
|
295
|
+
has_complex_outputs: has_complex_output_types?(signature_class),
|
296
|
+
requires_reasoning: task_requires_reasoning?(signature_class, examples)
|
297
|
+
}
|
298
|
+
end
|
299
|
+
|
300
|
+
# Generate instruction candidates using LLM
|
301
|
+
sig do
|
302
|
+
params(
|
303
|
+
signature_class: T.class_of(DSPy::Signature),
|
304
|
+
analysis: T::Hash[Symbol, T.untyped],
|
305
|
+
current_instruction: T.nilable(String)
|
306
|
+
).returns(T::Array[String])
|
307
|
+
end
|
308
|
+
def generate_instruction_candidates(signature_class, analysis, current_instruction)
|
309
|
+
# Build context for instruction generation
|
310
|
+
context = build_generation_context(signature_class, analysis, current_instruction)
|
311
|
+
|
312
|
+
# Create instruction generation signature
|
313
|
+
instruction_signature = create_instruction_generation_signature
|
314
|
+
|
315
|
+
# Generate candidates using LLM
|
316
|
+
generator = DSPy::Predict.new(instruction_signature)
|
317
|
+
|
318
|
+
candidates = []
|
319
|
+
@config.num_instruction_candidates.times do |i|
|
320
|
+
begin
|
321
|
+
result = generator.call(
|
322
|
+
task_context: context,
|
323
|
+
requirements: build_requirements_text(analysis),
|
324
|
+
candidate_number: i + 1
|
325
|
+
)
|
326
|
+
|
327
|
+
instruction = result.instruction.strip
|
328
|
+
|
329
|
+
# Truncate if too long
|
330
|
+
if instruction.length > @config.max_instruction_length
|
331
|
+
instruction = instruction[0, @config.max_instruction_length].strip
|
332
|
+
# Try to end at a word boundary
|
333
|
+
if instruction.include?(' ')
|
334
|
+
instruction = instruction.rpartition(' ').first + '.'
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
candidates << instruction if instruction.length > 0
|
339
|
+
rescue => error
|
340
|
+
DSPy.logger.warn("Failed to generate instruction candidate #{i + 1}: #{error.message}")
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Ensure we have at least one candidate
|
345
|
+
if candidates.empty?
|
346
|
+
candidates << generate_fallback_instruction(signature_class, analysis)
|
347
|
+
end
|
348
|
+
|
349
|
+
candidates.uniq
|
350
|
+
end
|
351
|
+
|
352
|
+
# Build context for instruction generation
|
353
|
+
sig do
|
354
|
+
params(
|
355
|
+
signature_class: T.class_of(DSPy::Signature),
|
356
|
+
analysis: T::Hash[Symbol, T.untyped],
|
357
|
+
current_instruction: T.nilable(String)
|
358
|
+
).returns(String)
|
359
|
+
end
|
360
|
+
def build_generation_context(signature_class, analysis, current_instruction)
|
361
|
+
context_parts = []
|
362
|
+
|
363
|
+
context_parts << "Task: #{signature_class.description}" if @config.use_task_description
|
364
|
+
|
365
|
+
if @config.use_input_output_analysis
|
366
|
+
context_parts << "Input fields: #{analysis[:input_fields].map { |f| "#{f[:name]} (#{f[:type]})" }.join(', ')}"
|
367
|
+
context_parts << "Output fields: #{analysis[:output_fields].map { |f| "#{f[:name]} (#{f[:type]})" }.join(', ')}"
|
368
|
+
end
|
369
|
+
|
370
|
+
if analysis[:common_themes] && analysis[:common_themes].any?
|
371
|
+
context_parts << "Task themes: #{analysis[:common_themes].join(', ')}"
|
372
|
+
end
|
373
|
+
|
374
|
+
if current_instruction
|
375
|
+
context_parts << "Current instruction: \"#{current_instruction}\""
|
376
|
+
end
|
377
|
+
|
378
|
+
context_parts.join("\n")
|
379
|
+
end
|
380
|
+
|
381
|
+
# Build requirements text for instruction generation
|
382
|
+
sig { params(analysis: T::Hash[Symbol, T.untyped]).returns(String) }
|
383
|
+
def build_requirements_text(analysis)
|
384
|
+
requirements = []
|
385
|
+
|
386
|
+
requirements << "Be specific and actionable"
|
387
|
+
requirements << "Guide the model's reasoning process"
|
388
|
+
|
389
|
+
if analysis[:complexity_indicators][:requires_reasoning]
|
390
|
+
requirements << "Encourage step-by-step thinking"
|
391
|
+
end
|
392
|
+
|
393
|
+
if analysis[:common_themes]&.include?("mathematical_reasoning")
|
394
|
+
requirements << "Emphasize mathematical accuracy"
|
395
|
+
end
|
396
|
+
|
397
|
+
if analysis[:common_themes]&.include?("classification")
|
398
|
+
requirements << "Encourage careful categorization"
|
399
|
+
end
|
400
|
+
|
401
|
+
requirements.join(". ") + "."
|
402
|
+
end
|
403
|
+
|
404
|
+
# Create signature for instruction generation
|
405
|
+
sig { returns(T.class_of(DSPy::Signature)) }
|
406
|
+
def create_instruction_generation_signature
|
407
|
+
Class.new(DSPy::Signature) do
|
408
|
+
description "Generate an improved instruction for a language model task"
|
409
|
+
|
410
|
+
input do
|
411
|
+
const :task_context, String, description: "Context about the task and current setup"
|
412
|
+
const :requirements, String, description: "Requirements for the instruction"
|
413
|
+
const :candidate_number, Integer, description: "Which candidate this is (for variety)"
|
414
|
+
end
|
415
|
+
|
416
|
+
output do
|
417
|
+
const :instruction, String, description: "A clear, specific instruction for the task"
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# Generate a fallback instruction when LLM generation fails
|
423
|
+
sig do
|
424
|
+
params(
|
425
|
+
signature_class: T.class_of(DSPy::Signature),
|
426
|
+
analysis: T::Hash[Symbol, T.untyped]
|
427
|
+
).returns(String)
|
428
|
+
end
|
429
|
+
def generate_fallback_instruction(signature_class, analysis)
|
430
|
+
base = signature_class.description || "Complete the given task"
|
431
|
+
|
432
|
+
if analysis[:complexity_indicators][:requires_reasoning]
|
433
|
+
"#{base} Think step by step and provide a clear explanation."
|
434
|
+
else
|
435
|
+
"#{base} Be accurate and specific in your response."
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
# Filter and rank instruction candidates
|
440
|
+
sig { params(candidates: T::Array[String], analysis: T::Hash[Symbol, T.untyped]).returns(T::Array[String]) }
|
441
|
+
def filter_and_rank_candidates(candidates, analysis)
|
442
|
+
# Filter out duplicates and empty candidates
|
443
|
+
filtered = candidates.uniq.reject(&:empty?)
|
444
|
+
|
445
|
+
# Simple ranking based on length and content quality
|
446
|
+
filtered.sort_by do |instruction|
|
447
|
+
score = 0
|
448
|
+
|
449
|
+
# Prefer moderate length instructions
|
450
|
+
length_score = [instruction.length, @config.max_instruction_length].min / @config.max_instruction_length.to_f
|
451
|
+
score += length_score * 0.3
|
452
|
+
|
453
|
+
# Prefer instructions with action words
|
454
|
+
action_words = %w[analyze classify generate explain solve determine identify]
|
455
|
+
action_score = action_words.count { |word| instruction.downcase.include?(word) }
|
456
|
+
score += action_score * 0.4
|
457
|
+
|
458
|
+
# Prefer instructions that mention reasoning for complex tasks
|
459
|
+
if analysis[:complexity_indicators][:requires_reasoning]
|
460
|
+
reasoning_score = instruction.downcase.match?(/\b(step|think|reason|explain)\b/) ? 1 : 0
|
461
|
+
score += reasoning_score * 0.3
|
462
|
+
end
|
463
|
+
|
464
|
+
-score # Negative for descending sort
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
# Helper methods for extracting values from examples
|
469
|
+
sig { params(example: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
470
|
+
def extract_input_values(example)
|
471
|
+
case example
|
472
|
+
when DSPy::Example
|
473
|
+
example.input_values
|
474
|
+
when Hash
|
475
|
+
example[:input] || example.select { |k, _| k != :expected && k != :output }
|
476
|
+
else
|
477
|
+
example.respond_to?(:input) ? example.input : {}
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
sig { params(example: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
482
|
+
def extract_expected_values(example)
|
483
|
+
case example
|
484
|
+
when DSPy::Example
|
485
|
+
example.expected_values
|
486
|
+
when Hash
|
487
|
+
example[:expected] || example[:output] || {}
|
488
|
+
else
|
489
|
+
example.respond_to?(:expected) ? example.expected : {}
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
# Check if example has reasoning field
|
494
|
+
sig { params(example: T.untyped).returns(T::Boolean) }
|
495
|
+
def has_reasoning_field?(example)
|
496
|
+
values = extract_expected_values(example)
|
497
|
+
values.key?(:reasoning) || values.key?(:explanation) || values.key?(:rationale)
|
498
|
+
end
|
499
|
+
|
500
|
+
# Assess variety in examples
|
501
|
+
sig { params(examples: T::Array[T.untyped]).returns(String) }
|
502
|
+
def assess_example_variety(examples)
|
503
|
+
return "low" if examples.size < 3
|
504
|
+
|
505
|
+
# Simple heuristic based on input diversity
|
506
|
+
input_patterns = examples.map { |ex| extract_input_values(ex).values.map(&:to_s).join(" ") }
|
507
|
+
unique_patterns = input_patterns.uniq.size
|
508
|
+
|
509
|
+
variety_ratio = unique_patterns.to_f / examples.size
|
510
|
+
|
511
|
+
case variety_ratio
|
512
|
+
when 0.8..1.0 then "high"
|
513
|
+
when 0.5..0.8 then "medium"
|
514
|
+
else "low"
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
# Check if signature has complex output types
|
519
|
+
sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T::Boolean) }
|
520
|
+
def has_complex_output_types?(signature_class)
|
521
|
+
signature_class.output_struct_class.props.any? do |_, prop_info|
|
522
|
+
type_str = prop_info[:type].to_s
|
523
|
+
type_str.include?("Array") || type_str.include?("Hash") || type_str.include?("T::Enum")
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
# Check if task requires reasoning
|
528
|
+
sig { params(signature_class: T.class_of(DSPy::Signature), examples: T::Array[T.untyped]).returns(T::Boolean) }
|
529
|
+
def task_requires_reasoning?(signature_class, examples)
|
530
|
+
# Check if output has reasoning fields
|
531
|
+
has_reasoning_outputs = signature_class.output_struct_class.props.any? do |name, _|
|
532
|
+
name.to_s.match?(/reason|explain|rational|justif/i)
|
533
|
+
end
|
534
|
+
|
535
|
+
return true if has_reasoning_outputs
|
536
|
+
|
537
|
+
# Check if examples suggest reasoning is needed
|
538
|
+
sample_examples = examples.take(5)
|
539
|
+
requires_reasoning = sample_examples.any? do |example|
|
540
|
+
input_values = extract_input_values(example)
|
541
|
+
input_text = input_values.values.select { |v| v.is_a?(String) }.join(" ")
|
542
|
+
input_text.downcase.match?(/\b(why|how|explain|analyze|reason)\b/)
|
543
|
+
end
|
544
|
+
|
545
|
+
requires_reasoning
|
546
|
+
end
|
547
|
+
|
548
|
+
# Emit instruction proposal completion event
|
549
|
+
sig { params(result: ProposalResult).void }
|
550
|
+
def emit_proposal_complete_event(result)
|
551
|
+
Instrumentation.emit('dspy.optimization.instruction_proposal_complete', {
|
552
|
+
num_candidates: result.num_candidates,
|
553
|
+
best_instruction_length: result.best_instruction.length,
|
554
|
+
analysis_themes: result.analysis[:common_themes] || [],
|
555
|
+
model_used: @config.proposal_model
|
556
|
+
})
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|