dspy 0.28.2 → 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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -3
  3. data/lib/dspy/code_act.rb +14 -1
  4. data/lib/dspy/datasets/ade.rb +90 -0
  5. data/lib/dspy/datasets.rb +8 -0
  6. data/lib/dspy/lm.rb +4 -8
  7. data/lib/dspy/mixins/struct_builder.rb +17 -25
  8. data/lib/dspy/module.rb +12 -1
  9. data/lib/dspy/observability/async_span_processor.rb +67 -93
  10. data/lib/dspy/observability.rb +43 -1
  11. data/lib/dspy/predict.rb +10 -0
  12. data/lib/dspy/propose/dataset_summary_generator.rb +36 -3
  13. data/lib/dspy/propose/grounded_proposer.rb +118 -11
  14. data/lib/dspy/re_act.rb +13 -0
  15. data/lib/dspy/reflection_lm.rb +36 -0
  16. data/lib/dspy/teleprompt/gepa.rb +448 -2803
  17. data/lib/dspy/teleprompt/mipro_v2.rb +564 -65
  18. data/lib/dspy/teleprompt/utils.rb +8 -3
  19. data/lib/dspy/version.rb +2 -2
  20. data/lib/dspy.rb +3 -2
  21. data/lib/gepa/api.rb +61 -0
  22. data/lib/gepa/core/engine.rb +226 -0
  23. data/lib/gepa/core/evaluation_batch.rb +26 -0
  24. data/lib/gepa/core/result.rb +92 -0
  25. data/lib/gepa/core/state.rb +231 -0
  26. data/lib/gepa/logging/experiment_tracker.rb +54 -0
  27. data/lib/gepa/logging/logger.rb +57 -0
  28. data/lib/gepa/logging.rb +9 -0
  29. data/lib/gepa/proposer/base.rb +27 -0
  30. data/lib/gepa/proposer/merge_proposer.rb +424 -0
  31. data/lib/gepa/proposer/reflective_mutation/base.rb +48 -0
  32. data/lib/gepa/proposer/reflective_mutation/reflective_mutation.rb +188 -0
  33. data/lib/gepa/strategies/batch_sampler.rb +91 -0
  34. data/lib/gepa/strategies/candidate_selector.rb +97 -0
  35. data/lib/gepa/strategies/component_selector.rb +57 -0
  36. data/lib/gepa/strategies/instruction_proposal.rb +120 -0
  37. data/lib/gepa/telemetry.rb +122 -0
  38. data/lib/gepa/utils/pareto.rb +119 -0
  39. data/lib/gepa.rb +21 -0
  40. metadata +42 -4
  41. data/lib/dspy/teleprompt/simple_optimizer.rb +0 -503
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1cc0ac1e2e1dc27f6255b11ca70ff0ad0eb37b5ae50ff38b97ae7d35b7b69b8
4
- data.tar.gz: 2399987757b4f037080632e646714e785328fbe3c1fd39138fe750336cdd7710
3
+ metadata.gz: 747119ce407283e4d8ed5f01014262f24a94418ad2cbef4305a28b21cb58c8bc
4
+ data.tar.gz: 3693faccd1fca98015864fd4404491b619b2aa600ab83a78dd3fc7d9e3342ef1
5
5
  SHA512:
6
- metadata.gz: ea48762186d3de89a005e8eac27f57ca90b294c4ab096ec1c7985d06396216d719ca3a2144406d1f43af472a560670e502329a785d33b8923b5c9c4c0f83dfd1
7
- data.tar.gz: 98fee1468f2692a0f7cc87622722f945dbc344fb299e71935ef592bc1d59e68e48e1390208981b6263998627dacb38dc325727cc191f81e1db79fff57c1537a4
6
+ metadata.gz: 7fecac3bc3389e11bdb2328234455cbbbcbf6c7544518cbf94082e75a3f0bde489339b70f91dfd31a43bbd87b17451bfb5a78387c90f5b669aefa09e8af73500
7
+ data.tar.gz: 01feee252179dd66016a8633658631dacc2262f5598ee07d4f78ec2947ebbb57cd15b19b433b8ea9a87b7120fbf5afa984fd929677de0fcfaf7a4368e5b29d85
data/README.md CHANGED
@@ -112,7 +112,6 @@ end
112
112
  - **Typed Examples** - Type-safe training data with automatic validation
