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.
@@ -0,0 +1,380 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+ require_relative '../instrumentation'
5
+ require_relative '../evaluate'
6
+ require_relative '../example'
7
+ require_relative 'data_handler'
8
+
9
+ module DSPy
10
+ module Teleprompt
11
+ # Bootstrap utilities for MIPROv2 optimization
12
+ # Handles few-shot example generation and candidate program evaluation
13
+ module Utils
14
+ extend T::Sig
15
+
16
+ # Configuration for bootstrap operations
17
+ class BootstrapConfig
18
+ extend T::Sig
19
+
20
+ sig { returns(Integer) }
21
+ attr_accessor :max_bootstrapped_examples
22
+
23
+ sig { returns(Integer) }
24
+ attr_accessor :max_labeled_examples
25
+
26
+ sig { returns(Integer) }
27
+ attr_accessor :num_candidate_sets
28
+
29
+ sig { returns(Integer) }
30
+ attr_accessor :max_errors
31
+
32
+ sig { returns(Integer) }
33
+ attr_accessor :num_threads
34
+
35
+ sig { returns(Float) }
36
+ attr_accessor :success_threshold
37
+
38
+ sig { returns(Integer) }
39
+ attr_accessor :minibatch_size
40
+
41
+ sig { void }
42
+ def initialize
43
+ @max_bootstrapped_examples = 4
44
+ @max_labeled_examples = 16
45
+ @num_candidate_sets = 10
46
+ @max_errors = 5
47
+ @num_threads = 1
48
+ @success_threshold = 0.8
49
+ @minibatch_size = 50
50
+ end
51
+ end
52
+
53
+ # Result of bootstrap operation
54
+ class BootstrapResult
55
+ extend T::Sig
56
+
57
+ sig { returns(T::Array[T::Array[DSPy::Example]]) }
58
+ attr_reader :candidate_sets
59
+
60
+ sig { returns(T::Array[DSPy::Example]) }
61
+ attr_reader :successful_examples
62
+
63
+ sig { returns(T::Array[DSPy::Example]) }
64
+ attr_reader :failed_examples
65
+
66
+ sig { returns(T::Hash[Symbol, T.untyped]) }
67
+ attr_reader :statistics
68
+
69
+ sig do
70
+ params(
71
+ candidate_sets: T::Array[T::Array[DSPy::Example]],
72
+ successful_examples: T::Array[DSPy::Example],
73
+ failed_examples: T::Array[DSPy::Example],
74
+ statistics: T::Hash[Symbol, T.untyped]
75
+ ).void
76
+ end
77
+ def initialize(candidate_sets:, successful_examples:, failed_examples:, statistics:)
78
+ @candidate_sets = candidate_sets.freeze
79
+ @successful_examples = successful_examples.freeze
80
+ @failed_examples = failed_examples.freeze
81
+ @statistics = statistics.freeze
82
+ end
83
+
84
+ sig { returns(Float) }
85
+ def success_rate
86
+ total = @successful_examples.size + @failed_examples.size
87
+ return 0.0 if total == 0
88
+ @successful_examples.size.to_f / total.to_f
89
+ end
90
+
91
+ sig { returns(Integer) }
92
+ def total_examples
93
+ @successful_examples.size + @failed_examples.size
94
+ end
95
+ end
96
+
97
+ # Create multiple candidate sets of few-shot examples through bootstrapping
98
+ sig do
99
+ params(
100
+ program: T.untyped,
101
+ trainset: T::Array[T.untyped],
102
+ config: BootstrapConfig,
103
+ metric: T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T::Boolean))
104
+ ).returns(BootstrapResult)
105
+ end
106
+ def self.create_n_fewshot_demo_sets(program, trainset, config: BootstrapConfig.new, metric: nil)
107
+ Instrumentation.instrument('dspy.optimization.bootstrap_start', {
108
+ trainset_size: trainset.size,
109
+ max_bootstrapped_examples: config.max_bootstrapped_examples,
110
+ num_candidate_sets: config.num_candidate_sets
111
+ }) do
112
+ # Convert to typed examples if needed
113
+ typed_examples = ensure_typed_examples(trainset)
114
+
115
+ # Generate successful examples through bootstrap
116
+ successful_examples, failed_examples = generate_successful_examples(
117
+ program,
118
+ typed_examples,
119
+ config,
120
+ metric
121
+ )
122
+
123
+ # Create candidate sets from successful examples
124
+ candidate_sets = create_candidate_sets(successful_examples, config)
125
+
126
+ # Gather statistics
127
+ statistics = {
128
+ total_trainset: trainset.size,
129
+ successful_count: successful_examples.size,
130
+ failed_count: failed_examples.size,
131
+ success_rate: successful_examples.size.to_f / (successful_examples.size + failed_examples.size),
132
+ candidate_sets_created: candidate_sets.size,
133
+ average_set_size: candidate_sets.empty? ? 0 : candidate_sets.map(&:size).sum.to_f / candidate_sets.size
134
+ }
135
+
136
+ emit_bootstrap_complete_event(statistics)
137
+
138
+ BootstrapResult.new(
139
+ candidate_sets: candidate_sets,
140
+ successful_examples: successful_examples,
141
+ failed_examples: failed_examples,
142
+ statistics: statistics
143
+ )
144
+ end
145
+ end
146
+
147
+ # Evaluate a candidate program on examples with proper error handling
148
+ sig do
149
+ params(
150
+ program: T.untyped,
151
+ examples: T::Array[T.untyped],
152
+ config: BootstrapConfig,
153
+ metric: T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T::Boolean))
154
+ ).returns(DSPy::Evaluate::BatchEvaluationResult)
155
+ end
156
+ def self.eval_candidate_program(program, examples, config: BootstrapConfig.new, metric: nil)
157
+ # Use minibatch evaluation for large datasets
158
+ if examples.size > config.minibatch_size
159
+ eval_candidate_program_minibatch(program, examples, config, metric)
160
+ else
161
+ eval_candidate_program_full(program, examples, config, metric)
162
+ end
163
+ end
164
+
165
+ # Minibatch evaluation for large datasets
166
+ sig do
167
+ params(
168
+ program: T.untyped,
169
+ examples: T::Array[T.untyped],
170
+ config: BootstrapConfig,
171
+ metric: T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T::Boolean))
172
+ ).returns(DSPy::Evaluate::BatchEvaluationResult)
173
+ end
174
+ def self.eval_candidate_program_minibatch(program, examples, config, metric)
175
+ Instrumentation.instrument('dspy.optimization.minibatch_evaluation', {
176
+ total_examples: examples.size,
177
+ minibatch_size: config.minibatch_size,
178
+ num_batches: (examples.size.to_f / config.minibatch_size).ceil
179
+ }) do
180
+ # Randomly sample a minibatch for evaluation
181
+ sample_size = [config.minibatch_size, examples.size].min
182
+ sampled_examples = examples.sample(sample_size)
183
+
184
+ eval_candidate_program_full(program, sampled_examples, config, metric)
185
+ end
186
+ end
187
+
188
+ # Full evaluation on all examples
189
+ sig do
190
+ params(
191
+ program: T.untyped,
192
+ examples: T::Array[T.untyped],
193
+ config: BootstrapConfig,
194
+ metric: T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T::Boolean))
195
+ ).returns(DSPy::Evaluate::BatchEvaluationResult)
196
+ end
197
+ def self.eval_candidate_program_full(program, examples, config, metric)
198
+ # Create evaluator with proper configuration
199
+ evaluator = DSPy::Evaluate.new(
200
+ program,
201
+ metric: metric || default_metric_for_examples(examples),
202
+ num_threads: config.num_threads,
203
+ max_errors: config.max_errors
204
+ )
205
+
206
+ # Run evaluation
207
+ evaluator.evaluate(examples, display_progress: false)
208
+ end
209
+
210
+ private
211
+
212
+ # Convert various example formats to typed examples
213
+ sig { params(examples: T::Array[T.untyped]).returns(T::Array[DSPy::Example]) }
214
+ def self.ensure_typed_examples(examples)
215
+ return examples if examples.all? { |ex| ex.is_a?(DSPy::Example) }
216
+
217
+ raise ArgumentError, "All examples must be DSPy::Example instances. Legacy format support has been removed. Please convert your examples to use the structured format with :input and :expected keys."
218
+ end
219
+
220
+ # Generate successful examples through program execution
221
+ sig do
222
+ params(
223
+ program: T.untyped,
224
+ examples: T::Array[DSPy::Example],
225
+ config: BootstrapConfig,
226
+ metric: T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T::Boolean))
227
+ ).returns([T::Array[DSPy::Example], T::Array[DSPy::Example]])
228
+ end
229
+ def self.generate_successful_examples(program, examples, config, metric)
230
+ successful = []
231
+ failed = []
232
+ error_count = 0
233
+
234
+ # Use DataHandler for efficient shuffling
235
+ data_handler = DataHandler.new(examples)
236
+ shuffled_examples = data_handler.shuffle(random_state: 42)
237
+
238
+ shuffled_examples.each_with_index do |example, index|
239
+ break if successful.size >= config.max_labeled_examples
240
+ break if error_count >= config.max_errors
241
+
242
+ begin
243
+ # Run program on example input
244
+ prediction = program.call(**example.input_values)
245
+
246
+ # Check if prediction matches expected output
247
+ if metric
248
+ success = metric.call(example, prediction.to_h)
249
+ else
250
+ success = example.matches_prediction?(prediction.to_h)
251
+ end
252
+
253
+ if success
254
+ # Create a new example with the successful prediction as reasoning/context
255
+ successful_example = create_successful_bootstrap_example(example, prediction)
256
+ successful << successful_example
257
+
258
+ emit_bootstrap_example_event(index, true, nil)
259
+ else
260
+ failed << example
261
+ emit_bootstrap_example_event(index, false, "Prediction did not match expected output")
262
+ end
263
+
264
+ rescue => error
265
+ error_count += 1
266
+ failed << example
267
+ emit_bootstrap_example_event(index, false, error.message)
268
+
269
+ # Log error but continue processing
270
+ DSPy.logger.warn("Bootstrap error on example #{index}: #{error.message}")
271
+
272
+ # Stop if too many errors
273
+ if error_count >= config.max_errors
274
+ DSPy.logger.error("Too many bootstrap errors (#{error_count}), stopping early")
275
+ break
276
+ end
277
+ end
278
+ end
279
+
280
+ [successful, failed]
281
+ end
282
+
283
+ # Create candidate sets from successful examples using efficient data handling
284
+ sig do
285
+ params(
286
+ successful_examples: T::Array[DSPy::Example],
287
+ config: BootstrapConfig
288
+ ).returns(T::Array[T::Array[DSPy::Example]])
289
+ end
290
+ def self.create_candidate_sets(successful_examples, config)
291
+ return [] if successful_examples.empty?
292
+
293
+ # Use DataHandler for efficient sampling
294
+ data_handler = DataHandler.new(successful_examples)
295
+ set_size = [config.max_bootstrapped_examples, successful_examples.size].min
296
+
297
+ # Create candidate sets efficiently
298
+ candidate_sets = data_handler.create_candidate_sets(
299
+ config.num_candidate_sets,
300
+ set_size,
301
+ random_state: 42 # For reproducible results
302
+ )
303
+
304
+ candidate_sets
305
+ end
306
+
307
+ # Create a bootstrap example that includes the successful prediction
308
+ sig do
309
+ params(
310
+ original_example: DSPy::Example,
311
+ prediction: T.untyped
312
+ ).returns(DSPy::Example)
313
+ end
314
+ def self.create_successful_bootstrap_example(original_example, prediction)
315
+ # Convert prediction to FewShotExample format
316
+ DSPy::Example.new(
317
+ signature_class: original_example.signature_class,
318
+ input: original_example.input_values,
319
+ expected: prediction.to_h,
320
+ id: "bootstrap_#{original_example.id || SecureRandom.uuid}",
321
+ metadata: {
322
+ source: "bootstrap",
323
+ original_expected: original_example.expected_values,
324
+ bootstrap_timestamp: Time.now.iso8601
325
+ }
326
+ )
327
+ end
328
+
329
+
330
+ # Create default metric for examples
331
+ sig { params(examples: T::Array[T.untyped]).returns(T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T::Boolean))) }
332
+ def self.default_metric_for_examples(examples)
333
+ if examples.first.is_a?(DSPy::Example)
334
+ proc { |example, prediction| example.matches_prediction?(prediction) }
335
+ else
336
+ nil
337
+ end
338
+ end
339
+
340
+ # Emit bootstrap completion event
341
+ sig { params(statistics: T::Hash[Symbol, T.untyped]).void }
342
+ def self.emit_bootstrap_complete_event(statistics)
343
+ Instrumentation.emit('dspy.optimization.bootstrap_complete', {
344
+ successful_count: statistics[:successful_count],
345
+ failed_count: statistics[:failed_count],
346
+ success_rate: statistics[:success_rate],
347
+ candidate_sets_created: statistics[:candidate_sets_created],
348
+ average_set_size: statistics[:average_set_size]
349
+ })
350
+ end
351
+
352
+ # Emit individual bootstrap example event
353
+ sig { params(index: Integer, success: T::Boolean, error: T.nilable(String)).void }
354
+ def self.emit_bootstrap_example_event(index, success, error)
355
+ Instrumentation.emit('dspy.optimization.bootstrap_example', {
356
+ example_index: index,
357
+ success: success,
358
+ error: error,
359
+ timestamp: Time.now.iso8601
360
+ })
361
+ end
362
+
363
+ # Infer signature class from examples
364
+ sig { params(examples: T::Array[T.untyped]).returns(T.nilable(T.class_of(Signature))) }
365
+ def self.infer_signature_class(examples)
366
+ return nil if examples.empty?
367
+
368
+ first_example = examples.first
369
+
370
+ if first_example.is_a?(DSPy::Example)
371
+ first_example.signature_class
372
+ elsif first_example.is_a?(Hash) && first_example[:signature_class]
373
+ first_example[:signature_class]
374
+ else
375
+ nil
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSPy
4
+ VERSION = "0.4.0"
5
+ end
data/lib/dspy.rb CHANGED
@@ -3,6 +3,8 @@ require 'sorbet-runtime'
3
3
  require 'dry-configurable'
