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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -3
  3. data/lib/dspy/callbacks.rb +222 -0
  4. data/lib/dspy/chain_of_thought.rb +2 -1
  5. data/lib/dspy/code_act.rb +14 -1
  6. data/lib/dspy/datasets/ade.rb +90 -0
  7. data/lib/dspy/datasets.rb +8 -0
  8. data/lib/dspy/lm.rb +9 -12
  9. data/lib/dspy/mixins/struct_builder.rb +17 -25
  10. data/lib/dspy/module.rb +45 -1
  11. data/lib/dspy/observability/async_span_processor.rb +67 -93
  12. data/lib/dspy/observability.rb +43 -1
  13. data/lib/dspy/predict.rb +17 -0
  14. data/lib/dspy/prompt.rb +90 -20
  15. data/lib/dspy/propose/dataset_summary_generator.rb +210 -0
  16. data/lib/dspy/propose/grounded_proposer.rb +320 -66
  17. data/lib/dspy/re_act.rb +13 -0
  18. data/lib/dspy/reflection_lm.rb +36 -0
  19. data/lib/dspy/teleprompt/bootstrap_strategy.rb +26 -0
  20. data/lib/dspy/teleprompt/gepa.rb +448 -2803
  21. data/lib/dspy/teleprompt/mipro_v2.rb +624 -100
  22. data/lib/dspy/teleprompt/utils.rb +349 -42
  23. data/lib/dspy/version.rb +2 -2
  24. data/lib/dspy.rb +4 -2
  25. data/lib/gepa/api.rb +61 -0
  26. data/lib/gepa/core/engine.rb +226 -0
  27. data/lib/gepa/core/evaluation_batch.rb +26 -0
  28. data/lib/gepa/core/result.rb +92 -0
  29. data/lib/gepa/core/state.rb +231 -0
  30. data/lib/gepa/logging/experiment_tracker.rb +54 -0
  31. data/lib/gepa/logging/logger.rb +57 -0
  32. data/lib/gepa/logging.rb +9 -0
  33. data/lib/gepa/proposer/base.rb +27 -0
  34. data/lib/gepa/proposer/merge_proposer.rb +424 -0
  35. data/lib/gepa/proposer/reflective_mutation/base.rb +48 -0
  36. data/lib/gepa/proposer/reflective_mutation/reflective_mutation.rb +188 -0
  37. data/lib/gepa/strategies/batch_sampler.rb +91 -0
  38. data/lib/gepa/strategies/candidate_selector.rb +97 -0
  39. data/lib/gepa/strategies/component_selector.rb +57 -0
  40. data/lib/gepa/strategies/instruction_proposal.rb +120 -0
  41. data/lib/gepa/telemetry.rb +122 -0
  42. data/lib/gepa/utils/pareto.rb +119 -0
  43. data/lib/gepa.rb +21 -0
  44. metadata +59 -4
  45. 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