113
113
  - **Evaluation Framework** - Advanced metrics beyond simple accuracy with error-resilient pipelines
114
114
  - **MIPROv2 Optimization** - Advanced Bayesian optimization with Gaussian Processes, multiple optimization strategies, and storage persistence
115
- - **GEPA Optimization** - Genetic-Pareto optimization for multi-objective prompt improvement
116
115
 
117
116
  **Production Features:**
118
117
  - **Reliable JSON Extraction** - Native structured outputs for OpenAI and Gemini, Anthropic tool-based extraction, and automatic strategy selection with fallback
@@ -168,11 +167,11 @@ For LLMs and AI assistants working with DSPy.rb:
168
167
  - **[Evaluation Framework](docs/src/optimization/evaluation.md)** - Advanced metrics beyond simple accuracy
169
168
  - **[Prompt Optimization](docs/src/optimization/prompt-optimization.md)** - Manipulate prompts as objects
170
169
  - **[MIPROv2 Optimizer](docs/src/optimization/miprov2.md)** - Advanced Bayesian optimization with Gaussian Processes
171
- - **[GEPA Optimizer](docs/src/optimization/gepa.md)** - Genetic-Pareto optimization for multi-objective prompt optimization
170
+ - **[GEPA Optimizer](docs/src/optimization/gepa.md)** *(beta)* - Reflective mutation with optional reflection LMs
172
171
 
173
172
  ### Production Features
174
173
  - **[Storage System](docs/src/production/storage.md)** - Persistence and optimization result storage
175
- - **[Observability](docs/src/production/observability.md)** - Zero-config Langfuse integration and structured logging
174
+ - **[Observability](docs/src/production/observability.md)** - Zero-config Langfuse integration with a dedicated export worker that never blocks your LLMs
176
175
 
177
176
  ### Advanced Usage
178
177
  - **[Complex Types](docs/src/advanced/complex-types.md)** - Sorbet type integration with automatic coercion for structs, enums, and arrays
data/lib/dspy/code_act.rb CHANGED
@@ -146,6 +146,19 @@ module DSPy
146
146
  super(enhanced_signature)
147
147
  end
148
148
 
149
+ sig { override.returns(T::Array[[String, DSPy::Module]]) }
150
+ def named_predictors
151
+ pairs = T.let([], T::Array[[String, DSPy::Module]])
152
+ pairs << ["code_generator", @code_generator]
153
+ pairs << ["observation_processor", @observation_processor]
154
+ pairs
155
+ end
156
+
157
+ sig { override.returns(T::Array[DSPy::Module]) }
158
+ def predictors
159
+ named_predictors.map { |(_, predictor)| predictor }
160
+ end
161
+
149
162
  sig { params(kwargs: T.untyped).returns(T.untyped).override }
150
163
  def forward(**kwargs)
151
164
  # Validate input and serialize all fields as task context
@@ -461,4 +474,4 @@ module DSPy
461
474
  example
462
475
  end
463
476
  end