4
4
  require 'dry/logger'
5
5
 
6
+ require_relative 'dspy/version'
7
+
6
8
  module DSPy
7
9
  extend Dry::Configurable
8
10
  setting :lm
@@ -16,12 +18,26 @@ end
16
18
  require_relative 'dspy/module'
17
19
  require_relative 'dspy/field'
18
20
  require_relative 'dspy/signature'
21
+ require_relative 'dspy/few_shot_example'
22
+ require_relative 'dspy/prompt'
23
+ require_relative 'dspy/example'
19
24
  require_relative 'dspy/lm'
20
25
  require_relative 'dspy/predict'
21
26
  require_relative 'dspy/chain_of_thought'
22
27
  require_relative 'dspy/re_act'
28
+ require_relative 'dspy/evaluate'
29
+ require_relative 'dspy/teleprompt/teleprompter'
30
+ require_relative 'dspy/teleprompt/utils'
31
+ require_relative 'dspy/teleprompt/data_handler'
32
+ require_relative 'dspy/propose/grounded_proposer'
33
+ require_relative 'dspy/teleprompt/simple_optimizer'
34
+ require_relative 'dspy/teleprompt/mipro_v2'
23
35
  require_relative 'dspy/subscribers/logger_subscriber'
24
36
  require_relative 'dspy/tools'
