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.
- checksums.yaml +4 -4
- data/README.md +2 -3
- 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 +4 -8
- data/lib/dspy/mixins/struct_builder.rb +17 -25
- data/lib/dspy/module.rb +12 -1
- data/lib/dspy/observability/async_span_processor.rb +67 -93
- data/lib/dspy/observability.rb +43 -1
- data/lib/dspy/predict.rb +10 -0
- data/lib/dspy/propose/dataset_summary_generator.rb +36 -3
- data/lib/dspy/propose/grounded_proposer.rb +118 -11
- data/lib/dspy/re_act.rb +13 -0
- data/lib/dspy/reflection_lm.rb +36 -0
- data/lib/dspy/teleprompt/gepa.rb +448 -2803
- data/lib/dspy/teleprompt/mipro_v2.rb +564 -65
- data/lib/dspy/teleprompt/utils.rb +8 -3
- data/lib/dspy/version.rb +2 -2
- data/lib/dspy.rb +3 -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 +42 -4
- data/lib/dspy/teleprompt/simple_optimizer.rb +0 -503
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 747119ce407283e4d8ed5f01014262f24a94418ad2cbef4305a28b21cb58c8bc
|
4
|
+
data.tar.gz: 3693faccd1fca98015864fd4404491b619b2aa600ab83a78dd3fc7d9e3342ef1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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)**
|
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
|
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
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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 '
|
4
|
-
require '
|
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
|
12
|
-
# Spans are queued and exported
|
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
|
-
@
|
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
|
-
|
91
|
-
|
89
|
+
future = Concurrent::Promises.future_on(@export_executor) do
|
90
|
+
@exporter.shutdown(timeout: timeout)
|
91
|
+
end
|
92
|
+
future.value!(timeout)
|
92
93
|
|
93
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
146
|
-
|
158
|
+
def schedule_async_export(export_all: false)
|
159
|
+
return if @shutdown_requested
|
147
160
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
179
|
+
result = export_spans_with_retry(spans)
|
180
|
+
break if result == OpenTelemetry::SDK::Trace::Export::FAILURE
|
158
181
|
|
159
|
-
|
182
|
+
break unless export_all || @queue.size >= @export_batch_size
|
183
|
+
end
|
184
|
+
|
185
|
+
result
|
160
186
|
end
|
161
187
|
|
162
|
-
def
|
188
|
+
def dequeue_spans(limit)
|
163
189
|
spans = []
|
164
190
|
|
165
|
-
|
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
|
-
|
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
|
data/lib/dspy/observability.rb
CHANGED
@@ -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
|
-
|
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
|