464
- end
477
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+ require 'cgi'
7
+ require 'fileutils'
8
+
9
+ module DSPy
10
+ module Datasets
11
+ module ADE
12
+ extend self
13
+
14
+ DATASET = 'ade-benchmark-corpus/ade_corpus_v2'
15
+ CLASSIFICATION_CONFIG = 'Ade_corpus_v2_classification'
16
+ BASE_URL = 'https://datasets-server.huggingface.co'
17
+
18
+ DEFAULT_CACHE_DIR = File.expand_path('../../../tmp/dspy_datasets/ade', __dir__)
19
+
20
+ MAX_BATCH_SIZE = 100
21
+
22
+ def examples(split: 'train', limit: 200, offset: 0, cache_dir: default_cache_dir)
23
+ remaining = limit
24
+ current_offset = offset
25
+ collected = []
26
+
27
+ while remaining.positive?
28
+ batch_size = [remaining, MAX_BATCH_SIZE].min
29
+ rows = fetch_rows(
30
+ split: split,
31
+ limit: batch_size,
32
+ offset: current_offset,
33
+ cache_dir: cache_dir
34
+ )
35
+
36
+ break if rows.empty?
37
+
38
+ collected.concat(rows.map do |row|
39
+ {
40
+ 'text' => row.fetch('text', ''),
41
+ 'label' => row.fetch('label', 0).to_i
42
+ }
43
+ end)
44
+
45
+ current_offset += batch_size
46
+ remaining -= batch_size
47
+ end
48
+
49
+ collected
50
+ end
51
+
52
+ def fetch_rows(split:, limit:, offset:, cache_dir:)
53
+ FileUtils.mkdir_p(cache_dir)
54
+ cache_path = File.join(cache_dir, "#{CLASSIFICATION_CONFIG}_#{split}_#{offset}_#{limit}.json")
55
+
56
+ if File.exist?(cache_path)
57
+ return JSON.parse(File.read(cache_path))
58
+ end
59
+
60
+ rows = request_rows(split: split, limit: limit, offset: offset)
61
+ File.write(cache_path, JSON.pretty_generate(rows))
62
+ rows
63
+ end
64
+
65
+ private
66
+
67
+ def request_rows(split:, limit:, offset:)
68
+ uri = URI("#{BASE_URL}/rows")
69
+ params = {
70
+ dataset: DATASET,
71
+ config: CLASSIFICATION_CONFIG,
72
+ split: split,
73
+ offset: offset,
74
+ length: limit
75
+ }
76
+ uri.query = URI.encode_www_form(params)
77
+
78
+ response = Net::HTTP.get_response(uri)
79
+ raise "ADE dataset request failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
80
+
81
+ body = JSON.parse(response.body)
82
+ body.fetch('rows', []).map { |row| row.fetch('row', {}) }
83
+ end
84
+
85
+ def default_cache_dir
86
+ ENV['DSPY_DATASETS_CACHE'] ? File.expand_path('ade', ENV['DSPY_DATASETS_CACHE']) : DEFAULT_CACHE_DIR
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'datasets/ade'
4
+
5
+ module DSPy
6
+ module Datasets
7
+ end
8
+ end
data/lib/dspy/lm.rb CHANGED
@@ -67,9 +67,6 @@ module DSPy
67
67
  chat_with_strategy(messages, signature_class, &block)
68
68
  end
69
69
 
70
- # Emit the standard lm.tokens event (consistent with raw_chat)
71
- emit_token_usage(response, signature_class.name)
72
-
73
70
  # Parse response (no longer needs separate instrumentation)
74
71
  parsed_result = parse_response(response, input_values, signature_class)
75
72
 
@@ -271,7 +268,7 @@ module DSPy
271
268
  'dspy.signature' => signature_class_name
272
269
  ) do |span|
273
270
  result = execution_block.call
274
-
271
+
275
272
  # Add output and usage data directly to span
276
273
  if span && result
277
274
  # Add completion output
@@ -293,7 +290,9 @@ module DSPy
293
290
  span.set_attribute('gen_ai.usage.total_tokens', usage.total_tokens) if usage.total_tokens
294
291
  end
295
292
  end
296
-
293
+
294
+ emit_token_usage(result, signature_class_name)
295
+
297
296
  result
298
297
  end
299
298
 
@@ -410,9 +409,6 @@ module DSPy
410
409
  adapter.chat(messages: hash_messages, signature: nil, &streaming_block)
411
410
  end
412
411
 
413
- # Emit the standard lm.tokens event (consistent with other LM calls)
414
- emit_token_usage(response, 'RawPrompt')
415
-
416
412
  # Return raw response content, not parsed JSON
417
413
  response.content
418
414
  ensure
@@ -19,20 +19,20 @@ module DSPy
19
19
 
20
20
  Class.new(T::Struct) do
21
21
  extend T::Sig
22
+ define_field = lambda do |name, type, options|
23
+ const_kwargs = {}
24
+ const_kwargs[:default] = options[:default] if options.key?(:default)
25
+ const_kwargs[:factory] = options[:factory] if options.key?(:factory)
26
+ const_kwargs[:override] = true if props.key?(name)
27
+ const name, type, **const_kwargs
28
+ end
22
29
 