25
37
  require_relative 'dspy/instrumentation'
38
+ require_relative 'dspy/storage/program_storage'
39
+ require_relative 'dspy/storage/storage_manager'
40
+ require_relative 'dspy/registry/signature_registry'
41
+ require_relative 'dspy/registry/registry_manager'
26
42
 
27
43
  # LoggerSubscriber will be lazy-initialized when first accessed
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-06-27 00:00:00.000000000 Z
10
+ date: 2025-07-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-configurable
@@ -94,47 +94,47 @@ dependencies:
94
94
  - !ruby/object:Gem::Version
95
95
  version: 1.1.0
96
96
  - !ruby/object:Gem::Dependency
97
- name: ruby_llm
97
+ name: sorbet-runtime
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: '1.0'
102
+ version: '0.5'
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '1.0'
109
+ version: '0.5'
110
110
  - !ruby/object:Gem::Dependency
111
- name: sorbet-runtime
111
+ name: sorbet-schema
112
112
  requirement: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: '0.5'
116
+ version: '0.3'
117
117
  type: :runtime
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: '0.5'
123
+ version: '0.3'
124
124
  - !ruby/object:Gem::Dependency
125
- name: sorbet-schema
125
+ name: polars-df
126
126
  requirement: !ruby/object:Gem::Requirement
