dspy 0.25.1 → 0.26.1
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 +3 -27
- data/lib/dspy/optimizers/gaussian_process.rb +141 -0
- data/lib/dspy/teleprompt/mipro_v2.rb +254 -186
- data/lib/dspy/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e243b7278275462baea2f493270166a1ae4b5419d4f072a769e4ba4b0f65e3e0
|
4
|
+
data.tar.gz: 13bcbcf4ee67c08f19ad619bc8118e39ca9a02045c5a18b3a664fb112724fa87
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 687385021bf9391b22ae51a3f7c05880bec9691347a4e8ecac9175b7e81190c9f63cb0670a94e7324a045d748346cc91b6f7e174808607eaf0d02b8a0a117992
|
7
|
+
data.tar.gz: 3212712d53aca34cbc475503396d4fbeb7b8c11632b673782e7cd2dc2e6fdb22a3ad67688d92b60bd72e845f7b598b446e94fd3f5c6efd5e87f212bb1be14b9e
|
data/README.md
CHANGED
@@ -73,7 +73,7 @@ puts result.confidence # => 0.85
|
|
73
73
|
- **Prompt Objects** - Manipulate prompts as first-class objects instead of strings
|
74
74
|
- **Typed Examples** - Type-safe training data with automatic validation
|
75
75
|
- **Evaluation Framework** - Advanced metrics beyond simple accuracy with error-resilient pipelines
|
76
|
-
- **MIPROv2 Optimization** -
|
76
|
+
- **MIPROv2 Optimization** - Advanced Bayesian optimization with Gaussian Processes, multiple optimization strategies, and storage persistence
|
77
77
|
- **GEPA Optimization** - Genetic-Pareto optimization for multi-objective prompt improvement
|
78
78
|
|
79
79
|
**Production Features:**
|
@@ -128,7 +128,7 @@ For LLMs and AI assistants working with DSPy.rb:
|
|
128
128
|
### Optimization
|
129
129
|
- **[Evaluation Framework](docs/src/optimization/evaluation.md)** - Advanced metrics beyond simple accuracy
|
130
130
|
- **[Prompt Optimization](docs/src/optimization/prompt-optimization.md)** - Manipulate prompts as objects
|
131
|
-
- **[MIPROv2 Optimizer](docs/src/optimization/miprov2.md)** -
|
131
|
+
- **[MIPROv2 Optimizer](docs/src/optimization/miprov2.md)** - Advanced Bayesian optimization with Gaussian Processes
|
132
132
|
- **[GEPA Optimizer](docs/src/optimization/gepa.md)** - Genetic-Pareto optimization for multi-objective prompt optimization
|
133
133
|
|
134
134
|
### Production Features
|
@@ -157,30 +157,6 @@ Then run:
|
|
157
157
|
bundle install
|
158
158
|
```
|
159
159
|
|
160
|
-
#### System Dependencies for Ubuntu/Pop!_OS
|
161
|
-
|
162
|
-
If you need to compile the `polars-df` dependency from source (used for data processing in evaluations), install these system packages:
|
163
|
-
|
164
|
-
```bash
|
165
|
-
# Update package list
|
166
|
-
sudo apt-get update
|
167
|
-
|
168
|
-
# Install Ruby development files (if not already installed)
|
169
|
-
sudo apt-get install ruby-full ruby-dev
|
170
|
-
|
171
|
-
# Install essential build tools
|
172
|
-
sudo apt-get install build-essential
|
173
|
-
|
174
|
-
# Install Rust and Cargo (required for polars-df compilation)
|
175
|
-
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
176
|
-
source $HOME/.cargo/env
|
177
|
-
|
178
|
-
# Install CMake (often needed for Rust projects)
|
179
|
-
sudo apt-get install cmake
|
180
|
-
```
|
181
|
-
|
182
|
-
**Note**: The `polars-df` gem compilation can take 15-20 minutes. Pre-built binaries are available for most platforms, so compilation is only needed if a pre-built binary isn't available for your system.
|
183
|
-
|
184
160
|
## Recent Achievements
|
185
161
|
|
186
162
|
DSPy.rb has rapidly evolved from experimental to production-ready:
|
@@ -190,7 +166,7 @@ DSPy.rb has rapidly evolved from experimental to production-ready:
|
|
190
166
|
- ✅ **Type-Safe Strategy Configuration** - Provider-optimized automatic strategy selection
|
191
167
|
- ✅ **Core Module System** - Predict, ChainOfThought, ReAct, CodeAct with type safety
|
192
168
|
- ✅ **Production Observability** - OpenTelemetry, New Relic, and Langfuse integration
|
193
|
-
- ✅ **Optimization
|
169
|
+
- ✅ **Advanced Optimization** - MIPROv2 with Bayesian optimization, Gaussian Processes, and multiple strategies
|
194
170
|
|
195
171
|
### Recent Advances
|
196
172
|
- ✅ **Enhanced Langfuse Integration (v0.25.0)** - Comprehensive OpenTelemetry span reporting with proper input/output, hierarchical nesting, accurate timing, and observation types
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'numo/narray'
|
5
|
+
require 'sorbet-runtime'
|
6
|
+
|
7
|
+
module DSPy
|
8
|
+
module Optimizers
|
9
|
+
# Pure Ruby Gaussian Process implementation for Bayesian optimization
|
10
|
+
# No external LAPACK/BLAS dependencies required
|
11
|
+
class GaussianProcess
|
12
|
+
extend T::Sig
|
13
|
+
|
14
|
+
sig { params(length_scale: Float, signal_variance: Float, noise_variance: Float).void }
|
15
|
+
def initialize(length_scale: 1.0, signal_variance: 1.0, noise_variance: 1e-6)
|
16
|
+
@length_scale = length_scale
|
17
|
+
@signal_variance = signal_variance
|
18
|
+
@noise_variance = noise_variance
|
19
|
+
@fitted = T.let(false, T::Boolean)
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { params(x1: T::Array[T::Array[Float]], x2: T::Array[T::Array[Float]]).returns(Numo::DFloat) }
|
23
|
+
def rbf_kernel(x1, x2)
|
24
|
+
# Convert to Numo arrays
|
25
|
+
x1_array = Numo::DFloat[*x1]
|
26
|
+
x2_array = Numo::DFloat[*x2]
|
27
|
+
|
28
|
+
# Compute squared Euclidean distances manually
|
29
|
+
n1, n2 = x1_array.shape[0], x2_array.shape[0]
|
30
|
+
sqdist = Numo::DFloat.zeros(n1, n2)
|
31
|
+
|
32
|
+
(0...n1).each do |i|
|
33
|
+
(0...n2).each do |j|
|
34
|
+
diff = x1_array[i, true] - x2_array[j, true]
|
35
|
+
sqdist[i, j] = (diff ** 2).sum
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# RBF kernel: σ² * exp(-0.5 * d² / ℓ²)
|
40
|
+
@signal_variance * Numo::NMath.exp(-0.5 * sqdist / (@length_scale ** 2))
|
41
|
+
end
|
42
|
+
|
43
|
+
sig { params(x_train: T::Array[T::Array[Float]], y_train: T::Array[Float]).void }
|
44
|
+
def fit(x_train, y_train)
|
45
|
+
@x_train = x_train
|
46
|
+
@y_train = Numo::DFloat[*y_train]
|
47
|
+
|
48
|
+
# Compute kernel matrix
|
49
|
+
k_matrix = rbf_kernel(x_train, x_train)
|
50
|
+
|
51
|
+
# Add noise to diagonal for numerical stability
|
52
|
+
n = k_matrix.shape[0]
|
53
|
+
(0...n).each { |i| k_matrix[i, i] += @noise_variance }
|
54
|
+
|
55
|
+
# Store inverted kernel matrix using simple LU decomposition
|
56
|
+
@k_inv = matrix_inverse(k_matrix)
|
57
|
+
@alpha = @k_inv.dot(@y_train)
|
58
|
+
|
59
|
+
@fitted = true
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { params(x_test: T::Array[T::Array[Float]], return_std: T::Boolean).returns(T.any(Numo::DFloat, [Numo::DFloat, Numo::DFloat])) }
|
63
|
+
def predict(x_test, return_std: false)
|
64
|
+
raise "Gaussian Process not fitted" unless @fitted
|
65
|
+
|
66
|
+
# Kernel between training and test points
|
67
|
+
k_star = rbf_kernel(T.must(@x_train), x_test)
|
68
|
+
|
69
|
+
# Predictive mean
|
70
|
+
mean = k_star.transpose.dot(@alpha)
|
71
|
+
|
72
|
+
return mean unless return_std
|
73
|
+
|
74
|
+
# Predictive variance (simplified for small matrices)
|
75
|
+
k_star_star = rbf_kernel(x_test, x_test)
|
76
|
+
var_matrix = k_star_star - k_star.transpose.dot(@k_inv).dot(k_star)
|
77
|
+
var = var_matrix.diagonal
|
78
|
+
|
79
|
+
# Ensure positive variance (element-wise maximum)
|
80
|
+
var = var.map { |v| [v, 1e-12].max }
|
81
|
+
std = Numo::NMath.sqrt(var)
|
82
|
+
|
83
|
+
[mean, std]
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
sig { returns(T.nilable(T::Array[T::Array[Float]])) }
|
89
|
+
attr_reader :x_train
|
90
|
+
|
91
|
+
sig { returns(T.nilable(Numo::DFloat)) }
|
92
|
+
attr_reader :y_train, :k_inv, :alpha
|
93
|
+
|
94
|
+
# Simple matrix inversion using Gauss-Jordan elimination
|
95
|
+
# Only suitable for small matrices (< 100x100)
|
96
|
+
sig { params(matrix: Numo::DFloat).returns(Numo::DFloat) }
|
97
|
+
def matrix_inverse(matrix)
|
98
|
+
n = matrix.shape[0]
|
99
|
+
raise "Matrix must be square" unless matrix.shape[0] == matrix.shape[1]
|
100
|
+
|
101
|
+
# Create augmented matrix [A|I]
|
102
|
+
augmented = Numo::DFloat.zeros(n, 2*n)
|
103
|
+
augmented[true, 0...n] = matrix.copy
|
104
|
+
(0...n).each { |i| augmented[i, n+i] = 1.0 }
|
105
|
+
|
106
|
+
# Gauss-Jordan elimination
|
107
|
+
(0...n).each do |i|
|
108
|
+
# Find pivot
|
109
|
+
max_row = i
|
110
|
+
(i+1...n).each do |k|
|
111
|
+
if augmented[k, i].abs > augmented[max_row, i].abs
|
112
|
+
max_row = k
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Swap rows if needed
|
117
|
+
if max_row != i
|
118
|
+
temp = augmented[i, true].copy
|
119
|
+
augmented[i, true] = augmented[max_row, true]
|
120
|
+
augmented[max_row, true] = temp
|
121
|
+
end
|
122
|
+
|
123
|
+
# Make diagonal element 1
|
124
|
+
pivot = augmented[i, i]
|
125
|
+
raise "Matrix is singular" if pivot.abs < 1e-12
|
126
|
+
augmented[i, true] /= pivot
|
127
|
+
|
128
|
+
# Eliminate column
|
129
|
+
(0...n).each do |j|
|
130
|
+
next if i == j
|
131
|
+
factor = augmented[j, i]
|
132
|
+
augmented[j, true] -= factor * augmented[i, true]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Extract inverse matrix
|
137
|
+
augmented[true, n...2*n]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -5,14 +5,34 @@ require 'sorbet-runtime'
|
|
5
5
|
require_relative 'teleprompter'
|
6
6
|
require_relative 'utils'
|
7
7
|
require_relative '../propose/grounded_proposer'
|
8
|
+
require_relative '../optimizers/gaussian_process'
|
8
9
|
|
9
10
|
module DSPy
|
10
11
|
module Teleprompt
|
12
|
+
# Enum for candidate configuration types
|
13
|
+
class CandidateType < T::Enum
|
14
|
+
enums do
|
15
|
+
Baseline = new("baseline")
|
16
|
+
InstructionOnly = new("instruction_only")
|
17
|
+
FewShotOnly = new("few_shot_only")
|
18
|
+
Combined = new("combined")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Enum for optimization strategies
|
23
|
+
class OptimizationStrategy < T::Enum
|
24
|
+
enums do
|
25
|
+
Greedy = new("greedy")
|
26
|
+
Adaptive = new("adaptive")
|
27
|
+
Bayesian = new("bayesian")
|
28
|
+
end
|
29
|
+
end
|
11
30
|
# MIPROv2: Multi-prompt Instruction Proposal with Retrieval Optimization
|
12
31
|
# State-of-the-art prompt optimization combining bootstrap sampling,
|
13
32
|
# instruction generation, and Bayesian optimization
|
14
33
|
class MIPROv2 < Teleprompter
|
15
34
|
extend T::Sig
|
35
|
+
include Dry::Configurable
|
16
36
|
|
17
37
|
# Auto-configuration modes for different optimization needs
|
18
38
|
module AutoMode
|
@@ -25,15 +45,17 @@ module DSPy
|
|
25
45
|
).returns(MIPROv2)
|
26
46
|
end
|
27
47
|
def self.light(metric: nil, **kwargs)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
48
|
+
optimizer = MIPROv2.new(metric: metric, **kwargs)
|
49
|
+
optimizer.configure do |config|
|
50
|
+
config.num_trials = 6
|
51
|
+
config.num_instruction_candidates = 3
|
52
|
+
config.max_bootstrapped_examples = 2
|
53
|
+
config.max_labeled_examples = 8
|
54
|
+
config.bootstrap_sets = 3
|
55
|
+
config.optimization_strategy = :greedy
|
56
|
+
config.early_stopping_patience = 2
|
57
|
+
end
|
58
|
+
optimizer
|
37
59
|
end
|
38
60
|
|
39
61
|
sig do
|
@@ -43,15 +65,17 @@ module DSPy
|
|
43
65
|
).returns(MIPROv2)
|
44
66
|
end
|
45
67
|
def self.medium(metric: nil, **kwargs)
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
68
|
+
optimizer = MIPROv2.new(metric: metric, **kwargs)
|
69
|
+
optimizer.configure do |config|
|
70
|
+
config.num_trials = 12
|
71
|
+
config.num_instruction_candidates = 5
|
72
|
+
config.max_bootstrapped_examples = 4
|
73
|
+
config.max_labeled_examples = 16
|
74
|
+
config.bootstrap_sets = 5
|
75
|
+
config.optimization_strategy = :adaptive
|
76
|
+
config.early_stopping_patience = 3
|
77
|
+
end
|
78
|
+
optimizer
|
55
79
|
end
|
56
80
|
|
57
81
|
sig do
|
@@ -61,137 +85,102 @@ module DSPy
|
|
61
85
|
).returns(MIPROv2)
|
62
86
|
end
|
63
87
|
def self.heavy(metric: nil, **kwargs)
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
88
|
+
optimizer = MIPROv2.new(metric: metric, **kwargs)
|
89
|
+
optimizer.configure do |config|
|
90
|
+
config.num_trials = 18
|
91
|
+
config.num_instruction_candidates = 8
|
92
|
+
config.max_bootstrapped_examples = 6
|
93
|
+
config.max_labeled_examples = 24
|
94
|
+
config.bootstrap_sets = 8
|
95
|
+
config.optimization_strategy = :bayesian
|
96
|
+
config.early_stopping_patience = 5
|
97
|
+
end
|
98
|
+
optimizer
|
73
99
|
end
|
74
100
|
end
|
75
101
|
|
76
|
-
#
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
sig { returns(Float) }
|
93
|
-
attr_accessor :init_temperature
|
94
|
-
|
95
|
-
sig { returns(Float) }
|
96
|
-
attr_accessor :final_temperature
|
97
|
-
|
98
|
-
sig { returns(Integer) }
|
99
|
-
attr_accessor :early_stopping_patience
|
100
|
-
|
101
|
-
sig { returns(T::Boolean) }
|
102
|
-
attr_accessor :use_bayesian_optimization
|
103
|
-
|
104
|
-
sig { returns(T::Boolean) }
|
105
|
-
attr_accessor :track_diversity
|
106
|
-
|
107
|
-
sig { returns(DSPy::Propose::GroundedProposer::Config) }
|
108
|
-
attr_accessor :proposer_config
|
109
|
-
|
110
|
-
sig { void }
|
111
|
-
def initialize
|
112
|
-
super
|
113
|
-
@num_trials = 12
|
114
|
-
@num_instruction_candidates = 5
|
115
|
-
@bootstrap_sets = 5
|
116
|
-
@optimization_strategy = "adaptive" # greedy, adaptive, bayesian
|
117
|
-
@init_temperature = 1.0
|
118
|
-
@final_temperature = 0.1
|
119
|
-
@early_stopping_patience = 3
|
120
|
-
@use_bayesian_optimization = true
|
121
|
-
@track_diversity = true
|
122
|
-
@proposer_config = DSPy::Propose::GroundedProposer::Config.new
|
102
|
+
# Dry-configurable settings for MIPROv2
|
103
|
+
setting :num_trials, default: 12
|
104
|
+
setting :num_instruction_candidates, default: 5
|
105
|
+
setting :bootstrap_sets, default: 5
|
106
|
+
setting :max_bootstrapped_examples, default: 4
|
107
|
+
setting :max_labeled_examples, default: 16
|
108
|
+
setting :optimization_strategy, default: OptimizationStrategy::Adaptive, constructor: ->(value) {
|
109
|
+
# Coerce symbols to enum values
|
110
|
+
case value
|
111
|
+
when :greedy then OptimizationStrategy::Greedy
|
112
|
+
when :adaptive then OptimizationStrategy::Adaptive
|
113
|
+
when :bayesian then OptimizationStrategy::Bayesian
|
114
|
+
when OptimizationStrategy then value
|
115
|
+
when nil then OptimizationStrategy::Adaptive
|
116
|
+
else
|
117
|
+
raise ArgumentError, "Invalid optimization strategy: #{value}. Must be one of :greedy, :adaptive, :bayesian"
|
123
118
|
end
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
119
|
+
}
|
120
|
+
setting :init_temperature, default: 1.0
|
121
|
+
setting :final_temperature, default: 0.1
|
122
|
+
setting :early_stopping_patience, default: 3
|
123
|
+
setting :use_bayesian_optimization, default: true
|
124
|
+
setting :track_diversity, default: true
|
125
|
+
setting :max_errors, default: 3
|
126
|
+
setting :num_threads, default: 1
|
127
|
+
|
128
|
+
# Class-level configuration method - sets defaults for new instances
|
129
|
+
def self.configure(&block)
|
130
|
+
if block_given?
|
131
|
+
# Store configuration in a class variable for new instances
|
132
|
+
@default_config_block = block
|
138
133
|
end
|
139
134
|
end
|
140
135
|
|
141
|
-
#
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
sig { returns(String) }
|
146
|
-
attr_reader :instruction
|
147
|
-
|
148
|
-
sig { returns(T::Array[T.untyped]) }
|
149
|
-
attr_reader :few_shot_examples
|
150
|
-
|
151
|
-
sig { returns(T::Hash[Symbol, T.untyped]) }
|
152
|
-
attr_reader :metadata
|
136
|
+
# Get the default configuration block
|
137
|
+
def self.default_config_block
|
138
|
+
@default_config_block
|
139
|
+
end
|
153
140
|
|
154
|
-
sig { returns(String) }
|
155
|
-
attr_reader :config_id
|
156
141
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
142
|
+
# Simple data structure for evaluated candidate configurations (immutable)
|
143
|
+
EvaluatedCandidate = Data.define(
|
144
|
+
:instruction,
|
145
|
+
:few_shot_examples,
|
146
|
+
:type,
|
147
|
+
:metadata,
|
148
|
+
:config_id
|
149
|
+
) do
|
150
|
+
extend T::Sig
|
151
|
+
|
152
|
+
# Generate a config ID based on content
|
153
|
+
sig { params(instruction: String, few_shot_examples: T::Array[T.untyped], type: CandidateType, metadata: T::Hash[Symbol, T.untyped]).returns(EvaluatedCandidate) }
|
154
|
+
def self.create(instruction:, few_shot_examples: [], type: CandidateType::Baseline, metadata: {})
|
155
|
+
content = "#{instruction}_#{few_shot_examples.size}_#{type.serialize}_#{metadata.hash}"
|
156
|
+
config_id = Digest::SHA256.hexdigest(content)[0, 12]
|
157
|
+
|
158
|
+
new(
|
159
|
+
instruction: instruction.freeze,
|
160
|
+
few_shot_examples: few_shot_examples.freeze,
|
161
|
+
type: type,
|
162
|
+
metadata: metadata.freeze,
|
163
|
+
config_id: config_id
|
164
|
+
)
|
169
165
|
end
|
170
166
|
|
171
167
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
172
168
|
def to_h
|
173
169
|
{
|
174
|
-
instruction:
|
175
|
-
few_shot_examples:
|
176
|
-
|
177
|
-
|
170
|
+
instruction: instruction,
|
171
|
+
few_shot_examples: few_shot_examples.size,
|
172
|
+
type: type.serialize,
|
173
|
+
metadata: metadata,
|
174
|
+
config_id: config_id
|
178
175
|
}
|
179
176
|
end
|
180
|
-
|
181
|
-
private
|
182
|
-
|
183
|
-
sig { returns(String) }
|
184
|
-
def generate_config_id
|
185
|
-
content = "#{@instruction}_#{@few_shot_examples.size}_#{@metadata.hash}"
|
186
|
-
Digest::SHA256.hexdigest(content)[0, 12]
|
187
|
-
end
|
188
177
|
end
|
189
178
|
|
190
179
|
# Result of MIPROv2 optimization
|
191
180
|
class MIPROv2Result < OptimizationResult
|
192
181
|
extend T::Sig
|
193
182
|
|
194
|
-
sig { returns(T::Array[
|
183
|
+
sig { returns(T::Array[EvaluatedCandidate]) }
|
195
184
|
attr_reader :evaluated_candidates
|
196
185
|
|
197
186
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
@@ -211,7 +200,7 @@ module DSPy
|
|
211
200
|
optimized_program: T.untyped,
|
212
201
|
scores: T::Hash[Symbol, T.untyped],
|
213
202
|
history: T::Hash[Symbol, T.untyped],
|
214
|
-
evaluated_candidates: T::Array[
|
203
|
+
evaluated_candidates: T::Array[EvaluatedCandidate],
|
215
204
|
optimization_trace: T::Hash[Symbol, T.untyped],
|
216
205
|
bootstrap_statistics: T::Hash[Symbol, T.untyped],
|
217
206
|
proposal_statistics: T::Hash[Symbol, T.untyped],
|
@@ -255,17 +244,25 @@ module DSPy
|
|
255
244
|
sig { returns(T.nilable(DSPy::Propose::GroundedProposer)) }
|
256
245
|
attr_reader :proposer
|
257
246
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
def initialize(metric: nil, config: nil)
|
265
|
-
@mipro_config = config || MIPROv2Config.new
|
266
|
-
super(metric: metric, config: @mipro_config)
|
247
|
+
# Override dry-configurable's initialize to add our parameter validation
|
248
|
+
def initialize(metric: nil, **kwargs)
|
249
|
+
# Reject old config parameter pattern
|
250
|
+
if kwargs.key?(:config)
|
251
|
+
raise ArgumentError, "config parameter is no longer supported. Use .configure blocks instead."
|
252
|
+
end
|
267
253
|
|
268
|
-
|
254
|
+
# Let dry-configurable handle its initialization
|
255
|
+
super(**kwargs)
|
256
|
+
|
257
|
+
# Apply class-level configuration if it exists
|
258
|
+
if self.class.default_config_block
|
259
|
+
configure(&self.class.default_config_block)
|
260
|
+
end
|
261
|
+
|
262
|
+
@metric = metric
|
263
|
+
|
264
|
+
# Initialize proposer with a basic config for now (will be updated later)
|
265
|
+
@proposer = DSPy::Propose::GroundedProposer.new(config: DSPy::Propose::GroundedProposer::Config.new)
|
269
266
|
@optimization_trace = []
|
270
267
|
@evaluated_candidates = []
|
271
268
|
end
|
@@ -284,8 +281,8 @@ module DSPy
|
|
284
281
|
instrument_step('miprov2_compile', {
|
285
282
|
trainset_size: trainset.size,
|
286
283
|
valset_size: valset&.size || 0,
|
287
|
-
num_trials:
|
288
|
-
optimization_strategy:
|
284
|
+
num_trials: config.num_trials,
|
285
|
+
optimization_strategy: config.optimization_strategy,
|
289
286
|
mode: infer_auto_mode
|
290
287
|
}) do
|
291
288
|
# Convert examples to typed format
|
@@ -345,11 +342,11 @@ module DSPy
|
|
345
342
|
sig { params(program: T.untyped, trainset: T::Array[DSPy::Example]).returns(Utils::BootstrapResult) }
|
346
343
|
def phase_1_bootstrap(program, trainset)
|
347
344
|
bootstrap_config = Utils::BootstrapConfig.new
|
348
|
-
bootstrap_config.max_bootstrapped_examples =
|
349
|
-
bootstrap_config.max_labeled_examples =
|
350
|
-
bootstrap_config.num_candidate_sets =
|
351
|
-
bootstrap_config.max_errors =
|
352
|
-
bootstrap_config.num_threads =
|
345
|
+
bootstrap_config.max_bootstrapped_examples = config.max_bootstrapped_examples
|
346
|
+
bootstrap_config.max_labeled_examples = config.max_labeled_examples
|
347
|
+
bootstrap_config.num_candidate_sets = config.bootstrap_sets
|
348
|
+
bootstrap_config.max_errors = config.max_errors
|
349
|
+
bootstrap_config.num_threads = config.num_threads
|
353
350
|
|
354
351
|
Utils.create_n_fewshot_demo_sets(program, trainset, config: bootstrap_config, metric: @metric)
|
355
352
|
end
|
@@ -374,7 +371,7 @@ module DSPy
|
|
374
371
|
raise ArgumentError, "Cannot extract signature class from program" unless signature_class
|
375
372
|
|
376
373
|
# Configure proposer for this optimization run
|
377
|
-
@
|
374
|
+
@proposer.config.num_instruction_candidates = config.num_instruction_candidates
|
378
375
|
|
379
376
|
@proposer.propose_instructions(
|
380
377
|
signature_class,
|
@@ -407,7 +404,7 @@ module DSPy
|
|
407
404
|
best_program = nil
|
408
405
|
best_evaluation_result = nil
|
409
406
|
|
410
|
-
|
407
|
+
config.num_trials.times do |trial_idx|
|
411
408
|
trials_completed = trial_idx + 1
|
412
409
|
|
413
410
|
# Select next candidate based on optimization strategy
|
@@ -476,33 +473,39 @@ module DSPy
|
|
476
473
|
params(
|
477
474
|
proposal_result: DSPy::Propose::GroundedProposer::ProposalResult,
|
478
475
|
bootstrap_result: Utils::BootstrapResult
|
479
|
-
).returns(T::Array[
|
476
|
+
).returns(T::Array[EvaluatedCandidate])
|
480
477
|
end
|
481
478
|
def generate_candidate_configurations(proposal_result, bootstrap_result)
|
482
479
|
candidates = []
|
483
480
|
|
484
481
|
# Base configuration (no modifications)
|
485
|
-
candidates <<
|
482
|
+
candidates << EvaluatedCandidate.new(
|
486
483
|
instruction: "",
|
487
484
|
few_shot_examples: [],
|
488
|
-
|
485
|
+
type: CandidateType::Baseline,
|
486
|
+
metadata: {},
|
487
|
+
config_id: SecureRandom.hex(6)
|
489
488
|
)
|
490
489
|
|
491
490
|
# Instruction-only candidates
|
492
491
|
proposal_result.candidate_instructions.each_with_index do |instruction, idx|
|
493
|
-
candidates <<
|
492
|
+
candidates << EvaluatedCandidate.new(
|
494
493
|
instruction: instruction,
|
495
494
|
few_shot_examples: [],
|
496
|
-
|
495
|
+
type: CandidateType::InstructionOnly,
|
496
|
+
metadata: { proposal_rank: idx },
|
497
|
+
config_id: SecureRandom.hex(6)
|
497
498
|
)
|
498
499
|
end
|
499
500
|
|
500
501
|
# Few-shot only candidates
|
501
502
|
bootstrap_result.candidate_sets.each_with_index do |candidate_set, idx|
|
502
|
-
candidates <<
|
503
|
+
candidates << EvaluatedCandidate.new(
|
503
504
|
instruction: "",
|
504
505
|
few_shot_examples: candidate_set,
|
505
|
-
|
506
|
+
type: CandidateType::FewShotOnly,
|
507
|
+
metadata: { bootstrap_rank: idx },
|
508
|
+
config_id: SecureRandom.hex(6)
|
506
509
|
)
|
507
510
|
end
|
508
511
|
|
@@ -512,14 +515,15 @@ module DSPy
|
|
512
515
|
|
513
516
|
top_instructions.each_with_index do |instruction, i_idx|
|
514
517
|
top_bootstrap_sets.each_with_index do |candidate_set, b_idx|
|
515
|
-
candidates <<
|
518
|
+
candidates << EvaluatedCandidate.new(
|
516
519
|
instruction: instruction,
|
517
520
|
few_shot_examples: candidate_set,
|
521
|
+
type: CandidateType::Combined,
|
518
522
|
metadata: {
|
519
|
-
type: "combined",
|
520
523
|
instruction_rank: i_idx,
|
521
524
|
bootstrap_rank: b_idx
|
522
|
-
}
|
525
|
+
},
|
526
|
+
config_id: SecureRandom.hex(6)
|
523
527
|
)
|
524
528
|
end
|
525
529
|
end
|
@@ -528,13 +532,13 @@ module DSPy
|
|
528
532
|
end
|
529
533
|
|
530
534
|
# Initialize optimization state for candidate selection
|
531
|
-
sig { params(candidates: T::Array[
|
535
|
+
sig { params(candidates: T::Array[EvaluatedCandidate]).returns(T::Hash[Symbol, T.untyped]) }
|
532
536
|
def initialize_optimization_state(candidates)
|
533
537
|
{
|
534
538
|
candidates: candidates,
|
535
539
|
scores: {},
|
536
540
|
exploration_counts: Hash.new(0),
|
537
|
-
temperature:
|
541
|
+
temperature: config.init_temperature,
|
538
542
|
best_score_history: [],
|
539
543
|
diversity_scores: {},
|
540
544
|
no_improvement_count: 0
|
@@ -544,18 +548,18 @@ module DSPy
|
|
544
548
|
# Select next candidate based on optimization strategy
|
545
549
|
sig do
|
546
550
|
params(
|
547
|
-
candidates: T::Array[
|
551
|
+
candidates: T::Array[EvaluatedCandidate],
|
548
552
|
state: T::Hash[Symbol, T.untyped],
|
549
553
|
trial_idx: Integer
|
550
|
-
).returns(
|
554
|
+
).returns(EvaluatedCandidate)
|
551
555
|
end
|
552
556
|
def select_next_candidate(candidates, state, trial_idx)
|
553
|
-
case
|
554
|
-
when
|
557
|
+
case config.optimization_strategy
|
558
|
+
when OptimizationStrategy::Greedy
|
555
559
|
select_candidate_greedy(candidates, state)
|
556
|
-
when
|
560
|
+
when OptimizationStrategy::Adaptive
|
557
561
|
select_candidate_adaptive(candidates, state, trial_idx)
|
558
|
-
when
|
562
|
+
when OptimizationStrategy::Bayesian
|
559
563
|
select_candidate_bayesian(candidates, state, trial_idx)
|
560
564
|
else
|
561
565
|
candidates.sample # Random fallback
|
@@ -563,7 +567,7 @@ module DSPy
|
|
563
567
|
end
|
564
568
|
|
565
569
|
# Greedy candidate selection (exploit best known configurations)
|
566
|
-
sig { params(candidates: T::Array[
|
570
|
+
sig { params(candidates: T::Array[EvaluatedCandidate], state: T::Hash[Symbol, T.untyped]).returns(EvaluatedCandidate) }
|
567
571
|
def select_candidate_greedy(candidates, state)
|
568
572
|
# Prioritize unexplored candidates, then highest scoring
|
569
573
|
unexplored = candidates.reject { |c| state[:scores].key?(c.config_id) }
|
@@ -577,15 +581,15 @@ module DSPy
|
|
577
581
|
# Adaptive candidate selection (balance exploration and exploitation)
|
578
582
|
sig do
|
579
583
|
params(
|
580
|
-
candidates: T::Array[
|
584
|
+
candidates: T::Array[EvaluatedCandidate],
|
581
585
|
state: T::Hash[Symbol, T.untyped],
|
582
586
|
trial_idx: Integer
|
583
|
-
).returns(
|
587
|
+
).returns(EvaluatedCandidate)
|
584
588
|
end
|
585
589
|
def select_candidate_adaptive(candidates, state, trial_idx)
|
586
590
|
# Update temperature based on progress
|
587
|
-
progress = trial_idx.to_f /
|
588
|
-
state[:temperature] =
|
591
|
+
progress = trial_idx.to_f / config.num_trials
|
592
|
+
state[:temperature] = config.init_temperature * (1 - progress) + config.final_temperature * progress
|
589
593
|
|
590
594
|
# Calculate selection scores combining exploitation and exploration
|
591
595
|
candidate_scores = candidates.map do |candidate|
|
@@ -618,22 +622,86 @@ module DSPy
|
|
618
622
|
# Bayesian candidate selection (use probabilistic model)
|
619
623
|
sig do
|
620
624
|
params(
|
621
|
-
candidates: T::Array[
|
625
|
+
candidates: T::Array[EvaluatedCandidate],
|
622
626
|
state: T::Hash[Symbol, T.untyped],
|
623
627
|
trial_idx: Integer
|
624
|
-
).returns(
|
628
|
+
).returns(EvaluatedCandidate)
|
625
629
|
end
|
626
630
|
def select_candidate_bayesian(candidates, state, trial_idx)
|
627
|
-
#
|
628
|
-
|
629
|
-
|
631
|
+
# Need at least 3 observations to fit GP, otherwise fall back to adaptive
|
632
|
+
return select_candidate_adaptive(candidates, state, trial_idx) if state[:scores].size < 3
|
633
|
+
|
634
|
+
# Get scored candidates for training the GP
|
635
|
+
scored_candidates = candidates.select { |c| state[:scores].key?(c.config_id) }
|
636
|
+
return select_candidate_adaptive(candidates, state, trial_idx) if scored_candidates.size < 3
|
637
|
+
|
638
|
+
begin
|
639
|
+
# Encode candidates as numerical features
|
640
|
+
all_candidate_features = encode_candidates_for_gp(candidates)
|
641
|
+
scored_features = encode_candidates_for_gp(scored_candidates)
|
642
|
+
scored_targets = scored_candidates.map { |c| state[:scores][c.config_id].to_f }
|
643
|
+
|
644
|
+
# Train Gaussian Process
|
645
|
+
gp = DSPy::Optimizers::GaussianProcess.new(
|
646
|
+
length_scale: 1.0,
|
647
|
+
signal_variance: 1.0,
|
648
|
+
noise_variance: 0.01
|
649
|
+
)
|
650
|
+
gp.fit(scored_features, scored_targets)
|
651
|
+
|
652
|
+
# Predict mean and uncertainty for all candidates
|
653
|
+
means, stds = gp.predict(all_candidate_features, return_std: true)
|
654
|
+
|
655
|
+
# Upper Confidence Bound (UCB) acquisition function
|
656
|
+
kappa = 2.0 * Math.sqrt(Math.log(trial_idx + 1)) # Exploration parameter
|
657
|
+
acquisition_scores = means.to_a.zip(stds.to_a).map { |m, s| m + kappa * s }
|
658
|
+
|
659
|
+
# Select candidate with highest acquisition score
|
660
|
+
best_idx = acquisition_scores.each_with_index.max_by { |score, _| score }[1]
|
661
|
+
candidates[best_idx]
|
662
|
+
|
663
|
+
rescue => e
|
664
|
+
# If GP fails for any reason, fall back to adaptive selection
|
665
|
+
DSPy.logger.warn("Bayesian optimization failed: #{e.message}. Falling back to adaptive selection.")
|
666
|
+
select_candidate_adaptive(candidates, state, trial_idx)
|
667
|
+
end
|
668
|
+
end
|
669
|
+
|
670
|
+
private
|
671
|
+
|
672
|
+
|
673
|
+
# Encode candidates as numerical features for Gaussian Process
|
674
|
+
sig { params(candidates: T::Array[EvaluatedCandidate]).returns(T::Array[T::Array[Float]]) }
|
675
|
+
def encode_candidates_for_gp(candidates)
|
676
|
+
# Simple encoding: use hash of config as features
|
677
|
+
# In practice, this could be more sophisticated (e.g., instruction embeddings)
|
678
|
+
candidates.map do |candidate|
|
679
|
+
# Create deterministic numerical features from the candidate config
|
680
|
+
config_hash = candidate.config_id.hash.abs
|
681
|
+
|
682
|
+
# Extract multiple features to create a feature vector
|
683
|
+
features = []
|
684
|
+
features << (config_hash % 1000).to_f / 1000.0 # Feature 1: hash mod 1000, normalized
|
685
|
+
features << ((config_hash / 1000) % 1000).to_f / 1000.0 # Feature 2: different part of hash
|
686
|
+
features << ((config_hash / 1_000_000) % 1000).to_f / 1000.0 # Feature 3: high bits
|
687
|
+
|
688
|
+
# Add instruction length if available
|
689
|
+
instruction = candidate.instruction
|
690
|
+
if instruction && !instruction.empty?
|
691
|
+
features << [instruction.length.to_f / 100.0, 2.0].min # Instruction length, capped at 200 chars
|
692
|
+
else
|
693
|
+
features << 0.5 # Default value
|
694
|
+
end
|
695
|
+
|
696
|
+
features
|
697
|
+
end
|
630
698
|
end
|
631
699
|
|
632
700
|
# Evaluate a candidate configuration
|
633
701
|
sig do
|
634
702
|
params(
|
635
703
|
program: T.untyped,
|
636
|
-
candidate:
|
704
|
+
candidate: EvaluatedCandidate,
|
637
705
|
evaluation_set: T::Array[DSPy::Example]
|
638
706
|
).returns([Float, T.untyped, DSPy::Evaluate::BatchEvaluationResult])
|
639
707
|
end
|
@@ -651,7 +719,7 @@ module DSPy
|
|
651
719
|
end
|
652
720
|
|
653
721
|
# Apply candidate configuration to program
|
654
|
-
sig { params(program: T.untyped, candidate:
|
722
|
+
sig { params(program: T.untyped, candidate: EvaluatedCandidate).returns(T.untyped) }
|
655
723
|
def apply_candidate_configuration(program, candidate)
|
656
724
|
modified_program = program
|
657
725
|
|
@@ -679,7 +747,7 @@ module DSPy
|
|
679
747
|
sig do
|
680
748
|
params(
|
681
749
|
state: T::Hash[Symbol, T.untyped],
|
682
|
-
candidate:
|
750
|
+
candidate: EvaluatedCandidate,
|
683
751
|
score: Float
|
684
752
|
).void
|
685
753
|
end
|
@@ -689,7 +757,7 @@ module DSPy
|
|
689
757
|
state[:best_score_history] << score
|
690
758
|
|
691
759
|
# Track diversity if enabled
|
692
|
-
if
|
760
|
+
if config.track_diversity
|
693
761
|
state[:diversity_scores][candidate.config_id] = calculate_diversity_score(candidate)
|
694
762
|
end
|
695
763
|
|
@@ -705,14 +773,14 @@ module DSPy
|
|
705
773
|
sig { params(state: T::Hash[Symbol, T.untyped], trial_idx: Integer).returns(T::Boolean) }
|
706
774
|
def should_early_stop?(state, trial_idx)
|
707
775
|
# Don't stop too early
|
708
|
-
return false if trial_idx <
|
776
|
+
return false if trial_idx < config.early_stopping_patience
|
709
777
|
|
710
778
|
# Stop if no improvement for patience trials
|
711
|
-
state[:no_improvement_count] >=
|
779
|
+
state[:no_improvement_count] >= config.early_stopping_patience
|
712
780
|
end
|
713
781
|
|
714
782
|
# Calculate diversity score for candidate
|
715
|
-
sig { params(candidate:
|
783
|
+
sig { params(candidate: EvaluatedCandidate).returns(Float) }
|
716
784
|
def calculate_diversity_score(candidate)
|
717
785
|
# Simple diversity metric based on instruction length and few-shot count
|
718
786
|
instruction_diversity = candidate.instruction.length / 200.0
|
@@ -739,8 +807,8 @@ module DSPy
|
|
739
807
|
|
740
808
|
history = {
|
741
809
|
total_trials: optimization_result[:trials_completed],
|
742
|
-
optimization_strategy:
|
743
|
-
early_stopped: optimization_result[:trials_completed] <
|
810
|
+
optimization_strategy: config.optimization_strategy,
|
811
|
+
early_stopped: optimization_result[:trials_completed] < config.num_trials,
|
744
812
|
score_history: optimization_result[:optimization_state][:best_score_history]
|
745
813
|
}
|
746
814
|
|
@@ -749,7 +817,7 @@ module DSPy
|
|
749
817
|
auto_mode: infer_auto_mode,
|
750
818
|
best_instruction: best_candidate&.instruction || "",
|
751
819
|
best_few_shot_count: best_candidate&.few_shot_examples&.size || 0,
|
752
|
-
best_candidate_type: best_candidate&.
|
820
|
+
best_candidate_type: best_candidate&.type&.serialize || "unknown",
|
753
821
|
optimization_timestamp: Time.now.iso8601
|
754
822
|
}
|
755
823
|
|
@@ -820,7 +888,7 @@ module DSPy
|
|
820
888
|
# Infer auto mode based on configuration
|
821
889
|
sig { returns(String) }
|
822
890
|
def infer_auto_mode
|
823
|
-
case
|
891
|
+
case config.num_trials
|
824
892
|
when 0..6 then "light"
|
825
893
|
when 7..12 then "medium"
|
826
894
|
else "heavy"
|
data/lib/dspy/version.rb
CHANGED
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.
|
4
|
+
version: 0.26.1
|
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-09-
|
10
|
+
date: 2025-09-10 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: dry-configurable
|
@@ -122,19 +122,19 @@ dependencies:
|
|
122
122
|
- !ruby/object:Gem::Version
|
123
123
|
version: '0.3'
|
124
124
|
- !ruby/object:Gem::Dependency
|
125
|
-
name:
|
125
|
+
name: numo-narray
|
126
126
|
requirement: !ruby/object:Gem::Requirement
|
127
127
|
requirements:
|
128
128
|
- - "~>"
|
129
129
|
- !ruby/object:Gem::Version
|
130
|
-
version: 0.
|
130
|
+
version: '0.9'
|
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.
|
137
|
+
version: '0.9'
|
138
138
|
- !ruby/object:Gem::Dependency
|
139
139
|
name: informers
|
140
140
|
requirement: !ruby/object:Gem::Requirement
|
@@ -239,6 +239,7 @@ files:
|
|
239
239
|
- lib/dspy/module.rb
|
240
240
|
- lib/dspy/observability.rb
|
241
241
|
- lib/dspy/observability/async_span_processor.rb
|
242
|
+
- lib/dspy/optimizers/gaussian_process.rb
|
242
243
|
- lib/dspy/predict.rb
|
243
244
|
- lib/dspy/prediction.rb
|
244
245
|
- lib/dspy/prompt.rb
|