23
30
  # Add properties from each source
24
31
  property_sources.each do |_source_name, props|
25
32
  props.each do |name, prop|
26
33
  type = builder.send(:extract_type_from_prop, prop)
27
34
  options = builder.send(:extract_options_from_prop, prop)
28
-
29
- if options[:default]
30
- const name, type, default: options[:default]
31
- elsif options[:factory]
32
- const name, type, factory: options[:factory]
33
- else
34
- const name, type
35
- end
35
+ define_field.call(name, type, options)
36
36
  end
37
37
  end
38
38
 
@@ -40,14 +40,7 @@ module DSPy
40
40
  additional_fields.each do |name, field_config|
41
41
  type = builder.send(:extract_type_from_prop, field_config)
42
42
  options = builder.send(:extract_options_from_prop, field_config)
43
-
44
- if options[:default]
45
- const name, type, default: options[:default]
46
- elsif options[:factory]
47
- const name, type, factory: options[:factory]
48
- else
49
- const name, type
50
- end
43
+ define_field.call(name, type, options)
51
44
  end
52
45
 
53
46
  include StructSerialization
@@ -65,14 +58,13 @@ module DSPy
65
58
  def build_single_property(name, prop)
66
59
  type = extract_type_from_prop(prop)
67
60
  options = extract_options_from_prop(prop)
68
-
69
- if options[:default]
70
- const name, type, default: options[:default]
71
- elsif options[:factory]
72
- const name, type, factory: options[:factory]
73
- else
74
- const name, type
75
- end
61
+
62
+ const_kwargs = {}
63
+ const_kwargs[:default] = options[:default] if options.key?(:default)
64
+ const_kwargs[:factory] = options[:factory] if options.key?(:factory)
65
+ const_kwargs[:override] = true if respond_to?(:props) && props.key?(name)
66
+
67
+ const name, type, **const_kwargs
76
68
  end
77
69
 
78
70
  # Extracts type from property configuration
@@ -142,4 +134,4 @@ module DSPy
142
134
  end
143
135
  end
144
136
  end
145
- end
137
+ end
data/lib/dspy/module.rb CHANGED
@@ -105,5 +105,16 @@ module DSPy
105
105
  state: {}
106
106
  }
107
107
  end
108
+
109
+ # Discover nested predictor modules (Python parity helper)
110
+ sig { returns(T::Array[[String, DSPy::Module]]) }
111
+ def named_predictors
112
+ []
113
+ end
114
+
115
+ sig { returns(T::Array[DSPy::Module]) }
116
+ def predictors
117
+ named_predictors.map { |(_, predictor)| predictor }
118
+ end
108
119
  end
109
- end
120
+ end
@@ -1,15 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'async'
4
- require 'async/queue'
5
- require 'async/barrier'
3
+ require 'concurrent-ruby'
4
+ require 'thread'
6
5
  require 'opentelemetry/sdk'
7
6
  require 'opentelemetry/sdk/trace/export'
8
7
 
9
8
  module DSPy
10
9
  class Observability
11
- # AsyncSpanProcessor provides truly non-blocking span export using Async gem.
12
- # Spans are queued and exported using async tasks with fiber-based concurrency.
10
+ # AsyncSpanProcessor provides non-blocking span export using concurrent-ruby.
11
+ # Spans are queued and exported on a dedicated single-thread executor to avoid blocking clients.
13
12
  # Implements the same interface as OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor
14
13
  class AsyncSpanProcessor
15
14
  # Default configuration values
@@ -33,12 +32,12 @@ module DSPy
33
32
  @export_batch_size = export_batch_size
34
33
  @shutdown_timeout = shutdown_timeout
35
34
  @max_retries = max_retries
35
+ @export_executor = Concurrent::SingleThreadExecutor.new
36
36
 
37
37
  # Use thread-safe queue for cross-fiber communication
38
38
  @queue = Thread::Queue.new
39
- @barrier = Async::Barrier.new
40
39
  @shutdown_requested = false
41
- @export_task = nil
40
+ @timer_thread = nil
42
41
 
43
42
  start_export_task
44
43
  end
@@ -85,22 +84,35 @@ module DSPy
85
84
 