127
127
  requirements:
128
128
  - - "~>"
129
129
  - !ruby/object:Gem::Version
130
- version: '0.3'
130
+ version: 0.20.0
131
131
  type: :runtime
132
132
  prerelease: false
133
133
  version_requirements: !ruby/object:Gem::Requirement
134
134
  requirements:
135
135
  - - "~>"
136
136
  - !ruby/object:Gem::Version
137
- version: '0.3'
137
+ version: 0.20.0
138
138
  description: A Ruby implementation of DSPy, a framework for programming with large
139
139
  language models
140
140
  email:
@@ -146,6 +146,9 @@ files:
146
146
  - README.md
147
147
  - lib/dspy.rb
148
148
  - lib/dspy/chain_of_thought.rb
149
+ - lib/dspy/evaluate.rb
150
+ - lib/dspy/example.rb
151
+ - lib/dspy/few_shot_example.rb
149
152
  - lib/dspy/field.rb
150
153
  - lib/dspy/instrumentation.rb
151
154
  - lib/dspy/instrumentation/token_tracker.rb
@@ -154,17 +157,31 @@ files:
154
157
  - lib/dspy/lm/adapter_factory.rb
155
158
  - lib/dspy/lm/adapters/anthropic_adapter.rb
156
159
  - lib/dspy/lm/adapters/openai_adapter.rb
