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
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
require 'json'
|
5
|
+
require_relative '../signature'
|
6
|
+
require_relative '../predict'
|
7
|
+
require_relative '../type_serializer'
|
8
|
+
require_relative '../few_shot_example'
|
9
|
+
|
10
|
+
module DSPy
|
11
|
+
module Propose
|
12
|
+
# Dataset Summary Generator for creating concise dataset descriptions
|
13
|
+
# Used by GroundedProposer for data-aware instruction generation
|
14
|
+
module DatasetSummaryGenerator
|
15
|
+
extend T::Sig
|
16
|
+
|
17
|
+
# Signature for summarizing observations into a brief summary
|
18
|
+
class ObservationSummarizer < DSPy::Signature
|
19
|
+
description "Given a series of observations I have made about my dataset, please summarize them into a brief 2-3 sentence summary which highlights only the most important details."
|
20
|
+
|
21
|
+
input do
|
22
|
+
const :observations, String, description: "Observations I have made about my dataset"
|
23
|
+
end
|
24
|
+
|
25
|
+
output do
|
26
|
+
const :summary, String, description: "Two to Three sentence summary of only the most significant highlights of my observations"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Signature for generating initial dataset observations
|
31
|
+
class DatasetDescriptor < DSPy::Signature
|
32
|
+
description "Given several examples from a dataset please write observations about trends that hold for most or all of the samples. " \
|
33
|
+
"Some areas you may consider in your observations: topics, content, syntax, conciceness, etc. " \
|
34
|
+
"It will be useful to make an educated guess as to the nature of the task this dataset will enable. Don't be afraid to be creative"
|
35
|
+
|
36
|
+
input do
|
37
|
+
const :examples, String, description: "Sample data points from the dataset"
|
38
|
+
end
|
39
|
+
|
40
|
+
output do
|
41
|
+
const :observations, String, description: "Somethings that holds true for most or all of the data you observed"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Signature for refining observations with prior context
|
46
|
+
class DatasetDescriptorWithPriorObservations < DSPy::Signature
|
47
|
+
description "Given several examples from a dataset please write observations about trends that hold for most or all of the samples. " \
|
48
|
+
"I will also provide you with a few observations I have already made. Please add your own observations or if you feel the observations are comprehensive say 'COMPLETE' " \
|
49
|
+
"Some areas you may consider in your observations: topics, content, syntax, conciceness, etc. " \
|
50
|
+
"It will be useful to make an educated guess as to the nature of the task this dataset will enable. Don't be afraid to be creative"
|
51
|
+
|
52
|
+
input do
|
53
|
+
const :examples, String, description: "Sample data points from the dataset"
|
54
|
+
const :prior_observations, String, description: "Some prior observations I made about the data"
|
55
|
+
end
|
56
|
+
|
57
|
+
output do
|
58
|
+
const :observations, String, description: "Somethings that holds true for most or all of the data you observed or COMPLETE if you have nothing to add"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Helper function to ensure consistent ordering of input keys in string representations
|
63
|
+
# This helps with caching and consistent LLM prompts
|
64
|
+
sig { params(unordered_repr: String).returns(String) }
|
65
|
+
def self.order_input_keys_in_string(unordered_repr)
|
66
|
+
# Regex pattern to match the input keys structure
|
67
|
+
pattern = /input_keys=\{([^}]+)\}/
|
68
|
+
|
69
|
+
# Function to reorder keys
|
70
|
+
unordered_repr.gsub(pattern) do |match|
|
71
|
+
keys_str = Regexp.last_match(1)
|
72
|
+
# Split the keys, strip extra spaces, and sort them
|
73
|
+
keys = keys_str.split(',').map(&:strip).sort
|
74
|
+
# Format the sorted keys back into the expected structure
|
75
|
+
"input_keys={#{keys.join(', ')}}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Strip common prefixes from LLM outputs (e.g., "Answer:", "Output:")
|
80
|
+
sig { params(text: String).returns(String) }
|
81
|
+
def self.strip_prefix(text)
|
82
|
+
# Pattern matches up to 4 words followed by a colon
|
83
|
+
pattern = /^[\*\s]*(([\w'\-]+\s+){0,4}[\w'\-]+):\s*/
|
84
|
+
modified_text = text.gsub(pattern, '')
|
85
|
+
modified_text.strip.gsub(/^["']|["']$/, '')
|
86
|
+
end
|
87
|
+
|
88
|
+
# Generate a concise 2-3 sentence summary of a training dataset
|
89
|
+
# Used for data-aware instruction proposal in MIPROv2
|
90
|
+
#
|
91
|
+
# @param trainset [Array<DSPy::Example>] Training examples to summarize
|
92
|
+
# @param view_data_batch_size [Integer] Number of examples to process per batch
|
93
|
+
# @param prompt_model [DSPy::LM, nil] Language model to use (defaults to DSPy.lm)
|
94
|
+
# @param verbose [Boolean] Whether to print progress information
|
95
|
+
# @return [String] 2-3 sentence summary of the dataset characteristics
|
96
|
+
#
|
97
|
+
# @example Basic usage
|
98
|
+
# summary = DatasetSummaryGenerator.create_dataset_summary(
|
99
|
+
# trainset,
|
100
|
+
# view_data_batch_size: 10,
|
101
|
+
# prompt_model: DSPy::LM.new('gpt-4o-mini')
|
102
|
+
# )
|
103
|
+
#
|
104
|
+
sig do
|
105
|
+
params(
|
106
|
+
trainset: T::Array[DSPy::Example],
|
107
|
+
view_data_batch_size: Integer,
|
108
|
+
prompt_model: T.nilable(DSPy::LM),
|
109
|
+
verbose: T::Boolean
|
110
|
+
).returns(String)
|
111
|
+
end
|
112
|
+
def self.create_dataset_summary(trainset, view_data_batch_size, prompt_model, verbose: false)
|
113
|
+
if verbose
|
114
|
+
puts "\nBootstrapping dataset summary (this will be used to generate instructions)..."
|
115
|
+
end
|
116
|
+
|
117
|
+
# Use provided model or fall back to global LM
|
118
|
+
lm = prompt_model || DSPy.lm
|
119
|
+
raise ArgumentError, "No language model configured. Set prompt_model or DSPy.lm" unless lm
|
120
|
+
|
121
|
+
# Use provided LM in a block context
|
122
|
+
DSPy.with_lm(lm) do
|
123
|
+
# Initial observation from first batch
|
124
|
+
upper_lim = [trainset.length, view_data_batch_size].min
|
125
|
+
batch_examples = trainset[0...upper_lim]
|
126
|
+
predictor = DSPy::Predict.new(DatasetDescriptor)
|
127
|
+
examples_repr = format_examples_for_prompt(batch_examples)
|
128
|
+
|
129
|
+
observation = predictor.call(examples: examples_repr)
|
130
|
+
observations = observation.observations
|
131
|
+
|
132
|
+
# Iteratively refine observations with additional batches
|
133
|
+
skips = 0
|
134
|
+
max_calls = 10
|
135
|
+
calls = 0
|
136
|
+
|
137
|
+
begin
|
138
|
+
(view_data_batch_size...trainset.length).step(view_data_batch_size) do |b|
|
139
|
+
calls += 1
|
140
|
+
break if calls >= max_calls
|
141
|
+
|
142
|
+
puts "Processing batch starting at index #{b}" if verbose
|
143
|
+
|
144
|
+
upper_lim = [trainset.length, b + view_data_batch_size].min
|
145
|
+
|
146
|
+
predictor = DSPy::Predict.new(DatasetDescriptorWithPriorObservations)
|
147
|
+
batch_examples = trainset[b...upper_lim]
|
148
|
+
examples_repr = format_examples_for_prompt(batch_examples)
|
149
|
+
|
150
|
+
output = predictor.call(
|
151
|
+
prior_observations: observations,
|
152
|
+
examples: examples_repr
|
153
|
+
)
|
154
|
+
|
155
|
+
# Check if LLM indicates observations are complete
|
156
|
+
if output.observations.length >= 8 && output.observations[0...8].upcase == "COMPLETE"
|
157
|
+
skips += 1
|
158
|
+
break if skips >= 5
|
159
|
+
next
|
160
|
+
end
|
161
|
+
|
162
|
+
observations += output.observations
|
163
|
+
end
|
164
|
+
rescue => e
|
165
|
+
if verbose
|
166
|
+
puts "Error during observation refinement: #{e.message}. Using observations from past round for summary."
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Generate final summary from accumulated observations
|
171
|
+
predictor = DSPy::Predict.new(ObservationSummarizer)
|
172
|
+
summary = predictor.call(observations: observations)
|
173
|
+
|
174
|
+
if verbose
|
175
|
+
puts "\nGenerated summary: #{strip_prefix(summary.summary)}\n"
|
176
|
+
end
|
177
|
+
|
178
|
+
strip_prefix(summary.summary)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
sig { params(examples: T::Array[T.untyped]).returns(String) }
|
183
|
+
def self.format_examples_for_prompt(examples)
|
184
|
+
serialized_examples = examples.map do |example|
|
185
|
+
case example
|
186
|
+
when DSPy::Example
|
187
|
+
{
|
188
|
+
signature: example.signature_class.name,
|
189
|
+
input: DSPy::TypeSerializer.serialize(example.input),
|
190
|
+
expected: DSPy::TypeSerializer.serialize(example.expected)
|
191
|
+
}
|
192
|
+
when DSPy::FewShotExample
|
193
|
+
base = {
|
194
|
+
input: example.input,
|
195
|
+
output: example.output
|
196
|
+
}
|
197
|
+
base[:reasoning] = example.reasoning if example.reasoning
|
198
|
+
base
|
199
|
+
when Hash
|
200
|
+
example
|
201
|
+
else
|
202
|
+
example.respond_to?(:to_h) ? example.to_h : { value: example }
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
JSON.pretty_generate(serialized_examples)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|