86
85
  begin
87
86
  # Export any remaining spans
88
- export_remaining_spans
87
+ result = export_remaining_spans(timeout: timeout, export_all: true)
89
88
 
90
- # Shutdown exporter
91
- @exporter.shutdown(timeout: timeout)
89
+ future = Concurrent::Promises.future_on(@export_executor) do
90
+ @exporter.shutdown(timeout: timeout)
91
+ end
92
+ future.value!(timeout)
92
93
 
93
- OpenTelemetry::SDK::Trace::Export::SUCCESS
94
+ result
94
95
  rescue => e
95
96
  DSPy.log('observability.shutdown_error', error: e.message, class: e.class.name)
96
97
  OpenTelemetry::SDK::Trace::Export::FAILURE
98
+ ensure
99
+ begin
100
+ @timer_thread&.join(timeout)
101
+ @timer_thread&.kill if @timer_thread&.alive?
102
+ rescue StandardError
103
+ # ignore timer shutdown issues
104
+ end
105
+ @export_executor.shutdown
106
+ unless @export_executor.wait_for_termination(timeout)
107
+ @export_executor.kill
108
+ end
97
109
  end
98
110
  end
99
111
 
100
112
  def force_flush(timeout: nil)
101
113
  return OpenTelemetry::SDK::Trace::Export::SUCCESS if @queue.empty?
102
114
 
103
- export_remaining_spans
115
+ export_remaining_spans(timeout: timeout, export_all: true)
104
116
  end
105
117
 
106
118
  private
@@ -109,19 +121,15 @@ module DSPy
109
121
  return if @export_interval <= 0 # Disable timer for testing
110
122
  return if ENV['DSPY_DISABLE_OBSERVABILITY'] == 'true' # Skip in tests
111
123
 
112
- # Start timer-based export task in background
113
- Thread.new do
124
+ @timer_thread = Thread.new do
114
125
  loop do
115
126
  break if @shutdown_requested
116
127
 
117
128
  sleep(@export_interval)
129
+ break if @shutdown_requested
130
+ next if @queue.empty?
118
131
 
119
- # Export queued spans in sync block
120
- unless @queue.empty?
121
- Sync do
122
- export_queued_spans
123
- end
124
- end
132
+ schedule_async_export(export_all: true)
125
133
  end
126
134
  rescue => e
127
135
  DSPy.log('observability.export_task_error', error: e.message, class: e.class.name)
@@ -131,39 +139,56 @@ module DSPy
131
139
  def trigger_export_if_batch_full
132
140
  return if @queue.size < @export_batch_size
133
141
  return if ENV['DSPY_DISABLE_OBSERVABILITY'] == 'true' # Skip in tests
142
+ schedule_async_export(export_all: false)
143
+ end
134
144
 
135
- # Trigger immediate export in background
136
- Thread.new do
137
- Sync do
138
- export_queued_spans
139
- end
140
- rescue => e
141
- DSPy.log('observability.batch_export_error', error: e.message)
145
+ def export_remaining_spans(timeout: nil, export_all: true)
146
+ return OpenTelemetry::SDK::Trace::Export::SUCCESS if @queue.empty?
147
+
148
+ future = Concurrent::Promises.future_on(@export_executor) do
149
+ export_queued_spans_internal(export_all: export_all)
142
150
  end
151
+
152
+ future.value!(timeout || @shutdown_timeout)
153
+ rescue => e
154
+ DSPy.log('observability.export_error', error: e.message, class: e.class.name)
155
+ OpenTelemetry::SDK::Trace::Export::FAILURE
143
156
  end
144
157
 
145
- def export_remaining_spans
146
- spans = []
158
+ def schedule_async_export(export_all: false)
159
+ return if @shutdown_requested
147
160
 
148
- # Drain entire queue
149
- until @queue.empty?
150
- begin
151
- spans << @queue.pop(true) # non-blocking pop
152
- rescue ThreadError
153
- break
154
- end
161
+ @export_executor.post do
162
+ export_queued_spans_internal(export_all: export_all)
163
+ rescue => e
164
+ DSPy.log('observability.batch_export_error', error: e.message, class: e.class.name)
155
165
  end