157
- - lib/dspy/lm/adapters/ruby_llm_adapter.rb
158
160
  - lib/dspy/lm/errors.rb
159
161
  - lib/dspy/lm/response.rb
160
162
  - lib/dspy/module.rb
161
163
  - lib/dspy/predict.rb
164
+ - lib/dspy/prompt.rb
165
+ - lib/dspy/propose/grounded_proposer.rb
162
166
  - lib/dspy/re_act.rb
167
+ - lib/dspy/registry/registry_manager.rb
168
+ - lib/dspy/registry/signature_registry.rb
163
169
  - lib/dspy/schema_adapters.rb
164
170
  - lib/dspy/signature.rb
171
+ - lib/dspy/storage/program_storage.rb
172
+ - lib/dspy/storage/storage_manager.rb
173
+ - lib/dspy/subscribers/langfuse_subscriber.rb
165
174
  - lib/dspy/subscribers/logger_subscriber.rb
175
+ - lib/dspy/subscribers/newrelic_subscriber.rb
176
+ - lib/dspy/subscribers/otel_subscriber.rb
177
+ - lib/dspy/teleprompt/data_handler.rb
178
+ - lib/dspy/teleprompt/mipro_v2.rb
179
+ - lib/dspy/teleprompt/simple_optimizer.rb
180
+ - lib/dspy/teleprompt/teleprompter.rb
181
+ - lib/dspy/teleprompt/utils.rb
166
182
  - lib/dspy/tools.rb
167
183
  - lib/dspy/tools/base.rb
184
+ - lib/dspy/version.rb
168
185
  homepage: https://github.com/vicentereig/dspy.rb
169
186
  licenses:
170
187
  - MIT
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- begin
4
- require 'ruby_llm'
5
- rescue LoadError
6
- # ruby_llm is optional for backward compatibility
7
- end
8
-
9
- module DSPy
10
- class LM
11
- class RubyLLMAdapter < Adapter
12
- def initialize(model:, api_key:)
13
- super
14
-
15
- unless defined?(RubyLLM)
16
- raise ConfigurationError,
17
- "ruby_llm gem is required for RubyLLMAdapter. " \
18
- "Add 'gem \"ruby_llm\"' to your Gemfile."
19
- end
20
-
21
- configure_ruby_llm
22
- end
23
-
24
- def chat(messages:, &block)
25
- begin
26
- chat = RubyLLM.chat(model: model)
27
-
28
- # Add messages to chat
29
- messages.each do |msg|
30
- chat.add_message(role: msg[:role].to_sym, content: msg[:content])
31
- end
32
-
33
- # Get the last user message for ask method
34
- last_user_message = messages.reverse.find { |msg| msg[:role] == 'user' }
35
-
36
- if last_user_message
37
- # Remove the last user message since ask() will add it
38
- chat.messages.pop if chat.messages.last&.content == last_user_message[:content]
39
- chat.ask(last_user_message[:content], &block)
40
- else
41
- raise AdapterError, "No user message found in conversation"
42
- end
43
-
44
- content = chat.messages.last&.content || ""
45
-
46
- Response.new(
47
- content: content,
48
- usage: nil, # ruby_llm doesn't provide usage info
49
- metadata: {
50
- provider: 'ruby_llm',
51
- model: model,
52
- message_count: chat.messages.length
53
- }
54
- )
55
- rescue => e
56
- raise AdapterError, "RubyLLM adapter error: #{e.message}"
57
- end
58
- end
59
-
60
- private
61
-
62
- def configure_ruby_llm
63
- # Determine provider from model for configuration
64
- if model.include?('gpt') || model.include?('openai')
65
- RubyLLM.configure do |config|
66
- config.openai_api_key = api_key
67
- end
68
- elsif model.include?('claude') || model.include?('anthropic')
69
- RubyLLM.configure do |config|
70
- config.anthropic_api_key = api_key
71
- end
72
- else
73
- # Default to OpenAI configuration
74
- RubyLLM.configure do |config|
75
- config.openai_api_key = api_key
76
- end
77
- end
78
- end
79
- end
80
- end
81
- end