166
+ end
167
+
168
+ def export_queued_spans
169
+ export_queued_spans_internal(export_all: false)
170
+ end
171
+
172
+ def export_queued_spans_internal(export_all: false)
173
+ result = OpenTelemetry::SDK::Trace::Export::SUCCESS
174
+
175
+ loop do
176
+ spans = dequeue_spans(export_all ? @queue_size : @export_batch_size)
177
+ break if spans.empty?
156
178
 
157
- return OpenTelemetry::SDK::Trace::Export::SUCCESS if spans.empty?
179
+ result = export_spans_with_retry(spans)
180
+ break if result == OpenTelemetry::SDK::Trace::Export::FAILURE
158
181
 
159
- export_spans_with_retry(spans)
182
+ break unless export_all || @queue.size >= @export_batch_size
183
+ end
184
+
185
+ result
160
186
  end
161
187
 
162
- def export_queued_spans
188
+ def dequeue_spans(limit)
163
189
  spans = []
164
190
 
165
- # Collect up to batch size
166
- @export_batch_size.times do
191
+ limit.times do
167
192
  begin
168
193
  spans << @queue.pop(true) # non-blocking pop
169
194
  rescue ThreadError
@@ -171,12 +196,7 @@ module DSPy
171
196
  end
172
197
  end
173
198
 
174
- return if spans.empty?
175
-
176
- # Export using async I/O
177
- Sync do
178
- export_spans_with_retry_async(spans)
179
- end
199
+ spans
180
200
  end
181
201
 
182
202
  def export_spans_with_retry(spans)
@@ -225,52 +245,6 @@ module DSPy
225
245
  OpenTelemetry::SDK::Trace::Export::FAILURE
226
246
  end
227
247
 
228
- def export_spans_with_retry_async(spans)
229
- retries = 0
230
-
231
- # Convert spans to SpanData objects (required by OTLP exporter)
232
- span_data_batch = spans.map(&:to_span_data)
233
-
234
- # Log export attempt
235
- DSPy.log('observability.export_attempt',
236
- spans_count: span_data_batch.size,
237
- batch_size: span_data_batch.size)
238
-
239
- loop do
240
- # Use current async task for potentially non-blocking export
241
- result = @exporter.export(span_data_batch, timeout: @shutdown_timeout)
242
-
243
- case result
244
- when OpenTelemetry::SDK::Trace::Export::SUCCESS
245
- DSPy.log('observability.export_success',
246
- spans_count: span_data_batch.size,
247
- export_result: 'SUCCESS')
248
- return result
249
- when OpenTelemetry::SDK::Trace::Export::FAILURE
250
- retries += 1
251
- if retries <= @max_retries
252
- backoff_seconds = 0.1 * (2 ** retries)
253
- DSPy.log('observability.export_retry',
254
- attempt: retries,
255
- spans_count: span_data_batch.size,
256
- backoff_seconds: backoff_seconds)
257
- # Async sleep for exponential backoff
258
- Async::Task.current.sleep(backoff_seconds)
259
- next
260
- else
261
- DSPy.log('observability.export_failed',
262
- spans_count: span_data_batch.size,
263
- retries: retries)
264
- return result
265
- end
266
- else
267
- return result
268
- end
269
- end
270
- rescue => e
271
- DSPy.log('observability.export_error', error: e.message, class: e.class.name)
272
- OpenTelemetry::SDK::Trace::Export::FAILURE
273
- end
274
248
  end
275
249
  end
276
250
  end
@@ -41,6 +41,8 @@ module DSPy
41
41
  require 'opentelemetry/sdk'
42
42
  require 'opentelemetry/exporter/otlp'
43
43
 
44
+ patch_frozen_ssl_context_for_otlp!
45
+
44
46
  # Generate Basic Auth header
45
47
  auth_string = Base64.strict_encode64("#{public_key}:#{secret_key}")
46
48
 
@@ -150,6 +152,46 @@ module DSPy
150
152
  @tracer = nil
151
153
  @endpoint = nil
152
154
  end
155
+
156
+ private
157
+
158
+ def patch_frozen_ssl_context_for_otlp!
159
+ return unless defined?(OpenTelemetry::Exporter::OTLP::Exporter)
160
+
161
+ ssl_context_frozen = begin
162
+ http = Net::HTTP.new('example.com', 443)
163
+ http.use_ssl = true
164
+ http.ssl_context&.frozen?
165
+ rescue StandardError
166
+ false
167
+ end
168
+
169
+ return unless ssl_context_frozen
170
+
171
+ exporter = OpenTelemetry::Exporter::OTLP::Exporter
172
+ return if exporter.instance_variable_defined?(:@_dspy_ssl_patch_applied)
173
+
174
+ exporter.class_eval do
175
+ define_method(:http_connection) do |uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_file|
176
+ http = Net::HTTP.new(uri.host, uri.port)
177
+ use_ssl = uri.scheme == 'https'
178
+ http.use_ssl = use_ssl
179
+
180
+ if use_ssl && http.ssl_context&.frozen?
181
+ http.instance_variable_set(:@ssl_context, OpenSSL::SSL::SSLContext.new)
182
+ end
183
+
184
+ http.verify_mode = ssl_verify_mode
185
+ http.ca_file = certificate_file unless certificate_file.nil?
186
+ http.cert = OpenSSL::X509::Certificate.new(File.read(client_certificate_file)) unless client_certificate_file.nil?
187
+ http.key = OpenSSL::PKey::RSA.new(File.read(client_key_file)) unless client_key_file.nil?
188
+ http.keep_alive_timeout = KEEP_ALIVE_TIMEOUT
189
+ http
190
+ end
191
+ end
192
+
193
+ exporter.instance_variable_set(:@_dspy_ssl_patch_applied, true)
194
+ end
153
195
  end
154
196
  end
155
- end
197
+ end
data/lib/dspy/predict.rb CHANGED
@@ -138,6 +138,16 @@ module DSPy
138
138
  with_prompt(@prompt.add_examples(examples))
139
139
  end
140
140
 
141
+ sig { override.returns(T::Array[[String, DSPy::Module]]) }
142
+ def named_predictors
143
+ [["self", self]]
144
+ end
145
+
146
+ sig { override.returns(T::Array[DSPy::Module]) }
147
+ def predictors
148
+ [self]
149
+ end
150
+
141
151
  # Remove forward override to let Module#forward handle span creation
142
152
 
143
153
  sig { params(input_values: T.untyped).returns(T.untyped) }
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'sorbet-runtime'
4
+ require 'json'
4
5
  require_relative '../signature'
5
6
  require_relative '../predict'
7
+ require_relative '../type_serializer'
8
+ require_relative '../few_shot_example'
6
9
 
7
10
  module DSPy
8
11
  module Propose
@@ -119,9 +122,10 @@ module DSPy
119
122
  DSPy.with_lm(lm) do
120
123
  # Initial observation from first batch
121
124
  upper_lim = [trainset.length, view_data_batch_size].min
122
- examples_repr = order_input_keys_in_string(trainset[0...upper_lim].inspect)
123
-
125
+ batch_examples = trainset[0...upper_lim]
124
126
  predictor = DSPy::Predict.new(DatasetDescriptor)
127
+ examples_repr = format_examples_for_prompt(batch_examples)
128
+
125
129
  observation = predictor.call(examples: examples_repr)
126
130
  observations = observation.observations
127
131
 
@@ -138,9 +142,11 @@ module DSPy
138
142
  puts "Processing batch starting at index #{b}" if verbose
139
143
 
140
144
  upper_lim = [trainset.length, b + view_data_batch_size].min
141
- examples_repr = order_input_keys_in_string(trainset[b...upper_lim].inspect)
142
145
 
143
146
  predictor = DSPy::Predict.new(DatasetDescriptorWithPriorObservations)
147
+ batch_examples = trainset[b...upper_lim]
148
+ examples_repr = format_examples_for_prompt(batch_examples)
149
+
144
150
  output = predictor.call(
145
151
  prior_observations: observations,
146
152
  examples: examples_repr
@@ -172,6 +178,33 @@ module DSPy
172
178
  strip_prefix(summary.summary)
173
179
  end
174
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
175
208
  end
176
209
  end
177
210
  end